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