Add HTML/web interface

This commit is contained in:
Andreas Öman 2007-11-20 08:04:28 +00:00
parent 0c7216354e
commit 7cbd3d88aa
3 changed files with 780 additions and 1 deletions

View file

@ -3,7 +3,7 @@
SRCS = main.c dispatch.c channels.c transports.c teletext.c psi.c \
subscriptions.c tsmux.c tsdemux.c pes.c buffer.c tcp.c
SRCS += http.c
SRCS += http.c htmlui.c
SRCS += pvr.c

753
htmlui.c Normal file
View file

@ -0,0 +1,753 @@
/*
* tvheadend, HTML user interface
* Copyright (C) 2007 Andreas Öman
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <pthread.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <fcntl.h>
#include <errno.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include "tvhead.h"
#include "htmlui.h"
#include "channels.h"
#include "epg.h"
#include "pvr.h"
#include "strtab.h"
static struct strtab recstatustxt[] = {
{ "Scheduled", HTSTV_PVR_STATUS_SCHEDULED },
{ "Recording", HTSTV_PVR_STATUS_RECORDING },
{ "Done", HTSTV_PVR_STATUS_DONE },
{ "Recording aborted", HTSTV_PVR_STATUS_ABORTED },
{ "No transponder", HTSTV_PVR_STATUS_NO_TRANSPONDER },
{ "File error", HTSTV_PVR_STATUS_FILE_ERROR },
{ "Disk full", HTSTV_PVR_STATUS_DISK_FULL },
{ "Buffer error", HTSTV_PVR_STATUS_BUFFER_ERROR },
};
static struct strtab recstatuscolor[] = {
{ "#3333aa", HTSTV_PVR_STATUS_SCHEDULED },
{ "#aa3333", HTSTV_PVR_STATUS_RECORDING },
{ "#33aa33", HTSTV_PVR_STATUS_DONE },
{ "#aa3333", HTSTV_PVR_STATUS_ABORTED },
{ "#aa3333", HTSTV_PVR_STATUS_NO_TRANSPONDER },
{ "#aa3333", HTSTV_PVR_STATUS_FILE_ERROR },
{ "#aa3333", HTSTV_PVR_STATUS_DISK_FULL },
{ "#aa3333", HTSTV_PVR_STATUS_BUFFER_ERROR },
};
static int
pvrstatus_to_html(tv_pvr_status_t pvrstatus, const char **text,
const char **col)
{
*text = val2str(pvrstatus, recstatustxt);
if(*text == NULL)
return -1;
*col = val2str(pvrstatus, recstatuscolor);
if(*col == NULL)
return -1;
return 0;
}
static void
html_header(tcp_queue_t *tq, const char *title, int javascript)
{
tcp_qprintf(tq,
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" "
"http://www.w3.org/TR/html4/strict.dtd\">\r\n"
"<html><head>\r\n"
"<title>%s</title>\r\n"
"<meta http-equiv=\"Content-Type\" "
"content=\"text/html; charset=utf-8\">\r\n", title);
tcp_qprintf(tq,
"<style type=\"text/css\">\r\n"
"<!--\r\n"
"img { border: 0px; }\r\n"
"a:link { text-decoration: none}\r\n"
"a:visited { text-decoration: none}\r\n"
"a:active { text-decoration: none}\r\n"
"a:link { text-decoration: none; color: #000000}\r\n"
"a:visited { text-decoration: none; color: #000000}\r\n"
"a:active { text-decoration: none; color: #000000}\r\n"
"a:hover { text-decoration: underline; color: CC3333}\r\n"
""
"body {margin: 4px 4px; "
"font: 75% Verdana, Arial, Helvetica, sans-serif; "
"width: 600px; margin-right: auto; margin-left: auto;}\r\n"
""
"#box {background: #cccc99;}\r\n"
".roundtop {background: #ffffff;}\r\n"
".roundbottom {background: #ffffff;}\r\n"
".r1{margin: 0 5px; height: 1px; overflow: hidden; "
"background: #000000; border-left: 1px solid #000000; "
"border-right: 1px solid #000000;}\r\n"
""
".r2{margin: 0 3px; height: 1px; overflow: hidden; "
"background: #cccc99; border-left: 1px solid #000000; "
"border-right: 1px solid #000000; border-width: 0 2px;}\r\n"
""
".r3{margin: 0 2px; height: 1px; overflow: hidden; "
"background: #cccc99; border-left: 1px solid #000000; "
"border-right: 1px solid #000000;}\r\n"
""
".r4{margin: 0 1px; height: 2px; overflow: hidden; "
"background: #cccc99; border-left: 1px solid #000000; "
"border-right: 1px solid #000000;}\r\n"
""
".content3 {padding-left: 3px; height: 60px; "
"border-left: 1px solid #000000; "
"border-right: 1px solid #000000;}\r\n"
""
".content {padding-left: 3px; border-left: 1px solid #000000; "
"border-right: 1px solid #000000;}\r\n"
""
".logo {padding: 2px; width: 60px; height: 56px; "
"float: left};\r\n"
""
".over {float: left}\r\n"
".toptxt {float: left; width: 165px; text-align: center}\r\n"
""
"#meny {margin: 0; padding: 0}\r\n"
"#meny li{display: inline; list-style-type: none;}\r\n"
"#meny a{padding: 1.15em 0.8em; text-decoration: none;}\r\n"
"-->\r\n"
"</style>");
if(javascript) {
tcp_qprintf(tq,
"<script type=\"text/javascript\" "
"src=\"http://www.olebyn.nu/hts/overlib.js\"></script>\r\n");
tcp_qprintf(tq,
"<script language=\"javascript\">\r\n"
"<!-- Begin\r\n"
"function epop() {\r\n"
" props=window.open(epop.arguments[0],"
"'poppage', 'toolbars=0, scrollbars=0, location=0, "
"statusbars=0, menubars=0, resizable=0, "
"width=600, height=300 left = 100, top = 100');\r\n}\r\n"
"// End -->\r\n"
"</script>\r\n");
}
/* BODY start */
tcp_qprintf(tq, "</head><body>\r\n");
if(javascript)
tcp_qprintf(tq,
"<div id=\"overDiv\" style=\"position:absolute; "
"visibility:hidden; z-index:1000;\"></div>\r\n");
}
static void
html_footer(tcp_queue_t *tq)
{
tcp_qprintf(tq,
"</body></html>\r\n\r\n");
}
static void
box_top(tcp_queue_t *tq, const char *style)
{
tcp_qprintf(tq, "<div id=\"%s\">"
"<div class=\"roundtop\">"
"<div class=\"r1\"></div>"
"<div class=\"r2\"></div>"
"<div class=\"r3\"></div>"
"<div class=\"r4\"></div>"
"</div>", style);
}
static void
box_bottom(tcp_queue_t *tq)
{
tcp_qprintf(tq,
"<div class=\"roundbottom\">"
"<div class=\"r4\"></div>"
"<div class=\"r3\"></div>"
"<div class=\"r2\"></div>"
"<div class=\"r1\"></div>"
"</div></div>");
}
static void
top_menu(tcp_queue_t *tq)
{
box_top(tq, "box");
tcp_qprintf(tq,
"<div class=\"content\">"
"<ul id=\"meny\">"
"<li><a href=\"/\">TV Guide</a></li>"
"<li><a href=\"/pvrlog\">Recordings</a></li>"
"<li><a href=\"/status\">System Status</a></li>"
"</ul></div>");
box_bottom(tq);
tcp_qprintf(tq, "<br>\r\n");
}
void
esacpe_char(char *dst, int dstlen, const char *src, char c,
const char *repl)
{
char v;
const char *r;
while((v = *src++) && dstlen > 1) {
if(v != c) {
*dst++ = v;
dstlen--;
} else {
r = repl;
while(dstlen > 1 && *r) {
*dst++ = *r++;
dstlen--;
}
}
}
*dst = 0;
}
static int
is_client_simple(http_connection_t *hc)
{
char *c;
if((c = http_arg_get(&hc->hc_args, "UA-OS")) != NULL) {
if(strstr(c, "Windows CE") || strstr(c, "Pocket PC"))
return 1;
}
if((c = http_arg_get(&hc->hc_args, "x-wap-profile")) != NULL) {
return 1;
}
return 0;
}
/*
* Output_event
*/
static void
output_event(http_connection_t *hc, tcp_queue_t *tq, th_channel_t *ch,
event_t *e, int simple)
{
char title[100];
char bufa[4000];
char overlibstuff[4000];
struct tm a, b;
time_t stop;
event_t *cur;
tv_pvr_status_t pvrstatus;
const char *pvr_txt, *pvr_color;
localtime_r(&e->e_start, &a);
stop = e->e_start + e->e_duration;
localtime_r(&stop, &b);
pvrstatus = pvr_prog_status(e);
cur = epg_event_get_current(ch);
if(!simple && e->e_desc != NULL) {
esacpe_char(bufa, sizeof(bufa), e->e_desc, '\'', "");
snprintf(overlibstuff, sizeof(overlibstuff),
"onmouseover=\"return overlib('%s')\" "
"onmouseout=\"return nd();\"",
bufa);
} else {
overlibstuff[0] = 0;
}
if(1 || simple) {
snprintf(bufa, sizeof(bufa),
"/event/%d", e->e_tag);
} else {
snprintf(bufa, sizeof(bufa),
"javascript:epop('/event/%d')", e->e_tag);
}
esacpe_char(title, sizeof(title), e->e_title, '"', "'");
tcp_qprintf(tq,
"<div>"
"<a href=\"%s\" %s>"
"<span style=\"width: %dpx;float: left%s\">"
"%02d:%02d - %02d:%02d</span>"
"<span style=\"width: %dpx; float: left%s\">%s</span></a>",
bufa,
overlibstuff,
simple ? 80 : 100,
e == cur ? ";font-weight:bold" : "",
a.tm_hour, a.tm_min, b.tm_hour, b.tm_min,
simple ? 100 : 250,
e == cur ? ";font-weight:bold" : "",
title
);
if(!pvrstatus_to_html(pvrstatus, &pvr_txt, &pvr_color)) {
tcp_qprintf(tq,
"<span style=\"font-style:italic;color:%s;font-weight:bold\">"
"%s</span></div><br>",
pvr_color, pvr_txt);
} else {
tcp_qprintf(tq, "</div><br>");
}
}
/*
* Root page
*/
static int
page_root(http_connection_t *hc, const char *remain, void *opaque)
{
tcp_queue_t tq;
th_channel_t *ch;
event_t *e;
int i;
int simple = is_client_simple(hc);
tcp_init_queue(&tq, -1);
html_header(&tq, "HTS/tvheadend", !simple);
top_menu(&tq);
epg_lock();
TAILQ_FOREACH(ch, &channels, ch_global_link) {
box_top(&tq, "box");
tcp_qprintf(&tq, "<div class=\"content3\">");
if(!simple) {
tcp_qprintf(&tq, "<div class=\"logo\">");
if(ch->ch_icon) {
tcp_qprintf(&tq, "<a href=\"channel/%d\">"
"<img src=\"%s\" height=56px>"
"</a>",
ch->ch_tag,
refstr_get(ch->ch_icon));
}
tcp_qprintf(&tq, "</div>");
}
tcp_qprintf(&tq, "<div class=\"over\">");
tcp_qprintf(&tq, "<strong><a href=\"channel/%d\">%s</a></strong><br>",
ch->ch_tag, ch->ch_name);
e = epg_event_find_current_or_upcoming(ch);
for(i = 0; i < 3 && e != NULL; i++) {
output_event(hc, &tq, ch, e, simple);
e = TAILQ_NEXT(e, e_link);
}
tcp_qprintf(&tq, "</div></div>");
box_bottom(&tq);
tcp_qprintf(&tq, "<br>\r\n");
}
epg_unlock();
html_footer(&tq);
http_output_queue(hc, &tq, "text/html; charset=UTF-8");
return 0;
}
/*
* Channel page
*/
const char *days[7] = {
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
};
static int
page_channel(http_connection_t *hc, const char *remain, void *opaque)
{
tcp_queue_t tq;
th_channel_t *ch = NULL;
event_t *e = NULL;
int i;
int simple = is_client_simple(hc);
int channeltag = -1;
int w, doff = 0, wday;
struct tm a;
i = sscanf(remain, "%d/%d", &channeltag, &doff);
ch = channel_by_tag(channeltag);
if(i != 2)
doff = 0;
if(ch == NULL) {
http_error(hc, 404);
return 0;
}
tcp_init_queue(&tq, -1);
html_header(&tq, "HTS/tvheadend", !simple);
top_menu(&tq);
epg_lock();
box_top(&tq, "box");
tcp_qprintf(&tq, "<div class=\"content\">");
tcp_qprintf(&tq, "<strong><a href=\"channel/%d\">%s</a></strong><br>",
ch->ch_tag, ch->ch_name);
e = epg_event_find_current_or_upcoming(ch);
if(e != NULL) {
localtime_r(&e->e_start, &a);
wday = a.tm_wday;
for(w = 0; w < 7; w++) {
tcp_qprintf(&tq,
"<a href=\"/channel/%d/%d\""
"<u><i>%s</i></u></a><br>",
ch->ch_tag, w,
days[(wday + w) % 7]);
while(e != NULL) {
localtime_r(&e->e_start, &a);
if(a.tm_wday != wday + w)
break;
if(a.tm_wday == wday + doff)
output_event(hc, &tq, ch, e, simple);
e = TAILQ_NEXT(e, e_link);
}
}
}
tcp_qprintf(&tq, "</div>");
box_bottom(&tq);
tcp_qprintf(&tq, "<br>\r\n");
epg_unlock();
html_footer(&tq);
http_output_queue(hc, &tq, "text/html; charset=UTF-8");
return 0;
}
/**
* Event page
*/
static int
page_event(http_connection_t *hc, const char *remain, void *opaque)
{
tcp_queue_t tq;
int simple = is_client_simple(hc);
int eventid = atoi(remain);
char title[100];
event_t *e;
struct tm a, b;
time_t stop;
char desc[4000];
recop_t cmd = 0;
tv_pvr_status_t pvrstatus;
const char *pvr_txt, *pvr_color;
epg_lock();
e = epg_event_find_by_tag(eventid);
if(e == NULL) {
epg_unlock();
return 404;
}
remain = strchr(remain, '?');
if(remain != NULL) {
remain++;
if(!strncmp(remain, "rec=", 4))
cmd = RECOP_ONCE;
if(!strncmp(remain, "cancel=", 7))
cmd = RECOP_CANCEL;
pvr_event_record_op(e->e_ch, e, cmd);
}
pvrstatus = pvr_prog_status(e);
localtime_r(&e->e_start, &a);
stop = e->e_start + e->e_duration;
localtime_r(&stop, &b);
tcp_init_queue(&tq, -1);
html_header(&tq, "HTS/tvheadend", 0);
top_menu(&tq);
tcp_qprintf(&tq, "<form method=\"get\" action=\"/event/%d\">", eventid);
box_top(&tq, "box");
tcp_qprintf(&tq, "<div class=\"content\">");
esacpe_char(title, sizeof(title), e->e_title, '"', "'");
esacpe_char(desc, sizeof(desc), e->e_desc, '\'', "");
tcp_qprintf(&tq,
"<div style=\"width: %dpx;float: left;font-weight:bold\">"
"%02d:%02d - %02d:%02d</div>"
"<div style=\"width: %dpx; float: left;font-weight:bold\">"
"%s</div></a>",
simple ? 80 : 100,
a.tm_hour, a.tm_min, b.tm_hour, b.tm_min,
simple ? 100 : 250,
title);
if(!pvrstatus_to_html(pvrstatus, &pvr_txt, &pvr_color))
tcp_qprintf(&tq,
"<div style=\"font-style:italic;color:%s;font-weight:bold\">"
"%s</div>",
pvr_color, pvr_txt);
else
tcp_qprintf(&tq, "<br>");
tcp_qprintf(&tq, "<br>%s", desc);
tcp_qprintf(&tq,"<div style=\"text-align: center\">");
switch(pvrstatus) {
case HTSTV_PVR_STATUS_SCHEDULED:
case HTSTV_PVR_STATUS_RECORDING:
tcp_qprintf(&tq,
"<input type=\"submit\" name=\"cancel\" "
"value=\"Cancel recording\">");
break;
case HTSTV_PVR_STATUS_NONE:
tcp_qprintf(&tq,
"<input type=\"submit\" name=\"rec\" "
"value=\"Record\">");
break;
default:
tcp_qprintf(&tq,
"<input type=\"submit\" name=\"cancel\" "
"value=\"Clear error status\">");
break;
}
tcp_qprintf(&tq, "</div></div></div>");
box_bottom(&tq);
tcp_qprintf(&tq, "</form>\r\n");
html_footer(&tq);
http_output_queue(hc, &tq, "text/html; charset=UTF-8");
epg_unlock();
return 0;
}
static int
pvrcmp(const void *A, const void *B)
{
const pvr_rec_t *a = *(const pvr_rec_t **)A;
const pvr_rec_t *b = *(const pvr_rec_t **)B;
return a->pvrr_start - b->pvrr_start;
}
/**
* Event page
*/
static int
page_pvrlog(http_connection_t *hc, const char *remain, void *opaque)
{
tcp_queue_t tq;
int simple = is_client_simple(hc);
pvr_rec_t *pvrr;
event_t *e;
char escapebuf[4000], href[4000];
char title[100];
char channel[100];
struct tm a, b, day;
const char *pvr_txt, *pvr_color;
int c, i;
pvr_rec_t **pv;
tcp_init_queue(&tq, -1);
html_header(&tq, "HTS/tvheadend", 0);
top_menu(&tq);
box_top(&tq, "box");
epg_lock();
tcp_qprintf(&tq, "<div class=\"content\">");
c = 0;
LIST_FOREACH(pvrr, &pvrr_global_list, pvrr_global_link)
c++;
pv = alloca(c * sizeof(pvr_rec_t *));
i = 0;
LIST_FOREACH(pvrr, &pvrr_global_list, pvrr_global_link)
pv[i++] = pvrr;
qsort(pv, i, sizeof(pvr_rec_t *), pvrcmp);
memset(&day, -1, sizeof(struct tm));
for(i = 0; i < c; i++) {
pvrr = pv[i];
e = epg_event_find_by_time(pvrr->pvrr_channel, pvrr->pvrr_start);
if(e != NULL && !simple && e->e_desc != NULL) {
esacpe_char(escapebuf, sizeof(escapebuf), e->e_desc, '\'', "");
snprintf(href, sizeof(href),
"<a href=\"/event/%d\" onmouseover=\"return overlib('%s')\" "
"onmouseout=\"return nd();\">",
e->e_tag,
escapebuf);
} else {
href[0] = 0;
}
esacpe_char(channel, sizeof(channel),
pvrr->pvrr_channel->ch_name, '"', "'");
esacpe_char(title, sizeof(title),
pvrr->pvrr_title ?: "Unnamed recording", '"', "'");
localtime_r(&pvrr->pvrr_start, &a);
localtime_r(&pvrr->pvrr_stop, &b);
if(a.tm_wday != day.tm_wday ||
a.tm_mday != day.tm_mday ||
a.tm_mon != day.tm_mon ||
a.tm_year != day.tm_year) {
memcpy(&day, &a, sizeof(struct tm));
tcp_qprintf(&tq,
"<br><b><i>%s, %d/%d</i></b><br>",
days[day.tm_wday],
day.tm_mday,
day.tm_mon + 1);
}
tcp_qprintf(&tq,
"%s"
"<div style=\"width: %dpx;float: left\">"
"%s</div>"
"<div style=\"width: %dpx;float: left\">"
"%02d:%02d - %02d:%02d</div>"
"<div style=\"width: %dpx; float: left\">%s</div>%s",
href,
simple ? 80 : 100,
channel,
simple ? 80 : 100,
a.tm_hour, a.tm_min, b.tm_hour, b.tm_min,
simple ? 100 : 250,
title,
href[0] ? "</a>" : "");
if(!pvrstatus_to_html(pvrr->pvrr_status, &pvr_txt, &pvr_color)) {
tcp_qprintf(&tq,
"<div style=\"font-style:italic;color:%s;font-weight:bold\">"
"%s</div>",
pvr_color, pvr_txt);
} else {
tcp_qprintf(&tq, "<br>");
}
}
epg_unlock();
tcp_qprintf(&tq, "<br></div>\r\n");
box_bottom(&tq);
tcp_qprintf(&tq, "</form>\r\n");
html_footer(&tq);
http_output_queue(hc, &tq, "text/html; charset=UTF-8");
return 0;
}
/**
* HTML user interface setup code
*/
void
htmlui_start(void)
{
http_path_add("/", NULL, page_root);
http_path_add("/event", NULL, page_event);
http_path_add("/channel", NULL, page_channel);
http_path_add("/pvrlog", NULL, page_pvrlog);
}

26
htmlui.h Normal file
View file

@ -0,0 +1,26 @@
/*
* tvheadend, HTML user interface
* Copyright (C) 2007 Andreas Öman
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <htmlui://www.gnu.org/licenses/>.
*/
#ifndef HTMLUI_H_
#define HTMLUI_H_
#include "http.h"
void htmlui_start(void);
#endif /* HTMLUI_H_ */