summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAntti Ajanki <antti.ajanki@iki.fi>2013-08-06 09:07:49 +0300
committerAntti Ajanki <antti.ajanki@iki.fi>2013-08-06 09:07:49 +0300
commit7c81286a59639e139ac7e947378be24410701a5e (patch)
tree88e43b758dc2330e8711ebae80eee0039cc57322 /src
downloadvdr-plugin-webvideo-7c81286a59639e139ac7e947378be24410701a5e.tar.gz
vdr-plugin-webvideo-7c81286a59639e139ac7e947378be24410701a5e.tar.bz2
import to vdr-developer repo
Diffstat (limited to 'src')
-rw-r--r--src/libwebvi/CMakeLists.txt50
-rw-r--r--src/libwebvi/libwebvi.c338
-rw-r--r--src/libwebvi/libwebvi.h370
-rw-r--r--src/libwebvi/link.c86
-rw-r--r--src/libwebvi/link.h45
-rw-r--r--src/libwebvi/linkextractor.c138
-rw-r--r--src/libwebvi/linkextractor.h34
-rw-r--r--src/libwebvi/linktemplates.c172
-rw-r--r--src/libwebvi/linktemplates.h34
-rw-r--r--src/libwebvi/mainmenu.c120
-rw-r--r--src/libwebvi/mainmenu.h27
-rw-r--r--src/libwebvi/menubuilder.c99
-rw-r--r--src/libwebvi/menubuilder.h37
-rw-r--r--src/libwebvi/pipecomponent.c689
-rw-r--r--src/libwebvi/pipecomponent.h95
-rw-r--r--src/libwebvi/request.c237
-rw-r--r--src/libwebvi/request.h42
-rw-r--r--src/libwebvi/urlutils.c132
-rw-r--r--src/libwebvi/urlutils.h32
-rw-r--r--src/libwebvi/webvicontext.c409
-rw-r--r--src/libwebvi/webvicontext.h63
-rw-r--r--src/pywebvi/pywebvi.py248
-rw-r--r--src/version.h.in1
-rwxr-xr-xsrc/webvicli/webvi22
-rw-r--r--src/webvicli/webvicli/__init__.py1
-rw-r--r--src/webvicli/webvicli/client.py825
-rw-r--r--src/webvicli/webvicli/menu.py177
27 files changed, 4523 insertions, 0 deletions
diff --git a/src/libwebvi/CMakeLists.txt b/src/libwebvi/CMakeLists.txt
new file mode 100644
index 0000000..6e4e155
--- /dev/null
+++ b/src/libwebvi/CMakeLists.txt
@@ -0,0 +1,50 @@
+SET(LIBWEBVI_SOURCES
+ libwebvi.c
+ mainmenu.c
+ webvicontext.c
+ request.c
+ link.c
+ linktemplates.c
+ linkextractor.c
+ menubuilder.c
+ pipecomponent.c
+ urlutils.c)
+
+SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules/")
+
+INCLUDE_DIRECTORIES(${PROJECT_SOURCE_DIR}/src/libwebvi)
+
+ADD_LIBRARY(webvi SHARED ${LIBWEBVI_SOURCES})
+ADD_LIBRARY(webvistatic STATIC ${LIBWEBVI_SOURCES})
+
+SET_TARGET_PROPERTIES(webvi PROPERTIES VERSION "${MAJOR_VERSION}.${MINOR_VERSION}.${PATCH_VERSION}")
+SET_TARGET_PROPERTIES(webvi PROPERTIES SOVERSION "${MAJOR_VERSION}.${MINOR_VERSION}")
+SET_TARGET_PROPERTIES(webvi PROPERTIES COMPILE_FLAGS "-fvisibility=hidden")
+SET_TARGET_PROPERTIES(webvistatic PROPERTIES OUTPUT_NAME webvi)
+
+ADD_DEFINITIONS(-DLIBWEBVI_LOG_DOMAIN="libwebvi")
+
+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -Wall")
+
+# Required libraries
+FIND_PACKAGE(LibXml2 REQUIRED)
+FIND_PACKAGE(CURL REQUIRED)
+FIND_PACKAGE(LibTidy REQUIRED)
+
+FIND_PACKAGE(PkgConfig)
+PKG_CHECK_MODULES(GLIB REQUIRED glib-2.0)
+ADD_DEFINITIONS(${GLIB_CFLAGS} ${GLIB_CFLAGS_OTHER})
+LINK_DIRECTORIES(${GLIB_LIBRARY_DIRS})
+
+INCLUDE_DIRECTORIES(${LIBXML2_INCLUDE_DIR})
+INCLUDE_DIRECTORIES(${CURL_INCLUDE_DIRS})
+INCLUDE_DIRECTORIES(${GLIB_INCLUDE_DIRS})
+INCLUDE_DIRECTORIES(${LIBTIDY_INCLUDE_DIRS})
+
+ADD_DEFINITIONS(-DHAVE_TIDY_ULONG_VERSION=${HAVE_TIDY_ULONG_VERSION})
+
+TARGET_LINK_LIBRARIES(webvi ${GLIB_LIBRARIES} ${LIBXML2_LIBRARIES} ${CURL_LIBRARIES} ${LIBTIDY_LIBRARIES})
+
+# Installing
+INSTALL(TARGETS webvi DESTINATION bin)
+INSTALL(FILES libwebvi.h DESTINATION include)
diff --git a/src/libwebvi/libwebvi.c b/src/libwebvi/libwebvi.c
new file mode 100644
index 0000000..b3d030a
--- /dev/null
+++ b/src/libwebvi/libwebvi.c
@@ -0,0 +1,338 @@
+/*
+ * libwebvi.c
+ *
+ * Copyright (c) 2010-2013 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdarg.h>
+#include <string.h>
+#include <libxml/xmlversion.h>
+#include "libwebvi.h"
+#include "webvicontext.h"
+#include "request.h"
+#include "version.h"
+
+static const char *VERSION = "libwebvi/" LIBWEBVI_VERSION;
+
+struct WebviErrorMessage {
+ WebviResult code;
+ const char *message;
+};
+
+int webvi_global_init() {
+ LIBXML_TEST_VERSION
+ return 0;
+}
+
+void webvi_cleanup() {
+ webvi_context_cleanup_all();
+}
+
+WebviCtx webvi_initialize_context(void) {
+ return webvi_context_initialize();
+}
+
+void webvi_cleanup_context(WebviCtx ctxhandle) {
+ webvi_context_cleanup(ctxhandle);
+}
+
+const char* webvi_version(void) {
+ return VERSION;
+}
+
+const char* webvi_strerror(WebviResult err) {
+ static struct WebviErrorMessage error_messages[] =
+ {{WEBVIERR_OK, "Succeeded"},
+ {WEBVIERR_INVALID_HANDLE, "Invalid handle"},
+ {WEBVIERR_INVALID_PARAMETER, "Invalid parameter"},
+ {WEBVIERR_UNKNOWN_ERROR, "Internal error"}};
+
+ for (int i=0; i<(sizeof(error_messages)/sizeof(error_messages[0])); i++) {
+ if (err == error_messages[i].code) {
+ return error_messages[i].message;
+ }
+ }
+
+ return "Internal error";
+}
+
+WebviResult webvi_set_config(WebviCtx ctxhandle, WebviConfig conf, ...) {
+ va_list argptr;
+ const char *p;
+ WebviResult res = WEBVIERR_OK;
+
+ WebviContext *ctx = get_context_by_handle(ctxhandle);
+ if (!ctx)
+ return WEBVIERR_INVALID_HANDLE;
+
+ va_start(argptr, conf);
+
+ switch (conf) {
+ case WEBVI_CONFIG_TEMPLATE_PATH:
+ p = va_arg(argptr, char *);
+ webvi_context_set_template_path(ctx, p);
+ break;
+ case WEBVI_CONFIG_DEBUG:
+ p = va_arg(argptr, char *);
+ webvi_context_set_debug(ctx, strcmp(p, "0") != 0);
+ break;
+ case WEBVI_CONFIG_TIMEOUT_CALLBACK:
+ // FIXME
+ // va_arg(argptr, long)
+ break;
+ case WEBVI_CONFIG_TIMEOUT_DATA:
+ // FIXME
+ // va_arg(argptr, long)
+ break;
+ default:
+ res = WEBVIERR_INVALID_PARAMETER;
+ };
+
+ va_end(argptr);
+
+ return res;
+}
+
+WebviHandle webvi_new_request(WebviCtx ctxhandle, const char *href) {
+ WebviContext *ctx = get_context_by_handle(ctxhandle);
+ if (!ctx)
+ return WEBVI_INVALID_HANDLE;
+
+ WebviRequest *req = request_create(href, ctx);
+ return webvi_context_add_request(ctx, req);
+}
+
+WebviResult webvi_start_request(WebviCtx ctxhandle, WebviHandle h) {
+ WebviContext *ctx = get_context_by_handle(ctxhandle);
+ if (!ctx)
+ return WEBVIERR_INVALID_HANDLE;
+
+ WebviRequest *req = webvi_context_get_request(ctx, h);
+ if (!req)
+ return WEBVIERR_INVALID_HANDLE;
+
+ request_start(req);
+
+ return WEBVIERR_OK;
+}
+
+WebviResult webvi_stop_request(WebviCtx ctxhandle, WebviHandle h) {
+ WebviContext *ctx = get_context_by_handle(ctxhandle);
+ if (!ctx)
+ return WEBVIERR_INVALID_HANDLE;
+
+ WebviRequest *req = webvi_context_get_request(ctx, h);
+ if (!req)
+ return WEBVIERR_INVALID_HANDLE;
+
+ request_stop(req);
+
+ return WEBVIERR_OK;
+}
+
+WebviResult webvi_delete_request(WebviCtx ctxhandle, WebviHandle h) {
+ WebviContext *ctx = get_context_by_handle(ctxhandle);
+ if (!ctx)
+ return WEBVIERR_INVALID_HANDLE;
+
+ WebviResult res = webvi_stop_request(ctxhandle, h);
+ if (res != WEBVIERR_OK)
+ return res;
+
+ webvi_context_remove_request(ctx, h);
+
+ return WEBVIERR_OK;
+}
+
+WebviResult webvi_set_opt(WebviCtx ctxhandle, WebviHandle h, WebviOption opt, ...) {
+ WebviContext *ctx = get_context_by_handle(ctxhandle);
+ if (!ctx)
+ return WEBVIERR_INVALID_HANDLE;
+
+ WebviRequest *req = webvi_context_get_request(ctx, h);
+ if (!req)
+ return WEBVIERR_INVALID_HANDLE;
+
+ va_list argptr;
+ WebviResult res = WEBVIERR_OK;
+
+ va_start(argptr, opt);
+
+ switch (opt) {
+ case WEBVIOPT_WRITEFUNC:
+ request_set_write_callback(req, va_arg(argptr, webvi_callback));
+ break;
+ case WEBVIOPT_READFUNC:
+ request_set_read_callback(req, va_arg(argptr, webvi_callback));
+ break;
+ case WEBVIOPT_WRITEDATA:
+ request_set_write_data(req, va_arg(argptr, void *));
+ break;
+ case WEBVIOPT_READDATA:
+ request_set_read_data(req, va_arg(argptr, void *));
+ break;
+ default:
+ res = WEBVIERR_INVALID_PARAMETER;
+ };
+
+ va_end(argptr);
+
+ return res;
+}
+
+WebviResult webvi_get_info(WebviCtx ctxhandle, WebviHandle h, WebviInfo info, ...) {
+ WebviContext *ctx = get_context_by_handle(ctxhandle);
+ if (!ctx)
+ return WEBVIERR_INVALID_HANDLE;
+
+ WebviRequest *req = webvi_context_get_request(ctx, h);
+ if (!req)
+ return WEBVIERR_INVALID_HANDLE;
+
+ va_list argptr;
+ WebviResult res = WEBVIERR_OK;
+
+ va_start(argptr, info);
+
+ switch (info) {
+ case WEBVIINFO_URL:
+ {
+ char **output = va_arg(argptr, char **);
+ if (output) {
+ *output = NULL;
+
+ const char *url = request_get_url(req);
+ if (url) {
+ *output = malloc(strlen(url)+1);
+ if (*output) {
+ strcpy(*output, url);
+ }
+ }
+ }
+ break;
+ }
+
+ case WEBVIINFO_CONTENT_LENGTH:
+ {
+ // FIXME
+ long *content_length = va_arg(argptr, long *);
+ if (content_length)
+ *content_length = -1;
+ break;
+ }
+
+ case WEBVIINFO_CONTENT_TYPE:
+ {
+ // FIXME
+ char **output = va_arg(argptr, char **);
+ if (output) {
+ *output = malloc(1);
+ **output = '\0';
+ }
+ break;
+ }
+
+ case WEBVIINFO_STREAM_TITLE:
+ {
+ // FIXME
+ char **output = va_arg(argptr, char **);
+ if (output) {
+ *output = malloc(1);
+ **output = '\0';
+ }
+ break;
+ }
+
+ default:
+ res = WEBVIERR_INVALID_PARAMETER;
+ };
+
+ va_end(argptr);
+
+ return res;
+}
+
+WebviResult webvi_fdset(WebviCtx ctxhandle, fd_set *readfd, fd_set *writefd,
+ fd_set *excfd, int *max_fd) {
+ WebviContext *ctx = get_context_by_handle(ctxhandle);
+ if (!ctx)
+ return WEBVIERR_INVALID_HANDLE;
+
+ return webvi_context_fdset(ctx, readfd, writefd, excfd, max_fd);
+}
+
+WebviResult webvi_perform(WebviCtx ctxhandle, int sockfd, int ev_bitmask,
+ long *running_handles) {
+ WebviContext *ctx = get_context_by_handle(ctxhandle);
+ if (!ctx)
+ return WEBVIERR_INVALID_HANDLE;
+
+ webvi_context_handle_socket_action(ctx, sockfd, ev_bitmask, running_handles);
+
+ return WEBVIERR_OK;
+}
+
+WebviMsg *webvi_get_message(WebviCtx ctxhandle, int *remaining_messages) {
+ WebviContext *ctx = get_context_by_handle(ctxhandle);
+ if (!ctx)
+ return NULL;
+
+ return webvi_context_next_message(ctx, remaining_messages);
+}
+
+int webvi_process_some(WebviCtx ctx, int timeout_seconds) {
+ fd_set readfds;
+ fd_set writefds;
+ fd_set excfds;
+ int maxfd;
+ int s;
+ WebviResult res;
+ struct timeval timeout;
+ long running_handles;
+
+ FD_ZERO(&readfds);
+ FD_ZERO(&writefds);
+ FD_ZERO(&excfds);
+ res = webvi_fdset(ctx, &readfds, &writefds, &excfds, &maxfd);
+ if (res != WEBVIERR_OK) {
+ return -1;
+ }
+
+ timeout.tv_sec = timeout_seconds;
+ timeout.tv_usec = 0;
+ s = select(maxfd+1, &readfds, &writefds, NULL, &timeout);
+
+ if (s == -1) {
+ // error
+ return -1;
+ } else if (s == 0) {
+ // timeout
+ webvi_perform(ctx, WEBVI_SELECT_TIMEOUT, WEBVI_SELECT_CHECK, &running_handles);
+ } else {
+ // handle one fd
+ for (int fd=0; fd<=maxfd; fd++) {
+ if (FD_ISSET(fd, &readfds)) {
+ webvi_perform(ctx, fd, WEBVI_SELECT_READ, &running_handles);
+ } else if (FD_ISSET(fd, &writefds)) {
+ webvi_perform(ctx, fd, WEBVI_SELECT_WRITE, &running_handles);
+ } else if (FD_ISSET(fd, &excfds)) {
+ webvi_perform(ctx, fd, WEBVI_SELECT_EXCEPTION, &running_handles);
+ }
+ }
+ }
+
+ return running_handles;
+}
diff --git a/src/libwebvi/libwebvi.h b/src/libwebvi/libwebvi.h
new file mode 100644
index 0000000..8efe953
--- /dev/null
+++ b/src/libwebvi/libwebvi.h
@@ -0,0 +1,370 @@
+/*
+ * libwebvi.h: C bindings for webvi Python module
+ *
+ * Copyright (c) 2010-2013 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __LIBWEBVI_H
+#define __LIBWEBVI_H
+
+#include <sys/select.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#if webvi_EXPORTS /* defined when building as shared library */
+ #if defined _WIN32 || defined __CYGWIN__
+ #define LIBWEBVI_DLL_EXPORT __declspec(dllexport)
+ #else
+ #if __GNUC__ >= 4
+ #define LIBWEBVI_DLL_EXPORT __attribute__((__visibility__("default")))
+ #endif
+ #endif
+#endif
+
+#ifndef LIBWEBVI_DLL_EXPORT
+#define LIBWEBVI_DLL_EXPORT
+#endif
+
+typedef int WebviHandle;
+typedef long WebviCtx;
+
+typedef ssize_t (*webvi_callback)(const char *, size_t, void *);
+typedef void (*webvi_timeout_callback)(long, void *);
+
+#define WEBVI_INVALID_HANDLE -1
+
+typedef enum {
+ WEBVIMSG_DONE
+} WebviMsgType;
+
+typedef enum {
+ WEBVISTATE_NOT_FINISHED = 0,
+ WEBVISTATE_FINISHED_OK = 1,
+ WEBVISTATE_MEMORY_ALLOCATION_ERROR = 2,
+ WEBVISTATE_NOT_FOUND = 3,
+ WEBVISTATE_NETWORK_READ_ERROR = 4,
+ WEBVISTATE_IO_ERROR = 5,
+ WEBVISTATE_TIMEDOUT = 6,
+ WEBVISTATE_SUBPROCESS_FAILED = 7,
+ WEBVISTATE_INTERNAL_ERROR = 999,
+} RequestState;
+
+typedef enum {
+ WEBVIREQ_MENU,
+ WEBVIREQ_FILE,
+ WEBVIREQ_STREAMURL
+} WebviRequestType;
+
+typedef enum {
+ WEBVIERR_UNKNOWN_ERROR = -1,
+ WEBVIERR_OK = 0,
+ WEBVIERR_INVALID_HANDLE,
+ WEBVIERR_INVALID_PARAMETER
+} WebviResult;
+
+typedef enum {
+ WEBVIOPT_WRITEFUNC,
+ WEBVIOPT_READFUNC,
+ WEBVIOPT_WRITEDATA,
+ WEBVIOPT_READDATA,
+} WebviOption;
+
+typedef enum {
+ WEBVIINFO_URL,
+ WEBVIINFO_CONTENT_LENGTH,
+ WEBVIINFO_CONTENT_TYPE,
+ WEBVIINFO_STREAM_TITLE
+} WebviInfo;
+
+#define WEBVI_SELECT_TIMEOUT -1
+
+typedef enum {
+ WEBVI_SELECT_CHECK = 0,
+ WEBVI_SELECT_READ = 1,
+ WEBVI_SELECT_WRITE = 2,
+ WEBVI_SELECT_EXCEPTION = 4
+} WebviSelectBitmask;
+
+typedef enum {
+ WEBVI_CONFIG_TEMPLATE_PATH,
+ WEBVI_CONFIG_DEBUG,
+ WEBVI_CONFIG_TIMEOUT_CALLBACK,
+ WEBVI_CONFIG_TIMEOUT_DATA
+} WebviConfig;
+
+typedef struct {
+ WebviMsgType msg;
+ WebviHandle handle;
+ RequestState status_code;
+ const char *data;
+} WebviMsg;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Initialize the library. Must be called before any other functions
+ * (the only exception is webvi_version() which can be called before
+ * the library is initialized).
+ *
+ * Returns 0, if initialization succeeds.
+ */
+LIBWEBVI_DLL_EXPORT int webvi_global_init(void);
+
+/*
+ * Frees all resources currently used by libwebvi and terminates all
+ * active connections. Do not call any libwebvi function after this.
+ */
+LIBWEBVI_DLL_EXPORT void webvi_cleanup();
+
+/*
+ * Create a new context. A valid context is required for calling other
+ * functions in the library. The created contextes are independent of
+ * each other. The context must be destroyed by a call to
+ * webvi_cleanup_context when no longer needed.
+ *
+ * Return value 0 indicates an error.
+ */
+LIBWEBVI_DLL_EXPORT WebviCtx webvi_initialize_context(void);
+
+/*
+ * Free resources allocated by context ctx. The context can not be
+ * used anymore after a call to this function.
+ */
+LIBWEBVI_DLL_EXPORT void webvi_cleanup_context(WebviCtx ctx);
+
+/*
+ * Return the version of libwebvi as a string. The returned value
+ * points to a static buffer, and the caller should modify or not free() it.
+ */
+LIBWEBVI_DLL_EXPORT const char* webvi_version(void);
+
+/*
+ * Return a string describing an error code. The returned value points
+ * to a read-only buffer, and the caller should not modify or free() it.
+ */
+LIBWEBVI_DLL_EXPORT const char* webvi_strerror(WebviResult err);
+
+/*
+ * Set a new value for a global configuration option conf.
+ *
+ * Possible values and their meanings:
+ *
+ * WEBVI_CONFIG_TEMPLATE_PATH
+ * Set the base directory for the XSLT templates (char *)
+ *
+ * WEBVI_CONFIG_DEBUG
+ * If value is not "0", print debug output to stdin (char *)
+ *
+ * WEBVI_CONFIG_TIMEOUT_CALLBACK
+ * Set timeout callback function (webvi_timeout_callback)
+ *
+ * WEBVI_CONFIG_TIMEOUT_DATA
+ * Set user data which will passed as second argument of the timeout
+ * callback (void *)
+ *
+ * The strings (char * arguments) are copied to the library (the user
+ * can free their original copy).
+ */
+LIBWEBVI_DLL_EXPORT WebviResult webvi_set_config(WebviCtx ctx, WebviConfig conf, ...);
+
+/*
+ * Creates a new download request.
+ *
+ * href is a URI of the resource that should be downloaded. Typically,
+ * the reference has been acquired from a previously downloaded menu.
+ * A special constant "wvt://mainmenu" can be used to download the
+ * mainmenu.
+ *
+ * The return value is a handle to the newly created request. Value
+ * WEBVI_INVALID_HANDLE indicates an error.
+ *
+ * The request is initialized but the actual network transfer is not
+ * started. You can set up additional configuration options on the
+ * handle using webvi_set_opt() before starting the handle with
+ * webvi_start_handle().
+ */
+LIBWEBVI_DLL_EXPORT WebviHandle webvi_new_request(WebviCtx ctx, const char *href);
+
+/*
+ * Starts the transfer on request h. The transfer one or more sockets
+ * whose file descriptors are returned by webvi_fdset(). The actual
+ * transfer is done during webvi_perform() calls.
+ */
+LIBWEBVI_DLL_EXPORT WebviResult webvi_start_request(WebviCtx ctx, WebviHandle h);
+
+/*
+ * Requests that the transfer on request h shoud be aborted. After the
+ * library has actually finished aborting the transfer, the handle h
+ * is returned by webvi_get_message() with non-zero status code.
+ */
+LIBWEBVI_DLL_EXPORT WebviResult webvi_stop_request(WebviCtx ctx, WebviHandle h);
+
+/*
+ * Frees resources associated with request h. The handle can not be
+ * used after this call. If the handle is still in the middle of a
+ * transfer, the transfer is forcefully aborted.
+ */
+LIBWEBVI_DLL_EXPORT WebviResult webvi_delete_request(WebviCtx ctx, WebviHandle h);
+
+/*
+ * Sets configuration options that changes behaviour of the handle.
+ * opt is one of the values of WebviOption enum as indicated below.
+ * The fourth parameter sets the value of the specified option. Its
+ * type depends on opt as discussed below.
+ *
+ * Possible values for opt:
+ *
+ * WEBVIOPT_WRITEFUNC
+ *
+ * Set the callback function that shall be called when data is read
+ * from the network. The fourth parameter is a pointer to the callback
+ * function
+ *
+ * ssize_t (*webvi_callback)(const char *, size_t, void *).
+ *
+ * When the function is called, the first parameter is a pointer to
+ * the incoming data, the second parameters is the size of the
+ * incoming data block in bytes, and the third parameter is a pointer
+ * to user's data structure can be set by WEBVIOPT_WRITEDATA option.
+ *
+ * The callback function should return the number of bytes is
+ * processed. If this differs from the size of the incoming data
+ * block, it indicates that an error occurred and the transfer will be
+ * aborted.
+ *
+ * If write callback has not been set (or if it is set to NULL) the
+ * incoming data is printed to stdout.
+ *
+ * WEBVIOPT_WRITEDATA
+ *
+ * Sets the value that will be passed to the write callback. The
+ * fourth parameter is of type void *.
+ *
+ * WEBVIOPT_READFUNC
+ *
+ * Set the callback function that shall be called when data is to be
+ * send to network. The fourth parameter is a pointer to the callback
+ * function
+ *
+ * ssize_t (*webvi_callback)(const char *, size_t, void *)
+ *
+ * The first parameter is a pointer to a buffer where the data that is
+ * to be sent should be written. The second parameter is the maximum
+ * size of the buffer. The thirs parameter is a pointer to user data
+ * set with WEBVIOPT_READDATA.
+ *
+ * The return value should be the number of bytes actually written to
+ * the buffer. If the return value is -1, the transfer is aborted.
+ *
+ * WEBVIOPT_READDATA
+ *
+ * Sets the value that will be passed to the read callback. The
+ * fourth parameter is of type void *.
+ *
+ */
+LIBWEBVI_DLL_EXPORT WebviResult webvi_set_opt(WebviCtx ctx, WebviHandle h, WebviOption opt, ...);
+
+/*
+ * Get information specific to a WebviHandle. The value will be
+ * written to the memory location pointed by the third argument. The
+ * type of the pointer depends in the second parameter as discused
+ * below.
+ *
+ * Available information:
+ *
+ * WEBVIINFO_URL
+ *
+ * Receive URL. The third parameter must be a pointer to char *. The
+ * caller must free() the memory.
+ *
+ * WEBVIINFO_CONTENT_LENGTH
+ *
+ * Receive the value of Content-length field, or -1 if the size is
+ * unknown. The third parameter must be a pointer to long.
+ *
+ * WEBVIINFO_CONTENT_TYPE
+ *
+ * Receive the Content-type string. The returned value is NULL, if the
+ * Content-type is unknown. The third parameter must be a pointer to
+ * char *. The caller must free() the memory.
+ *
+ * WEBVIINFO_STREAM_TITLE
+ *
+ * Receive stream title. The returned value is NULL, if title is
+ * unknown. The third parameter must be a pointer to char *. The
+ * caller must free() the memory.
+ *
+ */
+LIBWEBVI_DLL_EXPORT WebviResult webvi_get_info(WebviCtx ctx, WebviHandle h, WebviInfo info, ...);
+
+/*
+ * Get active file descriptors in use by the library. The file
+ * descriptors that should be waited for reading, writing or
+ * exceptions are returned in read_fd_set, write_fd_set and
+ * exc_fd_set, respectively. The fd_sets are not cleared, but the new
+ * file descriptors are added to them. max_fd will contain the highest
+ * numbered file descriptor that was returned in one of the fd_sets.
+ *
+ * One should wait for action in one of the file descriptors returned
+ * by this function using select(), poll() or similar system call,
+ * and, after seeing action on a file descriptor, call webvi_perform
+ * on that descriptor.
+ */
+LIBWEBVI_DLL_EXPORT WebviResult webvi_fdset(WebviCtx ctx, fd_set *readfd, fd_set *writefd, fd_set *excfd, int *max_fd);
+
+/*
+ * Perform input or output action on a file descriptor.
+ *
+ * activefd is a file descriptor that was returned by an earlier call
+ * to webvi_fdset and has been signalled to be ready by select() or
+ * similar function. ev_bitmask should be OR'ed combination of
+ * WEBVI_SELECT_READ, WEBVI_SELECT_WRITE, WEBVI_SELECT_EXCEPTION to
+ * indicate that activefd has been signalled to be ready for reading,
+ * writing or being in exception state, respectively. ev_bitmask can
+ * also set to WEBVI_SELECT_CHECK which means that the state is
+ * checked internally. On return, running_handles will contain the
+ * number of still active file descriptors.
+ *
+ * If a timeout occurs before any file descriptor becomes ready, this
+ * function should be called with sockfd set to WEBVI_SELECT_TIMEOUT
+ * and ev_bitmask set to WEBVI_SELECT_CHECK.
+ */
+LIBWEBVI_DLL_EXPORT WebviResult webvi_perform(WebviCtx ctx, int sockfd, int ev_bitmask, long *running_handles);
+
+
+LIBWEBVI_DLL_EXPORT int webvi_process_some(WebviCtx ctx, int timeout_seconds);
+
+
+/*
+ * Return the next message from the message queue. Currently the only
+ * message, WEBVIMSG_DONE, indicates that a transfer on a handle has
+ * finished. The number of messages remaining in the queue after this
+ * call is written to remaining_messages. The pointers in the returned
+ * WebviMsg point to handle's internal buffers and is valid until the
+ * next call to webvi_get_message(). The caller should not free the
+ * returned WebviMsg. The return value is NULL if there is no messages
+ * in the queue.
+ */
+LIBWEBVI_DLL_EXPORT WebviMsg *webvi_get_message(WebviCtx ctx, int *remaining_messages);
+
+#ifdef __cplusplus
+}
+#endif
+
+
+#endif
diff --git a/src/libwebvi/link.c b/src/libwebvi/link.c
new file mode 100644
index 0000000..501e70a
--- /dev/null
+++ b/src/libwebvi/link.c
@@ -0,0 +1,86 @@
+#include <stdlib.h>
+#include <glib.h>
+#include "link.h"
+
+struct Link {
+ gchar *href;
+ gchar *title;
+ LinkActionType type;
+};
+
+struct LinkAction {
+ LinkActionType type;
+ gchar *command;
+};
+
+struct Link *link_create(const char *href, const char *title, LinkActionType type) {
+ struct Link *self = malloc(sizeof(struct Link));
+ self->href = g_strdup(href ? href : "");
+ self->title = g_strdup(title ? title : "");
+ self->type = type;
+ return self;
+}
+
+const char *link_get_href(const struct Link *self) {
+ return self->href;
+}
+
+const char *link_get_title(const struct Link *self) {
+ return self->title;
+}
+
+LinkActionType link_get_type(const struct Link *self) {
+ return self->type;
+}
+
+void link_delete(struct Link *self) {
+ g_free(self->href);
+ g_free(self->title);
+ free(self);
+}
+
+void g_free_link(gpointer data) {
+ link_delete((struct Link *)data);
+}
+
+struct LinkAction *link_action_create(LinkActionType type, const char *command) {
+ struct LinkAction *self = malloc(sizeof(struct LinkAction));
+ self->type = type;
+ self->command = g_strdup(command ? command : "");
+ return self;
+}
+
+LinkActionType link_action_get_type(const struct LinkAction *self) {
+ return self->type;
+}
+
+const char *link_action_get_command(const struct LinkAction *self) {
+ return self->command;
+}
+
+void link_action_delete(struct LinkAction *self) {
+ if (self) {
+ g_free(self->command);
+ free(self);
+ }
+}
+
+struct ActionTypeMessage {
+ LinkActionType type;
+ const char *message;
+};
+
+const char *link_action_type_to_string(LinkActionType atype) {
+ static struct ActionTypeMessage messages[] =
+ {{LINK_ACTION_PARSE, "regular link"},
+ {LINK_ACTION_STREAM_LIBQUVI, "stream"},
+ {LINK_ACTION_EXTERNAL_COMMAND, "external command"}};
+
+ for (int i=0; i<(sizeof(messages)/sizeof(messages[0])); i++) {
+ if (atype == messages[i].type) {
+ return messages[i].message;
+ }
+ }
+
+ return "???";
+}
diff --git a/src/libwebvi/link.h b/src/libwebvi/link.h
new file mode 100644
index 0000000..0ddc05c
--- /dev/null
+++ b/src/libwebvi/link.h
@@ -0,0 +1,45 @@
+/*
+ * link.h
+ *
+ * Copyright (c) 2013 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __LINK_H
+#define __LINK_H
+
+typedef enum {
+ LINK_ACTION_PARSE,
+ LINK_ACTION_STREAM_LIBQUVI,
+ LINK_ACTION_EXTERNAL_COMMAND
+} LinkActionType;
+
+typedef struct Link Link;
+typedef struct LinkAction LinkAction;
+
+struct Link *link_create(const char *href, const char *title, LinkActionType type);
+const char *link_get_href(const struct Link *self);
+const char *link_get_title(const struct Link *self);
+LinkActionType link_get_type(const struct Link *self);
+void link_delete(struct Link *self);
+void g_free_link(gpointer link);
+
+struct LinkAction *link_action_create(LinkActionType type, const char *command);
+LinkActionType link_action_get_type(const struct LinkAction *self);
+const char *link_action_get_command(const struct LinkAction *self);
+void link_action_delete(struct LinkAction *self);
+const char *link_action_type_to_string(LinkActionType atype);
+
+#endif // __LINK_H
diff --git a/src/libwebvi/linkextractor.c b/src/libwebvi/linkextractor.c
new file mode 100644
index 0000000..d683df6
--- /dev/null
+++ b/src/libwebvi/linkextractor.c
@@ -0,0 +1,138 @@
+#include <string.h>
+#ifdef HAVE_TIDY_ULONG_VERSION
+#define __USE_MISC
+#include <sys/types.h>
+#undef __USE_MISC
+#endif
+#include <tidy/tidy.h>
+#include <tidy/buffio.h>
+#include "linkextractor.h"
+#include "urlutils.h"
+
+#define MENU_HEADER "<?xml version=\"1.0\"?><wvmenu>"
+#define MENU_FOOTER "</wvmenu>"
+
+struct LinkExtractor {
+ const LinkTemplates *link_templates;
+ TidyBuffer html_buffer;
+ gchar *baseurl;
+};
+
+static GPtrArray *extract_links(const LinkExtractor *self, TidyDoc tdoc);
+static void free_link(gpointer p);
+static void get_links_recursively(TidyDoc tdoc,
+ TidyNode node,
+ const LinkTemplates *link_templates,
+ const gchar *baseurl,
+ GPtrArray *links_found);
+static void getTextContent(TidyDoc tdoc, TidyNode node, TidyBuffer* buf);
+
+LinkExtractor *link_extractor_create(const LinkTemplates *link_templates, const gchar *baseurl) {
+ LinkExtractor *extractor;
+ extractor = malloc(sizeof(LinkExtractor));
+ memset(extractor, 0, sizeof(LinkExtractor));
+ extractor->link_templates = link_templates;
+ tidyBufInit(&extractor->html_buffer);
+ extractor->baseurl = baseurl ? g_strdup(baseurl) : g_strdup("");
+ return extractor;
+}
+
+void link_extractor_delete(LinkExtractor *self) {
+ if (self) {
+ tidyBufFree(&self->html_buffer);
+ g_free(self->baseurl);
+ free(self);
+ }
+}
+
+void link_extractor_append(LinkExtractor *self, const char *buf, size_t len) {
+ tidyBufAppend(&self->html_buffer, (void *)buf, len);
+}
+
+GPtrArray *link_extractor_get_links(LinkExtractor *self) {
+ GPtrArray *links = NULL;
+ TidyDoc tdoc;
+ int err;
+ TidyBuffer errbuf; // swallow errors here instead of printing to stderr
+
+ tdoc = tidyCreate();
+ tidyOptSetBool(tdoc, TidyForceOutput, yes);
+ tidyOptSetInt(tdoc, TidyWrapLen, 4096);
+ tidyBufInit(&errbuf);
+ tidySetErrorBuffer(tdoc, &errbuf);
+
+ err = tidyParseBuffer(tdoc, &self->html_buffer);
+ if (err >= 0) {
+ err = tidyCleanAndRepair(tdoc);
+ if ( err >= 0 ) {
+ links = extract_links(self, tdoc);
+ }
+ }
+
+ tidyBufFree(&errbuf);
+ tidyRelease(tdoc);
+
+ return links;
+}
+
+GPtrArray *extract_links(const LinkExtractor *self, TidyDoc tdoc) {
+ GPtrArray *links = g_ptr_array_new_full(0, free_link);
+ TidyNode root = tidyGetBody(tdoc);
+ get_links_recursively(tdoc, root, self->link_templates, self->baseurl, links);
+ return links;
+}
+
+void get_links_recursively(TidyDoc tdoc, TidyNode node,
+ const LinkTemplates *link_templates,
+ const gchar *baseurl,
+ GPtrArray *links_found) {
+ TidyNode child;
+ for (child = tidyGetChild(node); child; child = tidyGetNext(child)) {
+ if (tidyNodeIsA(child)) {
+ TidyAttr href_attr = tidyAttrGetById(child, TidyAttr_HREF);
+ ctmbstr href = tidyAttrValue(href_attr);
+ if (href && *href != '\0' && href[strlen(href)-1] != '#') {
+ gchar *absolute_href = relative_url_to_absolute(baseurl, href);
+ const LinkAction *action = \
+ link_templates_get_action(link_templates, absolute_href);
+ if (action) {
+ TidyBuffer titlebuf;
+ tidyBufInit(&titlebuf);
+ getTextContent(tdoc, child, &titlebuf);
+ tidyBufPutByte(&titlebuf, '\0');
+ gchar *title = g_strdup((const gchar*)titlebuf.bp);
+ g_strstrip(title);
+ LinkActionType type = link_action_get_type(action);
+ Link *link = link_create(absolute_href, title, type);
+ g_ptr_array_add(links_found, link);
+ g_free(title);
+ tidyBufFree(&titlebuf);
+ }
+ g_free(absolute_href);
+ }
+ } else {
+ TidyNodeType node_type = tidyNodeGetType(node);
+ if (node_type == TidyNode_Root || node_type == TidyNode_Start) {
+ get_links_recursively(tdoc, child, link_templates, baseurl, links_found);
+ }
+ }
+ }
+}
+
+void getTextContent(TidyDoc tdoc, TidyNode node, TidyBuffer* buf) {
+ if (tidyNodeGetType(node) == TidyNode_Text) {
+ TidyBuffer content;
+ tidyBufInit(&content);
+ tidyNodeGetValue(tdoc, node, &content);
+ tidyBufAppend(buf, content.bp, content.size);
+ } else {
+ TidyNode child;
+ for (child = tidyGetChild(node); child; child = tidyGetNext(child)) {
+ getTextContent(tdoc, child, buf);
+ }
+ }
+}
+
+void free_link(gpointer p) {
+ link_delete((Link *)p);
+}
diff --git a/src/libwebvi/linkextractor.h b/src/libwebvi/linkextractor.h
new file mode 100644
index 0000000..770b62f
--- /dev/null
+++ b/src/libwebvi/linkextractor.h
@@ -0,0 +1,34 @@
+/*
+ * linkextractor.h
+ *
+ * Copyright (c) 2013 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __LINKEXTRACTOR_H
+#define __LINKEXTRACTOR_H
+
+#include <glib.h>
+#include "linktemplates.h"
+
+typedef struct LinkExtractor LinkExtractor;
+
+LinkExtractor *link_extractor_create(const LinkTemplates *link_templates,
+ const gchar *baseurl);
+void link_extractor_delete(LinkExtractor *link_extractor);
+void link_extractor_append(LinkExtractor *link_extractor, const char *buf, size_t len);
+GPtrArray *link_extractor_get_links(LinkExtractor *link_extractor);
+
+#endif // __LINKEXTRACTOR_H
diff --git a/src/libwebvi/linktemplates.c b/src/libwebvi/linktemplates.c
new file mode 100644
index 0000000..a193df3
--- /dev/null
+++ b/src/libwebvi/linktemplates.c
@@ -0,0 +1,172 @@
+#include <string.h>
+#include <glib.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include "linktemplates.h"
+#include "link.h"
+
+#define STREAM_LIBQUVI_SELECTOR "stream:libquvi"
+#define STREAM_LIBQUVI_SELECTOR_LEN strlen(STREAM_LIBQUVI_SELECTOR)
+#define STREAM_SELECTOR "stream:"
+#define STREAM_SELECTOR_LEN strlen(STREAM_SELECTOR)
+#define EXT_CMD_SELECTOR "bin:"
+#define EXT_CMD_SELECTOR_LEN strlen(EXT_CMD_SELECTOR)
+
+struct LinkTemplates {
+ GPtrArray *matcher;
+};
+
+struct LinkMatcher {
+ GRegex *pattern;
+ LinkAction *action;
+};
+
+static void free_link_matcher(gpointer link);
+static struct LinkMatcher *parse_line(const char *line);
+static LinkAction *parse_action(const gchar *actionstr);
+
+LinkTemplates *link_templates_create() {
+ LinkTemplates *config = malloc(sizeof(LinkTemplates));
+ if (!config)
+ return NULL;
+ memset(config, 0, sizeof(LinkTemplates));
+ config->matcher = g_ptr_array_new_with_free_func(free_link_matcher);
+ return config;
+}
+
+void link_templates_delete(LinkTemplates *conf) {
+ g_ptr_array_free(conf->matcher, TRUE);
+ free(conf);
+}
+
+void link_templates_load(LinkTemplates *conf, const char *filename) {
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG,
+ "Loading matchers from %s", filename);
+ FILE *file = fopen(filename, "r");
+ if (!file) {
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
+ "Failed to read file %s", filename);
+ return;
+ }
+
+ char line[1024];
+ while (fgets(line, sizeof line, file)) {
+ struct LinkMatcher *link = parse_line(line);
+ if (link) {
+ g_ptr_array_add(conf->matcher, link);
+ }
+ }
+
+ fclose(file);
+}
+
+struct LinkMatcher *parse_line(const char *line) {
+ if (!line)
+ return NULL;
+
+ const char *p = line;
+ while (*p == ' ')
+ p++;
+
+ if (*p == '\0' || *p == '#' || *p == '\n' || *p == '\r')
+ return NULL;
+
+ const char *end = line + strlen(line);
+ while ((end-1 >= p) &&
+ (end[-1] == ' ' || end[-1] == '\r' || end[-1] == '\n' || end[-1] == '\t'))
+ end--;
+
+ if (end <= p)
+ return NULL;
+
+ const char *tab = memchr(p, '\t', end-p);
+ gchar *pattern;
+ LinkAction *action;
+ if (tab && tab < end) {
+ pattern = g_strndup(p, tab-p);
+ const char *cmdstart = tab+1;
+ gchar *action_field = g_strndup(cmdstart, end-cmdstart);
+ action = parse_action(action_field);
+ g_free(action_field);
+ } else {
+ pattern = g_strndup(p, end-p);
+ action = link_action_create(LINK_ACTION_PARSE, NULL);
+ }
+
+ if (!action) {
+ g_free(pattern);
+ return NULL;
+ }
+
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG,
+ "Compiling pattern %s %s %s", pattern,
+ link_action_type_to_string(link_action_get_type(action)),
+ link_action_get_command(action));
+
+ struct LinkMatcher *matcher = malloc(sizeof(struct LinkMatcher));
+ GError *err = NULL;
+ matcher->pattern = g_regex_new(pattern, G_REGEX_OPTIMIZE, 0, &err);
+ matcher->action = action;
+
+ if (err) {
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
+ "Error compiling pattern %s: %s", pattern, err->message);
+ g_error_free(err);
+ free_link_matcher(matcher);
+ matcher = NULL;
+ }
+
+ g_free(pattern);
+ return matcher;
+}
+
+LinkAction *parse_action(const gchar *actionstr) {
+ if (!actionstr)
+ return NULL;
+
+ if (*actionstr == '\0') {
+ return link_action_create(LINK_ACTION_PARSE, NULL);
+ } else if (strcmp(actionstr, STREAM_LIBQUVI_SELECTOR) == 0) {
+ return link_action_create(LINK_ACTION_STREAM_LIBQUVI, NULL);
+ } else if (strncmp(actionstr, EXT_CMD_SELECTOR, EXT_CMD_SELECTOR_LEN) == 0) {
+ const gchar *command = actionstr + EXT_CMD_SELECTOR_LEN;
+ return link_action_create(LINK_ACTION_EXTERNAL_COMMAND, command);
+ } else if (strncmp(actionstr, STREAM_SELECTOR, STREAM_SELECTOR_LEN) == 0) {
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
+ "Unknown streamer %s in link template file", actionstr);
+ return NULL;
+ } else {
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
+ "Invalid action %s in link template file", actionstr);
+ return NULL;
+ }
+}
+
+const LinkAction *link_templates_get_action(const LinkTemplates *conf,
+ const char *url)
+{
+ for (int i=0; i<link_templates_size(conf); i++) {
+ struct LinkMatcher *matcher = g_ptr_array_index(conf->matcher, i);
+ if (g_regex_match(matcher->pattern, url, 0, NULL))
+ {
+ return matcher->action;
+ }
+ }
+
+ return NULL;
+}
+
+int link_templates_size(const LinkTemplates *conf) {
+ return (int)conf->matcher->len;
+}
+
+void free_link_matcher(gpointer ptr) {
+ if (ptr) {
+ struct LinkMatcher *matcher = (struct LinkMatcher *)ptr;
+ if (matcher->pattern)
+ g_regex_unref(matcher->pattern);
+ if (matcher->action)
+ link_action_delete(matcher->action);
+ free(matcher);
+ }
+}
diff --git a/src/libwebvi/linktemplates.h b/src/libwebvi/linktemplates.h
new file mode 100644
index 0000000..1c184cc
--- /dev/null
+++ b/src/libwebvi/linktemplates.h
@@ -0,0 +1,34 @@
+/*
+ * linktemplates.h
+ *
+ * Copyright (c) 2013 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __LINKTEMPLATES_H
+#define __LINKTEMPLATES_H
+
+#include "link.h"
+
+typedef struct LinkTemplates LinkTemplates;
+
+LinkTemplates *link_templates_create();
+void link_templates_delete(LinkTemplates *conf);
+void link_templates_load(LinkTemplates *conf, const char *filename);
+int link_templates_size(const LinkTemplates *conf);
+const struct LinkAction *link_templates_get_action(const LinkTemplates *conf,
+ const char *url);
+
+#endif // __LINKTEMPLATES_H
diff --git a/src/libwebvi/mainmenu.c b/src/libwebvi/mainmenu.c
new file mode 100644
index 0000000..df08dce
--- /dev/null
+++ b/src/libwebvi/mainmenu.c
@@ -0,0 +1,120 @@
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <libxml/tree.h>
+#include <libxml/parser.h>
+#include "mainmenu.h"
+#include "menubuilder.h"
+
+static GPtrArray *load_websites(const char *path);
+static gint title_cmp(gconstpointer a, gconstpointer b);
+static gchar *get_site_title(gchar *sitemenu, gsize sitemenu_len);
+
+char *build_mainmenu(const char *path) {
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "building main menu %s", path);
+
+ MenuBuilder *menu_builder = menu_builder_create();
+ menu_builder_set_title(menu_builder, "Select video source");
+
+ GPtrArray *websites = load_websites(path);
+ menu_builder_append_link_list(menu_builder, websites);
+ char *menu = menu_builder_to_string(menu_builder);
+
+ g_ptr_array_free(websites, TRUE);
+ menu_builder_delete(menu_builder);
+ return menu;
+}
+
+/*
+ * Load known websites from the given directory.
+ *
+ * Traverses each subdirectory looking for file called menu.xml. If
+ * found, reads the file to find site title. Returns an array of
+ * titles and wvt references. The caller must call g_ptr_array_free()
+ * on the returned array (the content of the array will be freed
+ * automatically).
+ */
+GPtrArray *load_websites(const char *path) {
+ GPtrArray *websites = g_ptr_array_new_with_free_func(g_free_link);
+
+ GDir *dir = g_dir_open(path, 0, NULL);
+ if (!dir)
+ return websites;
+
+ const gchar *dirname;
+ while ((dirname = g_dir_read_name(dir))) {
+ gchar *menudir = g_strconcat(path, "/", dirname, NULL);
+ if (g_file_test(menudir, G_FILE_TEST_IS_DIR)) {
+ gchar *menufile = g_strconcat(menudir, "/menu.xml", NULL);
+
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "processing website menu %s", menufile);
+
+ gchar *sitemenu = NULL;
+ gsize sitemenu_len;
+ if (g_file_get_contents(menufile, &sitemenu, &sitemenu_len, NULL)) {
+ gchar *title = get_site_title(sitemenu, sitemenu_len);
+ if (!title) {
+ title = g_strdup(dirname);
+ }
+
+ gchar *href = g_strconcat("wvt://", menufile, NULL);
+ Link *menuitem = link_create(href, title, LINK_ACTION_PARSE);
+ g_ptr_array_add(websites, menuitem);
+ g_free(href);
+ g_free(title);
+ g_free(sitemenu);
+ }
+
+ g_free(menufile);
+ }
+
+ g_free(menudir);
+ }
+
+ g_dir_close(dir);
+
+ g_ptr_array_sort(websites, title_cmp);
+
+ return websites;
+}
+
+gint title_cmp(gconstpointer a, gconstpointer b) {
+ // a and b are pointers to Link pointers!
+ Link *link1 = *(Link **)a;
+ Link *link2 = *(Link **)b;
+
+ return g_ascii_strcasecmp(link_get_title(link1),
+ link_get_title(link2));
+}
+
+/*
+ * Parse the contents of website menu.xml and return site's title.
+ */
+gchar *get_site_title(gchar *menuxml, gsize menuxml_len) {
+ gchar *title = NULL;
+ xmlDocPtr doc = xmlReadMemory(menuxml, menuxml_len, "", NULL,
+ XML_PARSE_NOWARNING | XML_PARSE_NONET);
+ if (!doc)
+ return NULL;
+
+ xmlNode *root = xmlDocGetRootElement(doc);
+ xmlNode *node = root->children;
+ while (node) {
+ if (node->type == XML_ELEMENT_NODE &&
+ xmlStrEqual(node->name, BAD_CAST "title")) {
+ xmlChar *xmltitle = xmlNodeGetContent(node);
+ if (xmltitle) {
+ title = g_strdup((gchar *)xmltitle);
+ xmlFree(xmltitle);
+
+ break;
+ }
+ }
+
+ node = node->next;
+ }
+
+ xmlFreeDoc(doc);
+
+ return title;
+}
diff --git a/src/libwebvi/mainmenu.h b/src/libwebvi/mainmenu.h
new file mode 100644
index 0000000..b350a5f
--- /dev/null
+++ b/src/libwebvi/mainmenu.h
@@ -0,0 +1,27 @@
+/*
+ * mainmenu.h
+ *
+ * Copyright (c) 2013 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __MAINMENU_H
+#define __MAINMENU_H
+
+#include <glib.h>
+
+char *build_mainmenu(const char *path);
+
+#endif // __MAINMENU_H
diff --git a/src/libwebvi/menubuilder.c b/src/libwebvi/menubuilder.c
new file mode 100644
index 0000000..d2b54ce
--- /dev/null
+++ b/src/libwebvi/menubuilder.c
@@ -0,0 +1,99 @@
+#include <stdlib.h>
+#include <string.h>
+#include <libxml/tree.h>
+#include "menubuilder.h"
+
+struct MenuBuilder {
+ xmlDocPtr doc;
+ xmlNodePtr root;
+ xmlNodePtr ul_node;
+ xmlNodePtr title_node;
+};
+
+static void add_link_to_menu(gpointer data, gpointer instance);
+
+MenuBuilder *menu_builder_create() {
+ MenuBuilder *self = malloc(sizeof(MenuBuilder));
+ if (!self)
+ return NULL;
+ self->doc = xmlNewDoc(BAD_CAST "1.0");
+ self->root = xmlNewNode(NULL, BAD_CAST "wvmenu");
+ xmlDocSetRootElement(self->doc, self->root);
+ self->ul_node = xmlNewNode(NULL, BAD_CAST "ul");
+ xmlAddChild(self->root, self->ul_node);
+ self->title_node = NULL;
+ return self;
+}
+
+void menu_builder_set_title(MenuBuilder *self, const char *title) {
+ if (self->title_node) {
+ xmlUnlinkNode(self->title_node);
+ xmlFreeNode(self->title_node);
+ self->title_node = NULL;
+ }
+
+ self->title_node = xmlNewNode(NULL, BAD_CAST "title");
+ xmlNodeAddContent(self->title_node, BAD_CAST title);
+
+ if (self->root->children) {
+ xmlAddPrevSibling(self->root->children, self->title_node);
+ } else {
+ xmlAddChild(self->root, self->title_node);
+ }
+}
+
+char *menu_builder_to_string(MenuBuilder *self) {
+ xmlChar *buf;
+ int buflen;
+ char *menu;
+
+ xmlDocDumpMemoryEnc(self->doc, &buf, &buflen, "UTF-8");
+ menu = malloc(buflen+1);
+ strcpy(menu, (const char *)buf);
+ xmlFree(buf);
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "Menu:\n%s", menu);
+ return menu;
+}
+
+void menu_builder_append_link_list(MenuBuilder *self, GPtrArray *links) {
+ g_ptr_array_foreach(links, add_link_to_menu, self);
+}
+
+void add_link_to_menu(gpointer data, gpointer instance) {
+ MenuBuilder *menubuilder = (MenuBuilder *)instance;
+ Link *link = (Link *)data;
+ menu_builder_append_link(menubuilder, link);
+}
+
+void menu_builder_append_link(MenuBuilder *self, const Link *link) {
+ const char *class;
+ if (link_get_type(link) == LINK_ACTION_STREAM_LIBQUVI) {
+ class = "stream";
+ } else {
+ class = "webvi";
+ }
+ menu_builder_append_link_plain(self, link_get_href(link),
+ link_get_title(link), class);
+}
+
+void menu_builder_append_link_plain(MenuBuilder *self, const char *href,
+ const char *title, const char *class)
+{
+ xmlNodePtr li_node = xmlNewNode(NULL, BAD_CAST "li");
+ xmlNodePtr a_node = xmlNewNode(NULL, BAD_CAST "a");
+ if (title)
+ xmlNodeAddContent(a_node, BAD_CAST title);
+ if (href)
+ xmlNewProp(a_node, BAD_CAST "href", BAD_CAST href);
+ if (class)
+ xmlNewProp(a_node, BAD_CAST "class", BAD_CAST class);
+ xmlAddChild(li_node, a_node);
+ xmlAddChild(self->ul_node, li_node);
+}
+
+void menu_builder_delete(MenuBuilder *self) {
+ if (self) {
+ xmlFreeDoc(self->doc);
+ free(self);
+ }
+}
diff --git a/src/libwebvi/menubuilder.h b/src/libwebvi/menubuilder.h
new file mode 100644
index 0000000..37de6c2
--- /dev/null
+++ b/src/libwebvi/menubuilder.h
@@ -0,0 +1,37 @@
+/*
+ * menubuilder.h
+ *
+ * Copyright (c) 2013 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __MENUBUILDER_H
+#define __MENUBUILDER_H
+
+#include <glib.h>
+#include "link.h"
+
+typedef struct MenuBuilder MenuBuilder;
+
+MenuBuilder *menu_builder_create();
+void menu_builder_set_title(MenuBuilder *self, const char *title);
+char *menu_builder_to_string(MenuBuilder *self);
+void menu_builder_append_link_plain(MenuBuilder *self, const char *href,
+ const char *title, const char *class);
+void menu_builder_append_link(MenuBuilder *self, const Link *link);
+void menu_builder_append_link_list(MenuBuilder *self, GPtrArray *links);
+void menu_builder_delete(MenuBuilder *self);
+
+#endif // __MENUBUILDER_H
diff --git a/src/libwebvi/pipecomponent.c b/src/libwebvi/pipecomponent.c
new file mode 100644
index 0000000..a0743da
--- /dev/null
+++ b/src/libwebvi/pipecomponent.c
@@ -0,0 +1,689 @@
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <libxml/tree.h>
+#include <libxml/parser.h>
+#include "pipecomponent.h"
+#include "menubuilder.h"
+#include "linkextractor.h"
+#include "mainmenu.h"
+#include "webvicontext.h"
+#include "version.h"
+
+#define WEBVID_USER_AGENT "libwebvi/" LIBWEBVI_VERSION " libcurl/" LIBCURL_VERSION
+
+#define INITIALIZE_PIPE(pipetype, process, finish, delete) \
+ pipetype *self = malloc(sizeof(pipetype)); \
+ memset(self, 0, sizeof(pipetype)); \
+ pipe_component_initialize(&self->pipe_data, (process), (finish), (delete))
+
+#define INITIALIZE_PIPE_WITH_FDSET(pipetype, process, finish, delete, fdset, handle_socket) \
+ pipetype *self = malloc(sizeof(pipetype)); \
+ memset(self, 0, sizeof(pipetype)); \
+ pipe_component_initialize_fdset(&self->pipe_data, (process), (finish), (delete), (fdset), (handle_socket))
+
+struct PipeDownloader {
+ PipeComponent pipe_data;
+ CURL *curl;
+ CURLM *curlmulti;
+};
+
+struct PipeLinkExtractor {
+ PipeComponent pipe_data;
+ LinkExtractor *link_extractor;
+};
+
+struct PipeCallbackWrapper {
+ PipeComponent pipe_data;
+ void *write_data;
+ void *finish_data;
+ ssize_t (*write_callback)(const char *, size_t, void *);
+ void (*finish_callback)(RequestState, void *);
+};
+
+struct PipeMainMenuDownloader {
+ PipeComponent pipe_data;
+ const WebviContext *context; /* borrowed reference */
+};
+
+struct PipeExternalDownloader {
+ PipeComponent pipe_data;
+ gchar *url;
+ int fd;
+};
+
+struct PipeLocalFile {
+ PipeComponent pipe_data;
+ gchar *filename;
+ int fd;
+};
+
+struct PipeLibquvi {
+ PipeComponent pipe_data;
+ gchar *url;
+ GPid pid;
+ gint quvi_output;
+ xmlParserCtxtPtr parser;
+};
+
+static void pipe_component_delete(PipeComponent *self);
+
+static gboolean pipe_link_extractor_append(PipeComponent *instance, char *buf, size_t len);
+static void pipe_link_extractor_finished(PipeComponent *instance, RequestState state);
+static void pipe_link_extractor_delete(PipeComponent *instance);
+
+static gboolean pipe_callback_wrapper_process(PipeComponent *instance,
+ char *buf, size_t len);
+static void pipe_callback_wrapper_finished(PipeComponent *instance,
+ RequestState state);
+static void pipe_callback_wrapper_delete(PipeComponent *instance);
+
+static void pipe_downloader_finished(PipeComponent *instance, RequestState state);
+static void pipe_downloader_delete(PipeComponent *instance);
+static size_t curl_write_wrapper(char *ptr, size_t size, size_t nmemb, void *userdata);
+
+static void pipe_mainmenu_downloader_delete(PipeComponent *instance);
+
+static void pipe_local_file_fdset(PipeComponent *instance, fd_set *readfd,
+ fd_set *writefd, fd_set *excfd, int *maxfd);
+static gboolean pipe_local_file_handle_socket(PipeComponent *instance,
+ int fd, int bitmask);
+static void pipe_local_file_delete(PipeComponent *instance);
+
+static void pipe_external_downloader_fdset(PipeComponent *instance,
+ fd_set *readfd, fd_set *writefd,
+ fd_set *excfd, int *maxfd);
+static gboolean pipe_external_downloader_handle_socket(
+ PipeComponent *instance, int fd, int bitmask);
+static void pipe_external_downloader_delete(PipeComponent *instance);
+
+static void pipe_libquvi_fdset(PipeComponent *instance, fd_set *readfd,
+ fd_set *writefd, fd_set *excfd, int *maxfd);
+static gboolean pipe_libquvi_handle_socket(PipeComponent *instance,
+ int fd, int bitmask);
+static gboolean pipe_libquvi_parse(PipeComponent *instance, char *buf, size_t len);
+static void pipe_libquvi_finished(PipeComponent *instance, RequestState state);
+static xmlChar *quvi_xml_get_stream_url(xmlDoc *doc);
+static xmlChar *quvi_xml_get_stream_title(xmlDoc *doc);
+static void pipe_libquvi_delete(PipeComponent *instance);
+
+static CURL *start_curl(const char *url, CURLM *curlmulti,
+ PipeComponent *instance);
+static void append_to_fdset(int fd, fd_set *fdset, int *maxfd);
+static gboolean read_from_fd_to_pipe(PipeComponent *instance,
+ int *instance_fd_ptr, int fd, int bitmask);
+
+
+void pipe_component_initialize(PipeComponent *self,
+ gboolean (*process_cb)(PipeComponent *, char *, size_t),
+ void (*done_cb)(PipeComponent *, RequestState),
+ void (*delete_cb)(PipeComponent *))
+{
+ g_assert(self);
+ g_assert(delete_cb);
+
+ memset(self, 0, sizeof(PipeComponent));
+ self->process = process_cb;
+ self->finished = done_cb;
+ self->delete = delete_cb;
+ self->state = WEBVISTATE_NOT_FINISHED;
+}
+
+void pipe_component_initialize_fdset(PipeComponent *self,
+ gboolean (*process_cb)(PipeComponent *, char *, size_t),
+ void (*done_cb)(PipeComponent *, RequestState),
+ void (*delete_cb)(PipeComponent *),
+ void (*fdset_cb)(PipeComponent *, fd_set *, fd_set *, fd_set *, int *),
+ gboolean (*handle_socket_cb)(PipeComponent *, int, int))
+{
+ pipe_component_initialize(self, process_cb, done_cb, delete_cb);
+ self->fdset = fdset_cb;
+ self->handle_socket = handle_socket_cb;
+}
+
+void pipe_component_set_next(PipeComponent *self, PipeComponent *next) {
+ g_assert(!self->next);
+ self->next = next;
+}
+
+void pipe_component_append(PipeComponent *self, char *buf, size_t length) {
+ if (self->state == WEBVISTATE_NOT_FINISHED) {
+ gboolean propagate = TRUE;
+ if (self->process)
+ propagate = self->process(self, buf, length);
+ if (propagate && self->next)
+ pipe_component_append(self->next, buf, length);
+ }
+}
+
+void pipe_component_finished(PipeComponent *self, RequestState state) {
+ if (self->state == WEBVISTATE_NOT_FINISHED) {
+ self->state = state;
+ if (self->finished)
+ self->finished(self, state);
+ if (self->next)
+ pipe_component_finished(self->next, state);
+ }
+}
+
+RequestState pipe_component_get_state(const PipeComponent *self) {
+ return self->state;
+}
+
+void pipe_fdset(PipeComponent *head, fd_set *readfd,
+ fd_set *writefd, fd_set *excfd, int *max_fd)
+{
+ PipeComponent *component = head;
+ PipeComponent *next;
+ while (component) {
+ next = component->next;
+ if (component->fdset) {
+ component->fdset(component, readfd, writefd, excfd, max_fd);
+ }
+ component = next;
+ }
+}
+
+gboolean pipe_handle_socket(PipeComponent *head, int sockfd, int ev_bitmask) {
+ PipeComponent *component = head;
+ PipeComponent *next;
+ while (component) {
+ next = component->next;
+ if (component->handle_socket) {
+ if (component->handle_socket(component, sockfd, ev_bitmask)) {
+ return TRUE;
+ }
+ }
+ component = next;
+ }
+ return FALSE;
+}
+
+void pipe_component_delete(PipeComponent *self) {
+ if (self && self->delete)
+ self->delete(self);
+}
+
+void pipe_delete_full(PipeComponent *head) {
+ PipeComponent *component = head;
+ PipeComponent *next;
+ while (component) {
+ next = component->next;
+ pipe_component_delete(component);
+ component = next;
+ }
+}
+
+
+/***** PipeLinkExtractor *****/
+
+
+PipeLinkExtractor *pipe_link_extractor_create(
+ const LinkTemplates *link_templates, const gchar *baseurl)
+{
+ INITIALIZE_PIPE(PipeLinkExtractor, pipe_link_extractor_append,
+ pipe_link_extractor_finished, pipe_link_extractor_delete);
+ self->link_extractor = link_extractor_create(link_templates, baseurl);
+ return self;
+}
+
+void pipe_link_extractor_delete(PipeComponent *instance) {
+ PipeLinkExtractor *self = (PipeLinkExtractor *)instance;
+ link_extractor_delete(self->link_extractor);
+ free(self);
+}
+
+gboolean pipe_link_extractor_append(PipeComponent *instance, char *buf, size_t len) {
+ PipeLinkExtractor *self = (PipeLinkExtractor *)instance;
+ link_extractor_append(self->link_extractor, buf, len);
+ return FALSE;
+}
+
+void pipe_link_extractor_finished(PipeComponent *instance,
+ RequestState state)
+{
+ PipeLinkExtractor *self = (PipeLinkExtractor *)instance;
+ if (state == WEBVISTATE_FINISHED_OK) {
+ GPtrArray *links = link_extractor_get_links(self->link_extractor);
+ MenuBuilder *menu_builder = menu_builder_create();
+ menu_builder_append_link_list(menu_builder, links);
+ char *menu = menu_builder_to_string(menu_builder);
+ if (self->pipe_data.next) {
+ pipe_component_append(self->pipe_data.next, menu, strlen(menu));
+ pipe_component_finished(self->pipe_data.next, state);
+ }
+ menu_builder_delete(menu_builder);
+ free(menu);
+ g_ptr_array_free(links, TRUE);
+ }
+}
+
+
+/***** PipeDownloader *****/
+
+
+PipeDownloader *pipe_downloader_create(const char *url, CURLM *curlmulti) {
+ INITIALIZE_PIPE(PipeDownloader, NULL, pipe_downloader_finished,
+ pipe_downloader_delete);
+ self->curl = start_curl(url, curlmulti, (PipeComponent *)self);
+ if (!self->curl) {
+ pipe_downloader_delete((PipeComponent *)self);
+ return NULL;
+ }
+ self->curlmulti = curlmulti;
+ return self;
+}
+
+void pipe_downloader_start(PipeDownloader *self) {
+ CURLMcode mcode = curl_multi_add_handle(self->curlmulti, self->curl);
+ if (mcode != CURLM_OK) {
+ pipe_component_finished(&self->pipe_data, WEBVISTATE_INTERNAL_ERROR);
+ }
+}
+
+size_t curl_write_wrapper(char *ptr, size_t size, size_t nmemb, void *userdata)
+{
+ PipeComponent *pipedata = (PipeComponent *)userdata;
+ if (pipe_component_get_state(pipedata) == WEBVISTATE_NOT_FINISHED) {
+ pipe_component_append(pipedata, ptr, size*nmemb);
+ return size*nmemb;
+ } else {
+ return 0;
+ }
+}
+
+void pipe_downloader_finished(PipeComponent *instance, RequestState state) {
+ PipeDownloader *self = (PipeDownloader *)instance;
+ curl_multi_remove_handle(self->curlmulti, self->curl);
+}
+
+void pipe_downloader_delete(PipeComponent *instance) {
+ PipeDownloader *self = (PipeDownloader *)instance;
+ if (self->pipe_data.state == WEBVISTATE_NOT_FINISHED) {
+ curl_multi_remove_handle(self->curlmulti, self->curl);
+ }
+ curl_easy_cleanup(self->curl);
+ free(instance);
+}
+
+
+/***** PipeMainMenuDownloader *****/
+
+
+PipeMainMenuDownloader *pipe_mainmenu_downloader_create(WebviContext *context) {
+ INITIALIZE_PIPE(PipeMainMenuDownloader, NULL, NULL,
+ pipe_mainmenu_downloader_delete);
+ self->context = context;
+ return self;
+}
+
+void pipe_mainmenu_downloader_start(PipeMainMenuDownloader *self) {
+ char *mainmenu = build_mainmenu(webvi_context_get_template_path(self->context));
+ if (!mainmenu) {
+ pipe_component_finished(&self->pipe_data, WEBVISTATE_INTERNAL_ERROR);
+ return;
+ }
+
+ pipe_component_append(&self->pipe_data, mainmenu, strlen(mainmenu));
+ pipe_component_finished(&self->pipe_data, WEBVISTATE_FINISHED_OK);
+
+ g_free(mainmenu);
+}
+
+void pipe_mainmenu_downloader_delete(PipeComponent *instance) {
+ PipeMainMenuDownloader *self = (PipeMainMenuDownloader *)instance;
+ free(self);
+}
+
+
+/***** PipeCallbackWrapper *****/
+
+
+PipeCallbackWrapper *pipe_callback_wrapper_create(
+ ssize_t (*write_callback)(const char *, size_t, void *),
+ void *writedata,
+ void (*finish_callback)(RequestState, void *),
+ void *finishdata)
+{
+ INITIALIZE_PIPE(PipeCallbackWrapper,
+ pipe_callback_wrapper_process,
+ pipe_callback_wrapper_finished,
+ pipe_callback_wrapper_delete);
+ self->write_data = writedata;
+ self->finish_data = finishdata;
+ self->write_callback = write_callback;
+ self->finish_callback = finish_callback;
+ return self;
+}
+
+gboolean pipe_callback_wrapper_process(PipeComponent *instance,
+ char *buf, size_t len)
+{
+ PipeCallbackWrapper *self = (PipeCallbackWrapper *)instance;
+ if (self->write_callback)
+ self->write_callback(buf, len, self->write_data);
+ return TRUE;
+}
+
+void pipe_callback_wrapper_finished(PipeComponent *instance,
+ RequestState state)
+{
+ PipeCallbackWrapper *self = (PipeCallbackWrapper *)instance;
+ if (self->finish_callback)
+ self->finish_callback(state, self->finish_data);
+}
+
+void pipe_callback_wrapper_delete(PipeComponent *instance) {
+ PipeCallbackWrapper *self = (PipeCallbackWrapper *)instance;
+ free(self);
+}
+
+PipeLocalFile *pipe_local_file_create(const gchar *filename) {
+ INITIALIZE_PIPE_WITH_FDSET(PipeLocalFile, NULL, NULL,
+ pipe_local_file_delete,
+ pipe_local_file_fdset,
+ pipe_local_file_handle_socket);
+ self->filename = g_strdup(filename);
+ self->fd = -1;
+ return self;
+}
+
+void pipe_local_file_start(PipeLocalFile *self) {
+ if (!self->filename) {
+ pipe_component_finished((PipeComponent *)self, WEBVISTATE_NOT_FOUND);
+ }
+
+ self->fd = open(self->filename, O_RDONLY);
+ if (self->fd == -1) {
+ pipe_component_finished((PipeComponent *)self, WEBVISTATE_NOT_FOUND);
+ }
+}
+
+void pipe_local_file_fdset(PipeComponent *instance, fd_set *readfd,
+ fd_set *writefd, fd_set *excfd, int *maxfd)
+{
+ PipeLocalFile *self = (PipeLocalFile *)instance;
+ append_to_fdset(self->fd, readfd, maxfd);
+}
+
+gboolean pipe_local_file_handle_socket(PipeComponent *instance, int fd, int bitmask) {
+ PipeLocalFile *self = (PipeLocalFile *)instance;
+ return read_from_fd_to_pipe(instance, &self->fd, fd, bitmask);
+}
+
+void pipe_local_file_delete(PipeComponent *instance) {
+ PipeLocalFile *self = (PipeLocalFile *)instance;
+ g_free(self->filename);
+ if (self->fd != -1)
+ close(self->fd);
+ free(self);
+}
+
+
+/***** PipeExternalDownloader *****/
+
+
+PipeExternalDownloader *pipe_external_downloader_create(const gchar *url,
+ const gchar *command)
+{
+ INITIALIZE_PIPE_WITH_FDSET(PipeExternalDownloader, NULL, NULL,
+ pipe_external_downloader_delete,
+ pipe_external_downloader_fdset,
+ pipe_external_downloader_handle_socket);
+ self->url = g_strdup(url);
+ self->fd = -1;
+
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG,
+ "external downloader:\nurl: %s\n%s", url, command);
+
+ return self;
+}
+
+void pipe_external_downloader_start(PipeExternalDownloader *self) {
+ // FIXME
+ pipe_component_finished((PipeComponent *)self, WEBVISTATE_INTERNAL_ERROR);
+}
+
+void pipe_external_downloader_fdset(PipeComponent *instance, fd_set *readfd,
+ fd_set *writefd, fd_set *excfd, int *maxfd)
+{
+ PipeExternalDownloader *self = (PipeExternalDownloader *)instance;
+ append_to_fdset(self->fd, readfd, maxfd);
+}
+
+gboolean pipe_external_downloader_handle_socket(PipeComponent *instance,
+ int fd, int bitmask)
+{
+ PipeExternalDownloader *self = (PipeExternalDownloader *)instance;
+ return read_from_fd_to_pipe(instance, &self->fd, fd, bitmask);
+}
+
+void pipe_external_downloader_delete(PipeComponent *instance) {
+ PipeExternalDownloader *self = (PipeExternalDownloader *)instance;
+ if (self->fd != -1)
+ close(self->fd);
+ g_free(self->url);
+ g_free(self);
+}
+
+
+/***** PipeLibquvi *****/
+
+
+PipeLibquvi *pipe_libquvi_create(const gchar *url) {
+ INITIALIZE_PIPE_WITH_FDSET(PipeLibquvi,
+ pipe_libquvi_parse,
+ pipe_libquvi_finished,
+ pipe_libquvi_delete,
+ pipe_libquvi_fdset,
+ pipe_libquvi_handle_socket);
+ self->url = g_strdup(url);
+ self->pid = -1;
+ self->quvi_output = -1;
+ return self;
+}
+
+void pipe_libquvi_start(PipeLibquvi *self) {
+ GError *error = NULL;
+ gchar *argv[] = {"quvi", "--xml", self->url};
+
+ g_spawn_async_with_pipes(NULL, argv, NULL,
+ G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL,
+ NULL, NULL, &self->pid, NULL, &self->quvi_output,
+ NULL, &error);
+ if (error) {
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
+ "Calling quvi failed: %s", error->message);
+ pipe_component_finished((PipeComponent *)self, WEBVISTATE_SUBPROCESS_FAILED);
+ }
+}
+
+void pipe_libquvi_fdset(PipeComponent *instance, fd_set *readfd,
+ fd_set *writefd, fd_set *excfd, int *maxfd)
+{
+ PipeLibquvi *self = (PipeLibquvi *)instance;
+ append_to_fdset(self->quvi_output, readfd, maxfd);
+}
+
+gboolean pipe_libquvi_handle_socket(PipeComponent *instance,
+ int fd, int bitmask)
+{
+ PipeLibquvi *self = (PipeLibquvi *)instance;
+ return read_from_fd_to_pipe(instance, &self->quvi_output, fd, bitmask);
+}
+
+gboolean pipe_libquvi_parse(PipeComponent *instance, char *buf, size_t len) {
+ PipeLibquvi *self = (PipeLibquvi *)instance;
+ if (!self->parser) {
+ self->parser = xmlCreatePushParserCtxt(NULL, NULL, buf, len, "quvioutput.xml");
+ g_assert(self->parser);
+ } else {
+ xmlParseChunk(self->parser, buf, len, 0);
+ }
+
+ return FALSE;
+}
+
+void pipe_libquvi_finished(PipeComponent *instance, RequestState state) {
+ if (state != WEBVISTATE_FINISHED_OK) {
+ pipe_component_finished(instance, state);
+ return;
+ }
+
+ PipeLibquvi *self = (PipeLibquvi *)instance;
+ if (!self->parser) {
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "No output from quvi!");
+ pipe_component_finished(instance, WEBVISTATE_SUBPROCESS_FAILED);
+ return;
+ }
+
+ xmlParseChunk(self->parser, NULL, 0, 1);
+ xmlDoc *doc = self->parser->myDoc;
+
+ xmlChar *dump;
+ int dumpLen;
+ xmlDocDumpMemory(doc, &dump, &dumpLen);
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "quvi output:\n%s", dump);
+ xmlFree(dump);
+ dump = NULL;
+
+ xmlChar *encoded_url = quvi_xml_get_stream_url(doc);
+ if (!encoded_url) {
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "No url in quvi output!");
+ pipe_component_finished(instance, WEBVISTATE_SUBPROCESS_FAILED);
+ return;
+ }
+
+ /* URLs in quvi XML output are encoded by curl_easy_escape */
+ char *url = curl_unescape((char *)encoded_url, strlen((char *)encoded_url));
+ xmlChar *title = quvi_xml_get_stream_title(doc);
+
+ MenuBuilder *menu_builder = menu_builder_create();
+ menu_builder_set_title(menu_builder, (char *)title);
+ menu_builder_append_link_plain(menu_builder, url, (char *)title, NULL);
+ char *menu = menu_builder_to_string(menu_builder);
+ if (self->pipe_data.next) {
+ pipe_component_append(self->pipe_data.next, menu, strlen(menu));
+ pipe_component_finished(self->pipe_data.next, state);
+ }
+
+ free(menu);
+ menu_builder_delete(menu_builder);
+ xmlFree(title);
+ curl_free(url);
+ xmlFree(encoded_url);
+}
+
+xmlChar *quvi_xml_get_stream_url(xmlDoc *doc) {
+ xmlNode *root = xmlDocGetRootElement(doc);
+ xmlNode *node = root->children;
+ while (node) {
+ if (xmlStrEqual(node->name, BAD_CAST "link")) {
+ xmlNode *link_child = node->children;
+ while (link_child) {
+ if (xmlStrEqual(link_child->name, BAD_CAST "url")) {
+ return xmlNodeGetContent(link_child);
+ }
+ link_child = link_child->next;
+ }
+ }
+ node = node->next;
+ }
+
+ return NULL;
+}
+
+xmlChar *quvi_xml_get_stream_title(xmlDoc *doc) {
+ xmlNode *root = xmlDocGetRootElement(doc);
+ xmlNode *node = root->children;
+ while (node) {
+ if (xmlStrEqual(node->name, BAD_CAST "page_title")) {
+ return xmlNodeGetContent(node);
+ }
+ node = node->next;
+ }
+
+ return NULL;
+}
+
+void pipe_libquvi_delete(PipeComponent *instance) {
+ PipeLibquvi *self = (PipeLibquvi *)instance;
+ if (self->quvi_output != -1) {
+ close(self->quvi_output);
+ }
+ if (self->pid != -1) {
+ g_spawn_close_pid(self->pid);
+ }
+ if (self->parser) {
+ xmlFreeParserCtxt(self->parser);
+ }
+ g_free(self->url);
+ free(self);
+}
+
+
+/***** Utility functions *****/
+
+
+CURL *start_curl(const char *url, CURLM *curlmulti, PipeComponent *instance) {
+ CURL *curl = curl_easy_init();
+ if (!curl) {
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "curl initialization failed");
+ return NULL;
+ }
+
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "Downloading %s", url);
+
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, WEBVID_USER_AGENT);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_wrapper);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, instance);
+ if (url)
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ curl_easy_setopt(curl, CURLOPT_PRIVATE, instance);
+
+ // FIXME: headers, cookies
+
+ return curl;
+}
+
+void append_to_fdset(int fd, fd_set *fdset, int *maxfd) {
+ if (fd != -1) {
+ FD_SET(fd, fdset);
+ if (fd > *maxfd)
+ *maxfd = fd;
+ }
+}
+
+gboolean read_from_fd_to_pipe(PipeComponent *instance, int *instance_fd_ptr,
+ int fd, int bitmask)
+{
+ const int buflen = 4096;
+ char buf[buflen];
+ ssize_t numbytes;
+
+ gboolean owned_socket = (fd == -1) || (fd == *instance_fd_ptr);
+ gboolean read_operation = ((bitmask & WEBVI_SELECT_READ) != 0) ||
+ (bitmask == WEBVI_SELECT_CHECK);
+
+ if (owned_socket && read_operation) {
+ numbytes = read(fd, buf, buflen);
+ if (numbytes < 0) {
+ /* error */
+ pipe_component_finished(instance, WEBVISTATE_IO_ERROR);
+ } else if (numbytes == 0) {
+ /* end of file */
+ pipe_component_finished(instance, WEBVISTATE_FINISHED_OK);
+ close(fd);
+ *instance_fd_ptr = -1;
+ } else {
+ pipe_component_append(instance, buf, numbytes);
+ }
+
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+}
diff --git a/src/libwebvi/pipecomponent.h b/src/libwebvi/pipecomponent.h
new file mode 100644
index 0000000..00a05ef
--- /dev/null
+++ b/src/libwebvi/pipecomponent.h
@@ -0,0 +1,95 @@
+/*
+ * pipecomponent.h
+ *
+ * Copyright (c) 2013 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __PIPECOMPONENT_H
+#define __PIPECOMPONENT_H
+
+#include <stdlib.h>
+#include <sys/select.h>
+#include <glib.h>
+#include <curl/curl.h>
+#include "libwebvi.h"
+
+typedef struct PipeComponent {
+ struct PipeComponent *next;
+ RequestState state;
+ gboolean (*process)(struct PipeComponent *self, char *, size_t);
+ void (*finished)(struct PipeComponent *self, RequestState state);
+ void (*delete)(struct PipeComponent *self);
+ void (*fdset)(struct PipeComponent *self, fd_set *readfd,
+ fd_set *writefd, fd_set *excfd, int *max_fd);
+ gboolean (*handle_socket)(struct PipeComponent *self, int fd, int ev_bitmask);
+} PipeComponent;
+
+struct WebviContext;
+struct LinkTemplates;
+
+typedef struct PipeDownloader PipeDownloader;
+typedef struct PipeLinkExtractor PipeLinkExtractor;
+typedef struct PipeCallbackWrapper PipeCallbackWrapper;
+typedef struct PipeMainMenuDownloader PipeMainMenuDownloader;
+typedef struct PipeLocalFile PipeLocalFile;
+typedef struct PipeExternalDownloader PipeExternalDownloader;
+typedef struct PipeLibquvi PipeLibquvi;
+
+void pipe_component_initialize(PipeComponent *self,
+ gboolean (*process_cb)(PipeComponent *, char *, size_t),
+ void (*done_cb)(PipeComponent *, RequestState state),
+ void (*delete_cb)(PipeComponent *));
+void pipe_component_initialize_fdset(PipeComponent *self,
+ gboolean (*process_cb)(PipeComponent *, char *, size_t),
+ void (*done_cb)(PipeComponent *, RequestState state),
+ void (*delete_cb)(PipeComponent *),
+ void (*fdset_cb)(PipeComponent *, fd_set *, fd_set *, fd_set *, int *),
+ gboolean (*handle_socket_cb)(PipeComponent *, int, int));
+void pipe_component_append(PipeComponent *self, char *buf, size_t length);
+void pipe_component_finished(PipeComponent *self, RequestState state);
+void pipe_component_set_next(PipeComponent *self, PipeComponent *next);
+RequestState pipe_component_get_state(const PipeComponent *self);
+void pipe_fdset(PipeComponent *head, fd_set *readfd,
+ fd_set *writefd, fd_set *excfd, int *max_fd);
+gboolean pipe_handle_socket(PipeComponent *head, int sockfd, int ev_bitmask);
+void pipe_delete_full(PipeComponent *head);
+
+PipeDownloader *pipe_downloader_create(const char *url, CURLM *curlmulti);
+void pipe_downloader_start(PipeDownloader *self);
+
+PipeMainMenuDownloader *pipe_mainmenu_downloader_create(struct WebviContext *context);
+void pipe_mainmenu_downloader_start(PipeMainMenuDownloader *self);
+
+PipeLinkExtractor *pipe_link_extractor_create(
+ const struct LinkTemplates *link_templates, const gchar *baseurl);
+
+PipeLocalFile *pipe_local_file_create(const gchar *filename);
+void pipe_local_file_start(PipeLocalFile *self);
+
+PipeCallbackWrapper *pipe_callback_wrapper_create(
+ ssize_t (*write_callback)(const char *, size_t, void *),
+ void *writedata,
+ void (*finish_callback)(RequestState, void *),
+ void *finishdata);
+
+PipeExternalDownloader *pipe_external_downloader_create(const gchar *url,
+ const gchar *command);
+void pipe_external_downloader_start(PipeExternalDownloader *self);
+
+PipeLibquvi *pipe_libquvi_create(const gchar *url);
+void pipe_libquvi_start(PipeLibquvi *self);
+
+#endif // __PIPECOMPONENT_H
diff --git a/src/libwebvi/request.c b/src/libwebvi/request.c
new file mode 100644
index 0000000..af078c4
--- /dev/null
+++ b/src/libwebvi/request.c
@@ -0,0 +1,237 @@
+#include <glib.h>
+#include <string.h>
+#include "pipecomponent.h"
+#include "webvicontext.h"
+#include "request.h"
+#include "link.h"
+
+struct WebviRequest {
+ PipeComponent *pipe_head;
+ gchar *url;
+ webvi_callback write_cb;
+ webvi_callback read_cb;
+ void *writedata;
+ void *readdata;
+ struct WebviContext *ctx; /* borrowed reference */
+};
+
+struct RequestStateMessage {
+ RequestState state;
+ const char *message;
+};
+
+static PipeComponent *pipe_factory(const WebviRequest *self);
+static void notify_pipe_finished(RequestState state, void *data);
+static const char *pipe_state_to_message(RequestState state);
+static gchar *wvt_to_local_file(const WebviContext *ctx, const char *wvt);
+static PipeComponent *build_and_start_mainmenu_pipe(const WebviRequest *self);
+static PipeComponent *build_and_start_local_pipe(const WebviRequest *self);
+static PipeComponent *build_and_start_external_pipe(const WebviRequest *self,
+ const char *command);
+static PipeComponent *build_and_start_libquvi_pipe(const WebviRequest *self);
+static PipeComponent *build_and_start_menu_pipe(const WebviRequest *self);
+
+WebviRequest *request_create(const char *url, struct WebviContext *ctx) {
+ WebviRequest *req = malloc(sizeof(WebviRequest));
+ memset(req, 0, sizeof(WebviRequest));
+ req->url = g_strdup(url);
+ req->ctx = ctx;
+ return req;
+}
+
+void request_delete(WebviRequest *self) {
+ if (self) {
+ request_stop(self);
+ pipe_delete_full(self->pipe_head);
+ g_free(self->url);
+ }
+}
+
+gboolean request_start(WebviRequest *self) {
+ if (!self->pipe_head) {
+ self->pipe_head = pipe_factory(self);
+ if (!self->pipe_head) {
+ notify_pipe_finished(WEBVISTATE_NOT_FOUND, self);
+ }
+ }
+ return TRUE;
+}
+
+void request_stop(WebviRequest *self) {
+ // FIXME
+}
+
+PipeComponent *pipe_factory(const WebviRequest *self) {
+ PipeComponent *head;
+ if (strcmp(self->url, "wvt://mainmenu") == 0) {
+ head = build_and_start_mainmenu_pipe(self);
+
+ } else if (strncmp(self->url, "wvt://", 6) == 0) {
+ head = build_and_start_local_pipe(self);
+
+ } else {
+ const LinkAction *action = link_templates_get_action(
+ get_link_templates(self->ctx), self->url);
+ LinkActionType action_type = action ? link_action_get_type(action) : LINK_ACTION_PARSE;
+ if (action_type == LINK_ACTION_STREAM_LIBQUVI) {
+ head = build_and_start_libquvi_pipe(self);
+ } else if (action_type == LINK_ACTION_EXTERNAL_COMMAND) {
+ const char *command = link_action_get_command(action);
+ head = build_and_start_external_pipe(self, command);
+ } else {
+ head = build_and_start_menu_pipe(self);
+ }
+ }
+
+ return head;
+}
+
+PipeComponent *build_and_start_mainmenu_pipe(const WebviRequest *self) {
+ PipeMainMenuDownloader *p1 = pipe_mainmenu_downloader_create(self->ctx);
+ PipeComponent *p2 = (PipeComponent *)pipe_callback_wrapper_create(
+ self->read_cb, self->readdata, notify_pipe_finished, (void *)self);
+ pipe_component_set_next((PipeComponent *)p1, p2);
+ pipe_mainmenu_downloader_start(p1);
+
+ return (PipeComponent *)p1;
+}
+
+PipeComponent *build_and_start_local_pipe(const WebviRequest *self) {
+ gchar *filename = wvt_to_local_file(self->ctx, self->url);
+ PipeLocalFile *p1 = pipe_local_file_create(filename);
+ g_free(filename);
+ PipeComponent *p2 = (PipeComponent *)pipe_callback_wrapper_create(
+ self->read_cb, self->readdata, notify_pipe_finished, (void *)self);
+ pipe_component_set_next((PipeComponent *)p1, p2);
+ pipe_local_file_start(p1);
+
+ return (PipeComponent *)p1;
+}
+
+PipeComponent *build_and_start_external_pipe(const WebviRequest *self, const char *command) {
+ PipeExternalDownloader *p1 = pipe_external_downloader_create(self->url, command);
+ PipeComponent *p2 = (PipeComponent *)pipe_callback_wrapper_create(
+ self->read_cb, self->readdata, notify_pipe_finished, (void *)self);
+ pipe_component_set_next((PipeComponent *)p1, p2);
+ pipe_external_downloader_start(p1);
+
+ return (PipeComponent *)p1;
+}
+
+PipeComponent *build_and_start_libquvi_pipe(const WebviRequest *self) {
+ PipeLibquvi *p1 = pipe_libquvi_create(self->url);
+ PipeComponent *p2 = (PipeComponent *)pipe_callback_wrapper_create(
+ self->read_cb, self->readdata, notify_pipe_finished, (void *)self);
+ pipe_component_set_next((PipeComponent *)p1, p2);
+ pipe_libquvi_start(p1);
+
+ return (PipeComponent *)p1;
+}
+
+PipeComponent *build_and_start_menu_pipe(const WebviRequest *self) {
+ CURLM *curlmulti = webvi_context_get_curl_multi_handle(self->ctx);
+ PipeDownloader *p1 = pipe_downloader_create(self->url, curlmulti);
+ PipeComponent *p2 = (PipeComponent *)pipe_link_extractor_create(
+ get_link_templates(self->ctx), self->url);
+ PipeComponent *p3 = (PipeComponent *)pipe_callback_wrapper_create(
+ self->read_cb, self->readdata, notify_pipe_finished, (void *)self);
+ pipe_component_set_next((PipeComponent *)p1, p2);
+ pipe_component_set_next(p2, p3);
+ pipe_downloader_start(p1);
+
+ return (PipeComponent *)p1;
+}
+
+gchar *wvt_to_local_file(const WebviContext *ctx, const char *wvt) {
+ if (strncmp(wvt, "wvt://", 6) != 0)
+ return NULL;
+
+ const gchar *template_path = webvi_context_get_template_path(ctx);
+ if (!template_path)
+ return NULL; // FIXME
+
+ // FIXME: .. in paths
+
+ gchar *filename = g_strdup(wvt+6);
+ if (filename[0] == '/') {
+ // absolute path
+ // The path must be located under the template directory
+ if (strncmp(filename, template_path, strlen(template_path)) != 0) {
+ g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
+ "Invalid path in wvt:// url");
+ g_free(filename);
+ return NULL;
+ }
+ } else {
+ // relative path, concatenate to template_path
+ gchar *absolute_path = g_strconcat(template_path, "/", filename, NULL);
+ g_free(filename);
+ filename = absolute_path;
+ }
+
+ return filename;
+}
+
+void request_fdset(WebviRequest *self, fd_set *readfds,
+ fd_set *writefds, fd_set *excfds, int *max_fd)
+{
+ if (self->pipe_head) {
+ pipe_fdset(self->pipe_head, readfds, writefds, excfds, max_fd);
+ }
+}
+
+gboolean request_handle_socket(WebviRequest *self, int sockfd, int ev_bitmask)
+{
+ if (self->pipe_head) {
+ return pipe_handle_socket(self->pipe_head, sockfd, ev_bitmask);
+ } else {
+ return FALSE;
+ }
+}
+
+void request_set_write_callback(WebviRequest *self, webvi_callback func) {
+ self->write_cb = func;
+}
+
+void request_set_read_callback(WebviRequest *self, webvi_callback func) {
+ self->read_cb = func;
+}
+
+void request_set_write_data(WebviRequest *self, void *data) {
+ self->writedata = data;
+}
+
+void request_set_read_data(WebviRequest *self, void *data) {
+ self->readdata = data;
+}
+
+const char *request_get_url(const WebviRequest *self) {
+ return self->url;
+}
+
+void notify_pipe_finished(RequestState state, void *data) {
+ WebviRequest *req = (WebviRequest *)data;
+ const char *msg = pipe_state_to_message(state);
+ webvi_context_add_finished_message(req->ctx, req, state, msg);
+}
+
+const char *pipe_state_to_message(RequestState state) {
+ static struct RequestStateMessage messages[] =
+ {{WEBVISTATE_NOT_FINISHED, "Not finished"},
+ {WEBVISTATE_FINISHED_OK, "Success"},
+ {WEBVISTATE_MEMORY_ALLOCATION_ERROR, "Out of memory"},
+ {WEBVISTATE_NOT_FOUND, "Not found"},
+ {WEBVISTATE_NETWORK_READ_ERROR, "Failed to receive data from the network"},
+ {WEBVISTATE_IO_ERROR, "IO error"},
+ {WEBVISTATE_TIMEDOUT, "Timedout"},
+ {WEBVISTATE_SUBPROCESS_FAILED, "Failed to execute a subprocess"},
+ {WEBVISTATE_INTERNAL_ERROR, "Internal error"}};
+
+ for (int i=0; i<(sizeof(messages)/sizeof(messages[0])); i++) {
+ if (state == messages[i].state) {
+ return messages[i].message;
+ }
+ }
+
+ return "Internal error";
+}
diff --git a/src/libwebvi/request.h b/src/libwebvi/request.h
new file mode 100644
index 0000000..c52c91e
--- /dev/null
+++ b/src/libwebvi/request.h
@@ -0,0 +1,42 @@
+/*
+ * request.h
+ *
+ * Copyright (c) 2013 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __REQUEST_H
+#define __REQUEST_H
+
+#include "pipecomponent.h"
+
+struct WebviContext;
+
+typedef struct WebviRequest WebviRequest;
+
+WebviRequest *request_create(const char *url, struct WebviContext *ctx);
+void request_set_write_callback(WebviRequest *instance, webvi_callback func);
+void request_set_read_callback(WebviRequest *instance, webvi_callback func);
+void request_set_write_data(WebviRequest *instance, void *data);
+void request_set_read_data(WebviRequest *instance, void *data);
+const char *request_get_url(const WebviRequest *instance);
+gboolean request_start(WebviRequest *instance);
+void request_stop(WebviRequest *instance);
+void request_fdset(WebviRequest *instance, fd_set *readfd,
+ fd_set *writefd, fd_set *excfd, int *max_fd);
+gboolean request_handle_socket(WebviRequest *instance, int sockfd, int ev_bitmask);
+void request_delete(WebviRequest *instance);
+
+#endif // __REQUEST_H
diff --git a/src/libwebvi/urlutils.c b/src/libwebvi/urlutils.c
new file mode 100644
index 0000000..de64e06
--- /dev/null
+++ b/src/libwebvi/urlutils.c
@@ -0,0 +1,132 @@
+#include <string.h>
+#include "urlutils.h"
+
+static const gchar *skip_scheme(const char *url);
+static gboolean is_scheme_character(gchar c);
+
+gchar *relative_url_to_absolute(const gchar *baseurl, const gchar *href) {
+ gchar *absolute;
+ gchar *prefix;
+ const gchar *postfix = href;
+ if ((href[0] == '/') && (href[1] == '/')) {
+ gchar *scheme = url_scheme(baseurl);
+ prefix = g_strconcat(scheme, ":", NULL);
+ g_free(scheme);
+ } else if (href[0] == '/') {
+ prefix = url_root(baseurl);
+ if (g_str_has_suffix(prefix, "/")) {
+ postfix = href+1;
+ }
+ } else if (href[0] == '?') {
+ prefix = url_path_including_file(baseurl);
+ } else if (href[0] =='#') {
+ prefix = url_path_and_query(baseurl);
+ } else if (strstr(href, "://") == NULL) {
+ prefix = url_path_dirname(baseurl);
+ } else {
+ // href is absolute
+ prefix = NULL;
+ }
+
+ if (prefix) {
+ absolute = g_strconcat(prefix, postfix, NULL);
+ g_free(prefix);
+ } else {
+ absolute = g_strdup(href);
+ }
+
+ return absolute;
+}
+
+gchar *url_scheme(const gchar *url) {
+ if (!url)
+ return NULL;
+
+ const gchar *scheme_end = skip_scheme(url);
+ if (scheme_end == url) {
+ // no scheme
+ return g_strdup("");
+ } else {
+ // scheme found
+ // Do not include :// in the return value
+ g_assert(scheme_end >= url+3);
+ return g_strndup(url, scheme_end-3 - url);
+ }
+}
+
+gchar *url_root(const gchar *url) {
+ if (!url)
+ return NULL;
+
+ const gchar *authority = skip_scheme(url);
+ size_t authority_len = strcspn(authority, "/?#");
+ const gchar *authority_end = authority + authority_len;
+ gchar *root_without_slash = g_strndup(url, authority_end - url);
+ gchar *root = g_strconcat(root_without_slash, "/", NULL);
+ g_free(root_without_slash);
+ return root;
+}
+
+gchar *url_path_including_file(const gchar *url) {
+ if (!url)
+ return NULL;
+
+ const gchar *scheme_end = skip_scheme(url);
+ size_t path_len = strcspn(scheme_end, "?#");
+ const gchar *end = scheme_end + path_len;
+ gchar *path = g_strndup(url, end - url);
+ if (memchr(scheme_end, '/', path_len) == NULL) {
+ gchar *path2 = g_strconcat(path, "/", NULL);
+ g_free(path);
+ path = path2;
+ }
+
+ return path;
+}
+
+gchar *url_path_dirname(const gchar *url) {
+ if (!url)
+ return NULL;
+
+ const gchar *scheme_end = skip_scheme(url);
+ size_t path_len = strcspn(scheme_end, "?#");
+ const gchar *p = scheme_end + path_len;
+ while ((p >= url) && (*p != '/')) {
+ p--;
+ }
+
+ if (*p == '/') {
+ return g_strndup(url, (p+1) - url);
+ } else {
+ return g_strdup("/");
+ }
+}
+
+gchar *url_path_and_query(const gchar *url) {
+ if (!url)
+ return NULL;
+
+ const gchar *scheme_end = skip_scheme(url);
+ size_t path_len = strcspn(scheme_end, "#");
+ const gchar *end = scheme_end + path_len;
+ return g_strndup(url, end - url);
+}
+
+const gchar *skip_scheme(const char *url) {
+ const gchar *c = url;
+ while (is_scheme_character(*c)) {
+ c++;
+ }
+
+ if (strncmp(c, "://", 3) == 0) {
+ // scheme found
+ return c + 3;
+ } else {
+ // schemeless url
+ return url;
+ }
+}
+
+gboolean is_scheme_character(gchar c) {
+ return g_ascii_isalnum(c) || (c == '+') || (c == '-') || (c == '.');
+}
diff --git a/src/libwebvi/urlutils.h b/src/libwebvi/urlutils.h
new file mode 100644
index 0000000..374a941
--- /dev/null
+++ b/src/libwebvi/urlutils.h
@@ -0,0 +1,32 @@
+/*
+ * urlutils.h
+ *
+ * Copyright (c) 2013 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __URLUTILS_H
+#define __URLUTILS_H
+
+#include <glib.h>
+
+gchar *relative_url_to_absolute(const gchar *baseurl, const gchar *href);
+gchar *url_scheme(const gchar *baseurl);
+gchar *url_root(const gchar *baseurl);
+gchar *url_path_including_file(const gchar *baseurl);
+gchar *url_path_dirname(const gchar *baseurl);
+gchar *url_path_and_query(const gchar *baseurl);
+
+#endif // __URLUTILS_H
diff --git a/src/libwebvi/webvicontext.c b/src/libwebvi/webvicontext.c
new file mode 100644
index 0000000..4e33e94
--- /dev/null
+++ b/src/libwebvi/webvicontext.c
@@ -0,0 +1,409 @@
+#include <stdlib.h>
+#include <string.h>
+#include <glib/gprintf.h>
+#include "webvicontext.h"
+#include "libwebvi.h"
+#include "request.h"
+
+#define DEFAULT_TEMPLATE_PATH "/etc/webvi/websites"
+#define MAX_MESSAGE_LENGTH 128
+
+struct WebviContext {
+ GTree *requests;
+ LinkTemplates *link_templates;
+ WebviHandle next_request;
+ CURLM *curl_multi_handle;
+ gchar *template_path;
+ GArray *finish_messages;
+ /* The value returned by the latest webvi_context_next_message() call */
+ WebviMsg current_message;
+ bool debug;
+};
+
+typedef struct RequestAndHandle {
+ const WebviRequest *request;
+ WebviHandle handle;
+} RequestAndHandle;
+
+typedef struct FoundFds {
+ fd_set *readfds;
+ fd_set *writefds;
+ fd_set *excfds;
+ int *max_fd;
+} FoundFds;
+
+typedef struct SocketToHandle {
+ int sockfd;
+ int ev_bitmask;
+ gboolean handled;
+} SocketToHandle;
+
+static WebviCtx handle_for_context(WebviContext *ctx);
+static gint cmp_int(gconstpointer a, gconstpointer b, gpointer user_data);
+static gboolean gather_fds(gpointer key, gpointer value, gpointer data);
+static gboolean handle_request_socket(gpointer key, gpointer value, gpointer data);
+static WebviHandle get_handle_for_request(WebviContext *ctx, const WebviRequest *req);
+static gboolean search_by_request(gpointer key, gpointer value, gpointer data);
+static void check_for_finished_curl(CURLM *multi_handle);
+static RequestState curl_code_to_pipe_state(CURLcode curlcode);
+static WebviResult curlmcode_to_webvierr(CURLMcode mcode);
+static void webvi_log_handler(const gchar *log_domain, GLogLevelFlags log_level,
+ const gchar *message, gpointer user_data);
+static void register_context(WebviCtx key, WebviContext *value);
+static GTree *get_tls_contexts();
+static void webvi_context_delete(WebviContext *ctx);
+static void free_tls_context(gpointer data);
+static void free_context(gpointer data);
+static void free_request(gpointer data);
+
+static GPrivate tls_contexts = G_PRIVATE_INIT(free_tls_context);
+
+WebviCtx webvi_context_initialize() {
+ WebviContext *ctx = malloc(sizeof(WebviContext));
+ if (!ctx) {
+ return 0;
+ }
+
+ memset(ctx, 0, sizeof(WebviContext));
+
+ ctx->requests = g_tree_new_full(cmp_int, NULL, NULL, free_request);
+ if (!ctx->requests) {
+ webvi_context_delete(ctx);
+ return 0;
+ }
+
+ ctx->finish_messages = g_array_new(FALSE, TRUE, sizeof(WebviMsg));
+ if (!ctx->finish_messages) {
+ webvi_context_delete(ctx);
+ return 0;
+ }
+
+ ctx->next_request = 1;
+
+ WebviCtx ctxhandle = handle_for_context(ctx);
+ register_context(ctxhandle, ctx);
+
+ return ctxhandle;
+}
+
+void register_context(WebviCtx ctxhandle, WebviContext *ctx) {
+ GTree *contexts = get_tls_contexts();
+ g_tree_insert(contexts, GINT_TO_POINTER(ctxhandle), ctx);
+}
+
+void webvi_context_cleanup(WebviCtx ctxhandle) {
+ GTree *contexts = get_tls_contexts();
+ g_tree_remove(contexts, GINT_TO_POINTER(ctxhandle));
+}
+
+void webvi_context_set_debug(WebviContext *self, bool d) {
+ GLogFunc logfunc;
+
+ self->debug = d;
+ if (self->debug) {
+ logfunc = webvi_log_handler;
+ } else {
+ logfunc = g_log_default_handler;
+ }
+
+ g_log_set_handler(LIBWEBVI_LOG_DOMAIN,
+ G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG,
+ logfunc, NULL);
+}
+
+void webvi_log_handler(const gchar *log_domain, GLogLevelFlags log_level,
+ const gchar *message, gpointer user_data)
+{
+ g_fprintf(stderr, "%s: %s\n", log_domain, message);
+}
+
+void webvi_context_set_template_path(WebviContext *self, const char *path) {
+ if (self->link_templates) {
+ link_templates_delete(self->link_templates);
+ self->link_templates = NULL;
+ }
+ if (self->template_path) {
+ g_free(self->template_path);
+ }
+ self->template_path = path ? g_strdup(path) : NULL;
+}
+
+const char *webvi_context_get_template_path(const WebviContext *self) {
+ return self->template_path ? self->template_path : DEFAULT_TEMPLATE_PATH;
+}
+
+const LinkTemplates *get_link_templates(WebviContext *self) {
+ if (!self->link_templates) {
+ self->link_templates = link_templates_create();
+ if (self->link_templates) {
+ const gchar *path = webvi_context_get_template_path(self);
+ gchar *template_file = g_strconcat(path, "/links", NULL);
+ link_templates_load(self->link_templates, template_file);
+ g_free(template_file);
+ }
+ }
+
+ return self->link_templates;
+}
+
+WebviHandle webvi_context_add_request(WebviContext *self, WebviRequest *req) {
+ int h = self->next_request++;
+ g_tree_insert(self->requests, GINT_TO_POINTER(h), req);
+ return (WebviHandle)h;
+}
+
+WebviRequest *webvi_context_get_request(WebviContext *self, WebviHandle h) {
+ return (WebviRequest *)g_tree_lookup(self->requests, GINT_TO_POINTER(h));
+}
+
+void webvi_context_remove_request(WebviContext *self, WebviHandle h) {
+ g_tree_remove(self->requests, GINT_TO_POINTER(h));
+}
+
+CURLM *webvi_context_get_curl_multi_handle(WebviContext *self) {
+ if (!self->curl_multi_handle)
+ self->curl_multi_handle = curl_multi_init();
+ return self->curl_multi_handle;
+}
+
+WebviCtx handle_for_context(WebviContext *ctx) {
+ return (WebviCtx)ctx; // FIXME
+}
+
+WebviContext *get_context_by_handle(WebviCtx handle) {
+ GTree *contexts = get_tls_contexts();
+ return g_tree_lookup(contexts, GINT_TO_POINTER(handle));
+}
+
+WebviResult webvi_context_fdset(WebviContext *ctx, fd_set *readfds,
+ fd_set *writefds, fd_set *excfds, int *max_fd)
+{
+ WebviResult res = WEBVIERR_OK;
+ *max_fd = -1;
+
+ // curl sockets
+ CURLM *mhandle = webvi_context_get_curl_multi_handle(ctx);
+ if (mhandle) {
+ CURLMcode mcode = curl_multi_fdset(mhandle, readfds, writefds, excfds, max_fd);
+ res = curlmcode_to_webvierr(mcode);
+ }
+
+ // non-curl fds
+ FoundFds fds;
+ fds.readfds = readfds;
+ fds.writefds = writefds;
+ fds.excfds = excfds;
+ fds.max_fd = max_fd;
+ g_tree_foreach(ctx->requests, gather_fds, &fds);
+
+ return res;
+}
+
+gboolean gather_fds(gpointer key, gpointer value, gpointer data) {
+ FoundFds *fds = (FoundFds *)data;
+ WebviRequest *req = (WebviRequest *)value;
+ request_fdset(req, fds->readfds, fds->writefds, fds->excfds, fds->max_fd);
+ return FALSE;
+}
+
+void webvi_context_handle_socket_action(
+ WebviContext *ctx, int sockfd, int ev_bitmask, long *running_handles)
+{
+ SocketToHandle x;
+ x.sockfd = sockfd;
+ x.ev_bitmask = ev_bitmask;
+ x.handled = FALSE;
+ g_tree_foreach(ctx->requests, handle_request_socket, &x);
+
+ int curl_handles = 0;
+ if (!x.handled) {
+ // sockfd belongs to curl
+ CURLM *multi_handle = webvi_context_get_curl_multi_handle(ctx);
+ if (multi_handle) {
+ curl_socket_t curl_socket;
+ int curl_mask = 0;
+
+ if (sockfd == WEBVI_SELECT_TIMEOUT) {
+ curl_socket = CURL_SOCKET_TIMEOUT;
+ curl_mask = 0;
+ } else {
+ curl_socket = sockfd;
+ if ((ev_bitmask & WEBVI_SELECT_READ) != 0)
+ curl_mask |= CURL_CSELECT_IN;
+ if ((ev_bitmask & WEBVI_SELECT_WRITE) != 0)
+ curl_mask |= CURL_CSELECT_OUT;
+ if ((ev_bitmask & WEBVI_SELECT_EXCEPTION) != 0)
+ curl_mask |= CURL_CSELECT_ERR;
+ }
+
+ curl_multi_socket_action(multi_handle, curl_socket, curl_mask, &curl_handles);
+ check_for_finished_curl(multi_handle);
+ }
+ }
+
+ // FIXME: running_handles
+ *running_handles = curl_handles;
+}
+
+gboolean handle_request_socket(gpointer key, gpointer value, gpointer data) {
+ WebviRequest *req = (WebviRequest *)value;
+ SocketToHandle *to_handle = (SocketToHandle *)data;
+ return request_handle_socket(req, to_handle->sockfd, to_handle->ev_bitmask);
+}
+
+void check_for_finished_curl(CURLM *multi_handle) {
+ int num_messages;
+ CURLMsg *info;
+ while ((info = curl_multi_info_read(multi_handle, &num_messages))) {
+ if (info->msg == CURLMSG_DONE) {
+ char *instance;
+ if (curl_easy_getinfo(info->easy_handle, CURLINFO_PRIVATE, &instance) == CURLE_OK) {
+ PipeComponent *pipe = (PipeComponent *)instance;
+ pipe_component_finished(pipe, curl_code_to_pipe_state(info->data.result));
+ }
+ }
+ }
+}
+
+RequestState curl_code_to_pipe_state(CURLcode curlcode) {
+ switch (curlcode) {
+ case CURLE_OK:
+ return WEBVISTATE_FINISHED_OK;
+
+ case CURLE_COULDNT_CONNECT:
+ case CURLE_TOO_MANY_REDIRECTS:
+ case CURLE_GOT_NOTHING:
+ case CURLE_RECV_ERROR:
+ return WEBVISTATE_NETWORK_READ_ERROR;
+
+ case CURLE_REMOTE_FILE_NOT_FOUND:
+ return WEBVISTATE_NOT_FOUND;
+
+ case CURLE_OPERATION_TIMEDOUT:
+ return WEBVISTATE_TIMEDOUT;
+
+ default:
+ return WEBVISTATE_IO_ERROR;
+ }
+}
+
+WebviResult curlmcode_to_webvierr(CURLMcode mcode) {
+ switch (mcode) {
+ case CURLM_OK:
+ return WEBVIERR_OK;
+
+ case CURLM_BAD_HANDLE:
+ case CURLM_BAD_EASY_HANDLE:
+ return WEBVIERR_INVALID_PARAMETER;
+
+ default:
+ return WEBVIERR_UNKNOWN_ERROR;
+ };
+}
+
+void webvi_context_add_finished_message(WebviContext *ctx,
+ const WebviRequest *req,
+ RequestState status_code,
+ const char *message_text)
+{
+ WebviHandle h = get_handle_for_request(ctx, req);
+ if (h != 0) {
+ GArray *messages = ctx->finish_messages;
+ WebviMsg msg;
+ msg.msg = WEBVIMSG_DONE;
+ msg.handle = h;
+ msg.status_code = status_code;
+ msg.data = message_text;
+ g_array_append_val(messages, msg);
+ }
+}
+
+WebviHandle get_handle_for_request(WebviContext *ctx, const WebviRequest *req) {
+ RequestAndHandle query_and_result;
+ query_and_result.request = req;
+ query_and_result.handle = 0;
+ g_tree_foreach(ctx->requests, search_by_request, &query_and_result);
+ return query_and_result.handle;
+}
+
+gboolean search_by_request(gpointer key, gpointer value, gpointer data) {
+ WebviHandle h = GPOINTER_TO_INT(key);
+ WebviRequest *req = value;
+ RequestAndHandle *query_and_result = data;
+
+ if (query_and_result->request == req) {
+ query_and_result->handle = h;
+ return TRUE; /* stop traversal */
+ } else {
+ return FALSE;
+ }
+}
+
+WebviMsg *webvi_context_next_message(WebviContext *ctx, int *remaining_messages) {
+ guint len = ctx->finish_messages->len;
+ if (len > 0) {
+ if (remaining_messages)
+ *remaining_messages = (int)len-1;
+
+ ctx->current_message = g_array_index(ctx->finish_messages, WebviMsg, 0);
+ g_array_remove_index(ctx->finish_messages, 0);
+ return &ctx->current_message;
+ } else {
+ if (remaining_messages)
+ *remaining_messages = 0;
+ return NULL;
+ }
+}
+
+void webvi_context_delete(WebviContext *ctx) {
+ if (ctx) {
+ if (ctx->finish_messages)
+ g_array_free(ctx->finish_messages, TRUE);
+ if (ctx->curl_multi_handle)
+ curl_multi_cleanup(ctx->curl_multi_handle);
+ if (ctx->requests)
+ g_tree_unref(ctx->requests);
+ /* if (ctx->downloaders) */
+ /* g_ptr_array_unref(ctx->downloaders); */
+ if (ctx->link_templates)
+ link_templates_delete(ctx->link_templates);
+ g_free(ctx->template_path);
+
+ free(ctx);
+ }
+}
+
+GTree *get_tls_contexts() {
+ GTree *contexts = g_private_get(&tls_contexts);
+ if (!contexts) {
+ contexts = g_tree_new_full(cmp_int, NULL, NULL, free_context);
+ g_private_set(&tls_contexts, contexts);
+ }
+
+ return contexts;
+}
+
+void webvi_context_cleanup_all() {
+ /* This will cause free_tls_context to be called if tls_context was set */
+ g_private_set(&tls_contexts, NULL);
+}
+
+gint cmp_int(gconstpointer a, gconstpointer b, gpointer user_data) {
+ int aint = GPOINTER_TO_INT(a);
+ int bint = GPOINTER_TO_INT(b);
+ return aint - bint;
+}
+
+void free_tls_context(gpointer data) {
+ if (data) {
+ g_tree_destroy((GTree *)data);
+ }
+}
+
+void free_request(gpointer data) {
+ request_delete((WebviRequest *)data);
+}
+
+void free_context(gpointer data) {
+ webvi_context_delete((WebviContext *)data);
+}
diff --git a/src/libwebvi/webvicontext.h b/src/libwebvi/webvicontext.h
new file mode 100644
index 0000000..7fa0738
--- /dev/null
+++ b/src/libwebvi/webvicontext.h
@@ -0,0 +1,63 @@
+/*
+ * webvicontext.h
+ *
+ * Copyright (c) 2013 Antti Ajanki <antti.ajanki@iki.fi>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __WEBVICONTEXT_H
+#define __WEBVICONTEXT_H
+
+#include <stdbool.h>
+#include <curl/curl.h>
+#include <glib.h>
+#include "libwebvi.h"
+#include "linktemplates.h"
+#include "pipecomponent.h"
+
+typedef struct WebviContext WebviContext;
+typedef struct WebviRequest WebviRequest;
+
+WebviContext *get_context_by_handle(WebviCtx handle);
+
+WebviCtx webvi_context_initialize(void);
+void webvi_context_cleanup(WebviCtx ctxhandle);
+void webvi_context_cleanup_all();
+
+void webvi_context_set_debug(WebviContext *self,
+ bool d);
+void webvi_context_set_template_path(WebviContext *self,
+ const char *path);
+const char *webvi_context_get_template_path(const WebviContext *self);
+const LinkTemplates *get_link_templates(WebviContext *self);
+CURLM *webvi_context_get_curl_multi_handle(WebviContext *self);
+
+WebviHandle webvi_context_add_request(WebviContext *self, WebviRequest *req);
+void webvi_context_remove_request(WebviContext *self, WebviHandle h);
+WebviRequest *webvi_context_get_request(WebviContext *self, WebviHandle h);
+
+WebviResult webvi_context_fdset(WebviContext *ctx, fd_set *readfd,
+ fd_set *writefd, fd_set *excfd, int *max_fd);
+void webvi_context_handle_socket_action(
+ WebviContext *ctx, int sockfd, int ev_bitmask, long *running_handles);
+
+void webvi_context_add_finished_message(WebviContext *messages,
+ const WebviRequest *req,
+ RequestState status_code,
+ const char *message_text);
+WebviMsg *webvi_context_next_message(WebviContext *ctx,
+ int *remaining_messages);
+
+#endif // __WEBVICONTEXT_H
diff --git a/src/pywebvi/pywebvi.py b/src/pywebvi/pywebvi.py
new file mode 100644
index 0000000..b3df416
--- /dev/null
+++ b/src/pywebvi/pywebvi.py
@@ -0,0 +1,248 @@
+from ctypes import *
+import ctypes.util
+import weakref
+
+_WEBVIERR_OK = 0
+
+_WEBVI_INVALID_HANDLE = -1
+
+_WEBVIOPT_WRITEFUNC = 0
+_WEBVIOPT_READFUNC = 1
+_WEBVIOPT_WRITEDATA = 2
+_WEBVIOPT_READDATA = 3
+
+_WEBVIINFO_URL = 0
+_WEBVIINFO_CONTENT_LENGTH = 1
+_WEBVIINFO_CONTENT_TYPE = 2
+_WEBVIINFO_STREAM_TITLE = 3
+
+_WEBVI_CONFIG_TEMPLATE_PATH = 0
+_WEBVI_CONFIG_DEBUG = 1
+_WEBVI_CONFIG_TIMEOUT_CALLBACK = 2
+_WEBVI_CONFIG_TIMEOUT_DATA = 3
+
+class WebviState:
+ NOT_FINISHED = 0
+ FINISHED_OK = 1
+ NOT_FOUND = 2
+ NETWORK_READ_ERROR = 3
+ IO_ERROR = 4
+ TIMEDOUT = 5
+ INTERNAL_ERROR = 999
+
+class WebviMsg(Structure):
+ _fields_ = [("msg", c_int),
+ ("handle", c_int),
+ ("status_code", c_int),
+ ("data", c_char_p)]
+
+class WeakRequestRef(weakref.ref):
+ pass
+
+class WebviError(Exception):
+ pass
+
+def raise_if_webvi_result_not_ok(value):
+ if value != _WEBVIERR_OK:
+ raise WebviError('libwebvi function returned error code %d: %s' %
+ (value, strerror(value)))
+ return value
+
+def raise_if_request_not_ok(value):
+ if value == -1:
+ raise WebviError('libwebvi request initialization failed')
+ return value
+
+_libc = CDLL(ctypes.util.find_library("c"))
+_libc.free.argtypes = [c_void_p]
+_libc.free.restype = None
+
+libwebvi = CDLL("libwebvi.so")
+libwebvi.webvi_global_init()
+
+libwebvi.webvi_initialize_context.argtypes = []
+libwebvi.webvi_initialize_context.restype = c_long
+libwebvi.webvi_version.argtypes = []
+libwebvi.webvi_version.restype = c_char_p
+libwebvi.webvi_strerror.argtypes = [c_int]
+libwebvi.webvi_strerror.restype = c_char_p
+libwebvi.webvi_new_request.argtypes = [c_long, c_char_p]
+libwebvi.webvi_new_request.restype = raise_if_request_not_ok
+libwebvi.webvi_delete_request.argtypes = [c_long, c_int]
+libwebvi.webvi_delete_request.restype = raise_if_webvi_result_not_ok
+libwebvi.webvi_process_some.argtypes = [c_long, c_int]
+libwebvi.webvi_process_some.restype = c_int
+libwebvi.webvi_get_message.argtypes = [c_long, POINTER(c_int)]
+libwebvi.webvi_get_message.restype = POINTER(WebviMsg)
+libwebvi.webvi_start_request.argtypes = [c_long, c_int]
+libwebvi.webvi_start_request.restype = raise_if_webvi_result_not_ok
+libwebvi.webvi_stop_request.argtypes = [c_long, c_int]
+libwebvi.webvi_stop_request.restype = raise_if_webvi_result_not_ok
+
+WEBVICALLBACK = CFUNCTYPE(c_ssize_t, c_char_p, c_size_t, c_void_p)
+TIMEOUTFUNC = CFUNCTYPE(None, c_long, c_void_p)
+
+def version():
+ return string_at(libwebvi.webvi_version())
+
+def strerror(err):
+ return string_at(libwebvi.webvi_strerror(err))
+
+class WebviContext:
+ def __init__(self):
+ self.handle = libwebvi.webvi_initialize_context()
+ self._requests = {}
+ self.timeout_callback = None
+
+ def __del__(self):
+ libwebvi.webvi_cleanup_context(self.handle)
+ self.handle = None
+
+ def set_template_path(self, path):
+ if self.handle is None:
+ return
+
+ set_config = libwebvi.webvi_set_config
+ set_config.argtypes = [c_long, c_int, c_char_p]
+ set_config.restype = raise_if_webvi_result_not_ok
+ set_config(self.handle, _WEBVI_CONFIG_TEMPLATE_PATH, path)
+
+ def set_debug(self, enabled):
+ if self.handle is None:
+ return
+
+ set_config = libwebvi.webvi_set_config
+ set_config.argtypes = [c_long, c_int, c_char_p]
+ set_config.restype = raise_if_webvi_result_not_ok
+ if enabled:
+ debug = "1"
+ else:
+ debug = "0"
+ set_config(self.handle, _WEBVI_CONFIG_DEBUG, debug)
+
+ def set_timeout_callback(self, cb):
+ def callback_wrapper(timeout, userdata):
+ return cb(timeout)
+
+ set_config = libwebvi.webvi_set_config
+ set_config.argtypes = [c_long, c_int, TIMEOUTFUNC]
+ set_config.restype = raise_if_webvi_result_not_ok
+ self.timeout_callback = TIMEOUTFUNC(callback_wrapper)
+ set_config(self.handle, _WEBVI_CONFIG_TIMEOUT_CALLBACK,
+ self.timeout_callback)
+
+ def process_some(self, timeout_seconds=0):
+ return libwebvi.webvi_process_some(self.handle, int(timeout_seconds))
+
+ def get_finished_request(self):
+ remaining = c_int(0)
+ msg_ptr = libwebvi.webvi_get_message(self.handle, byref(remaining))
+
+ if not msg_ptr or msg_ptr.contents.msg != 0:
+ return None
+
+ reqhandle = msg_ptr.contents.handle
+ if reqhandle not in self._requests:
+ return None
+
+ request = self._requests[reqhandle]()
+ if request is None:
+ return None
+
+ return (request,
+ msg_ptr.contents.status_code,
+ string_at(msg_ptr.contents.data))
+
+ def register_request(self, request):
+ def unregister_request(ref):
+ del self._requests[ref.handle]
+ libwebvi.webvi_delete_request(self.handle, ref.handle)
+
+ ref = WeakRequestRef(request, unregister_request)
+ ref.handle = request.handle
+ self._requests[request.handle] = ref
+
+
+class WebviRequest:
+ def __init__(self, context, href):
+ self.context = context
+ self.read_callback = None
+ self.handle = libwebvi.webvi_new_request(context.handle, href)
+ if self.handle == _WEBVI_INVALID_HANDLE:
+ raise WebviError('Initializing request failed')
+ self.context.register_request(self)
+
+ def start(self):
+ libwebvi.webvi_start_request(self.context.handle, self.handle)
+
+ def stop(self):
+ libwebvi.webvi_stop_request(self.context.handle, self.handle)
+
+ def set_read_callback(self, cb):
+ def callback_wrapper(buf, length, userdata):
+ return cb(string_at(buf, length))
+
+ set_opt = libwebvi.webvi_set_opt
+ set_opt.argstypes = [c_long, c_int, c_int, WEBVICALLBACK]
+ set_opt.restype = raise_if_webvi_result_not_ok
+ # Must keep a reference to the callback!
+ self.read_callback = WEBVICALLBACK(callback_wrapper)
+ set_opt(self.context.handle, self.handle,
+ _WEBVIOPT_READFUNC, self.read_callback)
+
+
+ # def set_write_callback(self, cb):
+ # def callback_wrapper(buf, length, userdata):
+ # return cb(string_at(buf, length))
+
+ # set_opt = libwebvi.webvi_set_opt
+ # set_opt.argstypes = [c_long, c_int, c_int, WEBVICALLBACK]
+ # set_opt.restype = raise_if_webvi_result_not_ok
+ # set_opt(self.context.handle, self.handle, _WEBVIOPT_WRITEFUNC,
+ # WEBVICALLBACK(callback_wrapper))
+
+ def get_url(self):
+ get_info = libwebvi.webvi_get_info
+ get_info.argtypes = [c_long, c_int, c_int, POINTER(c_char_p)]
+ get_info.restype = raise_if_webvi_result_not_ok
+
+ output = c_char_p()
+ get_info(self.context.handle, self.handle,
+ _WEBVIINFO_URL, pointer(output))
+ url = string_at(output)
+ _libc.free(output)
+ return url
+
+ def get_content_length(self):
+ get_info = libwebvi.webvi_get_info
+ get_info.argtypes = [c_long, c_int, c_int, POINTER(c_long)]
+ get_info.restype = raise_if_webvi_result_not_ok
+
+ output = c_long(0)
+ get_info(self.context.handle, self.handle,
+ _WEBVIINFO_CONTENT_LENGTH, pointer(output))
+ return output.value
+
+ def get_content_type(self):
+ get_info = libwebvi.webvi_get_info
+ get_info.argtypes = [c_long, c_int, c_int, POINTER(c_char_p)]
+ get_info.restype = raise_if_webvi_result_not_ok
+
+ output = c_char_p()
+ get_info(self.context.handle, self.handle,
+ _WEBVIINFO_CONTENT_TYPE, pointer(output))
+ content_type = string_at(output)
+ _libc.free(output)
+ return content_type
+
+ def get_stream_title(self):
+ get_info = libwebvi.webvi_get_info
+ get_info.argtypes = [c_long, c_int, c_int, POINTER(c_char_p)]
+ get_info.restype = raise_if_webvi_result_not_ok
+
+ output = c_char_p()
+ get_info(self.context.handle, self.handle,
+ _WEBVIINFO_STREAM_TITLE, pointer(output))
+ title = string_at(output)
+ _libc.free(output)
+ return title
diff --git a/src/version.h.in b/src/version.h.in
new file mode 100644
index 0000000..5b7da4e
--- /dev/null
+++ b/src/version.h.in
@@ -0,0 +1 @@
+#define LIBWEBVI_VERSION "@MAJOR_VERSION@.@MINOR_VERSION@.@PATCH_VERSION@"
diff --git a/src/webvicli/webvi b/src/webvicli/webvi
new file mode 100755
index 0000000..cb98b5e
--- /dev/null
+++ b/src/webvicli/webvi
@@ -0,0 +1,22 @@
+#!/usr/bin/python
+
+# webvi - starter script for webvicli
+#
+# Copyright (c) 2010-2013 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+from webvicli import client
+client.main(sys.argv[1:])
diff --git a/src/webvicli/webvicli/__init__.py b/src/webvicli/webvicli/__init__.py
new file mode 100644
index 0000000..1cf59b7
--- /dev/null
+++ b/src/webvicli/webvicli/__init__.py
@@ -0,0 +1 @@
+__all__ = ['client', 'menu']
diff --git a/src/webvicli/webvicli/client.py b/src/webvicli/webvicli/client.py
new file mode 100644
index 0000000..5de29c9
--- /dev/null
+++ b/src/webvicli/webvicli/client.py
@@ -0,0 +1,825 @@
+#!/usr/bin/env python
+
+# client.py - webvi command line client
+#
+# Copyright (c) 2009-2013 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import cmd
+import mimetypes
+import select
+import os.path
+import subprocess
+import time
+import re
+import datetime
+import urllib
+import shlex
+import shutil
+import tempfile
+import libxml2
+from pywebvi import WebviContext, WebviRequest, WebviState
+from optparse import OptionParser
+from ConfigParser import RawConfigParser
+from urlparse import urlparse
+from StringIO import StringIO
+from . import menu
+
+VERSION = '0.5.0'
+
+# Default options
+DEFAULT_PLAYERS = ['mplayer -cache-min 10 "%s"',
+ 'vlc --play-and-exit --file-caching 5000 "%s"',
+ 'totem "%s"',
+ 'xine "%s"']
+
+# These mimetypes are common but often missing
+mimetypes.init()
+mimetypes.add_type('video/flv', '.flv')
+mimetypes.add_type('video/x-flv', '.flv')
+mimetypes.add_type('video/webm', '.webm')
+
+def safe_filename(name, vfat):
+ """Sanitize a filename. If vfat is False, replace '/' with '_', if
+ vfat is True, replace also other characters that are illegal on
+ VFAT. Remove dots from the beginning of the filename."""
+ if vfat:
+ excludechars = r'[\\"*/:<>?|]'
+ else:
+ excludechars = r'[/]'
+
+ res = re.sub(excludechars, '_', name)
+ res = res.lstrip('.')
+ res = res.encode(sys.getfilesystemencoding(), 'ignore')
+
+ return res
+
+def get_content_unicode(node):
+ """node.getContent() returns an UTF-8 encoded sequence of bytes (a
+ string). Convert it to a unicode object."""
+ return unicode(node.getContent(), 'UTF-8', 'replace')
+
+def guess_video_extension(mimetype, url):
+ """Return extension for a video at url with a given mimetype.
+
+ This assumes that the target is a video stream and therefore ignores
+ mimetype if it is text/plain, which some incorrectly configured servers
+ return as the mimetype.
+ """
+ ext = mimetypes.guess_extension(mimetype)
+ if (ext is None) or (mimetype == 'text/plain'):
+ lastcomponent = re.split(r'[?#]', url, 1)[0].split('/')[-1]
+ i = lastcomponent.rfind('.')
+ if i == -1:
+ ext = ''
+ else:
+ ext = lastcomponent[i:]
+ return ext
+
+def dl_progress(count, blockSize, totalSize):
+ if totalSize == -1:
+ return
+ percent = int(count*blockSize*100/totalSize)
+ sys.stdout.write("\r%d% %" % percent)
+ sys.stdout.flush()
+
+def next_available_file_name(basename, ext):
+ fullname = basename + ext
+ if not os.path.exists(fullname):
+ return fullname
+ i = 1
+ while os.path.exists('%s-%d%s' % (basename, i, ext)):
+ i += 1
+ return '%s-%d%s' % (basename, i, ext)
+
+class StringIOCallback(StringIO):
+ def write_and_return_length(self, buf):
+ self.write(buf)
+ return len(buf)
+
+
+class ProgressMeter:
+ def __init__(self, stream):
+ self.last_update = None
+ self.samples = []
+ self.total_bytes = 0
+ self.stream = stream
+ self.progress_len = 0
+ self.starttime = time.time()
+
+ def pretty_bytes(self, bytes):
+ """Pretty print bytes as kB or MB."""
+ if bytes < 1100:
+ return '%d B' % bytes
+ elif bytes < 1024*1024:
+ return '%.1f kB' % (float(bytes)/1024)
+ elif bytes < 1024*1024*1024:
+ return '%.1f MB' % (float(bytes)/1024/1024)
+ else:
+ return '%.1f GB' % (float(bytes)/1024/1024/1024)
+
+ def pretty_time(self, seconds):
+ """Pretty print seconds as hour and minutes."""
+ seconds = int(round(seconds))
+ if seconds < 60:
+ return '%d s' % seconds
+ elif seconds < 60*60:
+ secs = seconds % 60
+ mins = seconds/60
+ return '%d min %d s' % (mins, secs)
+ else:
+ hours = seconds / (60*60)
+ mins = (seconds-60*60*hours) / 60
+ return '%d hours %d min' % (hours, mins)
+
+ def update(self, bytes):
+ """Update progress bar.
+
+ Updates the estimates of download rate and remaining time.
+ Prints progress bar, if at least one second has passed since
+ the previous update.
+ """
+ now = time.time()
+
+ if self.total_bytes > 0:
+ percentage = float(bytes)/self.total_bytes * 100.0
+ else:
+ percentage = 0
+
+ if self.total_bytes > 0 and bytes >= self.total_bytes:
+ self.stream.write('\r')
+ self.stream.write(' '*self.progress_len)
+ self.stream.write('\r')
+ self.stream.write('%3.f %% of %s downloaded in %s (%.1f kB/s)\n' %
+ (percentage, self.pretty_bytes(self.total_bytes),
+ self.pretty_time(now-self.starttime),
+ float(bytes)/(now-self.starttime)/1024.0))
+ self.stream.flush()
+ return
+
+ force_refresh = False
+ if self.last_update is None:
+ # This is a new progress meter
+ self.last_update = now
+ force_refresh = True
+
+ if (not force_refresh) and (now <= self.last_update + 1):
+ # do not update too often
+ return
+
+ self.last_update = now
+
+ # Estimate bytes per second rate from the last 10 samples
+ self.samples.append((bytes, now))
+ if len(self.samples) > 10:
+ self.samples.pop(0)
+
+ bytes_old, time_old = self.samples[0]
+ if now > time_old:
+ rate = float(bytes-bytes_old)/(now-time_old)
+ else:
+ rate = 0
+
+ if self.total_bytes > 0:
+ remaining = self.total_bytes - bytes
+
+ if rate > 0:
+ time_left = self.pretty_time(remaining/rate)
+ else:
+ time_left = '???'
+
+ progress = '%3.f %% of %s (%.1f kB/s) %s remaining' % \
+ (percentage, self.pretty_bytes(self.total_bytes),
+ rate/1024.0, time_left)
+ else:
+ progress = '%s downloaded (%.1f kB/s)' % \
+ (self.pretty_bytes(bytes), rate/1024.0)
+
+ new_progress_len = len(progress)
+ if new_progress_len < self.progress_len:
+ progress += ' '*(self.progress_len - new_progress_len)
+ self.progress_len = new_progress_len
+
+ self.stream.write('\r')
+ self.stream.write(progress)
+ self.stream.flush()
+
+
+class WVClient:
+ def __init__(self, streamplayers, downloadlimits, streamlimits, vfatfilenames):
+ self.streamplayers = streamplayers
+ self.history = []
+ self.history_pointer = 0
+ self.quality_limits = {'download': downloadlimits,
+ 'stream': streamlimits}
+ self.vfatfilenames = vfatfilenames
+ self.alarm = None
+ self.webvi = WebviContext()
+ self.webvi.set_timeout_callback(self.update_timeout)
+
+ def set_debug(self, enabled):
+ self.webvi.set_debug(enabled)
+
+ def set_template_path(self, path):
+ self.webvi.set_template_path(path)
+
+ def update_timeout(self, timeout_ms, data):
+ if timeout_ms < 0:
+ self.alarm = None
+ else:
+ now = datetime.datetime.now()
+ self.alarm = now + datetime.timedelta(milliseconds=timeout_ms)
+
+ def parse_page(self, page):
+ if page is None:
+ return None
+ try:
+ doc = libxml2.parseDoc(page)
+ except libxml2.parserError:
+ return None
+
+ root = doc.getRootElement()
+ if root.name != 'wvmenu':
+ return None
+ queryitems = []
+ menupage = menu.Menu()
+ node = root.children
+ while node:
+ if node.name == 'title':
+ menupage.title = get_content_unicode(node)
+ elif node.name == 'ul':
+ li_node = node.children
+ while li_node:
+ if li_node.name == 'li':
+ menuitem = self.parse_link(li_node)
+ menupage.add(menuitem)
+ li_node = li_node.next
+
+ # elif node.name == 'link':
+ # menuitem = self.parse_link(node)
+ # menupage.add(menuitem)
+ # elif node.name == 'textfield':
+ # menuitem = self.parse_textfield(node)
+ # menupage.add(menuitem)
+ # queryitems.append(menuitem)
+ # elif node.name == 'itemlist':
+ # menuitem = self.parse_itemlist(node)
+ # menupage.add(menuitem)
+ # queryitems.append(menuitem)
+ # elif node.name == 'textarea':
+ # menuitem = self.parse_textarea(node)
+ # menupage.add(menuitem)
+ # elif node.name == 'button':
+ # menuitem = self.parse_button(node, queryitems)
+ # menupage.add(menuitem)
+ node = node.next
+ doc.freeDoc()
+ return menupage
+
+ def parse_link(self, node):
+ label = ''
+ ref = None
+ is_stream = False
+ child = node.children
+ while child:
+ if child.name == 'a':
+ label = get_content_unicode(child)
+ ref = child.prop('href')
+ is_stream = child.prop('class') != 'webvi'
+ child = child.next
+ return menu.MenuItemLink(label, ref, is_stream)
+
+ def parse_textfield(self, node):
+ label = ''
+ name = node.prop('name')
+ child = node.children
+ while child:
+ if child.name == 'label':
+ label = get_content_unicode(child)
+ child = child.next
+ return menu.MenuItemTextField(label, name)
+
+ def parse_textarea(self, node):
+ label = ''
+ child = node.children
+ while child:
+ if child.name == 'label':
+ label = get_content_unicode(child)
+ child = child.next
+ return menu.MenuItemTextArea(label)
+
+ def parse_itemlist(self, node):
+ label = ''
+ name = node.prop('name')
+ items = []
+ values = []
+ child = node.children
+ while child:
+ if child.name == 'label':
+ label = get_content_unicode(child)
+ elif child.name == 'item':
+ items.append(get_content_unicode(child))
+ values.append(child.prop('value'))
+ child = child.next
+ return menu.MenuItemList(label, name, items, values, sys.stdout)
+
+ def parse_button(self, node, queryitems):
+ label = ''
+ submission = None
+ encoding = 'utf-8'
+ child = node.children
+ while child:
+ if child.name == 'label':
+ label = get_content_unicode(child)
+ elif child.name == 'submission':
+ submission = get_content_unicode(child)
+ enc = child.hasProp('encoding')
+ if enc is not None:
+ encoding = get_content_unicode(enc)
+ child = child.next
+ return menu.MenuItemSubmitButton(label, submission, queryitems, encoding)
+
+ def execute_webvi(self, request):
+ """Call self.webvi.process_some until request is finished."""
+ while True:
+ if self.alarm is None:
+ timeout = 10
+ else:
+ delta = self.alarm - datetime.datetime.now()
+ if delta < datetime.timedelta(0):
+ timeout = 10
+ self.alarm = None
+ else:
+ timeout = delta.microseconds/1000000.0 + delta.seconds
+
+ self.webvi.process_some(timeout)
+ finished = self.webvi.get_finished_request()
+ if finished is not None and finished[0] == request:
+ return (finished[1], finished[2])
+
+ def getmenu(self, ref):
+ dlbuffer = StringIOCallback()
+ request = WebviRequest(self.webvi, ref)
+ request.set_read_callback(dlbuffer.write_and_return_length)
+ request.start()
+ status, err = self.execute_webvi(request)
+ del request
+
+ if status != WebviState.FINISHED_OK:
+ print 'Download failed:', err
+ return (status, err, None)
+
+ return (status, err, self.parse_page(dlbuffer.getvalue()))
+
+ def get_quality_params(self, videosite, streamtype):
+ params = []
+ lim = self.quality_limits[streamtype].get(videosite, {})
+ if lim.has_key('min'):
+ params.append('minquality=' + lim['min'])
+ if lim.has_key('max'):
+ params.append('maxquality=' + lim['max'])
+
+ return '&'.join(params)
+
+ def download(self, stream):
+ streamurl, streamtitle = self.get_stream_url_and_title(stream)
+ if streamurl is None:
+ return True
+
+ self.download_stream(streamurl, streamtitle)
+ return True
+
+ def get_stream_url_and_title(self, stream):
+ dlbuffer = StringIOCallback()
+ request = WebviRequest(self.webvi, stream)
+ request.set_read_callback(dlbuffer.write_and_return_length)
+ request.start()
+ status, err = self.execute_webvi(request)
+ del request
+
+ if status != WebviState.FINISHED_OK:
+ print 'Download failed:', err
+ return (None, None)
+
+ menu = self.parse_page(dlbuffer.getvalue())
+ if menu is None or len(menu) == 0:
+ print 'Failed to parse menu'
+ return (None, None)
+
+ return (menu[0].activate(), menu[0].label)
+
+ def download_stream(self, url, title):
+ try:
+ (tmpfilename, headers) = \
+ urllib.urlretrieve(url, reporthook=dl_progress)
+ print
+ except urllib.ContentTooShortError, exc:
+ print 'Got too few bytes, connection may have been interrupted'
+ headers = {}
+ tmpfile, tmpfilename = tempfile.mkstemp()
+ tmpfile.write(exc.content)
+ tmpfile.close()
+
+ # rename the tempfile to final name
+ contenttype = headers.get('Content-Type', 'video')
+ ext = guess_video_extension(contenttype, url)
+ safename = safe_filename(title, self.vfatfilenames)
+ destfilename = next_available_file_name(safename, ext)
+ shutil.move(tmpfilename, destfilename)
+ print 'Saved to %s' % destfilename
+
+ def play_stream(self, ref):
+ streamurl = self.get_stream_url(ref)
+ if streamurl == '':
+ print 'Did not find URL'
+ return False
+
+ if streamurl.startswith('wvt://'):
+ print 'Streaming not supported, try downloading'
+ return False
+
+ # Found url, now find a working media player
+ for player in self.streamplayers:
+ if '%s' not in player:
+ player = player + ' %s'
+
+ playcmd = shlex.split(player)
+
+ # Hack for playing from fifo in VLC
+ if 'vlc' in playcmd[0] and streamurl.startswith('file://'):
+ realurl = 'stream://' + streamurl[len('file://'):]
+ else:
+ realurl = streamurl
+
+ try:
+ playcmd[playcmd.index('%s')] = realurl
+ except ValueError:
+ print 'Can\'t substitute URL in', player
+ continue
+
+ try:
+ print 'Trying player: ' + ' '.join(playcmd)
+ retcode = subprocess.call(playcmd)
+ if retcode > 0:
+ print 'Player failed with returncode', retcode
+ # else:
+ # # After the player has finished, the library
+ # # generates a read event on a control socket. When
+ # # the client calls perform on the socket the
+ # # library removes temporary files.
+ # readfds, writefds = webvi.api.fdset()[1:3]
+ # readyread, readywrite, readyexc = \
+ # select.select(readfds, writefds, [], 0.1)
+ # for fd in readyread:
+ # webvi.api.perform(fd, WebviSelectBitmask.READ)
+ # for fd in readywrite:
+ # webvi.api.perform(fd, WebviSelectBitmask.WRITE)
+
+ return True
+ except OSError, err:
+ print 'Execution failed:', err
+
+ return False
+
+ def get_stream_url(self, ref):
+ m = re.match(r'wvt:///([^/]+)/', ref)
+ if m is not None:
+ ref += '&' + self.get_quality_params(m.group(1), 'stream')
+
+ request = WebviRequest(self.webvi, ref)
+
+ dlbuffer = StringIOCallback()
+ request.set_read_callback(dlbuffer.write_and_return_length)
+ request.start()
+ status, err = self.execute_webvi(request)
+ del request
+
+ if status != WebviState.FINISHED_OK:
+ print 'Download failed:', err
+ return ''
+
+ return dlbuffer.getvalue()
+
+ def get_current_menu(self):
+ if (self.history_pointer >= 0) and \
+ (self.history_pointer < len(self.history)):
+ return self.history[self.history_pointer]
+ else:
+ return None
+
+ def history_add(self, menupage):
+ if menupage is not None:
+ self.history = self.history[:(self.history_pointer+1)]
+ self.history.append(menupage)
+ self.history_pointer = len(self.history)-1
+
+ def history_back(self):
+ if self.history_pointer > 0:
+ self.history_pointer -= 1
+ return self.get_current_menu()
+
+ def history_forward(self):
+ if self.history_pointer < len(self.history)-1:
+ self.history_pointer += 1
+ return self.get_current_menu()
+
+
+class WVShell(cmd.Cmd):
+ def __init__(self, client, completekey='tab', stdin=None, stdout=None):
+ cmd.Cmd.__init__(self, completekey, stdin, stdout)
+ self.prompt = '> '
+ self.client = client
+
+ def preloop(self):
+ self.stdout.write('webvicli %s starting\n' % VERSION)
+ self.do_menu(None)
+
+ def precmd(self, arg):
+ try:
+ int(arg)
+ menuitem = self._get_numbered_item(int(arg))
+ if getattr(menuitem, 'is_stream', False):
+ return 'download ' + arg
+ else:
+ return 'select ' + arg
+ except ValueError:
+ return arg
+
+ def onecmd(self, c):
+ try:
+ return cmd.Cmd.onecmd(self, c)
+ except Exception:
+ import traceback
+ print 'Exception occurred while handling command "' + c + '"'
+ print traceback.format_exc()
+ return False
+
+ def emptyline(self):
+ pass
+
+ def display_menu(self, menupage):
+ if menupage is not None:
+ enc = self.stdout.encoding or 'UTF-8'
+ self.stdout.write(unicode(menupage).encode(enc, 'replace'))
+ self.stdout.flush()
+
+ def _get_numbered_item(self, arg):
+ menupage = self.client.get_current_menu()
+ try:
+ v = int(arg)-1
+ if (menupage is None) or (v < 0) or (v >= len(menupage)):
+ raise ValueError
+ except ValueError:
+ self.stdout.write('Invalid selection: %s\n' % arg)
+ self.stdout.flush()
+ return None
+ return menupage[v]
+
+ def do_select(self, arg):
+ """select x
+Select the link whose index is x.
+ """
+ menuitem = self._get_numbered_item(arg)
+ if menuitem is None:
+ return False
+ ref = menuitem.activate()
+ if ref is not None:
+ status, statusmsg, menupage = self.client.getmenu(ref)
+ if menupage is not None:
+ self.client.history_add(menupage)
+ else:
+ self.stdout.write('Error: %d %s\n' % (status, statusmsg))
+ self.stdout.flush()
+ else:
+ menupage = self.client.get_current_menu()
+ self.display_menu(menupage)
+ return False
+
+ def do_download(self, arg):
+ """download x
+Download a stream to a file. x can be an integer referring to a
+downloadable item (item without brackets) in the current menu or an
+URL of a video page.
+ """
+ stream = None
+ try:
+ menuitem = self._get_numbered_item(int(arg))
+ if menuitem is not None:
+ stream = menuitem.activate()
+ except (ValueError, AttributeError):
+ pass
+
+ if stream is None and arg.find('://') != -1:
+ stream = arg
+
+ if stream is not None:
+ self.client.download(stream)
+ else:
+ self.stdout.write('Not a stream\n')
+ self.stdout.flush()
+ return False
+
+ def do_stream(self, arg):
+ """stream x
+Play a stream. x can be an integer referring to a downloadable item
+(item without brackets) in the current menu or an URL of a video page.
+ """
+ stream = None
+ try:
+ menuitem = self._get_numbered_item(int(arg))
+ if menuitem is not None:
+ stream = menuitem.activate()
+ except (ValueError, AttributeError):
+ pass
+
+ if stream is None and arg.find('://') != -1:
+ stream = arg
+
+ if stream is not None:
+ self.client.play_stream(stream)
+ else:
+ self.stdout.write('Not a stream\n')
+ self.stdout.flush()
+ return False
+
+ def do_display(self, arg):
+ """Redisplay the current menu."""
+ if not arg:
+ self.display_menu(self.client.get_current_menu())
+ else:
+ self.stdout.write('Unknown parameter %s\n' % arg)
+ self.stdout.flush()
+ return False
+
+ def do_menu(self, arg):
+ """Get back to the main menu."""
+ status, statusmsg, menupage = self.client.getmenu('wvt://mainmenu')
+ if menupage is not None:
+ self.client.history_add(menupage)
+ self.display_menu(menupage)
+ else:
+ self.stdout.write('Error: %d %s\n' % (status, statusmsg))
+ self.stdout.flush()
+ return True
+ return False
+
+ def do_back(self, arg):
+ """Go to the previous menu in the history."""
+ menupage = self.client.history_back()
+ self.display_menu(menupage)
+ return False
+
+ def do_forward(self, arg):
+ """Go to the next menu in the history."""
+ menupage = self.client.history_forward()
+ self.display_menu(menupage)
+ return False
+
+ def do_quit(self, arg):
+ """Quit the program."""
+ return True
+
+ def do_EOF(self, arg):
+ """Quit the program."""
+ return True
+
+
+def load_config(options):
+ """Load options from config files."""
+ cfgprs = RawConfigParser()
+ cfgprs.read(['/etc/webvi.conf', os.path.expanduser('~/.webvi')])
+ for sec in cfgprs.sections():
+ if sec == 'webvi':
+ for opt, val in cfgprs.items('webvi'):
+ if opt in ['vfat', 'verbose']:
+ try:
+ options[opt] = cfgprs.getboolean(sec, opt)
+ except ValueError:
+ print 'Invalid config: %s = %s' % (opt, val)
+
+ # convert verbose to integer
+ if opt == 'verbose':
+ if options['verbose']:
+ options['verbose'] = 1
+ else:
+ options['verbose'] = 0
+
+ else:
+ options[opt] = val
+
+ else:
+ sitename = urlparse(sec).netloc
+ if sitename == '':
+ sitename = sec
+
+ if not options.has_key('download-limits'):
+ options['download-limits'] = {}
+ if not options.has_key('stream-limits'):
+ options['stream-limits'] = {}
+ options['download-limits'][sitename] = {}
+ options['stream-limits'][sitename] = {}
+
+ for opt, val in cfgprs.items(sec):
+ if opt == 'download-min-quality':
+ options['download-limits'][sitename]['min'] = val
+ elif opt == 'download-max-quality':
+ options['download-limits'][sitename]['max'] = val
+ elif opt == 'stream-min-quality':
+ options['stream-limits'][sitename]['min'] = val
+ elif opt == 'stream-max-quality':
+ options['stream-limits'][sitename]['max'] = val
+
+ return options
+
+def parse_command_line(cmdlineargs, options):
+ parser = OptionParser()
+ parser.add_option('-t', '--templatepath', type='string',
+ dest='templatepath',
+ help='read video site templates from DIR', metavar='DIR',
+ default=None)
+ parser.add_option('-v', '--verbose', action='store_const', const=1,
+ dest='verbose', help='debug output', default=0)
+ parser.add_option('--vfat', action='store_true',
+ dest='vfat', default=False,
+ help='generate Windows compatible filenames')
+ parser.add_option('-u', '--url', type='string',
+ dest='url',
+ help='Download video from URL and exit',
+ metavar='URL', default=None)
+ cmdlineopt = parser.parse_args(cmdlineargs)[0]
+
+ if cmdlineopt.templatepath is not None:
+ options['templatepath'] = cmdlineopt.templatepath
+ if cmdlineopt.verbose > 0:
+ options['verbose'] = cmdlineopt.verbose
+ if cmdlineopt.vfat:
+ options['vfat'] = cmdlineopt.vfat
+ if cmdlineopt.url:
+ options['url'] = cmdlineopt.url
+
+ return options
+
+def player_list(options):
+ """Return a sorted list of player commands extracted from options
+ dictionary."""
+ # Load streamplayer items from the config file and sort them
+ # according to quality.
+ players = []
+ for opt, val in options.iteritems():
+ m = re.match(r'streamplayer([1-9])$', opt)
+ if m is not None:
+ players.append((int(m.group(1)), val))
+
+ players.sort()
+ ret = []
+ for quality, playcmd in players:
+ ret.append(playcmd)
+
+ # If the config file did not define any players use the default
+ # players
+ if not ret:
+ ret = list(DEFAULT_PLAYERS)
+
+ return ret
+
+def main(argv):
+ options = load_config({})
+ options = parse_command_line(argv, options)
+
+ client = WVClient(player_list(options),
+ options.get('download-limits', {}),
+ options.get('stream-limits', {}),
+ options.get('vfat', False))
+
+ if options.has_key('verbose'):
+ client.set_debug(options['verbose'])
+ if options.has_key('templatepath'):
+ client.set_template_path(options['templatepath'])
+
+ if options.has_key('url'):
+ stream = options['url']
+ if not client.download(stream):
+ # FIXME: more helpful error message if URL is not a
+ # supported site
+ sys.exit(1)
+
+ sys.exit(0)
+
+ shell = WVShell(client)
+ shell.cmdloop()
+
+if __name__ == '__main__':
+ main([])
diff --git a/src/webvicli/webvicli/menu.py b/src/webvicli/webvicli/menu.py
new file mode 100644
index 0000000..509ba2c
--- /dev/null
+++ b/src/webvicli/webvicli/menu.py
@@ -0,0 +1,177 @@
+# menu.py - menu elements for webvicli
+#
+# Copyright (c) 2009-2012 Antti Ajanki <antti.ajanki@iki.fi>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import textwrap
+import urllib
+
+LINEWIDTH = 72
+
+class Menu:
+ def __init__(self):
+ self.title = None
+ self.items = []
+
+ def __str__(self):
+ s = u''
+ if self.title:
+ s = self.title + '\n' + '='*len(self.title) + '\n'
+ for i, item in enumerate(self.items):
+ if isinstance(item, MenuItemTextArea):
+ num = ' '
+ else:
+ num = '%d.' % (i+1)
+
+ s += u'%s %s\n' % (num, unicode(item).replace('\n', '\n '))
+ return s
+
+ def __getitem__(self, i):
+ return self.items[i]
+
+ def __len__(self):
+ return len(self.items)
+
+ def add(self, menuitem):
+ self.items.append(menuitem)
+
+
+class MenuItemLink:
+ def __init__(self, label, ref, is_stream):
+ self.label = label
+ if type(ref) == unicode:
+ self.ref = ref.encode('utf-8')
+ else:
+ self.ref = ref
+ self.is_stream = is_stream
+
+ def __str__(self):
+ res = self.label
+ if not self.is_stream:
+ res = '[' + res + ']'
+ return res
+
+ def activate(self):
+ return self.ref
+
+
+class MenuItemTextField:
+ def __init__(self, label, name):
+ self.label = label
+ self.name = name
+ self.value = u''
+
+ def __str__(self):
+ return u'%s: %s' % (self.label, self.value)
+
+ def get_query(self):
+ return {self.name: self.value}
+
+ def activate(self):
+ self.value = unicode(raw_input('%s> ' % self.label), sys.stdin.encoding or 'utf-8')
+ return None
+
+
+class MenuItemTextArea:
+ def __init__(self, label):
+ self.label = label
+
+ def __str__(self):
+ return textwrap.fill(self.label, width=LINEWIDTH)
+
+ def activate(self):
+ return None
+
+
+class MenuItemList:
+ def __init__(self, label, name, items, values, stdout):
+ self.label = label
+ self.name = name
+ assert len(items) == len(values)
+ self.items = items
+ self.values = values
+ self.current = 0
+ self.stdout = stdout
+
+ def __str__(self):
+ itemstrings = []
+ for i, itemname in enumerate(self.items):
+ if i == self.current:
+ itemstrings.append('<' + itemname + '>')
+ else:
+ itemstrings.append(itemname)
+
+ lab = self.label + ': '
+ return textwrap.fill(u', '.join(itemstrings), width=LINEWIDTH,
+ initial_indent=lab,
+ subsequent_indent=' '*len(lab))
+
+ def get_query(self):
+ if (self.current >= 0) and (self.current < len(self.items)):
+ return {self.name: self.values[self.current]}
+ else:
+ return {}
+
+ def activate(self):
+ itemstrings = []
+ for i, itemname in enumerate(self.items):
+ itemstrings.append('%d. %s' % (i+1, itemname))
+
+ self.stdout.write(u'\n'.join(itemstrings).encode(self.stdout.encoding, 'replace'))
+ self.stdout.write('\n')
+
+ tmp = raw_input('Select item (1-%d)> ' % len(self.items))
+ try:
+ i = int(tmp)
+ if (i < 1) or (i > len(self.items)):
+ raise ValueError
+ self.current = i-1
+ except ValueError:
+ self.stdout.write('Must be an integer in the range 1 - %d\n' % len(self.items))
+ return None
+
+
+class MenuItemSubmitButton:
+ def __init__(self, label, baseurl, subitems, encoding):
+ self.label = label
+ if type(baseurl) == unicode:
+ self.baseurl = baseurl.encode('utf-8')
+ else:
+ self.baseurl = baseurl
+ self.subitems = subitems
+ self.encoding = encoding
+
+ def __str__(self):
+ return '[' + self.label + ']'
+
+ def activate(self):
+ baseurl = self.baseurl
+ if baseurl.find('?') == -1:
+ baseurl += '?'
+ else:
+ baseurl += '&'
+
+ parts = []
+ for sub in self.subitems:
+ for key, val in sub.get_query().iteritems():
+ try:
+ parts.append('subst=%s,%s' % \
+ (urllib.quote(key.encode(self.encoding, 'ignore')),
+ urllib.quote(val.encode(self.encoding, 'ignore'))))
+ except LookupError:
+ pass
+
+ return baseurl + '&'.join(parts)