Add an electronic program guide (EPG) to the web interface.

Only for reading yet.
This commit is contained in:
Andreas Öman 2008-09-08 22:17:37 +00:00
parent 7122c27598
commit 2b854cb48d
7 changed files with 543 additions and 30 deletions

View file

@ -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;
}
/**
*

View file

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

133
epg.c
View file

@ -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);
}

17
epg.h
View file

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

View file

@ -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);
}

250
webui/static/app/epg.js Normal file
View file

@ -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('<div>{description}</div>')
});
function renderDate(value){
var dt = new Date(value);
return dt.format('l H:i');
}
function renderDuration(value){
value = value / 60; /* Nevermind the seconds */
if(value >= 60) {
var min = value % 60;
var hours = parseInt(value / 60);
if(min == 0) {
return hours + ' hrs';
}
return hours + ' hrs, ' + min + ' min';
} else {
return value + ' min';
}
}
var epgCm = new Ext.grid.ColumnModel([
expander,
{
width: 250,
id:'title',
header: "Title",
dataIndex: 'title',
},{
width: 100,
id:'start',
header: "Start",
dataIndex: 'start',
renderer: renderDate,
},{
width: 100,
hidden:true,
id:'end',
header: "End",
dataIndex: 'end',
renderer: renderDate,
},{
width: 100,
id:'duration',
header: "Duration",
dataIndex: 'duration',
renderer: renderDuration
},{
width: 250,
id:'channel',
header: "Channel",
dataIndex: 'channel',
},{
width: 250,
id:'contentgrp',
header: "Content Type",
dataIndex: 'contentgrp',
}
]);
// Title search box
var epgFilterTitle = new Ext.form.TextField({
emptyText: 'Search title...',
width: 200
});
// Channels, XXX: Perhaps we should make channes a global store as well
var epgFilterChannelsStore = new Ext.data.JsonStore({
root:'entries',
fields: [{name: 'name'}],
url:'chlist'
});
var epgFilterChannels = new Ext.form.ComboBox({
loadingText: 'Loading...',
width: 200,
displayField:'name',
store: epgFilterChannelsStore,
mode: 'remote',
editable: false,
triggerAction: 'all',
emptyText: 'Only include channel...'
});
// Tags, uses global store
var epgFilterChannelTags = new Ext.form.ComboBox({
width: 200,
displayField:'name',
store: tvheadend.channelTags,
mode: 'local',
editable: false,
triggerAction: 'all',
emptyText: 'Only include tag...'
});
// Content groups
var epgFilterContentGroupStore = new Ext.data.JsonStore({
root:'entries',
fields: [{name: 'name'}],
url:'ecglist',
});
var epgFilterContentGroup = new Ext.form.ComboBox({
loadingText: 'Loading...',
width: 200,
displayField:'name',
store: epgFilterContentGroupStore,
mode: 'remote',
editable: false,
triggerAction: 'all',
emptyText: 'Only include content...'
});
/*
function epgReload() {
epgStore.baseParams.channel = epgFilterChannels.getValue();
epgStore.baseParams.tag = epgFilterChannelTags.getValue();
epgStore.baseParams.contentgrp = epgFilterContentGroup.getValue();
epgStore.baseParams.title = epgFilterTitle.getValue();
console.log(epgStore.baseParams.title);
epgStore.reload();
}
*/
function epgQueryClear() {
epgStore.baseParams.channel = null;
epgStore.baseParams.tag = null;
epgStore.baseParams.contentgrp = null;
epgStore.baseParams.title = null;
epgFilterChannels.setValue("");
epgFilterChannelTags.setValue("");
epgFilterContentGroup.setValue("");
epgFilterTitle.setValue("");
epgStore.reload();
}
epgFilterChannels.on('select', function(c, r) {
if(epgStore.baseParams.channel != r.data.name) {
epgStore.baseParams.channel = r.data.name;
epgStore.reload();
}
});
epgFilterChannelTags.on('select', function(c, r) {
if(epgStore.baseParams.tag != r.data.name) {
epgStore.baseParams.tag = r.data.name;
epgStore.reload();
}
});
epgFilterContentGroup.on('select', function(c, r) {
if(epgStore.baseParams.contentgrp != r.data.name) {
epgStore.baseParams.contentgrp = r.data.name;
epgStore.reload();
}
});
epgFilterTitle.on('valid', function(c) {
var value = c.getValue();
if(value.length < 1)
value = null;
if(epgStore.baseParams.title != value) {
epgStore.baseParams.title = value;
epgStore.reload();
}
});
/*
epgFilterChannelTags.on('select', epgReload);
epgFilterContentGroup.on('select', epgReload);
epgFilterTitle.on('valid', epgReload);
*/
var panel = new Ext.grid.GridPanel({
loadMask: true,
title: 'Electronic Program Guide',
store: epgStore,
cm: epgCm,
plugins:[expander],
tbar: [
epgFilterTitle,
'-',
epgFilterChannels,
'-',
epgFilterChannelTags,
'-',
epgFilterContentGroup,
'-',
{
text: 'Reset',
handler: epgQueryClear
}
],
bbar: new Ext.PagingToolbar({
store: epgStore,
pageSize: 20,
displayInfo: true,
displayMsg: 'Programs {0} - {1} of {2}',
emptyMsg: "No programs to display"
})
});
return panel;
}

View file

@ -98,38 +98,29 @@ tvheadend.app = function() {
new tvheadend.acleditor,
new tvheadend.cwceditor]
});
var pvrpanel = new Ext.TabPanel({
autoScroll:true,
title: 'Video Recorder'
});
var chpanel = new Ext.TabPanel({
autoScroll:true,
title: 'Channels'
});
console.log(tvheadend);
var viewport = new Ext.Viewport({
layout:'border',
items:[{
region:'south',
contentEl: 'systemlog',
split:true,
autoScroll:true,
height: 150,
minSize: 100,
maxSize: 400,
collapsible: true,
title:'System log',
margins:'0 0 0 0'
},
new Ext.TabPanel({region:'center',
activeTab:0,
items:[confpanel,
pvrpanel,
chpanel]})
]
items:[
{
region:'south',
contentEl: 'systemlog',
split:true,
autoScroll:true,
height: 150,
minSize: 100,
maxSize: 400,
collapsible: true,
title:'System log',
margins:'0 0 0 0'
},new Ext.TabPanel({
region:'center',
activeTab:0,
items:[new tvheadend.epg,confpanel]
})
]
});
new tvheadend.comet_poller;