summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntti Ajanki <antti.ajanki@iki.fi>2010-07-23 20:55:11 +0300
committerAntti Ajanki <antti.ajanki@iki.fi>2010-07-23 20:55:11 +0300
commit310743fb9ebbf68b253b923a309cc5f635da89a1 (patch)
tree59c365db7459649344b4ab6d58fde1ceb362506d
downloadvdr-plugin-webvideo-310743fb9ebbf68b253b923a309cc5f635da89a1.tar.gz
vdr-plugin-webvideo-310743fb9ebbf68b253b923a309cc5f635da89a1.tar.bz2
release 0.3.0
-rw-r--r--COPYING701
-rw-r--r--HISTORY185
-rw-r--r--Makefile79
-rw-r--r--README48
-rw-r--r--README.vdrplugin180
-rw-r--r--README.webvi118
-rw-r--r--TODO18
-rw-r--r--debian/changelog150
-rw-r--r--debian/compat1
-rw-r--r--debian/control52
-rw-r--r--debian/copyright61
-rw-r--r--debian/libwebvi-dev.docs1
-rw-r--r--debian/libwebvi-dev.install3
-rw-r--r--debian/libwebvi0.docs1
-rw-r--r--debian/libwebvi0.install1
-rw-r--r--debian/plugin.webvideo.conf10
-rw-r--r--debian/postinst43
-rw-r--r--debian/pycompat1
-rw-r--r--debian/python-webvi.docs2
-rw-r--r--debian/python-webvi.install4
-rw-r--r--debian/python-webvi.manpages1
-rwxr-xr-xdebian/rules37
-rw-r--r--debian/vdr-plugin-webvideo.NEWS10
-rw-r--r--debian/vdr-plugin-webvideo.dirs1
-rw-r--r--debian/vdr-plugin-webvideo.docs2
-rw-r--r--debian/vdr-plugin-webvideo.install6
-rw-r--r--debian/watch2
-rw-r--r--debian/webvi.1.txt67
-rw-r--r--debian/webvi.conf20
-rw-r--r--debian/webvi.plugin.conf3
-rw-r--r--doc/developers.txt152
-rwxr-xr-xsetup.py38
-rw-r--r--src/libwebvi/Makefile34
-rw-r--r--src/libwebvi/libwebvi.c814
-rw-r--r--src/libwebvi/libwebvi.h330
-rwxr-xr-xsrc/libwebvi/pythonlibname.py14
-rw-r--r--src/libwebvi/webvi/__init__.py1
-rw-r--r--src/libwebvi/webvi/api.py289
-rw-r--r--src/libwebvi/webvi/asyncurl.py389
-rw-r--r--src/libwebvi/webvi/constants.py50
-rw-r--r--src/libwebvi/webvi/download.py470
-rw-r--r--src/libwebvi/webvi/json2xml.py69
-rw-r--r--src/libwebvi/webvi/request.py617
-rw-r--r--src/libwebvi/webvi/utils.py134
-rw-r--r--src/libwebvi/webvi/version.py20
-rw-r--r--src/unittest/Makefile11
-rwxr-xr-xsrc/unittest/runtests.sh7
-rw-r--r--src/unittest/testdownload.c195
-rw-r--r--src/unittest/testlibwebvi.c147
-rw-r--r--src/unittest/testwebvi.py407
-rw-r--r--src/vdr-plugin/Makefile115
-rw-r--r--src/vdr-plugin/buffer.c84
-rw-r--r--src/vdr-plugin/buffer.h44
-rw-r--r--src/vdr-plugin/common.c182
-rw-r--r--src/vdr-plugin/common.h42
-rw-r--r--src/vdr-plugin/config.c199
-rw-r--r--src/vdr-plugin/config.h64
-rw-r--r--src/vdr-plugin/dictionary.c410
-rw-r--r--src/vdr-plugin/dictionary.h178
-rw-r--r--src/vdr-plugin/download.c222
-rw-r--r--src/vdr-plugin/download.h59
-rw-r--r--src/vdr-plugin/history.c145
-rw-r--r--src/vdr-plugin/history.h62
-rw-r--r--src/vdr-plugin/iniparser.c650
-rw-r--r--src/vdr-plugin/iniparser.h284
-rw-r--r--src/vdr-plugin/menu.c670
-rw-r--r--src/vdr-plugin/menu.h114
-rw-r--r--src/vdr-plugin/menu_timer.c150
-rw-r--r--src/vdr-plugin/menu_timer.h46
-rw-r--r--src/vdr-plugin/menudata.c179
-rw-r--r--src/vdr-plugin/menudata.h100
-rw-r--r--src/vdr-plugin/mime.types4
-rw-r--r--src/vdr-plugin/mimetypes.c98
-rw-r--r--src/vdr-plugin/mimetypes.h35
-rw-r--r--src/vdr-plugin/player.c73
-rw-r--r--src/vdr-plugin/player.h29
-rw-r--r--src/vdr-plugin/po/de_DE.po137
-rw-r--r--src/vdr-plugin/po/fi_FI.po137
-rw-r--r--src/vdr-plugin/po/fr_FR.po156
-rw-r--r--src/vdr-plugin/po/it_IT.po158
-rw-r--r--src/vdr-plugin/request.c432
-rw-r--r--src/vdr-plugin/request.h170
-rw-r--r--src/vdr-plugin/timer.c465
-rw-r--r--src/vdr-plugin/timer.h111
-rw-r--r--src/vdr-plugin/webvideo.c444
-rw-r--r--src/version1
-rwxr-xr-xsrc/webvicli/webvi22
-rw-r--r--src/webvicli/webvicli/__init__.py1
-rw-r--r--src/webvicli/webvicli/client.py729
-rw-r--r--src/webvicli/webvicli/menu.py171
-rwxr-xr-xtemplates/bin/ruutu-dl36
-rwxr-xr-xtemplates/bin/yle-dl22
-rw-r--r--templates/google/description.xsl22
-rw-r--r--templates/google/search.xsl38
-rw-r--r--templates/google/searchresults.xsl85
-rw-r--r--templates/google/service.xml7
-rw-r--r--templates/google/video.xsl19
-rw-r--r--templates/katsomo/mainmenu.xsl26
-rw-r--r--templates/katsomo/navigation.xsl48
-rw-r--r--templates/katsomo/search.xsl23
-rw-r--r--templates/katsomo/searchresults.xsl32
-rw-r--r--templates/katsomo/service.xml7
-rw-r--r--templates/katsomo/video.xsl18
-rw-r--r--templates/metacafe/categories.xsl33
-rw-r--r--templates/metacafe/channellist.xsl22
-rw-r--r--templates/metacafe/description.xsl47
-rw-r--r--templates/metacafe/navigation.xsl36
-rw-r--r--templates/metacafe/search.xsl36
-rw-r--r--templates/metacafe/service.xml7
-rw-r--r--templates/metacafe/video.xsl14
-rw-r--r--templates/ruutufi/description.xsl51
-rw-r--r--templates/ruutufi/mainmenu.xsl32
-rw-r--r--templates/ruutufi/program.xsl120
-rw-r--r--templates/ruutufi/search.xsl23
-rw-r--r--templates/ruutufi/series.xsl28
-rw-r--r--templates/ruutufi/service.xml7
-rw-r--r--templates/ruutufi/video.xsl23
-rw-r--r--templates/ruutufi/video2.xsl15
-rw-r--r--templates/subtv/description.xsl32
-rw-r--r--templates/subtv/mainmenu.xsl21
-rw-r--r--templates/subtv/navigation.xsl42
-rw-r--r--templates/subtv/service.xml7
-rw-r--r--templates/subtv/video.xsl19
-rw-r--r--templates/svtplay/categories.xsl19
-rw-r--r--templates/svtplay/description.xsl41
-rw-r--r--templates/svtplay/navigation.xsl74
-rw-r--r--templates/svtplay/programmenu.xsl56
-rw-r--r--templates/svtplay/service.xml7
-rw-r--r--templates/svtplay/video.xsl24
-rw-r--r--templates/vimeo/channels.xsl33
-rw-r--r--templates/vimeo/description.xsl59
-rw-r--r--templates/vimeo/groups.xsl33
-rw-r--r--templates/vimeo/mainmenu.xsl28
-rw-r--r--templates/vimeo/navigation.xsl22
-rw-r--r--templates/vimeo/search.xsl30
-rw-r--r--templates/vimeo/searchresults.xsl34
-rw-r--r--templates/vimeo/service.xml7
-rw-r--r--templates/vimeo/video.xsl14
-rw-r--r--templates/yleareena/description.xsl31
-rw-r--r--templates/yleareena/livebroadcasts.xsl46
-rw-r--r--templates/yleareena/livestream.xsl23
-rw-r--r--templates/yleareena/mainmenu.xsl35
-rw-r--r--templates/yleareena/navigation.xsl119
-rw-r--r--templates/yleareena/programlist.xsl22
-rw-r--r--templates/yleareena/search.xsl45
-rw-r--r--templates/yleareena/service.xml7
-rw-r--r--templates/yleareena/video.xsl18
-rw-r--r--templates/youtube/categories.xsl29
-rw-r--r--templates/youtube/description.xsl73
-rw-r--r--templates/youtube/navigation.xsl62
-rw-r--r--templates/youtube/search.xsl39
-rw-r--r--templates/youtube/service.xml7
-rw-r--r--templates/youtube/video.xsl48
153 files changed, 15451 insertions, 0 deletions
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..109c7c4
--- /dev/null
+++ b/HISTORY
@@ -0,0 +1,185 @@
+VDR Plugin 'webvideo' Revision History
+--------------------------------------
+
+2008-06-25: Version 0.0.1
+
+- Initial revision.
+
+2008-07-24: Version 0.0.2
+
+- Italian translation (thanks to Diego Pierotto)
+- Support for mms URLs using libmms
+- Guess the file extension from the Content-Type header, not from the
+ defaultext tag
+- New video service: YLE Areena (the web service of the Finland's
+ national broadcasting company). Only partial support, some of the
+ URLs do not work with libmms.
+- Youtube: Download higher quality MPEG-4 videos
+
+2008-08-20: Version 0.0.3
+
+- Support for video search
+- Updated Italian translation (thanks to Diego Pierotto)
+- Try mmsh if mms protocol fails (requires libmms 0.4 or later). Most
+ videos on YLE Areena seem to work after this fix.
+- Fix segfault when deleting the plugin at VDR exit
+- Youtube: switch back to low quality FLV videos because not all
+ videos have MP4 version
+
+2008-08-21: Version 0.0.4
+
+- Updated Italian translation (thanks to Diego Pierotto)
+- Include a workaround for a bug in the libmms header file mmsx.h
+ which caused the compilation to fail
+- Fix compiler warnings
+
+2008-09-08: Version 0.0.5
+
+- New video service: SVT Play. Contributed by Lars Olsson.
+- More robust parsing of .asx files
+- Workaround for buggy servers: if the server reports the Content-Type
+ of a video file as text/plain do not use it for deciding the file
+ extension. Try to extract the extension from the URL instead.
+- Sort service names alphabetically
+
+2008-12-06: Version 0.0.6
+
+- French translation (Thanks to Bruno Roussel)
+- Fixed Youtube parsing to accommodate to recent changes
+
+2009-02-08: Version 0.1.0
+
+- The downloader backend is now a separate server process. The user
+ interface is no longer blocked while the plugin is waiting for a web
+ server to respond.
+- Support for streaming
+- A new command line client that has the same capabilities as the plugin
+ but can be used without VDR.
+- Alternative URLs for videos. For example, Youtube module first tries
+ to download high quality version, and falls back to standard version
+ if high quality version is not available.
+- Cleaning up of the XML menu scheme. New menu items: textfields, item
+ lists, query buttons.
+- Status page that lists uncompleted downloads
+- Updated YouTube, Google, and SVTPlay modules to work with the recent
+ changes on these sites
+
+2009-02-24: Version 0.1.1
+
+- Simplified building: better Makefile, fixed instructions in README
+- Updated Italian translations (thanks to Diego Pierotto)
+- German translation (contributed by Andre L.)
+- Daemon stops downloads gracefully when client disconnects
+- Fixed segfault when a menu title is NULL (this happened for
+ example on YouTube search results page)
+- sane filenames: no slashs, no dots in the beginning
+- Try to start daemon process automatically if can't open a connection
+- Removed busy polling when loading the main menu
+- Remove temporary file if the request fails
+- Ability to cancel downloads (through the status screen)
+- URLencode function in the plugin was bogus: the percent encoded
+ values should be in hex, not in decimal
+- Fixed problem with downloads never finishing if the server sends
+ shorter file than expected
+- History forward skipped over one page
+- SVTPlay: various improvements to the parsing of the web pages
+
+2009-03-07: Version 0.1.2
+
+- Unescape the stream URL before passing it to xineliboutput to make
+ Youtube streaming work.
+- Youtube: More robust parsing of search results page. Updated
+ categories parsing according to recent changes.
+- Updated Italian translations (thanks to Diego Pierotto)
+- Fixed a typo in German translation (thanks to Halim Sahin)
+
+2009-04-08: Version 0.1.3
+
+Plugin:
+- Call libxslt.init() only it exists (old versions of libxslt don't
+ have init())
+- Update download progress indicators in the status screen at regular
+ intervals
+
+webvi, the command line client:
+- Show download progress
+
+Video site modules:
+- YLE Areena: show error message if search fails, show categories in
+ the main menu, various smaller parsing improvements
+- Youtube: show error message if no search results, fix parsing of
+ Movies category
+
+2009-05-05: Version 0.1.4
+
+- Updated Italian translation (thanks to Diego Pierotto)
+- Config file for webvi for defining player programs and the address
+ of the daemon
+- Streaming now reverts back to lower quality video if high quality
+ version is not available (like downloading already did before)
+
+Video site modules:
+- Support for a new video site: Metacafe
+- Youtube: adapted parsing to comply with recent changes on Youtube.
+ Download HD quality video when available.
+- YLE Areena: download high quality videos by default
+- Google Video: support for videos hosted on Metacafe. Made parsing a
+ bit more robust.
+
+2009-05-10: Version 0.1.5
+
+- Don't crash VDR if can't connect to the daemon
+- Updated to work with Python 2.6 (a parameter name has changed in
+ asynchat)
+- Force the installation prefix for Python scripts to be /usr, not
+ /usr/local
+- Command line argument --daemoncmd specifies the command for starting
+ the webvid daemon
+
+2009-08-20: Version 0.1.6
+
+- Fixed compilation on gcc4.4. Thanks to Anssi Hannula.
+- Fixed Youtube module.
+- Removed the outdated YLE Areena support.
+
+2009-10-27: Version 0.1.7
+
+- Compatibility fixes for Youtube and Metacafe modules.
+
+2010-01-17: Version 0.2.0
+
+- The daemon is replaced by Python library with C bindings. This
+ simplifies the invocation of the VDR plugin and the command line
+ client.
+- New video service: Vimeo
+- Re-added support for YLE Areena (requires rtmpdump-yle from
+ http://users.tkk.fi/~aajanki/rtmpdump-yle/index.html).
+- Youtube: using the official API (except for video pages), this
+ should mean less breakage in the future. Various improvements on
+ the menus.
+
+2010-01-23: Version 0.2.1
+
+- Support for all Python versions.
+- Install the plugin with VDR's "make plugins". (If you use make
+ plugins, you still need to install the library separately.)
+
+2010-04-11: Version 0.2.2
+
+- Remember query terms and menu positions when moving in history.
+- Reduce delays when navigating the menu.
+- Install libwebvi.so* links correctly. Run ldconfig.
+- Write correct path to /etc/webvi.conf when installing to an
+ alternative location.
+- Show percentage as ??? on status page if the size is unknown.
+- Fixed Youtube module.
+
+2010-07-12: Version 0.3.0
+
+- Scheduled downloading
+- Show error details on status screen by pressing Info
+- Fix a crash when video URL is empty.
+- INI file options for controlling the download quality.
+- Add support for Finnish TV stations: MTV3 Katsomo, ruutu.fi, Subtv.
+- Make all downloads abortable.
+- Fixed Vimeo search.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..899da9e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,79 @@
+# prefix for non-VDR stuff
+PREFIX ?= /usr/local
+# VDR directory
+VDRDIR ?= /usr/src/vdr-1.6.0
+# VDR's library directory
+VDRPLUGINDIR ?= $(VDRDIR)/PLUGINS/lib
+# VDR's plugin conf directory
+VDRPLUGINCONFDIR ?= /video
+# VDR's locale directory
+VDRLOCALEDIR ?= $(VDRDIR)/locale
+
+VERSION := $(shell cat src/version)
+
+TMPDIR = /tmp
+ARCHIVE = webvideo-$(VERSION)
+PACKAGE = vdr-$(ARCHIVE)
+
+APIVERSION := $(shell sed -ne '/define APIVERSION/s/^.*"\(.*\)".*$$/\1/p' $(VDRDIR)/config.h)
+LIBDIR = $(VDRPLUGINDIR)
+
+# Default target compiles everything but does not install anything.
+all-noinstall: libwebvi vdr-plugin
+
+# This target is used by VDR's make plugins. It compiles everything
+# and installs VDR plugin.
+all: libwebvi vdr-plugin $(LIBDIR)/libvdr-webvideo.so.$(APIVERSION) webvi.conf
+
+vdr-plugin: libwebvi
+ $(MAKE) -C src/vdr-plugin LOCALEDIR=./locale LIBDIR=. VDRDIR=$(VDRDIR) CXXFLAGS="-fPIC -g -O2 -Wall -Woverloaded-virtual -Wno-parentheses"
+
+libwebvi: build-python
+ $(MAKE) -C src/libwebvi all libwebvi.a
+
+build-python: webvi.conf
+ python setup.py build
+
+webvi.conf:
+ @echo "[webvi]\n\ntemplatepath = $(PREFIX)/share/webvi/templates" > webvi.conf
+
+$(VDRPLUGINDIR)/libvdr-webvideo.so.$(APIVERSION): vdr-plugin
+ mkdir -p $(VDRPLUGINDIR)
+ cp -f src/vdr-plugin/libvdr-webvideo.so.$(APIVERSION) $(VDRPLUGINDIR)/libvdr-webvideo.so.$(APIVERSION)
+
+install-vdr-plugin: vdr-plugin $(VDRPLUGINDIR)/libvdr-webvideo.so.$(APIVERSION)
+ mkdir -p $(VDRLOCALEDIR)
+ cp -rf src/vdr-plugin/locale/* $(VDRLOCALEDIR)
+ mkdir -p $(VDRPLUGINCONFDIR)/webvideo
+ cp -f src/vdr-plugin/mime.types $(VDRPLUGINCONFDIR)/webvideo
+
+install-libwebvi: libwebvi
+ $(MAKE) -C src/libwebvi install
+
+install-python:
+ python setup.py install --prefix $(PREFIX)
+
+install-conf: webvi.conf
+ cp -f webvi.conf /etc/
+
+install-webvi: install-libwebvi install-python
+
+install: install-vdr-plugin install-webvi install-conf
+
+dist: clean
+ @-rm -rf $(TMPDIR)/$(ARCHIVE)
+ @mkdir $(TMPDIR)/$(ARCHIVE)
+ @cp -a * $(TMPDIR)/$(ARCHIVE)
+ @tar czf $(PACKAGE).tgz -C $(TMPDIR) $(ARCHIVE)
+ @-rm -rf $(TMPDIR)/$(ARCHIVE)
+ @echo Distribution package created as $(PACKAGE).tgz
+
+clean:
+ $(MAKE) -C src/vdr-plugin clean
+ $(MAKE) -C src/libwebvi clean
+ rm -rf src/vdr-plugin/locale webvi.conf
+ python setup.py clean -a
+ find . -name "*~" -exec rm {} \;
+ find . -name "*.pyc" -exec rm {} \;
+
+.PHONY: vdr-plugin libwebvi build-python install install-vdr-plugin install-webvi dist clean
diff --git a/README b/README
new file mode 100644
index 0000000..b0644e7
--- /dev/null
+++ b/README
@@ -0,0 +1,48 @@
+Written by: Antti Ajanki <antti.ajanki@iki.fi>
+Project's homepage: http://users.tkk.fi/~aajanki/vdr/webvideo
+
+Webvi is a tool for downloading and playing videos from popular video
+sharing webvites such as YouTube. There are two interfaces: a plugin
+for Video Disk Recorder (VDR), and a command line client. The two
+interfaces are described in README.vdrplugin and README.webvi.
+
+The common functionality of the VDR plugin and the command line client
+is implemented in a Python library (src/libwebvi/webvi). C bindings
+for the library are also provided (see src/libwebvi/libwebvi.h).
+
+Supported video sites:
+
+* Google Video [1]
+* Metacafe
+* MTV3 Katsomo
+* ruutu.fi [2]
+* Subtv
+* SVT Play
+* Vimeo
+* YLE Areena [2]
+* YouTube
+
+[1] Only videos hosted by Google, Youtube, Vimeo and SVT
+
+[2] Requires rtmpdump-yle
+ (http://users.tkk.fi/~aajanki/rtmpdump-yle/index.html). Streaming
+ does not work.
+
+Known problems:
+
+SVT Play: not all videos are working
+MTV3 Katsomo and Subtv: often the connection is lost before the
+ video is fully downloaded.
+
+Because of the modular design it is possible to add support for new
+sites quite easily. See doc/developers.txt for more information.
+
+License:
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3 of the License, or (at
+your option) any later version. The project includes files from
+iniparse library under MIT license.
+
+See the file COPYING for more information.
diff --git a/README.vdrplugin b/README.vdrplugin
new file mode 100644
index 0000000..838a936
--- /dev/null
+++ b/README.vdrplugin
@@ -0,0 +1,180 @@
+This is a "plugin" for the Video Disk Recorder (VDR).
+
+Written by: Antti Ajanki <antti.ajanki@iki.fi>
+
+Project's homepage: http://users.tkk.fi/~aajanki/vdr/webvideo
+
+Latest version available at: http://users.tkk.fi/~aajanki/vdr/webvideo
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3 of the License, or (at
+your option) any later version. The project includes files from
+iniparse library under MIT license.
+
+See the file COPYING for more information.
+
+Description:
+
+Webvideo is a VDR plugin for downloading videos from popular video
+sharing webvites such as YouTube. With the help of xineliboutput
+plugin the videos can be played directly in VDR without downloading
+them first. See README for the full list of supported sites.
+
+Requirements:
+
+* VDR 1.6.0 or later
+* Python 2.5 or later (http://www.python.org/)
+* simplejson (on Python 2.5, not needed on later Python versions)
+* libcurl (http://curl.haxx.se/)
+* pycurl 7.18.2 or newer (http://pycurl.sourceforge.net/)
+* libxml and libxslt (http://xmlsoft.org/)
+* a video player for viewing the downloaded videos or streaming videos
+ without downloading, for example xineliboutput plugin
+
+Suggested:
+
+* mimms 3.0 or later for downloading mms URLs
+ (http://savannah.nongnu.org/projects/mimms/)
+* rtmpdump-yle (http://users.tkk.fi/~aajanki/rtmpdump-yle/index.html)
+
+On Debian these dependencies can be satisfied by installing packages
+vdr, python-libxml2, python-libxslt1, python-pycurl,
+python-simplejson, mimms, either vdr-plugin-xineliboutput or
+vdr-plugin-mplayer, and their dependencies. For building the Debian
+package vdr-dev, libxml2-dev, python-all-dev, python-central,
+debhelper, cdbs, txt2man, gettext, and libglib2.0-dev are needed, as
+well.
+
+Installation and running
+------------------------
+
+These are the general install instructions. If you are using Debian,
+it easier to build and install the Debian package as instructed in the
+next section.
+
+tar -xzf /put/your/path/here/vdr-webvideo-X.Y.Z.tgz
+cd webvideo-X.Y.Z
+make VDRDIR=/path/to/VDR
+make install VDRDIR=/path/to/VDR
+
+These steps install the library and the VDR plugin. It is not
+necessary call VDR's make plugins.
+
+The installation locations can be further customized by appending the
+following variables to make install invocation:
+
+PREFIX prefix for the non-VDR files (default: /usr/local)
+VDRPLUGINDIR VDR's plugin dir (default: VDRDIR/PLUGINS/lib)
+VDRPLUGINCONFDIR VDR's plugin conf directory (default: /video)
+VDRLOCALEDIR VDR's locale directory (default: VDRDIR/locale)
+
+To start the VDR with the webvideo plugin run
+
+vdr -P "webvideo --templatedir=/usr/local/share/webvi/templates"
+
+The parameter --templatedir can be left out if the default PREFIX was
+used in make install.
+
+Installation on Debian
+----------------------
+
+tar -xzf /put/your/path/here/vdr-webvideo-X.Y.Z.tgz
+cd webvideo-X.Y.Z
+dpkg-buildpackage -rfakeroot -us -uc
+cd ..
+dpkg -i python-webvi_X.Y.Z-W_all.deb libwebvi0_X.Y.Z-W_i386.deb vdr-plugin-webvideo_X.Y.Z-W_i386.deb
+
+Debian's init scripts automatically load the plugin with proper
+parameters when VDR starts.
+
+VDR plugin command line parameters
+----------------------------------
+
+-d dir, --downloaddir=DIR Save downloaded files to DIR. The default
+ path is the VDR video directory.
+-t dir, --templatedir=DIR Read video site templates from DIR (default
+ /usr/local/share/webvi/templates)
+-c FILE, --conf=FILE Load settings from FILE
+
+Config file
+-----------
+
+Config file VDRPLUGINCONFDIR/webvi.plugin.conf (the default path can
+be overridden with the --conf argument) controls the quality of the
+downloaded and streamed videos.
+
+Currently only Youtube module supports multiple qualities. The
+following options are recognized in section [site-youtube]:
+
+download-min-quality, download-max-quality
+
+Minimum and maximum allowed quality when saving the video to disc. The
+default is to download the best available version of the video.
+
+stream-min-quality, stream-max-quality
+
+Minimum and maximum allowed quality when playing the video. The
+default is to download the best available version of the video.
+
+For Youtube, the available quality scores are (not all videos have the
+higher quality versions):
+
+ 50: standard quality (320x240, i.e. what you get in the web browser)
+ 60: medium quality (480x360 MP4)
+ 70: HD quality (720p)
+
+For example, if you don't have enough network bandwidth for playing
+the high quality versions smoothly, you may want to limit the maximum
+streaming quality score but still get the HD version when downloading.
+To do this, add the following snippet to the ini-file:
+
+[site-youtube]
+stream-max-quality = 50
+
+Usage
+-----
+
+Navigation links that lead to a new menu pages are marker with
+brackets [ ]. They can be followed by selecting them and pushing OK.
+
+The links without brackets are video or audio streams. They can be
+downloaded in the background by pushing OK. Pressing Blue on a media
+stream starts playing it immediately in xineliboutput plugin. Pressing
+Info shows more information about a media stream.
+
+Keys:
+
+OK Follow a link, or start to download a stream
+Red Go back to the previous menu /
+ Show download status screen
+Green Go forward in browsing history /
+ Edit timers
+Yellow Create timer
+Blue Play media stream without saving
+Info Show details of a media stream
+0 More options
+
+In the status screen:
+
+Red Cancel the selected download
+Info Show download error details
+
+Scheduled downloading
+---------------------
+
+The plugin can be configured to fetch new videos automatically from
+certain pages at regular intervals.
+
+To setup a timer, navigate to the page that contains the videos you
+want to fetch and press Yellow button. The the update interval can be
+set in the menu that opens. To save and execute the timer leave the
+timer menu with Back button.
+
+To list, edit or remove existing timers press 0 and Green.
+
+Hint: The timers work even on search results. To download new VDR
+related videos that appear in Youtube navigate to the Youtube search,
+enter "VDR Linux" as search term and "Date added" as sorting
+criterion, execute the search, and create a timer on the search
+results page.
diff --git a/README.webvi b/README.webvi
new file mode 100644
index 0000000..88f69eb
--- /dev/null
+++ b/README.webvi
@@ -0,0 +1,118 @@
+webvi - command line web video downloader
+
+Copyright 2009,2010 Antti Ajanki <antti.ajanki@iki.fi>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3 of the License, or (at
+your option) any later version. See the file COPYING for more
+information.
+
+Description
+-----------
+
+Webvi is a tool for downloading and playing videos from popular video
+sharing webvites such as YouTube. See README for the full list of
+supported sites.
+
+Installation
+------------
+
+To compile and install the command line client (without VDR plugin;
+see main README if you have VDR installed) run
+
+make libwebvi
+make install-webvi
+
+By default the program is installed under /usr/local. You can specify
+a different installation location by
+
+make libwebvi PREFIX=/usr
+make install-webvi install-conf PREFIX=/usr
+
+If you use an alternative installation location, you may need to put
+PREFIX/lib/pythonX.Y/site-packages/ or
+PREFIX/lib/pythonX.Y/dist-packages/ to your PYTHONPATH environment
+variable.
+
+Running
+-------
+
+webvi --templatedir=/usr/local/share/webvi/templates
+
+The parameter --templatedir can be left out if the default PREFIX was
+used in make install-library.
+
+Command line parameters
+-----------------------
+
+-h, --help show this help message and exit
+-t DIR, --templatepath=DIR read video site templates from DIR
+
+Usage
+-----
+
+The content of video sharing websites is presented as series of menus.
+The menus consists of two kinds of links. Navigation links, which are
+be identified by [brackets], are used to navigate the site.
+Non-bracketed links are media streams that can be downloaded or
+played.
+
+Following commands are recognized:
+
+help Show help
+select x Select a link whose index is x
+download x Download a media stream whose index is x
+stream x Play a media stream whose index is x
+back Go backward in history
+forward Go forward in history
+display Redisplay the current menu
+menu Go back to the main menu
+quit Quit the program
+
+x is an index of a link in the current menu. Entering an index number
+x without any command is a shorthand for "select x".
+
+Config file
+-----------
+
+Config files /etc/webvi.conf and ~/.webvi configure the behavior of
+the program. An example configuration file debian/webvi.conf is
+included in the sources.
+
+The config files are in ini format. The following items are recognized
+in section [webvi]:
+
+streamplayer1, ..., streamplayer9
+
+streamplayer1 to streamplayer9 are alternative media players to be
+used for streaming. The substring %s will be replaced by the stream
+URL. The players are tried one by one starting from streamplayer1
+until one of them succeeds playing the stream. If no players are
+defined in config files then vlc, totem, mplayer, and xine are tried
+(in that order).
+
+templatepath
+
+Path to video site templates.
+
+Quality of the downloaded and streamed videos can be selected in video
+site specific sections. Currently only Youtube module (section should
+be called [site-youtube]) supports multiple qualities. The following
+options are recognized:
+
+download-min-quality, download-max-quality
+
+Minimum and maximum allowed quality when saving the video to disc. The
+default is to download the best available version of the video.
+
+stream-min-quality, stream-max-quality
+
+Minimum and maximum allowed quality when playing the video. The
+default is to download the best available version of the video.
+
+For Youtube, the available quality scores are:
+
+ 50: standard quality (320x240, i.e. what you get in the web browser)
+ 60: medium quality (480x360 MP4)
+ 70: HD quality (720p)
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..af43a52
--- /dev/null
+++ b/TODO
@@ -0,0 +1,18 @@
+Keep connections alive by reusing the same curl handle
+
+Translation of strings in XSLT
+
+Show a poster image next to video's name
+
+plugin: scrolling does not work on video description pages
+
+SVTPlay: add search
+
+metacafe: fails to find video URL for videos with ID sy-*, cb-*
+(others?). The videos are probably hosted on some other sites (like
+yt-* -> youtube).
+
+Download percentage on the VDR plugin status page does is not updated
+when using external downloader process (YLE Areena)
+
+Streaming does not work with external downloader process (YLE Areena)
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..8a433db
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,150 @@
+vdr-plugin-webvideo (0.3.0-1) unstable; urgency=low
+
+ * Scheduled downloading
+ * Show error details on status screen by pressing Info.
+ * Fix a crash when video URL is empty.
+ * INI file options for controlling the download quality.
+ * Add support for Finnish TV stations: MTV3 Katsomo, ruutu.fi, Subtv.
+ * Make all downloads abortable.
+ * Fixed Vimeo search.
+ * Get the lower quality video when streaming from Youtube
+
+ -- Antti Ajanki <antti@gaspode> Mon, 12 Jul 2010 11:35:14 +0300
+
+vdr-plugin-webvideo (0.2.2-1) unstable; urgency=low
+
+ * New release
+ * Remember query terms and menu positions when moving in history.
+ * Reduce delays when navigating the menu.
+ * Show percentage as ??? on status page if the size is unknown.
+ * Fixed Youtube module.
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Sun, 11 Apr 2010 11:46:34 +0300
+
+vdr-plugin-webvideo (0.2.1-1) unstable; urgency=low
+
+ * Support for all Python versions.
+ * Install the plugin with VDR's "make plugins".
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Sat, 23 Jan 2010 15:03:45 +0200
+
+vdr-plugin-webvideo (0.2.0-1) unstable; urgency=low
+
+ * The daemon is replaced by Python library with C bindings.
+ * New video service: Vimeo
+ * Re-added support for YLE Areena
+ * Youtube: using the official API (except for video pages), this
+ should mean less breakage in the future. Various improvements on
+ the menus.
+ * Created new packages for the library: python-webvi, libwebvi0
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Sun, 17 Jan 2010 22:01:50 +0200
+
+vdr-plugin-webvideo (0.1.7-1) unstable; urgency=low
+
+ * New upstream release
+ - Moved the default download directory to /var/lib/webvideo
+ - Standards-Version: 3.8.3
+ * Moved webvid daemon to new package webvid
+ * Removed non-standard shebang line from debian/rules
+ * Removed DVBDIR from debian/rules
+ * Added myself to Uploaders and Debian Maintainers
+
+ -- Thomas Günther <tom@toms-cafe.de> Thu, 05 Nov 2009 02:34:31 +0100
+
+vdr-plugin-webvideo (0.1.6-1) unstable; urgency=low
+
+ * New release
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Thu, 20 Aug 2009 20:10:32 +0300
+
+vdr-plugin-webvideo (0.1.5-1) unstable; urgency=low
+
+ * New release
+ * Call postinst scripts fragments in the correct order
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Sun, 10 May 2009 20:24:06 +0300
+
+vdr-plugin-webvideo (0.1.4-1) unstable; urgency=low
+
+ * New release
+ * Moved the default download directory to /var/lib/webvideo.
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Sun, 03 May 2009 21:06:00 +0300
+
+vdr-plugin-webvideo (0.1.3-1) unstable; urgency=low
+
+ * New upstream release
+ * Bumped standards version to 3.8.1
+
+ -- Tobias Grimm <etobi@debian.org> Sun, 19 Apr 2009 16:46:26 +0200
+
+vdr-plugin-webvideo (0.1.2-1) unstable; urgency=low
+
+ * New upstream release
+ * Changed XS-Python-Version to current to fix FTBS problem
+ * Changed maintainer to Debian VDR Team
+ * Added Python to dependencies
+ * Removed dh_make boilerplates from debian/copyright
+ * Fixed homepage field in debian/control
+ * Bumped standards version to 3.8.1
+ * Install init script as /etc/init.d/webvid
+ * Fixed LOCALEDIR to build locales in correct directory
+ * Deactivated postinst-script-order.diff - seems not to be required anymore
+ * Added debian/watch
+ * Depend on libmms0
+ * Dropped patchlevel control field
+ * Build-Depend on vdr-dev (>=1.6.0-5)
+ * Added manpages
+ * Changed section to "video"
+
+ -- Tobias Grimm <etobi@debian.org> Sat, 11 Apr 2009 00:05:37 +0200
+
+vdr-plugin-webvideo (0.1.1-1) unstable; urgency=low
+
+ * New release
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Tue, 24 Feb 2009 19:38:28 +0300
+
+vdr-plugin-webvideo (0.1.0-1) unstable; urgency=low
+
+ * New release
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Sat, 7 Feb 2009 18:07:37 +0300
+
+vdr-plugin-webvideo (0.0.6-1) unstable; urgency=low
+
+ * New release
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Sat, 6 Dec 2008 11:27:05 +0300
+
+vdr-plugin-webvideo (0.0.5-1) unstable; urgency=low
+
+ * New release
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Tue, 2 Sep 2008 21:41:50 +0300
+
+vdr-plugin-webvideo (0.0.4-1) unstable; urgency=low
+
+ * New release
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Thu, 21 Aug 2008 12:29:38 +0300
+
+vdr-plugin-webvideo (0.0.3-1) unstable; urgency=low
+
+ * New release
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Tue, 19 Aug 2008 18:19:19 +0300
+
+vdr-plugin-webvideo (0.0.2-1) unstable; urgency=low
+
+ * New release
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Thu, 24 Jul 2008 20:44:15 +0300
+
+vdr-plugin-webvideo (0.0.1-1) unstable; urgency=low
+
+ * Initial release
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Wed, 25 Jun 2008 17:53:54 +0300
+
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..7ed6ff8
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+5
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..3deeccb
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,52 @@
+Source: vdr-plugin-webvideo
+Section: video
+Priority: extra
+Maintainer: Antti Ajanki <antti.ajanki@iki.fi>
+Uploaders: Tobias Grimm <etobi@debian.org>, Thomas Günther <tom@toms-cafe.de>
+Build-Depends: debhelper (>= 5.0.38), cdbs (>= 0.4.49), txt2man, vdr-dev (>= 1.6.0-5), gettext, libxml2-dev, python-all-dev, python-central (>= 0.5.6), libglib2.0-dev
+Standards-Version: 3.8.3
+Homepage: http://users.tkk.fi/~aajanki/vdr/webvideo
+XS-Python-Version: >= 2.5
+
+Package: python-webvi
+Architecture: all
+Section: python
+Depends: ${misc:Depends}, ${python:Depends}, python-libxml2, python-libxslt1, python-pycurl, python-simplejson, mimms
+Replaces: vdr-plugin-webvideo (<< 0.2.0), webvid (<< 0.2.0)
+Description: Web video downloader library - Python module
+ This package provides a library for downloading video and audio
+ streams from media sharing websites, such as YouTube or Google Video.
+ .
+ This is the Python module.
+XB-Python-Version: ${python:Versions}
+
+Package: libwebvi0
+Architecture: any
+Section: libs
+Depends: ${shlibs:Depends}, ${misc:Depends}, python, python-webvi
+Description: Web video downloader library - shared library
+ This package provides a library for downloading video and audio
+ streams from media sharing websites, such as YouTube or Google Video.
+ .
+ This is the shared library.
+XB-Python-Version: ${python:Versions}
+
+Package: libwebvi-dev
+Architecture: any
+Section: libdevel
+Depends: ${misc:Depends}, libc-dev
+Description: Web video downloader library - development files
+ This package provides a library for downloading video and audio
+ streams from media sharing websites, such as YouTube or Google Video.
+ .
+ This package contains the development files.
+
+Package: vdr-plugin-webvideo
+Architecture: any
+Section: video
+Depends: ${shlibs:Depends}, ${misc:Depends}, ${vdr:Depends}, libwebvi0
+Suggests: vdr-plugin-mplayer | vdr-plugin-xineliboutput
+Description: VDR plugin for downloading videos from the Web
+ This plugin for the Linux Video Disc Recorder (VDR) provides ability
+ to download video files from popular video sharing websites, such as
+ YouTube or Google Video.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..b1d4f0e
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,61 @@
+Upstream Homepage:
+ http://users.tkk.fi/~aajanki/vdr/webvideo
+
+Upstream Author:
+ Antti Ajanki <antti.ajanki@iki.fi>
+
+Debian Maintainers:
+ Antti Ajanki <antti.ajanki@iki.fi>
+ Tobias Grimm <etobi@debian.org>
+ Thomas Günther <tom@toms-cafe.de>
+
+Copyright:
+ (C) 2008, 2009, 2010 Antti Ajanki
+
+Copyright (Debian packaging):
+ (C) 2008, 2009 Antti Ajanki
+ (C) 2009 Tobias Grimm, Thomas Günther
+
+License:
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+ The complete text of the GNU General Public License can be found
+ in /usr/share/common-licenses/GPL-3 on most Debian systems.
+
+License (iniparser library):
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use, copy,
+ modify, merge, publish, distribute, sublicense, and/or sell copies of
+ the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
+License (Debian packaging):
+ The Debian packaging is licensed under the GPL, version 3 or any
+ later version, see /usr/share/common-licenses/GPL-3.
diff --git a/debian/libwebvi-dev.docs b/debian/libwebvi-dev.docs
new file mode 100644
index 0000000..523e527
--- /dev/null
+++ b/debian/libwebvi-dev.docs
@@ -0,0 +1 @@
+doc/developers.txt
diff --git a/debian/libwebvi-dev.install b/debian/libwebvi-dev.install
new file mode 100644
index 0000000..4946c7c
--- /dev/null
+++ b/debian/libwebvi-dev.install
@@ -0,0 +1,3 @@
+src/libwebvi/libwebvi.so usr/lib/
+src/libwebvi/libwebvi.h usr/include/
+src/libwebvi/libwebvi.a usr/lib/
diff --git a/debian/libwebvi0.docs b/debian/libwebvi0.docs
new file mode 100644
index 0000000..1333ed7
--- /dev/null
+++ b/debian/libwebvi0.docs
@@ -0,0 +1 @@
+TODO
diff --git a/debian/libwebvi0.install b/debian/libwebvi0.install
new file mode 100644
index 0000000..6c9de3d
--- /dev/null
+++ b/debian/libwebvi0.install
@@ -0,0 +1 @@
+src/libwebvi/libwebvi.so.0* usr/lib/
diff --git a/debian/plugin.webvideo.conf b/debian/plugin.webvideo.conf
new file mode 100644
index 0000000..8941e9b
--- /dev/null
+++ b/debian/plugin.webvideo.conf
@@ -0,0 +1,10 @@
+# Command line parameters for vdr-plugin-webvideo
+#
+# Recognized parameters:
+#
+# -d DIR, --downloaddir=DIR Save downloaded files to DIR
+# -t DIR, --templatedir=DIR Read video site templates from DIR
+# -c FILE, --conf=FILE Read settings from FILE
+#
+--downloaddir=/var/lib/webvideo
+--templatedir=/usr/share/webvi/templates
diff --git a/debian/postinst b/debian/postinst
new file mode 100644
index 0000000..33bb639
--- /dev/null
+++ b/debian/postinst
@@ -0,0 +1,43 @@
+#!/bin/sh
+# postinst script for vdr-plugin-webvideo
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * <postinst> `configure' <most-recently-configured-version>
+# * <old-postinst> `abort-upgrade' <new version>
+# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
+# <new-version>
+# * <postinst> `abort-remove'
+# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
+# <failed-install-package> <version> `removing'
+# <conflicting-package> <version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+ configure)
+ chown vdr:vdr /var/lib/webvideo
+ chmod g=rwx /var/lib/webvideo
+ ;;
+
+ abort-upgrade|abort-remove|abort-deconfigure)
+ ;;
+
+ *)
+ echo "postinst called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
+
+
diff --git a/debian/pycompat b/debian/pycompat
new file mode 100644
index 0000000..0cfbf08
--- /dev/null
+++ b/debian/pycompat
@@ -0,0 +1 @@
+2
diff --git a/debian/python-webvi.docs b/debian/python-webvi.docs
new file mode 100644
index 0000000..803a3ef
--- /dev/null
+++ b/debian/python-webvi.docs
@@ -0,0 +1,2 @@
+README
+README.webvi
diff --git a/debian/python-webvi.install b/debian/python-webvi.install
new file mode 100644
index 0000000..ad5c5ce
--- /dev/null
+++ b/debian/python-webvi.install
@@ -0,0 +1,4 @@
+debian/tmp/usr/lib/python*/site-packages/*
+debian/tmp/usr/bin/webvi
+debian/tmp/usr/share/webvi/templates/*
+debian/webvi.conf etc/
diff --git a/debian/python-webvi.manpages b/debian/python-webvi.manpages
new file mode 100644
index 0000000..a09ac56
--- /dev/null
+++ b/debian/python-webvi.manpages
@@ -0,0 +1 @@
+debian/webvi.1
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..44d5f4a
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,37 @@
+#!/usr/bin/make -f
+
+build/libwebvi0:: build/python-webvi
+build/vdr-plugin-webvideo:: build/libwebvi0
+
+DEB_PYTHON_SYSTEM := pycentral
+DEB_PYTHON_MODULE_PACKAGES := python-webvi
+
+# debhelper must be included before python-distutils
+include /usr/share/cdbs/1/rules/debhelper.mk
+include /usr/share/cdbs/1/class/makefile.mk
+include /usr/share/cdbs/1/class/python-distutils.mk
+
+DEB_COMPRESS_EXCLUDE := .py
+
+DEB_MAKE_BUILD_TARGET := all-noinstall VDRDIR=/usr/include/vdr
+DEB_MAKE_INSTALL_TARGET := install-vdr-plugin install-python VDRDIR=/usr/include/vdr PREFIX=$(CURDIR)/debian/tmp/usr VDRPLUGINDIR=$(CURDIR)/debian/tmp/usr/lib/vdr/plugins VDRPLUGINCONFDIR=$(CURDIR)/debian/tmp/var/lib/vdr/plugins VDRLOCALEDIR=$(CURDIR)/debian/tmp/usr/share/locale
+DEB_MAKE_CHECK_TARGET =
+
+DEB_INSTALL_CHANGELOGS_ALL = HISTORY
+
+#DEB_DH_STRIP_ARGS = -Xlibvdr-webvideo -Xlibwebvi
+
+TXT2MANPAGES = debian/webvi.1
+
+$(TXT2MANPAGES): %.1: %.1.txt
+ cat $< | grep -v "^###" | \
+ eval "`cat $< | grep "^### txt2man" | sed "s/### //"`" >$@
+
+common-build-indep:: $(TXT2MANPAGES)
+
+cleanbuilddir::
+ $(MAKE) -o .dependencies clean
+ rm -f $(TXT2MANPAGES)
+
+common-binary-predeb-arch::
+ sh /usr/share/vdr-dev/dependencies.sh
diff --git a/debian/vdr-plugin-webvideo.NEWS b/debian/vdr-plugin-webvideo.NEWS
new file mode 100644
index 0000000..5499f66
--- /dev/null
+++ b/debian/vdr-plugin-webvideo.NEWS
@@ -0,0 +1,10 @@
+vdr-plugin-webvideo (0.1.4-1) unstable; urgency=low
+
+ The default download directory is now /var/lib/webvideo instead of
+ /var/lib/video/webvideo, because the video directory is expected to
+ hold only VDR's own files. The videos are NOT automatically copied
+ to the new location. If you want to keep using the old location (it
+ should not cause problems in practice), leave the path in
+ /etc/vdr/plugins/plugin.webvideo.conf unchanged.
+
+ -- Antti Ajanki <antti.ajanki@iki.fi> Mon, 04 May 2009 20:42:36 +0300
diff --git a/debian/vdr-plugin-webvideo.dirs b/debian/vdr-plugin-webvideo.dirs
new file mode 100644
index 0000000..bab0473
--- /dev/null
+++ b/debian/vdr-plugin-webvideo.dirs
@@ -0,0 +1 @@
+var/lib/webvideo
diff --git a/debian/vdr-plugin-webvideo.docs b/debian/vdr-plugin-webvideo.docs
new file mode 100644
index 0000000..2707c31
--- /dev/null
+++ b/debian/vdr-plugin-webvideo.docs
@@ -0,0 +1,2 @@
+README
+README.vdrplugin
diff --git a/debian/vdr-plugin-webvideo.install b/debian/vdr-plugin-webvideo.install
new file mode 100644
index 0000000..0cf0829
--- /dev/null
+++ b/debian/vdr-plugin-webvideo.install
@@ -0,0 +1,6 @@
+debian/tmp/usr/lib/vdr/plugins/libvdr-webvideo.so.*
+debian/tmp/usr/share/locale
+debian/tmp/var/lib/vdr/plugins/webvideo
+debian/plugin.webvideo.conf etc/vdr/plugins/
+src/vdr-plugin/mime.types var/lib/vdr/plugins/webvideo/
+debian/webvi.plugin.conf var/lib/vdr/plugins/webvideo/
diff --git a/debian/watch b/debian/watch
new file mode 100644
index 0000000..2cb5267
--- /dev/null
+++ b/debian/watch
@@ -0,0 +1,2 @@
+version=2
+http://users.tkk.fi/~aajanki/vdr/webvideo/vdr-webvideo-(.*)\.tgz
diff --git a/debian/webvi.1.txt b/debian/webvi.1.txt
new file mode 100644
index 0000000..89140ed
--- /dev/null
+++ b/debian/webvi.1.txt
@@ -0,0 +1,67 @@
+### txt2man -s 1 -t WEBVI -v "download utility for media sharing websites"
+
+NAME
+ webvi - download video and audio from media sharing websites
+
+SYNOPSIS
+ webvi [options]
+
+DESCRIPTION
+ webvi is a command line tool for downloading video and audio files
+ from certain media sharing websites, such as YouTube or Google Video.
+
+OPTIONS
+ -h, --help show this help message and exit
+ -t DIR, --templatepath=DIR read video site templates from DIR
+
+USAGE
+ The program communicates with webvid daemon, which must be running in
+ the background.
+
+ The content of video sharing websites is presented as series of menus.
+ The menus consists of two kinds of links. Navigation links, which can
+ be identified by [brackets], are used to navigate the site.
+ Non-bracketed links are media streams that can be downloaded.
+
+ Following commands are recognized:
+
+ help Show help
+ select x Select a link whose index is x
+ download x Download a media stream whose index is x
+ stream x Play a media stream whose index is x
+ back Go backward in history
+ forward Go forward in history
+ display Redisplay the current menu
+ menu Go back to the main menu
+ quit Quit the program
+
+ x is an index of a link in the current menu. Entering an index number
+ x without any command is a shorthand for "select x".
+
+CONFIG FILE
+ webvi will read the following config files: /etc/webvi.conf and
+ ~/.webvi. The files are in INI format. The following options are
+ recognized in [webvi] section:
+
+ templatepath Path to video site templates
+ streamplayers1 to streamplayer9 are alternative player commands to be used for streaming videos
+
+ It is possible to set lower and upper bounds for stream quality in
+ [site-*] sections:
+
+ download-min-quality Minimum accepted quality for downloading
+ download-max-quality Maximum accepted quality for downloading
+ stream-min-quality Minimum accepted quality for streaming
+ stream-max-quality Maximum accepted quality for streaming
+
+ Currently only Youtube module offers multiple versions of the
+ streams. These are the available quality scores for Youtube
+ (section [site-youtube]):
+
+ 50 standard quality (320x240, i.e. what you get in the web browser)
+ 60 medium quality (480x360 MP4)
+ 70 HD quality (720p)
+
+AUTHOR
+ This manual page was written by Tobias Grimm <tg@e-tobi.net> and
+ Antti Ajanki <antti.ajanki@iki.fi>.
diff --git a/debian/webvi.conf b/debian/webvi.conf
new file mode 100644
index 0000000..647376b
--- /dev/null
+++ b/debian/webvi.conf
@@ -0,0 +1,20 @@
+[webvi]
+
+# streamplayer1 to streamplayer9 are alternative media players to be
+# used for streaming. The substring %s will be replaced by the stream
+# URL. The players are tried one by one starting from streamplayer1
+# until one of them succeeds playing the stream.
+#
+#streamplayer1 = vlc "%s"
+#streamplayer2 = totem "%s"
+#streamplayer3 = mplayer "%s"
+#streamplayer4 = xine "%s"
+
+# templatepath is path to the video service template directory
+templatepath = /usr/share/webvi/templates
+
+[site-youtube]
+
+# Limit the quality when streaming to make the playback smooth even if
+# the network connection is slow.
+stream-max-quality = 50
diff --git a/debian/webvi.plugin.conf b/debian/webvi.plugin.conf
new file mode 100644
index 0000000..675f8d7
--- /dev/null
+++ b/debian/webvi.plugin.conf
@@ -0,0 +1,3 @@
+[site-youtube]
+
+stream-max-quality = 50
diff --git a/doc/developers.txt b/doc/developers.txt
new file mode 100644
index 0000000..8259e9c
--- /dev/null
+++ b/doc/developers.txt
@@ -0,0 +1,152 @@
+libwebvi interface
+==================
+
+See src/libwebvi/libwebvi.h for C API, src/unittest/testlibwebvi.c for
+example code in C, and src/libwebvi/webvi/api.py for Python API.
+
+
+XSLT templates for video sites
+==============================
+
+libwebvi transforms the HTML of the web pages into a simple XML-based
+format using the sites specific XSLT stylesheets stored under
+templates directory. The XML describes navigation links and video
+streams found on the web pages. The VDR plugin or the command line
+client interprets the XML and displays a menu to the user.
+
+The following is an example libwebvi XML response for navigation page
+(WEBVIREQ_MENU) query:
+
+<wvmenu>
+ <title>Page title</title>
+
+ <link>
+ <label>Label of the link</label>
+ <ref>wvt:///youtube/description.xsl?srcurl=...</ref>
+ <stream>wvt:///youtube/video.xsl?srcurl=...</stream>
+ </link>
+
+ <textarea>
+ <label>Text that will be shown to the user</label>
+ </textarea>
+
+ <textfield name="search_query">
+ <label>Search terms</label>
+ </textfield>
+
+ <itemlist name="search_sort">
+ <label>Sort by</label>
+ <item value="">Relevance</item>
+ <item value="video_date_uploaded">Date Added</item>
+ <item value="video_view_count">View Count</item>
+ <item value="video_avg_rating">Rating</item>
+ </itemlist>
+
+ <button>
+ <label>Send</label>
+ <submission>wvt:///youtube/navigation.xsl?srcurl=...</submission>
+ </button>
+</wvmenu>
+
+<wvmenu> is the root node of a menu page. Possible children are
+<title>, <link>, <textfield>, <itemlist>, <textarea>, and <button>
+nodes.
+
+The content of <title> will be shown as the title of the menu.
+
+<link> is a navigation link. It must have a child <label>, which
+contains the text that will be shown to the user, and at least one of
+<ref> (a navigation reference) or <stream> (a media stream reference).
+The user interface provides three ways for selecting the link: a
+navigation action loads a new menu page by retrieving the reference in
+<ref> node from the library (using request type WEBVIREQ_MENU), a file
+action downloads the stream by requesting <stream> reference from the
+library (WEBVIREQ_FILE), stream URL action retrieves a direct URL to
+the stream by requesting reference <stream> (WEBVIREQ_STREAMURL).
+
+<textarea> defines a text widget. Again, child node <label> contains
+the text that will be displayed to the user.
+
+<textfield> defines a control for entering a string.
+
+<itemlist> defines a control for selecting one of the specified
+<item>s.
+
+<button> is a special kind of link. It encodes the state of
+<textfield>s and <itemlist>s into its reference. When the <button> is
+selected the state of other controls is send to the library. The
+reference is constructed by concatenating encoded values of each
+<textfield> and <itemlist> to the content of <submission> node. Each
+control is encoded as string "subst=name,value" where name is
+URL encoded presentation of the name nodes name attribute, and value
+is URL encoded presentation of the <textfield>'s UTF-8 input or value
+attribute of currently selected <item> in an <itemlist>. The encodings
+are joined by putting a "&" between them.
+
+
+Format of wvt:// references
+===========================
+
+In libwebvi, the navigation and menu requests are encoded encoded as
+URIs with scheme wvt:// . Typically, these references are extracted
+from <ref> or <stream> nodes in the XML menu documents. This section
+explains the format of the references.
+
+The references are of the form
+
+wvt://templatedir/template.xsl?srcurl=...&other_params=value
+
+The value of the srcurl parameter is a URL (typically an HTTP URL) of
+a web page on a video site. templatedir/template.xsl specifies a name
+of the XSLT template that is applied to the srcurl to get an XML menu.
+srcurl can be empty.
+
+templatedir called bin is a special directory. It contains executable
+programs or scripts instead of XSLT templates. When a reference to the
+bin directory is encountered, the named script is executed. Script's
+standard output is returned as the results of the operation. The
+script should return 0 on success.
+
+Parameters that affect the template processing can be appended to the
+reference. The parameter name and value are separated by '=',
+different parameters are separated by '&'. Following parameters are
+understood:
+
+param=name,value
+
+name and value are passed to the XSLT processor as an external
+parameter.
+
+subst=name,value
+
+Replaces string {name} in srcurl with value before retrieving it. This
+is used in search pages as discussed in previous section.
+
+contenttype=value
+
+value is used as content type if the server does not send a proper
+content type header.
+
+arg=value
+
+Used to pass command line arguments to the external scripts in the bin
+directory. There can be multiple args.
+
+minquality=value
+maxquality=value
+
+Only the streams whose quality ("priority" attribute of the "url" tag)
+is between minquality and maxquality are considered as candidates when
+selecting a stream to play. The default limits are 0 and 100.
+
+postprocess=json2xml
+
+The source is a JSON document which should be converted to XML before
+the XSLT transformation.
+
+http-header=headername,headervalue
+
+Append a HTTP header to the request. Ignored on transfers that use
+some other protocol besides HTTP.
+
+The reference to the main menu is wvt:///?srcurl=mainmenu .
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..30f3ee0
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,38 @@
+import re
+import os
+import os.path
+import sys
+from distutils.core import setup
+
+def extract_version():
+ sys.path.append('src/libwebvi/webvi')
+ import version
+ sys.path.pop()
+ return version.VERSION
+
+def install_service_files():
+ sourcedir = 'templates'
+ destdir = 'share/webvi/templates'
+
+ res = []
+ for service in os.listdir(sourcedir):
+ sdir = os.path.join(sourcedir, service)
+ sfiles = []
+ for f in os.listdir(sdir):
+ sfiles.append(os.path.join(sdir, f))
+ res.append((os.path.join(destdir, service), sfiles))
+ return res
+
+setup(
+ name='libwebvi',
+ version=extract_version(),
+ description='webvideo downloader library and command line client',
+ author='Antti Ajanki',
+ author_email='antti.ajanki@iki.fi',
+ license='GPLv3',
+ url='http://users.tkk.fi/~aajanki/vdr/webvideo',
+ package_dir = {'webvi': 'src/libwebvi/webvi', 'webvicli': 'src/webvicli/webvicli'},
+ packages=['webvi', 'webvicli'],
+ scripts=['src/webvicli/webvi'],
+ data_files=install_service_files()
+ )
diff --git a/src/libwebvi/Makefile b/src/libwebvi/Makefile
new file mode 100644
index 0000000..131c4a7
--- /dev/null
+++ b/src/libwebvi/Makefile
@@ -0,0 +1,34 @@
+PREFIX ?= /usr/local
+
+LIBNAME=libwebvi.so
+LIBSONAME=$(LIBNAME).0
+LIBMINOR=$(LIBSONAME).4
+
+VERSION:=$(shell cat ../version)
+PYLIB:=$(shell python pythonlibname.py)
+DEFINES:=-DPYTHONSHAREDLIB=\"$(PYLIB)\" -DLIBWEBVI_VERSION=\"$(VERSION)\"
+# append -DDEBUG to DEFINES to get debug output
+
+all: $(LIBMINOR)
+
+libwebvi.o: libwebvi.c libwebvi.h
+ $(CC) -fPIC -Wall -O2 -g $(CFLAGS) $(DEFINES) `python-config --cflags` -c -o libwebvi.o libwebvi.c
+
+$(LIBMINOR): libwebvi.o
+ $(CC) -shared -Wl,-soname,$(LIBSONAME) -Wl,--as-needed libwebvi.o `python-config --ldflags` -o $(LIBMINOR)
+ ln -sf $(LIBMINOR) $(LIBSONAME)
+ ln -sf $(LIBSONAME) $(LIBNAME)
+
+libwebvi.a: libwebvi.o
+ ar rsc libwebvi.a libwebvi.o
+
+clean:
+ rm -f *.o *~ libwebvi.so* libwebvi.a
+ rm -f webvi/*.pyc webvi/*~
+
+install: $(LIBMINOR)
+ mkdir -p $(PREFIX)/lib
+ cp --remove-destination -d $(LIBNAME)* $(PREFIX)/lib
+ /sbin/ldconfig $(PREFIX)/lib
+
+.PHONY: clean install
diff --git a/src/libwebvi/libwebvi.c b/src/libwebvi/libwebvi.c
new file mode 100644
index 0000000..c4d9aed
--- /dev/null
+++ b/src/libwebvi/libwebvi.c
@@ -0,0 +1,814 @@
+/*
+ * libwebvi.c: C bindings for webvi Python module
+ *
+ * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <Python.h>
+#include <stdio.h>
+#include <dlfcn.h>
+
+#include "libwebvi.h"
+
+static const char *VERSION = "libwebvi/" LIBWEBVI_VERSION;
+
+static const int MAX_ERROR_MESSAGE_LENGTH = 512;
+static const int MAX_MSG_STRING_LENGTH = 512;
+
+static PyThreadState *main_state = NULL;
+
+typedef struct per_interpreter_data_t {
+ PyThreadState *interp;
+ PyObject *webvi_module;
+ char *last_error;
+ WebviMsg latest_message;
+} per_interpreter_data;
+
+#ifdef DEBUG
+
+#define debug(x...) fprintf(stderr, x)
+#define handle_pyerr() { if (PyErr_Occurred()) { PyErr_Print(); } }
+
+#else
+
+#define debug(x...)
+#define handle_pyerr() PyErr_Clear()
+
+#endif
+
+
+/**********************************************************************
+ *
+ * Internal functions
+ */
+
+static PyObject *call_python(PyObject *webvi_module,
+ const char *funcname,
+ PyObject *args) {
+ PyObject *func, *val = NULL;
+
+#ifdef DEBUG
+ debug("call_python %s ", funcname);
+ if (PyObject_Print(args, stderr, 0) == -1)
+ debug("<print failed>");
+ debug("\n");
+#endif
+
+ func = PyObject_GetAttrString(webvi_module, funcname);
+ if (func) {
+ val = PyObject_CallObject(func, args);
+
+ Py_DECREF(func);
+ }
+
+ return val;
+}
+
+static long set_callback(PyObject *webvi_module, WebviHandle h,
+ WebviOption callbacktype,
+ webvi_callback callback,
+ PyObject *prototype) {
+ long res = WEBVIERR_UNKNOWN_ERROR;
+
+ if (prototype && PyCallable_Check(prototype)) {
+ PyObject *args = Py_BuildValue("(l)", (long)callback);
+ PyObject *val = PyObject_CallObject(prototype, args);
+ Py_DECREF(args);
+
+ if (val) {
+ PyObject *webvihandle = PyInt_FromLong(h);
+ PyObject *option = PyInt_FromLong(callbacktype);
+ PyObject *args2 = PyTuple_Pack(3, webvihandle, option, val);
+ PyObject *retval = call_python(webvi_module, "set_opt", args2);
+ Py_DECREF(args2);
+ Py_DECREF(option);
+ Py_DECREF(webvihandle);
+ Py_DECREF(val);
+
+ if (retval) {
+ if (PyInt_Check(retval))
+ res = PyInt_AsLong(retval);
+ Py_DECREF(retval);
+ }
+ }
+ }
+
+ if (res == WEBVIERR_UNKNOWN_ERROR)
+ handle_pyerr();
+
+ return res;
+}
+
+/*
+ * Converts PyInt to WebviResult.
+ *
+ * If intobject is NULL, assumes that a Python exception has occurred.
+ */
+static WebviResult pyint_as_webviresult(PyObject *intobject) {
+ if (intobject && PyInt_Check(intobject))
+ return PyInt_AsLong(intobject);
+
+ handle_pyerr();
+
+ return WEBVIERR_UNKNOWN_ERROR;
+}
+
+/*
+ * Duplicate Python string as C string. If the parameter is a unicode
+ * object, it is encoded to UTF-8. The caller must free the returned
+ * memory.
+ */
+static char *PyString_strdupUTF8(PyObject *string) {
+ char *buffer = NULL;
+ Py_ssize_t len = -1;
+ char *ret = NULL;
+ PyObject *realstring = string;
+ Py_INCREF(realstring);
+
+ if (PyUnicode_Check(realstring)) {
+ PyObject *encoded = PyUnicode_AsUTF8String(realstring);
+ if (encoded) {
+ Py_DECREF(realstring);
+ realstring = encoded;
+ } else {
+ handle_pyerr();
+ }
+ }
+
+ if (PyString_AsStringAndSize(realstring, &buffer, &len) == -1) {
+ handle_pyerr();
+ buffer = "";
+ len = 0;
+ }
+
+ if (buffer) {
+ ret = (char *)malloc((len+1)*sizeof(char));
+ if (ret)
+ memcpy(ret, buffer, len+1);
+ }
+
+ Py_DECREF(realstring);
+
+ return ret;
+}
+
+/**********************************************************************
+ *
+ * Public functions
+ */
+
+int webvi_global_init() {
+ if (main_state)
+ return 0;
+
+ // Python modules in lib-dynload/*.so do not correctly depend on
+ // libpython*.so. We need to dlopen the library here, otherwise
+ // importing webvi dies with "undefined symbol:
+ // PyExc_ValueError". See http://bugs.python.org/issue4434
+ dlopen(PYTHONSHAREDLIB, RTLD_LAZY | RTLD_GLOBAL);
+
+ Py_InitializeEx(0);
+ PyEval_InitThreads();
+ main_state = PyThreadState_Get();
+ PyEval_ReleaseLock(); /* release GIL acquired by PyEval_InitThreads */
+
+ return 0;
+}
+
+void webvi_cleanup(int cleanup_python) {
+ /* Should we kill active interpreters first? */
+
+ if (cleanup_python != 0) {
+ PyEval_AcquireLock();
+ PyThreadState_Swap(main_state);
+ Py_Finalize();
+ }
+}
+
+WebviCtx webvi_initialize_context(void) {
+ per_interpreter_data *ctx = (per_interpreter_data *)malloc(sizeof(per_interpreter_data));
+ if (!ctx)
+ goto err;
+
+ PyEval_AcquireLock();
+
+ ctx->interp = NULL;
+ ctx->last_error = NULL;
+ ctx->latest_message.msg = 0;
+ ctx->latest_message.handle = -1;
+ ctx->latest_message.status_code = -1;
+ ctx->latest_message.data = (char *)malloc(MAX_MSG_STRING_LENGTH*sizeof(char));
+ if (!ctx->latest_message.data)
+ goto err;
+
+ ctx->interp = Py_NewInterpreter();
+ if (!ctx->interp) {
+ debug("Py_NewInterpreter failed\n");
+ goto err;
+ }
+
+ PyThreadState_Swap(ctx->interp);
+
+ ctx->webvi_module = PyImport_ImportModule("webvi.api");
+ if (!ctx->webvi_module) {
+ debug("import webvi.api failed\n");
+ handle_pyerr();
+ goto err;
+ }
+
+ /* These are used to wrap C-callbacks into Python callables.
+ Keep in sync with libwebvi.h. */
+ if (PyRun_SimpleString("from ctypes import CFUNCTYPE, c_int, c_size_t, c_char_p, c_void_p\n"
+ "WriteCallback = CFUNCTYPE(c_size_t, c_char_p, c_size_t, c_void_p)\n"
+ "ReadCallback = CFUNCTYPE(c_size_t, c_char_p, c_size_t, c_void_p)\n") != 0) {
+ debug("callback definitions failed\n");
+ goto err;
+ }
+
+ PyEval_ReleaseThread(ctx->interp);
+
+ return (WebviCtx)ctx;
+
+err:
+ if (ctx) {
+ if (ctx->interp) {
+ Py_EndInterpreter(ctx->interp);
+ PyThreadState_Swap(NULL);
+ }
+
+ PyEval_ReleaseLock();
+
+ if (ctx->latest_message.data)
+ free(ctx->latest_message.data);
+ free(ctx);
+ }
+
+ return 0;
+}
+
+void webvi_cleanup_context(WebviCtx ctx) {
+ if (ctx == 0)
+ return;
+
+ per_interpreter_data *c = (per_interpreter_data *)ctx;
+
+ PyThreadState_Swap(c->interp);
+
+ /* FIXME: explicitly terminate all active handles? */
+
+ Py_DECREF(c->webvi_module);
+ c->webvi_module = NULL;
+
+ Py_EndInterpreter(c->interp);
+ c->interp = NULL;
+
+ PyThreadState_Swap(NULL);
+
+ free(c);
+}
+
+const char* webvi_version(void) {
+ return VERSION;
+}
+
+const char* webvi_strerror(WebviCtx ctx, WebviResult res) {
+ char *errmsg;
+
+ per_interpreter_data *c = (per_interpreter_data *)ctx;
+
+ if (!c->last_error) {
+ /* We are going to leak c->last_error */
+ c->last_error = (char *)malloc(MAX_ERROR_MESSAGE_LENGTH*sizeof(char));
+ if (!c->last_error)
+ return NULL;
+ }
+
+ PyEval_AcquireThread(c->interp);
+
+ PyObject *args = Py_BuildValue("(i)", res);
+ PyObject *msg = call_python(c->webvi_module, "strerror", args);
+ Py_DECREF(args);
+
+ if (msg) {
+ errmsg = PyString_AsString(msg);
+ if (!errmsg) {
+ handle_pyerr();
+ errmsg = "Internal error";
+ }
+
+ strncpy(c->last_error, errmsg, MAX_ERROR_MESSAGE_LENGTH-1);
+ c->last_error[MAX_ERROR_MESSAGE_LENGTH] = '\0';
+
+ Py_DECREF(msg);
+ } else {
+ handle_pyerr();
+ }
+
+ PyEval_ReleaseThread(c->interp);
+
+ return c->last_error;
+}
+
+WebviResult webvi_set_config(WebviCtx ctx, WebviConfig conf, const char *value) {
+ WebviResult res;
+ per_interpreter_data *c = (per_interpreter_data *)ctx;
+
+ PyEval_AcquireThread(c->interp);
+
+ PyObject *args = Py_BuildValue("(is)", conf, value);
+ PyObject *v = call_python(c->webvi_module, "set_config", args);
+ Py_DECREF(args);
+
+ res = pyint_as_webviresult(v);
+ Py_XDECREF(v);
+
+ PyEval_ReleaseThread(c->interp);
+
+ return res;
+}
+
+WebviHandle webvi_new_request(WebviCtx ctx, const char *webvireference, WebviRequestType type) {
+ WebviHandle res = -1;
+ per_interpreter_data *c = (per_interpreter_data *)ctx;
+
+ PyEval_AcquireThread(c->interp);
+
+ PyObject *args = Py_BuildValue("(si)", webvireference, type);
+ PyObject *v = call_python(c->webvi_module, "new_request", args);
+ Py_DECREF(args);
+
+ res = pyint_as_webviresult(v);
+ Py_XDECREF(v);
+
+ PyEval_ReleaseThread(c->interp);
+
+ return res;
+}
+
+WebviResult webvi_start_handle(WebviCtx ctx, WebviHandle h) {
+ WebviResult res;
+ per_interpreter_data *c = (per_interpreter_data *)ctx;
+
+ PyEval_AcquireThread(c->interp);
+
+ PyObject *args = Py_BuildValue("(i)", h);
+ PyObject *v = call_python(c->webvi_module, "start_handle", args);
+ Py_DECREF(args);
+
+ res = pyint_as_webviresult(v);
+ Py_XDECREF(v);
+
+ PyEval_ReleaseThread(c->interp);
+
+ return res;
+}
+
+WebviResult webvi_stop_handle(WebviCtx ctx, WebviHandle h) {
+ WebviResult res = WEBVIERR_UNKNOWN_ERROR;
+ per_interpreter_data *c = (per_interpreter_data *)ctx;
+
+ PyEval_AcquireThread(c->interp);
+
+ PyObject *args = Py_BuildValue("(i)", h);
+ PyObject *v = call_python(c->webvi_module, "stop_handle", args);
+ Py_DECREF(args);
+
+ res = pyint_as_webviresult(v);
+ Py_XDECREF(v);
+
+ PyEval_ReleaseThread(c->interp);
+
+ return res;
+}
+
+WebviResult webvi_delete_handle(WebviCtx ctx, WebviHandle h) {
+ WebviResult res;
+ per_interpreter_data *c = (per_interpreter_data *)ctx;
+
+ PyEval_AcquireThread(c->interp);
+
+ PyObject *args = Py_BuildValue("(i)", h);
+ PyObject *v = call_python(c->webvi_module, "delete_handle", args);
+ Py_DECREF(args);
+
+ res = pyint_as_webviresult(v);
+ Py_XDECREF(v);
+
+ PyEval_ReleaseThread(c->interp);
+
+ return res;
+}
+
+WebviResult webvi_set_opt(WebviCtx ctx, WebviHandle h, WebviOption opt, ...) {
+ va_list argptr;
+ WebviResult res = WEBVIERR_UNKNOWN_ERROR;
+ per_interpreter_data *c = (per_interpreter_data *)ctx;
+
+ PyEval_AcquireThread(c->interp);
+
+ PyObject *m = PyImport_AddModule("__main__");
+ if (!m) {
+ handle_pyerr();
+ PyEval_ReleaseThread(c->interp);
+ return res;
+ }
+
+ PyObject *maindict = PyModule_GetDict(m);
+
+ va_start(argptr, opt);
+
+ switch (opt) {
+ case WEBVIOPT_WRITEFUNC:
+ {
+ webvi_callback writerptr = va_arg(argptr, webvi_callback);
+ PyObject *write_prototype = PyDict_GetItemString(maindict, "WriteCallback");
+ if (write_prototype)
+ res = set_callback(c->webvi_module, h, WEBVIOPT_WRITEFUNC,
+ writerptr, write_prototype);
+ break;
+ }
+
+ case WEBVIOPT_WRITEDATA:
+ {
+ void *data = va_arg(argptr, void *);
+ PyObject *args = Py_BuildValue("(iil)", h, WEBVIOPT_WRITEDATA, (long)data);
+ PyObject *v = call_python(c->webvi_module, "set_opt", args);
+ Py_DECREF(args);
+
+ res = pyint_as_webviresult(v);
+ Py_XDECREF(v);
+
+ break;
+ }
+
+ case WEBVIOPT_READFUNC:
+ {
+ webvi_callback readerptr = va_arg(argptr, webvi_callback);
+ PyObject *read_prototype = PyDict_GetItemString(maindict, "ReadCallback");
+ if (read_prototype)
+ res = set_callback(c->webvi_module, h, WEBVIOPT_READFUNC,
+ readerptr, read_prototype);
+ break;
+ }
+
+ case WEBVIOPT_READDATA:
+ {
+ void *data = va_arg(argptr, void *);
+ PyObject *args = Py_BuildValue("(iil)", h, WEBVIOPT_READDATA, (long)data);
+ PyObject *v = call_python(c->webvi_module, "set_opt", args);
+ Py_DECREF(args);
+
+ res = pyint_as_webviresult(v);
+ Py_XDECREF(v);
+
+ break;
+ }
+
+ default:
+ res = WEBVIERR_INVALID_PARAMETER;
+ break;
+ }
+
+ va_end(argptr);
+
+ PyEval_ReleaseThread(c->interp);
+
+ return res;
+}
+
+WebviResult webvi_get_info(WebviCtx ctx, WebviHandle h, WebviInfo info, ...) {
+ va_list argptr;
+ WebviResult res = WEBVIERR_UNKNOWN_ERROR;
+ per_interpreter_data *c = (per_interpreter_data *)ctx;
+
+ PyEval_AcquireThread(c->interp);
+
+ va_start(argptr, info);
+
+ switch (info) {
+ case WEBVIINFO_URL:
+ {
+ char **dest = va_arg(argptr, char **);
+ PyObject *args = Py_BuildValue("(ii)", h, info);
+ PyObject *v = call_python(c->webvi_module, "get_info", args);
+ Py_DECREF(args);
+
+ *dest = NULL;
+
+ if (v) {
+ if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) {
+ PyObject *retval = PySequence_GetItem(v, 0);
+ PyObject *val = PySequence_GetItem(v, 1);
+
+ if (PyInt_Check(retval) &&
+ (PyString_Check(val) || PyUnicode_Check(val))) {
+ *dest = PyString_strdupUTF8(val);
+ res = PyInt_AsLong(retval);
+ }
+
+ Py_DECREF(val);
+ Py_DECREF(retval);
+ }
+
+ Py_DECREF(v);
+ } else {
+ handle_pyerr();
+ }
+
+ break;
+ }
+
+ case WEBVIINFO_CONTENT_LENGTH:
+ {
+ long *dest = va_arg(argptr, long *);
+ PyObject *args = Py_BuildValue("(ii)", h, info);
+ PyObject *v = call_python(c->webvi_module, "get_info", args);
+ Py_DECREF(args);
+
+ *dest = -1;
+
+ if (v) {
+ if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) {
+ PyObject *retval = PySequence_GetItem(v, 0);
+ PyObject *val = PySequence_GetItem(v, 1);
+
+ if (PyInt_Check(retval) && PyInt_Check(val)) {
+ *dest = PyInt_AsLong(val);
+ res = PyInt_AsLong(retval);
+ }
+
+ Py_DECREF(val);
+ Py_DECREF(retval);
+ }
+
+ Py_DECREF(v);
+ } else {
+ handle_pyerr();
+ }
+
+ break;
+ }
+
+ case WEBVIINFO_CONTENT_TYPE:
+ {
+ char **dest = va_arg(argptr, char **);
+ PyObject *args = Py_BuildValue("(ii)", h, info);
+ PyObject *v = call_python(c->webvi_module, "get_info", args);
+ Py_DECREF(args);
+
+ *dest = NULL;
+
+ if (v) {
+ if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) {
+ PyObject *retval = PySequence_GetItem(v, 0);
+ PyObject *val = PySequence_GetItem(v, 1);
+
+ if (PyInt_Check(retval) &&
+ (PyString_Check(val) || PyUnicode_Check(val))) {
+ *dest = PyString_strdupUTF8(val);
+ res = PyInt_AsLong(retval);
+ }
+
+ Py_DECREF(val);
+ Py_DECREF(retval);
+ }
+
+ Py_DECREF(v);
+ } else {
+ handle_pyerr();
+ }
+
+ break;
+ }
+
+ case WEBVIINFO_STREAM_TITLE:
+ {
+ char **dest = va_arg(argptr, char **);
+ PyObject *args = Py_BuildValue("(ii)", h, info);
+ PyObject *v = call_python(c->webvi_module, "get_info", args);
+ Py_DECREF(args);
+
+ *dest = NULL;
+
+ if (v) {
+ if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) {
+ PyObject *retval = PySequence_GetItem(v, 0);
+ PyObject *val = PySequence_GetItem(v, 1);
+
+ if (PyInt_Check(retval) &&
+ (PyString_Check(val) || PyUnicode_Check(val))) {
+ *dest = PyString_strdupUTF8(val);
+ res = PyInt_AsLong(retval);
+ }
+
+ Py_DECREF(val);
+ Py_DECREF(retval);
+ }
+
+ Py_DECREF(v);
+ } else {
+ handle_pyerr();
+ }
+
+ break;
+ }
+
+ default:
+ res = WEBVIERR_INVALID_PARAMETER;
+ break;
+ }
+
+ va_end(argptr);
+
+ PyEval_ReleaseThread(c->interp);
+
+ return res;
+}
+
+WebviResult webvi_fdset(WebviCtx ctx,
+ fd_set *read_fd_set,
+ fd_set *write_fd_set,
+ fd_set *exc_fd_set,
+ int *max_fd)
+{
+ WebviResult res = WEBVIERR_UNKNOWN_ERROR;
+ per_interpreter_data *c = (per_interpreter_data *)ctx;
+
+ PyEval_AcquireThread(c->interp);
+
+ PyObject *v = call_python(c->webvi_module, "fdset", NULL);
+
+ if (v && PySequence_Check(v) && (PySequence_Length(v) == 5)) {
+ PyObject *retval = PySequence_GetItem(v, 0);
+ PyObject *readfd = PySequence_GetItem(v, 1);
+ PyObject *writefd = PySequence_GetItem(v, 2);
+ PyObject *excfd = PySequence_GetItem(v, 3);
+ PyObject *maxfd = PySequence_GetItem(v, 4);
+ PyObject *fd;
+ int i;
+
+ if (readfd && PySequence_Check(readfd)) {
+ for (i=0; i<PySequence_Length(readfd); i++) {
+ fd = PySequence_GetItem(readfd, i);
+ if (fd && PyInt_Check(fd))
+ FD_SET(PyInt_AsLong(fd), read_fd_set);
+ else
+ handle_pyerr();
+
+ Py_XDECREF(fd);
+ }
+ }
+
+ if (writefd && PySequence_Check(writefd)) {
+ for (i=0; i<PySequence_Length(writefd); i++) {
+ fd = PySequence_GetItem(writefd, i);
+ if (fd && PyInt_Check(fd))
+ FD_SET(PyInt_AsLong(fd), write_fd_set);
+ else
+ handle_pyerr();
+
+ Py_XDECREF(fd);
+ }
+ }
+
+ if (excfd && PySequence_Check(excfd)) {
+ for (i=0; i<PySequence_Length(excfd); i++) {
+ fd = PySequence_GetItem(excfd, i);
+ if (fd && PyInt_Check(fd))
+ FD_SET(PyInt_AsLong(fd), exc_fd_set);
+ else
+ handle_pyerr();
+
+ Py_XDECREF(fd);
+ }
+ }
+
+ if (maxfd && PyInt_Check(maxfd))
+ *max_fd = PyInt_AsLong(maxfd);
+ else
+ handle_pyerr();
+
+ if (retval && PyInt_Check(retval))
+ res = PyInt_AsLong(retval);
+ else
+ handle_pyerr();
+
+ Py_XDECREF(maxfd);
+ Py_XDECREF(excfd);
+ Py_XDECREF(writefd);
+ Py_XDECREF(readfd);
+ Py_XDECREF(retval);
+ } else {
+ handle_pyerr();
+ }
+
+ PyEval_ReleaseThread(c->interp);
+
+ return res;
+}
+
+WebviResult webvi_perform(WebviCtx ctx, int fd, int ev_bitmask, long *running_handles) {
+ WebviResult res = WEBVIERR_UNKNOWN_ERROR;
+ per_interpreter_data *c = (per_interpreter_data *)ctx;
+
+ PyEval_AcquireThread(c->interp);
+
+ PyObject *args = Py_BuildValue("(ii)", fd, ev_bitmask);
+ PyObject *v = call_python(c->webvi_module, "perform", args);
+ Py_DECREF(args);
+
+ if (v && (PySequence_Check(v) == 1) && (PySequence_Size(v) == 2)) {
+ PyObject *retval = PySequence_GetItem(v, 0);
+ PyObject *numhandles = PySequence_GetItem(v, 1);
+
+ if (PyInt_Check(numhandles))
+ *running_handles = PyInt_AsLong(numhandles);
+ if (PyInt_Check(retval))
+ res = PyInt_AsLong(retval);
+
+ Py_DECREF(numhandles);
+ Py_DECREF(retval);
+ } else {
+ handle_pyerr();
+ }
+
+ Py_XDECREF(v);
+
+ PyEval_ReleaseThread(c->interp);
+
+ return res;
+}
+
+WebviMsg *webvi_get_message(WebviCtx ctx, int *remaining_messages) {
+ per_interpreter_data *c = (per_interpreter_data *)ctx;
+
+ WebviMsg *msg = NULL;
+
+ PyEval_AcquireThread(c->interp);
+
+ PyObject *v = call_python(c->webvi_module, "pop_message", NULL);
+
+ if (v) {
+ if ((PySequence_Check(v) == 1) && (PySequence_Length(v) == 4)) {
+ msg = &(c->latest_message);
+ msg->msg = WEBVIMSG_DONE;
+ msg->handle = -1;
+ msg->status_code = -1;
+ msg->data[0] = '\0';
+
+ PyObject *handle = PySequence_GetItem(v, 0);
+ if (handle && PyInt_Check(handle))
+ msg->handle = (WebviHandle)PyInt_AsLong(handle);
+ Py_XDECREF(handle);
+
+ PyObject *status = PySequence_GetItem(v, 1);
+ if (status && PyInt_Check(status))
+ msg->status_code = (int)PyInt_AsLong(status);
+ Py_XDECREF(status);
+
+ PyObject *errmsg = PySequence_GetItem(v, 2);
+ if (errmsg &&
+ (PyString_Check(errmsg) || PyUnicode_Check(errmsg))) {
+ char *cstr = PyString_strdupUTF8(errmsg);
+ if (cstr) {
+ strncpy(msg->data, cstr, MAX_MSG_STRING_LENGTH);
+ msg->data[MAX_MSG_STRING_LENGTH-1] = '\0';
+
+ free(cstr);
+ }
+ }
+ Py_XDECREF(errmsg);
+
+ PyObject *remaining = PySequence_GetItem(v, 3);
+ if (remaining && PyInt_Check(remaining))
+ *remaining_messages = (int)PyInt_AsLong(remaining);
+ else
+ *remaining_messages = 0;
+ Py_XDECREF(remaining);
+ }
+
+ if (msg->handle == -1)
+ msg = NULL;
+
+ Py_DECREF(v);
+ } else {
+ handle_pyerr();
+ }
+
+ PyEval_ReleaseThread(c->interp);
+
+ return msg;
+}
diff --git a/src/libwebvi/libwebvi.h b/src/libwebvi/libwebvi.h
new file mode 100644
index 0000000..dd7ff39
--- /dev/null
+++ b/src/libwebvi/libwebvi.h
@@ -0,0 +1,330 @@
+/*
+ * libwebvi.h: C bindings for webvi Python module
+ *
+ * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __LIBWEBVI_H
+#define __LIBWEBVI_H
+
+#include <sys/select.h>
+#include <stdlib.h>
+
+typedef int WebviHandle;
+
+typedef ssize_t (*webvi_callback)(const char *, size_t, void *);
+
+typedef enum {
+ WEBVIMSG_DONE
+} WebviMsgType;
+
+typedef struct {
+ WebviMsgType msg;
+ WebviHandle handle;
+ int status_code;
+ char *data;
+} WebviMsg;
+
+typedef enum {
+ WEBVIREQ_MENU,
+ WEBVIREQ_FILE,
+ WEBVIREQ_STREAMURL
+} WebviRequestType;
+
+typedef enum {
+ WEBVIERR_UNKNOWN_ERROR = -1,
+ WEBVIERR_OK = 0,
+ WEBVIERR_INVALID_HANDLE,
+ WEBVIERR_INVALID_PARAMETER
+} WebviResult;
+
+typedef enum {
+ WEBVIOPT_WRITEFUNC,
+ WEBVIOPT_READFUNC,
+ WEBVIOPT_WRITEDATA,
+ WEBVIOPT_READDATA,
+} WebviOption;
+
+typedef enum {
+ WEBVIINFO_URL,
+ WEBVIINFO_CONTENT_LENGTH,
+ WEBVIINFO_CONTENT_TYPE,
+ WEBVIINFO_STREAM_TITLE
+} WebviInfo;
+
+typedef enum {
+ WEBVI_SELECT_TIMEOUT = 0,
+ WEBVI_SELECT_READ = 1,
+ WEBVI_SELECT_WRITE = 2,
+ WEBVI_SELECT_EXCEPTION = 4
+} WebviSelectBitmask;
+
+typedef enum {
+ WEBVI_CONFIG_TEMPLATE_PATH
+} WebviConfig;
+
+typedef long WebviCtx;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Initialize the library. Must be called before any other functions
+ * (the only exception is webvi_version() which can be called before
+ * the library is initialized).
+ *
+ * Returns 0, if initialization succeeds.
+ */
+int webvi_global_init(void);
+
+/*
+ * Frees all resources currently used by libwebvi and terminates all
+ * active connections. Do not call any libwebvi function after this.
+ * If the cleanup_python equals 0, the Python library is deinitialized
+ * by calling Py_Finalize(), otherwise the Python library is left
+ * loaded to be used by other modules of the program.
+ */
+void webvi_cleanup(int cleanup_python);
+
+/*
+ * Create a new context. A valid context is required for calling other
+ * functions in the library. The created contextes are independent of
+ * each other. The context must be destroyed by a call to
+ * webvi_cleanup_context when no longer needed.
+ *
+ * Return value 0 indicates an error.
+ */
+WebviCtx webvi_initialize_context(void);
+
+/*
+ * Free resources allocated by context ctx. The context can not be
+ * used anymore after a call to this function.
+ */
+void webvi_cleanup_context(WebviCtx ctx);
+
+/*
+ * Return the version of libwebvi as a string. The returned value
+ * points to a status buffer, and the caller should modify or not free() it.
+ */
+const char* webvi_version(void);
+
+/*
+ * Return a string describing an error code. The returned value points
+ * to a status buffer, and the caller should not modify or free() it.
+ */
+const char* webvi_strerror(WebviCtx ctx, WebviResult err);
+
+/*
+ * Set a new value for a global configuration option conf.
+ *
+ * Currently the only legal value for conf is TEMPLATE_PATH, which
+ * sets the base directory for the XSLT templates.
+ *
+ * The string pointed by value is copied to the library.
+ */
+WebviResult webvi_set_config(WebviCtx ctx, WebviConfig conf, const char *value);
+
+/*
+ * Creates a new download request.
+ *
+ * webvireference is a wvt:// URI of the resource that should be
+ * downloaded. type should be WEBVIREQ_MENU, if the resource should be
+ * transformed into a XML menu (that is if webvireferece comes from
+ * <ref> tag), WEBVIREQ_FILE, if the resource points to a media stream
+ * (from <stream> tag) whose contents should be downloaded, or
+ * WEBVIREQ_STREAMURL, if the resource is points to a media stream
+ * whose real URL should be resolved.
+ *
+ * Typically, the reference has been acquired from a previously
+ * downloaded menu. A special constant "wvt:///?srcurl=mainmenu" with
+ * type WEBVIREQ_MENU can be used to download mainmenu.
+ *
+ * The return value is a handle to the newly created request. Value -1
+ * indicates an error.
+ *
+ * The request is initialized but the actual network transfer is not
+ * started. You can set up additional configuration options on the
+ * handle using webvi_set_opt() before starting the handle with
+ * webvi_start_handle().
+ */
+WebviHandle webvi_new_request(WebviCtx ctx, const char *wvtreference, WebviRequestType type);
+
+/*
+ * Starts the transfer on handle h. The transfer one or more sockets
+ * whose file descriptors are returned by webvi_fdset(). The actual
+ * transfer is done during webvi_perform() calls.
+ */
+WebviResult webvi_start_handle(WebviCtx ctx, WebviHandle h);
+
+/*
+ * Requests that the transfer on handle h shoud be aborted. After the
+ * library has actually finished aborting the transfer, the handle h
+ * is returned by webvi_get_message() with non-zero status code.
+ */
+WebviResult webvi_stop_handle(WebviCtx ctx, WebviHandle h);
+
+/*
+ * Frees resources associated with handle h. The handle can not be
+ * used after this call. If the handle is still in the middle of a
+ * transfer, the transfer is forcefully aborted.
+ */
+WebviResult webvi_delete_handle(WebviCtx ctx, WebviHandle h);
+
+/*
+ * Sets configuration options that changes behaviour of the handle.
+ * opt is one of the values of WebviOption enum as indicated below.
+ * The fourth parameter sets the value of the specified option. Its
+ * type depends on opt as discussed below.
+ *
+ * Possible values for opt:
+ *
+ * WEBVIOPT_WRITEFUNC
+ *
+ * Set the callback function that shall be called when data is read
+ * from the network. The fourth parameter is a pointer to the callback
+ * funtion
+ *
+ * ssize_t (*webvi_callback)(const char *, size_t, void *).
+ *
+ * When the function is called, the first parameter is a pointer to
+ * the incoming data, the second parameters is the size of the
+ * incoming data block in bytes, and the third parameter is a pointer
+ * to user's data structure can be set by WEBVIOPT_WRITEDATA option.
+ *
+ * The callback funtion should return the number of bytes is
+ * processed. If this differs from the size of the incoming data
+ * block, it indicates that an error occurred and the transfer will be
+ * aborted.
+ *
+ * If write callback has not been set (or if it is set to NULL) the
+ * incoming data is printed to stdout.
+ *
+ * WEBVIOPT_WRITEDATA
+ *
+ * Sets the value that will be passed to the write callback. The
+ * fourth parameter is of type void *.
+ *
+ * WEBVIOPT_READFUNC
+ *
+ * Set the callback function that shall be called when data is to be
+ * send to network. The fourth parameter is a pointer to the callback
+ * funtion
+ *
+ * ssize_t (*webvi_callback)(const char *, size_t, void *)
+ *
+ * The first parameter is a pointer to a buffer where the data that is
+ * to be sent should be written. The second parameter is the maximum
+ * size of the buffer. The thirs parameter is a pointer to user data
+ * set with WEBVIOPT_READDATA.
+ *
+ * The return value should be the number of bytes actually written to
+ * the buffer. If the return value is -1, the transfer is aborted.
+ *
+ * WEBVIOPT_READDATA
+ *
+ * Sets the value that will be passed to the read callback. The
+ * fourth parameter is of type void *.
+ *
+ */
+WebviResult webvi_set_opt(WebviCtx ctx, WebviHandle h, WebviOption opt, ...);
+
+/*
+ * Get information specific to a WebviHandle. The value will be
+ * written to the memory location pointed by the third argument. The
+ * type of the pointer depends in the second parameter as discused
+ * below.
+ *
+ * Available information:
+ *
+ * WEBVIINFO_URL
+ *
+ * Receive URL. The third parameter must be a pointer to char *. The
+ * caller must free() the memory.
+ *
+ * WEBVIINFO_CONTENT_LENGTH
+ *
+ * Receive the value of Content-length field, or -1 if the size is
+ * unknown. The third parameter must be a pointer to long.
+ *
+ * WEBVIINFO_CONTENT_TYPE
+ *
+ * Receive the Content-type string. The returned value is NULL, if the
+ * Content-type is unknown. The third parameter must be a pointer to
+ * char *. The caller must free() the memory.
+ *
+ * WEBVIINFO_STREAM_TITLE
+ *
+ * Receive stream title. The returned value is NULL, if title is
+ * unknown. The third parameter must be a pointer to char *. The
+ * caller must free() the memory.
+ *
+ */
+WebviResult webvi_get_info(WebviCtx ctx, WebviHandle h, WebviInfo info, ...);
+
+/*
+ * Get active file descriptors in use by the library. The file
+ * descriptors that should be waited for reading, writing or
+ * exceptions are returned in read_fd_set, write_fd_set and
+ * exc_fd_set, respectively. The fd_sets are not cleared, but the new
+ * file descriptors are added to them. max_fd will contain the highest
+ * numbered file descriptor that was returned in one of the fd_sets.
+ *
+ * One should wait for action in one of the file descriptors returned
+ * by this function using select(), poll() or similar system call,
+ * and, after seeing action on a file descriptor, call webvi_perform
+ * on that descriptor.
+ */
+WebviResult webvi_fdset(WebviCtx ctx, fd_set *readfd, fd_set *writefd, fd_set *excfd, int *max_fd);
+
+/*
+ * Perform input or output action on a file descriptor.
+ *
+ * activefd is a file descriptor that was returned by an earlier call
+ * to webvi_fdset and has been signalled to be ready by select() or
+ * similar funtion. ev_bitmask should be OR'ed combination of
+ * WEBVI_SELECT_READ, WEBVI_SELECT_WRITE, WEBVI_SELECT_EXCEPTION to
+ * indicate that activefd has been signalled to be ready for reading,
+ * writing or being in exception state, respectively. ev_bitmask can
+ * also set to WEBVI_SELECT_TIMEOUT which means that the state is
+ * checked internally. On return, running_handles will contain the
+ * number of still active file descriptors.
+ *
+ * This function should be called with activefd set to 0 and
+ * ev_bitmask to WEBVI_SELECT_TIMEOUT periodically (every few seconds)
+ * even if no file descriptors have become ready to allow for timeout
+ * handling and other internal tasks.
+ */
+WebviResult webvi_perform(WebviCtx ctx, int sockfd, int ev_bitmask, long *running_handles);
+
+/*
+ * Return the next message from the message queue. Currently the only
+ * message, WEBVIMSG_DONE, indicates that a transfer on a handle has
+ * finished. The number of messages remaining in the queue after this
+ * call is written to remaining_messages. The pointers in the returned
+ * WebviMsg point to handle's internal buffers and is valid until the
+ * next call to webvi_get_message(). The caller should free the
+ * returned WebviMsg. The return value is NULL if there is no messages
+ * in the queue.
+ */
+WebviMsg *webvi_get_message(WebviCtx ctx, int *remaining_messages);
+
+#ifdef __cplusplus
+}
+#endif
+
+
+#endif
diff --git a/src/libwebvi/pythonlibname.py b/src/libwebvi/pythonlibname.py
new file mode 100755
index 0000000..48f4b97
--- /dev/null
+++ b/src/libwebvi/pythonlibname.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+
+import distutils.sysconfig
+import os
+import os.path
+
+libdir = distutils.sysconfig.get_config_var('LIBDIR')
+ldlibrary = distutils.sysconfig.get_config_var('LDLIBRARY')
+
+libfile = os.readlink(os.path.join(libdir, ldlibrary))
+if not os.path.isabs(libfile):
+ libfile = os.path.join(libdir, libfile)
+
+print libfile
diff --git a/src/libwebvi/webvi/__init__.py b/src/libwebvi/webvi/__init__.py
new file mode 100644
index 0000000..b6d50d5
--- /dev/null
+++ b/src/libwebvi/webvi/__init__.py
@@ -0,0 +1 @@
+__all__ = ['api', 'asyncurl', 'constants', 'download', 'request', 'utils']
diff --git a/src/libwebvi/webvi/api.py b/src/libwebvi/webvi/api.py
new file mode 100644
index 0000000..2fb24ab
--- /dev/null
+++ b/src/libwebvi/webvi/api.py
@@ -0,0 +1,289 @@
+# api.py - webvi API
+#
+# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""webvi API
+
+Example workflow:
+
+1) Create a new request. ref is a wvt:// URI.
+
+handle = new_request(ref, WebviRequestType.MENU)
+
+2) Setup a callback function:
+
+setopt(handle, WebviOpt.WRITEFUNC, my_callback)
+
+3) Start the network transfer:
+
+start_handle(handle)
+
+4) Get active file descriptors, wait for activity on them, and let
+webvi process the file descriptor.
+
+import select
+
+...
+
+readfd, writefd, excfd = fdset()[1:4]
+readfd, writefd, excfd = select.select(readfd, writefd, excfd, 5.0)
+for fd in readfd:
+ perform(fd, WebviSelectBitmask.READ)
+for fd in writefd:
+ perform(fd, WebviSelectBitmask.WRITE)
+
+5) Iterate 4) until pop_message returns handle, which indicates that
+the request has been completed.
+
+finished, status, errmsg, remaining = pop_message()
+if finished == handle:
+ print 'done'
+"""
+
+import request
+import asyncore
+import asyncurl
+from constants import WebviErr, WebviOpt, WebviInfo, WebviSelectBitmask, WebviConfig
+
+# Human readable messages for WebviErr items
+error_messages = {
+ WebviErr.OK: 'Succeeded',
+ WebviErr.INVALID_HANDLE: 'Invalid handle',
+ WebviErr.INVALID_PARAMETER: "Invalid parameter",
+ WebviErr.INTERNAL_ERROR: "Internal error"
+ }
+
+# Module-level variables
+finished_queue = []
+request_list = request.RequestList()
+socket_map = asyncore.socket_map
+
+# Internal functions
+
+class MyRequest(request.Request):
+ def request_done(self, err, errmsg):
+ """Calls the inherited function and puts the handle of the
+ finished request to the finished_queue."""
+ finished_queue.append(self)
+ request.Request.request_done(self, err, errmsg)
+
+# Public functions
+
+def strerror(err):
+ """Return human readable error message for conststants.WebviErr"""
+ try:
+ return error_messages[err]
+ except KeyError:
+ return error_messages[WebviErr.INTERNAL_ERROR]
+
+def set_config(conf, value):
+ """Set a new value for a global configuration option conf.
+
+ Currently the only legal value for conf is
+ constants.WebviConfig.TEMPLATE_PATH, which sets the base directory
+ for the XSLT templates.
+ """
+ if conf == WebviConfig.TEMPLATE_PATH:
+ request.set_template_path(value)
+ return WebviErr.OK
+ else:
+ return WebviErr.INVALID_PARAMETER
+
+def new_request(reference, reqtype):
+ """Create a new request.
+
+ reference is a wvt:// URI which typically comes from previously
+ opened menu. reqtype is one of conststants.WebviRequestType and
+ indicates wheter the reference is a navigation menu, stream that
+ should be downloaded, or a stream whose URL should be returned.
+
+ Returns a handle (an integer) will be given to following
+ functions. Return value -1 indicates an error.
+ """
+ req = MyRequest(reference, reqtype)
+
+ if req.srcurl is None:
+ return -1
+
+ return request_list.put(req)
+
+def set_opt(handle, option, value):
+ """Set configuration options on a handle.
+
+ option specifies option's name (one of constants.WebviOpt values)
+ and value is the new value for the option.
+ """
+
+ try:
+ req = request_list[handle]
+ except KeyError:
+ return WebviErr.INVALID_HANDLE
+
+ if option == WebviOpt.WRITEFUNC:
+ req.writefunc = value
+ elif option == WebviOpt.WRITEDATA:
+ req.writedata = value
+ elif option == WebviOpt.READFUNC:
+ req.readfunc = value
+ elif option == WebviOpt.READDATA:
+ req.readdata = value
+ else:
+ return WebviErr.INVALID_PARAMETER
+
+ return WebviErr.OK
+
+def get_info(handle, info):
+ """Get information about a handle.
+
+ info is the type of data that is to be returned (one of
+ constants.WebviInfo values).
+ """
+ try:
+ req = request_list[handle]
+ except KeyError:
+ return (WebviErr.INVALID_HANDLE, None)
+
+ val = None
+ if info == WebviInfo.URL:
+ if req.dl is not None:
+ val = req.dl.get_url()
+ else:
+ val = req.srcurl
+ elif info == WebviInfo.CONTENT_LENGTH:
+ val = req.contentlength
+ elif info == WebviInfo.CONTENT_TYPE:
+ val = req.contenttype
+ elif info == WebviInfo.STREAM_TITLE:
+ val = req.streamtitle
+ else:
+ return (WebviErr.INVALID_PARAMETER, None)
+
+ return (WebviErr.OK, val)
+
+def start_handle(handle):
+ """Start the network transfer on a handle."""
+ try:
+ req = request_list[handle]
+ except KeyError:
+ return WebviErr.INVALID_HANDLE
+
+ req.start()
+ return WebviErr.OK
+
+def stop_handle(handle):
+ """Aborts network transfer on a handle.
+
+ The abort is confirmed by pop_message() returning the handle with
+ an non-zero error code.
+ """
+ try:
+ req = request_list[handle]
+ except KeyError:
+ return WebviErr.INVALID_HANDLE
+
+ if not req.is_finished():
+ req.stop()
+
+ return WebviErr.OK
+
+def delete_handle(handle):
+ """Frees resources related to handle.
+
+ This should be called when the transfer has been completed and the
+ user is done with the handle. If the transfer is still in progress
+ when delete_handle() is called, the transfer is aborted. After
+ calling delete_handle() the handle value will be invalid, and
+ should not be feed to other functions anymore.
+ """
+ try:
+ del request_list[handle]
+ except KeyError:
+ return WebviErr.INVALID_HANDLE
+
+ return WebviErr.OK
+
+def pop_message():
+ """Retrieve messages about finished requests.
+
+ If a request has been finished since the last call to this
+ function, returns a tuple (handle, status, msg, num_messages),
+ where handle identifies the finished request, status is a numeric
+ status code (non-zero for an error), msg is a description of an
+ error as string, and num_messages is the number of messages that
+ can be retrieved by calling pop_messages() again immediately. If
+ the finished requests queue is empty, returns (-1, -1, "", 0).
+ """
+ if finished_queue:
+ req = finished_queue.pop()
+ return (req.handle, req.status, req.errmsg, len(finished_queue))
+ else:
+ return (-1, -1, "", 0)
+
+def fdset():
+ """Get the list of file descriptors that are currently in use by
+ the library.
+
+ Returrns a tuple, where the first item is a constants.WebviErr
+ value indicating the success of the call, the next three values
+ are lists of descriptors that should be monitored for reading,
+ writing, and exceptional conditions, respectively. The last item
+ is the maximum of the file descriptors in the three lists.
+ """
+ readfd = []
+ writefd = []
+ excfd = []
+ maxfd = -1
+
+ for fd, disp in socket_map.iteritems():
+ if disp.readable():
+ readfd.append(fd)
+ if fd > maxfd:
+ maxfd = fd
+ if disp.writable():
+ writefd.append(fd)
+ if fd > maxfd:
+ maxfd = fd
+
+ return (WebviErr.OK, readfd, writefd, excfd, maxfd)
+
+def perform(fd, ev_bitmask):
+ """Perform transfer on file descriptor fd.
+
+ fd is a file descriptor that has been signalled to be ready by
+ select() or similar system call. ev_bitmask specifies what kind of
+ activity has been detected using values of
+ constants.WebviSelectBitmask. If ev_bitmask is
+ constants.WebviSelectBitmask.TIMEOUT the type of activity is check
+ by the function.
+
+ This function should be called every few seconds with fd=-1,
+ ev_bitmask=constants.WebviSelectBitmask.TIMEOUT even if no
+ activity has been signalled on the file descriptors to ensure
+ correct handling of timeouts and other internal processing.
+ """
+ if fd < 0:
+ asyncurl.poll()
+ else:
+ disp = socket_map.get(fd)
+ if disp is not None:
+ if ev_bitmask & WebviSelectBitmask.READ != 0 or \
+ (ev_bitmask == 0 and disp.readable()):
+ disp.handle_read_event()
+ if ev_bitmask & WebviSelectBitmask.WRITE != 0 or \
+ (ev_bitmask == 0 and disp.writable()):
+ disp.handle_write_event()
+
+ return (WebviErr.OK, len(socket_map))
diff --git a/src/libwebvi/webvi/asyncurl.py b/src/libwebvi/webvi/asyncurl.py
new file mode 100644
index 0000000..afc575a
--- /dev/null
+++ b/src/libwebvi/webvi/asyncurl.py
@@ -0,0 +1,389 @@
+# asyncurl.py - Wrapper class for using pycurl objects in asyncore
+#
+# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""This is a wrapper for using pycurl objects in asyncore.
+
+Start a transfer by creating an async_curl_dispatch, and call
+asyncurl.loop() instead of asyncore.loop().
+"""
+
+import asyncore
+import pycurl
+import traceback
+import os
+import select
+import time
+import cStringIO
+from errno import EINTR
+
+SOCKET_TIMEOUT = pycurl.SOCKET_TIMEOUT
+CSELECT_IN = pycurl.CSELECT_IN
+CSELECT_OUT = pycurl.CSELECT_OUT
+CSELECT_ERR = pycurl.CSELECT_ERR
+
+def poll(timeout=0.0, map=None, mdisp=None):
+ if map is None:
+ map = asyncore.socket_map
+ if mdisp is None:
+ mdisp = multi_dispatcher
+ if map:
+ timeout = min(timeout, mdisp.timeout/1000.0)
+
+ r = []; w = []; e = []
+ for fd, obj in map.items():
+ is_r = obj.readable()
+ is_w = obj.writable()
+ if is_r:
+ r.append(fd)
+ if is_w:
+ w.append(fd)
+ if is_r or is_w:
+ e.append(fd)
+ if [] == r == w == e:
+ time.sleep(timeout)
+ else:
+ try:
+ r, w, e = select.select(r, w, e, timeout)
+ except select.error, err:
+ if err[0] != EINTR:
+ raise
+ else:
+ return
+
+ if [] == r == w == e:
+ mdisp.socket_action(SOCKET_TIMEOUT, 0)
+ return
+
+ for fd in r:
+ obj = map.get(fd)
+ if obj is None:
+ continue
+ asyncore.read(obj)
+
+ for fd in w:
+ obj = map.get(fd)
+ if obj is None:
+ continue
+ asyncore.write(obj)
+
+ for fd in e:
+ obj = map.get(fd)
+ if obj is None:
+ continue
+ asyncore._exception(obj)
+
+def loop(timeout=30.0, use_poll=False, map=None, count=None, mdisp=None):
+ if map is None:
+ map = asyncore.socket_map
+ if mdisp is None:
+ mdisp = multi_dispatcher
+
+ if use_poll and hasattr(select, 'poll'):
+ print 'poll2 not implemented'
+ poll_fun = poll
+
+ if count is None:
+ while map:
+ poll_fun(timeout, map, mdisp)
+
+ else:
+ while map and count > 0:
+ poll_fun(timeout, map, mdisp)
+ count = count - 1
+
+def noop_callback(s):
+ pass
+
+
+class curl_multi_dispatcher:
+ """A dispatcher for pycurl.CurlMulti() objects. An instance of
+ this class is created automatically. There is usually no need to
+ construct one manually."""
+ def __init__(self, socket_map=None):
+ if socket_map is None:
+ self._map = asyncore.socket_map
+ else:
+ self._map = socket_map
+ self.dispatchers = {}
+ self.timeout = 1000
+ self._sockets_removed = False
+ self._curlm = pycurl.CurlMulti()
+ self._curlm.setopt(pycurl.M_SOCKETFUNCTION, self.socket_callback)
+ self._curlm.setopt(pycurl.M_TIMERFUNCTION, self.timeout_callback)
+
+ def socket_callback(self, action, socket, user_data, socket_data):
+# print 'socket callback: %d, %s' % \
+# (socket, {pycurl.POLL_NONE: "NONE",
+# pycurl.POLL_IN: "IN",
+# pycurl.POLL_OUT: "OUT",
+# pycurl.POLL_INOUT: "INOUT",
+# pycurl.POLL_REMOVE: "REMOVE"}[action])
+
+ if action == pycurl.POLL_NONE:
+ return
+ elif action == pycurl.POLL_REMOVE:
+ if socket in self._map:
+ del self._map[socket]
+ self._sockets_removed = True
+ return
+
+ obj = self._map.get(socket)
+ if obj is None:
+ obj = dispatcher_wrapper(socket, self)
+ self._map[socket] = obj
+
+ if action == pycurl.POLL_IN:
+ obj.set_readable(True)
+ obj.set_writable(False)
+ elif action == pycurl.POLL_OUT:
+ obj.set_readable(False)
+ obj.set_writable(True)
+ elif action == pycurl.POLL_INOUT:
+ obj.set_readable(True)
+ obj.set_writable(True)
+
+ def timeout_callback(self, msec):
+ self.timeout = msec
+
+ def attach(self, curldisp):
+ """Starts a transfer on curl handle by attaching it to this
+ multihandle."""
+ self.dispatchers[curldisp.curl] = curldisp
+ try:
+ self._curlm.add_handle(curldisp.curl)
+ except pycurl.error:
+ # the curl object is already on this multi-stack
+ pass
+
+ while self._curlm.socket_all()[0] == pycurl.E_CALL_MULTI_PERFORM:
+ pass
+
+ self.check_completed(True)
+
+ def detach(self, curldisp):
+ """Removes curl handle from this multihandle, and fire its
+ completion callback function."""
+ self.del_curl(curldisp.curl)
+
+ # libcurl does not send POLL_REMOVE when a handle is aborted
+ for socket, curlobj in self._map.items():
+ if curlobj == curldisp:
+
+ print 'handle stopped but socket in map'
+
+ del self._map[socket]
+ break
+
+ def del_curl(self, curl):
+ try:
+ self._curlm.remove_handle(curl)
+ except pycurl.error:
+ # the curl object is not on this multi-stack
+ pass
+ if curl in self.dispatchers:
+ del self.dispatchers[curl]
+ curl.close()
+
+ def socket_action(self, fd, evbitmask):
+ res = -1
+ OK = False
+ while not OK:
+ try:
+ res = self._curlm.socket_action(fd, evbitmask)
+ OK = True
+ except pycurl.error:
+ # Older libcurls may return CURLM_CALL_MULTI_PERFORM,
+ # which pycurl (at least 7.19.0) converts to an
+ # exception. If that happens, call socket_action
+ # again.
+ pass
+ return res
+
+ def check_completed(self, force):
+ if not force and not self._sockets_removed:
+ return
+ self._sockets_removed = False
+
+ nmsg, success, failed = self._curlm.info_read()
+ for handle in success:
+ disp = self.dispatchers.get(handle)
+ if disp is not None:
+ try:
+ disp.handle_completed(0, None)
+ except:
+ self.handle_error()
+ self.del_curl(handle)
+ for handle, err, errmsg in failed:
+ disp = self.dispatchers.get(handle)
+ if disp is not None:
+ try:
+ disp.handle_completed(err, errmsg)
+ except:
+ self.handle_error()
+ self.del_curl(handle)
+
+ def handle_error(self):
+ print 'Exception occurred in multicurl processing'
+ print traceback.format_exc()
+
+
+class dispatcher_wrapper:
+ """An internal helper class that connects a file descriptor in the
+ asyncore.socket_map to a curl_multi_dispatcher."""
+ def __init__(self, fd, multicurl):
+ self.fd = fd
+ self.multicurl = multicurl
+ self.read_flag = False
+ self.write_flag = False
+
+ def readable(self):
+ return self.read_flag
+
+ def writable(self):
+ return self.write_flag
+
+ def set_readable(self, x):
+ self.read_flag = x
+
+ def set_writable(self, x):
+ self.write_flag = x
+
+ def handle_read_event(self):
+ self.multicurl.socket_action(self.fd, CSELECT_IN)
+ self.multicurl.check_completed(False)
+
+ def handle_write_event(self):
+ self.multicurl.socket_action(self.fd, CSELECT_OUT)
+ self.multicurl.check_completed(False)
+
+ def handle_expt_event(self):
+ self.multicurl.socket_action(self.fd, CSELECT_ERR)
+ self.multicurl.check_completed(False)
+
+ def handle_error(self):
+ print 'Exception occurred during processing of a curl request'
+ print traceback.format_exc()
+
+
+class async_curl_dispatcher:
+ """A dispatcher class for pycurl transfers."""
+ def __init__(self, url, auto_start=True):
+ """Initializes a pycurl object self.curl. The default is to
+ download url to an internal buffer whose content can be read
+ with self.recv(). If auto_start is False, the transfer is not
+ started before a call to add_channel().
+ """
+ self.url = url
+ self.socket = None
+ self.buffer = cStringIO.StringIO()
+ self.curl = pycurl.Curl()
+ self.curl.setopt(pycurl.URL, self.url)
+ self.curl.setopt(pycurl.FOLLOWLOCATION, 1)
+ self.curl.setopt(pycurl.AUTOREFERER, 1)
+ self.curl.setopt(pycurl.MAXREDIRS, 10)
+ self.curl.setopt(pycurl.FAILONERROR, 1)
+ self.curl.setopt(pycurl.WRITEFUNCTION, self.write_to_buf)
+ if auto_start:
+ self.add_channel()
+
+ def write_to_buf(self, msg):
+ self.buffer.write(msg)
+ self.handle_read()
+
+ def send(self, data):
+ raise NotImplementedError
+
+ def recv(self, buffer_size):
+ # buffer_size is ignored
+ ret = self.buffer.getvalue()
+ self.buffer.reset()
+ self.buffer.truncate()
+ return ret
+
+ def add_channel(self, multidisp=None):
+ if multidisp is None:
+ multidisp = multi_dispatcher
+ multidisp.attach(self)
+
+ def del_channel(self, multidisp=None):
+ if multidisp is None:
+ multidisp = multi_dispatcher
+ multidisp.detach(self)
+
+ def close(self):
+ self.del_channel()
+
+ def log_info(self, message, type='info'):
+ if type != 'info':
+ print '%s: %s' % (type, message)
+
+ def handle_error(self):
+ print 'Exception occurred during processing of a curl request'
+ print traceback.format_exc()
+ self.close()
+
+ def handle_read(self):
+ self.log_info('unhandled read event', 'warning')
+
+ def handle_write(self):
+ self.log_info('unhandled write event', 'warning')
+
+ def handle_completed(self, err, errmsg):
+ """Called when the download has finished. err is a numeric
+ error code (or 0 if the download was successfull) and errmsg
+ is a curl error message as a string."""
+ # It seems that a reference to self.write_to_buf forbids
+ # garbage collection from deleting this object. unsetopt() or
+ # setting the callback to None are not allowed. Is there a
+ # better way?
+ self.curl.setopt(pycurl.WRITEFUNCTION, noop_callback)
+ self.close()
+
+
+def test():
+
+ class curl_request(async_curl_dispatcher):
+ def __init__(self, url, outfile, i):
+ async_curl_dispatcher.__init__(self, url, False)
+ self.id = i
+ self.outfile = outfile
+ self.add_channel()
+
+ def handle_read(self):
+ buf = self.recv(4096)
+ print '%s: writing %d bytes' % (self.id, len(buf))
+ self.outfile.write(buf)
+
+ def handle_completed(self, err, errmsg):
+ if err != 0:
+ print '%s: error: %d %s' % (self.id, err, errmsg)
+ else:
+ print '%s: completed' % self.id
+
+ curl_request('http://www.python.org', open('python.out', 'w'), 1)
+ curl_request('http://en.wikipedia.org/wiki/Main_Page', open('wikipedia.out', 'w'), 2)
+ loop(timeout=5.0)
+
+
+pycurl.global_init(pycurl.GLOBAL_DEFAULT)
+try:
+ multi_dispatcher
+except NameError:
+ multi_dispatcher = curl_multi_dispatcher()
+
+if __name__ == '__main__':
+ test()
diff --git a/src/libwebvi/webvi/constants.py b/src/libwebvi/webvi/constants.py
new file mode 100644
index 0000000..2797178
--- /dev/null
+++ b/src/libwebvi/webvi/constants.py
@@ -0,0 +1,50 @@
+# constants.py - Python definitions for constants in libwebvi.h
+#
+# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Keep these in sync with libwebvi.h
+
+class WebviRequestType:
+ MENU = 0
+ FILE = 1
+ STREAMURL = 2
+
+class WebviErr:
+ OK = 0
+ INVALID_HANDLE = 1
+ INVALID_PARAMETER = 2
+ INTERNAL_ERROR = -1
+
+class WebviOpt:
+ WRITEFUNC = 0
+ READFUNC = 1
+ WRITEDATA = 2
+ READDATA = 3
+
+class WebviInfo:
+ URL = 0
+ CONTENT_LENGTH = 1
+ CONTENT_TYPE = 2
+ STREAM_TITLE = 3
+
+class WebviSelectBitmask:
+ TIMEOUT = 0
+ READ = 1
+ WRITE = 2
+ EXCEPTION = 4
+
+class WebviConfig:
+ TEMPLATE_PATH = 0
diff --git a/src/libwebvi/webvi/download.py b/src/libwebvi/webvi/download.py
new file mode 100644
index 0000000..34240ff
--- /dev/null
+++ b/src/libwebvi/webvi/download.py
@@ -0,0 +1,470 @@
+# download.py - webvi downloader backend
+#
+# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import asyncore
+import asynchat
+import cStringIO
+import urllib
+import subprocess
+import socket
+import signal
+import pycurl
+import asyncurl
+import utils
+import version
+
+WEBVID_USER_AGENT = 'libwebvi/%s %s' % (version.VERSION, pycurl.version)
+MOZILLA_USER_AGENT = 'Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5'
+
+try:
+ from libmimms import libmms
+except ImportError, e:
+ pass
+
+# Mapping from curl error codes to webvi errors. The error constants
+# are defined only in pycurl 7.16.1 and newer.
+if pycurl.version_info()[2] >= 0x071001:
+ CURL_ERROR_CODE_MAPPING = \
+ {pycurl.E_OK: 0,
+ pycurl.E_OPERATION_TIMEOUTED: 408,
+ pycurl.E_OUT_OF_MEMORY: 500,
+ pycurl.E_PARTIAL_FILE: 504,
+ pycurl.E_READ_ERROR: 504,
+ pycurl.E_RECV_ERROR: 504,
+ pycurl.E_REMOTE_FILE_NOT_FOUND: 404,
+ pycurl.E_TOO_MANY_REDIRECTS: 404,
+ pycurl.E_UNSUPPORTED_PROTOCOL: 500,
+ pycurl.E_URL_MALFORMAT: 400,
+ pycurl.E_COULDNT_CONNECT: 506,
+ pycurl.E_COULDNT_RESOLVE_HOST: 506,
+ pycurl.E_COULDNT_RESOLVE_PROXY: 506,
+ pycurl.E_FILE_COULDNT_READ_FILE: 404,
+ pycurl.E_GOT_NOTHING: 504,
+ pycurl.E_HTTP_RETURNED_ERROR: 404,
+ pycurl.E_INTERFACE_FAILED: 506,
+ pycurl.E_LOGIN_DENIED: 403}
+else:
+ CURL_ERROR_CODE_MAPPING = {pycurl.E_OK: 0}
+
+class DownloaderException(Exception):
+ def __init__(self, errcode, errmsg):
+ self.code = errcode
+ self.msg = errmsg
+
+ def __str__(self):
+ return '%s %s' % (self.code, self.msg)
+
+def create_downloader(url, templatedir, writefunc=None, headerfunc=None,
+ donefunc=None, HTTPheaders=None, headers_only=False):
+ """Downloader factory.
+
+ Returns a suitable downloader object according to url type. Raises
+ DownloaderException if creating the downloader fails.
+ """
+ if url == '':
+ return DummyDownloader('', writefunc, headerfunc, donefunc,
+ headers_only)
+
+ elif url.startswith('mms://') or url.startswith('mmsh://'):
+ try:
+ libmms
+ except (NameError, OSError):
+ raise DownloaderException(501, 'MMS scheme not supported. Install mimms.')
+ return MMSDownload(url, writefunc, headerfunc, donefunc,
+ headers_only)
+
+ elif url.startswith('wvt://'):
+ executable, parameters = parse_external_downloader_wvt_uri(url, templatedir)
+ if executable is None:
+ raise DownloaderException(400, 'Invalid wvt:// URL')
+ try:
+ return ExternalDownloader(executable, parameters, writefunc,
+ headerfunc, donefunc, headers_only)
+ except OSError, (errno, strerror):
+ raise DownloaderException(500, 'Failed to execute %s: %s' %
+ (executable, strerror))
+
+ else:
+ return CurlDownload(url, writefunc, headerfunc, donefunc,
+ HTTPheaders, headers_only)
+
+def convert_curl_error(err, errmsg, aborted):
+ """Convert a curl error code err to webvi error code."""
+ if err == pycurl.E_WRITE_ERROR:
+ return (402, 'Aborted')
+ elif err not in CURL_ERROR_CODE_MAPPING:
+ return (500, errmsg)
+ else:
+ return (CURL_ERROR_CODE_MAPPING[err], errmsg)
+
+def parse_external_downloader_wvt_uri(url, templatedir):
+ exe = None
+ params = []
+ if not url.startswith('wvt:///bin/'):
+ return (exe, params)
+
+ split = url[len('wvt:///bin/'):].split('?', 1)
+ exe = templatedir + '/bin/' + split[0]
+
+ if len(split) > 1:
+ params = [urllib.unquote(x) for x in split[1].split('&')]
+
+ return (exe, params)
+
+def _new_process_group():
+ os.setpgid(0, 0)
+
+class DownloaderBase:
+ """Base class for downloaders."""
+ def __init__(self, url):
+ self.url = url
+
+ def start(self):
+ """Should start the download process."""
+ pass
+
+ def abort(self):
+ """Signals that the download should be aborted."""
+ pass
+
+ def get_url(self):
+ """Return the URL where the data was downloaded."""
+ return self.url
+
+ def get_body(self):
+ return ''
+
+ def get_encoding(self):
+ """Return the encoding of the downloaded object, or None if
+ encoding is not known."""
+ return None
+
+
+class DummyDownloader(DownloaderBase, asyncore.file_dispatcher):
+ """This class doesn't actually download anything, but returns msg
+ string as if it had been result of a download operation.
+ """
+ def __init__(self, msg, writefunc=None, headerfunc=None,
+ donefunc=None, headers_only=False):
+ DownloaderBase.__init__(self, '')
+ self.donefunc = donefunc
+ self.writefunc = writefunc
+ self.headers_only = headers_only
+
+ readfd, writefd = os.pipe()
+ asyncore.file_dispatcher.__init__(self, readfd)
+ os.write(writefd, msg)
+ os.close(writefd)
+
+ def set_file(self, fd):
+ # Like asyncore.file_dispatcher.set_file() but doesn't call
+ # add_channel(). We'll call add_channel() in start() when the
+ # download shall begin.
+ self.socket = asyncore.file_wrapper(fd)
+ self._fileno = self.socket.fileno()
+
+ def start(self):
+ if self.headers_only:
+ self.donefunc(0, None)
+ else:
+ self.add_channel()
+
+ def readable(self):
+ return True
+
+ def writable(self):
+ return False
+
+ def handle_read(self):
+ try:
+ data = self.recv(4096)
+ if data and self.writefunc is not None:
+ self.writefunc(data)
+ except socket.error:
+ self.handle_error()
+
+ def handle_close(self):
+ self.close()
+
+ if self.donefunc is not None:
+ self.donefunc(0, '')
+
+
+class CurlDownload(DownloaderBase, asyncurl.async_curl_dispatcher):
+ """Downloads a large number of different URL schemes using
+ libcurl."""
+ def __init__(self, url, writefunc=None, headerfunc=None,
+ donefunc=None, HTTPheaders=None, headers_only=False):
+ DownloaderBase.__init__(self, url)
+ asyncurl.async_curl_dispatcher.__init__(self, url, False)
+ self.donefunc = donefunc
+ self.writefunc = writefunc
+ self.contenttype = None
+ self.running = True
+ self.aborted = False
+
+ self.curl.setopt(pycurl.USERAGENT, WEBVID_USER_AGENT)
+ if headers_only:
+ self.curl.setopt(pycurl.NOBODY, 1)
+ if headerfunc is not None:
+ self.curl.setopt(pycurl.HEADERFUNCTION, headerfunc)
+ self.curl.setopt(pycurl.WRITEFUNCTION, self.writewrapper)
+
+ headers = []
+ if HTTPheaders is not None:
+ for headername, headerdata in HTTPheaders.iteritems():
+ if headername == 'cookie':
+ self.curl.setopt(pycurl.COOKIE, headerdata)
+ else:
+ headers.append(headername + ': ' + headerdata)
+
+ self.curl.setopt(pycurl.HTTPHEADER, headers)
+
+ def start(self):
+ self.add_channel()
+
+ def close(self):
+ self.contenttype = self.curl.getinfo(pycurl.CONTENT_TYPE)
+ asyncurl.async_curl_dispatcher.close(self)
+ self.running = False
+
+ def abort(self):
+ self.aborted = True
+
+ def writewrapper(self, data):
+ if self.aborted:
+ return 0
+
+ if self.writefunc is None:
+ return self.write_to_buf(data)
+ else:
+ return self.writefunc(data)
+
+ def get_body(self):
+ return self.buffer.getvalue()
+
+ def get_encoding(self):
+ if self.running:
+ self.contenttype = self.curl.getinfo(pycurl.CONTENT_TYPE)
+
+ if self.contenttype is None:
+ return None
+
+ values = self.contenttype.split(';', 1)
+ if len(values) > 1:
+ for par in values[1].split(' '):
+ if par.startswith('charset='):
+ return par[len('charset='):].strip('"')
+
+ return None
+
+ def handle_read(self):
+ # Do nothing to the read data here. Instead, let the base
+ # class to collect the data to self.buffer.
+ pass
+
+ def handle_completed(self, err, errmsg):
+ asyncurl.async_curl_dispatcher.handle_completed(self, err, errmsg)
+ if self.donefunc is not None:
+ err, errmsg = convert_curl_error(err, errmsg, self.aborted)
+ self.donefunc(err, errmsg)
+
+
+class MMSDownload(DownloaderBase, asyncore.file_dispatcher):
+ def __init__(self, url, writefunc=None, headerfunc=None,
+ donefunc=None, headers_only=False):
+ DownloaderBase.__init__(self, url)
+ self.r, self.w = os.pipe()
+ asyncore.file_dispatcher.__init__(self, self.r)
+
+ self.writefunc = writefunc
+ self.headerfunc = headerfunc
+ self.donefunc = donefunc
+ self.relaylen = -1
+ self.expectedlen = -1
+ self.headers_only = headers_only
+ self.stream = None
+ self.errmsg = None
+ self.aborted = False
+
+ def set_file(self, fd):
+ self.socket = asyncore.file_wrapper(fd)
+ self._fileno = self.socket.fileno()
+
+ def recv(self, buffer_size):
+ data = self.stream.read()
+ if not data:
+ self.handle_close()
+ return ''
+ else:
+ return data
+
+ def close(self):
+ if self.stream is not None:
+ self.stream.close()
+
+ os.close(self.w)
+ asyncore.file_dispatcher.close(self)
+
+ def readable(self):
+ return self.stream is not None
+
+ def writable(self):
+ return False
+
+ def start(self):
+ try:
+ self.stream = libmms.Stream(self.url, 1000000)
+ except libmms.Error, e:
+ self.errmsg = e.message
+ self.handle_close()
+ return
+
+ os.write(self.w, '0') # signal that this dispatcher has data available
+
+ if self.headerfunc:
+ # Output the length in a HTTP-like header field so that we
+ # can use the same callbacks as with HTTP downloads.
+ ext = utils.get_url_extension(self.url)
+ if ext == 'wma':
+ self.headerfunc('Content-Type: audio/x-ms-wma')
+ else: # if ext == 'wmv':
+ self.headerfunc('Content-Type: video/x-ms-wmv')
+ self.headerfunc('Content-Length: %d' % self.stream.length())
+
+ if self.headers_only:
+ self.handle_close()
+ else:
+ self.add_channel()
+
+ def abort(self):
+ self.aborted = True
+
+ def handle_read(self):
+ if self.aborted:
+ self.handle_close()
+ return ''
+
+ try:
+ data = self.recv(4096)
+ if data and (self.writefunc is not None):
+ self.writefunc(data)
+ except libmms.Error, e:
+ self.errmsg = e.message
+ self.handle_close()
+ return
+
+ def handle_close(self):
+ self.close()
+ self.stream = None
+
+ if self.errmsg is not None:
+ self.donefunc(500, self.errmsg)
+ elif self.aborted:
+ self.donefunc(402, 'Aborted')
+ elif self.relaylen < self.expectedlen:
+ # We got fewer bytes than expected. Maybe the connection
+ # was lost?
+ self.donefunc(504, 'Download may be incomplete (length %d < %d)' %
+ (self.relaylen, self.expectedlen))
+ else:
+ self.donefunc(0, '')
+
+
+class ExternalDownloader(DownloaderBase, asyncore.file_dispatcher):
+ """Executes an external process and reads its result on standard
+ output."""
+ def __init__(self, executable, parameters, writefunc=None,
+ headerfunc=None, donefunc=None, headers_only=False):
+ DownloaderBase.__init__(self, '')
+ asyncore.dispatcher.__init__(self, None, None)
+ self.executable = executable
+ self.writefunc = writefunc
+ self.headerfunc = headerfunc
+ self.donefunc = donefunc
+ self.headers_only = headers_only
+ self.contenttype = ''
+ self.aborted = False
+
+ args = []
+ for par in parameters:
+ try:
+ key, val = par.split('=', 1)
+ if key == 'contenttype':
+ self.contenttype = val
+ elif key == 'arg':
+ args.append(val)
+ except ValueError:
+ pass
+
+ if args:
+ self.url = args[0]
+ else:
+ self.url = executable
+ self.cmd = [executable] + args
+
+ self.process = None
+
+ def start(self):
+ self.headerfunc('Content-Type: ' + self.contenttype)
+
+ if self.headers_only:
+ self.donefunc(0, None)
+ return
+
+ self.process = subprocess.Popen(self.cmd, stdout=subprocess.PIPE,
+ close_fds=True,
+ preexec_fn=_new_process_group)
+ asyncore.file_dispatcher.__init__(self, os.dup(self.process.stdout.fileno()))
+
+ def abort(self):
+ if self.process is not None:
+ self.aborted = True
+ pg = os.getpgid(self.process.pid)
+ os.killpg(pg, signal.SIGTERM)
+
+ def readable(self):
+ # Return True if the subprocess is still alive
+ return self.process is not None and self.process.returncode is None
+
+ def writable(self):
+ return False
+
+ def handle_read(self):
+ try:
+ data = self.recv(4096)
+ if data and self.writefunc is not None:
+ self.writefunc(data)
+ except socket.error:
+ self.handle_error()
+ return
+
+ def handle_close(self):
+ self.close()
+ self.process.wait()
+
+ if self.donefunc is not None:
+ if self.process.returncode == 0:
+ self.donefunc(0, '')
+ elif self.aborted and self.process.returncode == -signal.SIGTERM:
+ self.donefunc(402, 'Aborted')
+ else:
+ self.donefunc(500, 'Child process "%s" returned error %s' % \
+ (' '.join(self.cmd), str(self.process.returncode)))
+
+ self.process = None
diff --git a/src/libwebvi/webvi/json2xml.py b/src/libwebvi/webvi/json2xml.py
new file mode 100644
index 0000000..372e6c6
--- /dev/null
+++ b/src/libwebvi/webvi/json2xml.py
@@ -0,0 +1,69 @@
+import sys
+import libxml2
+
+try:
+ import json
+except ImportError:
+ try:
+ import simplejson as json
+ except ImportError:
+ print 'Error: install simplejson'
+ raise
+
+def _serialize_to_xml(obj, xmlnode):
+ """Create XML representation of a Python object (list, tuple,
+ dist, or basic number and string types)."""
+ if type(obj) in (list, tuple):
+ listnode = libxml2.newNode('list')
+ for li in obj:
+ itemnode = libxml2.newNode('li')
+ _serialize_to_xml(li, itemnode)
+ listnode.addChild(itemnode)
+ xmlnode.addChild(listnode)
+
+ elif type(obj) == dict:
+ dictnode = libxml2.newNode('dict')
+ for key, val in obj.iteritems():
+ itemnode = libxml2.newNode(key.encode('utf-8'))
+ _serialize_to_xml(val, itemnode)
+ dictnode.addChild(itemnode)
+ xmlnode.addChild(dictnode)
+
+ elif type(obj) in (str, unicode, int, long, float, complex, bool):
+ content = libxml2.newText(unicode(obj).encode('utf-8'))
+ xmlnode.addChild(content)
+
+ elif type(obj) == type(None):
+ pass
+
+ else:
+ raise TypeError('Unsupported type %s while serializing to xml'
+ % type(obj))
+
+def json2xml(jsonstr, encoding=None):
+ """Convert JSON string jsonstr to XML tree."""
+ try:
+ parsed = json.loads(jsonstr, encoding)
+ except ValueError:
+ return None
+
+ xmldoc = libxml2.newDoc("1.0")
+ root = libxml2.newNode("jsondocument")
+ xmldoc.setRootElement(root)
+
+ _serialize_to_xml(parsed, root)
+
+ return xmldoc
+
+def test():
+ xml = json2xml(open(sys.argv[1]).read())
+
+ if xml is None:
+ return
+
+ print xml.serialize('utf-8')
+
+ xml.freeDoc()
+
+if __name__ == '__main__':
+ test()
diff --git a/src/libwebvi/webvi/request.py b/src/libwebvi/webvi/request.py
new file mode 100644
index 0000000..e19eb9c
--- /dev/null
+++ b/src/libwebvi/webvi/request.py
@@ -0,0 +1,617 @@
+# request.py - webvi request class
+#
+# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import urllib
+import libxml2
+import os.path
+import cStringIO
+import re
+import download
+import sys
+import utils
+import json2xml
+from constants import WebviRequestType
+
+DEBUG = False
+
+DEFAULT_TEMPLATE_PATH = '/usr/local/share/webvi/templates'
+template_path = DEFAULT_TEMPLATE_PATH
+
+def debug(msg):
+ if DEBUG:
+ if type(msg) == unicode:
+ sys.stderr.write(msg.encode('ascii', 'replace'))
+ else:
+ sys.stderr.write(msg)
+ sys.stderr.write('\n')
+
+def set_template_path(path):
+ global template_path
+
+ if path is None:
+ template_path = os.path.realpath(DEFAULT_TEMPLATE_PATH)
+ else:
+ template_path = os.path.realpath(path)
+
+ debug("set_template_path " + template_path)
+
+def parse_reference(reference):
+ """Parses URLs of the following form:
+
+ wvt:///youtube/video.xsl?srcurl=http%3A%2F%2Fwww.youtube.com%2F&param=name1,value1&param=name2,value2
+
+ reference is assumed to be URL-encoded UTF-8 string.
+
+ Returns (template, srcurl, params, processing_instructions) where
+ template if the URL path name (the part before ?), srcurl is the
+ parameter called srcurl, and params is a dictionary of (name,
+ quoted-value) pairs extracted from param parameters. Parameter
+ values are quoted so that the xslt parser handles them as string.
+ processing_instructions is dictionary of options that affect the
+ further processing of the data.
+ """
+ try:
+ reference = str(reference)
+ except UnicodeEncodeError:
+ return (None, None, None, None)
+
+ if not reference.startswith('wvt:///'):
+ return (None, None, None, None)
+
+ ref = reference[len('wvt:///'):]
+
+ template = None
+ srcurl = ''
+ parameters = {}
+ substitutions = {}
+ refsettings = {'HTTP-headers': {}}
+
+ fields = ref.split('?', 1)
+ template = fields[0]
+ if len(fields) == 1:
+ return (template, srcurl, parameters, refsettings)
+
+ for par in fields[1].split('&'):
+ paramfields = par.split('=', 1)
+ key = paramfields[0]
+
+ if len(paramfields) == 2:
+ value = urllib.unquote(paramfields[1])
+ else:
+ value = ''
+
+ if key.lower() == 'srcurl':
+ srcurl = value
+
+ elif key.lower() == 'param':
+ fields2 = value.split(',', 1)
+ pname = fields2[0].lower()
+ if len(fields2) == 2:
+ pvalue = "'" + fields2[1] + "'"
+ else:
+ pvalue = "''"
+ parameters[pname] = pvalue
+
+ elif key.lower() == 'subst':
+ substfields = value.split(',', 1)
+ if len(substfields) == 2:
+ substitutions[substfields[0]] = substfields[1]
+
+ elif key.lower() == 'minquality':
+ try:
+ refsettings['minquality'] = int(value)
+ except ValueError:
+ pass
+
+ elif key.lower() == 'maxquality':
+ try:
+ refsettings['maxquality'] = int(value)
+ except ValueError:
+ pass
+
+ elif key.lower() == 'postprocess':
+ refsettings.setdefault('postprocess', []).append(value)
+
+ elif key.lower() == 'contenttype':
+ refsettings['overridecontenttype'] = value
+
+ elif key.lower() == 'http-header':
+ try:
+ headername, headerdata = value.split(',', 1)
+ except ValueError:
+ continue
+ refsettings['HTTP-headers'][headername] = headerdata
+
+ if substitutions:
+ srcurl = brace_substitution(srcurl, substitutions)
+
+ return (template, srcurl, parameters, refsettings)
+
+def brace_substitution(template, subs):
+ """Substitute subs[x] for '{x}' in template. Unescape {{ to { and
+ }} to }. Unescaping is not done in substitution keys, i.e. while
+ scanning for a closing brace after a single opening brace."""
+ strbuf = cStringIO.StringIO()
+
+ last_pos = 0
+ for match in re.finditer(r'{{?|}}', template):
+ next_pos = match.start()
+ if next_pos < last_pos:
+ continue
+
+ strbuf.write(template[last_pos:next_pos])
+ if match.group(0) == '{{':
+ strbuf.write('{')
+ last_pos = next_pos+2
+
+ elif match.group(0) == '}}':
+ strbuf.write('}')
+ last_pos = next_pos+2
+
+ else: # match.group(0) == '{'
+ key_end = template.find('}', next_pos+1)
+ if key_end == -1:
+ strbuf.write(template[next_pos:])
+ last_pos = len(template)
+ break
+
+ try:
+ strbuf.write(urllib.quote(subs[template[next_pos+1:key_end]]))
+ except KeyError:
+ strbuf.write(template[next_pos:key_end+1])
+ last_pos = key_end+1
+
+ strbuf.write(template[last_pos:])
+ return strbuf.getvalue()
+
+
+class Request:
+ DEFAULT_URL_PRIORITY = 50
+
+ def __init__(self, reference, reqtype):
+ self.handle = None
+ self.dl = None
+
+ # state variables
+ self.xsltfile, self.srcurl, self.xsltparameters, self.processing = \
+ parse_reference(reference)
+ self.type = reqtype
+ self.status = -1
+ self.errmsg = None
+ self.mediaurls = []
+
+ # stream information
+ self.contenttype = 'text/xml'
+ self.contentlength = -1
+ self.streamtitle = ''
+
+ # callbacks
+ self.writefunc = None
+ self.writedata = None
+ self.readfunc = None
+ self.readdata = None
+
+ def handle_header(self, buf):
+ namedata = buf.split(':', 1)
+ if len(namedata) == 2:
+ headername, headerdata = namedata
+ if headername.lower() == 'content-type':
+ # Strip parameters like charset="utf-8"
+ self.contenttype = headerdata.split(';', 1)[0].strip()
+ elif headername.lower() == 'content-length':
+ try:
+ self.contentlength = int(headerdata.strip())
+ except ValueError:
+ self.contentlength = -1
+
+ def setup_downloader(self, url, writefunc, headerfunc, donefunc,
+ HTTPheaders=None, headers_only=False):
+ try:
+ self.dl = download.create_downloader(url,
+ template_path,
+ writefunc,
+ headerfunc,
+ donefunc,
+ HTTPheaders,
+ headers_only)
+ self.dl.start()
+ except download.DownloaderException, exc:
+ self.dl = None
+ if donefunc is not None:
+ donefunc(exc.code, exc.msg)
+
+ def start(self):
+ debug('start %s\ntemplate = %s, type = %s\n'
+ 'parameters = %s, processing = %s' %
+ (self.srcurl, self.xsltfile, self.type, str(self.xsltparameters),
+ str(self.processing)))
+
+ if self.type == WebviRequestType.MENU and self.srcurl == 'mainmenu':
+ self.send_mainmenu()
+ else:
+ self.setup_downloader(self.srcurl, None,
+ self.handle_header,
+ self.finished_apply_xslt,
+ self.processing['HTTP-headers'])
+
+ def stop(self):
+ if self.dl is not None:
+ debug("aborting")
+ self.dl.abort()
+
+ def start_download(self, url=None):
+ """Initialize a download.
+
+ If url is None, pop the first URL out of self.mediaurls. If
+ URL is an ASX playlist, read the content URL from it and start
+ to download the actual content.
+ """
+ while url is None or url == '':
+ try:
+ url = self.mediaurls.pop(0)
+ except IndexError:
+ self.request_done(406, 'No more URLs left')
+
+ debug('Start_download ' + url)
+
+ # reset stream status
+ self.contenttype = 'text/xml'
+ self.contentlength = -1
+
+ if self.is_asx_playlist(url):
+ self.setup_downloader(url, None,
+ self.handle_header,
+ self.finished_playlist_loaded,
+ self.processing['HTTP-headers'])
+
+ else:
+ self.setup_downloader(url, self.writewrapper,
+ self.handle_header,
+ self.finished_download,
+ self.processing['HTTP-headers'])
+
+ def check_and_send_url(self, url=None):
+ """Check if the target exists (currently only for HTTP URLs)
+ before relaying the URL to the client."""
+ while url is None or url == '':
+ try:
+ url = self.mediaurls.pop(0)
+ except IndexError:
+ self.request_done(406, 'No more URLs left')
+ return
+
+ debug('check_and_send_url ' + str(url))
+
+ if self.is_asx_playlist(url):
+ self.setup_downloader(url, None, self.handle_header,
+ self.finished_playlist_loaded,
+ self.processing['HTTP-headers'])
+ elif url.startswith('http://') or url.startswith('https://'):
+ self.checking_url = url
+ self.setup_downloader(url, None, None,
+ self.finished_check_url,
+ self.processing['HTTP-headers'], True)
+ else:
+ self.writewrapper(url)
+ self.request_done(0, None)
+
+ def send_mainmenu(self):
+ """Build the XML main menu from the module description files
+ in the hard drive.
+ """
+ if not os.path.isdir(template_path):
+ self.request_done(404, "Can't access service directory %s" %
+ template_path)
+ return
+
+ debug('Reading XSLT templates from ' + template_path)
+
+ # Find menu items in the service.xml files in the subdirectories
+ menuitems = {}
+ for f in os.listdir(template_path):
+ if f == 'bin':
+ continue
+
+ filename = os.path.join(template_path, f, 'service.xml')
+ try:
+ doc = libxml2.parseFile(filename)
+ except libxml2.parserError:
+ debug("Failed to parse " + filename);
+ continue
+
+ title = ''
+ url = ''
+
+ root = doc.getRootElement()
+ if (root is None) or (root.name != 'service'):
+ debug("Root node is not 'service' in " + filename);
+ doc.freeDoc()
+ continue
+ node = root.children
+ while node is not None:
+ if node.name == 'title':
+ title = utils.get_content_unicode(node)
+ elif node.name == 'ref':
+ url = utils.get_content_unicode(node)
+ node = node.next
+ doc.freeDoc()
+
+ if (title == '') or (url == ''):
+ debug("Empty <title> or <ref> in " + filename);
+ continue
+
+ menuitems[title.lower()] = ('<link>\n'
+ '<label>%s</label>\n'
+ '<ref>%s</ref>\n'
+ '</link>\n' %
+ (libxml2.newText(title),
+ libxml2.newText(url)))
+ # Sort the menu items
+ titles = menuitems.keys()
+ titles.sort()
+
+ # Build the menu
+ mainmenu = ('<?xml version="1.0"?>\n'
+ '<wvmenu>\n'
+ '<title>Select video source</title>\n')
+ for t in titles:
+ mainmenu += menuitems[t]
+ mainmenu += '</wvmenu>'
+
+ self.dl = download.DummyDownloader(mainmenu,
+ writefunc=self.writewrapper,
+ donefunc=self.request_done)
+ self.dl.start()
+
+ def writewrapper(self, inp):
+ """Wraps pycurl write callback (with the data as the only
+ parameter) into webvi write callback (with signature (data,
+ length, usertag)). If self.writefunc is not set, write to
+ stdout."""
+ if self.writefunc is not None:
+ inplen = len(inp)
+ written = self.writefunc(inp, inplen, self.writedata)
+ if written != inplen:
+ self.dl.close()
+ self.request_done(405, 'Write callback failed')
+ else:
+ sys.stdout.write(inp)
+
+ def is_asx_playlist(self, url):
+ if utils.get_url_extension(url).lower() == 'asx':
+ return True
+ else:
+ return False
+
+ def get_url_from_asx(self, asx, asxurl):
+ """Simple ASX parser. Return the content of the first <ref>
+ tag."""
+ try:
+ doc = libxml2.htmlReadDoc(asx, asxurl, None,
+ libxml2.HTML_PARSE_NOERROR |
+ libxml2.HTML_PARSE_NOWARNING |
+ libxml2.HTML_PARSE_NONET)
+ except libxml2.treeError:
+ debug('Can\'t parse ASX:\n' + asx)
+ return None
+ root = doc.getRootElement()
+ ret = self._get_ref_recursive(root).strip()
+ doc.freeDoc()
+ return ret
+
+ def _get_ref_recursive(self, node):
+ if node is None:
+ return None
+ if node.name.lower() == 'ref':
+ href = node.prop('href')
+ if href is not None:
+ return href
+ child = node.children
+ while child:
+ res = self._get_ref_recursive(child)
+ if res is not None:
+ return res
+ child = child.next
+ return None
+
+ def parse_mediaurl(self, xml, minpriority, maxpriority):
+ debug('parse_mediaurl\n' + xml)
+
+ self.streamtitle = '???'
+ mediaurls = []
+
+ try:
+ doc = libxml2.parseDoc(xml)
+ except libxml2.parserError:
+ debug('Invalid XML')
+ return mediaurls
+
+ root = doc.getRootElement()
+ if root is None:
+ debug('No root node')
+ return mediaurls
+
+ urls_and_priorities = []
+ node = root.children
+ while node:
+ if node.name == 'title':
+ self.streamtitle = utils.get_content_unicode(node)
+ elif node.name == 'url':
+ try:
+ priority = int(node.prop('priority'))
+ except (ValueError, TypeError):
+ priority = self.DEFAULT_URL_PRIORITY
+
+ content = node.getContent()
+ if priority >= minpriority and priority <= maxpriority and content != '':
+ urls_and_priorities.append((priority, content))
+ node = node.next
+ doc.freeDoc()
+
+ urls_and_priorities.sort()
+ urls_and_priorities.reverse()
+ mediaurls = [b[1] for b in urls_and_priorities]
+
+ return mediaurls
+
+ def finished_download(self, err, errmsg):
+ if err == 0:
+ self.request_done(0, None)
+ elif err != 402 and self.mediaurls:
+ debug('Download failed (%s %s).\nTrying the next one.' % (err, errmsg))
+ self.dl = None
+ self.start_download()
+ else:
+ self.request_done(err, errmsg)
+
+ def finished_playlist_loaded(self, err, errmsg):
+ if err == 0:
+ url = self.get_url_from_asx(self.dl.get_body(),
+ self.dl.get_url())
+ if url is None:
+ err = 404
+ errmsg = 'No ref tag in ASX file'
+ else:
+ if not self.is_asx_playlist(url) and url.startswith('http:'):
+ # The protocol is really "Windows Media HTTP
+ # Streaming Protocol", not plain HTTP, even though
+ # the scheme in the ASX file says "http://". We
+ # can't do MS-WMSP but luckily most MS-WMSP
+ # servers support MMS, too.
+ url = 'mms:' + url[5:]
+
+ if self.type == WebviRequestType.STREAMURL:
+ self.check_and_send_url(url)
+ else:
+ self.start_download(url)
+
+ if err != 0:
+ if not self.mediaurls:
+ self.request_done(err, errmsg)
+ else:
+ if self.type == WebviRequestType.STREAMURL:
+ self.check_and_send_url()
+ else:
+ self.start_download()
+
+ def finished_apply_xslt(self, err, errmsg):
+ if err != 0:
+ self.request_done(err, errmsg)
+ return
+
+ url = self.srcurl
+
+ # Add input documentURL to the parameters
+ params = self.xsltparameters.copy()
+ params['docurl'] = "'" + url + "'"
+
+ minpriority = self.processing.get('minquality', 0)
+ maxpriority = self.processing.get('maxquality', 100)
+
+ xsltpath = os.path.join(template_path, self.xsltfile)
+
+ # Check that xsltpath is inside the template directory
+ if os.path.commonprefix([template_path, os.path.realpath(xsltpath)]) != template_path:
+ self.request_done(503, 'Insecure template path')
+ return
+
+ xml = self.dl.get_body()
+ encoding = self.dl.get_encoding()
+
+ if self.processing.has_key('postprocess') and \
+ 'json2xml' in self.processing['postprocess']:
+ xmldoc = json2xml.json2xml(xml, encoding)
+ if xmldoc is None:
+ self.request_done(503, 'Invalid JSON content')
+ return
+ xml = xmldoc.serialize('utf-8')
+ encoding = 'utf-8'
+
+ #debug(xml)
+
+ resulttree = utils.apply_xslt(xml, encoding, url,
+ xsltpath, params)
+ if resulttree is None:
+ self.request_done(503, 'XSLT transformation failed')
+ return
+
+ if self.type == WebviRequestType.MENU:
+ debug("result:")
+ debug(resulttree)
+ self.writewrapper(resulttree)
+ self.request_done(0, None)
+ elif self.type == WebviRequestType.STREAMURL:
+ self.mediaurls = self.parse_mediaurl(resulttree, minpriority, maxpriority)
+ if self.mediaurls:
+ self.check_and_send_url()
+ else:
+ self.request_done(406, 'No valid URLs found')
+ elif self.type == WebviRequestType.FILE:
+ self.mediaurls = self.parse_mediaurl(resulttree, minpriority, maxpriority)
+ if self.mediaurls:
+ self.start_download()
+ else:
+ self.request_done(406, 'No valid URLs found')
+ else:
+ self.request_done(0, None)
+
+ def finished_extract_playlist_url(self, err, errmsg):
+ if err == 0:
+ url = self.get_url_from_asx(self.dl.get_body(),
+ self.dl.get_url())
+ if url is not None:
+ if self.is_asx_playlist(url):
+ self.setup_downloader(url, None, None,
+ self.finished_extract_playlist_url,
+ self.processing['HTTP-headers'])
+ else:
+ if url.startswith('http:'):
+ url = 'mms:' + url[5:]
+ self.check_and_send_url(url)
+ else:
+ self.request_done(503, 'XSLT tranformation failed to produce URL')
+ else:
+ self.request_done(err, errmsg)
+
+
+ def finished_check_url(self, err, errmsg):
+ if err == 0:
+ self.writewrapper(self.checking_url)
+ self.request_done(0, None)
+ else:
+ self.check_and_send_url()
+
+ def request_done(self, err, errmsg):
+ debug('request_done: %d %s' % (err, errmsg))
+
+ self.status = err
+ self.errmsg = errmsg
+ self.dl = None
+
+ def is_finished(self):
+ return self.status >= 0
+
+
+class RequestList(dict):
+ nextreqnum = 1
+
+ def put(self, req):
+ reqnum = RequestList.nextreqnum
+ RequestList.nextreqnum += 1
+ req.handle = reqnum
+ self[reqnum] = req
+ return reqnum
diff --git a/src/libwebvi/webvi/utils.py b/src/libwebvi/webvi/utils.py
new file mode 100644
index 0000000..cefe09a
--- /dev/null
+++ b/src/libwebvi/webvi/utils.py
@@ -0,0 +1,134 @@
+# utils.py - misc. utility functions
+#
+# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import urlparse
+import re
+import libxml2
+import libxslt
+import urllib
+
+def get_url_extension(url):
+ """Extracts and returns the file extension from a URL."""
+ # The extension is located right before possible query
+ # ("?query=foo") or fragment ("#bar").
+ try:
+ i = url.index('?')
+ url = url[:i]
+ except ValueError:
+ pass
+ # The extension is the part after the last '.' that does not
+ # contain '/'.
+ idot = url.rfind('.')
+ islash = url.rfind('/')
+ if idot > islash:
+ return url[idot+1:]
+ else:
+ return ''
+
+def urljoin_query_fix(base, url, allow_fragments=True):
+ """urlparse.urljoin in Python 2.5 (2.6?) and older is broken in
+ case url is a pure query. See http://bugs.python.org/issue1432.
+ This handles correctly the case where base is a full (http) url
+ and url is a query, and calls urljoin() for other cases."""
+ if url.startswith('?'):
+ bscheme, bnetloc, bpath, bparams, bquery, bfragment = \
+ urlparse.urlparse(base, '', allow_fragments)
+ bquery = url[1:]
+ return urlparse.urlunparse((bscheme, bnetloc, bpath,
+ bparams, bquery, bfragment))
+ else:
+ return urlparse.urljoin(base, url, allow_fragments)
+
+def get_content_unicode(node):
+ """node.getContent() returns an UTF-8 encoded sequence of bytes (a
+ string). Convert it to a unicode object."""
+ return unicode(node.getContent(), 'UTF-8', 'replace')
+
+def apply_xslt(buf, encoding, url, xsltfile, params=None):
+ """Apply xslt transform from file xsltfile to the string buf
+ with parameters params. url is the location of buf. Returns
+ the transformed file as a string, or None if the
+ transformation couldn't be completed."""
+ stylesheet = libxslt.parseStylesheetFile(xsltfile)
+
+ if stylesheet is None:
+ #self.log_info('Can\'t open stylesheet %s' % xsltfile, 'warning')
+ return None
+ try:
+ # htmlReadDoc fails if the buffer is empty but succeeds
+ # (returning an empty tree) if the buffer is a single
+ # space.
+ if buf == '':
+ buf = ' '
+
+ # Guess whether this is an XML or HTML document.
+ if buf.startswith('<?xml'):
+ doc = libxml2.readDoc(buf, url, None,
+ libxml2.XML_PARSE_NOERROR |
+ libxml2.XML_PARSE_NOWARNING |
+ libxml2.XML_PARSE_NONET)
+ else:
+ #self.log_info('Using HTML parser', 'debug')
+ doc = libxml2.htmlReadDoc(buf, url, encoding,
+ libxml2.HTML_PARSE_NOERROR |
+ libxml2.HTML_PARSE_NOWARNING |
+ libxml2.HTML_PARSE_NONET)
+ except libxml2.treeError:
+ stylesheet.freeStylesheet()
+ #self.log_info('Can\'t parse XML document', 'warning')
+ return None
+ resultdoc = stylesheet.applyStylesheet(doc, params)
+ stylesheet.freeStylesheet()
+ doc.freeDoc()
+ if resultdoc is None:
+ #self.log_info('Can\'t apply stylesheet', 'warning')
+ return None
+
+ # Postprocess the document:
+ # Resolve relative URLs in srcurl (TODO: this should be done in XSLT)
+ root = resultdoc.getRootElement()
+ if root is None:
+ resultdoc.freeDoc()
+ return None
+
+ node2 = root.children
+ while node2 is not None:
+ if node2.name not in ['link', 'button']:
+ node2 = node2.next
+ continue
+
+ node = node2.children
+ while node is not None:
+ if (node.name == 'ref') or (node.name == 'stream') or \
+ (node.name == 'submission'):
+ refurl = node.getContent()
+
+ match = re.search(r'\?.*srcurl=([^&]*)', refurl)
+ if match is not None:
+ oldurl = urllib.unquote(match.group(1))
+ absurl = urljoin_query_fix(url, oldurl)
+ newurl = refurl[:match.start(1)] + \
+ urllib.quote(absurl) + \
+ refurl[match.end(1):]
+ node.setContent(resultdoc.encodeSpecialChars(newurl))
+
+ node = node.next
+ node2 = node2.next
+
+ ret = resultdoc.serialize('UTF-8')
+ resultdoc.freeDoc()
+ return ret
diff --git a/src/libwebvi/webvi/version.py b/src/libwebvi/webvi/version.py
new file mode 100644
index 0000000..26cb817
--- /dev/null
+++ b/src/libwebvi/webvi/version.py
@@ -0,0 +1,20 @@
+# version.py - webvi version
+#
+# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+MAJOR = '0'
+MINOR = '2'
+VERSION = MAJOR + '.' + MINOR
diff --git a/src/unittest/Makefile b/src/unittest/Makefile
new file mode 100644
index 0000000..81b0ea2
--- /dev/null
+++ b/src/unittest/Makefile
@@ -0,0 +1,11 @@
+CFLAGS=-O2 -g -Wall -I../libwebvi
+LDFLAGS=-L../libwebvi -Wl,-rpath=../libwebvi -lwebvi
+
+all: testlibwebvi testdownload
+
+testlibwebvi: testlibwebvi.o ../libwebvi/libwebvi.so
+
+testdownload: testdownload.o ../libwebvi/libwebvi.so
+
+clean:
+ rm -f testlibwebvi testlibwebvi.o testdownload testdownload.o
diff --git a/src/unittest/runtests.sh b/src/unittest/runtests.sh
new file mode 100755
index 0000000..9afc7a5
--- /dev/null
+++ b/src/unittest/runtests.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+export PYTHONPATH=../libwebvi
+
+./testlibwebvi
+#./testdownload
+#./testwebvi.py
diff --git a/src/unittest/testdownload.c b/src/unittest/testdownload.c
new file mode 100644
index 0000000..134150a
--- /dev/null
+++ b/src/unittest/testdownload.c
@@ -0,0 +1,195 @@
+/*
+ * testlibwebvi.c: unittest for webvi C bindings
+ *
+ * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <sys/select.h>
+#include <errno.h>
+
+#include "libwebvi.h"
+
+#define WVTREFERENCE "wvt:///youtube/video.xsl?srcurl=http%3A//www.youtube.com/watch%3Fv%3Dk5LmKNYTqvk"
+
+#define CHECK_WEBVI_CALL(err, funcname) \
+ if (err != WEBVIERR_OK) { \
+ fprintf(stderr, "%s FAILED: %s\n", funcname, webvi_strerror(ctx, err)); \
+ returncode = 127; \
+ goto cleanup; \
+ }
+
+struct download_data {
+ long bytes_downloaded;
+ WebviCtx ctx;
+ WebviHandle handle;
+};
+
+ssize_t file_callback(const char *buf, size_t len, void *data) {
+ struct download_data *dldata = (struct download_data *)data;
+
+ if (dldata->bytes_downloaded == 0) {
+ char *url, *title, *contentType;
+ long contentLength;
+
+ if (webvi_get_info(dldata->ctx, dldata->handle, WEBVIINFO_URL, &url) != WEBVIERR_OK) {
+ fprintf(stderr, "webvi_get_info FAILED\n");
+ return -1;
+ }
+
+ if (url) {
+ printf("File URL: %s\n", url);
+ free(url);
+ }
+
+ if (webvi_get_info(dldata->ctx, dldata->handle, WEBVIINFO_STREAM_TITLE, &title) != WEBVIERR_OK) {
+ fprintf(stderr, "webvi_get_info FAILED\n");
+ return -1;
+ }
+
+ if (title) {
+ printf("Title: %s\n", title);
+ free(title);
+ }
+
+ if (webvi_get_info(dldata->ctx, dldata->handle, WEBVIINFO_CONTENT_TYPE, &contentType) != WEBVIERR_OK) {
+ fprintf(stderr, "webvi_get_info FAILED\n");
+ return -1;
+ }
+
+ if (contentType) {
+ printf("Content type: %s\n", contentType);
+ free(contentType);
+ }
+
+ if (webvi_get_info(dldata->ctx, dldata->handle, WEBVIINFO_CONTENT_LENGTH, &contentLength) != WEBVIERR_OK) {
+ fprintf(stderr, "webvi_get_info FAILED\n");
+ return -1;
+ }
+
+ printf("Content length: %ld\n", contentLength);
+ }
+
+ dldata->bytes_downloaded += len;
+
+ printf("\r%ld", dldata->bytes_downloaded);
+
+ return len;
+}
+
+int main(int argc, const char* argv[]) {
+ int returncode = 0;
+ WebviCtx ctx = 0;
+ WebviHandle handle = -1;
+ fd_set readfd, writefd, excfd;
+ int maxfd, fd, s, msg_remaining;
+ struct timeval timeout;
+ long running;
+ WebviMsg *donemsg;
+ int done;
+ struct download_data callback_data;
+
+ printf("Testing %s\n", webvi_version());
+
+ if (webvi_global_init() != 0) {
+ fprintf(stderr, "webvi_global_init FAILED\n");
+ return 127;
+ }
+
+ ctx = webvi_initialize_context();
+ if (ctx == 0) {
+ fprintf(stderr, "webvi_initialize_context FAILED\n");
+ returncode = 127;
+ goto cleanup;
+ }
+
+ CHECK_WEBVI_CALL(webvi_set_config(ctx, WEBVI_CONFIG_TEMPLATE_PATH, "../../templates"),
+ "webvi_set_config(WEBVI_CONFIG_TEMPLATE_PATH)");
+
+ handle = webvi_new_request(ctx, WVTREFERENCE, WEBVIREQ_FILE);
+ if (handle == -1) {
+ fprintf(stderr, "webvi_new_request FAILED\n");
+ returncode = 127;
+ goto cleanup;
+ }
+
+ callback_data.bytes_downloaded = 0;
+ callback_data.ctx = ctx;
+ callback_data.handle = handle;
+ CHECK_WEBVI_CALL(webvi_set_opt(ctx, handle, WEBVIOPT_WRITEDATA, &callback_data),
+ "webvi_set_opt(WEBVIOPT_WRITEDATA)");
+ CHECK_WEBVI_CALL(webvi_set_opt(ctx, handle, WEBVIOPT_WRITEFUNC, file_callback),
+ "webvi_set_opt(WEBVIOPT_WRITEFUNC)");
+ CHECK_WEBVI_CALL(webvi_start_handle(ctx, handle),
+ "webvi_start_handle");
+
+ done = 0;
+ do {
+ FD_ZERO(&readfd);
+ FD_ZERO(&writefd);
+ FD_ZERO(&excfd);
+ CHECK_WEBVI_CALL(webvi_fdset(ctx, &readfd, &writefd, &excfd, &maxfd),
+ "webvi_fdset");
+
+ timeout.tv_sec = 1;
+ timeout.tv_usec = 0;
+ s = select(maxfd+1, &readfd, &writefd, NULL, &timeout);
+
+ if (s < 0) {
+ if (errno == EINTR)
+ continue;
+
+ perror("select FAILED");
+ returncode = 127;
+ goto cleanup;
+
+ } if (s == 0) {
+ CHECK_WEBVI_CALL(webvi_perform(ctx, 0, WEBVI_SELECT_TIMEOUT, &running),
+ "webvi_perform");
+ } else {
+ for (fd=0; fd<=maxfd; fd++) {
+ if (FD_ISSET(fd, &readfd)) {
+ CHECK_WEBVI_CALL(webvi_perform(ctx, fd, WEBVI_SELECT_READ, &running),
+ "webvi_perform");
+ }
+ if (FD_ISSET(fd, &writefd)) {
+ CHECK_WEBVI_CALL(webvi_perform(ctx, fd, WEBVI_SELECT_WRITE, &running),
+ "webvi_perform");
+ }
+ }
+ }
+
+ do {
+ donemsg = webvi_get_message(ctx, &msg_remaining);
+ if (donemsg && donemsg->msg == WEBVIMSG_DONE && donemsg->handle == handle) {
+ done = 1;
+ }
+ } while (msg_remaining > 0);
+ } while (!done);
+
+ printf("\nRead %ld bytes.\n"
+ "Test successful.\n", callback_data.bytes_downloaded);
+
+cleanup:
+ if (ctx != 0) {
+ if (handle != -1)
+ webvi_delete_handle(ctx, handle);
+ webvi_cleanup_context(ctx);
+ }
+ webvi_cleanup(1);
+
+ return returncode;
+}
diff --git a/src/unittest/testlibwebvi.c b/src/unittest/testlibwebvi.c
new file mode 100644
index 0000000..0dda58a
--- /dev/null
+++ b/src/unittest/testlibwebvi.c
@@ -0,0 +1,147 @@
+/*
+ * testlibwebvi.c: unittest for webvi C bindings
+ *
+ * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <sys/select.h>
+#include <errno.h>
+
+#include "libwebvi.h"
+
+#define WVTREFERENCE "wvt:///?srcurl=mainmenu"
+//#define WVTREFERENCE "wvt:///youtube/categories.xsl?srcurl=http://gdata.youtube.com/schemas/2007/categories.cat"
+//#define WVTREFERENCE "wvt:///youtube/search.xsl"
+
+#define CHECK_WEBVI_CALL(err, funcname) \
+ if (err != WEBVIERR_OK) { \
+ fprintf(stderr, "%s FAILED: %s\n", funcname, webvi_strerror(ctx, err)); \
+ returncode = 127; \
+ goto cleanup; \
+ }
+
+ssize_t count_bytes_callback(const char *buf, size_t len, void *data) {
+ long *bytes = (long *)data;
+ *bytes += len;
+ return len;
+}
+
+int main(int argc, const char* argv[]) {
+ int returncode = 0;
+ WebviCtx ctx = 0;
+ WebviHandle handle = -1;
+ long bytes = 0;
+ fd_set readfd, writefd, excfd;
+ int maxfd, fd, s, msg_remaining;
+ struct timeval timeout;
+ long running;
+ WebviMsg *donemsg;
+ int done;
+ char *contenttype;
+
+ printf("Testing %s\n", webvi_version());
+
+ if (webvi_global_init() != 0) {
+ fprintf(stderr, "webvi_global_init FAILED\n");
+ return 127;
+ }
+
+ ctx = webvi_initialize_context();
+ if (ctx == 0) {
+ fprintf(stderr, "webvi_initialize_context FAILED\n");
+ returncode = 127;
+ goto cleanup;
+ }
+
+ CHECK_WEBVI_CALL(webvi_set_config(ctx, WEBVI_CONFIG_TEMPLATE_PATH, "../../templates"),
+ "webvi_set_config(WEBVI_CONFIG_TEMPLATE_PATH)");
+
+ handle = webvi_new_request(ctx, WVTREFERENCE, WEBVIREQ_MENU);
+ if (handle == -1) {
+ fprintf(stderr, "webvi_new_request FAILED\n");
+ returncode = 127;
+ goto cleanup;
+ }
+
+ CHECK_WEBVI_CALL(webvi_set_opt(ctx, handle, WEBVIOPT_WRITEDATA, &bytes),
+ "webvi_set_opt(WEBVIOPT_WRITEDATA)");
+ CHECK_WEBVI_CALL(webvi_set_opt(ctx, handle, WEBVIOPT_WRITEFUNC, count_bytes_callback),
+ "webvi_set_opt(WEBVIOPT_WRITEFUNC)");
+ CHECK_WEBVI_CALL(webvi_start_handle(ctx, handle),
+ "webvi_start_handle");
+
+ done = 0;
+ do {
+ FD_ZERO(&readfd);
+ FD_ZERO(&writefd);
+ FD_ZERO(&excfd);
+ CHECK_WEBVI_CALL(webvi_fdset(ctx, &readfd, &writefd, &excfd, &maxfd),
+ "webvi_fdset");
+
+ timeout.tv_sec = 10;
+ timeout.tv_usec = 0;
+ s = select(maxfd+1, &readfd, &writefd, NULL, &timeout);
+
+ if (s < 0) {
+ if (errno == EINTR)
+ continue;
+
+ perror("select FAILED");
+ returncode = 127;
+ goto cleanup;
+
+ } if (s == 0) {
+ CHECK_WEBVI_CALL(webvi_perform(ctx, 0, WEBVI_SELECT_TIMEOUT, &running),
+ "webvi_perform");
+ } else {
+ for (fd=0; fd<=maxfd; fd++) {
+ if (FD_ISSET(fd, &readfd)) {
+ CHECK_WEBVI_CALL(webvi_perform(ctx, fd, WEBVI_SELECT_READ, &running),
+ "webvi_perform");
+ }
+ if (FD_ISSET(fd, &writefd)) {
+ CHECK_WEBVI_CALL(webvi_perform(ctx, fd, WEBVI_SELECT_WRITE, &running),
+ "webvi_perform");
+ }
+ }
+ }
+
+ do {
+ donemsg = webvi_get_message(ctx, &msg_remaining);
+ if (donemsg && donemsg->msg == WEBVIMSG_DONE && donemsg->handle == handle) {
+ done = 1;
+ }
+ } while (msg_remaining > 0);
+ } while (!done);
+
+ CHECK_WEBVI_CALL(webvi_get_info(ctx, handle, WEBVIINFO_CONTENT_TYPE, &contenttype),
+ "webvi_get_info");
+ printf("Read %ld bytes. Content type: %s\n", bytes, contenttype);
+ free(contenttype);
+
+ printf("Test successful.\n");
+
+cleanup:
+ if (ctx != 0) {
+ if (handle != -1)
+ webvi_delete_handle(ctx, handle);
+ webvi_cleanup_context(ctx);
+ }
+ webvi_cleanup(1);
+
+ return returncode;
+}
diff --git a/src/unittest/testwebvi.py b/src/unittest/testwebvi.py
new file mode 100644
index 0000000..6017ded
--- /dev/null
+++ b/src/unittest/testwebvi.py
@@ -0,0 +1,407 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# This file is part of vdr-webvideo-plugin.
+#
+# Copyright 2009,2010 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""Blackbox tests for each of thee supported video sites and webvicli.
+
+Mainly useful for checking if the web sites have changed so much that
+the XSLT templates don't match to them anymore. Requires network
+connection because the tests automatically connect and navigate
+through links on the video sites.
+"""
+
+import unittest
+import sys
+import re
+
+sys.path.append('../webvicli')
+sys.path.append('../libwebvi')
+from webvicli import client, menu
+import webvi.api
+from webvi.constants import WebviConfig
+
+class TestServiceModules(unittest.TestCase):
+
+ # ========== Helper functions ==========
+
+ def setUp(self):
+ webvi.api.set_config(WebviConfig.TEMPLATE_PATH, '../../templates')
+ self.client = client.WVClient([], {}, {})
+
+ def getLinks(self, menuobj):
+ links = []
+ for i in xrange(len(menuobj)):
+ if isinstance(menuobj[i], menu.MenuItemLink):
+ links.append(menuobj[i])
+ return links
+
+ def downloadMenuPage(self, reference, menuname):
+ (status, statusmsg, menuobj) = self.client.getmenu(reference)
+ self.assertEqual(status, 0, 'Unexpected status code %s (%s) in %s menu\nFailed ref was %s' % (status, statusmsg, menuname, reference))
+ self.assertNotEqual(menuobj, None, 'Failed to get %s menu' % menuname)
+ return menuobj
+
+ def downloadAndExtractLinks(self, reference, minlinks, menuname):
+ menuobj = self.downloadMenuPage(reference, menuname)
+ links = self.getLinks(menuobj)
+ self.assertTrue(len(links) >= minlinks, 'Too few links in %s menu' % menuname)
+ return links
+
+ def checkMediaUrl(self, reference):
+ streamurl = self.client.get_stream_url(reference)
+ self.assertNotEqual(streamurl, None, 'get_stream_url returned None')
+ self.assertNotEqual(streamurl, '', 'get_stream_url returned empty string')
+
+ def getServiceReference(self, templatedir):
+ service = open(templatedir + '/service.xml').read()
+ m = re.search(r'<ref>(.*)</ref>', service)
+ self.assertNotEqual(m, None, 'no <ref> in service.xml')
+ return m.group(1)
+
+ # ========== Tests for supported websites ==========
+
+ def testMainMenu(self):
+ self.downloadAndExtractLinks('wvt:///?srcurl=mainmenu', 4, 'main')
+
+ def testYoutube(self):
+ # Category page
+ ref = self.getServiceReference('../../templates/youtube')
+ links = self.downloadAndExtractLinks(ref, 3, 'category')
+
+ # Navigation page
+ # The third one is the first "proper" category. The first and second are "Search" and "All"
+ navigationref = links[2].ref
+ links = self.downloadAndExtractLinks(navigationref, 2, 'navigation')
+
+ # Video link
+ videolink = links[0]
+ self.assertNotEqual(videolink.stream, None, 'No media object in a video link')
+ self.assertNotEqual(videolink.ref, None, 'No description page in a video link')
+ self.checkMediaUrl(videolink.stream)
+
+ def testYoutubeSearch(self):
+ menuobj = self.downloadMenuPage('wvt:///youtube/search.xsl', 'search')
+ self.assertTrue(len(menuobj) >= 4, 'Too few items in search menu')
+
+ self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField))
+ self.assertTrue(isinstance(menuobj[1], menu.MenuItemList))
+ self.assertTrue(len(menuobj[1].items) >= 4)
+ self.assertTrue(isinstance(menuobj[2], menu.MenuItemList))
+ self.assertTrue(len(menuobj[2].items) >= 4)
+ self.assertTrue(isinstance(menuobj[3], menu.MenuItemSubmitButton))
+
+ # Query term
+ menuobj[0].value = 'youtube'
+ # Sort by: rating
+ menuobj[1].current = 3
+ # Uploaded: This month
+ menuobj[2].current = 3
+
+ resultref = menuobj[3].activate()
+ self.assertNotEqual(resultref, None)
+ self.downloadAndExtractLinks(resultref, 1, 'search result')
+
+ def testGoogleSearch(self):
+ ref = self.getServiceReference('../../templates/google')
+ menuobj = self.downloadMenuPage(ref, 'search')
+ self.assertTrue(len(menuobj) == 4, 'Unexpected number of items in Google search menu')
+
+ self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField))
+ self.assertTrue(isinstance(menuobj[1], menu.MenuItemList))
+ self.assertTrue(len(menuobj[1].items) >= 4)
+ self.assertTrue(isinstance(menuobj[2], menu.MenuItemList))
+ self.assertTrue(len(menuobj[2].items) >= 4)
+ self.assertTrue(isinstance(menuobj[3], menu.MenuItemSubmitButton))
+
+ # Query term
+ menuobj[0].value = 'google'
+ # Sort by: date
+ menuobj[1].current = 3
+ # Duration: Short
+ menuobj[2].current = 1
+
+ resultref = menuobj[3].activate()
+ self.assertNotEqual(resultref, None)
+ self.downloadAndExtractLinks(resultref, 1, 'search result')
+
+ def testSVTPlay(self):
+ # Category page
+ ref = self.getServiceReference('../../templates/svtplay')
+ links = self.downloadAndExtractLinks(ref, 3, 'category')
+
+ # Navigation page
+ navigationref = links[0].ref
+ links = self.downloadAndExtractLinks(navigationref, 2, 'navigation')
+
+ # Single program
+ programref = links[0].ref
+ links = self.downloadAndExtractLinks(programref, 1, 'program')
+
+ # Video link
+ videolink = links[0]
+ self.assertNotEqual(videolink.stream, None, 'No media object in a video link')
+ self.assertNotEqual(videolink.ref, None, 'No description page in a video link')
+ self.checkMediaUrl(videolink.stream)
+
+ def testMetacafe(self):
+ # Category page
+ ref = self.getServiceReference('../../templates/metacafe')
+ links = self.downloadAndExtractLinks(ref, 3, 'category')
+
+ # The first is "Search", the second is "Channels" and the
+ # third is the first "proper" navigation.
+ channelsref = links[1].ref
+ navigationref = links[2].ref
+
+ # Navigation page
+ links = self.downloadAndExtractLinks(navigationref, 2, 'navigation')
+
+ # Video link
+ videolink = links[0]
+ self.assertNotEqual(videolink.stream, None, 'No media object in a video link')
+ self.assertNotEqual(videolink.ref, None, 'No description page in a video link')
+ self.checkMediaUrl(videolink.stream)
+
+ # User channels
+ links = self.downloadAndExtractLinks(channelsref, 3, 'channel list')
+
+ def testMetacafeSearch(self):
+ menuobj = self.downloadMenuPage('wvt:///metacafe/search.xsl', 'search')
+ self.assertTrue(len(menuobj) >= 4, 'Too few items in search menu')
+
+ self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField))
+ self.assertTrue(isinstance(menuobj[1], menu.MenuItemList))
+ self.assertTrue(len(menuobj[1].items) == 3)
+ self.assertTrue(isinstance(menuobj[2], menu.MenuItemList))
+ self.assertTrue(len(menuobj[2].items) == 4)
+ self.assertTrue(isinstance(menuobj[3], menu.MenuItemSubmitButton))
+
+ # Query term
+ menuobj[0].value = 'metacafe'
+ # Sort by: most discussed
+ menuobj[1].current = 2
+ # Published: Anytime
+ menuobj[2].current = 2
+
+ resultref = menuobj[3].activate()
+ self.assertNotEqual(resultref, None)
+ self.downloadAndExtractLinks(resultref, 1, 'search result')
+
+ def testVimeo(self):
+ # Category page
+ ref = self.getServiceReference('../../templates/vimeo')
+ links = self.downloadAndExtractLinks(ref, 3, 'Vimeo main page')
+
+ # The first is "Search", the second is "Channels" and the
+ # third is "Groups"
+ channelsref = links[1].ref
+ groupsref = links[2].ref
+
+ # Channels page
+ links = self.downloadAndExtractLinks(channelsref, 2, 'channels')
+
+ # Navigation page
+ links = self.downloadAndExtractLinks(links[0].ref, 2, 'channels navigation')
+
+ # Video link
+ videolink = links[0]
+ self.assertNotEqual(videolink.stream, None, 'No media object in a video link')
+ self.assertNotEqual(videolink.ref, None, 'No description page in a video link')
+ self.checkMediaUrl(videolink.stream)
+
+ # User groups
+ links = self.downloadAndExtractLinks(groupsref, 2, 'channel list')
+
+ # Navigation page
+ links = self.downloadAndExtractLinks(links[0].ref, 2, 'groups navigation')
+
+ def testVimeoSearch(self):
+ menuobj = self.downloadMenuPage('wvt:///vimeo/search.xsl?srcurl=http://www.vimeo.com/', 'search')
+ self.assertTrue(len(menuobj) >= 3, 'Too few items in search menu')
+
+ self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField))
+ self.assertTrue(isinstance(menuobj[1], menu.MenuItemList))
+ self.assertTrue(len(menuobj[1].items) >= 2)
+ self.assertTrue(isinstance(menuobj[2], menu.MenuItemSubmitButton))
+
+ # Query term
+ menuobj[0].value = 'vimeo'
+ # Sort by: newest
+ menuobj[1].current = 1
+
+ resultref = menuobj[2].activate()
+ self.assertNotEqual(resultref, None)
+ self.downloadAndExtractLinks(resultref, 1, 'search result')
+
+ def testYLEAreena(self):
+ # Category page
+ ref = self.getServiceReference('../../templates/yleareena')
+ links = self.downloadAndExtractLinks(ref, 3, 'category')
+
+ # The first is "Search", the second is "live", the third is
+ # "all", the rest are navigation links.
+ liveref = links[1].ref
+ navigationref = links[3].ref
+
+ # Navigation page
+ links = self.downloadAndExtractLinks(navigationref, 2, 'navigation')
+
+ # Video link
+ videolink = links[0]
+ self.assertNotEqual(videolink.stream, None, 'No media object in a video link')
+ self.assertNotEqual(videolink.ref, None, 'No description page in a video link')
+
+ # live broadcasts
+ links = self.downloadAndExtractLinks(liveref, 2, 'live broadcasts')
+
+ def testYLEAreenaSearch(self):
+ menuobj = self.downloadMenuPage('wvt:///yleareena/search.xsl?srcurl=http://areena.yle.fi/haku', 'search')
+ self.assertTrue(len(menuobj) >= 8, 'Too few items in search menu')
+
+ self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField))
+ self.assertTrue(isinstance(menuobj[1], menu.MenuItemList))
+ self.assertTrue(len(menuobj[1].items) >= 3)
+ self.assertTrue(isinstance(menuobj[2], menu.MenuItemList))
+ self.assertTrue(len(menuobj[2].items) >= 2)
+ self.assertTrue(isinstance(menuobj[3], menu.MenuItemList))
+ self.assertTrue(len(menuobj[3].items) >= 2)
+ self.assertTrue(isinstance(menuobj[4], menu.MenuItemList))
+ self.assertTrue(len(menuobj[4].items) >= 3)
+ self.assertTrue(isinstance(menuobj[5], menu.MenuItemList))
+ self.assertTrue(len(menuobj[5].items) >= 4)
+ self.assertTrue(isinstance(menuobj[6], menu.MenuItemList))
+ self.assertTrue(len(menuobj[6].items) >= 2)
+ self.assertTrue(isinstance(menuobj[7], menu.MenuItemSubmitButton))
+
+ # Query term
+ menuobj[0].value = 'yle'
+ # Media: video
+ menuobj[1].current = 1
+ # Category: all
+ menuobj[2].current = 0
+ # Channel: all
+ menuobj[3].current = 0
+ # Language: Finnish
+ menuobj[4].current = 1
+ # Uploaded: all
+ menuobj[5].current = 0
+ # Only outside Finland: no
+ menuobj[6].current = 0
+
+ resultref = menuobj[7].activate()
+ self.assertNotEqual(resultref, None)
+ self.downloadAndExtractLinks(resultref, 1, 'search result')
+
+ def testKatsomo(self):
+ # Category page
+ ref = self.getServiceReference('../../templates/katsomo')
+ links = self.downloadAndExtractLinks(ref, 2, 'category')
+
+ # The first is "Search", the rest are navigation links.
+ navigationref = links[1].ref
+
+ # Navigation page
+ links = self.downloadAndExtractLinks(navigationref, 1, 'navigation')
+
+ # Program page
+ links = self.downloadAndExtractLinks(links[0].ref, 1, 'program')
+
+ # Video link
+ # The first few links may be navigation links, but there
+ # should be video links after them.
+ foundVideo = False
+ for link in links:
+ if link.stream is not None:
+ foundVideo = True
+
+ self.assertTrue(link, 'No a video links in the program page')
+
+ def testKatsomoSearch(self):
+ menuobj = self.downloadMenuPage('wvt:///katsomo/search.xsl', 'search')
+ self.assertTrue(len(menuobj) >= 2, 'Too few items in search menu')
+
+ self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField))
+ self.assertTrue(isinstance(menuobj[1], menu.MenuItemSubmitButton))
+
+ # Query term
+ menuobj[0].value = 'mtv3'
+
+ resultref = menuobj[1].activate()
+ self.assertNotEqual(resultref, None)
+ self.downloadAndExtractLinks(resultref, 1, 'search result')
+
+ def testRuutuFi(self):
+ # Category page
+ ref = self.getServiceReference('../../templates/ruutufi')
+ links = self.downloadAndExtractLinks(ref, 4, 'category')
+
+ # The first is "Search", the second is "Series"
+ seriesref = links[1].ref
+
+ # Series page
+ links = self.downloadAndExtractLinks(seriesref, 1, 'series')
+
+ # Program page
+ links = self.downloadAndExtractLinks(links[0].ref, 1, 'program')
+
+ # Video link
+ videolink = links[0]
+ self.assertNotEqual(videolink.stream, None, 'No media object in a video link')
+ self.assertNotEqual(videolink.ref, None, 'No description page in a video link')
+
+ def testRuutuFiSearch(self):
+ menuobj = self.downloadMenuPage('wvt:///ruutufi/search.xsl', 'search')
+ self.assertTrue(len(menuobj) >= 2, 'Too few items in search menu')
+
+ self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField))
+ self.assertTrue(isinstance(menuobj[1], menu.MenuItemSubmitButton))
+
+ # Query term
+ menuobj[0].value = 'nelonen'
+
+ resultref = menuobj[1].activate()
+ self.assertNotEqual(resultref, None)
+ self.downloadAndExtractLinks(resultref, 1, 'search result')
+
+ def testSubtv(self):
+ # Category page
+ ref = self.getServiceReference('../../templates/subtv')
+ links = self.downloadAndExtractLinks(ref, 4, 'series')
+
+ # Program page
+ links = self.downloadAndExtractLinks(links[0].ref, 1, 'program')
+
+ # Video link
+ videolink = links[0]
+ self.assertNotEqual(videolink.stream, None, 'No media object in a video link')
+ self.assertNotEqual(videolink.ref, None, 'No description page in a video link')
+
+
+if __name__ == '__main__':
+ testnames = sys.argv[1:]
+
+ if testnames == []:
+ # Run all tests
+ unittest.main()
+ else:
+ # Run test listed on the command line
+ for test in testnames:
+ suite = unittest.TestSuite()
+ suite.addTest(TestServiceModules(test))
+ unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/src/vdr-plugin/Makefile b/src/vdr-plugin/Makefile
new file mode 100644
index 0000000..ccc5641
--- /dev/null
+++ b/src/vdr-plugin/Makefile
@@ -0,0 +1,115 @@
+#
+# Makefile for a Video Disk Recorder plugin
+#
+# $Id$
+
+# The official name of this plugin.
+# This name will be used in the '-P...' option of VDR to load the plugin.
+# By default the main source file also carries this name.
+# IMPORTANT: the presence of this macro is important for the Make.config
+# file. So it must be defined, even if it is not used here!
+#
+PLUGIN = webvideo
+
+### The version number of this plugin (taken from the main source file):
+
+VERSION = $(shell grep 'const char \*VERSION *=' $(PLUGIN).c | awk '{ print $$5 }' | sed -e 's/[";]//g')
+
+### The C++ compiler and options:
+
+CXX ?= g++
+CXXFLAGS ?= -fPIC -g -O2 -Wall -Woverloaded-virtual -Wno-parentheses
+
+### The directory environment:
+
+VDRDIR = ../../../../..
+LIBDIR = ../../../../lib
+TMPDIR = /tmp
+
+### Libraries
+
+LIBS = `xml2-config --libs` -L../libwebvi -lwebvi
+
+### Allow user defined options to overwrite defaults:
+
+-include $(VDRDIR)/Make.config
+
+### The version number of VDR's plugin API (taken from VDR's "config.h"):
+
+APIVERSION = $(shell sed -ne '/define APIVERSION/s/^.*"\(.*\)".*$$/\1/p' $(VDRDIR)/config.h)
+
+### The name of the distribution archive:
+
+ARCHIVE = $(PLUGIN)-$(VERSION)
+PACKAGE = vdr-$(ARCHIVE)
+
+### Includes and Defines (add further entries here):
+
+LIBWEBVIINCPATH = ../libwebvi
+INCLUDES += -I$(VDRDIR)/include $(LIBWEBVIINCLUDES) -I$(LIBWEBVIINCPATH) `xml2-config --cflags`
+
+DEFINES += -D_GNU_SOURCE -DPLUGIN_NAME_I18N='"$(PLUGIN)"'
+
+### The object files (add further files here):
+
+OBJS = $(PLUGIN).o buffer.o common.o config.o download.o history.o menu.o menudata.o mimetypes.o request.o player.o dictionary.o iniparser.o timer.o menu_timer.o
+
+### The main target:
+
+all: libvdr-$(PLUGIN).so i18n
+
+### Implicit rules:
+
+%.o: %.c
+ $(CXX) $(CXXFLAGS) -c $(DEFINES) $(INCLUDES) $<
+
+### Dependencies:
+
+MAKEDEP = $(CXX) -MM -MG
+DEPFILE = .dependencies
+$(DEPFILE): Makefile
+ @$(MAKEDEP) $(DEFINES) $(INCLUDES) $(OBJS:%.o=%.c) > $@
+
+-include $(DEPFILE)
+
+### Internationalization (I18N):
+
+PODIR = po
+LOCALEDIR = $(VDRDIR)/locale
+I18Npo = $(wildcard $(PODIR)/*.po)
+I18Nmsgs = $(addprefix $(LOCALEDIR)/, $(addsuffix /LC_MESSAGES/vdr-$(PLUGIN).mo, $(notdir $(foreach file, $(I18Npo), $(basename $(file))))))
+I18Npot = $(PODIR)/$(PLUGIN).pot
+
+%.mo: %.po
+ msgfmt -c -o $@ $<
+
+$(I18Npot): $(wildcard *.c)
+ xgettext -C -cTRANSLATORS --no-wrap --no-location -k -ktr -ktrNOOP --msgid-bugs-address='<see README>' -o $@ $^
+
+%.po: $(I18Npot)
+ msgmerge -U --no-wrap --no-location --backup=none -q $@ $<
+ @touch $@
+
+$(I18Nmsgs): $(LOCALEDIR)/%/LC_MESSAGES/vdr-$(PLUGIN).mo: $(PODIR)/%.mo
+ @mkdir -p $(dir $@)
+ cp $< $@
+
+.PHONY: i18n
+i18n: $(I18Nmsgs) $(I18Npot)
+
+### Targets:
+
+libvdr-$(PLUGIN).so: $(OBJS)
+ $(CXX) $(CXXFLAGS) -shared $(OBJS) $(LIBS) -o $@
+ cp --remove-destination $@ $(LIBDIR)/$@.$(APIVERSION)
+
+dist: clean
+ @-rm -rf $(TMPDIR)/$(ARCHIVE)
+ @mkdir $(TMPDIR)/$(ARCHIVE)
+ @cp -a * $(TMPDIR)/$(ARCHIVE)
+ @tar czf $(PACKAGE).tgz -C $(TMPDIR) $(ARCHIVE)
+ @-rm -rf $(TMPDIR)/$(ARCHIVE)
+ @echo Distribution package created as $(PACKAGE).tgz
+
+clean:
+ @-rm -f $(OBJS) $(DEPFILE) *.so *.so.* *.tgz core* *~ $(PODIR)/*.mo $(PODIR)/*.pot
diff --git a/src/vdr-plugin/buffer.c b/src/vdr-plugin/buffer.c
new file mode 100644
index 0000000..41b2c38
--- /dev/null
+++ b/src/vdr-plugin/buffer.c
@@ -0,0 +1,84 @@
+/*
+ * buffer.c: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <vdr/tools.h>
+#include "buffer.h"
+
+// --- cMemoryBuffer -------------------------------------------------------
+
+cMemoryBuffer::cMemoryBuffer(size_t prealloc) {
+ capacity = prealloc;
+ buf = (char *)malloc(capacity*sizeof(char));
+ offset = 0;
+ len = 0;
+}
+
+cMemoryBuffer::~cMemoryBuffer() {
+ if (buf)
+ free(buf);
+}
+
+void cMemoryBuffer::Realloc(size_t newsize) {
+ if (newsize > capacity-offset) {
+ if (newsize <= capacity) {
+ // The new buffer fits in the memory if we just move the current
+ // content offset bytes backwards.
+ buf = (char *)memmove(buf, &buf[offset], len);
+ offset = 0;
+ } else {
+ // We need to realloc. Move the content to the beginning of the
+ // buffer while we are at it.
+ capacity += min(capacity, (size_t)10*1024);
+ capacity = max(capacity, newsize);
+ char *newbuf = (char *)malloc(capacity*sizeof(char));
+ if (newbuf) {
+ memcpy(newbuf, &buf[offset], len);
+ offset = 0;
+ free(buf);
+ buf = newbuf;
+ }
+ }
+ }
+}
+
+ssize_t cMemoryBuffer::Put(const char *data, size_t bytes) {
+ if (len+bytes > Free()) {
+ Realloc(len+bytes);
+ }
+
+ if (buf) {
+ memcpy(&buf[offset+len], data, bytes);
+ len += bytes;
+ return bytes;
+ }
+ return -1;
+}
+
+ssize_t cMemoryBuffer::PutFromFile(int fd, size_t bytes) {
+ if (len+bytes > Free()) {
+ Realloc(len+bytes);
+ }
+
+ if (buf) {
+ ssize_t r = safe_read(fd, &buf[offset+len], bytes);
+ if (r > 0)
+ len += r;
+ return r;
+ } else
+ return -1;
+}
+
+void cMemoryBuffer::Pop(size_t bytes) {
+ if (bytes <= len) {
+ offset += bytes;
+ len -= bytes;
+ }
+}
diff --git a/src/vdr-plugin/buffer.h b/src/vdr-plugin/buffer.h
new file mode 100644
index 0000000..0a5ee5c
--- /dev/null
+++ b/src/vdr-plugin/buffer.h
@@ -0,0 +1,44 @@
+/*
+ * buffer.h: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef __WEBVIDEO_BUFFER_H
+#define __WEBVIDEO_BUFFER_H
+
+#include <unistd.h>
+
+// --- cMemoryBuffer -------------------------------------------------------
+
+// FIFO character buffer.
+
+class cMemoryBuffer {
+private:
+ char *buf;
+ size_t offset;
+ size_t len;
+ size_t capacity;
+protected:
+ size_t Free() { return capacity-len-offset; }
+ virtual void Realloc(size_t newsize);
+public:
+ cMemoryBuffer(size_t prealloc = 10*1024);
+ virtual ~cMemoryBuffer();
+
+ // Put data into the end of the buffer
+ virtual ssize_t Put(const char *data, size_t length);
+ // Put data from a file descriptor fd to the buffer
+ virtual ssize_t PutFromFile(int fd, size_t length);
+ // The pointer to the beginning of the buffer. Only valid until the
+ // next Put() or PutFromFile().
+ virtual char *Get() { return &buf[offset]; }
+ // Remove first n bytes from the buffer.
+ void Pop(size_t n);
+ // Returns the current length of the buffer
+ virtual size_t Length() { return len; }
+};
+
+#endif // __WEBVIDEO_BUFFER_H
diff --git a/src/vdr-plugin/common.c b/src/vdr-plugin/common.c
new file mode 100644
index 0000000..0731da9
--- /dev/null
+++ b/src/vdr-plugin/common.c
@@ -0,0 +1,182 @@
+/*
+ * common.c: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <vdr/tools.h>
+#include "common.h"
+
+char *extensionFromUrl(const char *url) {
+ if (!url)
+ return NULL;
+
+ // Find the possible query ("?query=foo") or fragment ("#bar"). The
+ // extension is located right before them.
+ size_t extendpos = strcspn(url, "?#");
+
+ size_t extstartpos = extendpos-1;
+ while ((extstartpos > 0) && (url[extstartpos] != '.') && (url[extstartpos] != '/'))
+ extstartpos--;
+
+ if ((extstartpos > 0) && (url[extstartpos] == '.')) {
+ // We found the extension. Copy it to a buffer, and return it.
+ char *ext = (char *)malloc(sizeof(char)*(extendpos-extstartpos+1));
+ memcpy(ext, &url[extstartpos], extendpos-extstartpos);
+ ext[extendpos-extstartpos] = '\0';
+
+ return ext;
+ }
+
+ return NULL;
+}
+
+char *validateFileName(const char *filename) {
+ if (!filename)
+ return NULL;
+
+ char *validated = (char *)malloc(strlen(filename)+1);
+ int j=0;
+ for (unsigned int i=0; i<strlen(filename); i++) {
+ if (filename[i] != '/') {
+ validated[j++] = filename[i];
+ }
+ }
+ validated[j] = '\0';
+ return validated;
+}
+
+int moveFile(const char *oldpath, const char *newpath) {
+ if (rename(oldpath, newpath) == 0) {
+ return 0;
+ } else if (errno == EXDEV) {
+ // rename can't move a file between file systems. We have to copy
+ // the file manually.
+ int fdout = open(newpath, O_WRONLY | O_CREAT | O_EXCL, DEFFILEMODE);
+ if (fdout < 0) {
+ return -1;
+ }
+
+ int fdin = open(oldpath, O_RDONLY);
+ if (fdin < 0) {
+ close(fdout);
+ return -1;
+ }
+
+ const int bufsize = 4096;
+ char buffer[bufsize];
+ bool ok = true;
+ while (true) {
+ ssize_t len = safe_read(fdin, &buffer, bufsize);
+ if (len == 0) {
+ break;
+ } else if (len < 0) {
+ ok = false;
+ break;
+ }
+
+ if (safe_write(fdout, &buffer, len) != len) {
+ ok = false;
+ break;
+ }
+ }
+
+ close(fdin);
+ close(fdout);
+
+ if (ok && (unlink(oldpath) <0)) {
+ return -1;
+ }
+
+ return 0;
+ } else {
+ return -1;
+ }
+}
+
+char *URLencode(const char *s) {
+ char reserved_and_unsafe[] =
+ { // reserved characters
+ '$', '&', '+', ',', '/', ':', ';', '=', '?', '@',
+ // unsafe characters
+ ' ', '"', '<', '>', '#', '%', '{', '}',
+ '|', '\\', '^', '~', '[', ']', '`',
+ '\0'
+ };
+
+ char *buf = (char *)malloc((3*strlen(s)+1)*sizeof(char));
+ if (!buf)
+ return NULL;
+
+ unsigned char *out;
+ const unsigned char *in;
+ for (out=(unsigned char *)buf, in=(const unsigned char *)s; *in != '\0'; in++) {
+ if ((*in < 32) // control chracters
+ || (strchr(reserved_and_unsafe, *in)) // reserved and unsafe
+ || (*in > 127)) // non-ASCII
+ {
+ snprintf((char *)out, 4, "%%%02hhX", *in);
+ out += 3;
+ } else {
+ *out = *in;
+ out++;
+ }
+ }
+ *out = '\0';
+
+ return buf;
+}
+
+char *URLdecode(const char *s) {
+ char *res = (char *)malloc(strlen(s)+1);
+ const char *in = s;
+ char *out = res;
+ const char *hex = "0123456789ABCDEF";
+ const char *h1, *h2;
+
+ while (*in) {
+ if ((*in == '%') && (in[1] != '\0') && (in[2] != '\0')) {
+ h1 = strchr(hex, toupper(in[1]));
+ h2 = strchr(hex, toupper(in[2]));
+ if (h1 && h2) {
+ *out = ((h1-hex) << 4) + (h2-hex);
+ in += 3;
+ } else {
+ *out = *in;
+ in++;
+ }
+ } else {
+ *out = *in;
+ in++;
+ }
+ out++;
+ }
+ *out = '\0';
+
+ return res;
+}
+
+char *safeFilename(char *filename) {
+ if (filename) {
+ strreplace(filename, '/', '!');
+
+ char *p = filename;
+ while ((*p == '.') || isspace(*p)) {
+ p++;
+ }
+
+ if (p != filename) {
+ memmove(filename, p, strlen(p)+1);
+ }
+ }
+
+ return filename;
+}
diff --git a/src/vdr-plugin/common.h b/src/vdr-plugin/common.h
new file mode 100644
index 0000000..5b4385f
--- /dev/null
+++ b/src/vdr-plugin/common.h
@@ -0,0 +1,42 @@
+/*
+ * common.h: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef __WEBVIDEO_COMMON_H
+#define __WEBVIDEO_COMMON_H
+
+#ifdef DEBUG
+#define debug(x...) dsyslog("Webvideo: " x);
+#define info(x...) isyslog("Webvideo: " x);
+#define warning(x...) esyslog("Webvideo: Warning: " x);
+#define error(x...) esyslog("Webvideo: " x);
+#else
+#define debug(x...) ;
+#define info(x...) isyslog("Webvideo: " x);
+#define warning(x...) esyslog("Webvideo: Warning: " x);
+#define error(x...) esyslog("Webvideo: " x);
+#endif
+
+// Return the extension of the url or NULL, if the url has no
+// extension. The caller must free the returned string.
+char *extensionFromUrl(const char *url);
+// Returns a "safe" version of filename. Currently just removes / from
+// the name. The caller must free the returned string.
+char *validateFileName(const char *filename);
+int moveFile(const char *oldpath, const char *newpath);
+// Return the URL encoded version of s. The called must free the
+// returned memory.
+char *URLencode(const char *s);
+// Remove URL encoding from s. The called must free the returned
+// memory.
+char *URLdecode(const char *s);
+// Return a "safe" version of filename. Remove path (replace '/' with
+// '!') and dots from the beginning. The string is modified in-place,
+// i.e. returns the pointer filename that was passed as argument.
+char *safeFilename(char *filename);
+
+#endif // __WEBVIDEO_COMMON_H
diff --git a/src/vdr-plugin/config.c b/src/vdr-plugin/config.c
new file mode 100644
index 0000000..f294e60
--- /dev/null
+++ b/src/vdr-plugin/config.c
@@ -0,0 +1,199 @@
+/*
+ * config.c: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include "config.h"
+#include "dictionary.h"
+#include "iniparser.h"
+#include "common.h"
+
+// --- cDownloadQuality ---------------------------------------------------
+
+cDownloadQuality::cDownloadQuality(const char *sitename)
+: min(NULL), max(NULL) {
+ site = sitename ? strdup(sitename) : NULL;
+}
+
+cDownloadQuality::~cDownloadQuality() {
+ if (site)
+ free(site);
+ if (min)
+ free(min);
+ if (max)
+ free(max);
+}
+
+void cDownloadQuality::SetMin(const char *val) {
+ if (min)
+ free(min);
+
+ min = val ? strdup(val) : NULL;
+}
+
+void cDownloadQuality::SetMax(const char *val) {
+ if (max)
+ free(max);
+
+ max = val ? strdup(val) : NULL;
+}
+
+const char *cDownloadQuality::GetSite() {
+ return site;
+}
+
+const char *cDownloadQuality::GetMin() {
+ return min;
+}
+
+const char *cDownloadQuality::GetMax() {
+ return max;
+}
+
+// --- cWebvideoConfig -----------------------------------------------------
+
+cWebvideoConfig *webvideoConfig = new cWebvideoConfig();
+
+cWebvideoConfig::cWebvideoConfig() {
+ downloadPath = NULL;
+ templatePath = NULL;
+ preferXine = true;
+}
+
+cWebvideoConfig::~cWebvideoConfig() {
+ if (downloadPath)
+ free(downloadPath);
+ if (templatePath)
+ free(templatePath);
+}
+
+void cWebvideoConfig::SetDownloadPath(const char *path) {
+ if (downloadPath)
+ free(downloadPath);
+ downloadPath = path ? strdup(path) : NULL;
+}
+
+const char *cWebvideoConfig::GetDownloadPath() {
+ return downloadPath;
+}
+
+void cWebvideoConfig::SetTemplatePath(const char *path) {
+ if (templatePath)
+ free(templatePath);
+ templatePath = path ? strdup(path) : NULL;
+}
+
+const char *cWebvideoConfig::GetTemplatePath() {
+ return templatePath;
+}
+
+void cWebvideoConfig::SetPreferXineliboutput(bool pref) {
+ preferXine = pref;
+}
+
+bool cWebvideoConfig::GetPreferXineliboutput() {
+ return preferXine;
+}
+
+bool cWebvideoConfig::ReadConfigFile(const char *inifile) {
+ dictionary *conf = iniparser_load(inifile);
+
+ if (!conf)
+ return false;
+
+ info("loading config file %s", inifile);
+
+ const char *templatepath = iniparser_getstring(conf, "webvi:templatepath", NULL);
+ if (templatepath) {
+ debug("templatepath = %s (from %s)", templatepath, inifile);
+ SetTemplatePath(templatepath);
+ }
+
+ for (int i=0; i<iniparser_getnsec(conf); i++) {
+ const char *section = iniparser_getsecname(conf, i);
+
+ if (strncmp(section, "site-", 5) == 0) {
+ const char *sitename = section+5;
+ const int maxsectionlen = 40;
+ char key[64];
+ char *keyname;
+
+ strncpy(key, section, maxsectionlen);
+ key[maxsectionlen] = '\0';
+ strcat(key, ":");
+ keyname = key+strlen(key);
+
+ strcpy(keyname, "download-min-quality");
+ const char *download_min = iniparser_getstring(conf, key, NULL);
+
+ strcpy(keyname, "download-max-quality");
+ const char *download_max = iniparser_getstring(conf, key, NULL);
+
+ strcpy(keyname, "stream-min-quality");
+ const char *stream_min = iniparser_getstring(conf, key, NULL);
+
+ strcpy(keyname, "stream-max-quality");
+ const char *stream_max = iniparser_getstring(conf, key, NULL);
+
+ if (download_min || download_max) {
+ cDownloadQuality *limits = new cDownloadQuality(sitename);
+ limits->SetMin(download_min);
+ limits->SetMax(download_max);
+ downloadLimits.Add(limits);
+
+ debug("download priorities for %s (from %s): min = %s, max = %s",
+ sitename, inifile, download_min, download_max);
+ }
+
+ if (stream_min || stream_max) {
+ cDownloadQuality *limits = new cDownloadQuality(sitename);
+ limits->SetMin(stream_min);
+ limits->SetMax(stream_max);
+ streamLimits.Add(limits);
+
+ debug("streaming priorities for %s (from %s): min = %s, max = %s",
+ sitename, inifile, stream_min, stream_max);
+ }
+ }
+ }
+
+ iniparser_freedict(conf);
+
+ return true;
+}
+
+const char *cWebvideoConfig::GetQuality(const char *site, eRequestType type, int limit) {
+ if (type != REQT_FILE && type != REQT_STREAM)
+ return NULL;
+
+ cList<cDownloadQuality>& priorlist = downloadLimits;
+ if (type == REQT_STREAM)
+ priorlist = streamLimits;
+
+ cDownloadQuality *node = priorlist.First();
+
+ while (node && (strcmp(site, node->GetSite()) != 0)) {
+ node = priorlist.Next(node);
+ }
+
+ if (!node)
+ return NULL;
+
+ if (limit == 0)
+ return node->GetMin();
+ else
+ return node->GetMax();
+}
+
+const char *cWebvideoConfig::GetMinQuality(const char *site, eRequestType type) {
+ return GetQuality(site, type, 0);
+}
+
+const char *cWebvideoConfig::GetMaxQuality(const char *site, eRequestType type) {
+ return GetQuality(site, type, 1);
+}
diff --git a/src/vdr-plugin/config.h b/src/vdr-plugin/config.h
new file mode 100644
index 0000000..29304b4
--- /dev/null
+++ b/src/vdr-plugin/config.h
@@ -0,0 +1,64 @@
+/*
+ * config.h: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef __WEBVIDEO_CONFIG_H
+#define __WEBVIDEO_CONFIG_H
+
+#include <vdr/tools.h>
+#include "request.h"
+
+class cDownloadQuality : public cListObject {
+private:
+ char *site;
+ char *min;
+ char *max;
+
+public:
+ cDownloadQuality(const char *site);
+ ~cDownloadQuality();
+
+ void SetMin(const char *val);
+ void SetMax(const char *val);
+
+ const char *GetSite();
+ const char *GetMin();
+ const char *GetMax();
+};
+
+class cWebvideoConfig {
+private:
+ char *downloadPath;
+ char *templatePath;
+ bool preferXine;
+ cList<cDownloadQuality> downloadLimits;
+ cList<cDownloadQuality> streamLimits;
+
+ const char *GetQuality(const char *site, eRequestType type, int limit);
+
+public:
+ cWebvideoConfig();
+ ~cWebvideoConfig();
+
+ bool ReadConfigFile(const char *inifile);
+
+ void SetDownloadPath(const char *path);
+ const char *GetDownloadPath();
+
+ void SetTemplatePath(const char *path);
+ const char *GetTemplatePath();
+
+ void SetPreferXineliboutput(bool pref);
+ bool GetPreferXineliboutput();
+
+ const char *GetMinQuality(const char *site, eRequestType type);
+ const char *GetMaxQuality(const char *site, eRequestType type);
+};
+
+extern cWebvideoConfig *webvideoConfig;
+
+#endif
diff --git a/src/vdr-plugin/dictionary.c b/src/vdr-plugin/dictionary.c
new file mode 100644
index 0000000..4c5ae08
--- /dev/null
+++ b/src/vdr-plugin/dictionary.c
@@ -0,0 +1,410 @@
+/*
+ Copyright (c) 2000-2007 by Nicolas Devillard.
+ MIT License, see COPYING for more information.
+*/
+
+/*-------------------------------------------------------------------------*/
+/**
+ @file dictionary.c
+ @author N. Devillard
+ @date Sep 2007
+ @version $Revision: 1.27 $
+ @brief Implements a dictionary for string variables.
+
+ This module implements a simple dictionary object, i.e. a list
+ of string/string associations. This object is useful to store e.g.
+ informations retrieved from a configuration file (ini files).
+*/
+/*--------------------------------------------------------------------------*/
+
+/*
+ $Id: dictionary.c,v 1.27 2007-11-23 21:39:18 ndevilla Exp $
+ $Revision: 1.27 $
+*/
+/*---------------------------------------------------------------------------
+ Includes
+ ---------------------------------------------------------------------------*/
+#include "dictionary.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+/** Maximum value size for integers and doubles. */
+#define MAXVALSZ 1024
+
+/** Minimal allocated number of entries in a dictionary */
+#define DICTMINSZ 128
+
+/** Invalid key token */
+#define DICT_INVALID_KEY ((char*)-1)
+
+/*---------------------------------------------------------------------------
+ Private functions
+ ---------------------------------------------------------------------------*/
+
+/* Doubles the allocated size associated to a pointer */
+/* 'size' is the current allocated size. */
+static void * mem_double(void * ptr, int size)
+{
+ void * newptr ;
+
+ newptr = calloc(2*size, 1);
+ if (newptr==NULL) {
+ return NULL ;
+ }
+ memcpy(newptr, ptr, size);
+ free(ptr);
+ return newptr ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Duplicate a string
+ @param s String to duplicate
+ @return Pointer to a newly allocated string, to be freed with free()
+
+ This is a replacement for strdup(). This implementation is provided
+ for systems that do not have it.
+ */
+/*--------------------------------------------------------------------------*/
+static char * xstrdup(char * s)
+{
+ char * t ;
+ if (!s)
+ return NULL ;
+ t = (char *)malloc(strlen(s)+1) ;
+ if (t) {
+ strcpy(t,s);
+ }
+ return t ;
+}
+
+/*---------------------------------------------------------------------------
+ Function codes
+ ---------------------------------------------------------------------------*/
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Compute the hash key for a string.
+ @param key Character string to use for key.
+ @return 1 unsigned int on at least 32 bits.
+
+ This hash function has been taken from an Article in Dr Dobbs Journal.
+ This is normally a collision-free function, distributing keys evenly.
+ The key is stored anyway in the struct so that collision can be avoided
+ by comparing the key itself in last resort.
+ */
+/*--------------------------------------------------------------------------*/
+unsigned dictionary_hash(char * key)
+{
+ int len ;
+ unsigned hash ;
+ int i ;
+
+ len = strlen(key);
+ for (hash=0, i=0 ; i<len ; i++) {
+ hash += (unsigned)key[i] ;
+ hash += (hash<<10);
+ hash ^= (hash>>6) ;
+ }
+ hash += (hash <<3);
+ hash ^= (hash >>11);
+ hash += (hash <<15);
+ return hash ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Create a new dictionary object.
+ @param size Optional initial size of the dictionary.
+ @return 1 newly allocated dictionary objet.
+
+ This function allocates a new dictionary object of given size and returns
+ it. If you do not know in advance (roughly) the number of entries in the
+ dictionary, give size=0.
+ */
+/*--------------------------------------------------------------------------*/
+dictionary * dictionary_new(int size)
+{
+ dictionary * d ;
+
+ /* If no size was specified, allocate space for DICTMINSZ */
+ if (size<DICTMINSZ) size=DICTMINSZ ;
+
+ if (!(d = (dictionary *)calloc(1, sizeof(dictionary)))) {
+ return NULL;
+ }
+ d->size = size ;
+ d->val = (char **)calloc(size, sizeof(char*));
+ d->key = (char **)calloc(size, sizeof(char*));
+ d->hash = (unsigned int *)calloc(size, sizeof(unsigned));
+ return d ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Delete a dictionary object
+ @param d dictionary object to deallocate.
+ @return void
+
+ Deallocate a dictionary object and all memory associated to it.
+ */
+/*--------------------------------------------------------------------------*/
+void dictionary_del(dictionary * d)
+{
+ int i ;
+
+ if (d==NULL) return ;
+ for (i=0 ; i<d->size ; i++) {
+ if (d->key[i]!=NULL)
+ free(d->key[i]);
+ if (d->val[i]!=NULL)
+ free(d->val[i]);
+ }
+ free(d->val);
+ free(d->key);
+ free(d->hash);
+ free(d);
+ return ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get a value from a dictionary.
+ @param d dictionary object to search.
+ @param key Key to look for in the dictionary.
+ @param def Default value to return if key not found.
+ @return 1 pointer to internally allocated character string.
+
+ This function locates a key in a dictionary and returns a pointer to its
+ value, or the passed 'def' pointer if no such key can be found in
+ dictionary. The returned character pointer points to data internal to the
+ dictionary object, you should not try to free it or modify it.
+ */
+/*--------------------------------------------------------------------------*/
+char * dictionary_get(dictionary * d, char * key, char * def)
+{
+ unsigned hash ;
+ int i ;
+
+ hash = dictionary_hash(key);
+ for (i=0 ; i<d->size ; i++) {
+ if (d->key[i]==NULL)
+ continue ;
+ /* Compare hash */
+ if (hash==d->hash[i]) {
+ /* Compare string, to avoid hash collisions */
+ if (!strcmp(key, d->key[i])) {
+ return d->val[i] ;
+ }
+ }
+ }
+ return def ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Set a value in a dictionary.
+ @param d dictionary object to modify.
+ @param key Key to modify or add.
+ @param val Value to add.
+ @return int 0 if Ok, anything else otherwise
+
+ If the given key is found in the dictionary, the associated value is
+ replaced by the provided one. If the key cannot be found in the
+ dictionary, it is added to it.
+
+ It is Ok to provide a NULL value for val, but NULL values for the dictionary
+ or the key are considered as errors: the function will return immediately
+ in such a case.
+
+ Notice that if you dictionary_set a variable to NULL, a call to
+ dictionary_get will return a NULL value: the variable will be found, and
+ its value (NULL) is returned. In other words, setting the variable
+ content to NULL is equivalent to deleting the variable from the
+ dictionary. It is not possible (in this implementation) to have a key in
+ the dictionary without value.
+
+ This function returns non-zero in case of failure.
+ */
+/*--------------------------------------------------------------------------*/
+int dictionary_set(dictionary * d, char * key, char * val)
+{
+ int i ;
+ unsigned hash ;
+
+ if (d==NULL || key==NULL) return -1 ;
+
+ /* Compute hash for this key */
+ hash = dictionary_hash(key) ;
+ /* Find if value is already in dictionary */
+ if (d->n>0) {
+ for (i=0 ; i<d->size ; i++) {
+ if (d->key[i]==NULL)
+ continue ;
+ if (hash==d->hash[i]) { /* Same hash value */
+ if (!strcmp(key, d->key[i])) { /* Same key */
+ /* Found a value: modify and return */
+ if (d->val[i]!=NULL)
+ free(d->val[i]);
+ d->val[i] = val ? xstrdup(val) : NULL ;
+ /* Value has been modified: return */
+ return 0 ;
+ }
+ }
+ }
+ }
+ /* Add a new value */
+ /* See if dictionary needs to grow */
+ if (d->n==d->size) {
+
+ /* Reached maximum size: reallocate dictionary */
+ d->val = (char **)mem_double(d->val, d->size * sizeof(char*)) ;
+ d->key = (char **)mem_double(d->key, d->size * sizeof(char*)) ;
+ d->hash = (unsigned int *)mem_double(d->hash, d->size * sizeof(unsigned)) ;
+ if ((d->val==NULL) || (d->key==NULL) || (d->hash==NULL)) {
+ /* Cannot grow dictionary */
+ return -1 ;
+ }
+ /* Double size */
+ d->size *= 2 ;
+ }
+
+ /* Insert key in the first empty slot */
+ for (i=0 ; i<d->size ; i++) {
+ if (d->key[i]==NULL) {
+ /* Add key here */
+ break ;
+ }
+ }
+ /* Copy key */
+ d->key[i] = xstrdup(key);
+ d->val[i] = val ? xstrdup(val) : NULL ;
+ d->hash[i] = hash;
+ d->n ++ ;
+ return 0 ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Delete a key in a dictionary
+ @param d dictionary object to modify.
+ @param key Key to remove.
+ @return void
+
+ This function deletes a key in a dictionary. Nothing is done if the
+ key cannot be found.
+ */
+/*--------------------------------------------------------------------------*/
+void dictionary_unset(dictionary * d, char * key)
+{
+ unsigned hash ;
+ int i ;
+
+ if (key == NULL) {
+ return;
+ }
+
+ hash = dictionary_hash(key);
+ for (i=0 ; i<d->size ; i++) {
+ if (d->key[i]==NULL)
+ continue ;
+ /* Compare hash */
+ if (hash==d->hash[i]) {
+ /* Compare string, to avoid hash collisions */
+ if (!strcmp(key, d->key[i])) {
+ /* Found key */
+ break ;
+ }
+ }
+ }
+ if (i>=d->size)
+ /* Key not found */
+ return ;
+
+ free(d->key[i]);
+ d->key[i] = NULL ;
+ if (d->val[i]!=NULL) {
+ free(d->val[i]);
+ d->val[i] = NULL ;
+ }
+ d->hash[i] = 0 ;
+ d->n -- ;
+ return ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Dump a dictionary to an opened file pointer.
+ @param d Dictionary to dump
+ @param f Opened file pointer.
+ @return void
+
+ Dumps a dictionary onto an opened file pointer. Key pairs are printed out
+ as @c [Key]=[Value], one per line. It is Ok to provide stdout or stderr as
+ output file pointers.
+ */
+/*--------------------------------------------------------------------------*/
+void dictionary_dump(dictionary * d, FILE * out)
+{
+ int i ;
+
+ if (d==NULL || out==NULL) return ;
+ if (d->n<1) {
+ fprintf(out, "empty dictionary\n");
+ return ;
+ }
+ for (i=0 ; i<d->size ; i++) {
+ if (d->key[i]) {
+ fprintf(out, "%20s\t[%s]\n",
+ d->key[i],
+ d->val[i] ? d->val[i] : "UNDEF");
+ }
+ }
+ return ;
+}
+
+
+/* Test code */
+#ifdef TESTDIC
+#define NVALS 20000
+int main(int argc, char *argv[])
+{
+ dictionary * d ;
+ char * val ;
+ int i ;
+ char cval[90] ;
+
+ /* Allocate dictionary */
+ printf("allocating...\n");
+ d = dictionary_new(0);
+
+ /* Set values in dictionary */
+ printf("setting %d values...\n", NVALS);
+ for (i=0 ; i<NVALS ; i++) {
+ sprintf(cval, "%04d", i);
+ dictionary_set(d, cval, "salut");
+ }
+ printf("getting %d values...\n", NVALS);
+ for (i=0 ; i<NVALS ; i++) {
+ sprintf(cval, "%04d", i);
+ val = dictionary_get(d, cval, DICT_INVALID_KEY);
+ if (val==DICT_INVALID_KEY) {
+ printf("cannot get value for key [%s]\n", cval);
+ }
+ }
+ printf("unsetting %d values...\n", NVALS);
+ for (i=0 ; i<NVALS ; i++) {
+ sprintf(cval, "%04d", i);
+ dictionary_unset(d, cval);
+ }
+ if (d->n != 0) {
+ printf("error deleting values\n");
+ }
+ printf("deallocating...\n");
+ dictionary_del(d);
+ return 0 ;
+}
+#endif
+/* vim: set ts=4 et sw=4 tw=75 */
diff --git a/src/vdr-plugin/dictionary.h b/src/vdr-plugin/dictionary.h
new file mode 100644
index 0000000..f39493e
--- /dev/null
+++ b/src/vdr-plugin/dictionary.h
@@ -0,0 +1,178 @@
+/*
+ Copyright (c) 2000-2007 by Nicolas Devillard.
+ MIT License, see COPYING for more information.
+*/
+
+/*-------------------------------------------------------------------------*/
+/**
+ @file dictionary.h
+ @author N. Devillard
+ @date Sep 2007
+ @version $Revision: 1.12 $
+ @brief Implements a dictionary for string variables.
+
+ This module implements a simple dictionary object, i.e. a list
+ of string/string associations. This object is useful to store e.g.
+ informations retrieved from a configuration file (ini files).
+*/
+/*--------------------------------------------------------------------------*/
+
+/*
+ $Id: dictionary.h,v 1.12 2007-11-23 21:37:00 ndevilla Exp $
+ $Author: ndevilla $
+ $Date: 2007-11-23 21:37:00 $
+ $Revision: 1.12 $
+*/
+
+#ifndef _DICTIONARY_H_
+#define _DICTIONARY_H_
+
+/*---------------------------------------------------------------------------
+ Includes
+ ---------------------------------------------------------------------------*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+/*---------------------------------------------------------------------------
+ New types
+ ---------------------------------------------------------------------------*/
+
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Dictionary object
+
+ This object contains a list of string/string associations. Each
+ association is identified by a unique string key. Looking up values
+ in the dictionary is speeded up by the use of a (hopefully collision-free)
+ hash function.
+ */
+/*-------------------------------------------------------------------------*/
+typedef struct _dictionary_ {
+ int n ; /** Number of entries in dictionary */
+ int size ; /** Storage size */
+ char ** val ; /** List of string values */
+ char ** key ; /** List of string keys */
+ unsigned * hash ; /** List of hash values for keys */
+} dictionary ;
+
+
+/*---------------------------------------------------------------------------
+ Function prototypes
+ ---------------------------------------------------------------------------*/
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Compute the hash key for a string.
+ @param key Character string to use for key.
+ @return 1 unsigned int on at least 32 bits.
+
+ This hash function has been taken from an Article in Dr Dobbs Journal.
+ This is normally a collision-free function, distributing keys evenly.
+ The key is stored anyway in the struct so that collision can be avoided
+ by comparing the key itself in last resort.
+ */
+/*--------------------------------------------------------------------------*/
+unsigned dictionary_hash(char * key);
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Create a new dictionary object.
+ @param size Optional initial size of the dictionary.
+ @return 1 newly allocated dictionary objet.
+
+ This function allocates a new dictionary object of given size and returns
+ it. If you do not know in advance (roughly) the number of entries in the
+ dictionary, give size=0.
+ */
+/*--------------------------------------------------------------------------*/
+dictionary * dictionary_new(int size);
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Delete a dictionary object
+ @param d dictionary object to deallocate.
+ @return void
+
+ Deallocate a dictionary object and all memory associated to it.
+ */
+/*--------------------------------------------------------------------------*/
+void dictionary_del(dictionary * vd);
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get a value from a dictionary.
+ @param d dictionary object to search.
+ @param key Key to look for in the dictionary.
+ @param def Default value to return if key not found.
+ @return 1 pointer to internally allocated character string.
+
+ This function locates a key in a dictionary and returns a pointer to its
+ value, or the passed 'def' pointer if no such key can be found in
+ dictionary. The returned character pointer points to data internal to the
+ dictionary object, you should not try to free it or modify it.
+ */
+/*--------------------------------------------------------------------------*/
+char * dictionary_get(dictionary * d, char * key, char * def);
+
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Set a value in a dictionary.
+ @param d dictionary object to modify.
+ @param key Key to modify or add.
+ @param val Value to add.
+ @return int 0 if Ok, anything else otherwise
+
+ If the given key is found in the dictionary, the associated value is
+ replaced by the provided one. If the key cannot be found in the
+ dictionary, it is added to it.
+
+ It is Ok to provide a NULL value for val, but NULL values for the dictionary
+ or the key are considered as errors: the function will return immediately
+ in such a case.
+
+ Notice that if you dictionary_set a variable to NULL, a call to
+ dictionary_get will return a NULL value: the variable will be found, and
+ its value (NULL) is returned. In other words, setting the variable
+ content to NULL is equivalent to deleting the variable from the
+ dictionary. It is not possible (in this implementation) to have a key in
+ the dictionary without value.
+
+ This function returns non-zero in case of failure.
+ */
+/*--------------------------------------------------------------------------*/
+int dictionary_set(dictionary * vd, char * key, char * val);
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Delete a key in a dictionary
+ @param d dictionary object to modify.
+ @param key Key to remove.
+ @return void
+
+ This function deletes a key in a dictionary. Nothing is done if the
+ key cannot be found.
+ */
+/*--------------------------------------------------------------------------*/
+void dictionary_unset(dictionary * d, char * key);
+
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Dump a dictionary to an opened file pointer.
+ @param d Dictionary to dump
+ @param f Opened file pointer.
+ @return void
+
+ Dumps a dictionary onto an opened file pointer. Key pairs are printed out
+ as @c [Key]=[Value], one per line. It is Ok to provide stdout or stderr as
+ output file pointers.
+ */
+/*--------------------------------------------------------------------------*/
+void dictionary_dump(dictionary * d, FILE * out);
+
+#endif
diff --git a/src/vdr-plugin/download.c b/src/vdr-plugin/download.c
new file mode 100644
index 0000000..f9d956f
--- /dev/null
+++ b/src/vdr-plugin/download.c
@@ -0,0 +1,222 @@
+/*
+ * download.c: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <errno.h>
+#include <sys/select.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <vdr/tools.h>
+#include "download.h"
+#include "common.h"
+
+// --- cWebviThread --------------------------------------------------------
+
+cWebviThread::cWebviThread() {
+ int pipefd[2];
+
+ if (pipe(pipefd) == -1)
+ LOG_ERROR_STR("new request pipe");
+ newreqread = pipefd[0];
+ newreqwrite = pipefd[1];
+ //fcntl(newreqread, F_SETFL, O_NONBLOCK);
+ //fcntl(newreqwrite, F_SETFL, O_NONBLOCK);
+
+ webvi = webvi_initialize_context();
+}
+
+cWebviThread::~cWebviThread() {
+ int numactive = activeRequestList.Size();
+ for (int i=0; i<activeRequestList.Size(); i++)
+ delete activeRequestList[i];
+ activeRequestList.Clear();
+
+ for (int i=0; i<finishedRequestList.Size(); i++) {
+ delete finishedRequestList[i];
+ }
+ finishedRequestList.Clear();
+
+ webvi_cleanup_context(webvi);
+
+ if (numactive > 0) {
+ esyslog("%d requests failed to complete", numactive);
+ }
+}
+
+cWebviThread &cWebviThread::Instance() {
+ static cWebviThread instance;
+
+ return instance;
+}
+
+void cWebviThread::SetTemplatePath(const char *path) {
+ if (webvi != 0 && path)
+ webvi_set_config(webvi, WEBVI_CONFIG_TEMPLATE_PATH, path);
+}
+
+void cWebviThread::MoveToFinishedList(cMenuRequest *req) {
+ // Move the request from the activeList to finishedList.
+ requestMutex.Lock();
+ for (int i=0; i<activeRequestList.Size(); i++) {
+ if (activeRequestList[i] == req) {
+ activeRequestList.Remove(i);
+ break;
+ }
+ }
+ finishedRequestList.Append(req);
+
+ requestMutex.Unlock();
+}
+
+void cWebviThread::ActivateNewRequest() {
+ // Move requests from newRequestList to activeRequestList and start
+ // them.
+ requestMutex.Lock();
+ for (int i=0; i<newRequestList.Size(); i++) {
+ cMenuRequest *req = newRequestList[i];
+ if (req->IsAborted()) {
+ // The request has been aborted even before we got a chance to
+ // start it.
+ MoveToFinishedList(req);
+ } else {
+ debug("starting request %d", req->GetID());
+
+ if (!req->Start(webvi)) {
+ error("Request failed to start");
+ req->RequestDone(-1, "Request failed to start");
+ MoveToFinishedList(req);
+ } else {
+ activeRequestList.Append(req);
+ }
+ }
+ }
+
+ newRequestList.Clear();
+ requestMutex.Unlock();
+}
+
+void cWebviThread::StopFinishedRequests() {
+ // Check if some requests have finished, and move them to
+ // finishedRequestList.
+ int msg_remaining;
+ WebviMsg *donemsg;
+ cMenuRequest *req;
+
+ do {
+ donemsg = webvi_get_message(webvi, &msg_remaining);
+
+ if (donemsg && donemsg->msg == WEBVIMSG_DONE) {
+ requestMutex.Lock();
+ req = activeRequestList.FindByHandle(donemsg->handle);
+ if (req) {
+ debug("Finished request %d", req->GetID());
+ req->RequestDone(donemsg->status_code, donemsg->data);
+ MoveToFinishedList(req);
+ }
+ requestMutex.Unlock();
+ }
+ } while (msg_remaining > 0);
+}
+
+void cWebviThread::Stop() {
+ // The thread may be sleeping, wake it up first.
+ TEMP_FAILURE_RETRY(write(newreqwrite, "S", 1));
+ Cancel(5);
+}
+
+void cWebviThread::Action(void) {
+ fd_set readfds, writefds, excfds;
+ int maxfd;
+ struct timeval timeout;
+ long running_handles;
+ bool check_done = false;
+
+ if (webvi == 0) {
+ error("Failed to get libwebvi context");
+ return;
+ }
+
+ while (Running()) {
+ FD_ZERO(&readfds);
+ FD_ZERO(&writefds);
+ FD_ZERO(&excfds);
+ webvi_fdset(webvi, &readfds, &writefds, &excfds, &maxfd);
+ FD_SET(newreqread, &readfds);
+ if (newreqread > maxfd)
+ maxfd = newreqread;
+
+ timeout.tv_sec = 5;
+ timeout.tv_usec = 0;
+
+ int s = TEMP_FAILURE_RETRY(select(maxfd+1, &readfds, &writefds, NULL,
+ &timeout));
+ if (s == -1) {
+ // select error
+ LOG_ERROR_STR("select() error in webvideo downloader thread:");
+ Cancel(-1);
+
+ } else if (s == 0) {
+ // timeout
+ webvi_perform(webvi, 0, WEBVI_SELECT_TIMEOUT, &running_handles);
+ check_done = true;
+
+ } else {
+ for (int fd=0; fd<=maxfd; fd++) {
+ if (FD_ISSET(fd, &readfds)) {
+ if (fd == newreqread) {
+ char tmpbuf[8];
+ int n = read(fd, tmpbuf, 8);
+ if (n > 0 && memchr(tmpbuf, 'S', n))
+ Cancel(-1);
+ ActivateNewRequest();
+ } else {
+ webvi_perform(webvi, fd, WEBVI_SELECT_READ, &running_handles);
+ check_done = true;
+ }
+ }
+ if (FD_ISSET(fd, &writefds))
+ webvi_perform(webvi, fd, WEBVI_SELECT_WRITE, &running_handles);
+ if (FD_ISSET(fd, &excfds))
+ webvi_perform(webvi, fd, WEBVI_SELECT_EXCEPTION, &running_handles);
+ }
+ }
+
+ if (check_done) {
+ StopFinishedRequests();
+ check_done = false;
+ }
+ }
+}
+
+void cWebviThread::AddRequest(cMenuRequest *req) {
+ requestMutex.Lock();
+ newRequestList.Append(req);
+ requestMutex.Unlock();
+
+ int s = TEMP_FAILURE_RETRY(write(newreqwrite, "*", 1));
+ if (s == -1)
+ LOG_ERROR_STR("Failed to signal new webvideo request");
+}
+
+cMenuRequest *cWebviThread::GetFinishedRequest() {
+ cMenuRequest *res = NULL;
+ requestMutex.Lock();
+ if (finishedRequestList.Size() > 0) {
+ res = finishedRequestList[finishedRequestList.Size()-1];
+ finishedRequestList.Remove(finishedRequestList.Size()-1);
+ }
+ requestMutex.Unlock();
+
+ return res;
+}
+
+int cWebviThread::GetUnfinishedCount() {
+ if (!Running())
+ return 0;
+ else
+ return activeRequestList.Size();
+}
diff --git a/src/vdr-plugin/download.h b/src/vdr-plugin/download.h
new file mode 100644
index 0000000..5f29150
--- /dev/null
+++ b/src/vdr-plugin/download.h
@@ -0,0 +1,59 @@
+/*
+ * download.h: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef __WEBVIDEO_DOWNLOAD_H
+#define __WEBVIDEO_DOWNLOAD_H
+
+#include <vdr/thread.h>
+#include <libwebvi.h>
+#include "request.h"
+
+// --- cWebviThread --------------------------------------------------------
+
+class cWebviThread : public cThread {
+private:
+ WebviCtx webvi;
+ cMutex requestMutex;
+ cRequestVector activeRequestList;
+ cRequestVector newRequestList;
+ cRequestVector finishedRequestList;
+ int newreqread, newreqwrite;
+
+ void MoveToFinishedList(cMenuRequest *req);
+ void ActivateNewRequest();
+ void StopFinishedRequests();
+
+protected:
+ void Action(void);
+
+public:
+ cWebviThread();
+ ~cWebviThread();
+
+ static cWebviThread &Instance();
+
+ // Stop the thread
+ void Stop();
+ // Set path to the site templates. Should be set before
+ // Start()ing the thread.
+ void SetTemplatePath(const char *path);
+ // Start executing req. The control of req is handed over to the
+ // downloader thread. The main thread should not access req until
+ // the request is handed back to the main thread by
+ // GetFinishedRequest().
+ void AddRequest(cMenuRequest *req);
+ // Return a request that has finished or NULL if no requests are
+ // finished. The ownership of the returned cMenuRequest object
+ // is again assigned to the main thread. The main thread should poll
+ // this function periodically.
+ cMenuRequest *GetFinishedRequest();
+ // Returns the number download requests currectly active
+ int GetUnfinishedCount();
+};
+
+#endif
diff --git a/src/vdr-plugin/history.c b/src/vdr-plugin/history.c
new file mode 100644
index 0000000..a463bac
--- /dev/null
+++ b/src/vdr-plugin/history.c
@@ -0,0 +1,145 @@
+/*
+ * history.c: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <string.h>
+#include "history.h"
+#include "menu.h"
+
+// --- cHistoryObject -----------------------------------------------------
+
+cHistoryObject::cHistoryObject(const char *xml, const char *ref, int ID) {
+ osdxml = strdup(xml);
+ reference = strdup(ref);
+ id = ID;
+ selected = 0;
+}
+
+cHistoryObject::~cHistoryObject() {
+ if (osdxml)
+ free(osdxml);
+ if (reference)
+ free(reference);
+
+ for (int i=0; i < editData.Size(); i++)
+ delete editData[i];
+}
+
+cQueryData *cHistoryObject::GetEditItem(const char *controlName) {
+ for (int i=0; i < editData.Size(); i++) {
+ if (strcmp(editData[i]->GetName(), controlName) == 0) {
+ return editData[i];
+ }
+ }
+
+ return NULL;
+}
+
+int cHistoryObject::QuerySize() const {
+ return editData.Size();
+}
+
+char *cHistoryObject::GetQueryFragment(int i) const {
+ if (i < 0 && i >= editData.Size())
+ return NULL;
+ else
+ return editData[i]->GetQueryFragment();
+}
+
+cTextFieldData *cHistoryObject::GetTextFieldData(const char *controlName) {
+ cQueryData *edititem = GetEditItem(controlName);
+ cTextFieldData *tfdata = dynamic_cast<cTextFieldData *>(edititem);
+
+ if (!tfdata) {
+ tfdata = new cTextFieldData(controlName, 256);
+ editData.Append(tfdata);
+ }
+
+ return tfdata;
+}
+
+cItemListData *cHistoryObject::GetItemListData(const char *controlName,
+ cStringList &items,
+ cStringList &values) {
+ int n;
+ char **itemtable, **itemvaluetable;
+ cQueryData *edititem = GetEditItem(controlName);
+ cItemListData *ildata = dynamic_cast<cItemListData *>(edititem);
+
+ if (!ildata) {
+ n = min(items.Size(), values.Size());
+ itemtable = (char **)malloc(n*sizeof(char *));
+ itemvaluetable = (char **)malloc(n*sizeof(char *));
+
+ for (int i=0; i<n; i++) {
+ itemtable[i] = strdup(csc.Convert(items[i]));
+ itemvaluetable[i] = strdup(values[i]);
+ }
+
+ ildata = new cItemListData(controlName,
+ itemtable,
+ itemvaluetable,
+ n);
+
+ editData.Append(ildata);
+ }
+
+ return ildata;
+}
+
+// --- cHistory ------------------------------------------------------------
+
+cHistory::cHistory() {
+ current = NULL;
+}
+
+void cHistory::Clear() {
+ current = NULL;
+ cList<cHistoryObject>::Clear();
+}
+
+void cHistory::TruncateAndAdd(cHistoryObject *page) {
+ cHistoryObject *last = Last();
+ while ((last) && (last != current)) {
+ Del(last);
+ last = Last();
+ }
+
+ Add(page);
+ current = Last();
+}
+
+void cHistory::Reset() {
+ current = NULL;
+}
+
+cHistoryObject *cHistory::Current() {
+ return current;
+}
+
+cHistoryObject *cHistory::Home() {
+ current = First();
+ return current;
+}
+
+cHistoryObject *cHistory::Back() {
+ if (current)
+ current = Prev(current);
+ return current;
+}
+
+cHistoryObject *cHistory::Forward() {
+ cHistoryObject *next;
+ if (current) {
+ next = Next(current);
+ if (next)
+ current = next;
+ } else {
+ current = First();
+ }
+ return current;
+}
diff --git a/src/vdr-plugin/history.h b/src/vdr-plugin/history.h
new file mode 100644
index 0000000..fd5fcf9
--- /dev/null
+++ b/src/vdr-plugin/history.h
@@ -0,0 +1,62 @@
+/*
+ * history.h: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef __WEBVIDEO_HISTORY_H
+#define __WEBVIDEO_HISTORY_H
+
+#include <vdr/tools.h>
+#include "menudata.h"
+
+// --- cHistoryObject -----------------------------------------------------
+
+class cHistoryObject : public cListObject {
+private:
+ char *osdxml;
+ int id;
+ int selected;
+ cVector<cQueryData *> editData;
+ char *reference;
+
+ cQueryData *GetEditItem(const char *controlName);
+
+public:
+ cHistoryObject(const char *xml, const char *reference, int ID);
+ ~cHistoryObject();
+
+ int GetID() const { return id; }
+ const char *GetOSD() const { return osdxml; }
+ const char *GetReference() const { return reference; }
+ void RememberSelected(int sel) { selected = sel; }
+ int GetSelected() const { return selected; }
+
+ int QuerySize() const;
+ char *GetQueryFragment(int i) const;
+ cTextFieldData *GetTextFieldData(const char *controlName);
+ cItemListData *GetItemListData(const char *controlName,
+ cStringList &items,
+ cStringList &itemvalues);
+};
+
+// --- cHistory ------------------------------------------------------------
+
+class cHistory : public cList<cHistoryObject> {
+private:
+ cHistoryObject *current;
+public:
+ cHistory();
+
+ void Clear();
+ void TruncateAndAdd(cHistoryObject *page);
+ void Reset();
+ cHistoryObject *Current();
+ cHistoryObject *Home();
+ cHistoryObject *Back();
+ cHistoryObject *Forward();
+};
+
+#endif // __WEBVIDEO_HISTORY_H
diff --git a/src/vdr-plugin/iniparser.c b/src/vdr-plugin/iniparser.c
new file mode 100644
index 0000000..3990e74
--- /dev/null
+++ b/src/vdr-plugin/iniparser.c
@@ -0,0 +1,650 @@
+/*
+ Copyright (c) 2000-2007 by Nicolas Devillard.
+ MIT License, see COPYING for more information.
+*/
+
+/*-------------------------------------------------------------------------*/
+/**
+ @file iniparser.c
+ @author N. Devillard
+ @date Sep 2007
+ @version 3.0
+ @brief Parser for ini files.
+*/
+/*--------------------------------------------------------------------------*/
+/*
+ $Id: iniparser.c,v 2.18 2008-01-03 18:35:39 ndevilla Exp $
+ $Revision: 2.18 $
+ $Date: 2008-01-03 18:35:39 $
+*/
+/*---------------------------- Includes ------------------------------------*/
+#include <ctype.h>
+#include "iniparser.h"
+
+/*---------------------------- Defines -------------------------------------*/
+#define ASCIILINESZ (1024)
+#define INI_INVALID_KEY ((char*)-1)
+
+/*---------------------------------------------------------------------------
+ Private to this module
+ ---------------------------------------------------------------------------*/
+/**
+ * This enum stores the status for each parsed line (internal use only).
+ */
+typedef enum _line_status_ {
+ LINE_UNPROCESSED,
+ LINE_ERROR,
+ LINE_EMPTY,
+ LINE_COMMENT,
+ LINE_SECTION,
+ LINE_VALUE
+} line_status ;
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Convert a string to lowercase.
+ @param s String to convert.
+ @return ptr to statically allocated string.
+
+ This function returns a pointer to a statically allocated string
+ containing a lowercased version of the input string. Do not free
+ or modify the returned string! Since the returned string is statically
+ allocated, it will be modified at each function call (not re-entrant).
+ */
+/*--------------------------------------------------------------------------*/
+static char * strlwc(const char * s)
+{
+ static char l[ASCIILINESZ+1];
+ int i ;
+
+ if (s==NULL) return NULL ;
+ memset(l, 0, ASCIILINESZ+1);
+ i=0 ;
+ while (s[i] && i<ASCIILINESZ) {
+ l[i] = (char)tolower((int)s[i]);
+ i++ ;
+ }
+ l[ASCIILINESZ]=(char)0;
+ return l ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Remove blanks at the beginning and the end of a string.
+ @param s String to parse.
+ @return ptr to statically allocated string.
+
+ This function returns a pointer to a statically allocated string,
+ which is identical to the input string, except that all blank
+ characters at the end and the beg. of the string have been removed.
+ Do not free or modify the returned string! Since the returned string
+ is statically allocated, it will be modified at each function call
+ (not re-entrant).
+ */
+/*--------------------------------------------------------------------------*/
+static char * strstrip(char * s)
+{
+ static char l[ASCIILINESZ+1];
+ char * last ;
+
+ if (s==NULL) return NULL ;
+
+ while (isspace((int)*s) && *s) s++;
+ memset(l, 0, ASCIILINESZ+1);
+ strcpy(l, s);
+ last = l + strlen(l);
+ while (last > l) {
+ if (!isspace((int)*(last-1)))
+ break ;
+ last -- ;
+ }
+ *last = (char)0;
+ return (char*)l ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get number of sections in a dictionary
+ @param d Dictionary to examine
+ @return int Number of sections found in dictionary
+
+ This function returns the number of sections found in a dictionary.
+ The test to recognize sections is done on the string stored in the
+ dictionary: a section name is given as "section" whereas a key is
+ stored as "section:key", thus the test looks for entries that do not
+ contain a colon.
+
+ This clearly fails in the case a section name contains a colon, but
+ this should simply be avoided.
+
+ This function returns -1 in case of error.
+ */
+/*--------------------------------------------------------------------------*/
+int iniparser_getnsec(dictionary * d)
+{
+ int i ;
+ int nsec ;
+
+ if (d==NULL) return -1 ;
+ nsec=0 ;
+ for (i=0 ; i<d->size ; i++) {
+ if (d->key[i]==NULL)
+ continue ;
+ if (strchr(d->key[i], ':')==NULL) {
+ nsec ++ ;
+ }
+ }
+ return nsec ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get name for section n in a dictionary.
+ @param d Dictionary to examine
+ @param n Section number (from 0 to nsec-1).
+ @return Pointer to char string
+
+ This function locates the n-th section in a dictionary and returns
+ its name as a pointer to a string statically allocated inside the
+ dictionary. Do not free or modify the returned string!
+
+ This function returns NULL in case of error.
+ */
+/*--------------------------------------------------------------------------*/
+char * iniparser_getsecname(dictionary * d, int n)
+{
+ int i ;
+ int foundsec ;
+
+ if (d==NULL || n<0) return NULL ;
+ foundsec=0 ;
+ for (i=0 ; i<d->size ; i++) {
+ if (d->key[i]==NULL)
+ continue ;
+ if (strchr(d->key[i], ':')==NULL) {
+ foundsec++ ;
+ if (foundsec>n)
+ break ;
+ }
+ }
+ if (foundsec<=n) {
+ return NULL ;
+ }
+ return d->key[i] ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Dump a dictionary to an opened file pointer.
+ @param d Dictionary to dump.
+ @param f Opened file pointer to dump to.
+ @return void
+
+ This function prints out the contents of a dictionary, one element by
+ line, onto the provided file pointer. It is OK to specify @c stderr
+ or @c stdout as output files. This function is meant for debugging
+ purposes mostly.
+ */
+/*--------------------------------------------------------------------------*/
+void iniparser_dump(dictionary * d, FILE * f)
+{
+ int i ;
+
+ if (d==NULL || f==NULL) return ;
+ for (i=0 ; i<d->size ; i++) {
+ if (d->key[i]==NULL)
+ continue ;
+ if (d->val[i]!=NULL) {
+ fprintf(f, "[%s]=[%s]\n", d->key[i], d->val[i]);
+ } else {
+ fprintf(f, "[%s]=UNDEF\n", d->key[i]);
+ }
+ }
+ return ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Save a dictionary to a loadable ini file
+ @param d Dictionary to dump
+ @param f Opened file pointer to dump to
+ @return void
+
+ This function dumps a given dictionary into a loadable ini file.
+ It is Ok to specify @c stderr or @c stdout as output files.
+ */
+/*--------------------------------------------------------------------------*/
+void iniparser_dump_ini(dictionary * d, FILE * f)
+{
+ int i, j ;
+ char keym[ASCIILINESZ+1];
+ int nsec ;
+ char * secname ;
+ int seclen ;
+
+ if (d==NULL || f==NULL) return ;
+
+ nsec = iniparser_getnsec(d);
+ if (nsec<1) {
+ /* No section in file: dump all keys as they are */
+ for (i=0 ; i<d->size ; i++) {
+ if (d->key[i]==NULL)
+ continue ;
+ fprintf(f, "%s = %s\n", d->key[i], d->val[i]);
+ }
+ return ;
+ }
+ for (i=0 ; i<nsec ; i++) {
+ secname = iniparser_getsecname(d, i) ;
+ seclen = (int)strlen(secname);
+ fprintf(f, "\n[%s]\n", secname);
+ sprintf(keym, "%s:", secname);
+ for (j=0 ; j<d->size ; j++) {
+ if (d->key[j]==NULL)
+ continue ;
+ if (!strncmp(d->key[j], keym, seclen+1)) {
+ fprintf(f,
+ "%-30s = %s\n",
+ d->key[j]+seclen+1,
+ d->val[j] ? d->val[j] : "");
+ }
+ }
+ }
+ fprintf(f, "\n");
+ return ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get the string associated to a key
+ @param d Dictionary to search
+ @param key Key string to look for
+ @param def Default value to return if key not found.
+ @return pointer to statically allocated character string
+
+ This function queries a dictionary for a key. A key as read from an
+ ini file is given as "section:key". If the key cannot be found,
+ the pointer passed as 'def' is returned.
+ The returned char pointer is pointing to a string allocated in
+ the dictionary, do not free or modify it.
+ */
+/*--------------------------------------------------------------------------*/
+char * iniparser_getstring(dictionary * d, const char * key, char * def)
+{
+ char * lc_key ;
+ char * sval ;
+
+ if (d==NULL || key==NULL)
+ return def ;
+
+ lc_key = strlwc(key);
+ sval = dictionary_get(d, lc_key, def);
+ return sval ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get the string associated to a key, convert to an int
+ @param d Dictionary to search
+ @param key Key string to look for
+ @param notfound Value to return in case of error
+ @return integer
+
+ This function queries a dictionary for a key. A key as read from an
+ ini file is given as "section:key". If the key cannot be found,
+ the notfound value is returned.
+
+ Supported values for integers include the usual C notation
+ so decimal, octal (starting with 0) and hexadecimal (starting with 0x)
+ are supported. Examples:
+
+ "42" -> 42
+ "042" -> 34 (octal -> decimal)
+ "0x42" -> 66 (hexa -> decimal)
+
+ Warning: the conversion may overflow in various ways. Conversion is
+ totally outsourced to strtol(), see the associated man page for overflow
+ handling.
+
+ Credits: Thanks to A. Becker for suggesting strtol()
+ */
+/*--------------------------------------------------------------------------*/
+int iniparser_getint(dictionary * d, const char * key, int notfound)
+{
+ char * str ;
+
+ str = iniparser_getstring(d, key, INI_INVALID_KEY);
+ if (str==INI_INVALID_KEY) return notfound ;
+ return (int)strtol(str, NULL, 0);
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get the string associated to a key, convert to a double
+ @param d Dictionary to search
+ @param key Key string to look for
+ @param notfound Value to return in case of error
+ @return double
+
+ This function queries a dictionary for a key. A key as read from an
+ ini file is given as "section:key". If the key cannot be found,
+ the notfound value is returned.
+ */
+/*--------------------------------------------------------------------------*/
+double iniparser_getdouble(dictionary * d, char * key, double notfound)
+{
+ char * str ;
+
+ str = iniparser_getstring(d, key, INI_INVALID_KEY);
+ if (str==INI_INVALID_KEY) return notfound ;
+ return atof(str);
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get the string associated to a key, convert to a boolean
+ @param d Dictionary to search
+ @param key Key string to look for
+ @param notfound Value to return in case of error
+ @return integer
+
+ This function queries a dictionary for a key. A key as read from an
+ ini file is given as "section:key". If the key cannot be found,
+ the notfound value is returned.
+
+ A true boolean is found if one of the following is matched:
+
+ - A string starting with 'y'
+ - A string starting with 'Y'
+ - A string starting with 't'
+ - A string starting with 'T'
+ - A string starting with '1'
+
+ A false boolean is found if one of the following is matched:
+
+ - A string starting with 'n'
+ - A string starting with 'N'
+ - A string starting with 'f'
+ - A string starting with 'F'
+ - A string starting with '0'
+
+ The notfound value returned if no boolean is identified, does not
+ necessarily have to be 0 or 1.
+ */
+/*--------------------------------------------------------------------------*/
+int iniparser_getboolean(dictionary * d, const char * key, int notfound)
+{
+ char * c ;
+ int ret ;
+
+ c = iniparser_getstring(d, key, INI_INVALID_KEY);
+ if (c==INI_INVALID_KEY) return notfound ;
+ if (c[0]=='y' || c[0]=='Y' || c[0]=='1' || c[0]=='t' || c[0]=='T') {
+ ret = 1 ;
+ } else if (c[0]=='n' || c[0]=='N' || c[0]=='0' || c[0]=='f' || c[0]=='F') {
+ ret = 0 ;
+ } else {
+ ret = notfound ;
+ }
+ return ret;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Finds out if a given entry exists in a dictionary
+ @param ini Dictionary to search
+ @param entry Name of the entry to look for
+ @return integer 1 if entry exists, 0 otherwise
+
+ Finds out if a given entry exists in the dictionary. Since sections
+ are stored as keys with NULL associated values, this is the only way
+ of querying for the presence of sections in a dictionary.
+ */
+/*--------------------------------------------------------------------------*/
+int iniparser_find_entry(
+ dictionary * ini,
+ char * entry
+)
+{
+ int found=0 ;
+ if (iniparser_getstring(ini, entry, INI_INVALID_KEY)!=INI_INVALID_KEY) {
+ found = 1 ;
+ }
+ return found ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Set an entry in a dictionary.
+ @param ini Dictionary to modify.
+ @param entry Entry to modify (entry name)
+ @param val New value to associate to the entry.
+ @return int 0 if Ok, -1 otherwise.
+
+ If the given entry can be found in the dictionary, it is modified to
+ contain the provided value. If it cannot be found, -1 is returned.
+ It is Ok to set val to NULL.
+ */
+/*--------------------------------------------------------------------------*/
+int iniparser_set(dictionary * ini, char * entry, char * val)
+{
+ return dictionary_set(ini, strlwc(entry), val) ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Delete an entry in a dictionary
+ @param ini Dictionary to modify
+ @param entry Entry to delete (entry name)
+ @return void
+
+ If the given entry can be found, it is deleted from the dictionary.
+ */
+/*--------------------------------------------------------------------------*/
+void iniparser_unset(dictionary * ini, char * entry)
+{
+ dictionary_unset(ini, strlwc(entry));
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Load a single line from an INI file
+ @param input_line Input line, may be concatenated multi-line input
+ @param section Output space to store section
+ @param key Output space to store key
+ @param value Output space to store value
+ @return line_status value
+ */
+/*--------------------------------------------------------------------------*/
+static line_status iniparser_line(
+ char * input_line,
+ char * section,
+ char * key,
+ char * value)
+{
+ line_status sta ;
+ char line[ASCIILINESZ+1];
+ int len ;
+
+ strcpy(line, strstrip(input_line));
+ len = (int)strlen(line);
+
+ sta = LINE_UNPROCESSED ;
+ if (len<1) {
+ /* Empty line */
+ sta = LINE_EMPTY ;
+ } else if (line[0]=='#') {
+ /* Comment line */
+ sta = LINE_COMMENT ;
+ } else if (line[0]=='[' && line[len-1]==']') {
+ /* Section name */
+ sscanf(line, "[%[^]]", section);
+ strcpy(section, strstrip(section));
+ strcpy(section, strlwc(section));
+ sta = LINE_SECTION ;
+ } else if (sscanf (line, "%[^=] = \"%[^\"]\"", key, value) == 2
+ || sscanf (line, "%[^=] = '%[^\']'", key, value) == 2
+ || sscanf (line, "%[^=] = %[^;#]", key, value) == 2) {
+ /* Usual key=value, with or without comments */
+ strcpy(key, strstrip(key));
+ strcpy(key, strlwc(key));
+ strcpy(value, strstrip(value));
+ /*
+ * sscanf cannot handle '' or "" as empty values
+ * this is done here
+ */
+ if (!strcmp(value, "\"\"") || (!strcmp(value, "''"))) {
+ value[0]=0 ;
+ }
+ sta = LINE_VALUE ;
+ } else if (sscanf(line, "%[^=] = %[;#]", key, value)==2
+ || sscanf(line, "%[^=] %[=]", key, value) == 2) {
+ /*
+ * Special cases:
+ * key=
+ * key=;
+ * key=#
+ */
+ strcpy(key, strstrip(key));
+ strcpy(key, strlwc(key));
+ value[0]=0 ;
+ sta = LINE_VALUE ;
+ } else {
+ /* Generate syntax error */
+ sta = LINE_ERROR ;
+ }
+ return sta ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Parse an ini file and return an allocated dictionary object
+ @param ininame Name of the ini file to read.
+ @return Pointer to newly allocated dictionary
+
+ This is the parser for ini files. This function is called, providing
+ the name of the file to be read. It returns a dictionary object that
+ should not be accessed directly, but through accessor functions
+ instead.
+
+ The returned dictionary must be freed using iniparser_freedict().
+ */
+/*--------------------------------------------------------------------------*/
+dictionary * iniparser_load(const char * ininame)
+{
+ FILE * in ;
+
+ char line [ASCIILINESZ+1] ;
+ char section [ASCIILINESZ+1] ;
+ char key [ASCIILINESZ+1] ;
+ char tmp [ASCIILINESZ+1] ;
+ char val [ASCIILINESZ+1] ;
+
+ int last=0 ;
+ int len ;
+ int lineno=0 ;
+ int errs=0;
+
+ dictionary * dict ;
+
+ if ((in=fopen(ininame, "r"))==NULL) {
+ fprintf(stderr, "iniparser: cannot open %s\n", ininame);
+ return NULL ;
+ }
+
+ dict = dictionary_new(0) ;
+ if (!dict) {
+ fclose(in);
+ return NULL ;
+ }
+
+ memset(line, 0, ASCIILINESZ);
+ memset(section, 0, ASCIILINESZ);
+ memset(key, 0, ASCIILINESZ);
+ memset(val, 0, ASCIILINESZ);
+ last=0 ;
+
+ while (fgets(line+last, ASCIILINESZ-last, in)!=NULL) {
+ lineno++ ;
+ len = (int)strlen(line)-1;
+ /* Safety check against buffer overflows */
+ if (line[len]!='\n') {
+ fprintf(stderr,
+ "iniparser: input line too long in %s (%d)\n",
+ ininame,
+ lineno);
+ dictionary_del(dict);
+ fclose(in);
+ return NULL ;
+ }
+ /* Get rid of \n and spaces at end of line */
+ while ((len>=0) &&
+ ((line[len]=='\n') || (isspace(line[len])))) {
+ line[len]=0 ;
+ len-- ;
+ }
+ /* Detect multi-line */
+ if (line[len]=='\\') {
+ /* Multi-line value */
+ last=len ;
+ continue ;
+ } else {
+ last=0 ;
+ }
+ switch (iniparser_line(line, section, key, val)) {
+ case LINE_EMPTY:
+ case LINE_COMMENT:
+ break ;
+
+ case LINE_SECTION:
+ errs = dictionary_set(dict, section, NULL);
+ break ;
+
+ case LINE_VALUE:
+ sprintf(tmp, "%s:%s", section, key);
+ errs = dictionary_set(dict, tmp, val) ;
+ break ;
+
+ case LINE_ERROR:
+ fprintf(stderr, "iniparser: syntax error in %s (%d):\n",
+ ininame,
+ lineno);
+ fprintf(stderr, "-> %s\n", line);
+ errs++ ;
+ break;
+
+ default:
+ break ;
+ }
+ memset(line, 0, ASCIILINESZ);
+ last=0;
+ if (errs<0) {
+ fprintf(stderr, "iniparser: memory allocation failure\n");
+ break ;
+ }
+ }
+ if (errs) {
+ dictionary_del(dict);
+ dict = NULL ;
+ }
+ fclose(in);
+ return dict ;
+}
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Free all memory associated to an ini dictionary
+ @param d Dictionary to free
+ @return void
+
+ Free all memory associated to an ini dictionary.
+ It is mandatory to call this function before the dictionary object
+ gets out of the current context.
+ */
+/*--------------------------------------------------------------------------*/
+void iniparser_freedict(dictionary * d)
+{
+ dictionary_del(d);
+}
+
+/* vim: set ts=4 et sw=4 tw=75 */
diff --git a/src/vdr-plugin/iniparser.h b/src/vdr-plugin/iniparser.h
new file mode 100644
index 0000000..78bf339
--- /dev/null
+++ b/src/vdr-plugin/iniparser.h
@@ -0,0 +1,284 @@
+/*
+ Copyright (c) 2000-2007 by Nicolas Devillard.
+ MIT License, see COPYING for more information.
+*/
+
+/*-------------------------------------------------------------------------*/
+/**
+ @file iniparser.h
+ @author N. Devillard
+ @date Sep 2007
+ @version 3.0
+ @brief Parser for ini files.
+*/
+/*--------------------------------------------------------------------------*/
+
+/*
+ $Id: iniparser.h,v 1.24 2007-11-23 21:38:19 ndevilla Exp $
+ $Revision: 1.24 $
+*/
+
+#ifndef _INIPARSER_H_
+#define _INIPARSER_H_
+
+/*---------------------------------------------------------------------------
+ Includes
+ ---------------------------------------------------------------------------*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/*
+ * The following #include is necessary on many Unixes but not Linux.
+ * It is not needed for Windows platforms.
+ * Uncomment it if needed.
+ */
+/* #include <unistd.h> */
+
+#include "dictionary.h"
+
+/*---------------------------------------------------------------------------
+ Macros
+ ---------------------------------------------------------------------------*/
+/** For backwards compatibility only */
+#define iniparser_getstr(d, k) iniparser_getstring(d, k, NULL)
+#define iniparser_setstr iniparser_setstring
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get number of sections in a dictionary
+ @param d Dictionary to examine
+ @return int Number of sections found in dictionary
+
+ This function returns the number of sections found in a dictionary.
+ The test to recognize sections is done on the string stored in the
+ dictionary: a section name is given as "section" whereas a key is
+ stored as "section:key", thus the test looks for entries that do not
+ contain a colon.
+
+ This clearly fails in the case a section name contains a colon, but
+ this should simply be avoided.
+
+ This function returns -1 in case of error.
+ */
+/*--------------------------------------------------------------------------*/
+
+int iniparser_getnsec(dictionary * d);
+
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get name for section n in a dictionary.
+ @param d Dictionary to examine
+ @param n Section number (from 0 to nsec-1).
+ @return Pointer to char string
+
+ This function locates the n-th section in a dictionary and returns
+ its name as a pointer to a string statically allocated inside the
+ dictionary. Do not free or modify the returned string!
+
+ This function returns NULL in case of error.
+ */
+/*--------------------------------------------------------------------------*/
+
+char * iniparser_getsecname(dictionary * d, int n);
+
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Save a dictionary to a loadable ini file
+ @param d Dictionary to dump
+ @param f Opened file pointer to dump to
+ @return void
+
+ This function dumps a given dictionary into a loadable ini file.
+ It is Ok to specify @c stderr or @c stdout as output files.
+ */
+/*--------------------------------------------------------------------------*/
+
+void iniparser_dump_ini(dictionary * d, FILE * f);
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Dump a dictionary to an opened file pointer.
+ @param d Dictionary to dump.
+ @param f Opened file pointer to dump to.
+ @return void
+
+ This function prints out the contents of a dictionary, one element by
+ line, onto the provided file pointer. It is OK to specify @c stderr
+ or @c stdout as output files. This function is meant for debugging
+ purposes mostly.
+ */
+/*--------------------------------------------------------------------------*/
+void iniparser_dump(dictionary * d, FILE * f);
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get the string associated to a key
+ @param d Dictionary to search
+ @param key Key string to look for
+ @param def Default value to return if key not found.
+ @return pointer to statically allocated character string
+
+ This function queries a dictionary for a key. A key as read from an
+ ini file is given as "section:key". If the key cannot be found,
+ the pointer passed as 'def' is returned.
+ The returned char pointer is pointing to a string allocated in
+ the dictionary, do not free or modify it.
+ */
+/*--------------------------------------------------------------------------*/
+char * iniparser_getstring(dictionary * d, const char * key, char * def);
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get the string associated to a key, convert to an int
+ @param d Dictionary to search
+ @param key Key string to look for
+ @param notfound Value to return in case of error
+ @return integer
+
+ This function queries a dictionary for a key. A key as read from an
+ ini file is given as "section:key". If the key cannot be found,
+ the notfound value is returned.
+
+ Supported values for integers include the usual C notation
+ so decimal, octal (starting with 0) and hexadecimal (starting with 0x)
+ are supported. Examples:
+
+ - "42" -> 42
+ - "042" -> 34 (octal -> decimal)
+ - "0x42" -> 66 (hexa -> decimal)
+
+ Warning: the conversion may overflow in various ways. Conversion is
+ totally outsourced to strtol(), see the associated man page for overflow
+ handling.
+
+ Credits: Thanks to A. Becker for suggesting strtol()
+ */
+/*--------------------------------------------------------------------------*/
+int iniparser_getint(dictionary * d, const char * key, int notfound);
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get the string associated to a key, convert to a double
+ @param d Dictionary to search
+ @param key Key string to look for
+ @param notfound Value to return in case of error
+ @return double
+
+ This function queries a dictionary for a key. A key as read from an
+ ini file is given as "section:key". If the key cannot be found,
+ the notfound value is returned.
+ */
+/*--------------------------------------------------------------------------*/
+double iniparser_getdouble(dictionary * d, char * key, double notfound);
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Get the string associated to a key, convert to a boolean
+ @param d Dictionary to search
+ @param key Key string to look for
+ @param notfound Value to return in case of error
+ @return integer
+
+ This function queries a dictionary for a key. A key as read from an
+ ini file is given as "section:key". If the key cannot be found,
+ the notfound value is returned.
+
+ A true boolean is found if one of the following is matched:
+
+ - A string starting with 'y'
+ - A string starting with 'Y'
+ - A string starting with 't'
+ - A string starting with 'T'
+ - A string starting with '1'
+
+ A false boolean is found if one of the following is matched:
+
+ - A string starting with 'n'
+ - A string starting with 'N'
+ - A string starting with 'f'
+ - A string starting with 'F'
+ - A string starting with '0'
+
+ The notfound value returned if no boolean is identified, does not
+ necessarily have to be 0 or 1.
+ */
+/*--------------------------------------------------------------------------*/
+int iniparser_getboolean(dictionary * d, const char * key, int notfound);
+
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Set an entry in a dictionary.
+ @param ini Dictionary to modify.
+ @param entry Entry to modify (entry name)
+ @param val New value to associate to the entry.
+ @return int 0 if Ok, -1 otherwise.
+
+ If the given entry can be found in the dictionary, it is modified to
+ contain the provided value. If it cannot be found, -1 is returned.
+ It is Ok to set val to NULL.
+ */
+/*--------------------------------------------------------------------------*/
+int iniparser_setstring(dictionary * ini, char * entry, char * val);
+
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Delete an entry in a dictionary
+ @param ini Dictionary to modify
+ @param entry Entry to delete (entry name)
+ @return void
+
+ If the given entry can be found, it is deleted from the dictionary.
+ */
+/*--------------------------------------------------------------------------*/
+void iniparser_unset(dictionary * ini, char * entry);
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Finds out if a given entry exists in a dictionary
+ @param ini Dictionary to search
+ @param entry Name of the entry to look for
+ @return integer 1 if entry exists, 0 otherwise
+
+ Finds out if a given entry exists in the dictionary. Since sections
+ are stored as keys with NULL associated values, this is the only way
+ of querying for the presence of sections in a dictionary.
+ */
+/*--------------------------------------------------------------------------*/
+int iniparser_find_entry(dictionary * ini, char * entry) ;
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Parse an ini file and return an allocated dictionary object
+ @param ininame Name of the ini file to read.
+ @return Pointer to newly allocated dictionary
+
+ This is the parser for ini files. This function is called, providing
+ the name of the file to be read. It returns a dictionary object that
+ should not be accessed directly, but through accessor functions
+ instead.
+
+ The returned dictionary must be freed using iniparser_freedict().
+ */
+/*--------------------------------------------------------------------------*/
+dictionary * iniparser_load(const char * ininame);
+
+/*-------------------------------------------------------------------------*/
+/**
+ @brief Free all memory associated to an ini dictionary
+ @param d Dictionary to free
+ @return void
+
+ Free all memory associated to an ini dictionary.
+ It is mandatory to call this function before the dictionary object
+ gets out of the current context.
+ */
+/*--------------------------------------------------------------------------*/
+void iniparser_freedict(dictionary * d);
+
+#endif
diff --git a/src/vdr-plugin/menu.c b/src/vdr-plugin/menu.c
new file mode 100644
index 0000000..3add4d4
--- /dev/null
+++ b/src/vdr-plugin/menu.c
@@ -0,0 +1,670 @@
+/*
+ * menu.c: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <stdlib.h>
+#include <time.h>
+#include <vdr/skins.h>
+#include <vdr/tools.h>
+#include <vdr/i18n.h>
+#include <vdr/osdbase.h>
+#include <vdr/skins.h>
+#include <vdr/font.h>
+#include <vdr/osd.h>
+#include <vdr/interface.h>
+#include "menu.h"
+#include "download.h"
+#include "config.h"
+#include "common.h"
+#include "history.h"
+#include "timer.h"
+#include "menu_timer.h"
+
+cCharSetConv csc = cCharSetConv("UTF-8", cCharSetConv::SystemCharacterTable());
+struct MenuPointers menuPointers;
+
+// --- cXMLMenu --------------------------------------------------
+
+cXMLMenu::cXMLMenu(const char *Title, int c0, int c1, int c2,
+ int c3, int c4)
+: cOsdMenu(Title, c0, c1, c2, c3, c4)
+{
+}
+
+bool cXMLMenu::Deserialize(const char *xml) {
+ xmlDocPtr doc = xmlParseMemory(xml, strlen(xml));
+ if (!doc) {
+ xmlErrorPtr xmlerr = xmlGetLastError();
+ if (xmlerr) {
+ error("libxml error: %s", xmlerr->message);
+ }
+
+ return false;
+ }
+
+ xmlNodePtr node = xmlDocGetRootElement(doc);
+ if (node)
+ node = node->xmlChildrenNode;
+
+ while (node) {
+ if (node->type == XML_ELEMENT_NODE) {
+ if (!CreateItemFromTag(doc, node)) {
+ warning("Failed to parse menu tag: %s", (char *)node->name);
+ }
+ }
+ node = node->next;
+ }
+
+ xmlFreeDoc(doc);
+ return true;
+}
+
+int cXMLMenu::Load(const char *xmlstr) {
+ Clear();
+ Deserialize(xmlstr);
+
+ return 0;
+}
+
+
+// --- cNavigationMenu -----------------------------------------------------
+
+cNavigationMenu::cNavigationMenu(cHistory *History,
+ cProgressVector& dlsummaries)
+ : cXMLMenu("", 25), summaries(dlsummaries)
+{
+ title = NULL;
+ reference = NULL;
+ shortcutMode = 0;
+ history = History;
+ UpdateHelp();
+}
+
+cNavigationMenu::~cNavigationMenu() {
+ menuPointers.navigationMenu = NULL;
+ Clear();
+ if (reference)
+ free(reference);
+}
+
+bool cNavigationMenu::CreateItemFromTag(xmlDocPtr doc, xmlNodePtr node) {
+ if (!xmlStrcmp(node->name, BAD_CAST "link")) {
+ NewLinkItem(doc, node);
+ return true;
+ } else if (!xmlStrcmp(node->name, BAD_CAST "textfield")) {
+ NewTextField(doc, node);
+ return true;
+ } else if (!xmlStrcmp(node->name, BAD_CAST "itemlist")) {
+ NewItemList(doc, node);
+ return true;
+ } else if (!xmlStrcmp(node->name, BAD_CAST "textarea")) {
+ NewTextArea(doc, node);
+ return true;
+ } else if (!xmlStrcmp(node->name, BAD_CAST "button")) {
+ NewButton(doc, node);
+ return true;
+ } else if (!xmlStrcmp(node->name, BAD_CAST "title")) {
+ NewTitle(doc, node);
+ return true;
+ }
+
+ return false;
+}
+
+void cNavigationMenu::AddLinkItem(cOsdItem *item,
+ cLinkBase *ref,
+ cLinkBase *streamref) {
+ Add(item);
+
+ if (ref)
+ links.Append(ref);
+ else
+ links.Append(NULL);
+
+ if (streamref)
+ streams.Append(streamref);
+ else
+ streams.Append(NULL);
+}
+
+void cNavigationMenu::NewLinkItem(xmlDocPtr doc, xmlNodePtr node) {
+ // label, ref and object tags
+ xmlChar *itemtitle = NULL, *ref = NULL, *streamref = NULL;
+
+ node = node->xmlChildrenNode;
+ while (node) {
+ if (!xmlStrcmp(node->name, BAD_CAST "label")) {
+ if (itemtitle)
+ xmlFree(itemtitle);
+ itemtitle = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
+ } else if (!xmlStrcmp(node->name, BAD_CAST "ref")) {
+ if (ref)
+ xmlFree(ref);
+ ref = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
+ } else if (!xmlStrcmp(node->name, BAD_CAST "stream")) {
+ if (streamref)
+ xmlFree(streamref);
+ streamref = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
+ }
+ node = node->next;
+ }
+ if (!itemtitle)
+ itemtitle = xmlCharStrdup("???");
+
+ const char *titleconv = csc.Convert((char *)itemtitle);
+ cOsdItem *item = new cOsdItem(titleconv);
+ cSimpleLink *objlinkdata = NULL;
+ cSimpleLink *linkdata = NULL;
+ if (ref)
+ linkdata = new cSimpleLink((char *)ref);
+ if (streamref) {
+ // media object
+ objlinkdata = new cSimpleLink((char *)streamref);
+ } else {
+ // navigation link
+ char *bracketed = (char *)malloc((strlen(titleconv)+3)*sizeof(char));
+ if (bracketed) {
+ bracketed[0] = '\0';
+ strcat(bracketed, "[");
+ strcat(bracketed, titleconv);
+ strcat(bracketed, "]");
+ item->SetText(bracketed, false);
+ }
+ }
+ AddLinkItem(item, linkdata, objlinkdata);
+
+ xmlFree(itemtitle);
+ if (ref)
+ xmlFree(ref);
+ if (streamref)
+ xmlFree(streamref);
+}
+
+void cNavigationMenu::NewTextField(xmlDocPtr doc, xmlNodePtr node) {
+ // name attribute
+ xmlChar *name = xmlGetProp(node, BAD_CAST "name");
+ cHistoryObject *curhistpage = history->Current();
+
+ // label tag
+ xmlChar *text = NULL;
+ node = node->xmlChildrenNode;
+ while (node) {
+ if (!xmlStrcmp(node->name, BAD_CAST "label")) {
+ if (text)
+ xmlFree(text);
+ text = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
+ }
+ node = node->next;
+ }
+ if (!text)
+ text = xmlCharStrdup("???");
+
+ cTextFieldData *data = curhistpage->GetTextFieldData((char *)name);
+ cMenuEditStrItem *item = new cMenuEditStrItem(csc.Convert((char *)text),
+ data->GetValue(),
+ data->GetLength());
+ AddLinkItem(item, NULL, NULL);
+
+ free(text);
+ if (name)
+ xmlFree(name);
+}
+
+void cNavigationMenu::NewItemList(xmlDocPtr doc, xmlNodePtr node) {
+ // name attribute
+ xmlChar *name = xmlGetProp(node, BAD_CAST "name");
+ cHistoryObject *curhistpage = history->Current();
+
+ // label and item tags
+ xmlChar *text = NULL;
+ cStringList items;
+ cStringList itemvalues;
+ node = node->xmlChildrenNode;
+ while (node) {
+ if (!xmlStrcmp(node->name, BAD_CAST "label")) {
+ if (text)
+ xmlFree(text);
+ text = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
+ } else if (!xmlStrcmp(node->name, BAD_CAST "item")) {
+ xmlChar *str = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
+ if (!str)
+ str = xmlCharStrdup("???");
+ xmlChar *strvalue = xmlGetProp(node, BAD_CAST "value");
+ if (!strvalue)
+ strvalue = xmlCharStrdup("");
+
+ items.Append(strdup((char *)str));
+ itemvalues.Append(strdup((char *)strvalue));
+
+ xmlFree(str);
+ xmlFree(strvalue);
+ }
+ node = node->next;
+ }
+ if (!text)
+ text = xmlCharStrdup("???");
+
+ cItemListData *data = curhistpage->GetItemListData((const char *)name,
+ items,
+ itemvalues);
+
+ cMenuEditStraItem *item = new cMenuEditStraItem(csc.Convert((char *)text),
+ data->GetValuePtr(),
+ data->GetNumStrings(),
+ data->GetStrings());
+ AddLinkItem(item, NULL, NULL);
+
+ xmlFree(text);
+ if (name)
+ xmlFree(name);
+}
+
+void cNavigationMenu::NewTextArea(xmlDocPtr doc, xmlNodePtr node) {
+ // label tag
+ xmlChar *itemtitle = NULL;
+ node = node->xmlChildrenNode;
+ while (node) {
+ if (!xmlStrcmp(node->name, BAD_CAST "label")) {
+ if (itemtitle)
+ xmlFree(itemtitle);
+ itemtitle = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
+ }
+ node = node->next;
+ }
+ if (!itemtitle)
+ return;
+
+ const cFont *font = cFont::GetFont(fontOsd);
+ cTextWrapper tw(csc.Convert((char *)itemtitle), font, cOsd::OsdWidth());
+ for (int i=0; i < tw.Lines(); i++) {
+ AddLinkItem(new cOsdItem(tw.GetLine(i), osUnknown, false), NULL, NULL);
+ }
+
+ xmlFree(itemtitle);
+}
+
+void cNavigationMenu::NewButton(xmlDocPtr doc, xmlNodePtr node) {
+ // label and submission tags
+ xmlChar *itemtitle = NULL, *submission = NULL;
+ cHistoryObject *curhistpage = history->Current();
+
+ node = node->xmlChildrenNode;
+ while (node) {
+ if (!xmlStrcmp(node->name, BAD_CAST "label")) {
+ if (itemtitle)
+ xmlFree(itemtitle);
+ itemtitle = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
+ } else if (!xmlStrcmp(node->name, BAD_CAST "submission")) {
+ if (submission)
+ xmlFree(submission);
+ submission = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
+ }
+ node = node->next;
+ }
+ if (!itemtitle)
+ itemtitle = xmlCharStrdup("???");
+
+ cSubmissionButtonData *data = \
+ new cSubmissionButtonData((char *)submission, curhistpage);
+ const char *titleconv = csc.Convert((char *)itemtitle); // do not free
+ char *newtitle = (char *)malloc((strlen(titleconv)+3)*sizeof(char));
+ if (newtitle) {
+ newtitle[0] = '\0';
+ strcat(newtitle, "[");
+ strcat(newtitle, titleconv);
+ strcat(newtitle, "]");
+
+ cOsdItem *item = new cOsdItem(newtitle);
+ AddLinkItem(item, data, NULL);
+ free(newtitle);
+ }
+
+ xmlFree(itemtitle);
+ if (submission)
+ xmlFree(submission);
+}
+
+void cNavigationMenu::NewTitle(xmlDocPtr doc, xmlNodePtr node) {
+ xmlChar *newtitle = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
+ if (newtitle) {
+ const char *conv = csc.Convert((char *)newtitle);
+ SetTitle(conv);
+ if (title)
+ free(title);
+ title = strdup(conv);
+ xmlFree(newtitle);
+ }
+}
+
+eOSState cNavigationMenu::ProcessKey(eKeys Key)
+{
+ cWebviTimer *timer;
+ bool hasStreams;
+ int old = Current();
+ eOSState state = cXMLMenu::ProcessKey(Key);
+ bool validItem = Current() >= 0 && Current() < links.Size();
+
+ if (HasSubMenu())
+ return state;
+
+ if (state == osUnknown) {
+ switch (Key) {
+ case kInfo:
+ // The alternative link is active only when object links are
+ // present.
+ if (validItem && streams.At(Current()))
+ state = Select(links.At(Current()), LT_REGULAR);
+ break;
+
+ case kOk:
+ // Primary action: download media object or, if not a media
+ // link, follow the navigation link.
+ if (validItem) {
+ if (streams.At(Current()))
+ state = Select(streams.At(Current()), LT_MEDIA);
+ else
+ state = Select(links.At(Current()), LT_REGULAR);
+ }
+ break;
+
+ case kRed:
+ if (shortcutMode == 0) {
+ state = HistoryBack();
+ } else {
+ menuPointers.statusScreen = new cStatusScreen(summaries);
+ state = AddSubMenu(menuPointers.statusScreen);
+ }
+ break;
+
+ case kGreen:
+ if (shortcutMode == 0) {
+ state = HistoryForward();
+ } else {
+ return AddSubMenu(new cWebviTimerListMenu(cWebviTimerManager::Instance()));
+ }
+ break;
+
+ case kYellow:
+ if (shortcutMode == 0) {
+ hasStreams = false;
+ for (int i=0; i < streams.Size(); i++) {
+ if (streams[i]) {
+ hasStreams = true;
+ break;
+ }
+ }
+
+ if (hasStreams || Interface->Confirm(tr("No streams on this page, create timer anyway?"))) {
+ timer = cWebviTimerManager::Instance().Create(title, reference);
+ if (timer)
+ return AddSubMenu(new cEditWebviTimerMenu(*timer, true, false));
+ }
+
+ state = osContinue;
+ }
+ break;
+
+ case kBlue:
+ if (shortcutMode == 0) {
+ // Secondary action: start streaming if a media object
+ if (validItem && streams.At(Current()))
+ state = Select(streams.At(Current()), LT_STREAMINGMEDIA);
+ }
+ break;
+
+ case k0:
+ shortcutMode = shortcutMode == 0 ? 1 : 0;
+ UpdateHelp();
+ break;
+
+ default:
+ break;
+ }
+ } else {
+ // If the key press caused the selected item to change, we need to
+ // update the help texts.
+ //
+ // In cMenuEditStrItem key == kOk with state == osContinue
+ // indicates leaving the edit mode. We want to update the help
+ // texts in this case also.
+ if ((old != Current()) ||
+ ((Key == kOk) && (state == osContinue))) {
+ UpdateHelp();
+ }
+ }
+
+ return state;
+}
+
+eOSState cNavigationMenu::Select(cLinkBase *link, eLinkType type)
+{
+ if (!link) {
+ return osContinue;
+ }
+ char *ref = link->GetURL();
+ if (!ref) {
+ error("link->GetURL() == NULL in cNavigationMenu::Select");
+ return osContinue;
+ }
+
+ if (type == LT_MEDIA) {
+ cDownloadProgress *progress = summaries.NewDownload();
+ cFileDownloadRequest *req = \
+ new cFileDownloadRequest(history->Current()->GetID(), ref,
+ webvideoConfig->GetDownloadPath(),
+ progress);
+ cWebviThread::Instance().AddRequest(req);
+
+ Skins.Message(mtInfo, tr("Downloading in the background"));
+ } else if (type == LT_STREAMINGMEDIA) {
+ cWebviThread::Instance().AddRequest(new cStreamUrlRequest(history->Current()->GetID(),
+ ref));
+ Skins.Message(mtInfo, tr("Starting player..."));
+ return osEnd;
+ } else {
+ cWebviThread::Instance().AddRequest(new cMenuRequest(history->Current()->GetID(),
+ ref));
+ Skins.Message(mtStatus, tr("Retrieving..."));
+ }
+
+ return osContinue;
+}
+
+void cNavigationMenu::Clear(void) {
+ cXMLMenu::Clear();
+ SetTitle("");
+ if (title)
+ free(title);
+ title = NULL;
+ for (int i=0; i < links.Size(); i++) {
+ if (links[i])
+ delete links[i];
+ if (streams[i])
+ delete streams[i];
+ }
+ links.Clear();
+ streams.Clear();
+}
+
+void cNavigationMenu::Populate(const cHistoryObject *page, const char *statusmsg) {
+ Load(page->GetOSD());
+
+ if (reference)
+ free(reference);
+ reference = strdup(page->GetReference());
+
+ // Make sure that an item is selected (if there is at least
+ // one). The help texts are not updated correctly if no item is
+ // selected.
+
+ SetCurrent(Get(page->GetSelected()));
+ UpdateHelp();
+ SetStatus(statusmsg);
+}
+
+eOSState cNavigationMenu::HistoryBack() {
+ cHistoryObject *cur = history->Current();
+
+ if (cur)
+ cur->RememberSelected(Current());
+
+ cHistoryObject *page = history->Back();
+ if (page) {
+ Populate(page);
+ Display();
+ }
+ return osContinue;
+}
+
+eOSState cNavigationMenu::HistoryForward() {
+ cHistoryObject *before = history->Current();
+ cHistoryObject *after = history->Forward();
+
+ if (before)
+ before->RememberSelected(Current());
+
+ // Update only if the menu really changed
+ if (before != after) {
+ Populate(after);
+ Display();
+ }
+ return osContinue;
+}
+
+void cNavigationMenu::UpdateHelp() {
+ const char *red = NULL;
+ const char *green = NULL;
+ const char *yellow = NULL;
+ const char *blue = NULL;
+
+ if (shortcutMode == 0) {
+ red = (history->Current() != history->First()) ? tr("Back") : NULL;
+ green = (history->Current() != history->Last()) ? tr("Forward") : NULL;
+ yellow = (Current() >= 0) ? tr("Create timer") : NULL;
+ blue = ((Current() >= 0) && (streams.At(Current()))) ? tr("Play") : NULL;
+ } else {
+ red = tr("Status");
+ green = tr("Timers");
+ }
+
+ SetHelp(red, green, yellow, blue);
+}
+
+// --- cStatusScreen -------------------------------------------------------
+
+cStatusScreen::cStatusScreen(cProgressVector& dlsummaries)
+ : cOsdMenu(tr("Unfinished downloads"), 40), summaries(dlsummaries)
+{
+ int charsperline = cOsd::OsdWidth() / cFont::GetFont(fontOsd)->Width('M');
+ SetCols(charsperline-5);
+
+ UpdateHelp();
+ Update();
+}
+
+cStatusScreen::~cStatusScreen() {
+ menuPointers.statusScreen = NULL;
+}
+
+void cStatusScreen::Update() {
+ int c = Current();
+
+ Clear();
+
+ if (summaries.Size() == 0) {
+ SetTitle(tr("No active downloads"));
+ } else {
+
+ for (int i=0; i<summaries.Size(); i++) {
+ cString dltitle;
+ cDownloadProgress *s = summaries[i];
+ dltitle = cString::sprintf("%s\t%s",
+ (const char *)s->GetTitle(),
+ (const char *)s->GetPercentage());
+
+ Add(new cOsdItem(dltitle));
+ }
+
+ if (c >= 0)
+ SetCurrent(Get(c));
+ }
+
+ lastupdate = time(NULL);
+
+ UpdateHelp();
+ Display();
+}
+
+bool cStatusScreen::NeedsUpdate() {
+ return (Count() > 0) && (time(NULL) - lastupdate >= updateInterval);
+}
+
+eOSState cStatusScreen::ProcessKey(eKeys Key) {
+ cFileDownloadRequest *req;
+ int old = Current();
+ eOSState state = cOsdMenu::ProcessKey(Key);
+
+ if (HasSubMenu())
+ return state;
+
+ if (state == osUnknown) {
+ switch (Key) {
+ case kYellow:
+ if ((Current() >= 0) && (Current() < summaries.Size())) {
+ if (summaries[Current()]->IsFinished()) {
+ delete summaries[Current()];
+ summaries.Remove(Current());
+ Update();
+ } else if ((req = summaries[Current()]->GetRequest()) &&
+ !req->IsFinished()) {
+ req->Abort();
+ Update();
+ }
+ }
+ return osContinue;
+
+ case kOk:
+ case kInfo:
+ if (summaries[Current()]->Error()) {
+ cString msg = cString::sprintf("%s\n%s: %s",
+ (const char *)summaries[Current()]->GetTitle(),
+ tr("Error"),
+ (const char *)summaries[Current()]->GetStatusPharse());
+ return AddSubMenu(new cMenuText(tr("Error details"), msg));
+ } else {
+ cString msg = cString::sprintf("%s (%s)",
+ (const char *)summaries[Current()]->GetTitle(),
+ (const char *)summaries[Current()]->GetPercentage());
+ return AddSubMenu(new cMenuText(tr("Download details"), msg));
+ }
+
+ return osContinue;
+
+ default:
+ break;
+ }
+ } else {
+ // Update help if the key press caused the menu item to change.
+ if (old != Current())
+ UpdateHelp();
+ }
+
+ return state;
+}
+
+void cStatusScreen::UpdateHelp() {
+ bool remove = false;
+ if ((Current() >= 0) && (Current() < summaries.Size())) {
+ if (summaries[Current()]->IsFinished()) {
+ remove = true;
+ }
+ }
+
+ const char *yellow = remove ? tr("Remove") : tr("Abort");
+
+ SetHelp(NULL, NULL, yellow, NULL);
+}
diff --git a/src/vdr-plugin/menu.h b/src/vdr-plugin/menu.h
new file mode 100644
index 0000000..b1e67df
--- /dev/null
+++ b/src/vdr-plugin/menu.h
@@ -0,0 +1,114 @@
+/*
+ * menu.h: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef __WEBVIDEO_MENU_H
+#define __WEBVIDEO_MENU_H
+
+#include <time.h>
+#include <vdr/osdbase.h>
+#include <vdr/menuitems.h>
+#include <vdr/menu.h>
+#include <libxml/parser.h>
+#include "download.h"
+#include "menudata.h"
+
+extern cCharSetConv csc;
+
+// --- cXMLMenu --------------------------------------------------
+
+class cXMLMenu : public cOsdMenu {
+protected:
+ virtual bool Deserialize(const char *xml);
+ virtual bool CreateItemFromTag(xmlDocPtr doc, xmlNodePtr node) = 0;
+public:
+ cXMLMenu(const char *Title, int c0 = 0, int c1 = 0,
+ int c2 = 0, int c3 = 0, int c4 = 0);
+
+ int Load(const char *xmlstr);
+};
+
+// --- cNavigationMenu -----------------------------------------------------
+
+enum eLinkType { LT_REGULAR, LT_MEDIA, LT_STREAMINGMEDIA };
+
+class cHistory;
+class cHistoryObject;
+class cStatusScreen;
+
+class cNavigationMenu : public cXMLMenu {
+private:
+ // links[i] is the navigation link of the i:th item
+ cVector<cLinkBase *> links;
+ // streams[i] is the media stream link of the i:th item
+ cVector<cLinkBase *> streams;
+ cProgressVector& summaries;
+ char *title;
+ char *reference;
+ int shortcutMode;
+
+protected:
+ cHistory *history;
+
+ virtual bool CreateItemFromTag(xmlDocPtr doc, xmlNodePtr node);
+ void AddLinkItem(cOsdItem *item, cLinkBase *ref, cLinkBase *streamref);
+ void NewLinkItem(xmlDocPtr doc, xmlNodePtr node);
+ void NewTextField(xmlDocPtr doc, xmlNodePtr node);
+ void NewItemList(xmlDocPtr doc, xmlNodePtr node);
+ void NewTextArea(xmlDocPtr doc, xmlNodePtr node);
+ void NewButton(xmlDocPtr doc, xmlNodePtr node);
+ void NewTitle(xmlDocPtr doc, xmlNodePtr node);
+ void UpdateHelp();
+
+public:
+ cNavigationMenu(cHistory *History, cProgressVector& dlsummaries);
+ virtual ~cNavigationMenu();
+
+ virtual eOSState ProcessKey(eKeys Key);
+ virtual eOSState Select(cLinkBase *link, eLinkType type);
+ virtual void Clear(void);
+ eOSState HistoryBack();
+ eOSState HistoryForward();
+
+ const char *Reference() const { return reference; }
+ void Populate(const cHistoryObject *page, const char *statusmsg=NULL);
+};
+
+// --- cStatusScreen -------------------------------------------------------
+
+class cStatusScreen : public cOsdMenu {
+public:
+ const static time_t updateInterval = 5; // seconds
+private:
+ cProgressVector& summaries;
+ time_t lastupdate;
+
+protected:
+ void UpdateHelp();
+
+public:
+ cStatusScreen(cProgressVector& dlsummaries);
+ ~cStatusScreen();
+
+ void Update();
+ bool NeedsUpdate();
+
+ virtual eOSState ProcessKey(eKeys Key);
+};
+
+// --- MenuPointers --------------------------------------------------------
+
+struct MenuPointers {
+ cNavigationMenu *navigationMenu;
+ cStatusScreen *statusScreen;
+
+ MenuPointers() : navigationMenu(NULL), statusScreen(NULL) {};
+};
+
+extern struct MenuPointers menuPointers;
+
+#endif // __WEBVIDEO_MENU_H
diff --git a/src/vdr-plugin/menu_timer.c b/src/vdr-plugin/menu_timer.c
new file mode 100644
index 0000000..0501e0d
--- /dev/null
+++ b/src/vdr-plugin/menu_timer.c
@@ -0,0 +1,150 @@
+/*
+ * menu.c: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <time.h>
+#include <vdr/i18n.h>
+#include <vdr/tools.h>
+#include <vdr/menuitems.h>
+#include <vdr/interface.h>
+#include "menu_timer.h"
+
+#define ARRAYSIZE(a) sizeof(a)/sizeof(a[0])
+
+/*
+const char *intervalNames[] = {trNOOP("Once per day"), trNOOP("Once per week"),
+ trNOOP("Once per month")};
+*/
+
+const char *intervalNames[] = {NULL, NULL, NULL};
+const int intervalValues[] = {24*60*60, 7*24*60*60, 30*24*60*60};
+
+// --- cEditWebviTimerMenu -------------------------------------------------
+
+cEditWebviTimerMenu::cEditWebviTimerMenu(cWebviTimer &timer,
+ bool refreshWhenDone,
+ bool execButton)
+ : cOsdMenu(tr("Edit timer"), 20), timer(timer), interval(1),
+ refresh(refreshWhenDone)
+{
+ // title
+ strn0cpy(title, timer.GetTitle(), maxTitleLen);
+ Add(new cMenuEditStrItem(tr("Title"), title, maxTitleLen));
+
+ // interval
+ for (unsigned i=0; i<ARRAYSIZE(intervalValues); i++) {
+ if (timer.GetInterval() == intervalValues[i]) {
+ interval = i;
+ break;
+ }
+ }
+
+ if (!intervalNames[0]) {
+ // Initialize manually to make the translations work
+ intervalNames[0] = tr("Once per day");
+ intervalNames[1] = tr("Once per week");
+ intervalNames[2] = tr("Once per month");
+ }
+
+ Add(new cMenuEditStraItem(tr("Update interval"), &interval,
+ ARRAYSIZE(intervalNames), intervalNames));
+
+ // "execute now" button
+ if (execButton)
+ Add(new cOsdItem(tr("Execute now"), osUser1, true));
+
+ // last update time
+ char lastTime[25];
+ if (timer.LastUpdate() == 0) {
+ // TRANSLATORS: at most 24 chars
+ strcpy(lastTime, tr("Never"));
+ } else {
+ time_t updateTime = timer.LastUpdate();
+ strftime(lastTime, 25, "%x %X", localtime(&updateTime));
+ }
+
+ cString lastUpdated = cString::sprintf("%s\t%s", tr("Last fetched:"), lastTime);
+ Add(new cOsdItem(lastUpdated, osUnknown, false));
+
+ // error
+ if (!timer.Success()) {
+ Add(new cOsdItem(tr("Error on last refresh!"), osUnknown, false));
+ Add(new cOsdItem(timer.LastError(), osUnknown, false));
+ }
+}
+
+cEditWebviTimerMenu::~cEditWebviTimerMenu() {
+ if (refresh)
+ timer.Execute();
+}
+
+eOSState cEditWebviTimerMenu::ProcessKey(eKeys Key) {
+ eOSState state = cOsdMenu::ProcessKey(Key);
+
+ if (state == osContinue) {
+ timer.SetTitle(title);
+ timer.SetInterval(intervalValues[interval]);
+ } else if (state == osUser1) {
+ timer.Execute();
+ Skins.Message(mtInfo, tr("Downloading in the background"));
+ }
+
+ return state;
+}
+
+// --- cWebviTimerListMenu -------------------------------------------------
+
+cWebviTimerListMenu::cWebviTimerListMenu(cWebviTimerManager &timers)
+ : cOsdMenu(tr("Timers")), timers(timers)
+{
+ cWebviTimer *t = timers.First();
+ while (t) {
+ Add(new cOsdItem(t->GetTitle(), osUnknown, true));
+ t = timers.Next(t);
+ }
+
+ SetHelp(NULL, NULL, tr("Remove"), NULL);
+}
+
+eOSState cWebviTimerListMenu::ProcessKey(eKeys Key) {
+ cWebviTimer *t;
+ eOSState state = cOsdMenu::ProcessKey(Key);
+
+ if (HasSubMenu())
+ return state;
+
+ if (state == osUnknown) {
+ switch (Key) {
+ case kOk:
+ t = timers.GetLinear(Current());
+ if (t)
+ return AddSubMenu(new cEditWebviTimerMenu(*t));
+ break;
+
+ case kYellow:
+ t = timers.GetLinear(Current());
+ if (t) {
+ if (t->Running()) {
+ // FIXME: ask if the user wants to cancel the downloads
+ Skins.Message(mtInfo, tr("Timer running, can't remove"));
+ } else if (Interface->Confirm(tr("Remove timer?"))) {
+ timers.Remove(t);
+ Del(Current());
+ Display();
+ }
+
+ return osContinue;
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return state;
+}
diff --git a/src/vdr-plugin/menu_timer.h b/src/vdr-plugin/menu_timer.h
new file mode 100644
index 0000000..192c062
--- /dev/null
+++ b/src/vdr-plugin/menu_timer.h
@@ -0,0 +1,46 @@
+/*
+ * menu_timer.h: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef __WEBVIDEO_MENU_TIMER_H
+#define __WEBVIDEO_MENU_TIMER_H
+
+#include <vdr/osdbase.h>
+#include "timer.h"
+
+// --- cEditWebviTimerMenu -------------------------------------------------
+
+class cEditWebviTimerMenu : public cOsdMenu {
+private:
+ static const int maxTitleLen = 128;
+
+ cWebviTimer &timer;
+ char title[maxTitleLen];
+ int interval;
+ bool refresh;
+
+public:
+ cEditWebviTimerMenu(cWebviTimer &timer, bool refreshWhenDone=false,
+ bool execButton=true);
+ ~cEditWebviTimerMenu();
+
+ virtual eOSState ProcessKey(eKeys Key);
+};
+
+// --- cWebviTimerListMenu -------------------------------------------------
+
+class cWebviTimerListMenu : public cOsdMenu {
+private:
+ cWebviTimerManager& timers;
+
+public:
+ cWebviTimerListMenu(cWebviTimerManager &timers);
+
+ virtual eOSState ProcessKey(eKeys Key);
+};
+
+#endif
diff --git a/src/vdr-plugin/menudata.c b/src/vdr-plugin/menudata.c
new file mode 100644
index 0000000..45db133
--- /dev/null
+++ b/src/vdr-plugin/menudata.c
@@ -0,0 +1,179 @@
+/*
+ * menudata.c: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <vdr/tools.h>
+#include "menudata.h"
+#include "common.h"
+#include "history.h"
+
+// --- cQueryData ----------------------------------------------------------
+
+cQueryData::cQueryData(const char *Name) {
+ name = Name ? strdup(Name) : NULL;
+}
+
+cQueryData::~cQueryData() {
+ if (name)
+ free(name);
+}
+
+// --- cSimpleLink ---------------------------------------------------------
+
+cSimpleLink::cSimpleLink(const char *reference) {
+ ref = reference ? strdup(reference) : NULL;
+}
+
+cSimpleLink::~cSimpleLink() {
+ if (ref) {
+ free(ref);
+ }
+}
+
+char *cSimpleLink::GetURL() {
+ return ref;
+}
+
+// --- cTextFieldData ------------------------------------------------------
+
+cTextFieldData::cTextFieldData(const char *Name, int Length)
+: cQueryData(Name)
+{
+ valuebufferlength = Length;
+ valuebuffer = (char *)malloc(Length*sizeof(char));
+ *valuebuffer = '\0';
+}
+
+cTextFieldData::~cTextFieldData() {
+ if(valuebuffer)
+ free(valuebuffer);
+}
+
+char *cTextFieldData::GetQueryFragment() {
+ const char *name = GetName();
+
+ if (name && *name && valuebuffer) {
+ char *encoded = URLencode(valuebuffer);
+ cString tmp = cString::sprintf("%s,%s", name, encoded);
+ free(encoded);
+ return strdup(tmp);
+ }
+
+ return NULL;
+}
+
+char *cTextFieldData::GetValue() {
+ return valuebuffer;
+}
+
+int cTextFieldData::GetLength() {
+ return valuebufferlength;
+}
+
+// --- cItemListData -------------------------------------------------------
+
+cItemListData::cItemListData(const char *Name, char **Strings, char **StringValues, int NumStrings)
+: cQueryData(Name)
+{
+ strings = Strings;
+ stringvalues = StringValues;
+ numstrings = NumStrings;
+ value = 0;
+}
+
+cItemListData::~cItemListData() {
+ for (int i=0; i < numstrings; i++) {
+ free(strings[i]);
+ free(stringvalues[i]);
+ }
+ if (strings)
+ free(strings);
+ if (stringvalues)
+ free(stringvalues);
+}
+
+char *cItemListData::GetQueryFragment() {
+ const char *name = GetName();
+
+ if (name && *name) {
+ cString tmp = cString::sprintf("%s,%s", name, stringvalues[value]);
+ return strdup(tmp);
+ }
+
+ return NULL;
+}
+
+char **cItemListData::GetStrings() {
+ return strings;
+}
+
+char **cItemListData::GetStringValues() {
+ return stringvalues;
+}
+
+int cItemListData::GetNumStrings() {
+ return numstrings;
+}
+
+int *cItemListData::GetValuePtr() {
+ return &value;
+}
+
+// --- cSubmissionButtonData -----------------------------------------------
+
+cSubmissionButtonData::cSubmissionButtonData(
+ const char *queryUrl, const cHistoryObject *currentPage)
+{
+ querybase = queryUrl ? strdup(queryUrl) : NULL;
+ page = currentPage;
+}
+
+cSubmissionButtonData::~cSubmissionButtonData() {
+ if (querybase)
+ free(querybase);
+ // do not free page
+}
+
+char *cSubmissionButtonData::GetURL() {
+ if (!querybase)
+ return NULL;
+
+ char *querystr = (char *)malloc(sizeof(char)*(strlen(querybase)+2));
+ strcpy(querystr, querybase);
+
+ if (!page)
+ return querystr;
+
+ if (strchr(querystr, '?'))
+ strcat(querystr, "&");
+ else
+ strcat(querystr, "?");
+
+ int numparameters = 0;
+ for (int i=0; i<page->QuerySize(); i++) {
+ char *parameter = page->GetQueryFragment(i);
+ if (parameter) {
+ querystr = (char *)realloc(querystr, (strlen(querystr)+strlen(parameter)+8)*sizeof(char));
+ if (i > 0)
+ strcat(querystr, "&");
+ strcat(querystr, "subst=");
+ strcat(querystr, parameter);
+ numparameters++;
+
+ free(parameter);
+ }
+ }
+
+ if (numparameters == 0) {
+ // remove the '?' or '&' because no parameters were added to the url
+ querystr[strlen(querystr)-1] = '\0';
+ }
+
+ return querystr;
+}
diff --git a/src/vdr-plugin/menudata.h b/src/vdr-plugin/menudata.h
new file mode 100644
index 0000000..23a126c
--- /dev/null
+++ b/src/vdr-plugin/menudata.h
@@ -0,0 +1,100 @@
+/*
+ * menudata.h: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef __WEBVIDEO_MENUDATA_H
+#define __WEBVIDEO_MENUDATA_H
+
+// --- cLinkBase -----------------------------------------------------------
+
+class cLinkBase {
+public:
+ virtual ~cLinkBase() {}; // avoids "virtual functions but
+ // non-virtual destructor" warning
+
+ virtual char *GetURL() = 0;
+};
+
+// --- cQueryData ----------------------------------------------------------
+
+class cQueryData {
+private:
+ char *name;
+
+public:
+ cQueryData(const char *Name);
+ virtual ~cQueryData();
+
+ const char *GetName() { return name; }
+ virtual char *GetQueryFragment() = 0;
+};
+
+// --- cSimpleLink ---------------------------------------------------------
+
+class cSimpleLink : public cLinkBase {
+private:
+ char *ref;
+public:
+ cSimpleLink(const char *ref);
+ virtual ~cSimpleLink();
+
+ virtual char *GetURL();
+};
+
+// --- cTextFieldData ------------------------------------------------------
+
+class cTextFieldData : public cQueryData {
+private:
+ char *name;
+ char *valuebuffer;
+ int valuebufferlength;
+public:
+ cTextFieldData(const char *Name, int Length);
+ virtual ~cTextFieldData();
+
+ virtual char *GetQueryFragment();
+ char *GetValue();
+ int GetLength();
+};
+
+// --- cItemListData -------------------------------------------------------
+
+class cItemListData : public cQueryData {
+private:
+ char *name;
+ int value;
+ int numstrings;
+ char **strings;
+ char **stringvalues;
+public:
+ cItemListData(const char *Name, char **Strings, char **StringValues, int NumStrings);
+ virtual ~cItemListData();
+
+ virtual char *GetQueryFragment();
+ char **GetStrings();
+ char **GetStringValues();
+ int GetNumStrings();
+ int *GetValuePtr();
+};
+
+// --- cSubmissionButtonData -----------------------------------------------
+
+class cHistoryObject;
+
+class cSubmissionButtonData : public cLinkBase {
+private:
+ char *querybase;
+ const cHistoryObject *page;
+public:
+ cSubmissionButtonData(const char *queryUrl,
+ const cHistoryObject *currentPage);
+ virtual ~cSubmissionButtonData();
+
+ virtual char *GetURL();
+};
+
+#endif
diff --git a/src/vdr-plugin/mime.types b/src/vdr-plugin/mime.types
new file mode 100644
index 0000000..beefdc3
--- /dev/null
+++ b/src/vdr-plugin/mime.types
@@ -0,0 +1,4 @@
+# Some non-standard, but common, MIME types
+
+video/flv flv
+video/x-flv flv
diff --git a/src/vdr-plugin/mimetypes.c b/src/vdr-plugin/mimetypes.c
new file mode 100644
index 0000000..17c29e6
--- /dev/null
+++ b/src/vdr-plugin/mimetypes.c
@@ -0,0 +1,98 @@
+/*
+ * mimetypes.c: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <vdr/tools.h>
+#include "mimetypes.h"
+#include "common.h"
+
+// --- cMimeListObject -----------------------------------------------------
+
+cMimeListObject::cMimeListObject(const char *mimetype, const char *extension) {
+ type = strdup(mimetype);
+ ext = strdup(extension);
+}
+
+cMimeListObject::~cMimeListObject() {
+ free(type);
+ free(ext);
+}
+
+// --- cMimeTypes ----------------------------------------------------------
+
+cMimeTypes::cMimeTypes(const char **mimetypefiles) {
+ for (const char **filename=mimetypefiles; *filename; filename++) {
+ FILE *f = fopen(*filename, "r");
+ if (!f) {
+ LOG_ERROR_STR((const char *)cString::sprintf("failed to open mime type file %s", *filename));
+ continue;
+ }
+
+ cReadLine rl;
+ char *line = rl.Read(f);
+ while (line) {
+ // Comment lines starting with '#' and empty lines are skipped
+ // Expected format for the lines:
+ // mime/type ext
+ if (*line && (*line != '#')) {
+ char *ptr = line;
+ while ((*ptr != '\0') && (!isspace(*ptr)))
+ ptr++;
+
+ if (ptr == line) {
+ // empty line, ignore
+ line = rl.Read(f);
+ continue;
+ }
+
+ char *mimetype = (char *)malloc(ptr-line+1);
+ strncpy(mimetype, line, ptr-line);
+ mimetype[ptr-line] = '\0';
+
+ while (*ptr && isspace(*ptr))
+ ptr++;
+ char *eptr = ptr;
+ while (*ptr && !isspace(*ptr))
+ ptr++;
+
+ if (ptr == eptr) {
+ // no extension, ignore
+ free(mimetype);
+ line = rl.Read(f);
+ continue;
+ }
+
+ char *extension = (char *)malloc(ptr-eptr+1);
+ strncpy(extension, eptr, ptr-eptr);
+ extension[ptr-eptr] = '\0';
+
+ types.Add(new cMimeListObject(mimetype, extension));
+ free(extension);
+ free(mimetype);
+ }
+ line = rl.Read(f);
+ }
+
+ fclose(f);
+ }
+}
+
+char *cMimeTypes::ExtensionFromMimeType(const char *mimetype) {
+ if (!mimetype)
+ return NULL;
+
+ for (cMimeListObject *m = types.First(); m; m = types.Next(m))
+ if (strcmp(m->GetType(), mimetype) == 0) {
+ return strdup(m->GetExtension());
+ }
+
+ return NULL;
+}
diff --git a/src/vdr-plugin/mimetypes.h b/src/vdr-plugin/mimetypes.h
new file mode 100644
index 0000000..76e735b
--- /dev/null
+++ b/src/vdr-plugin/mimetypes.h
@@ -0,0 +1,35 @@
+/*
+ * mimetypes.h: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef __WEBVIDEO_MIMETYPES_H
+#define __WEBVIDEO_MIMETYPES_H
+
+class cMimeListObject : public cListObject {
+private:
+ char *type;
+ char *ext;
+public:
+ cMimeListObject(const char *mimetype, const char *extension);
+ ~cMimeListObject();
+
+ char *GetType() { return type; };
+ char *GetExtension() { return ext; };
+};
+
+class cMimeTypes {
+private:
+ cList<cMimeListObject> types;
+public:
+ cMimeTypes(const char **filenames);
+
+ char *ExtensionFromMimeType(const char *mimetype);
+};
+
+extern cMimeTypes *MimeTypes;
+
+#endif
diff --git a/src/vdr-plugin/player.c b/src/vdr-plugin/player.c
new file mode 100644
index 0000000..42bd56e
--- /dev/null
+++ b/src/vdr-plugin/player.c
@@ -0,0 +1,73 @@
+/*
+ * player.c: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <vdr/plugin.h>
+#include "player.h"
+#include "common.h"
+
+bool cXineliboutputPlayer::Launch(const char *url) {
+ debug("launching xinelib player, url = %s", url);
+
+ /*
+ * xineliboutput plugin insists on percent encoding (certain
+ * characters in) the URL. A properly encoded URL will get broken if
+ * we let xineliboutput to encode it the second time. For example,
+ * current (Feb 2009) Youtube URLs are affected by this. We will
+ * decode the URL before passing it to xineliboutput to fix Youtube
+ *
+ * On the other hand, some URLs will get broken if the encoding is
+ * removed here. There simply isn't a way to make all URLs work
+ * because of the way xineliboutput handles the encoding.
+ */
+ char *decoded = URLdecode(url);
+ debug("decoded = %s", decoded);
+ bool ret = cPluginManager::CallFirstService("MediaPlayer-1.0", (void *)decoded);
+ free(decoded);
+ return ret;
+}
+
+bool cMPlayerPlayer::Launch(const char *url) {
+ /*
+ * This code for launching mplayer plugin is just for testing, and
+ * most likely does not work.
+ */
+
+ debug("launching MPlayer");
+ warning("Support for MPlayer is experimental. Don't expect this to work!");
+
+ struct MPlayerServiceData
+ {
+ int result;
+ union
+ {
+ const char *filename;
+ } data;
+ };
+
+ const char* const tmpPlayListFileName = "/tmp/webvideo.m3u";
+ FILE *f = fopen(tmpPlayListFileName, "w");
+ fwrite(url, strlen(url), 1, f);
+ fclose(f);
+
+ MPlayerServiceData mplayerdata;
+ mplayerdata.data.filename = tmpPlayListFileName;
+
+ if (!cPluginManager::CallFirstService("MPlayer-Play-v1", &mplayerdata)) {
+ debug("Failed to locate Mplayer service");
+ return false;
+ }
+
+ if (!mplayerdata.result) {
+ debug("Mplayer service failed");
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/vdr-plugin/player.h b/src/vdr-plugin/player.h
new file mode 100644
index 0000000..dbaf448
--- /dev/null
+++ b/src/vdr-plugin/player.h
@@ -0,0 +1,29 @@
+/*
+ * menu.h: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef __WEBVIDEO_PLAYER_H
+#define __WEBVIDEO_PLAYER_H
+
+class cMediaPlayer {
+public:
+ virtual ~cMediaPlayer() {};
+ virtual bool Launch(const char *url) = 0;
+};
+
+class cXineliboutputPlayer : public cMediaPlayer {
+public:
+ bool Launch(const char *url);
+};
+
+class cMPlayerPlayer : public cMediaPlayer {
+public:
+ bool Launch(const char *url);
+};
+
+
+#endif
diff --git a/src/vdr-plugin/po/de_DE.po b/src/vdr-plugin/po/de_DE.po
new file mode 100644
index 0000000..f096ba9
--- /dev/null
+++ b/src/vdr-plugin/po/de_DE.po
@@ -0,0 +1,137 @@
+# German translations for webvideo package.
+# Copyright (C) 2009 THE webvideo'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the webvideo package.
+# Antti Ajanki <antti.ajanki@iki.fi>, 2009.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: webvideo 0.1.1\n"
+"Report-Msgid-Bugs-To: <see README>\n"
+"POT-Creation-Date: 2010-07-09 15:12+0300\n"
+"PO-Revision-Date: 2009-02-18 20:04+0200\n"
+"Last-Translator: <cnc@gmx.de>\n"
+"Language-Team: German\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: \n"
+
+msgid "No streams on this page, create timer anyway?"
+msgstr ""
+
+msgid "Downloading in the background"
+msgstr "Download im Hintergrund"
+
+msgid "Starting player..."
+msgstr "Player wird gestartet..."
+
+msgid "Retrieving..."
+msgstr "Abrufen..."
+
+msgid "Back"
+msgstr "Zurück"
+
+msgid "Forward"
+msgstr "Vor"
+
+msgid "Create timer"
+msgstr ""
+
+msgid "Play"
+msgstr "Play"
+
+msgid "Status"
+msgstr "Status"
+
+msgid "Timers"
+msgstr ""
+
+msgid "Unfinished downloads"
+msgstr "Nicht beendete Downloads"
+
+msgid "No active downloads"
+msgstr "Kein aktiver Download"
+
+#. TRANSLATORS: at most 5 characters
+msgid "Error"
+msgstr ""
+
+msgid "Error details"
+msgstr ""
+
+msgid "Download details"
+msgstr ""
+
+msgid "Remove"
+msgstr ""
+
+msgid "Abort"
+msgstr "Abbruch"
+
+msgid "Edit timer"
+msgstr ""
+
+msgid "Title"
+msgstr ""
+
+msgid "Once per day"
+msgstr ""
+
+msgid "Once per week"
+msgstr ""
+
+msgid "Once per month"
+msgstr ""
+
+msgid "Update interval"
+msgstr ""
+
+msgid "Execute now"
+msgstr ""
+
+#. TRANSLATORS: at most 24 chars
+msgid "Never"
+msgstr ""
+
+msgid "Last fetched:"
+msgstr ""
+
+msgid "Error on last refresh!"
+msgstr ""
+
+msgid "Timer running, can't remove"
+msgstr ""
+
+msgid "Remove timer?"
+msgstr ""
+
+#, fuzzy
+msgid "Aborted"
+msgstr "Abbruch"
+
+msgid "Download video files from the web"
+msgstr "Download Video Files aus dem Web"
+
+msgid "Streaming failed: no URL"
+msgstr "Streaming fehlgeschlagen: Keine URL"
+
+msgid "Failed to launch media player"
+msgstr "Media Player konnte nicht gestartet werden"
+
+msgid "timer"
+msgstr ""
+
+#, c-format
+msgid "One download completed, %d remains%s"
+msgstr "Ein Download komplett, %d verbleibend%s"
+
+msgid "Download aborted"
+msgstr ""
+
+#, c-format
+msgid "Download failed (error = %d)"
+msgstr "Download fehlgeschlagen (Error = %d)"
+
+#, c-format
+msgid "%d downloads not finished"
+msgstr "%d laufende Downloads"
diff --git a/src/vdr-plugin/po/fi_FI.po b/src/vdr-plugin/po/fi_FI.po
new file mode 100644
index 0000000..6e6df2f
--- /dev/null
+++ b/src/vdr-plugin/po/fi_FI.po
@@ -0,0 +1,137 @@
+# Finnish translations for webvideo package.
+# Copyright (C) 2008,2009 THE webvideo'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the webvideo package.
+# Antti Ajanki <antti.ajanki@iki.fi>, 2008,2009.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: webvideo 0.1.1\n"
+"Report-Msgid-Bugs-To: <see README>\n"
+"POT-Creation-Date: 2010-07-09 15:12+0300\n"
+"PO-Revision-Date: 2008-06-07 18:03+0300\n"
+"Last-Translator: Antti Ajanki <antti.ajanki@iki.fi>\n"
+"Language-Team: Finnish\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "No streams on this page, create timer anyway?"
+msgstr "Luo ajastin vaikka tällä sivulla ei videoita?"
+
+msgid "Downloading in the background"
+msgstr "Ladataan taustalla"
+
+msgid "Starting player..."
+msgstr "Käynnistetään toistin..."
+
+msgid "Retrieving..."
+msgstr "Ladataan..."
+
+msgid "Back"
+msgstr "Peruuta"
+
+msgid "Forward"
+msgstr "Eteenpäin"
+
+msgid "Create timer"
+msgstr "Luo ajastin"
+
+msgid "Play"
+msgstr "Toista"
+
+msgid "Status"
+msgstr "Tila"
+
+msgid "Timers"
+msgstr "Ajastimet"
+
+msgid "Unfinished downloads"
+msgstr "Ladattavat tiedostot"
+
+msgid "No active downloads"
+msgstr "Ei keskeneräisia latauksia"
+
+#. TRANSLATORS: at most 5 characters
+msgid "Error"
+msgstr "Virhe"
+
+msgid "Error details"
+msgstr "Virhe"
+
+msgid "Download details"
+msgstr "Latauksen tiedot"
+
+msgid "Remove"
+msgstr "Poista"
+
+msgid "Abort"
+msgstr "Keskeytä"
+
+msgid "Edit timer"
+msgstr "Muokkaa ajastinta"
+
+msgid "Title"
+msgstr "Nimi"
+
+msgid "Once per day"
+msgstr "Kerran päivässä"
+
+msgid "Once per week"
+msgstr "Kerran viikossa"
+
+msgid "Once per month"
+msgstr "Kerran kuussa"
+
+msgid "Update interval"
+msgstr "Päivitystahti"
+
+msgid "Execute now"
+msgstr "Suorita nyt"
+
+#. TRANSLATORS: at most 24 chars
+msgid "Never"
+msgstr "Ei koskaan"
+
+msgid "Last fetched:"
+msgstr "Viimeisin päivitys"
+
+msgid "Error on last refresh!"
+msgstr "Virhe edellisessä päivityksessä"
+
+msgid "Timer running, can't remove"
+msgstr "Poisto ei onnistu, koska ajastin on käynnissä"
+
+msgid "Remove timer?"
+msgstr "Poista ajastin?"
+
+msgid "Aborted"
+msgstr "Keskeytetty"
+
+msgid "Download video files from the web"
+msgstr "Lataa videotiedostoja Internetistä"
+
+msgid "Streaming failed: no URL"
+msgstr "Toisto epäonnistui: ei URLia"
+
+msgid "Failed to launch media player"
+msgstr "Toistimen käynnistäminen epäonnistui"
+
+msgid "timer"
+msgstr "ajastin"
+
+#, c-format
+msgid "One download completed, %d remains%s"
+msgstr "Yksi tiedosto ladattu, %d jäljellä%s"
+
+msgid "Download aborted"
+msgstr "Lataaminen keskeytetty"
+
+#, c-format
+msgid "Download failed (error = %d)"
+msgstr "Lataus epäonnistui (virhe = %d)"
+
+#, c-format
+msgid "%d downloads not finished"
+msgstr "%d tiedostoa lataamatta"
diff --git a/src/vdr-plugin/po/fr_FR.po b/src/vdr-plugin/po/fr_FR.po
new file mode 100644
index 0000000..79f31b5
--- /dev/null
+++ b/src/vdr-plugin/po/fr_FR.po
@@ -0,0 +1,156 @@
+# French translations for webvideo package.
+# Copyright (C) 2008 THE webvideo'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the webvideo package.
+# Bruno ROUSSEL <bruno.roussel@free.fr>, 2008.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: webvideo 0.0.5\n"
+"Report-Msgid-Bugs-To: <see README>\n"
+"POT-Creation-Date: 2010-07-09 15:12+0300\n"
+"PO-Revision-Date: 2008-09-08 20:34+0100\n"
+"Last-Translator: Bruno ROUSSEL <bruno.roussel@free.fr>\n"
+"Language-Team: French\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "No streams on this page, create timer anyway?"
+msgstr ""
+
+msgid "Downloading in the background"
+msgstr "Téléchargement en tâche de fond"
+
+msgid "Starting player..."
+msgstr ""
+
+msgid "Retrieving..."
+msgstr "Récupération..."
+
+msgid "Back"
+msgstr "Arrière"
+
+msgid "Forward"
+msgstr "Avant"
+
+msgid "Create timer"
+msgstr ""
+
+msgid "Play"
+msgstr ""
+
+msgid "Status"
+msgstr "Status"
+
+msgid "Timers"
+msgstr ""
+
+msgid "Unfinished downloads"
+msgstr ""
+
+msgid "No active downloads"
+msgstr ""
+
+#. TRANSLATORS: at most 5 characters
+msgid "Error"
+msgstr ""
+
+msgid "Error details"
+msgstr ""
+
+#, fuzzy
+msgid "Download details"
+msgstr "Status du téléchargement"
+
+msgid "Remove"
+msgstr ""
+
+msgid "Abort"
+msgstr ""
+
+msgid "Edit timer"
+msgstr ""
+
+msgid "Title"
+msgstr ""
+
+msgid "Once per day"
+msgstr ""
+
+msgid "Once per week"
+msgstr ""
+
+msgid "Once per month"
+msgstr ""
+
+msgid "Update interval"
+msgstr ""
+
+msgid "Execute now"
+msgstr ""
+
+#. TRANSLATORS: at most 24 chars
+msgid "Never"
+msgstr ""
+
+msgid "Last fetched:"
+msgstr ""
+
+msgid "Error on last refresh!"
+msgstr ""
+
+msgid "Timer running, can't remove"
+msgstr ""
+
+msgid "Remove timer?"
+msgstr ""
+
+msgid "Aborted"
+msgstr ""
+
+msgid "Download video files from the web"
+msgstr "Téléchargement du fichier vidéo depuis le web"
+
+msgid "Streaming failed: no URL"
+msgstr ""
+
+msgid "Failed to launch media player"
+msgstr ""
+
+msgid "timer"
+msgstr ""
+
+#, c-format
+msgid "One download completed, %d remains%s"
+msgstr "Un téléchargement terminé, il en reste %d%s"
+
+msgid "Download aborted"
+msgstr ""
+
+#, c-format
+msgid "Download failed (error = %d)"
+msgstr "Erreur de téléchargement (Erreur = %d)"
+
+#, c-format
+msgid "%d downloads not finished"
+msgstr "%d téléchargement(s) non terminé(s)."
+
+#~ msgid "<No title>"
+#~ msgstr "<Pas de titre>"
+
+#~ msgid "Can't download web page!"
+#~ msgstr "Impossible de télécharger la page web !"
+
+#~ msgid "XSLT transformation produced no URL!"
+#~ msgstr "La conversion XSLT n'a pas généré d'URL !"
+
+#~ msgid "XSLT transformation failed."
+#~ msgstr "Erreur de conversion XSLT."
+
+#~ msgid "Unknown error!"
+#~ msgstr "Erreur inconnue !"
+
+#~ msgid "Select video source"
+#~ msgstr "Sélectionner la source vidéo"
diff --git a/src/vdr-plugin/po/it_IT.po b/src/vdr-plugin/po/it_IT.po
new file mode 100644
index 0000000..c7f1f00
--- /dev/null
+++ b/src/vdr-plugin/po/it_IT.po
@@ -0,0 +1,158 @@
+# Italian translations for webvideo package.
+# Copyright (C) 2008 THE webvideo'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the webvideo package.
+# Diego Pierotto <vdr-italian@tiscali.it>, 2008.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: webvideo 0.0.1\n"
+"Report-Msgid-Bugs-To: <see README>\n"
+"POT-Creation-Date: 2010-07-09 15:12+0300\n"
+"PO-Revision-Date: 2009-04-11 01:48+0100\n"
+"Last-Translator: Diego Pierotto <vdr-italian@tiscali.it>\n"
+"Language-Team: Italian\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: \n"
+"X-Poedit-Language: Italian\n"
+"X-Poedit-Country: ITALY\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+msgid "No streams on this page, create timer anyway?"
+msgstr ""
+
+msgid "Downloading in the background"
+msgstr "Scaricamento in sottofondo"
+
+msgid "Starting player..."
+msgstr "Avvio lettore..."
+
+msgid "Retrieving..."
+msgstr "Recupero..."
+
+msgid "Back"
+msgstr "Indietro"
+
+msgid "Forward"
+msgstr "Avanti"
+
+msgid "Create timer"
+msgstr ""
+
+msgid "Play"
+msgstr "Riproduci"
+
+msgid "Status"
+msgstr "Stato"
+
+msgid "Timers"
+msgstr ""
+
+msgid "Unfinished downloads"
+msgstr "Scaricamenti non completati"
+
+msgid "No active downloads"
+msgstr "Nessun scaricamento attivo"
+
+#. TRANSLATORS: at most 5 characters
+msgid "Error"
+msgstr ""
+
+msgid "Error details"
+msgstr ""
+
+#, fuzzy
+msgid "Download details"
+msgstr "Richiesta scaricamento fallita!"
+
+msgid "Remove"
+msgstr ""
+
+msgid "Abort"
+msgstr "Annulla"
+
+msgid "Edit timer"
+msgstr ""
+
+msgid "Title"
+msgstr ""
+
+msgid "Once per day"
+msgstr ""
+
+msgid "Once per week"
+msgstr ""
+
+msgid "Once per month"
+msgstr ""
+
+msgid "Update interval"
+msgstr ""
+
+msgid "Execute now"
+msgstr ""
+
+#. TRANSLATORS: at most 24 chars
+msgid "Never"
+msgstr ""
+
+msgid "Last fetched:"
+msgstr ""
+
+msgid "Error on last refresh!"
+msgstr ""
+
+msgid "Timer running, can't remove"
+msgstr ""
+
+msgid "Remove timer?"
+msgstr ""
+
+msgid "Aborted"
+msgstr "Annullato"
+
+msgid "Download video files from the web"
+msgstr "Scarica file video dal web"
+
+msgid "Streaming failed: no URL"
+msgstr "Trasmissione fallita: nessun URL"
+
+msgid "Failed to launch media player"
+msgstr "Impossibile avviare il lettore multimediale"
+
+msgid "timer"
+msgstr ""
+
+#, c-format
+msgid "One download completed, %d remains%s"
+msgstr "Scaricamento completato, %d rimanente/i%s"
+
+msgid "Download aborted"
+msgstr "Scaricamento annullato"
+
+#, c-format
+msgid "Download failed (error = %d)"
+msgstr "Scaricamento fallito (errore = %d)"
+
+#, c-format
+msgid "%d downloads not finished"
+msgstr "%d scaricamenti non conclusi"
+
+#~ msgid "<No title>"
+#~ msgstr "<Senza titolo>"
+
+#~ msgid "Can't download web page!"
+#~ msgstr "Impossibile scaricare la pagina web!"
+
+#~ msgid "XSLT transformation produced no URL!"
+#~ msgstr "La conversione XSLT non ha generato alcun URL!"
+
+#~ msgid "XSLT transformation failed."
+#~ msgstr "Conversione XSLT fallita."
+
+#~ msgid "Unknown error!"
+#~ msgstr "Errore sconosciuto!"
+
+#~ msgid "Select video source"
+#~ msgstr "Seleziona fonte video"
diff --git a/src/vdr-plugin/request.c b/src/vdr-plugin/request.c
new file mode 100644
index 0000000..edc5432
--- /dev/null
+++ b/src/vdr-plugin/request.c
@@ -0,0 +1,432 @@
+/*
+ * request.c: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <vdr/tools.h>
+#include <vdr/i18n.h>
+#include "request.h"
+#include "common.h"
+#include "mimetypes.h"
+#include "config.h"
+#include "timer.h"
+
+// --- cDownloadProgress ---------------------------------------------------
+
+cDownloadProgress::cDownloadProgress() {
+ strcpy(name, "???");
+ downloaded = -1;
+ total = -1;
+ statusCode = -1;
+ req = NULL;
+}
+
+void cDownloadProgress::AssociateWith(cFileDownloadRequest *request) {
+ req = request;
+}
+
+void cDownloadProgress::SetContentLength(long bytes) {
+ total = bytes;
+}
+
+void cDownloadProgress::SetTitle(const char *title) {
+ cMutexLock lock(&mutex);
+
+ strncpy(name, title, NAME_LEN-1);
+ name[NAME_LEN-1] = '\0';
+}
+
+void cDownloadProgress::Progress(long downloadedbytes) {
+ // Atomic operation, no mutex needed
+ downloaded = downloadedbytes;
+}
+
+void cDownloadProgress::MarkDone(int errorcode, cString pharse) {
+ cMutexLock lock(&mutex);
+
+ statusCode = errorcode;
+ statusPharse = pharse;
+}
+
+bool cDownloadProgress::IsFinished() {
+ return statusCode != -1;
+}
+
+cString cDownloadProgress::GetTitle() {
+ cMutexLock lock(&mutex);
+
+ if (req && req->IsAborted())
+ return cString::sprintf("[%s] %s", tr("Aborted"), name);
+ else
+ return cString(name);
+}
+
+cString cDownloadProgress::GetPercentage() {
+ cMutexLock lock(&mutex);
+
+ if ((const char*)statusPharse != NULL && statusCode != 0)
+ // TRANSLATORS: at most 5 characters
+ return cString(tr("Error"));
+ else if ((downloaded < 0) || (total < 0))
+ return cString("???");
+ else
+ return cString::sprintf("%3d%%", (int) (100*(float)downloaded/total + 0.5));
+}
+
+cString cDownloadProgress::GetStatusPharse() {
+ cMutexLock lock(&mutex);
+
+ return statusPharse;
+}
+
+bool cDownloadProgress::Error() {
+ return (const char *)statusPharse != NULL;
+}
+
+// --- cProgressVector -----------------------------------------------------
+
+cDownloadProgress *cProgressVector::NewDownload() {
+ cDownloadProgress *progress = new cDownloadProgress();
+ Append(progress);
+ return progress;
+}
+
+// --- cMenuRequest --------------------------------------------------------
+
+cMenuRequest::cMenuRequest(int ID, const char *wvtreference)
+: reqID(ID), aborted(false), finished(false), status(0), webvi(-1),
+ handle(-1), timer(NULL)
+{
+ wvtref = strdup(wvtreference);
+}
+
+cMenuRequest::~cMenuRequest() {
+ if (handle != -1) {
+ if (!finished)
+ Abort();
+ webvi_delete_handle(webvi, handle);
+ }
+
+ // do not delete timer
+}
+
+ssize_t cMenuRequest::WriteCallback(const char *ptr, size_t len, void *request) {
+ cMenuRequest *instance = (cMenuRequest *)request;
+ if (instance)
+ return instance->WriteData(ptr, len);
+ else
+ return len;
+}
+
+ssize_t cMenuRequest::WriteData(const char *ptr, size_t len) {
+ return inBuffer.Put(ptr, len);
+}
+
+char *cMenuRequest::ExtractSiteName(const char *ref) {
+ if (strncmp(ref, "wvt:///", 7) != 0)
+ return NULL;
+
+ const char *first = ref+7;
+ const char *last = strchr(first, '/');
+ if (!last)
+ last = first+strlen(first);
+
+ return strndup(first, last-first);
+}
+
+void cMenuRequest::AppendQualityParamsToRef() {
+ if (!wvtref)
+ return;
+
+ char *site = ExtractSiteName(wvtref);
+ if (site) {
+ const char *min = webvideoConfig->GetMinQuality(site, GetType());
+ const char *max = webvideoConfig->GetMaxQuality(site, GetType());
+ free(site);
+
+ if (min && !max) {
+ cString newref = cString::sprintf("%s&minquality=%s", wvtref, min);
+ free(wvtref);
+ wvtref = strdup((const char *)newref);
+
+ } else if (!min && max) {
+ cString newref = cString::sprintf("%s&maxquality=%s", wvtref, max);
+ free(wvtref);
+ wvtref = strdup((const char *)newref);
+
+ } else if (min && max) {
+ cString newref = cString::sprintf("%s&minquality=%s&maxquality=%s", wvtref, min, max);
+ free(wvtref);
+ wvtref = strdup((const char *)newref);
+ }
+ }
+}
+
+WebviHandle cMenuRequest::PrepareHandle() {
+ if (handle == -1) {
+ handle = webvi_new_request(webvi, wvtref, WEBVIREQ_MENU);
+
+ if (handle != -1) {
+ webvi_set_opt(webvi, handle, WEBVIOPT_WRITEFUNC, WriteCallback);
+ webvi_set_opt(webvi, handle, WEBVIOPT_WRITEDATA, this);
+ }
+ }
+
+ return handle;
+}
+
+bool cMenuRequest::Start(WebviCtx webvictx) {
+ webvi = webvictx;
+
+ if ((PrepareHandle() != -1) && (webvi_start_handle(webvi, handle) == WEBVIERR_OK)) {
+ finished = false;
+ return true;
+ } else
+ return false;
+}
+
+void cMenuRequest::RequestDone(int errorcode, cString pharse) {
+ finished = true;
+ status = errorcode;
+ statusPharse = pharse;
+}
+
+void cMenuRequest::Abort() {
+ if (finished || handle == -1)
+ return;
+
+ aborted = true;
+ webvi_stop_handle(webvi, handle);
+};
+
+bool cMenuRequest::Success() {
+ return status == 0;
+}
+
+cString cMenuRequest::GetStatusPharse() {
+ return statusPharse;
+}
+
+cString cMenuRequest::GetResponse() {
+ size_t len = inBuffer.Length();
+ const char *src = inBuffer.Get();
+ char *buf = (char *)malloc((len+1)*sizeof(char));
+ strncpy(buf, src, len);
+ buf[len] = '\0';
+ return cString(buf, true);
+}
+
+// --- cFileDownloadRequest ------------------------------------------------
+
+cFileDownloadRequest::cFileDownloadRequest(int ID, const char *streamref,
+ const char *destdir,
+ cDownloadProgress *progress)
+: cMenuRequest(ID, streamref), title(NULL), bytesDownloaded(0),
+ contentLength(-1), destfile(NULL), progressUpdater(progress)
+{
+ this->destdir = strdup(destdir);
+ if (progressUpdater)
+ progressUpdater->AssociateWith(this);
+
+ AppendQualityParamsToRef();
+}
+
+cFileDownloadRequest::~cFileDownloadRequest() {
+ if (destfile) {
+ destfile->Close();
+ delete destfile;
+ }
+ if (destdir)
+ free(destdir);
+ if (title)
+ free(title);
+ // do not delete progressUpdater
+}
+
+WebviHandle cFileDownloadRequest::PrepareHandle() {
+ if (handle == -1) {
+ handle = webvi_new_request(webvi, wvtref, WEBVIREQ_FILE);
+
+ if (handle != -1) {
+ webvi_set_opt(webvi, handle, WEBVIOPT_WRITEFUNC, WriteCallback);
+ webvi_set_opt(webvi, handle, WEBVIOPT_WRITEDATA, this);
+ }
+ }
+
+ return handle;
+}
+
+ssize_t cFileDownloadRequest::WriteData(const char *ptr, size_t len) {
+ if (!destfile) {
+ if (!OpenDestFile())
+ return -1;
+ }
+
+ bytesDownloaded += len;
+ if (progressUpdater)
+ progressUpdater->Progress(bytesDownloaded);
+
+ return destfile->Write(ptr, len);
+}
+
+bool cFileDownloadRequest::OpenDestFile() {
+ char *contentType;
+ char *url;
+ char *ext;
+ cString destfilename;
+ int fd, i;
+
+ if (handle == -1) {
+ error("handle == -1 while trying to open destination file");
+ return false;
+ }
+
+ if (destfile)
+ delete destfile;
+
+ destfile = new cUnbufferedFile;
+
+ webvi_get_info(webvi, handle, WEBVIINFO_URL, &url);
+ webvi_get_info(webvi, handle, WEBVIINFO_STREAM_TITLE, &title);
+ webvi_get_info(webvi, handle, WEBVIINFO_CONTENT_TYPE, &contentType);
+ webvi_get_info(webvi, handle, WEBVIINFO_CONTENT_LENGTH, &contentLength);
+
+ if (!contentType || !url) {
+ if(contentType)
+ free(contentType);
+ if (url)
+ free(url);
+
+ error("no content type or url, can't infer extension");
+ return false;
+ }
+
+ ext = GetExtension(contentType, url);
+
+ free(url);
+ free(contentType);
+
+ char *basename = strdup(title ? title : "???");
+ basename = safeFilename(basename);
+
+ i = 1;
+ destfilename = cString::sprintf("%s/%s%s", destdir, basename, ext);
+ while (true) {
+ debug("trying to open %s", (const char *)destfilename);
+
+ fd = destfile->Open(destfilename, O_WRONLY | O_CREAT | O_EXCL, DEFFILEMODE);
+
+ if (fd == -1 && errno == EEXIST)
+ destfilename = cString::sprintf("%s/%s-%d%s", destdir, basename, i++, ext);
+ else
+ break;
+ };
+
+ free(basename);
+ free(ext);
+
+ if (fd < 0) {
+ error("Failed to open file %s: %m", (const char *)destfilename);
+ delete destfile;
+ destfile = NULL;
+ return false;
+ }
+
+ info("Saving to %s", (const char *)destfilename);
+
+ if (progressUpdater) {
+ progressUpdater->SetTitle(title);
+ progressUpdater->SetContentLength(contentLength);
+ }
+
+ return true;
+}
+
+char *cFileDownloadRequest::GetExtension(const char *contentType, const char *url) {
+ // Get extension from Content-Type
+ char *ext = NULL;
+ char *ext2 = MimeTypes->ExtensionFromMimeType(contentType);
+
+ // Workaround for buggy servers: If the server claims that the mime
+ // type is text/plain, ignore the server and fall back to extracting
+ // the extension from the URL. This function should be called only
+ // for video, audio or ASX files and therefore text/plain is clearly
+ // incorrect.
+ if (ext2 && contentType && !strcasecmp(contentType, "text/plain")) {
+ debug("Ignoring content type text/plain, getting extension from url.");
+ free(ext2);
+ ext2 = NULL;
+ }
+
+ if (ext2) {
+ // Append dot in the start of the extension
+ ext = (char *)malloc(strlen(ext2)+2);
+ ext[0] = '.';
+ ext[1] = '\0';
+ strcat(ext, ext2);
+ free(ext2);
+ return ext;
+ }
+
+ // Get extension from URL
+ ext = extensionFromUrl(url);
+ if (ext)
+ return ext;
+
+ // No extension!
+ return strdup("");
+}
+
+void cFileDownloadRequest::RequestDone(int errorcode, cString pharse) {
+ cMenuRequest::RequestDone(errorcode, pharse);
+ if (progressUpdater)
+ progressUpdater->MarkDone(errorcode, pharse);
+ if (destfile)
+ destfile->Close();
+}
+
+// --- cStreamUrlRequest ---------------------------------------------------
+
+cStreamUrlRequest::cStreamUrlRequest(int ID, const char *ref)
+: cMenuRequest(ID, ref) {
+ AppendQualityParamsToRef();
+}
+
+WebviHandle cStreamUrlRequest::PrepareHandle() {
+ if (handle == -1) {
+ handle = webvi_new_request(webvi, wvtref, WEBVIREQ_STREAMURL);
+
+ if (handle != -1) {
+ webvi_set_opt(webvi, handle, WEBVIOPT_WRITEFUNC, WriteCallback);
+ webvi_set_opt(webvi, handle, WEBVIOPT_WRITEDATA, this);
+ }
+ }
+
+ return handle;
+}
+
+// --- cTimerRequest -------------------------------------------------------
+
+cTimerRequest::cTimerRequest(int ID, const char *ref)
+: cMenuRequest(ID, ref)
+{
+}
+
+// --- cRequestVector ------------------------------------------------------
+
+cMenuRequest *cRequestVector::FindByHandle(WebviHandle handle) {
+ for (int i=0; i<Size(); i++)
+ if (At(i)->GetHandle() == handle)
+ return At(i);
+
+ return NULL;
+}
diff --git a/src/vdr-plugin/request.h b/src/vdr-plugin/request.h
new file mode 100644
index 0000000..f481fc8
--- /dev/null
+++ b/src/vdr-plugin/request.h
@@ -0,0 +1,170 @@
+/*
+ * request.h: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef __WEBVIDEO_REQUEST_H
+#define __WEBVIDEO_REQUEST_H
+
+#include <vdr/tools.h>
+#include <vdr/thread.h>
+#include <libwebvi.h>
+#include "buffer.h"
+
+enum eRequestType { REQT_NONE, REQT_MENU, REQT_FILE, REQT_STREAM, REQT_TIMER };
+
+class cFileDownloadRequest;
+class cWebviTimer;
+
+// --- cDownloadProgress ---------------------------------------------------
+
+class cDownloadProgress {
+private:
+ const static int NAME_LEN = 128;
+
+ char name[NAME_LEN];
+ long downloaded;
+ long total;
+ int statusCode;
+ cString statusPharse;
+ cFileDownloadRequest *req;
+ cMutex mutex;
+public:
+ cDownloadProgress();
+
+ void AssociateWith(cFileDownloadRequest *request);
+ void SetContentLength(long bytes);
+ void SetTitle(const char *title);
+ void Progress(long downloadedbytes);
+ void MarkDone(int errorcode, cString pharse);
+ bool IsFinished();
+
+ cString GetTitle();
+ cString GetPercentage();
+ cString GetStatusPharse();
+ bool Error();
+ cFileDownloadRequest *GetRequest() { return req; }
+};
+
+// --- cProgressVector -----------------------------------------------------
+
+class cProgressVector : public cVector<cDownloadProgress *> {
+public:
+ cDownloadProgress *NewDownload();
+};
+
+// --- cMenuRequest ----------------------------------------------------
+
+class cMenuRequest {
+private:
+ int reqID;
+ bool aborted;
+ bool finished;
+ int status;
+ cString statusPharse;
+
+protected:
+ WebviCtx webvi;
+ WebviHandle handle;
+ char *wvtref;
+ cMemoryBuffer inBuffer;
+ cWebviTimer *timer;
+
+ virtual ssize_t WriteData(const char *ptr, size_t len);
+ virtual WebviHandle PrepareHandle();
+ static ssize_t WriteCallback(const char *ptr, size_t len, void *request);
+
+ char *ExtractSiteName(const char *ref);
+ void AppendQualityParamsToRef();
+
+public:
+ cMenuRequest(int ID, const char *wvtreference);
+ virtual ~cMenuRequest();
+
+ int GetID() { return reqID; }
+ WebviHandle GetHandle() { return handle; }
+ const char *GetReference() { return wvtref; }
+
+ bool Start(WebviCtx webvictx);
+ virtual void RequestDone(int errorcode, cString pharse);
+ bool IsFinished() { return finished; }
+ void Abort();
+ bool IsAborted() { return aborted; }
+
+ // Return true if the lastest status code indicates success.
+ bool Success();
+ // Return the status code
+ int GetStatusCode() { return status; }
+ // Return the response pharse
+ cString GetStatusPharse();
+
+ virtual eRequestType GetType() { return REQT_MENU; }
+
+ // Return the content of the reponse message
+ virtual cString GetResponse();
+
+ void SetTimer(cWebviTimer *t) { timer = t; }
+ cWebviTimer *GetTimer() { return timer; }
+};
+
+// --- cFileDownloadRequest ------------------------------------------------
+
+class cFileDownloadRequest : public cMenuRequest {
+private:
+ char *destdir;
+ char *title;
+ long bytesDownloaded;
+ long contentLength;
+ cUnbufferedFile *destfile;
+ cDownloadProgress *progressUpdater;
+
+protected:
+ virtual WebviHandle PrepareHandle();
+ virtual ssize_t WriteData(const char *ptr, size_t len);
+ bool OpenDestFile();
+ char *GetExtension(const char *contentType, const char *url);
+
+public:
+ cFileDownloadRequest(int ID, const char *streamref,
+ const char *destdir,
+ cDownloadProgress *progress);
+ virtual ~cFileDownloadRequest();
+
+ eRequestType GetType() { return REQT_FILE; }
+ void RequestDone(int errorcode, cString pharse);
+};
+
+// --- cStreamUrlRequest ---------------------------------------------------
+
+class cStreamUrlRequest : public cMenuRequest {
+protected:
+ virtual WebviHandle PrepareHandle();
+
+public:
+ cStreamUrlRequest(int ID, const char *ref);
+
+ eRequestType GetType() { return REQT_STREAM; }
+};
+
+// --- cTimerRequest -------------------------------------------------------
+
+class cTimerRequest : public cMenuRequest {
+public:
+ cTimerRequest(int ID, const char *ref);
+
+ eRequestType GetType() { return REQT_TIMER; }
+};
+
+// --- cRequestVector ------------------------------------------------------
+
+class cRequestVector : public cVector<cMenuRequest *> {
+public:
+ cRequestVector(int Allocated = 10) : cVector<cMenuRequest *>(Allocated) {}
+
+ cMenuRequest *FindByHandle(WebviHandle handle);
+};
+
+#endif
diff --git a/src/vdr-plugin/timer.c b/src/vdr-plugin/timer.c
new file mode 100644
index 0000000..f9fef59
--- /dev/null
+++ b/src/vdr-plugin/timer.c
@@ -0,0 +1,465 @@
+/*
+ * timer.c: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <string.h>
+#include <errno.h>
+#include <libxml/parser.h>
+#include "timer.h"
+#include "request.h"
+#include "common.h"
+#include "download.h"
+#include "config.h"
+
+// --- cWebviTimer -----------------------------------------------
+
+cWebviTimer::cWebviTimer(int ID, const char *title,
+ const char *ref, cWebviTimerManager *manager,
+ time_t last, int interval, bool success,
+ const char *errmsg)
+ : id(ID), title(title ? strdup(title) : strdup("???")),
+ reference(ref ? strdup(ref) : NULL), lastUpdate(last),
+ interval(interval), running(false), lastSucceeded(success),
+ lastError(errmsg ? strdup(errmsg) : NULL),
+ parent(manager)
+{
+}
+
+cWebviTimer::~cWebviTimer() {
+ if(title)
+ free(title);
+ if (reference)
+ free(reference);
+ if (lastError)
+ free(lastError);
+}
+
+void cWebviTimer::SetTitle(const char *newTitle) {
+ if (title)
+ free(title);
+ title = newTitle ? strdup(newTitle) : strdup("???");
+
+ parent->SetModified();
+}
+
+void cWebviTimer::SetInterval(int interval) {
+ if (interval < MIN_TIMER_INTERVAL)
+ this->interval = MIN_TIMER_INTERVAL;
+ else
+ this->interval = interval;
+
+ parent->SetModified();
+}
+
+int cWebviTimer::GetInterval() const {
+ return interval;
+}
+
+time_t cWebviTimer::NextUpdate() const {
+ int delta = interval;
+
+ // Retry again soon if the last try failed
+ if (!lastSucceeded && delta > RETRY_TIMER_INTERVAL)
+ delta = RETRY_TIMER_INTERVAL;
+
+ return lastUpdate + delta;
+}
+
+void cWebviTimer::Execute() {
+ if (running) {
+ debug("previous instance of this timer is still running");
+ return;
+ }
+
+ info("Executing timer \"%s\"", title);
+
+ running = true;
+ cTimerRequest *req = new cTimerRequest(id, reference);
+ req->SetTimer(this);
+ cWebviThread::Instance().AddRequest(req);
+
+ lastUpdate = time(NULL);
+ SetError("Unfinished");
+ parent->SetModified();
+
+ activeStreams.Clear();
+}
+
+void cWebviTimer::SetError(const char *errmsg) {
+ bool oldSuccess = lastSucceeded;
+
+ if (lastError)
+ free(lastError);
+ lastError = NULL;
+
+ if (errmsg) {
+ lastSucceeded = false;
+ lastError = strdup(errmsg);
+ } else {
+ lastSucceeded = true;
+ }
+
+ if (oldSuccess != lastSucceeded)
+ parent->SetModified();
+}
+
+const char *cWebviTimer::LastError() const {
+ return lastError ? lastError : "";
+}
+
+void cWebviTimer::DownloadStreams(const char *menuxml, cProgressVector& summaries) {
+ if (!menuxml) {
+ SetError("xml == NULL");
+ return;
+ }
+
+ xmlDocPtr doc = xmlParseMemory(menuxml, strlen(menuxml));
+ if (!doc) {
+ xmlErrorPtr xmlerr = xmlGetLastError();
+ if (xmlerr)
+ error("libxml error: %s", xmlerr->message);
+ SetError(xmlerr->message);
+ return;
+ }
+
+ xmlNodePtr node = xmlDocGetRootElement(doc);
+ if (node)
+ node = node->xmlChildrenNode;
+
+ while (node) {
+ if (!xmlStrcmp(node->name, BAD_CAST "link")) {
+ xmlNodePtr node2 = node->children;
+
+ while(node2) {
+ if (!xmlStrcmp(node2->name, BAD_CAST "stream")) {
+ xmlChar *streamref = xmlNodeListGetString(doc, node2->xmlChildrenNode, 1);
+ const char *ref = (const char *)streamref;
+
+ if (parent->AlreadyDownloaded(ref)) {
+ debug("timer: %s has already been downloaded", ref);
+ } else if (*ref) {
+ info("timer: downloading %s", ref);
+
+ activeStreams.Append(strdup(ref));
+ cFileDownloadRequest *req = \
+ new cFileDownloadRequest(REQ_ID_TIMER, ref,
+ webvideoConfig->GetDownloadPath(),
+ summaries.NewDownload());
+ req->SetTimer(this);
+ cWebviThread::Instance().AddRequest(req);
+ }
+
+ xmlFree(streamref);
+ }
+
+ node2 = node2->next;
+ }
+ }
+
+ node = node->next;
+ }
+
+ xmlFreeDoc(doc);
+
+ if (activeStreams.Size() == 0) {
+ SetError(NULL);
+ running = false;
+ }
+}
+
+void cWebviTimer::CheckFailed(const char *errmsg) {
+ SetError(errmsg);
+ running = false;
+}
+
+void cWebviTimer::RequestFinished(const char *ref, const char *errmsg) {
+ if (errmsg && !lastError)
+ SetError(errmsg);
+
+ if (ref) {
+ if (parent)
+ parent->MarkDownloaded(ref);
+
+ int i = activeStreams.Find(ref);
+ if (i != -1) {
+ free(activeStreams[i]);
+ activeStreams.Remove(i);
+ }
+ }
+
+ if (activeStreams.Size() == 0) {
+ info("timer \"%s\" done", title);
+ running = false;
+ } else {
+ debug("timer %s is still downloading %d streams", reference, activeStreams.Size());
+ }
+}
+
+// --- cWebviTimerManager ----------------------------------------
+
+cWebviTimerManager::cWebviTimerManager()
+ : nextID(1), modified(false), disableSaving(false)
+{
+}
+
+cWebviTimerManager &cWebviTimerManager::Instance() {
+ static cWebviTimerManager instance;
+
+ return instance;
+}
+
+void cWebviTimerManager::LoadTimers(FILE *f) {
+ cReadLine rl;
+ long lastRefresh;
+ int interval;
+ int success;
+ char *ref;
+ const char *ver;
+ const char *title;
+ const char *errmsg;
+ int n, i;
+
+ ver = rl.Read(f);
+ if (strcmp(ver, "# WVTIMER1") != 0) {
+ error("Can't load timers. Unknown format: %s", ver);
+ disableSaving = true;
+ return;
+ }
+
+ i = 1;
+ while (true) {
+ n = fscanf(f, "%ld %d %d %ms", &lastRefresh, &interval, &success, &ref);
+ if (n != 4) {
+ if (n != EOF) {
+ error("Error while reading webvi timers file");
+ } else if (ferror(f)) {
+ LOG_ERROR_STR("webvi timers file");
+ }
+
+ break;
+ }
+
+ title = rl.Read(f);
+ title = title ? skipspace(title) : "???";
+ errmsg = success ? NULL : "";
+
+ info("timer %d: title %s", i++, title);
+ debug(" ref %s, lastRefresh %ld, interval %d", ref, lastRefresh, interval);
+
+ timers.Add(new cWebviTimer(nextID++, title, ref, this,
+ (time_t)lastRefresh, interval,
+ success, errmsg));
+
+ free(ref);
+ }
+}
+
+void cWebviTimerManager::LoadHistory(FILE *f) {
+ cReadLine rl;
+ char *line;
+
+ while ((line = rl.Read(f)))
+ refHistory.Append(strdup(line));
+
+ debug("loaded history: len = %d", refHistory.Size());
+}
+
+void cWebviTimerManager::SaveTimers(FILE *f) {
+ // Format: space separated field in this order:
+ // lastUpdate interval lastSucceeded reference title
+
+ fprintf(f, "# WVTIMER1\n");
+
+ cWebviTimer *t = timers.First();
+ while (t) {
+ if (fprintf(f, "%ld %d %d %s %s\n",
+ t->LastUpdate(), t->GetInterval(), t->Success(),
+ t->GetReference(), t->GetTitle()) < 0) {
+ error("Failed to save timer data!");
+ }
+
+ t = timers.Next(t);
+ }
+}
+
+void cWebviTimerManager::SaveHistory(FILE *f) {
+ int size = refHistory.Size();
+ int first;
+
+ if (size <= MAX_TIMER_HISTORY_SIZE)
+ first = 0;
+ else
+ first = size - MAX_TIMER_HISTORY_SIZE;
+
+ for (int i=first; i<size; i++) {
+ const char *ref = refHistory[i];
+ if (fwrite(ref, strlen(ref), 1, f) != 1 ||
+ fwrite("\n", 1, 1, f) != 1) {
+ error("Error while writing timer history");
+ break;
+ }
+ }
+}
+
+bool cWebviTimerManager::Load(const char *path) {
+ FILE *f;
+ bool ok = true;
+
+ cString timersname = AddDirectory(path, "timers.dat");
+ f = fopen(timersname, "r");
+ if (f) {
+ debug("loading webvi timers from %s", (const char *)timersname);
+ LoadTimers(f);
+ fclose(f);
+ } else {
+ if (errno != ENOENT)
+ LOG_ERROR_STR("Can't load webvi timers");
+ ok = false;
+ }
+
+ cString historyname = AddDirectory(path, "timers.hst");
+ f = fopen(historyname, "r");
+ if (f) {
+ debug("loading webvi history from %s", (const char *)historyname);
+ LoadHistory(f);
+ fclose(f);
+ } else {
+ if (errno != ENOENT)
+ LOG_ERROR_STR("Can't load webvi timer history");
+ ok = false;
+ }
+
+ return ok;
+}
+
+bool cWebviTimerManager::Save(const char *path) {
+ FILE *f;
+ bool ok = true;
+
+ if (!modified)
+ return true;
+ if (disableSaving) {
+ error("Not saving timers because the file format is unknown.");
+ return false;
+ }
+
+ cString timersname = AddDirectory(path, "timers.dat");
+ f = fopen(timersname, "w");
+ if (f) {
+ debug("saving webvi timers to %s", (const char *)timersname);
+ SaveTimers(f);
+ fclose(f);
+ } else {
+ LOG_ERROR_STR("Can't save webvi timers");
+ ok = false;
+ }
+
+ cString historyname = AddDirectory(path, "timers.hst");
+ f = fopen(historyname, "w");
+ if (f) {
+ debug("saving webvi timer history to %s", (const char *)historyname);
+ SaveHistory(f);
+ fclose(f);
+ } else {
+ LOG_ERROR_STR("Can't save webvi timer history");
+ ok = false;
+ }
+
+ modified = !ok;
+
+ return ok;
+}
+
+void cWebviTimerManager::Update() {
+ char timestr[25];
+ cWebviTimer *timer = timers.First();
+ if (!timer)
+ return;
+
+ time_t now = time(NULL);
+
+#ifdef DEBUG
+ strftime(timestr, 25, "%x %X", localtime(&now));
+ debug("Running webvi timers update at %s", timestr);
+#endif
+
+ while (timer) {
+ if (timer->NextUpdate() < now) {
+ debug("%d. %s: launching now",
+ timer->GetID(), timer->GetTitle());
+ timer->Execute();
+ } else {
+#ifdef DEBUG
+ time_t next = timer->NextUpdate();
+ strftime(timestr, 25, "%x %X", localtime(&next));
+ debug("%d. %s: next update at %s",
+ timer->GetID(), timer->GetTitle(), timestr);
+#endif
+ }
+
+ timer = timers.Next(timer);
+ }
+}
+
+cWebviTimer *cWebviTimerManager::GetByID(int id) const {
+ cWebviTimer *timer = timers.First();
+
+ while (timer) {
+ if (timer->GetID() == id)
+ return timer;
+
+ timer = timers.Next(timer);
+ }
+
+ return NULL;
+}
+
+cWebviTimer *cWebviTimerManager::Create(const char *title,
+ const char *ref,
+ bool getExisting) {
+ cWebviTimer *t;
+
+ if (!ref)
+ return NULL;
+
+ if (getExisting) {
+ t = timers.First();
+ while (t) {
+ if (strcmp(t->GetReference(), ref) == 0) {
+ return t;
+ }
+
+ t = timers.Next(t);
+ }
+ }
+
+ t = new cWebviTimer(nextID++, title, ref, this);
+ timers.Add(t);
+
+ modified = true;
+
+ return t;
+}
+
+void cWebviTimerManager::Remove(cWebviTimer *timer) {
+ timers.Del(timer);
+ modified = true;
+}
+
+void cWebviTimerManager::MarkDownloaded(const char *ref) {
+ if (!ref)
+ return;
+
+ if (refHistory.Find(ref) == -1) {
+ refHistory.Append(strdup(ref));
+ modified = true;
+ }
+}
+
+bool cWebviTimerManager::AlreadyDownloaded(const char *ref) {
+ return refHistory.Find(ref) != -1;
+}
diff --git a/src/vdr-plugin/timer.h b/src/vdr-plugin/timer.h
new file mode 100644
index 0000000..048014a
--- /dev/null
+++ b/src/vdr-plugin/timer.h
@@ -0,0 +1,111 @@
+/*
+ * timer.h: Web video plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef __WEBVIDEO_TIMER_H
+#define __WEBVIDEO_TIMER_H
+
+#include <time.h>
+#include <stdio.h>
+#include <vdr/tools.h>
+#include "request.h"
+
+#define REQ_ID_TIMER -2
+#define DEFAULT_TIMER_INTERVAL 7*24*60*60
+#define RETRY_TIMER_INTERVAL 60*60
+#define MIN_TIMER_INTERVAL 10*60
+#define MAX_TIMER_HISTORY_SIZE 2000
+
+class cWebviTimerManager;
+
+// --- cWebviTimer -----------------------------------------------
+
+class cWebviTimer : public cListObject {
+private:
+ int id;
+ char *title;
+ char *reference;
+
+ time_t lastUpdate;
+ int interval;
+
+ bool running;
+ cStringList activeStreams;
+ bool lastSucceeded;
+ char *lastError;
+
+ cWebviTimerManager *parent;
+
+public:
+ cWebviTimer(int ID, const char *title, const char *ref,
+ cWebviTimerManager *manager,
+ time_t last=0, int interval=DEFAULT_TIMER_INTERVAL,
+ bool success=true, const char *errmsg=NULL);
+ ~cWebviTimer();
+
+ int GetID() const { return id; }
+ void SetTitle(const char *newTitle);
+ const char *GetTitle() const { return title; }
+ void SetInterval(int interval);
+ int GetInterval() const;
+ const char *GetReference() const { return reference; }
+
+ time_t LastUpdate() const { return lastUpdate; }
+ time_t NextUpdate() const;
+
+ void SetError(const char *errmsg);
+ bool Success() const { return lastSucceeded; }
+ const char *LastError() const;
+
+ void Execute();
+ bool Running() { return running; }
+ void DownloadStreams(const char *menuxml, cProgressVector& summaries);
+ void CheckFailed(const char *errmsg);
+ void RequestFinished(const char *ref, const char *errmsg);
+};
+
+// --- cWebviTimerManager ----------------------------------------
+
+class cWebviTimerManager {
+private:
+ cList<cWebviTimer> timers;
+ int nextID;
+ cStringList refHistory;
+ bool modified;
+ bool disableSaving;
+
+ cWebviTimerManager();
+ ~cWebviTimerManager() {};
+ cWebviTimerManager(const cWebviTimerManager &); // intentionally undefined
+ cWebviTimerManager &operator=(const cWebviTimerManager &); // intentionally undefined
+
+ void LoadTimers(FILE *f);
+ void LoadHistory(FILE *f);
+ void SaveTimers(FILE *f);
+ void SaveHistory(FILE *f);
+
+public:
+ static cWebviTimerManager &Instance();
+
+ bool Load(const char *path);
+ bool Save(const char *path);
+
+ cWebviTimer *Create(const char *title, const char *reference,
+ bool getExisting=true);
+ void Remove(cWebviTimer *timer);
+ cWebviTimer *First() const { return timers.First(); }
+ cWebviTimer *Next(const cWebviTimer *cur) const { return timers.Next(cur); }
+ cWebviTimer *GetLinear(int idx) const { return timers.Get(idx); }
+ cWebviTimer *GetByID(int id) const;
+ void SetModified() { modified = true; }
+
+ void Update();
+ void MarkDownloaded(const char *ref);
+ bool AlreadyDownloaded(const char *ref);
+};
+
+#endif
diff --git a/src/vdr-plugin/webvideo.c b/src/vdr-plugin/webvideo.c
new file mode 100644
index 0000000..554ef28
--- /dev/null
+++ b/src/vdr-plugin/webvideo.c
@@ -0,0 +1,444 @@
+/*
+ * webvideo.c: A plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#include <getopt.h>
+#include <time.h>
+#include <vdr/plugin.h>
+#include <vdr/tools.h>
+#include <vdr/videodir.h>
+#include <vdr/i18n.h>
+#include <vdr/skins.h>
+#include <libwebvi.h>
+#include "menu.h"
+#include "history.h"
+#include "download.h"
+#include "request.h"
+#include "mimetypes.h"
+#include "config.h"
+#include "player.h"
+#include "common.h"
+#include "timer.h"
+
+const char *VERSION = "0.3.0";
+static const char *DESCRIPTION = trNOOP("Download video files from the web");
+static const char *MAINMENUENTRY = "Webvideo";
+cMimeTypes *MimeTypes = NULL;
+
+class cPluginWebvideo : public cPlugin {
+private:
+ // Add any member variables or functions you may need here.
+ cHistory history;
+ cProgressVector summaries;
+ cString templatedir;
+ cString destdir;
+ cString conffile;
+
+ static int nextMenuID;
+
+ void UpdateOSDFromHistory(const char *statusmsg=NULL);
+ void UpdateStatusMenu(bool force=false);
+ bool StartStreaming(const cString &streamurl);
+ void ExecuteTimers(void);
+ void HandleFinishedRequests(void);
+
+public:
+ cPluginWebvideo(void);
+ virtual ~cPluginWebvideo();
+ virtual const char *Version(void) { return VERSION; }
+ virtual const char *Description(void) { return tr(DESCRIPTION); }
+ virtual const char *CommandLineHelp(void);
+ virtual bool ProcessArgs(int argc, char *argv[]);
+ virtual bool Initialize(void);
+ virtual bool Start(void);
+ virtual void Stop(void);
+ virtual void Housekeeping(void);
+ virtual void MainThreadHook(void);
+ virtual cString Active(void);
+ virtual const char *MainMenuEntry(void) { return MAINMENUENTRY; }
+ virtual cOsdObject *MainMenuAction(void);
+ virtual cMenuSetupPage *SetupMenu(void);
+ virtual bool SetupParse(const char *Name, const char *Value);
+ virtual bool Service(const char *Id, void *Data = NULL);
+ virtual const char **SVDRPHelpPages(void);
+ virtual cString SVDRPCommand(const char *Command, const char *Option, int &ReplyCode);
+ };
+
+int cPluginWebvideo::nextMenuID = 1;
+
+cPluginWebvideo::cPluginWebvideo(void)
+{
+ // Initialize any member variables here.
+ // DON'T DO ANYTHING ELSE THAT MAY HAVE SIDE EFFECTS, REQUIRE GLOBAL
+ // VDR OBJECTS TO EXIST OR PRODUCE ANY OUTPUT!
+}
+
+cPluginWebvideo::~cPluginWebvideo()
+{
+ // Clean up after yourself!
+ webvi_cleanup(0);
+}
+
+const char *cPluginWebvideo::CommandLineHelp(void)
+{
+ // Return a string that describes all known command line options.
+ return " -d DIR, --downloaddir=DIR Save downloaded files to DIR\n" \
+ " -t DIR, --templatedir=DIR Read video site templates from DIR\n" \
+ " -c FILE, --conf=FILE Load settings from FILE\n";
+}
+
+bool cPluginWebvideo::ProcessArgs(int argc, char *argv[])
+{
+ // Implement command line argument processing here if applicable.
+ static struct option long_options[] = {
+ { "downloaddir", required_argument, NULL, 'd' },
+ { "templatedir", required_argument, NULL, 't' },
+ { "conf", required_argument, NULL, 'c' },
+ { NULL }
+ };
+
+ int c;
+ while ((c = getopt_long(argc, argv, "d:t:c:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'd':
+ destdir = cString(optarg);
+ break;
+ case 't':
+ templatedir = cString(optarg);
+ break;
+ case 'c':
+ conffile = cString(optarg);
+ break;
+ default:
+ return false;
+ }
+ }
+ return true;
+}
+
+bool cPluginWebvideo::Initialize(void)
+{
+ // Initialize any background activities the plugin shall perform.
+
+ // Test that run-time and compile-time libxml versions are compatible
+ LIBXML_TEST_VERSION;
+
+ // default values if not given on the command line
+ if ((const char *)destdir == NULL)
+ destdir = cString(VideoDirectory);
+ if ((const char *)conffile == NULL)
+ conffile = AddDirectory(ConfigDirectory(Name()), "webvi.plugin.conf");
+
+ webvideoConfig->SetDownloadPath(destdir);
+ webvideoConfig->SetTemplatePath(templatedir);
+ webvideoConfig->ReadConfigFile(conffile);
+
+ cString mymimetypes = AddDirectory(ConfigDirectory(Name()), "mime.types");
+ const char *mimefiles [] = {"/etc/mime.types", (const char *)mymimetypes, NULL};
+ MimeTypes = new cMimeTypes(mimefiles);
+
+ if (webvi_global_init() != 0) {
+ error("Failed to initialize libwebvi");
+ return false;
+ }
+
+ cWebviTimerManager::Instance().Load(ConfigDirectory(Name()));
+
+ cWebviThread::Instance().SetTemplatePath(webvideoConfig->GetTemplatePath());
+
+ return true;
+}
+
+bool cPluginWebvideo::Start(void)
+{
+ // Start any background activities the plugin shall perform.
+ cWebviThread::Instance().Start();
+
+ return true;
+}
+
+void cPluginWebvideo::Stop(void)
+{
+ // Stop any background activities the plugin shall perform.
+ cWebviThread::Instance().Stop();
+ delete MimeTypes;
+
+ cWebviTimerManager::Instance().Save(ConfigDirectory(Name()));
+
+ xmlCleanupParser();
+}
+
+void cPluginWebvideo::Housekeeping(void)
+{
+ // Perform any cleanup or other regular tasks.
+
+ cWebviTimerManager::Instance().Save(ConfigDirectory(Name()));
+}
+
+void cPluginWebvideo::MainThreadHook(void)
+{
+ // Perform actions in the context of the main program thread.
+ // WARNING: Use with great care - see PLUGINS.html!
+ ExecuteTimers();
+
+ HandleFinishedRequests();
+}
+
+void cPluginWebvideo::ExecuteTimers(void)
+{
+ static int counter = 0;
+
+ // don't do this too often
+ if (counter++ > 1800) {
+ cWebviTimerManager::Instance().Update();
+ counter = 0;
+ }
+}
+
+void cPluginWebvideo::HandleFinishedRequests(void)
+{
+ bool forceStatusUpdate = false;
+ cMenuRequest *req;
+ cFileDownloadRequest *dlreq;
+ cString streamurl;
+ cWebviTimer *timer;
+ cString timermsg;
+
+ while ((req = cWebviThread::Instance().GetFinishedRequest())) {
+ int cid = -1;
+ int code = req->GetStatusCode();
+ if (history.Current()) {
+ cid = history.Current()->GetID();
+ }
+
+ debug("Finished request: %d (current: %d), type = %d, status = %d",
+ req->GetID(), cid, req->GetType(), code);
+
+ if (req->Success()) {
+ switch (req->GetType()) {
+ case REQT_MENU:
+ // Only change the menu if the request was launched from the
+ // current menu.
+ if (req->GetID() == cid) {
+ if (cid == 0) {
+ // Special case: replace the placeholder menu
+ history.Clear();
+ }
+
+ if (history.Current())
+ history.Current()->RememberSelected(menuPointers.navigationMenu->Current());
+ history.TruncateAndAdd(new cHistoryObject(req->GetResponse(),
+ req->GetReference(),
+ nextMenuID++));
+ UpdateOSDFromHistory();
+ }
+ break;
+
+ case REQT_STREAM:
+ streamurl = req->GetResponse();
+ if (streamurl[0] == '\0')
+ Skins.Message(mtError, tr("Streaming failed: no URL"));
+ else if (!StartStreaming(streamurl))
+ Skins.Message(mtError, tr("Failed to launch media player"));
+ break;
+
+ case REQT_FILE:
+ dlreq = dynamic_cast<cFileDownloadRequest *>(req);
+
+ if (dlreq) {
+ for (int i=0; i<summaries.Size(); i++) {
+ if (summaries[i]->GetRequest() == dlreq) {
+ delete summaries[i];
+ summaries.Remove(i);
+ break;
+ }
+ }
+ }
+
+ timermsg = cString("");
+ if (req->GetTimer()) {
+ req->GetTimer()->RequestFinished(req->GetReference(), NULL);
+
+ timermsg = cString::sprintf(" (%s)", tr("timer"));
+ }
+
+ Skins.Message(mtInfo, cString::sprintf(tr("One download completed, %d remains%s"),
+ cWebviThread::Instance().GetUnfinishedCount(),
+ (const char *)timermsg));
+ forceStatusUpdate = true;
+ break;
+
+ case REQT_TIMER:
+ timer = req->GetTimer();
+ if (timer)
+ timer->DownloadStreams(req->GetResponse(), summaries);
+ break;
+
+ default:
+ break;
+ }
+ } else { // failed request
+ if (req->GetType() == REQT_TIMER) {
+ warning("timer request failed (%d: %s)",
+ code, (const char*)req->GetStatusPharse());
+
+ timer = req->GetTimer();
+ if (timer)
+ timer->CheckFailed(req->GetStatusPharse());
+ } else {
+ warning("request failed (%d: %s)",
+ code, (const char*)req->GetStatusPharse());
+
+ if (code == -2 || code == 402)
+ Skins.Message(mtError, tr("Download aborted"));
+ else
+ Skins.Message(mtError, cString::sprintf(tr("Download failed (error = %d)"), code));
+
+ dlreq = dynamic_cast<cFileDownloadRequest *>(req);
+ if (dlreq) {
+ for (int i=0; i<summaries.Size(); i++) {
+ if (summaries[i]->GetRequest() == dlreq) {
+ summaries[i]->AssociateWith(NULL);
+ break;
+ }
+ }
+ }
+
+ if (req->GetTimer())
+ req->GetTimer()->RequestFinished(req->GetReference(),
+ (const char*)req->GetStatusPharse());
+
+ forceStatusUpdate = true;
+ }
+ }
+
+ delete req;
+ }
+
+ UpdateStatusMenu(forceStatusUpdate);
+}
+
+cString cPluginWebvideo::Active(void)
+{
+ // Return a message string if shutdown should be postponed
+ int c = cWebviThread::Instance().GetUnfinishedCount();
+ if (c > 0)
+ return cString::sprintf(tr("%d downloads not finished"), c);
+ else
+ return NULL;
+}
+
+cOsdObject *cPluginWebvideo::MainMenuAction(void)
+{
+ // Perform the action when selected from the main VDR menu.
+ const char *mainMenuReference = "wvt:///?srcurl=mainmenu";
+ const char *placeholderMenu = "<wvmenu><title>Webvideo</title></wvmenu>";
+ const char *statusmsg = NULL;
+ struct timespec ts;
+ ts.tv_sec = 0;
+ ts.tv_nsec = 100*1000*1000; // 100 ms
+
+ menuPointers.navigationMenu = new cNavigationMenu(&history, summaries);
+
+ cHistoryObject *hist = history.Home();
+ if (!hist) {
+ cWebviThread::Instance().AddRequest(new cMenuRequest(0, mainMenuReference));
+ cHistoryObject *placeholder = new cHistoryObject(placeholderMenu, mainMenuReference, 0);
+ history.TruncateAndAdd(placeholder);
+
+ // The main menu response should come right away. Try to update
+ // the menu here without having to wait for the next
+ // MainThreadHook call by VDR main loop.
+ for (int i=0; i<4; i++) {
+ nanosleep(&ts, NULL);
+ HandleFinishedRequests();
+ if (history.Current() != placeholder) {
+ return menuPointers.navigationMenu;
+ }
+ };
+
+ statusmsg = tr("Retrieving...");
+ }
+
+ UpdateOSDFromHistory(statusmsg);
+ return menuPointers.navigationMenu;
+}
+
+cMenuSetupPage *cPluginWebvideo::SetupMenu(void)
+{
+ // Return a setup menu in case the plugin supports one.
+ return NULL;
+}
+
+bool cPluginWebvideo::SetupParse(const char *Name, const char *Value)
+{
+ // Parse your own setup parameters and store their values.
+ return false;
+}
+
+bool cPluginWebvideo::Service(const char *Id, void *Data)
+{
+ // Handle custom service requests from other plugins
+ return false;
+}
+
+const char **cPluginWebvideo::SVDRPHelpPages(void)
+{
+ // Return help text for SVDRP commands this plugin implements
+ return NULL;
+}
+
+cString cPluginWebvideo::SVDRPCommand(const char *Command, const char *Option, int &ReplyCode)
+{
+ // Process SVDRP commands this plugin implements
+ return NULL;
+}
+
+void cPluginWebvideo::UpdateOSDFromHistory(const char *statusmsg) {
+ if (menuPointers.navigationMenu) {
+ cHistoryObject *hist = history.Current();
+ menuPointers.navigationMenu->Populate(hist, statusmsg);
+ menuPointers.navigationMenu->Display();
+ } else {
+ debug("OSD is not ours.");
+ }
+}
+
+void cPluginWebvideo::UpdateStatusMenu(bool force) {
+ if (menuPointers.statusScreen &&
+ (force || menuPointers.statusScreen->NeedsUpdate())) {
+ menuPointers.statusScreen->Update();
+ }
+}
+
+bool cPluginWebvideo::StartStreaming(const cString &streamurl) {
+ cMediaPlayer *players[2];
+
+ if (webvideoConfig->GetPreferXineliboutput()) {
+ players[0] = new cXineliboutputPlayer();
+ players[1] = new cMPlayerPlayer();
+ } else {
+ players[0] = new cMPlayerPlayer();
+ players[1] = new cXineliboutputPlayer();
+ }
+
+ bool ret = false;
+ for (int i=0; i<2; i++) {
+ if (players[i]->Launch(streamurl)) {
+ ret = true;
+ break;
+ }
+ }
+
+ for (int i=0; i<2 ; i++) {
+ delete players[i];
+ }
+
+ return ret;
+}
+
+VDRPLUGINCREATOR(cPluginWebvideo); // Don't touch this!
diff --git a/src/version b/src/version
new file mode 100644
index 0000000..9325c3c
--- /dev/null
+++ b/src/version
@@ -0,0 +1 @@
+0.3.0 \ No newline at end of file
diff --git a/src/webvicli/webvi b/src/webvicli/webvi
new file mode 100755
index 0000000..b8fa190
--- /dev/null
+++ b/src/webvicli/webvi
@@ -0,0 +1,22 @@
+#!/usr/bin/python
+
+# menu.py - starter script for webvicli
+#
+# Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+from webvicli import client
+client.main(sys.argv[1:])
diff --git a/src/webvicli/webvicli/__init__.py b/src/webvicli/webvicli/__init__.py
new file mode 100644
index 0000000..1cf59b7
--- /dev/null
+++ b/src/webvicli/webvicli/__init__.py
@@ -0,0 +1 @@
+__all__ = ['client', 'menu']
diff --git a/src/webvicli/webvicli/client.py b/src/webvicli/webvicli/client.py
new file mode 100644
index 0000000..782c47c
--- /dev/null
+++ b/src/webvicli/webvicli/client.py
@@ -0,0 +1,729 @@
+#!/usr/bin/env python
+
+# webvicli.py - webvi command line client
+#
+# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import cStringIO
+import sys
+import cmd
+import mimetypes
+import select
+import os.path
+import subprocess
+import time
+import re
+import libxml2
+import webvi.api
+import webvi.utils
+from optparse import OptionParser
+from ConfigParser import RawConfigParser
+from webvi.constants import WebviRequestType, WebviErr, WebviOpt, WebviInfo, WebviSelectBitmask, WebviConfig
+from . import menu
+
+VERSION = '0.3.0'
+
+# Default options
+DEFAULT_PLAYERS = ['vlc --play-and-exit "%s"',
+ 'totem "%s"',
+ 'mplayer "%s"',
+ 'xine "%s"']
+
+# These mimetypes are common but often missing
+mimetypes.init()
+mimetypes.add_type('video/flv', '.flv')
+mimetypes.add_type('video/x-flv', '.flv')
+
+def safe_filename(name):
+ """Sanitize a filename. No paths (replace '/' -> '!') and no
+ names starting with a dot."""
+ res = name.replace('/', '!').lstrip('.')
+ res = res.encode(sys.getfilesystemencoding(), 'ignore')
+ return res
+
+class DownloadData:
+ def __init__(self, handle, progressstream):
+ self.handle = handle
+ self.destfile = None
+ self.destfilename = ''
+ self.contentlength = -1
+ self.bytes_downloaded = 0
+ self.progress = ProgressMeter(progressstream)
+
+class ProgressMeter:
+ def __init__(self, stream):
+ self.last_update = None
+ self.samples = []
+ self.total_bytes = 0
+ self.stream = stream
+ self.progress_len = 0
+ self.starttime = time.time()
+
+ def pretty_bytes(self, bytes):
+ """Pretty print bytes as kB or MB."""
+ if bytes < 1100:
+ return '%d B' % bytes
+ elif bytes < 1024*1024:
+ return '%.1f kB' % (float(bytes)/1024)
+ elif bytes < 1024*1024*1024:
+ return '%.1f MB' % (float(bytes)/1024/1024)
+ else:
+ return '%.1f GB' % (float(bytes)/1024/1024/1024)
+
+ def pretty_time(self, seconds):
+ """Pretty print seconds as hour and minutes."""
+ seconds = int(round(seconds))
+ if seconds < 60:
+ return '%d s' % seconds
+ elif seconds < 60*60:
+ secs = seconds % 60
+ mins = seconds/60
+ return '%d min %d s' % (mins, secs)
+ else:
+ hours = seconds / (60*60)
+ mins = (seconds-60*60*hours) / 60
+ return '%d hours %d min' % (hours, mins)
+
+ def update(self, bytes):
+ """Update progress bar.
+
+ Updates the estimates of download rate and remaining time.
+ Prints progress bar, if at least one second has passed since
+ the previous update.
+ """
+ now = time.time()
+
+ if self.total_bytes > 0:
+ percentage = float(bytes)/self.total_bytes * 100.0
+ else:
+ percentage = 0
+
+ if self.total_bytes > 0 and bytes >= self.total_bytes:
+ self.stream.write('\r')
+ self.stream.write(' '*self.progress_len)
+ self.stream.write('\r')
+ self.stream.write('%3.f %% of %s downloaded in %s (%.1f kB/s)\n' %
+ (percentage, self.pretty_bytes(self.total_bytes),
+ self.pretty_time(now-self.starttime),
+ float(bytes)/(now-self.starttime)/1024.0))
+ self.stream.flush()
+ return
+
+ force_refresh = False
+ if self.last_update is None:
+ # This is a new progress meter
+ self.last_update = now
+ force_refresh = True
+
+ if (not force_refresh) and (now <= self.last_update + 1):
+ # do not update too often
+ return
+
+ self.last_update = now
+
+ # Estimate bytes per second rate from the last 10 samples
+ self.samples.append((bytes, now))
+ if len(self.samples) > 10:
+ self.samples.pop(0)
+
+ bytes_old, time_old = self.samples[0]
+ if now > time_old:
+ rate = float(bytes-bytes_old)/(now-time_old)
+ else:
+ rate = 0
+
+ if self.total_bytes > 0:
+ remaining = self.total_bytes - bytes
+
+ if rate > 0:
+ time_left = self.pretty_time(remaining/rate)
+ else:
+ time_left = '???'
+
+ progress = '%3.f %% of %s (%.1f kB/s) %s remaining' % \
+ (percentage, self.pretty_bytes(self.total_bytes),
+ rate/1024.0, time_left)
+ else:
+ progress = '%s downloaded (%.1f kB/s)' % \
+ (self.pretty_bytes(bytes), rate/1024.0)
+
+ new_progress_len = len(progress)
+ if new_progress_len < self.progress_len:
+ progress += ' '*(self.progress_len - new_progress_len)
+ self.progress_len = new_progress_len
+
+ self.stream.write('\r')
+ self.stream.write(progress)
+ self.stream.flush()
+
+
+class WVClient:
+ def __init__(self, streamplayers, downloadlimits, streamlimits):
+ self.streamplayers = streamplayers
+ self.history = []
+ self.history_pointer = 0
+ self.quality_limits = {'download': downloadlimits,
+ 'stream': streamlimits}
+
+ def parse_page(self, page):
+ if page is None:
+ return None
+ try:
+ doc = libxml2.parseDoc(page)
+ except libxml2.parserError:
+ return None
+
+ root = doc.getRootElement()
+ if root.name != 'wvmenu':
+ return None
+ queryitems = []
+ menupage = menu.Menu()
+ node = root.children
+ while node:
+ if node.name == 'title':
+ menupage.title = webvi.utils.get_content_unicode(node)
+ elif node.name == 'link':
+ menuitem = self.parse_link(node)
+ menupage.add(menuitem)
+ elif node.name == 'textfield':
+ menuitem = self.parse_textfield(node)
+ menupage.add(menuitem)
+ queryitems.append(menuitem)
+ elif node.name == 'itemlist':
+ menuitem = self.parse_itemlist(node)
+ menupage.add(menuitem)
+ queryitems.append(menuitem)
+ elif node.name == 'textarea':
+ menuitem = self.parse_textarea(node)
+ menupage.add(menuitem)
+ elif node.name == 'button':
+ menuitem = self.parse_button(node, queryitems)
+ menupage.add(menuitem)
+ node = node.next
+ doc.freeDoc()
+ return menupage
+
+ def parse_link(self, node):
+ label = ''
+ ref = None
+ stream = None
+ child = node.children
+ while child:
+ if child.name == 'label':
+ label = webvi.utils.get_content_unicode(child)
+ elif child.name == 'ref':
+ ref = webvi.utils.get_content_unicode(child)
+ elif child.name == 'stream':
+ stream = webvi.utils.get_content_unicode(child)
+ child = child.next
+ return menu.MenuItemLink(label, ref, stream)
+
+ def parse_textfield(self, node):
+ label = ''
+ name = node.prop('name')
+ child = node.children
+ while child:
+ if child.name == 'label':
+ label = webvi.utils.get_content_unicode(child)
+ child = child.next
+ return menu.MenuItemTextField(label, name)
+
+ def parse_textarea(self, node):
+ label = ''
+ child = node.children
+ while child:
+ if child.name == 'label':
+ label = webvi.utils.get_content_unicode(child)
+ child = child.next
+ return menu.MenuItemTextArea(label)
+
+ def parse_itemlist(self, node):
+ label = ''
+ name = node.prop('name')
+ items = []
+ values = []
+ child = node.children
+ while child:
+ if child.name == 'label':
+ label = webvi.utils.get_content_unicode(child)
+ elif child.name == 'item':
+ items.append(webvi.utils.get_content_unicode(child))
+ values.append(child.prop('value'))
+ child = child.next
+ return menu.MenuItemList(label, name, items, values, sys.stdout)
+
+ def parse_button(self, node, queryitems):
+ label = ''
+ submission = None
+ child = node.children
+ while child:
+ if child.name == 'label':
+ label = webvi.utils.get_content_unicode(child)
+ elif child.name == 'submission':
+ submission = webvi.utils.get_content_unicode(child)
+ child = child.next
+ return menu.MenuItemSubmitButton(label, submission, queryitems)
+
+ def guess_extension(self, mimetype, url):
+ ext = mimetypes.guess_extension(mimetype)
+ if (ext is None) or (mimetype == 'text/plain'):
+ # This function is only called for video files. Try to
+ # extract the extension from url because text/plain is
+ # clearly wrong.
+ lastcomponent = re.split(r'[?#]', url, 1)[0].split('/')[-1]
+ i = lastcomponent.rfind('.')
+ if i == -1:
+ ext = ''
+ else:
+ ext = lastcomponent[i:]
+
+ return ext
+
+ def execute_webvi(self, handle):
+ """Call webvi.api.perform until handle is finished."""
+ while True:
+ rescode, readfds, writefds, excfds, maxfd = webvi.api.fdset()
+ if [] == readfds == writefds == excfds:
+ finished, status, errmsg, remaining = webvi.api.pop_message()
+ if finished == handle:
+ return (status, errmsg)
+ else:
+ return (501, 'No active sockets')
+
+ readyread, readywrite, readyexc = select.select(readfds, writefds, excfds, 30.0)
+
+ for fd in readyread:
+ webvi.api.perform(fd, WebviSelectBitmask.READ)
+ for fd in readywrite:
+ webvi.api.perform(fd, WebviSelectBitmask.WRITE)
+
+ remaining = -1
+ while remaining != 0:
+ finished, status, errmsg, remaining = webvi.api.pop_message()
+ if finished == handle:
+ return (status, errmsg)
+
+ def collect_data(self, inp, inplen, dlbuffer):
+ """Callback that writes the downloaded data to dlbuffer.
+ """
+ dlbuffer.write(inp)
+ return inplen
+
+ def open_dest_file(self, inp, inplen, dldata):
+ """Initial download callback. This opens the destination file,
+ and reseats the callback to self.write_to_dest. The
+ destination file can not be opened until now, because the
+ stream title and final URL are not known before.
+ """
+ title = webvi.api.get_info(dldata.handle, WebviInfo.STREAM_TITLE)[1]
+ contenttype = webvi.api.get_info(dldata.handle, WebviInfo.CONTENT_TYPE)[1]
+ contentlength = webvi.api.get_info(dldata.handle, WebviInfo.CONTENT_LENGTH)[1]
+ url = webvi.api.get_info(dldata.handle, WebviInfo.URL)[1]
+ ext = self.guess_extension(contenttype, url)
+ destfilename = self.next_available_file_name(safe_filename(title), ext)
+
+ try:
+ destfile = open(destfilename, 'w')
+ except IOError, err:
+ print 'Failed to open the destination file %s: %s' % (destfilename, err.args[1])
+ return -1
+
+ dldata.destfile = destfile
+ dldata.destfilename = destfilename
+ dldata.contentlength = contentlength
+ dldata.progress.total_bytes = contentlength
+ webvi.api.set_opt(dldata.handle, WebviOpt.WRITEFUNC, self.write_to_dest)
+
+ return self.write_to_dest(inp, inplen, dldata)
+
+ def write_to_dest(self, inp, inplen, dldata):
+ """Callback that writes downloaded data to self.destfile."""
+ try:
+ dldata.destfile.write(inp)
+ except IOError, err:
+ print 'IOError while writing to %s: %s' % \
+ (dldata.destfilename, err.args[1])
+ return -1
+
+ dldata.bytes_downloaded += inplen
+
+ dldata.progress.update(dldata.bytes_downloaded)
+
+ return inplen
+
+ def getmenu(self, ref):
+ dlbuffer = cStringIO.StringIO()
+ handle = webvi.api.new_request(ref, WebviRequestType.MENU)
+ if handle == -1:
+ print 'Failed to open handle'
+ return (-1, '', None)
+
+ webvi.api.set_opt(handle, WebviOpt.WRITEFUNC, self.collect_data)
+ webvi.api.set_opt(handle, WebviOpt.WRITEDATA, dlbuffer)
+ webvi.api.start_handle(handle)
+
+ status, err = self.execute_webvi(handle)
+ webvi.api.delete_handle(handle)
+
+ if status != 0:
+ print 'Download failed:', err
+ return (status, err, None)
+
+ return (status, err, self.parse_page(dlbuffer.getvalue()))
+
+ def get_quality_params(self, videosite, streamtype):
+ params = []
+ lim = self.quality_limits[streamtype].get(videosite, {})
+
+ if lim.has_key('min'):
+ params.append('minquality=' + lim['min'])
+ if lim.has_key('max'):
+ params.append('maxquality=' + lim['max'])
+
+ return '&'.join(params)
+
+ def download(self, stream):
+ m = re.match(r'wvt:///([^/]+)/', stream)
+ if m is not None:
+ stream += '&' + self.get_quality_params(m.group(1), 'download')
+
+ handle = webvi.api.new_request(stream, WebviRequestType.FILE)
+ if handle == -1:
+ print 'Failed to open handle'
+ return False
+
+ dldata = DownloadData(handle, sys.stdout)
+
+ webvi.api.set_opt(handle, WebviOpt.WRITEFUNC, self.open_dest_file)
+ webvi.api.set_opt(handle, WebviOpt.WRITEDATA, dldata)
+ webvi.api.start_handle(handle)
+
+ status, err = self.execute_webvi(handle)
+ if dldata.destfile is not None:
+ dldata.destfile.close()
+
+ webvi.api.delete_handle(handle)
+
+ if status not in (0, 504):
+ print 'Download failed:', err
+ return
+
+ if dldata.contentlength != -1 and \
+ dldata.bytes_downloaded != dldata.contentlength:
+ print 'Warning: the size of the file (%d) differs from expected (%d)' % \
+ (dldata.bytes_downloaded, dldata.contentlength)
+
+ print 'Saved to %s' % dldata.destfilename
+
+ return True
+
+ def play_stream(self, ref):
+ streamurl = self.get_stream_url(ref)
+ if streamurl == '':
+ print 'Did not find URL'
+ return False
+
+ # Found url, now find a working media player
+ for player in self.streamplayers:
+ if '%s' not in player:
+ playcmd = player + ' ' + streamurl
+ else:
+ try:
+ playcmd = player % streamurl
+ except TypeError:
+ print 'Can\'t substitute URL in', player
+ continue
+
+ try:
+ print 'Trying player: ' + playcmd
+ retcode = subprocess.call(playcmd, shell=True)
+ if retcode > 0:
+ print 'Player failed with returncode', retcode
+ else:
+ return True
+ except OSError, err:
+ print 'Execution failed:', err
+
+ return False
+
+ def get_stream_url(self, ref):
+ m = re.match(r'wvt:///([^/]+)/', ref)
+ if m is not None:
+ ref += '&' + self.get_quality_params(m.group(1), 'stream')
+
+ handle = webvi.api.new_request(ref, WebviRequestType.STREAMURL)
+ if handle == -1:
+ print 'Failed to open handle'
+ return ''
+
+ dlbuffer = cStringIO.StringIO()
+ webvi.api.set_opt(handle, WebviOpt.WRITEFUNC, self.collect_data)
+ webvi.api.set_opt(handle, WebviOpt.WRITEDATA, dlbuffer)
+ webvi.api.start_handle(handle)
+ status, err = self.execute_webvi(handle)
+ webvi.api.delete_handle(handle)
+
+ if status != 0:
+ print 'Download failed:', err
+ return ''
+
+ return dlbuffer.getvalue()
+
+ def next_available_file_name(self, basename, ext):
+ fullname = basename + ext
+ if not os.path.exists(fullname):
+ return fullname
+ i = 1
+ while os.path.exists('%s-%d%s' % (basename, i, ext)):
+ i += 1
+ return '%s-%d%s' % (basename, i, ext)
+
+ def get_current_menu(self):
+ if (self.history_pointer >= 0) and \
+ (self.history_pointer < len(self.history)):
+ return self.history[self.history_pointer]
+ else:
+ return None
+
+ def history_add(self, menupage):
+ if menupage is not None:
+ self.history = self.history[:(self.history_pointer+1)]
+ self.history.append(menupage)
+ self.history_pointer = len(self.history)-1
+
+ def history_back(self):
+ if self.history_pointer > 0:
+ self.history_pointer -= 1
+ return self.get_current_menu()
+
+ def history_forward(self):
+ if self.history_pointer < len(self.history)-1:
+ self.history_pointer += 1
+ return self.get_current_menu()
+
+
+class WVShell(cmd.Cmd):
+ def __init__(self, client, completekey='tab', stdin=None, stdout=None):
+ cmd.Cmd.__init__(self, completekey, stdin, stdout)
+ self.prompt = '> '
+ self.client = client
+
+ def preloop(self):
+ self.stdout.write('webvicli %s starting\n' % VERSION)
+ self.do_menu(None)
+
+ def precmd(self, arg):
+ try:
+ int(arg)
+ return 'select ' + arg
+ except ValueError:
+ return arg
+
+ def onecmd(self, c):
+ try:
+ return cmd.Cmd.onecmd(self, c)
+ except Exception:
+ import traceback
+ print 'Exception occured while handling command "' + c + '"'
+ print traceback.format_exc()
+ return False
+
+ def emptyline(self):
+ pass
+
+ def display_menu(self, menupage):
+ if menupage is not None:
+ self.stdout.write(unicode(menupage).encode(self.stdout.encoding, 'replace'))
+
+ def _get_numbered_item(self, arg):
+ menupage = self.client.get_current_menu()
+ try:
+ v = int(arg)-1
+ if (menupage is None) or (v < 0) or (v >= len(menupage)):
+ raise ValueError
+ except ValueError:
+ self.stdout.write('Invalid selection: %s\n' % arg)
+ return None
+ return menupage[v]
+
+ def do_select(self, arg):
+ """select x
+Select the link whose index is x.
+ """
+ menuitem = self._get_numbered_item(arg)
+ if menuitem is None:
+ return False
+ ref = menuitem.activate()
+ if ref is not None:
+ status, statusmsg, menupage = self.client.getmenu(ref)
+ if menupage is not None:
+ self.client.history_add(menupage)
+ else:
+ self.stdout.write('Error: %d %s\n' % (status, statusmsg))
+ else:
+ menupage = self.client.get_current_menu()
+ self.display_menu(menupage)
+ return False
+
+ def do_download(self, arg):
+ """download x
+Download media stream whose index is x to a file. Downloadable items
+are the ones without brackets.
+ """
+ menuitem = self._get_numbered_item(arg)
+ if menuitem is None:
+ return False
+ elif hasattr(menuitem, 'stream') and menuitem.stream is not None:
+ self.client.download(menuitem.stream)
+ else:
+ self.stdout.write('Not a stream\n')
+ return False
+
+ def do_stream(self, arg):
+ """stream x
+Play the media file whose index is x. Streams are the ones
+without brackets.
+ """
+ menuitem = self._get_numbered_item(arg)
+ if menuitem is None:
+ return False
+ elif hasattr(menuitem, 'stream') and menuitem.stream is not None:
+ self.client.play_stream(menuitem.stream)
+ else:
+ self.stdout.write('Not a stream\n')
+ return False
+
+ def do_display(self, arg):
+ """Redisplay the current menu."""
+ if not arg:
+ self.display_menu(self.client.get_current_menu())
+ else:
+ self.stdout.write('Unknown parameter %s\n' % arg)
+ return False
+
+ def do_menu(self, arg):
+ """Get back to the main menu."""
+ status, statusmsg, menupage = self.client.getmenu('wvt:///?srcurl=mainmenu')
+ if menupage is not None:
+ self.client.history_add(menupage)
+ self.display_menu(menupage)
+ else:
+ self.stdout.write('Error: %d %s\n' % (status, statusmsg))
+ return True
+ return False
+
+ def do_back(self, arg):
+ """Go to the previous menu in the history."""
+ menupage = self.client.history_back()
+ self.display_menu(menupage)
+ return False
+
+ def do_forward(self, arg):
+ """Go to the next menu in the history."""
+ menupage = self.client.history_forward()
+ self.display_menu(menupage)
+ return False
+
+ def do_quit(self, arg):
+ """Quit the program."""
+ return True
+
+ def do_EOF(self, arg):
+ """Quit the program."""
+ return True
+
+
+def load_config(options):
+ """Load options from config files."""
+ cfgprs = RawConfigParser()
+ cfgprs.read(['/etc/webvi.conf', os.path.expanduser('~/.webvi')])
+ for sec in cfgprs.sections():
+ if sec == 'webvi':
+ for opt, val in cfgprs.items('webvi'):
+ options[opt] = val
+
+ elif sec.startswith('site-'):
+ sitename = sec[5:]
+
+ if not options.has_key('download-limits'):
+ options['download-limits'] = {}
+ if not options.has_key('stream-limits'):
+ options['stream-limits'] = {}
+ options['download-limits'][sitename] = {}
+ options['stream-limits'][sitename] = {}
+
+ for opt, val in cfgprs.items(sec):
+ if opt == 'download-min-quality':
+ options['download-limits'][sitename]['min'] = val
+ elif opt == 'download-max-quality':
+ options['download-limits'][sitename]['max'] = val
+ elif opt == 'stream-min-quality':
+ options['stream-limits'][sitename]['min'] = val
+ elif opt == 'stream-max-quality':
+ options['stream-limits'][sitename]['max'] = val
+
+ return options
+
+def parse_command_line(cmdlineargs, options):
+ parser = OptionParser()
+ parser.add_option('-t', '--templatepath', type='string',
+ dest='templatepath',
+ help='read video site templates from DIR', metavar='DIR',
+ default=None)
+ cmdlineopt = parser.parse_args(cmdlineargs)[0]
+
+ if cmdlineopt.templatepath is not None:
+ options['templatepath'] = cmdlineopt.templatepath
+
+ return options
+
+def player_list(options):
+ """Return a sorted list of player commands extracted from options
+ dictionary."""
+ # Load streamplayer items from the config file and sort them
+ # according to quality.
+ players = []
+ for opt, val in options.iteritems():
+ m = re.match(r'streamplayer([1-9])$', opt)
+ if m is not None:
+ players.append((int(m.group(1)), val))
+
+ players.sort()
+ ret = []
+ for quality, playcmd in players:
+ ret.append(playcmd)
+
+ # If the config file did not define any players use the default
+ # players
+ if not ret:
+ ret = list(DEFAULT_PLAYERS)
+
+ return ret
+
+def main(argv):
+ options = load_config({})
+ options = parse_command_line(argv, options)
+
+ if options.has_key('templatepath'):
+ webvi.api.set_config(WebviConfig.TEMPLATE_PATH, options['templatepath'])
+
+ shell = WVShell(WVClient(player_list(options),
+ options.get('download-limits', {}),
+ options.get('stream-limits', {})))
+ shell.cmdloop()
+
+if __name__ == '__main__':
+ main([])
diff --git a/src/webvicli/webvicli/menu.py b/src/webvicli/webvicli/menu.py
new file mode 100644
index 0000000..70ef6ea
--- /dev/null
+++ b/src/webvicli/webvicli/menu.py
@@ -0,0 +1,171 @@
+# menu.py - menu elements for webvicli
+#
+# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import textwrap
+import urllib
+
+LINEWIDTH = 72
+
+class Menu:
+ def __init__(self):
+ self.title = None
+ self.items = []
+
+ def __str__(self):
+ s = u''
+ if self.title:
+ s = self.title + '\n' + '='*len(self.title) + '\n'
+ for i, item in enumerate(self.items):
+ if isinstance(item, MenuItemTextArea):
+ num = ' '
+ else:
+ num = '%d.' % (i+1)
+
+ s += u'%s %s\n' % (num, unicode(item).replace('\n', '\n '))
+ return s
+
+ def __getitem__(self, i):
+ return self.items[i]
+
+ def __len__(self):
+ return len(self.items)
+
+ def add(self, menuitem):
+ self.items.append(menuitem)
+
+
+class MenuItemLink:
+ def __init__(self, label, ref, stream):
+ self.label = label
+ if type(ref) == unicode:
+ self.ref = ref.encode('utf-8')
+ else:
+ self.ref = ref
+ self.stream = stream
+
+ def __str__(self):
+ res = self.label
+ if not self.stream:
+ res = '[' + res + ']'
+ return res
+
+ def activate(self):
+ return self.ref
+
+
+class MenuItemTextField:
+ def __init__(self, label, name):
+ self.label = label
+ self.name = name
+ self.value = u''
+
+ def __str__(self):
+ return u'%s: %s' % (self.label, self.value)
+
+ def get_query(self):
+ return {self.name: self.value}
+
+ def activate(self):
+ self.value = unicode(raw_input('%s> ' % self.label), sys.stdin.encoding)
+ return None
+
+
+class MenuItemTextArea:
+ def __init__(self, label):
+ self.label = label
+
+ def __str__(self):
+ return textwrap.fill(self.label, width=LINEWIDTH)
+
+ def activate(self):
+ return None
+
+
+class MenuItemList:
+ def __init__(self, label, name, items, values, stdout):
+ self.label = label
+ self.name = name
+ assert len(items) == len(values)
+ self.items = items
+ self.values = values
+ self.current = 0
+ self.stdout = stdout
+
+ def __str__(self):
+ itemstrings = []
+ for i, itemname in enumerate(self.items):
+ if i == self.current:
+ itemstrings.append('<' + itemname + '>')
+ else:
+ itemstrings.append(itemname)
+
+ lab = self.label + ': '
+ return textwrap.fill(u', '.join(itemstrings), width=LINEWIDTH,
+ initial_indent=lab,
+ subsequent_indent=' '*len(lab))
+
+ def get_query(self):
+ if (self.current >= 0) and (self.current < len(self.items)):
+ return {self.name: self.values[self.current]}
+ else:
+ return {}
+
+ def activate(self):
+ itemstrings = []
+ for i, itemname in enumerate(self.items):
+ itemstrings.append('%d. %s' % (i+1, itemname))
+
+ self.stdout.write(u'\n'.join(itemstrings).encode(self.stdout.encoding, 'replace'))
+ self.stdout.write('\n')
+
+ tmp = raw_input('Select item (1-%d)> ' % len(self.items))
+ try:
+ i = int(tmp)
+ if (i < 1) or (i > len(self.items)):
+ raise ValueError
+ self.current = i-1
+ except ValueError:
+ self.stdout.write('Must be an integer in the range 1 - %d\n' % len(self.items))
+ return None
+
+
+class MenuItemSubmitButton:
+ def __init__(self, label, baseurl, subitems):
+ self.label = label
+ if type(baseurl) == unicode:
+ self.baseurl = baseurl.encode('utf-8')
+ else:
+ self.baseurl = baseurl
+ self.subitems = subitems
+
+ def __str__(self):
+ return '[' + self.label + ']'
+
+ def activate(self):
+ baseurl = self.baseurl
+ if baseurl.find('?') == -1:
+ baseurl += '?'
+ else:
+ baseurl += '&'
+
+ parts = []
+ for sub in self.subitems:
+ for key, val in sub.get_query().iteritems():
+ parts.append('subst=' + urllib.quote_plus(key.encode('utf-8')) + ',' + urllib.quote_plus(val.encode('utf-8')))
+
+ return baseurl + '&'.join(parts)
diff --git a/templates/bin/ruutu-dl b/templates/bin/ruutu-dl
new file mode 100755
index 0000000..be8d01e
--- /dev/null
+++ b/templates/bin/ruutu-dl
@@ -0,0 +1,36 @@
+#!/bin/sh
+
+# Downloads a video stream from ruutu.fi to stdout using
+# rtmpdump(-yle). The first parameter is the rtmp URL, the second
+# parameter is the video page URL.
+
+RTMPDUMP=
+
+which rtmpdump > /dev/null 2>&1
+if [ $? = 0 ]; then
+ RTMPDUMP=rtmpdump
+else
+ which rtmpdump-yle > /dev/null 2>&1
+ if [ $? = 0 ]; then
+ RTMPDUMP=rtmpdump-yle
+ fi
+fi
+
+if [ "x$RTMPDUMP" = "x" ]; then
+ echo "ERROR: neither rtmpdump nor rtmpdump-yle not on \$PATH" 1>&2
+ exit 1
+fi
+
+if [ "x$1" = "x" ]; then
+ echo "Expected rtmp URL as parameter" 1>&2
+ exit 1
+fi
+
+if [ "x$2" = "x" ]; then
+ echo "Expected ruutu.fi video page URL as parameter" 1>&2
+ exit 1
+fi
+
+$RTMPDUMP -r $1 -q --swfUrl http://n.sestatic.fi/sites/all/modules/media/Nelonen_mediaplayer_4.6.swf --pageUrl $2 -o -
+
+exit $?
diff --git a/templates/bin/yle-dl b/templates/bin/yle-dl
new file mode 100755
index 0000000..a317b12
--- /dev/null
+++ b/templates/bin/yle-dl
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+# Downloads a video stream from Yle Areena to stdout using yle-dl
+# script. The first parameter is the video page URL.
+
+YLEDL=yle-dl
+
+which $YLEDL > /dev/null 2>&1
+if [ $? != 0 ]; then
+ echo "ERROR: $YLEDL is not on \$PATH" 1>&2
+ echo "Install rtmpdump-yle from http://users.tkk.fi/~aajanki/rtmpdump-yle/index.html" 1>&2
+ exit 1
+fi
+
+if [ "x$1" = "x" ]; then
+ echo "Expected Areena URL as parameter" 1>&2
+ exit 1
+fi
+
+$YLEDL $1 -q -o -
+
+exit $?
diff --git a/templates/google/description.xsl b/templates/google/description.xsl
new file mode 100644
index 0000000..b7cab19
--- /dev/null
+++ b/templates/google/description.xsl
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="/">
+<wvmenu>
+ <title><xsl:value-of select="//title"/></title>
+
+ <textarea>
+ <label><xsl:value-of select="//span[@id='long-desc']"/></label>
+ </textarea>
+ <textarea>
+ <label>Duration: <xsl:value-of select="//span[@id='video-duration']"/></label>
+ </textarea>
+ <textarea>
+ <label>Date: <xsl:value-of select="//span[@id='video-date']"/></label>
+ </textarea>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/google/search.xsl b/templates/google/search.xsl
new file mode 100644
index 0000000..a7a3ab0
--- /dev/null
+++ b/templates/google/search.xsl
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Google video search</title>
+
+ <textfield name="q">
+ <label>Search terms</label>
+ </textfield>
+
+ <itemlist name="so">
+ <label>Sort by</label>
+ <item value="0">Relevance</item>
+ <item value="3">Rating</item>
+ <item value="4">Popularity</item>
+ <item value="1">Date</item>
+ </itemlist>
+
+ <itemlist name="dur">
+ <label>Duration</label>
+ <item value="">All durations</item>
+ <item value="1">Short (&lt; 4 min)</item>
+ <item value="2">Medium (4-20 min)</item>
+ <item value="3">Long (&gt; 20 min)</item>
+ </itemlist>
+
+ <button>
+ <label>Search</label>
+ <submission>wvt:///google/searchresults.xsl?srcurl=<xsl:value-of select="str:encode-uri('http://video.google.com/videosearch?q={q}&amp;so={so}&amp;dur={dur}', true())"/></submission>
+ </button>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/google/searchresults.xsl b/templates/google/searchresults.xsl
new file mode 100644
index 0000000..863d1d8
--- /dev/null
+++ b/templates/google/searchresults.xsl
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Search results</title>
+
+ <xsl:choose>
+ <xsl:when test="not(//div[@class='rl-item'])">
+ <textarea>
+ <label>
+ <xsl:text>Your search did not return any results.</xsl:text>
+ </label>
+ </textarea>
+ </xsl:when>
+
+ <xsl:otherwise>
+ <xsl:for-each select="//div[@class='rl-item']">
+ <xsl:choose>
+ <xsl:when test="starts-with(div/@srcurl, 'http://www.youtube.com/')">
+ <link>
+ <label><xsl:value-of select="normalize-space(div/div/div[@class='rl-title']/a)" /></label>
+ <stream>wvt:///youtube/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl, true())"/></stream>
+ <ref>wvt:///youtube/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(concat('http://gdata.youtube.com/feeds/api/videos/', substring-after(div/@srcurl, 'v='), '?v=2'), true())"/></ref>
+ </link>
+ </xsl:when>
+
+ <xsl:when test="starts-with(div/@srcurl, 'http://video.google.com/')">
+ <link>
+ <label><xsl:value-of select="normalize-space(div/div/div[@class='rl-title']/a)"/></label>
+ <stream>wvt:///google/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl, true())"/></stream>
+ <ref>wvt:///google/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl, true())"/></ref>
+ </link>
+ </xsl:when>
+
+ <xsl:when test="starts-with(div/@srcurl, 'http://www.metacafe.com/')">
+ <link>
+ <label><xsl:value-of select="normalize-space(div/div/div[@class='rl-title']/a)"/></label>
+ <stream>wvt:///metacafe/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl)"/></stream>
+ <ref>wvt:///metacafe/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl)"/></ref>
+ </link>
+ </xsl:when>
+
+ <xsl:when test="starts-with(div/@srcurl, 'http://vimeo.com/')">
+ <link>
+ <label><xsl:value-of select="normalize-space(div/div/div[@class='rl-title']/a)"/></label>
+ <stream>wvt:///vimeo/video.xsl?srcurl=http://www.vimeo.com/moogaloop/load/clip:<xsl:value-of select="substring-after(div/@srcurl, 'http://vimeo.com/')"/></stream>
+ <ref>wvt:///vimeo/description.xsl?srcurl=http://vimeo.com/api/v2/video/<xsl:value-of select="substring-after(div/@srcurl, 'http://vimeo.com/')"/>.xml</ref>
+ </link>
+ </xsl:when>
+
+ <xsl:when test="starts-with(div/@srcurl, 'http://svtplay.se/')">
+ <link>
+ <label><xsl:value-of select="normalize-space(div/div/div[@class='rl-title']/a)"/></label>
+ <stream>wvt:///svtplay/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl, true())"/></stream>
+ <ref>wvt:///svtplay/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl, true())"/></ref>
+ </link>
+ </xsl:when>
+
+ </xsl:choose>
+ </xsl:for-each>
+
+ <xsl:if test="//td[@class='prev']/a">
+ <link>
+ <label>Previous</label>
+ <ref>wvt:///google/searchresults.xsl?srcurl=<xsl:value-of select="str:encode-uri(//td[@class='prev']/a/@href, true())"/></ref>
+ </link>
+ </xsl:if>
+
+ <xsl:if test="//td[@class='next']/a">
+ <link>
+ <label>Next</label>
+ <ref>wvt:///google/searchresults.xsl?srcurl=<xsl:value-of select="str:encode-uri(//td[@class='next']/a/@href, true())"/></ref>
+ </link>
+ </xsl:if>
+ </xsl:otherwise>
+ </xsl:choose>
+
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/google/service.xml b/templates/google/service.xml
new file mode 100644
index 0000000..3e02e93
--- /dev/null
+++ b/templates/google/service.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<service>
+ <title>Google Video</title>
+ <ref>wvt:///google/search.xsl</ref>
+ <description>Google video search</description>
+</service>
diff --git a/templates/google/video.xsl b/templates/google/video.xsl
new file mode 100644
index 0000000..52d6d98
--- /dev/null
+++ b/templates/google/video.xsl
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="/">
+<mediaurl>
+ <title><xsl:value-of select="/html/head/title" /></title>
+ <xsl:for-each select="/html/body/script">
+ <xsl:variable name="videourl" select="str:decode-uri(substring-before(substring-after(., 'videoUrl\x3d'), '\x26'))"/>
+ <xsl:if test="$videourl">
+ <url><xsl:value-of select="$videourl"/></url>
+ </xsl:if>
+ </xsl:for-each>
+</mediaurl>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/katsomo/mainmenu.xsl b/templates/katsomo/mainmenu.xsl
new file mode 100644
index 0000000..b7ba1cf
--- /dev/null
+++ b/templates/katsomo/mainmenu.xsl
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>MTV3 Katsomo</title>
+
+ <link>
+ <label>Haku</label>
+ <ref>wvt:///katsomo/search.xsl</ref>
+ </link>
+
+ <xsl:for-each select="id('mainMenu')/li[a/@href != '/']">
+ <link>
+ <label><xsl:value-of select="a"/></label>
+ <ref>wvt:///katsomo/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(a/@href, true())"/></ref>
+ </link>
+ </xsl:for-each>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/katsomo/navigation.xsl b/templates/katsomo/navigation.xsl
new file mode 100644
index 0000000..e43753d
--- /dev/null
+++ b/templates/katsomo/navigation.xsl
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:param name="docurl"/>
+
+<xsl:template match="ol[@class='categoryList']/li">
+ <link>
+ <label><xsl:value-of select="normalize-space(a)"/></label>
+ <ref>wvt:///katsomo/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(a/@href, true())"/></ref>
+ </link>
+</xsl:template>
+
+<xsl:template match="ol[@class='programList']/li">
+ <xsl:variable name="progId" select="substring-after(a/@href, 'progId=')"/>
+ <xsl:variable name="treeId" select="substring-after($docurl, 'treeId=')"/>
+ <xsl:variable name="title" select="normalize-space(a[string(.)])"/>
+
+ <link>
+ <label><xsl:value-of select="$title"/></label>
+ <stream>wvt:///katsomo/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(concat('http://katsomo.fi/showContent.do?treeId=', $treeId, '&amp;progId=', $progId, '&amp;adData=%7B%22ad%22%3A%20%7B%7D%7D&amp;ajax=true&amp;serial=1'), true())"/>&amp;param=title,<xsl:value-of select="str:encode-uri($title, true())"/>&amp;HTTP-header=cookie,webtv.bandwidth%3D1000%3BautoFullScreen%3Dfalse%3Bwebtv.playerPlatform%3D0</stream>
+ </link>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title><xsl:value-of select="/html/head/meta[@name='title']/@content"/></title>
+
+ <xsl:if test="//ol[@class='categoryList']/li and //ol[@class='programList']/li">
+ <textarea>
+ <label>Ohjelmat</label>
+ </textarea>
+ </xsl:if>
+ <xsl:apply-templates select="//ol[@class='categoryList']/li"/>
+
+ <xsl:if test="//ol[@class='categoryList']/li and //ol[@class='programList']/li">
+ <textarea>
+ <label>Jaksot</label>
+ </textarea>
+ </xsl:if>
+ <xsl:apply-templates select="//ol[@class='programList']/li"/>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/katsomo/search.xsl b/templates/katsomo/search.xsl
new file mode 100644
index 0000000..c963b71
--- /dev/null
+++ b/templates/katsomo/search.xsl
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTf-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Haku</title>
+
+ <textfield name="query">
+ <label>Hakusana</label>
+ </textfield>
+
+ <button>
+ <label>Hae</label>
+ <submission>wvt:///katsomo/searchresults.xsl?srcurl=<xsl:value-of select="str:encode-uri('http://katsomo.fi/search.do?keywords={query}&amp;treeId=9992', true())"/></submission>
+ </button>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/katsomo/searchresults.xsl b/templates/katsomo/searchresults.xsl
new file mode 100644
index 0000000..2747afc
--- /dev/null
+++ b/templates/katsomo/searchresults.xsl
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:template match="a">
+ <xsl:variable name="progId" select="substring-after(@href, 'progId=')"/>
+ <xsl:variable name="title" select="normalize-space(.)"/>
+
+ <link>
+ <label><xsl:value-of select="$title"/></label>
+ <stream>wvt:///katsomo/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(concat('http://katsomo.fi/showContent.do?progId=', $progId, '&amp;adData=%7B%22ad%22%3A%20%7B%7D%7D&amp;ajax=true&amp;serial=1'), true())"/>&amp;param=title,<xsl:value-of select="str:encode-uri($title, true())"/>&amp;HTTP-header=cookie,webtv.bandwidth%3D1000%3BautoFullScreen%3Dfalse%3Bwebtv.playerPlatform%3D0</stream>
+ </link>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Hakutulokset: <xsl:value-of select="id('searchResults')/div/div[@class='description']/span"/></title>
+
+ <xsl:if test="not(id('resultList')/div[@class='item'])">
+ <textarea>
+ <label><xsl:value-of select="normalize-space(id('siteMapList')/p)"/></label>
+ </textarea>
+ </xsl:if>
+
+ <xsl:apply-templates select="id('resultList')/div[@class='item']/h6/a[not(@class='programType')]"/>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/katsomo/service.xml b/templates/katsomo/service.xml
new file mode 100644
index 0000000..b1bd0bc
--- /dev/null
+++ b/templates/katsomo/service.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<service>
+ <title>MTV3 Katsomo</title>
+ <ref>wvt:///katsomo/mainmenu.xsl?srcurl=http%3A//katsomo.fi/</ref>
+ <description>Net TV service of the Finnish broadcasting company MTV3</description>
+</service>
diff --git a/templates/katsomo/video.xsl b/templates/katsomo/video.xsl
new file mode 100644
index 0000000..9d20c49
--- /dev/null
+++ b/templates/katsomo/video.xsl
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:param name="title">katsomovideo</xsl:param>
+
+<xsl:template match="/">
+<mediaurl>
+ <title><xsl:value-of select="$title"/></title>
+
+ <url><xsl:value-of select='substring-before(substring-after(//script, "metaUrl&apos;: &apos;"), "&apos;")'/></url>
+</mediaurl>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/metacafe/categories.xsl b/templates/metacafe/categories.xsl
new file mode 100644
index 0000000..7dc155e
--- /dev/null
+++ b/templates/metacafe/categories.xsl
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Metacafe</title>
+
+ <link>
+ <label>Search</label>
+ <ref>wvt:///metacafe/search.xsl</ref>
+ </link>
+
+ <link>
+ <label>Most viewed channels</label>
+ <ref>wvt:///metacafe/channellist.xsl?srcurl=/api/channels/</ref>
+ </link>
+
+ <xsl:for-each select="id('LeftCol')/ul/li/a">
+ <!-- '18+ Only' is empty unless family filter is off. Ignore the
+ category until I find a way to turn off the filter. -->
+ <xsl:if test="@title != '18+ Only'">
+ <link>
+ <label><xsl:value-of select="@title"/></label>
+ <ref>wvt:///metacafe/navigation.xsl?srcurl=/api/videos/-/<xsl:value-of select="substring-after(@href, '/videos/')"/></ref>
+ </link>
+ </xsl:if>
+ </xsl:for-each>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/metacafe/channellist.xsl b/templates/metacafe/channellist.xsl
new file mode 100644
index 0000000..2bb74ec
--- /dev/null
+++ b/templates/metacafe/channellist.xsl
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="item">
+ <link>
+ <label><xsl:value-of select="title" /> (<xsl:value-of select="videos"/> videos, avg. rank: <xsl:value-of select="avg_rank"/>)</label>
+ <ref>wvt:///metacafe/navigation.xsl?srcurl=/api/users/<xsl:value-of select="str:encode-uri(translate(title, ' ', '+'), true())"/>/channel?time=all_time</ref>
+ </link>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title><xsl:value-of select="/rss/channel/title"/></title>
+
+ <xsl:apply-templates select="/rss/channel/item"/>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/metacafe/description.xsl b/templates/metacafe/description.xsl
new file mode 100644
index 0000000..3cb7f2b
--- /dev/null
+++ b/templates/metacafe/description.xsl
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+xmlns:media="http://search.yahoo.com/mrss/">
+
+<!-- Convert $seconds to hours:min:sec format -->
+<xsl:template name="pretty-print-seconds">
+ <xsl:param name="seconds"/>
+
+ <xsl:variable name="sec" select="$seconds mod 60"/>
+ <xsl:variable name="min" select="floor($seconds div 60) mod 60"/>
+ <xsl:variable name="hour" select="floor($seconds div 3600)"/>
+
+ <xsl:value-of select="concat($hour, ':', format-number($min, '00'), ':', format-number($sec, '00'))"/>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title><xsl:value-of select="/rss/channel/item/title"/></title>
+
+ <textarea>
+ <label><xsl:value-of select="/rss/channel/item/media:description"/></label>
+ </textarea>
+
+ <textarea>
+ <label>Duration: <xsl:call-template name="pretty-print-seconds">
+ <xsl:with-param name="seconds">
+ <xsl:value-of select="/rss/channel/item/media:content/@duration"/>
+ </xsl:with-param>
+ </xsl:call-template>
+ </label>
+ </textarea>
+
+ <textarea>
+ <label>Rating: <xsl:value-of select="/rss/channel/item/rank"/></label>
+ </textarea>
+
+ <textarea>
+ <label>published: <xsl:value-of select="/rss/channel/item/pubDate"/></label>
+ </textarea>
+
+
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/metacafe/navigation.xsl b/templates/metacafe/navigation.xsl
new file mode 100644
index 0000000..4ff821a
--- /dev/null
+++ b/templates/metacafe/navigation.xsl
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="item">
+ <link>
+ <label><xsl:value-of select="title" /></label>
+ <xsl:choose>
+ <xsl:when test="starts-with(id, 'yt-')">
+ <stream>wvt:///youtube/video.xsl?srcurl=http%3A//www.youtube.com/watch%3Fv=<xsl:value-of select="substring(id, 4)"/></stream>
+ </xsl:when>
+ <xsl:otherwise>
+ <stream>wvt:///metacafe/video.xsl?srcurl=<xsl:value-of select="link"/></stream>
+ </xsl:otherwise>
+ </xsl:choose>
+
+ <ref>wvt:///metacafe/description.xsl?srcurl=/api/item/<xsl:value-of select="id"/></ref>
+ </link>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title><xsl:value-of select="/rss/channel/title"/></title>
+
+ <xsl:apply-templates select="/rss/channel/item"/>
+
+ <xsl:if test="count(/rss/channel/item) = 0">
+ <textarea>
+ <label>No matching results.</label>
+ </textarea>
+ </xsl:if>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/metacafe/search.xsl b/templates/metacafe/search.xsl
new file mode 100644
index 0000000..205bd98
--- /dev/null
+++ b/templates/metacafe/search.xsl
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Metacafe Search</title>
+
+ <textfield name="vq">
+ <label>Keywords</label>
+ </textfield>
+
+ <itemlist name="orderby">
+ <label>Sort by</label>
+ <item value="updated">Most recent</item>
+ <item value="viewCount">View Count</item>
+ <item value="discussed">Most discussed</item>
+ </itemlist>
+
+ <itemlist name="time">
+ <label>Published</label>
+ <item value="all_time">Anytime</item>
+ <item value="today">During last 24 hours</item>
+ <item value="this_week">This week</item>
+ <item value="this_month">This month</item>
+ </itemlist>
+
+ <button>
+ <label>Search</label>
+ <submission>wvt:///metacafe/navigation.xsl?srcurl=http%3A//www.metacafe.com/api/videos%3Fvq=%7Bvq%7D%26orderby=%7Borderby%7D%26time=%7Btime%7D</submission>
+ </button>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/metacafe/service.xml b/templates/metacafe/service.xml
new file mode 100644
index 0000000..8e9fc33
--- /dev/null
+++ b/templates/metacafe/service.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<service>
+ <title>Metacafe</title>
+ <ref>wvt:///metacafe/categories.xsl?srcurl=http%3A//www.metacafe.com/videos/</ref>
+ <description>Video sharing site specializing in short-form original content</description>
+</service>
diff --git a/templates/metacafe/video.xsl b/templates/metacafe/video.xsl
new file mode 100644
index 0000000..884e87f
--- /dev/null
+++ b/templates/metacafe/video.xsl
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="/">
+ <mediaurl>
+ <title><xsl:value-of select="normalize-space(id('ItemTitle'))"/></title>
+ <url><xsl:value-of select="str:decode-uri(substring-before(substring-after(//param[@name='flashvars']/@value, 'mediaURL='), '&amp;'))"/></url>
+ </mediaurl>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/ruutufi/description.xsl b/templates/ruutufi/description.xsl
new file mode 100644
index 0000000..ad04d79
--- /dev/null
+++ b/templates/ruutufi/description.xsl
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:param name="docurl"/>
+
+<!-- Convert $seconds to hours:min:sec format -->
+<xsl:template name="pretty-print-seconds">
+ <xsl:param name="seconds"/>
+
+ <xsl:variable name="sec" select="$seconds mod 60"/>
+ <xsl:variable name="min" select="floor($seconds div 60) mod 60"/>
+ <xsl:variable name="hour" select="floor($seconds div 3600)"/>
+
+ <xsl:value-of select="concat($hour, ':', format-number($min, '00'), ':', format-number($sec, '00'))"/>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title><xsl:value-of select="/Playerdata/Behavior/Program/@program_name"/></title>
+
+ <xsl:if test="/Playerdata/Behavior/Program/@description">
+ <textarea>
+ <label><xsl:value-of select="/Playerdata/Behavior/Program/@description"/></label>
+ </textarea>
+ </xsl:if>
+
+ <textarea>
+ <label><xsl:value-of select="/Playerdata/Behavior/Program/@episode_name"/></label>
+ </textarea>
+
+ <textarea>
+ <label>Kesto: <xsl:call-template name="pretty-print-seconds">
+ <xsl:with-param name="seconds">
+ <xsl:value-of select="/Playerdata/Behavior/Program/@episode_duration"/>
+ </xsl:with-param>
+ </xsl:call-template>
+ </label>
+ </textarea>
+
+ <link>
+ <label>Lataa</label>
+ <stream>wvt:///ruutufi/video.xsl?srcurl=<xsl:value-of select="str:encode-uri($docurl, true())"/></stream>
+ </link>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/ruutufi/mainmenu.xsl b/templates/ruutufi/mainmenu.xsl
new file mode 100644
index 0000000..1d70ac3
--- /dev/null
+++ b/templates/ruutufi/mainmenu.xsl
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Ruutu.fi</title>
+
+ <link>
+ <label>Haku</label>
+ <ref>wvt:///ruutufi/search.xsl</ref>
+ </link>
+
+ <link>
+ <label>Listaa sarjat</label>
+ <ref>wvt:///ruutufi/series.xsl?srcurl=http://www.ruutu.fi/ajax/media_get_netti_tv_series_list/all/false&amp;postprocess=json2xml</ref>
+ </link>
+
+ <link>
+ <label>Uusimmat</label>
+ <ref>wvt:///ruutufi/program.xsl?srcurl=http://www.ruutu.fi/ajax/media_get_nettitv_media/all/video_episode/__/latestdesc/0/25/true/__&amp;postprocess=json2xml</ref>
+ </link>
+
+ <link>
+ <label>Katsotuimmat</label>
+ <ref>wvt:///ruutufi/program.xsl?srcurl=http://www.ruutu.fi/ajax/media_get_nettitv_media/all/video_episode/__/most_watched/0/25/true/__&amp;postprocess=json2xml</ref>
+ </link>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/ruutufi/program.xsl b/templates/ruutufi/program.xsl
new file mode 100644
index 0000000..593036f
--- /dev/null
+++ b/templates/ruutufi/program.xsl
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:param name="docurl"/>
+
+<xsl:template match="dict">
+ <xsl:param name="mediatype" select="video"/>
+
+ <xsl:variable name="videoid">
+ <xsl:choose>
+ <xsl:when test="video_id_to_use">
+ <xsl:value-of select="video_id_to_use"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="substring-after(nodeurl, 'vid=')"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:variable>
+
+ <link>
+ <label>
+ <xsl:choose>
+ <xsl:when test="program_episode_name">
+ <xsl:value-of select="concat(program_episode_name, ' ', video_datetime_to_use)"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="title"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </label>
+
+ <xsl:variable name="videourl">http://www.nelonen.fi/utils/video_config/%3Fq%3D<xsl:value-of select="$mediatype"/>/<xsl:value-of select="$videoid"/>%26site%3Dwww.ruutu.fi%26ageCheckURL%3Dhttp://sso.nelonenmedia.fi/ajax/check_age/%26current_page%3Dhttp://www.ruutu.fi/video</xsl:variable>
+
+ <ref>wvt:///ruutufi/description.xsl?srcurl=<xsl:value-of select="$videourl"/></ref>
+ <stream>wvt:///ruutufi/video.xsl?srcurl=<xsl:value-of select="$videourl"/></stream>
+ </link>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <xsl:variable name="start">
+ <xsl:value-of select="number(str:tokenize($docurl, '/')[9])"/>
+ </xsl:variable>
+
+ <!-- title -->
+ <title>
+ <xsl:choose>
+ <xsl:when test="/jsondocument/dict/video_episode/list/li[1]/dict/series_name">
+ <xsl:value-of select="/jsondocument/dict/video_episode/list/li[1]/dict/series_name"/>
+ </xsl:when>
+ <xsl:when test="/jsondocument/dict/video/list/li[1]/dict/clip_series_name">
+ <xsl:value-of select="/jsondocument/dict/video/list/li[1]/dict/clip_series_name"/>
+ </xsl:when>
+ <xsl:otherwise>Ruutu.fi</xsl:otherwise>
+ </xsl:choose>
+ </title>
+
+ <!-- Video links -->
+ <xsl:if test="not(/jsondocument/dict/video | /jsondocument/dict/video_episode)">
+ <textarea>
+ <label>Ei jaksoja</label>
+ </textarea>
+ </xsl:if>
+
+ <xsl:apply-templates select="/jsondocument/dict/video_episode/list/li/dict">
+ <xsl:with-param name="mediatype">video_episode</xsl:with-param>
+ </xsl:apply-templates>
+ <xsl:apply-templates select="/jsondocument/dict/video/list/li/dict">
+ <xsl:with-param name="mediatype">video</xsl:with-param>
+ </xsl:apply-templates>
+
+ <xsl:if test="contains($docurl, '/video_episode/') and ($start = 0)">
+ <link>
+ <label>Klipit</label>
+ <ref>wvt:///ruutufi/program.xsl?srcurl=<xsl:value-of select="str:replace($docurl, '/video_episode/', '/video/')"/>&amp;postprocess=json2xml</ref>
+ </link>
+ </xsl:if>
+
+ <!-- prev/next links -->
+ <xsl:variable name="total">
+ <xsl:value-of select="number(/jsondocument/dict/total_count)"/>
+ </xsl:variable>
+
+ <xsl:variable name="urlend">
+ <xsl:text>/</xsl:text><xsl:value-of select="str:tokenize($docurl, '/')[10]"/><xsl:text>/</xsl:text><xsl:value-of select="str:tokenize($docurl, '/')[11]"/><xsl:text>/</xsl:text><xsl:value-of select="str:tokenize($docurl, '/')[12]"/>
+ </xsl:variable>
+
+ <xsl:variable name="prevstart">
+ <xsl:choose>
+ <xsl:when test="$start >= 25">
+ <xsl:value-of select="string($start - 25)"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:text>0</xsl:text>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:variable>
+
+ <xsl:if test="$start > 0">
+ <link>
+ <label>Edellinen</label>
+ <ref>wvt:///ruutufi/program.xsl?srcurl=<xsl:value-of select="str:encode-uri(str:replace($docurl, concat(string($start), $urlend), concat($prevstart, $urlend)), true())"/>&amp;postprocess=json2xml</ref>
+ </link>
+ </xsl:if>
+
+ <xsl:if test="$start + 25 &lt; $total">
+ <link>
+ <label>Seuraava</label>
+ <ref>wvt:///ruutufi/program.xsl?srcurl=<xsl:value-of select="str:encode-uri(str:replace($docurl, concat(string($start), $urlend), concat(string($start+25), $urlend)), true())"/>&amp;postprocess=json2xml</ref>
+ </link>
+ </xsl:if>
+
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/ruutufi/search.xsl b/templates/ruutufi/search.xsl
new file mode 100644
index 0000000..07f2700
--- /dev/null
+++ b/templates/ruutufi/search.xsl
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTf-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Haku</title>
+
+ <textfield name="query">
+ <label>Hakusana</label>
+ </textfield>
+
+ <button>
+ <label>Hae</label>
+ <submission>wvt:///ruutufi/program.xsl?srcurl=<xsl:value-of select="str:encode-uri('http://www.ruutu.fi/search/search_new.php?params=%7B%22search%22%3A%22{query}%22%2C%22groups%22%3A%7B%22video%22%3A%7B%22types%22%3A%5B%22video_clip%22%5D%7D%2C%22video_episode%22%3A%7B%22types%22%3A%5B%22video_episode%22%5D%7D%2C%22audio%22%3A%7B%22types%22%3A%5B%22audio%22%5D%7D%7D%7D', true())"/>&amp;postprocess=json2xml</submission>
+ </button>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/ruutufi/series.xsl b/templates/ruutufi/series.xsl
new file mode 100644
index 0000000..2e8f4d2
--- /dev/null
+++ b/templates/ruutufi/series.xsl
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:output method="xml" version="1.0" encoding="UTF-8" />
+
+<xsl:template match="dict">
+ <xsl:if test="is_video=1">
+ <link>
+ <label><xsl:value-of select="name"/></label>
+ <ref>wvt:///ruutufi/program.xsl?srcurl=http://www.ruutu.fi/ajax/media_get_nettitv_video/all/video_episode/<xsl:value-of select="str:encode-uri(str:encode-uri(url_encode_name, true()), true())"/>/latestdesc/0/25/true/__&amp;postprocess=json2xml</ref>
+ <!-- Yes, ruutu.fi really expects url_encode_name to be double-url-encoded! -->
+ </link>
+ </xsl:if>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Kaikki sarjat</title>
+
+ <xsl:apply-templates select="/jsondocument/list/li/dict"/>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/ruutufi/service.xml b/templates/ruutufi/service.xml
new file mode 100644
index 0000000..7a106aa
--- /dev/null
+++ b/templates/ruutufi/service.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<service>
+ <title>ruutu.fi</title>
+ <ref>wvt:///ruutufi/mainmenu.xsl?srcurl=http%3A//www.ruutu.fi/</ref>
+ <description>Net TV service of the Finnish broadcasting company Nelonen</description>
+</service>
diff --git a/templates/ruutufi/video.xsl b/templates/ruutufi/video.xsl
new file mode 100644
index 0000000..e6af547
--- /dev/null
+++ b/templates/ruutufi/video.xsl
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:template match="/">
+<mediaurl>
+ <title><xsl:value-of select="concat(/Playerdata/Behavior/Program/@program_name, ' ', /Playerdata/Behavior/Program/@episode_name)"/></title>
+
+ <xsl:choose>
+ <xsl:when test="starts-with(/Playerdata/Clip/SourceFile, 'rtmp://')">
+ <url priority="50">wvt:///bin/ruutu-dl?contenttype=video/x-flv&amp;arg=<xsl:value-of select="str:encode-uri(/Playerdata/Clip/SourceFile, true())"/>&amp;arg=http://www.ruutu.fi/video</url>
+ </xsl:when>
+ <xsl:otherwise>
+ <url priority="50"><xsl:value-of select="/Playerdata/Clip/SourceFile"/></url>
+ </xsl:otherwise>
+ </xsl:choose>
+</mediaurl>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/ruutufi/video2.xsl b/templates/ruutufi/video2.xsl
new file mode 100644
index 0000000..39bef06
--- /dev/null
+++ b/templates/ruutufi/video2.xsl
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="/">
+<mediaurl>
+ <title><xsl:value-of select="concat(id('ruutuVideoInfo')/p[@class='name'], ' ', id('ruutuVideoInfo')/p[@class='timeStamp'])"/></title>
+
+ <url priority="50">wvt:///bin/ruutu-dl?contenttype=video/x-flv&amp;arg=<xsl:value-of select='substring-before(substring-after(//script[contains(., "vplayer1")], "providerURL&apos;, &apos;"), "&apos;")'/>&amp;arg=<xsl:value-of select="str:encode-uri($docurl, true())"/></url>
+</mediaurl>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/subtv/description.xsl b/templates/subtv/description.xsl
new file mode 100644
index 0000000..c914f3b
--- /dev/null
+++ b/templates/subtv/description.xsl
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:param name="title"/>
+<xsl:param name="desc"/>
+<xsl:param name="pubdate"/>
+<xsl:param name="pid"/>
+
+<xsl:template match="/">
+<wvmenu>
+ <title><xsl:value-of select="$title"/></title>
+
+ <textarea>
+ <label><xsl:value-of select="$desc"/></label>
+ </textarea>
+
+ <textarea>
+ <label><xsl:value-of select="$pubdate"/></label>
+ </textarea>
+
+ <link>
+ <label>Lataa</label>
+ <stream>wvt:///subtv/video.xsl?param=pid,<xsl:value-of select="$pid"/>&amp;param=title,<xsl:value-of select="str:encode-uri($title, true())"/></stream>
+ </link>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/subtv/mainmenu.xsl b/templates/subtv/mainmenu.xsl
new file mode 100644
index 0000000..2158295
--- /dev/null
+++ b/templates/subtv/mainmenu.xsl
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Subin netti-TV</title>
+
+ <xsl:for-each select="//div[@class='netissakaikki']/ul/li/a">
+ <link>
+ <label><xsl:value-of select="."/></label>
+ <ref>wvt:///subtv/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(@href, true())"/></ref>
+ </link>
+ </xsl:for-each>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/subtv/navigation.xsl b/templates/subtv/navigation.xsl
new file mode 100644
index 0000000..3c0b039
--- /dev/null
+++ b/templates/subtv/navigation.xsl
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:param name="docurl"/>
+<xsl:variable name="programname" select="id('page')/div[@class='ohjelma_yla ohjelmanavi']/h1"/>
+
+<xsl:template match="li">
+ <xsl:variable name="progId" select="substring-after(div[@class='outerwrap']//a/@href, '?')"/>
+ <xsl:variable name="title" select="concat($programname, ' - ', normalize-space(.//h5))"/>
+
+ <xsl:if test="$progId">
+ <link>
+ <label><xsl:value-of select="normalize-space(.//h5)"/></label>
+ <stream>wvt:///subtv/video.xsl?srcurl=<xsl:value-of select="str:encode-uri($docurl, true())"/>&amp;param=pid,<xsl:value-of select="$progId"/>&amp;param=title,<xsl:value-of select="str:encode-uri($title, true())"/></stream>
+ <ref>wvt:///subtv/description.xsl?param=title,<xsl:value-of select="str:encode-uri($title, true())"/>&amp;param=desc,<xsl:value-of select="str:encode-uri(.//span[@class='verho_content']/div, true())"/>&amp;param=pubdate,<xsl:value-of select="str:encode-uri(p[@class='julkaistu'], true())"/>&amp;param=pid,<xsl:value-of select="$progId"/></ref>
+ </link>
+ </xsl:if>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title><xsl:value-of select="$programname"/></title>
+
+ <xsl:choose>
+ <xsl:when test="id('uusimmat')/li">
+ <xsl:apply-templates select="id('uusimmat')/li"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <textarea>
+ <label>Ei jaksoja</label>
+ </textarea>
+ </xsl:otherwise>
+ </xsl:choose>
+
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/subtv/service.xml b/templates/subtv/service.xml
new file mode 100644
index 0000000..6a7a44f
--- /dev/null
+++ b/templates/subtv/service.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<service>
+ <title>Subtv</title>
+ <ref>wvt:///subtv/mainmenu.xsl?srcurl=http%3A//www.sub.fi/katsonetista/</ref>
+ <description>Sub is the third biggest commercial tv channel in Finland.</description>
+</service>
diff --git a/templates/subtv/video.xsl b/templates/subtv/video.xsl
new file mode 100644
index 0000000..32e5b1e
--- /dev/null
+++ b/templates/subtv/video.xsl
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:param name="title"/>
+<xsl:param name="pid"/>
+
+<xsl:template match="/">
+<mediaurl>
+ <title><xsl:value-of select="$title"/></title>
+
+ <url><xsl:value-of select="concat('http://www.katsomo.fi/metafile.asx?p=', $pid, '&amp;bw=800')"/></url>
+</mediaurl>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/svtplay/categories.xsl b/templates/svtplay/categories.xsl
new file mode 100644
index 0000000..bd64abd
--- /dev/null
+++ b/templates/svtplay/categories.xsl
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>SVT Play</title>
+
+ <xsl:for-each select="//div[@id='categorylist']//ul/li//a">
+ <link>
+ <label><xsl:value-of select="span[@class='category-header']"/></label>
+ <ref>wvt:///svtplay/navigation.xsl?srcurl=<xsl:value-of select="@href"/></ref>
+ </link>
+ </xsl:for-each>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/svtplay/description.xsl b/templates/svtplay/description.xsl
new file mode 100644
index 0000000..f3c3ae6
--- /dev/null
+++ b/templates/svtplay/description.xsl
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="div[@class='info']/ul">
+ <textarea>
+ <label>
+ <xsl:value-of select="normalize-space(li[@class='title']/div)"/>
+ </label>
+ </textarea>
+ <textarea>
+ <label>
+ <xsl:value-of select="normalize-space(li[@class='episode']/div)"/>
+ </label>
+ </textarea>
+ <textarea>
+ <label>
+ <xsl:value-of select="concat(normalize-space(li[1]/span[2]), ' ', normalize-space(li/span[2]/following-sibling::text()))"/>
+ </label>
+ </textarea>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title>
+ <xsl:choose>
+ <xsl:when test="normalize-space(//h1/a/img/@alt)">
+ <xsl:value-of select="concat(normalize-space(//h1/a/img/@alt), ' ', //div[@class='info']//h2)"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="concat(normalize-space(//h1/a), ' ', //div[@class='info']//h2)"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </title>
+
+ <xsl:apply-templates select="//div[@class='info']/ul"/>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/svtplay/navigation.xsl b/templates/svtplay/navigation.xsl
new file mode 100644
index 0000000..7071dfe
--- /dev/null
+++ b/templates/svtplay/navigation.xsl
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="text()" />
+
+<xsl:template match="div[@id='pb']">
+ <xsl:apply-templates/>
+</xsl:template>
+
+<xsl:template match="div[@id='sb']">
+ <xsl:apply-templates/>
+</xsl:template>
+
+<xsl:template match="div[@id='se']">
+ <xsl:apply-templates/>
+</xsl:template>
+
+<!-- Programs -->
+<xsl:template match="div[@class='content']//ul/li/a[1]">
+ <link>
+ <label><xsl:value-of select="normalize-space(span)"/></label>
+ <ref>wvt:///svtplay/programmenu.xsl?srcurl=<xsl:value-of select="str:encode-uri(@href, true())"/></ref>
+ </link>
+</xsl:template>
+
+<!-- next/prev links -->
+<xsl:template match="div[@class='footer']/div[@class='pagination']/ul[@class='pagination program']/li">
+ <xsl:if test="@class='prev '">
+ <link>
+ <label><xsl:value-of select="a/img/@alt"/></label>
+ <ref>wvt:///svtplay/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(a/@href, true())"/></ref>
+ </link>
+ </xsl:if>
+
+ <xsl:if test="@class='next '">
+ <link>
+ <label><xsl:value-of select="a/img/@alt"/></label>
+ <ref>wvt:///svtplay/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(a/@href, true())"/></ref>
+ </link>
+ </xsl:if>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title>
+ <xsl:choose>
+ <xsl:when test="normalize-space(//h1)">
+ <xsl:value-of select="normalize-space(//h1)"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="normalize-space(//h1/a/img/@alt)"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </title>
+
+ <!-- In most categories the content is in pb and se nodes, except
+ for Nyheter and Sport, where the content is in sb and se nodes.
+ On the other hand, we can't match sb unconditionally because in
+ Öppet arkiv sb contains klips instead of programs! -->
+ <xsl:choose>
+ <xsl:when test="//div[@id='pb']">
+ <xsl:apply-templates select="//div[@id='pb']|//div[@id='se']"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:apply-templates select="//div[@id='sb']|//div[@id='se']"/>
+ </xsl:otherwise>
+ </xsl:choose>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/svtplay/programmenu.xsl b/templates/svtplay/programmenu.xsl
new file mode 100644
index 0000000..4bd120c
--- /dev/null
+++ b/templates/svtplay/programmenu.xsl
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="text()" />
+
+<xsl:template match="div[@id='pb']">
+ <xsl:apply-templates/>
+</xsl:template>
+
+<!-- Broadcasts -->
+<xsl:template match="div[@class='content']//ul/li/a">
+ <link>
+ <label><xsl:value-of select="normalize-space(span)"/></label>
+ <ref>wvt:///svtplay/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(@href, true())"/></ref>
+ <stream>wvt:///svtplay/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(@href, true())"/></stream>
+ </link>
+</xsl:template>
+
+<!-- next/prev links -->
+<xsl:template match="div[@class='footer']/div[@class='pagination']/ul[@class='pagination program']/li">
+ <xsl:if test="@class='prev '">
+ <link>
+ <label><xsl:value-of select="a/img/@alt"/></label>
+ <ref>wvt:///svtplay/programmenu.xsl?srcurl=<xsl:value-of select="str:encode-uri(a/@href, true())"/></ref>
+ </link>
+ </xsl:if>
+
+ <xsl:if test="@class='next '">
+ <link>
+ <label><xsl:value-of select="a/img/@alt"/></label>
+ <ref>wvt:///svtplay/programmenu.xsl?srcurl=<xsl:value-of select="str:encode-uri(a/@href, true())"/></ref>
+ </link>
+ </xsl:if>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title>
+ <xsl:choose>
+ <xsl:when test="normalize-space(//h1/a)">
+ <xsl:value-of select="normalize-space(//h1/a)"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="normalize-space(//h1/a/img/@alt)"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </title>
+
+ <xsl:apply-templates select="//div[@id='sb']"/>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/svtplay/service.xml b/templates/svtplay/service.xml
new file mode 100644
index 0000000..86a36f6
--- /dev/null
+++ b/templates/svtplay/service.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<service>
+ <title>SVT Play</title>
+ <ref>wvt:///svtplay/categories.xsl?srcurl=http://svtplay.se/kategorier</ref>
+ <description>Swedish Television, online TV service</description>
+</service>
diff --git a/templates/svtplay/video.xsl b/templates/svtplay/video.xsl
new file mode 100644
index 0000000..af6aeb9
--- /dev/null
+++ b/templates/svtplay/video.xsl
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="/">
+<mediaurl>
+ <title>
+ <xsl:choose>
+ <xsl:when test="normalize-space(//h1/a/img/@alt)">
+ <xsl:value-of select="concat(normalize-space(//h1/a/img/@alt), ' ', //div[@class='info']//h2)"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="concat(normalize-space(//h1/a), ' ', //div[@class='info']//h2)"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </title>
+
+ <url priority="50"><xsl:value-of select="substring-before(substring-after((//object/param[@name='flashvars'])[1]/@value, 'pathflv='), '&amp;')"/></url>
+ <url priority="40"><xsl:value-of select="//a[@class='external-player']/@href"/></url>
+</mediaurl>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/vimeo/channels.xsl b/templates/vimeo/channels.xsl
new file mode 100644
index 0000000..ae3c0d8
--- /dev/null
+++ b/templates/vimeo/channels.xsl
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Vimeo Channels</title>
+
+ <xsl:for-each select="//div[@class='title']/a">
+ <link>
+ <label><xsl:value-of select="."/></label>
+ <ref>wvt:///vimeo/navigation.xsl?srcurl=http://vimeo.com/api/v2/channel/<xsl:value-of select="str:split(@href, '/')[last()]"/>/videos.xml</ref>
+ </link>
+ </xsl:for-each>
+
+ <xsl:for-each select="//div[@class='pagination']/ul/li[@class='arrow']/a">
+ <link>
+ <xsl:if test="img/@alt = 'previous'">
+ <label>Previous</label>
+ </xsl:if>
+ <xsl:if test="img/@alt = 'next'">
+ <label>Next</label>
+ </xsl:if>
+ <ref>wvt:///vimeo/channels.xsl?srcurl=<xsl:value-of select="./@href"/></ref>
+ </link>
+ </xsl:for-each>
+
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/vimeo/description.xsl b/templates/vimeo/description.xsl
new file mode 100644
index 0000000..a8797cd
--- /dev/null
+++ b/templates/vimeo/description.xsl
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<!-- Convert $seconds to hours:min:sec format -->
+<xsl:template name="pretty-print-seconds">
+ <xsl:param name="seconds"/>
+
+ <xsl:variable name="sec" select="$seconds mod 60"/>
+ <xsl:variable name="min" select="floor($seconds div 60) mod 60"/>
+ <xsl:variable name="hour" select="floor($seconds div 3600)"/>
+
+ <xsl:value-of select="concat($hour, ':', format-number($min, '00'), ':', format-number($sec, '00'))"/>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title><xsl:value-of select="/videos/video/title"/></title>
+ <textarea>
+ <label><xsl:value-of select="/videos/video/description"/></label>
+ </textarea>
+
+ <textarea>
+ <label>Duration: <xsl:call-template name="pretty-print-seconds">
+ <xsl:with-param name="seconds">
+ <xsl:value-of select="/videos/video/duration"/>
+ </xsl:with-param>
+ </xsl:call-template>
+ </label>
+ </textarea>
+
+ <textarea>
+ <label>Views: <xsl:value-of select="/videos/video/stats_number_of_plays"/></label>
+ </textarea>
+
+ <textarea>
+ <label>Likes: <xsl:value-of select="/videos/video/stats_number_of_likes"/></label>
+ </textarea>
+
+ <textarea>
+ <label>Published: <xsl:value-of select="/videos/video/upload_date"/></label>
+ </textarea>
+
+ <link>
+ <label>More videos by <xsl:value-of select="/videos/video/user_name"/></label>
+ <ref>wvt:///vimeo/navigation.xsl?srcurl=http://vimeo.com/api/v2/<xsl:value-of select="str:split(/videos/video/user_url, '/')[last()]"/>/videos.xml</ref>
+ </link>
+
+ <link>
+ <label>Download this video</label>
+ <stream>wvt:///vimeo/video.xsl?srcurl=http://www.vimeo.com/moogaloop/load/clip:<xsl:value-of select="/videos/video/id"/></stream>
+ </link>
+
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/vimeo/groups.xsl b/templates/vimeo/groups.xsl
new file mode 100644
index 0000000..2379058
--- /dev/null
+++ b/templates/vimeo/groups.xsl
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Vimeo Groups</title>
+
+ <xsl:for-each select="//div[@class='title']/a">
+ <link>
+ <label><xsl:value-of select="."/></label>
+ <ref>wvt:///vimeo/navigation.xsl?srcurl=http://vimeo.com/api/v2/group/<xsl:value-of select="str:split(@href, '/')[last()]"/>/videos.xml</ref>
+ </link>
+ </xsl:for-each>
+
+ <xsl:for-each select="//div[@class='pagination']/ul/li[@class='arrow']/a">
+ <link>
+ <xsl:if test="img/@alt = 'previous'">
+ <label>Previous</label>
+ </xsl:if>
+ <xsl:if test="img/@alt = 'next'">
+ <label>Next</label>
+ </xsl:if>
+ <ref>wvt:///vimeo/groups.xsl?srcurl=<xsl:value-of select="./@href"/></ref>
+ </link>
+ </xsl:for-each>
+
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/vimeo/mainmenu.xsl b/templates/vimeo/mainmenu.xsl
new file mode 100644
index 0000000..3667ed7
--- /dev/null
+++ b/templates/vimeo/mainmenu.xsl
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Vimeo</title>
+
+ <link>
+ <label>Search</label>
+ <ref>wvt:///vimeo/search.xsl?srcurl=http://www.vimeo.com/</ref>
+ </link>
+
+ <link>
+ <label>Channels</label>
+ <ref>wvt:///vimeo/channels.xsl?srcurl=http://www.vimeo.com/channels/all</ref>
+ </link>
+
+ <link>
+ <label>Groups</label>
+ <ref>wvt:///vimeo/groups.xsl?srcurl=http://www.vimeo.com/groups/all</ref>
+ </link>
+
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/vimeo/navigation.xsl b/templates/vimeo/navigation.xsl
new file mode 100644
index 0000000..8583212
--- /dev/null
+++ b/templates/vimeo/navigation.xsl
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="video">
+ <link>
+ <label><xsl:value-of select="title"/></label>
+ <stream>wvt:///vimeo/video.xsl?srcurl=http://www.vimeo.com/moogaloop/load/clip:<xsl:value-of select="id"/></stream>
+ <ref>wvt:///vimeo/description.xsl?srcurl=http://vimeo.com/api/v2/video/<xsl:value-of select="id"/>.xml</ref>
+ </link>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Vimeo videos</title>
+
+ <xsl:apply-templates select="/videos/video"/>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/vimeo/search.xsl b/templates/vimeo/search.xsl
new file mode 100644
index 0000000..4939e25
--- /dev/null
+++ b/templates/vimeo/search.xsl
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Vimeo Search</title>
+
+ <textfield name="keywords">
+ <label>Search terms</label>
+ </textfield>
+
+ <itemlist name="orderby">
+ <label>Show me</label>
+ <item value="">most relevant</item>
+ <item value="/sort:newest">newest</item>
+ <item value="/sort:plays">most played</item>
+ <item value="/sort:likes">most liked</item>
+ </itemlist>
+
+ <button>
+ <label>Search</label>
+ <submission>wvt:///vimeo/searchresults.xsl?srcurl=<xsl:value-of select="concat(str:encode-uri('http://vimeo.com/videos/search:', true()), '{keywords}/', substring(id('xsrft')/@value, 0, 9), '{orderby}')"/>&amp;HTTP-header=cookie,xsrft%3D<xsl:value-of select="substring(id('xsrft')/@value, 0, 9)"/></submission>
+ </button>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/vimeo/searchresults.xsl b/templates/vimeo/searchresults.xsl
new file mode 100644
index 0000000..6f5d817
--- /dev/null
+++ b/templates/vimeo/searchresults.xsl
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Search results</title>
+
+ <xsl:for-each select="//div[@class='title']/a">
+ <link>
+ <label><xsl:value-of select="."/></label>
+ <stream>wvt:///vimeo/video.xsl?srcurl=http://www.vimeo.com/moogaloop/load/clip:<xsl:value-of select="str:split(@href, '/')[last()]"/></stream>
+ <ref>wvt:///vimeo/description.xsl?srcurl=http://vimeo.com/api/v2/video/<xsl:value-of select="str:split(@href, '/')[last()]"/>.xml</ref>
+ </link>
+ </xsl:for-each>
+
+ <xsl:for-each select="//div[@class='pagination']/ul/li[@class='arrow']/a">
+ <link>
+ <xsl:if test="img/@alt = 'previous'">
+ <label>Previous</label>
+ </xsl:if>
+ <xsl:if test="img/@alt = 'next'">
+ <label>Next</label>
+ </xsl:if>
+ <ref>wvt:///vimeo/searchresults.xsl?srcurl=<xsl:value-of select="./@href"/></ref>
+ </link>
+ </xsl:for-each>
+
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/vimeo/service.xml b/templates/vimeo/service.xml
new file mode 100644
index 0000000..77af401
--- /dev/null
+++ b/templates/vimeo/service.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<service>
+ <title>Vimeo</title>
+ <ref>wvt:///vimeo/mainmenu.xsl</ref>
+ <description>Vimeo is a video-centric social networking site</description>
+</service>
diff --git a/templates/vimeo/video.xsl b/templates/vimeo/video.xsl
new file mode 100644
index 0000000..3b1b7a9
--- /dev/null
+++ b/templates/vimeo/video.xsl
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="/">
+<mediaurl>
+ <title><xsl:value-of select="/xml/video/caption"/></title>
+
+ <url priority="50">http://www.vimeo.com/moogaloop/play/clip:<xsl:value-of select="/xml/video/nodeId"/>/<xsl:value-of select="/xml/request_signature"/>/<xsl:value-of select="/xml/request_signature_expires"/>/?q=sd</url>
+</mediaurl>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/yleareena/description.xsl b/templates/yleareena/description.xsl
new file mode 100644
index 0000000..2340cb2
--- /dev/null
+++ b/templates/yleareena/description.xsl
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="/">
+<wvmenu>
+ <title><xsl:value-of select="normalize-space(//h1[@class='cliptitle'])"/></title>
+ <textarea>
+ <label><xsl:value-of select="normalize-space(id('relatedinfo')//div[@class='relatedinfo-text description'])"/></label>
+ </textarea>
+ <textarea>
+ <!-- Kesto -->
+ <label><xsl:value-of select="id('relatedinfo')/div/div/div[@class='relatedinfo-text meta']/ul/li[contains(., 'Kesto')]"/></label>
+ </textarea>
+ <textarea>
+ <!-- Julkaistu -->
+ <label><xsl:value-of select="id('relatedinfo-more')/div/div[1]/ul/li[contains(., 'Julkaistu')]"/></label>
+ </textarea>
+ <textarea>
+ <!-- Kieli -->
+ <label><xsl:value-of select="id('relatedinfo-more')/div/div[2]/ul[1]/li[1]"/></label>
+ </textarea>
+ <textarea>
+ <!-- Kanava -->
+ <label><xsl:value-of select="id('relatedinfo')//div[@class='relatedinfo-text meta']/ul/li[1]"/></label>
+ </textarea>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/yleareena/livebroadcasts.xsl b/templates/yleareena/livebroadcasts.xsl
new file mode 100644
index 0000000..865fcee
--- /dev/null
+++ b/templates/yleareena/livebroadcasts.xsl
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="text()"/>
+
+<!-- Käynnissä olevat lähetykset -->
+<xsl:template match="div[@class='ongoing']//div[@class='showlistitem-description']">
+ <link>
+ <label><xsl:value-of select="a"/></label>
+ <stream>wvt:///yleareena/livestream.xsl?param=stream,<xsl:value-of select='substring-before(substring-after(a/@onclick, "stream&apos;, &apos;"), "&apos;")'/></stream>
+ </link>
+</xsl:template>
+
+<!-- "Aina suorana" -->
+<xsl:template match="div[contains(@class, 'live-container')]">
+ <link>
+ <label><xsl:value-of select="h2/span/a"/></label>
+ <stream>wvt:///yleareena/livestream.xsl?param=stream,<xsl:value-of select='substring-before(substring-after(h2/span/a/@onclick, "stream&apos;, &apos;"), "&apos;")'/></stream>
+ </link>
+</xsl:template>
+
+<!-- Tulevat lähetykset -->
+<xsl:template match="div[@class='upcoming']/div/div[@class='showlistitem-description']">
+ <textarea>
+ <label><xsl:value-of select="h3"/>, <xsl:value-of select="ul/li[1]"/></label>
+ </textarea>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Suorat lähetykset</title>
+
+ <xsl:apply-templates select="id('liveshows')/div[@class='ongoing']"/>
+
+ <xsl:apply-templates select="id('liveshows')/div/div[contains(@class, 'live-container')]"/>
+
+ <textarea>
+ <label>Tulossa seuraavaksi:</label>
+ </textarea>
+ <xsl:apply-templates select="id('liveshows')/div[@class='upcoming']"/>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/yleareena/livestream.xsl b/templates/yleareena/livestream.xsl
new file mode 100644
index 0000000..b6d7ee2
--- /dev/null
+++ b/templates/yleareena/livestream.xsl
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:param name="stream"></xsl:param>
+
+<xsl:template match="/">
+ <mediaurl>
+ <title>livestream-<xsl:value-of select="$stream"/></title>
+ <xsl:choose>
+ <xsl:when test="$stream">
+ <url>wvt:///bin/yle-dl?contenttype=video/x-flv&amp;arg=http%3A//areena.yle.fi/player/index.php%3Fstream%3D<xsl:value-of select="$stream"/>%26language%3Dfi</url>
+ </xsl:when>
+ <xsl:otherwise>
+ <url/>
+ </xsl:otherwise>
+ </xsl:choose>
+</mediaurl>
+
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/yleareena/mainmenu.xsl b/templates/yleareena/mainmenu.xsl
new file mode 100644
index 0000000..d17ede6
--- /dev/null
+++ b/templates/yleareena/mainmenu.xsl
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>YLE Areena</title>
+
+ <link>
+ <label>Haku</label>
+ <ref>wvt:///yleareena/search.xsl?srcurl=http://areena.yle.fi/haku</ref>
+ </link>
+
+ <link>
+ <label>Suorat lähetykset</label>
+ <ref>wvt:///yleareena/livebroadcasts.xsl?srcurl=http://areena.yle.fi/live</ref>
+ </link>
+
+ <link>
+ <label>Kaikki ohjelmat</label>
+ <ref>wvt:///yleareena/programlist.xsl?srcurl=http://areena.yle.fi/ohjelmat</ref>
+ </link>
+
+ <xsl:for-each select="//div[h4='Sisältö aihealueittain']/ul/li/a">
+ <link>
+ <label><xsl:value-of select="."/></label>
+ <ref><xsl:value-of select="concat('wvt:///yleareena/navigation.xsl?srcurl=', str:encode-uri(concat(./@href, '/feed/rss'), true()))"/></ref>
+ </link>
+ </xsl:for-each>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/yleareena/navigation.xsl b/templates/yleareena/navigation.xsl
new file mode 100644
index 0000000..bbf0ad7
--- /dev/null
+++ b/templates/yleareena/navigation.xsl
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:param name="docurl"/>
+<xsl:param name="title" select="/rss/channel/title"/>
+
+<xsl:template name="prevnextlinks">
+ <!-- Add previous and next links for a navigation page.
+
+ Extract the current page number from the URL (the number after
+ /sivu/) and adds links to previous and following pages. If the
+ page number is missing, it is assumed to be 1.
+
+ BUG: if the last page has 20 links, an extra "next" link is
+ generated
+ -->
+
+ <xsl:variable name="page" select="number(substring-before(substring-after($docurl, '/sivu/'), '/'))"/>
+
+ <xsl:choose>
+ <xsl:when test="$page &gt; 1">
+
+ <xsl:variable name="urlprefix" select="substring-before($docurl, '/sivu/')"/>
+ <xsl:variable name="urlpostfix" select="substring-after(substring-after($docurl, '/sivu/'), '/')"/>
+
+ <xsl:variable name="prevurl" select="concat($urlprefix, '/sivu/', $page - 1, '/', $urlpostfix)"/>
+ <xsl:variable name="nexturl" select="concat($urlprefix, '/sivu/', $page + 1, '/', $urlpostfix)"/>
+
+ <link>
+ <label>Edellinen</label>
+ <ref>wvt:///yleareena/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri($prevurl, true())"/></ref>
+ </link>
+
+ <xsl:if test="count(/rss/channel/item) &gt;= 20">
+ <link>
+ <label>Seuraava</label>
+ <ref>wvt:///yleareena/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri($nexturl, true())"/></ref>
+ </link>
+ </xsl:if>
+ </xsl:when>
+
+ <xsl:otherwise>
+
+ <xsl:if test="count(/rss/channel/item) &gt;= 20">
+ <xsl:variable name="nexturl">
+ <xsl:choose>
+ <xsl:when test="contains($docurl, '/sivu/')">
+ <xsl:value-of select="concat(substring-before($docurl, '/sivu/'), '/sivu/2/', substring-after(substring-after($docurl, '/sivu/'), '/'))"/>
+ </xsl:when>
+
+ <xsl:when test="contains($docurl, '/feed/rss')">
+ <xsl:value-of select="str:replace($docurl, '/feed/rss', '/sivu/2/feed/rss')"/>
+ </xsl:when>
+
+ <xsl:otherwise>
+ <xsl:value-of select="concat($docurl, '/sivu/2')"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:variable>
+
+ <link>
+ <label>Seuraava</label>
+ <ref>wvt:///yleareena/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri($nexturl, true())"/></ref>
+ </link>
+ </xsl:if>
+
+ </xsl:otherwise>
+
+
+ </xsl:choose>
+</xsl:template>
+
+
+<xsl:template match="/rss/channel/item">
+ <link>
+ <label><xsl:value-of select="title"/></label>
+ <ref>wvt:///yleareena/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(link, true())"/></ref>
+ <stream>wvt:///yleareena/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(link, true())"/>&amp;param=title,<xsl:value-of select="str:encode-uri(concat(title, '-', str:split(pubDate, ' ')[2], '-', str:split(pubDate, ' ')[3], '-', str:split(pubDate, ' ')[4]), true())"/></stream>
+ </link>
+</xsl:template>
+
+
+<xsl:template match="/">
+<wvmenu>
+ <xsl:choose>
+
+ <!-- Regular video links -->
+ <xsl:when test="/rss">
+ <title><xsl:value-of select="$title"/></title>
+
+ <xsl:apply-templates select="/rss/channel/item"/>
+
+ <xsl:call-template name="prevnextlinks"/>
+ </xsl:when>
+
+ <!-- No search results -->
+ <xsl:otherwise>
+ <title>Hae Areenasta: Ei osumia</title>
+
+ <textarea>
+ <xsl:choose>
+ <xsl:when test="//h4">
+ <label><xsl:value-of select="//h4"/></label>
+ </xsl:when>
+ <xsl:otherwise>
+ <label>Ei osumia</label>
+ </xsl:otherwise>
+ </xsl:choose>
+ </textarea>
+ </xsl:otherwise>
+
+ </xsl:choose>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/yleareena/programlist.xsl b/templates/yleareena/programlist.xsl
new file mode 100644
index 0000000..0a4ece4
--- /dev/null
+++ b/templates/yleareena/programlist.xsl
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="tr">
+ <link>
+ <label><xsl:value-of select="td[1]/a"/></label>
+ <ref>wvt:///yleareena/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(concat(td[1]/a/@href, '/feed/rss'), true())"/>&amp;param=title,<xsl:value-of select="str:encode-uri(td[1]/a, true())"/></ref>
+ </link>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Ohjelmat A-Ö</title>
+
+ <xsl:apply-templates select="id('programlist-ao')/table/tbody/tr[td]"/>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/yleareena/search.xsl b/templates/yleareena/search.xsl
new file mode 100644
index 0000000..fa487f4
--- /dev/null
+++ b/templates/yleareena/search.xsl
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:template match="fieldset">
+ <xsl:if test="select">
+ <itemlist>
+ <xsl:attribute name="name"><xsl:value-of select="select/@name"/></xsl:attribute>
+ <label><xsl:value-of select="label"/></label>
+ <xsl:for-each select="select/option|select/optgroup/option">
+ <item>
+ <xsl:attribute name="value"><xsl:value-of select="@value"/></xsl:attribute>
+ <xsl:value-of select="."/>
+ </item>
+ </xsl:for-each>
+ </itemlist>
+ </xsl:if>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Hae Areenasta</title>
+
+ <textfield name="keyword">
+ <label>Hakusana</label>
+ </textfield>
+
+ <xsl:apply-templates select="id('widesearch')/form/fieldset[not(contains(@class, 'search-keyword'))]"/>
+
+ <itemlist name="naytetaan_ulkomailla">
+ <label>Vain Suomen ulkopuolella katsottavat</label>
+ <item value="kaikki">Kaikki</item>
+ <item value="kylla">Kyllä</item>
+ </itemlist>
+
+ <button>
+ <label>Hae</label>
+ <submission>wvt:///yleareena/navigation.xsl?srcurl=http%3A//areena.yle.fi/haku/{category}/uusimmat/hakusana/{keyword}/kanava/{channel}/media/{mediatype}/julkaistu/{date}/kieli/{language}/naytetaan_ulkomailla/{naytetaan_ulkomailla}/feed/rss</submission>
+ </button>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/yleareena/service.xml b/templates/yleareena/service.xml
new file mode 100644
index 0000000..0d7aa03
--- /dev/null
+++ b/templates/yleareena/service.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<service>
+ <title>YLE Areena</title>
+ <ref>wvt:///yleareena/mainmenu.xsl?srcurl=http%3A//areena.yle.fi/</ref>
+ <description>Video service by YLE, the Finland's national public service broadcasting company</description>
+</service>
diff --git a/templates/yleareena/video.xsl b/templates/yleareena/video.xsl
new file mode 100644
index 0000000..f0c2d6a
--- /dev/null
+++ b/templates/yleareena/video.xsl
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings">
+
+<xsl:param name="title"/>
+<xsl:param name="docurl"/>
+
+<xsl:template match="/">
+ <mediaurl>
+ <title><xsl:value-of select="$title"/></title>
+ <url>wvt:///bin/yle-dl?contenttype=video/x-flv&amp;arg=<xsl:value-of select="str:encode-uri($docurl, true())"/></url>
+</mediaurl>
+
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/youtube/categories.xsl b/templates/youtube/categories.xsl
new file mode 100644
index 0000000..080218b
--- /dev/null
+++ b/templates/youtube/categories.xsl
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ xmlns:app="http://www.w3.org/2007/app"
+ xmlns:atom="http://www.w3.org/2005/Atom"
+ xmlns:yt="http://gdata.youtube.com/schemas/2007"
+ exclude-result-prefixes="str app atom yt">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Youtube</title>
+
+ <link>
+ <label>Search</label>
+ <ref>wvt:///youtube/search.xsl</ref>
+ </link>
+
+ <xsl:for-each select="/app:categories/atom:category[yt:browsable]">
+ <link>
+ <label><xsl:value-of select="@label"/></label>
+ <ref>wvt:///youtube/navigation.xsl?srcurl=http://gdata.youtube.com/feeds/api/standardfeeds/most_popular_<xsl:value-of select="str:encode-uri(@term, true())"/>%3Fmax-results%3D20%26v%3D2</ref>
+ </link>
+ </xsl:for-each>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/youtube/description.xsl b/templates/youtube/description.xsl
new file mode 100644
index 0000000..e728961
--- /dev/null
+++ b/templates/youtube/description.xsl
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ xmlns:atom="http://www.w3.org/2005/Atom"
+ xmlns:media="http://search.yahoo.com/mrss/"
+ xmlns:gd="http://schemas.google.com/g/2005"
+ xmlns:yt="http://gdata.youtube.com/schemas/2007"
+ exclude-result-prefixes="atom str media gd yt">
+
+<!-- Convert $seconds to hours:min:sec format -->
+<xsl:template name="pretty-print-seconds">
+ <xsl:param name="seconds"/>
+
+ <xsl:variable name="sec" select="$seconds mod 60"/>
+ <xsl:variable name="min" select="floor($seconds div 60) mod 60"/>
+ <xsl:variable name="hour" select="floor($seconds div 3600)"/>
+
+ <xsl:value-of select="concat($hour, ':', format-number($min, '00'), ':', format-number($sec, '00'))"/>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title><xsl:value-of select="/atom:entry/atom:title"/></title>
+ <textarea>
+ <label><xsl:value-of select="/atom:entry/media:group/media:description"/></label>
+ </textarea>
+
+ <textarea>
+ <label>Duration: <xsl:call-template name="pretty-print-seconds">
+ <xsl:with-param name="seconds">
+ <xsl:value-of select="/atom:entry/media:group/yt:duration/@seconds"/>
+ </xsl:with-param>
+ </xsl:call-template>
+ </label>
+ </textarea>
+
+ <textarea>
+ <label>Views: <xsl:value-of select="/atom:entry/yt:statistics/@viewCount"/></label>
+ </textarea>
+
+ <textarea>
+ <label>Rating: <xsl:value-of select="/atom:entry/gd:rating/@average"/></label>
+ </textarea>
+
+ <textarea>
+ <label>Published: <xsl:value-of select="str:replace(str:replace(/atom:entry/atom:published, '.000', ' '), 'T', ' ')"/></label>
+ </textarea>
+
+ <xsl:if test="/atom:entry/atom:link[@rel='http://gdata.youtube.com/schemas/2007#video.responses']">
+ <link>
+ <label>Video responses</label>
+ <ref>wvt:///youtube/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(/atom:entry/atom:link[@rel='http://gdata.youtube.com/schemas/2007#video.responses']/@href, true())"/></ref>
+ </link>
+ </xsl:if>
+
+ <xsl:if test="/atom:entry/atom:link[@rel='http://gdata.youtube.com/schemas/2007#video.related']">
+ <link>
+ <label>Related videos</label>
+ <ref>wvt:///youtube/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(/atom:entry/atom:link[@rel='http://gdata.youtube.com/schemas/2007#video.related']/@href, true())"/></ref>
+ </link>
+ </xsl:if>
+
+ <link>
+ <label>Download this video</label>
+ <stream>wvt:///youtube/video.xsl?srcurl=http://www.youtube.com/watch?v=<xsl:value-of select="/atom:entry/media:group/yt:videoid"/></stream>
+ </link>
+
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/youtube/navigation.xsl b/templates/youtube/navigation.xsl
new file mode 100644
index 0000000..a5fd1c7
--- /dev/null
+++ b/templates/youtube/navigation.xsl
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:atom="http://www.w3.org/2005/Atom"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ xmlns:media="http://search.yahoo.com/mrss/"
+ xmlns:yt="http://gdata.youtube.com/schemas/2007"
+ exclude-result-prefixes="atom str media yt">
+
+<xsl:template match="atom:entry">
+ <link>
+ <label><xsl:value-of select="atom:title"/></label>
+ <stream>wvt:///youtube/video.xsl?srcurl=http://www.youtube.com/watch?v=<xsl:value-of select="media:group/yt:videoid"/></stream>
+ <ref>wvt:///youtube/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(atom:link[@rel='self']/@href, true())"/></ref>
+ </link>
+</xsl:template>
+
+<xsl:template match="atom:link">
+ <xsl:if test="@rel = 'previous'">
+ <link>
+ <label>Previous</label>
+ <ref>wvt:///youtube/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(@href, true())"/></ref>
+ </link>
+ </xsl:if>
+
+ <xsl:if test="@rel = 'next'">
+ <link>
+ <label>Next</label>
+ <ref>wvt:///youtube/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(@href, true())"/></ref>a
+ </link>
+ </xsl:if>
+</xsl:template>
+
+<xsl:template match="/">
+<wvmenu>
+ <title><xsl:value-of select="/atom:feed/atom:title"/></title>
+
+ <xsl:if test="/atom:feed/atom:link[@rel='http://schemas.google.com/g/2006#spellcorrection']">
+ <link>
+ <label>Did you mean <xsl:value-of select="/atom:feed/atom:link[@rel='http://schemas.google.com/g/2006#spellcorrection']/@title"/>?</label>
+ <ref>wvt:///youtube/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(/atom:feed/atom:link[@rel='http://schemas.google.com/g/2006#spellcorrection']/@href, true())"/></ref>
+ </link>
+ </xsl:if>
+
+
+ <!-- Video links -->
+ <xsl:apply-templates select="/atom:feed/atom:entry"/>
+
+ <xsl:if test="count(/atom:feed/atom:entry) = 0">
+ <textarea>
+ <label>No match</label>
+ </textarea>
+ </xsl:if>
+
+ <!-- Next and prev links -->
+ <xsl:apply-templates select="/atom:feed/atom:link[@rel='previous']|/atom:feed/atom:link[@rel='next']"/>
+
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/youtube/search.xsl b/templates/youtube/search.xsl
new file mode 100644
index 0000000..d28e3c6
--- /dev/null
+++ b/templates/youtube/search.xsl
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:str="http://exslt.org/strings"
+ exclude-result-prefixes="str">
+
+<xsl:template match="/">
+<wvmenu>
+ <title>Youtube Search</title>
+
+ <textfield name="q">
+ <label>Search terms</label>
+ </textfield>
+
+ <itemlist name="orderby">
+ <label>Sort by</label>
+ <item value="relevance">Relevance</item>
+ <item value="published">Date Added</item>
+ <item value="viewCount">View Count</item>
+ <item value="rating">Rating</item>
+ </itemlist>
+
+ <itemlist name="time">
+ <label>Uploaded</label>
+ <item value="all_time">Anytime</item>
+ <item value="today">Today</item>
+ <item value="this_week">This week</item>
+ <item value="this_month">This month</item>
+ </itemlist>
+
+ <button>
+ <label>Search</label>
+ <submission>wvt:///youtube/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri('http://gdata.youtube.com/feeds/api/videos?q={q}&amp;orderby={orderby}&amp;time={time}&amp;max-results=20&amp;safeSearch=none&amp;format=5&amp;v=2', true())"/></submission>
+ </button>
+</wvmenu>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/templates/youtube/service.xml b/templates/youtube/service.xml
new file mode 100644
index 0000000..f5719a6
--- /dev/null
+++ b/templates/youtube/service.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<service>
+ <title>YouTube</title>
+ <ref>wvt:///youtube/categories.xsl?srcurl=http://gdata.youtube.com/schemas/2007/categories.cat</ref>
+ <description>Video sharing service on which users worldwide can upload their videos</description>
+</service>
diff --git a/templates/youtube/video.xsl b/templates/youtube/video.xsl
new file mode 100644
index 0000000..d38a99e
--- /dev/null
+++ b/templates/youtube/video.xsl
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:strip-space elements="div" />
+
+<!-- old variables (before appr. April 2010) -->
+<xsl:variable name="t1" select="substring-before(substring-after(html/head/script[contains(., 'swfArgs') or contains(., 'SWF_ARGS')], '&quot;t&quot;: &quot;'), '&quot;')"/>
+<xsl:variable name="video_id1" select="substring-before(substring-after(html/head/script[contains(., 'swfArgs') or contains(., 'SWF_ARGS')], '&quot;video_id&quot;: &quot;'), '&quot;')"/>
+
+<!-- new variables -->
+<xsl:variable name="t2" select="substring-before(substring-after(//script[contains(., 'swfHTML')], '&amp;t='), '&amp;')"/>
+<xsl:variable name="video_id2" select="substring-before(substring-after(//script[contains(., 'swfHTML')], '&amp;video_id='), '&amp;')"/>
+
+<xsl:variable name="t">
+ <xsl:choose>
+ <xsl:when test="$t1"><xsl:value-of select="$t1"/></xsl:when>
+ <xsl:otherwise><xsl:value-of select="$t2"/></xsl:otherwise>
+ </xsl:choose>
+</xsl:variable>
+<xsl:variable name="video_id">
+ <xsl:choose>
+ <xsl:when test="$video_id1"><xsl:value-of select="$video_id1"/></xsl:when>
+ <xsl:otherwise><xsl:value-of select="$video_id2"/></xsl:otherwise>
+ </xsl:choose>
+</xsl:variable>
+
+<xsl:template match="/">
+<mediaurl>
+ <title>
+ <xsl:choose>
+ <xsl:when test="/html/head/meta[@name='title']/@content">
+ <xsl:value-of select="/html/head/meta[@name='title']/@content"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="//div[@id='watch-vid-title']//h1"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </title>
+
+ <url priority="70">http://www.youtube.com/get_video?video_id=<xsl:value-of select="$video_id"/>&amp;t=<xsl:value-of select="$t"/>&amp;fmt=22</url>
+ <url priority="60">http://www.youtube.com/get_video?video_id=<xsl:value-of select="$video_id"/>&amp;t=<xsl:value-of select="$t"/>&amp;fmt=18</url>
+ <url priority="50">http://www.youtube.com/get_video?video_id=<xsl:value-of select="$video_id"/>&amp;t=<xsl:value-of select="$t"/></url>
+</mediaurl>
+</xsl:template>
+
+</xsl:stylesheet>