From 310743fb9ebbf68b253b923a309cc5f635da89a1 Mon Sep 17 00:00:00 2001 From: Antti Ajanki Date: Fri, 23 Jul 2010 20:55:11 +0300 Subject: release 0.3.0 --- COPYING | 701 ++++++++++++++++++++++++++++ HISTORY | 185 ++++++++ Makefile | 79 ++++ README | 48 ++ README.vdrplugin | 180 ++++++++ README.webvi | 118 +++++ TODO | 18 + debian/changelog | 150 ++++++ debian/compat | 1 + debian/control | 52 +++ debian/copyright | 61 +++ debian/libwebvi-dev.docs | 1 + debian/libwebvi-dev.install | 3 + debian/libwebvi0.docs | 1 + debian/libwebvi0.install | 1 + debian/plugin.webvideo.conf | 10 + debian/postinst | 43 ++ debian/pycompat | 1 + debian/python-webvi.docs | 2 + debian/python-webvi.install | 4 + debian/python-webvi.manpages | 1 + debian/rules | 37 ++ debian/vdr-plugin-webvideo.NEWS | 10 + debian/vdr-plugin-webvideo.dirs | 1 + debian/vdr-plugin-webvideo.docs | 2 + debian/vdr-plugin-webvideo.install | 6 + debian/watch | 2 + debian/webvi.1.txt | 67 +++ debian/webvi.conf | 20 + debian/webvi.plugin.conf | 3 + doc/developers.txt | 152 ++++++ setup.py | 38 ++ src/libwebvi/Makefile | 34 ++ src/libwebvi/libwebvi.c | 814 +++++++++++++++++++++++++++++++++ src/libwebvi/libwebvi.h | 330 +++++++++++++ src/libwebvi/pythonlibname.py | 14 + src/libwebvi/webvi/__init__.py | 1 + src/libwebvi/webvi/api.py | 289 ++++++++++++ src/libwebvi/webvi/asyncurl.py | 389 ++++++++++++++++ src/libwebvi/webvi/constants.py | 50 ++ src/libwebvi/webvi/download.py | 470 +++++++++++++++++++ src/libwebvi/webvi/json2xml.py | 69 +++ src/libwebvi/webvi/request.py | 617 +++++++++++++++++++++++++ src/libwebvi/webvi/utils.py | 134 ++++++ src/libwebvi/webvi/version.py | 20 + src/unittest/Makefile | 11 + src/unittest/runtests.sh | 7 + src/unittest/testdownload.c | 195 ++++++++ src/unittest/testlibwebvi.c | 147 ++++++ src/unittest/testwebvi.py | 407 +++++++++++++++++ src/vdr-plugin/Makefile | 115 +++++ src/vdr-plugin/buffer.c | 84 ++++ src/vdr-plugin/buffer.h | 44 ++ src/vdr-plugin/common.c | 182 ++++++++ src/vdr-plugin/common.h | 42 ++ src/vdr-plugin/config.c | 199 ++++++++ src/vdr-plugin/config.h | 64 +++ src/vdr-plugin/dictionary.c | 410 +++++++++++++++++ src/vdr-plugin/dictionary.h | 178 +++++++ src/vdr-plugin/download.c | 222 +++++++++ src/vdr-plugin/download.h | 59 +++ src/vdr-plugin/history.c | 145 ++++++ src/vdr-plugin/history.h | 62 +++ src/vdr-plugin/iniparser.c | 650 ++++++++++++++++++++++++++ src/vdr-plugin/iniparser.h | 284 ++++++++++++ src/vdr-plugin/menu.c | 670 +++++++++++++++++++++++++++ src/vdr-plugin/menu.h | 114 +++++ src/vdr-plugin/menu_timer.c | 150 ++++++ src/vdr-plugin/menu_timer.h | 46 ++ src/vdr-plugin/menudata.c | 179 ++++++++ src/vdr-plugin/menudata.h | 100 ++++ src/vdr-plugin/mime.types | 4 + src/vdr-plugin/mimetypes.c | 98 ++++ src/vdr-plugin/mimetypes.h | 35 ++ src/vdr-plugin/player.c | 73 +++ src/vdr-plugin/player.h | 29 ++ src/vdr-plugin/po/de_DE.po | 137 ++++++ src/vdr-plugin/po/fi_FI.po | 137 ++++++ src/vdr-plugin/po/fr_FR.po | 156 +++++++ src/vdr-plugin/po/it_IT.po | 158 +++++++ src/vdr-plugin/request.c | 432 +++++++++++++++++ src/vdr-plugin/request.h | 170 +++++++ src/vdr-plugin/timer.c | 465 +++++++++++++++++++ src/vdr-plugin/timer.h | 111 +++++ src/vdr-plugin/webvideo.c | 444 ++++++++++++++++++ src/version | 1 + src/webvicli/webvi | 22 + src/webvicli/webvicli/__init__.py | 1 + src/webvicli/webvicli/client.py | 729 +++++++++++++++++++++++++++++ src/webvicli/webvicli/menu.py | 171 +++++++ templates/bin/ruutu-dl | 36 ++ templates/bin/yle-dl | 22 + templates/google/description.xsl | 22 + templates/google/search.xsl | 38 ++ templates/google/searchresults.xsl | 85 ++++ templates/google/service.xml | 7 + templates/google/video.xsl | 19 + templates/katsomo/mainmenu.xsl | 26 ++ templates/katsomo/navigation.xsl | 48 ++ templates/katsomo/search.xsl | 23 + templates/katsomo/searchresults.xsl | 32 ++ templates/katsomo/service.xml | 7 + templates/katsomo/video.xsl | 18 + templates/metacafe/categories.xsl | 33 ++ templates/metacafe/channellist.xsl | 22 + templates/metacafe/description.xsl | 47 ++ templates/metacafe/navigation.xsl | 36 ++ templates/metacafe/search.xsl | 36 ++ templates/metacafe/service.xml | 7 + templates/metacafe/video.xsl | 14 + templates/ruutufi/description.xsl | 51 +++ templates/ruutufi/mainmenu.xsl | 32 ++ templates/ruutufi/program.xsl | 120 +++++ templates/ruutufi/search.xsl | 23 + templates/ruutufi/series.xsl | 28 ++ templates/ruutufi/service.xml | 7 + templates/ruutufi/video.xsl | 23 + templates/ruutufi/video2.xsl | 15 + templates/subtv/description.xsl | 32 ++ templates/subtv/mainmenu.xsl | 21 + templates/subtv/navigation.xsl | 42 ++ templates/subtv/service.xml | 7 + templates/subtv/video.xsl | 19 + templates/svtplay/categories.xsl | 19 + templates/svtplay/description.xsl | 41 ++ templates/svtplay/navigation.xsl | 74 +++ templates/svtplay/programmenu.xsl | 56 +++ templates/svtplay/service.xml | 7 + templates/svtplay/video.xsl | 24 + templates/vimeo/channels.xsl | 33 ++ templates/vimeo/description.xsl | 59 +++ templates/vimeo/groups.xsl | 33 ++ templates/vimeo/mainmenu.xsl | 28 ++ templates/vimeo/navigation.xsl | 22 + templates/vimeo/search.xsl | 30 ++ templates/vimeo/searchresults.xsl | 34 ++ templates/vimeo/service.xml | 7 + templates/vimeo/video.xsl | 14 + templates/yleareena/description.xsl | 31 ++ templates/yleareena/livebroadcasts.xsl | 46 ++ templates/yleareena/livestream.xsl | 23 + templates/yleareena/mainmenu.xsl | 35 ++ templates/yleareena/navigation.xsl | 119 +++++ templates/yleareena/programlist.xsl | 22 + templates/yleareena/search.xsl | 45 ++ templates/yleareena/service.xml | 7 + templates/yleareena/video.xsl | 18 + templates/youtube/categories.xsl | 29 ++ templates/youtube/description.xsl | 73 +++ templates/youtube/navigation.xsl | 62 +++ templates/youtube/search.xsl | 39 ++ templates/youtube/service.xml | 7 + templates/youtube/video.xsl | 48 ++ 153 files changed, 15451 insertions(+) create mode 100644 COPYING create mode 100644 HISTORY create mode 100644 Makefile create mode 100644 README create mode 100644 README.vdrplugin create mode 100644 README.webvi create mode 100644 TODO create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/libwebvi-dev.docs create mode 100644 debian/libwebvi-dev.install create mode 100644 debian/libwebvi0.docs create mode 100644 debian/libwebvi0.install create mode 100644 debian/plugin.webvideo.conf create mode 100644 debian/postinst create mode 100644 debian/pycompat create mode 100644 debian/python-webvi.docs create mode 100644 debian/python-webvi.install create mode 100644 debian/python-webvi.manpages create mode 100755 debian/rules create mode 100644 debian/vdr-plugin-webvideo.NEWS create mode 100644 debian/vdr-plugin-webvideo.dirs create mode 100644 debian/vdr-plugin-webvideo.docs create mode 100644 debian/vdr-plugin-webvideo.install create mode 100644 debian/watch create mode 100644 debian/webvi.1.txt create mode 100644 debian/webvi.conf create mode 100644 debian/webvi.plugin.conf create mode 100644 doc/developers.txt create mode 100755 setup.py create mode 100644 src/libwebvi/Makefile create mode 100644 src/libwebvi/libwebvi.c create mode 100644 src/libwebvi/libwebvi.h create mode 100755 src/libwebvi/pythonlibname.py create mode 100644 src/libwebvi/webvi/__init__.py create mode 100644 src/libwebvi/webvi/api.py create mode 100644 src/libwebvi/webvi/asyncurl.py create mode 100644 src/libwebvi/webvi/constants.py create mode 100644 src/libwebvi/webvi/download.py create mode 100644 src/libwebvi/webvi/json2xml.py create mode 100644 src/libwebvi/webvi/request.py create mode 100644 src/libwebvi/webvi/utils.py create mode 100644 src/libwebvi/webvi/version.py create mode 100644 src/unittest/Makefile create mode 100755 src/unittest/runtests.sh create mode 100644 src/unittest/testdownload.c create mode 100644 src/unittest/testlibwebvi.c create mode 100644 src/unittest/testwebvi.py create mode 100644 src/vdr-plugin/Makefile create mode 100644 src/vdr-plugin/buffer.c create mode 100644 src/vdr-plugin/buffer.h create mode 100644 src/vdr-plugin/common.c create mode 100644 src/vdr-plugin/common.h create mode 100644 src/vdr-plugin/config.c create mode 100644 src/vdr-plugin/config.h create mode 100644 src/vdr-plugin/dictionary.c create mode 100644 src/vdr-plugin/dictionary.h create mode 100644 src/vdr-plugin/download.c create mode 100644 src/vdr-plugin/download.h create mode 100644 src/vdr-plugin/history.c create mode 100644 src/vdr-plugin/history.h create mode 100644 src/vdr-plugin/iniparser.c create mode 100644 src/vdr-plugin/iniparser.h create mode 100644 src/vdr-plugin/menu.c create mode 100644 src/vdr-plugin/menu.h create mode 100644 src/vdr-plugin/menu_timer.c create mode 100644 src/vdr-plugin/menu_timer.h create mode 100644 src/vdr-plugin/menudata.c create mode 100644 src/vdr-plugin/menudata.h create mode 100644 src/vdr-plugin/mime.types create mode 100644 src/vdr-plugin/mimetypes.c create mode 100644 src/vdr-plugin/mimetypes.h create mode 100644 src/vdr-plugin/player.c create mode 100644 src/vdr-plugin/player.h create mode 100644 src/vdr-plugin/po/de_DE.po create mode 100644 src/vdr-plugin/po/fi_FI.po create mode 100644 src/vdr-plugin/po/fr_FR.po create mode 100644 src/vdr-plugin/po/it_IT.po create mode 100644 src/vdr-plugin/request.c create mode 100644 src/vdr-plugin/request.h create mode 100644 src/vdr-plugin/timer.c create mode 100644 src/vdr-plugin/timer.h create mode 100644 src/vdr-plugin/webvideo.c create mode 100644 src/version create mode 100755 src/webvicli/webvi create mode 100644 src/webvicli/webvicli/__init__.py create mode 100644 src/webvicli/webvicli/client.py create mode 100644 src/webvicli/webvicli/menu.py create mode 100755 templates/bin/ruutu-dl create mode 100755 templates/bin/yle-dl create mode 100644 templates/google/description.xsl create mode 100644 templates/google/search.xsl create mode 100644 templates/google/searchresults.xsl create mode 100644 templates/google/service.xml create mode 100644 templates/google/video.xsl create mode 100644 templates/katsomo/mainmenu.xsl create mode 100644 templates/katsomo/navigation.xsl create mode 100644 templates/katsomo/search.xsl create mode 100644 templates/katsomo/searchresults.xsl create mode 100644 templates/katsomo/service.xml create mode 100644 templates/katsomo/video.xsl create mode 100644 templates/metacafe/categories.xsl create mode 100644 templates/metacafe/channellist.xsl create mode 100644 templates/metacafe/description.xsl create mode 100644 templates/metacafe/navigation.xsl create mode 100644 templates/metacafe/search.xsl create mode 100644 templates/metacafe/service.xml create mode 100644 templates/metacafe/video.xsl create mode 100644 templates/ruutufi/description.xsl create mode 100644 templates/ruutufi/mainmenu.xsl create mode 100644 templates/ruutufi/program.xsl create mode 100644 templates/ruutufi/search.xsl create mode 100644 templates/ruutufi/series.xsl create mode 100644 templates/ruutufi/service.xml create mode 100644 templates/ruutufi/video.xsl create mode 100644 templates/ruutufi/video2.xsl create mode 100644 templates/subtv/description.xsl create mode 100644 templates/subtv/mainmenu.xsl create mode 100644 templates/subtv/navigation.xsl create mode 100644 templates/subtv/service.xml create mode 100644 templates/subtv/video.xsl create mode 100644 templates/svtplay/categories.xsl create mode 100644 templates/svtplay/description.xsl create mode 100644 templates/svtplay/navigation.xsl create mode 100644 templates/svtplay/programmenu.xsl create mode 100644 templates/svtplay/service.xml create mode 100644 templates/svtplay/video.xsl create mode 100644 templates/vimeo/channels.xsl create mode 100644 templates/vimeo/description.xsl create mode 100644 templates/vimeo/groups.xsl create mode 100644 templates/vimeo/mainmenu.xsl create mode 100644 templates/vimeo/navigation.xsl create mode 100644 templates/vimeo/search.xsl create mode 100644 templates/vimeo/searchresults.xsl create mode 100644 templates/vimeo/service.xml create mode 100644 templates/vimeo/video.xsl create mode 100644 templates/yleareena/description.xsl create mode 100644 templates/yleareena/livebroadcasts.xsl create mode 100644 templates/yleareena/livestream.xsl create mode 100644 templates/yleareena/mainmenu.xsl create mode 100644 templates/yleareena/navigation.xsl create mode 100644 templates/yleareena/programlist.xsl create mode 100644 templates/yleareena/search.xsl create mode 100644 templates/yleareena/service.xml create mode 100644 templates/yleareena/video.xsl create mode 100644 templates/youtube/categories.xsl create mode 100644 templates/youtube/description.xsl create mode 100644 templates/youtube/navigation.xsl create mode 100644 templates/youtube/search.xsl create mode 100644 templates/youtube/service.xml create mode 100644 templates/youtube/video.xsl 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. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. + + + + +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 +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 + +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 + +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 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 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 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 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 Thu, 05 Nov 2009 02:34:31 +0100 + +vdr-plugin-webvideo (0.1.6-1) unstable; urgency=low + + * New release + + -- Antti Ajanki 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 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 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 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 Sat, 11 Apr 2009 00:05:37 +0200 + +vdr-plugin-webvideo (0.1.1-1) unstable; urgency=low + + * New release + + -- Antti Ajanki Tue, 24 Feb 2009 19:38:28 +0300 + +vdr-plugin-webvideo (0.1.0-1) unstable; urgency=low + + * New release + + -- Antti Ajanki Sat, 7 Feb 2009 18:07:37 +0300 + +vdr-plugin-webvideo (0.0.6-1) unstable; urgency=low + + * New release + + -- Antti Ajanki Sat, 6 Dec 2008 11:27:05 +0300 + +vdr-plugin-webvideo (0.0.5-1) unstable; urgency=low + + * New release + + -- Antti Ajanki Tue, 2 Sep 2008 21:41:50 +0300 + +vdr-plugin-webvideo (0.0.4-1) unstable; urgency=low + + * New release + + -- Antti Ajanki Thu, 21 Aug 2008 12:29:38 +0300 + +vdr-plugin-webvideo (0.0.3-1) unstable; urgency=low + + * New release + + -- Antti Ajanki Tue, 19 Aug 2008 18:19:19 +0300 + +vdr-plugin-webvideo (0.0.2-1) unstable; urgency=low + + * New release + + -- Antti Ajanki Thu, 24 Jul 2008 20:44:15 +0300 + +vdr-plugin-webvideo (0.0.1-1) unstable; urgency=low + + * Initial release + + -- Antti Ajanki 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 +Uploaders: Tobias Grimm , Thomas Günther +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 + +Debian Maintainers: + Antti Ajanki + Tobias Grimm + Thomas Günther + +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: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# 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 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 and + Antti Ajanki . 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: + + + Page title + + + + wvt:///youtube/description.xsl?srcurl=... + wvt:///youtube/video.xsl?srcurl=... + + + + + + + + + + + Relevance + Date Added + View Count + Rating + + + + + + is the root node of a menu page. Possible children are +, <link>, <textfield>, <itemlist>, <textarea>, and <button> +nodes. + +The content of <title> will be shown as the title of the menu. + +<link> is a navigation link. It must have a child <label>, which +contains the text that will be shown to the user, and at least one of +<ref> (a navigation reference) or <stream> (a media stream reference). +The user interface provides three ways for selecting the link: a +navigation action loads a new menu page by retrieving the reference in +<ref> node from the library (using request type WEBVIREQ_MENU), a file +action downloads the stream by requesting <stream> reference from the +library (WEBVIREQ_FILE), stream URL action retrieves a direct URL to +the stream by requesting reference <stream> (WEBVIREQ_STREAMURL). + +<textarea> defines a text widget. Again, child node <label> contains +the text that will be displayed to the user. + +<textfield> defines a control for entering a string. + +<itemlist> defines a control for selecting one of the specified +<item>s. + +<button> is a special kind of link. It encodes the state of +<textfield>s and <itemlist>s into its reference. When the <button> is +selected the state of other controls is send to the library. The +reference is constructed by concatenating encoded values of each +<textfield> and <itemlist> to the content of <submission> node. Each +control is encoded as string "subst=name,value" where name is +URL encoded presentation of the name nodes name attribute, and value +is URL encoded presentation of the <textfield>'s UTF-8 input or value +attribute of currently selected <item> in an <itemlist>. The encodings +are joined by putting a "&" between them. + + +Format of wvt:// references +=========================== + +In libwebvi, the navigation and menu requests are encoded encoded as +URIs with scheme wvt:// . Typically, these references are extracted +from <ref> or <stream> nodes in the XML menu documents. This section +explains the format of the references. + +The references are of the form + +wvt://templatedir/template.xsl?srcurl=...&other_params=value + +The value of the srcurl parameter is a URL (typically an HTTP URL) of +a web page on a video site. templatedir/template.xsl specifies a name +of the XSLT template that is applied to the srcurl to get an XML menu. +srcurl can be empty. + +templatedir called bin is a special directory. It contains executable +programs or scripts instead of XSLT templates. When a reference to the +bin directory is encountered, the named script is executed. Script's +standard output is returned as the results of the operation. The +script should return 0 on success. + +Parameters that affect the template processing can be appended to the +reference. The parameter name and value are separated by '=', +different parameters are separated by '&'. Following parameters are +understood: + +param=name,value + +name and value are passed to the XSLT processor as an external +parameter. + +subst=name,value + +Replaces string {name} in srcurl with value before retrieving it. This +is used in search pages as discussed in previous section. + +contenttype=value + +value is used as content type if the server does not send a proper +content type header. + +arg=value + +Used to pass command line arguments to the external scripts in the bin +directory. There can be multiple args. + +minquality=value +maxquality=value + +Only the streams whose quality ("priority" attribute of the "url" tag) +is between minquality and maxquality are considered as candidates when +selecting a stream to play. The default limits are 0 and 100. + +postprocess=json2xml + +The source is a JSON document which should be converted to XML before +the XSLT transformation. + +http-header=headername,headervalue + +Append a HTTP header to the request. Ignored on transfers that use +some other protocol besides HTTP. + +The reference to the main menu is wvt:///?srcurl=mainmenu . diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..30f3ee0 --- /dev/null +++ b/setup.py @@ -0,0 +1,38 @@ +import re +import os +import os.path +import sys +from distutils.core import setup + +def extract_version(): + sys.path.append('src/libwebvi/webvi') + import version + sys.path.pop() + return version.VERSION + +def install_service_files(): + sourcedir = 'templates' + destdir = 'share/webvi/templates' + + res = [] + for service in os.listdir(sourcedir): + sdir = os.path.join(sourcedir, service) + sfiles = [] + for f in os.listdir(sdir): + sfiles.append(os.path.join(sdir, f)) + res.append((os.path.join(destdir, service), sfiles)) + return res + +setup( + name='libwebvi', + version=extract_version(), + description='webvideo downloader library and command line client', + author='Antti Ajanki', + author_email='antti.ajanki@iki.fi', + license='GPLv3', + url='http://users.tkk.fi/~aajanki/vdr/webvideo', + package_dir = {'webvi': 'src/libwebvi/webvi', 'webvicli': 'src/webvicli/webvicli'}, + packages=['webvi', 'webvicli'], + scripts=['src/webvicli/webvi'], + data_files=install_service_files() + ) diff --git a/src/libwebvi/Makefile b/src/libwebvi/Makefile new file mode 100644 index 0000000..131c4a7 --- /dev/null +++ b/src/libwebvi/Makefile @@ -0,0 +1,34 @@ +PREFIX ?= /usr/local + +LIBNAME=libwebvi.so +LIBSONAME=$(LIBNAME).0 +LIBMINOR=$(LIBSONAME).4 + +VERSION:=$(shell cat ../version) +PYLIB:=$(shell python pythonlibname.py) +DEFINES:=-DPYTHONSHAREDLIB=\"$(PYLIB)\" -DLIBWEBVI_VERSION=\"$(VERSION)\" +# append -DDEBUG to DEFINES to get debug output + +all: $(LIBMINOR) + +libwebvi.o: libwebvi.c libwebvi.h + $(CC) -fPIC -Wall -O2 -g $(CFLAGS) $(DEFINES) `python-config --cflags` -c -o libwebvi.o libwebvi.c + +$(LIBMINOR): libwebvi.o + $(CC) -shared -Wl,-soname,$(LIBSONAME) -Wl,--as-needed libwebvi.o `python-config --ldflags` -o $(LIBMINOR) + ln -sf $(LIBMINOR) $(LIBSONAME) + ln -sf $(LIBSONAME) $(LIBNAME) + +libwebvi.a: libwebvi.o + ar rsc libwebvi.a libwebvi.o + +clean: + rm -f *.o *~ libwebvi.so* libwebvi.a + rm -f webvi/*.pyc webvi/*~ + +install: $(LIBMINOR) + mkdir -p $(PREFIX)/lib + cp --remove-destination -d $(LIBNAME)* $(PREFIX)/lib + /sbin/ldconfig $(PREFIX)/lib + +.PHONY: clean install diff --git a/src/libwebvi/libwebvi.c b/src/libwebvi/libwebvi.c new file mode 100644 index 0000000..c4d9aed --- /dev/null +++ b/src/libwebvi/libwebvi.c @@ -0,0 +1,814 @@ +/* + * libwebvi.c: C bindings for webvi Python module + * + * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <Python.h> +#include <stdio.h> +#include <dlfcn.h> + +#include "libwebvi.h" + +static const char *VERSION = "libwebvi/" LIBWEBVI_VERSION; + +static const int MAX_ERROR_MESSAGE_LENGTH = 512; +static const int MAX_MSG_STRING_LENGTH = 512; + +static PyThreadState *main_state = NULL; + +typedef struct per_interpreter_data_t { + PyThreadState *interp; + PyObject *webvi_module; + char *last_error; + WebviMsg latest_message; +} per_interpreter_data; + +#ifdef DEBUG + +#define debug(x...) fprintf(stderr, x) +#define handle_pyerr() { if (PyErr_Occurred()) { PyErr_Print(); } } + +#else + +#define debug(x...) +#define handle_pyerr() PyErr_Clear() + +#endif + + +/********************************************************************** + * + * Internal functions + */ + +static PyObject *call_python(PyObject *webvi_module, + const char *funcname, + PyObject *args) { + PyObject *func, *val = NULL; + +#ifdef DEBUG + debug("call_python %s ", funcname); + if (PyObject_Print(args, stderr, 0) == -1) + debug("<print failed>"); + debug("\n"); +#endif + + func = PyObject_GetAttrString(webvi_module, funcname); + if (func) { + val = PyObject_CallObject(func, args); + + Py_DECREF(func); + } + + return val; +} + +static long set_callback(PyObject *webvi_module, WebviHandle h, + WebviOption callbacktype, + webvi_callback callback, + PyObject *prototype) { + long res = WEBVIERR_UNKNOWN_ERROR; + + if (prototype && PyCallable_Check(prototype)) { + PyObject *args = Py_BuildValue("(l)", (long)callback); + PyObject *val = PyObject_CallObject(prototype, args); + Py_DECREF(args); + + if (val) { + PyObject *webvihandle = PyInt_FromLong(h); + PyObject *option = PyInt_FromLong(callbacktype); + PyObject *args2 = PyTuple_Pack(3, webvihandle, option, val); + PyObject *retval = call_python(webvi_module, "set_opt", args2); + Py_DECREF(args2); + Py_DECREF(option); + Py_DECREF(webvihandle); + Py_DECREF(val); + + if (retval) { + if (PyInt_Check(retval)) + res = PyInt_AsLong(retval); + Py_DECREF(retval); + } + } + } + + if (res == WEBVIERR_UNKNOWN_ERROR) + handle_pyerr(); + + return res; +} + +/* + * Converts PyInt to WebviResult. + * + * If intobject is NULL, assumes that a Python exception has occurred. + */ +static WebviResult pyint_as_webviresult(PyObject *intobject) { + if (intobject && PyInt_Check(intobject)) + return PyInt_AsLong(intobject); + + handle_pyerr(); + + return WEBVIERR_UNKNOWN_ERROR; +} + +/* + * Duplicate Python string as C string. If the parameter is a unicode + * object, it is encoded to UTF-8. The caller must free the returned + * memory. + */ +static char *PyString_strdupUTF8(PyObject *string) { + char *buffer = NULL; + Py_ssize_t len = -1; + char *ret = NULL; + PyObject *realstring = string; + Py_INCREF(realstring); + + if (PyUnicode_Check(realstring)) { + PyObject *encoded = PyUnicode_AsUTF8String(realstring); + if (encoded) { + Py_DECREF(realstring); + realstring = encoded; + } else { + handle_pyerr(); + } + } + + if (PyString_AsStringAndSize(realstring, &buffer, &len) == -1) { + handle_pyerr(); + buffer = ""; + len = 0; + } + + if (buffer) { + ret = (char *)malloc((len+1)*sizeof(char)); + if (ret) + memcpy(ret, buffer, len+1); + } + + Py_DECREF(realstring); + + return ret; +} + +/********************************************************************** + * + * Public functions + */ + +int webvi_global_init() { + if (main_state) + return 0; + + // Python modules in lib-dynload/*.so do not correctly depend on + // libpython*.so. We need to dlopen the library here, otherwise + // importing webvi dies with "undefined symbol: + // PyExc_ValueError". See http://bugs.python.org/issue4434 + dlopen(PYTHONSHAREDLIB, RTLD_LAZY | RTLD_GLOBAL); + + Py_InitializeEx(0); + PyEval_InitThreads(); + main_state = PyThreadState_Get(); + PyEval_ReleaseLock(); /* release GIL acquired by PyEval_InitThreads */ + + return 0; +} + +void webvi_cleanup(int cleanup_python) { + /* Should we kill active interpreters first? */ + + if (cleanup_python != 0) { + PyEval_AcquireLock(); + PyThreadState_Swap(main_state); + Py_Finalize(); + } +} + +WebviCtx webvi_initialize_context(void) { + per_interpreter_data *ctx = (per_interpreter_data *)malloc(sizeof(per_interpreter_data)); + if (!ctx) + goto err; + + PyEval_AcquireLock(); + + ctx->interp = NULL; + ctx->last_error = NULL; + ctx->latest_message.msg = 0; + ctx->latest_message.handle = -1; + ctx->latest_message.status_code = -1; + ctx->latest_message.data = (char *)malloc(MAX_MSG_STRING_LENGTH*sizeof(char)); + if (!ctx->latest_message.data) + goto err; + + ctx->interp = Py_NewInterpreter(); + if (!ctx->interp) { + debug("Py_NewInterpreter failed\n"); + goto err; + } + + PyThreadState_Swap(ctx->interp); + + ctx->webvi_module = PyImport_ImportModule("webvi.api"); + if (!ctx->webvi_module) { + debug("import webvi.api failed\n"); + handle_pyerr(); + goto err; + } + + /* These are used to wrap C-callbacks into Python callables. + Keep in sync with libwebvi.h. */ + if (PyRun_SimpleString("from ctypes import CFUNCTYPE, c_int, c_size_t, c_char_p, c_void_p\n" + "WriteCallback = CFUNCTYPE(c_size_t, c_char_p, c_size_t, c_void_p)\n" + "ReadCallback = CFUNCTYPE(c_size_t, c_char_p, c_size_t, c_void_p)\n") != 0) { + debug("callback definitions failed\n"); + goto err; + } + + PyEval_ReleaseThread(ctx->interp); + + return (WebviCtx)ctx; + +err: + if (ctx) { + if (ctx->interp) { + Py_EndInterpreter(ctx->interp); + PyThreadState_Swap(NULL); + } + + PyEval_ReleaseLock(); + + if (ctx->latest_message.data) + free(ctx->latest_message.data); + free(ctx); + } + + return 0; +} + +void webvi_cleanup_context(WebviCtx ctx) { + if (ctx == 0) + return; + + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyThreadState_Swap(c->interp); + + /* FIXME: explicitly terminate all active handles? */ + + Py_DECREF(c->webvi_module); + c->webvi_module = NULL; + + Py_EndInterpreter(c->interp); + c->interp = NULL; + + PyThreadState_Swap(NULL); + + free(c); +} + +const char* webvi_version(void) { + return VERSION; +} + +const char* webvi_strerror(WebviCtx ctx, WebviResult res) { + char *errmsg; + + per_interpreter_data *c = (per_interpreter_data *)ctx; + + if (!c->last_error) { + /* We are going to leak c->last_error */ + c->last_error = (char *)malloc(MAX_ERROR_MESSAGE_LENGTH*sizeof(char)); + if (!c->last_error) + return NULL; + } + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(i)", res); + PyObject *msg = call_python(c->webvi_module, "strerror", args); + Py_DECREF(args); + + if (msg) { + errmsg = PyString_AsString(msg); + if (!errmsg) { + handle_pyerr(); + errmsg = "Internal error"; + } + + strncpy(c->last_error, errmsg, MAX_ERROR_MESSAGE_LENGTH-1); + c->last_error[MAX_ERROR_MESSAGE_LENGTH] = '\0'; + + Py_DECREF(msg); + } else { + handle_pyerr(); + } + + PyEval_ReleaseThread(c->interp); + + return c->last_error; +} + +WebviResult webvi_set_config(WebviCtx ctx, WebviConfig conf, const char *value) { + WebviResult res; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(is)", conf, value); + PyObject *v = call_python(c->webvi_module, "set_config", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviHandle webvi_new_request(WebviCtx ctx, const char *webvireference, WebviRequestType type) { + WebviHandle res = -1; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(si)", webvireference, type); + PyObject *v = call_python(c->webvi_module, "new_request", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_start_handle(WebviCtx ctx, WebviHandle h) { + WebviResult res; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(i)", h); + PyObject *v = call_python(c->webvi_module, "start_handle", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_stop_handle(WebviCtx ctx, WebviHandle h) { + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(i)", h); + PyObject *v = call_python(c->webvi_module, "stop_handle", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_delete_handle(WebviCtx ctx, WebviHandle h) { + WebviResult res; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(i)", h); + PyObject *v = call_python(c->webvi_module, "delete_handle", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_set_opt(WebviCtx ctx, WebviHandle h, WebviOption opt, ...) { + va_list argptr; + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *m = PyImport_AddModule("__main__"); + if (!m) { + handle_pyerr(); + PyEval_ReleaseThread(c->interp); + return res; + } + + PyObject *maindict = PyModule_GetDict(m); + + va_start(argptr, opt); + + switch (opt) { + case WEBVIOPT_WRITEFUNC: + { + webvi_callback writerptr = va_arg(argptr, webvi_callback); + PyObject *write_prototype = PyDict_GetItemString(maindict, "WriteCallback"); + if (write_prototype) + res = set_callback(c->webvi_module, h, WEBVIOPT_WRITEFUNC, + writerptr, write_prototype); + break; + } + + case WEBVIOPT_WRITEDATA: + { + void *data = va_arg(argptr, void *); + PyObject *args = Py_BuildValue("(iil)", h, WEBVIOPT_WRITEDATA, (long)data); + PyObject *v = call_python(c->webvi_module, "set_opt", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + break; + } + + case WEBVIOPT_READFUNC: + { + webvi_callback readerptr = va_arg(argptr, webvi_callback); + PyObject *read_prototype = PyDict_GetItemString(maindict, "ReadCallback"); + if (read_prototype) + res = set_callback(c->webvi_module, h, WEBVIOPT_READFUNC, + readerptr, read_prototype); + break; + } + + case WEBVIOPT_READDATA: + { + void *data = va_arg(argptr, void *); + PyObject *args = Py_BuildValue("(iil)", h, WEBVIOPT_READDATA, (long)data); + PyObject *v = call_python(c->webvi_module, "set_opt", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + break; + } + + default: + res = WEBVIERR_INVALID_PARAMETER; + break; + } + + va_end(argptr); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_get_info(WebviCtx ctx, WebviHandle h, WebviInfo info, ...) { + va_list argptr; + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + va_start(argptr, info); + + switch (info) { + case WEBVIINFO_URL: + { + char **dest = va_arg(argptr, char **); + PyObject *args = Py_BuildValue("(ii)", h, info); + PyObject *v = call_python(c->webvi_module, "get_info", args); + Py_DECREF(args); + + *dest = NULL; + + if (v) { + if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *val = PySequence_GetItem(v, 1); + + if (PyInt_Check(retval) && + (PyString_Check(val) || PyUnicode_Check(val))) { + *dest = PyString_strdupUTF8(val); + res = PyInt_AsLong(retval); + } + + Py_DECREF(val); + Py_DECREF(retval); + } + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + break; + } + + case WEBVIINFO_CONTENT_LENGTH: + { + long *dest = va_arg(argptr, long *); + PyObject *args = Py_BuildValue("(ii)", h, info); + PyObject *v = call_python(c->webvi_module, "get_info", args); + Py_DECREF(args); + + *dest = -1; + + if (v) { + if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *val = PySequence_GetItem(v, 1); + + if (PyInt_Check(retval) && PyInt_Check(val)) { + *dest = PyInt_AsLong(val); + res = PyInt_AsLong(retval); + } + + Py_DECREF(val); + Py_DECREF(retval); + } + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + break; + } + + case WEBVIINFO_CONTENT_TYPE: + { + char **dest = va_arg(argptr, char **); + PyObject *args = Py_BuildValue("(ii)", h, info); + PyObject *v = call_python(c->webvi_module, "get_info", args); + Py_DECREF(args); + + *dest = NULL; + + if (v) { + if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *val = PySequence_GetItem(v, 1); + + if (PyInt_Check(retval) && + (PyString_Check(val) || PyUnicode_Check(val))) { + *dest = PyString_strdupUTF8(val); + res = PyInt_AsLong(retval); + } + + Py_DECREF(val); + Py_DECREF(retval); + } + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + break; + } + + case WEBVIINFO_STREAM_TITLE: + { + char **dest = va_arg(argptr, char **); + PyObject *args = Py_BuildValue("(ii)", h, info); + PyObject *v = call_python(c->webvi_module, "get_info", args); + Py_DECREF(args); + + *dest = NULL; + + if (v) { + if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *val = PySequence_GetItem(v, 1); + + if (PyInt_Check(retval) && + (PyString_Check(val) || PyUnicode_Check(val))) { + *dest = PyString_strdupUTF8(val); + res = PyInt_AsLong(retval); + } + + Py_DECREF(val); + Py_DECREF(retval); + } + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + break; + } + + default: + res = WEBVIERR_INVALID_PARAMETER; + break; + } + + va_end(argptr); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_fdset(WebviCtx ctx, + fd_set *read_fd_set, + fd_set *write_fd_set, + fd_set *exc_fd_set, + int *max_fd) +{ + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *v = call_python(c->webvi_module, "fdset", NULL); + + if (v && PySequence_Check(v) && (PySequence_Length(v) == 5)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *readfd = PySequence_GetItem(v, 1); + PyObject *writefd = PySequence_GetItem(v, 2); + PyObject *excfd = PySequence_GetItem(v, 3); + PyObject *maxfd = PySequence_GetItem(v, 4); + PyObject *fd; + int i; + + if (readfd && PySequence_Check(readfd)) { + for (i=0; i<PySequence_Length(readfd); i++) { + fd = PySequence_GetItem(readfd, i); + if (fd && PyInt_Check(fd)) + FD_SET(PyInt_AsLong(fd), read_fd_set); + else + handle_pyerr(); + + Py_XDECREF(fd); + } + } + + if (writefd && PySequence_Check(writefd)) { + for (i=0; i<PySequence_Length(writefd); i++) { + fd = PySequence_GetItem(writefd, i); + if (fd && PyInt_Check(fd)) + FD_SET(PyInt_AsLong(fd), write_fd_set); + else + handle_pyerr(); + + Py_XDECREF(fd); + } + } + + if (excfd && PySequence_Check(excfd)) { + for (i=0; i<PySequence_Length(excfd); i++) { + fd = PySequence_GetItem(excfd, i); + if (fd && PyInt_Check(fd)) + FD_SET(PyInt_AsLong(fd), exc_fd_set); + else + handle_pyerr(); + + Py_XDECREF(fd); + } + } + + if (maxfd && PyInt_Check(maxfd)) + *max_fd = PyInt_AsLong(maxfd); + else + handle_pyerr(); + + if (retval && PyInt_Check(retval)) + res = PyInt_AsLong(retval); + else + handle_pyerr(); + + Py_XDECREF(maxfd); + Py_XDECREF(excfd); + Py_XDECREF(writefd); + Py_XDECREF(readfd); + Py_XDECREF(retval); + } else { + handle_pyerr(); + } + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_perform(WebviCtx ctx, int fd, int ev_bitmask, long *running_handles) { + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(ii)", fd, ev_bitmask); + PyObject *v = call_python(c->webvi_module, "perform", args); + Py_DECREF(args); + + if (v && (PySequence_Check(v) == 1) && (PySequence_Size(v) == 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *numhandles = PySequence_GetItem(v, 1); + + if (PyInt_Check(numhandles)) + *running_handles = PyInt_AsLong(numhandles); + if (PyInt_Check(retval)) + res = PyInt_AsLong(retval); + + Py_DECREF(numhandles); + Py_DECREF(retval); + } else { + handle_pyerr(); + } + + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviMsg *webvi_get_message(WebviCtx ctx, int *remaining_messages) { + per_interpreter_data *c = (per_interpreter_data *)ctx; + + WebviMsg *msg = NULL; + + PyEval_AcquireThread(c->interp); + + PyObject *v = call_python(c->webvi_module, "pop_message", NULL); + + if (v) { + if ((PySequence_Check(v) == 1) && (PySequence_Length(v) == 4)) { + msg = &(c->latest_message); + msg->msg = WEBVIMSG_DONE; + msg->handle = -1; + msg->status_code = -1; + msg->data[0] = '\0'; + + PyObject *handle = PySequence_GetItem(v, 0); + if (handle && PyInt_Check(handle)) + msg->handle = (WebviHandle)PyInt_AsLong(handle); + Py_XDECREF(handle); + + PyObject *status = PySequence_GetItem(v, 1); + if (status && PyInt_Check(status)) + msg->status_code = (int)PyInt_AsLong(status); + Py_XDECREF(status); + + PyObject *errmsg = PySequence_GetItem(v, 2); + if (errmsg && + (PyString_Check(errmsg) || PyUnicode_Check(errmsg))) { + char *cstr = PyString_strdupUTF8(errmsg); + if (cstr) { + strncpy(msg->data, cstr, MAX_MSG_STRING_LENGTH); + msg->data[MAX_MSG_STRING_LENGTH-1] = '\0'; + + free(cstr); + } + } + Py_XDECREF(errmsg); + + PyObject *remaining = PySequence_GetItem(v, 3); + if (remaining && PyInt_Check(remaining)) + *remaining_messages = (int)PyInt_AsLong(remaining); + else + *remaining_messages = 0; + Py_XDECREF(remaining); + } + + if (msg->handle == -1) + msg = NULL; + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + PyEval_ReleaseThread(c->interp); + + return msg; +} diff --git a/src/libwebvi/libwebvi.h b/src/libwebvi/libwebvi.h new file mode 100644 index 0000000..dd7ff39 --- /dev/null +++ b/src/libwebvi/libwebvi.h @@ -0,0 +1,330 @@ +/* + * libwebvi.h: C bindings for webvi Python module + * + * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __LIBWEBVI_H +#define __LIBWEBVI_H + +#include <sys/select.h> +#include <stdlib.h> + +typedef int WebviHandle; + +typedef ssize_t (*webvi_callback)(const char *, size_t, void *); + +typedef enum { + WEBVIMSG_DONE +} WebviMsgType; + +typedef struct { + WebviMsgType msg; + WebviHandle handle; + int status_code; + char *data; +} WebviMsg; + +typedef enum { + WEBVIREQ_MENU, + WEBVIREQ_FILE, + WEBVIREQ_STREAMURL +} WebviRequestType; + +typedef enum { + WEBVIERR_UNKNOWN_ERROR = -1, + WEBVIERR_OK = 0, + WEBVIERR_INVALID_HANDLE, + WEBVIERR_INVALID_PARAMETER +} WebviResult; + +typedef enum { + WEBVIOPT_WRITEFUNC, + WEBVIOPT_READFUNC, + WEBVIOPT_WRITEDATA, + WEBVIOPT_READDATA, +} WebviOption; + +typedef enum { + WEBVIINFO_URL, + WEBVIINFO_CONTENT_LENGTH, + WEBVIINFO_CONTENT_TYPE, + WEBVIINFO_STREAM_TITLE +} WebviInfo; + +typedef enum { + WEBVI_SELECT_TIMEOUT = 0, + WEBVI_SELECT_READ = 1, + WEBVI_SELECT_WRITE = 2, + WEBVI_SELECT_EXCEPTION = 4 +} WebviSelectBitmask; + +typedef enum { + WEBVI_CONFIG_TEMPLATE_PATH +} WebviConfig; + +typedef long WebviCtx; + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Initialize the library. Must be called before any other functions + * (the only exception is webvi_version() which can be called before + * the library is initialized). + * + * Returns 0, if initialization succeeds. + */ +int webvi_global_init(void); + +/* + * Frees all resources currently used by libwebvi and terminates all + * active connections. Do not call any libwebvi function after this. + * If the cleanup_python equals 0, the Python library is deinitialized + * by calling Py_Finalize(), otherwise the Python library is left + * loaded to be used by other modules of the program. + */ +void webvi_cleanup(int cleanup_python); + +/* + * Create a new context. A valid context is required for calling other + * functions in the library. The created contextes are independent of + * each other. The context must be destroyed by a call to + * webvi_cleanup_context when no longer needed. + * + * Return value 0 indicates an error. + */ +WebviCtx webvi_initialize_context(void); + +/* + * Free resources allocated by context ctx. The context can not be + * used anymore after a call to this function. + */ +void webvi_cleanup_context(WebviCtx ctx); + +/* + * Return the version of libwebvi as a string. The returned value + * points to a status buffer, and the caller should modify or not free() it. + */ +const char* webvi_version(void); + +/* + * Return a string describing an error code. The returned value points + * to a status buffer, and the caller should not modify or free() it. + */ +const char* webvi_strerror(WebviCtx ctx, WebviResult err); + +/* + * Set a new value for a global configuration option conf. + * + * Currently the only legal value for conf is TEMPLATE_PATH, which + * sets the base directory for the XSLT templates. + * + * The string pointed by value is copied to the library. + */ +WebviResult webvi_set_config(WebviCtx ctx, WebviConfig conf, const char *value); + +/* + * Creates a new download request. + * + * webvireference is a wvt:// URI of the resource that should be + * downloaded. type should be WEBVIREQ_MENU, if the resource should be + * transformed into a XML menu (that is if webvireferece comes from + * <ref> tag), WEBVIREQ_FILE, if the resource points to a media stream + * (from <stream> tag) whose contents should be downloaded, or + * WEBVIREQ_STREAMURL, if the resource is points to a media stream + * whose real URL should be resolved. + * + * Typically, the reference has been acquired from a previously + * downloaded menu. A special constant "wvt:///?srcurl=mainmenu" with + * type WEBVIREQ_MENU can be used to download mainmenu. + * + * The return value is a handle to the newly created request. Value -1 + * indicates an error. + * + * The request is initialized but the actual network transfer is not + * started. You can set up additional configuration options on the + * handle using webvi_set_opt() before starting the handle with + * webvi_start_handle(). + */ +WebviHandle webvi_new_request(WebviCtx ctx, const char *wvtreference, WebviRequestType type); + +/* + * Starts the transfer on handle h. The transfer one or more sockets + * whose file descriptors are returned by webvi_fdset(). The actual + * transfer is done during webvi_perform() calls. + */ +WebviResult webvi_start_handle(WebviCtx ctx, WebviHandle h); + +/* + * Requests that the transfer on handle h shoud be aborted. After the + * library has actually finished aborting the transfer, the handle h + * is returned by webvi_get_message() with non-zero status code. + */ +WebviResult webvi_stop_handle(WebviCtx ctx, WebviHandle h); + +/* + * Frees resources associated with handle h. The handle can not be + * used after this call. If the handle is still in the middle of a + * transfer, the transfer is forcefully aborted. + */ +WebviResult webvi_delete_handle(WebviCtx ctx, WebviHandle h); + +/* + * Sets configuration options that changes behaviour of the handle. + * opt is one of the values of WebviOption enum as indicated below. + * The fourth parameter sets the value of the specified option. Its + * type depends on opt as discussed below. + * + * Possible values for opt: + * + * WEBVIOPT_WRITEFUNC + * + * Set the callback function that shall be called when data is read + * from the network. The fourth parameter is a pointer to the callback + * funtion + * + * ssize_t (*webvi_callback)(const char *, size_t, void *). + * + * When the function is called, the first parameter is a pointer to + * the incoming data, the second parameters is the size of the + * incoming data block in bytes, and the third parameter is a pointer + * to user's data structure can be set by WEBVIOPT_WRITEDATA option. + * + * The callback funtion should return the number of bytes is + * processed. If this differs from the size of the incoming data + * block, it indicates that an error occurred and the transfer will be + * aborted. + * + * If write callback has not been set (or if it is set to NULL) the + * incoming data is printed to stdout. + * + * WEBVIOPT_WRITEDATA + * + * Sets the value that will be passed to the write callback. The + * fourth parameter is of type void *. + * + * WEBVIOPT_READFUNC + * + * Set the callback function that shall be called when data is to be + * send to network. The fourth parameter is a pointer to the callback + * funtion + * + * ssize_t (*webvi_callback)(const char *, size_t, void *) + * + * The first parameter is a pointer to a buffer where the data that is + * to be sent should be written. The second parameter is the maximum + * size of the buffer. The thirs parameter is a pointer to user data + * set with WEBVIOPT_READDATA. + * + * The return value should be the number of bytes actually written to + * the buffer. If the return value is -1, the transfer is aborted. + * + * WEBVIOPT_READDATA + * + * Sets the value that will be passed to the read callback. The + * fourth parameter is of type void *. + * + */ +WebviResult webvi_set_opt(WebviCtx ctx, WebviHandle h, WebviOption opt, ...); + +/* + * Get information specific to a WebviHandle. The value will be + * written to the memory location pointed by the third argument. The + * type of the pointer depends in the second parameter as discused + * below. + * + * Available information: + * + * WEBVIINFO_URL + * + * Receive URL. The third parameter must be a pointer to char *. The + * caller must free() the memory. + * + * WEBVIINFO_CONTENT_LENGTH + * + * Receive the value of Content-length field, or -1 if the size is + * unknown. The third parameter must be a pointer to long. + * + * WEBVIINFO_CONTENT_TYPE + * + * Receive the Content-type string. The returned value is NULL, if the + * Content-type is unknown. The third parameter must be a pointer to + * char *. The caller must free() the memory. + * + * WEBVIINFO_STREAM_TITLE + * + * Receive stream title. The returned value is NULL, if title is + * unknown. The third parameter must be a pointer to char *. The + * caller must free() the memory. + * + */ +WebviResult webvi_get_info(WebviCtx ctx, WebviHandle h, WebviInfo info, ...); + +/* + * Get active file descriptors in use by the library. The file + * descriptors that should be waited for reading, writing or + * exceptions are returned in read_fd_set, write_fd_set and + * exc_fd_set, respectively. The fd_sets are not cleared, but the new + * file descriptors are added to them. max_fd will contain the highest + * numbered file descriptor that was returned in one of the fd_sets. + * + * One should wait for action in one of the file descriptors returned + * by this function using select(), poll() or similar system call, + * and, after seeing action on a file descriptor, call webvi_perform + * on that descriptor. + */ +WebviResult webvi_fdset(WebviCtx ctx, fd_set *readfd, fd_set *writefd, fd_set *excfd, int *max_fd); + +/* + * Perform input or output action on a file descriptor. + * + * activefd is a file descriptor that was returned by an earlier call + * to webvi_fdset and has been signalled to be ready by select() or + * similar funtion. ev_bitmask should be OR'ed combination of + * WEBVI_SELECT_READ, WEBVI_SELECT_WRITE, WEBVI_SELECT_EXCEPTION to + * indicate that activefd has been signalled to be ready for reading, + * writing or being in exception state, respectively. ev_bitmask can + * also set to WEBVI_SELECT_TIMEOUT which means that the state is + * checked internally. On return, running_handles will contain the + * number of still active file descriptors. + * + * This function should be called with activefd set to 0 and + * ev_bitmask to WEBVI_SELECT_TIMEOUT periodically (every few seconds) + * even if no file descriptors have become ready to allow for timeout + * handling and other internal tasks. + */ +WebviResult webvi_perform(WebviCtx ctx, int sockfd, int ev_bitmask, long *running_handles); + +/* + * Return the next message from the message queue. Currently the only + * message, WEBVIMSG_DONE, indicates that a transfer on a handle has + * finished. The number of messages remaining in the queue after this + * call is written to remaining_messages. The pointers in the returned + * WebviMsg point to handle's internal buffers and is valid until the + * next call to webvi_get_message(). The caller should free the + * returned WebviMsg. The return value is NULL if there is no messages + * in the queue. + */ +WebviMsg *webvi_get_message(WebviCtx ctx, int *remaining_messages); + +#ifdef __cplusplus +} +#endif + + +#endif diff --git a/src/libwebvi/pythonlibname.py b/src/libwebvi/pythonlibname.py new file mode 100755 index 0000000..48f4b97 --- /dev/null +++ b/src/libwebvi/pythonlibname.py @@ -0,0 +1,14 @@ +#!/usr/bin/python + +import distutils.sysconfig +import os +import os.path + +libdir = distutils.sysconfig.get_config_var('LIBDIR') +ldlibrary = distutils.sysconfig.get_config_var('LDLIBRARY') + +libfile = os.readlink(os.path.join(libdir, ldlibrary)) +if not os.path.isabs(libfile): + libfile = os.path.join(libdir, libfile) + +print libfile diff --git a/src/libwebvi/webvi/__init__.py b/src/libwebvi/webvi/__init__.py new file mode 100644 index 0000000..b6d50d5 --- /dev/null +++ b/src/libwebvi/webvi/__init__.py @@ -0,0 +1 @@ +__all__ = ['api', 'asyncurl', 'constants', 'download', 'request', 'utils'] diff --git a/src/libwebvi/webvi/api.py b/src/libwebvi/webvi/api.py new file mode 100644 index 0000000..2fb24ab --- /dev/null +++ b/src/libwebvi/webvi/api.py @@ -0,0 +1,289 @@ +# api.py - webvi API +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""webvi API + +Example workflow: + +1) Create a new request. ref is a wvt:// URI. + +handle = new_request(ref, WebviRequestType.MENU) + +2) Setup a callback function: + +setopt(handle, WebviOpt.WRITEFUNC, my_callback) + +3) Start the network transfer: + +start_handle(handle) + +4) Get active file descriptors, wait for activity on them, and let +webvi process the file descriptor. + +import select + +... + +readfd, writefd, excfd = fdset()[1:4] +readfd, writefd, excfd = select.select(readfd, writefd, excfd, 5.0) +for fd in readfd: + perform(fd, WebviSelectBitmask.READ) +for fd in writefd: + perform(fd, WebviSelectBitmask.WRITE) + +5) Iterate 4) until pop_message returns handle, which indicates that +the request has been completed. + +finished, status, errmsg, remaining = pop_message() +if finished == handle: + print 'done' +""" + +import request +import asyncore +import asyncurl +from constants import WebviErr, WebviOpt, WebviInfo, WebviSelectBitmask, WebviConfig + +# Human readable messages for WebviErr items +error_messages = { + WebviErr.OK: 'Succeeded', + WebviErr.INVALID_HANDLE: 'Invalid handle', + WebviErr.INVALID_PARAMETER: "Invalid parameter", + WebviErr.INTERNAL_ERROR: "Internal error" + } + +# Module-level variables +finished_queue = [] +request_list = request.RequestList() +socket_map = asyncore.socket_map + +# Internal functions + +class MyRequest(request.Request): + def request_done(self, err, errmsg): + """Calls the inherited function and puts the handle of the + finished request to the finished_queue.""" + finished_queue.append(self) + request.Request.request_done(self, err, errmsg) + +# Public functions + +def strerror(err): + """Return human readable error message for conststants.WebviErr""" + try: + return error_messages[err] + except KeyError: + return error_messages[WebviErr.INTERNAL_ERROR] + +def set_config(conf, value): + """Set a new value for a global configuration option conf. + + Currently the only legal value for conf is + constants.WebviConfig.TEMPLATE_PATH, which sets the base directory + for the XSLT templates. + """ + if conf == WebviConfig.TEMPLATE_PATH: + request.set_template_path(value) + return WebviErr.OK + else: + return WebviErr.INVALID_PARAMETER + +def new_request(reference, reqtype): + """Create a new request. + + reference is a wvt:// URI which typically comes from previously + opened menu. reqtype is one of conststants.WebviRequestType and + indicates wheter the reference is a navigation menu, stream that + should be downloaded, or a stream whose URL should be returned. + + Returns a handle (an integer) will be given to following + functions. Return value -1 indicates an error. + """ + req = MyRequest(reference, reqtype) + + if req.srcurl is None: + return -1 + + return request_list.put(req) + +def set_opt(handle, option, value): + """Set configuration options on a handle. + + option specifies option's name (one of constants.WebviOpt values) + and value is the new value for the option. + """ + + try: + req = request_list[handle] + except KeyError: + return WebviErr.INVALID_HANDLE + + if option == WebviOpt.WRITEFUNC: + req.writefunc = value + elif option == WebviOpt.WRITEDATA: + req.writedata = value + elif option == WebviOpt.READFUNC: + req.readfunc = value + elif option == WebviOpt.READDATA: + req.readdata = value + else: + return WebviErr.INVALID_PARAMETER + + return WebviErr.OK + +def get_info(handle, info): + """Get information about a handle. + + info is the type of data that is to be returned (one of + constants.WebviInfo values). + """ + try: + req = request_list[handle] + except KeyError: + return (WebviErr.INVALID_HANDLE, None) + + val = None + if info == WebviInfo.URL: + if req.dl is not None: + val = req.dl.get_url() + else: + val = req.srcurl + elif info == WebviInfo.CONTENT_LENGTH: + val = req.contentlength + elif info == WebviInfo.CONTENT_TYPE: + val = req.contenttype + elif info == WebviInfo.STREAM_TITLE: + val = req.streamtitle + else: + return (WebviErr.INVALID_PARAMETER, None) + + return (WebviErr.OK, val) + +def start_handle(handle): + """Start the network transfer on a handle.""" + try: + req = request_list[handle] + except KeyError: + return WebviErr.INVALID_HANDLE + + req.start() + return WebviErr.OK + +def stop_handle(handle): + """Aborts network transfer on a handle. + + The abort is confirmed by pop_message() returning the handle with + an non-zero error code. + """ + try: + req = request_list[handle] + except KeyError: + return WebviErr.INVALID_HANDLE + + if not req.is_finished(): + req.stop() + + return WebviErr.OK + +def delete_handle(handle): + """Frees resources related to handle. + + This should be called when the transfer has been completed and the + user is done with the handle. If the transfer is still in progress + when delete_handle() is called, the transfer is aborted. After + calling delete_handle() the handle value will be invalid, and + should not be feed to other functions anymore. + """ + try: + del request_list[handle] + except KeyError: + return WebviErr.INVALID_HANDLE + + return WebviErr.OK + +def pop_message(): + """Retrieve messages about finished requests. + + If a request has been finished since the last call to this + function, returns a tuple (handle, status, msg, num_messages), + where handle identifies the finished request, status is a numeric + status code (non-zero for an error), msg is a description of an + error as string, and num_messages is the number of messages that + can be retrieved by calling pop_messages() again immediately. If + the finished requests queue is empty, returns (-1, -1, "", 0). + """ + if finished_queue: + req = finished_queue.pop() + return (req.handle, req.status, req.errmsg, len(finished_queue)) + else: + return (-1, -1, "", 0) + +def fdset(): + """Get the list of file descriptors that are currently in use by + the library. + + Returrns a tuple, where the first item is a constants.WebviErr + value indicating the success of the call, the next three values + are lists of descriptors that should be monitored for reading, + writing, and exceptional conditions, respectively. The last item + is the maximum of the file descriptors in the three lists. + """ + readfd = [] + writefd = [] + excfd = [] + maxfd = -1 + + for fd, disp in socket_map.iteritems(): + if disp.readable(): + readfd.append(fd) + if fd > maxfd: + maxfd = fd + if disp.writable(): + writefd.append(fd) + if fd > maxfd: + maxfd = fd + + return (WebviErr.OK, readfd, writefd, excfd, maxfd) + +def perform(fd, ev_bitmask): + """Perform transfer on file descriptor fd. + + fd is a file descriptor that has been signalled to be ready by + select() or similar system call. ev_bitmask specifies what kind of + activity has been detected using values of + constants.WebviSelectBitmask. If ev_bitmask is + constants.WebviSelectBitmask.TIMEOUT the type of activity is check + by the function. + + This function should be called every few seconds with fd=-1, + ev_bitmask=constants.WebviSelectBitmask.TIMEOUT even if no + activity has been signalled on the file descriptors to ensure + correct handling of timeouts and other internal processing. + """ + if fd < 0: + asyncurl.poll() + else: + disp = socket_map.get(fd) + if disp is not None: + if ev_bitmask & WebviSelectBitmask.READ != 0 or \ + (ev_bitmask == 0 and disp.readable()): + disp.handle_read_event() + if ev_bitmask & WebviSelectBitmask.WRITE != 0 or \ + (ev_bitmask == 0 and disp.writable()): + disp.handle_write_event() + + return (WebviErr.OK, len(socket_map)) diff --git a/src/libwebvi/webvi/asyncurl.py b/src/libwebvi/webvi/asyncurl.py new file mode 100644 index 0000000..afc575a --- /dev/null +++ b/src/libwebvi/webvi/asyncurl.py @@ -0,0 +1,389 @@ +# asyncurl.py - Wrapper class for using pycurl objects in asyncore +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""This is a wrapper for using pycurl objects in asyncore. + +Start a transfer by creating an async_curl_dispatch, and call +asyncurl.loop() instead of asyncore.loop(). +""" + +import asyncore +import pycurl +import traceback +import os +import select +import time +import cStringIO +from errno import EINTR + +SOCKET_TIMEOUT = pycurl.SOCKET_TIMEOUT +CSELECT_IN = pycurl.CSELECT_IN +CSELECT_OUT = pycurl.CSELECT_OUT +CSELECT_ERR = pycurl.CSELECT_ERR + +def poll(timeout=0.0, map=None, mdisp=None): + if map is None: + map = asyncore.socket_map + if mdisp is None: + mdisp = multi_dispatcher + if map: + timeout = min(timeout, mdisp.timeout/1000.0) + + r = []; w = []; e = [] + for fd, obj in map.items(): + is_r = obj.readable() + is_w = obj.writable() + if is_r: + r.append(fd) + if is_w: + w.append(fd) + if is_r or is_w: + e.append(fd) + if [] == r == w == e: + time.sleep(timeout) + else: + try: + r, w, e = select.select(r, w, e, timeout) + except select.error, err: + if err[0] != EINTR: + raise + else: + return + + if [] == r == w == e: + mdisp.socket_action(SOCKET_TIMEOUT, 0) + return + + for fd in r: + obj = map.get(fd) + if obj is None: + continue + asyncore.read(obj) + + for fd in w: + obj = map.get(fd) + if obj is None: + continue + asyncore.write(obj) + + for fd in e: + obj = map.get(fd) + if obj is None: + continue + asyncore._exception(obj) + +def loop(timeout=30.0, use_poll=False, map=None, count=None, mdisp=None): + if map is None: + map = asyncore.socket_map + if mdisp is None: + mdisp = multi_dispatcher + + if use_poll and hasattr(select, 'poll'): + print 'poll2 not implemented' + poll_fun = poll + + if count is None: + while map: + poll_fun(timeout, map, mdisp) + + else: + while map and count > 0: + poll_fun(timeout, map, mdisp) + count = count - 1 + +def noop_callback(s): + pass + + +class curl_multi_dispatcher: + """A dispatcher for pycurl.CurlMulti() objects. An instance of + this class is created automatically. There is usually no need to + construct one manually.""" + def __init__(self, socket_map=None): + if socket_map is None: + self._map = asyncore.socket_map + else: + self._map = socket_map + self.dispatchers = {} + self.timeout = 1000 + self._sockets_removed = False + self._curlm = pycurl.CurlMulti() + self._curlm.setopt(pycurl.M_SOCKETFUNCTION, self.socket_callback) + self._curlm.setopt(pycurl.M_TIMERFUNCTION, self.timeout_callback) + + def socket_callback(self, action, socket, user_data, socket_data): +# print 'socket callback: %d, %s' % \ +# (socket, {pycurl.POLL_NONE: "NONE", +# pycurl.POLL_IN: "IN", +# pycurl.POLL_OUT: "OUT", +# pycurl.POLL_INOUT: "INOUT", +# pycurl.POLL_REMOVE: "REMOVE"}[action]) + + if action == pycurl.POLL_NONE: + return + elif action == pycurl.POLL_REMOVE: + if socket in self._map: + del self._map[socket] + self._sockets_removed = True + return + + obj = self._map.get(socket) + if obj is None: + obj = dispatcher_wrapper(socket, self) + self._map[socket] = obj + + if action == pycurl.POLL_IN: + obj.set_readable(True) + obj.set_writable(False) + elif action == pycurl.POLL_OUT: + obj.set_readable(False) + obj.set_writable(True) + elif action == pycurl.POLL_INOUT: + obj.set_readable(True) + obj.set_writable(True) + + def timeout_callback(self, msec): + self.timeout = msec + + def attach(self, curldisp): + """Starts a transfer on curl handle by attaching it to this + multihandle.""" + self.dispatchers[curldisp.curl] = curldisp + try: + self._curlm.add_handle(curldisp.curl) + except pycurl.error: + # the curl object is already on this multi-stack + pass + + while self._curlm.socket_all()[0] == pycurl.E_CALL_MULTI_PERFORM: + pass + + self.check_completed(True) + + def detach(self, curldisp): + """Removes curl handle from this multihandle, and fire its + completion callback function.""" + self.del_curl(curldisp.curl) + + # libcurl does not send POLL_REMOVE when a handle is aborted + for socket, curlobj in self._map.items(): + if curlobj == curldisp: + + print 'handle stopped but socket in map' + + del self._map[socket] + break + + def del_curl(self, curl): + try: + self._curlm.remove_handle(curl) + except pycurl.error: + # the curl object is not on this multi-stack + pass + if curl in self.dispatchers: + del self.dispatchers[curl] + curl.close() + + def socket_action(self, fd, evbitmask): + res = -1 + OK = False + while not OK: + try: + res = self._curlm.socket_action(fd, evbitmask) + OK = True + except pycurl.error: + # Older libcurls may return CURLM_CALL_MULTI_PERFORM, + # which pycurl (at least 7.19.0) converts to an + # exception. If that happens, call socket_action + # again. + pass + return res + + def check_completed(self, force): + if not force and not self._sockets_removed: + return + self._sockets_removed = False + + nmsg, success, failed = self._curlm.info_read() + for handle in success: + disp = self.dispatchers.get(handle) + if disp is not None: + try: + disp.handle_completed(0, None) + except: + self.handle_error() + self.del_curl(handle) + for handle, err, errmsg in failed: + disp = self.dispatchers.get(handle) + if disp is not None: + try: + disp.handle_completed(err, errmsg) + except: + self.handle_error() + self.del_curl(handle) + + def handle_error(self): + print 'Exception occurred in multicurl processing' + print traceback.format_exc() + + +class dispatcher_wrapper: + """An internal helper class that connects a file descriptor in the + asyncore.socket_map to a curl_multi_dispatcher.""" + def __init__(self, fd, multicurl): + self.fd = fd + self.multicurl = multicurl + self.read_flag = False + self.write_flag = False + + def readable(self): + return self.read_flag + + def writable(self): + return self.write_flag + + def set_readable(self, x): + self.read_flag = x + + def set_writable(self, x): + self.write_flag = x + + def handle_read_event(self): + self.multicurl.socket_action(self.fd, CSELECT_IN) + self.multicurl.check_completed(False) + + def handle_write_event(self): + self.multicurl.socket_action(self.fd, CSELECT_OUT) + self.multicurl.check_completed(False) + + def handle_expt_event(self): + self.multicurl.socket_action(self.fd, CSELECT_ERR) + self.multicurl.check_completed(False) + + def handle_error(self): + print 'Exception occurred during processing of a curl request' + print traceback.format_exc() + + +class async_curl_dispatcher: + """A dispatcher class for pycurl transfers.""" + def __init__(self, url, auto_start=True): + """Initializes a pycurl object self.curl. The default is to + download url to an internal buffer whose content can be read + with self.recv(). If auto_start is False, the transfer is not + started before a call to add_channel(). + """ + self.url = url + self.socket = None + self.buffer = cStringIO.StringIO() + self.curl = pycurl.Curl() + self.curl.setopt(pycurl.URL, self.url) + self.curl.setopt(pycurl.FOLLOWLOCATION, 1) + self.curl.setopt(pycurl.AUTOREFERER, 1) + self.curl.setopt(pycurl.MAXREDIRS, 10) + self.curl.setopt(pycurl.FAILONERROR, 1) + self.curl.setopt(pycurl.WRITEFUNCTION, self.write_to_buf) + if auto_start: + self.add_channel() + + def write_to_buf(self, msg): + self.buffer.write(msg) + self.handle_read() + + def send(self, data): + raise NotImplementedError + + def recv(self, buffer_size): + # buffer_size is ignored + ret = self.buffer.getvalue() + self.buffer.reset() + self.buffer.truncate() + return ret + + def add_channel(self, multidisp=None): + if multidisp is None: + multidisp = multi_dispatcher + multidisp.attach(self) + + def del_channel(self, multidisp=None): + if multidisp is None: + multidisp = multi_dispatcher + multidisp.detach(self) + + def close(self): + self.del_channel() + + def log_info(self, message, type='info'): + if type != 'info': + print '%s: %s' % (type, message) + + def handle_error(self): + print 'Exception occurred during processing of a curl request' + print traceback.format_exc() + self.close() + + def handle_read(self): + self.log_info('unhandled read event', 'warning') + + def handle_write(self): + self.log_info('unhandled write event', 'warning') + + def handle_completed(self, err, errmsg): + """Called when the download has finished. err is a numeric + error code (or 0 if the download was successfull) and errmsg + is a curl error message as a string.""" + # It seems that a reference to self.write_to_buf forbids + # garbage collection from deleting this object. unsetopt() or + # setting the callback to None are not allowed. Is there a + # better way? + self.curl.setopt(pycurl.WRITEFUNCTION, noop_callback) + self.close() + + +def test(): + + class curl_request(async_curl_dispatcher): + def __init__(self, url, outfile, i): + async_curl_dispatcher.__init__(self, url, False) + self.id = i + self.outfile = outfile + self.add_channel() + + def handle_read(self): + buf = self.recv(4096) + print '%s: writing %d bytes' % (self.id, len(buf)) + self.outfile.write(buf) + + def handle_completed(self, err, errmsg): + if err != 0: + print '%s: error: %d %s' % (self.id, err, errmsg) + else: + print '%s: completed' % self.id + + curl_request('http://www.python.org', open('python.out', 'w'), 1) + curl_request('http://en.wikipedia.org/wiki/Main_Page', open('wikipedia.out', 'w'), 2) + loop(timeout=5.0) + + +pycurl.global_init(pycurl.GLOBAL_DEFAULT) +try: + multi_dispatcher +except NameError: + multi_dispatcher = curl_multi_dispatcher() + +if __name__ == '__main__': + test() diff --git a/src/libwebvi/webvi/constants.py b/src/libwebvi/webvi/constants.py new file mode 100644 index 0000000..2797178 --- /dev/null +++ b/src/libwebvi/webvi/constants.py @@ -0,0 +1,50 @@ +# constants.py - Python definitions for constants in libwebvi.h +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Keep these in sync with libwebvi.h + +class WebviRequestType: + MENU = 0 + FILE = 1 + STREAMURL = 2 + +class WebviErr: + OK = 0 + INVALID_HANDLE = 1 + INVALID_PARAMETER = 2 + INTERNAL_ERROR = -1 + +class WebviOpt: + WRITEFUNC = 0 + READFUNC = 1 + WRITEDATA = 2 + READDATA = 3 + +class WebviInfo: + URL = 0 + CONTENT_LENGTH = 1 + CONTENT_TYPE = 2 + STREAM_TITLE = 3 + +class WebviSelectBitmask: + TIMEOUT = 0 + READ = 1 + WRITE = 2 + EXCEPTION = 4 + +class WebviConfig: + TEMPLATE_PATH = 0 diff --git a/src/libwebvi/webvi/download.py b/src/libwebvi/webvi/download.py new file mode 100644 index 0000000..34240ff --- /dev/null +++ b/src/libwebvi/webvi/download.py @@ -0,0 +1,470 @@ +# download.py - webvi downloader backend +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import asyncore +import asynchat +import cStringIO +import urllib +import subprocess +import socket +import signal +import pycurl +import asyncurl +import utils +import version + +WEBVID_USER_AGENT = 'libwebvi/%s %s' % (version.VERSION, pycurl.version) +MOZILLA_USER_AGENT = 'Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5' + +try: + from libmimms import libmms +except ImportError, e: + pass + +# Mapping from curl error codes to webvi errors. The error constants +# are defined only in pycurl 7.16.1 and newer. +if pycurl.version_info()[2] >= 0x071001: + CURL_ERROR_CODE_MAPPING = \ + {pycurl.E_OK: 0, + pycurl.E_OPERATION_TIMEOUTED: 408, + pycurl.E_OUT_OF_MEMORY: 500, + pycurl.E_PARTIAL_FILE: 504, + pycurl.E_READ_ERROR: 504, + pycurl.E_RECV_ERROR: 504, + pycurl.E_REMOTE_FILE_NOT_FOUND: 404, + pycurl.E_TOO_MANY_REDIRECTS: 404, + pycurl.E_UNSUPPORTED_PROTOCOL: 500, + pycurl.E_URL_MALFORMAT: 400, + pycurl.E_COULDNT_CONNECT: 506, + pycurl.E_COULDNT_RESOLVE_HOST: 506, + pycurl.E_COULDNT_RESOLVE_PROXY: 506, + pycurl.E_FILE_COULDNT_READ_FILE: 404, + pycurl.E_GOT_NOTHING: 504, + pycurl.E_HTTP_RETURNED_ERROR: 404, + pycurl.E_INTERFACE_FAILED: 506, + pycurl.E_LOGIN_DENIED: 403} +else: + CURL_ERROR_CODE_MAPPING = {pycurl.E_OK: 0} + +class DownloaderException(Exception): + def __init__(self, errcode, errmsg): + self.code = errcode + self.msg = errmsg + + def __str__(self): + return '%s %s' % (self.code, self.msg) + +def create_downloader(url, templatedir, writefunc=None, headerfunc=None, + donefunc=None, HTTPheaders=None, headers_only=False): + """Downloader factory. + + Returns a suitable downloader object according to url type. Raises + DownloaderException if creating the downloader fails. + """ + if url == '': + return DummyDownloader('', writefunc, headerfunc, donefunc, + headers_only) + + elif url.startswith('mms://') or url.startswith('mmsh://'): + try: + libmms + except (NameError, OSError): + raise DownloaderException(501, 'MMS scheme not supported. Install mimms.') + return MMSDownload(url, writefunc, headerfunc, donefunc, + headers_only) + + elif url.startswith('wvt://'): + executable, parameters = parse_external_downloader_wvt_uri(url, templatedir) + if executable is None: + raise DownloaderException(400, 'Invalid wvt:// URL') + try: + return ExternalDownloader(executable, parameters, writefunc, + headerfunc, donefunc, headers_only) + except OSError, (errno, strerror): + raise DownloaderException(500, 'Failed to execute %s: %s' % + (executable, strerror)) + + else: + return CurlDownload(url, writefunc, headerfunc, donefunc, + HTTPheaders, headers_only) + +def convert_curl_error(err, errmsg, aborted): + """Convert a curl error code err to webvi error code.""" + if err == pycurl.E_WRITE_ERROR: + return (402, 'Aborted') + elif err not in CURL_ERROR_CODE_MAPPING: + return (500, errmsg) + else: + return (CURL_ERROR_CODE_MAPPING[err], errmsg) + +def parse_external_downloader_wvt_uri(url, templatedir): + exe = None + params = [] + if not url.startswith('wvt:///bin/'): + return (exe, params) + + split = url[len('wvt:///bin/'):].split('?', 1) + exe = templatedir + '/bin/' + split[0] + + if len(split) > 1: + params = [urllib.unquote(x) for x in split[1].split('&')] + + return (exe, params) + +def _new_process_group(): + os.setpgid(0, 0) + +class DownloaderBase: + """Base class for downloaders.""" + def __init__(self, url): + self.url = url + + def start(self): + """Should start the download process.""" + pass + + def abort(self): + """Signals that the download should be aborted.""" + pass + + def get_url(self): + """Return the URL where the data was downloaded.""" + return self.url + + def get_body(self): + return '' + + def get_encoding(self): + """Return the encoding of the downloaded object, or None if + encoding is not known.""" + return None + + +class DummyDownloader(DownloaderBase, asyncore.file_dispatcher): + """This class doesn't actually download anything, but returns msg + string as if it had been result of a download operation. + """ + def __init__(self, msg, writefunc=None, headerfunc=None, + donefunc=None, headers_only=False): + DownloaderBase.__init__(self, '') + self.donefunc = donefunc + self.writefunc = writefunc + self.headers_only = headers_only + + readfd, writefd = os.pipe() + asyncore.file_dispatcher.__init__(self, readfd) + os.write(writefd, msg) + os.close(writefd) + + def set_file(self, fd): + # Like asyncore.file_dispatcher.set_file() but doesn't call + # add_channel(). We'll call add_channel() in start() when the + # download shall begin. + self.socket = asyncore.file_wrapper(fd) + self._fileno = self.socket.fileno() + + def start(self): + if self.headers_only: + self.donefunc(0, None) + else: + self.add_channel() + + def readable(self): + return True + + def writable(self): + return False + + def handle_read(self): + try: + data = self.recv(4096) + if data and self.writefunc is not None: + self.writefunc(data) + except socket.error: + self.handle_error() + + def handle_close(self): + self.close() + + if self.donefunc is not None: + self.donefunc(0, '') + + +class CurlDownload(DownloaderBase, asyncurl.async_curl_dispatcher): + """Downloads a large number of different URL schemes using + libcurl.""" + def __init__(self, url, writefunc=None, headerfunc=None, + donefunc=None, HTTPheaders=None, headers_only=False): + DownloaderBase.__init__(self, url) + asyncurl.async_curl_dispatcher.__init__(self, url, False) + self.donefunc = donefunc + self.writefunc = writefunc + self.contenttype = None + self.running = True + self.aborted = False + + self.curl.setopt(pycurl.USERAGENT, WEBVID_USER_AGENT) + if headers_only: + self.curl.setopt(pycurl.NOBODY, 1) + if headerfunc is not None: + self.curl.setopt(pycurl.HEADERFUNCTION, headerfunc) + self.curl.setopt(pycurl.WRITEFUNCTION, self.writewrapper) + + headers = [] + if HTTPheaders is not None: + for headername, headerdata in HTTPheaders.iteritems(): + if headername == 'cookie': + self.curl.setopt(pycurl.COOKIE, headerdata) + else: + headers.append(headername + ': ' + headerdata) + + self.curl.setopt(pycurl.HTTPHEADER, headers) + + def start(self): + self.add_channel() + + def close(self): + self.contenttype = self.curl.getinfo(pycurl.CONTENT_TYPE) + asyncurl.async_curl_dispatcher.close(self) + self.running = False + + def abort(self): + self.aborted = True + + def writewrapper(self, data): + if self.aborted: + return 0 + + if self.writefunc is None: + return self.write_to_buf(data) + else: + return self.writefunc(data) + + def get_body(self): + return self.buffer.getvalue() + + def get_encoding(self): + if self.running: + self.contenttype = self.curl.getinfo(pycurl.CONTENT_TYPE) + + if self.contenttype is None: + return None + + values = self.contenttype.split(';', 1) + if len(values) > 1: + for par in values[1].split(' '): + if par.startswith('charset='): + return par[len('charset='):].strip('"') + + return None + + def handle_read(self): + # Do nothing to the read data here. Instead, let the base + # class to collect the data to self.buffer. + pass + + def handle_completed(self, err, errmsg): + asyncurl.async_curl_dispatcher.handle_completed(self, err, errmsg) + if self.donefunc is not None: + err, errmsg = convert_curl_error(err, errmsg, self.aborted) + self.donefunc(err, errmsg) + + +class MMSDownload(DownloaderBase, asyncore.file_dispatcher): + def __init__(self, url, writefunc=None, headerfunc=None, + donefunc=None, headers_only=False): + DownloaderBase.__init__(self, url) + self.r, self.w = os.pipe() + asyncore.file_dispatcher.__init__(self, self.r) + + self.writefunc = writefunc + self.headerfunc = headerfunc + self.donefunc = donefunc + self.relaylen = -1 + self.expectedlen = -1 + self.headers_only = headers_only + self.stream = None + self.errmsg = None + self.aborted = False + + def set_file(self, fd): + self.socket = asyncore.file_wrapper(fd) + self._fileno = self.socket.fileno() + + def recv(self, buffer_size): + data = self.stream.read() + if not data: + self.handle_close() + return '' + else: + return data + + def close(self): + if self.stream is not None: + self.stream.close() + + os.close(self.w) + asyncore.file_dispatcher.close(self) + + def readable(self): + return self.stream is not None + + def writable(self): + return False + + def start(self): + try: + self.stream = libmms.Stream(self.url, 1000000) + except libmms.Error, e: + self.errmsg = e.message + self.handle_close() + return + + os.write(self.w, '0') # signal that this dispatcher has data available + + if self.headerfunc: + # Output the length in a HTTP-like header field so that we + # can use the same callbacks as with HTTP downloads. + ext = utils.get_url_extension(self.url) + if ext == 'wma': + self.headerfunc('Content-Type: audio/x-ms-wma') + else: # if ext == 'wmv': + self.headerfunc('Content-Type: video/x-ms-wmv') + self.headerfunc('Content-Length: %d' % self.stream.length()) + + if self.headers_only: + self.handle_close() + else: + self.add_channel() + + def abort(self): + self.aborted = True + + def handle_read(self): + if self.aborted: + self.handle_close() + return '' + + try: + data = self.recv(4096) + if data and (self.writefunc is not None): + self.writefunc(data) + except libmms.Error, e: + self.errmsg = e.message + self.handle_close() + return + + def handle_close(self): + self.close() + self.stream = None + + if self.errmsg is not None: + self.donefunc(500, self.errmsg) + elif self.aborted: + self.donefunc(402, 'Aborted') + elif self.relaylen < self.expectedlen: + # We got fewer bytes than expected. Maybe the connection + # was lost? + self.donefunc(504, 'Download may be incomplete (length %d < %d)' % + (self.relaylen, self.expectedlen)) + else: + self.donefunc(0, '') + + +class ExternalDownloader(DownloaderBase, asyncore.file_dispatcher): + """Executes an external process and reads its result on standard + output.""" + def __init__(self, executable, parameters, writefunc=None, + headerfunc=None, donefunc=None, headers_only=False): + DownloaderBase.__init__(self, '') + asyncore.dispatcher.__init__(self, None, None) + self.executable = executable + self.writefunc = writefunc + self.headerfunc = headerfunc + self.donefunc = donefunc + self.headers_only = headers_only + self.contenttype = '' + self.aborted = False + + args = [] + for par in parameters: + try: + key, val = par.split('=', 1) + if key == 'contenttype': + self.contenttype = val + elif key == 'arg': + args.append(val) + except ValueError: + pass + + if args: + self.url = args[0] + else: + self.url = executable + self.cmd = [executable] + args + + self.process = None + + def start(self): + self.headerfunc('Content-Type: ' + self.contenttype) + + if self.headers_only: + self.donefunc(0, None) + return + + self.process = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, + close_fds=True, + preexec_fn=_new_process_group) + asyncore.file_dispatcher.__init__(self, os.dup(self.process.stdout.fileno())) + + def abort(self): + if self.process is not None: + self.aborted = True + pg = os.getpgid(self.process.pid) + os.killpg(pg, signal.SIGTERM) + + def readable(self): + # Return True if the subprocess is still alive + return self.process is not None and self.process.returncode is None + + def writable(self): + return False + + def handle_read(self): + try: + data = self.recv(4096) + if data and self.writefunc is not None: + self.writefunc(data) + except socket.error: + self.handle_error() + return + + def handle_close(self): + self.close() + self.process.wait() + + if self.donefunc is not None: + if self.process.returncode == 0: + self.donefunc(0, '') + elif self.aborted and self.process.returncode == -signal.SIGTERM: + self.donefunc(402, 'Aborted') + else: + self.donefunc(500, 'Child process "%s" returned error %s' % \ + (' '.join(self.cmd), str(self.process.returncode))) + + self.process = None diff --git a/src/libwebvi/webvi/json2xml.py b/src/libwebvi/webvi/json2xml.py new file mode 100644 index 0000000..372e6c6 --- /dev/null +++ b/src/libwebvi/webvi/json2xml.py @@ -0,0 +1,69 @@ +import sys +import libxml2 + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + print 'Error: install simplejson' + raise + +def _serialize_to_xml(obj, xmlnode): + """Create XML representation of a Python object (list, tuple, + dist, or basic number and string types).""" + if type(obj) in (list, tuple): + listnode = libxml2.newNode('list') + for li in obj: + itemnode = libxml2.newNode('li') + _serialize_to_xml(li, itemnode) + listnode.addChild(itemnode) + xmlnode.addChild(listnode) + + elif type(obj) == dict: + dictnode = libxml2.newNode('dict') + for key, val in obj.iteritems(): + itemnode = libxml2.newNode(key.encode('utf-8')) + _serialize_to_xml(val, itemnode) + dictnode.addChild(itemnode) + xmlnode.addChild(dictnode) + + elif type(obj) in (str, unicode, int, long, float, complex, bool): + content = libxml2.newText(unicode(obj).encode('utf-8')) + xmlnode.addChild(content) + + elif type(obj) == type(None): + pass + + else: + raise TypeError('Unsupported type %s while serializing to xml' + % type(obj)) + +def json2xml(jsonstr, encoding=None): + """Convert JSON string jsonstr to XML tree.""" + try: + parsed = json.loads(jsonstr, encoding) + except ValueError: + return None + + xmldoc = libxml2.newDoc("1.0") + root = libxml2.newNode("jsondocument") + xmldoc.setRootElement(root) + + _serialize_to_xml(parsed, root) + + return xmldoc + +def test(): + xml = json2xml(open(sys.argv[1]).read()) + + if xml is None: + return + + print xml.serialize('utf-8') + + xml.freeDoc() + +if __name__ == '__main__': + test() diff --git a/src/libwebvi/webvi/request.py b/src/libwebvi/webvi/request.py new file mode 100644 index 0000000..e19eb9c --- /dev/null +++ b/src/libwebvi/webvi/request.py @@ -0,0 +1,617 @@ +# request.py - webvi request class +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import urllib +import libxml2 +import os.path +import cStringIO +import re +import download +import sys +import utils +import json2xml +from constants import WebviRequestType + +DEBUG = False + +DEFAULT_TEMPLATE_PATH = '/usr/local/share/webvi/templates' +template_path = DEFAULT_TEMPLATE_PATH + +def debug(msg): + if DEBUG: + if type(msg) == unicode: + sys.stderr.write(msg.encode('ascii', 'replace')) + else: + sys.stderr.write(msg) + sys.stderr.write('\n') + +def set_template_path(path): + global template_path + + if path is None: + template_path = os.path.realpath(DEFAULT_TEMPLATE_PATH) + else: + template_path = os.path.realpath(path) + + debug("set_template_path " + template_path) + +def parse_reference(reference): + """Parses URLs of the following form: + + wvt:///youtube/video.xsl?srcurl=http%3A%2F%2Fwww.youtube.com%2F¶m=name1,value1¶m=name2,value2 + + reference is assumed to be URL-encoded UTF-8 string. + + Returns (template, srcurl, params, processing_instructions) where + template if the URL path name (the part before ?), srcurl is the + parameter called srcurl, and params is a dictionary of (name, + quoted-value) pairs extracted from param parameters. Parameter + values are quoted so that the xslt parser handles them as string. + processing_instructions is dictionary of options that affect the + further processing of the data. + """ + try: + reference = str(reference) + except UnicodeEncodeError: + return (None, None, None, None) + + if not reference.startswith('wvt:///'): + return (None, None, None, None) + + ref = reference[len('wvt:///'):] + + template = None + srcurl = '' + parameters = {} + substitutions = {} + refsettings = {'HTTP-headers': {}} + + fields = ref.split('?', 1) + template = fields[0] + if len(fields) == 1: + return (template, srcurl, parameters, refsettings) + + for par in fields[1].split('&'): + paramfields = par.split('=', 1) + key = paramfields[0] + + if len(paramfields) == 2: + value = urllib.unquote(paramfields[1]) + else: + value = '' + + if key.lower() == 'srcurl': + srcurl = value + + elif key.lower() == 'param': + fields2 = value.split(',', 1) + pname = fields2[0].lower() + if len(fields2) == 2: + pvalue = "'" + fields2[1] + "'" + else: + pvalue = "''" + parameters[pname] = pvalue + + elif key.lower() == 'subst': + substfields = value.split(',', 1) + if len(substfields) == 2: + substitutions[substfields[0]] = substfields[1] + + elif key.lower() == 'minquality': + try: + refsettings['minquality'] = int(value) + except ValueError: + pass + + elif key.lower() == 'maxquality': + try: + refsettings['maxquality'] = int(value) + except ValueError: + pass + + elif key.lower() == 'postprocess': + refsettings.setdefault('postprocess', []).append(value) + + elif key.lower() == 'contenttype': + refsettings['overridecontenttype'] = value + + elif key.lower() == 'http-header': + try: + headername, headerdata = value.split(',', 1) + except ValueError: + continue + refsettings['HTTP-headers'][headername] = headerdata + + if substitutions: + srcurl = brace_substitution(srcurl, substitutions) + + return (template, srcurl, parameters, refsettings) + +def brace_substitution(template, subs): + """Substitute subs[x] for '{x}' in template. Unescape {{ to { and + }} to }. Unescaping is not done in substitution keys, i.e. while + scanning for a closing brace after a single opening brace.""" + strbuf = cStringIO.StringIO() + + last_pos = 0 + for match in re.finditer(r'{{?|}}', template): + next_pos = match.start() + if next_pos < last_pos: + continue + + strbuf.write(template[last_pos:next_pos]) + if match.group(0) == '{{': + strbuf.write('{') + last_pos = next_pos+2 + + elif match.group(0) == '}}': + strbuf.write('}') + last_pos = next_pos+2 + + else: # match.group(0) == '{' + key_end = template.find('}', next_pos+1) + if key_end == -1: + strbuf.write(template[next_pos:]) + last_pos = len(template) + break + + try: + strbuf.write(urllib.quote(subs[template[next_pos+1:key_end]])) + except KeyError: + strbuf.write(template[next_pos:key_end+1]) + last_pos = key_end+1 + + strbuf.write(template[last_pos:]) + return strbuf.getvalue() + + +class Request: + DEFAULT_URL_PRIORITY = 50 + + def __init__(self, reference, reqtype): + self.handle = None + self.dl = None + + # state variables + self.xsltfile, self.srcurl, self.xsltparameters, self.processing = \ + parse_reference(reference) + self.type = reqtype + self.status = -1 + self.errmsg = None + self.mediaurls = [] + + # stream information + self.contenttype = 'text/xml' + self.contentlength = -1 + self.streamtitle = '' + + # callbacks + self.writefunc = None + self.writedata = None + self.readfunc = None + self.readdata = None + + def handle_header(self, buf): + namedata = buf.split(':', 1) + if len(namedata) == 2: + headername, headerdata = namedata + if headername.lower() == 'content-type': + # Strip parameters like charset="utf-8" + self.contenttype = headerdata.split(';', 1)[0].strip() + elif headername.lower() == 'content-length': + try: + self.contentlength = int(headerdata.strip()) + except ValueError: + self.contentlength = -1 + + def setup_downloader(self, url, writefunc, headerfunc, donefunc, + HTTPheaders=None, headers_only=False): + try: + self.dl = download.create_downloader(url, + template_path, + writefunc, + headerfunc, + donefunc, + HTTPheaders, + headers_only) + self.dl.start() + except download.DownloaderException, exc: + self.dl = None + if donefunc is not None: + donefunc(exc.code, exc.msg) + + def start(self): + debug('start %s\ntemplate = %s, type = %s\n' + 'parameters = %s, processing = %s' % + (self.srcurl, self.xsltfile, self.type, str(self.xsltparameters), + str(self.processing))) + + if self.type == WebviRequestType.MENU and self.srcurl == 'mainmenu': + self.send_mainmenu() + else: + self.setup_downloader(self.srcurl, None, + self.handle_header, + self.finished_apply_xslt, + self.processing['HTTP-headers']) + + def stop(self): + if self.dl is not None: + debug("aborting") + self.dl.abort() + + def start_download(self, url=None): + """Initialize a download. + + If url is None, pop the first URL out of self.mediaurls. If + URL is an ASX playlist, read the content URL from it and start + to download the actual content. + """ + while url is None or url == '': + try: + url = self.mediaurls.pop(0) + except IndexError: + self.request_done(406, 'No more URLs left') + + debug('Start_download ' + url) + + # reset stream status + self.contenttype = 'text/xml' + self.contentlength = -1 + + if self.is_asx_playlist(url): + self.setup_downloader(url, None, + self.handle_header, + self.finished_playlist_loaded, + self.processing['HTTP-headers']) + + else: + self.setup_downloader(url, self.writewrapper, + self.handle_header, + self.finished_download, + self.processing['HTTP-headers']) + + def check_and_send_url(self, url=None): + """Check if the target exists (currently only for HTTP URLs) + before relaying the URL to the client.""" + while url is None or url == '': + try: + url = self.mediaurls.pop(0) + except IndexError: + self.request_done(406, 'No more URLs left') + return + + debug('check_and_send_url ' + str(url)) + + if self.is_asx_playlist(url): + self.setup_downloader(url, None, self.handle_header, + self.finished_playlist_loaded, + self.processing['HTTP-headers']) + elif url.startswith('http://') or url.startswith('https://'): + self.checking_url = url + self.setup_downloader(url, None, None, + self.finished_check_url, + self.processing['HTTP-headers'], True) + else: + self.writewrapper(url) + self.request_done(0, None) + + def send_mainmenu(self): + """Build the XML main menu from the module description files + in the hard drive. + """ + if not os.path.isdir(template_path): + self.request_done(404, "Can't access service directory %s" % + template_path) + return + + debug('Reading XSLT templates from ' + template_path) + + # Find menu items in the service.xml files in the subdirectories + menuitems = {} + for f in os.listdir(template_path): + if f == 'bin': + continue + + filename = os.path.join(template_path, f, 'service.xml') + try: + doc = libxml2.parseFile(filename) + except libxml2.parserError: + debug("Failed to parse " + filename); + continue + + title = '' + url = '' + + root = doc.getRootElement() + if (root is None) or (root.name != 'service'): + debug("Root node is not 'service' in " + filename); + doc.freeDoc() + continue + node = root.children + while node is not None: + if node.name == 'title': + title = utils.get_content_unicode(node) + elif node.name == 'ref': + url = utils.get_content_unicode(node) + node = node.next + doc.freeDoc() + + if (title == '') or (url == ''): + debug("Empty <title> or <ref> in " + filename); + continue + + menuitems[title.lower()] = ('<link>\n' + '<label>%s</label>\n' + '<ref>%s</ref>\n' + '</link>\n' % + (libxml2.newText(title), + libxml2.newText(url))) + # Sort the menu items + titles = menuitems.keys() + titles.sort() + + # Build the menu + mainmenu = ('<?xml version="1.0"?>\n' + '<wvmenu>\n' + '<title>Select video source\n') + for t in titles: + mainmenu += menuitems[t] + mainmenu += '' + + 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 + 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 +# +# 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 . + +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(' +# +# 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 . + +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 + * + * 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 . + */ + +#include +#include +#include + +#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 + * + * 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 . + */ + +#include +#include +#include + +#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 +# +# 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 . + +"""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'(.*)', service) + self.assertNotEqual(m, None, 'no 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='' -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 +#include +#include +#include +#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 + +// --- 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 +#include +#include +#include +#include +#include +#include +#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', '#', '%', '{', '}', + '|', '\\', '^', '~', '[', ']', '`', + '\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 +#include +#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; iSetMin(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& 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 +#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 downloadLimits; + cList 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 +#include +#include +#include + +/** 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>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 (sizesize = 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 ; isize ; 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 ; isize ; 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 ; isize ; 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 ; isize ; 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 ; isize ; 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 ; isize ; 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 ; in != 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 +#include +#include +#include + +/*--------------------------------------------------------------------------- + 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 +#include +#include +#include +#include +#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 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; iIsAborted()) { + // 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 +#include +#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 +#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(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(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::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 +#include "menudata.h" + +// --- cHistoryObject ----------------------------------------------------- + +class cHistoryObject : public cListObject { +private: + char *osdxml; + int id; + int selected; + cVector 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 { +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 +#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 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 ; isize ; 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 ; isize ; 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 ; isize ; 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 ; isize ; i++) { + if (d->key[i]==NULL) + continue ; + fprintf(f, "%s = %s\n", d->key[i], d->val[i]); + } + return ; + } + for (i=0 ; isize ; 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 +#include +#include + +/* + * The following #include is necessary on many Unixes but not Linux. + * It is not needed for Windows platforms. + * Uncomment it if needed. + */ +/* #include */ + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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; iGetTitle(), + (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 +#include +#include +#include +#include +#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 links; + // streams[i] is the media stream link of the i:th item + cVector 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 +#include +#include +#include +#include +#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; iGetTitle(), 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 +#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 +#include +#include +#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; iQuerySize(); 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 +#include +#include +#include +#include +#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 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 +#include +#include +#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 , 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.1.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-07-09 15:12+0300\n" +"PO-Revision-Date: 2009-02-18 20:04+0200\n" +"Last-Translator: \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 , 2008,2009. +# +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.1.1\n" +"Report-Msgid-Bugs-To: \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 \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 , 2008. +# +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.0.5\n" +"Report-Msgid-Bugs-To: \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 \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 "" +#~ msgstr "" + +#~ 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 , 2008. +# +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.0.1\n" +"Report-Msgid-Bugs-To: \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 \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 "" +#~ msgstr "" + +#~ 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 +#include +#include +#include +#include +#include +#include +#include +#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; iGetHandle() == 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 +#include +#include +#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 { +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 { +public: + cRequestVector(int Allocated = 10) : cVector(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 +#include +#include +#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; iNextUpdate() < 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 +#include +#include +#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 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 +#include +#include +#include +#include +#include +#include +#include +#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(req); + + if (dlreq) { + for (int i=0; iGetRequest() == 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(req); + if (dlreq) { + for (int i=0; iGetRequest() == 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 = "Webvideo"; + 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 +# +# 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 . + +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 +# +# 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 . + +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 +# +# 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 . + +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 @@ + + + + + + + <xsl:value-of select="//title"/> + + + + + + + + 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 @@ + + + + + + + Google video search + + + + + + + + Relevance + Rating + Popularity + Date + + + + + All durations + Short (< 4 min) + Medium (4-20 min) + Long (> 20 min) + + + + + + + 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 @@ + + + + + + + Search results + + + + + + + + + + + + + wvt:///youtube/video.xsl?srcurl= + wvt:///youtube/description.xsl?srcurl= + + + + + + + wvt:///google/video.xsl?srcurl= + wvt:///google/description.xsl?srcurl= + + + + + + + wvt:///metacafe/video.xsl?srcurl= + wvt:///metacafe/description.xsl?srcurl= + + + + + + + wvt:///vimeo/video.xsl?srcurl=http://www.vimeo.com/moogaloop/load/clip: + wvt:///vimeo/description.xsl?srcurl=http://vimeo.com/api/v2/video/.xml + + + + + + + wvt:///svtplay/video.xsl?srcurl= + wvt:///svtplay/description.xsl?srcurl= + + + + + + + + + + wvt:///google/searchresults.xsl?srcurl= + + + + + + + wvt:///google/searchresults.xsl?srcurl= + + + + + + + + + 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 @@ + + + + Google Video + wvt:///google/search.xsl + Google video search + 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 @@ + + + + + + + <xsl:value-of select="/html/head/title" /> + + + + + + + + + + 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 @@ + + + + + + + MTV3 Katsomo + + + + wvt:///katsomo/search.xsl + + + + + + wvt:///katsomo/navigation.xsl?srcurl= + + + + + + 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 @@ + + + + + + + + + + wvt:///katsomo/navigation.xsl?srcurl= + + + + + + + + + + + wvt:///katsomo/video.xsl?srcurl=&param=title,&HTTP-header=cookie,webtv.bandwidth%3D1000%3BautoFullScreen%3Dfalse%3Bwebtv.playerPlatform%3D0 + + + + + + <xsl:value-of select="/html/head/meta[@name='title']/@content"/> + + + + + + + + + + + + + + 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 @@ + + + + + + + Haku + + + + + + + + + + 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 @@ + + + + + + + + + + + wvt:///katsomo/video.xsl?srcurl=&param=title,&HTTP-header=cookie,webtv.bandwidth%3D1000%3BautoFullScreen%3Dfalse%3Bwebtv.playerPlatform%3D0 + + + + + + Hakutulokset: <xsl:value-of select="id('searchResults')/div/div[@class='description']/span"/> + + + + + + + + + + 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 @@ + + + + MTV3 Katsomo + wvt:///katsomo/mainmenu.xsl?srcurl=http%3A//katsomo.fi/ + Net TV service of the Finnish broadcasting company MTV3 + 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 @@ + + + + +katsomovideo + + + + <xsl:value-of select="$title"/> + + + + + + 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 @@ + + + + + + + Metacafe + + + + wvt:///metacafe/search.xsl + + + + + wvt:///metacafe/channellist.xsl?srcurl=/api/channels/ + + + + + + + + wvt:///metacafe/navigation.xsl?srcurl=/api/videos/-/ + + + + + + + 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 @@ + + + + + + + + wvt:///metacafe/navigation.xsl?srcurl=/api/users//channel?time=all_time + + + + + + <xsl:value-of select="/rss/channel/title"/> + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + <xsl:value-of select="/rss/channel/item/title"/> + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + wvt:///youtube/video.xsl?srcurl=http%3A//www.youtube.com/watch%3Fv= + + + wvt:///metacafe/video.xsl?srcurl= + + + + wvt:///metacafe/description.xsl?srcurl=/api/item/ + + + + + + <xsl:value-of select="/rss/channel/title"/> + + + + + + + + + + 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 @@ + + + + + + + Metacafe Search + + + + + + + + Most recent + View Count + Most discussed + + + + + Anytime + During last 24 hours + This week + This month + + + + + + + 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 @@ + + + + Metacafe + wvt:///metacafe/categories.xsl?srcurl=http%3A//www.metacafe.com/videos/ + Video sharing site specializing in short-form original content + 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 @@ + + + + + + + <xsl:value-of select="normalize-space(id('ItemTitle'))"/> + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + <xsl:value-of select="/Playerdata/Behavior/Program/@program_name"/> + + + + + + + + + + + + wvt:///ruutufi/video.xsl?srcurl= + + + + + 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 @@ + + + + + + + Ruutu.fi + + + + wvt:///ruutufi/search.xsl + + + + + wvt:///ruutufi/series.xsl?srcurl=http://www.ruutu.fi/ajax/media_get_netti_tv_series_list/all/false&postprocess=json2xml + + + + + wvt:///ruutufi/program.xsl?srcurl=http://www.ruutu.fi/ajax/media_get_nettitv_media/all/video_episode/__/latestdesc/0/25/true/__&postprocess=json2xml + + + + + wvt:///ruutufi/program.xsl?srcurl=http://www.ruutu.fi/ajax/media_get_nettitv_media/all/video_episode/__/most_watched/0/25/true/__&postprocess=json2xml + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + http://www.nelonen.fi/utils/video_config/%3Fq%3D/%26site%3Dwww.ruutu.fi%26ageCheckURL%3Dhttp://sso.nelonenmedia.fi/ajax/check_age/%26current_page%3Dhttp://www.ruutu.fi/video + + wvt:///ruutufi/description.xsl?srcurl= + wvt:///ruutufi/video.xsl?srcurl= + + + + + + + + + + + + <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> + + + + + + + + + video_episode + + + video + + + + + + wvt:///ruutufi/program.xsl?srcurl=&postprocess=json2xml + + + + + + + + + + /// + + + + + + + + + 0 + + + + + + + + wvt:///ruutufi/program.xsl?srcurl=&postprocess=json2xml + + + + + + + wvt:///ruutufi/program.xsl?srcurl=&postprocess=json2xml + + + + + + + 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 @@ + + + + + + + Haku + + + + + + + + + + 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 @@ + + + + + + + + + + + wvt:///ruutufi/program.xsl?srcurl=http://www.ruutu.fi/ajax/media_get_nettitv_video/all/video_episode//latestdesc/0/25/true/__&postprocess=json2xml + + + + + + + + Kaikki sarjat + + + + + + 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 @@ + + + + ruutu.fi + wvt:///ruutufi/mainmenu.xsl?srcurl=http%3A//www.ruutu.fi/ + Net TV service of the Finnish broadcasting company Nelonen + 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 @@ + + + + + + + <xsl:value-of select="concat(/Playerdata/Behavior/Program/@program_name, ' ', /Playerdata/Behavior/Program/@episode_name)"/> + + + + wvt:///bin/ruutu-dl?contenttype=video/x-flv&arg=&arg=http://www.ruutu.fi/video + + + + + + + + + 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 @@ + + + + + + + <xsl:value-of select="concat(id('ruutuVideoInfo')/p[@class='name'], ' ', id('ruutuVideoInfo')/p[@class='timeStamp'])"/> + + wvt:///bin/ruutu-dl?contenttype=video/x-flv&arg=&arg= + + + + 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 @@ + + + + + + + + + + + + <xsl:value-of select="$title"/> + + + + + + + + wvt:///subtv/video.xsl?param=pid,&param=title, + + + + + 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 @@ + + + + + + + Subin netti-TV + + + + + wvt:///subtv/navigation.xsl?srcurl= + + + + + + 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 @@ + + + + + + + + + + + + + + + wvt:///subtv/video.xsl?srcurl=&param=pid,&param=title, + wvt:///subtv/description.xsl?param=title,&param=desc,&param=pubdate,&param=pid, + + + + + + + <xsl:value-of select="$programname"/> + + + + + + + + + + + + + + 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 @@ + + + + Subtv + wvt:///subtv/mainmenu.xsl?srcurl=http%3A//www.sub.fi/katsonetista/ + Sub is the third biggest commercial tv channel in Finland. + 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 @@ + + + + + + + + + + <xsl:value-of select="$title"/> + + + + + + 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 @@ + + + + + + + SVT Play + + + + + wvt:///svtplay/navigation.xsl?srcurl= + + + + + + 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 @@ + + + + + + + + + + + + + + <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> + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + wvt:///svtplay/programmenu.xsl?srcurl= + + + + + + + + + wvt:///svtplay/navigation.xsl?srcurl= + + + + + + + wvt:///svtplay/navigation.xsl?srcurl= + + + + + + + + <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> + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + wvt:///svtplay/description.xsl?srcurl= + wvt:///svtplay/video.xsl?srcurl= + + + + + + + + + wvt:///svtplay/programmenu.xsl?srcurl= + + + + + + + wvt:///svtplay/programmenu.xsl?srcurl= + + + + + + + + <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> + + + + + + + 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 @@ + + + + SVT Play + wvt:///svtplay/categories.xsl?srcurl=http://svtplay.se/kategorier + Swedish Television, online TV 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 @@ + + + + + + + + <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> + + + + + + + + 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 @@ + + + + + + + Vimeo Channels + + + + + wvt:///vimeo/navigation.xsl?srcurl=http://vimeo.com/api/v2/channel//videos.xml + + + + + + + + + + + + wvt:///vimeo/channels.xsl?srcurl= + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + <xsl:value-of select="/videos/video/title"/> + + + + + + + + + + + + + wvt:///vimeo/navigation.xsl?srcurl=http://vimeo.com/api/v2//videos.xml + + + + + wvt:///vimeo/video.xsl?srcurl=http://www.vimeo.com/moogaloop/load/clip: + + + + + + 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 @@ + + + + + + + Vimeo Groups + + + + + wvt:///vimeo/navigation.xsl?srcurl=http://vimeo.com/api/v2/group//videos.xml + + + + + + + + + + + + wvt:///vimeo/groups.xsl?srcurl= + + + + + + + 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 @@ + + + + + + + Vimeo + + + + wvt:///vimeo/search.xsl?srcurl=http://www.vimeo.com/ + + + + + wvt:///vimeo/channels.xsl?srcurl=http://www.vimeo.com/channels/all + + + + + wvt:///vimeo/groups.xsl?srcurl=http://www.vimeo.com/groups/all + + + + + + 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 @@ + + + + + + + + wvt:///vimeo/video.xsl?srcurl=http://www.vimeo.com/moogaloop/load/clip: + wvt:///vimeo/description.xsl?srcurl=http://vimeo.com/api/v2/video/.xml + + + + + + Vimeo videos + + + + + + 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 @@ + + + + + + + Vimeo Search + + + + + + + + most relevant + newest + most played + most liked + + + + + + + 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 @@ + + + + + + + Search results + + + + + wvt:///vimeo/video.xsl?srcurl=http://www.vimeo.com/moogaloop/load/clip: + wvt:///vimeo/description.xsl?srcurl=http://vimeo.com/api/v2/video/.xml + + + + + + + + + + + + wvt:///vimeo/searchresults.xsl?srcurl= + + + + + + + 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 @@ + + + + Vimeo + wvt:///vimeo/mainmenu.xsl + Vimeo is a video-centric social networking site + 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 @@ + + + + + + + <xsl:value-of select="/xml/video/caption"/> + + http://www.vimeo.com/moogaloop/play/clip:///?q=sd + + + + 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 @@ + + + + + + + <xsl:value-of select="normalize-space(//h1[@class='cliptitle'])"/> + + + + + + + + + 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 @@ + + + + + + + + + + + wvt:///yleareena/livestream.xsl?param=stream, + + + + + + + + wvt:///yleareena/livestream.xsl?param=stream, + + + + + + + + + + + Suorat lähetykset + + + + + + + + + + + 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 @@ + + + + + + + + + livestream-<xsl:value-of select="$stream"/> + + + wvt:///bin/yle-dl?contenttype=video/x-flv&arg=http%3A//areena.yle.fi/player/index.php%3Fstream%3D%26language%3Dfi + + + + + + + + + + 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 @@ + + + + + + + YLE Areena + + + + wvt:///yleareena/search.xsl?srcurl=http://areena.yle.fi/haku + + + + + wvt:///yleareena/livebroadcasts.xsl?srcurl=http://areena.yle.fi/live + + + + + wvt:///yleareena/programlist.xsl?srcurl=http://areena.yle.fi/ohjelmat + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + wvt:///yleareena/navigation.xsl?srcurl= + + + + + + wvt:///yleareena/navigation.xsl?srcurl= + + + + + + + + + + + + + + + + + + + + + + + + + + wvt:///yleareena/navigation.xsl?srcurl= + + + + + + + + + + + + + + wvt:///yleareena/description.xsl?srcurl= + wvt:///yleareena/video.xsl?srcurl=&param=title, + + + + + + + + + + + <xsl:value-of select="$title"/> + + + + + + + + + Hae Areenasta: Ei osumia + + + + + + + + + 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 @@ + + + + + + + + wvt:///yleareena/navigation.xsl?srcurl=&param=title, + + + + + + Ohjelmat A-Ö + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + Hae Areenasta + + + + + + + + + + Kaikki + Kyllä + + + + + + + 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 @@ + + + + YLE Areena + wvt:///yleareena/mainmenu.xsl?srcurl=http%3A//areena.yle.fi/ + Video service by YLE, the Finland's national public service broadcasting company + 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 @@ + + + + + + + + + + <xsl:value-of select="$title"/> + wvt:///bin/yle-dl?contenttype=video/x-flv&arg= + + + + + 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 @@ + + + + + + + Youtube + + + + wvt:///youtube/search.xsl + + + + + + wvt:///youtube/navigation.xsl?srcurl=http://gdata.youtube.com/feeds/api/standardfeeds/most_popular_%3Fmax-results%3D20%26v%3D2 + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + <xsl:value-of select="/atom:entry/atom:title"/> + + + + + + + + + + + + + + wvt:///youtube/navigation.xsl?srcurl= + + + + + + + wvt:///youtube/navigation.xsl?srcurl= + + + + + + wvt:///youtube/video.xsl?srcurl=http://www.youtube.com/watch?v= + + + + + + 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 @@ + + + + + + + + wvt:///youtube/video.xsl?srcurl=http://www.youtube.com/watch?v= + wvt:///youtube/description.xsl?srcurl= + + + + + + + + wvt:///youtube/navigation.xsl?srcurl= + + + + + + + wvt:///youtube/navigation.xsl?srcurl=a + + + + + + + <xsl:value-of select="/atom:feed/atom:title"/> + + + + + wvt:///youtube/navigation.xsl?srcurl= + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + Youtube Search + + + + + + + + Relevance + Date Added + View Count + Rating + + + + + Anytime + Today + This week + This month + + + + + + + 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 @@ + + + + YouTube + wvt:///youtube/categories.xsl?srcurl=http://gdata.youtube.com/schemas/2007/categories.cat + Video sharing service on which users worldwide can upload their videos + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <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> + + + http://www.youtube.com/get_video?video_id=&t=&fmt=22 + http://www.youtube.com/get_video?video_id=&t=&fmt=18 + http://www.youtube.com/get_video?video_id=&t= + + + + -- cgit v1.2.3