diff --git a/webui/extjs.c b/webui/extjs.c
index 7b09a3ea..d0304e4a 100644
--- a/webui/extjs.c
+++ b/webui/extjs.c
@@ -113,6 +113,7 @@ extjs_root(http_connection_t *hc, const char *remain, void *opaque)
extjs_load(hq, "static/app/chconf.js");
extjs_load(hq, "static/app/epg.js");
extjs_load(hq, "static/app/dvr.js");
+ extjs_load(hq, "static/app/xmltv.js");
/**
* Finally, the app itself
@@ -717,9 +718,8 @@ extjs_xmltv(http_connection_t *hc, const char *remain, void *opaque)
htsbuf_queue_t *hq = &hc->hc_reply;
const char *op = http_arg_get(&hc->hc_req_args, "op");
xmltv_channel_t *xc;
- htsmsg_t *out, *array, *e;
-
- pthread_mutex_lock(&global_lock);
+ htsmsg_t *out, *array, *e, *r;
+ const char *s;
if(!strcmp(op, "listChannels")) {
@@ -730,21 +730,55 @@ extjs_xmltv(http_connection_t *hc, const char *remain, void *opaque)
htsmsg_add_str(e, "xcTitle", "None");
htsmsg_add_msg(array, NULL, e);
+ pthread_mutex_lock(&global_lock);
LIST_FOREACH(xc, &xmltv_displaylist, xc_displayname_link) {
e = htsmsg_create();
htsmsg_add_str(e, "xcTitle", xc->xc_displayname);
htsmsg_add_msg(array, NULL, e);
}
+ pthread_mutex_unlock(&global_lock);
htsmsg_add_msg(out, "entries", array);
+ } else if(!strcmp(op, "loadSettings")) {
+
+ pthread_mutex_lock(&xmltv_mutex);
+ r = htsmsg_create();
+
+ if((s = xmltv_get_current_grabber()) != NULL)
+ htsmsg_add_str(r, "grabber", s);
+
+ htsmsg_add_u32(r, "grabinterval", xmltv_grab_interval);
+ pthread_mutex_unlock(&xmltv_mutex);
+
+ out = json_single_record(r, "xmltvSettings");
+
+ } else if(!strcmp(op, "saveSettings")) {
+
+ pthread_mutex_lock(&xmltv_mutex);
+
+ s = http_arg_get(&hc->hc_req_args, "grabber");
+ xmltv_set_current_grabber(s);
+
+ pthread_mutex_unlock(&xmltv_mutex);
+
+ out = htsmsg_create();
+ htsmsg_add_u32(out, "success", 1);
+
+ } else if(!strcmp(op, "listGrabbers")) {
+
+ out = htsmsg_create();
+
+ pthread_mutex_lock(&xmltv_mutex);
+ array = xmltv_list_grabbers();
+ pthread_mutex_unlock(&xmltv_mutex);
+ if(array != NULL)
+ htsmsg_add_msg(out, "entries", array);
+
} else {
- pthread_mutex_unlock(&global_lock);
return HTTP_STATUS_BAD_REQUEST;
}
- pthread_mutex_unlock(&global_lock);
-
htsmsg_json_serialize(out, hq, 0);
htsmsg_destroy(out);
http_output_content(hc, "text/x-json; charset=UTF-8");
@@ -1072,7 +1106,6 @@ extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque)
}
-
/**
* WEB user interface
*/
diff --git a/webui/static/app/tvheadend.js b/webui/static/app/tvheadend.js
index c61eb7fc..eb9806b3 100644
--- a/webui/static/app/tvheadend.js
+++ b/webui/static/app/tvheadend.js
@@ -102,6 +102,7 @@ tvheadend.app = function() {
autoScroll:true,
title: 'Configuration',
items: [new tvheadend.chconf,
+ new tvheadend.xmltv,
new tvheadend.cteditor,
new tvheadend.dvrsettings,
new tvheadend.dvb,
@@ -126,10 +127,11 @@ tvheadend.app = function() {
},new Ext.TabPanel({
region:'center',
activeTab:0,
- items:[
+ items:[
new tvheadend.epg,
new tvheadend.dvr,
- confpanel]
+ confpanel
+ ]
})
]
});
diff --git a/webui/static/app/xmltv.js b/webui/static/app/xmltv.js
new file mode 100644
index 00000000..930caa54
--- /dev/null
+++ b/webui/static/app/xmltv.js
@@ -0,0 +1,194 @@
+tvheadend.grabberStore = new Ext.data.JsonStore({
+ root:'entries',
+ fields: ['identifier','name','version','apiconfig'],
+ url:'xmltv',
+ baseParams: {op: 'listGrabbers'},
+});
+
+/*
+
+tvheadend.xmltvsetup = function() {
+
+ var deck1info = new Ext.form.Label({
+ fieldLabel: 'Version',
+ html:'',
+ });
+
+ var deck1cb = new Ext.form.ComboBox({
+ loadingText: 'Scanning for XMLTV grabbers, please wait...',
+ fieldLabel: 'XML-TV Source',
+ name: 'xmltvchannel',
+ width: 350,
+ displayField:'name',
+ valueField:'identifier',
+ store: tvheadend.grabberStore,
+ forceSelection: true,
+ editable: false,
+ triggerAction: 'all',
+ mode: 'remote',
+ emptyText: 'Select grabber'
+ });
+
+ var deck1 = new Ext.FormPanel({
+ labelAlign: 'right',
+ labelWidth: 100,
+ bodyStyle: 'padding: 5px',
+ defaultType: 'label',
+ layout: 'form',
+ border:false,
+ items: [deck1cb, deck1info]
+ });
+
+
+
+ var win = new Ext.Window({
+ title: 'Configure XMLTV grabbers',
+ bodyStyle: 'padding: 5px',
+ layout: 'fit',
+ width: 500,
+ height: 500,
+ constrainHeader: true,
+ buttonAlign: 'center',
+ items: [deck1],
+ bbar: [
+ {
+ id: 'move-back',
+ text: 'Back',
+ disabled: true
+ },
+ '->',
+ {
+ id: 'move-next',
+ text: 'Next'
+ }
+ ]
+ });
+
+ win.show();
+
+ deck1cb.on('select', function(c,r,i) {
+ deck1info.setText(r.data.version);
+ });
+}
+*/
+
+
+
+
+tvheadend.xmltv = function() {
+
+ var confreader = new Ext.data.JsonReader({
+ root: 'xmltvSettings',
+ }, ['grabber','grabinterval']);
+
+ var grabberSelect = new Ext.form.ComboBox({
+ loadingText: 'Loading, please wait...',
+ fieldLabel: 'XML-TV Source',
+ name: 'grabber',
+ width: 350,
+ displayField:'name',
+ valueField:'identifier',
+ store: tvheadend.grabberStore,
+ forceSelection: true,
+ editable: false,
+ triggerAction: 'all',
+ mode: 'remote',
+ emptyText: 'Select grabber'
+ });
+
+ var confpanel = new Ext.FormPanel({
+ title:'XML TV',
+ border:false,
+ bodyStyle:'padding:15px',
+ labelAlign: 'right',
+ labelWidth: 200,
+ waitMsgTarget: true,
+ reader: confreader,
+ layout: 'form',
+ defaultType: 'textfield',
+ items: [
+ grabberSelect,
+ new Ext.form.NumberField({
+ allowNegative: false,
+ allowDecimals: false,
+ minValue: 1,
+ maxValue: 100,
+ fieldLabel: 'Grab interval (hours)',
+ name: 'grabinterval'
+ })
+ ],
+ tbar: [{
+ tooltip: 'Save changes made to configuration below',
+ iconCls:'save',
+ text: "Save configuration",
+ handler: saveChanges
+ }],
+
+ });
+
+ confpanel.on('render', function() {
+ confpanel.getForm().load({
+ url:'/xmltv',
+ params:{'op':'loadSettings'},
+ success:function(form, action) {
+ confpanel.enable();
+ }
+ });
+ });
+
+
+ grabberSelect.on('select', function(c,r,i) {
+ if(r.data.apiconfig) {
+
+ Ext.MessageBox.confirm('XMLTV',
+ 'Configure grabber? ' +
+ 'If you know that the grabber is already '+
+ 'set up or if you want to configure it '+
+ 'manually you may skip this step',
+ function(button) {
+ Ext.MessageBox.alert('XMLTV',
+ 'oops, embedded '+
+ 'config not '+
+ 'implemeted yet');
+ }
+ );
+
+ } else {
+ Ext.MessageBox.alert('XMLTV',
+ 'This grabber does not support being ' +
+ 'configured from external application ' +
+ '(such as Tvheadend).
' +
+ 'Make sure that the grabber is properly ' +
+ 'configured before saving configuration.
'+
+ '
' +
+ 'To configure manually execute the ' +
+ 'following command in a shell on the ' +
+ 'server:
' +
+ '$ ' + r.data.identifier +
+ ' --configure
' +
+ '
' +
+ 'Note: It is important to configure the ' +
+ 'grabber using the same userid as tvheadend '+
+ 'since most grabbers save their '+
+ 'configuration in the users home directory.'+
+ '
' +
+ '
' +
+ 'Grabber version: ' + r.data.version
+ );
+ }
+ });
+
+ function saveChanges() {
+ confpanel.getForm().submit({
+ url:'/xmltv',
+ params:{'op':'saveSettings'},
+ waitMsg:'Saving Data...',
+ failure: function(form, action) {
+ Ext.Msg.alert('Save failed', action.result.errormsg);
+ }
+ });
+ }
+
+ return confpanel;
+}
+
diff --git a/xmltv.c b/xmltv.c
index 5693a7cc..0a6f9936 100644
--- a/xmltv.c
+++ b/xmltv.c
@@ -17,11 +17,13 @@
*/
#include
+#include
#include
#include
#include
#include
#include
+#include
#include
#include
@@ -32,11 +34,47 @@
#include "xmltv.h"
#include "spawn.h"
+
+/**
+ *
+ */
+LIST_HEAD(xmltv_grabber_list, xmltv_grabber);
+static struct xmltv_grabber_list xmltv_grabbers;
+
+typedef struct xmltv_grabber {
+ LIST_ENTRY(xmltv_grabber) xg_link;
+ char *xg_path;
+ time_t xg_mtime;
+
+ char *xg_version;
+ char *xg_description;
+
+ uint32_t xg_capabilities;
+#define XG_CAP_BASELINE 0x1
+#define XG_CAP_MANUALCONFIG 0x2
+#define XG_CAP_CACHE 0x4
+#define XG_CAP_APICONFIG 0x8
+
+ int xg_dirty;
+
+} xmltv_grabber_t;
+
+static xmltv_grabber_t *xg_current;
+
+
+uint32_t xmltv_grab_interval;
+
/* xmltv_channels is protected by global_lock */
static struct xmltv_channel_tree xmltv_channels;
struct xmltv_channel_list xmltv_displaylist;
+pthread_mutex_t xmltv_mutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t xmltv_cond;
+static int xmltv_confver;
+static void xmltv_grabbers_index(void);
+static void xmltv_grabbers_load(void);
+static void xmltv_grabbers_save(void);
/**
*
@@ -382,16 +420,13 @@ xmltv_parse(htsmsg_t *body)
*
*/
static void
-xmltv_grab(void)
+xmltv_grab(const char *prog)
{
- const char *prog = "/usr/bin/tv_grab_se_swedb";
int outlen;
char *outbuf;
htsmsg_t *body;
char errbuf[100];
- tvhlog(LOG_INFO, "xmltv", "Starting grabber \"%s\"", prog);
-
outlen = spawn_and_store_stdout(prog, NULL, &outbuf);
if(outlen < 1) {
tvhlog(LOG_ERR, "xmltv", "No output from \"%s\"", prog);
@@ -419,9 +454,50 @@ xmltv_grab(void)
static void *
xmltv_thread(void *aux)
{
+ int confver = 0;
+ struct timespec ts;
+ char *p;
+
+ pthread_mutex_lock(&xmltv_mutex);
+
+ xmltv_grabbers_load();
+
+ xmltv_grabbers_index();
+
+ ts.tv_nsec = 0;
+ ts.tv_sec = 0;
+
while(1) {
- xmltv_grab();
- sleep(3600);
+
+ while(confver == xmltv_confver) {
+
+ if(xg_current == NULL) {
+ pthread_cond_wait(&xmltv_cond, &xmltv_mutex);
+ continue;
+ }
+ if(pthread_cond_timedwait(&xmltv_cond, &xmltv_mutex, &ts) == ETIMEDOUT)
+ break;
+ }
+
+ ts.tv_sec = time(NULL) + xmltv_grab_interval * 3600;
+
+ confver = xmltv_confver;
+
+ if(xg_current != NULL) {
+ tvhlog(LOG_INFO, "xmltv",
+ "Grabbing \"%s\" using command \"%s\"",
+ xg_current->xg_description, xg_current->xg_path);
+
+ /* Dup path so we can unlock */
+ p = strdup(xg_current->xg_path);
+
+ pthread_mutex_unlock(&xmltv_mutex);
+
+ xmltv_grab(p);
+
+ pthread_mutex_lock(&xmltv_mutex);
+ free(p);
+ }
}
}
@@ -433,6 +509,360 @@ void
xmltv_init(void)
{
pthread_t ptid;
+ xmltv_grab_interval = 12; /* Default half a day */
+
+ /* Load all channels */
xmltv_load();
+
pthread_create(&ptid, NULL, xmltv_thread, NULL);
}
+
+
+/**
+ *
+ */
+static void
+xmltv_grabber_destroy(xmltv_grabber_t *xg)
+{
+ LIST_REMOVE(xg, xg_link);
+ free(xg->xg_path);
+ free(xg->xg_version);
+ free(xg->xg_description);
+ free(xg);
+}
+
+/**
+ *
+ */
+static char *
+xmltv_grabber_get_description(const char *prog)
+{
+ int i, outlen;
+ char *outbuf;
+ const char *args[] = {prog, "--description", NULL};
+
+ if((outlen = spawn_and_store_stdout(prog, (void *)args, &outbuf)) < 1)
+ return NULL;
+
+ for(i = 0; i < outlen; i++)
+ if(outbuf[i] < 32)
+ outbuf[i] = 0;
+ return outbuf;
+}
+
+
+
+/**
+ *
+ */
+static char *
+xmltv_grabber_get_version(const char *prog)
+{
+ int outlen;
+ char *outbuf;
+ const char *args[] = {prog, "--version", NULL};
+
+ if((outlen = spawn_and_store_stdout(prog, (void *)args, &outbuf)) < 1)
+ return NULL;
+ return outbuf;
+}
+
+/**
+ *
+ */
+static int
+xmltv_grabber_get_capabilities(const char *prog)
+{
+ int outlen;
+ char *outbuf;
+ int r;
+
+ const char *args[] = {prog, "--capabilities", NULL};
+
+ if((outlen = spawn_and_store_stdout(prog, (void *)args, &outbuf)) < 1)
+ return 0;
+
+ r = strstr(outbuf, "baseline") ? XG_CAP_BASELINE : 0;
+ r |= strstr(outbuf, "manualconfig") ? XG_CAP_MANUALCONFIG : 0;
+ r |= strstr(outbuf, "cache") ? XG_CAP_CACHE : 0;
+ r |= strstr(outbuf, "apiconfig") ? XG_CAP_APICONFIG : 0;
+ return r;
+}
+
+/**
+ *
+ */
+static int
+xg_desc_cmp(const xmltv_grabber_t *a, const xmltv_grabber_t *b)
+{
+ return strcmp(a->xg_description, b->xg_description);
+}
+
+/**
+ *
+ */
+static int
+xmltv_grabber_add(const char *path, time_t mtime)
+{
+ char *desc;
+ xmltv_grabber_t *xg;
+ int wasthisgrabber = 0;
+
+ LIST_FOREACH(xg, &xmltv_grabbers, xg_link)
+ if(!strcmp(xg->xg_path, path))
+ break;
+
+ if(xg != NULL) {
+ if(xg->xg_mtime == mtime) {
+ xg->xg_dirty = 0;
+ return 0; /* Already there */
+ }
+ wasthisgrabber = xg == xg_current;
+ xmltv_grabber_destroy(xg);
+ xg_current = NULL;
+ }
+ if((desc = xmltv_grabber_get_description(path)) == NULL)
+ return 0; /* Probably not a working grabber */
+
+ xg = calloc(1, sizeof(xmltv_grabber_t));
+ xg->xg_path = strdup(path);
+ xg->xg_mtime = mtime;
+ xg->xg_description = strdup(desc);
+ xg->xg_capabilities = xmltv_grabber_get_capabilities(path);
+ tvh_str_set(&xg->xg_version, xmltv_grabber_get_version(path));
+ LIST_INSERT_SORTED(&xmltv_grabbers, xg, xg_link, xg_desc_cmp);
+ if(wasthisgrabber)
+ xg_current = xg;
+ return 1;
+}
+
+
+/**
+ *
+ */
+static xmltv_grabber_t *
+xmltv_grabber_find_by_path(const char *path)
+{
+ xmltv_grabber_t *xg;
+
+ LIST_FOREACH(xg, &xmltv_grabbers, xg_link)
+ if(!strcmp(xg->xg_path, path))
+ break;
+ return xg;
+}
+
+
+/**
+ *
+ */
+static int
+xmltv_grabber_filter(const struct dirent *d)
+{
+ if(strncmp(d->d_name, "tv_grab_", 8))
+ return 0;
+ return 1;
+
+}
+
+
+/**
+ *
+ */
+static int
+xmltv_scan_grabbers(const char *path)
+{
+ struct dirent **namelist;
+ int n, change = 0;
+ char fname[300];
+ struct stat st;
+
+ if((n = scandir(path, &namelist, xmltv_grabber_filter, NULL)) < 0)
+ return 0;
+
+ while(n--) {
+ snprintf(fname, sizeof(fname), "%s/%s", path, namelist[n]->d_name);
+ if(!stat(fname, &st))
+ change |= xmltv_grabber_add(fname, st.st_mtime);
+ }
+ free(namelist);
+ return change;
+}
+
+
+/**
+ *
+ */
+static void
+xmltv_grabbers_index(void)
+{
+ xmltv_grabber_t *xg, *next;
+ int change;
+
+ LIST_FOREACH(xg, &xmltv_grabbers, xg_link)
+ xg->xg_dirty = 1;
+
+ change = xmltv_scan_grabbers("/bin");
+ change |= xmltv_scan_grabbers("/usr/bin");
+ change |= xmltv_scan_grabbers("/usr/local/bin");
+
+ for(xg = LIST_FIRST(&xmltv_grabbers); xg != NULL; xg = next) {
+ next = LIST_NEXT(xg, xg_link);
+ if(xg->xg_dirty) {
+ change = 1;
+ xmltv_grabber_destroy(xg);
+ }
+ }
+
+ if(change)
+ xmltv_grabbers_save();
+}
+
+/**
+ *
+ */
+static void
+xmltv_grabbers_load(void)
+{
+ xmltv_grabber_t *xg;
+ htsmsg_t *l, *c, *m;
+ htsmsg_field_t *f;
+ const char *path, *desc;
+ uint32_t u32;
+
+ if((m = hts_settings_load("xmltv/config")) == NULL)
+ return;
+
+ htsmsg_get_u32(m, "grab-interval", &xmltv_grab_interval);
+
+ if((l = htsmsg_get_array(m, "grabbers")) != NULL) {
+
+ HTSMSG_FOREACH(f, l) {
+ if((c = htsmsg_get_msg_by_field(f)) == NULL)
+ continue;
+
+ path = htsmsg_get_str(c, "path");
+ desc = htsmsg_get_str(c, "description");
+ if(path == NULL || desc == NULL)
+ continue;
+
+ xg = calloc(1, sizeof(xmltv_grabber_t));
+ xg->xg_path = strdup(path);
+ xg->xg_description = strdup(desc);
+
+ if(!htsmsg_get_u32(c, "mtime", &u32))
+ xg->xg_mtime = u32;
+
+ tvh_str_set(&xg->xg_version, htsmsg_get_str(c, "version"));
+ htsmsg_get_u32(c, "capabilities", &xg->xg_capabilities);
+ LIST_INSERT_SORTED(&xmltv_grabbers, xg, xg_link, xg_desc_cmp);
+ }
+ }
+
+ if((path = htsmsg_get_str(m, "current-grabber")) != NULL) {
+ if((xg = xmltv_grabber_find_by_path(path)) != NULL)
+ xg_current = xg;
+ }
+}
+
+
+static void
+xmltv_grabbers_save(void)
+{
+ htsmsg_t *array, *e, *m;
+ xmltv_grabber_t *xg;
+
+ m = htsmsg_create();
+
+ array = htsmsg_create_array();
+
+ LIST_FOREACH(xg, &xmltv_grabbers, xg_link) {
+ e = htsmsg_create();
+ htsmsg_add_str(e, "path", xg->xg_path);
+ htsmsg_add_str(e, "description", xg->xg_description);
+ if(xg->xg_version != NULL)
+ htsmsg_add_str(e, "version", xg->xg_version);
+ htsmsg_add_u32(e, "mtime", xg->xg_mtime);
+ htsmsg_add_u32(e, "capabilities", xg->xg_capabilities);
+
+ htsmsg_add_msg(array, NULL, e);
+ }
+
+ htsmsg_add_msg(m, "grabbers", array);
+
+ htsmsg_add_u32(m, "grab-interval", xmltv_grab_interval);
+ if(xg_current != NULL)
+ htsmsg_add_str(m, "current-grabber", xg_current->xg_path);
+
+ hts_settings_save(m, "xmltv/config");
+ htsmsg_destroy(m);
+}
+
+
+/**
+ *
+ */
+htsmsg_t *
+xmltv_list_grabbers(void)
+{
+ htsmsg_t *array, *m;
+ xmltv_grabber_t *xg;
+
+ lock_assert(&xmltv_mutex);
+
+ xmltv_grabbers_index();
+
+ array = htsmsg_create_array();
+
+ LIST_FOREACH(xg, &xmltv_grabbers, xg_link) {
+ m = htsmsg_create();
+ htsmsg_add_str(m, "identifier", xg->xg_path);
+ htsmsg_add_str(m, "name", xg->xg_description);
+ htsmsg_add_str(m, "version", xg->xg_version ?: "Unknown version");
+ htsmsg_add_u32(m, "apiconfig", !!(xg->xg_capabilities & XG_CAP_APICONFIG));
+ htsmsg_add_msg(array, NULL, m);
+ }
+
+ return array;
+}
+
+
+/**
+ *
+ */
+const char *
+xmltv_get_current_grabber(void)
+{
+ lock_assert(&xmltv_mutex);
+ return xg_current ? xg_current->xg_description : NULL;
+}
+
+
+/**
+ *
+ */
+void
+xmltv_set_current_grabber(const char *desc)
+{
+ xmltv_grabber_t *xg;
+
+ lock_assert(&xmltv_mutex);
+
+ if(desc != NULL) {
+ LIST_FOREACH(xg, &xmltv_grabbers, xg_link)
+ if(!strcmp(xg->xg_description, desc))
+ break;
+ if(xg == NULL)
+ return;
+ } else {
+ xg = NULL;
+ }
+
+ if(xg_current == xg)
+ return;
+
+ xg_current = xg;
+
+ xmltv_confver++;
+ pthread_cond_signal(&xmltv_cond);
+ xmltv_grabbers_save();
+}
diff --git a/xmltv.h b/xmltv.h
index fb95dd1a..f89f108f 100644
--- a/xmltv.h
+++ b/xmltv.h
@@ -46,6 +46,13 @@ xmltv_channel_t *xmltv_channel_find(const char *id, int create);
xmltv_channel_t *xmltv_channel_find_by_displayname(const char *name);
+htsmsg_t *xmltv_list_grabbers(void);
+
+const char *xmltv_get_current_grabber(void);
+void xmltv_set_current_grabber(const char *path);
+
extern struct xmltv_channel_list xmltv_displaylist;
+extern uint32_t xmltv_grab_interval;
+extern pthread_mutex_t xmltv_mutex;
#endif /* EPG_XMLTV_H__ */