tvheadend/src/epg.c
Andreas Öman 48f845e02c * If the EPG receives an updated description for an even that is shorter
than the current description it will be ignored.
   This typically happens if the XMLTV and DVB EIT (Event Information Table)
   differs. In other words, we assume that a longer description of an
   event is better than a short.
2009-07-26 10:25:12 +00:00

611 lines
12 KiB
C

/*
* Electronic Program Guide - Common functions
* 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 <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>
#include <assert.h>
#include "tvhead.h"
#include "channels.h"
#include "epg.h"
#include "dvr/dvr.h"
#include "htsp.h"
#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 void epg_expire_event_from_channel(void *opauqe);
static void epg_ch_check_current_event(void *aux);
static int
e_ch_cmp(const event_t *a, const event_t *b)
{
return a->e_start - b->e_start;
}
/**
*
*/
static void
epg_set_current(channel_t *ch, event_t *e)
{
if(ch->ch_epg_current == e)
return;
ch->ch_epg_current = e;
if(e != NULL) {
gtimer_arm_abs(&ch->ch_epg_timer_current, epg_ch_check_current_event,
ch, MAX(e->e_stop, dispatch_clock + 1));
dvr_autorec_check(e);
}
htsp_event_update(ch, e);
}
/**
*
*/
static void
epg_ch_check_current_event(void *aux)
{
channel_t *ch = aux;
event_t skel, *e = ch->ch_epg_current;
if(e != NULL) {
if(e->e_start <= dispatch_clock && e->e_stop > dispatch_clock) {
epg_set_current(ch, e);
return;
}
if((e = RB_NEXT(e, e_channel_link)) != NULL) {
if(e->e_start <= dispatch_clock && e->e_stop > dispatch_clock) {
epg_set_current(ch, e);
return;
}
}
}
e = epg_event_find_by_time(ch, dispatch_clock);
if(e != NULL) {
epg_set_current(ch, e);
return;
}
epg_set_current(ch, NULL);
skel.e_start = dispatch_clock;
e = RB_FIND_GT(&ch->ch_epg_events, &skel, e_channel_link, e_ch_cmp);
if(e != NULL) {
gtimer_arm_abs(&ch->ch_epg_timer_current, epg_ch_check_current_event,
ch, MAX(e->e_start, dispatch_clock + 1));
}
}
/**
*
*/
static void
epg_event_changed(event_t *e)
{
/* nothing atm */
}
/**
*
*/
void
epg_event_set_title(event_t *e, const char *title)
{
if(e->e_title != NULL && !strcmp(e->e_title, title))
return;
free(e->e_title);
e->e_title = strdup(title);
epg_event_changed(e);
}
/**
*
*/
void
epg_event_set_desc(event_t *e, const char *desc)
{
if(e->e_desc != NULL && strlen(e->e_desc) >= strlen(desc)) {
/* The current description is longer than the one we try to set.
* We assume that a longer description is better than a shorter
* so we just bail out.
* Typically happens when the XMLTV and DVB EPG feed differs.
*/
return;
}
free(e->e_desc);
e->e_desc = strdup(desc);
epg_event_changed(e);
}
/**
*
*/
void
epg_event_set_content_type(event_t *e, epg_content_type_t *ect)
{
if(e->e_content_type == ect)
return;
if(e->e_content_type != NULL)
LIST_REMOVE(e, e_content_type_link);
e->e_content_type = ect;
if(ect != NULL)
LIST_INSERT_HEAD(&ect->ect_events, e, e_content_type_link);
epg_event_changed(e);
}
/**
*
*/
static void
epg_event_destroy(event_t *e)
{
if(e->e_content_type != NULL)
LIST_REMOVE(e, e_content_type_link);
free(e->e_title);
free(e->e_desc);
LIST_REMOVE(e, e_global_link);
free(e);
}
/**
*
*/
static void
epg_event_unref(event_t *e)
{
if(e->e_refcount > 1) {
e->e_refcount--;
return;
}
assert(e->e_refcount == 1);
epg_event_destroy(e);
}
/**
*
*/
static void
epg_remove_event_from_channel(channel_t *ch, event_t *e)
{
int wasfirst = e == RB_FIRST(&ch->ch_epg_events);
event_t *n = RB_NEXT(e, e_channel_link);
assert(e->e_channel == ch);
RB_REMOVE(&ch->ch_epg_events, e, e_channel_link);
e->e_channel = NULL;
epg_event_unref(e);
if(ch->ch_epg_current == e) {
epg_set_current(ch, NULL);
if(n != NULL)
gtimer_arm_abs(&ch->ch_epg_timer_current, epg_ch_check_current_event,
ch, n->e_start);
}
if(wasfirst && (e = RB_FIRST(&ch->ch_epg_events)) != NULL) {
gtimer_arm_abs(&ch->ch_epg_timer_head, epg_expire_event_from_channel,
ch, e->e_stop);
}
}
/**
*
*/
event_t *
epg_event_create(channel_t *ch, time_t start, time_t stop, int dvb_id)
{
static event_t *skel;
event_t *e, *p, *n;
static int tally;
if((stop - start) > 11 * 3600)
return NULL;
if(stop <= start)
return NULL;
lock_assert(&global_lock);
if(skel == NULL)
skel = calloc(1, sizeof(event_t));
skel->e_start = start;
e = RB_INSERT_SORTED(&ch->ch_epg_events, skel, e_channel_link, e_ch_cmp);
if(e == NULL) {
/* New entry was inserted */
e = skel;
skel = NULL;
e->e_id = ++tally;
e->e_stop = stop;
e->e_dvb_id = dvb_id;
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);
if(e == RB_FIRST(&ch->ch_epg_events)) {
/* First in temporal order, arm expiration timer */
gtimer_arm_abs(&ch->ch_epg_timer_head, epg_expire_event_from_channel,
ch, e->e_stop);
}
if(ch->ch_epg_timer_current.gti_callback == NULL ||
start < ch->ch_epg_timer_current.gti_expire) {
gtimer_arm_abs(&ch->ch_epg_timer_current, epg_ch_check_current_event,
ch, e->e_start);
}
} else {
/* Already exist */
if(stop > e->e_stop) {
/* We allow the event to extend in time */
#if 0
printf("Event %s on %s extended stop time\n", e->e_title,
e->e_channel->ch_name);
printf("Previous %s", ctime(&e->e_stop));
printf(" New %s", ctime(&stop));
#endif
e->e_stop = stop;
epg_event_changed(e);
if(e == ch->ch_epg_current) {
gtimer_arm_abs(&ch->ch_epg_timer_current, epg_ch_check_current_event,
ch, e->e_start);
}
}
}
if(dvb_id != -1) {
/* Erase any close events with the same DVB event id */
if((p = RB_PREV(e, e_channel_link)) != NULL) {
if(p->e_dvb_id == dvb_id) {
epg_remove_event_from_channel(ch, p);
} else if((p = RB_PREV(p, e_channel_link)) != NULL) {
if(p->e_dvb_id == dvb_id)
epg_remove_event_from_channel(ch, p);
}
}
if((n = RB_NEXT(e, e_channel_link)) != NULL) {
if(n->e_dvb_id == dvb_id) {
epg_remove_event_from_channel(ch, n);
} else if((n = RB_NEXT(n, e_channel_link)) != NULL) {
if(n->e_dvb_id == dvb_id)
epg_remove_event_from_channel(ch, n);
}
}
}
return e;
}
/**
*
*/
event_t *
epg_event_find_by_time(channel_t *ch, time_t t)
{
event_t skel, *e;
skel.e_start = t;
e = RB_FIND_LE(&ch->ch_epg_events, &skel, e_channel_link, e_ch_cmp);
if(e == NULL || e->e_stop < t)
return NULL;
return e;
}
/**
*
*/
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;
}
/**
*
*/
static void
epg_expire_event_from_channel(void *opaque)
{
channel_t *ch = opaque;
event_t *e = RB_FIRST(&ch->ch_epg_events);
epg_remove_event_from_channel(ch, e);
}
/**
*
*/
void
epg_unlink_from_channel(channel_t *ch)
{
event_t *e;
gtimer_disarm(&ch->ch_epg_timer_head);
gtimer_disarm(&ch->ch_epg_timer_current);
while((e = ch->ch_epg_events.root) != NULL)
epg_remove_event_from_channel(ch, e);
}
/**
*
*/
static const char *groupnames[16] = {
[0] = "Unclassified",
[1] = "Movie / Drama",
[2] = "News / Current affairs",
[3] = "Show / Games",
[4] = "Sports",
[5] = "Children's/Youth",
[6] = "Music",
[7] = "Art/Culture",
[8] = "Social/Political issues/Economics",
[9] = "Education/Science/Factual topics",
[10] = "Leisure hobbies",
[11] = "Special characteristics",
};
/**
*
*/
const char *
epg_content_group_get_name(unsigned int id)
{
return id < 16 ? groupnames[id] : NULL;
}
/**
* Find a content type
*/
epg_content_type_t *
epg_content_type_find_by_dvbcode(uint8_t dvbcode)
{
epg_content_group_t *ecg;
epg_content_type_t *ect;
int group = dvbcode >> 4;
int type = dvbcode & 0xf;
char buf[20];
ecg = epg_content_groups[group];
if(ecg == NULL) {
ecg = epg_content_groups[group] = calloc(1, sizeof(epg_content_group_t));
ecg->ecg_name = groupnames[group];
}
ect = ecg->ecg_types[type];
if(ect == NULL) {
ect = ecg->ecg_types[type] = calloc(1, sizeof(epg_content_type_t));
ect->ect_group = ecg;
snprintf(buf, sizeof(buf), "type%d", type);
ect->ect_name = strdup(buf);
}
return ect;
}
/**
*
*/
epg_content_group_t *
epg_content_group_find_by_name(const char *name)
{
epg_content_group_t *ecg;
int i;
for(i = 0; i < 16; i++) {
ecg = epg_content_groups[i];
if(ecg != NULL && ecg->ecg_name && !strcmp(name, ecg->ecg_name))
return ecg;
}
return NULL;
}
/*
*
*/
void
epg_init(void)
{
int i;
for(i = 0x0; i < 0x100; i+=16)
epg_content_type_find_by_dvbcode(i);
}
/**
*
*/
static void
eqr_add(epg_query_result_t *eqr, event_t *e, regex_t *preg, time_t now)
{
if(e->e_title == NULL)
return;
if(preg != NULL && regexec(preg, e->e_title, 0, NULL, 0))
return;
if(e->e_stop < now)
return; /* Already passed */
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, time_t now)
{
event_t *e;
if(ecg == NULL) {
RB_FOREACH(e, &ch->ch_epg_events, e_channel_link)
eqr_add(eqr, e, preg, now);
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, now);
}
/**
*
*/
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;
time_t now;
time(&now);
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, now);
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, now);
return;
}
RB_FOREACH(ch, &channel_name_tree, ch_name_link)
epg_query_add_channel(eqr, ch, ecg, preg, now);
}
/**
*
*/
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);
}