diff --git a/channels.c b/channels.c index e0a079b8..f53830c4 100644 --- a/channels.c +++ b/channels.c @@ -599,6 +599,20 @@ channel_tag_find(const char *id, int create) return ct; } +/** + * + */ +channel_tag_t * +channel_tag_find_by_name(const char *name) +{ + channel_tag_t *ct; + + TAILQ_FOREACH(ct, &channel_tags, ct_link) + if(!strcmp(ct->ct_name, name)) + break; + return ct; +} + /** * diff --git a/channels.h b/channels.h index 27d5944f..5e4dbe73 100644 --- a/channels.h +++ b/channels.h @@ -120,6 +120,8 @@ void channel_set_xmltv_source(channel_t *ch, struct xmltv_channel *xc); void channel_set_tags_from_list(channel_t *ch, const char *maplist); +channel_tag_t *channel_tag_find_by_name(const char *name); + extern struct channel_list channels_not_xmltv_mapped; #endif /* CHANNELS_H */ diff --git a/epg.c b/epg.c index 9d78b84b..e4eb6f12 100644 --- a/epg.c +++ b/epg.c @@ -117,6 +117,7 @@ event_t * epg_event_find_by_start(channel_t *ch, time_t start, int create) { static event_t *skel, *e; + static int tally; lock_assert(&global_lock); @@ -134,6 +135,7 @@ epg_event_find_by_start(channel_t *ch, time_t start, int create) e = skel; skel = NULL; + e->e_id = ++tally; e->e_refcount = 1; e->e_channel = ch; epg_event_changed(e); @@ -269,6 +271,15 @@ static const char *groupnames[16] = { [11] = "Special characteristics", }; +/** + * + */ +const char * +epg_content_group_get_name(unsigned int id) +{ + return id < 16 ? groupnames[id] : NULL; +} + /** * Find a content type */ @@ -309,7 +320,7 @@ epg_content_group_find_by_name(const char *name) for(i = 0; i < 16; i++) { ecg = epg_content_groups[i]; - if(ecg->ecg_name && !strcmp(name, ecg->ecg_name)) + if(ecg != NULL && ecg->ecg_name && !strcmp(name, ecg->ecg_name)) return ecg; } return NULL; @@ -384,3 +395,123 @@ epg_init(void) epg_content_type_find_by_dvbcode(i); } + +/** + * + */ +static void +eqr_add(epg_query_result_t *eqr, event_t *e, regex_t *preg) +{ + if(preg != NULL && regexec(preg, e->e_title, 0, NULL, 0)) + return; + + if(eqr->eqr_entries == eqr->eqr_alloced) { + /* Need to alloc more space */ + + eqr->eqr_alloced = MAX(100, eqr->eqr_alloced * 2); + eqr->eqr_array = realloc(eqr->eqr_array, + eqr->eqr_alloced * sizeof(event_t *)); + } + eqr->eqr_array[eqr->eqr_entries++] = e; + e->e_refcount++; +} + +/** + * + */ +static void +epg_query_add_channel(epg_query_result_t *eqr, channel_t *ch, + epg_content_group_t *ecg, regex_t *preg) +{ + event_t *e; + + if(ecg == NULL) { + RB_FOREACH(e, &ch->ch_epg_events, e_channel_link) + eqr_add(eqr, e, preg); + return; + } + + RB_FOREACH(e, &ch->ch_epg_events, e_channel_link) + if(e->e_content_type != NULL && ecg == e->e_content_type->ect_group) + eqr_add(eqr, e, preg); +} + +/** + * + */ +void +epg_query(epg_query_result_t *eqr, const char *channel, const char *tag, + const char *contentgroup, const char *title) +{ + channel_t *ch = channel ? channel_find_by_name(channel, 0) : NULL; + channel_tag_t *ct = tag ? channel_tag_find_by_name(tag) : NULL; + epg_content_group_t *ecg = contentgroup ? + epg_content_group_find_by_name(contentgroup) : NULL; + channel_tag_mapping_t *ctm; + + regex_t preg0, *preg; + + if(title != NULL) { + if(regcomp(&preg0, title, REG_ICASE | REG_EXTENDED | REG_NOSUB)) + return; + preg = &preg0; + } else { + preg = NULL; + } + + lock_assert(&global_lock); + memset(eqr, 0, sizeof(epg_query_result_t)); + + if(ch != NULL && ct == NULL) { + epg_query_add_channel(eqr, ch, ecg, preg); + return; + } + + if(ct != NULL) { + LIST_FOREACH(ctm, &ct->ct_ctms, ctm_tag_link) + if(ch == NULL || ctm->ctm_channel == ch) + epg_query_add_channel(eqr, ctm->ctm_channel, ecg, preg); + return; + } + + RB_FOREACH(ch, &channel_name_tree, ch_name_link) + epg_query_add_channel(eqr, ch, ecg, preg); +} + + +/** + * + */ +void +epg_query_free(epg_query_result_t *eqr) +{ + int i; + + for(i = 0; i < eqr->eqr_entries; i++) + epg_event_unref(eqr->eqr_array[i]); + free(eqr->eqr_array); +} + +/** + * Sorting functions + */ +static int +epg_sort_start_ascending(const void *A, const void *B) +{ + event_t *a = *(event_t **)A; + event_t *b = *(event_t **)B; + return a->e_start - b->e_start; +} + +/** + * + */ +void +epg_query_sort(epg_query_result_t *eqr) +{ + int (*sf)(const void *a, const void *b); + + sf = epg_sort_start_ascending; + + qsort(eqr->eqr_array, eqr->eqr_entries, sizeof(event_t *), sf); +} diff --git a/epg.h b/epg.h index 2d51d34c..f3edfce6 100644 --- a/epg.h +++ b/epg.h @@ -48,6 +48,7 @@ typedef struct event { RB_ENTRY(event) e_channel_link; int e_refcount; + uint32_t e_id; LIST_ENTRY(event) e_content_type_link; epg_content_type_t *e_content_type; @@ -93,4 +94,20 @@ epg_content_type_t *epg_content_type_find_by_dvbcode(uint8_t dvbcode); epg_content_group_t *epg_content_group_find_by_name(const char *name); +const char *epg_content_group_get_name(unsigned int id); + +/** + * + */ +typedef struct epg_query_result { + event_t **eqr_array; + int eqr_entries; + int eqr_alloced; +} epg_query_result_t; + +void epg_query(epg_query_result_t *eqr, const char *channel, const char *tag, + const char *contentgroup, const char *title); +void epg_query_free(epg_query_result_t *eqr); +void epg_query_sort(epg_query_result_t *eqr); + #endif /* EPG_H */ diff --git a/webui/extjs.c b/webui/extjs.c index a29eb183..1aca6a4d 100644 --- a/webui/extjs.c +++ b/webui/extjs.c @@ -41,6 +41,7 @@ #include "transports.h" #include "serviceprobe.h" #include "xmltv.h" +#include "epg.h" extern const char *htsversion; @@ -109,6 +110,7 @@ extjs_root(http_connection_t *hc, const char *remain, void *opaque) extjs_load(hq, "static/app/cwceditor.js"); extjs_load(hq, "static/app/dvb.js"); extjs_load(hq, "static/app/chconf.js"); + extjs_load(hq, "static/app/epg.js"); /** * Finally, the app itself @@ -243,6 +245,38 @@ extjs_chlist(http_connection_t *hc, const char *remain, void *opaque) } +/** + * EPG Content Groups + */ +static int +extjs_ecglist(http_connection_t *hc, const char *remain, void *opaque) +{ + htsbuf_queue_t *hq = &hc->hc_reply; + htsmsg_t *out, *array, *c; + const char *s; + int i; + + out = htsmsg_create(); + array = htsmsg_create_array(); + + for(i = 0; i < 16; i++) { + if((s = epg_content_group_get_name(i)) == NULL) + continue; + + c = htsmsg_create(); + htsmsg_add_str(c, "name", s); + htsmsg_add_msg(array, NULL, c); + } + + htsmsg_add_msg(out, "entries", array); + + htsmsg_json_serialize(out, hq, 0); + htsmsg_destroy(out); + http_output_content(hc, "text/x-json; charset=UTF-8"); + return 0; +} + + /** * */ @@ -757,7 +791,79 @@ extjs_channeltags(http_connection_t *hc, const char *remain, void *opaque) } +/** + * + */ +static int +extjs_epg(http_connection_t *hc, const char *remain, void *opaque) +{ + htsbuf_queue_t *hq = &hc->hc_reply; + htsmsg_t *out, *array, *m; + epg_query_result_t eqr; + event_t *e; + int start = 0, end, limit, i; + const char *s; + const char *channel = http_arg_get(&hc->hc_req_args, "channel"); + const char *tag = http_arg_get(&hc->hc_req_args, "tag"); + const char *cgrp = http_arg_get(&hc->hc_req_args, "contentgrp"); + const char *title = http_arg_get(&hc->hc_req_args, "title"); + if(channel && !channel[0]) channel = NULL; + if(tag && !tag[0]) tag = NULL; + + if((s = http_arg_get(&hc->hc_req_args, "start")) != NULL) + start = atoi(s); + + if((s = http_arg_get(&hc->hc_req_args, "limit")) != NULL) + limit = atoi(s); + else + limit = 20; /* XXX */ + + out = htsmsg_create(); + array = htsmsg_create_array(); + + pthread_mutex_lock(&global_lock); + + epg_query(&eqr, channel, tag, cgrp, title); + + epg_query_sort(&eqr); + + htsmsg_add_u32(out, "totalCount", eqr.eqr_entries); + + + start = MIN(start, eqr.eqr_entries); + end = MIN(start + limit, eqr.eqr_entries); + + for(i = start; i < end; i++) { + e = eqr.eqr_array[i]; + + m = htsmsg_create(); + + if(e->e_channel != NULL) + htsmsg_add_str(m, "channel", e->e_channel->ch_name); + htsmsg_add_str(m, "title", e->e_title); + htsmsg_add_str(m, "description", e->e_desc); + htsmsg_add_u32(m, "id", e->e_id); + htsmsg_add_u32(m, "start", e->e_start); + htsmsg_add_u32(m, "end", e->e_start + e->e_duration); + htsmsg_add_u32(m, "duration", e->e_duration); + + if(e->e_content_type != NULL) + htsmsg_add_str(m, "contentgrp", e->e_content_type->ect_group->ecg_name); + htsmsg_add_msg(array, NULL, m); + } + + epg_query_free(&eqr); + + pthread_mutex_unlock(&global_lock); + + htsmsg_add_msg(out, "entries", array); + + htsmsg_json_serialize(out, hq, 0); + htsmsg_destroy(out); + http_output_content(hc, "text/x-json; charset=UTF-8"); + return 0; +} /** * WEB user interface */ @@ -773,4 +879,6 @@ extjs_start(void) http_path_add("/channel", NULL, extjs_channel, ACCESS_WEB_INTERFACE); http_path_add("/xmltv", NULL, extjs_xmltv, ACCESS_WEB_INTERFACE); http_path_add("/channeltags", NULL, extjs_channeltags, ACCESS_WEB_INTERFACE); + http_path_add("/epg", NULL, extjs_epg, ACCESS_WEB_INTERFACE); + http_path_add("/ecglist", NULL, extjs_ecglist, ACCESS_WEB_INTERFACE); } diff --git a/webui/static/app/epg.js b/webui/static/app/epg.js new file mode 100644 index 00000000..4f9ec2e8 --- /dev/null +++ b/webui/static/app/epg.js @@ -0,0 +1,250 @@ +/** + * + */ + +tvheadend.epg = function() { + var xg = Ext.grid; + + var epgRecord = Ext.data.Record.create([ + {name: 'id'}, + {name: 'channel'}, + {name: 'title'}, + {name: 'description'}, + {name: 'start', type: 'date', dateFormat: 'U' /* unix time */}, + {name: 'end', type: 'date', dateFormat: 'U' /* unix time */}, + {name: 'duration'}, + {name: 'contentgrp'} + ]); + + var epgStore = new Ext.data.JsonStore({ + root: 'entries', + totalProperty: 'totalCount', + fields: epgRecord, + url: 'epg', + autoLoad: true, + id: 'id', + remoteSort: true, + }); + + var expander = new xg.RowExpander({ + tpl : new Ext.Template('