mirror of
https://github.com/vdr-projects/vdr.git
synced 2025-03-01 10:50:46 +00:00
- Updated the Finnish OSD texts (thanks to Rolf Ahrenberg). - Fixed a crash when setting the time transponder in the Setup menu, caused by the new 'noneString' in cMenuEditChanItem (reported by Thomas Günther). - Added NULL checks to some strdup() calls in menuitems.c (suggested by Darren Salt). - Removed the unnecessary 'value' member from cMenuEditItem. - Fixed the initial setting of the time transponder setup parameter (reported by Thomas Günther). - Fixed unnecessary Set() calls in cMenuEditIntItem::ProcessKey(). - Allowing a tolerance for symbol rate values that are off by one (thanks to Richard Lithvall). - Added VBITeletextDescriptorTag, TeletextDescriptorTag, LocalTimeOffsetDescriptorTag and PremiereContentTransmissionDescriptor to 'libsi' (thanks to Marco Schlüßler). - Using geteuid() to check whether VDR is running as user 'root' (suggested by Tobias Grimm). - Added a missing "Key$" in skincurses.c (reported by Darren Salt). - Reintroduced the log message "deleting plugin: ..." when shutting down VDR (upon request by Ville Skyttä, as in the initial patch from Christoph Haubrich). - Fixed the vdr.1 man page (a single DVB card can record and do live tv). - The preferred audio language is now automatically selected when starting replay. - Updated the Danish OSD texts (thanks to Mogens Elneff). - The new function cPlugin::Active() can be used by a plugin to indicate that it is still busy and the system should not shut down or restart (based on a patch from Sascha Volkenandt). See PLUGINS.html for details. - Fixed setting the 'Delta' parameter when calling the shutdown script with no active timer (reported by Helge Lenz). - In order to make sure that plugins are compiled with the same DVB driver header files as VDR itself, the definition of DVBDIR has been removed from the VDR Makefile. If you are using a driver version that has its header files at a location other than /usr/include/linux/dvb, you can define DVBDIR in the Make.config file (see also INSTALL). Note that if you already have a Make.config file of your own, you should add the lines ifdef DVBDIR INCLUDES += -I$(DVBDIR)/include endif to its end, as in the new Make.config.template. Any reference to DVBDIR should be removed from all plugins' Makefiles, like this: ------------------------------------------------------------ --- PLUGINS/src/hello/Makefile 2005/11/11 13:20:14 1.10 +++ PLUGINS/src/hello/Makefile 2006/04/15 11:58:46 1.11 @@ -20,7 +20,6 @@ ### The directory environment: -DVBDIR = ../../../../DVB VDRDIR = ../../.. LIBDIR = ../../lib TMPDIR = /tmp @@ -40,7 +39,7 @@ ### Includes and Defines (add further entries here): -INCLUDES += -I$(VDRDIR)/include -I$(DVBDIR)/include +INCLUDES += -I$(VDRDIR)/include DEFINES += -D_GNU_SOURCE -DPLUGIN_NAME_I18N='"$(PLUGIN)"' ------------------------------------------------------------ Thanks to Marco Schlüßler for pointing out this problem. - Implemented kNext and kPrev keys (based on a patch from Peter Juszack). See MANUAL for details. - Implemented kChanPrev (from a patch from Darren Salt). - The "Update channels" parameter in the "Setup/DVB" menu has been extended to allow updating only the PIDs (see MANUAL for details). When updating to this version of VDR, please verify that the setting is as you want it to be, because the values have been shifted. - The new APIVERSION (see config.h) now allows existing compiled plugins to be used with newer versions of VDR, as long as there have been no changes to the VDR header files since the last APIVERSION. Existing plugins' Makefiles should have all references to VDRVERSION changed to APIVERSION, like this: ------------------------------------------------------------ --- PLUGINS/src/hello/Makefile 2006/04/15 11:58:46 1.11 +++ PLUGINS/src/hello/Makefile 2006/04/16 09:03:50 1.12 @@ -28,9 +28,9 @@ -include $(VDRDIR)/Make.config -### The version number of VDR (taken from VDR's "config.h"): +### The version number of VDR's plugin API (taken from VDR's "config.h"): -VDRVERSION = $(shell grep 'define VDRVERSION ' $(VDRDIR)/config.h | awk '{ print $$3 }' | sed -e 's/"//g') +APIVERSION = $(shell sed -ne '/define APIVERSION/s/^.*"\(.*\)".*$$/\1/p' $(VDRDIR)/config.h) ### The name of the distribution archive: @@ -67,7 +67,7 @@ libvdr-$(PLUGIN).so: $(OBJS) $(CXX) $(CXXFLAGS) -shared $(OBJS) -o $@ - @cp $@ $(LIBDIR)/$@.$(VDRVERSION) + @cp $@ $(LIBDIR)/$@.$(APIVERSION) dist: clean @-rm -rf $(TMPDIR)/$(ARCHIVE) ------------------------------------------------------------ - If a timer is newly created from within the "Schedule" menu, and its event is already running or has its start time within the next two minutes, it now goes directly into the "Edit timer" menu in order to allow the user to make further changes to timer parameters before the actual recording starts (inspired by Christian Wieninger's "epgsearch" plugin). - Added format check to syslog_with_tid(). - Updated the INSTALL file's section about non-VDR files in the video directory (since version 1.3.38 disk access is done in a separate thread, so the watchdog timer won't hit). - Changed the behaviour of the "Use small font" setup option to *always* use the small font if set to '2' - even if it would have been a fixed font (suggested by Ronny Kornexl). - No longer using characters 0x01 and 0x02 for mapping single quote and slash in recording names (thanks to Patrick Fischer for reporting that this was a problem with XML). The single quote is not mapped at all, and the slash is interchanged with the tilde. Existing recordings will be handled like before, so there is no need to actually rename them. - The cRemote::CallPlugin() function now has a boolean return value that tells the caller whether initiating the plugin call was successful (suggested by Reinhard Nissl). If it returns false, another plugin call is currently pending and the caller should try again later. This also means that the SVDRP command PLUG can now return an error code is the call fails. - The new function cPlugin::MainThreadHook() can be used by plugins to perform actions in the context of the main program thread. Use this function with great care and only of you absolutely have to! See also PLUGINS.html.
1056 lines
31 KiB
C
1056 lines
31 KiB
C
/*
|
|
* 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 <Robert.Schneider@web.de> and Rolf Hakenes <hakenes@hippomi.de>.
|
|
*
|
|
* $Id: epg.c 1.71 2006/04/17 12:18:59 kls Exp $
|
|
*/
|
|
|
|
#include "epg.h"
|
|
#include <ctype.h>
|
|
#include <time.h>
|
|
#include "libsi/si.h"
|
|
#include "timers.h"
|
|
|
|
#define RUNNINGSTATUSTIMEOUT 30 // seconds before the running status is considered unknown
|
|
|
|
// --- 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 %a[^\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);
|
|
}
|
|
|
|
void cComponents::Realloc(int Index)
|
|
{
|
|
if (Index >= numComponents) {
|
|
int n = numComponents;
|
|
numComponents = Index + 1;
|
|
components = (tComponent *)realloc(components, numComponents * sizeof(tComponent));
|
|
memset(&components[n], 0, sizeof(tComponent) * (numComponents - n));
|
|
}
|
|
}
|
|
|
|
void cComponents::SetComponent(int Index, const char *s)
|
|
{
|
|
Realloc(Index);
|
|
components[Index].FromString(s);
|
|
}
|
|
|
|
void cComponents::SetComponent(int Index, uchar Stream, uchar Type, const char *Language, const char *Description)
|
|
{
|
|
Realloc(Index);
|
|
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 && components[i].type == Type) {
|
|
if (!Index--)
|
|
return &components[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// --- cEvent ----------------------------------------------------------------
|
|
|
|
cEvent::cEvent(tEventID EventID)
|
|
{
|
|
schedule = NULL;
|
|
eventID = EventID;
|
|
tableID = 0;
|
|
version = 0xFF; // actual version numbers are 0..31
|
|
runningStatus = SI::RunningStatusUndefined;
|
|
title = NULL;
|
|
shortText = NULL;
|
|
description = NULL;
|
|
components = NULL;
|
|
startTime = 0;
|
|
duration = 0;
|
|
vps = 0;
|
|
SetSeen();
|
|
}
|
|
|
|
cEvent::~cEvent()
|
|
{
|
|
free(title);
|
|
free(shortText);
|
|
free(description);
|
|
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, cChannel *Channel)
|
|
{
|
|
if (Channel && runningStatus != RunningStatus && (RunningStatus > SI::RunningStatusNotRunning || runningStatus > SI::RunningStatusUndefined) && Channel->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::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);
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
bool cEvent::HasTimer(void) const
|
|
{
|
|
for (cTimer *t = Timers.First(); t; t = Timers.Next(t)) {
|
|
if (t->Event() == this)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool cEvent::IsRunning(bool OrAboutToStart) const
|
|
{
|
|
return SeenWithin(RUNNINGSTATUSTIMEOUT) && runningStatus >= (OrAboutToStart ? SI::RunningStatusStartsInAFewSeconds : SI::RunningStatusPausing);
|
|
}
|
|
|
|
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 + Setup.EPGLinger * 60 >= time(NULL)) {
|
|
fprintf(f, "%sE %u %ld %d %X %X\n", Prefix, eventID, 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 (components) {
|
|
for (int i = 0; i < components->NumComponents(); i++) {
|
|
tComponent *p = components->Component(i);
|
|
if (!Setup.UseDolbyDigital && p->stream == 0x02 && p->type == 0x05)
|
|
continue;
|
|
fprintf(f, "%sX %s\n", Prefix, *p->ToString());
|
|
}
|
|
}
|
|
if (vps)
|
|
fprintf(f, "%sV %ld\n", Prefix, vps);
|
|
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 'X': if (!components)
|
|
components = new cComponents;
|
|
components->SetComponent(components->NumComponents(), t);
|
|
break;
|
|
case 'V': SetVps(atoi(t));
|
|
break;
|
|
default: esyslog("ERROR: unexpected tag while reading EPG data: %s", s);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool cEvent::Read(FILE *f, cSchedule *Schedule)
|
|
{
|
|
if (Schedule) {
|
|
cEvent *Event = NULL;
|
|
char *s;
|
|
int line = 0;
|
|
cReadLine ReadLine;
|
|
while ((s = ReadLine.Read(f)) != NULL) {
|
|
line++;
|
|
char *t = skipspace(s + 1);
|
|
switch (*s) {
|
|
case 'E': if (!Event) {
|
|
unsigned int EventID;
|
|
time_t StartTime;
|
|
int Duration;
|
|
unsigned int TableID = 0;
|
|
unsigned int Version = 0xFF; // actual value is ignored
|
|
int n = sscanf(t, "%u %ld %d %X %X", &EventID, &StartTime, &Duration, &TableID, &Version);
|
|
if (n >= 3 && n <= 5) {
|
|
Event = (cEvent *)Schedule->GetEvent(EventID, StartTime);
|
|
cEvent *newEvent = NULL;
|
|
if (Event)
|
|
DELETENULL(Event->components);
|
|
if (!Event)
|
|
Event = newEvent = new cEvent(EventID);
|
|
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 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 title and shortText of EPG
|
|
// data, so let's always convert them to blanks (independent of the setting of EPGBugfixLevel):
|
|
strreplace(title, '\n', ' ');
|
|
strreplace(shortText, '\n', ' ');
|
|
// Same for control characters:
|
|
strreplace(title, '\x86', ' ');
|
|
strreplace(title, '\x87', ' ');
|
|
strreplace(shortText, '\x86', ' ');
|
|
strreplace(shortText, '\x87', ' ');
|
|
strreplace(description, '\x86', ' ');
|
|
strreplace(description, '\x87', ' ');
|
|
|
|
if (!title) {
|
|
// we don't want any "(null)" titles
|
|
title = strcpyrealloc(title, tr("No title"));
|
|
EpgBugFixStat(12, ChannelID());
|
|
}
|
|
|
|
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:
|
|
|
|
// 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)
|
|
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);
|
|
|
|
#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)
|
|
return;
|
|
|
|
// 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;
|
|
}
|
|
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;
|
|
// all others will just display the language
|
|
}
|
|
EpgBugFixStat(11, ChannelID());
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- cSchedule -------------------------------------------------------------
|
|
|
|
cSchedule::cSchedule(tChannelID ChannelID)
|
|
{
|
|
channelID = ChannelID;
|
|
hasRunning = false;;
|
|
modified = 0;
|
|
presentSeen = 0;
|
|
}
|
|
|
|
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);
|
|
events.Del(Event);
|
|
}
|
|
}
|
|
|
|
void cSchedule::HashEvent(cEvent *Event)
|
|
{
|
|
eventsHashID.Add(Event, Event->EventID());
|
|
if (Event->StartTime() > 0) // 'StartTime < 0' is apparently used with NVOD channels
|
|
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 (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;
|
|
}
|
|
|
|
const cEvent *cSchedule::GetEvent(tEventID 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 = eventsHashID.Get(EventID);
|
|
if (!pt && StartTime > 0) // 'StartTime < 0' is apparently used with NVOD channels
|
|
pt = eventsHashStartTime.Get(StartTime);
|
|
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->EndTime() >= Time) {
|
|
delta = dt;
|
|
pe = p;
|
|
}
|
|
}
|
|
return pe;
|
|
}
|
|
|
|
void cSchedule::SetRunningStatus(cEvent *Event, int RunningStatus, cChannel *Channel)
|
|
{
|
|
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);
|
|
}
|
|
else if (RunningStatus >= SI::RunningStatusPausing && p->StartTime() < Event->StartTime())
|
|
p->SetRunningStatus(SI::RunningStatusNotRunning);
|
|
}
|
|
if (RunningStatus >= SI::RunningStatusPausing)
|
|
hasRunning = true;
|
|
}
|
|
|
|
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;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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::DropOutdated(time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version)
|
|
{
|
|
if (SegmentStart > 0 && SegmentEnd > 0) {
|
|
for (cEvent *p = events.First(); p; p = events.Next(p)) {
|
|
if (p->EndTime() > SegmentStart) {
|
|
if (p->StartTime() < SegmentEnd) {
|
|
// The event overlaps with the given time segment.
|
|
if (p->TableID() > TableID || p->TableID() == TableID && p->Version() != Version) {
|
|
// The segment overwrites all events from tables with higher ids, and
|
|
// within the same table id all events must have the same version.
|
|
// We can't delete the event right here because a timer might have
|
|
// a pointer to it, so let's set its id and start time to 0 to have it
|
|
// "phased out":
|
|
UnhashEvent(p);
|
|
p->eventID = 0;
|
|
p->startTime = 0;
|
|
}
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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() + Setup.EPGLinger * 60 + 3600 < Time) // adding one hour for safety
|
|
DelEvent(Event);
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
|
|
void cSchedule::Dump(FILE *f, const char *Prefix, eDumpMode DumpMode, time_t AtTime) const
|
|
{
|
|
cChannel *channel = Channels.GetByChannelID(channelID, true);
|
|
if (channel) {
|
|
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;
|
|
}
|
|
fprintf(f, "%sc\n", Prefix);
|
|
}
|
|
}
|
|
|
|
bool cSchedule::Read(FILE *f, cSchedules *Schedules)
|
|
{
|
|
if (Schedules) {
|
|
cReadLine ReadLine;
|
|
char *s;
|
|
while ((s = ReadLine.Read(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();
|
|
Schedules->SetModified(p);
|
|
}
|
|
}
|
|
else {
|
|
esyslog("ERROR: invalid 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);
|
|
time_t cSchedules::modified = 0;
|
|
|
|
const cSchedules *cSchedules::Schedules(cSchedulesLock &SchedulesLock)
|
|
{
|
|
return SchedulesLock.Locked() ? &schedules : NULL;
|
|
}
|
|
|
|
void cSchedules::SetEpgDataFileName(const char *FileName)
|
|
{
|
|
delete epgDataFileName;
|
|
epgDataFileName = FileName ? strdup(FileName) : NULL;
|
|
}
|
|
|
|
void cSchedules::SetModified(cSchedule *Schedule)
|
|
{
|
|
Schedule->SetModified();
|
|
modified = time(NULL);
|
|
}
|
|
|
|
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) {
|
|
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;
|
|
if (ptm->tm_hour == 5)
|
|
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) {
|
|
for (cTimer *Timer = Timers.First(); Timer; Timer = Timers.Next(Timer))
|
|
Timer->SetEvent(NULL);
|
|
for (cSchedule *Schedule = s->First(); Schedule; Schedule = s->Next(Schedule))
|
|
Schedule->Cleanup(INT_MAX);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool cSchedules::Dump(FILE *f, const char *Prefix, eDumpMode DumpMode, time_t AtTime)
|
|
{
|
|
cSchedulesLock SchedulesLock;
|
|
cSchedules *s = (cSchedules *)Schedules(SchedulesLock);
|
|
if (s) {
|
|
for (cSchedule *p = s->First(); p; p = s->Next(p))
|
|
p->Dump(f, Prefix, DumpMode, AtTime);
|
|
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);
|
|
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))
|
|
s->GetSchedule(Channel);
|
|
}
|
|
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;
|
|
}
|
|
|
|
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.
|
|
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;
|
|
}
|