diff --git a/Makefile b/Makefile index 40c242f2..2abd8db1 100644 --- a/Makefile +++ b/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 diff --git a/channels.h b/channels.h index 5e4dbe73..d03e0c4b 100644 --- a/channels.h +++ b/channels.h @@ -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; diff --git a/epg.c b/epg.c index 3989d30e..dd2e05de 100644 --- a/epg.c +++ b/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); } diff --git a/epg.h b/epg.h index f3edfce6..c6bedcdd 100644 --- a/epg.h +++ b/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); diff --git a/main.c b/main.c index 62f95b03..44a3cb1f 100644 --- a/main.c +++ b/main.c @@ -47,6 +47,7 @@ #include "subscriptions.h" #include "serviceprobe.h" #include "cwc.h" +#include "dvr/dvr.h" #include #include @@ -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); diff --git a/tvhead.h b/tvhead.h index aee0e467..3db9bdbb 100644 --- a/tvhead.h +++ b/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; diff --git a/webui/extjs.c b/webui/extjs.c index 695adf21..03d48cab 100644 --- a/webui/extjs.c +++ b/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); } diff --git a/webui/static/app/dvr.js b/webui/static/app/dvr.js new file mode 100644 index 00000000..4fba7070 --- /dev/null +++ b/webui/static/app/dvr.js @@ -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 += ''; + + content += '
' + entry.title + '
'; + content += '
' + entry.description + '
'; + content += '
' + content += '
Status: ' + entry.status + '
'; + + + 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; +} + diff --git a/webui/static/app/epg.js b/webui/static/app/epg.js index 1be5bae2..cb93aee4 100644 --- a/webui/static/app/epg.js +++ b/webui/static/app/epg.js @@ -12,26 +12,42 @@ tvheadend.epgDetails = function(event) { content += '
' + event.title + '
'; content += '
' + event.description + '
'; - content += '
' + event.contentgrp + '
'; + content += '
' + event.contentgrp + '
'; 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); + } + }); + } + } diff --git a/webui/static/app/ext.css b/webui/static/app/ext.css index f58083a1..ec34fb05 100644 --- a/webui/static/app/ext.css +++ b/webui/static/app/ext.css @@ -115,6 +115,6 @@ margin: 5px; } -.x-epg-cgrp { +.x-epg-meta { margin: 5px; } diff --git a/webui/static/app/tvheadend.js b/webui/static/app/tvheadend.js index c21f7abe..a2672b68 100644 --- a/webui/static/app/tvheadend.js +++ b/webui/static/app/tvheadend.js @@ -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] }) ] });