Add database and webui functionality for Digital Video Recorder.

The recorder itself is yet to be written though.
This commit is contained in:
Andreas Öman 2008-09-16 21:04:50 +00:00
parent d6a20d720c
commit 8d10a0352f
11 changed files with 428 additions and 13 deletions

View file

@ -2,6 +2,9 @@
SRCS = main.c access.c dtable.c tcp.c http.c notify.c epg.c xmltv.c spawn.c
VPATH += dvr
SRCS += dvr_db.c
SRCS += buffer.c channels.c subscriptions.c transports.c
SRCS += psi.c parsers.c parser_h264.c tsdemux.c bitstream.c

View file

@ -55,7 +55,7 @@ typedef struct channel {
struct event *ch_epg_cur_event;
char *ch_icon;
struct pvr_rec_list ch_pvrrs;
struct dvr_entry_list ch_dvrs;
struct autorec_list ch_autorecs;

24
epg.c
View file

@ -29,6 +29,10 @@
#define EPG_MAX_AGE 86400
#define EPG_GLOBAL_HASH_WIDTH 1024
#define EPG_GLOBAL_HASH_MASK (EPG_GLOBAL_HASH_WIDTH - 1)
static struct event_list epg_hash[EPG_GLOBAL_HASH_WIDTH];
epg_content_group_t *epg_content_groups[16];
static int
@ -136,6 +140,10 @@ epg_event_find_by_start(channel_t *ch, time_t start, int create)
skel = NULL;
e->e_id = ++tally;
LIST_INSERT_HEAD(&epg_hash[e->e_id & EPG_GLOBAL_HASH_MASK], e,
e_global_link);
e->e_refcount = 1;
e->e_channel = ch;
epg_event_changed(e);
@ -161,6 +169,21 @@ epg_event_find_by_time(channel_t *ch, time_t t)
}
/**
*
*/
event_t *
epg_event_find_by_id(int eventid)
{
event_t *e;
LIST_FOREACH(e, &epg_hash[eventid & EPG_GLOBAL_HASH_MASK], e_global_link)
if(e->e_id == eventid)
break;
return e;
}
/**
*
*/
@ -172,6 +195,7 @@ epg_event_destroy(event_t *e)
free((void *)e->e_title);
free((void *)e->e_desc);
LIST_REMOVE(e, e_global_link);
free(e);
}

4
epg.h
View file

@ -44,6 +44,8 @@ typedef struct epg_content_type {
* EPG event
*/
typedef struct event {
LIST_ENTRY(event) e_global_link;
struct channel *e_channel;
RB_ENTRY(event) e_channel_link;
@ -84,6 +86,8 @@ event_t *epg_event_find_by_start(channel_t *ch, time_t start, int create);
event_t *epg_event_find_by_time(channel_t *ch, time_t t);
event_t *epg_event_find_by_id(int eventid);
void epg_unlink_from_channel(channel_t *ch);

23
main.c
View file

@ -47,6 +47,7 @@
#include "subscriptions.h"
#include "serviceprobe.h"
#include "cwc.h"
#include "dvr/dvr.h"
#include <libhts/htsparachute.h>
#include <libhts/htssettings.h>
@ -100,11 +101,9 @@ gtimercmp(gtimer_t *a, gtimer_t *b)
*
*/
void
gtimer_arm(gtimer_t *gti, gti_callback_t *callback, void *opaque, int delta)
gtimer_arm_abs(gtimer_t *gti, gti_callback_t *callback, void *opaque,
time_t when)
{
time_t now;
time(&now);
lock_assert(&global_lock);
if(gti->gti_callback != NULL)
@ -112,11 +111,23 @@ gtimer_arm(gtimer_t *gti, gti_callback_t *callback, void *opaque, int delta)
gti->gti_callback = callback;
gti->gti_opaque = opaque;
gti->gti_expire = now + delta;
gti->gti_expire = when;
LIST_INSERT_SORTED(&gtimers, gti, gti_link, gtimercmp);
}
/**
*
*/
void
gtimer_arm(gtimer_t *gti, gti_callback_t *callback, void *opaque, int delta)
{
time_t now;
time(&now);
gtimer_arm_abs(gti, callback, opaque, now + delta);
}
/**
*
*/
@ -272,6 +283,8 @@ main(int argc, char **argv)
cwc_init();
dvr_init();
pthread_mutex_unlock(&global_lock);

View file

@ -75,6 +75,9 @@ typedef struct gtimer {
void gtimer_arm(gtimer_t *gti, gti_callback_t *callback, void *opaque,
int delta);
void gtimer_arm_abs(gtimer_t *gti, gti_callback_t *callback, void *opaque,
time_t when);
void gtimer_disarm(gtimer_t *gti);
@ -89,7 +92,7 @@ TAILQ_HEAD(th_dvb_adapter_queue, th_dvb_adapter);
LIST_HEAD(th_v4l_adapter_list, th_v4l_adapter);
LIST_HEAD(event_list, event);
RB_HEAD(event_tree, event);
LIST_HEAD(pvr_rec_list, pvr_rec);
LIST_HEAD(dvr_entry_list, dvr_entry);
TAILQ_HEAD(ref_update_queue, ref_update);
LIST_HEAD(th_transport_list, th_transport);
RB_HEAD(th_transport_tree, th_transport);
@ -109,7 +112,6 @@ extern time_t dispatch_clock;
extern int startupcounter;
extern struct th_transport_list all_transports;
extern struct channel_tree channel_name_tree;
extern struct pvr_rec_list pvrr_global_list;
extern struct th_subscription_list subscriptions;
struct th_transport;

View file

@ -38,6 +38,7 @@
#include "dvb/dvb.h"
#include "dvb/dvb_support.h"
#include "dvb/dvb_preconf.h"
#include "dvr/dvr.h"
#include "transports.h"
#include "serviceprobe.h"
#include "xmltv.h"
@ -111,6 +112,7 @@ extjs_root(http_connection_t *hc, const char *remain, void *opaque)
extjs_load(hq, "static/app/dvb.js");
extjs_load(hq, "static/app/chconf.js");
extjs_load(hq, "static/app/epg.js");
extjs_load(hq, "static/app/dvr.js");
/**
* Finally, the app itself
@ -872,6 +874,159 @@ extjs_epg(http_connection_t *hc, const char *remain, void *opaque)
http_output_content(hc, "text/x-json; charset=UTF-8");
return 0;
}
/**
*
*/
static int
extjs_dvr(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");
htsmsg_t *out;
event_t *e;
dvr_entry_t *de;
const char *s;
pthread_mutex_lock(&global_lock);
if(!strcmp(op, "recordEvent")) {
s = http_arg_get(&hc->hc_req_args, "eventId");
if((e = epg_event_find_by_id(atoi(s))) == NULL) {
pthread_mutex_unlock(&global_lock);
return HTTP_STATUS_BAD_REQUEST;
}
dvr_entry_create_by_event(e, hc->hc_representative);
out = htsmsg_create();
htsmsg_add_u32(out, "success", 1);
} else if(!strcmp(op, "cancelEntry")) {
s = http_arg_get(&hc->hc_req_args, "entryId");
if((de = dvr_entry_find_by_id(atoi(s))) == NULL) {
pthread_mutex_unlock(&global_lock);
return HTTP_STATUS_BAD_REQUEST;
}
dvr_entry_cancel(de);
out = htsmsg_create();
htsmsg_add_u32(out, "success", 1);
} 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");
return 0;
}
/**
*
*/
static int
extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque)
{
htsbuf_queue_t *hq = &hc->hc_reply;
htsmsg_t *out, *array, *m;
dvr_query_result_t dqr;
dvr_entry_t *de;
int start = 0, end, limit, i;
const char *s, *t = 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);
dvr_query(&dqr);
dvr_query_sort(&dqr);
htsmsg_add_u32(out, "totalCount", dqr.dqr_entries);
start = MIN(start, dqr.dqr_entries);
end = MIN(start + limit, dqr.dqr_entries);
for(i = start; i < end; i++) {
de = dqr.dqr_array[i];
m = htsmsg_create();
if(de->de_channel != NULL) {
htsmsg_add_str(m, "channel", de->de_channel->ch_name);
if(de->de_channel->ch_icon != NULL)
htsmsg_add_str(m, "chicon", de->de_channel->ch_icon);
}
if(de->de_title != NULL)
htsmsg_add_str(m, "title", de->de_title);
if(de->de_desc != NULL)
htsmsg_add_str(m, "description", de->de_desc);
htsmsg_add_u32(m, "id", de->de_id);
htsmsg_add_u32(m, "start", de->de_start);
htsmsg_add_u32(m, "end", de->de_stop);
htsmsg_add_u32(m, "duration", de->de_stop - de->de_start);
htsmsg_add_str(m, "creator", de->de_creator);
switch(de->de_sched_state) {
case DVR_SCHEDULED:
s = "Scheduled for recording";
t = "sched";
break;
case DVR_RECORDING:
s = "Recording";
t = "rec";
break;
case DVR_COMPLETED:
s = de->de_error ?: "Completed OK";
t = "done";
break;
default:
s = "Invalid";
break;
}
htsmsg_add_str(m, "status", s);
if(t != NULL) htsmsg_add_str(m, "schedstate", t);
htsmsg_add_msg(array, NULL, m);
}
dvr_query_free(&dqr);
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
*/
@ -888,5 +1043,7 @@ extjs_start(void)
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("/dvr", NULL, extjs_dvr, ACCESS_WEB_INTERFACE);
http_path_add("/dvrlist", NULL, extjs_dvrlist, ACCESS_WEB_INTERFACE);
http_path_add("/ecglist", NULL, extjs_ecglist, ACCESS_WEB_INTERFACE);
}

188
webui/static/app/dvr.js Normal file
View file

@ -0,0 +1,188 @@
tvheadend.dvrStore = new Ext.data.JsonStore({
root: 'entries',
totalProperty: 'totalCount',
fields: [
{name: 'id'},
{name: 'channel'},
{name: 'title'},
{name: 'description'},
{name: 'chicon'},
{name: 'start', type: 'date', dateFormat: 'U' /* unix time */},
{name: 'end', type: 'date', dateFormat: 'U' /* unix time */},
{name: 'status'},
{name: 'schedstate'},
{name: 'creator'},
{name: 'duration'},
],
url: 'dvrlist',
autoLoad: true,
id: 'id',
remoteSort: true,
});
/**
*
*/
tvheadend.dvrDetails = function(entry) {
var content = '';
var but;
if(entry.chicon != null && entry.chicon.length > 0)
content += '<img class="x-epg-chicon" src="' + entry.chicon + '">';
content += '<div class="x-epg-title">' + entry.title + '</div>';
content += '<div class="x-epg-desc">' + entry.description + '</div>';
content += '<hr>'
content += '<div class="x-epg-meta">Status: ' + entry.status + '</div>';
var win = new Ext.Window({
title: entry.title,
bodyStyle: 'margin: 5px',
layout: 'fit',
width: 400,
height: 300,
constrainHeader: true,
buttonAlign: 'center',
html: content,
});
switch(entry.schedstate) {
case 'sched':
win.addButton({
handler: cancelEvent,
text: "Remove from schedule"
});
break;
case 'rec':
win.addButton({
handler: cancelEvent,
text: "Abort recording"
});
break;
}
win.show();
function cancelEvent() {
Ext.Ajax.request({
url: '/dvr',
params: {entryId: entry.id, op: 'cancelEntry'},
success:function(response, options) {
win.close();
},
failure:function(response, options) {
Ext.MessageBox.alert('DVR', response.statusText);
}
});
}
}
/**
*
*/
tvheadend.dvr = function() {
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 dvrCm = new Ext.grid.ColumnModel([
{
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: 200,
id:'creator',
header: "Created by",
hidden:true,
dataIndex: 'creator',
},{
width: 200,
id:'status',
header: "Status",
dataIndex: 'status',
}
]);
var panel = new Ext.grid.GridPanel({
loadMask: true,
title: 'Digital Video Recorder',
store: tvheadend.dvrStore,
cm: dvrCm,
viewConfig: {forceFit:true},
bbar: new Ext.PagingToolbar({
store: tvheadend.dvrStore,
pageSize: 20,
displayInfo: true,
displayMsg: 'Programs {0} - {1} of {2}',
emptyMsg: "No programs to display"
})
});
panel.on('rowclick', rowclicked);
function rowclicked(grid, index) {
new tvheadend.dvrDetails(grid.getStore().getAt(index).data);
}
return panel;
}

View file

@ -12,26 +12,42 @@ tvheadend.epgDetails = function(event) {
content += '<div class="x-epg-title">' + event.title + '</div>';
content += '<div class="x-epg-desc">' + event.description + '</div>';
content += '<div class="x-epg-cgrp">' + event.contentgrp + '</div>';
content += '<div class="x-epg-meta">' + event.contentgrp + '</div>';
var win = new Ext.Window({
title: event.title,
bodyStyle: 'margin: 5px',
layout: 'fit',
width: 400,
height: 300,
constrainHeader: true,
/*
buttons: [
new Ext.Button({
handler: recordEvent,
text: "Record program"
})
],
*/
buttonAlign: 'center',
html: content,
});
win.show();
function recordEvent() {
Ext.Ajax.request({
url: '/dvr',
params: {eventId: event.id, op: 'recordEvent'},
success:function(response, options) {
win.close();
},
failure:function(response, options) {
Ext.MessageBox.alert('DVR', response.statusText);
}
});
}
}

View file

@ -115,6 +115,6 @@
margin: 5px;
}
.x-epg-cgrp {
.x-epg-meta {
margin: 5px;
}

View file

@ -19,6 +19,11 @@ tvheadend.comet_poller = function() {
tvheadend.channelTags.reload();
break;
case 'dvrdb':
if(m.reload != null)
tvheadend.dvrStore.reload();
break;
case 'channels':
if(m.reload != null)
tvheadend.channels.reload();
@ -122,7 +127,10 @@ tvheadend.app = function() {
},new Ext.TabPanel({
region:'center',
activeTab:0,
items:[new tvheadend.epg,confpanel]
items:[
new tvheadend.epg,
new tvheadend.dvr,
confpanel]
})
]
});