/* * 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 5.7 2022/11/22 14:33:48 kls Exp $ */ #include "epg.h" #include #include #include #include "libsi/si.h" #define RUNNINGSTATUSTIMEOUT 30 // seconds before the running status is considered unknown #define EPGDATAWRITEDELTA 600 // seconds between writing the epg.data file // --- tComponent ------------------------------------------------------------ cString tComponent::ToString(void) { char buffer[256]; snprintf(buffer, sizeof(buffer), "%X %02X %s %s", stream, type, language, description ? description : ""); return buffer; } bool tComponent::FromString(const char *s) { unsigned int Stream, Type; int n = sscanf(s, "%X %02X %7s %m[^\n]", &Stream, &Type, language, &description); // 7 = MAXLANGCODE2 - 1 if (n != 4 || isempty(description)) { free(description); description = NULL; } stream = Stream; type = Type; return n >= 3; } // --- cComponents ----------------------------------------------------------- cComponents::cComponents(void) { numComponents = 0; components = NULL; } cComponents::~cComponents(void) { for (int i = 0; i < numComponents; i++) free(components[i].description); free(components); } bool cComponents::Realloc(int Index) { if (Index >= numComponents) { Index++; if (tComponent *NewBuffer = (tComponent *)realloc(components, Index * sizeof(tComponent))) { int n = numComponents; numComponents = Index; components = NewBuffer; memset(&components[n], 0, sizeof(tComponent) * (numComponents - n)); } else { esyslog("ERROR: out of memory"); return false; } } return true; } void cComponents::SetComponent(int Index, const char *s) { if (Realloc(Index)) components[Index].FromString(s); } void cComponents::SetComponent(int Index, uchar Stream, uchar Type, const char *Language, const char *Description) { if (!Realloc(Index)) return; tComponent *p = &components[Index]; p->stream = Stream; p->type = Type; strn0cpy(p->language, Language, sizeof(p->language)); char *q = strchr(p->language, ','); if (q) *q = 0; // strips rest of "normalized" language codes p->description = strcpyrealloc(p->description, !isempty(Description) ? Description : NULL); } tComponent *cComponents::GetComponent(int Index, uchar Stream, uchar Type) { for (int i = 0; i < numComponents; i++) { if (components[i].stream == Stream && ( Type == 0 || // don't care about the actual Type Stream == 2 && (components[i].type < 5) == (Type < 5) // fallback "Dolby" component according to the "Premiere pseudo standard" )) { if (!Index--) return &components[i]; } } return NULL; } // --- cEvent ---------------------------------------------------------------- cMutex cEvent::numTimersMutex; cEvent::cEvent(tEventID EventID) { schedule = NULL; numTimers = 0; eventID = EventID; tableID = 0xFF; // actual table ids are 0x4E..0x60 version = 0xFF; // actual version numbers are 0..31 runningStatus = SI::RunningStatusUndefined; title = NULL; shortText = NULL; description = NULL; components = NULL; memset(contents, 0, sizeof(contents)); parentalRating = 0; startTime = 0; duration = 0; vps = 0; aux = NULL; SetSeen(); } cEvent::~cEvent() { free(title); free(shortText); free(description); free(aux); delete components; } int cEvent::Compare(const cListObject &ListObject) const { cEvent *e = (cEvent *)&ListObject; return startTime - e->startTime; } tChannelID cEvent::ChannelID(void) const { return schedule ? schedule->ChannelID() : tChannelID(); } void cEvent::SetEventID(tEventID EventID) { if (eventID != EventID) { if (schedule) schedule->UnhashEvent(this); eventID = EventID; if (schedule) schedule->HashEvent(this); } } void cEvent::SetTableID(uchar TableID) { tableID = TableID; } void cEvent::SetVersion(uchar Version) { version = Version; } void cEvent::SetRunningStatus(int RunningStatus, const cChannel *Channel) { if (Channel && runningStatus != RunningStatus && (RunningStatus > SI::RunningStatusNotRunning || runningStatus > SI::RunningStatusUndefined) && schedule && schedule->HasTimer()) isyslog("channel %d (%s) event %s status %d", Channel->Number(), Channel->Name(), *ToDescr(), 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::SetComponents(cComponents *Components) { delete components; components = Components; } void cEvent::SetContents(uchar *Contents) { for (int i = 0; i < MaxEventContents; i++) contents[i] = Contents[i]; } void cEvent::SetParentalRating(int ParentalRating) { parentalRating = ParentalRating; } void cEvent::SetStartTime(time_t StartTime) { if (startTime != StartTime) { if (schedule) schedule->UnhashEvent(this); startTime = StartTime; if (schedule) schedule->HashEvent(this); } } void cEvent::SetDuration(int Duration) { duration = Duration; } void cEvent::SetVps(time_t Vps) { vps = Vps; } void cEvent::SetSeen(void) { seen = time(NULL); } void cEvent::SetAux(const char *Aux) { free(aux); aux = Aux ? strdup(Aux) : NULL; } cString cEvent::ToDescr(void) const { char vpsbuf[64] = ""; if (Vps()) sprintf(vpsbuf, "(VPS: %s) ", *GetVpsString()); return cString::sprintf("%s %s-%s %s'%s'", *GetDateString(), *GetTimeString(), *GetEndTimeString(), vpsbuf, Title()); } void cEvent::IncNumTimers(void) const { numTimersMutex.Lock(); numTimers++; if (schedule) schedule->IncNumTimers(); numTimersMutex.Unlock(); } void cEvent::DecNumTimers(void) const { numTimersMutex.Lock(); numTimers--; if (schedule) schedule->DecNumTimers(); numTimersMutex.Unlock(); } bool cEvent::IsRunning(bool OrAboutToStart) const { return runningStatus >= (OrAboutToStart ? SI::RunningStatusStartsInAFewSeconds : SI::RunningStatusPausing); } const char *cEvent::ContentToString(uchar Content) { switch (Content & 0xF0) { case ecgMovieDrama: switch (Content & 0x0F) { default: case 0x00: return tr("Content$Movie/Drama"); case 0x01: return tr("Content$Detective/Thriller"); case 0x02: return tr("Content$Adventure/Western/War"); case 0x03: return tr("Content$Science Fiction/Fantasy/Horror"); case 0x04: return tr("Content$Comedy"); case 0x05: return tr("Content$Soap/Melodrama/Folkloric"); case 0x06: return tr("Content$Romance"); case 0x07: return tr("Content$Serious/Classical/Religious/Historical Movie/Drama"); case 0x08: return tr("Content$Adult Movie/Drama"); } break; case ecgNewsCurrentAffairs: switch (Content & 0x0F) { default: case 0x00: return tr("Content$News/Current Affairs"); case 0x01: return tr("Content$News/Weather Report"); case 0x02: return tr("Content$News Magazine"); case 0x03: return tr("Content$Documentary"); case 0x04: return tr("Content$Discussion/Inverview/Debate"); } break; case ecgShow: switch (Content & 0x0F) { default: case 0x00: return tr("Content$Show/Game Show"); case 0x01: return tr("Content$Game Show/Quiz/Contest"); case 0x02: return tr("Content$Variety Show"); case 0x03: return tr("Content$Talk Show"); } break; case ecgSports: switch (Content & 0x0F) { default: case 0x00: return tr("Content$Sports"); case 0x01: return tr("Content$Special Event"); case 0x02: return tr("Content$Sport Magazine"); case 0x03: return tr("Content$Football/Soccer"); case 0x04: return tr("Content$Tennis/Squash"); case 0x05: return tr("Content$Team Sports"); case 0x06: return tr("Content$Athletics"); case 0x07: return tr("Content$Motor Sport"); case 0x08: return tr("Content$Water Sport"); case 0x09: return tr("Content$Winter Sports"); case 0x0A: return tr("Content$Equestrian"); case 0x0B: return tr("Content$Martial Sports"); } break; case ecgChildrenYouth: switch (Content & 0x0F) { default: case 0x00: return tr("Content$Children's/Youth Programme"); case 0x01: return tr("Content$Pre-school Children's Programme"); case 0x02: return tr("Content$Entertainment Programme for 6 to 14"); case 0x03: return tr("Content$Entertainment Programme for 10 to 16"); case 0x04: return tr("Content$Informational/Educational/School Programme"); case 0x05: return tr("Content$Cartoons/Puppets"); } break; case ecgMusicBalletDance: switch (Content & 0x0F) { default: case 0x00: return tr("Content$Music/Ballet/Dance"); case 0x01: return tr("Content$Rock/Pop"); case 0x02: return tr("Content$Serious/Classical Music"); case 0x03: return tr("Content$Folk/Tradional Music"); case 0x04: return tr("Content$Jazz"); case 0x05: return tr("Content$Musical/Opera"); case 0x06: return tr("Content$Ballet"); } break; case ecgArtsCulture: switch (Content & 0x0F) { default: case 0x00: return tr("Content$Arts/Culture"); case 0x01: return tr("Content$Performing Arts"); case 0x02: return tr("Content$Fine Arts"); case 0x03: return tr("Content$Religion"); case 0x04: return tr("Content$Popular Culture/Traditional Arts"); case 0x05: return tr("Content$Literature"); case 0x06: return tr("Content$Film/Cinema"); case 0x07: return tr("Content$Experimental Film/Video"); case 0x08: return tr("Content$Broadcasting/Press"); case 0x09: return tr("Content$New Media"); case 0x0A: return tr("Content$Arts/Culture Magazine"); case 0x0B: return tr("Content$Fashion"); } break; case ecgSocialPoliticalEconomics: switch (Content & 0x0F) { default: case 0x00: return tr("Content$Social/Political/Economics"); case 0x01: return tr("Content$Magazine/Report/Documentary"); case 0x02: return tr("Content$Economics/Social Advisory"); case 0x03: return tr("Content$Remarkable People"); } break; case ecgEducationalScience: switch (Content & 0x0F) { default: case 0x00: return tr("Content$Education/Science/Factual"); case 0x01: return tr("Content$Nature/Animals/Environment"); case 0x02: return tr("Content$Technology/Natural Sciences"); case 0x03: return tr("Content$Medicine/Physiology/Psychology"); case 0x04: return tr("Content$Foreign Countries/Expeditions"); case 0x05: return tr("Content$Social/Spiritual Sciences"); case 0x06: return tr("Content$Further Education"); case 0x07: return tr("Content$Languages"); } break; case ecgLeisureHobbies: switch (Content & 0x0F) { default: case 0x00: return tr("Content$Leisure/Hobbies"); case 0x01: return tr("Content$Tourism/Travel"); case 0x02: return tr("Content$Handicraft"); case 0x03: return tr("Content$Motoring"); case 0x04: return tr("Content$Fitness & Health"); case 0x05: return tr("Content$Cooking"); case 0x06: return tr("Content$Advertisement/Shopping"); case 0x07: return tr("Content$Gardening"); } break; case ecgSpecial: switch (Content & 0x0F) { case 0x00: return tr("Content$Original Language"); case 0x01: return tr("Content$Black & White"); case 0x02: return tr("Content$Unpublished"); case 0x03: return tr("Content$Live Broadcast"); default: ; } break; default: ; } return ""; } cString cEvent::GetParentalRatingString(void) const { if (parentalRating) return cString::sprintf(tr("ParentalRating$from %d"), parentalRating); return NULL; } cString cEvent::GetDateString(void) const { return DateString(startTime); } cString cEvent::GetTimeString(void) const { return TimeString(startTime); } cString cEvent::GetEndTimeString(void) const { return TimeString(startTime + duration); } cString cEvent::GetVpsString(void) const { 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, bool InfoOnly) const { if (InfoOnly || startTime + duration + EPG_LINGER_TIME >= time(NULL)) { fprintf(f, "%sE %u %jd %d %X %X\n", Prefix, eventID, intmax_t(startTime), duration, tableID, version); if (!isempty(title)) fprintf(f, "%sT %s\n", Prefix, title); if (!isempty(shortText)) fprintf(f, "%sS %s\n", Prefix, shortText); if (!isempty(description)) { strreplace(description, '\n', '|'); fprintf(f, "%sD %s\n", Prefix, description); strreplace(description, '|', '\n'); } if (contents[0]) { fprintf(f, "%sG", Prefix); for (int i = 0; Contents(i); i++) fprintf(f, " %02X", Contents(i)); fprintf(f, "\n"); } if (parentalRating) fprintf(f, "%sR %d\n", Prefix, parentalRating); if (components) { for (int i = 0; i < components->NumComponents(); i++) { tComponent *p = components->Component(i); fprintf(f, "%sX %s\n", Prefix, *p->ToString()); } } if (vps) fprintf(f, "%sV %jd\n", Prefix, intmax_t(vps)); if (!InfoOnly && !isempty(aux)) { strreplace(aux, '\n', '|'); fprintf(f, "%s@ %s\n", Prefix, aux); strreplace(aux, '|', '\n'); } if (!InfoOnly) fprintf(f, "%se\n", Prefix); } } bool cEvent::Parse(char *s) { char *t = skipspace(s + 1); switch (*s) { case 'T': SetTitle(t); break; case 'S': SetShortText(t); break; case 'D': strreplace(t, '|', '\n'); SetDescription(t); break; case 'G': { memset(contents, 0, sizeof(contents)); for (int i = 0; i < MaxEventContents; i++) { char *tail = NULL; int c = strtol(t, &tail, 16); if (0x00 < c && c <= 0xFF) { contents[i] = c; t = tail; } else break; } } break; case 'R': SetParentalRating(atoi(t)); break; case 'X': if (!components) components = new cComponents; components->SetComponent(components->NumComponents(), t); break; case 'V': SetVps(atol(t)); break; case '@': strreplace(t, '|', '\n'); SetAux(t); break; default: esyslog("ERROR: unexpected tag while reading EPG data: %s", s); return false; } return true; } bool cEvent::Read(FILE *f, cSchedule *Schedule, int &Line) { if (Schedule) { cEvent *Event = NULL; char *s; cReadLine ReadLine; while ((s = ReadLine.Read(f)) != NULL) { Line++; char *t = skipspace(s + 1); switch (*s) { case 'E': if (!Event) { unsigned int EventID; intmax_t StartTime; // actually time_t, but intmax_t for scanning with "%jd" int Duration; unsigned int TableID = 0; unsigned int Version = 0xFF; // actual value is ignored int n = sscanf(t, "%u %jd %d %X %X", &EventID, &StartTime, &Duration, &TableID, &Version); if (n >= 3 && n <= 5) { Event = (cEvent *)Schedule->GetEventByTime(StartTime); cEvent *newEvent = NULL; if (Event) DELETENULL(Event->components); if (!Event) { Event = newEvent = new cEvent(EventID); Event->seen = 0; } if (Event) { Event->SetTableID(TableID); Event->SetStartTime(StartTime); Event->SetDuration(Duration); if (newEvent) Schedule->AddEvent(newEvent); } } } break; case 'e': if (Event && !Event->Title()) Event->SetTitle(tr("No title")); Event = NULL; break; case 'c': // to keep things simple we react on 'c' here return true; default: if (Event && !Event->Parse(s)) { esyslog("ERROR: EPG data problem in line %d", Line); return false; } } } esyslog("ERROR: unexpected end of file while reading EPG data"); } return false; } #define MAXEPGBUGFIXSTATS 13 #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 Force) { if (Setup.EPGBugfixLevel > 0) { static time_t LastReport = 0; time_t now = time(NULL); if (now - LastReport > 3600 || Force) { LastReport = now; struct tm tm_r; struct tm *ptm = localtime_r(&now, &tm_r); if (ptm->tm_hour != 5) return; } else return; bool GotHits = false; char buffer[1024]; for (int i = 0; i < MAXEPGBUGFIXSTATS; i++) { const char *delim = " "; tEpgBugFixStats *p = &EpgBugFixStats[i]; if (p->hits) { bool PrintedStats = false; char *q = buffer; *buffer = 0; LOCK_CHANNELS_READ; for (int c = 0; c < p->n; c++) { if (const cChannel *Channel = Channels->GetByChannelID(p->channelIDs[c], true)) { 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 Hits Channels"); GotHits = true; } if (!PrintedStats) { q += snprintf(q, sizeof(buffer) - (q - buffer), "%-3d %-4d", 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); } p->hits = p->n = 0; } if (GotHits) dsyslog("====================="); } } static void StripControlCharacters(char *s) { if (s) { int len = strlen(s); while (len > 0) { int l = Utf8CharLen(s); uchar *p = (uchar *)s; if (l == 2 && *p == 0xC2) // UTF-8 sequence p++; if (*p == 0x86 || *p == 0x87 || *p == 0x0D) { memmove(s, p + 1, len - l + 1); // we also copy the terminating 0! len -= l; l = 0; } s += l; len -= l; } } } void cEvent::FixEpgBugs(void) { if (isempty(title)) { // we don't want any "(null)" titles title = strcpyrealloc(title, tr("No title")); EpgBugFixStat(12, ChannelID()); } if (Setup.EPGBugfixLevel == 0) goto Final; // 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: // 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 (preceded // 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) goto Final; // 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); #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, '`', '\''); if (Setup.EPGBugfixLevel <= 2) goto Final; // The stream components have a "description" field which some channels // apparently have no idea of how to set correctly: if (components) { for (int i = 0; i < components->NumComponents(); i++) { tComponent *p = components->Component(i); switch (p->stream) { case 0x01: { // video if (p->description) { if (strcasecmp(p->description, "Video") == 0 || strcasecmp(p->description, "Bildformat") == 0) { // Yes, we know it's video - that's what the 'stream' code // is for! But _which_ video is it? free(p->description); p->description = NULL; EpgBugFixStat(8, ChannelID()); } } if (!p->description) { switch (p->type) { case 0x01: case 0x05: p->description = strdup("4:3"); break; case 0x02: case 0x03: case 0x06: case 0x07: p->description = strdup("16:9"); break; case 0x04: case 0x08: p->description = strdup(">16:9"); break; case 0x09: case 0x0D: p->description = strdup("HD 4:3"); break; case 0x0A: case 0x0B: case 0x0E: case 0x0F: p->description = strdup("HD 16:9"); break; case 0x0C: case 0x10: p->description = strdup("HD >16:9"); break; default: ; } EpgBugFixStat(9, ChannelID()); } } break; case 0x02: { // audio if (p->description) { if (strcasecmp(p->description, "Audio") == 0) { // Yes, we know it's audio - that's what the 'stream' code // is for! But _which_ audio is it? free(p->description); p->description = NULL; EpgBugFixStat(10, ChannelID()); } } if (!p->description) { switch (p->type) { case 0x05: p->description = strdup("Dolby Digital"); break; default: ; // all others will just display the language } EpgBugFixStat(11, ChannelID()); } } break; default: ; } } } Final: // VDR can't usefully handle newline characters in the title, shortText or component description of EPG // data, so let's always convert them to blanks (independent of the setting of EPGBugfixLevel): strreplace(title, '\n', ' '); strreplace(shortText, '\n', ' '); if (components) { for (int i = 0; i < components->NumComponents(); i++) { tComponent *p = components->Component(i); if (p->description) strreplace(p->description, '\n', ' '); } } // Same for control characters: StripControlCharacters(title); StripControlCharacters(shortText); StripControlCharacters(description); } // --- cSchedule ------------------------------------------------------------- cMutex cSchedule::numTimersMutex; cSchedule::cSchedule(tChannelID ChannelID) { channelID = ChannelID; events.SetUseGarbageCollector(); numTimers = 0; hasRunning = false; modified = 0; onActualTp = false; presentSeen = 0; } void cSchedule::IncNumTimers(void) const { numTimersMutex.Lock(); numTimers++; numTimersMutex.Unlock(); } void cSchedule::DecNumTimers(void) const { numTimersMutex.Lock(); numTimers--; numTimersMutex.Unlock(); } bool cSchedule::OnActualTp(uchar TableId) { if ((TableId & 0xF0) == 0x50) onActualTp = true; return onActualTp; } cEvent *cSchedule::AddEvent(cEvent *Event) { events.Add(Event); Event->schedule = this; HashEvent(Event); return Event; } void cSchedule::DelEvent(cEvent *Event) { if (Event->schedule == this) { UnhashEvent(Event); Event->schedule = NULL; // Removing the event from its schedule prevents it from decrementing the // schedule's timer counter, so we do it here: cEvent::numTimersMutex.Lock(); numTimersMutex.Lock(); numTimers -= Event->numTimers; numTimersMutex.Unlock(); cEvent::numTimersMutex.Unlock(); events.Del(Event); } } void cSchedule::HashEvent(cEvent *Event) { if (cEvent *p = eventsHashID.Get(Event->EventID())) eventsHashID.Del(p, p->EventID()); eventsHashID.Add(Event, Event->EventID()); if (Event->StartTime() > 0) { // 'StartTime < 0' is apparently used with NVOD channels if (cEvent *p = eventsHashStartTime.Get(Event->StartTime())) eventsHashStartTime.Del(p, p->StartTime()); eventsHashStartTime.Add(Event, Event->StartTime()); } } void cSchedule::UnhashEvent(cEvent *Event) { eventsHashID.Del(Event, Event->EventID()); if (Event->StartTime() > 0) // 'StartTime < 0' is apparently used with NVOD channels eventsHashStartTime.Del(Event, Event->StartTime()); } const cEvent *cSchedule::GetPresentEvent(void) const { const cEvent *pe = NULL; time_t now = time(NULL); for (const cEvent *p = events.First(); p; p = events.Next(p)) { if (p->StartTime() <= now) pe = p; else if (p->StartTime() > now + 3600) break; if (p->SeenWithin(RUNNINGSTATUSTIMEOUT) && p->RunningStatus() >= SI::RunningStatusPausing) return p; } return pe; } const cEvent *cSchedule::GetFollowingEvent(void) const { const cEvent *p = GetPresentEvent(); if (p) p = events.Next(p); else { time_t now = time(NULL); for (p = events.First(); p; p = events.Next(p)) { if (p->StartTime() >= now) break; } } return p; } #if DEPRECATED_SCHEDULE_GET_EVENT const cEvent *cSchedule::GetEvent(tEventID EventID, time_t StartTime) const { // Returns the event info with the given StartTime or, if no actual StartTime // is given, the one with the given EventID. if (StartTime > 0) // 'StartTime < 0' is apparently used with NVOD channels return eventsHashStartTime.Get(StartTime); else return eventsHashID.Get(EventID); } #endif const cEvent *cSchedule::GetEventById(tEventID EventID) const { return eventsHashID.Get(EventID); } const cEvent *cSchedule::GetEventByTime(time_t StartTime) const { if (StartTime > 0) // 'StartTime < 0' is apparently used with NVOD channels return eventsHashStartTime.Get(StartTime); return NULL; } const cEvent *cSchedule::GetEventAround(time_t Time) const { const cEvent *pe = NULL; time_t delta = INT_MAX; for (const cEvent *p = events.First(); p; p = events.Next(p)) { time_t dt = Time - p->StartTime(); if (dt >= 0 && dt < delta && p->EndTime() >= Time) { delta = dt; pe = p; } } return pe; } void cSchedule::SetRunningStatus(cEvent *Event, int RunningStatus, const cChannel *Channel) { hasRunning = false; for (cEvent *p = events.First(); p; p = events.Next(p)) { if (p == Event) { if (p->RunningStatus() > SI::RunningStatusNotRunning || RunningStatus > SI::RunningStatusNotRunning) { p->SetRunningStatus(RunningStatus, Channel); break; } } else if (RunningStatus >= SI::RunningStatusPausing && p->StartTime() < Event->StartTime()) p->SetRunningStatus(SI::RunningStatusNotRunning, Channel); if (p->RunningStatus() >= SI::RunningStatusPausing) hasRunning = true; } SetPresentSeen(); } void cSchedule::ClrRunningStatus(cChannel *Channel) { if (hasRunning) { for (cEvent *p = events.First(); p; p = events.Next(p)) { if (p->RunningStatus() >= SI::RunningStatusPausing) { p->SetRunningStatus(SI::RunningStatusNotRunning, Channel); hasRunning = false; SetModified(); break; } } } } void cSchedule::ResetVersions(void) { for (cEvent *p = events.First(); p; p = events.Next(p)) p->SetVersion(0xFF); } void cSchedule::Sort(void) { events.Sort(); // Make sure there are no RunningStatusUndefined before the currently running event: if (hasRunning) { for (cEvent *p = events.First(); p; p = events.Next(p)) { if (p->RunningStatus() >= SI::RunningStatusPausing) break; p->SetRunningStatus(SI::RunningStatusNotRunning); } } SetModified(); } void cSchedule::DropOutdated(time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version) { // Events are sorted by start time. if (SegmentStart > 0 && SegmentEnd > 0) { cEvent *p = events.First(); while (p) { cEvent *n = events.Next(p); if (p->StartTime() >= SegmentStart) { if (p->StartTime() < SegmentEnd) { // The event starts within the given time segment. if ((p->TableID() > 0x4E || TableID == 0x4E) && (p->TableID() != TableID || p->Version() != Version)) { // The segment overwrites all events from tables with other ids, and // within the same table id all events must have the same version. // Special consideration: table 0x4E can only be overwritten with the same id! DelEvent(p); } } else break; } p = n; } } } void cSchedule::Cleanup(void) { Cleanup(time(NULL)); } void cSchedule::Cleanup(time_t Time) { cEvent *Event; while ((Event = events.First()) != NULL) { if (!Event->HasTimer() && Event->EndTime() + EPG_LINGER_TIME < Time) DelEvent(Event); else break; } } void cSchedule::Dump(const cChannels *Channels, FILE *f, const char *Prefix, eDumpMode DumpMode, time_t AtTime) const { if (const cChannel *Channel = Channels->GetByChannelID(channelID, true)) { fprintf(f, "%sC %s %s\n", Prefix, *Channel->GetChannelID().ToString(), Channel->Name()); const cEvent *p; switch (DumpMode) { case dmAll: { for (p = events.First(); p; p = events.Next(p)) p->Dump(f, Prefix); } break; case dmPresent: { if ((p = GetPresentEvent()) != NULL) p->Dump(f, Prefix); } break; case dmFollowing: { if ((p = GetFollowingEvent()) != NULL) p->Dump(f, Prefix); } break; case dmAtTime: { if ((p = GetEventAround(AtTime)) != NULL) p->Dump(f, Prefix); } break; default: esyslog("ERROR: unknown DumpMode %d (%s %d)", DumpMode, __FUNCTION__, __LINE__); } fprintf(f, "%sc\n", Prefix); } } bool cSchedule::Read(FILE *f, cSchedules *Schedules) { if (Schedules) { int Line = 0; cReadLine ReadLine; char *s; while ((s = ReadLine.Read(f)) != NULL) { Line++; 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()) { if (cSchedule *p = Schedules->AddSchedule(channelID)) { if (!cEvent::Read(f, p, Line)) return false; p->Sort(); } } else { esyslog("ERROR: invalid channel ID: %s", s); return false; } } } else { esyslog("ERROR: unexpected tag in line %d while reading EPG data: %s", Line, s); return false; } } return true; } return false; } // --- cEpgDataWriter -------------------------------------------------------- class cEpgDataWriter : public cThread { private: cMutex mutex; bool dump; protected: virtual void Action(void); public: cEpgDataWriter(void); void SetDump(bool Dump) { dump = Dump; } void Perform(void); }; cEpgDataWriter::cEpgDataWriter(void) :cThread("epg data writer", true) { dump = false; } void cEpgDataWriter::Action(void) { Perform(); } void cEpgDataWriter::Perform(void) { cMutexLock MutexLock(&mutex); // to make sure fore- and background calls don't cause parellel dumps! { cStateKey StateKey; if (cSchedules *Schedules = cSchedules::GetSchedulesWrite(StateKey, 1000)) { time_t now = time(NULL); for (cSchedule *p = Schedules->First(); p; p = Schedules->Next(p)) p->Cleanup(now); StateKey.Remove(); } } if (dump) cSchedules::Dump(); } static cEpgDataWriter EpgDataWriter; // --- cSchedules ------------------------------------------------------------ cSchedules cSchedules::schedules; char *cSchedules::epgDataFileName = NULL; time_t cSchedules::lastDump = time(NULL); cSchedules::cSchedules(void) :cList("5 Schedules") { } const cSchedules *cSchedules::GetSchedulesRead(cStateKey &StateKey, int TimeoutMs) { return schedules.Lock(StateKey, false, TimeoutMs) ? &schedules : NULL; } cSchedules *cSchedules::GetSchedulesWrite(cStateKey &StateKey, int TimeoutMs) { return schedules.Lock(StateKey, true, TimeoutMs) ? &schedules : NULL; } void cSchedules::SetEpgDataFileName(const char *FileName) { free(epgDataFileName); epgDataFileName = FileName ? strdup(FileName) : NULL; EpgDataWriter.SetDump(epgDataFileName != NULL); } void cSchedules::Cleanup(bool Force) { if (Force) lastDump = 0; time_t now = time(NULL); if (now - lastDump > EPGDATAWRITEDELTA) { if (Force) EpgDataWriter.Perform(); else if (!EpgDataWriter.Active()) EpgDataWriter.Start(); lastDump = now; } } void cSchedules::ResetVersions(void) { LOCK_SCHEDULES_WRITE; for (cSchedule *Schedule = Schedules->First(); Schedule; Schedule = Schedules->Next(Schedule)) Schedule->ResetVersions(); } bool cSchedules::Dump(FILE *f, const char *Prefix, eDumpMode DumpMode, time_t AtTime) { cSafeFile *sf = NULL; if (!f) { sf = new cSafeFile(epgDataFileName); if (sf->Open()) f = *sf; else { LOG_ERROR; delete sf; return false; } } LOCK_CHANNELS_READ; LOCK_SCHEDULES_READ; for (const cSchedule *p = Schedules->First(); p; p = Schedules->Next(p)) p->Dump(Channels, f, Prefix, DumpMode, AtTime); if (sf) { sf->Close(); delete sf; } return true; } bool cSchedules::Read(FILE *f) { 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; } LOCK_CHANNELS_WRITE; LOCK_SCHEDULES_WRITE; bool result = cSchedule::Read(f, Schedules); if (OwnFile) fclose(f); if (result) { // Initialize the channels' schedule pointers, so that the first WhatsOn menu will come up faster: for (cChannel *Channel = Channels->First(); Channel; Channel = Channels->Next(Channel)) { if (const cSchedule *Schedule = Channel->schedule) { if (!Schedule->ChannelID().Valid()) // this is the DummySchedule Channel->schedule = NULL; } Schedules->GetSchedule(Channel); } } return result; } 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 (const cSchedule *p = First(); p; p = Next(p)) { if (p->ChannelID() == ChannelID) return p; } return NULL; } const cSchedule *cSchedules::GetSchedule(const cChannel *Channel, bool AddIfMissing) const { // This is not very beautiful, but it dramatically speeds up the // "What's on now/next?" menus. if (!Channel) return NULL; static cSchedule DummySchedule(tChannelID::InvalidID); if (!Channel->schedule) Channel->schedule = GetSchedule(Channel->GetChannelID()); if (!Channel->schedule) Channel->schedule = &DummySchedule; if (Channel->schedule == &DummySchedule && AddIfMissing) { cSchedule *Schedule = new cSchedule(Channel->GetChannelID()); ((cSchedules *)this)->Add(Schedule); Channel->schedule = Schedule; } return Channel->schedule != &DummySchedule? Channel->schedule : NULL; } // --- cEpgDataReader -------------------------------------------------------- cEpgDataReader::cEpgDataReader(void) :cThread("epg data reader") { } void cEpgDataReader::Action(void) { cSchedules::Read(); } // --- cEpgHandler ----------------------------------------------------------- cEpgHandler::cEpgHandler(void) { EpgHandlers.Add(this); } cEpgHandler::~cEpgHandler() { EpgHandlers.Del(this, false); } // --- cEpgHandlers ---------------------------------------------------------- cEpgHandlers EpgHandlers; bool cEpgHandlers::IgnoreChannel(const cChannel *Channel) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->IgnoreChannel(Channel)) return true; } return false; } bool cEpgHandlers::HandleEitEvent(cSchedule *Schedule, const SI::EIT::Event *EitEvent, uchar TableID, uchar Version) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->HandleEitEvent(Schedule, EitEvent, TableID, Version)) return true; } return false; } bool cEpgHandlers::HandledExternally(const cChannel *Channel) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->HandledExternally(Channel)) return true; } return false; } bool cEpgHandlers::IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->IsUpdate(EventID, StartTime, TableID, Version)) return true; } return false; } void cEpgHandlers::SetEventID(cEvent *Event, tEventID EventID) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->SetEventID(Event, EventID)) return; } Event->SetEventID(EventID); } void cEpgHandlers::SetTitle(cEvent *Event, const char *Title) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->SetTitle(Event, Title)) return; } Event->SetTitle(Title); } void cEpgHandlers::SetShortText(cEvent *Event, const char *ShortText) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->SetShortText(Event, ShortText)) return; } Event->SetShortText(ShortText); } void cEpgHandlers::SetDescription(cEvent *Event, const char *Description) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->SetDescription(Event, Description)) return; } Event->SetDescription(Description); } void cEpgHandlers::SetContents(cEvent *Event, uchar *Contents) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->SetContents(Event, Contents)) return; } Event->SetContents(Contents); } void cEpgHandlers::SetParentalRating(cEvent *Event, int ParentalRating) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->SetParentalRating(Event, ParentalRating)) return; } Event->SetParentalRating(ParentalRating); } void cEpgHandlers::SetStartTime(cEvent *Event, time_t StartTime) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->SetStartTime(Event, StartTime)) return; } Event->SetStartTime(StartTime); } void cEpgHandlers::SetDuration(cEvent *Event, int Duration) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->SetDuration(Event, Duration)) return; } Event->SetDuration(Duration); } void cEpgHandlers::SetVps(cEvent *Event, time_t Vps) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->SetVps(Event, Vps)) return; } Event->SetVps(Vps); } void cEpgHandlers::SetComponents(cEvent *Event, cComponents *Components) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->SetComponents(Event, Components)) return; } Event->SetComponents(Components); } void cEpgHandlers::FixEpgBugs(cEvent *Event) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->FixEpgBugs(Event)) return; } Event->FixEpgBugs(); } void cEpgHandlers::HandleEvent(cEvent *Event) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->HandleEvent(Event)) break; } } void cEpgHandlers::SortSchedule(cSchedule *Schedule) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->SortSchedule(Schedule)) return; } Schedule->Sort(); } void cEpgHandlers::DropOutdated(cSchedule *Schedule, time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->DropOutdated(Schedule, SegmentStart, SegmentEnd, TableID, Version)) return; } Schedule->DropOutdated(SegmentStart, SegmentEnd, TableID, Version); } bool cEpgHandlers::BeginSegmentTransfer(const cChannel *Channel) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (!eh->BeginSegmentTransfer(Channel, false)) return false; } return true; } void cEpgHandlers::EndSegmentTransfer(bool Modified) { for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { if (eh->EndSegmentTransfer(Modified, false)) return; } }