vdr  1.7.27
epg.c
Go to the documentation of this file.
00001 /*
00002  * epg.c: Electronic Program Guide
00003  *
00004  * See the main source file 'vdr.c' for copyright information and
00005  * how to reach the author.
00006  *
00007  * Original version (as used in VDR before 1.3.0) written by
00008  * Robert Schneider <Robert.Schneider@web.de> and Rolf Hakenes <hakenes@hippomi.de>.
00009  *
00010  * $Id: epg.c 2.12 2012/03/10 13:14:27 kls Exp $
00011  */
00012 
00013 #include "epg.h"
00014 #include <ctype.h>
00015 #include <limits.h>
00016 #include <time.h>
00017 #include "libsi/si.h"
00018 #include "timers.h"
00019 
00020 #define RUNNINGSTATUSTIMEOUT 30 // seconds before the running status is considered unknown
00021 
00022 // --- tComponent ------------------------------------------------------------
00023 
00024 cString tComponent::ToString(void)
00025 {
00026   char buffer[256];
00027   snprintf(buffer, sizeof(buffer), "%X %02X %s %s", stream, type, language, description ? description : "");
00028   return buffer;
00029 }
00030 
00031 bool tComponent::FromString(const char *s)
00032 {
00033   unsigned int Stream, Type;
00034   int n = sscanf(s, "%X %02X %7s %a[^\n]", &Stream, &Type, language, &description); // 7 = MAXLANGCODE2 - 1
00035   if (n != 4 || isempty(description)) {
00036      free(description);
00037      description = NULL;
00038      }
00039   stream = Stream;
00040   type = Type;
00041   return n >= 3;
00042 }
00043 
00044 // --- cComponents -----------------------------------------------------------
00045 
00046 cComponents::cComponents(void)
00047 {
00048   numComponents = 0;
00049   components = NULL;
00050 }
00051 
00052 cComponents::~cComponents(void)
00053 {
00054   for (int i = 0; i < numComponents; i++)
00055       free(components[i].description);
00056   free(components);
00057 }
00058 
00059 bool cComponents::Realloc(int Index)
00060 {
00061   if (Index >= numComponents) {
00062      Index++;
00063      if (tComponent *NewBuffer = (tComponent *)realloc(components, Index * sizeof(tComponent))) {
00064         int n = numComponents;
00065         numComponents = Index;
00066         components = NewBuffer;
00067         memset(&components[n], 0, sizeof(tComponent) * (numComponents - n));
00068         }
00069      else {
00070         esyslog("ERROR: out of memory");
00071         return false;
00072         }
00073      }
00074   return true;
00075 }
00076 
00077 void cComponents::SetComponent(int Index, const char *s)
00078 {
00079   if (Realloc(Index))
00080      components[Index].FromString(s);
00081 }
00082 
00083 void cComponents::SetComponent(int Index, uchar Stream, uchar Type, const char *Language, const char *Description)
00084 {
00085   if (!Realloc(Index))
00086      return;
00087   tComponent *p = &components[Index];
00088   p->stream = Stream;
00089   p->type = Type;
00090   strn0cpy(p->language, Language, sizeof(p->language));
00091   char *q = strchr(p->language, ',');
00092   if (q)
00093      *q = 0; // strips rest of "normalized" language codes
00094   p->description = strcpyrealloc(p->description, !isempty(Description) ? Description : NULL);
00095 }
00096 
00097 tComponent *cComponents::GetComponent(int Index, uchar Stream, uchar Type)
00098 {
00099   for (int i = 0; i < numComponents; i++) {
00100       if (components[i].stream == Stream && (
00101           Type == 0 || // don't care about the actual Type
00102           Stream == 2 && (components[i].type < 5) == (Type < 5) // fallback "Dolby" component according to the "Premiere pseudo standard"
00103          )) {
00104          if (!Index--)
00105             return &components[i];
00106          }
00107       }
00108   return NULL;
00109 }
00110 
00111 // --- cEvent ----------------------------------------------------------------
00112 
00113 cEvent::cEvent(tEventID EventID)
00114 {
00115   schedule = NULL;
00116   eventID = EventID;
00117   tableID = 0xFF; // actual table ids are 0x4E..0x60
00118   version = 0xFF; // actual version numbers are 0..31
00119   runningStatus = SI::RunningStatusUndefined;
00120   title = NULL;
00121   shortText = NULL;
00122   description = NULL;
00123   components = NULL;
00124   memset(contents, 0, sizeof(contents));
00125   parentalRating = 0;
00126   startTime = 0;
00127   duration = 0;
00128   vps = 0;
00129   SetSeen();
00130 }
00131 
00132 cEvent::~cEvent()
00133 {
00134   free(title);
00135   free(shortText);
00136   free(description);
00137   delete components;
00138 }
00139 
00140 int cEvent::Compare(const cListObject &ListObject) const
00141 {
00142   cEvent *e = (cEvent *)&ListObject;
00143   return startTime - e->startTime;
00144 }
00145 
00146 tChannelID cEvent::ChannelID(void) const
00147 {
00148   return schedule ? schedule->ChannelID() : tChannelID();
00149 }
00150 
00151 void cEvent::SetEventID(tEventID EventID)
00152 {
00153   if (eventID != EventID) {
00154      if (schedule)
00155         schedule->UnhashEvent(this);
00156      eventID = EventID;
00157      if (schedule)
00158         schedule->HashEvent(this);
00159      }
00160 }
00161 
00162 void cEvent::SetTableID(uchar TableID)
00163 {
00164   tableID = TableID;
00165 }
00166 
00167 void cEvent::SetVersion(uchar Version)
00168 {
00169   version = Version;
00170 }
00171 
00172 void cEvent::SetRunningStatus(int RunningStatus, cChannel *Channel)
00173 {
00174   if (Channel && runningStatus != RunningStatus && (RunningStatus > SI::RunningStatusNotRunning || runningStatus > SI::RunningStatusUndefined) && Channel->HasTimer())
00175      isyslog("channel %d (%s) event %s status %d", Channel->Number(), Channel->Name(), *ToDescr(), RunningStatus);
00176   runningStatus = RunningStatus;
00177 }
00178 
00179 void cEvent::SetTitle(const char *Title)
00180 {
00181   title = strcpyrealloc(title, Title);
00182 }
00183 
00184 void cEvent::SetShortText(const char *ShortText)
00185 {
00186   shortText = strcpyrealloc(shortText, ShortText);
00187 }
00188 
00189 void cEvent::SetDescription(const char *Description)
00190 {
00191   description = strcpyrealloc(description, Description);
00192 }
00193 
00194 void cEvent::SetComponents(cComponents *Components)
00195 {
00196   delete components;
00197   components = Components;
00198 }
00199 
00200 void cEvent::SetContents(uchar *Contents)
00201 {
00202   for (int i = 0; i < MaxEventContents; i++)
00203       contents[i] = Contents[i];
00204 }
00205 
00206 void cEvent::SetParentalRating(int ParentalRating)
00207 {
00208   parentalRating = ParentalRating;
00209 }
00210 
00211 void cEvent::SetStartTime(time_t StartTime)
00212 {
00213   if (startTime != StartTime) {
00214      if (schedule)
00215         schedule->UnhashEvent(this);
00216      startTime = StartTime;
00217      if (schedule)
00218         schedule->HashEvent(this);
00219      }
00220 }
00221 
00222 void cEvent::SetDuration(int Duration)
00223 {
00224   duration = Duration;
00225 }
00226 
00227 void cEvent::SetVps(time_t Vps)
00228 {
00229   vps = Vps;
00230 }
00231 
00232 void cEvent::SetSeen(void)
00233 {
00234   seen = time(NULL);
00235 }
00236 
00237 cString cEvent::ToDescr(void) const
00238 {
00239   char vpsbuf[64] = "";
00240   if (Vps())
00241      sprintf(vpsbuf, "(VPS: %s) ", *GetVpsString());
00242   return cString::sprintf("%s %s-%s %s'%s'", *GetDateString(), *GetTimeString(), *GetEndTimeString(), vpsbuf, Title());
00243 }
00244 
00245 bool cEvent::HasTimer(void) const
00246 {
00247   for (cTimer *t = Timers.First(); t; t = Timers.Next(t)) {
00248       if (t->Event() == this)
00249          return true;
00250       }
00251   return false;
00252 }
00253 
00254 bool cEvent::IsRunning(bool OrAboutToStart) const
00255 {
00256   return runningStatus >= (OrAboutToStart ? SI::RunningStatusStartsInAFewSeconds : SI::RunningStatusPausing);
00257 }
00258 
00259 const char *cEvent::ContentToString(uchar Content)
00260 {
00261   switch (Content & 0xF0) {
00262     case ecgMovieDrama:
00263          switch (Content & 0x0F) {
00264            default:
00265            case 0x00: return tr("Content$Movie/Drama");
00266            case 0x01: return tr("Content$Detective/Thriller");
00267            case 0x02: return tr("Content$Adventure/Western/War");
00268            case 0x03: return tr("Content$Science Fiction/Fantasy/Horror");
00269            case 0x04: return tr("Content$Comedy");
00270            case 0x05: return tr("Content$Soap/Melodrama/Folkloric");
00271            case 0x06: return tr("Content$Romance");
00272            case 0x07: return tr("Content$Serious/Classical/Religious/Historical Movie/Drama");
00273            case 0x08: return tr("Content$Adult Movie/Drama");
00274            }
00275          break;
00276     case ecgNewsCurrentAffairs:
00277          switch (Content & 0x0F) {
00278            default:
00279            case 0x00: return tr("Content$News/Current Affairs");
00280            case 0x01: return tr("Content$News/Weather Report");
00281            case 0x02: return tr("Content$News Magazine");
00282            case 0x03: return tr("Content$Documentary");
00283            case 0x04: return tr("Content$Discussion/Inverview/Debate");
00284            }
00285          break;
00286     case ecgShow:
00287          switch (Content & 0x0F) {
00288            default:
00289            case 0x00: return tr("Content$Show/Game Show");
00290            case 0x01: return tr("Content$Game Show/Quiz/Contest");
00291            case 0x02: return tr("Content$Variety Show");
00292            case 0x03: return tr("Content$Talk Show");
00293            }
00294          break;
00295     case ecgSports:
00296          switch (Content & 0x0F) {
00297            default:
00298            case 0x00: return tr("Content$Sports");
00299            case 0x01: return tr("Content$Special Event");
00300            case 0x02: return tr("Content$Sport Magazine");
00301            case 0x03: return tr("Content$Football/Soccer");
00302            case 0x04: return tr("Content$Tennis/Squash");
00303            case 0x05: return tr("Content$Team Sports");
00304            case 0x06: return tr("Content$Athletics");
00305            case 0x07: return tr("Content$Motor Sport");
00306            case 0x08: return tr("Content$Water Sport");
00307            case 0x09: return tr("Content$Winter Sports");
00308            case 0x0A: return tr("Content$Equestrian");
00309            case 0x0B: return tr("Content$Martial Sports");
00310            }
00311          break;
00312     case ecgChildrenYouth:
00313          switch (Content & 0x0F) {
00314            default:
00315            case 0x00: return tr("Content$Children's/Youth Programme");
00316            case 0x01: return tr("Content$Pre-school Children's Programme");
00317            case 0x02: return tr("Content$Entertainment Programme for 6 to 14");
00318            case 0x03: return tr("Content$Entertainment Programme for 10 to 16");
00319            case 0x04: return tr("Content$Informational/Educational/School Programme");
00320            case 0x05: return tr("Content$Cartoons/Puppets");
00321            }
00322          break;
00323     case ecgMusicBalletDance:
00324          switch (Content & 0x0F) {
00325            default:
00326            case 0x00: return tr("Content$Music/Ballet/Dance");
00327            case 0x01: return tr("Content$Rock/Pop");
00328            case 0x02: return tr("Content$Serious/Classical Music");
00329            case 0x03: return tr("Content$Folk/Tradional Music");
00330            case 0x04: return tr("Content$Jazz");
00331            case 0x05: return tr("Content$Musical/Opera");
00332            case 0x06: return tr("Content$Ballet");
00333            }
00334          break;
00335     case ecgArtsCulture:
00336          switch (Content & 0x0F) {
00337            default:
00338            case 0x00: return tr("Content$Arts/Culture");
00339            case 0x01: return tr("Content$Performing Arts");
00340            case 0x02: return tr("Content$Fine Arts");
00341            case 0x03: return tr("Content$Religion");
00342            case 0x04: return tr("Content$Popular Culture/Traditional Arts");
00343            case 0x05: return tr("Content$Literature");
00344            case 0x06: return tr("Content$Film/Cinema");
00345            case 0x07: return tr("Content$Experimental Film/Video");
00346            case 0x08: return tr("Content$Broadcasting/Press");
00347            case 0x09: return tr("Content$New Media");
00348            case 0x0A: return tr("Content$Arts/Culture Magazine");
00349            case 0x0B: return tr("Content$Fashion");
00350            }
00351          break;
00352     case ecgSocialPoliticalEconomics:
00353          switch (Content & 0x0F) {
00354            default:
00355            case 0x00: return tr("Content$Social/Political/Economics");
00356            case 0x01: return tr("Content$Magazine/Report/Documentary");
00357            case 0x02: return tr("Content$Economics/Social Advisory");
00358            case 0x03: return tr("Content$Remarkable People");
00359            }
00360          break;
00361     case ecgEducationalScience:
00362          switch (Content & 0x0F) {
00363            default:
00364            case 0x00: return tr("Content$Education/Science/Factual");
00365            case 0x01: return tr("Content$Nature/Animals/Environment");
00366            case 0x02: return tr("Content$Technology/Natural Sciences");
00367            case 0x03: return tr("Content$Medicine/Physiology/Psychology");
00368            case 0x04: return tr("Content$Foreign Countries/Expeditions");
00369            case 0x05: return tr("Content$Social/Spiritual Sciences");
00370            case 0x06: return tr("Content$Further Education");
00371            case 0x07: return tr("Content$Languages");
00372            }
00373          break;
00374     case ecgLeisureHobbies:
00375          switch (Content & 0x0F) {
00376            default:
00377            case 0x00: return tr("Content$Leisure/Hobbies");
00378            case 0x01: return tr("Content$Tourism/Travel");
00379            case 0x02: return tr("Content$Handicraft");
00380            case 0x03: return tr("Content$Motoring");
00381            case 0x04: return tr("Content$Fitness & Health");
00382            case 0x05: return tr("Content$Cooking");
00383            case 0x06: return tr("Content$Advertisement/Shopping");
00384            case 0x07: return tr("Content$Gardening");
00385            }
00386          break;
00387     case ecgSpecial:
00388          switch (Content & 0x0F) {
00389            case 0x00: return tr("Content$Original Language");
00390            case 0x01: return tr("Content$Black & White");
00391            case 0x02: return tr("Content$Unpublished");
00392            case 0x03: return tr("Content$Live Broadcast");
00393            default: ;
00394            }
00395          break;
00396     default: ;
00397     }
00398   return "";
00399 }
00400 
00401 cString cEvent::GetParentalRatingString(void) const
00402 {
00403   if (parentalRating)
00404      return cString::sprintf(tr("ParentalRating$from %d"), parentalRating);
00405   return NULL;
00406 }
00407 
00408 cString cEvent::GetDateString(void) const
00409 {
00410   return DateString(startTime);
00411 }
00412 
00413 cString cEvent::GetTimeString(void) const
00414 {
00415   return TimeString(startTime);
00416 }
00417 
00418 cString cEvent::GetEndTimeString(void) const
00419 {
00420   return TimeString(startTime + duration);
00421 }
00422 
00423 cString cEvent::GetVpsString(void) const
00424 {
00425   char buf[25];
00426   struct tm tm_r;
00427   strftime(buf, sizeof(buf), "%d.%m. %R", localtime_r(&vps, &tm_r));
00428   return buf;
00429 }
00430 
00431 void cEvent::Dump(FILE *f, const char *Prefix, bool InfoOnly) const
00432 {
00433   if (InfoOnly || startTime + duration + Setup.EPGLinger * 60 >= time(NULL)) {
00434      fprintf(f, "%sE %u %ld %d %X %X\n", Prefix, eventID, startTime, duration, tableID, version);
00435      if (!isempty(title))
00436         fprintf(f, "%sT %s\n", Prefix, title);
00437      if (!isempty(shortText))
00438         fprintf(f, "%sS %s\n", Prefix, shortText);
00439      if (!isempty(description)) {
00440         strreplace(description, '\n', '|');
00441         fprintf(f, "%sD %s\n", Prefix, description);
00442         strreplace(description, '|', '\n');
00443         }
00444      if (contents[0]) {
00445         fprintf(f, "%sG", Prefix);
00446         for (int i = 0; Contents(i); i++)
00447             fprintf(f, " %02X", Contents(i));
00448         fprintf(f, "\n");
00449         }
00450      if (parentalRating)
00451         fprintf(f, "%sR %d\n", Prefix, parentalRating);
00452      if (components) {
00453         for (int i = 0; i < components->NumComponents(); i++) {
00454             tComponent *p = components->Component(i);
00455             fprintf(f, "%sX %s\n", Prefix, *p->ToString());
00456             }
00457         }
00458      if (vps)
00459         fprintf(f, "%sV %ld\n", Prefix, vps);
00460      if (!InfoOnly)
00461         fprintf(f, "%se\n", Prefix);
00462      }
00463 }
00464 
00465 bool cEvent::Parse(char *s)
00466 {
00467   char *t = skipspace(s + 1);
00468   switch (*s) {
00469     case 'T': SetTitle(t);
00470               break;
00471     case 'S': SetShortText(t);
00472               break;
00473     case 'D': strreplace(t, '|', '\n');
00474               SetDescription(t);
00475               break;
00476     case 'G': {
00477                 memset(contents, 0, sizeof(contents));
00478                 for (int i = 0; i < MaxEventContents; i++) {
00479                     char *tail = NULL;
00480                     int c = strtol(t, &tail, 16);
00481                     if (0x00 < c && c <= 0xFF) {
00482                        contents[i] = c;
00483                        t = tail;
00484                        }
00485                     else
00486                        break;
00487                     }
00488               }
00489               break;
00490     case 'R': SetParentalRating(atoi(t));
00491               break;
00492     case 'X': if (!components)
00493                  components = new cComponents;
00494               components->SetComponent(components->NumComponents(), t);
00495               break;
00496     case 'V': SetVps(atoi(t));
00497               break;
00498     default:  esyslog("ERROR: unexpected tag while reading EPG data: %s", s);
00499               return false;
00500     }
00501   return true;
00502 }
00503 
00504 bool cEvent::Read(FILE *f, cSchedule *Schedule)
00505 {
00506   if (Schedule) {
00507      cEvent *Event = NULL;
00508      char *s;
00509      int line = 0;
00510      cReadLine ReadLine;
00511      while ((s = ReadLine.Read(f)) != NULL) {
00512            line++;
00513            char *t = skipspace(s + 1);
00514            switch (*s) {
00515              case 'E': if (!Event) {
00516                           unsigned int EventID;
00517                           time_t StartTime;
00518                           int Duration;
00519                           unsigned int TableID = 0;
00520                           unsigned int Version = 0xFF; // actual value is ignored
00521                           int n = sscanf(t, "%u %ld %d %X %X", &EventID, &StartTime, &Duration, &TableID, &Version);
00522                           if (n >= 3 && n <= 5) {
00523                              Event = (cEvent *)Schedule->GetEvent(EventID, StartTime);
00524                              cEvent *newEvent = NULL;
00525                              if (Event)
00526                                 DELETENULL(Event->components);
00527                              if (!Event) {
00528                                 Event = newEvent = new cEvent(EventID);
00529                                 Event->seen = 0;
00530                                 }
00531                              if (Event) {
00532                                 Event->SetTableID(TableID);
00533                                 Event->SetStartTime(StartTime);
00534                                 Event->SetDuration(Duration);
00535                                 if (newEvent)
00536                                    Schedule->AddEvent(newEvent);
00537                                 }
00538                              }
00539                           }
00540                        break;
00541              case 'e': if (Event && !Event->Title())
00542                           Event->SetTitle(tr("No title"));
00543                        Event = NULL;
00544                        break;
00545              case 'c': // to keep things simple we react on 'c' here
00546                        return true;
00547              default:  if (Event && !Event->Parse(s)) {
00548                           esyslog("ERROR: EPG data problem in line %d", line);
00549                           return false;
00550                           }
00551              }
00552            }
00553      esyslog("ERROR: unexpected end of file while reading EPG data");
00554      }
00555   return false;
00556 }
00557 
00558 #define MAXEPGBUGFIXSTATS 13
00559 #define MAXEPGBUGFIXCHANS 100
00560 struct tEpgBugFixStats {
00561   int hits;
00562   int n;
00563   tChannelID channelIDs[MAXEPGBUGFIXCHANS];
00564   tEpgBugFixStats(void) { hits = n = 0; }
00565   };
00566 
00567 tEpgBugFixStats EpgBugFixStats[MAXEPGBUGFIXSTATS];
00568 
00569 static void EpgBugFixStat(int Number, tChannelID ChannelID)
00570 {
00571   if (0 <= Number && Number < MAXEPGBUGFIXSTATS) {
00572      tEpgBugFixStats *p = &EpgBugFixStats[Number];
00573      p->hits++;
00574      int i = 0;
00575      for (; i < p->n; i++) {
00576          if (p->channelIDs[i] == ChannelID)
00577             break;
00578          }
00579      if (i == p->n && p->n < MAXEPGBUGFIXCHANS)
00580         p->channelIDs[p->n++] = ChannelID;
00581      }
00582 }
00583 
00584 void ReportEpgBugFixStats(bool Reset)
00585 {
00586   if (Setup.EPGBugfixLevel > 0) {
00587      bool GotHits = false;
00588      char buffer[1024];
00589      for (int i = 0; i < MAXEPGBUGFIXSTATS; i++) {
00590          const char *delim = " ";
00591          tEpgBugFixStats *p = &EpgBugFixStats[i];
00592          if (p->hits) {
00593             bool PrintedStats = false;
00594             char *q = buffer;
00595             *buffer = 0;
00596             for (int c = 0; c < p->n; c++) {
00597                 cChannel *channel = Channels.GetByChannelID(p->channelIDs[c], true);
00598                 if (channel) {
00599                    if (!GotHits) {
00600                       dsyslog("=====================");
00601                       dsyslog("EPG bugfix statistics");
00602                       dsyslog("=====================");
00603                       dsyslog("IF SOMEBODY WHO IS IN CHARGE OF THE EPG DATA FOR ONE OF THE LISTED");
00604                       dsyslog("CHANNELS READS THIS: PLEASE TAKE A LOOK AT THE FUNCTION cEvent::FixEpgBugs()");
00605                       dsyslog("IN VDR/epg.c TO LEARN WHAT'S WRONG WITH YOUR DATA, AND FIX IT!");
00606                       dsyslog("=====================");
00607                       dsyslog("Fix Hits Channels");
00608                       GotHits = true;
00609                       }
00610                    if (!PrintedStats) {
00611                       q += snprintf(q, sizeof(buffer) - (q - buffer), "%-3d %-4d", i, p->hits);
00612                       PrintedStats = true;
00613                       }
00614                    q += snprintf(q, sizeof(buffer) - (q - buffer), "%s%s", delim, channel->Name());
00615                    delim = ", ";
00616                    if (q - buffer > 80) {
00617                       q += snprintf(q, sizeof(buffer) - (q - buffer), "%s...", delim);
00618                       break;
00619                       }
00620                    }
00621                 }
00622             if (*buffer)
00623                dsyslog("%s", buffer);
00624             }
00625          if (Reset)
00626             p->hits = p->n = 0;
00627          }
00628      if (GotHits)
00629         dsyslog("=====================");
00630      }
00631 }
00632 
00633 void cEvent::FixEpgBugs(void)
00634 {
00635   if (isempty(title)) {
00636      // we don't want any "(null)" titles
00637      title = strcpyrealloc(title, tr("No title"));
00638      EpgBugFixStat(12, ChannelID());
00639      }
00640 
00641   if (Setup.EPGBugfixLevel == 0)
00642      goto Final;
00643 
00644   // Some TV stations apparently have their own idea about how to fill in the
00645   // EPG data. Let's fix their bugs as good as we can:
00646 
00647   // Some channels put the ShortText in quotes and use either the ShortText
00648   // or the Description field, depending on how long the string is:
00649   //
00650   // Title
00651   // "ShortText". Description
00652   //
00653   if ((shortText == NULL) != (description == NULL)) {
00654      char *p = shortText ? shortText : description;
00655      if (*p == '"') {
00656         const char *delim = "\".";
00657         char *e = strstr(p + 1, delim);
00658         if (e) {
00659            *e = 0;
00660            char *s = strdup(p + 1);
00661            char *d = strdup(e + strlen(delim));
00662            free(shortText);
00663            free(description);
00664            shortText = s;
00665            description = d;
00666            EpgBugFixStat(1, ChannelID());
00667            }
00668         }
00669      }
00670 
00671   // Some channels put the Description into the ShortText (preceded
00672   // by a blank) if there is no actual ShortText and the Description
00673   // is short enough:
00674   //
00675   // Title
00676   //  Description
00677   //
00678   if (shortText && !description) {
00679      if (*shortText == ' ') {
00680         memmove(shortText, shortText + 1, strlen(shortText));
00681         description = shortText;
00682         shortText = NULL;
00683         EpgBugFixStat(2, ChannelID());
00684         }
00685      }
00686 
00687   // Sometimes they repeat the Title in the ShortText:
00688   //
00689   // Title
00690   // Title
00691   //
00692   if (shortText && strcmp(title, shortText) == 0) {
00693      free(shortText);
00694      shortText = NULL;
00695      EpgBugFixStat(3, ChannelID());
00696      }
00697 
00698   // Some channels put the ShortText between double quotes, which is nothing
00699   // but annoying (some even put a '.' after the closing '"'):
00700   //
00701   // Title
00702   // "ShortText"[.]
00703   //
00704   if (shortText && *shortText == '"') {
00705      int l = strlen(shortText);
00706      if (l > 2 && (shortText[l - 1] == '"' || (shortText[l - 1] == '.' && shortText[l - 2] == '"'))) {
00707         memmove(shortText, shortText + 1, l);
00708         char *p = strrchr(shortText, '"');
00709         if (p)
00710            *p = 0;
00711         EpgBugFixStat(4, ChannelID());
00712         }
00713      }
00714 
00715   if (Setup.EPGBugfixLevel <= 1)
00716      goto Final;
00717 
00718   // Some channels apparently try to do some formatting in the texts,
00719   // which is a bad idea because they have no way of knowing the width
00720   // of the window that will actually display the text.
00721   // Remove excess whitespace:
00722   title = compactspace(title);
00723   shortText = compactspace(shortText);
00724   description = compactspace(description);
00725 
00726 #define MAX_USEFUL_EPISODE_LENGTH 40
00727   // Some channels put a whole lot of information in the ShortText and leave
00728   // the Description totally empty. So if the ShortText length exceeds
00729   // MAX_USEFUL_EPISODE_LENGTH, let's put this into the Description
00730   // instead:
00731   if (!isempty(shortText) && isempty(description)) {
00732      if (strlen(shortText) > MAX_USEFUL_EPISODE_LENGTH) {
00733         free(description);
00734         description = shortText;
00735         shortText = NULL;
00736         EpgBugFixStat(6, ChannelID());
00737         }
00738      }
00739 
00740   // Some channels put the same information into ShortText and Description.
00741   // In that case we delete one of them:
00742   if (shortText && description && strcmp(shortText, description) == 0) {
00743      if (strlen(shortText) > MAX_USEFUL_EPISODE_LENGTH) {
00744         free(shortText);
00745         shortText = NULL;
00746         }
00747      else {
00748         free(description);
00749         description = NULL;
00750         }
00751      EpgBugFixStat(7, ChannelID());
00752      }
00753 
00754   // Some channels use the ` ("backtick") character, where a ' (single quote)
00755   // would be normally used. Actually, "backticks" in normal text don't make
00756   // much sense, so let's replace them:
00757   strreplace(title, '`', '\'');
00758   strreplace(shortText, '`', '\'');
00759   strreplace(description, '`', '\'');
00760 
00761   if (Setup.EPGBugfixLevel <= 2)
00762      goto Final;
00763 
00764   // The stream components have a "description" field which some channels
00765   // apparently have no idea of how to set correctly:
00766   if (components) {
00767      for (int i = 0; i < components->NumComponents(); i++) {
00768          tComponent *p = components->Component(i);
00769          switch (p->stream) {
00770            case 0x01: { // video
00771                 if (p->description) {
00772                    if (strcasecmp(p->description, "Video") == 0 ||
00773                         strcasecmp(p->description, "Bildformat") == 0) {
00774                       // Yes, we know it's video - that's what the 'stream' code
00775                       // is for! But _which_ video is it?
00776                       free(p->description);
00777                       p->description = NULL;
00778                       EpgBugFixStat(8, ChannelID());
00779                       }
00780                    }
00781                 if (!p->description) {
00782                    switch (p->type) {
00783                      case 0x01:
00784                      case 0x05: p->description = strdup("4:3"); break;
00785                      case 0x02:
00786                      case 0x03:
00787                      case 0x06:
00788                      case 0x07: p->description = strdup("16:9"); break;
00789                      case 0x04:
00790                      case 0x08: p->description = strdup(">16:9"); break;
00791                      case 0x09:
00792                      case 0x0D: p->description = strdup("HD 4:3"); break;
00793                      case 0x0A:
00794                      case 0x0B:
00795                      case 0x0E:
00796                      case 0x0F: p->description = strdup("HD 16:9"); break;
00797                      case 0x0C:
00798                      case 0x10: p->description = strdup("HD >16:9"); break;
00799                      default: ;
00800                      }
00801                    EpgBugFixStat(9, ChannelID());
00802                    }
00803                 }
00804                 break;
00805            case 0x02: { // audio
00806                 if (p->description) {
00807                    if (strcasecmp(p->description, "Audio") == 0) {
00808                       // Yes, we know it's audio - that's what the 'stream' code
00809                       // is for! But _which_ audio is it?
00810                       free(p->description);
00811                       p->description = NULL;
00812                       EpgBugFixStat(10, ChannelID());
00813                       }
00814                    }
00815                 if (!p->description) {
00816                    switch (p->type) {
00817                      case 0x05: p->description = strdup("Dolby Digital"); break;
00818                      default: ; // all others will just display the language
00819                      }
00820                    EpgBugFixStat(11, ChannelID());
00821                    }
00822                 }
00823                 break;
00824            default: ;
00825            }
00826          }
00827      }
00828 
00829 Final:
00830 
00831   // VDR can't usefully handle newline characters in the title, shortText or component description of EPG
00832   // data, so let's always convert them to blanks (independent of the setting of EPGBugfixLevel):
00833   strreplace(title, '\n', ' ');
00834   strreplace(shortText, '\n', ' ');
00835   if (components) {
00836      for (int i = 0; i < components->NumComponents(); i++) {
00837          tComponent *p = components->Component(i);
00838          if (p->description)
00839             strreplace(p->description, '\n', ' ');
00840          }
00841      }
00842   /* TODO adapt to UTF-8
00843   // Same for control characters:
00844   strreplace(title, '\x86', ' ');
00845   strreplace(title, '\x87', ' ');
00846   strreplace(shortText, '\x86', ' ');
00847   strreplace(shortText, '\x87', ' ');
00848   strreplace(description, '\x86', ' ');
00849   strreplace(description, '\x87', ' ');
00850   XXX*/
00851 }
00852 
00853 // --- cSchedule -------------------------------------------------------------
00854 
00855 cSchedule::cSchedule(tChannelID ChannelID)
00856 {
00857   channelID = ChannelID;
00858   hasRunning = false;
00859   modified = 0;
00860   presentSeen = 0;
00861 }
00862 
00863 cEvent *cSchedule::AddEvent(cEvent *Event)
00864 {
00865   events.Add(Event);
00866   Event->schedule = this;
00867   HashEvent(Event);
00868   return Event;
00869 }
00870 
00871 void cSchedule::DelEvent(cEvent *Event)
00872 {
00873   if (Event->schedule == this) {
00874      if (hasRunning && Event->IsRunning())
00875         ClrRunningStatus();
00876      UnhashEvent(Event);
00877      events.Del(Event);
00878      }
00879 }
00880 
00881 void cSchedule::HashEvent(cEvent *Event)
00882 {
00883   eventsHashID.Add(Event, Event->EventID());
00884   if (Event->StartTime() > 0) // 'StartTime < 0' is apparently used with NVOD channels
00885      eventsHashStartTime.Add(Event, Event->StartTime());
00886 }
00887 
00888 void cSchedule::UnhashEvent(cEvent *Event)
00889 {
00890   eventsHashID.Del(Event, Event->EventID());
00891   if (Event->StartTime() > 0) // 'StartTime < 0' is apparently used with NVOD channels
00892      eventsHashStartTime.Del(Event, Event->StartTime());
00893 }
00894 
00895 const cEvent *cSchedule::GetPresentEvent(void) const
00896 {
00897   const cEvent *pe = NULL;
00898   time_t now = time(NULL);
00899   for (cEvent *p = events.First(); p; p = events.Next(p)) {
00900       if (p->StartTime() <= now)
00901          pe = p;
00902       else if (p->StartTime() > now + 3600)
00903          break;
00904       if (p->SeenWithin(RUNNINGSTATUSTIMEOUT) && p->RunningStatus() >= SI::RunningStatusPausing)
00905          return p;
00906       }
00907   return pe;
00908 }
00909 
00910 const cEvent *cSchedule::GetFollowingEvent(void) const
00911 {
00912   const cEvent *p = GetPresentEvent();
00913   if (p)
00914      p = events.Next(p);
00915   else {
00916      time_t now = time(NULL);
00917      for (p = events.First(); p; p = events.Next(p)) {
00918          if (p->StartTime() >= now)
00919             break;
00920          }
00921      }
00922   return p;
00923 }
00924 
00925 const cEvent *cSchedule::GetEvent(tEventID EventID, time_t StartTime) const
00926 {
00927   // Returns the event info with the given StartTime or, if no actual StartTime
00928   // is given, the one with the given EventID.
00929   if (StartTime > 0) // 'StartTime < 0' is apparently used with NVOD channels
00930      return eventsHashStartTime.Get(StartTime);
00931   else
00932      return eventsHashID.Get(EventID);
00933 }
00934 
00935 const cEvent *cSchedule::GetEventAround(time_t Time) const
00936 {
00937   const cEvent *pe = NULL;
00938   time_t delta = INT_MAX;
00939   for (cEvent *p = events.First(); p; p = events.Next(p)) {
00940       time_t dt = Time - p->StartTime();
00941       if (dt >= 0 && dt < delta && p->EndTime() >= Time) {
00942          delta = dt;
00943          pe = p;
00944          }
00945       }
00946   return pe;
00947 }
00948 
00949 void cSchedule::SetRunningStatus(cEvent *Event, int RunningStatus, cChannel *Channel)
00950 {
00951   hasRunning = false;
00952   for (cEvent *p = events.First(); p; p = events.Next(p)) {
00953       if (p == Event) {
00954          if (p->RunningStatus() > SI::RunningStatusNotRunning || RunningStatus > SI::RunningStatusNotRunning) {
00955             p->SetRunningStatus(RunningStatus, Channel);
00956             break;
00957             }
00958          }
00959       else if (RunningStatus >= SI::RunningStatusPausing && p->StartTime() < Event->StartTime())
00960          p->SetRunningStatus(SI::RunningStatusNotRunning);
00961       if (p->RunningStatus() >= SI::RunningStatusPausing)
00962          hasRunning = true;
00963       }
00964 }
00965 
00966 void cSchedule::ClrRunningStatus(cChannel *Channel)
00967 {
00968   if (hasRunning) {
00969      for (cEvent *p = events.First(); p; p = events.Next(p)) {
00970          if (p->RunningStatus() >= SI::RunningStatusPausing) {
00971             p->SetRunningStatus(SI::RunningStatusNotRunning, Channel);
00972             hasRunning = false;
00973             break;
00974             }
00975          }
00976      }
00977 }
00978 
00979 void cSchedule::ResetVersions(void)
00980 {
00981   for (cEvent *p = events.First(); p; p = events.Next(p))
00982       p->SetVersion(0xFF);
00983 }
00984 
00985 void cSchedule::Sort(void)
00986 {
00987   events.Sort();
00988   // Make sure there are no RunningStatusUndefined before the currently running event:
00989   if (hasRunning) {
00990      for (cEvent *p = events.First(); p; p = events.Next(p)) {
00991          if (p->RunningStatus() >= SI::RunningStatusPausing)
00992             break;
00993          p->SetRunningStatus(SI::RunningStatusNotRunning);
00994          }
00995      }
00996 }
00997 
00998 void cSchedule::DropOutdated(time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version)
00999 {
01000   if (SegmentStart > 0 && SegmentEnd > 0) {
01001      for (cEvent *p = events.First(); p; p = events.Next(p)) {
01002          if (p->EndTime() > SegmentStart) {
01003             if (p->StartTime() < SegmentEnd) {
01004                // The event overlaps with the given time segment.
01005                if (p->TableID() > TableID || p->TableID() == TableID && p->Version() != Version) {
01006                   // The segment overwrites all events from tables with higher ids, and
01007                   // within the same table id all events must have the same version.
01008                   // We can't delete the event right here because a timer might have
01009                   // a pointer to it, so let's set its id and start time to 0 to have it
01010                   // "phased out":
01011                   if (hasRunning && p->IsRunning())
01012                      ClrRunningStatus();
01013                   UnhashEvent(p);
01014                   p->eventID = 0;
01015                   p->startTime = 0;
01016                   }
01017                }
01018             else
01019                break;
01020             }
01021          }
01022      }
01023 }
01024 
01025 void cSchedule::Cleanup(void)
01026 {
01027   Cleanup(time(NULL));
01028 }
01029 
01030 void cSchedule::Cleanup(time_t Time)
01031 {
01032   cEvent *Event;
01033   while ((Event = events.First()) != NULL) {
01034         if (!Event->HasTimer() && Event->EndTime() + Setup.EPGLinger * 60 + 3600 < Time) // adding one hour for safety
01035            DelEvent(Event);
01036         else
01037            break;
01038         }
01039 }
01040 
01041 void cSchedule::Dump(FILE *f, const char *Prefix, eDumpMode DumpMode, time_t AtTime) const
01042 {
01043   cChannel *channel = Channels.GetByChannelID(channelID, true);
01044   if (channel) {
01045      fprintf(f, "%sC %s %s\n", Prefix, *channel->GetChannelID().ToString(), channel->Name());
01046      const cEvent *p;
01047      switch (DumpMode) {
01048        case dmAll: {
01049             for (p = events.First(); p; p = events.Next(p))
01050                 p->Dump(f, Prefix);
01051             }
01052             break;
01053        case dmPresent: {
01054             if ((p = GetPresentEvent()) != NULL)
01055                p->Dump(f, Prefix);
01056             }
01057             break;
01058        case dmFollowing: {
01059             if ((p = GetFollowingEvent()) != NULL)
01060                p->Dump(f, Prefix);
01061             }
01062             break;
01063        case dmAtTime: {
01064             if ((p = GetEventAround(AtTime)) != NULL)
01065                p->Dump(f, Prefix);
01066             }
01067             break;
01068        default: esyslog("ERROR: unknown DumpMode %d (%s %d)", DumpMode, __FUNCTION__, __LINE__);
01069        }
01070      fprintf(f, "%sc\n", Prefix);
01071      }
01072 }
01073 
01074 bool cSchedule::Read(FILE *f, cSchedules *Schedules)
01075 {
01076   if (Schedules) {
01077      cReadLine ReadLine;
01078      char *s;
01079      while ((s = ReadLine.Read(f)) != NULL) {
01080            if (*s == 'C') {
01081               s = skipspace(s + 1);
01082               char *p = strchr(s, ' ');
01083               if (p)
01084                  *p = 0; // strips optional channel name
01085               if (*s) {
01086                  tChannelID channelID = tChannelID::FromString(s);
01087                  if (channelID.Valid()) {
01088                     cSchedule *p = Schedules->AddSchedule(channelID);
01089                     if (p) {
01090                        if (!cEvent::Read(f, p))
01091                           return false;
01092                        p->Sort();
01093                        Schedules->SetModified(p);
01094                        }
01095                     }
01096                  else {
01097                     esyslog("ERROR: invalid channel ID: %s", s);
01098                     return false;
01099                     }
01100                  }
01101               }
01102            else {
01103               esyslog("ERROR: unexpected tag while reading EPG data: %s", s);
01104               return false;
01105               }
01106            }
01107      return true;
01108      }
01109   return false;
01110 }
01111 
01112 // --- cSchedulesLock --------------------------------------------------------
01113 
01114 cSchedulesLock::cSchedulesLock(bool WriteLock, int TimeoutMs)
01115 {
01116   locked = cSchedules::schedules.rwlock.Lock(WriteLock, TimeoutMs);
01117 }
01118 
01119 cSchedulesLock::~cSchedulesLock()
01120 {
01121   if (locked)
01122      cSchedules::schedules.rwlock.Unlock();
01123 }
01124 
01125 // --- cSchedules ------------------------------------------------------------
01126 
01127 cSchedules cSchedules::schedules;
01128 const char *cSchedules::epgDataFileName = NULL;
01129 time_t cSchedules::lastCleanup = time(NULL);
01130 time_t cSchedules::lastDump = time(NULL);
01131 time_t cSchedules::modified = 0;
01132 
01133 const cSchedules *cSchedules::Schedules(cSchedulesLock &SchedulesLock)
01134 {
01135   return SchedulesLock.Locked() ? &schedules : NULL;
01136 }
01137 
01138 void cSchedules::SetEpgDataFileName(const char *FileName)
01139 {
01140   delete epgDataFileName;
01141   epgDataFileName = FileName ? strdup(FileName) : NULL;
01142 }
01143 
01144 void cSchedules::SetModified(cSchedule *Schedule)
01145 {
01146   Schedule->SetModified();
01147   modified = time(NULL);
01148 }
01149 
01150 void cSchedules::Cleanup(bool Force)
01151 {
01152   if (Force)
01153      lastDump = 0;
01154   time_t now = time(NULL);
01155   struct tm tm_r;
01156   struct tm *ptm = localtime_r(&now, &tm_r);
01157   if (now - lastCleanup > 3600) {
01158      isyslog("cleaning up schedules data");
01159      cSchedulesLock SchedulesLock(true, 1000);
01160      cSchedules *s = (cSchedules *)Schedules(SchedulesLock);
01161      if (s) {
01162         for (cSchedule *p = s->First(); p; p = s->Next(p))
01163             p->Cleanup(now);
01164         }
01165      lastCleanup = now;
01166      if (ptm->tm_hour == 5)
01167         ReportEpgBugFixStats(true);
01168      }
01169   if (epgDataFileName && now - lastDump > 600) {
01170      cSafeFile f(epgDataFileName);
01171      if (f.Open()) {
01172         Dump(f);
01173         f.Close();
01174         }
01175      else
01176         LOG_ERROR;
01177      lastDump = now;
01178      }
01179 }
01180 
01181 void cSchedules::ResetVersions(void)
01182 {
01183   cSchedulesLock SchedulesLock(true);
01184   cSchedules *s = (cSchedules *)Schedules(SchedulesLock);
01185   if (s) {
01186      for (cSchedule *Schedule = s->First(); Schedule; Schedule = s->Next(Schedule))
01187          Schedule->ResetVersions();
01188      }
01189 }
01190 
01191 bool cSchedules::ClearAll(void)
01192 {
01193   cSchedulesLock SchedulesLock(true, 1000);
01194   cSchedules *s = (cSchedules *)Schedules(SchedulesLock);
01195   if (s) {
01196      for (cTimer *Timer = Timers.First(); Timer; Timer = Timers.Next(Timer))
01197          Timer->SetEvent(NULL);
01198      for (cSchedule *Schedule = s->First(); Schedule; Schedule = s->Next(Schedule))
01199          Schedule->Cleanup(INT_MAX);
01200      return true;
01201      }
01202   return false;
01203 }
01204 
01205 bool cSchedules::Dump(FILE *f, const char *Prefix, eDumpMode DumpMode, time_t AtTime)
01206 {
01207   cSchedulesLock SchedulesLock;
01208   cSchedules *s = (cSchedules *)Schedules(SchedulesLock);
01209   if (s) {
01210      for (cSchedule *p = s->First(); p; p = s->Next(p))
01211          p->Dump(f, Prefix, DumpMode, AtTime);
01212      return true;
01213      }
01214   return false;
01215 }
01216 
01217 bool cSchedules::Read(FILE *f)
01218 {
01219   cSchedulesLock SchedulesLock(true, 1000);
01220   cSchedules *s = (cSchedules *)Schedules(SchedulesLock);
01221   if (s) {
01222      bool OwnFile = f == NULL;
01223      if (OwnFile) {
01224         if (epgDataFileName && access(epgDataFileName, R_OK) == 0) {
01225            dsyslog("reading EPG data from %s", epgDataFileName);
01226            if ((f = fopen(epgDataFileName, "r")) == NULL) {
01227               LOG_ERROR;
01228               return false;
01229               }
01230            }
01231         else
01232            return false;
01233         }
01234      bool result = cSchedule::Read(f, s);
01235      if (OwnFile)
01236         fclose(f);
01237      if (result) {
01238         // Initialize the channels' schedule pointers, so that the first WhatsOn menu will come up faster:
01239         for (cChannel *Channel = Channels.First(); Channel; Channel = Channels.Next(Channel))
01240             s->GetSchedule(Channel);
01241         }
01242      return result;
01243      }
01244   return false;
01245 }
01246 
01247 cSchedule *cSchedules::AddSchedule(tChannelID ChannelID)
01248 {
01249   ChannelID.ClrRid();
01250   cSchedule *p = (cSchedule *)GetSchedule(ChannelID);
01251   if (!p) {
01252      p = new cSchedule(ChannelID);
01253      Add(p);
01254      cChannel *channel = Channels.GetByChannelID(ChannelID);
01255      if (channel)
01256         channel->schedule = p;
01257      }
01258   return p;
01259 }
01260 
01261 const cSchedule *cSchedules::GetSchedule(tChannelID ChannelID) const
01262 {
01263   ChannelID.ClrRid();
01264   for (cSchedule *p = First(); p; p = Next(p)) {
01265       if (p->ChannelID() == ChannelID)
01266          return p;
01267       }
01268   return NULL;
01269 }
01270 
01271 const cSchedule *cSchedules::GetSchedule(const cChannel *Channel, bool AddIfMissing) const
01272 {
01273   // This is not very beautiful, but it dramatically speeds up the
01274   // "What's on now/next?" menus.
01275   static cSchedule DummySchedule(tChannelID::InvalidID);
01276   if (!Channel->schedule)
01277      Channel->schedule = GetSchedule(Channel->GetChannelID());
01278   if (!Channel->schedule)
01279      Channel->schedule = &DummySchedule;
01280   if (Channel->schedule == &DummySchedule && AddIfMissing) {
01281      cSchedule *Schedule = new cSchedule(Channel->GetChannelID());
01282      ((cSchedules *)this)->Add(Schedule);
01283      Channel->schedule = Schedule;
01284      }
01285   return Channel->schedule != &DummySchedule? Channel->schedule : NULL;
01286 }
01287 
01288 // --- cEpgDataReader --------------------------------------------------------
01289 
01290 cEpgDataReader::cEpgDataReader(void)
01291 :cThread("epg data reader")
01292 {
01293 }
01294 
01295 void cEpgDataReader::Action(void)
01296 {
01297   cSchedules::Read();
01298 }
01299 
01300 // --- cEpgHandler -----------------------------------------------------------
01301 
01302 cEpgHandler::cEpgHandler(void)
01303 {
01304   EpgHandlers.Add(this);
01305 }
01306 
01307 cEpgHandler::~cEpgHandler()
01308 {
01309   EpgHandlers.Del(this, false);
01310 }
01311 
01312 // --- cEpgHandlers ----------------------------------------------------------
01313 
01314 cEpgHandlers EpgHandlers;
01315 
01316 bool cEpgHandlers::IgnoreChannel(const cChannel *Channel)
01317 {
01318   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01319       if (eh->IgnoreChannel(Channel))
01320          return true;
01321       }
01322   return false;
01323 }
01324 
01325 bool cEpgHandlers::HandleEitEvent(cSchedule *Schedule, const SI::EIT::Event *EitEvent, uchar TableID, uchar Version)
01326 {
01327   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01328       if (eh->HandleEitEvent(Schedule, EitEvent, TableID, Version))
01329          return true;
01330       }
01331   return false;
01332 }
01333 
01334 void cEpgHandlers::SetEventID(cEvent *Event, tEventID EventID)
01335 {
01336   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01337       if (eh->SetEventID(Event, EventID))
01338          return;
01339       }
01340   Event->SetEventID(EventID);
01341 }
01342 
01343 void cEpgHandlers::SetTitle(cEvent *Event, const char *Title)
01344 {
01345   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01346       if (eh->SetTitle(Event, Title))
01347          return;
01348       }
01349   Event->SetTitle(Title);
01350 }
01351 
01352 void cEpgHandlers::SetShortText(cEvent *Event, const char *ShortText)
01353 {
01354   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01355       if (eh->SetShortText(Event, ShortText))
01356          return;
01357       }
01358   Event->SetShortText(ShortText);
01359 }
01360 
01361 void cEpgHandlers::SetDescription(cEvent *Event, const char *Description)
01362 {
01363   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01364       if (eh->SetDescription(Event, Description))
01365          return;
01366       }
01367   Event->SetDescription(Description);
01368 }
01369 
01370 void cEpgHandlers::SetContents(cEvent *Event, uchar *Contents)
01371 {
01372   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01373       if (eh->SetContents(Event, Contents))
01374          return;
01375       }
01376   Event->SetContents(Contents);
01377 }
01378 
01379 void cEpgHandlers::SetParentalRating(cEvent *Event, int ParentalRating)
01380 {
01381   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01382       if (eh->SetParentalRating(Event, ParentalRating))
01383          return;
01384       }
01385   Event->SetParentalRating(ParentalRating);
01386 }
01387 
01388 void cEpgHandlers::SetStartTime(cEvent *Event, time_t StartTime)
01389 {
01390   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01391       if (eh->SetStartTime(Event, StartTime))
01392          return;
01393       }
01394   Event->SetStartTime(StartTime);
01395 }
01396 
01397 void cEpgHandlers::SetDuration(cEvent *Event, int Duration)
01398 {
01399   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01400       if (eh->SetDuration(Event, Duration))
01401          return;
01402       }
01403   Event->SetDuration(Duration);
01404 }
01405 
01406 void cEpgHandlers::SetVps(cEvent *Event, time_t Vps)
01407 {
01408   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01409       if (eh->SetVps(Event, Vps))
01410          return;
01411       }
01412   Event->SetVps(Vps);
01413 }
01414 
01415 void cEpgHandlers::FixEpgBugs(cEvent *Event)
01416 {
01417   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01418       if (eh->FixEpgBugs(Event))
01419          return;
01420       }
01421   Event->FixEpgBugs();
01422 }
01423 
01424 void cEpgHandlers::HandleEvent(cEvent *Event)
01425 {
01426   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01427       if (eh->HandleEvent(Event))
01428          break;
01429       }
01430 }
01431 
01432 void cEpgHandlers::SortSchedule(cSchedule *Schedule)
01433 {
01434   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01435       if (eh->SortSchedule(Schedule))
01436          return;
01437       }
01438   Schedule->Sort();
01439 }
01440 
01441 void cEpgHandlers::DropOutdated(cSchedule *Schedule, time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version)
01442 {
01443   for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
01444       if (eh->DropOutdated(Schedule, SegmentStart, SegmentEnd, TableID, Version))
01445          return;
01446       }
01447   Schedule->DropOutdated(SegmentStart, SegmentEnd, TableID, Version);
01448 }