diff options
author | Antti Ajanki <antti.ajanki@iki.fi> | 2013-08-09 21:39:11 +0300 |
---|---|---|
committer | Antti Ajanki <antti.ajanki@iki.fi> | 2013-08-09 21:39:11 +0300 |
commit | 451f691d367a46f365101f39cc093ce2feaacd13 (patch) | |
tree | 6ac88c6ecf46755c4826719f58a2cf00ac9ecb8c | |
parent | 2d4d55cfedccfa80d283592af349e93d0968f58e (diff) | |
download | vdr-plugin-webvideo-451f691d367a46f365101f39cc093ce2feaacd13.tar.gz vdr-plugin-webvideo-451f691d367a46f365101f39cc093ce2feaacd13.tar.bz2 |
Import vdr-plugin from the master branch
36 files changed, 7110 insertions, 0 deletions
diff --git a/src/vdr-plugin/Makefile b/src/vdr-plugin/Makefile new file mode 100644 index 0000000..0fcaffe --- /dev/null +++ b/src/vdr-plugin/Makefile @@ -0,0 +1,126 @@ +# +# 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. + +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 directory environment: + +# Use package data if installed...otherwise assume we're under the VDR source directory: +PKGCFG = $(if $(VDRDIR),$(shell pkg-config --variable=$(1) $(VDRDIR)/vdr.pc),$(shell pkg-config --variable=$(1) vdr || pkg-config --variable=$(1) ../../../../../vdr.pc)) +LIBDIR = $(call PKGCFG,libdir) +LOCDIR = $(call PKGCFG,locdir) +PLGCFG = $(call PKGCFG,plgcfg) +# +TMPDIR ?= /tmp + +### The compiler options: + +override CFLAGS += $(call PKGCFG,cflags) $(shell xml2-config --cflags) +override CXXFLAGS += $(call PKGCFG,cxxflags) $(shell xml2-config --cflags) +override LDFLAGS += $(shell xml2-config --libs) -L../libwebvi -lwebvi + +export CFLAGS +export CXXFLAGS + +### The version number of VDR's plugin API: + +APIVERSION = $(call PKGCFG,apiversion) + +### Allow user defined options to overwrite defaults: + +-include $(PLGCFG) + +### The name of the distribution archive: + +ARCHIVE = $(PLUGIN)-$(VERSION) +PACKAGE = vdr-$(ARCHIVE) + +### The name of the shared object file: + +SOFILE = libvdr-$(PLUGIN).so + +### Includes and Defines (add further entries here): + +INCLUDES += -I../libwebvi + +DEFINES += -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: $(SOFILE) i18n + +### Implicit rules: + +%.o: %.c + $(CXX) $(CXXFLAGS) -c $(DEFINES) $(INCLUDES) -o $@ $< + +### Dependencies: + +MAKEDEP = $(CXX) -MM -MG +DEPFILE = .dependencies +$(DEPFILE): Makefile + @$(MAKEDEP) $(CXXFLAGS) $(DEFINES) $(INCLUDES) $(OBJS:%.o=%.c) > $@ + +-include $(DEPFILE) + +### Internationalization (I18N): + +PODIR = po +I18Npo = $(wildcard $(PODIR)/*.po) +I18Nmo = $(addsuffix .mo, $(foreach file, $(I18Npo), $(basename $(file)))) +I18Nmsgs = $(addprefix $(DESTDIR)$(LOCDIR)/, $(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 --package-name=vdr-$(PLUGIN) --package-version=$(VERSION) --msgid-bugs-address='<see README>' -o $@ `ls $^` + +%.po: $(I18Npot) + msgmerge -U --no-wrap --no-location --backup=none -q -N $@ $< + @touch $@ + +$(I18Nmsgs): $(DESTDIR)$(LOCDIR)/%/LC_MESSAGES/vdr-$(PLUGIN).mo: $(PODIR)/%.mo + install -D -m644 $< $@ + +.PHONY: i18n +i18n: $(I18Nmo) $(I18Npot) + +install-i18n: $(I18Nmsgs) + +### Targets: + +$(SOFILE): $(OBJS) + $(CXX) $(CXXFLAGS) -shared $(OBJS) $(LDFLAGS) -o $@ + +install-lib: $(SOFILE) + install -D $^ $(DESTDIR)$(LIBDIR)/$^.$(APIVERSION) + +install: install-lib install-i18n + +dist: $(I18Npo) clean + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @mkdir $(TMPDIR)/$(ARCHIVE) + @cp -a * $(TMPDIR)/$(ARCHIVE) + @tar czf $(PACKAGE).tgz -C $(TMPDIR) --exclude debian --exclude CVS --exclude .svn $(ARCHIVE) + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @echo Distribution package created as $(PACKAGE).tgz + +clean: + @-rm -f $(PODIR)/*.mo $(PODIR)/*.pot + @-rm -f $(OBJS) $(DEPFILE) *.so *.so.* *.tgz core* *~ 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..2792d24 --- /dev/null +++ b/src/vdr-plugin/common.c @@ -0,0 +1,261 @@ +/* + * 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; +} + +cString parseDomain(const char *url) { + const char *schemesep = strstr(url, "://"); + if (!schemesep) + return ""; + + const char *domainstart = schemesep+3; + const char *domainend = strchr(domainstart, '/'); + + int len = domainend-domainstart; + char *domain = (char *)malloc((len+1)*sizeof(char)); + strncpy(domain, domainstart, len); + domain[len] = '\0'; + + const char *user = strchr(domain, '@'); + if (user) { + len -= user+1 - domain; + memmove(domain, user+1, len+1); + } + + const char *port = strchr(domain, ':'); + if (port) { + len = port - domain; + domain[len] = '\0'; + } + + strlower(domain); + + return cString(domain, true); +} + +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' + }; + + if (!s) + return NULL; + + 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, bool vfatnames) { + if (filename) { + strreplace(filename, '/', '_'); + + if (vfatnames) { + strreplace(filename, '\\', '_'); + strreplace(filename, '"', '_'); + strreplace(filename, '*', '_'); + strreplace(filename, ':', '_'); + strreplace(filename, '<', '_'); + strreplace(filename, '>', '_'); + strreplace(filename, '?', '_'); + strreplace(filename, '|', '_'); + } + + char *p = filename; + while ((*p == '.') || isspace(*p)) { + p++; + } + + if (p != filename) { + memmove(filename, p, strlen(p)+1); + } + } + + return filename; +} + +cString shellEscape(const char *s) { + char *buffer = (char *)malloc((4*strlen(s)+3)*sizeof(char)); + const char *src = s; + char *dst = buffer; + + *dst++ = '\''; + while (*src) { + if (*src == '\'') { + *dst++ = '\''; + *dst++ = '\\'; + *dst++ = '\''; + *dst++ = '\''; + src++; + } else { + *dst++ = *src++; + } + } + *dst++ = '\''; + *dst = '\0'; + + return cString(buffer, true); +} + +char *strlower(char *s) { + if (!s) return NULL; + + char *p = s; + while (*p) { + *p = tolower(*p); + p++; + } + + return s; +} diff --git a/src/vdr-plugin/common.h b/src/vdr-plugin/common.h new file mode 100644 index 0000000..8443081 --- /dev/null +++ b/src/vdr-plugin/common.h @@ -0,0 +1,50 @@ +/* + * 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); +// Return the domain part from url. +cString parseDomain(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. Replace '/' with '_' and +// remove dots from the beginning. If vfatnames is true, replace also +// other characters which are not allowed on VFAT. The string is +// modified in-place, i.e. returns the pointer filename that was +// passed as argument. +char *safeFilename(char *filename, bool vfatnames); +// Escape s so that it can be passed as parameter to a shell command. +cString shellEscape(const char *s); +// Convert string s to lower case +char *strlower(char *s); + +#endif // __WEBVIDEO_COMMON_H diff --git a/src/vdr-plugin/config.c b/src/vdr-plugin/config.c new file mode 100644 index 0000000..1fa62c8 --- /dev/null +++ b/src/vdr-plugin/config.c @@ -0,0 +1,249 @@ +/* + * 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; + postProcessCmd = NULL; + vfatNames = false; +} + +cWebvideoConfig::~cWebvideoConfig() { + if (downloadPath) + free(downloadPath); + if (templatePath) + free(templatePath); + if (postProcessCmd) + free(postProcessCmd); +} + +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); + } + + const char *vfat = iniparser_getstring(conf, "webvi:vfat", NULL); + if (vfat) { + debug("vfat = %s (from %s)", vfat, inifile); + + if (strcmp(vfat, "1") == 0 || + strcmp(vfat, "true") == 0 || + strcmp(vfat, "yes") == 0 || + strcmp(vfat, "on") == 0) + { + vfatNames = true; + } else if (strcmp(vfat, "0") == 0 || + strcmp(vfat, "false") == 0 || + strcmp(vfat, "no") == 0 || + strcmp(vfat, "off") == 0) + { + vfatNames = false; + } else { + warning("Invalid value for config option vfat: %s in %s", vfat, inifile); + } + } + + for (int i=0; i<iniparser_getnsec(conf); i++) { + const char *section = iniparser_getsecname(conf, i); + + if (strcmp(section, "webvi") != 0) { + const int maxsectionlen = 100; + char key[128]; + char *keyname; + const char *sitename; + + cString domain = parseDomain(section); + if (strlen(domain) == 0) + sitename = section; + else + sitename = domain; + + 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); +} + +void cWebvideoConfig::SetPostProcessCmd(const char *cmd) { + if (postProcessCmd) + free(postProcessCmd); + postProcessCmd = cmd ? strdup(cmd) : NULL; +} + +const char *cWebvideoConfig::GetPostProcessCmd() { + return postProcessCmd; +} + +void cWebvideoConfig::SetUseVFATNames(bool vfat) { + vfatNames = vfat; +} + +bool cWebvideoConfig::GetUseVFATNames() { + return vfatNames; +} + diff --git a/src/vdr-plugin/config.h b/src/vdr-plugin/config.h new file mode 100644 index 0000000..ccda574 --- /dev/null +++ b/src/vdr-plugin/config.h @@ -0,0 +1,72 @@ +/* + * 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; + char *postProcessCmd; + bool preferXine; + bool vfatNames; + 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(); + + void SetUseVFATNames(bool vfat); + bool GetUseVFATNames(); + + const char *GetMinQuality(const char *site, eRequestType type); + const char *GetMaxQuality(const char *site, eRequestType type); + + void SetPostProcessCmd(const char *cmd); + const char *GetPostProcessCmd(); +}; + +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..4b7a971 --- /dev/null +++ b/src/vdr-plugin/download.c @@ -0,0 +1,302 @@ +/* + * 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" + +static void diff_timeval(struct timeval *a, struct timeval *b, + struct timeval *result) { + long usec_diff = a->tv_usec - b->tv_usec; + result->tv_sec = a->tv_sec - b->tv_sec; + + while (usec_diff < 0) { + usec_diff += 1000000; + result->tv_sec -= 1; + } + + result->tv_usec = usec_diff; +} + +// --- 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); + timerActive = false; + + webvi = webvi_initialize_context(); + if (webvi != 0) { + webvi_set_config(webvi, WEBVI_CONFIG_TIMEOUT_DATA, this); + webvi_set_config(webvi, WEBVI_CONFIG_TIMEOUT_CALLBACK, UpdateTimeout); + } +} + +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); + } +} + +void cWebviThread::UpdateTimeout(long timeout, void *instance) { + cWebviThread *self = (cWebviThread *)instance; + if (!self) + return; + + if (timeout < 0) { + self->timerActive = false; + } else { + struct timeval now; + long alrm; + gettimeofday(&now, NULL); + alrm = timeout + now.tv_usec/1000; + self->timer.tv_sec = now.tv_sec + alrm/1000; + self->timer.tv_usec = (alrm % 1000) * 1000; + self->timerActive = true; + } +} + +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 { + 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) { + req->RequestDone(donemsg->status_code, donemsg->data); + if (req->IsFinished()) + 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, s; + struct timeval timeout, now; + long running_handles; + bool check_done = false; + bool has_request_files = 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; + + has_request_files = false; + requestMutex.Lock(); + for (int i=0; i<activeRequestList.Size(); i++) { + int fd = activeRequestList[i]->File(); + if (fd != -1) { + FD_SET(fd, &readfds); + if (fd > maxfd) + maxfd = fd; + has_request_files = true; + } + } + requestMutex.Unlock(); + + if (!timerActive) { + timeout.tv_sec = 60; + timeout.tv_usec = 0; + } else { + gettimeofday(&now, NULL); + diff_timeval(&timer, &now, &timeout); + if (timeout.tv_sec < 0 || timeout.tv_usec < 0) { + timeout.tv_sec = 0; + timeout.tv_usec = 0; + } + } + + 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 + timerActive = false; + webvi_perform(webvi, WEBVI_SELECT_TIMEOUT, WEBVI_SELECT_CHECK, &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 { + cMenuRequest *match = NULL; + + if (has_request_files) { + requestMutex.Lock(); + for (int i=0; i<activeRequestList.Size(); i++) { + if (fd == activeRequestList[i]->File()) { + match = activeRequestList[i]; + break; + } + } + requestMutex.Unlock(); + + // call Read() after releasing the mutex + if (match) { + match->Read(); + if (match->IsFinished()) + MoveToFinishedList(match); + } + } + + if (!match) { + 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..14a5c66 --- /dev/null +++ b/src/vdr-plugin/download.h @@ -0,0 +1,63 @@ +/* + * 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 <sys/time.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; + bool timerActive; + struct timeval timer; + + void MoveToFinishedList(cMenuRequest *req); + void ActivateNewRequest(); + void StopFinishedRequests(); + +protected: + void Action(void); + static void UpdateTimeout(long timeout, void *data); + +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..fe444ca --- /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 char *encoding) const { + if (i < 0 && i >= editData.Size()) + return NULL; + else + return editData[i]->GetQueryFragment(encoding); +} + +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..a999098 --- /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 char *encoding) 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..d03c61d --- /dev/null +++ b/src/vdr-plugin/menu.c @@ -0,0 +1,680 @@ +/* + * 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(); + xmlChar *encoding = 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 "submission")) { + if (submission) + xmlFree(submission); + submission = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + + xmlChar *enc = xmlGetProp(node, BAD_CAST "encoding"); + if (enc) { + if (encoding) + xmlFree(encoding); + encoding = enc; + } + } + node = node->next; + } + if (!itemtitle) + itemtitle = xmlCharStrdup("???"); + + cSubmissionButtonData *data = \ + new cSubmissionButtonData((char *)submission, curhistpage, + (char *)encoding); + 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); + if (encoding) + xmlFree(encoding); +} + +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, + 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..6a79f4e --- /dev/null +++ b/src/vdr-plugin/menu_timer.c @@ -0,0 +1,149 @@ +/* + * 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[] = {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)); + } + + const char *active =""; + if (timer.Running()) + active = " (active)"; + + cString lastUpdated = cString::sprintf("%s\t%s%s", tr("Last fetched:"), lastTime, active); + 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..6fc899c --- /dev/null +++ b/src/vdr-plugin/menudata.c @@ -0,0 +1,200 @@ +/* + * 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 *encoding) { + const char *name = GetName(); + char *val; + + if (name && *name && valuebuffer) { + if (encoding) { + cCharSetConv charsetconv = cCharSetConv("UTF-8", encoding); + val = URLencode(charsetconv.Convert(valuebuffer)); + } else { + val = URLencode(valuebuffer); + } + + cString tmp = cString::sprintf("%s,%s", name, val); + free(val); + 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 *encoding) { + const char *name = GetName(); + char *val; + + if (name && *name) { + if (encoding) { + cCharSetConv charsetconv = cCharSetConv("UTF-8", encoding); + val = URLencode(charsetconv.Convert(stringvalues[value])); + } else { + val = URLencode(stringvalues[value]); + } + + cString tmp = cString::sprintf("%s,%s", name, val); + free(val); + 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, + const char *enc) +{ + querybase = queryUrl ? strdup(queryUrl) : NULL; + page = currentPage; + encoding = enc ? strdup(enc) : NULL; +} + +cSubmissionButtonData::~cSubmissionButtonData() { + if (querybase) + free(querybase); + if (encoding) + free(encoding); + // 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, encoding); + if (parameter) { + size_t len = strlen(querystr) + strlen(parameter) + 8; + querystr = (char *)realloc(querystr, len*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..98e5915 --- /dev/null +++ b/src/vdr-plugin/menudata.h @@ -0,0 +1,102 @@ +/* + * 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(const char *encoding) = 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(const char *encoding); + 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(const char *encoding); + char **GetStrings(); + char **GetStringValues(); + int GetNumStrings(); + int *GetValuePtr(); +}; + +// --- cSubmissionButtonData ----------------------------------------------- + +class cHistoryObject; + +class cSubmissionButtonData : public cLinkBase { +private: + char *querybase; + const cHistoryObject *page; + char *encoding; +public: + cSubmissionButtonData(const char *queryUrl, + const cHistoryObject *currentPage, + const char *encoding); + 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..5e76948 --- /dev/null +++ b/src/vdr-plugin/player.c @@ -0,0 +1,83 @@ +/* + * 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); + + char *url2; + if (strncmp(url, "file://", 7) == 0) { + url2 = (char *)malloc(strlen(url)+1); + strcpy(url2, "fifo://"); + strcat(url2, url+7); + } else { + url2 = strdup(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(url2); + debug("decoded = %s", decoded); + bool ret = cPluginManager::CallFirstService("MediaPlayer-1.0", (void *)decoded); + free(decoded); + free(url2); + 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..a33d3e3 --- /dev/null +++ b/src/vdr-plugin/po/de_DE.po @@ -0,0 +1,140 @@ +# 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-11-18 20:19+0200\n" +"PO-Revision-Date: 2009-02-18 20:04+0200\n" +"Last-Translator: <cnc@gmx.de>\n" +"Language-Team: German\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\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 "Streaming not supported, try downloading" +msgstr "" + +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..351929d --- /dev/null +++ b/src/vdr-plugin/po/fi_FI.po @@ -0,0 +1,140 @@ +# 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-11-15 19:23+0200\n" +"PO-Revision-Date: 2008-06-07 18:03+0300\n" +"Last-Translator: Antti Ajanki <antti.ajanki@iki.fi>\n" +"Language-Team: Finnish\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\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 "Streaming not supported, try downloading" +msgstr "Toisto ei tuettu, kokeile lataamista" + +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..c4176a6 --- /dev/null +++ b/src/vdr-plugin/po/fr_FR.po @@ -0,0 +1,159 @@ +# 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-11-18 20:19+0200\n" +"PO-Revision-Date: 2008-09-08 20:34+0100\n" +"Last-Translator: Bruno ROUSSEL <bruno.roussel@free.fr>\n" +"Language-Team: French\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\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 "Streaming not supported, try downloading" +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 100755 index 0000000..809fc2f --- /dev/null +++ b/src/vdr-plugin/po/it_IT.po @@ -0,0 +1,156 @@ +# 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-11-18 20:19+0200\n" +"PO-Revision-Date: 2010-12-30 22:00+0100\n" +"Last-Translator: Diego Pierotto <vdr-italian@tiscali.it>\n" +"Language-Team: Italian\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\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 "Nessun flusso in questa pagina, create comunque il timer?" + +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 "Crea nuovo timer" + +msgid "Play" +msgstr "Riproduci" + +msgid "Status" +msgstr "Stato" + +msgid "Timers" +msgstr "Timer" + +msgid "Unfinished downloads" +msgstr "Scaricamenti non completati" + +msgid "No active downloads" +msgstr "Nessun scaricamento attivo" + +#. TRANSLATORS: at most 5 characters +msgid "Error" +msgstr "Errore" + +msgid "Error details" +msgstr "Dettagli errore" + +msgid "Download details" +msgstr "Dettagli scaricamento" + +msgid "Remove" +msgstr "Elimina" + +msgid "Abort" +msgstr "Annulla" + +msgid "Edit timer" +msgstr "Modifica timer" + +msgid "Title" +msgstr "Titolo" + +msgid "Once per day" +msgstr "Una volta al giorno" + +msgid "Once per week" +msgstr "Una volta alla settimana" + +msgid "Once per month" +msgstr "Una volta al mese" + +msgid "Update interval" +msgstr "Intervallo di aggiornamento" + +msgid "Execute now" +msgstr "Riproduci ora" + +#. TRANSLATORS: at most 24 chars +msgid "Never" +msgstr "Mai" + +msgid "Last fetched:" +msgstr "Ultima analisi:" + +msgid "Error on last refresh!" +msgstr "Ultimo aggiornamento fallito!" + +msgid "Timer running, can't remove" +msgstr "Timer in esecuzione, impossibile eliminare" + +msgid "Remove timer?" +msgstr "Eliminare il timer?" + +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 "Streaming not supported, try downloading" +msgstr "Trasmissione non supportata, provare scaricando" + +msgid "Failed to launch media player" +msgstr "Impossibile avviare il lettore multimediale" + +msgid "timer" +msgstr "timer" + +#, 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/po/uk_UA.po b/src/vdr-plugin/po/uk_UA.po new file mode 100644 index 0000000..a2f4db4 --- /dev/null +++ b/src/vdr-plugin/po/uk_UA.po @@ -0,0 +1,134 @@ +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.5.0\n" +"Report-Msgid-Bugs-To: <see README>\n" +"POT-Creation-Date: 2013-05-19 00:52+0200\n" +"PO-Revision-Date: 2013-05-19 00:42+0200\n" +"Last-Translator: Yarema aka Knedlyk <yupadmin@gmail.com>\n" +"Language-Team: <vdr@linuxtv.org>\n" +"Language: Ukrainian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "No streams on this page, create timer anyway?" +msgstr "Немає потоку на цій сторінці, створити таймер все одно?" + +msgid "Downloading in the background" +msgstr "Звантаження в тлі" + +msgid "Starting player..." +msgstr "Запуск програвача..." + +msgid "Retrieving..." +msgstr "Отримання..." + +msgid "Back" +msgstr "Назад" + +msgid "Forward" +msgstr "Вперед" + +msgid "Create timer" +msgstr "Ств. таймер" + +msgid "Play" +msgstr "Програвання" + +msgid "Status" +msgstr "Статус" + +msgid "Timers" +msgstr "Таймери" + +msgid "Unfinished downloads" +msgstr "Незавершені звантаження" + +msgid "No active downloads" +msgstr "Немає активних звантажень" + +#. TRANSLATORS: at most 5 characters +msgid "Error" +msgstr "Помилка" + +msgid "Error details" +msgstr "Деталі помилки" + +msgid "Download details" +msgstr "Деталі звантаження" + +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 "Звантаження відео-файлів з мережі" + +msgid "Streaming failed: no URL" +msgstr "Програвання потоку не вдалося: немає посилання" + +msgid "Streaming not supported, try downloading" +msgstr "Програвання потоку не підтримується, спробуйте звантажити" + +msgid "Failed to launch media player" +msgstr "Помилка запуску програвача медіа" + +msgid "timer" +msgstr "таймер" + +#, c-format +msgid "One download completed, %d remains%s" +msgstr "Одне звантаження закінчено, %d залишилося%s" + +msgid "Download aborted" +msgstr "Звантаження перервано" + +#, c-format +msgid "Download failed (error = %d)" +msgstr "Звантаження не вдалося (помилка = %d)" + +#, c-format +msgid "%d downloads not finished" +msgstr "%d звантажень не завершено" diff --git a/src/vdr-plugin/request.c b/src/vdr-plugin/request.c new file mode 100644 index 0000000..0d1abf1 --- /dev/null +++ b/src/vdr-plugin/request.c @@ -0,0 +1,530 @@ +/* + * 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 <stdio.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) { + debug("starting request %d", reqID); + + 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) { + debug("RequestDone %d %s", errorcode, (const char *)pharse); + + finished = true; + status = errorcode; + statusPharse = pharse; +} + +void cMenuRequest::Abort() { + if (aborted || 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, + cDownloadProgress *progress) +: cMenuRequest(ID, streamref), title(NULL), bytesDownloaded(0), + contentLength(-1), destfile(NULL), destfilename(NULL), + progressUpdater(progress), state(STATE_WEBVI) +{ + if (progressUpdater) + progressUpdater->AssociateWith(this); + + AppendQualityParamsToRef(); +} + +cFileDownloadRequest::~cFileDownloadRequest() { + if (destfile) { + destfile->Close(); + delete destfile; + } + if (destfilename) + free(destfilename); + 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 filename; + 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); + + const char *destdir = webvideoConfig->GetDownloadPath(); + char *basename = strdup(title ? title : "???"); + basename = safeFilename(basename, webvideoConfig->GetUseVFATNames()); + + i = 1; + filename = cString::sprintf("%s/%s%s", destdir, basename, ext); + while (true) { + debug("trying to open %s", (const char *)filename); + + fd = destfile->Open(filename, O_WRONLY | O_CREAT | O_EXCL, DEFFILEMODE); + + if (fd == -1 && errno == EEXIST) + filename = 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 *)filename); + delete destfile; + destfile = NULL; + return false; + } + + if (destfilename) + free(destfilename); + destfilename = strdup(filename); + info("Saving to %s", 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) { + if (state == STATE_WEBVI) { + if (destfile) + destfile->Close(); + + if (errorcode == 0) + StartPostProcessing(); + else + state = STATE_FINISHED; + + } else if (state == STATE_POSTPROCESS) { + postProcessPipe.Close(); + state = STATE_FINISHED; + } + + if (state == STATE_FINISHED) { + cMenuRequest::RequestDone(errorcode, pharse); + if (progressUpdater) + progressUpdater->MarkDone(errorcode, pharse); + } +} + +void cFileDownloadRequest::Abort() { + if (state == STATE_POSTPROCESS) + postProcessPipe.Close(); + + cMenuRequest::Abort(); +} + +void cFileDownloadRequest::StartPostProcessing() { + state = STATE_POSTPROCESS; + + const char *script = webvideoConfig->GetPostProcessCmd(); + if (!script || !destfilename) { + state = STATE_FINISHED; + return; + } + + info("post-processing %s", destfilename); + + cString cmd = cString::sprintf("%s %s", + (const char *)shellEscape(script), + (const char *)shellEscape(destfilename)); + debug("executing %s", (const char *)cmd); + + if (!postProcessPipe.Open(cmd, "r")) { + state = STATE_FINISHED; + return; + } + + int flags = fcntl(fileno(postProcessPipe), F_GETFL, 0); + flags |= O_NONBLOCK; + fcntl(fileno(postProcessPipe), F_SETFL, flags); +} + +int cFileDownloadRequest::File() { + FILE *f = postProcessPipe; + + if (f) + return fileno(f); + else + return -1; +} + +bool cFileDownloadRequest::Read() { + const size_t BUF_LEN = 512; + char buf[BUF_LEN]; + + if (!(FILE *)postProcessPipe) + return false; + + while (true) { + ssize_t nbytes = read(fileno(postProcessPipe), buf, BUF_LEN); + + if (nbytes < 0) { + if (errno != EAGAIN && errno != EINTR) { + LOG_ERROR_STR("post process pipe"); + return false; + } + } else if (nbytes == 0) { + info("post-processing of %s finished", destfilename); + + if (IsAborted()) + RequestDone(-2, "Aborted"); + else + RequestDone(0, ""); + + return true; + } else { + debug("pp: %.*s", nbytes, buf); + + if (nbytes < (ssize_t)BUF_LEN) + return true; + } + } + + return true; +} + +// --- 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..4a37741 --- /dev/null +++ b/src/vdr-plugin/request.h @@ -0,0 +1,181 @@ +/* + * 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; } + virtual 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 response message + virtual cString GetResponse(); + + void SetTimer(cWebviTimer *t) { timer = t; } + cWebviTimer *GetTimer() { return timer; } + + virtual int File() { return -1; } + virtual bool Read() { return true; } +}; + +// --- cFileDownloadRequest ------------------------------------------------ + +class cFileDownloadRequest : public cMenuRequest { +private: + enum eDownloadState { STATE_WEBVI, STATE_POSTPROCESS, STATE_FINISHED }; + + char *title; + long bytesDownloaded; + long contentLength; + cUnbufferedFile *destfile; + char *destfilename; + cDownloadProgress *progressUpdater; + cPipe postProcessPipe; + eDownloadState state; + +protected: + virtual WebviHandle PrepareHandle(); + virtual ssize_t WriteData(const char *ptr, size_t len); + bool OpenDestFile(); + char *GetExtension(const char *contentType, const char *url); + void StartPostProcessing(); + +public: + cFileDownloadRequest(int ID, const char *streamref, + cDownloadProgress *progress); + virtual ~cFileDownloadRequest(); + + eRequestType GetType() { return REQT_FILE; } + void RequestDone(int errorcode, cString pharse); + void Abort(); + + int File(); + bool Read(); +}; + +// --- 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..6346763 --- /dev/null +++ b/src/vdr-plugin/timer.c @@ -0,0 +1,519 @@ +/* + * 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(NULL); + 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, + summaries.NewDownload()); + req->SetTimer(this); + cWebviThread::Instance().AddRequest(req); + } + + xmlFree(streamref); + } + + node2 = node2->next; + } + } + + node = node->next; + } + + xmlFreeDoc(doc); + + if (activeStreams.Size() == 0) { + 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 (!errmsg && 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), convertTemplatePaths(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 && + strncmp(ver, "# WVTIMER1/", 11) != 0) { + error("Can't load timers. Unknown format: %s", ver); + disableSaving = true; + return; + } + + convertTemplatePaths = (strcmp(ver, "# WVTIMER1") == 0); + + 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; + } + + if (convertTemplatePaths) { + char *newref = UpgradedTemplatePath(ref); + if (newref) { + free(ref); + ref = newref; + } + } + + 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, const char *version) { + // Format: space separated field in this order: + // lastUpdate interval lastSucceeded reference title + + fprintf(f, "# WVTIMER1/%s\n", version); + + 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; + } + } +} + +char *cWebviTimerManager::UpgradedTemplatePath(char *ref) { + // template names changed in 0.4.0 + const char *templateNameMap[10][2] = \ + {{"wvt:///youtube/", "wvt:///www.youtube.com/"}, + {"wvt:///svtplay/", "wvt:///svtplay.se/"}, + {"wvt:///moontv/", "wvt:///moontv.fi/"}, + {"wvt:///metacafe/", "wvt:///www.metacafe.com/"}, + {"wvt:///vimeo/", "wvt:///www.vimeo.com/"}, + {"wvt:///katsomo/", "wvt:///www.katsomo.fi/"}, + {"wvt:///ruutufi/", "wvt:///www.ruutu.fi/"}, + {"wvt:///google/", "wvt:///video.google.com/"}, + {"wvt:///yleareena/", "wvt:///areena.yle.fi/"}}; + + for (int i=0; i<10; i++) { + int oldlen = strlen(templateNameMap[i][0]); + if (strncmp(ref, templateNameMap[i][0], oldlen) == 0) { + int newlen = strlen(templateNameMap[i][1]) + strlen(ref); + char *newref = (char *)malloc((newlen+1)*sizeof(char)); + strcpy(newref, templateNameMap[i][1]); + strcat(newref, ref+oldlen); + return newref; + } + } + + return NULL; +} + +void cWebviTimerManager::ConvertTimerHistoryTemplates() { + for (int i=0; i<refHistory.Size(); i++) { + char *oldref = refHistory[i]; + char *newref = UpgradedTemplatePath(oldref); + if (!newref) + continue; + + refHistory[i] = newref; + free(oldref); + } + + modified = true; +} + +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; + } + + if (convertTemplatePaths) + ConvertTimerHistoryTemplates(); + + return ok; +} + +bool cWebviTimerManager::Save(const char *path, const char *version) { + 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, version); + 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() { + cWebviTimer *timer = timers.First(); + if (!timer) + return; + + time_t now = time(NULL); + +#ifdef DEBUG + char timestr[25]; + + 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..369fbce --- /dev/null +++ b/src/vdr-plugin/timer.h @@ -0,0 +1,115 @@ +/* + * 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; + bool convertTemplatePaths; + + 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, const char *version); + void SaveHistory(FILE *f); + + char *UpgradedTemplatePath(char *ref); + void ConvertTimerHistoryTemplates(); + +public: + static cWebviTimerManager &Instance(); + + bool Load(const char *path); + bool Save(const char *path, const char *version); + + 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..4f3cf68 --- /dev/null +++ b/src/vdr-plugin/webvideo.c @@ -0,0 +1,516 @@ +/* + * 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.5.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; + cString postprocesscmd; + bool prefermplayer; + bool vfatnames; + + 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); + cString CreateWvtRef(const char *url); + +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! + prefermplayer = false; + vfatnames = false; +} + +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" \ + " -p CMD, --postprocess=CMD Execute CMD after downloading\n" \ + " --vfat Generate Windows compatible filenames\n" \ + " -m, --prefermplayer Prefer mplayer over xineliboutput when streaming\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' }, + { "postprocess", required_argument, NULL, 'p' }, + { "prefermplayer", no_argument, NULL, 'm' }, + { "vfat", no_argument, NULL, 'v' }, + { NULL } + }; + + int c; + while ((c = getopt_long(argc, argv, "d:t:c:p:mv", long_options, NULL)) != -1) { + switch (c) { + case 'd': + destdir = cString(optarg); + break; + case 't': + templatedir = cString(optarg); + break; + case 'c': + conffile = cString(optarg); + break; + case 'p': + postprocesscmd = cString(optarg); + break; + case 'm': + prefermplayer = true; + break; + case 'v': + vfatnames = true; + 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) + webvideoConfig->SetDownloadPath(cString(VideoDirectory)); + if ((const char *)conffile == NULL) + conffile = AddDirectory(ConfigDirectory(Name()), "webvi.plugin.conf"); + + webvideoConfig->ReadConfigFile(conffile); + + if ((const char *)destdir) + webvideoConfig->SetDownloadPath(destdir); + if ((const char *)templatedir) + webvideoConfig->SetTemplatePath(templatedir); + if ((const char *)postprocesscmd) + webvideoConfig->SetPostProcessCmd(postprocesscmd); + if (prefermplayer) + webvideoConfig->SetPreferXineliboutput(false); + if (vfatnames) + webvideoConfig->SetUseVFATNames(vfatnames); + + 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()), Version()); + + xmlCleanupParser(); +} + +void cPluginWebvideo::Housekeeping(void) +{ + // Perform any cleanup or other regular tasks. + + cWebviTimerManager::Instance().Save(ConfigDirectory(Name()), Version()); +} + +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 (strncmp(streamurl, "wvt://", 6) == 0) + Skins.Message(mtError, tr("Streaming not supported, try downloading")); + 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) +{ + static const char *HelpPages[] = { + "PLAY <url>\n" + " Stream a media file embedded on web page at <url>.", + "DWLD <url>\n" + " Download a media file embedded on web page at <url>.", + NULL + }; + return HelpPages; +} + +cString cPluginWebvideo::SVDRPCommand(const char *Command, const char *Option, int &ReplyCode) +{ + if(strcasecmp(Command, "PLAY") == 0 || strcasecmp(Command, "DWLD") == 0) { + if(*Option) { + debug("SVDRP(%s, %s)", Command, Option); + cString twvtref = CreateWvtRef(Option); + if (strcmp(twvtref, "") != 0) { + cMenuRequest *req; + if (strcasecmp(Command, "PLAY") == 0) + req = new cStreamUrlRequest(0, twvtref); + else + req = new cFileDownloadRequest(0, twvtref, summaries.NewDownload()); + cWebviThread::Instance().AddRequest(req); + ReplyCode = 250; // Ok + return cString("Downloading video file"); + } else { + ReplyCode = 550; // Requested action not taken + return cString("Unable to parse URL"); + } + } else { + ReplyCode = 550; // Requested action not taken + return cString("File name missing"); + } + } + + return NULL; +} + +cString cPluginWebvideo::CreateWvtRef(const char *url) { + cString domain = parseDomain(url); + if (strcmp(domain, "") == 0) + return ""; + + char *encoded = URLencode(url); + cString res = cString::sprintf("wvt:///%s/videopage.xsl?srcurl=%s", + (const char *)domain, encoded); + free(encoded); + return res; +} + +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! |