diff options
Diffstat (limited to 'src')
58 files changed, 11370 insertions, 0 deletions
diff --git a/src/libwebvi/Makefile b/src/libwebvi/Makefile new file mode 100644 index 0000000..131c4a7 --- /dev/null +++ b/src/libwebvi/Makefile @@ -0,0 +1,34 @@ +PREFIX ?= /usr/local + +LIBNAME=libwebvi.so +LIBSONAME=$(LIBNAME).0 +LIBMINOR=$(LIBSONAME).4 + +VERSION:=$(shell cat ../version) +PYLIB:=$(shell python pythonlibname.py) +DEFINES:=-DPYTHONSHAREDLIB=\"$(PYLIB)\" -DLIBWEBVI_VERSION=\"$(VERSION)\" +# append -DDEBUG to DEFINES to get debug output + +all: $(LIBMINOR) + +libwebvi.o: libwebvi.c libwebvi.h + $(CC) -fPIC -Wall -O2 -g $(CFLAGS) $(DEFINES) `python-config --cflags` -c -o libwebvi.o libwebvi.c + +$(LIBMINOR): libwebvi.o + $(CC) -shared -Wl,-soname,$(LIBSONAME) -Wl,--as-needed libwebvi.o `python-config --ldflags` -o $(LIBMINOR) + ln -sf $(LIBMINOR) $(LIBSONAME) + ln -sf $(LIBSONAME) $(LIBNAME) + +libwebvi.a: libwebvi.o + ar rsc libwebvi.a libwebvi.o + +clean: + rm -f *.o *~ libwebvi.so* libwebvi.a + rm -f webvi/*.pyc webvi/*~ + +install: $(LIBMINOR) + mkdir -p $(PREFIX)/lib + cp --remove-destination -d $(LIBNAME)* $(PREFIX)/lib + /sbin/ldconfig $(PREFIX)/lib + +.PHONY: clean install diff --git a/src/libwebvi/libwebvi.c b/src/libwebvi/libwebvi.c new file mode 100644 index 0000000..c4d9aed --- /dev/null +++ b/src/libwebvi/libwebvi.c @@ -0,0 +1,814 @@ +/* + * libwebvi.c: C bindings for webvi Python module + * + * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <Python.h> +#include <stdio.h> +#include <dlfcn.h> + +#include "libwebvi.h" + +static const char *VERSION = "libwebvi/" LIBWEBVI_VERSION; + +static const int MAX_ERROR_MESSAGE_LENGTH = 512; +static const int MAX_MSG_STRING_LENGTH = 512; + +static PyThreadState *main_state = NULL; + +typedef struct per_interpreter_data_t { + PyThreadState *interp; + PyObject *webvi_module; + char *last_error; + WebviMsg latest_message; +} per_interpreter_data; + +#ifdef DEBUG + +#define debug(x...) fprintf(stderr, x) +#define handle_pyerr() { if (PyErr_Occurred()) { PyErr_Print(); } } + +#else + +#define debug(x...) +#define handle_pyerr() PyErr_Clear() + +#endif + + +/********************************************************************** + * + * Internal functions + */ + +static PyObject *call_python(PyObject *webvi_module, + const char *funcname, + PyObject *args) { + PyObject *func, *val = NULL; + +#ifdef DEBUG + debug("call_python %s ", funcname); + if (PyObject_Print(args, stderr, 0) == -1) + debug("<print failed>"); + debug("\n"); +#endif + + func = PyObject_GetAttrString(webvi_module, funcname); + if (func) { + val = PyObject_CallObject(func, args); + + Py_DECREF(func); + } + + return val; +} + +static long set_callback(PyObject *webvi_module, WebviHandle h, + WebviOption callbacktype, + webvi_callback callback, + PyObject *prototype) { + long res = WEBVIERR_UNKNOWN_ERROR; + + if (prototype && PyCallable_Check(prototype)) { + PyObject *args = Py_BuildValue("(l)", (long)callback); + PyObject *val = PyObject_CallObject(prototype, args); + Py_DECREF(args); + + if (val) { + PyObject *webvihandle = PyInt_FromLong(h); + PyObject *option = PyInt_FromLong(callbacktype); + PyObject *args2 = PyTuple_Pack(3, webvihandle, option, val); + PyObject *retval = call_python(webvi_module, "set_opt", args2); + Py_DECREF(args2); + Py_DECREF(option); + Py_DECREF(webvihandle); + Py_DECREF(val); + + if (retval) { + if (PyInt_Check(retval)) + res = PyInt_AsLong(retval); + Py_DECREF(retval); + } + } + } + + if (res == WEBVIERR_UNKNOWN_ERROR) + handle_pyerr(); + + return res; +} + +/* + * Converts PyInt to WebviResult. + * + * If intobject is NULL, assumes that a Python exception has occurred. + */ +static WebviResult pyint_as_webviresult(PyObject *intobject) { + if (intobject && PyInt_Check(intobject)) + return PyInt_AsLong(intobject); + + handle_pyerr(); + + return WEBVIERR_UNKNOWN_ERROR; +} + +/* + * Duplicate Python string as C string. If the parameter is a unicode + * object, it is encoded to UTF-8. The caller must free the returned + * memory. + */ +static char *PyString_strdupUTF8(PyObject *string) { + char *buffer = NULL; + Py_ssize_t len = -1; + char *ret = NULL; + PyObject *realstring = string; + Py_INCREF(realstring); + + if (PyUnicode_Check(realstring)) { + PyObject *encoded = PyUnicode_AsUTF8String(realstring); + if (encoded) { + Py_DECREF(realstring); + realstring = encoded; + } else { + handle_pyerr(); + } + } + + if (PyString_AsStringAndSize(realstring, &buffer, &len) == -1) { + handle_pyerr(); + buffer = ""; + len = 0; + } + + if (buffer) { + ret = (char *)malloc((len+1)*sizeof(char)); + if (ret) + memcpy(ret, buffer, len+1); + } + + Py_DECREF(realstring); + + return ret; +} + +/********************************************************************** + * + * Public functions + */ + +int webvi_global_init() { + if (main_state) + return 0; + + // Python modules in lib-dynload/*.so do not correctly depend on + // libpython*.so. We need to dlopen the library here, otherwise + // importing webvi dies with "undefined symbol: + // PyExc_ValueError". See http://bugs.python.org/issue4434 + dlopen(PYTHONSHAREDLIB, RTLD_LAZY | RTLD_GLOBAL); + + Py_InitializeEx(0); + PyEval_InitThreads(); + main_state = PyThreadState_Get(); + PyEval_ReleaseLock(); /* release GIL acquired by PyEval_InitThreads */ + + return 0; +} + +void webvi_cleanup(int cleanup_python) { + /* Should we kill active interpreters first? */ + + if (cleanup_python != 0) { + PyEval_AcquireLock(); + PyThreadState_Swap(main_state); + Py_Finalize(); + } +} + +WebviCtx webvi_initialize_context(void) { + per_interpreter_data *ctx = (per_interpreter_data *)malloc(sizeof(per_interpreter_data)); + if (!ctx) + goto err; + + PyEval_AcquireLock(); + + ctx->interp = NULL; + ctx->last_error = NULL; + ctx->latest_message.msg = 0; + ctx->latest_message.handle = -1; + ctx->latest_message.status_code = -1; + ctx->latest_message.data = (char *)malloc(MAX_MSG_STRING_LENGTH*sizeof(char)); + if (!ctx->latest_message.data) + goto err; + + ctx->interp = Py_NewInterpreter(); + if (!ctx->interp) { + debug("Py_NewInterpreter failed\n"); + goto err; + } + + PyThreadState_Swap(ctx->interp); + + ctx->webvi_module = PyImport_ImportModule("webvi.api"); + if (!ctx->webvi_module) { + debug("import webvi.api failed\n"); + handle_pyerr(); + goto err; + } + + /* These are used to wrap C-callbacks into Python callables. + Keep in sync with libwebvi.h. */ + if (PyRun_SimpleString("from ctypes import CFUNCTYPE, c_int, c_size_t, c_char_p, c_void_p\n" + "WriteCallback = CFUNCTYPE(c_size_t, c_char_p, c_size_t, c_void_p)\n" + "ReadCallback = CFUNCTYPE(c_size_t, c_char_p, c_size_t, c_void_p)\n") != 0) { + debug("callback definitions failed\n"); + goto err; + } + + PyEval_ReleaseThread(ctx->interp); + + return (WebviCtx)ctx; + +err: + if (ctx) { + if (ctx->interp) { + Py_EndInterpreter(ctx->interp); + PyThreadState_Swap(NULL); + } + + PyEval_ReleaseLock(); + + if (ctx->latest_message.data) + free(ctx->latest_message.data); + free(ctx); + } + + return 0; +} + +void webvi_cleanup_context(WebviCtx ctx) { + if (ctx == 0) + return; + + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyThreadState_Swap(c->interp); + + /* FIXME: explicitly terminate all active handles? */ + + Py_DECREF(c->webvi_module); + c->webvi_module = NULL; + + Py_EndInterpreter(c->interp); + c->interp = NULL; + + PyThreadState_Swap(NULL); + + free(c); +} + +const char* webvi_version(void) { + return VERSION; +} + +const char* webvi_strerror(WebviCtx ctx, WebviResult res) { + char *errmsg; + + per_interpreter_data *c = (per_interpreter_data *)ctx; + + if (!c->last_error) { + /* We are going to leak c->last_error */ + c->last_error = (char *)malloc(MAX_ERROR_MESSAGE_LENGTH*sizeof(char)); + if (!c->last_error) + return NULL; + } + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(i)", res); + PyObject *msg = call_python(c->webvi_module, "strerror", args); + Py_DECREF(args); + + if (msg) { + errmsg = PyString_AsString(msg); + if (!errmsg) { + handle_pyerr(); + errmsg = "Internal error"; + } + + strncpy(c->last_error, errmsg, MAX_ERROR_MESSAGE_LENGTH-1); + c->last_error[MAX_ERROR_MESSAGE_LENGTH] = '\0'; + + Py_DECREF(msg); + } else { + handle_pyerr(); + } + + PyEval_ReleaseThread(c->interp); + + return c->last_error; +} + +WebviResult webvi_set_config(WebviCtx ctx, WebviConfig conf, const char *value) { + WebviResult res; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(is)", conf, value); + PyObject *v = call_python(c->webvi_module, "set_config", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviHandle webvi_new_request(WebviCtx ctx, const char *webvireference, WebviRequestType type) { + WebviHandle res = -1; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(si)", webvireference, type); + PyObject *v = call_python(c->webvi_module, "new_request", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_start_handle(WebviCtx ctx, WebviHandle h) { + WebviResult res; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(i)", h); + PyObject *v = call_python(c->webvi_module, "start_handle", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_stop_handle(WebviCtx ctx, WebviHandle h) { + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(i)", h); + PyObject *v = call_python(c->webvi_module, "stop_handle", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_delete_handle(WebviCtx ctx, WebviHandle h) { + WebviResult res; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(i)", h); + PyObject *v = call_python(c->webvi_module, "delete_handle", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_set_opt(WebviCtx ctx, WebviHandle h, WebviOption opt, ...) { + va_list argptr; + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *m = PyImport_AddModule("__main__"); + if (!m) { + handle_pyerr(); + PyEval_ReleaseThread(c->interp); + return res; + } + + PyObject *maindict = PyModule_GetDict(m); + + va_start(argptr, opt); + + switch (opt) { + case WEBVIOPT_WRITEFUNC: + { + webvi_callback writerptr = va_arg(argptr, webvi_callback); + PyObject *write_prototype = PyDict_GetItemString(maindict, "WriteCallback"); + if (write_prototype) + res = set_callback(c->webvi_module, h, WEBVIOPT_WRITEFUNC, + writerptr, write_prototype); + break; + } + + case WEBVIOPT_WRITEDATA: + { + void *data = va_arg(argptr, void *); + PyObject *args = Py_BuildValue("(iil)", h, WEBVIOPT_WRITEDATA, (long)data); + PyObject *v = call_python(c->webvi_module, "set_opt", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + break; + } + + case WEBVIOPT_READFUNC: + { + webvi_callback readerptr = va_arg(argptr, webvi_callback); + PyObject *read_prototype = PyDict_GetItemString(maindict, "ReadCallback"); + if (read_prototype) + res = set_callback(c->webvi_module, h, WEBVIOPT_READFUNC, + readerptr, read_prototype); + break; + } + + case WEBVIOPT_READDATA: + { + void *data = va_arg(argptr, void *); + PyObject *args = Py_BuildValue("(iil)", h, WEBVIOPT_READDATA, (long)data); + PyObject *v = call_python(c->webvi_module, "set_opt", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + break; + } + + default: + res = WEBVIERR_INVALID_PARAMETER; + break; + } + + va_end(argptr); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_get_info(WebviCtx ctx, WebviHandle h, WebviInfo info, ...) { + va_list argptr; + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + va_start(argptr, info); + + switch (info) { + case WEBVIINFO_URL: + { + char **dest = va_arg(argptr, char **); + PyObject *args = Py_BuildValue("(ii)", h, info); + PyObject *v = call_python(c->webvi_module, "get_info", args); + Py_DECREF(args); + + *dest = NULL; + + if (v) { + if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *val = PySequence_GetItem(v, 1); + + if (PyInt_Check(retval) && + (PyString_Check(val) || PyUnicode_Check(val))) { + *dest = PyString_strdupUTF8(val); + res = PyInt_AsLong(retval); + } + + Py_DECREF(val); + Py_DECREF(retval); + } + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + break; + } + + case WEBVIINFO_CONTENT_LENGTH: + { + long *dest = va_arg(argptr, long *); + PyObject *args = Py_BuildValue("(ii)", h, info); + PyObject *v = call_python(c->webvi_module, "get_info", args); + Py_DECREF(args); + + *dest = -1; + + if (v) { + if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *val = PySequence_GetItem(v, 1); + + if (PyInt_Check(retval) && PyInt_Check(val)) { + *dest = PyInt_AsLong(val); + res = PyInt_AsLong(retval); + } + + Py_DECREF(val); + Py_DECREF(retval); + } + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + break; + } + + case WEBVIINFO_CONTENT_TYPE: + { + char **dest = va_arg(argptr, char **); + PyObject *args = Py_BuildValue("(ii)", h, info); + PyObject *v = call_python(c->webvi_module, "get_info", args); + Py_DECREF(args); + + *dest = NULL; + + if (v) { + if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *val = PySequence_GetItem(v, 1); + + if (PyInt_Check(retval) && + (PyString_Check(val) || PyUnicode_Check(val))) { + *dest = PyString_strdupUTF8(val); + res = PyInt_AsLong(retval); + } + + Py_DECREF(val); + Py_DECREF(retval); + } + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + break; + } + + case WEBVIINFO_STREAM_TITLE: + { + char **dest = va_arg(argptr, char **); + PyObject *args = Py_BuildValue("(ii)", h, info); + PyObject *v = call_python(c->webvi_module, "get_info", args); + Py_DECREF(args); + + *dest = NULL; + + if (v) { + if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *val = PySequence_GetItem(v, 1); + + if (PyInt_Check(retval) && + (PyString_Check(val) || PyUnicode_Check(val))) { + *dest = PyString_strdupUTF8(val); + res = PyInt_AsLong(retval); + } + + Py_DECREF(val); + Py_DECREF(retval); + } + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + break; + } + + default: + res = WEBVIERR_INVALID_PARAMETER; + break; + } + + va_end(argptr); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_fdset(WebviCtx ctx, + fd_set *read_fd_set, + fd_set *write_fd_set, + fd_set *exc_fd_set, + int *max_fd) +{ + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *v = call_python(c->webvi_module, "fdset", NULL); + + if (v && PySequence_Check(v) && (PySequence_Length(v) == 5)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *readfd = PySequence_GetItem(v, 1); + PyObject *writefd = PySequence_GetItem(v, 2); + PyObject *excfd = PySequence_GetItem(v, 3); + PyObject *maxfd = PySequence_GetItem(v, 4); + PyObject *fd; + int i; + + if (readfd && PySequence_Check(readfd)) { + for (i=0; i<PySequence_Length(readfd); i++) { + fd = PySequence_GetItem(readfd, i); + if (fd && PyInt_Check(fd)) + FD_SET(PyInt_AsLong(fd), read_fd_set); + else + handle_pyerr(); + + Py_XDECREF(fd); + } + } + + if (writefd && PySequence_Check(writefd)) { + for (i=0; i<PySequence_Length(writefd); i++) { + fd = PySequence_GetItem(writefd, i); + if (fd && PyInt_Check(fd)) + FD_SET(PyInt_AsLong(fd), write_fd_set); + else + handle_pyerr(); + + Py_XDECREF(fd); + } + } + + if (excfd && PySequence_Check(excfd)) { + for (i=0; i<PySequence_Length(excfd); i++) { + fd = PySequence_GetItem(excfd, i); + if (fd && PyInt_Check(fd)) + FD_SET(PyInt_AsLong(fd), exc_fd_set); + else + handle_pyerr(); + + Py_XDECREF(fd); + } + } + + if (maxfd && PyInt_Check(maxfd)) + *max_fd = PyInt_AsLong(maxfd); + else + handle_pyerr(); + + if (retval && PyInt_Check(retval)) + res = PyInt_AsLong(retval); + else + handle_pyerr(); + + Py_XDECREF(maxfd); + Py_XDECREF(excfd); + Py_XDECREF(writefd); + Py_XDECREF(readfd); + Py_XDECREF(retval); + } else { + handle_pyerr(); + } + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_perform(WebviCtx ctx, int fd, int ev_bitmask, long *running_handles) { + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(ii)", fd, ev_bitmask); + PyObject *v = call_python(c->webvi_module, "perform", args); + Py_DECREF(args); + + if (v && (PySequence_Check(v) == 1) && (PySequence_Size(v) == 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *numhandles = PySequence_GetItem(v, 1); + + if (PyInt_Check(numhandles)) + *running_handles = PyInt_AsLong(numhandles); + if (PyInt_Check(retval)) + res = PyInt_AsLong(retval); + + Py_DECREF(numhandles); + Py_DECREF(retval); + } else { + handle_pyerr(); + } + + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviMsg *webvi_get_message(WebviCtx ctx, int *remaining_messages) { + per_interpreter_data *c = (per_interpreter_data *)ctx; + + WebviMsg *msg = NULL; + + PyEval_AcquireThread(c->interp); + + PyObject *v = call_python(c->webvi_module, "pop_message", NULL); + + if (v) { + if ((PySequence_Check(v) == 1) && (PySequence_Length(v) == 4)) { + msg = &(c->latest_message); + msg->msg = WEBVIMSG_DONE; + msg->handle = -1; + msg->status_code = -1; + msg->data[0] = '\0'; + + PyObject *handle = PySequence_GetItem(v, 0); + if (handle && PyInt_Check(handle)) + msg->handle = (WebviHandle)PyInt_AsLong(handle); + Py_XDECREF(handle); + + PyObject *status = PySequence_GetItem(v, 1); + if (status && PyInt_Check(status)) + msg->status_code = (int)PyInt_AsLong(status); + Py_XDECREF(status); + + PyObject *errmsg = PySequence_GetItem(v, 2); + if (errmsg && + (PyString_Check(errmsg) || PyUnicode_Check(errmsg))) { + char *cstr = PyString_strdupUTF8(errmsg); + if (cstr) { + strncpy(msg->data, cstr, MAX_MSG_STRING_LENGTH); + msg->data[MAX_MSG_STRING_LENGTH-1] = '\0'; + + free(cstr); + } + } + Py_XDECREF(errmsg); + + PyObject *remaining = PySequence_GetItem(v, 3); + if (remaining && PyInt_Check(remaining)) + *remaining_messages = (int)PyInt_AsLong(remaining); + else + *remaining_messages = 0; + Py_XDECREF(remaining); + } + + if (msg->handle == -1) + msg = NULL; + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + PyEval_ReleaseThread(c->interp); + + return msg; +} diff --git a/src/libwebvi/libwebvi.h b/src/libwebvi/libwebvi.h new file mode 100644 index 0000000..dd7ff39 --- /dev/null +++ b/src/libwebvi/libwebvi.h @@ -0,0 +1,330 @@ +/* + * libwebvi.h: C bindings for webvi Python module + * + * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __LIBWEBVI_H +#define __LIBWEBVI_H + +#include <sys/select.h> +#include <stdlib.h> + +typedef int WebviHandle; + +typedef ssize_t (*webvi_callback)(const char *, size_t, void *); + +typedef enum { + WEBVIMSG_DONE +} WebviMsgType; + +typedef struct { + WebviMsgType msg; + WebviHandle handle; + int status_code; + char *data; +} WebviMsg; + +typedef enum { + WEBVIREQ_MENU, + WEBVIREQ_FILE, + WEBVIREQ_STREAMURL +} WebviRequestType; + +typedef enum { + WEBVIERR_UNKNOWN_ERROR = -1, + WEBVIERR_OK = 0, + WEBVIERR_INVALID_HANDLE, + WEBVIERR_INVALID_PARAMETER +} WebviResult; + +typedef enum { + WEBVIOPT_WRITEFUNC, + WEBVIOPT_READFUNC, + WEBVIOPT_WRITEDATA, + WEBVIOPT_READDATA, +} WebviOption; + +typedef enum { + WEBVIINFO_URL, + WEBVIINFO_CONTENT_LENGTH, + WEBVIINFO_CONTENT_TYPE, + WEBVIINFO_STREAM_TITLE +} WebviInfo; + +typedef enum { + WEBVI_SELECT_TIMEOUT = 0, + WEBVI_SELECT_READ = 1, + WEBVI_SELECT_WRITE = 2, + WEBVI_SELECT_EXCEPTION = 4 +} WebviSelectBitmask; + +typedef enum { + WEBVI_CONFIG_TEMPLATE_PATH +} WebviConfig; + +typedef long WebviCtx; + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Initialize the library. Must be called before any other functions + * (the only exception is webvi_version() which can be called before + * the library is initialized). + * + * Returns 0, if initialization succeeds. + */ +int webvi_global_init(void); + +/* + * Frees all resources currently used by libwebvi and terminates all + * active connections. Do not call any libwebvi function after this. + * If the cleanup_python equals 0, the Python library is deinitialized + * by calling Py_Finalize(), otherwise the Python library is left + * loaded to be used by other modules of the program. + */ +void webvi_cleanup(int cleanup_python); + +/* + * Create a new context. A valid context is required for calling other + * functions in the library. The created contextes are independent of + * each other. The context must be destroyed by a call to + * webvi_cleanup_context when no longer needed. + * + * Return value 0 indicates an error. + */ +WebviCtx webvi_initialize_context(void); + +/* + * Free resources allocated by context ctx. The context can not be + * used anymore after a call to this function. + */ +void webvi_cleanup_context(WebviCtx ctx); + +/* + * Return the version of libwebvi as a string. The returned value + * points to a status buffer, and the caller should modify or not free() it. + */ +const char* webvi_version(void); + +/* + * Return a string describing an error code. The returned value points + * to a status buffer, and the caller should not modify or free() it. + */ +const char* webvi_strerror(WebviCtx ctx, WebviResult err); + +/* + * Set a new value for a global configuration option conf. + * + * Currently the only legal value for conf is TEMPLATE_PATH, which + * sets the base directory for the XSLT templates. + * + * The string pointed by value is copied to the library. + */ +WebviResult webvi_set_config(WebviCtx ctx, WebviConfig conf, const char *value); + +/* + * Creates a new download request. + * + * webvireference is a wvt:// URI of the resource that should be + * downloaded. type should be WEBVIREQ_MENU, if the resource should be + * transformed into a XML menu (that is if webvireferece comes from + * <ref> tag), WEBVIREQ_FILE, if the resource points to a media stream + * (from <stream> tag) whose contents should be downloaded, or + * WEBVIREQ_STREAMURL, if the resource is points to a media stream + * whose real URL should be resolved. + * + * Typically, the reference has been acquired from a previously + * downloaded menu. A special constant "wvt:///?srcurl=mainmenu" with + * type WEBVIREQ_MENU can be used to download mainmenu. + * + * The return value is a handle to the newly created request. Value -1 + * indicates an error. + * + * The request is initialized but the actual network transfer is not + * started. You can set up additional configuration options on the + * handle using webvi_set_opt() before starting the handle with + * webvi_start_handle(). + */ +WebviHandle webvi_new_request(WebviCtx ctx, const char *wvtreference, WebviRequestType type); + +/* + * Starts the transfer on handle h. The transfer one or more sockets + * whose file descriptors are returned by webvi_fdset(). The actual + * transfer is done during webvi_perform() calls. + */ +WebviResult webvi_start_handle(WebviCtx ctx, WebviHandle h); + +/* + * Requests that the transfer on handle h shoud be aborted. After the + * library has actually finished aborting the transfer, the handle h + * is returned by webvi_get_message() with non-zero status code. + */ +WebviResult webvi_stop_handle(WebviCtx ctx, WebviHandle h); + +/* + * Frees resources associated with handle h. The handle can not be + * used after this call. If the handle is still in the middle of a + * transfer, the transfer is forcefully aborted. + */ +WebviResult webvi_delete_handle(WebviCtx ctx, WebviHandle h); + +/* + * Sets configuration options that changes behaviour of the handle. + * opt is one of the values of WebviOption enum as indicated below. + * The fourth parameter sets the value of the specified option. Its + * type depends on opt as discussed below. + * + * Possible values for opt: + * + * WEBVIOPT_WRITEFUNC + * + * Set the callback function that shall be called when data is read + * from the network. The fourth parameter is a pointer to the callback + * funtion + * + * ssize_t (*webvi_callback)(const char *, size_t, void *). + * + * When the function is called, the first parameter is a pointer to + * the incoming data, the second parameters is the size of the + * incoming data block in bytes, and the third parameter is a pointer + * to user's data structure can be set by WEBVIOPT_WRITEDATA option. + * + * The callback funtion should return the number of bytes is + * processed. If this differs from the size of the incoming data + * block, it indicates that an error occurred and the transfer will be + * aborted. + * + * If write callback has not been set (or if it is set to NULL) the + * incoming data is printed to stdout. + * + * WEBVIOPT_WRITEDATA + * + * Sets the value that will be passed to the write callback. The + * fourth parameter is of type void *. + * + * WEBVIOPT_READFUNC + * + * Set the callback function that shall be called when data is to be + * send to network. The fourth parameter is a pointer to the callback + * funtion + * + * ssize_t (*webvi_callback)(const char *, size_t, void *) + * + * The first parameter is a pointer to a buffer where the data that is + * to be sent should be written. The second parameter is the maximum + * size of the buffer. The thirs parameter is a pointer to user data + * set with WEBVIOPT_READDATA. + * + * The return value should be the number of bytes actually written to + * the buffer. If the return value is -1, the transfer is aborted. + * + * WEBVIOPT_READDATA + * + * Sets the value that will be passed to the read callback. The + * fourth parameter is of type void *. + * + */ +WebviResult webvi_set_opt(WebviCtx ctx, WebviHandle h, WebviOption opt, ...); + +/* + * Get information specific to a WebviHandle. The value will be + * written to the memory location pointed by the third argument. The + * type of the pointer depends in the second parameter as discused + * below. + * + * Available information: + * + * WEBVIINFO_URL + * + * Receive URL. The third parameter must be a pointer to char *. The + * caller must free() the memory. + * + * WEBVIINFO_CONTENT_LENGTH + * + * Receive the value of Content-length field, or -1 if the size is + * unknown. The third parameter must be a pointer to long. + * + * WEBVIINFO_CONTENT_TYPE + * + * Receive the Content-type string. The returned value is NULL, if the + * Content-type is unknown. The third parameter must be a pointer to + * char *. The caller must free() the memory. + * + * WEBVIINFO_STREAM_TITLE + * + * Receive stream title. The returned value is NULL, if title is + * unknown. The third parameter must be a pointer to char *. The + * caller must free() the memory. + * + */ +WebviResult webvi_get_info(WebviCtx ctx, WebviHandle h, WebviInfo info, ...); + +/* + * Get active file descriptors in use by the library. The file + * descriptors that should be waited for reading, writing or + * exceptions are returned in read_fd_set, write_fd_set and + * exc_fd_set, respectively. The fd_sets are not cleared, but the new + * file descriptors are added to them. max_fd will contain the highest + * numbered file descriptor that was returned in one of the fd_sets. + * + * One should wait for action in one of the file descriptors returned + * by this function using select(), poll() or similar system call, + * and, after seeing action on a file descriptor, call webvi_perform + * on that descriptor. + */ +WebviResult webvi_fdset(WebviCtx ctx, fd_set *readfd, fd_set *writefd, fd_set *excfd, int *max_fd); + +/* + * Perform input or output action on a file descriptor. + * + * activefd is a file descriptor that was returned by an earlier call + * to webvi_fdset and has been signalled to be ready by select() or + * similar funtion. ev_bitmask should be OR'ed combination of + * WEBVI_SELECT_READ, WEBVI_SELECT_WRITE, WEBVI_SELECT_EXCEPTION to + * indicate that activefd has been signalled to be ready for reading, + * writing or being in exception state, respectively. ev_bitmask can + * also set to WEBVI_SELECT_TIMEOUT which means that the state is + * checked internally. On return, running_handles will contain the + * number of still active file descriptors. + * + * This function should be called with activefd set to 0 and + * ev_bitmask to WEBVI_SELECT_TIMEOUT periodically (every few seconds) + * even if no file descriptors have become ready to allow for timeout + * handling and other internal tasks. + */ +WebviResult webvi_perform(WebviCtx ctx, int sockfd, int ev_bitmask, long *running_handles); + +/* + * Return the next message from the message queue. Currently the only + * message, WEBVIMSG_DONE, indicates that a transfer on a handle has + * finished. The number of messages remaining in the queue after this + * call is written to remaining_messages. The pointers in the returned + * WebviMsg point to handle's internal buffers and is valid until the + * next call to webvi_get_message(). The caller should free the + * returned WebviMsg. The return value is NULL if there is no messages + * in the queue. + */ +WebviMsg *webvi_get_message(WebviCtx ctx, int *remaining_messages); + +#ifdef __cplusplus +} +#endif + + +#endif diff --git a/src/libwebvi/pythonlibname.py b/src/libwebvi/pythonlibname.py new file mode 100755 index 0000000..48f4b97 --- /dev/null +++ b/src/libwebvi/pythonlibname.py @@ -0,0 +1,14 @@ +#!/usr/bin/python + +import distutils.sysconfig +import os +import os.path + +libdir = distutils.sysconfig.get_config_var('LIBDIR') +ldlibrary = distutils.sysconfig.get_config_var('LDLIBRARY') + +libfile = os.readlink(os.path.join(libdir, ldlibrary)) +if not os.path.isabs(libfile): + libfile = os.path.join(libdir, libfile) + +print libfile diff --git a/src/libwebvi/webvi/__init__.py b/src/libwebvi/webvi/__init__.py new file mode 100644 index 0000000..b6d50d5 --- /dev/null +++ b/src/libwebvi/webvi/__init__.py @@ -0,0 +1 @@ +__all__ = ['api', 'asyncurl', 'constants', 'download', 'request', 'utils'] diff --git a/src/libwebvi/webvi/api.py b/src/libwebvi/webvi/api.py new file mode 100644 index 0000000..2fb24ab --- /dev/null +++ b/src/libwebvi/webvi/api.py @@ -0,0 +1,289 @@ +# api.py - webvi API +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""webvi API + +Example workflow: + +1) Create a new request. ref is a wvt:// URI. + +handle = new_request(ref, WebviRequestType.MENU) + +2) Setup a callback function: + +setopt(handle, WebviOpt.WRITEFUNC, my_callback) + +3) Start the network transfer: + +start_handle(handle) + +4) Get active file descriptors, wait for activity on them, and let +webvi process the file descriptor. + +import select + +... + +readfd, writefd, excfd = fdset()[1:4] +readfd, writefd, excfd = select.select(readfd, writefd, excfd, 5.0) +for fd in readfd: + perform(fd, WebviSelectBitmask.READ) +for fd in writefd: + perform(fd, WebviSelectBitmask.WRITE) + +5) Iterate 4) until pop_message returns handle, which indicates that +the request has been completed. + +finished, status, errmsg, remaining = pop_message() +if finished == handle: + print 'done' +""" + +import request +import asyncore +import asyncurl +from constants import WebviErr, WebviOpt, WebviInfo, WebviSelectBitmask, WebviConfig + +# Human readable messages for WebviErr items +error_messages = { + WebviErr.OK: 'Succeeded', + WebviErr.INVALID_HANDLE: 'Invalid handle', + WebviErr.INVALID_PARAMETER: "Invalid parameter", + WebviErr.INTERNAL_ERROR: "Internal error" + } + +# Module-level variables +finished_queue = [] +request_list = request.RequestList() +socket_map = asyncore.socket_map + +# Internal functions + +class MyRequest(request.Request): + def request_done(self, err, errmsg): + """Calls the inherited function and puts the handle of the + finished request to the finished_queue.""" + finished_queue.append(self) + request.Request.request_done(self, err, errmsg) + +# Public functions + +def strerror(err): + """Return human readable error message for conststants.WebviErr""" + try: + return error_messages[err] + except KeyError: + return error_messages[WebviErr.INTERNAL_ERROR] + +def set_config(conf, value): + """Set a new value for a global configuration option conf. + + Currently the only legal value for conf is + constants.WebviConfig.TEMPLATE_PATH, which sets the base directory + for the XSLT templates. + """ + if conf == WebviConfig.TEMPLATE_PATH: + request.set_template_path(value) + return WebviErr.OK + else: + return WebviErr.INVALID_PARAMETER + +def new_request(reference, reqtype): + """Create a new request. + + reference is a wvt:// URI which typically comes from previously + opened menu. reqtype is one of conststants.WebviRequestType and + indicates wheter the reference is a navigation menu, stream that + should be downloaded, or a stream whose URL should be returned. + + Returns a handle (an integer) will be given to following + functions. Return value -1 indicates an error. + """ + req = MyRequest(reference, reqtype) + + if req.srcurl is None: + return -1 + + return request_list.put(req) + +def set_opt(handle, option, value): + """Set configuration options on a handle. + + option specifies option's name (one of constants.WebviOpt values) + and value is the new value for the option. + """ + + try: + req = request_list[handle] + except KeyError: + return WebviErr.INVALID_HANDLE + + if option == WebviOpt.WRITEFUNC: + req.writefunc = value + elif option == WebviOpt.WRITEDATA: + req.writedata = value + elif option == WebviOpt.READFUNC: + req.readfunc = value + elif option == WebviOpt.READDATA: + req.readdata = value + else: + return WebviErr.INVALID_PARAMETER + + return WebviErr.OK + +def get_info(handle, info): + """Get information about a handle. + + info is the type of data that is to be returned (one of + constants.WebviInfo values). + """ + try: + req = request_list[handle] + except KeyError: + return (WebviErr.INVALID_HANDLE, None) + + val = None + if info == WebviInfo.URL: + if req.dl is not None: + val = req.dl.get_url() + else: + val = req.srcurl + elif info == WebviInfo.CONTENT_LENGTH: + val = req.contentlength + elif info == WebviInfo.CONTENT_TYPE: + val = req.contenttype + elif info == WebviInfo.STREAM_TITLE: + val = req.streamtitle + else: + return (WebviErr.INVALID_PARAMETER, None) + + return (WebviErr.OK, val) + +def start_handle(handle): + """Start the network transfer on a handle.""" + try: + req = request_list[handle] + except KeyError: + return WebviErr.INVALID_HANDLE + + req.start() + return WebviErr.OK + +def stop_handle(handle): + """Aborts network transfer on a handle. + + The abort is confirmed by pop_message() returning the handle with + an non-zero error code. + """ + try: + req = request_list[handle] + except KeyError: + return WebviErr.INVALID_HANDLE + + if not req.is_finished(): + req.stop() + + return WebviErr.OK + +def delete_handle(handle): + """Frees resources related to handle. + + This should be called when the transfer has been completed and the + user is done with the handle. If the transfer is still in progress + when delete_handle() is called, the transfer is aborted. After + calling delete_handle() the handle value will be invalid, and + should not be feed to other functions anymore. + """ + try: + del request_list[handle] + except KeyError: + return WebviErr.INVALID_HANDLE + + return WebviErr.OK + +def pop_message(): + """Retrieve messages about finished requests. + + If a request has been finished since the last call to this + function, returns a tuple (handle, status, msg, num_messages), + where handle identifies the finished request, status is a numeric + status code (non-zero for an error), msg is a description of an + error as string, and num_messages is the number of messages that + can be retrieved by calling pop_messages() again immediately. If + the finished requests queue is empty, returns (-1, -1, "", 0). + """ + if finished_queue: + req = finished_queue.pop() + return (req.handle, req.status, req.errmsg, len(finished_queue)) + else: + return (-1, -1, "", 0) + +def fdset(): + """Get the list of file descriptors that are currently in use by + the library. + + Returrns a tuple, where the first item is a constants.WebviErr + value indicating the success of the call, the next three values + are lists of descriptors that should be monitored for reading, + writing, and exceptional conditions, respectively. The last item + is the maximum of the file descriptors in the three lists. + """ + readfd = [] + writefd = [] + excfd = [] + maxfd = -1 + + for fd, disp in socket_map.iteritems(): + if disp.readable(): + readfd.append(fd) + if fd > maxfd: + maxfd = fd + if disp.writable(): + writefd.append(fd) + if fd > maxfd: + maxfd = fd + + return (WebviErr.OK, readfd, writefd, excfd, maxfd) + +def perform(fd, ev_bitmask): + """Perform transfer on file descriptor fd. + + fd is a file descriptor that has been signalled to be ready by + select() or similar system call. ev_bitmask specifies what kind of + activity has been detected using values of + constants.WebviSelectBitmask. If ev_bitmask is + constants.WebviSelectBitmask.TIMEOUT the type of activity is check + by the function. + + This function should be called every few seconds with fd=-1, + ev_bitmask=constants.WebviSelectBitmask.TIMEOUT even if no + activity has been signalled on the file descriptors to ensure + correct handling of timeouts and other internal processing. + """ + if fd < 0: + asyncurl.poll() + else: + disp = socket_map.get(fd) + if disp is not None: + if ev_bitmask & WebviSelectBitmask.READ != 0 or \ + (ev_bitmask == 0 and disp.readable()): + disp.handle_read_event() + if ev_bitmask & WebviSelectBitmask.WRITE != 0 or \ + (ev_bitmask == 0 and disp.writable()): + disp.handle_write_event() + + return (WebviErr.OK, len(socket_map)) diff --git a/src/libwebvi/webvi/asyncurl.py b/src/libwebvi/webvi/asyncurl.py new file mode 100644 index 0000000..afc575a --- /dev/null +++ b/src/libwebvi/webvi/asyncurl.py @@ -0,0 +1,389 @@ +# asyncurl.py - Wrapper class for using pycurl objects in asyncore +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""This is a wrapper for using pycurl objects in asyncore. + +Start a transfer by creating an async_curl_dispatch, and call +asyncurl.loop() instead of asyncore.loop(). +""" + +import asyncore +import pycurl +import traceback +import os +import select +import time +import cStringIO +from errno import EINTR + +SOCKET_TIMEOUT = pycurl.SOCKET_TIMEOUT +CSELECT_IN = pycurl.CSELECT_IN +CSELECT_OUT = pycurl.CSELECT_OUT +CSELECT_ERR = pycurl.CSELECT_ERR + +def poll(timeout=0.0, map=None, mdisp=None): + if map is None: + map = asyncore.socket_map + if mdisp is None: + mdisp = multi_dispatcher + if map: + timeout = min(timeout, mdisp.timeout/1000.0) + + r = []; w = []; e = [] + for fd, obj in map.items(): + is_r = obj.readable() + is_w = obj.writable() + if is_r: + r.append(fd) + if is_w: + w.append(fd) + if is_r or is_w: + e.append(fd) + if [] == r == w == e: + time.sleep(timeout) + else: + try: + r, w, e = select.select(r, w, e, timeout) + except select.error, err: + if err[0] != EINTR: + raise + else: + return + + if [] == r == w == e: + mdisp.socket_action(SOCKET_TIMEOUT, 0) + return + + for fd in r: + obj = map.get(fd) + if obj is None: + continue + asyncore.read(obj) + + for fd in w: + obj = map.get(fd) + if obj is None: + continue + asyncore.write(obj) + + for fd in e: + obj = map.get(fd) + if obj is None: + continue + asyncore._exception(obj) + +def loop(timeout=30.0, use_poll=False, map=None, count=None, mdisp=None): + if map is None: + map = asyncore.socket_map + if mdisp is None: + mdisp = multi_dispatcher + + if use_poll and hasattr(select, 'poll'): + print 'poll2 not implemented' + poll_fun = poll + + if count is None: + while map: + poll_fun(timeout, map, mdisp) + + else: + while map and count > 0: + poll_fun(timeout, map, mdisp) + count = count - 1 + +def noop_callback(s): + pass + + +class curl_multi_dispatcher: + """A dispatcher for pycurl.CurlMulti() objects. An instance of + this class is created automatically. There is usually no need to + construct one manually.""" + def __init__(self, socket_map=None): + if socket_map is None: + self._map = asyncore.socket_map + else: + self._map = socket_map + self.dispatchers = {} + self.timeout = 1000 + self._sockets_removed = False + self._curlm = pycurl.CurlMulti() + self._curlm.setopt(pycurl.M_SOCKETFUNCTION, self.socket_callback) + self._curlm.setopt(pycurl.M_TIMERFUNCTION, self.timeout_callback) + + def socket_callback(self, action, socket, user_data, socket_data): +# print 'socket callback: %d, %s' % \ +# (socket, {pycurl.POLL_NONE: "NONE", +# pycurl.POLL_IN: "IN", +# pycurl.POLL_OUT: "OUT", +# pycurl.POLL_INOUT: "INOUT", +# pycurl.POLL_REMOVE: "REMOVE"}[action]) + + if action == pycurl.POLL_NONE: + return + elif action == pycurl.POLL_REMOVE: + if socket in self._map: + del self._map[socket] + self._sockets_removed = True + return + + obj = self._map.get(socket) + if obj is None: + obj = dispatcher_wrapper(socket, self) + self._map[socket] = obj + + if action == pycurl.POLL_IN: + obj.set_readable(True) + obj.set_writable(False) + elif action == pycurl.POLL_OUT: + obj.set_readable(False) + obj.set_writable(True) + elif action == pycurl.POLL_INOUT: + obj.set_readable(True) + obj.set_writable(True) + + def timeout_callback(self, msec): + self.timeout = msec + + def attach(self, curldisp): + """Starts a transfer on curl handle by attaching it to this + multihandle.""" + self.dispatchers[curldisp.curl] = curldisp + try: + self._curlm.add_handle(curldisp.curl) + except pycurl.error: + # the curl object is already on this multi-stack + pass + + while self._curlm.socket_all()[0] == pycurl.E_CALL_MULTI_PERFORM: + pass + + self.check_completed(True) + + def detach(self, curldisp): + """Removes curl handle from this multihandle, and fire its + completion callback function.""" + self.del_curl(curldisp.curl) + + # libcurl does not send POLL_REMOVE when a handle is aborted + for socket, curlobj in self._map.items(): + if curlobj == curldisp: + + print 'handle stopped but socket in map' + + del self._map[socket] + break + + def del_curl(self, curl): + try: + self._curlm.remove_handle(curl) + except pycurl.error: + # the curl object is not on this multi-stack + pass + if curl in self.dispatchers: + del self.dispatchers[curl] + curl.close() + + def socket_action(self, fd, evbitmask): + res = -1 + OK = False + while not OK: + try: + res = self._curlm.socket_action(fd, evbitmask) + OK = True + except pycurl.error: + # Older libcurls may return CURLM_CALL_MULTI_PERFORM, + # which pycurl (at least 7.19.0) converts to an + # exception. If that happens, call socket_action + # again. + pass + return res + + def check_completed(self, force): + if not force and not self._sockets_removed: + return + self._sockets_removed = False + + nmsg, success, failed = self._curlm.info_read() + for handle in success: + disp = self.dispatchers.get(handle) + if disp is not None: + try: + disp.handle_completed(0, None) + except: + self.handle_error() + self.del_curl(handle) + for handle, err, errmsg in failed: + disp = self.dispatchers.get(handle) + if disp is not None: + try: + disp.handle_completed(err, errmsg) + except: + self.handle_error() + self.del_curl(handle) + + def handle_error(self): + print 'Exception occurred in multicurl processing' + print traceback.format_exc() + + +class dispatcher_wrapper: + """An internal helper class that connects a file descriptor in the + asyncore.socket_map to a curl_multi_dispatcher.""" + def __init__(self, fd, multicurl): + self.fd = fd + self.multicurl = multicurl + self.read_flag = False + self.write_flag = False + + def readable(self): + return self.read_flag + + def writable(self): + return self.write_flag + + def set_readable(self, x): + self.read_flag = x + + def set_writable(self, x): + self.write_flag = x + + def handle_read_event(self): + self.multicurl.socket_action(self.fd, CSELECT_IN) + self.multicurl.check_completed(False) + + def handle_write_event(self): + self.multicurl.socket_action(self.fd, CSELECT_OUT) + self.multicurl.check_completed(False) + + def handle_expt_event(self): + self.multicurl.socket_action(self.fd, CSELECT_ERR) + self.multicurl.check_completed(False) + + def handle_error(self): + print 'Exception occurred during processing of a curl request' + print traceback.format_exc() + + +class async_curl_dispatcher: + """A dispatcher class for pycurl transfers.""" + def __init__(self, url, auto_start=True): + """Initializes a pycurl object self.curl. The default is to + download url to an internal buffer whose content can be read + with self.recv(). If auto_start is False, the transfer is not + started before a call to add_channel(). + """ + self.url = url + self.socket = None + self.buffer = cStringIO.StringIO() + self.curl = pycurl.Curl() + self.curl.setopt(pycurl.URL, self.url) + self.curl.setopt(pycurl.FOLLOWLOCATION, 1) + self.curl.setopt(pycurl.AUTOREFERER, 1) + self.curl.setopt(pycurl.MAXREDIRS, 10) + self.curl.setopt(pycurl.FAILONERROR, 1) + self.curl.setopt(pycurl.WRITEFUNCTION, self.write_to_buf) + if auto_start: + self.add_channel() + + def write_to_buf(self, msg): + self.buffer.write(msg) + self.handle_read() + + def send(self, data): + raise NotImplementedError + + def recv(self, buffer_size): + # buffer_size is ignored + ret = self.buffer.getvalue() + self.buffer.reset() + self.buffer.truncate() + return ret + + def add_channel(self, multidisp=None): + if multidisp is None: + multidisp = multi_dispatcher + multidisp.attach(self) + + def del_channel(self, multidisp=None): + if multidisp is None: + multidisp = multi_dispatcher + multidisp.detach(self) + + def close(self): + self.del_channel() + + def log_info(self, message, type='info'): + if type != 'info': + print '%s: %s' % (type, message) + + def handle_error(self): + print 'Exception occurred during processing of a curl request' + print traceback.format_exc() + self.close() + + def handle_read(self): + self.log_info('unhandled read event', 'warning') + + def handle_write(self): + self.log_info('unhandled write event', 'warning') + + def handle_completed(self, err, errmsg): + """Called when the download has finished. err is a numeric + error code (or 0 if the download was successfull) and errmsg + is a curl error message as a string.""" + # It seems that a reference to self.write_to_buf forbids + # garbage collection from deleting this object. unsetopt() or + # setting the callback to None are not allowed. Is there a + # better way? + self.curl.setopt(pycurl.WRITEFUNCTION, noop_callback) + self.close() + + +def test(): + + class curl_request(async_curl_dispatcher): + def __init__(self, url, outfile, i): + async_curl_dispatcher.__init__(self, url, False) + self.id = i + self.outfile = outfile + self.add_channel() + + def handle_read(self): + buf = self.recv(4096) + print '%s: writing %d bytes' % (self.id, len(buf)) + self.outfile.write(buf) + + def handle_completed(self, err, errmsg): + if err != 0: + print '%s: error: %d %s' % (self.id, err, errmsg) + else: + print '%s: completed' % self.id + + curl_request('http://www.python.org', open('python.out', 'w'), 1) + curl_request('http://en.wikipedia.org/wiki/Main_Page', open('wikipedia.out', 'w'), 2) + loop(timeout=5.0) + + +pycurl.global_init(pycurl.GLOBAL_DEFAULT) +try: + multi_dispatcher +except NameError: + multi_dispatcher = curl_multi_dispatcher() + +if __name__ == '__main__': + test() diff --git a/src/libwebvi/webvi/constants.py b/src/libwebvi/webvi/constants.py new file mode 100644 index 0000000..2797178 --- /dev/null +++ b/src/libwebvi/webvi/constants.py @@ -0,0 +1,50 @@ +# constants.py - Python definitions for constants in libwebvi.h +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Keep these in sync with libwebvi.h + +class WebviRequestType: + MENU = 0 + FILE = 1 + STREAMURL = 2 + +class WebviErr: + OK = 0 + INVALID_HANDLE = 1 + INVALID_PARAMETER = 2 + INTERNAL_ERROR = -1 + +class WebviOpt: + WRITEFUNC = 0 + READFUNC = 1 + WRITEDATA = 2 + READDATA = 3 + +class WebviInfo: + URL = 0 + CONTENT_LENGTH = 1 + CONTENT_TYPE = 2 + STREAM_TITLE = 3 + +class WebviSelectBitmask: + TIMEOUT = 0 + READ = 1 + WRITE = 2 + EXCEPTION = 4 + +class WebviConfig: + TEMPLATE_PATH = 0 diff --git a/src/libwebvi/webvi/download.py b/src/libwebvi/webvi/download.py new file mode 100644 index 0000000..34240ff --- /dev/null +++ b/src/libwebvi/webvi/download.py @@ -0,0 +1,470 @@ +# download.py - webvi downloader backend +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import asyncore +import asynchat +import cStringIO +import urllib +import subprocess +import socket +import signal +import pycurl +import asyncurl +import utils +import version + +WEBVID_USER_AGENT = 'libwebvi/%s %s' % (version.VERSION, pycurl.version) +MOZILLA_USER_AGENT = 'Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5' + +try: + from libmimms import libmms +except ImportError, e: + pass + +# Mapping from curl error codes to webvi errors. The error constants +# are defined only in pycurl 7.16.1 and newer. +if pycurl.version_info()[2] >= 0x071001: + CURL_ERROR_CODE_MAPPING = \ + {pycurl.E_OK: 0, + pycurl.E_OPERATION_TIMEOUTED: 408, + pycurl.E_OUT_OF_MEMORY: 500, + pycurl.E_PARTIAL_FILE: 504, + pycurl.E_READ_ERROR: 504, + pycurl.E_RECV_ERROR: 504, + pycurl.E_REMOTE_FILE_NOT_FOUND: 404, + pycurl.E_TOO_MANY_REDIRECTS: 404, + pycurl.E_UNSUPPORTED_PROTOCOL: 500, + pycurl.E_URL_MALFORMAT: 400, + pycurl.E_COULDNT_CONNECT: 506, + pycurl.E_COULDNT_RESOLVE_HOST: 506, + pycurl.E_COULDNT_RESOLVE_PROXY: 506, + pycurl.E_FILE_COULDNT_READ_FILE: 404, + pycurl.E_GOT_NOTHING: 504, + pycurl.E_HTTP_RETURNED_ERROR: 404, + pycurl.E_INTERFACE_FAILED: 506, + pycurl.E_LOGIN_DENIED: 403} +else: + CURL_ERROR_CODE_MAPPING = {pycurl.E_OK: 0} + +class DownloaderException(Exception): + def __init__(self, errcode, errmsg): + self.code = errcode + self.msg = errmsg + + def __str__(self): + return '%s %s' % (self.code, self.msg) + +def create_downloader(url, templatedir, writefunc=None, headerfunc=None, + donefunc=None, HTTPheaders=None, headers_only=False): + """Downloader factory. + + Returns a suitable downloader object according to url type. Raises + DownloaderException if creating the downloader fails. + """ + if url == '': + return DummyDownloader('', writefunc, headerfunc, donefunc, + headers_only) + + elif url.startswith('mms://') or url.startswith('mmsh://'): + try: + libmms + except (NameError, OSError): + raise DownloaderException(501, 'MMS scheme not supported. Install mimms.') + return MMSDownload(url, writefunc, headerfunc, donefunc, + headers_only) + + elif url.startswith('wvt://'): + executable, parameters = parse_external_downloader_wvt_uri(url, templatedir) + if executable is None: + raise DownloaderException(400, 'Invalid wvt:// URL') + try: + return ExternalDownloader(executable, parameters, writefunc, + headerfunc, donefunc, headers_only) + except OSError, (errno, strerror): + raise DownloaderException(500, 'Failed to execute %s: %s' % + (executable, strerror)) + + else: + return CurlDownload(url, writefunc, headerfunc, donefunc, + HTTPheaders, headers_only) + +def convert_curl_error(err, errmsg, aborted): + """Convert a curl error code err to webvi error code.""" + if err == pycurl.E_WRITE_ERROR: + return (402, 'Aborted') + elif err not in CURL_ERROR_CODE_MAPPING: + return (500, errmsg) + else: + return (CURL_ERROR_CODE_MAPPING[err], errmsg) + +def parse_external_downloader_wvt_uri(url, templatedir): + exe = None + params = [] + if not url.startswith('wvt:///bin/'): + return (exe, params) + + split = url[len('wvt:///bin/'):].split('?', 1) + exe = templatedir + '/bin/' + split[0] + + if len(split) > 1: + params = [urllib.unquote(x) for x in split[1].split('&')] + + return (exe, params) + +def _new_process_group(): + os.setpgid(0, 0) + +class DownloaderBase: + """Base class for downloaders.""" + def __init__(self, url): + self.url = url + + def start(self): + """Should start the download process.""" + pass + + def abort(self): + """Signals that the download should be aborted.""" + pass + + def get_url(self): + """Return the URL where the data was downloaded.""" + return self.url + + def get_body(self): + return '' + + def get_encoding(self): + """Return the encoding of the downloaded object, or None if + encoding is not known.""" + return None + + +class DummyDownloader(DownloaderBase, asyncore.file_dispatcher): + """This class doesn't actually download anything, but returns msg + string as if it had been result of a download operation. + """ + def __init__(self, msg, writefunc=None, headerfunc=None, + donefunc=None, headers_only=False): + DownloaderBase.__init__(self, '') + self.donefunc = donefunc + self.writefunc = writefunc + self.headers_only = headers_only + + readfd, writefd = os.pipe() + asyncore.file_dispatcher.__init__(self, readfd) + os.write(writefd, msg) + os.close(writefd) + + def set_file(self, fd): + # Like asyncore.file_dispatcher.set_file() but doesn't call + # add_channel(). We'll call add_channel() in start() when the + # download shall begin. + self.socket = asyncore.file_wrapper(fd) + self._fileno = self.socket.fileno() + + def start(self): + if self.headers_only: + self.donefunc(0, None) + else: + self.add_channel() + + def readable(self): + return True + + def writable(self): + return False + + def handle_read(self): + try: + data = self.recv(4096) + if data and self.writefunc is not None: + self.writefunc(data) + except socket.error: + self.handle_error() + + def handle_close(self): + self.close() + + if self.donefunc is not None: + self.donefunc(0, '') + + +class CurlDownload(DownloaderBase, asyncurl.async_curl_dispatcher): + """Downloads a large number of different URL schemes using + libcurl.""" + def __init__(self, url, writefunc=None, headerfunc=None, + donefunc=None, HTTPheaders=None, headers_only=False): + DownloaderBase.__init__(self, url) + asyncurl.async_curl_dispatcher.__init__(self, url, False) + self.donefunc = donefunc + self.writefunc = writefunc + self.contenttype = None + self.running = True + self.aborted = False + + self.curl.setopt(pycurl.USERAGENT, WEBVID_USER_AGENT) + if headers_only: + self.curl.setopt(pycurl.NOBODY, 1) + if headerfunc is not None: + self.curl.setopt(pycurl.HEADERFUNCTION, headerfunc) + self.curl.setopt(pycurl.WRITEFUNCTION, self.writewrapper) + + headers = [] + if HTTPheaders is not None: + for headername, headerdata in HTTPheaders.iteritems(): + if headername == 'cookie': + self.curl.setopt(pycurl.COOKIE, headerdata) + else: + headers.append(headername + ': ' + headerdata) + + self.curl.setopt(pycurl.HTTPHEADER, headers) + + def start(self): + self.add_channel() + + def close(self): + self.contenttype = self.curl.getinfo(pycurl.CONTENT_TYPE) + asyncurl.async_curl_dispatcher.close(self) + self.running = False + + def abort(self): + self.aborted = True + + def writewrapper(self, data): + if self.aborted: + return 0 + + if self.writefunc is None: + return self.write_to_buf(data) + else: + return self.writefunc(data) + + def get_body(self): + return self.buffer.getvalue() + + def get_encoding(self): + if self.running: + self.contenttype = self.curl.getinfo(pycurl.CONTENT_TYPE) + + if self.contenttype is None: + return None + + values = self.contenttype.split(';', 1) + if len(values) > 1: + for par in values[1].split(' '): + if par.startswith('charset='): + return par[len('charset='):].strip('"') + + return None + + def handle_read(self): + # Do nothing to the read data here. Instead, let the base + # class to collect the data to self.buffer. + pass + + def handle_completed(self, err, errmsg): + asyncurl.async_curl_dispatcher.handle_completed(self, err, errmsg) + if self.donefunc is not None: + err, errmsg = convert_curl_error(err, errmsg, self.aborted) + self.donefunc(err, errmsg) + + +class MMSDownload(DownloaderBase, asyncore.file_dispatcher): + def __init__(self, url, writefunc=None, headerfunc=None, + donefunc=None, headers_only=False): + DownloaderBase.__init__(self, url) + self.r, self.w = os.pipe() + asyncore.file_dispatcher.__init__(self, self.r) + + self.writefunc = writefunc + self.headerfunc = headerfunc + self.donefunc = donefunc + self.relaylen = -1 + self.expectedlen = -1 + self.headers_only = headers_only + self.stream = None + self.errmsg = None + self.aborted = False + + def set_file(self, fd): + self.socket = asyncore.file_wrapper(fd) + self._fileno = self.socket.fileno() + + def recv(self, buffer_size): + data = self.stream.read() + if not data: + self.handle_close() + return '' + else: + return data + + def close(self): + if self.stream is not None: + self.stream.close() + + os.close(self.w) + asyncore.file_dispatcher.close(self) + + def readable(self): + return self.stream is not None + + def writable(self): + return False + + def start(self): + try: + self.stream = libmms.Stream(self.url, 1000000) + except libmms.Error, e: + self.errmsg = e.message + self.handle_close() + return + + os.write(self.w, '0') # signal that this dispatcher has data available + + if self.headerfunc: + # Output the length in a HTTP-like header field so that we + # can use the same callbacks as with HTTP downloads. + ext = utils.get_url_extension(self.url) + if ext == 'wma': + self.headerfunc('Content-Type: audio/x-ms-wma') + else: # if ext == 'wmv': + self.headerfunc('Content-Type: video/x-ms-wmv') + self.headerfunc('Content-Length: %d' % self.stream.length()) + + if self.headers_only: + self.handle_close() + else: + self.add_channel() + + def abort(self): + self.aborted = True + + def handle_read(self): + if self.aborted: + self.handle_close() + return '' + + try: + data = self.recv(4096) + if data and (self.writefunc is not None): + self.writefunc(data) + except libmms.Error, e: + self.errmsg = e.message + self.handle_close() + return + + def handle_close(self): + self.close() + self.stream = None + + if self.errmsg is not None: + self.donefunc(500, self.errmsg) + elif self.aborted: + self.donefunc(402, 'Aborted') + elif self.relaylen < self.expectedlen: + # We got fewer bytes than expected. Maybe the connection + # was lost? + self.donefunc(504, 'Download may be incomplete (length %d < %d)' % + (self.relaylen, self.expectedlen)) + else: + self.donefunc(0, '') + + +class ExternalDownloader(DownloaderBase, asyncore.file_dispatcher): + """Executes an external process and reads its result on standard + output.""" + def __init__(self, executable, parameters, writefunc=None, + headerfunc=None, donefunc=None, headers_only=False): + DownloaderBase.__init__(self, '') + asyncore.dispatcher.__init__(self, None, None) + self.executable = executable + self.writefunc = writefunc + self.headerfunc = headerfunc + self.donefunc = donefunc + self.headers_only = headers_only + self.contenttype = '' + self.aborted = False + + args = [] + for par in parameters: + try: + key, val = par.split('=', 1) + if key == 'contenttype': + self.contenttype = val + elif key == 'arg': + args.append(val) + except ValueError: + pass + + if args: + self.url = args[0] + else: + self.url = executable + self.cmd = [executable] + args + + self.process = None + + def start(self): + self.headerfunc('Content-Type: ' + self.contenttype) + + if self.headers_only: + self.donefunc(0, None) + return + + self.process = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, + close_fds=True, + preexec_fn=_new_process_group) + asyncore.file_dispatcher.__init__(self, os.dup(self.process.stdout.fileno())) + + def abort(self): + if self.process is not None: + self.aborted = True + pg = os.getpgid(self.process.pid) + os.killpg(pg, signal.SIGTERM) + + def readable(self): + # Return True if the subprocess is still alive + return self.process is not None and self.process.returncode is None + + def writable(self): + return False + + def handle_read(self): + try: + data = self.recv(4096) + if data and self.writefunc is not None: + self.writefunc(data) + except socket.error: + self.handle_error() + return + + def handle_close(self): + self.close() + self.process.wait() + + if self.donefunc is not None: + if self.process.returncode == 0: + self.donefunc(0, '') + elif self.aborted and self.process.returncode == -signal.SIGTERM: + self.donefunc(402, 'Aborted') + else: + self.donefunc(500, 'Child process "%s" returned error %s' % \ + (' '.join(self.cmd), str(self.process.returncode))) + + self.process = None diff --git a/src/libwebvi/webvi/json2xml.py b/src/libwebvi/webvi/json2xml.py new file mode 100644 index 0000000..372e6c6 --- /dev/null +++ b/src/libwebvi/webvi/json2xml.py @@ -0,0 +1,69 @@ +import sys +import libxml2 + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + print 'Error: install simplejson' + raise + +def _serialize_to_xml(obj, xmlnode): + """Create XML representation of a Python object (list, tuple, + dist, or basic number and string types).""" + if type(obj) in (list, tuple): + listnode = libxml2.newNode('list') + for li in obj: + itemnode = libxml2.newNode('li') + _serialize_to_xml(li, itemnode) + listnode.addChild(itemnode) + xmlnode.addChild(listnode) + + elif type(obj) == dict: + dictnode = libxml2.newNode('dict') + for key, val in obj.iteritems(): + itemnode = libxml2.newNode(key.encode('utf-8')) + _serialize_to_xml(val, itemnode) + dictnode.addChild(itemnode) + xmlnode.addChild(dictnode) + + elif type(obj) in (str, unicode, int, long, float, complex, bool): + content = libxml2.newText(unicode(obj).encode('utf-8')) + xmlnode.addChild(content) + + elif type(obj) == type(None): + pass + + else: + raise TypeError('Unsupported type %s while serializing to xml' + % type(obj)) + +def json2xml(jsonstr, encoding=None): + """Convert JSON string jsonstr to XML tree.""" + try: + parsed = json.loads(jsonstr, encoding) + except ValueError: + return None + + xmldoc = libxml2.newDoc("1.0") + root = libxml2.newNode("jsondocument") + xmldoc.setRootElement(root) + + _serialize_to_xml(parsed, root) + + return xmldoc + +def test(): + xml = json2xml(open(sys.argv[1]).read()) + + if xml is None: + return + + print xml.serialize('utf-8') + + xml.freeDoc() + +if __name__ == '__main__': + test() diff --git a/src/libwebvi/webvi/request.py b/src/libwebvi/webvi/request.py new file mode 100644 index 0000000..e19eb9c --- /dev/null +++ b/src/libwebvi/webvi/request.py @@ -0,0 +1,617 @@ +# request.py - webvi request class +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import urllib +import libxml2 +import os.path +import cStringIO +import re +import download +import sys +import utils +import json2xml +from constants import WebviRequestType + +DEBUG = False + +DEFAULT_TEMPLATE_PATH = '/usr/local/share/webvi/templates' +template_path = DEFAULT_TEMPLATE_PATH + +def debug(msg): + if DEBUG: + if type(msg) == unicode: + sys.stderr.write(msg.encode('ascii', 'replace')) + else: + sys.stderr.write(msg) + sys.stderr.write('\n') + +def set_template_path(path): + global template_path + + if path is None: + template_path = os.path.realpath(DEFAULT_TEMPLATE_PATH) + else: + template_path = os.path.realpath(path) + + debug("set_template_path " + template_path) + +def parse_reference(reference): + """Parses URLs of the following form: + + wvt:///youtube/video.xsl?srcurl=http%3A%2F%2Fwww.youtube.com%2F¶m=name1,value1¶m=name2,value2 + + reference is assumed to be URL-encoded UTF-8 string. + + Returns (template, srcurl, params, processing_instructions) where + template if the URL path name (the part before ?), srcurl is the + parameter called srcurl, and params is a dictionary of (name, + quoted-value) pairs extracted from param parameters. Parameter + values are quoted so that the xslt parser handles them as string. + processing_instructions is dictionary of options that affect the + further processing of the data. + """ + try: + reference = str(reference) + except UnicodeEncodeError: + return (None, None, None, None) + + if not reference.startswith('wvt:///'): + return (None, None, None, None) + + ref = reference[len('wvt:///'):] + + template = None + srcurl = '' + parameters = {} + substitutions = {} + refsettings = {'HTTP-headers': {}} + + fields = ref.split('?', 1) + template = fields[0] + if len(fields) == 1: + return (template, srcurl, parameters, refsettings) + + for par in fields[1].split('&'): + paramfields = par.split('=', 1) + key = paramfields[0] + + if len(paramfields) == 2: + value = urllib.unquote(paramfields[1]) + else: + value = '' + + if key.lower() == 'srcurl': + srcurl = value + + elif key.lower() == 'param': + fields2 = value.split(',', 1) + pname = fields2[0].lower() + if len(fields2) == 2: + pvalue = "'" + fields2[1] + "'" + else: + pvalue = "''" + parameters[pname] = pvalue + + elif key.lower() == 'subst': + substfields = value.split(',', 1) + if len(substfields) == 2: + substitutions[substfields[0]] = substfields[1] + + elif key.lower() == 'minquality': + try: + refsettings['minquality'] = int(value) + except ValueError: + pass + + elif key.lower() == 'maxquality': + try: + refsettings['maxquality'] = int(value) + except ValueError: + pass + + elif key.lower() == 'postprocess': + refsettings.setdefault('postprocess', []).append(value) + + elif key.lower() == 'contenttype': + refsettings['overridecontenttype'] = value + + elif key.lower() == 'http-header': + try: + headername, headerdata = value.split(',', 1) + except ValueError: + continue + refsettings['HTTP-headers'][headername] = headerdata + + if substitutions: + srcurl = brace_substitution(srcurl, substitutions) + + return (template, srcurl, parameters, refsettings) + +def brace_substitution(template, subs): + """Substitute subs[x] for '{x}' in template. Unescape {{ to { and + }} to }. Unescaping is not done in substitution keys, i.e. while + scanning for a closing brace after a single opening brace.""" + strbuf = cStringIO.StringIO() + + last_pos = 0 + for match in re.finditer(r'{{?|}}', template): + next_pos = match.start() + if next_pos < last_pos: + continue + + strbuf.write(template[last_pos:next_pos]) + if match.group(0) == '{{': + strbuf.write('{') + last_pos = next_pos+2 + + elif match.group(0) == '}}': + strbuf.write('}') + last_pos = next_pos+2 + + else: # match.group(0) == '{' + key_end = template.find('}', next_pos+1) + if key_end == -1: + strbuf.write(template[next_pos:]) + last_pos = len(template) + break + + try: + strbuf.write(urllib.quote(subs[template[next_pos+1:key_end]])) + except KeyError: + strbuf.write(template[next_pos:key_end+1]) + last_pos = key_end+1 + + strbuf.write(template[last_pos:]) + return strbuf.getvalue() + + +class Request: + DEFAULT_URL_PRIORITY = 50 + + def __init__(self, reference, reqtype): + self.handle = None + self.dl = None + + # state variables + self.xsltfile, self.srcurl, self.xsltparameters, self.processing = \ + parse_reference(reference) + self.type = reqtype + self.status = -1 + self.errmsg = None + self.mediaurls = [] + + # stream information + self.contenttype = 'text/xml' + self.contentlength = -1 + self.streamtitle = '' + + # callbacks + self.writefunc = None + self.writedata = None + self.readfunc = None + self.readdata = None + + def handle_header(self, buf): + namedata = buf.split(':', 1) + if len(namedata) == 2: + headername, headerdata = namedata + if headername.lower() == 'content-type': + # Strip parameters like charset="utf-8" + self.contenttype = headerdata.split(';', 1)[0].strip() + elif headername.lower() == 'content-length': + try: + self.contentlength = int(headerdata.strip()) + except ValueError: + self.contentlength = -1 + + def setup_downloader(self, url, writefunc, headerfunc, donefunc, + HTTPheaders=None, headers_only=False): + try: + self.dl = download.create_downloader(url, + template_path, + writefunc, + headerfunc, + donefunc, + HTTPheaders, + headers_only) + self.dl.start() + except download.DownloaderException, exc: + self.dl = None + if donefunc is not None: + donefunc(exc.code, exc.msg) + + def start(self): + debug('start %s\ntemplate = %s, type = %s\n' + 'parameters = %s, processing = %s' % + (self.srcurl, self.xsltfile, self.type, str(self.xsltparameters), + str(self.processing))) + + if self.type == WebviRequestType.MENU and self.srcurl == 'mainmenu': + self.send_mainmenu() + else: + self.setup_downloader(self.srcurl, None, + self.handle_header, + self.finished_apply_xslt, + self.processing['HTTP-headers']) + + def stop(self): + if self.dl is not None: + debug("aborting") + self.dl.abort() + + def start_download(self, url=None): + """Initialize a download. + + If url is None, pop the first URL out of self.mediaurls. If + URL is an ASX playlist, read the content URL from it and start + to download the actual content. + """ + while url is None or url == '': + try: + url = self.mediaurls.pop(0) + except IndexError: + self.request_done(406, 'No more URLs left') + + debug('Start_download ' + url) + + # reset stream status + self.contenttype = 'text/xml' + self.contentlength = -1 + + if self.is_asx_playlist(url): + self.setup_downloader(url, None, + self.handle_header, + self.finished_playlist_loaded, + self.processing['HTTP-headers']) + + else: + self.setup_downloader(url, self.writewrapper, + self.handle_header, + self.finished_download, + self.processing['HTTP-headers']) + + def check_and_send_url(self, url=None): + """Check if the target exists (currently only for HTTP URLs) + before relaying the URL to the client.""" + while url is None or url == '': + try: + url = self.mediaurls.pop(0) + except IndexError: + self.request_done(406, 'No more URLs left') + return + + debug('check_and_send_url ' + str(url)) + + if self.is_asx_playlist(url): + self.setup_downloader(url, None, self.handle_header, + self.finished_playlist_loaded, + self.processing['HTTP-headers']) + elif url.startswith('http://') or url.startswith('https://'): + self.checking_url = url + self.setup_downloader(url, None, None, + self.finished_check_url, + self.processing['HTTP-headers'], True) + else: + self.writewrapper(url) + self.request_done(0, None) + + def send_mainmenu(self): + """Build the XML main menu from the module description files + in the hard drive. + """ + if not os.path.isdir(template_path): + self.request_done(404, "Can't access service directory %s" % + template_path) + return + + debug('Reading XSLT templates from ' + template_path) + + # Find menu items in the service.xml files in the subdirectories + menuitems = {} + for f in os.listdir(template_path): + if f == 'bin': + continue + + filename = os.path.join(template_path, f, 'service.xml') + try: + doc = libxml2.parseFile(filename) + except libxml2.parserError: + debug("Failed to parse " + filename); + continue + + title = '' + url = '' + + root = doc.getRootElement() + if (root is None) or (root.name != 'service'): + debug("Root node is not 'service' in " + filename); + doc.freeDoc() + continue + node = root.children + while node is not None: + if node.name == 'title': + title = utils.get_content_unicode(node) + elif node.name == 'ref': + url = utils.get_content_unicode(node) + node = node.next + doc.freeDoc() + + if (title == '') or (url == ''): + debug("Empty <title> or <ref> in " + filename); + continue + + menuitems[title.lower()] = ('<link>\n' + '<label>%s</label>\n' + '<ref>%s</ref>\n' + '</link>\n' % + (libxml2.newText(title), + libxml2.newText(url))) + # Sort the menu items + titles = menuitems.keys() + titles.sort() + + # Build the menu + mainmenu = ('<?xml version="1.0"?>\n' + '<wvmenu>\n' + '<title>Select video source</title>\n') + for t in titles: + mainmenu += menuitems[t] + mainmenu += '</wvmenu>' + + self.dl = download.DummyDownloader(mainmenu, + writefunc=self.writewrapper, + donefunc=self.request_done) + self.dl.start() + + def writewrapper(self, inp): + """Wraps pycurl write callback (with the data as the only + parameter) into webvi write callback (with signature (data, + length, usertag)). If self.writefunc is not set, write to + stdout.""" + if self.writefunc is not None: + inplen = len(inp) + written = self.writefunc(inp, inplen, self.writedata) + if written != inplen: + self.dl.close() + self.request_done(405, 'Write callback failed') + else: + sys.stdout.write(inp) + + def is_asx_playlist(self, url): + if utils.get_url_extension(url).lower() == 'asx': + return True + else: + return False + + def get_url_from_asx(self, asx, asxurl): + """Simple ASX parser. Return the content of the first <ref> + tag.""" + try: + doc = libxml2.htmlReadDoc(asx, asxurl, None, + libxml2.HTML_PARSE_NOERROR | + libxml2.HTML_PARSE_NOWARNING | + libxml2.HTML_PARSE_NONET) + except libxml2.treeError: + debug('Can\'t parse ASX:\n' + asx) + return None + root = doc.getRootElement() + ret = self._get_ref_recursive(root).strip() + doc.freeDoc() + return ret + + def _get_ref_recursive(self, node): + if node is None: + return None + if node.name.lower() == 'ref': + href = node.prop('href') + if href is not None: + return href + child = node.children + while child: + res = self._get_ref_recursive(child) + if res is not None: + return res + child = child.next + return None + + def parse_mediaurl(self, xml, minpriority, maxpriority): + debug('parse_mediaurl\n' + xml) + + self.streamtitle = '???' + mediaurls = [] + + try: + doc = libxml2.parseDoc(xml) + except libxml2.parserError: + debug('Invalid XML') + return mediaurls + + root = doc.getRootElement() + if root is None: + debug('No root node') + return mediaurls + + urls_and_priorities = [] + node = root.children + while node: + if node.name == 'title': + self.streamtitle = utils.get_content_unicode(node) + elif node.name == 'url': + try: + priority = int(node.prop('priority')) + except (ValueError, TypeError): + priority = self.DEFAULT_URL_PRIORITY + + content = node.getContent() + if priority >= minpriority and priority <= maxpriority and content != '': + urls_and_priorities.append((priority, content)) + node = node.next + doc.freeDoc() + + urls_and_priorities.sort() + urls_and_priorities.reverse() + mediaurls = [b[1] for b in urls_and_priorities] + + return mediaurls + + def finished_download(self, err, errmsg): + if err == 0: + self.request_done(0, None) + elif err != 402 and self.mediaurls: + debug('Download failed (%s %s).\nTrying the next one.' % (err, errmsg)) + self.dl = None + self.start_download() + else: + self.request_done(err, errmsg) + + def finished_playlist_loaded(self, err, errmsg): + if err == 0: + url = self.get_url_from_asx(self.dl.get_body(), + self.dl.get_url()) + if url is None: + err = 404 + errmsg = 'No ref tag in ASX file' + else: + if not self.is_asx_playlist(url) and url.startswith('http:'): + # The protocol is really "Windows Media HTTP + # Streaming Protocol", not plain HTTP, even though + # the scheme in the ASX file says "http://". We + # can't do MS-WMSP but luckily most MS-WMSP + # servers support MMS, too. + url = 'mms:' + url[5:] + + if self.type == WebviRequestType.STREAMURL: + self.check_and_send_url(url) + else: + self.start_download(url) + + if err != 0: + if not self.mediaurls: + self.request_done(err, errmsg) + else: + if self.type == WebviRequestType.STREAMURL: + self.check_and_send_url() + else: + self.start_download() + + def finished_apply_xslt(self, err, errmsg): + if err != 0: + self.request_done(err, errmsg) + return + + url = self.srcurl + + # Add input documentURL to the parameters + params = self.xsltparameters.copy() + params['docurl'] = "'" + url + "'" + + minpriority = self.processing.get('minquality', 0) + maxpriority = self.processing.get('maxquality', 100) + + xsltpath = os.path.join(template_path, self.xsltfile) + + # Check that xsltpath is inside the template directory + if os.path.commonprefix([template_path, os.path.realpath(xsltpath)]) != template_path: + self.request_done(503, 'Insecure template path') + return + + xml = self.dl.get_body() + encoding = self.dl.get_encoding() + + if self.processing.has_key('postprocess') and \ + 'json2xml' in self.processing['postprocess']: + xmldoc = json2xml.json2xml(xml, encoding) + if xmldoc is None: + self.request_done(503, 'Invalid JSON content') + return + xml = xmldoc.serialize('utf-8') + encoding = 'utf-8' + + #debug(xml) + + resulttree = utils.apply_xslt(xml, encoding, url, + xsltpath, params) + if resulttree is None: + self.request_done(503, 'XSLT transformation failed') + return + + if self.type == WebviRequestType.MENU: + debug("result:") + debug(resulttree) + self.writewrapper(resulttree) + self.request_done(0, None) + elif self.type == WebviRequestType.STREAMURL: + self.mediaurls = self.parse_mediaurl(resulttree, minpriority, maxpriority) + if self.mediaurls: + self.check_and_send_url() + else: + self.request_done(406, 'No valid URLs found') + elif self.type == WebviRequestType.FILE: + self.mediaurls = self.parse_mediaurl(resulttree, minpriority, maxpriority) + if self.mediaurls: + self.start_download() + else: + self.request_done(406, 'No valid URLs found') + else: + self.request_done(0, None) + + def finished_extract_playlist_url(self, err, errmsg): + if err == 0: + url = self.get_url_from_asx(self.dl.get_body(), + self.dl.get_url()) + if url is not None: + if self.is_asx_playlist(url): + self.setup_downloader(url, None, None, + self.finished_extract_playlist_url, + self.processing['HTTP-headers']) + else: + if url.startswith('http:'): + url = 'mms:' + url[5:] + self.check_and_send_url(url) + else: + self.request_done(503, 'XSLT tranformation failed to produce URL') + else: + self.request_done(err, errmsg) + + + def finished_check_url(self, err, errmsg): + if err == 0: + self.writewrapper(self.checking_url) + self.request_done(0, None) + else: + self.check_and_send_url() + + def request_done(self, err, errmsg): + debug('request_done: %d %s' % (err, errmsg)) + + self.status = err + self.errmsg = errmsg + self.dl = None + + def is_finished(self): + return self.status >= 0 + + +class RequestList(dict): + nextreqnum = 1 + + def put(self, req): + reqnum = RequestList.nextreqnum + RequestList.nextreqnum += 1 + req.handle = reqnum + self[reqnum] = req + return reqnum diff --git a/src/libwebvi/webvi/utils.py b/src/libwebvi/webvi/utils.py new file mode 100644 index 0000000..cefe09a --- /dev/null +++ b/src/libwebvi/webvi/utils.py @@ -0,0 +1,134 @@ +# utils.py - misc. utility functions +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import urlparse +import re +import libxml2 +import libxslt +import urllib + +def get_url_extension(url): + """Extracts and returns the file extension from a URL.""" + # The extension is located right before possible query + # ("?query=foo") or fragment ("#bar"). + try: + i = url.index('?') + url = url[:i] + except ValueError: + pass + # The extension is the part after the last '.' that does not + # contain '/'. + idot = url.rfind('.') + islash = url.rfind('/') + if idot > islash: + return url[idot+1:] + else: + return '' + +def urljoin_query_fix(base, url, allow_fragments=True): + """urlparse.urljoin in Python 2.5 (2.6?) and older is broken in + case url is a pure query. See http://bugs.python.org/issue1432. + This handles correctly the case where base is a full (http) url + and url is a query, and calls urljoin() for other cases.""" + if url.startswith('?'): + bscheme, bnetloc, bpath, bparams, bquery, bfragment = \ + urlparse.urlparse(base, '', allow_fragments) + bquery = url[1:] + return urlparse.urlunparse((bscheme, bnetloc, bpath, + bparams, bquery, bfragment)) + else: + return urlparse.urljoin(base, url, allow_fragments) + +def get_content_unicode(node): + """node.getContent() returns an UTF-8 encoded sequence of bytes (a + string). Convert it to a unicode object.""" + return unicode(node.getContent(), 'UTF-8', 'replace') + +def apply_xslt(buf, encoding, url, xsltfile, params=None): + """Apply xslt transform from file xsltfile to the string buf + with parameters params. url is the location of buf. Returns + the transformed file as a string, or None if the + transformation couldn't be completed.""" + stylesheet = libxslt.parseStylesheetFile(xsltfile) + + if stylesheet is None: + #self.log_info('Can\'t open stylesheet %s' % xsltfile, 'warning') + return None + try: + # htmlReadDoc fails if the buffer is empty but succeeds + # (returning an empty tree) if the buffer is a single + # space. + if buf == '': + buf = ' ' + + # Guess whether this is an XML or HTML document. + if buf.startswith('<?xml'): + doc = libxml2.readDoc(buf, url, None, + libxml2.XML_PARSE_NOERROR | + libxml2.XML_PARSE_NOWARNING | + libxml2.XML_PARSE_NONET) + else: + #self.log_info('Using HTML parser', 'debug') + doc = libxml2.htmlReadDoc(buf, url, encoding, + libxml2.HTML_PARSE_NOERROR | + libxml2.HTML_PARSE_NOWARNING | + libxml2.HTML_PARSE_NONET) + except libxml2.treeError: + stylesheet.freeStylesheet() + #self.log_info('Can\'t parse XML document', 'warning') + return None + resultdoc = stylesheet.applyStylesheet(doc, params) + stylesheet.freeStylesheet() + doc.freeDoc() + if resultdoc is None: + #self.log_info('Can\'t apply stylesheet', 'warning') + return None + + # Postprocess the document: + # Resolve relative URLs in srcurl (TODO: this should be done in XSLT) + root = resultdoc.getRootElement() + if root is None: + resultdoc.freeDoc() + return None + + node2 = root.children + while node2 is not None: + if node2.name not in ['link', 'button']: + node2 = node2.next + continue + + node = node2.children + while node is not None: + if (node.name == 'ref') or (node.name == 'stream') or \ + (node.name == 'submission'): + refurl = node.getContent() + + match = re.search(r'\?.*srcurl=([^&]*)', refurl) + if match is not None: + oldurl = urllib.unquote(match.group(1)) + absurl = urljoin_query_fix(url, oldurl) + newurl = refurl[:match.start(1)] + \ + urllib.quote(absurl) + \ + refurl[match.end(1):] + node.setContent(resultdoc.encodeSpecialChars(newurl)) + + node = node.next + node2 = node2.next + + ret = resultdoc.serialize('UTF-8') + resultdoc.freeDoc() + return ret diff --git a/src/libwebvi/webvi/version.py b/src/libwebvi/webvi/version.py new file mode 100644 index 0000000..26cb817 --- /dev/null +++ b/src/libwebvi/webvi/version.py @@ -0,0 +1,20 @@ +# version.py - webvi version +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +MAJOR = '0' +MINOR = '2' +VERSION = MAJOR + '.' + MINOR diff --git a/src/unittest/Makefile b/src/unittest/Makefile new file mode 100644 index 0000000..81b0ea2 --- /dev/null +++ b/src/unittest/Makefile @@ -0,0 +1,11 @@ +CFLAGS=-O2 -g -Wall -I../libwebvi +LDFLAGS=-L../libwebvi -Wl,-rpath=../libwebvi -lwebvi + +all: testlibwebvi testdownload + +testlibwebvi: testlibwebvi.o ../libwebvi/libwebvi.so + +testdownload: testdownload.o ../libwebvi/libwebvi.so + +clean: + rm -f testlibwebvi testlibwebvi.o testdownload testdownload.o diff --git a/src/unittest/runtests.sh b/src/unittest/runtests.sh new file mode 100755 index 0000000..9afc7a5 --- /dev/null +++ b/src/unittest/runtests.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +export PYTHONPATH=../libwebvi + +./testlibwebvi +#./testdownload +#./testwebvi.py diff --git a/src/unittest/testdownload.c b/src/unittest/testdownload.c new file mode 100644 index 0000000..134150a --- /dev/null +++ b/src/unittest/testdownload.c @@ -0,0 +1,195 @@ +/* + * testlibwebvi.c: unittest for webvi C bindings + * + * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include <sys/select.h> +#include <errno.h> + +#include "libwebvi.h" + +#define WVTREFERENCE "wvt:///youtube/video.xsl?srcurl=http%3A//www.youtube.com/watch%3Fv%3Dk5LmKNYTqvk" + +#define CHECK_WEBVI_CALL(err, funcname) \ + if (err != WEBVIERR_OK) { \ + fprintf(stderr, "%s FAILED: %s\n", funcname, webvi_strerror(ctx, err)); \ + returncode = 127; \ + goto cleanup; \ + } + +struct download_data { + long bytes_downloaded; + WebviCtx ctx; + WebviHandle handle; +}; + +ssize_t file_callback(const char *buf, size_t len, void *data) { + struct download_data *dldata = (struct download_data *)data; + + if (dldata->bytes_downloaded == 0) { + char *url, *title, *contentType; + long contentLength; + + if (webvi_get_info(dldata->ctx, dldata->handle, WEBVIINFO_URL, &url) != WEBVIERR_OK) { + fprintf(stderr, "webvi_get_info FAILED\n"); + return -1; + } + + if (url) { + printf("File URL: %s\n", url); + free(url); + } + + if (webvi_get_info(dldata->ctx, dldata->handle, WEBVIINFO_STREAM_TITLE, &title) != WEBVIERR_OK) { + fprintf(stderr, "webvi_get_info FAILED\n"); + return -1; + } + + if (title) { + printf("Title: %s\n", title); + free(title); + } + + if (webvi_get_info(dldata->ctx, dldata->handle, WEBVIINFO_CONTENT_TYPE, &contentType) != WEBVIERR_OK) { + fprintf(stderr, "webvi_get_info FAILED\n"); + return -1; + } + + if (contentType) { + printf("Content type: %s\n", contentType); + free(contentType); + } + + if (webvi_get_info(dldata->ctx, dldata->handle, WEBVIINFO_CONTENT_LENGTH, &contentLength) != WEBVIERR_OK) { + fprintf(stderr, "webvi_get_info FAILED\n"); + return -1; + } + + printf("Content length: %ld\n", contentLength); + } + + dldata->bytes_downloaded += len; + + printf("\r%ld", dldata->bytes_downloaded); + + return len; +} + +int main(int argc, const char* argv[]) { + int returncode = 0; + WebviCtx ctx = 0; + WebviHandle handle = -1; + fd_set readfd, writefd, excfd; + int maxfd, fd, s, msg_remaining; + struct timeval timeout; + long running; + WebviMsg *donemsg; + int done; + struct download_data callback_data; + + printf("Testing %s\n", webvi_version()); + + if (webvi_global_init() != 0) { + fprintf(stderr, "webvi_global_init FAILED\n"); + return 127; + } + + ctx = webvi_initialize_context(); + if (ctx == 0) { + fprintf(stderr, "webvi_initialize_context FAILED\n"); + returncode = 127; + goto cleanup; + } + + CHECK_WEBVI_CALL(webvi_set_config(ctx, WEBVI_CONFIG_TEMPLATE_PATH, "../../templates"), + "webvi_set_config(WEBVI_CONFIG_TEMPLATE_PATH)"); + + handle = webvi_new_request(ctx, WVTREFERENCE, WEBVIREQ_FILE); + if (handle == -1) { + fprintf(stderr, "webvi_new_request FAILED\n"); + returncode = 127; + goto cleanup; + } + + callback_data.bytes_downloaded = 0; + callback_data.ctx = ctx; + callback_data.handle = handle; + CHECK_WEBVI_CALL(webvi_set_opt(ctx, handle, WEBVIOPT_WRITEDATA, &callback_data), + "webvi_set_opt(WEBVIOPT_WRITEDATA)"); + CHECK_WEBVI_CALL(webvi_set_opt(ctx, handle, WEBVIOPT_WRITEFUNC, file_callback), + "webvi_set_opt(WEBVIOPT_WRITEFUNC)"); + CHECK_WEBVI_CALL(webvi_start_handle(ctx, handle), + "webvi_start_handle"); + + done = 0; + do { + FD_ZERO(&readfd); + FD_ZERO(&writefd); + FD_ZERO(&excfd); + CHECK_WEBVI_CALL(webvi_fdset(ctx, &readfd, &writefd, &excfd, &maxfd), + "webvi_fdset"); + + timeout.tv_sec = 1; + timeout.tv_usec = 0; + s = select(maxfd+1, &readfd, &writefd, NULL, &timeout); + + if (s < 0) { + if (errno == EINTR) + continue; + + perror("select FAILED"); + returncode = 127; + goto cleanup; + + } if (s == 0) { + CHECK_WEBVI_CALL(webvi_perform(ctx, 0, WEBVI_SELECT_TIMEOUT, &running), + "webvi_perform"); + } else { + for (fd=0; fd<=maxfd; fd++) { + if (FD_ISSET(fd, &readfd)) { + CHECK_WEBVI_CALL(webvi_perform(ctx, fd, WEBVI_SELECT_READ, &running), + "webvi_perform"); + } + if (FD_ISSET(fd, &writefd)) { + CHECK_WEBVI_CALL(webvi_perform(ctx, fd, WEBVI_SELECT_WRITE, &running), + "webvi_perform"); + } + } + } + + do { + donemsg = webvi_get_message(ctx, &msg_remaining); + if (donemsg && donemsg->msg == WEBVIMSG_DONE && donemsg->handle == handle) { + done = 1; + } + } while (msg_remaining > 0); + } while (!done); + + printf("\nRead %ld bytes.\n" + "Test successful.\n", callback_data.bytes_downloaded); + +cleanup: + if (ctx != 0) { + if (handle != -1) + webvi_delete_handle(ctx, handle); + webvi_cleanup_context(ctx); + } + webvi_cleanup(1); + + return returncode; +} diff --git a/src/unittest/testlibwebvi.c b/src/unittest/testlibwebvi.c new file mode 100644 index 0000000..0dda58a --- /dev/null +++ b/src/unittest/testlibwebvi.c @@ -0,0 +1,147 @@ +/* + * testlibwebvi.c: unittest for webvi C bindings + * + * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include <sys/select.h> +#include <errno.h> + +#include "libwebvi.h" + +#define WVTREFERENCE "wvt:///?srcurl=mainmenu" +//#define WVTREFERENCE "wvt:///youtube/categories.xsl?srcurl=http://gdata.youtube.com/schemas/2007/categories.cat" +//#define WVTREFERENCE "wvt:///youtube/search.xsl" + +#define CHECK_WEBVI_CALL(err, funcname) \ + if (err != WEBVIERR_OK) { \ + fprintf(stderr, "%s FAILED: %s\n", funcname, webvi_strerror(ctx, err)); \ + returncode = 127; \ + goto cleanup; \ + } + +ssize_t count_bytes_callback(const char *buf, size_t len, void *data) { + long *bytes = (long *)data; + *bytes += len; + return len; +} + +int main(int argc, const char* argv[]) { + int returncode = 0; + WebviCtx ctx = 0; + WebviHandle handle = -1; + long bytes = 0; + fd_set readfd, writefd, excfd; + int maxfd, fd, s, msg_remaining; + struct timeval timeout; + long running; + WebviMsg *donemsg; + int done; + char *contenttype; + + printf("Testing %s\n", webvi_version()); + + if (webvi_global_init() != 0) { + fprintf(stderr, "webvi_global_init FAILED\n"); + return 127; + } + + ctx = webvi_initialize_context(); + if (ctx == 0) { + fprintf(stderr, "webvi_initialize_context FAILED\n"); + returncode = 127; + goto cleanup; + } + + CHECK_WEBVI_CALL(webvi_set_config(ctx, WEBVI_CONFIG_TEMPLATE_PATH, "../../templates"), + "webvi_set_config(WEBVI_CONFIG_TEMPLATE_PATH)"); + + handle = webvi_new_request(ctx, WVTREFERENCE, WEBVIREQ_MENU); + if (handle == -1) { + fprintf(stderr, "webvi_new_request FAILED\n"); + returncode = 127; + goto cleanup; + } + + CHECK_WEBVI_CALL(webvi_set_opt(ctx, handle, WEBVIOPT_WRITEDATA, &bytes), + "webvi_set_opt(WEBVIOPT_WRITEDATA)"); + CHECK_WEBVI_CALL(webvi_set_opt(ctx, handle, WEBVIOPT_WRITEFUNC, count_bytes_callback), + "webvi_set_opt(WEBVIOPT_WRITEFUNC)"); + CHECK_WEBVI_CALL(webvi_start_handle(ctx, handle), + "webvi_start_handle"); + + done = 0; + do { + FD_ZERO(&readfd); + FD_ZERO(&writefd); + FD_ZERO(&excfd); + CHECK_WEBVI_CALL(webvi_fdset(ctx, &readfd, &writefd, &excfd, &maxfd), + "webvi_fdset"); + + timeout.tv_sec = 10; + timeout.tv_usec = 0; + s = select(maxfd+1, &readfd, &writefd, NULL, &timeout); + + if (s < 0) { + if (errno == EINTR) + continue; + + perror("select FAILED"); + returncode = 127; + goto cleanup; + + } if (s == 0) { + CHECK_WEBVI_CALL(webvi_perform(ctx, 0, WEBVI_SELECT_TIMEOUT, &running), + "webvi_perform"); + } else { + for (fd=0; fd<=maxfd; fd++) { + if (FD_ISSET(fd, &readfd)) { + CHECK_WEBVI_CALL(webvi_perform(ctx, fd, WEBVI_SELECT_READ, &running), + "webvi_perform"); + } + if (FD_ISSET(fd, &writefd)) { + CHECK_WEBVI_CALL(webvi_perform(ctx, fd, WEBVI_SELECT_WRITE, &running), + "webvi_perform"); + } + } + } + + do { + donemsg = webvi_get_message(ctx, &msg_remaining); + if (donemsg && donemsg->msg == WEBVIMSG_DONE && donemsg->handle == handle) { + done = 1; + } + } while (msg_remaining > 0); + } while (!done); + + CHECK_WEBVI_CALL(webvi_get_info(ctx, handle, WEBVIINFO_CONTENT_TYPE, &contenttype), + "webvi_get_info"); + printf("Read %ld bytes. Content type: %s\n", bytes, contenttype); + free(contenttype); + + printf("Test successful.\n"); + +cleanup: + if (ctx != 0) { + if (handle != -1) + webvi_delete_handle(ctx, handle); + webvi_cleanup_context(ctx); + } + webvi_cleanup(1); + + return returncode; +} diff --git a/src/unittest/testwebvi.py b/src/unittest/testwebvi.py new file mode 100644 index 0000000..6017ded --- /dev/null +++ b/src/unittest/testwebvi.py @@ -0,0 +1,407 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of vdr-webvideo-plugin. +# +# Copyright 2009,2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Blackbox tests for each of thee supported video sites and webvicli. + +Mainly useful for checking if the web sites have changed so much that +the XSLT templates don't match to them anymore. Requires network +connection because the tests automatically connect and navigate +through links on the video sites. +""" + +import unittest +import sys +import re + +sys.path.append('../webvicli') +sys.path.append('../libwebvi') +from webvicli import client, menu +import webvi.api +from webvi.constants import WebviConfig + +class TestServiceModules(unittest.TestCase): + + # ========== Helper functions ========== + + def setUp(self): + webvi.api.set_config(WebviConfig.TEMPLATE_PATH, '../../templates') + self.client = client.WVClient([], {}, {}) + + def getLinks(self, menuobj): + links = [] + for i in xrange(len(menuobj)): + if isinstance(menuobj[i], menu.MenuItemLink): + links.append(menuobj[i]) + return links + + def downloadMenuPage(self, reference, menuname): + (status, statusmsg, menuobj) = self.client.getmenu(reference) + self.assertEqual(status, 0, 'Unexpected status code %s (%s) in %s menu\nFailed ref was %s' % (status, statusmsg, menuname, reference)) + self.assertNotEqual(menuobj, None, 'Failed to get %s menu' % menuname) + return menuobj + + def downloadAndExtractLinks(self, reference, minlinks, menuname): + menuobj = self.downloadMenuPage(reference, menuname) + links = self.getLinks(menuobj) + self.assertTrue(len(links) >= minlinks, 'Too few links in %s menu' % menuname) + return links + + def checkMediaUrl(self, reference): + streamurl = self.client.get_stream_url(reference) + self.assertNotEqual(streamurl, None, 'get_stream_url returned None') + self.assertNotEqual(streamurl, '', 'get_stream_url returned empty string') + + def getServiceReference(self, templatedir): + service = open(templatedir + '/service.xml').read() + m = re.search(r'<ref>(.*)</ref>', service) + self.assertNotEqual(m, None, 'no <ref> in service.xml') + return m.group(1) + + # ========== Tests for supported websites ========== + + def testMainMenu(self): + self.downloadAndExtractLinks('wvt:///?srcurl=mainmenu', 4, 'main') + + def testYoutube(self): + # Category page + ref = self.getServiceReference('../../templates/youtube') + links = self.downloadAndExtractLinks(ref, 3, 'category') + + # Navigation page + # The third one is the first "proper" category. The first and second are "Search" and "All" + navigationref = links[2].ref + links = self.downloadAndExtractLinks(navigationref, 2, 'navigation') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + self.checkMediaUrl(videolink.stream) + + def testYoutubeSearch(self): + menuobj = self.downloadMenuPage('wvt:///youtube/search.xsl', 'search') + self.assertTrue(len(menuobj) >= 4, 'Too few items in search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemList)) + self.assertTrue(len(menuobj[1].items) >= 4) + self.assertTrue(isinstance(menuobj[2], menu.MenuItemList)) + self.assertTrue(len(menuobj[2].items) >= 4) + self.assertTrue(isinstance(menuobj[3], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'youtube' + # Sort by: rating + menuobj[1].current = 3 + # Uploaded: This month + menuobj[2].current = 3 + + resultref = menuobj[3].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testGoogleSearch(self): + ref = self.getServiceReference('../../templates/google') + menuobj = self.downloadMenuPage(ref, 'search') + self.assertTrue(len(menuobj) == 4, 'Unexpected number of items in Google search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemList)) + self.assertTrue(len(menuobj[1].items) >= 4) + self.assertTrue(isinstance(menuobj[2], menu.MenuItemList)) + self.assertTrue(len(menuobj[2].items) >= 4) + self.assertTrue(isinstance(menuobj[3], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'google' + # Sort by: date + menuobj[1].current = 3 + # Duration: Short + menuobj[2].current = 1 + + resultref = menuobj[3].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testSVTPlay(self): + # Category page + ref = self.getServiceReference('../../templates/svtplay') + links = self.downloadAndExtractLinks(ref, 3, 'category') + + # Navigation page + navigationref = links[0].ref + links = self.downloadAndExtractLinks(navigationref, 2, 'navigation') + + # Single program + programref = links[0].ref + links = self.downloadAndExtractLinks(programref, 1, 'program') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + self.checkMediaUrl(videolink.stream) + + def testMetacafe(self): + # Category page + ref = self.getServiceReference('../../templates/metacafe') + links = self.downloadAndExtractLinks(ref, 3, 'category') + + # The first is "Search", the second is "Channels" and the + # third is the first "proper" navigation. + channelsref = links[1].ref + navigationref = links[2].ref + + # Navigation page + links = self.downloadAndExtractLinks(navigationref, 2, 'navigation') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + self.checkMediaUrl(videolink.stream) + + # User channels + links = self.downloadAndExtractLinks(channelsref, 3, 'channel list') + + def testMetacafeSearch(self): + menuobj = self.downloadMenuPage('wvt:///metacafe/search.xsl', 'search') + self.assertTrue(len(menuobj) >= 4, 'Too few items in search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemList)) + self.assertTrue(len(menuobj[1].items) == 3) + self.assertTrue(isinstance(menuobj[2], menu.MenuItemList)) + self.assertTrue(len(menuobj[2].items) == 4) + self.assertTrue(isinstance(menuobj[3], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'metacafe' + # Sort by: most discussed + menuobj[1].current = 2 + # Published: Anytime + menuobj[2].current = 2 + + resultref = menuobj[3].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testVimeo(self): + # Category page + ref = self.getServiceReference('../../templates/vimeo') + links = self.downloadAndExtractLinks(ref, 3, 'Vimeo main page') + + # The first is "Search", the second is "Channels" and the + # third is "Groups" + channelsref = links[1].ref + groupsref = links[2].ref + + # Channels page + links = self.downloadAndExtractLinks(channelsref, 2, 'channels') + + # Navigation page + links = self.downloadAndExtractLinks(links[0].ref, 2, 'channels navigation') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + self.checkMediaUrl(videolink.stream) + + # User groups + links = self.downloadAndExtractLinks(groupsref, 2, 'channel list') + + # Navigation page + links = self.downloadAndExtractLinks(links[0].ref, 2, 'groups navigation') + + def testVimeoSearch(self): + menuobj = self.downloadMenuPage('wvt:///vimeo/search.xsl?srcurl=http://www.vimeo.com/', 'search') + self.assertTrue(len(menuobj) >= 3, 'Too few items in search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemList)) + self.assertTrue(len(menuobj[1].items) >= 2) + self.assertTrue(isinstance(menuobj[2], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'vimeo' + # Sort by: newest + menuobj[1].current = 1 + + resultref = menuobj[2].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testYLEAreena(self): + # Category page + ref = self.getServiceReference('../../templates/yleareena') + links = self.downloadAndExtractLinks(ref, 3, 'category') + + # The first is "Search", the second is "live", the third is + # "all", the rest are navigation links. + liveref = links[1].ref + navigationref = links[3].ref + + # Navigation page + links = self.downloadAndExtractLinks(navigationref, 2, 'navigation') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + + # live broadcasts + links = self.downloadAndExtractLinks(liveref, 2, 'live broadcasts') + + def testYLEAreenaSearch(self): + menuobj = self.downloadMenuPage('wvt:///yleareena/search.xsl?srcurl=http://areena.yle.fi/haku', 'search') + self.assertTrue(len(menuobj) >= 8, 'Too few items in search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemList)) + self.assertTrue(len(menuobj[1].items) >= 3) + self.assertTrue(isinstance(menuobj[2], menu.MenuItemList)) + self.assertTrue(len(menuobj[2].items) >= 2) + self.assertTrue(isinstance(menuobj[3], menu.MenuItemList)) + self.assertTrue(len(menuobj[3].items) >= 2) + self.assertTrue(isinstance(menuobj[4], menu.MenuItemList)) + self.assertTrue(len(menuobj[4].items) >= 3) + self.assertTrue(isinstance(menuobj[5], menu.MenuItemList)) + self.assertTrue(len(menuobj[5].items) >= 4) + self.assertTrue(isinstance(menuobj[6], menu.MenuItemList)) + self.assertTrue(len(menuobj[6].items) >= 2) + self.assertTrue(isinstance(menuobj[7], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'yle' + # Media: video + menuobj[1].current = 1 + # Category: all + menuobj[2].current = 0 + # Channel: all + menuobj[3].current = 0 + # Language: Finnish + menuobj[4].current = 1 + # Uploaded: all + menuobj[5].current = 0 + # Only outside Finland: no + menuobj[6].current = 0 + + resultref = menuobj[7].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testKatsomo(self): + # Category page + ref = self.getServiceReference('../../templates/katsomo') + links = self.downloadAndExtractLinks(ref, 2, 'category') + + # The first is "Search", the rest are navigation links. + navigationref = links[1].ref + + # Navigation page + links = self.downloadAndExtractLinks(navigationref, 1, 'navigation') + + # Program page + links = self.downloadAndExtractLinks(links[0].ref, 1, 'program') + + # Video link + # The first few links may be navigation links, but there + # should be video links after them. + foundVideo = False + for link in links: + if link.stream is not None: + foundVideo = True + + self.assertTrue(link, 'No a video links in the program page') + + def testKatsomoSearch(self): + menuobj = self.downloadMenuPage('wvt:///katsomo/search.xsl', 'search') + self.assertTrue(len(menuobj) >= 2, 'Too few items in search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'mtv3' + + resultref = menuobj[1].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testRuutuFi(self): + # Category page + ref = self.getServiceReference('../../templates/ruutufi') + links = self.downloadAndExtractLinks(ref, 4, 'category') + + # The first is "Search", the second is "Series" + seriesref = links[1].ref + + # Series page + links = self.downloadAndExtractLinks(seriesref, 1, 'series') + + # Program page + links = self.downloadAndExtractLinks(links[0].ref, 1, 'program') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + + def testRuutuFiSearch(self): + menuobj = self.downloadMenuPage('wvt:///ruutufi/search.xsl', 'search') + self.assertTrue(len(menuobj) >= 2, 'Too few items in search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'nelonen' + + resultref = menuobj[1].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testSubtv(self): + # Category page + ref = self.getServiceReference('../../templates/subtv') + links = self.downloadAndExtractLinks(ref, 4, 'series') + + # Program page + links = self.downloadAndExtractLinks(links[0].ref, 1, 'program') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + + +if __name__ == '__main__': + testnames = sys.argv[1:] + + if testnames == []: + # Run all tests + unittest.main() + else: + # Run test listed on the command line + for test in testnames: + suite = unittest.TestSuite() + suite.addTest(TestServiceModules(test)) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/src/vdr-plugin/Makefile b/src/vdr-plugin/Makefile new file mode 100644 index 0000000..ccc5641 --- /dev/null +++ b/src/vdr-plugin/Makefile @@ -0,0 +1,115 @@ +# +# Makefile for a Video Disk Recorder plugin +# +# $Id$ + +# The official name of this plugin. +# This name will be used in the '-P...' option of VDR to load the plugin. +# By default the main source file also carries this name. +# IMPORTANT: the presence of this macro is important for the Make.config +# file. So it must be defined, even if it is not used here! +# +PLUGIN = webvideo + +### The version number of this plugin (taken from the main source file): + +VERSION = $(shell grep 'const char \*VERSION *=' $(PLUGIN).c | awk '{ print $$5 }' | sed -e 's/[";]//g') + +### The C++ compiler and options: + +CXX ?= g++ +CXXFLAGS ?= -fPIC -g -O2 -Wall -Woverloaded-virtual -Wno-parentheses + +### The directory environment: + +VDRDIR = ../../../../.. +LIBDIR = ../../../../lib +TMPDIR = /tmp + +### Libraries + +LIBS = `xml2-config --libs` -L../libwebvi -lwebvi + +### Allow user defined options to overwrite defaults: + +-include $(VDRDIR)/Make.config + +### The version number of VDR's plugin API (taken from VDR's "config.h"): + +APIVERSION = $(shell sed -ne '/define APIVERSION/s/^.*"\(.*\)".*$$/\1/p' $(VDRDIR)/config.h) + +### The name of the distribution archive: + +ARCHIVE = $(PLUGIN)-$(VERSION) +PACKAGE = vdr-$(ARCHIVE) + +### Includes and Defines (add further entries here): + +LIBWEBVIINCPATH = ../libwebvi +INCLUDES += -I$(VDRDIR)/include $(LIBWEBVIINCLUDES) -I$(LIBWEBVIINCPATH) `xml2-config --cflags` + +DEFINES += -D_GNU_SOURCE -DPLUGIN_NAME_I18N='"$(PLUGIN)"' + +### The object files (add further files here): + +OBJS = $(PLUGIN).o buffer.o common.o config.o download.o history.o menu.o menudata.o mimetypes.o request.o player.o dictionary.o iniparser.o timer.o menu_timer.o + +### The main target: + +all: libvdr-$(PLUGIN).so i18n + +### Implicit rules: + +%.o: %.c + $(CXX) $(CXXFLAGS) -c $(DEFINES) $(INCLUDES) $< + +### Dependencies: + +MAKEDEP = $(CXX) -MM -MG +DEPFILE = .dependencies +$(DEPFILE): Makefile + @$(MAKEDEP) $(DEFINES) $(INCLUDES) $(OBJS:%.o=%.c) > $@ + +-include $(DEPFILE) + +### Internationalization (I18N): + +PODIR = po +LOCALEDIR = $(VDRDIR)/locale +I18Npo = $(wildcard $(PODIR)/*.po) +I18Nmsgs = $(addprefix $(LOCALEDIR)/, $(addsuffix /LC_MESSAGES/vdr-$(PLUGIN).mo, $(notdir $(foreach file, $(I18Npo), $(basename $(file)))))) +I18Npot = $(PODIR)/$(PLUGIN).pot + +%.mo: %.po + msgfmt -c -o $@ $< + +$(I18Npot): $(wildcard *.c) + xgettext -C -cTRANSLATORS --no-wrap --no-location -k -ktr -ktrNOOP --msgid-bugs-address='<see README>' -o $@ $^ + +%.po: $(I18Npot) + msgmerge -U --no-wrap --no-location --backup=none -q $@ $< + @touch $@ + +$(I18Nmsgs): $(LOCALEDIR)/%/LC_MESSAGES/vdr-$(PLUGIN).mo: $(PODIR)/%.mo + @mkdir -p $(dir $@) + cp $< $@ + +.PHONY: i18n +i18n: $(I18Nmsgs) $(I18Npot) + +### Targets: + +libvdr-$(PLUGIN).so: $(OBJS) + $(CXX) $(CXXFLAGS) -shared $(OBJS) $(LIBS) -o $@ + cp --remove-destination $@ $(LIBDIR)/$@.$(APIVERSION) + +dist: clean + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @mkdir $(TMPDIR)/$(ARCHIVE) + @cp -a * $(TMPDIR)/$(ARCHIVE) + @tar czf $(PACKAGE).tgz -C $(TMPDIR) $(ARCHIVE) + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @echo Distribution package created as $(PACKAGE).tgz + +clean: + @-rm -f $(OBJS) $(DEPFILE) *.so *.so.* *.tgz core* *~ $(PODIR)/*.mo $(PODIR)/*.pot diff --git a/src/vdr-plugin/buffer.c b/src/vdr-plugin/buffer.c new file mode 100644 index 0000000..41b2c38 --- /dev/null +++ b/src/vdr-plugin/buffer.c @@ -0,0 +1,84 @@ +/* + * buffer.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <vdr/tools.h> +#include "buffer.h" + +// --- cMemoryBuffer ------------------------------------------------------- + +cMemoryBuffer::cMemoryBuffer(size_t prealloc) { + capacity = prealloc; + buf = (char *)malloc(capacity*sizeof(char)); + offset = 0; + len = 0; +} + +cMemoryBuffer::~cMemoryBuffer() { + if (buf) + free(buf); +} + +void cMemoryBuffer::Realloc(size_t newsize) { + if (newsize > capacity-offset) { + if (newsize <= capacity) { + // The new buffer fits in the memory if we just move the current + // content offset bytes backwards. + buf = (char *)memmove(buf, &buf[offset], len); + offset = 0; + } else { + // We need to realloc. Move the content to the beginning of the + // buffer while we are at it. + capacity += min(capacity, (size_t)10*1024); + capacity = max(capacity, newsize); + char *newbuf = (char *)malloc(capacity*sizeof(char)); + if (newbuf) { + memcpy(newbuf, &buf[offset], len); + offset = 0; + free(buf); + buf = newbuf; + } + } + } +} + +ssize_t cMemoryBuffer::Put(const char *data, size_t bytes) { + if (len+bytes > Free()) { + Realloc(len+bytes); + } + + if (buf) { + memcpy(&buf[offset+len], data, bytes); + len += bytes; + return bytes; + } + return -1; +} + +ssize_t cMemoryBuffer::PutFromFile(int fd, size_t bytes) { + if (len+bytes > Free()) { + Realloc(len+bytes); + } + + if (buf) { + ssize_t r = safe_read(fd, &buf[offset+len], bytes); + if (r > 0) + len += r; + return r; + } else + return -1; +} + +void cMemoryBuffer::Pop(size_t bytes) { + if (bytes <= len) { + offset += bytes; + len -= bytes; + } +} diff --git a/src/vdr-plugin/buffer.h b/src/vdr-plugin/buffer.h new file mode 100644 index 0000000..0a5ee5c --- /dev/null +++ b/src/vdr-plugin/buffer.h @@ -0,0 +1,44 @@ +/* + * buffer.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_BUFFER_H +#define __WEBVIDEO_BUFFER_H + +#include <unistd.h> + +// --- cMemoryBuffer ------------------------------------------------------- + +// FIFO character buffer. + +class cMemoryBuffer { +private: + char *buf; + size_t offset; + size_t len; + size_t capacity; +protected: + size_t Free() { return capacity-len-offset; } + virtual void Realloc(size_t newsize); +public: + cMemoryBuffer(size_t prealloc = 10*1024); + virtual ~cMemoryBuffer(); + + // Put data into the end of the buffer + virtual ssize_t Put(const char *data, size_t length); + // Put data from a file descriptor fd to the buffer + virtual ssize_t PutFromFile(int fd, size_t length); + // The pointer to the beginning of the buffer. Only valid until the + // next Put() or PutFromFile(). + virtual char *Get() { return &buf[offset]; } + // Remove first n bytes from the buffer. + void Pop(size_t n); + // Returns the current length of the buffer + virtual size_t Length() { return len; } +}; + +#endif // __WEBVIDEO_BUFFER_H diff --git a/src/vdr-plugin/common.c b/src/vdr-plugin/common.c new file mode 100644 index 0000000..0731da9 --- /dev/null +++ b/src/vdr-plugin/common.c @@ -0,0 +1,182 @@ +/* + * common.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <string.h> +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <unistd.h> +#include <ctype.h> +#include <vdr/tools.h> +#include "common.h" + +char *extensionFromUrl(const char *url) { + if (!url) + return NULL; + + // Find the possible query ("?query=foo") or fragment ("#bar"). The + // extension is located right before them. + size_t extendpos = strcspn(url, "?#"); + + size_t extstartpos = extendpos-1; + while ((extstartpos > 0) && (url[extstartpos] != '.') && (url[extstartpos] != '/')) + extstartpos--; + + if ((extstartpos > 0) && (url[extstartpos] == '.')) { + // We found the extension. Copy it to a buffer, and return it. + char *ext = (char *)malloc(sizeof(char)*(extendpos-extstartpos+1)); + memcpy(ext, &url[extstartpos], extendpos-extstartpos); + ext[extendpos-extstartpos] = '\0'; + + return ext; + } + + return NULL; +} + +char *validateFileName(const char *filename) { + if (!filename) + return NULL; + + char *validated = (char *)malloc(strlen(filename)+1); + int j=0; + for (unsigned int i=0; i<strlen(filename); i++) { + if (filename[i] != '/') { + validated[j++] = filename[i]; + } + } + validated[j] = '\0'; + return validated; +} + +int moveFile(const char *oldpath, const char *newpath) { + if (rename(oldpath, newpath) == 0) { + return 0; + } else if (errno == EXDEV) { + // rename can't move a file between file systems. We have to copy + // the file manually. + int fdout = open(newpath, O_WRONLY | O_CREAT | O_EXCL, DEFFILEMODE); + if (fdout < 0) { + return -1; + } + + int fdin = open(oldpath, O_RDONLY); + if (fdin < 0) { + close(fdout); + return -1; + } + + const int bufsize = 4096; + char buffer[bufsize]; + bool ok = true; + while (true) { + ssize_t len = safe_read(fdin, &buffer, bufsize); + if (len == 0) { + break; + } else if (len < 0) { + ok = false; + break; + } + + if (safe_write(fdout, &buffer, len) != len) { + ok = false; + break; + } + } + + close(fdin); + close(fdout); + + if (ok && (unlink(oldpath) <0)) { + return -1; + } + + return 0; + } else { + return -1; + } +} + +char *URLencode(const char *s) { + char reserved_and_unsafe[] = + { // reserved characters + '$', '&', '+', ',', '/', ':', ';', '=', '?', '@', + // unsafe characters + ' ', '"', '<', '>', '#', '%', '{', '}', + '|', '\\', '^', '~', '[', ']', '`', + '\0' + }; + + char *buf = (char *)malloc((3*strlen(s)+1)*sizeof(char)); + if (!buf) + return NULL; + + unsigned char *out; + const unsigned char *in; + for (out=(unsigned char *)buf, in=(const unsigned char *)s; *in != '\0'; in++) { + if ((*in < 32) // control chracters + || (strchr(reserved_and_unsafe, *in)) // reserved and unsafe + || (*in > 127)) // non-ASCII + { + snprintf((char *)out, 4, "%%%02hhX", *in); + out += 3; + } else { + *out = *in; + out++; + } + } + *out = '\0'; + + return buf; +} + +char *URLdecode(const char *s) { + char *res = (char *)malloc(strlen(s)+1); + const char *in = s; + char *out = res; + const char *hex = "0123456789ABCDEF"; + const char *h1, *h2; + + while (*in) { + if ((*in == '%') && (in[1] != '\0') && (in[2] != '\0')) { + h1 = strchr(hex, toupper(in[1])); + h2 = strchr(hex, toupper(in[2])); + if (h1 && h2) { + *out = ((h1-hex) << 4) + (h2-hex); + in += 3; + } else { + *out = *in; + in++; + } + } else { + *out = *in; + in++; + } + out++; + } + *out = '\0'; + + return res; +} + +char *safeFilename(char *filename) { + if (filename) { + strreplace(filename, '/', '!'); + + char *p = filename; + while ((*p == '.') || isspace(*p)) { + p++; + } + + if (p != filename) { + memmove(filename, p, strlen(p)+1); + } + } + + return filename; +} diff --git a/src/vdr-plugin/common.h b/src/vdr-plugin/common.h new file mode 100644 index 0000000..5b4385f --- /dev/null +++ b/src/vdr-plugin/common.h @@ -0,0 +1,42 @@ +/* + * common.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_COMMON_H +#define __WEBVIDEO_COMMON_H + +#ifdef DEBUG +#define debug(x...) dsyslog("Webvideo: " x); +#define info(x...) isyslog("Webvideo: " x); +#define warning(x...) esyslog("Webvideo: Warning: " x); +#define error(x...) esyslog("Webvideo: " x); +#else +#define debug(x...) ; +#define info(x...) isyslog("Webvideo: " x); +#define warning(x...) esyslog("Webvideo: Warning: " x); +#define error(x...) esyslog("Webvideo: " x); +#endif + +// Return the extension of the url or NULL, if the url has no +// extension. The caller must free the returned string. +char *extensionFromUrl(const char *url); +// Returns a "safe" version of filename. Currently just removes / from +// the name. The caller must free the returned string. +char *validateFileName(const char *filename); +int moveFile(const char *oldpath, const char *newpath); +// Return the URL encoded version of s. The called must free the +// returned memory. +char *URLencode(const char *s); +// Remove URL encoding from s. The called must free the returned +// memory. +char *URLdecode(const char *s); +// Return a "safe" version of filename. Remove path (replace '/' with +// '!') and dots from the beginning. The string is modified in-place, +// i.e. returns the pointer filename that was passed as argument. +char *safeFilename(char *filename); + +#endif // __WEBVIDEO_COMMON_H diff --git a/src/vdr-plugin/config.c b/src/vdr-plugin/config.c new file mode 100644 index 0000000..f294e60 --- /dev/null +++ b/src/vdr-plugin/config.c @@ -0,0 +1,199 @@ +/* + * config.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <stdlib.h> +#include <string.h> +#include "config.h" +#include "dictionary.h" +#include "iniparser.h" +#include "common.h" + +// --- cDownloadQuality --------------------------------------------------- + +cDownloadQuality::cDownloadQuality(const char *sitename) +: min(NULL), max(NULL) { + site = sitename ? strdup(sitename) : NULL; +} + +cDownloadQuality::~cDownloadQuality() { + if (site) + free(site); + if (min) + free(min); + if (max) + free(max); +} + +void cDownloadQuality::SetMin(const char *val) { + if (min) + free(min); + + min = val ? strdup(val) : NULL; +} + +void cDownloadQuality::SetMax(const char *val) { + if (max) + free(max); + + max = val ? strdup(val) : NULL; +} + +const char *cDownloadQuality::GetSite() { + return site; +} + +const char *cDownloadQuality::GetMin() { + return min; +} + +const char *cDownloadQuality::GetMax() { + return max; +} + +// --- cWebvideoConfig ----------------------------------------------------- + +cWebvideoConfig *webvideoConfig = new cWebvideoConfig(); + +cWebvideoConfig::cWebvideoConfig() { + downloadPath = NULL; + templatePath = NULL; + preferXine = true; +} + +cWebvideoConfig::~cWebvideoConfig() { + if (downloadPath) + free(downloadPath); + if (templatePath) + free(templatePath); +} + +void cWebvideoConfig::SetDownloadPath(const char *path) { + if (downloadPath) + free(downloadPath); + downloadPath = path ? strdup(path) : NULL; +} + +const char *cWebvideoConfig::GetDownloadPath() { + return downloadPath; +} + +void cWebvideoConfig::SetTemplatePath(const char *path) { + if (templatePath) + free(templatePath); + templatePath = path ? strdup(path) : NULL; +} + +const char *cWebvideoConfig::GetTemplatePath() { + return templatePath; +} + +void cWebvideoConfig::SetPreferXineliboutput(bool pref) { + preferXine = pref; +} + +bool cWebvideoConfig::GetPreferXineliboutput() { + return preferXine; +} + +bool cWebvideoConfig::ReadConfigFile(const char *inifile) { + dictionary *conf = iniparser_load(inifile); + + if (!conf) + return false; + + info("loading config file %s", inifile); + + const char *templatepath = iniparser_getstring(conf, "webvi:templatepath", NULL); + if (templatepath) { + debug("templatepath = %s (from %s)", templatepath, inifile); + SetTemplatePath(templatepath); + } + + for (int i=0; i<iniparser_getnsec(conf); i++) { + const char *section = iniparser_getsecname(conf, i); + + if (strncmp(section, "site-", 5) == 0) { + const char *sitename = section+5; + const int maxsectionlen = 40; + char key[64]; + char *keyname; + + strncpy(key, section, maxsectionlen); + key[maxsectionlen] = '\0'; + strcat(key, ":"); + keyname = key+strlen(key); + + strcpy(keyname, "download-min-quality"); + const char *download_min = iniparser_getstring(conf, key, NULL); + + strcpy(keyname, "download-max-quality"); + const char *download_max = iniparser_getstring(conf, key, NULL); + + strcpy(keyname, "stream-min-quality"); + const char *stream_min = iniparser_getstring(conf, key, NULL); + + strcpy(keyname, "stream-max-quality"); + const char *stream_max = iniparser_getstring(conf, key, NULL); + + if (download_min || download_max) { + cDownloadQuality *limits = new cDownloadQuality(sitename); + limits->SetMin(download_min); + limits->SetMax(download_max); + downloadLimits.Add(limits); + + debug("download priorities for %s (from %s): min = %s, max = %s", + sitename, inifile, download_min, download_max); + } + + if (stream_min || stream_max) { + cDownloadQuality *limits = new cDownloadQuality(sitename); + limits->SetMin(stream_min); + limits->SetMax(stream_max); + streamLimits.Add(limits); + + debug("streaming priorities for %s (from %s): min = %s, max = %s", + sitename, inifile, stream_min, stream_max); + } + } + } + + iniparser_freedict(conf); + + return true; +} + +const char *cWebvideoConfig::GetQuality(const char *site, eRequestType type, int limit) { + if (type != REQT_FILE && type != REQT_STREAM) + return NULL; + + cList<cDownloadQuality>& priorlist = downloadLimits; + if (type == REQT_STREAM) + priorlist = streamLimits; + + cDownloadQuality *node = priorlist.First(); + + while (node && (strcmp(site, node->GetSite()) != 0)) { + node = priorlist.Next(node); + } + + if (!node) + return NULL; + + if (limit == 0) + return node->GetMin(); + else + return node->GetMax(); +} + +const char *cWebvideoConfig::GetMinQuality(const char *site, eRequestType type) { + return GetQuality(site, type, 0); +} + +const char *cWebvideoConfig::GetMaxQuality(const char *site, eRequestType type) { + return GetQuality(site, type, 1); +} diff --git a/src/vdr-plugin/config.h b/src/vdr-plugin/config.h new file mode 100644 index 0000000..29304b4 --- /dev/null +++ b/src/vdr-plugin/config.h @@ -0,0 +1,64 @@ +/* + * config.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_CONFIG_H +#define __WEBVIDEO_CONFIG_H + +#include <vdr/tools.h> +#include "request.h" + +class cDownloadQuality : public cListObject { +private: + char *site; + char *min; + char *max; + +public: + cDownloadQuality(const char *site); + ~cDownloadQuality(); + + void SetMin(const char *val); + void SetMax(const char *val); + + const char *GetSite(); + const char *GetMin(); + const char *GetMax(); +}; + +class cWebvideoConfig { +private: + char *downloadPath; + char *templatePath; + bool preferXine; + cList<cDownloadQuality> downloadLimits; + cList<cDownloadQuality> streamLimits; + + const char *GetQuality(const char *site, eRequestType type, int limit); + +public: + cWebvideoConfig(); + ~cWebvideoConfig(); + + bool ReadConfigFile(const char *inifile); + + void SetDownloadPath(const char *path); + const char *GetDownloadPath(); + + void SetTemplatePath(const char *path); + const char *GetTemplatePath(); + + void SetPreferXineliboutput(bool pref); + bool GetPreferXineliboutput(); + + const char *GetMinQuality(const char *site, eRequestType type); + const char *GetMaxQuality(const char *site, eRequestType type); +}; + +extern cWebvideoConfig *webvideoConfig; + +#endif diff --git a/src/vdr-plugin/dictionary.c b/src/vdr-plugin/dictionary.c new file mode 100644 index 0000000..4c5ae08 --- /dev/null +++ b/src/vdr-plugin/dictionary.c @@ -0,0 +1,410 @@ +/* + Copyright (c) 2000-2007 by Nicolas Devillard. + MIT License, see COPYING for more information. +*/ + +/*-------------------------------------------------------------------------*/ +/** + @file dictionary.c + @author N. Devillard + @date Sep 2007 + @version $Revision: 1.27 $ + @brief Implements a dictionary for string variables. + + This module implements a simple dictionary object, i.e. a list + of string/string associations. This object is useful to store e.g. + informations retrieved from a configuration file (ini files). +*/ +/*--------------------------------------------------------------------------*/ + +/* + $Id: dictionary.c,v 1.27 2007-11-23 21:39:18 ndevilla Exp $ + $Revision: 1.27 $ +*/ +/*--------------------------------------------------------------------------- + Includes + ---------------------------------------------------------------------------*/ +#include "dictionary.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +/** Maximum value size for integers and doubles. */ +#define MAXVALSZ 1024 + +/** Minimal allocated number of entries in a dictionary */ +#define DICTMINSZ 128 + +/** Invalid key token */ +#define DICT_INVALID_KEY ((char*)-1) + +/*--------------------------------------------------------------------------- + Private functions + ---------------------------------------------------------------------------*/ + +/* Doubles the allocated size associated to a pointer */ +/* 'size' is the current allocated size. */ +static void * mem_double(void * ptr, int size) +{ + void * newptr ; + + newptr = calloc(2*size, 1); + if (newptr==NULL) { + return NULL ; + } + memcpy(newptr, ptr, size); + free(ptr); + return newptr ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Duplicate a string + @param s String to duplicate + @return Pointer to a newly allocated string, to be freed with free() + + This is a replacement for strdup(). This implementation is provided + for systems that do not have it. + */ +/*--------------------------------------------------------------------------*/ +static char * xstrdup(char * s) +{ + char * t ; + if (!s) + return NULL ; + t = (char *)malloc(strlen(s)+1) ; + if (t) { + strcpy(t,s); + } + return t ; +} + +/*--------------------------------------------------------------------------- + Function codes + ---------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------*/ +/** + @brief Compute the hash key for a string. + @param key Character string to use for key. + @return 1 unsigned int on at least 32 bits. + + This hash function has been taken from an Article in Dr Dobbs Journal. + This is normally a collision-free function, distributing keys evenly. + The key is stored anyway in the struct so that collision can be avoided + by comparing the key itself in last resort. + */ +/*--------------------------------------------------------------------------*/ +unsigned dictionary_hash(char * key) +{ + int len ; + unsigned hash ; + int i ; + + len = strlen(key); + for (hash=0, i=0 ; i<len ; i++) { + hash += (unsigned)key[i] ; + hash += (hash<<10); + hash ^= (hash>>6) ; + } + hash += (hash <<3); + hash ^= (hash >>11); + hash += (hash <<15); + return hash ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Create a new dictionary object. + @param size Optional initial size of the dictionary. + @return 1 newly allocated dictionary objet. + + This function allocates a new dictionary object of given size and returns + it. If you do not know in advance (roughly) the number of entries in the + dictionary, give size=0. + */ +/*--------------------------------------------------------------------------*/ +dictionary * dictionary_new(int size) +{ + dictionary * d ; + + /* If no size was specified, allocate space for DICTMINSZ */ + if (size<DICTMINSZ) size=DICTMINSZ ; + + if (!(d = (dictionary *)calloc(1, sizeof(dictionary)))) { + return NULL; + } + d->size = size ; + d->val = (char **)calloc(size, sizeof(char*)); + d->key = (char **)calloc(size, sizeof(char*)); + d->hash = (unsigned int *)calloc(size, sizeof(unsigned)); + return d ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Delete a dictionary object + @param d dictionary object to deallocate. + @return void + + Deallocate a dictionary object and all memory associated to it. + */ +/*--------------------------------------------------------------------------*/ +void dictionary_del(dictionary * d) +{ + int i ; + + if (d==NULL) return ; + for (i=0 ; i<d->size ; i++) { + if (d->key[i]!=NULL) + free(d->key[i]); + if (d->val[i]!=NULL) + free(d->val[i]); + } + free(d->val); + free(d->key); + free(d->hash); + free(d); + return ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get a value from a dictionary. + @param d dictionary object to search. + @param key Key to look for in the dictionary. + @param def Default value to return if key not found. + @return 1 pointer to internally allocated character string. + + This function locates a key in a dictionary and returns a pointer to its + value, or the passed 'def' pointer if no such key can be found in + dictionary. The returned character pointer points to data internal to the + dictionary object, you should not try to free it or modify it. + */ +/*--------------------------------------------------------------------------*/ +char * dictionary_get(dictionary * d, char * key, char * def) +{ + unsigned hash ; + int i ; + + hash = dictionary_hash(key); + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + /* Compare hash */ + if (hash==d->hash[i]) { + /* Compare string, to avoid hash collisions */ + if (!strcmp(key, d->key[i])) { + return d->val[i] ; + } + } + } + return def ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Set a value in a dictionary. + @param d dictionary object to modify. + @param key Key to modify or add. + @param val Value to add. + @return int 0 if Ok, anything else otherwise + + If the given key is found in the dictionary, the associated value is + replaced by the provided one. If the key cannot be found in the + dictionary, it is added to it. + + It is Ok to provide a NULL value for val, but NULL values for the dictionary + or the key are considered as errors: the function will return immediately + in such a case. + + Notice that if you dictionary_set a variable to NULL, a call to + dictionary_get will return a NULL value: the variable will be found, and + its value (NULL) is returned. In other words, setting the variable + content to NULL is equivalent to deleting the variable from the + dictionary. It is not possible (in this implementation) to have a key in + the dictionary without value. + + This function returns non-zero in case of failure. + */ +/*--------------------------------------------------------------------------*/ +int dictionary_set(dictionary * d, char * key, char * val) +{ + int i ; + unsigned hash ; + + if (d==NULL || key==NULL) return -1 ; + + /* Compute hash for this key */ + hash = dictionary_hash(key) ; + /* Find if value is already in dictionary */ + if (d->n>0) { + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + if (hash==d->hash[i]) { /* Same hash value */ + if (!strcmp(key, d->key[i])) { /* Same key */ + /* Found a value: modify and return */ + if (d->val[i]!=NULL) + free(d->val[i]); + d->val[i] = val ? xstrdup(val) : NULL ; + /* Value has been modified: return */ + return 0 ; + } + } + } + } + /* Add a new value */ + /* See if dictionary needs to grow */ + if (d->n==d->size) { + + /* Reached maximum size: reallocate dictionary */ + d->val = (char **)mem_double(d->val, d->size * sizeof(char*)) ; + d->key = (char **)mem_double(d->key, d->size * sizeof(char*)) ; + d->hash = (unsigned int *)mem_double(d->hash, d->size * sizeof(unsigned)) ; + if ((d->val==NULL) || (d->key==NULL) || (d->hash==NULL)) { + /* Cannot grow dictionary */ + return -1 ; + } + /* Double size */ + d->size *= 2 ; + } + + /* Insert key in the first empty slot */ + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) { + /* Add key here */ + break ; + } + } + /* Copy key */ + d->key[i] = xstrdup(key); + d->val[i] = val ? xstrdup(val) : NULL ; + d->hash[i] = hash; + d->n ++ ; + return 0 ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Delete a key in a dictionary + @param d dictionary object to modify. + @param key Key to remove. + @return void + + This function deletes a key in a dictionary. Nothing is done if the + key cannot be found. + */ +/*--------------------------------------------------------------------------*/ +void dictionary_unset(dictionary * d, char * key) +{ + unsigned hash ; + int i ; + + if (key == NULL) { + return; + } + + hash = dictionary_hash(key); + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + /* Compare hash */ + if (hash==d->hash[i]) { + /* Compare string, to avoid hash collisions */ + if (!strcmp(key, d->key[i])) { + /* Found key */ + break ; + } + } + } + if (i>=d->size) + /* Key not found */ + return ; + + free(d->key[i]); + d->key[i] = NULL ; + if (d->val[i]!=NULL) { + free(d->val[i]); + d->val[i] = NULL ; + } + d->hash[i] = 0 ; + d->n -- ; + return ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Dump a dictionary to an opened file pointer. + @param d Dictionary to dump + @param f Opened file pointer. + @return void + + Dumps a dictionary onto an opened file pointer. Key pairs are printed out + as @c [Key]=[Value], one per line. It is Ok to provide stdout or stderr as + output file pointers. + */ +/*--------------------------------------------------------------------------*/ +void dictionary_dump(dictionary * d, FILE * out) +{ + int i ; + + if (d==NULL || out==NULL) return ; + if (d->n<1) { + fprintf(out, "empty dictionary\n"); + return ; + } + for (i=0 ; i<d->size ; i++) { + if (d->key[i]) { + fprintf(out, "%20s\t[%s]\n", + d->key[i], + d->val[i] ? d->val[i] : "UNDEF"); + } + } + return ; +} + + +/* Test code */ +#ifdef TESTDIC +#define NVALS 20000 +int main(int argc, char *argv[]) +{ + dictionary * d ; + char * val ; + int i ; + char cval[90] ; + + /* Allocate dictionary */ + printf("allocating...\n"); + d = dictionary_new(0); + + /* Set values in dictionary */ + printf("setting %d values...\n", NVALS); + for (i=0 ; i<NVALS ; i++) { + sprintf(cval, "%04d", i); + dictionary_set(d, cval, "salut"); + } + printf("getting %d values...\n", NVALS); + for (i=0 ; i<NVALS ; i++) { + sprintf(cval, "%04d", i); + val = dictionary_get(d, cval, DICT_INVALID_KEY); + if (val==DICT_INVALID_KEY) { + printf("cannot get value for key [%s]\n", cval); + } + } + printf("unsetting %d values...\n", NVALS); + for (i=0 ; i<NVALS ; i++) { + sprintf(cval, "%04d", i); + dictionary_unset(d, cval); + } + if (d->n != 0) { + printf("error deleting values\n"); + } + printf("deallocating...\n"); + dictionary_del(d); + return 0 ; +} +#endif +/* vim: set ts=4 et sw=4 tw=75 */ diff --git a/src/vdr-plugin/dictionary.h b/src/vdr-plugin/dictionary.h new file mode 100644 index 0000000..f39493e --- /dev/null +++ b/src/vdr-plugin/dictionary.h @@ -0,0 +1,178 @@ +/* + Copyright (c) 2000-2007 by Nicolas Devillard. + MIT License, see COPYING for more information. +*/ + +/*-------------------------------------------------------------------------*/ +/** + @file dictionary.h + @author N. Devillard + @date Sep 2007 + @version $Revision: 1.12 $ + @brief Implements a dictionary for string variables. + + This module implements a simple dictionary object, i.e. a list + of string/string associations. This object is useful to store e.g. + informations retrieved from a configuration file (ini files). +*/ +/*--------------------------------------------------------------------------*/ + +/* + $Id: dictionary.h,v 1.12 2007-11-23 21:37:00 ndevilla Exp $ + $Author: ndevilla $ + $Date: 2007-11-23 21:37:00 $ + $Revision: 1.12 $ +*/ + +#ifndef _DICTIONARY_H_ +#define _DICTIONARY_H_ + +/*--------------------------------------------------------------------------- + Includes + ---------------------------------------------------------------------------*/ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +/*--------------------------------------------------------------------------- + New types + ---------------------------------------------------------------------------*/ + + +/*-------------------------------------------------------------------------*/ +/** + @brief Dictionary object + + This object contains a list of string/string associations. Each + association is identified by a unique string key. Looking up values + in the dictionary is speeded up by the use of a (hopefully collision-free) + hash function. + */ +/*-------------------------------------------------------------------------*/ +typedef struct _dictionary_ { + int n ; /** Number of entries in dictionary */ + int size ; /** Storage size */ + char ** val ; /** List of string values */ + char ** key ; /** List of string keys */ + unsigned * hash ; /** List of hash values for keys */ +} dictionary ; + + +/*--------------------------------------------------------------------------- + Function prototypes + ---------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------*/ +/** + @brief Compute the hash key for a string. + @param key Character string to use for key. + @return 1 unsigned int on at least 32 bits. + + This hash function has been taken from an Article in Dr Dobbs Journal. + This is normally a collision-free function, distributing keys evenly. + The key is stored anyway in the struct so that collision can be avoided + by comparing the key itself in last resort. + */ +/*--------------------------------------------------------------------------*/ +unsigned dictionary_hash(char * key); + +/*-------------------------------------------------------------------------*/ +/** + @brief Create a new dictionary object. + @param size Optional initial size of the dictionary. + @return 1 newly allocated dictionary objet. + + This function allocates a new dictionary object of given size and returns + it. If you do not know in advance (roughly) the number of entries in the + dictionary, give size=0. + */ +/*--------------------------------------------------------------------------*/ +dictionary * dictionary_new(int size); + +/*-------------------------------------------------------------------------*/ +/** + @brief Delete a dictionary object + @param d dictionary object to deallocate. + @return void + + Deallocate a dictionary object and all memory associated to it. + */ +/*--------------------------------------------------------------------------*/ +void dictionary_del(dictionary * vd); + +/*-------------------------------------------------------------------------*/ +/** + @brief Get a value from a dictionary. + @param d dictionary object to search. + @param key Key to look for in the dictionary. + @param def Default value to return if key not found. + @return 1 pointer to internally allocated character string. + + This function locates a key in a dictionary and returns a pointer to its + value, or the passed 'def' pointer if no such key can be found in + dictionary. The returned character pointer points to data internal to the + dictionary object, you should not try to free it or modify it. + */ +/*--------------------------------------------------------------------------*/ +char * dictionary_get(dictionary * d, char * key, char * def); + + +/*-------------------------------------------------------------------------*/ +/** + @brief Set a value in a dictionary. + @param d dictionary object to modify. + @param key Key to modify or add. + @param val Value to add. + @return int 0 if Ok, anything else otherwise + + If the given key is found in the dictionary, the associated value is + replaced by the provided one. If the key cannot be found in the + dictionary, it is added to it. + + It is Ok to provide a NULL value for val, but NULL values for the dictionary + or the key are considered as errors: the function will return immediately + in such a case. + + Notice that if you dictionary_set a variable to NULL, a call to + dictionary_get will return a NULL value: the variable will be found, and + its value (NULL) is returned. In other words, setting the variable + content to NULL is equivalent to deleting the variable from the + dictionary. It is not possible (in this implementation) to have a key in + the dictionary without value. + + This function returns non-zero in case of failure. + */ +/*--------------------------------------------------------------------------*/ +int dictionary_set(dictionary * vd, char * key, char * val); + +/*-------------------------------------------------------------------------*/ +/** + @brief Delete a key in a dictionary + @param d dictionary object to modify. + @param key Key to remove. + @return void + + This function deletes a key in a dictionary. Nothing is done if the + key cannot be found. + */ +/*--------------------------------------------------------------------------*/ +void dictionary_unset(dictionary * d, char * key); + + +/*-------------------------------------------------------------------------*/ +/** + @brief Dump a dictionary to an opened file pointer. + @param d Dictionary to dump + @param f Opened file pointer. + @return void + + Dumps a dictionary onto an opened file pointer. Key pairs are printed out + as @c [Key]=[Value], one per line. It is Ok to provide stdout or stderr as + output file pointers. + */ +/*--------------------------------------------------------------------------*/ +void dictionary_dump(dictionary * d, FILE * out); + +#endif diff --git a/src/vdr-plugin/download.c b/src/vdr-plugin/download.c new file mode 100644 index 0000000..f9d956f --- /dev/null +++ b/src/vdr-plugin/download.c @@ -0,0 +1,222 @@ +/* + * download.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <errno.h> +#include <sys/select.h> +#include <unistd.h> +#include <fcntl.h> +#include <vdr/tools.h> +#include "download.h" +#include "common.h" + +// --- cWebviThread -------------------------------------------------------- + +cWebviThread::cWebviThread() { + int pipefd[2]; + + if (pipe(pipefd) == -1) + LOG_ERROR_STR("new request pipe"); + newreqread = pipefd[0]; + newreqwrite = pipefd[1]; + //fcntl(newreqread, F_SETFL, O_NONBLOCK); + //fcntl(newreqwrite, F_SETFL, O_NONBLOCK); + + webvi = webvi_initialize_context(); +} + +cWebviThread::~cWebviThread() { + int numactive = activeRequestList.Size(); + for (int i=0; i<activeRequestList.Size(); i++) + delete activeRequestList[i]; + activeRequestList.Clear(); + + for (int i=0; i<finishedRequestList.Size(); i++) { + delete finishedRequestList[i]; + } + finishedRequestList.Clear(); + + webvi_cleanup_context(webvi); + + if (numactive > 0) { + esyslog("%d requests failed to complete", numactive); + } +} + +cWebviThread &cWebviThread::Instance() { + static cWebviThread instance; + + return instance; +} + +void cWebviThread::SetTemplatePath(const char *path) { + if (webvi != 0 && path) + webvi_set_config(webvi, WEBVI_CONFIG_TEMPLATE_PATH, path); +} + +void cWebviThread::MoveToFinishedList(cMenuRequest *req) { + // Move the request from the activeList to finishedList. + requestMutex.Lock(); + for (int i=0; i<activeRequestList.Size(); i++) { + if (activeRequestList[i] == req) { + activeRequestList.Remove(i); + break; + } + } + finishedRequestList.Append(req); + + requestMutex.Unlock(); +} + +void cWebviThread::ActivateNewRequest() { + // Move requests from newRequestList to activeRequestList and start + // them. + requestMutex.Lock(); + for (int i=0; i<newRequestList.Size(); i++) { + cMenuRequest *req = newRequestList[i]; + if (req->IsAborted()) { + // The request has been aborted even before we got a chance to + // start it. + MoveToFinishedList(req); + } else { + debug("starting request %d", req->GetID()); + + if (!req->Start(webvi)) { + error("Request failed to start"); + req->RequestDone(-1, "Request failed to start"); + MoveToFinishedList(req); + } else { + activeRequestList.Append(req); + } + } + } + + newRequestList.Clear(); + requestMutex.Unlock(); +} + +void cWebviThread::StopFinishedRequests() { + // Check if some requests have finished, and move them to + // finishedRequestList. + int msg_remaining; + WebviMsg *donemsg; + cMenuRequest *req; + + do { + donemsg = webvi_get_message(webvi, &msg_remaining); + + if (donemsg && donemsg->msg == WEBVIMSG_DONE) { + requestMutex.Lock(); + req = activeRequestList.FindByHandle(donemsg->handle); + if (req) { + debug("Finished request %d", req->GetID()); + req->RequestDone(donemsg->status_code, donemsg->data); + MoveToFinishedList(req); + } + requestMutex.Unlock(); + } + } while (msg_remaining > 0); +} + +void cWebviThread::Stop() { + // The thread may be sleeping, wake it up first. + TEMP_FAILURE_RETRY(write(newreqwrite, "S", 1)); + Cancel(5); +} + +void cWebviThread::Action(void) { + fd_set readfds, writefds, excfds; + int maxfd; + struct timeval timeout; + long running_handles; + bool check_done = false; + + if (webvi == 0) { + error("Failed to get libwebvi context"); + return; + } + + while (Running()) { + FD_ZERO(&readfds); + FD_ZERO(&writefds); + FD_ZERO(&excfds); + webvi_fdset(webvi, &readfds, &writefds, &excfds, &maxfd); + FD_SET(newreqread, &readfds); + if (newreqread > maxfd) + maxfd = newreqread; + + timeout.tv_sec = 5; + timeout.tv_usec = 0; + + int s = TEMP_FAILURE_RETRY(select(maxfd+1, &readfds, &writefds, NULL, + &timeout)); + if (s == -1) { + // select error + LOG_ERROR_STR("select() error in webvideo downloader thread:"); + Cancel(-1); + + } else if (s == 0) { + // timeout + webvi_perform(webvi, 0, WEBVI_SELECT_TIMEOUT, &running_handles); + check_done = true; + + } else { + for (int fd=0; fd<=maxfd; fd++) { + if (FD_ISSET(fd, &readfds)) { + if (fd == newreqread) { + char tmpbuf[8]; + int n = read(fd, tmpbuf, 8); + if (n > 0 && memchr(tmpbuf, 'S', n)) + Cancel(-1); + ActivateNewRequest(); + } else { + webvi_perform(webvi, fd, WEBVI_SELECT_READ, &running_handles); + check_done = true; + } + } + if (FD_ISSET(fd, &writefds)) + webvi_perform(webvi, fd, WEBVI_SELECT_WRITE, &running_handles); + if (FD_ISSET(fd, &excfds)) + webvi_perform(webvi, fd, WEBVI_SELECT_EXCEPTION, &running_handles); + } + } + + if (check_done) { + StopFinishedRequests(); + check_done = false; + } + } +} + +void cWebviThread::AddRequest(cMenuRequest *req) { + requestMutex.Lock(); + newRequestList.Append(req); + requestMutex.Unlock(); + + int s = TEMP_FAILURE_RETRY(write(newreqwrite, "*", 1)); + if (s == -1) + LOG_ERROR_STR("Failed to signal new webvideo request"); +} + +cMenuRequest *cWebviThread::GetFinishedRequest() { + cMenuRequest *res = NULL; + requestMutex.Lock(); + if (finishedRequestList.Size() > 0) { + res = finishedRequestList[finishedRequestList.Size()-1]; + finishedRequestList.Remove(finishedRequestList.Size()-1); + } + requestMutex.Unlock(); + + return res; +} + +int cWebviThread::GetUnfinishedCount() { + if (!Running()) + return 0; + else + return activeRequestList.Size(); +} diff --git a/src/vdr-plugin/download.h b/src/vdr-plugin/download.h new file mode 100644 index 0000000..5f29150 --- /dev/null +++ b/src/vdr-plugin/download.h @@ -0,0 +1,59 @@ +/* + * download.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_DOWNLOAD_H +#define __WEBVIDEO_DOWNLOAD_H + +#include <vdr/thread.h> +#include <libwebvi.h> +#include "request.h" + +// --- cWebviThread -------------------------------------------------------- + +class cWebviThread : public cThread { +private: + WebviCtx webvi; + cMutex requestMutex; + cRequestVector activeRequestList; + cRequestVector newRequestList; + cRequestVector finishedRequestList; + int newreqread, newreqwrite; + + void MoveToFinishedList(cMenuRequest *req); + void ActivateNewRequest(); + void StopFinishedRequests(); + +protected: + void Action(void); + +public: + cWebviThread(); + ~cWebviThread(); + + static cWebviThread &Instance(); + + // Stop the thread + void Stop(); + // Set path to the site templates. Should be set before + // Start()ing the thread. + void SetTemplatePath(const char *path); + // Start executing req. The control of req is handed over to the + // downloader thread. The main thread should not access req until + // the request is handed back to the main thread by + // GetFinishedRequest(). + void AddRequest(cMenuRequest *req); + // Return a request that has finished or NULL if no requests are + // finished. The ownership of the returned cMenuRequest object + // is again assigned to the main thread. The main thread should poll + // this function periodically. + cMenuRequest *GetFinishedRequest(); + // Returns the number download requests currectly active + int GetUnfinishedCount(); +}; + +#endif diff --git a/src/vdr-plugin/history.c b/src/vdr-plugin/history.c new file mode 100644 index 0000000..a463bac --- /dev/null +++ b/src/vdr-plugin/history.c @@ -0,0 +1,145 @@ +/* + * history.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <string.h> +#include "history.h" +#include "menu.h" + +// --- cHistoryObject ----------------------------------------------------- + +cHistoryObject::cHistoryObject(const char *xml, const char *ref, int ID) { + osdxml = strdup(xml); + reference = strdup(ref); + id = ID; + selected = 0; +} + +cHistoryObject::~cHistoryObject() { + if (osdxml) + free(osdxml); + if (reference) + free(reference); + + for (int i=0; i < editData.Size(); i++) + delete editData[i]; +} + +cQueryData *cHistoryObject::GetEditItem(const char *controlName) { + for (int i=0; i < editData.Size(); i++) { + if (strcmp(editData[i]->GetName(), controlName) == 0) { + return editData[i]; + } + } + + return NULL; +} + +int cHistoryObject::QuerySize() const { + return editData.Size(); +} + +char *cHistoryObject::GetQueryFragment(int i) const { + if (i < 0 && i >= editData.Size()) + return NULL; + else + return editData[i]->GetQueryFragment(); +} + +cTextFieldData *cHistoryObject::GetTextFieldData(const char *controlName) { + cQueryData *edititem = GetEditItem(controlName); + cTextFieldData *tfdata = dynamic_cast<cTextFieldData *>(edititem); + + if (!tfdata) { + tfdata = new cTextFieldData(controlName, 256); + editData.Append(tfdata); + } + + return tfdata; +} + +cItemListData *cHistoryObject::GetItemListData(const char *controlName, + cStringList &items, + cStringList &values) { + int n; + char **itemtable, **itemvaluetable; + cQueryData *edititem = GetEditItem(controlName); + cItemListData *ildata = dynamic_cast<cItemListData *>(edititem); + + if (!ildata) { + n = min(items.Size(), values.Size()); + itemtable = (char **)malloc(n*sizeof(char *)); + itemvaluetable = (char **)malloc(n*sizeof(char *)); + + for (int i=0; i<n; i++) { + itemtable[i] = strdup(csc.Convert(items[i])); + itemvaluetable[i] = strdup(values[i]); + } + + ildata = new cItemListData(controlName, + itemtable, + itemvaluetable, + n); + + editData.Append(ildata); + } + + return ildata; +} + +// --- cHistory ------------------------------------------------------------ + +cHistory::cHistory() { + current = NULL; +} + +void cHistory::Clear() { + current = NULL; + cList<cHistoryObject>::Clear(); +} + +void cHistory::TruncateAndAdd(cHistoryObject *page) { + cHistoryObject *last = Last(); + while ((last) && (last != current)) { + Del(last); + last = Last(); + } + + Add(page); + current = Last(); +} + +void cHistory::Reset() { + current = NULL; +} + +cHistoryObject *cHistory::Current() { + return current; +} + +cHistoryObject *cHistory::Home() { + current = First(); + return current; +} + +cHistoryObject *cHistory::Back() { + if (current) + current = Prev(current); + return current; +} + +cHistoryObject *cHistory::Forward() { + cHistoryObject *next; + if (current) { + next = Next(current); + if (next) + current = next; + } else { + current = First(); + } + return current; +} diff --git a/src/vdr-plugin/history.h b/src/vdr-plugin/history.h new file mode 100644 index 0000000..fd5fcf9 --- /dev/null +++ b/src/vdr-plugin/history.h @@ -0,0 +1,62 @@ +/* + * history.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_HISTORY_H +#define __WEBVIDEO_HISTORY_H + +#include <vdr/tools.h> +#include "menudata.h" + +// --- cHistoryObject ----------------------------------------------------- + +class cHistoryObject : public cListObject { +private: + char *osdxml; + int id; + int selected; + cVector<cQueryData *> editData; + char *reference; + + cQueryData *GetEditItem(const char *controlName); + +public: + cHistoryObject(const char *xml, const char *reference, int ID); + ~cHistoryObject(); + + int GetID() const { return id; } + const char *GetOSD() const { return osdxml; } + const char *GetReference() const { return reference; } + void RememberSelected(int sel) { selected = sel; } + int GetSelected() const { return selected; } + + int QuerySize() const; + char *GetQueryFragment(int i) const; + cTextFieldData *GetTextFieldData(const char *controlName); + cItemListData *GetItemListData(const char *controlName, + cStringList &items, + cStringList &itemvalues); +}; + +// --- cHistory ------------------------------------------------------------ + +class cHistory : public cList<cHistoryObject> { +private: + cHistoryObject *current; +public: + cHistory(); + + void Clear(); + void TruncateAndAdd(cHistoryObject *page); + void Reset(); + cHistoryObject *Current(); + cHistoryObject *Home(); + cHistoryObject *Back(); + cHistoryObject *Forward(); +}; + +#endif // __WEBVIDEO_HISTORY_H diff --git a/src/vdr-plugin/iniparser.c b/src/vdr-plugin/iniparser.c new file mode 100644 index 0000000..3990e74 --- /dev/null +++ b/src/vdr-plugin/iniparser.c @@ -0,0 +1,650 @@ +/* + Copyright (c) 2000-2007 by Nicolas Devillard. + MIT License, see COPYING for more information. +*/ + +/*-------------------------------------------------------------------------*/ +/** + @file iniparser.c + @author N. Devillard + @date Sep 2007 + @version 3.0 + @brief Parser for ini files. +*/ +/*--------------------------------------------------------------------------*/ +/* + $Id: iniparser.c,v 2.18 2008-01-03 18:35:39 ndevilla Exp $ + $Revision: 2.18 $ + $Date: 2008-01-03 18:35:39 $ +*/ +/*---------------------------- Includes ------------------------------------*/ +#include <ctype.h> +#include "iniparser.h" + +/*---------------------------- Defines -------------------------------------*/ +#define ASCIILINESZ (1024) +#define INI_INVALID_KEY ((char*)-1) + +/*--------------------------------------------------------------------------- + Private to this module + ---------------------------------------------------------------------------*/ +/** + * This enum stores the status for each parsed line (internal use only). + */ +typedef enum _line_status_ { + LINE_UNPROCESSED, + LINE_ERROR, + LINE_EMPTY, + LINE_COMMENT, + LINE_SECTION, + LINE_VALUE +} line_status ; + +/*-------------------------------------------------------------------------*/ +/** + @brief Convert a string to lowercase. + @param s String to convert. + @return ptr to statically allocated string. + + This function returns a pointer to a statically allocated string + containing a lowercased version of the input string. Do not free + or modify the returned string! Since the returned string is statically + allocated, it will be modified at each function call (not re-entrant). + */ +/*--------------------------------------------------------------------------*/ +static char * strlwc(const char * s) +{ + static char l[ASCIILINESZ+1]; + int i ; + + if (s==NULL) return NULL ; + memset(l, 0, ASCIILINESZ+1); + i=0 ; + while (s[i] && i<ASCIILINESZ) { + l[i] = (char)tolower((int)s[i]); + i++ ; + } + l[ASCIILINESZ]=(char)0; + return l ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Remove blanks at the beginning and the end of a string. + @param s String to parse. + @return ptr to statically allocated string. + + This function returns a pointer to a statically allocated string, + which is identical to the input string, except that all blank + characters at the end and the beg. of the string have been removed. + Do not free or modify the returned string! Since the returned string + is statically allocated, it will be modified at each function call + (not re-entrant). + */ +/*--------------------------------------------------------------------------*/ +static char * strstrip(char * s) +{ + static char l[ASCIILINESZ+1]; + char * last ; + + if (s==NULL) return NULL ; + + while (isspace((int)*s) && *s) s++; + memset(l, 0, ASCIILINESZ+1); + strcpy(l, s); + last = l + strlen(l); + while (last > l) { + if (!isspace((int)*(last-1))) + break ; + last -- ; + } + *last = (char)0; + return (char*)l ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get number of sections in a dictionary + @param d Dictionary to examine + @return int Number of sections found in dictionary + + This function returns the number of sections found in a dictionary. + The test to recognize sections is done on the string stored in the + dictionary: a section name is given as "section" whereas a key is + stored as "section:key", thus the test looks for entries that do not + contain a colon. + + This clearly fails in the case a section name contains a colon, but + this should simply be avoided. + + This function returns -1 in case of error. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_getnsec(dictionary * d) +{ + int i ; + int nsec ; + + if (d==NULL) return -1 ; + nsec=0 ; + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + if (strchr(d->key[i], ':')==NULL) { + nsec ++ ; + } + } + return nsec ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get name for section n in a dictionary. + @param d Dictionary to examine + @param n Section number (from 0 to nsec-1). + @return Pointer to char string + + This function locates the n-th section in a dictionary and returns + its name as a pointer to a string statically allocated inside the + dictionary. Do not free or modify the returned string! + + This function returns NULL in case of error. + */ +/*--------------------------------------------------------------------------*/ +char * iniparser_getsecname(dictionary * d, int n) +{ + int i ; + int foundsec ; + + if (d==NULL || n<0) return NULL ; + foundsec=0 ; + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + if (strchr(d->key[i], ':')==NULL) { + foundsec++ ; + if (foundsec>n) + break ; + } + } + if (foundsec<=n) { + return NULL ; + } + return d->key[i] ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Dump a dictionary to an opened file pointer. + @param d Dictionary to dump. + @param f Opened file pointer to dump to. + @return void + + This function prints out the contents of a dictionary, one element by + line, onto the provided file pointer. It is OK to specify @c stderr + or @c stdout as output files. This function is meant for debugging + purposes mostly. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_dump(dictionary * d, FILE * f) +{ + int i ; + + if (d==NULL || f==NULL) return ; + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + if (d->val[i]!=NULL) { + fprintf(f, "[%s]=[%s]\n", d->key[i], d->val[i]); + } else { + fprintf(f, "[%s]=UNDEF\n", d->key[i]); + } + } + return ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Save a dictionary to a loadable ini file + @param d Dictionary to dump + @param f Opened file pointer to dump to + @return void + + This function dumps a given dictionary into a loadable ini file. + It is Ok to specify @c stderr or @c stdout as output files. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_dump_ini(dictionary * d, FILE * f) +{ + int i, j ; + char keym[ASCIILINESZ+1]; + int nsec ; + char * secname ; + int seclen ; + + if (d==NULL || f==NULL) return ; + + nsec = iniparser_getnsec(d); + if (nsec<1) { + /* No section in file: dump all keys as they are */ + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + fprintf(f, "%s = %s\n", d->key[i], d->val[i]); + } + return ; + } + for (i=0 ; i<nsec ; i++) { + secname = iniparser_getsecname(d, i) ; + seclen = (int)strlen(secname); + fprintf(f, "\n[%s]\n", secname); + sprintf(keym, "%s:", secname); + for (j=0 ; j<d->size ; j++) { + if (d->key[j]==NULL) + continue ; + if (!strncmp(d->key[j], keym, seclen+1)) { + fprintf(f, + "%-30s = %s\n", + d->key[j]+seclen+1, + d->val[j] ? d->val[j] : ""); + } + } + } + fprintf(f, "\n"); + return ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key + @param d Dictionary to search + @param key Key string to look for + @param def Default value to return if key not found. + @return pointer to statically allocated character string + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the pointer passed as 'def' is returned. + The returned char pointer is pointing to a string allocated in + the dictionary, do not free or modify it. + */ +/*--------------------------------------------------------------------------*/ +char * iniparser_getstring(dictionary * d, const char * key, char * def) +{ + char * lc_key ; + char * sval ; + + if (d==NULL || key==NULL) + return def ; + + lc_key = strlwc(key); + sval = dictionary_get(d, lc_key, def); + return sval ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key, convert to an int + @param d Dictionary to search + @param key Key string to look for + @param notfound Value to return in case of error + @return integer + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the notfound value is returned. + + Supported values for integers include the usual C notation + so decimal, octal (starting with 0) and hexadecimal (starting with 0x) + are supported. Examples: + + "42" -> 42 + "042" -> 34 (octal -> decimal) + "0x42" -> 66 (hexa -> decimal) + + Warning: the conversion may overflow in various ways. Conversion is + totally outsourced to strtol(), see the associated man page for overflow + handling. + + Credits: Thanks to A. Becker for suggesting strtol() + */ +/*--------------------------------------------------------------------------*/ +int iniparser_getint(dictionary * d, const char * key, int notfound) +{ + char * str ; + + str = iniparser_getstring(d, key, INI_INVALID_KEY); + if (str==INI_INVALID_KEY) return notfound ; + return (int)strtol(str, NULL, 0); +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key, convert to a double + @param d Dictionary to search + @param key Key string to look for + @param notfound Value to return in case of error + @return double + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the notfound value is returned. + */ +/*--------------------------------------------------------------------------*/ +double iniparser_getdouble(dictionary * d, char * key, double notfound) +{ + char * str ; + + str = iniparser_getstring(d, key, INI_INVALID_KEY); + if (str==INI_INVALID_KEY) return notfound ; + return atof(str); +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key, convert to a boolean + @param d Dictionary to search + @param key Key string to look for + @param notfound Value to return in case of error + @return integer + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the notfound value is returned. + + A true boolean is found if one of the following is matched: + + - A string starting with 'y' + - A string starting with 'Y' + - A string starting with 't' + - A string starting with 'T' + - A string starting with '1' + + A false boolean is found if one of the following is matched: + + - A string starting with 'n' + - A string starting with 'N' + - A string starting with 'f' + - A string starting with 'F' + - A string starting with '0' + + The notfound value returned if no boolean is identified, does not + necessarily have to be 0 or 1. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_getboolean(dictionary * d, const char * key, int notfound) +{ + char * c ; + int ret ; + + c = iniparser_getstring(d, key, INI_INVALID_KEY); + if (c==INI_INVALID_KEY) return notfound ; + if (c[0]=='y' || c[0]=='Y' || c[0]=='1' || c[0]=='t' || c[0]=='T') { + ret = 1 ; + } else if (c[0]=='n' || c[0]=='N' || c[0]=='0' || c[0]=='f' || c[0]=='F') { + ret = 0 ; + } else { + ret = notfound ; + } + return ret; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Finds out if a given entry exists in a dictionary + @param ini Dictionary to search + @param entry Name of the entry to look for + @return integer 1 if entry exists, 0 otherwise + + Finds out if a given entry exists in the dictionary. Since sections + are stored as keys with NULL associated values, this is the only way + of querying for the presence of sections in a dictionary. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_find_entry( + dictionary * ini, + char * entry +) +{ + int found=0 ; + if (iniparser_getstring(ini, entry, INI_INVALID_KEY)!=INI_INVALID_KEY) { + found = 1 ; + } + return found ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Set an entry in a dictionary. + @param ini Dictionary to modify. + @param entry Entry to modify (entry name) + @param val New value to associate to the entry. + @return int 0 if Ok, -1 otherwise. + + If the given entry can be found in the dictionary, it is modified to + contain the provided value. If it cannot be found, -1 is returned. + It is Ok to set val to NULL. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_set(dictionary * ini, char * entry, char * val) +{ + return dictionary_set(ini, strlwc(entry), val) ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Delete an entry in a dictionary + @param ini Dictionary to modify + @param entry Entry to delete (entry name) + @return void + + If the given entry can be found, it is deleted from the dictionary. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_unset(dictionary * ini, char * entry) +{ + dictionary_unset(ini, strlwc(entry)); +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Load a single line from an INI file + @param input_line Input line, may be concatenated multi-line input + @param section Output space to store section + @param key Output space to store key + @param value Output space to store value + @return line_status value + */ +/*--------------------------------------------------------------------------*/ +static line_status iniparser_line( + char * input_line, + char * section, + char * key, + char * value) +{ + line_status sta ; + char line[ASCIILINESZ+1]; + int len ; + + strcpy(line, strstrip(input_line)); + len = (int)strlen(line); + + sta = LINE_UNPROCESSED ; + if (len<1) { + /* Empty line */ + sta = LINE_EMPTY ; + } else if (line[0]=='#') { + /* Comment line */ + sta = LINE_COMMENT ; + } else if (line[0]=='[' && line[len-1]==']') { + /* Section name */ + sscanf(line, "[%[^]]", section); + strcpy(section, strstrip(section)); + strcpy(section, strlwc(section)); + sta = LINE_SECTION ; + } else if (sscanf (line, "%[^=] = \"%[^\"]\"", key, value) == 2 + || sscanf (line, "%[^=] = '%[^\']'", key, value) == 2 + || sscanf (line, "%[^=] = %[^;#]", key, value) == 2) { + /* Usual key=value, with or without comments */ + strcpy(key, strstrip(key)); + strcpy(key, strlwc(key)); + strcpy(value, strstrip(value)); + /* + * sscanf cannot handle '' or "" as empty values + * this is done here + */ + if (!strcmp(value, "\"\"") || (!strcmp(value, "''"))) { + value[0]=0 ; + } + sta = LINE_VALUE ; + } else if (sscanf(line, "%[^=] = %[;#]", key, value)==2 + || sscanf(line, "%[^=] %[=]", key, value) == 2) { + /* + * Special cases: + * key= + * key=; + * key=# + */ + strcpy(key, strstrip(key)); + strcpy(key, strlwc(key)); + value[0]=0 ; + sta = LINE_VALUE ; + } else { + /* Generate syntax error */ + sta = LINE_ERROR ; + } + return sta ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Parse an ini file and return an allocated dictionary object + @param ininame Name of the ini file to read. + @return Pointer to newly allocated dictionary + + This is the parser for ini files. This function is called, providing + the name of the file to be read. It returns a dictionary object that + should not be accessed directly, but through accessor functions + instead. + + The returned dictionary must be freed using iniparser_freedict(). + */ +/*--------------------------------------------------------------------------*/ +dictionary * iniparser_load(const char * ininame) +{ + FILE * in ; + + char line [ASCIILINESZ+1] ; + char section [ASCIILINESZ+1] ; + char key [ASCIILINESZ+1] ; + char tmp [ASCIILINESZ+1] ; + char val [ASCIILINESZ+1] ; + + int last=0 ; + int len ; + int lineno=0 ; + int errs=0; + + dictionary * dict ; + + if ((in=fopen(ininame, "r"))==NULL) { + fprintf(stderr, "iniparser: cannot open %s\n", ininame); + return NULL ; + } + + dict = dictionary_new(0) ; + if (!dict) { + fclose(in); + return NULL ; + } + + memset(line, 0, ASCIILINESZ); + memset(section, 0, ASCIILINESZ); + memset(key, 0, ASCIILINESZ); + memset(val, 0, ASCIILINESZ); + last=0 ; + + while (fgets(line+last, ASCIILINESZ-last, in)!=NULL) { + lineno++ ; + len = (int)strlen(line)-1; + /* Safety check against buffer overflows */ + if (line[len]!='\n') { + fprintf(stderr, + "iniparser: input line too long in %s (%d)\n", + ininame, + lineno); + dictionary_del(dict); + fclose(in); + return NULL ; + } + /* Get rid of \n and spaces at end of line */ + while ((len>=0) && + ((line[len]=='\n') || (isspace(line[len])))) { + line[len]=0 ; + len-- ; + } + /* Detect multi-line */ + if (line[len]=='\\') { + /* Multi-line value */ + last=len ; + continue ; + } else { + last=0 ; + } + switch (iniparser_line(line, section, key, val)) { + case LINE_EMPTY: + case LINE_COMMENT: + break ; + + case LINE_SECTION: + errs = dictionary_set(dict, section, NULL); + break ; + + case LINE_VALUE: + sprintf(tmp, "%s:%s", section, key); + errs = dictionary_set(dict, tmp, val) ; + break ; + + case LINE_ERROR: + fprintf(stderr, "iniparser: syntax error in %s (%d):\n", + ininame, + lineno); + fprintf(stderr, "-> %s\n", line); + errs++ ; + break; + + default: + break ; + } + memset(line, 0, ASCIILINESZ); + last=0; + if (errs<0) { + fprintf(stderr, "iniparser: memory allocation failure\n"); + break ; + } + } + if (errs) { + dictionary_del(dict); + dict = NULL ; + } + fclose(in); + return dict ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Free all memory associated to an ini dictionary + @param d Dictionary to free + @return void + + Free all memory associated to an ini dictionary. + It is mandatory to call this function before the dictionary object + gets out of the current context. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_freedict(dictionary * d) +{ + dictionary_del(d); +} + +/* vim: set ts=4 et sw=4 tw=75 */ diff --git a/src/vdr-plugin/iniparser.h b/src/vdr-plugin/iniparser.h new file mode 100644 index 0000000..78bf339 --- /dev/null +++ b/src/vdr-plugin/iniparser.h @@ -0,0 +1,284 @@ +/* + Copyright (c) 2000-2007 by Nicolas Devillard. + MIT License, see COPYING for more information. +*/ + +/*-------------------------------------------------------------------------*/ +/** + @file iniparser.h + @author N. Devillard + @date Sep 2007 + @version 3.0 + @brief Parser for ini files. +*/ +/*--------------------------------------------------------------------------*/ + +/* + $Id: iniparser.h,v 1.24 2007-11-23 21:38:19 ndevilla Exp $ + $Revision: 1.24 $ +*/ + +#ifndef _INIPARSER_H_ +#define _INIPARSER_H_ + +/*--------------------------------------------------------------------------- + Includes + ---------------------------------------------------------------------------*/ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/* + * The following #include is necessary on many Unixes but not Linux. + * It is not needed for Windows platforms. + * Uncomment it if needed. + */ +/* #include <unistd.h> */ + +#include "dictionary.h" + +/*--------------------------------------------------------------------------- + Macros + ---------------------------------------------------------------------------*/ +/** For backwards compatibility only */ +#define iniparser_getstr(d, k) iniparser_getstring(d, k, NULL) +#define iniparser_setstr iniparser_setstring + +/*-------------------------------------------------------------------------*/ +/** + @brief Get number of sections in a dictionary + @param d Dictionary to examine + @return int Number of sections found in dictionary + + This function returns the number of sections found in a dictionary. + The test to recognize sections is done on the string stored in the + dictionary: a section name is given as "section" whereas a key is + stored as "section:key", thus the test looks for entries that do not + contain a colon. + + This clearly fails in the case a section name contains a colon, but + this should simply be avoided. + + This function returns -1 in case of error. + */ +/*--------------------------------------------------------------------------*/ + +int iniparser_getnsec(dictionary * d); + + +/*-------------------------------------------------------------------------*/ +/** + @brief Get name for section n in a dictionary. + @param d Dictionary to examine + @param n Section number (from 0 to nsec-1). + @return Pointer to char string + + This function locates the n-th section in a dictionary and returns + its name as a pointer to a string statically allocated inside the + dictionary. Do not free or modify the returned string! + + This function returns NULL in case of error. + */ +/*--------------------------------------------------------------------------*/ + +char * iniparser_getsecname(dictionary * d, int n); + + +/*-------------------------------------------------------------------------*/ +/** + @brief Save a dictionary to a loadable ini file + @param d Dictionary to dump + @param f Opened file pointer to dump to + @return void + + This function dumps a given dictionary into a loadable ini file. + It is Ok to specify @c stderr or @c stdout as output files. + */ +/*--------------------------------------------------------------------------*/ + +void iniparser_dump_ini(dictionary * d, FILE * f); + +/*-------------------------------------------------------------------------*/ +/** + @brief Dump a dictionary to an opened file pointer. + @param d Dictionary to dump. + @param f Opened file pointer to dump to. + @return void + + This function prints out the contents of a dictionary, one element by + line, onto the provided file pointer. It is OK to specify @c stderr + or @c stdout as output files. This function is meant for debugging + purposes mostly. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_dump(dictionary * d, FILE * f); + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key + @param d Dictionary to search + @param key Key string to look for + @param def Default value to return if key not found. + @return pointer to statically allocated character string + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the pointer passed as 'def' is returned. + The returned char pointer is pointing to a string allocated in + the dictionary, do not free or modify it. + */ +/*--------------------------------------------------------------------------*/ +char * iniparser_getstring(dictionary * d, const char * key, char * def); + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key, convert to an int + @param d Dictionary to search + @param key Key string to look for + @param notfound Value to return in case of error + @return integer + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the notfound value is returned. + + Supported values for integers include the usual C notation + so decimal, octal (starting with 0) and hexadecimal (starting with 0x) + are supported. Examples: + + - "42" -> 42 + - "042" -> 34 (octal -> decimal) + - "0x42" -> 66 (hexa -> decimal) + + Warning: the conversion may overflow in various ways. Conversion is + totally outsourced to strtol(), see the associated man page for overflow + handling. + + Credits: Thanks to A. Becker for suggesting strtol() + */ +/*--------------------------------------------------------------------------*/ +int iniparser_getint(dictionary * d, const char * key, int notfound); + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key, convert to a double + @param d Dictionary to search + @param key Key string to look for + @param notfound Value to return in case of error + @return double + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the notfound value is returned. + */ +/*--------------------------------------------------------------------------*/ +double iniparser_getdouble(dictionary * d, char * key, double notfound); + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key, convert to a boolean + @param d Dictionary to search + @param key Key string to look for + @param notfound Value to return in case of error + @return integer + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the notfound value is returned. + + A true boolean is found if one of the following is matched: + + - A string starting with 'y' + - A string starting with 'Y' + - A string starting with 't' + - A string starting with 'T' + - A string starting with '1' + + A false boolean is found if one of the following is matched: + + - A string starting with 'n' + - A string starting with 'N' + - A string starting with 'f' + - A string starting with 'F' + - A string starting with '0' + + The notfound value returned if no boolean is identified, does not + necessarily have to be 0 or 1. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_getboolean(dictionary * d, const char * key, int notfound); + + +/*-------------------------------------------------------------------------*/ +/** + @brief Set an entry in a dictionary. + @param ini Dictionary to modify. + @param entry Entry to modify (entry name) + @param val New value to associate to the entry. + @return int 0 if Ok, -1 otherwise. + + If the given entry can be found in the dictionary, it is modified to + contain the provided value. If it cannot be found, -1 is returned. + It is Ok to set val to NULL. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_setstring(dictionary * ini, char * entry, char * val); + + +/*-------------------------------------------------------------------------*/ +/** + @brief Delete an entry in a dictionary + @param ini Dictionary to modify + @param entry Entry to delete (entry name) + @return void + + If the given entry can be found, it is deleted from the dictionary. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_unset(dictionary * ini, char * entry); + +/*-------------------------------------------------------------------------*/ +/** + @brief Finds out if a given entry exists in a dictionary + @param ini Dictionary to search + @param entry Name of the entry to look for + @return integer 1 if entry exists, 0 otherwise + + Finds out if a given entry exists in the dictionary. Since sections + are stored as keys with NULL associated values, this is the only way + of querying for the presence of sections in a dictionary. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_find_entry(dictionary * ini, char * entry) ; + +/*-------------------------------------------------------------------------*/ +/** + @brief Parse an ini file and return an allocated dictionary object + @param ininame Name of the ini file to read. + @return Pointer to newly allocated dictionary + + This is the parser for ini files. This function is called, providing + the name of the file to be read. It returns a dictionary object that + should not be accessed directly, but through accessor functions + instead. + + The returned dictionary must be freed using iniparser_freedict(). + */ +/*--------------------------------------------------------------------------*/ +dictionary * iniparser_load(const char * ininame); + +/*-------------------------------------------------------------------------*/ +/** + @brief Free all memory associated to an ini dictionary + @param d Dictionary to free + @return void + + Free all memory associated to an ini dictionary. + It is mandatory to call this function before the dictionary object + gets out of the current context. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_freedict(dictionary * d); + +#endif diff --git a/src/vdr-plugin/menu.c b/src/vdr-plugin/menu.c new file mode 100644 index 0000000..3add4d4 --- /dev/null +++ b/src/vdr-plugin/menu.c @@ -0,0 +1,670 @@ +/* + * menu.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <stdlib.h> +#include <time.h> +#include <vdr/skins.h> +#include <vdr/tools.h> +#include <vdr/i18n.h> +#include <vdr/osdbase.h> +#include <vdr/skins.h> +#include <vdr/font.h> +#include <vdr/osd.h> +#include <vdr/interface.h> +#include "menu.h" +#include "download.h" +#include "config.h" +#include "common.h" +#include "history.h" +#include "timer.h" +#include "menu_timer.h" + +cCharSetConv csc = cCharSetConv("UTF-8", cCharSetConv::SystemCharacterTable()); +struct MenuPointers menuPointers; + +// --- cXMLMenu -------------------------------------------------- + +cXMLMenu::cXMLMenu(const char *Title, int c0, int c1, int c2, + int c3, int c4) +: cOsdMenu(Title, c0, c1, c2, c3, c4) +{ +} + +bool cXMLMenu::Deserialize(const char *xml) { + xmlDocPtr doc = xmlParseMemory(xml, strlen(xml)); + if (!doc) { + xmlErrorPtr xmlerr = xmlGetLastError(); + if (xmlerr) { + error("libxml error: %s", xmlerr->message); + } + + return false; + } + + xmlNodePtr node = xmlDocGetRootElement(doc); + if (node) + node = node->xmlChildrenNode; + + while (node) { + if (node->type == XML_ELEMENT_NODE) { + if (!CreateItemFromTag(doc, node)) { + warning("Failed to parse menu tag: %s", (char *)node->name); + } + } + node = node->next; + } + + xmlFreeDoc(doc); + return true; +} + +int cXMLMenu::Load(const char *xmlstr) { + Clear(); + Deserialize(xmlstr); + + return 0; +} + + +// --- cNavigationMenu ----------------------------------------------------- + +cNavigationMenu::cNavigationMenu(cHistory *History, + cProgressVector& dlsummaries) + : cXMLMenu("", 25), summaries(dlsummaries) +{ + title = NULL; + reference = NULL; + shortcutMode = 0; + history = History; + UpdateHelp(); +} + +cNavigationMenu::~cNavigationMenu() { + menuPointers.navigationMenu = NULL; + Clear(); + if (reference) + free(reference); +} + +bool cNavigationMenu::CreateItemFromTag(xmlDocPtr doc, xmlNodePtr node) { + if (!xmlStrcmp(node->name, BAD_CAST "link")) { + NewLinkItem(doc, node); + return true; + } else if (!xmlStrcmp(node->name, BAD_CAST "textfield")) { + NewTextField(doc, node); + return true; + } else if (!xmlStrcmp(node->name, BAD_CAST "itemlist")) { + NewItemList(doc, node); + return true; + } else if (!xmlStrcmp(node->name, BAD_CAST "textarea")) { + NewTextArea(doc, node); + return true; + } else if (!xmlStrcmp(node->name, BAD_CAST "button")) { + NewButton(doc, node); + return true; + } else if (!xmlStrcmp(node->name, BAD_CAST "title")) { + NewTitle(doc, node); + return true; + } + + return false; +} + +void cNavigationMenu::AddLinkItem(cOsdItem *item, + cLinkBase *ref, + cLinkBase *streamref) { + Add(item); + + if (ref) + links.Append(ref); + else + links.Append(NULL); + + if (streamref) + streams.Append(streamref); + else + streams.Append(NULL); +} + +void cNavigationMenu::NewLinkItem(xmlDocPtr doc, xmlNodePtr node) { + // label, ref and object tags + xmlChar *itemtitle = NULL, *ref = NULL, *streamref = NULL; + + node = node->xmlChildrenNode; + while (node) { + if (!xmlStrcmp(node->name, BAD_CAST "label")) { + if (itemtitle) + xmlFree(itemtitle); + itemtitle = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } else if (!xmlStrcmp(node->name, BAD_CAST "ref")) { + if (ref) + xmlFree(ref); + ref = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } else if (!xmlStrcmp(node->name, BAD_CAST "stream")) { + if (streamref) + xmlFree(streamref); + streamref = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } + node = node->next; + } + if (!itemtitle) + itemtitle = xmlCharStrdup("???"); + + const char *titleconv = csc.Convert((char *)itemtitle); + cOsdItem *item = new cOsdItem(titleconv); + cSimpleLink *objlinkdata = NULL; + cSimpleLink *linkdata = NULL; + if (ref) + linkdata = new cSimpleLink((char *)ref); + if (streamref) { + // media object + objlinkdata = new cSimpleLink((char *)streamref); + } else { + // navigation link + char *bracketed = (char *)malloc((strlen(titleconv)+3)*sizeof(char)); + if (bracketed) { + bracketed[0] = '\0'; + strcat(bracketed, "["); + strcat(bracketed, titleconv); + strcat(bracketed, "]"); + item->SetText(bracketed, false); + } + } + AddLinkItem(item, linkdata, objlinkdata); + + xmlFree(itemtitle); + if (ref) + xmlFree(ref); + if (streamref) + xmlFree(streamref); +} + +void cNavigationMenu::NewTextField(xmlDocPtr doc, xmlNodePtr node) { + // name attribute + xmlChar *name = xmlGetProp(node, BAD_CAST "name"); + cHistoryObject *curhistpage = history->Current(); + + // label tag + xmlChar *text = NULL; + node = node->xmlChildrenNode; + while (node) { + if (!xmlStrcmp(node->name, BAD_CAST "label")) { + if (text) + xmlFree(text); + text = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } + node = node->next; + } + if (!text) + text = xmlCharStrdup("???"); + + cTextFieldData *data = curhistpage->GetTextFieldData((char *)name); + cMenuEditStrItem *item = new cMenuEditStrItem(csc.Convert((char *)text), + data->GetValue(), + data->GetLength()); + AddLinkItem(item, NULL, NULL); + + free(text); + if (name) + xmlFree(name); +} + +void cNavigationMenu::NewItemList(xmlDocPtr doc, xmlNodePtr node) { + // name attribute + xmlChar *name = xmlGetProp(node, BAD_CAST "name"); + cHistoryObject *curhistpage = history->Current(); + + // label and item tags + xmlChar *text = NULL; + cStringList items; + cStringList itemvalues; + node = node->xmlChildrenNode; + while (node) { + if (!xmlStrcmp(node->name, BAD_CAST "label")) { + if (text) + xmlFree(text); + text = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } else if (!xmlStrcmp(node->name, BAD_CAST "item")) { + xmlChar *str = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + if (!str) + str = xmlCharStrdup("???"); + xmlChar *strvalue = xmlGetProp(node, BAD_CAST "value"); + if (!strvalue) + strvalue = xmlCharStrdup(""); + + items.Append(strdup((char *)str)); + itemvalues.Append(strdup((char *)strvalue)); + + xmlFree(str); + xmlFree(strvalue); + } + node = node->next; + } + if (!text) + text = xmlCharStrdup("???"); + + cItemListData *data = curhistpage->GetItemListData((const char *)name, + items, + itemvalues); + + cMenuEditStraItem *item = new cMenuEditStraItem(csc.Convert((char *)text), + data->GetValuePtr(), + data->GetNumStrings(), + data->GetStrings()); + AddLinkItem(item, NULL, NULL); + + xmlFree(text); + if (name) + xmlFree(name); +} + +void cNavigationMenu::NewTextArea(xmlDocPtr doc, xmlNodePtr node) { + // label tag + xmlChar *itemtitle = NULL; + node = node->xmlChildrenNode; + while (node) { + if (!xmlStrcmp(node->name, BAD_CAST "label")) { + if (itemtitle) + xmlFree(itemtitle); + itemtitle = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } + node = node->next; + } + if (!itemtitle) + return; + + const cFont *font = cFont::GetFont(fontOsd); + cTextWrapper tw(csc.Convert((char *)itemtitle), font, cOsd::OsdWidth()); + for (int i=0; i < tw.Lines(); i++) { + AddLinkItem(new cOsdItem(tw.GetLine(i), osUnknown, false), NULL, NULL); + } + + xmlFree(itemtitle); +} + +void cNavigationMenu::NewButton(xmlDocPtr doc, xmlNodePtr node) { + // label and submission tags + xmlChar *itemtitle = NULL, *submission = NULL; + cHistoryObject *curhistpage = history->Current(); + + node = node->xmlChildrenNode; + while (node) { + if (!xmlStrcmp(node->name, BAD_CAST "label")) { + if (itemtitle) + xmlFree(itemtitle); + itemtitle = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } else if (!xmlStrcmp(node->name, BAD_CAST "submission")) { + if (submission) + xmlFree(submission); + submission = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } + node = node->next; + } + if (!itemtitle) + itemtitle = xmlCharStrdup("???"); + + cSubmissionButtonData *data = \ + new cSubmissionButtonData((char *)submission, curhistpage); + const char *titleconv = csc.Convert((char *)itemtitle); // do not free + char *newtitle = (char *)malloc((strlen(titleconv)+3)*sizeof(char)); + if (newtitle) { + newtitle[0] = '\0'; + strcat(newtitle, "["); + strcat(newtitle, titleconv); + strcat(newtitle, "]"); + + cOsdItem *item = new cOsdItem(newtitle); + AddLinkItem(item, data, NULL); + free(newtitle); + } + + xmlFree(itemtitle); + if (submission) + xmlFree(submission); +} + +void cNavigationMenu::NewTitle(xmlDocPtr doc, xmlNodePtr node) { + xmlChar *newtitle = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + if (newtitle) { + const char *conv = csc.Convert((char *)newtitle); + SetTitle(conv); + if (title) + free(title); + title = strdup(conv); + xmlFree(newtitle); + } +} + +eOSState cNavigationMenu::ProcessKey(eKeys Key) +{ + cWebviTimer *timer; + bool hasStreams; + int old = Current(); + eOSState state = cXMLMenu::ProcessKey(Key); + bool validItem = Current() >= 0 && Current() < links.Size(); + + if (HasSubMenu()) + return state; + + if (state == osUnknown) { + switch (Key) { + case kInfo: + // The alternative link is active only when object links are + // present. + if (validItem && streams.At(Current())) + state = Select(links.At(Current()), LT_REGULAR); + break; + + case kOk: + // Primary action: download media object or, if not a media + // link, follow the navigation link. + if (validItem) { + if (streams.At(Current())) + state = Select(streams.At(Current()), LT_MEDIA); + else + state = Select(links.At(Current()), LT_REGULAR); + } + break; + + case kRed: + if (shortcutMode == 0) { + state = HistoryBack(); + } else { + menuPointers.statusScreen = new cStatusScreen(summaries); + state = AddSubMenu(menuPointers.statusScreen); + } + break; + + case kGreen: + if (shortcutMode == 0) { + state = HistoryForward(); + } else { + return AddSubMenu(new cWebviTimerListMenu(cWebviTimerManager::Instance())); + } + break; + + case kYellow: + if (shortcutMode == 0) { + hasStreams = false; + for (int i=0; i < streams.Size(); i++) { + if (streams[i]) { + hasStreams = true; + break; + } + } + + if (hasStreams || Interface->Confirm(tr("No streams on this page, create timer anyway?"))) { + timer = cWebviTimerManager::Instance().Create(title, reference); + if (timer) + return AddSubMenu(new cEditWebviTimerMenu(*timer, true, false)); + } + + state = osContinue; + } + break; + + case kBlue: + if (shortcutMode == 0) { + // Secondary action: start streaming if a media object + if (validItem && streams.At(Current())) + state = Select(streams.At(Current()), LT_STREAMINGMEDIA); + } + break; + + case k0: + shortcutMode = shortcutMode == 0 ? 1 : 0; + UpdateHelp(); + break; + + default: + break; + } + } else { + // If the key press caused the selected item to change, we need to + // update the help texts. + // + // In cMenuEditStrItem key == kOk with state == osContinue + // indicates leaving the edit mode. We want to update the help + // texts in this case also. + if ((old != Current()) || + ((Key == kOk) && (state == osContinue))) { + UpdateHelp(); + } + } + + return state; +} + +eOSState cNavigationMenu::Select(cLinkBase *link, eLinkType type) +{ + if (!link) { + return osContinue; + } + char *ref = link->GetURL(); + if (!ref) { + error("link->GetURL() == NULL in cNavigationMenu::Select"); + return osContinue; + } + + if (type == LT_MEDIA) { + cDownloadProgress *progress = summaries.NewDownload(); + cFileDownloadRequest *req = \ + new cFileDownloadRequest(history->Current()->GetID(), ref, + webvideoConfig->GetDownloadPath(), + progress); + cWebviThread::Instance().AddRequest(req); + + Skins.Message(mtInfo, tr("Downloading in the background")); + } else if (type == LT_STREAMINGMEDIA) { + cWebviThread::Instance().AddRequest(new cStreamUrlRequest(history->Current()->GetID(), + ref)); + Skins.Message(mtInfo, tr("Starting player...")); + return osEnd; + } else { + cWebviThread::Instance().AddRequest(new cMenuRequest(history->Current()->GetID(), + ref)); + Skins.Message(mtStatus, tr("Retrieving...")); + } + + return osContinue; +} + +void cNavigationMenu::Clear(void) { + cXMLMenu::Clear(); + SetTitle(""); + if (title) + free(title); + title = NULL; + for (int i=0; i < links.Size(); i++) { + if (links[i]) + delete links[i]; + if (streams[i]) + delete streams[i]; + } + links.Clear(); + streams.Clear(); +} + +void cNavigationMenu::Populate(const cHistoryObject *page, const char *statusmsg) { + Load(page->GetOSD()); + + if (reference) + free(reference); + reference = strdup(page->GetReference()); + + // Make sure that an item is selected (if there is at least + // one). The help texts are not updated correctly if no item is + // selected. + + SetCurrent(Get(page->GetSelected())); + UpdateHelp(); + SetStatus(statusmsg); +} + +eOSState cNavigationMenu::HistoryBack() { + cHistoryObject *cur = history->Current(); + + if (cur) + cur->RememberSelected(Current()); + + cHistoryObject *page = history->Back(); + if (page) { + Populate(page); + Display(); + } + return osContinue; +} + +eOSState cNavigationMenu::HistoryForward() { + cHistoryObject *before = history->Current(); + cHistoryObject *after = history->Forward(); + + if (before) + before->RememberSelected(Current()); + + // Update only if the menu really changed + if (before != after) { + Populate(after); + Display(); + } + return osContinue; +} + +void cNavigationMenu::UpdateHelp() { + const char *red = NULL; + const char *green = NULL; + const char *yellow = NULL; + const char *blue = NULL; + + if (shortcutMode == 0) { + red = (history->Current() != history->First()) ? tr("Back") : NULL; + green = (history->Current() != history->Last()) ? tr("Forward") : NULL; + yellow = (Current() >= 0) ? tr("Create timer") : NULL; + blue = ((Current() >= 0) && (streams.At(Current()))) ? tr("Play") : NULL; + } else { + red = tr("Status"); + green = tr("Timers"); + } + + SetHelp(red, green, yellow, blue); +} + +// --- cStatusScreen ------------------------------------------------------- + +cStatusScreen::cStatusScreen(cProgressVector& dlsummaries) + : cOsdMenu(tr("Unfinished downloads"), 40), summaries(dlsummaries) +{ + int charsperline = cOsd::OsdWidth() / cFont::GetFont(fontOsd)->Width('M'); + SetCols(charsperline-5); + + UpdateHelp(); + Update(); +} + +cStatusScreen::~cStatusScreen() { + menuPointers.statusScreen = NULL; +} + +void cStatusScreen::Update() { + int c = Current(); + + Clear(); + + if (summaries.Size() == 0) { + SetTitle(tr("No active downloads")); + } else { + + for (int i=0; i<summaries.Size(); i++) { + cString dltitle; + cDownloadProgress *s = summaries[i]; + dltitle = cString::sprintf("%s\t%s", + (const char *)s->GetTitle(), + (const char *)s->GetPercentage()); + + Add(new cOsdItem(dltitle)); + } + + if (c >= 0) + SetCurrent(Get(c)); + } + + lastupdate = time(NULL); + + UpdateHelp(); + Display(); +} + +bool cStatusScreen::NeedsUpdate() { + return (Count() > 0) && (time(NULL) - lastupdate >= updateInterval); +} + +eOSState cStatusScreen::ProcessKey(eKeys Key) { + cFileDownloadRequest *req; + int old = Current(); + eOSState state = cOsdMenu::ProcessKey(Key); + + if (HasSubMenu()) + return state; + + if (state == osUnknown) { + switch (Key) { + case kYellow: + if ((Current() >= 0) && (Current() < summaries.Size())) { + if (summaries[Current()]->IsFinished()) { + delete summaries[Current()]; + summaries.Remove(Current()); + Update(); + } else if ((req = summaries[Current()]->GetRequest()) && + !req->IsFinished()) { + req->Abort(); + Update(); + } + } + return osContinue; + + case kOk: + case kInfo: + if (summaries[Current()]->Error()) { + cString msg = cString::sprintf("%s\n%s: %s", + (const char *)summaries[Current()]->GetTitle(), + tr("Error"), + (const char *)summaries[Current()]->GetStatusPharse()); + return AddSubMenu(new cMenuText(tr("Error details"), msg)); + } else { + cString msg = cString::sprintf("%s (%s)", + (const char *)summaries[Current()]->GetTitle(), + (const char *)summaries[Current()]->GetPercentage()); + return AddSubMenu(new cMenuText(tr("Download details"), msg)); + } + + return osContinue; + + default: + break; + } + } else { + // Update help if the key press caused the menu item to change. + if (old != Current()) + UpdateHelp(); + } + + return state; +} + +void cStatusScreen::UpdateHelp() { + bool remove = false; + if ((Current() >= 0) && (Current() < summaries.Size())) { + if (summaries[Current()]->IsFinished()) { + remove = true; + } + } + + const char *yellow = remove ? tr("Remove") : tr("Abort"); + + SetHelp(NULL, NULL, yellow, NULL); +} diff --git a/src/vdr-plugin/menu.h b/src/vdr-plugin/menu.h new file mode 100644 index 0000000..b1e67df --- /dev/null +++ b/src/vdr-plugin/menu.h @@ -0,0 +1,114 @@ +/* + * menu.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_MENU_H +#define __WEBVIDEO_MENU_H + +#include <time.h> +#include <vdr/osdbase.h> +#include <vdr/menuitems.h> +#include <vdr/menu.h> +#include <libxml/parser.h> +#include "download.h" +#include "menudata.h" + +extern cCharSetConv csc; + +// --- cXMLMenu -------------------------------------------------- + +class cXMLMenu : public cOsdMenu { +protected: + virtual bool Deserialize(const char *xml); + virtual bool CreateItemFromTag(xmlDocPtr doc, xmlNodePtr node) = 0; +public: + cXMLMenu(const char *Title, int c0 = 0, int c1 = 0, + int c2 = 0, int c3 = 0, int c4 = 0); + + int Load(const char *xmlstr); +}; + +// --- cNavigationMenu ----------------------------------------------------- + +enum eLinkType { LT_REGULAR, LT_MEDIA, LT_STREAMINGMEDIA }; + +class cHistory; +class cHistoryObject; +class cStatusScreen; + +class cNavigationMenu : public cXMLMenu { +private: + // links[i] is the navigation link of the i:th item + cVector<cLinkBase *> links; + // streams[i] is the media stream link of the i:th item + cVector<cLinkBase *> streams; + cProgressVector& summaries; + char *title; + char *reference; + int shortcutMode; + +protected: + cHistory *history; + + virtual bool CreateItemFromTag(xmlDocPtr doc, xmlNodePtr node); + void AddLinkItem(cOsdItem *item, cLinkBase *ref, cLinkBase *streamref); + void NewLinkItem(xmlDocPtr doc, xmlNodePtr node); + void NewTextField(xmlDocPtr doc, xmlNodePtr node); + void NewItemList(xmlDocPtr doc, xmlNodePtr node); + void NewTextArea(xmlDocPtr doc, xmlNodePtr node); + void NewButton(xmlDocPtr doc, xmlNodePtr node); + void NewTitle(xmlDocPtr doc, xmlNodePtr node); + void UpdateHelp(); + +public: + cNavigationMenu(cHistory *History, cProgressVector& dlsummaries); + virtual ~cNavigationMenu(); + + virtual eOSState ProcessKey(eKeys Key); + virtual eOSState Select(cLinkBase *link, eLinkType type); + virtual void Clear(void); + eOSState HistoryBack(); + eOSState HistoryForward(); + + const char *Reference() const { return reference; } + void Populate(const cHistoryObject *page, const char *statusmsg=NULL); +}; + +// --- cStatusScreen ------------------------------------------------------- + +class cStatusScreen : public cOsdMenu { +public: + const static time_t updateInterval = 5; // seconds +private: + cProgressVector& summaries; + time_t lastupdate; + +protected: + void UpdateHelp(); + +public: + cStatusScreen(cProgressVector& dlsummaries); + ~cStatusScreen(); + + void Update(); + bool NeedsUpdate(); + + virtual eOSState ProcessKey(eKeys Key); +}; + +// --- MenuPointers -------------------------------------------------------- + +struct MenuPointers { + cNavigationMenu *navigationMenu; + cStatusScreen *statusScreen; + + MenuPointers() : navigationMenu(NULL), statusScreen(NULL) {}; +}; + +extern struct MenuPointers menuPointers; + +#endif // __WEBVIDEO_MENU_H diff --git a/src/vdr-plugin/menu_timer.c b/src/vdr-plugin/menu_timer.c new file mode 100644 index 0000000..0501e0d --- /dev/null +++ b/src/vdr-plugin/menu_timer.c @@ -0,0 +1,150 @@ +/* + * menu.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <time.h> +#include <vdr/i18n.h> +#include <vdr/tools.h> +#include <vdr/menuitems.h> +#include <vdr/interface.h> +#include "menu_timer.h" + +#define ARRAYSIZE(a) sizeof(a)/sizeof(a[0]) + +/* +const char *intervalNames[] = {trNOOP("Once per day"), trNOOP("Once per week"), + trNOOP("Once per month")}; +*/ + +const char *intervalNames[] = {NULL, NULL, NULL}; +const int intervalValues[] = {24*60*60, 7*24*60*60, 30*24*60*60}; + +// --- cEditWebviTimerMenu ------------------------------------------------- + +cEditWebviTimerMenu::cEditWebviTimerMenu(cWebviTimer &timer, + bool refreshWhenDone, + bool execButton) + : cOsdMenu(tr("Edit timer"), 20), timer(timer), interval(1), + refresh(refreshWhenDone) +{ + // title + strn0cpy(title, timer.GetTitle(), maxTitleLen); + Add(new cMenuEditStrItem(tr("Title"), title, maxTitleLen)); + + // interval + for (unsigned i=0; i<ARRAYSIZE(intervalValues); i++) { + if (timer.GetInterval() == intervalValues[i]) { + interval = i; + break; + } + } + + if (!intervalNames[0]) { + // Initialize manually to make the translations work + intervalNames[0] = tr("Once per day"); + intervalNames[1] = tr("Once per week"); + intervalNames[2] = tr("Once per month"); + } + + Add(new cMenuEditStraItem(tr("Update interval"), &interval, + ARRAYSIZE(intervalNames), intervalNames)); + + // "execute now" button + if (execButton) + Add(new cOsdItem(tr("Execute now"), osUser1, true)); + + // last update time + char lastTime[25]; + if (timer.LastUpdate() == 0) { + // TRANSLATORS: at most 24 chars + strcpy(lastTime, tr("Never")); + } else { + time_t updateTime = timer.LastUpdate(); + strftime(lastTime, 25, "%x %X", localtime(&updateTime)); + } + + cString lastUpdated = cString::sprintf("%s\t%s", tr("Last fetched:"), lastTime); + Add(new cOsdItem(lastUpdated, osUnknown, false)); + + // error + if (!timer.Success()) { + Add(new cOsdItem(tr("Error on last refresh!"), osUnknown, false)); + Add(new cOsdItem(timer.LastError(), osUnknown, false)); + } +} + +cEditWebviTimerMenu::~cEditWebviTimerMenu() { + if (refresh) + timer.Execute(); +} + +eOSState cEditWebviTimerMenu::ProcessKey(eKeys Key) { + eOSState state = cOsdMenu::ProcessKey(Key); + + if (state == osContinue) { + timer.SetTitle(title); + timer.SetInterval(intervalValues[interval]); + } else if (state == osUser1) { + timer.Execute(); + Skins.Message(mtInfo, tr("Downloading in the background")); + } + + return state; +} + +// --- cWebviTimerListMenu ------------------------------------------------- + +cWebviTimerListMenu::cWebviTimerListMenu(cWebviTimerManager &timers) + : cOsdMenu(tr("Timers")), timers(timers) +{ + cWebviTimer *t = timers.First(); + while (t) { + Add(new cOsdItem(t->GetTitle(), osUnknown, true)); + t = timers.Next(t); + } + + SetHelp(NULL, NULL, tr("Remove"), NULL); +} + +eOSState cWebviTimerListMenu::ProcessKey(eKeys Key) { + cWebviTimer *t; + eOSState state = cOsdMenu::ProcessKey(Key); + + if (HasSubMenu()) + return state; + + if (state == osUnknown) { + switch (Key) { + case kOk: + t = timers.GetLinear(Current()); + if (t) + return AddSubMenu(new cEditWebviTimerMenu(*t)); + break; + + case kYellow: + t = timers.GetLinear(Current()); + if (t) { + if (t->Running()) { + // FIXME: ask if the user wants to cancel the downloads + Skins.Message(mtInfo, tr("Timer running, can't remove")); + } else if (Interface->Confirm(tr("Remove timer?"))) { + timers.Remove(t); + Del(Current()); + Display(); + } + + return osContinue; + } + break; + + default: + break; + } + } + + return state; +} diff --git a/src/vdr-plugin/menu_timer.h b/src/vdr-plugin/menu_timer.h new file mode 100644 index 0000000..192c062 --- /dev/null +++ b/src/vdr-plugin/menu_timer.h @@ -0,0 +1,46 @@ +/* + * menu_timer.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_MENU_TIMER_H +#define __WEBVIDEO_MENU_TIMER_H + +#include <vdr/osdbase.h> +#include "timer.h" + +// --- cEditWebviTimerMenu ------------------------------------------------- + +class cEditWebviTimerMenu : public cOsdMenu { +private: + static const int maxTitleLen = 128; + + cWebviTimer &timer; + char title[maxTitleLen]; + int interval; + bool refresh; + +public: + cEditWebviTimerMenu(cWebviTimer &timer, bool refreshWhenDone=false, + bool execButton=true); + ~cEditWebviTimerMenu(); + + virtual eOSState ProcessKey(eKeys Key); +}; + +// --- cWebviTimerListMenu ------------------------------------------------- + +class cWebviTimerListMenu : public cOsdMenu { +private: + cWebviTimerManager& timers; + +public: + cWebviTimerListMenu(cWebviTimerManager &timers); + + virtual eOSState ProcessKey(eKeys Key); +}; + +#endif diff --git a/src/vdr-plugin/menudata.c b/src/vdr-plugin/menudata.c new file mode 100644 index 0000000..45db133 --- /dev/null +++ b/src/vdr-plugin/menudata.c @@ -0,0 +1,179 @@ +/* + * menudata.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <string.h> +#include <stdlib.h> +#include <vdr/tools.h> +#include "menudata.h" +#include "common.h" +#include "history.h" + +// --- cQueryData ---------------------------------------------------------- + +cQueryData::cQueryData(const char *Name) { + name = Name ? strdup(Name) : NULL; +} + +cQueryData::~cQueryData() { + if (name) + free(name); +} + +// --- cSimpleLink --------------------------------------------------------- + +cSimpleLink::cSimpleLink(const char *reference) { + ref = reference ? strdup(reference) : NULL; +} + +cSimpleLink::~cSimpleLink() { + if (ref) { + free(ref); + } +} + +char *cSimpleLink::GetURL() { + return ref; +} + +// --- cTextFieldData ------------------------------------------------------ + +cTextFieldData::cTextFieldData(const char *Name, int Length) +: cQueryData(Name) +{ + valuebufferlength = Length; + valuebuffer = (char *)malloc(Length*sizeof(char)); + *valuebuffer = '\0'; +} + +cTextFieldData::~cTextFieldData() { + if(valuebuffer) + free(valuebuffer); +} + +char *cTextFieldData::GetQueryFragment() { + const char *name = GetName(); + + if (name && *name && valuebuffer) { + char *encoded = URLencode(valuebuffer); + cString tmp = cString::sprintf("%s,%s", name, encoded); + free(encoded); + return strdup(tmp); + } + + return NULL; +} + +char *cTextFieldData::GetValue() { + return valuebuffer; +} + +int cTextFieldData::GetLength() { + return valuebufferlength; +} + +// --- cItemListData ------------------------------------------------------- + +cItemListData::cItemListData(const char *Name, char **Strings, char **StringValues, int NumStrings) +: cQueryData(Name) +{ + strings = Strings; + stringvalues = StringValues; + numstrings = NumStrings; + value = 0; +} + +cItemListData::~cItemListData() { + for (int i=0; i < numstrings; i++) { + free(strings[i]); + free(stringvalues[i]); + } + if (strings) + free(strings); + if (stringvalues) + free(stringvalues); +} + +char *cItemListData::GetQueryFragment() { + const char *name = GetName(); + + if (name && *name) { + cString tmp = cString::sprintf("%s,%s", name, stringvalues[value]); + return strdup(tmp); + } + + return NULL; +} + +char **cItemListData::GetStrings() { + return strings; +} + +char **cItemListData::GetStringValues() { + return stringvalues; +} + +int cItemListData::GetNumStrings() { + return numstrings; +} + +int *cItemListData::GetValuePtr() { + return &value; +} + +// --- cSubmissionButtonData ----------------------------------------------- + +cSubmissionButtonData::cSubmissionButtonData( + const char *queryUrl, const cHistoryObject *currentPage) +{ + querybase = queryUrl ? strdup(queryUrl) : NULL; + page = currentPage; +} + +cSubmissionButtonData::~cSubmissionButtonData() { + if (querybase) + free(querybase); + // do not free page +} + +char *cSubmissionButtonData::GetURL() { + if (!querybase) + return NULL; + + char *querystr = (char *)malloc(sizeof(char)*(strlen(querybase)+2)); + strcpy(querystr, querybase); + + if (!page) + return querystr; + + if (strchr(querystr, '?')) + strcat(querystr, "&"); + else + strcat(querystr, "?"); + + int numparameters = 0; + for (int i=0; i<page->QuerySize(); i++) { + char *parameter = page->GetQueryFragment(i); + if (parameter) { + querystr = (char *)realloc(querystr, (strlen(querystr)+strlen(parameter)+8)*sizeof(char)); + if (i > 0) + strcat(querystr, "&"); + strcat(querystr, "subst="); + strcat(querystr, parameter); + numparameters++; + + free(parameter); + } + } + + if (numparameters == 0) { + // remove the '?' or '&' because no parameters were added to the url + querystr[strlen(querystr)-1] = '\0'; + } + + return querystr; +} diff --git a/src/vdr-plugin/menudata.h b/src/vdr-plugin/menudata.h new file mode 100644 index 0000000..23a126c --- /dev/null +++ b/src/vdr-plugin/menudata.h @@ -0,0 +1,100 @@ +/* + * menudata.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_MENUDATA_H +#define __WEBVIDEO_MENUDATA_H + +// --- cLinkBase ----------------------------------------------------------- + +class cLinkBase { +public: + virtual ~cLinkBase() {}; // avoids "virtual functions but + // non-virtual destructor" warning + + virtual char *GetURL() = 0; +}; + +// --- cQueryData ---------------------------------------------------------- + +class cQueryData { +private: + char *name; + +public: + cQueryData(const char *Name); + virtual ~cQueryData(); + + const char *GetName() { return name; } + virtual char *GetQueryFragment() = 0; +}; + +// --- cSimpleLink --------------------------------------------------------- + +class cSimpleLink : public cLinkBase { +private: + char *ref; +public: + cSimpleLink(const char *ref); + virtual ~cSimpleLink(); + + virtual char *GetURL(); +}; + +// --- cTextFieldData ------------------------------------------------------ + +class cTextFieldData : public cQueryData { +private: + char *name; + char *valuebuffer; + int valuebufferlength; +public: + cTextFieldData(const char *Name, int Length); + virtual ~cTextFieldData(); + + virtual char *GetQueryFragment(); + char *GetValue(); + int GetLength(); +}; + +// --- cItemListData ------------------------------------------------------- + +class cItemListData : public cQueryData { +private: + char *name; + int value; + int numstrings; + char **strings; + char **stringvalues; +public: + cItemListData(const char *Name, char **Strings, char **StringValues, int NumStrings); + virtual ~cItemListData(); + + virtual char *GetQueryFragment(); + char **GetStrings(); + char **GetStringValues(); + int GetNumStrings(); + int *GetValuePtr(); +}; + +// --- cSubmissionButtonData ----------------------------------------------- + +class cHistoryObject; + +class cSubmissionButtonData : public cLinkBase { +private: + char *querybase; + const cHistoryObject *page; +public: + cSubmissionButtonData(const char *queryUrl, + const cHistoryObject *currentPage); + virtual ~cSubmissionButtonData(); + + virtual char *GetURL(); +}; + +#endif diff --git a/src/vdr-plugin/mime.types b/src/vdr-plugin/mime.types new file mode 100644 index 0000000..beefdc3 --- /dev/null +++ b/src/vdr-plugin/mime.types @@ -0,0 +1,4 @@ +# Some non-standard, but common, MIME types + +video/flv flv +video/x-flv flv diff --git a/src/vdr-plugin/mimetypes.c b/src/vdr-plugin/mimetypes.c new file mode 100644 index 0000000..17c29e6 --- /dev/null +++ b/src/vdr-plugin/mimetypes.c @@ -0,0 +1,98 @@ +/* + * mimetypes.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <vdr/tools.h> +#include "mimetypes.h" +#include "common.h" + +// --- cMimeListObject ----------------------------------------------------- + +cMimeListObject::cMimeListObject(const char *mimetype, const char *extension) { + type = strdup(mimetype); + ext = strdup(extension); +} + +cMimeListObject::~cMimeListObject() { + free(type); + free(ext); +} + +// --- cMimeTypes ---------------------------------------------------------- + +cMimeTypes::cMimeTypes(const char **mimetypefiles) { + for (const char **filename=mimetypefiles; *filename; filename++) { + FILE *f = fopen(*filename, "r"); + if (!f) { + LOG_ERROR_STR((const char *)cString::sprintf("failed to open mime type file %s", *filename)); + continue; + } + + cReadLine rl; + char *line = rl.Read(f); + while (line) { + // Comment lines starting with '#' and empty lines are skipped + // Expected format for the lines: + // mime/type ext + if (*line && (*line != '#')) { + char *ptr = line; + while ((*ptr != '\0') && (!isspace(*ptr))) + ptr++; + + if (ptr == line) { + // empty line, ignore + line = rl.Read(f); + continue; + } + + char *mimetype = (char *)malloc(ptr-line+1); + strncpy(mimetype, line, ptr-line); + mimetype[ptr-line] = '\0'; + + while (*ptr && isspace(*ptr)) + ptr++; + char *eptr = ptr; + while (*ptr && !isspace(*ptr)) + ptr++; + + if (ptr == eptr) { + // no extension, ignore + free(mimetype); + line = rl.Read(f); + continue; + } + + char *extension = (char *)malloc(ptr-eptr+1); + strncpy(extension, eptr, ptr-eptr); + extension[ptr-eptr] = '\0'; + + types.Add(new cMimeListObject(mimetype, extension)); + free(extension); + free(mimetype); + } + line = rl.Read(f); + } + + fclose(f); + } +} + +char *cMimeTypes::ExtensionFromMimeType(const char *mimetype) { + if (!mimetype) + return NULL; + + for (cMimeListObject *m = types.First(); m; m = types.Next(m)) + if (strcmp(m->GetType(), mimetype) == 0) { + return strdup(m->GetExtension()); + } + + return NULL; +} diff --git a/src/vdr-plugin/mimetypes.h b/src/vdr-plugin/mimetypes.h new file mode 100644 index 0000000..76e735b --- /dev/null +++ b/src/vdr-plugin/mimetypes.h @@ -0,0 +1,35 @@ +/* + * mimetypes.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_MIMETYPES_H +#define __WEBVIDEO_MIMETYPES_H + +class cMimeListObject : public cListObject { +private: + char *type; + char *ext; +public: + cMimeListObject(const char *mimetype, const char *extension); + ~cMimeListObject(); + + char *GetType() { return type; }; + char *GetExtension() { return ext; }; +}; + +class cMimeTypes { +private: + cList<cMimeListObject> types; +public: + cMimeTypes(const char **filenames); + + char *ExtensionFromMimeType(const char *mimetype); +}; + +extern cMimeTypes *MimeTypes; + +#endif diff --git a/src/vdr-plugin/player.c b/src/vdr-plugin/player.c new file mode 100644 index 0000000..42bd56e --- /dev/null +++ b/src/vdr-plugin/player.c @@ -0,0 +1,73 @@ +/* + * player.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <stdio.h> +#include <string.h> +#include <vdr/plugin.h> +#include "player.h" +#include "common.h" + +bool cXineliboutputPlayer::Launch(const char *url) { + debug("launching xinelib player, url = %s", url); + + /* + * xineliboutput plugin insists on percent encoding (certain + * characters in) the URL. A properly encoded URL will get broken if + * we let xineliboutput to encode it the second time. For example, + * current (Feb 2009) Youtube URLs are affected by this. We will + * decode the URL before passing it to xineliboutput to fix Youtube + * + * On the other hand, some URLs will get broken if the encoding is + * removed here. There simply isn't a way to make all URLs work + * because of the way xineliboutput handles the encoding. + */ + char *decoded = URLdecode(url); + debug("decoded = %s", decoded); + bool ret = cPluginManager::CallFirstService("MediaPlayer-1.0", (void *)decoded); + free(decoded); + return ret; +} + +bool cMPlayerPlayer::Launch(const char *url) { + /* + * This code for launching mplayer plugin is just for testing, and + * most likely does not work. + */ + + debug("launching MPlayer"); + warning("Support for MPlayer is experimental. Don't expect this to work!"); + + struct MPlayerServiceData + { + int result; + union + { + const char *filename; + } data; + }; + + const char* const tmpPlayListFileName = "/tmp/webvideo.m3u"; + FILE *f = fopen(tmpPlayListFileName, "w"); + fwrite(url, strlen(url), 1, f); + fclose(f); + + MPlayerServiceData mplayerdata; + mplayerdata.data.filename = tmpPlayListFileName; + + if (!cPluginManager::CallFirstService("MPlayer-Play-v1", &mplayerdata)) { + debug("Failed to locate Mplayer service"); + return false; + } + + if (!mplayerdata.result) { + debug("Mplayer service failed"); + return false; + } + + return true; +} diff --git a/src/vdr-plugin/player.h b/src/vdr-plugin/player.h new file mode 100644 index 0000000..dbaf448 --- /dev/null +++ b/src/vdr-plugin/player.h @@ -0,0 +1,29 @@ +/* + * menu.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_PLAYER_H +#define __WEBVIDEO_PLAYER_H + +class cMediaPlayer { +public: + virtual ~cMediaPlayer() {}; + virtual bool Launch(const char *url) = 0; +}; + +class cXineliboutputPlayer : public cMediaPlayer { +public: + bool Launch(const char *url); +}; + +class cMPlayerPlayer : public cMediaPlayer { +public: + bool Launch(const char *url); +}; + + +#endif diff --git a/src/vdr-plugin/po/de_DE.po b/src/vdr-plugin/po/de_DE.po new file mode 100644 index 0000000..f096ba9 --- /dev/null +++ b/src/vdr-plugin/po/de_DE.po @@ -0,0 +1,137 @@ +# German translations for webvideo package. +# Copyright (C) 2009 THE webvideo'S COPYRIGHT HOLDER +# This file is distributed under the same license as the webvideo package. +# Antti Ajanki <antti.ajanki@iki.fi>, 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.1.1\n" +"Report-Msgid-Bugs-To: <see README>\n" +"POT-Creation-Date: 2010-07-09 15:12+0300\n" +"PO-Revision-Date: 2009-02-18 20:04+0200\n" +"Last-Translator: <cnc@gmx.de>\n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" + +msgid "No streams on this page, create timer anyway?" +msgstr "" + +msgid "Downloading in the background" +msgstr "Download im Hintergrund" + +msgid "Starting player..." +msgstr "Player wird gestartet..." + +msgid "Retrieving..." +msgstr "Abrufen..." + +msgid "Back" +msgstr "Zurück" + +msgid "Forward" +msgstr "Vor" + +msgid "Create timer" +msgstr "" + +msgid "Play" +msgstr "Play" + +msgid "Status" +msgstr "Status" + +msgid "Timers" +msgstr "" + +msgid "Unfinished downloads" +msgstr "Nicht beendete Downloads" + +msgid "No active downloads" +msgstr "Kein aktiver Download" + +#. TRANSLATORS: at most 5 characters +msgid "Error" +msgstr "" + +msgid "Error details" +msgstr "" + +msgid "Download details" +msgstr "" + +msgid "Remove" +msgstr "" + +msgid "Abort" +msgstr "Abbruch" + +msgid "Edit timer" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Once per day" +msgstr "" + +msgid "Once per week" +msgstr "" + +msgid "Once per month" +msgstr "" + +msgid "Update interval" +msgstr "" + +msgid "Execute now" +msgstr "" + +#. TRANSLATORS: at most 24 chars +msgid "Never" +msgstr "" + +msgid "Last fetched:" +msgstr "" + +msgid "Error on last refresh!" +msgstr "" + +msgid "Timer running, can't remove" +msgstr "" + +msgid "Remove timer?" +msgstr "" + +#, fuzzy +msgid "Aborted" +msgstr "Abbruch" + +msgid "Download video files from the web" +msgstr "Download Video Files aus dem Web" + +msgid "Streaming failed: no URL" +msgstr "Streaming fehlgeschlagen: Keine URL" + +msgid "Failed to launch media player" +msgstr "Media Player konnte nicht gestartet werden" + +msgid "timer" +msgstr "" + +#, c-format +msgid "One download completed, %d remains%s" +msgstr "Ein Download komplett, %d verbleibend%s" + +msgid "Download aborted" +msgstr "" + +#, c-format +msgid "Download failed (error = %d)" +msgstr "Download fehlgeschlagen (Error = %d)" + +#, c-format +msgid "%d downloads not finished" +msgstr "%d laufende Downloads" diff --git a/src/vdr-plugin/po/fi_FI.po b/src/vdr-plugin/po/fi_FI.po new file mode 100644 index 0000000..6e6df2f --- /dev/null +++ b/src/vdr-plugin/po/fi_FI.po @@ -0,0 +1,137 @@ +# Finnish translations for webvideo package. +# Copyright (C) 2008,2009 THE webvideo'S COPYRIGHT HOLDER +# This file is distributed under the same license as the webvideo package. +# Antti Ajanki <antti.ajanki@iki.fi>, 2008,2009. +# +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.1.1\n" +"Report-Msgid-Bugs-To: <see README>\n" +"POT-Creation-Date: 2010-07-09 15:12+0300\n" +"PO-Revision-Date: 2008-06-07 18:03+0300\n" +"Last-Translator: Antti Ajanki <antti.ajanki@iki.fi>\n" +"Language-Team: Finnish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "No streams on this page, create timer anyway?" +msgstr "Luo ajastin vaikka tällä sivulla ei videoita?" + +msgid "Downloading in the background" +msgstr "Ladataan taustalla" + +msgid "Starting player..." +msgstr "Käynnistetään toistin..." + +msgid "Retrieving..." +msgstr "Ladataan..." + +msgid "Back" +msgstr "Peruuta" + +msgid "Forward" +msgstr "Eteenpäin" + +msgid "Create timer" +msgstr "Luo ajastin" + +msgid "Play" +msgstr "Toista" + +msgid "Status" +msgstr "Tila" + +msgid "Timers" +msgstr "Ajastimet" + +msgid "Unfinished downloads" +msgstr "Ladattavat tiedostot" + +msgid "No active downloads" +msgstr "Ei keskeneräisia latauksia" + +#. TRANSLATORS: at most 5 characters +msgid "Error" +msgstr "Virhe" + +msgid "Error details" +msgstr "Virhe" + +msgid "Download details" +msgstr "Latauksen tiedot" + +msgid "Remove" +msgstr "Poista" + +msgid "Abort" +msgstr "Keskeytä" + +msgid "Edit timer" +msgstr "Muokkaa ajastinta" + +msgid "Title" +msgstr "Nimi" + +msgid "Once per day" +msgstr "Kerran päivässä" + +msgid "Once per week" +msgstr "Kerran viikossa" + +msgid "Once per month" +msgstr "Kerran kuussa" + +msgid "Update interval" +msgstr "Päivitystahti" + +msgid "Execute now" +msgstr "Suorita nyt" + +#. TRANSLATORS: at most 24 chars +msgid "Never" +msgstr "Ei koskaan" + +msgid "Last fetched:" +msgstr "Viimeisin päivitys" + +msgid "Error on last refresh!" +msgstr "Virhe edellisessä päivityksessä" + +msgid "Timer running, can't remove" +msgstr "Poisto ei onnistu, koska ajastin on käynnissä" + +msgid "Remove timer?" +msgstr "Poista ajastin?" + +msgid "Aborted" +msgstr "Keskeytetty" + +msgid "Download video files from the web" +msgstr "Lataa videotiedostoja Internetistä" + +msgid "Streaming failed: no URL" +msgstr "Toisto epäonnistui: ei URLia" + +msgid "Failed to launch media player" +msgstr "Toistimen käynnistäminen epäonnistui" + +msgid "timer" +msgstr "ajastin" + +#, c-format +msgid "One download completed, %d remains%s" +msgstr "Yksi tiedosto ladattu, %d jäljellä%s" + +msgid "Download aborted" +msgstr "Lataaminen keskeytetty" + +#, c-format +msgid "Download failed (error = %d)" +msgstr "Lataus epäonnistui (virhe = %d)" + +#, c-format +msgid "%d downloads not finished" +msgstr "%d tiedostoa lataamatta" diff --git a/src/vdr-plugin/po/fr_FR.po b/src/vdr-plugin/po/fr_FR.po new file mode 100644 index 0000000..79f31b5 --- /dev/null +++ b/src/vdr-plugin/po/fr_FR.po @@ -0,0 +1,156 @@ +# French translations for webvideo package. +# Copyright (C) 2008 THE webvideo'S COPYRIGHT HOLDER +# This file is distributed under the same license as the webvideo package. +# Bruno ROUSSEL <bruno.roussel@free.fr>, 2008. +# +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.0.5\n" +"Report-Msgid-Bugs-To: <see README>\n" +"POT-Creation-Date: 2010-07-09 15:12+0300\n" +"PO-Revision-Date: 2008-09-08 20:34+0100\n" +"Last-Translator: Bruno ROUSSEL <bruno.roussel@free.fr>\n" +"Language-Team: French\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "No streams on this page, create timer anyway?" +msgstr "" + +msgid "Downloading in the background" +msgstr "Téléchargement en tâche de fond" + +msgid "Starting player..." +msgstr "" + +msgid "Retrieving..." +msgstr "Récupération..." + +msgid "Back" +msgstr "Arrière" + +msgid "Forward" +msgstr "Avant" + +msgid "Create timer" +msgstr "" + +msgid "Play" +msgstr "" + +msgid "Status" +msgstr "Status" + +msgid "Timers" +msgstr "" + +msgid "Unfinished downloads" +msgstr "" + +msgid "No active downloads" +msgstr "" + +#. TRANSLATORS: at most 5 characters +msgid "Error" +msgstr "" + +msgid "Error details" +msgstr "" + +#, fuzzy +msgid "Download details" +msgstr "Status du téléchargement" + +msgid "Remove" +msgstr "" + +msgid "Abort" +msgstr "" + +msgid "Edit timer" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Once per day" +msgstr "" + +msgid "Once per week" +msgstr "" + +msgid "Once per month" +msgstr "" + +msgid "Update interval" +msgstr "" + +msgid "Execute now" +msgstr "" + +#. TRANSLATORS: at most 24 chars +msgid "Never" +msgstr "" + +msgid "Last fetched:" +msgstr "" + +msgid "Error on last refresh!" +msgstr "" + +msgid "Timer running, can't remove" +msgstr "" + +msgid "Remove timer?" +msgstr "" + +msgid "Aborted" +msgstr "" + +msgid "Download video files from the web" +msgstr "Téléchargement du fichier vidéo depuis le web" + +msgid "Streaming failed: no URL" +msgstr "" + +msgid "Failed to launch media player" +msgstr "" + +msgid "timer" +msgstr "" + +#, c-format +msgid "One download completed, %d remains%s" +msgstr "Un téléchargement terminé, il en reste %d%s" + +msgid "Download aborted" +msgstr "" + +#, c-format +msgid "Download failed (error = %d)" +msgstr "Erreur de téléchargement (Erreur = %d)" + +#, c-format +msgid "%d downloads not finished" +msgstr "%d téléchargement(s) non terminé(s)." + +#~ msgid "<No title>" +#~ msgstr "<Pas de titre>" + +#~ msgid "Can't download web page!" +#~ msgstr "Impossible de télécharger la page web !" + +#~ msgid "XSLT transformation produced no URL!" +#~ msgstr "La conversion XSLT n'a pas généré d'URL !" + +#~ msgid "XSLT transformation failed." +#~ msgstr "Erreur de conversion XSLT." + +#~ msgid "Unknown error!" +#~ msgstr "Erreur inconnue !" + +#~ msgid "Select video source" +#~ msgstr "Sélectionner la source vidéo" diff --git a/src/vdr-plugin/po/it_IT.po b/src/vdr-plugin/po/it_IT.po new file mode 100644 index 0000000..c7f1f00 --- /dev/null +++ b/src/vdr-plugin/po/it_IT.po @@ -0,0 +1,158 @@ +# Italian translations for webvideo package. +# Copyright (C) 2008 THE webvideo'S COPYRIGHT HOLDER +# This file is distributed under the same license as the webvideo package. +# Diego Pierotto <vdr-italian@tiscali.it>, 2008. +# +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.0.1\n" +"Report-Msgid-Bugs-To: <see README>\n" +"POT-Creation-Date: 2010-07-09 15:12+0300\n" +"PO-Revision-Date: 2009-04-11 01:48+0100\n" +"Last-Translator: Diego Pierotto <vdr-italian@tiscali.it>\n" +"Language-Team: Italian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"X-Poedit-Language: Italian\n" +"X-Poedit-Country: ITALY\n" +"X-Poedit-SourceCharset: utf-8\n" + +msgid "No streams on this page, create timer anyway?" +msgstr "" + +msgid "Downloading in the background" +msgstr "Scaricamento in sottofondo" + +msgid "Starting player..." +msgstr "Avvio lettore..." + +msgid "Retrieving..." +msgstr "Recupero..." + +msgid "Back" +msgstr "Indietro" + +msgid "Forward" +msgstr "Avanti" + +msgid "Create timer" +msgstr "" + +msgid "Play" +msgstr "Riproduci" + +msgid "Status" +msgstr "Stato" + +msgid "Timers" +msgstr "" + +msgid "Unfinished downloads" +msgstr "Scaricamenti non completati" + +msgid "No active downloads" +msgstr "Nessun scaricamento attivo" + +#. TRANSLATORS: at most 5 characters +msgid "Error" +msgstr "" + +msgid "Error details" +msgstr "" + +#, fuzzy +msgid "Download details" +msgstr "Richiesta scaricamento fallita!" + +msgid "Remove" +msgstr "" + +msgid "Abort" +msgstr "Annulla" + +msgid "Edit timer" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Once per day" +msgstr "" + +msgid "Once per week" +msgstr "" + +msgid "Once per month" +msgstr "" + +msgid "Update interval" +msgstr "" + +msgid "Execute now" +msgstr "" + +#. TRANSLATORS: at most 24 chars +msgid "Never" +msgstr "" + +msgid "Last fetched:" +msgstr "" + +msgid "Error on last refresh!" +msgstr "" + +msgid "Timer running, can't remove" +msgstr "" + +msgid "Remove timer?" +msgstr "" + +msgid "Aborted" +msgstr "Annullato" + +msgid "Download video files from the web" +msgstr "Scarica file video dal web" + +msgid "Streaming failed: no URL" +msgstr "Trasmissione fallita: nessun URL" + +msgid "Failed to launch media player" +msgstr "Impossibile avviare il lettore multimediale" + +msgid "timer" +msgstr "" + +#, c-format +msgid "One download completed, %d remains%s" +msgstr "Scaricamento completato, %d rimanente/i%s" + +msgid "Download aborted" +msgstr "Scaricamento annullato" + +#, c-format +msgid "Download failed (error = %d)" +msgstr "Scaricamento fallito (errore = %d)" + +#, c-format +msgid "%d downloads not finished" +msgstr "%d scaricamenti non conclusi" + +#~ msgid "<No title>" +#~ msgstr "<Senza titolo>" + +#~ msgid "Can't download web page!" +#~ msgstr "Impossibile scaricare la pagina web!" + +#~ msgid "XSLT transformation produced no URL!" +#~ msgstr "La conversione XSLT non ha generato alcun URL!" + +#~ msgid "XSLT transformation failed." +#~ msgstr "Conversione XSLT fallita." + +#~ msgid "Unknown error!" +#~ msgstr "Errore sconosciuto!" + +#~ msgid "Select video source" +#~ msgstr "Seleziona fonte video" diff --git a/src/vdr-plugin/request.c b/src/vdr-plugin/request.c new file mode 100644 index 0000000..edc5432 --- /dev/null +++ b/src/vdr-plugin/request.c @@ -0,0 +1,432 @@ +/* + * request.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <vdr/tools.h> +#include <vdr/i18n.h> +#include "request.h" +#include "common.h" +#include "mimetypes.h" +#include "config.h" +#include "timer.h" + +// --- cDownloadProgress --------------------------------------------------- + +cDownloadProgress::cDownloadProgress() { + strcpy(name, "???"); + downloaded = -1; + total = -1; + statusCode = -1; + req = NULL; +} + +void cDownloadProgress::AssociateWith(cFileDownloadRequest *request) { + req = request; +} + +void cDownloadProgress::SetContentLength(long bytes) { + total = bytes; +} + +void cDownloadProgress::SetTitle(const char *title) { + cMutexLock lock(&mutex); + + strncpy(name, title, NAME_LEN-1); + name[NAME_LEN-1] = '\0'; +} + +void cDownloadProgress::Progress(long downloadedbytes) { + // Atomic operation, no mutex needed + downloaded = downloadedbytes; +} + +void cDownloadProgress::MarkDone(int errorcode, cString pharse) { + cMutexLock lock(&mutex); + + statusCode = errorcode; + statusPharse = pharse; +} + +bool cDownloadProgress::IsFinished() { + return statusCode != -1; +} + +cString cDownloadProgress::GetTitle() { + cMutexLock lock(&mutex); + + if (req && req->IsAborted()) + return cString::sprintf("[%s] %s", tr("Aborted"), name); + else + return cString(name); +} + +cString cDownloadProgress::GetPercentage() { + cMutexLock lock(&mutex); + + if ((const char*)statusPharse != NULL && statusCode != 0) + // TRANSLATORS: at most 5 characters + return cString(tr("Error")); + else if ((downloaded < 0) || (total < 0)) + return cString("???"); + else + return cString::sprintf("%3d%%", (int) (100*(float)downloaded/total + 0.5)); +} + +cString cDownloadProgress::GetStatusPharse() { + cMutexLock lock(&mutex); + + return statusPharse; +} + +bool cDownloadProgress::Error() { + return (const char *)statusPharse != NULL; +} + +// --- cProgressVector ----------------------------------------------------- + +cDownloadProgress *cProgressVector::NewDownload() { + cDownloadProgress *progress = new cDownloadProgress(); + Append(progress); + return progress; +} + +// --- cMenuRequest -------------------------------------------------------- + +cMenuRequest::cMenuRequest(int ID, const char *wvtreference) +: reqID(ID), aborted(false), finished(false), status(0), webvi(-1), + handle(-1), timer(NULL) +{ + wvtref = strdup(wvtreference); +} + +cMenuRequest::~cMenuRequest() { + if (handle != -1) { + if (!finished) + Abort(); + webvi_delete_handle(webvi, handle); + } + + // do not delete timer +} + +ssize_t cMenuRequest::WriteCallback(const char *ptr, size_t len, void *request) { + cMenuRequest *instance = (cMenuRequest *)request; + if (instance) + return instance->WriteData(ptr, len); + else + return len; +} + +ssize_t cMenuRequest::WriteData(const char *ptr, size_t len) { + return inBuffer.Put(ptr, len); +} + +char *cMenuRequest::ExtractSiteName(const char *ref) { + if (strncmp(ref, "wvt:///", 7) != 0) + return NULL; + + const char *first = ref+7; + const char *last = strchr(first, '/'); + if (!last) + last = first+strlen(first); + + return strndup(first, last-first); +} + +void cMenuRequest::AppendQualityParamsToRef() { + if (!wvtref) + return; + + char *site = ExtractSiteName(wvtref); + if (site) { + const char *min = webvideoConfig->GetMinQuality(site, GetType()); + const char *max = webvideoConfig->GetMaxQuality(site, GetType()); + free(site); + + if (min && !max) { + cString newref = cString::sprintf("%s&minquality=%s", wvtref, min); + free(wvtref); + wvtref = strdup((const char *)newref); + + } else if (!min && max) { + cString newref = cString::sprintf("%s&maxquality=%s", wvtref, max); + free(wvtref); + wvtref = strdup((const char *)newref); + + } else if (min && max) { + cString newref = cString::sprintf("%s&minquality=%s&maxquality=%s", wvtref, min, max); + free(wvtref); + wvtref = strdup((const char *)newref); + } + } +} + +WebviHandle cMenuRequest::PrepareHandle() { + if (handle == -1) { + handle = webvi_new_request(webvi, wvtref, WEBVIREQ_MENU); + + if (handle != -1) { + webvi_set_opt(webvi, handle, WEBVIOPT_WRITEFUNC, WriteCallback); + webvi_set_opt(webvi, handle, WEBVIOPT_WRITEDATA, this); + } + } + + return handle; +} + +bool cMenuRequest::Start(WebviCtx webvictx) { + webvi = webvictx; + + if ((PrepareHandle() != -1) && (webvi_start_handle(webvi, handle) == WEBVIERR_OK)) { + finished = false; + return true; + } else + return false; +} + +void cMenuRequest::RequestDone(int errorcode, cString pharse) { + finished = true; + status = errorcode; + statusPharse = pharse; +} + +void cMenuRequest::Abort() { + if (finished || handle == -1) + return; + + aborted = true; + webvi_stop_handle(webvi, handle); +}; + +bool cMenuRequest::Success() { + return status == 0; +} + +cString cMenuRequest::GetStatusPharse() { + return statusPharse; +} + +cString cMenuRequest::GetResponse() { + size_t len = inBuffer.Length(); + const char *src = inBuffer.Get(); + char *buf = (char *)malloc((len+1)*sizeof(char)); + strncpy(buf, src, len); + buf[len] = '\0'; + return cString(buf, true); +} + +// --- cFileDownloadRequest ------------------------------------------------ + +cFileDownloadRequest::cFileDownloadRequest(int ID, const char *streamref, + const char *destdir, + cDownloadProgress *progress) +: cMenuRequest(ID, streamref), title(NULL), bytesDownloaded(0), + contentLength(-1), destfile(NULL), progressUpdater(progress) +{ + this->destdir = strdup(destdir); + if (progressUpdater) + progressUpdater->AssociateWith(this); + + AppendQualityParamsToRef(); +} + +cFileDownloadRequest::~cFileDownloadRequest() { + if (destfile) { + destfile->Close(); + delete destfile; + } + if (destdir) + free(destdir); + if (title) + free(title); + // do not delete progressUpdater +} + +WebviHandle cFileDownloadRequest::PrepareHandle() { + if (handle == -1) { + handle = webvi_new_request(webvi, wvtref, WEBVIREQ_FILE); + + if (handle != -1) { + webvi_set_opt(webvi, handle, WEBVIOPT_WRITEFUNC, WriteCallback); + webvi_set_opt(webvi, handle, WEBVIOPT_WRITEDATA, this); + } + } + + return handle; +} + +ssize_t cFileDownloadRequest::WriteData(const char *ptr, size_t len) { + if (!destfile) { + if (!OpenDestFile()) + return -1; + } + + bytesDownloaded += len; + if (progressUpdater) + progressUpdater->Progress(bytesDownloaded); + + return destfile->Write(ptr, len); +} + +bool cFileDownloadRequest::OpenDestFile() { + char *contentType; + char *url; + char *ext; + cString destfilename; + int fd, i; + + if (handle == -1) { + error("handle == -1 while trying to open destination file"); + return false; + } + + if (destfile) + delete destfile; + + destfile = new cUnbufferedFile; + + webvi_get_info(webvi, handle, WEBVIINFO_URL, &url); + webvi_get_info(webvi, handle, WEBVIINFO_STREAM_TITLE, &title); + webvi_get_info(webvi, handle, WEBVIINFO_CONTENT_TYPE, &contentType); + webvi_get_info(webvi, handle, WEBVIINFO_CONTENT_LENGTH, &contentLength); + + if (!contentType || !url) { + if(contentType) + free(contentType); + if (url) + free(url); + + error("no content type or url, can't infer extension"); + return false; + } + + ext = GetExtension(contentType, url); + + free(url); + free(contentType); + + char *basename = strdup(title ? title : "???"); + basename = safeFilename(basename); + + i = 1; + destfilename = cString::sprintf("%s/%s%s", destdir, basename, ext); + while (true) { + debug("trying to open %s", (const char *)destfilename); + + fd = destfile->Open(destfilename, O_WRONLY | O_CREAT | O_EXCL, DEFFILEMODE); + + if (fd == -1 && errno == EEXIST) + destfilename = cString::sprintf("%s/%s-%d%s", destdir, basename, i++, ext); + else + break; + }; + + free(basename); + free(ext); + + if (fd < 0) { + error("Failed to open file %s: %m", (const char *)destfilename); + delete destfile; + destfile = NULL; + return false; + } + + info("Saving to %s", (const char *)destfilename); + + if (progressUpdater) { + progressUpdater->SetTitle(title); + progressUpdater->SetContentLength(contentLength); + } + + return true; +} + +char *cFileDownloadRequest::GetExtension(const char *contentType, const char *url) { + // Get extension from Content-Type + char *ext = NULL; + char *ext2 = MimeTypes->ExtensionFromMimeType(contentType); + + // Workaround for buggy servers: If the server claims that the mime + // type is text/plain, ignore the server and fall back to extracting + // the extension from the URL. This function should be called only + // for video, audio or ASX files and therefore text/plain is clearly + // incorrect. + if (ext2 && contentType && !strcasecmp(contentType, "text/plain")) { + debug("Ignoring content type text/plain, getting extension from url."); + free(ext2); + ext2 = NULL; + } + + if (ext2) { + // Append dot in the start of the extension + ext = (char *)malloc(strlen(ext2)+2); + ext[0] = '.'; + ext[1] = '\0'; + strcat(ext, ext2); + free(ext2); + return ext; + } + + // Get extension from URL + ext = extensionFromUrl(url); + if (ext) + return ext; + + // No extension! + return strdup(""); +} + +void cFileDownloadRequest::RequestDone(int errorcode, cString pharse) { + cMenuRequest::RequestDone(errorcode, pharse); + if (progressUpdater) + progressUpdater->MarkDone(errorcode, pharse); + if (destfile) + destfile->Close(); +} + +// --- cStreamUrlRequest --------------------------------------------------- + +cStreamUrlRequest::cStreamUrlRequest(int ID, const char *ref) +: cMenuRequest(ID, ref) { + AppendQualityParamsToRef(); +} + +WebviHandle cStreamUrlRequest::PrepareHandle() { + if (handle == -1) { + handle = webvi_new_request(webvi, wvtref, WEBVIREQ_STREAMURL); + + if (handle != -1) { + webvi_set_opt(webvi, handle, WEBVIOPT_WRITEFUNC, WriteCallback); + webvi_set_opt(webvi, handle, WEBVIOPT_WRITEDATA, this); + } + } + + return handle; +} + +// --- cTimerRequest ------------------------------------------------------- + +cTimerRequest::cTimerRequest(int ID, const char *ref) +: cMenuRequest(ID, ref) +{ +} + +// --- cRequestVector ------------------------------------------------------ + +cMenuRequest *cRequestVector::FindByHandle(WebviHandle handle) { + for (int i=0; i<Size(); i++) + if (At(i)->GetHandle() == handle) + return At(i); + + return NULL; +} diff --git a/src/vdr-plugin/request.h b/src/vdr-plugin/request.h new file mode 100644 index 0000000..f481fc8 --- /dev/null +++ b/src/vdr-plugin/request.h @@ -0,0 +1,170 @@ +/* + * request.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_REQUEST_H +#define __WEBVIDEO_REQUEST_H + +#include <vdr/tools.h> +#include <vdr/thread.h> +#include <libwebvi.h> +#include "buffer.h" + +enum eRequestType { REQT_NONE, REQT_MENU, REQT_FILE, REQT_STREAM, REQT_TIMER }; + +class cFileDownloadRequest; +class cWebviTimer; + +// --- cDownloadProgress --------------------------------------------------- + +class cDownloadProgress { +private: + const static int NAME_LEN = 128; + + char name[NAME_LEN]; + long downloaded; + long total; + int statusCode; + cString statusPharse; + cFileDownloadRequest *req; + cMutex mutex; +public: + cDownloadProgress(); + + void AssociateWith(cFileDownloadRequest *request); + void SetContentLength(long bytes); + void SetTitle(const char *title); + void Progress(long downloadedbytes); + void MarkDone(int errorcode, cString pharse); + bool IsFinished(); + + cString GetTitle(); + cString GetPercentage(); + cString GetStatusPharse(); + bool Error(); + cFileDownloadRequest *GetRequest() { return req; } +}; + +// --- cProgressVector ----------------------------------------------------- + +class cProgressVector : public cVector<cDownloadProgress *> { +public: + cDownloadProgress *NewDownload(); +}; + +// --- cMenuRequest ---------------------------------------------------- + +class cMenuRequest { +private: + int reqID; + bool aborted; + bool finished; + int status; + cString statusPharse; + +protected: + WebviCtx webvi; + WebviHandle handle; + char *wvtref; + cMemoryBuffer inBuffer; + cWebviTimer *timer; + + virtual ssize_t WriteData(const char *ptr, size_t len); + virtual WebviHandle PrepareHandle(); + static ssize_t WriteCallback(const char *ptr, size_t len, void *request); + + char *ExtractSiteName(const char *ref); + void AppendQualityParamsToRef(); + +public: + cMenuRequest(int ID, const char *wvtreference); + virtual ~cMenuRequest(); + + int GetID() { return reqID; } + WebviHandle GetHandle() { return handle; } + const char *GetReference() { return wvtref; } + + bool Start(WebviCtx webvictx); + virtual void RequestDone(int errorcode, cString pharse); + bool IsFinished() { return finished; } + void Abort(); + bool IsAborted() { return aborted; } + + // Return true if the lastest status code indicates success. + bool Success(); + // Return the status code + int GetStatusCode() { return status; } + // Return the response pharse + cString GetStatusPharse(); + + virtual eRequestType GetType() { return REQT_MENU; } + + // Return the content of the reponse message + virtual cString GetResponse(); + + void SetTimer(cWebviTimer *t) { timer = t; } + cWebviTimer *GetTimer() { return timer; } +}; + +// --- cFileDownloadRequest ------------------------------------------------ + +class cFileDownloadRequest : public cMenuRequest { +private: + char *destdir; + char *title; + long bytesDownloaded; + long contentLength; + cUnbufferedFile *destfile; + cDownloadProgress *progressUpdater; + +protected: + virtual WebviHandle PrepareHandle(); + virtual ssize_t WriteData(const char *ptr, size_t len); + bool OpenDestFile(); + char *GetExtension(const char *contentType, const char *url); + +public: + cFileDownloadRequest(int ID, const char *streamref, + const char *destdir, + cDownloadProgress *progress); + virtual ~cFileDownloadRequest(); + + eRequestType GetType() { return REQT_FILE; } + void RequestDone(int errorcode, cString pharse); +}; + +// --- cStreamUrlRequest --------------------------------------------------- + +class cStreamUrlRequest : public cMenuRequest { +protected: + virtual WebviHandle PrepareHandle(); + +public: + cStreamUrlRequest(int ID, const char *ref); + + eRequestType GetType() { return REQT_STREAM; } +}; + +// --- cTimerRequest ------------------------------------------------------- + +class cTimerRequest : public cMenuRequest { +public: + cTimerRequest(int ID, const char *ref); + + eRequestType GetType() { return REQT_TIMER; } +}; + +// --- cRequestVector ------------------------------------------------------ + +class cRequestVector : public cVector<cMenuRequest *> { +public: + cRequestVector(int Allocated = 10) : cVector<cMenuRequest *>(Allocated) {} + + cMenuRequest *FindByHandle(WebviHandle handle); +}; + +#endif diff --git a/src/vdr-plugin/timer.c b/src/vdr-plugin/timer.c new file mode 100644 index 0000000..f9fef59 --- /dev/null +++ b/src/vdr-plugin/timer.c @@ -0,0 +1,465 @@ +/* + * timer.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <string.h> +#include <errno.h> +#include <libxml/parser.h> +#include "timer.h" +#include "request.h" +#include "common.h" +#include "download.h" +#include "config.h" + +// --- cWebviTimer ----------------------------------------------- + +cWebviTimer::cWebviTimer(int ID, const char *title, + const char *ref, cWebviTimerManager *manager, + time_t last, int interval, bool success, + const char *errmsg) + : id(ID), title(title ? strdup(title) : strdup("???")), + reference(ref ? strdup(ref) : NULL), lastUpdate(last), + interval(interval), running(false), lastSucceeded(success), + lastError(errmsg ? strdup(errmsg) : NULL), + parent(manager) +{ +} + +cWebviTimer::~cWebviTimer() { + if(title) + free(title); + if (reference) + free(reference); + if (lastError) + free(lastError); +} + +void cWebviTimer::SetTitle(const char *newTitle) { + if (title) + free(title); + title = newTitle ? strdup(newTitle) : strdup("???"); + + parent->SetModified(); +} + +void cWebviTimer::SetInterval(int interval) { + if (interval < MIN_TIMER_INTERVAL) + this->interval = MIN_TIMER_INTERVAL; + else + this->interval = interval; + + parent->SetModified(); +} + +int cWebviTimer::GetInterval() const { + return interval; +} + +time_t cWebviTimer::NextUpdate() const { + int delta = interval; + + // Retry again soon if the last try failed + if (!lastSucceeded && delta > RETRY_TIMER_INTERVAL) + delta = RETRY_TIMER_INTERVAL; + + return lastUpdate + delta; +} + +void cWebviTimer::Execute() { + if (running) { + debug("previous instance of this timer is still running"); + return; + } + + info("Executing timer \"%s\"", title); + + running = true; + cTimerRequest *req = new cTimerRequest(id, reference); + req->SetTimer(this); + cWebviThread::Instance().AddRequest(req); + + lastUpdate = time(NULL); + SetError("Unfinished"); + parent->SetModified(); + + activeStreams.Clear(); +} + +void cWebviTimer::SetError(const char *errmsg) { + bool oldSuccess = lastSucceeded; + + if (lastError) + free(lastError); + lastError = NULL; + + if (errmsg) { + lastSucceeded = false; + lastError = strdup(errmsg); + } else { + lastSucceeded = true; + } + + if (oldSuccess != lastSucceeded) + parent->SetModified(); +} + +const char *cWebviTimer::LastError() const { + return lastError ? lastError : ""; +} + +void cWebviTimer::DownloadStreams(const char *menuxml, cProgressVector& summaries) { + if (!menuxml) { + SetError("xml == NULL"); + return; + } + + xmlDocPtr doc = xmlParseMemory(menuxml, strlen(menuxml)); + if (!doc) { + xmlErrorPtr xmlerr = xmlGetLastError(); + if (xmlerr) + error("libxml error: %s", xmlerr->message); + SetError(xmlerr->message); + return; + } + + xmlNodePtr node = xmlDocGetRootElement(doc); + if (node) + node = node->xmlChildrenNode; + + while (node) { + if (!xmlStrcmp(node->name, BAD_CAST "link")) { + xmlNodePtr node2 = node->children; + + while(node2) { + if (!xmlStrcmp(node2->name, BAD_CAST "stream")) { + xmlChar *streamref = xmlNodeListGetString(doc, node2->xmlChildrenNode, 1); + const char *ref = (const char *)streamref; + + if (parent->AlreadyDownloaded(ref)) { + debug("timer: %s has already been downloaded", ref); + } else if (*ref) { + info("timer: downloading %s", ref); + + activeStreams.Append(strdup(ref)); + cFileDownloadRequest *req = \ + new cFileDownloadRequest(REQ_ID_TIMER, ref, + webvideoConfig->GetDownloadPath(), + summaries.NewDownload()); + req->SetTimer(this); + cWebviThread::Instance().AddRequest(req); + } + + xmlFree(streamref); + } + + node2 = node2->next; + } + } + + node = node->next; + } + + xmlFreeDoc(doc); + + if (activeStreams.Size() == 0) { + SetError(NULL); + running = false; + } +} + +void cWebviTimer::CheckFailed(const char *errmsg) { + SetError(errmsg); + running = false; +} + +void cWebviTimer::RequestFinished(const char *ref, const char *errmsg) { + if (errmsg && !lastError) + SetError(errmsg); + + if (ref) { + if (parent) + parent->MarkDownloaded(ref); + + int i = activeStreams.Find(ref); + if (i != -1) { + free(activeStreams[i]); + activeStreams.Remove(i); + } + } + + if (activeStreams.Size() == 0) { + info("timer \"%s\" done", title); + running = false; + } else { + debug("timer %s is still downloading %d streams", reference, activeStreams.Size()); + } +} + +// --- cWebviTimerManager ---------------------------------------- + +cWebviTimerManager::cWebviTimerManager() + : nextID(1), modified(false), disableSaving(false) +{ +} + +cWebviTimerManager &cWebviTimerManager::Instance() { + static cWebviTimerManager instance; + + return instance; +} + +void cWebviTimerManager::LoadTimers(FILE *f) { + cReadLine rl; + long lastRefresh; + int interval; + int success; + char *ref; + const char *ver; + const char *title; + const char *errmsg; + int n, i; + + ver = rl.Read(f); + if (strcmp(ver, "# WVTIMER1") != 0) { + error("Can't load timers. Unknown format: %s", ver); + disableSaving = true; + return; + } + + i = 1; + while (true) { + n = fscanf(f, "%ld %d %d %ms", &lastRefresh, &interval, &success, &ref); + if (n != 4) { + if (n != EOF) { + error("Error while reading webvi timers file"); + } else if (ferror(f)) { + LOG_ERROR_STR("webvi timers file"); + } + + break; + } + + title = rl.Read(f); + title = title ? skipspace(title) : "???"; + errmsg = success ? NULL : ""; + + info("timer %d: title %s", i++, title); + debug(" ref %s, lastRefresh %ld, interval %d", ref, lastRefresh, interval); + + timers.Add(new cWebviTimer(nextID++, title, ref, this, + (time_t)lastRefresh, interval, + success, errmsg)); + + free(ref); + } +} + +void cWebviTimerManager::LoadHistory(FILE *f) { + cReadLine rl; + char *line; + + while ((line = rl.Read(f))) + refHistory.Append(strdup(line)); + + debug("loaded history: len = %d", refHistory.Size()); +} + +void cWebviTimerManager::SaveTimers(FILE *f) { + // Format: space separated field in this order: + // lastUpdate interval lastSucceeded reference title + + fprintf(f, "# WVTIMER1\n"); + + cWebviTimer *t = timers.First(); + while (t) { + if (fprintf(f, "%ld %d %d %s %s\n", + t->LastUpdate(), t->GetInterval(), t->Success(), + t->GetReference(), t->GetTitle()) < 0) { + error("Failed to save timer data!"); + } + + t = timers.Next(t); + } +} + +void cWebviTimerManager::SaveHistory(FILE *f) { + int size = refHistory.Size(); + int first; + + if (size <= MAX_TIMER_HISTORY_SIZE) + first = 0; + else + first = size - MAX_TIMER_HISTORY_SIZE; + + for (int i=first; i<size; i++) { + const char *ref = refHistory[i]; + if (fwrite(ref, strlen(ref), 1, f) != 1 || + fwrite("\n", 1, 1, f) != 1) { + error("Error while writing timer history"); + break; + } + } +} + +bool cWebviTimerManager::Load(const char *path) { + FILE *f; + bool ok = true; + + cString timersname = AddDirectory(path, "timers.dat"); + f = fopen(timersname, "r"); + if (f) { + debug("loading webvi timers from %s", (const char *)timersname); + LoadTimers(f); + fclose(f); + } else { + if (errno != ENOENT) + LOG_ERROR_STR("Can't load webvi timers"); + ok = false; + } + + cString historyname = AddDirectory(path, "timers.hst"); + f = fopen(historyname, "r"); + if (f) { + debug("loading webvi history from %s", (const char *)historyname); + LoadHistory(f); + fclose(f); + } else { + if (errno != ENOENT) + LOG_ERROR_STR("Can't load webvi timer history"); + ok = false; + } + + return ok; +} + +bool cWebviTimerManager::Save(const char *path) { + FILE *f; + bool ok = true; + + if (!modified) + return true; + if (disableSaving) { + error("Not saving timers because the file format is unknown."); + return false; + } + + cString timersname = AddDirectory(path, "timers.dat"); + f = fopen(timersname, "w"); + if (f) { + debug("saving webvi timers to %s", (const char *)timersname); + SaveTimers(f); + fclose(f); + } else { + LOG_ERROR_STR("Can't save webvi timers"); + ok = false; + } + + cString historyname = AddDirectory(path, "timers.hst"); + f = fopen(historyname, "w"); + if (f) { + debug("saving webvi timer history to %s", (const char *)historyname); + SaveHistory(f); + fclose(f); + } else { + LOG_ERROR_STR("Can't save webvi timer history"); + ok = false; + } + + modified = !ok; + + return ok; +} + +void cWebviTimerManager::Update() { + char timestr[25]; + cWebviTimer *timer = timers.First(); + if (!timer) + return; + + time_t now = time(NULL); + +#ifdef DEBUG + strftime(timestr, 25, "%x %X", localtime(&now)); + debug("Running webvi timers update at %s", timestr); +#endif + + while (timer) { + if (timer->NextUpdate() < now) { + debug("%d. %s: launching now", + timer->GetID(), timer->GetTitle()); + timer->Execute(); + } else { +#ifdef DEBUG + time_t next = timer->NextUpdate(); + strftime(timestr, 25, "%x %X", localtime(&next)); + debug("%d. %s: next update at %s", + timer->GetID(), timer->GetTitle(), timestr); +#endif + } + + timer = timers.Next(timer); + } +} + +cWebviTimer *cWebviTimerManager::GetByID(int id) const { + cWebviTimer *timer = timers.First(); + + while (timer) { + if (timer->GetID() == id) + return timer; + + timer = timers.Next(timer); + } + + return NULL; +} + +cWebviTimer *cWebviTimerManager::Create(const char *title, + const char *ref, + bool getExisting) { + cWebviTimer *t; + + if (!ref) + return NULL; + + if (getExisting) { + t = timers.First(); + while (t) { + if (strcmp(t->GetReference(), ref) == 0) { + return t; + } + + t = timers.Next(t); + } + } + + t = new cWebviTimer(nextID++, title, ref, this); + timers.Add(t); + + modified = true; + + return t; +} + +void cWebviTimerManager::Remove(cWebviTimer *timer) { + timers.Del(timer); + modified = true; +} + +void cWebviTimerManager::MarkDownloaded(const char *ref) { + if (!ref) + return; + + if (refHistory.Find(ref) == -1) { + refHistory.Append(strdup(ref)); + modified = true; + } +} + +bool cWebviTimerManager::AlreadyDownloaded(const char *ref) { + return refHistory.Find(ref) != -1; +} diff --git a/src/vdr-plugin/timer.h b/src/vdr-plugin/timer.h new file mode 100644 index 0000000..048014a --- /dev/null +++ b/src/vdr-plugin/timer.h @@ -0,0 +1,111 @@ +/* + * timer.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_TIMER_H +#define __WEBVIDEO_TIMER_H + +#include <time.h> +#include <stdio.h> +#include <vdr/tools.h> +#include "request.h" + +#define REQ_ID_TIMER -2 +#define DEFAULT_TIMER_INTERVAL 7*24*60*60 +#define RETRY_TIMER_INTERVAL 60*60 +#define MIN_TIMER_INTERVAL 10*60 +#define MAX_TIMER_HISTORY_SIZE 2000 + +class cWebviTimerManager; + +// --- cWebviTimer ----------------------------------------------- + +class cWebviTimer : public cListObject { +private: + int id; + char *title; + char *reference; + + time_t lastUpdate; + int interval; + + bool running; + cStringList activeStreams; + bool lastSucceeded; + char *lastError; + + cWebviTimerManager *parent; + +public: + cWebviTimer(int ID, const char *title, const char *ref, + cWebviTimerManager *manager, + time_t last=0, int interval=DEFAULT_TIMER_INTERVAL, + bool success=true, const char *errmsg=NULL); + ~cWebviTimer(); + + int GetID() const { return id; } + void SetTitle(const char *newTitle); + const char *GetTitle() const { return title; } + void SetInterval(int interval); + int GetInterval() const; + const char *GetReference() const { return reference; } + + time_t LastUpdate() const { return lastUpdate; } + time_t NextUpdate() const; + + void SetError(const char *errmsg); + bool Success() const { return lastSucceeded; } + const char *LastError() const; + + void Execute(); + bool Running() { return running; } + void DownloadStreams(const char *menuxml, cProgressVector& summaries); + void CheckFailed(const char *errmsg); + void RequestFinished(const char *ref, const char *errmsg); +}; + +// --- cWebviTimerManager ---------------------------------------- + +class cWebviTimerManager { +private: + cList<cWebviTimer> timers; + int nextID; + cStringList refHistory; + bool modified; + bool disableSaving; + + cWebviTimerManager(); + ~cWebviTimerManager() {}; + cWebviTimerManager(const cWebviTimerManager &); // intentionally undefined + cWebviTimerManager &operator=(const cWebviTimerManager &); // intentionally undefined + + void LoadTimers(FILE *f); + void LoadHistory(FILE *f); + void SaveTimers(FILE *f); + void SaveHistory(FILE *f); + +public: + static cWebviTimerManager &Instance(); + + bool Load(const char *path); + bool Save(const char *path); + + cWebviTimer *Create(const char *title, const char *reference, + bool getExisting=true); + void Remove(cWebviTimer *timer); + cWebviTimer *First() const { return timers.First(); } + cWebviTimer *Next(const cWebviTimer *cur) const { return timers.Next(cur); } + cWebviTimer *GetLinear(int idx) const { return timers.Get(idx); } + cWebviTimer *GetByID(int id) const; + void SetModified() { modified = true; } + + void Update(); + void MarkDownloaded(const char *ref); + bool AlreadyDownloaded(const char *ref); +}; + +#endif diff --git a/src/vdr-plugin/webvideo.c b/src/vdr-plugin/webvideo.c new file mode 100644 index 0000000..554ef28 --- /dev/null +++ b/src/vdr-plugin/webvideo.c @@ -0,0 +1,444 @@ +/* + * webvideo.c: A plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <getopt.h> +#include <time.h> +#include <vdr/plugin.h> +#include <vdr/tools.h> +#include <vdr/videodir.h> +#include <vdr/i18n.h> +#include <vdr/skins.h> +#include <libwebvi.h> +#include "menu.h" +#include "history.h" +#include "download.h" +#include "request.h" +#include "mimetypes.h" +#include "config.h" +#include "player.h" +#include "common.h" +#include "timer.h" + +const char *VERSION = "0.3.0"; +static const char *DESCRIPTION = trNOOP("Download video files from the web"); +static const char *MAINMENUENTRY = "Webvideo"; +cMimeTypes *MimeTypes = NULL; + +class cPluginWebvideo : public cPlugin { +private: + // Add any member variables or functions you may need here. + cHistory history; + cProgressVector summaries; + cString templatedir; + cString destdir; + cString conffile; + + static int nextMenuID; + + void UpdateOSDFromHistory(const char *statusmsg=NULL); + void UpdateStatusMenu(bool force=false); + bool StartStreaming(const cString &streamurl); + void ExecuteTimers(void); + void HandleFinishedRequests(void); + +public: + cPluginWebvideo(void); + virtual ~cPluginWebvideo(); + virtual const char *Version(void) { return VERSION; } + virtual const char *Description(void) { return tr(DESCRIPTION); } + virtual const char *CommandLineHelp(void); + virtual bool ProcessArgs(int argc, char *argv[]); + virtual bool Initialize(void); + virtual bool Start(void); + virtual void Stop(void); + virtual void Housekeeping(void); + virtual void MainThreadHook(void); + virtual cString Active(void); + virtual const char *MainMenuEntry(void) { return MAINMENUENTRY; } + virtual cOsdObject *MainMenuAction(void); + virtual cMenuSetupPage *SetupMenu(void); + virtual bool SetupParse(const char *Name, const char *Value); + virtual bool Service(const char *Id, void *Data = NULL); + virtual const char **SVDRPHelpPages(void); + virtual cString SVDRPCommand(const char *Command, const char *Option, int &ReplyCode); + }; + +int cPluginWebvideo::nextMenuID = 1; + +cPluginWebvideo::cPluginWebvideo(void) +{ + // Initialize any member variables here. + // DON'T DO ANYTHING ELSE THAT MAY HAVE SIDE EFFECTS, REQUIRE GLOBAL + // VDR OBJECTS TO EXIST OR PRODUCE ANY OUTPUT! +} + +cPluginWebvideo::~cPluginWebvideo() +{ + // Clean up after yourself! + webvi_cleanup(0); +} + +const char *cPluginWebvideo::CommandLineHelp(void) +{ + // Return a string that describes all known command line options. + return " -d DIR, --downloaddir=DIR Save downloaded files to DIR\n" \ + " -t DIR, --templatedir=DIR Read video site templates from DIR\n" \ + " -c FILE, --conf=FILE Load settings from FILE\n"; +} + +bool cPluginWebvideo::ProcessArgs(int argc, char *argv[]) +{ + // Implement command line argument processing here if applicable. + static struct option long_options[] = { + { "downloaddir", required_argument, NULL, 'd' }, + { "templatedir", required_argument, NULL, 't' }, + { "conf", required_argument, NULL, 'c' }, + { NULL } + }; + + int c; + while ((c = getopt_long(argc, argv, "d:t:c:", long_options, NULL)) != -1) { + switch (c) { + case 'd': + destdir = cString(optarg); + break; + case 't': + templatedir = cString(optarg); + break; + case 'c': + conffile = cString(optarg); + break; + default: + return false; + } + } + return true; +} + +bool cPluginWebvideo::Initialize(void) +{ + // Initialize any background activities the plugin shall perform. + + // Test that run-time and compile-time libxml versions are compatible + LIBXML_TEST_VERSION; + + // default values if not given on the command line + if ((const char *)destdir == NULL) + destdir = cString(VideoDirectory); + if ((const char *)conffile == NULL) + conffile = AddDirectory(ConfigDirectory(Name()), "webvi.plugin.conf"); + + webvideoConfig->SetDownloadPath(destdir); + webvideoConfig->SetTemplatePath(templatedir); + webvideoConfig->ReadConfigFile(conffile); + + cString mymimetypes = AddDirectory(ConfigDirectory(Name()), "mime.types"); + const char *mimefiles [] = {"/etc/mime.types", (const char *)mymimetypes, NULL}; + MimeTypes = new cMimeTypes(mimefiles); + + if (webvi_global_init() != 0) { + error("Failed to initialize libwebvi"); + return false; + } + + cWebviTimerManager::Instance().Load(ConfigDirectory(Name())); + + cWebviThread::Instance().SetTemplatePath(webvideoConfig->GetTemplatePath()); + + return true; +} + +bool cPluginWebvideo::Start(void) +{ + // Start any background activities the plugin shall perform. + cWebviThread::Instance().Start(); + + return true; +} + +void cPluginWebvideo::Stop(void) +{ + // Stop any background activities the plugin shall perform. + cWebviThread::Instance().Stop(); + delete MimeTypes; + + cWebviTimerManager::Instance().Save(ConfigDirectory(Name())); + + xmlCleanupParser(); +} + +void cPluginWebvideo::Housekeeping(void) +{ + // Perform any cleanup or other regular tasks. + + cWebviTimerManager::Instance().Save(ConfigDirectory(Name())); +} + +void cPluginWebvideo::MainThreadHook(void) +{ + // Perform actions in the context of the main program thread. + // WARNING: Use with great care - see PLUGINS.html! + ExecuteTimers(); + + HandleFinishedRequests(); +} + +void cPluginWebvideo::ExecuteTimers(void) +{ + static int counter = 0; + + // don't do this too often + if (counter++ > 1800) { + cWebviTimerManager::Instance().Update(); + counter = 0; + } +} + +void cPluginWebvideo::HandleFinishedRequests(void) +{ + bool forceStatusUpdate = false; + cMenuRequest *req; + cFileDownloadRequest *dlreq; + cString streamurl; + cWebviTimer *timer; + cString timermsg; + + while ((req = cWebviThread::Instance().GetFinishedRequest())) { + int cid = -1; + int code = req->GetStatusCode(); + if (history.Current()) { + cid = history.Current()->GetID(); + } + + debug("Finished request: %d (current: %d), type = %d, status = %d", + req->GetID(), cid, req->GetType(), code); + + if (req->Success()) { + switch (req->GetType()) { + case REQT_MENU: + // Only change the menu if the request was launched from the + // current menu. + if (req->GetID() == cid) { + if (cid == 0) { + // Special case: replace the placeholder menu + history.Clear(); + } + + if (history.Current()) + history.Current()->RememberSelected(menuPointers.navigationMenu->Current()); + history.TruncateAndAdd(new cHistoryObject(req->GetResponse(), + req->GetReference(), + nextMenuID++)); + UpdateOSDFromHistory(); + } + break; + + case REQT_STREAM: + streamurl = req->GetResponse(); + if (streamurl[0] == '\0') + Skins.Message(mtError, tr("Streaming failed: no URL")); + else if (!StartStreaming(streamurl)) + Skins.Message(mtError, tr("Failed to launch media player")); + break; + + case REQT_FILE: + dlreq = dynamic_cast<cFileDownloadRequest *>(req); + + if (dlreq) { + for (int i=0; i<summaries.Size(); i++) { + if (summaries[i]->GetRequest() == dlreq) { + delete summaries[i]; + summaries.Remove(i); + break; + } + } + } + + timermsg = cString(""); + if (req->GetTimer()) { + req->GetTimer()->RequestFinished(req->GetReference(), NULL); + + timermsg = cString::sprintf(" (%s)", tr("timer")); + } + + Skins.Message(mtInfo, cString::sprintf(tr("One download completed, %d remains%s"), + cWebviThread::Instance().GetUnfinishedCount(), + (const char *)timermsg)); + forceStatusUpdate = true; + break; + + case REQT_TIMER: + timer = req->GetTimer(); + if (timer) + timer->DownloadStreams(req->GetResponse(), summaries); + break; + + default: + break; + } + } else { // failed request + if (req->GetType() == REQT_TIMER) { + warning("timer request failed (%d: %s)", + code, (const char*)req->GetStatusPharse()); + + timer = req->GetTimer(); + if (timer) + timer->CheckFailed(req->GetStatusPharse()); + } else { + warning("request failed (%d: %s)", + code, (const char*)req->GetStatusPharse()); + + if (code == -2 || code == 402) + Skins.Message(mtError, tr("Download aborted")); + else + Skins.Message(mtError, cString::sprintf(tr("Download failed (error = %d)"), code)); + + dlreq = dynamic_cast<cFileDownloadRequest *>(req); + if (dlreq) { + for (int i=0; i<summaries.Size(); i++) { + if (summaries[i]->GetRequest() == dlreq) { + summaries[i]->AssociateWith(NULL); + break; + } + } + } + + if (req->GetTimer()) + req->GetTimer()->RequestFinished(req->GetReference(), + (const char*)req->GetStatusPharse()); + + forceStatusUpdate = true; + } + } + + delete req; + } + + UpdateStatusMenu(forceStatusUpdate); +} + +cString cPluginWebvideo::Active(void) +{ + // Return a message string if shutdown should be postponed + int c = cWebviThread::Instance().GetUnfinishedCount(); + if (c > 0) + return cString::sprintf(tr("%d downloads not finished"), c); + else + return NULL; +} + +cOsdObject *cPluginWebvideo::MainMenuAction(void) +{ + // Perform the action when selected from the main VDR menu. + const char *mainMenuReference = "wvt:///?srcurl=mainmenu"; + const char *placeholderMenu = "<wvmenu><title>Webvideo</title></wvmenu>"; + const char *statusmsg = NULL; + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 100*1000*1000; // 100 ms + + menuPointers.navigationMenu = new cNavigationMenu(&history, summaries); + + cHistoryObject *hist = history.Home(); + if (!hist) { + cWebviThread::Instance().AddRequest(new cMenuRequest(0, mainMenuReference)); + cHistoryObject *placeholder = new cHistoryObject(placeholderMenu, mainMenuReference, 0); + history.TruncateAndAdd(placeholder); + + // The main menu response should come right away. Try to update + // the menu here without having to wait for the next + // MainThreadHook call by VDR main loop. + for (int i=0; i<4; i++) { + nanosleep(&ts, NULL); + HandleFinishedRequests(); + if (history.Current() != placeholder) { + return menuPointers.navigationMenu; + } + }; + + statusmsg = tr("Retrieving..."); + } + + UpdateOSDFromHistory(statusmsg); + return menuPointers.navigationMenu; +} + +cMenuSetupPage *cPluginWebvideo::SetupMenu(void) +{ + // Return a setup menu in case the plugin supports one. + return NULL; +} + +bool cPluginWebvideo::SetupParse(const char *Name, const char *Value) +{ + // Parse your own setup parameters and store their values. + return false; +} + +bool cPluginWebvideo::Service(const char *Id, void *Data) +{ + // Handle custom service requests from other plugins + return false; +} + +const char **cPluginWebvideo::SVDRPHelpPages(void) +{ + // Return help text for SVDRP commands this plugin implements + return NULL; +} + +cString cPluginWebvideo::SVDRPCommand(const char *Command, const char *Option, int &ReplyCode) +{ + // Process SVDRP commands this plugin implements + return NULL; +} + +void cPluginWebvideo::UpdateOSDFromHistory(const char *statusmsg) { + if (menuPointers.navigationMenu) { + cHistoryObject *hist = history.Current(); + menuPointers.navigationMenu->Populate(hist, statusmsg); + menuPointers.navigationMenu->Display(); + } else { + debug("OSD is not ours."); + } +} + +void cPluginWebvideo::UpdateStatusMenu(bool force) { + if (menuPointers.statusScreen && + (force || menuPointers.statusScreen->NeedsUpdate())) { + menuPointers.statusScreen->Update(); + } +} + +bool cPluginWebvideo::StartStreaming(const cString &streamurl) { + cMediaPlayer *players[2]; + + if (webvideoConfig->GetPreferXineliboutput()) { + players[0] = new cXineliboutputPlayer(); + players[1] = new cMPlayerPlayer(); + } else { + players[0] = new cMPlayerPlayer(); + players[1] = new cXineliboutputPlayer(); + } + + bool ret = false; + for (int i=0; i<2; i++) { + if (players[i]->Launch(streamurl)) { + ret = true; + break; + } + } + + for (int i=0; i<2 ; i++) { + delete players[i]; + } + + return ret; +} + +VDRPLUGINCREATOR(cPluginWebvideo); // Don't touch this! diff --git a/src/version b/src/version new file mode 100644 index 0000000..9325c3c --- /dev/null +++ b/src/version @@ -0,0 +1 @@ +0.3.0
\ No newline at end of file diff --git a/src/webvicli/webvi b/src/webvicli/webvi new file mode 100755 index 0000000..b8fa190 --- /dev/null +++ b/src/webvicli/webvi @@ -0,0 +1,22 @@ +#!/usr/bin/python + +# menu.py - starter script for webvicli +# +# Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +from webvicli import client +client.main(sys.argv[1:]) diff --git a/src/webvicli/webvicli/__init__.py b/src/webvicli/webvicli/__init__.py new file mode 100644 index 0000000..1cf59b7 --- /dev/null +++ b/src/webvicli/webvicli/__init__.py @@ -0,0 +1 @@ +__all__ = ['client', 'menu'] diff --git a/src/webvicli/webvicli/client.py b/src/webvicli/webvicli/client.py new file mode 100644 index 0000000..782c47c --- /dev/null +++ b/src/webvicli/webvicli/client.py @@ -0,0 +1,729 @@ +#!/usr/bin/env python + +# webvicli.py - webvi command line client +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import cStringIO +import sys +import cmd +import mimetypes +import select +import os.path +import subprocess +import time +import re +import libxml2 +import webvi.api +import webvi.utils +from optparse import OptionParser +from ConfigParser import RawConfigParser +from webvi.constants import WebviRequestType, WebviErr, WebviOpt, WebviInfo, WebviSelectBitmask, WebviConfig +from . import menu + +VERSION = '0.3.0' + +# Default options +DEFAULT_PLAYERS = ['vlc --play-and-exit "%s"', + 'totem "%s"', + 'mplayer "%s"', + 'xine "%s"'] + +# These mimetypes are common but often missing +mimetypes.init() +mimetypes.add_type('video/flv', '.flv') +mimetypes.add_type('video/x-flv', '.flv') + +def safe_filename(name): + """Sanitize a filename. No paths (replace '/' -> '!') and no + names starting with a dot.""" + res = name.replace('/', '!').lstrip('.') + res = res.encode(sys.getfilesystemencoding(), 'ignore') + return res + +class DownloadData: + def __init__(self, handle, progressstream): + self.handle = handle + self.destfile = None + self.destfilename = '' + self.contentlength = -1 + self.bytes_downloaded = 0 + self.progress = ProgressMeter(progressstream) + +class ProgressMeter: + def __init__(self, stream): + self.last_update = None + self.samples = [] + self.total_bytes = 0 + self.stream = stream + self.progress_len = 0 + self.starttime = time.time() + + def pretty_bytes(self, bytes): + """Pretty print bytes as kB or MB.""" + if bytes < 1100: + return '%d B' % bytes + elif bytes < 1024*1024: + return '%.1f kB' % (float(bytes)/1024) + elif bytes < 1024*1024*1024: + return '%.1f MB' % (float(bytes)/1024/1024) + else: + return '%.1f GB' % (float(bytes)/1024/1024/1024) + + def pretty_time(self, seconds): + """Pretty print seconds as hour and minutes.""" + seconds = int(round(seconds)) + if seconds < 60: + return '%d s' % seconds + elif seconds < 60*60: + secs = seconds % 60 + mins = seconds/60 + return '%d min %d s' % (mins, secs) + else: + hours = seconds / (60*60) + mins = (seconds-60*60*hours) / 60 + return '%d hours %d min' % (hours, mins) + + def update(self, bytes): + """Update progress bar. + + Updates the estimates of download rate and remaining time. + Prints progress bar, if at least one second has passed since + the previous update. + """ + now = time.time() + + if self.total_bytes > 0: + percentage = float(bytes)/self.total_bytes * 100.0 + else: + percentage = 0 + + if self.total_bytes > 0 and bytes >= self.total_bytes: + self.stream.write('\r') + self.stream.write(' '*self.progress_len) + self.stream.write('\r') + self.stream.write('%3.f %% of %s downloaded in %s (%.1f kB/s)\n' % + (percentage, self.pretty_bytes(self.total_bytes), + self.pretty_time(now-self.starttime), + float(bytes)/(now-self.starttime)/1024.0)) + self.stream.flush() + return + + force_refresh = False + if self.last_update is None: + # This is a new progress meter + self.last_update = now + force_refresh = True + + if (not force_refresh) and (now <= self.last_update + 1): + # do not update too often + return + + self.last_update = now + + # Estimate bytes per second rate from the last 10 samples + self.samples.append((bytes, now)) + if len(self.samples) > 10: + self.samples.pop(0) + + bytes_old, time_old = self.samples[0] + if now > time_old: + rate = float(bytes-bytes_old)/(now-time_old) + else: + rate = 0 + + if self.total_bytes > 0: + remaining = self.total_bytes - bytes + + if rate > 0: + time_left = self.pretty_time(remaining/rate) + else: + time_left = '???' + + progress = '%3.f %% of %s (%.1f kB/s) %s remaining' % \ + (percentage, self.pretty_bytes(self.total_bytes), + rate/1024.0, time_left) + else: + progress = '%s downloaded (%.1f kB/s)' % \ + (self.pretty_bytes(bytes), rate/1024.0) + + new_progress_len = len(progress) + if new_progress_len < self.progress_len: + progress += ' '*(self.progress_len - new_progress_len) + self.progress_len = new_progress_len + + self.stream.write('\r') + self.stream.write(progress) + self.stream.flush() + + +class WVClient: + def __init__(self, streamplayers, downloadlimits, streamlimits): + self.streamplayers = streamplayers + self.history = [] + self.history_pointer = 0 + self.quality_limits = {'download': downloadlimits, + 'stream': streamlimits} + + def parse_page(self, page): + if page is None: + return None + try: + doc = libxml2.parseDoc(page) + except libxml2.parserError: + return None + + root = doc.getRootElement() + if root.name != 'wvmenu': + return None + queryitems = [] + menupage = menu.Menu() + node = root.children + while node: + if node.name == 'title': + menupage.title = webvi.utils.get_content_unicode(node) + elif node.name == 'link': + menuitem = self.parse_link(node) + menupage.add(menuitem) + elif node.name == 'textfield': + menuitem = self.parse_textfield(node) + menupage.add(menuitem) + queryitems.append(menuitem) + elif node.name == 'itemlist': + menuitem = self.parse_itemlist(node) + menupage.add(menuitem) + queryitems.append(menuitem) + elif node.name == 'textarea': + menuitem = self.parse_textarea(node) + menupage.add(menuitem) + elif node.name == 'button': + menuitem = self.parse_button(node, queryitems) + menupage.add(menuitem) + node = node.next + doc.freeDoc() + return menupage + + def parse_link(self, node): + label = '' + ref = None + stream = None + child = node.children + while child: + if child.name == 'label': + label = webvi.utils.get_content_unicode(child) + elif child.name == 'ref': + ref = webvi.utils.get_content_unicode(child) + elif child.name == 'stream': + stream = webvi.utils.get_content_unicode(child) + child = child.next + return menu.MenuItemLink(label, ref, stream) + + def parse_textfield(self, node): + label = '' + name = node.prop('name') + child = node.children + while child: + if child.name == 'label': + label = webvi.utils.get_content_unicode(child) + child = child.next + return menu.MenuItemTextField(label, name) + + def parse_textarea(self, node): + label = '' + child = node.children + while child: + if child.name == 'label': + label = webvi.utils.get_content_unicode(child) + child = child.next + return menu.MenuItemTextArea(label) + + def parse_itemlist(self, node): + label = '' + name = node.prop('name') + items = [] + values = [] + child = node.children + while child: + if child.name == 'label': + label = webvi.utils.get_content_unicode(child) + elif child.name == 'item': + items.append(webvi.utils.get_content_unicode(child)) + values.append(child.prop('value')) + child = child.next + return menu.MenuItemList(label, name, items, values, sys.stdout) + + def parse_button(self, node, queryitems): + label = '' + submission = None + child = node.children + while child: + if child.name == 'label': + label = webvi.utils.get_content_unicode(child) + elif child.name == 'submission': + submission = webvi.utils.get_content_unicode(child) + child = child.next + return menu.MenuItemSubmitButton(label, submission, queryitems) + + def guess_extension(self, mimetype, url): + ext = mimetypes.guess_extension(mimetype) + if (ext is None) or (mimetype == 'text/plain'): + # This function is only called for video files. Try to + # extract the extension from url because text/plain is + # clearly wrong. + lastcomponent = re.split(r'[?#]', url, 1)[0].split('/')[-1] + i = lastcomponent.rfind('.') + if i == -1: + ext = '' + else: + ext = lastcomponent[i:] + + return ext + + def execute_webvi(self, handle): + """Call webvi.api.perform until handle is finished.""" + while True: + rescode, readfds, writefds, excfds, maxfd = webvi.api.fdset() + if [] == readfds == writefds == excfds: + finished, status, errmsg, remaining = webvi.api.pop_message() + if finished == handle: + return (status, errmsg) + else: + return (501, 'No active sockets') + + readyread, readywrite, readyexc = select.select(readfds, writefds, excfds, 30.0) + + for fd in readyread: + webvi.api.perform(fd, WebviSelectBitmask.READ) + for fd in readywrite: + webvi.api.perform(fd, WebviSelectBitmask.WRITE) + + remaining = -1 + while remaining != 0: + finished, status, errmsg, remaining = webvi.api.pop_message() + if finished == handle: + return (status, errmsg) + + def collect_data(self, inp, inplen, dlbuffer): + """Callback that writes the downloaded data to dlbuffer. + """ + dlbuffer.write(inp) + return inplen + + def open_dest_file(self, inp, inplen, dldata): + """Initial download callback. This opens the destination file, + and reseats the callback to self.write_to_dest. The + destination file can not be opened until now, because the + stream title and final URL are not known before. + """ + title = webvi.api.get_info(dldata.handle, WebviInfo.STREAM_TITLE)[1] + contenttype = webvi.api.get_info(dldata.handle, WebviInfo.CONTENT_TYPE)[1] + contentlength = webvi.api.get_info(dldata.handle, WebviInfo.CONTENT_LENGTH)[1] + url = webvi.api.get_info(dldata.handle, WebviInfo.URL)[1] + ext = self.guess_extension(contenttype, url) + destfilename = self.next_available_file_name(safe_filename(title), ext) + + try: + destfile = open(destfilename, 'w') + except IOError, err: + print 'Failed to open the destination file %s: %s' % (destfilename, err.args[1]) + return -1 + + dldata.destfile = destfile + dldata.destfilename = destfilename + dldata.contentlength = contentlength + dldata.progress.total_bytes = contentlength + webvi.api.set_opt(dldata.handle, WebviOpt.WRITEFUNC, self.write_to_dest) + + return self.write_to_dest(inp, inplen, dldata) + + def write_to_dest(self, inp, inplen, dldata): + """Callback that writes downloaded data to self.destfile.""" + try: + dldata.destfile.write(inp) + except IOError, err: + print 'IOError while writing to %s: %s' % \ + (dldata.destfilename, err.args[1]) + return -1 + + dldata.bytes_downloaded += inplen + + dldata.progress.update(dldata.bytes_downloaded) + + return inplen + + def getmenu(self, ref): + dlbuffer = cStringIO.StringIO() + handle = webvi.api.new_request(ref, WebviRequestType.MENU) + if handle == -1: + print 'Failed to open handle' + return (-1, '', None) + + webvi.api.set_opt(handle, WebviOpt.WRITEFUNC, self.collect_data) + webvi.api.set_opt(handle, WebviOpt.WRITEDATA, dlbuffer) + webvi.api.start_handle(handle) + + status, err = self.execute_webvi(handle) + webvi.api.delete_handle(handle) + + if status != 0: + print 'Download failed:', err + return (status, err, None) + + return (status, err, self.parse_page(dlbuffer.getvalue())) + + def get_quality_params(self, videosite, streamtype): + params = [] + lim = self.quality_limits[streamtype].get(videosite, {}) + + if lim.has_key('min'): + params.append('minquality=' + lim['min']) + if lim.has_key('max'): + params.append('maxquality=' + lim['max']) + + return '&'.join(params) + + def download(self, stream): + m = re.match(r'wvt:///([^/]+)/', stream) + if m is not None: + stream += '&' + self.get_quality_params(m.group(1), 'download') + + handle = webvi.api.new_request(stream, WebviRequestType.FILE) + if handle == -1: + print 'Failed to open handle' + return False + + dldata = DownloadData(handle, sys.stdout) + + webvi.api.set_opt(handle, WebviOpt.WRITEFUNC, self.open_dest_file) + webvi.api.set_opt(handle, WebviOpt.WRITEDATA, dldata) + webvi.api.start_handle(handle) + + status, err = self.execute_webvi(handle) + if dldata.destfile is not None: + dldata.destfile.close() + + webvi.api.delete_handle(handle) + + if status not in (0, 504): + print 'Download failed:', err + return + + if dldata.contentlength != -1 and \ + dldata.bytes_downloaded != dldata.contentlength: + print 'Warning: the size of the file (%d) differs from expected (%d)' % \ + (dldata.bytes_downloaded, dldata.contentlength) + + print 'Saved to %s' % dldata.destfilename + + return True + + def play_stream(self, ref): + streamurl = self.get_stream_url(ref) + if streamurl == '': + print 'Did not find URL' + return False + + # Found url, now find a working media player + for player in self.streamplayers: + if '%s' not in player: + playcmd = player + ' ' + streamurl + else: + try: + playcmd = player % streamurl + except TypeError: + print 'Can\'t substitute URL in', player + continue + + try: + print 'Trying player: ' + playcmd + retcode = subprocess.call(playcmd, shell=True) + if retcode > 0: + print 'Player failed with returncode', retcode + else: + return True + except OSError, err: + print 'Execution failed:', err + + return False + + def get_stream_url(self, ref): + m = re.match(r'wvt:///([^/]+)/', ref) + if m is not None: + ref += '&' + self.get_quality_params(m.group(1), 'stream') + + handle = webvi.api.new_request(ref, WebviRequestType.STREAMURL) + if handle == -1: + print 'Failed to open handle' + return '' + + dlbuffer = cStringIO.StringIO() + webvi.api.set_opt(handle, WebviOpt.WRITEFUNC, self.collect_data) + webvi.api.set_opt(handle, WebviOpt.WRITEDATA, dlbuffer) + webvi.api.start_handle(handle) + status, err = self.execute_webvi(handle) + webvi.api.delete_handle(handle) + + if status != 0: + print 'Download failed:', err + return '' + + return dlbuffer.getvalue() + + def next_available_file_name(self, basename, ext): + fullname = basename + ext + if not os.path.exists(fullname): + return fullname + i = 1 + while os.path.exists('%s-%d%s' % (basename, i, ext)): + i += 1 + return '%s-%d%s' % (basename, i, ext) + + def get_current_menu(self): + if (self.history_pointer >= 0) and \ + (self.history_pointer < len(self.history)): + return self.history[self.history_pointer] + else: + return None + + def history_add(self, menupage): + if menupage is not None: + self.history = self.history[:(self.history_pointer+1)] + self.history.append(menupage) + self.history_pointer = len(self.history)-1 + + def history_back(self): + if self.history_pointer > 0: + self.history_pointer -= 1 + return self.get_current_menu() + + def history_forward(self): + if self.history_pointer < len(self.history)-1: + self.history_pointer += 1 + return self.get_current_menu() + + +class WVShell(cmd.Cmd): + def __init__(self, client, completekey='tab', stdin=None, stdout=None): + cmd.Cmd.__init__(self, completekey, stdin, stdout) + self.prompt = '> ' + self.client = client + + def preloop(self): + self.stdout.write('webvicli %s starting\n' % VERSION) + self.do_menu(None) + + def precmd(self, arg): + try: + int(arg) + return 'select ' + arg + except ValueError: + return arg + + def onecmd(self, c): + try: + return cmd.Cmd.onecmd(self, c) + except Exception: + import traceback + print 'Exception occured while handling command "' + c + '"' + print traceback.format_exc() + return False + + def emptyline(self): + pass + + def display_menu(self, menupage): + if menupage is not None: + self.stdout.write(unicode(menupage).encode(self.stdout.encoding, 'replace')) + + def _get_numbered_item(self, arg): + menupage = self.client.get_current_menu() + try: + v = int(arg)-1 + if (menupage is None) or (v < 0) or (v >= len(menupage)): + raise ValueError + except ValueError: + self.stdout.write('Invalid selection: %s\n' % arg) + return None + return menupage[v] + + def do_select(self, arg): + """select x +Select the link whose index is x. + """ + menuitem = self._get_numbered_item(arg) + if menuitem is None: + return False + ref = menuitem.activate() + if ref is not None: + status, statusmsg, menupage = self.client.getmenu(ref) + if menupage is not None: + self.client.history_add(menupage) + else: + self.stdout.write('Error: %d %s\n' % (status, statusmsg)) + else: + menupage = self.client.get_current_menu() + self.display_menu(menupage) + return False + + def do_download(self, arg): + """download x +Download media stream whose index is x to a file. Downloadable items +are the ones without brackets. + """ + menuitem = self._get_numbered_item(arg) + if menuitem is None: + return False + elif hasattr(menuitem, 'stream') and menuitem.stream is not None: + self.client.download(menuitem.stream) + else: + self.stdout.write('Not a stream\n') + return False + + def do_stream(self, arg): + """stream x +Play the media file whose index is x. Streams are the ones +without brackets. + """ + menuitem = self._get_numbered_item(arg) + if menuitem is None: + return False + elif hasattr(menuitem, 'stream') and menuitem.stream is not None: + self.client.play_stream(menuitem.stream) + else: + self.stdout.write('Not a stream\n') + return False + + def do_display(self, arg): + """Redisplay the current menu.""" + if not arg: + self.display_menu(self.client.get_current_menu()) + else: + self.stdout.write('Unknown parameter %s\n' % arg) + return False + + def do_menu(self, arg): + """Get back to the main menu.""" + status, statusmsg, menupage = self.client.getmenu('wvt:///?srcurl=mainmenu') + if menupage is not None: + self.client.history_add(menupage) + self.display_menu(menupage) + else: + self.stdout.write('Error: %d %s\n' % (status, statusmsg)) + return True + return False + + def do_back(self, arg): + """Go to the previous menu in the history.""" + menupage = self.client.history_back() + self.display_menu(menupage) + return False + + def do_forward(self, arg): + """Go to the next menu in the history.""" + menupage = self.client.history_forward() + self.display_menu(menupage) + return False + + def do_quit(self, arg): + """Quit the program.""" + return True + + def do_EOF(self, arg): + """Quit the program.""" + return True + + +def load_config(options): + """Load options from config files.""" + cfgprs = RawConfigParser() + cfgprs.read(['/etc/webvi.conf', os.path.expanduser('~/.webvi')]) + for sec in cfgprs.sections(): + if sec == 'webvi': + for opt, val in cfgprs.items('webvi'): + options[opt] = val + + elif sec.startswith('site-'): + sitename = sec[5:] + + if not options.has_key('download-limits'): + options['download-limits'] = {} + if not options.has_key('stream-limits'): + options['stream-limits'] = {} + options['download-limits'][sitename] = {} + options['stream-limits'][sitename] = {} + + for opt, val in cfgprs.items(sec): + if opt == 'download-min-quality': + options['download-limits'][sitename]['min'] = val + elif opt == 'download-max-quality': + options['download-limits'][sitename]['max'] = val + elif opt == 'stream-min-quality': + options['stream-limits'][sitename]['min'] = val + elif opt == 'stream-max-quality': + options['stream-limits'][sitename]['max'] = val + + return options + +def parse_command_line(cmdlineargs, options): + parser = OptionParser() + parser.add_option('-t', '--templatepath', type='string', + dest='templatepath', + help='read video site templates from DIR', metavar='DIR', + default=None) + cmdlineopt = parser.parse_args(cmdlineargs)[0] + + if cmdlineopt.templatepath is not None: + options['templatepath'] = cmdlineopt.templatepath + + return options + +def player_list(options): + """Return a sorted list of player commands extracted from options + dictionary.""" + # Load streamplayer items from the config file and sort them + # according to quality. + players = [] + for opt, val in options.iteritems(): + m = re.match(r'streamplayer([1-9])$', opt) + if m is not None: + players.append((int(m.group(1)), val)) + + players.sort() + ret = [] + for quality, playcmd in players: + ret.append(playcmd) + + # If the config file did not define any players use the default + # players + if not ret: + ret = list(DEFAULT_PLAYERS) + + return ret + +def main(argv): + options = load_config({}) + options = parse_command_line(argv, options) + + if options.has_key('templatepath'): + webvi.api.set_config(WebviConfig.TEMPLATE_PATH, options['templatepath']) + + shell = WVShell(WVClient(player_list(options), + options.get('download-limits', {}), + options.get('stream-limits', {}))) + shell.cmdloop() + +if __name__ == '__main__': + main([]) diff --git a/src/webvicli/webvicli/menu.py b/src/webvicli/webvicli/menu.py new file mode 100644 index 0000000..70ef6ea --- /dev/null +++ b/src/webvicli/webvicli/menu.py @@ -0,0 +1,171 @@ +# menu.py - menu elements for webvicli +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import textwrap +import urllib + +LINEWIDTH = 72 + +class Menu: + def __init__(self): + self.title = None + self.items = [] + + def __str__(self): + s = u'' + if self.title: + s = self.title + '\n' + '='*len(self.title) + '\n' + for i, item in enumerate(self.items): + if isinstance(item, MenuItemTextArea): + num = ' ' + else: + num = '%d.' % (i+1) + + s += u'%s %s\n' % (num, unicode(item).replace('\n', '\n ')) + return s + + def __getitem__(self, i): + return self.items[i] + + def __len__(self): + return len(self.items) + + def add(self, menuitem): + self.items.append(menuitem) + + +class MenuItemLink: + def __init__(self, label, ref, stream): + self.label = label + if type(ref) == unicode: + self.ref = ref.encode('utf-8') + else: + self.ref = ref + self.stream = stream + + def __str__(self): + res = self.label + if not self.stream: + res = '[' + res + ']' + return res + + def activate(self): + return self.ref + + +class MenuItemTextField: + def __init__(self, label, name): + self.label = label + self.name = name + self.value = u'' + + def __str__(self): + return u'%s: %s' % (self.label, self.value) + + def get_query(self): + return {self.name: self.value} + + def activate(self): + self.value = unicode(raw_input('%s> ' % self.label), sys.stdin.encoding) + return None + + +class MenuItemTextArea: + def __init__(self, label): + self.label = label + + def __str__(self): + return textwrap.fill(self.label, width=LINEWIDTH) + + def activate(self): + return None + + +class MenuItemList: + def __init__(self, label, name, items, values, stdout): + self.label = label + self.name = name + assert len(items) == len(values) + self.items = items + self.values = values + self.current = 0 + self.stdout = stdout + + def __str__(self): + itemstrings = [] + for i, itemname in enumerate(self.items): + if i == self.current: + itemstrings.append('<' + itemname + '>') + else: + itemstrings.append(itemname) + + lab = self.label + ': ' + return textwrap.fill(u', '.join(itemstrings), width=LINEWIDTH, + initial_indent=lab, + subsequent_indent=' '*len(lab)) + + def get_query(self): + if (self.current >= 0) and (self.current < len(self.items)): + return {self.name: self.values[self.current]} + else: + return {} + + def activate(self): + itemstrings = [] + for i, itemname in enumerate(self.items): + itemstrings.append('%d. %s' % (i+1, itemname)) + + self.stdout.write(u'\n'.join(itemstrings).encode(self.stdout.encoding, 'replace')) + self.stdout.write('\n') + + tmp = raw_input('Select item (1-%d)> ' % len(self.items)) + try: + i = int(tmp) + if (i < 1) or (i > len(self.items)): + raise ValueError + self.current = i-1 + except ValueError: + self.stdout.write('Must be an integer in the range 1 - %d\n' % len(self.items)) + return None + + +class MenuItemSubmitButton: + def __init__(self, label, baseurl, subitems): + self.label = label + if type(baseurl) == unicode: + self.baseurl = baseurl.encode('utf-8') + else: + self.baseurl = baseurl + self.subitems = subitems + + def __str__(self): + return '[' + self.label + ']' + + def activate(self): + baseurl = self.baseurl + if baseurl.find('?') == -1: + baseurl += '?' + else: + baseurl += '&' + + parts = [] + for sub in self.subitems: + for key, val in sub.get_query().iteritems(): + parts.append('subst=' + urllib.quote_plus(key.encode('utf-8')) + ',' + urllib.quote_plus(val.encode('utf-8'))) + + return baseurl + '&'.join(parts) |