/* * 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 "tvhead.h" #include "channels.h" #include "epg.h" #include "dispatch.h" #include "htsp.h" #include "autorec.h" #define EPG_MAX_AGE 86400 #define EPG_HASH_ID_WIDTH 256 struct event_list epg_hash[EPG_HASH_ID_WIDTH]; static dtimer_t epg_channel_maintain_timer; epg_content_group_t *epg_content_groups[16]; void epg_event_set_title(event_t *e, const char *title) { free((void *)e->e_title); e->e_title = strdup(title); } void epg_event_set_desc(event_t *e, const char *desc) { free((void *)e->e_desc); e->e_desc = strdup(desc); } static void epg_event_set_content_type(event_t *e, epg_content_type_t *ect) { 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); } event_t * epg_event_find_by_time0(struct event_queue *q, time_t start) { event_t *e; TAILQ_FOREACH(e, q, e_channel_link) if(start >= e->e_start && start < e->e_start + e->e_duration) break; return e; } event_t * epg_event_find_by_time(channel_t *ch, time_t start) { return epg_event_find_by_time0(&ch->ch_epg_events, start); } event_t * epg_event_find_by_tag(uint32_t tag) { event_t *e; unsigned int l = tag % EPG_HASH_ID_WIDTH; LIST_FOREACH(e, &epg_hash[l], e_hash_link) if(e->e_tag == tag) break; return e; } event_t * epg_event_get_current(channel_t *ch) { event_t *e; time_t now; time(&now); e = ch->ch_epg_cur_event; if(e == NULL || now < e->e_start || now > e->e_start + e->e_duration) return NULL; return e; } event_t * epg_event_find_current_or_upcoming(channel_t *ch) { event_t *e; TAILQ_FOREACH(e, &ch->ch_epg_events, e_channel_link) if(e->e_start + e->e_duration > dispatch_clock) break; return e; } static int startcmp(event_t *a, event_t *b) { return a->e_start - b->e_start; } event_t * epg_event_build(struct event_queue *head, time_t start, int duration) { time_t now; event_t *e; time(&now); if(duration < 1 || start + duration < now - EPG_MAX_AGE) return NULL; TAILQ_FOREACH(e, head, e_channel_link) if(start == e->e_start && duration == e->e_duration) return e; e = calloc(1, sizeof(event_t)); e->e_duration = duration; e->e_start = start; TAILQ_INSERT_SORTED(head, e, e_channel_link, startcmp); return e; } void epg_event_free(event_t *e) { if(e->e_content_type != NULL) LIST_REMOVE(e, e_content_type_link); free((void *)e->e_title); free((void *)e->e_desc); free(e); } static void epg_event_destroy(channel_t *ch, event_t *e) { // printf("epg: flushed event %s\n", e->e_title); if(ch->ch_epg_cur_event == e) ch->ch_epg_cur_event = NULL; TAILQ_REMOVE(&ch->ch_epg_events, e, e_channel_link); LIST_REMOVE(e, e_hash_link); epg_event_free(e); } void event_time_txt(time_t start, int duration, char *out, int outlen) { char tmp1[40]; char tmp2[40]; char *c; time_t stop = start + duration; ctime_r(&start, tmp1); c = strchr(tmp1, '\n'); if(c) *c = 0; ctime_r(&stop, tmp2); c = strchr(tmp2, '\n'); if(c) *c = 0; snprintf(out, outlen, "[%s - %s]", tmp1, tmp2); } static int check_overlap0(channel_t *ch, event_t *a) { char atime[100]; char btime[100]; event_t *b; int overshot; b = TAILQ_NEXT(a, e_channel_link); if(b == NULL) return 0; overshot = a->e_start + a->e_duration - b->e_start; if(overshot < 1) return 0; event_time_txt(a->e_start, a->e_duration, atime, sizeof(atime)); event_time_txt(b->e_start, b->e_duration, btime, sizeof(btime)); if(a->e_source > b->e_source) { b->e_start += overshot; b->e_duration -= overshot; if(b->e_duration < 1) { epg_event_destroy(ch, b); return 1; } } else { a->e_duration -= overshot; if(a->e_duration < 1) { epg_event_destroy(ch, a); return 1; } } return 0; } static int check_overlap(channel_t *ch, event_t *e) { event_t *p; p = TAILQ_PREV(e, event_queue, e_channel_link); if(p != NULL) { if(check_overlap0(ch, p)) return 1; } return check_overlap0(ch, e); } static void epg_event_create(channel_t *ch, time_t start, int duration, const char *title, const char *desc, int source, uint16_t id, epg_content_type_t *ect) { unsigned int l; time_t now; event_t *e; time(&now); if(duration < 1 || start + duration < now - EPG_MAX_AGE) return; TAILQ_FOREACH(e, &ch->ch_epg_events, e_channel_link) { if(start == e->e_start && duration == e->e_duration) break; if(start == e->e_start && !strcmp(e->e_title ?: "", title)) break; } if(e == NULL) { e = calloc(1, sizeof(event_t)); e->e_start = start; TAILQ_INSERT_SORTED(&ch->ch_epg_events, e, e_channel_link, startcmp); e->e_channel = ch; e->e_event_id = id; e->e_duration = duration; e->e_tag = tag_get(); l = e->e_tag % EPG_HASH_ID_WIDTH; LIST_INSERT_HEAD(&epg_hash[l], e, e_hash_link); } if(source > e->e_source) { e->e_source = source; if(e->e_duration != duration) { char before[100]; char after[100]; event_time_txt(e->e_start, e->e_duration, before, sizeof(before)); event_time_txt(e->e_start, duration, after, sizeof(after)); e->e_duration = duration; } if(title != NULL) epg_event_set_title(e, title); if(desc != NULL) epg_event_set_desc(e, desc); if(ect != NULL) epg_event_set_content_type(e, ect); } check_overlap(ch, e); } void epg_update_event_by_id(channel_t *ch, uint16_t event_id, time_t start, int duration, const char *title, const char *desc, epg_content_type_t *ect) { event_t *e; TAILQ_FOREACH(e, &ch->ch_epg_events, e_channel_link) if(e->e_event_id == event_id) break; if(e != NULL) { /* We already have information about this event */ if(e->e_duration != duration || e->e_start != start) { char before[100]; char after[100]; event_time_txt(e->e_start, e->e_duration, before, sizeof(before)); event_time_txt(start, duration, after, sizeof(after)); TAILQ_REMOVE(&ch->ch_epg_events, e, e_channel_link); e->e_duration = duration; e->e_start = start; TAILQ_INSERT_SORTED(&ch->ch_epg_events, e, e_channel_link, startcmp); if(check_overlap(ch, e)) return; /* event was destroyed, return at once */ } epg_event_set_title(e, title); epg_event_set_desc(e, desc); epg_event_set_content_type(e, ect); } else { epg_event_create(ch, start, duration, title, desc, EVENT_SRC_DVB, event_id, ect); } } static void epg_set_current_event(channel_t *ch, event_t *e) { if(e == ch->ch_epg_cur_event) return; ch->ch_epg_cur_event = e; /* Notify clients that a new programme is on */ htsp_async_channel_update(ch); if(e == NULL) return; /* Let autorecorder check this event */ autorec_check_new_event(e); e = TAILQ_NEXT(e, e_channel_link); /* .. and next one, to make better scheduling */ if(e != NULL) autorec_check_new_event(e); } static void epg_locate_current_event(channel_t *ch, time_t now) { event_t *e; e = epg_event_find_by_time(ch, now); epg_set_current_event(ch, e); } static void epg_channel_maintain(void *aux, int64_t clk) { channel_t *ch; event_t *e, *cur; time_t now; dtimer_arm(&epg_channel_maintain_timer, epg_channel_maintain, NULL, 5); now = dispatch_clock; RB_FOREACH(ch, &channel_name_tree, ch_name_link) { /* Age out any old events */ e = TAILQ_FIRST(&ch->ch_epg_events); if(e != NULL && e->e_start + e->e_duration < now - EPG_MAX_AGE) epg_event_destroy(ch, e); cur = ch->ch_epg_cur_event; e = ch->ch_epg_cur_event; if(e == NULL) { epg_locate_current_event(ch, now); continue; } if(now >= e->e_start && now < e->e_start + e->e_duration) continue; e = TAILQ_NEXT(e, e_channel_link); if(e != NULL && now >= e->e_start && now < e->e_start + e->e_duration) { epg_set_current_event(ch, e); continue; } epg_locate_current_event(ch, now); } } /** * */ void epg_destroy_by_channel(channel_t *ch) { event_t *e; while((e = TAILQ_FIRST(&ch->ch_epg_events)) != NULL) epg_event_destroy(ch, e); } /** * */ void epg_transfer_events(channel_t *ch, struct event_queue *src, const char *srcname, char *icon) { event_t *e; int cnt = 0; if(strcmp(icon ?: "", ch->ch_icon ?: "")) channel_set_icon(ch, icon); TAILQ_FOREACH(e, src, e_channel_link) { epg_event_create(ch, e->e_start, e->e_duration, e->e_title, e->e_desc, EVENT_SRC_XMLTV, 0, e->e_content_type); cnt++; } } 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", }; /** * 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->ecg_name && !strcmp(name, ecg->ecg_name)) return ecg; } return NULL; } /** * Given the arguments, search all EPG events and enlist them * * XXX: Optimize if we know channel, group, etc */ int epg_search(struct event_list *h, const char *title, epg_content_group_t *s_ecg, channel_t *s_ch) { channel_t *ch; event_t *e; int num = 0; regex_t preg; if(title != NULL && regcomp(&preg, title, REG_ICASE | REG_EXTENDED | REG_NOSUB)) return -1; RB_FOREACH(ch, &channel_name_tree, ch_name_link) { if(LIST_FIRST(&ch->ch_transports) == NULL) continue; if(s_ch != NULL && s_ch != ch) continue; TAILQ_FOREACH(e, &ch->ch_epg_events, e_channel_link) { if(e->e_start + e->e_duration < dispatch_clock) continue; /* old */ if(s_ecg != NULL) { if(e->e_content_type == NULL) continue; if(e->e_content_type->ect_group != s_ecg) continue; } if(title != NULL) { if(regexec(&preg, e->e_title, 0, NULL, 0)) continue; } LIST_INSERT_HEAD(h, e, e_tmp_link); num++; } } if(title != NULL) regfree(&preg); return num; } /* * */ void epg_init(void) { int i; for(i = 0x0; i < 0x100; i+=16) epg_content_type_find_by_dvbcode(i); dtimer_arm(&epg_channel_maintain_timer, epg_channel_maintain, NULL, 5); }