diff options
author | Antti Ajanki <antti.ajanki@iki.fi> | 2013-08-06 09:07:49 +0300 |
---|---|---|
committer | Antti Ajanki <antti.ajanki@iki.fi> | 2013-08-06 09:07:49 +0300 |
commit | 7c81286a59639e139ac7e947378be24410701a5e (patch) | |
tree | 88e43b758dc2330e8711ebae80eee0039cc57322 /src | |
download | vdr-plugin-webvideo-7c81286a59639e139ac7e947378be24410701a5e.tar.gz vdr-plugin-webvideo-7c81286a59639e139ac7e947378be24410701a5e.tar.bz2 |
import to vdr-developer repo
Diffstat (limited to 'src')
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) |