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__ */