Add database and webui functionality for Digital Video Recorder.
The recorder itself is yet to be written though.
This commit is contained in:
parent
d6a20d720c
commit
8d10a0352f
11 changed files with 428 additions and 13 deletions
3
Makefile
3
Makefile
|
@ -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
|
||||
|
|
|
@ -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
24
epg.c
|
@ -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
4
epg.h
|
@ -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
23
main.c
|
@ -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(>imers, 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);
|
||||
|
||||
|
||||
|
|
6
tvhead.h
6
tvhead.h
|
@ -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;
|
||||
|
|
157
webui/extjs.c
157
webui/extjs.c
|
@ -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
188
webui/static/app/dvr.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -115,6 +115,6 @@
|
|||
margin: 5px;
|
||||
}
|
||||
|
||||
.x-epg-cgrp {
|
||||
.x-epg-meta {
|
||||
margin: 5px;
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
})
|
||||
]
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue