/* * epg.c: Electronic Program Guide * * See the main source file 'vdr.c' for copyright information and * how to reach the author. * * Original version (as used in VDR before 1.3.0) written by * Robert Schneider and Rolf Hakenes . * * $Id: epg.c 1.12 2004/02/22 13:55:12 kls Exp $ */ #include "epg.h" #include "libsi/si.h" #include #include // --- cEvent ---------------------------------------------------------------- cEvent::cEvent(tChannelID ChannelID, u_int16_t EventID) { channelID = ChannelID; eventID = EventID; tableID = 0; version = 0xFF; // actual version numbers are 0..31 runningStatus = 0; title = NULL; shortText = NULL; description = NULL; startTime = 0; duration = 0; vps = 0; } cEvent::~cEvent() { free(title); free(shortText); free(description); } bool cEvent::operator< (const cListObject &ListObject) { cEvent *e = (cEvent *)&ListObject; return startTime < e->startTime; } void cEvent::SetEventID(u_int16_t EventID) { eventID = EventID; } void cEvent::SetTableID(uchar TableID) { tableID = TableID; } void cEvent::SetVersion(uchar Version) { version = Version; } void cEvent::SetRunningStatus(int RunningStatus) { runningStatus = RunningStatus; } void cEvent::SetTitle(const char *Title) { title = strcpyrealloc(title, Title); } void cEvent::SetShortText(const char *ShortText) { shortText = strcpyrealloc(shortText, ShortText); } void cEvent::SetDescription(const char *Description) { description = strcpyrealloc(description, Description); } void cEvent::SetStartTime(time_t StartTime) { startTime = StartTime; } void cEvent::SetDuration(int Duration) { duration = Duration; } void cEvent::SetVps(time_t Vps) { vps = Vps; } const char *cEvent::GetDateString(void) const { static char buf[25]; struct tm tm_r; strftime(buf, sizeof(buf), "%d.%m.%Y", localtime_r(&startTime, &tm_r)); return buf; } const char *cEvent::GetTimeString(void) const { static char buf[25]; struct tm tm_r; strftime(buf, sizeof(buf), "%R", localtime_r(&startTime, &tm_r)); return buf; } const char *cEvent::GetEndTimeString(void) const { static char buf[25]; time_t EndTime = startTime + duration; struct tm tm_r; strftime(buf, sizeof(buf), "%R", localtime_r(&EndTime, &tm_r)); return buf; } const char *cEvent::GetVpsString(void) const { static char buf[25]; struct tm tm_r; strftime(buf, sizeof(buf), "%d.%m %R", localtime_r(&vps, &tm_r)); return buf; } void cEvent::Dump(FILE *f, const char *Prefix) const { if (startTime + duration >= time(NULL)) { fprintf(f, "%sE %u %ld %d %X\n", Prefix, eventID, startTime, duration, tableID); if (!isempty(title)) fprintf(f, "%sT %s\n", Prefix, title); if (!isempty(shortText)) fprintf(f, "%sS %s\n", Prefix, shortText); if (!isempty(description)) fprintf(f, "%sD %s\n", Prefix, description); if (vps) fprintf(f, "%sV %ld\n", Prefix, vps); fprintf(f, "%se\n", Prefix); } } bool cEvent::Read(FILE *f, cSchedule *Schedule) { if (Schedule) { cEvent *Event = NULL; char *s; while ((s = readline(f)) != NULL) { char *t = skipspace(s + 1); switch (*s) { case 'E': if (!Event) { unsigned int EventID; time_t StartTime; int Duration; unsigned int TableID = 0; int n = sscanf(t, "%u %ld %d %X", &EventID, &StartTime, &Duration, &TableID); if (n == 3 || n == 4) { Event = (cEvent *)Schedule->GetEvent(EventID, StartTime); if (!Event) Event = Schedule->AddEvent(new cEvent(Schedule->ChannelID(), EventID)); if (Event) { Event->SetTableID(TableID); Event->SetStartTime(StartTime); Event->SetDuration(Duration); } } } break; case 'T': if (Event) Event->SetTitle(t); break; case 'S': if (Event) Event->SetShortText(t); break; case 'D': if (Event) Event->SetDescription(t); break; case 'V': if (Event) Event->SetVps(atoi(t)); break; case 'e': Event = NULL; break; case 'c': // to keep things simple we react on 'c' here return true; default: esyslog("ERROR: unexpected tag while reading EPG data: %s", s); return false; } } esyslog("ERROR: unexpected end of file while reading EPG data"); } return false; } #define MAXEPGBUGFIXSTATS 8 #define MAXEPGBUGFIXCHANS 100 struct tEpgBugFixStats { int hits; int n; tChannelID channelIDs[MAXEPGBUGFIXCHANS]; tEpgBugFixStats(void) { hits = n = 0; } }; tEpgBugFixStats EpgBugFixStats[MAXEPGBUGFIXSTATS]; static void EpgBugFixStat(int Number, tChannelID ChannelID) { if (0 <= Number && Number < MAXEPGBUGFIXSTATS) { tEpgBugFixStats *p = &EpgBugFixStats[Number]; p->hits++; int i = 0; for (; i < p->n; i++) { if (p->channelIDs[i] == ChannelID) break; } if (i == p->n && p->n < MAXEPGBUGFIXCHANS) p->channelIDs[p->n++] = ChannelID; } } void ReportEpgBugFixStats(bool Reset) { if (Setup.EPGBugfixLevel > 0) { bool GotHits = false; char buffer[1024]; for (int i = 0; i < MAXEPGBUGFIXSTATS; i++) { const char *delim = "\t"; tEpgBugFixStats *p = &EpgBugFixStats[i]; if (p->hits) { bool PrintedStats = false; char *q = buffer; *buffer = 0; for (int c = 0; c < p->n; c++) { cChannel *channel = Channels.GetByChannelID(p->channelIDs[c], true); if (channel) { if (!GotHits) { dsyslog("====================="); dsyslog("EPG bugfix statistics"); dsyslog("====================="); dsyslog("IF SOMEBODY WHO IS IN CHARGE OF THE EPG DATA FOR ONE OF THE LISTED"); dsyslog("CHANNELS READS THIS: PLEASE TAKE A LOOK AT THE FUNCTION cEvent::FixEpgBugs()"); dsyslog("IN VDR/epg.c TO LEARN WHAT'S WRONG WITH YOUR DATA, AND FIX IT!"); dsyslog("====================="); dsyslog("Fix\tHits\tChannels"); GotHits = true; } if (!PrintedStats) { q += snprintf(q, sizeof(buffer) - (q - buffer), "%d\t%d", i, p->hits); PrintedStats = true; } q += snprintf(q, sizeof(buffer) - (q - buffer), "%s%s", delim, channel->Name()); delim = ", "; if (q - buffer > 80) { q += snprintf(q, sizeof(buffer) - (q - buffer), "%s...", delim); break; } } } if (*buffer) dsyslog("%s", buffer); } if (Reset) p->hits = p->n = 0; } if (GotHits) dsyslog("====================="); } } void cEvent::FixEpgBugs(void) { // VDR can't usefully handle newline characters in the EPG data, so let's // always convert them to blanks (independent of the setting of EPGBugfixLevel): strreplace(title, '\n', ' '); strreplace(shortText, '\n', ' '); strreplace(description, '\n', ' '); // Same for control characters: strreplace(title, '\x86', ' '); strreplace(title, '\x87', ' '); strreplace(shortText, '\x86', ' '); strreplace(shortText, '\x87', ' '); strreplace(description, '\x86', ' '); strreplace(description, '\x87', ' '); if (Setup.EPGBugfixLevel == 0) return; // Some TV stations apparently have their own idea about how to fill in the // EPG data. Let's fix their bugs as good as we can: if (title) { // Some channels put too much information into the ShortText and leave the // Description empty: // // Title // (NAT, Year Min')[ ["ShortText". ]Description] // if (shortText && !description) { if (*shortText == '(') { char *e = strchr(shortText + 1, ')'); if (e) { if (*(e + 1)) { if (*++e == ' ') if (*(e + 1) == '"') e++; } else e = NULL; char *s = e ? strdup(e) : NULL; free(shortText); shortText = s; EpgBugFixStat(0, ChannelID()); // now the fixes #1 and #2 below will handle the rest } } } // Some channels put the ShortText in quotes and use either the ShortText // or the Description field, depending on how long the string is: // // Title // "ShortText". Description // if ((shortText == NULL) != (description == NULL)) { char *p = shortText ? shortText : description; if (*p == '"') { const char *delim = "\"."; char *e = strstr(p + 1, delim); if (e) { *e = 0; char *s = strdup(p + 1); char *d = strdup(e + strlen(delim)); free(shortText); free(description); shortText = s; description = d; EpgBugFixStat(1, ChannelID()); } } } // Some channels put the Description into the ShortText (preceeded // by a blank) if there is no actual ShortText and the Description // is short enough: // // Title // Description // if (shortText && !description) { if (*shortText == ' ') { memmove(shortText, shortText + 1, strlen(shortText)); description = shortText; shortText = NULL; EpgBugFixStat(2, ChannelID()); } } // Sometimes they repeat the Title in the ShortText: // // Title // Title // if (shortText && strcmp(title, shortText) == 0) { free(shortText); shortText = NULL; EpgBugFixStat(3, ChannelID()); } // Some channels put the ShortText between double quotes, which is nothing // but annoying (some even put a '.' after the closing '"'): // // Title // "ShortText"[.] // if (shortText && *shortText == '"') { int l = strlen(shortText); if (l > 2 && (shortText[l - 1] == '"' || (shortText[l - 1] == '.' && shortText[l - 2] == '"'))) { memmove(shortText, shortText + 1, l); char *p = strrchr(shortText, '"'); if (p) *p = 0; EpgBugFixStat(4, ChannelID()); } } if (Setup.EPGBugfixLevel <= 1) return; // Some channels apparently try to do some formatting in the texts, // which is a bad idea because they have no way of knowing the width // of the window that will actually display the text. // Remove excess whitespace: title = compactspace(title); shortText = compactspace(shortText); description = compactspace(description); // Remove superfluous hyphens: if (description) { char *p = description; while (*p && *(p + 1) && *(p + 2)) { if (*p == '-' && *(p + 1) == ' ' && *(p + 2) && islower(*(p - 1)) && islower(*(p + 2))) { if (!startswith(p + 2, "und ")) { // special case in German, as in "Lach- und Sachgeschichten" memmove(p, p + 2, strlen(p + 2) + 1); EpgBugFixStat(5, ChannelID()); } } p++; } } #define MAX_USEFUL_EPISODE_LENGTH 40 // Some channels put a whole lot of information in the ShortText and leave // the Description totally empty. So if the ShortText length exceeds // MAX_USEFUL_EPISODE_LENGTH, let's put this into the Description // instead: if (!isempty(shortText) && isempty(description)) { if (strlen(shortText) > MAX_USEFUL_EPISODE_LENGTH) { free(description); description = shortText; shortText = NULL; EpgBugFixStat(6, ChannelID()); } } // Some channels put the same information into ShortText and Description. // In that case we delete one of them: if (shortText && description && strcmp(shortText, description) == 0) { if (strlen(shortText) > MAX_USEFUL_EPISODE_LENGTH) { free(shortText); shortText = NULL; } else { free(description); description = NULL; } EpgBugFixStat(7, ChannelID()); } // Some channels use the ` ("backtick") character, where a ' (single quote) // would be normally used. Actually, "backticks" in normal text don't make // much sense, so let's replace them: strreplace(title, '`', '\''); strreplace(shortText, '`', '\''); strreplace(description, '`', '\''); } } // --- cSchedule ------------------------------------------------------------- cSchedule::cSchedule(tChannelID ChannelID) { channelID = ChannelID; } cEvent *cSchedule::AddEvent(cEvent *Event) { events.Add(Event); return Event; } const cEvent *cSchedule::GetPresentEvent(bool CheckRunningStatus) const { const cEvent *pe = NULL; time_t now = time(NULL); for (cEvent *p = events.First(); p; p = events.Next(p)) { if (p->StartTime() <= now && now < p->StartTime() + p->Duration()) { pe = p; if (!CheckRunningStatus) break; } if (CheckRunningStatus && p->RunningStatus() >= SI::RunningStatusPausing) return p; } return pe; } const cEvent *cSchedule::GetFollowingEvent(bool CheckRunningStatus) const { const cEvent *p = GetPresentEvent(CheckRunningStatus); if (p) p = events.Next(p); return p; } const cEvent *cSchedule::GetEvent(u_int16_t EventID, time_t StartTime) const { // Returns either the event info with the given EventID or, if that one can't // be found, the one with the given StartTime (or NULL if neither can be found) cEvent *pt = NULL; for (cEvent *pe = events.First(); pe; pe = events.Next(pe)) { if (pe->EventID() == EventID) return pe; if (StartTime > 0 && pe->StartTime() == StartTime) // 'StartTime < 0' is apparently used with NVOD channels pt = pe; } return pt; } const cEvent *cSchedule::GetEventAround(time_t Time) const { const cEvent *pe = NULL; time_t delta = INT_MAX; for (cEvent *p = events.First(); p; p = events.Next(p)) { time_t dt = Time - p->StartTime(); if (dt >= 0 && dt < delta && p->StartTime() + p->Duration() >= Time) { delta = dt; pe = p; } } return pe; } void cSchedule::SetRunningStatus(cEvent *Event, int RunningStatus) { for (cEvent *p = events.First(); p; p = events.Next(p)) { if (p == Event) p->SetRunningStatus(RunningStatus); else if (RunningStatus >= SI::RunningStatusPausing && p->RunningStatus() > SI::RunningStatusNotRunning) p->SetRunningStatus(SI::RunningStatusNotRunning); } } void cSchedule::ResetVersions(void) { for (cEvent *p = events.First(); p; p = events.Next(p)) p->SetVersion(0xFF); } void cSchedule::Sort(void) { events.Sort(); } void cSchedule::Cleanup(void) { Cleanup(time(NULL)); } void cSchedule::Cleanup(time_t Time) { cEvent *Event; for (int a = 0; true ; a++) { Event = events.Get(a); if (!Event) break; if (Event->StartTime() + Event->Duration() + Setup.EPGLinger * 60 + 3600 < Time) { // adding one hour for safety events.Del(Event); a--; } } } void cSchedule::Dump(FILE *f, const char *Prefix) const { cChannel *channel = Channels.GetByChannelID(channelID, true); if (channel) { fprintf(f, "%sC %s %s\n", Prefix, channel->GetChannelID().ToString(), channel->Name()); for (cEvent *p = events.First(); p; p = events.Next(p)) p->Dump(f, Prefix); fprintf(f, "%sc\n", Prefix); } } bool cSchedule::Read(FILE *f, cSchedules *Schedules) { if (Schedules) { char *s; while ((s = readline(f)) != NULL) { if (*s == 'C') { s = skipspace(s + 1); char *p = strchr(s, ' '); if (p) *p = 0; // strips optional channel name if (*s) { tChannelID channelID = tChannelID::FromString(s); if (channelID.Valid()) { cSchedule *p = Schedules->AddSchedule(channelID); if (p) { if (!cEvent::Read(f, p)) return false; p->Sort(); } } else { esyslog("ERROR: illegal channel ID: %s", s); return false; } } } else { esyslog("ERROR: unexpected tag while reading EPG data: %s", s); return false; } } return true; } return false; } // --- cSchedulesLock -------------------------------------------------------- cSchedulesLock::cSchedulesLock(bool WriteLock, int TimeoutMs) { locked = cSchedules::schedules.rwlock.Lock(WriteLock, TimeoutMs); } cSchedulesLock::~cSchedulesLock() { if (locked) cSchedules::schedules.rwlock.Unlock(); } // --- cSchedules ------------------------------------------------------------ cSchedules cSchedules::schedules; const char *cSchedules::epgDataFileName = NULL; time_t cSchedules::lastCleanup = time(NULL); time_t cSchedules::lastDump = time(NULL); const cSchedules *cSchedules::Schedules(cSchedulesLock &SchedulesLock) { return SchedulesLock.Locked() ? &schedules : NULL; } void cSchedules::SetEpgDataFileName(const char *FileName) { delete epgDataFileName; if (FileName) epgDataFileName = strdup(FileName); } void cSchedules::Cleanup(bool Force) { if (Force) lastDump = 0; time_t now = time(NULL); struct tm tm_r; struct tm *ptm = localtime_r(&now, &tm_r); if (now - lastCleanup > 3600 && ptm->tm_hour == 5) { isyslog("cleaning up schedules data"); cSchedulesLock SchedulesLock(true, 1000); cSchedules *s = (cSchedules *)Schedules(SchedulesLock); if (s) { for (cSchedule *p = s->First(); p; p = s->Next(p)) p->Cleanup(now); } lastCleanup = now; ReportEpgBugFixStats(true); } if (epgDataFileName && now - lastDump > 600) { cSafeFile f(epgDataFileName); if (f.Open()) { Dump(f); f.Close(); } else LOG_ERROR; lastDump = now; } } void cSchedules::ResetVersions(void) { cSchedulesLock SchedulesLock(true); cSchedules *s = (cSchedules *)Schedules(SchedulesLock); if (s) { for (cSchedule *Schedule = s->First(); Schedule; Schedule = s->Next(Schedule)) Schedule->ResetVersions(); } } bool cSchedules::ClearAll(void) { cSchedulesLock SchedulesLock(true, 1000); cSchedules *s = (cSchedules *)Schedules(SchedulesLock); if (s) { s->Clear(); return true; } return false; } bool cSchedules::Dump(FILE *f, const char *Prefix) { cSchedulesLock SchedulesLock; cSchedules *s = (cSchedules *)Schedules(SchedulesLock); if (s) { for (cSchedule *p = s->First(); p; p = s->Next(p)) p->Dump(f, Prefix); return true; } return false; } bool cSchedules::Read(FILE *f) { cSchedulesLock SchedulesLock(true, 1000); cSchedules *s = (cSchedules *)Schedules(SchedulesLock); if (s) { bool OwnFile = f == NULL; if (OwnFile) { if (epgDataFileName && access(epgDataFileName, R_OK) == 0) { dsyslog("reading EPG data from %s", epgDataFileName); if ((f = fopen(epgDataFileName, "r")) == NULL) { LOG_ERROR; return false; } } else return false; } bool result = cSchedule::Read(f, s); if (OwnFile) fclose(f); return result; } return false; } cSchedule *cSchedules::AddSchedule(tChannelID ChannelID) { ChannelID.ClrRid(); cSchedule *p = (cSchedule *)GetSchedule(ChannelID); if (!p) { p = new cSchedule(ChannelID); Add(p); } return p; } const cSchedule *cSchedules::GetSchedule(tChannelID ChannelID) const { ChannelID.ClrRid(); for (cSchedule *p = First(); p; p = Next(p)) { if (p->ChannelID() == ChannelID) return p; } return NULL; }