mirror of
https://github.com/VDR4Arch/vdr.git
synced 2023-10-10 13:36:52 +02:00
1284 lines
35 KiB
C
1284 lines
35 KiB
C
/***************************************************************************
|
|
eit.c - description
|
|
-------------------
|
|
begin : Fri Aug 25 2000
|
|
copyright : (C) 2000 by Robert Schneider
|
|
email : Robert.Schneider@web.de
|
|
|
|
2001-08-15: Adapted to 'libdtv' by Rolf Hakenes <hakenes@hippomi.de>
|
|
|
|
***************************************************************************/
|
|
|
|
/***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* $Id: eit.c 1.60 2002/11/10 15:50:21 kls Exp $
|
|
***************************************************************************/
|
|
|
|
#include "eit.h"
|
|
#include <ctype.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <linux/dvb/dmx.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/poll.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include "channels.h"
|
|
#include "config.h"
|
|
#include "libdtv/libdtv.h"
|
|
#include "videodir.h"
|
|
|
|
// --- cMJD ------------------------------------------------------------------
|
|
|
|
class cMJD {
|
|
public:
|
|
cMJD();
|
|
cMJD(u_char date_hi, u_char date_lo);
|
|
cMJD(u_char date_hi, u_char date_lo, u_char timehr, u_char timemi, u_char timese);
|
|
~cMJD();
|
|
/** */
|
|
void ConvertToTime();
|
|
/** */
|
|
bool SetSystemTime();
|
|
/** */
|
|
time_t GetTime_t();
|
|
protected: // Protected attributes
|
|
/** */
|
|
time_t mjdtime;
|
|
protected: // Protected attributes
|
|
/** */
|
|
u_char time_second;
|
|
protected: // Protected attributes
|
|
/** */
|
|
u_char time_minute;
|
|
protected: // Protected attributes
|
|
/** */
|
|
u_char time_hour;
|
|
protected: // Protected attributes
|
|
/** */
|
|
u_short mjd;
|
|
};
|
|
|
|
cMJD::cMJD()
|
|
{
|
|
}
|
|
|
|
cMJD::cMJD(u_char date_hi, u_char date_lo)
|
|
{
|
|
mjd = date_hi << 8 | date_lo;
|
|
time_hour = time_minute = time_second = 0;
|
|
ConvertToTime();
|
|
}
|
|
|
|
cMJD::cMJD(u_char date_hi, u_char date_lo, u_char timehr, u_char timemi, u_char timese)
|
|
{
|
|
mjd = date_hi << 8 | date_lo;
|
|
time_hour = timehr;
|
|
time_minute = timemi;
|
|
time_second = timese;
|
|
ConvertToTime();
|
|
}
|
|
|
|
cMJD::~cMJD()
|
|
{
|
|
}
|
|
|
|
/** */
|
|
void cMJD::ConvertToTime()
|
|
{
|
|
struct tm t;
|
|
|
|
t.tm_sec = time_second;
|
|
t.tm_min = time_minute;
|
|
t.tm_hour = time_hour;
|
|
int k;
|
|
|
|
t.tm_year = (int) ((mjd - 15078.2) / 365.25);
|
|
t.tm_mon = (int) ((mjd - 14956.1 - (int)(t.tm_year * 365.25)) / 30.6001);
|
|
t.tm_mday = (int) (mjd - 14956 - (int)(t.tm_year * 365.25) - (int)(t.tm_mon * 30.6001));
|
|
k = (t.tm_mon == 14 || t.tm_mon == 15) ? 1 : 0;
|
|
t.tm_year = t.tm_year + k;
|
|
t.tm_mon = t.tm_mon - 1 - k * 12;
|
|
t.tm_mon--;
|
|
|
|
t.tm_isdst = -1;
|
|
t.tm_gmtoff = 0;
|
|
|
|
mjdtime = timegm(&t);
|
|
|
|
//isyslog("Time parsed = %s\n", ctime(&mjdtime));
|
|
}
|
|
|
|
/** */
|
|
bool cMJD::SetSystemTime()
|
|
{
|
|
struct tm *ptm;
|
|
time_t loctim;
|
|
|
|
struct tm tm_r;
|
|
ptm = localtime_r(&mjdtime, &tm_r);
|
|
loctim = time(NULL);
|
|
|
|
if (abs(mjdtime - loctim) > 2)
|
|
{
|
|
isyslog("System Time = %s (%ld)\n", ctime(&loctim), loctim);
|
|
isyslog("Local Time = %s (%ld)\n", ctime(&mjdtime), mjdtime);
|
|
if (stime(&mjdtime) < 0)
|
|
esyslog("ERROR while setting system time: %m");
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
/** */
|
|
time_t cMJD::GetTime_t()
|
|
{
|
|
return mjdtime;
|
|
}
|
|
|
|
// --- cTDT ------------------------------------------------------------------
|
|
|
|
class cTDT {
|
|
public:
|
|
cTDT(tdt_t *ptdt);
|
|
~cTDT();
|
|
/** */
|
|
bool SetSystemTime();
|
|
protected: // Protected attributes
|
|
/** */
|
|
tdt_t tdt;
|
|
/** */
|
|
cMJD mjd; // kls 2001-03-02: made this a member instead of a pointer (it wasn't deleted in the destructor!)
|
|
};
|
|
|
|
#define BCD2DEC(b) (((b >> 4) & 0x0F) * 10 + (b & 0x0F))
|
|
|
|
cTDT::cTDT(tdt_t *ptdt)
|
|
:tdt(*ptdt)
|
|
,mjd(tdt.utc_mjd_hi, tdt.utc_mjd_lo, BCD2DEC(tdt.utc_time_h), BCD2DEC(tdt.utc_time_m), BCD2DEC(tdt.utc_time_s))
|
|
{
|
|
}
|
|
|
|
cTDT::~cTDT()
|
|
{
|
|
}
|
|
/** */
|
|
bool cTDT::SetSystemTime()
|
|
{
|
|
return mjd.SetSystemTime();
|
|
}
|
|
|
|
// --- cEventInfo ------------------------------------------------------------
|
|
|
|
cEventInfo::cEventInfo(uint64 channelid, unsigned short eventid)
|
|
{
|
|
pTitle = NULL;
|
|
pSubtitle = NULL;
|
|
pExtendedDescription = NULL;
|
|
bIsPresent = bIsFollowing = false;
|
|
lDuration = 0;
|
|
tTime = 0;
|
|
uTableID = 0;
|
|
uEventID = eventid;
|
|
uChannelID = channelid;
|
|
nChannelNumber = 0;
|
|
}
|
|
|
|
cEventInfo::~cEventInfo()
|
|
{
|
|
free(pTitle);
|
|
free(pSubtitle);
|
|
free(pExtendedDescription);
|
|
}
|
|
|
|
/** */
|
|
const char * cEventInfo::GetTitle() const
|
|
{
|
|
return pTitle;
|
|
}
|
|
/** */
|
|
const char * cEventInfo::GetSubtitle() const
|
|
{
|
|
return pSubtitle;
|
|
}
|
|
/** */
|
|
const char * cEventInfo::GetExtendedDescription() const
|
|
{
|
|
return pExtendedDescription;
|
|
}
|
|
/** */
|
|
bool cEventInfo::IsPresent() const
|
|
{
|
|
return bIsPresent;
|
|
}
|
|
/** */
|
|
void cEventInfo::SetPresent(bool pres)
|
|
{
|
|
bIsPresent = pres;
|
|
}
|
|
/** */
|
|
bool cEventInfo::IsFollowing() const
|
|
{
|
|
return bIsFollowing;
|
|
}
|
|
|
|
void cEventInfo::SetTableID(unsigned char tableid)
|
|
{
|
|
uTableID = tableid;
|
|
}
|
|
|
|
/** */
|
|
void cEventInfo::SetFollowing(bool foll)
|
|
{
|
|
bIsFollowing = foll;
|
|
}
|
|
/** */
|
|
const char * cEventInfo::GetDate() const
|
|
{
|
|
static char szDate[25];
|
|
|
|
struct tm tm_r;
|
|
strftime(szDate, sizeof(szDate), "%d.%m.%Y", localtime_r(&tTime, &tm_r));
|
|
|
|
return szDate;
|
|
}
|
|
|
|
const unsigned char cEventInfo::GetTableID(void) const
|
|
{
|
|
return uTableID;
|
|
}
|
|
|
|
/** */
|
|
const char * cEventInfo::GetTimeString() const
|
|
{
|
|
static char szTime[25];
|
|
|
|
struct tm tm_r;
|
|
strftime(szTime, sizeof(szTime), "%R", localtime_r(&tTime, &tm_r));
|
|
|
|
return szTime;
|
|
}
|
|
/** */
|
|
const char * cEventInfo::GetEndTimeString() const
|
|
{
|
|
static char szEndTime[25];
|
|
time_t tEndTime = tTime + lDuration;
|
|
|
|
struct tm tm_r;
|
|
strftime(szEndTime, sizeof(szEndTime), "%R", localtime_r(&tEndTime, &tm_r));
|
|
|
|
return szEndTime;
|
|
}
|
|
/** */
|
|
time_t cEventInfo::GetTime() const
|
|
{
|
|
return tTime;
|
|
}
|
|
/** */
|
|
long cEventInfo::GetDuration() const
|
|
{
|
|
return lDuration;
|
|
}
|
|
/** */
|
|
unsigned short cEventInfo::GetEventID() const
|
|
{
|
|
return uEventID;
|
|
}
|
|
/** */
|
|
void cEventInfo::SetTitle(const char *string)
|
|
{
|
|
pTitle = strcpyrealloc(pTitle, string);
|
|
}
|
|
/** */
|
|
void cEventInfo::SetSubtitle(const char *string)
|
|
{
|
|
pSubtitle = strcpyrealloc(pSubtitle, string);
|
|
}
|
|
/** */
|
|
void cEventInfo::SetExtendedDescription(const char *string)
|
|
{
|
|
pExtendedDescription = strcpyrealloc(pExtendedDescription, string);
|
|
}
|
|
/** */
|
|
void cEventInfo::SetTime(time_t t)
|
|
{
|
|
tTime = t;
|
|
}
|
|
/** */
|
|
void cEventInfo::SetDuration(long l)
|
|
{
|
|
lDuration = l;
|
|
}
|
|
/** */
|
|
void cEventInfo::SetEventID(unsigned short evid)
|
|
{
|
|
uEventID = evid;
|
|
}
|
|
/** */
|
|
void cEventInfo::SetChannelID(uint64 channelid)
|
|
{
|
|
uChannelID = channelid;
|
|
}
|
|
|
|
/** */
|
|
uint64 cEventInfo::GetChannelID() const
|
|
{
|
|
return uChannelID;
|
|
}
|
|
|
|
/** */
|
|
void cEventInfo::Dump(FILE *f, const char *Prefix) const
|
|
{
|
|
if (tTime + lDuration >= time(NULL)) {
|
|
fprintf(f, "%sE %u %ld %ld %X\n", Prefix, uEventID, tTime, lDuration, uTableID);
|
|
if (!isempty(pTitle))
|
|
fprintf(f, "%sT %s\n", Prefix, pTitle);
|
|
if (!isempty(pSubtitle))
|
|
fprintf(f, "%sS %s\n", Prefix, pSubtitle);
|
|
if (!isempty(pExtendedDescription))
|
|
fprintf(f, "%sD %s\n", Prefix, pExtendedDescription);
|
|
fprintf(f, "%se\n", Prefix);
|
|
}
|
|
}
|
|
|
|
bool cEventInfo::Read(FILE *f, cSchedule *Schedule)
|
|
{
|
|
if (Schedule) {
|
|
cEventInfo *pEvent = NULL;
|
|
char *s;
|
|
while ((s = readline(f)) != NULL) {
|
|
char *t = skipspace(s + 1);
|
|
switch (*s) {
|
|
case 'E': if (!pEvent) {
|
|
unsigned int uEventID;
|
|
time_t tTime;
|
|
long lDuration;
|
|
unsigned int uTableID = 0;
|
|
int n = sscanf(t, "%u %ld %ld %X", &uEventID, &tTime, &lDuration, &uTableID);
|
|
if (n == 3 || n == 4) {
|
|
pEvent = (cEventInfo *)Schedule->GetEvent(uEventID, tTime);
|
|
if (!pEvent)
|
|
pEvent = Schedule->AddEvent(new cEventInfo(Schedule->GetChannelID(), uEventID));
|
|
if (pEvent) {
|
|
pEvent->SetTableID(uTableID);
|
|
pEvent->SetTime(tTime);
|
|
pEvent->SetDuration(lDuration);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 'T': if (pEvent)
|
|
pEvent->SetTitle(t);
|
|
break;
|
|
case 'S': if (pEvent)
|
|
pEvent->SetSubtitle(t);
|
|
break;
|
|
case 'D': if (pEvent)
|
|
pEvent->SetExtendedDescription(t);
|
|
break;
|
|
case 'e': pEvent = NULL;
|
|
break;
|
|
case 'c': // to keep things simple we react on 'c' here
|
|
return true;
|
|
default: esyslog("ERROR: unexpected tag while reading EPG data: %s", s);
|
|
return false;
|
|
}
|
|
}
|
|
esyslog("ERROR: unexpected end of file while reading EPG data");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#define MAXEPGBUGFIXSTATS 6
|
|
#define MAXEPGBUGFIXCHANS 50
|
|
struct tEpgBugFixStats {
|
|
int hits;
|
|
int n;
|
|
uint64 channelIDs[MAXEPGBUGFIXCHANS];
|
|
tEpgBugFixStats(void) { hits = n = 0; }
|
|
};
|
|
|
|
tEpgBugFixStats EpgBugFixStats[MAXEPGBUGFIXSTATS];
|
|
|
|
static void EpgBugFixStat(int Number, uint64 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;
|
|
}
|
|
}
|
|
|
|
static void ReportEpgBugFixStats(bool Reset = false)
|
|
{
|
|
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) {
|
|
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 cEventInfo::FixEpgBugs()");
|
|
dsyslog("IN VDR/eit.c TO LEARN WHAT'S WRONG WITH YOUR DATA, AND FIX IT!");
|
|
dsyslog("=====================");
|
|
dsyslog("Fix\tHits\tChannels");
|
|
GotHits = true;
|
|
}
|
|
char *q = buffer;
|
|
q += snprintf(q, sizeof(buffer) - (q - buffer), "%d\t%d", i, p->hits);
|
|
for (int c = 0; c < p->n; c++) {
|
|
cChannel *channel = Channels.GetByChannelID(p->channelIDs[c]);
|
|
if (channel) {
|
|
q += snprintf(q, sizeof(buffer) - (q - buffer), "%s%s", delim, channel->Name());
|
|
delim = ", ";
|
|
}
|
|
}
|
|
dsyslog("%s", buffer);
|
|
}
|
|
if (Reset)
|
|
p->hits = p->n = 0;
|
|
}
|
|
if (GotHits)
|
|
dsyslog("=====================");
|
|
}
|
|
}
|
|
|
|
void cEventInfo::FixEpgBugs(void)
|
|
{
|
|
// VDR can't usefully handle newline characters in the EPG data, so let's
|
|
// always convert them to blanks (independent of the setting of EPGBugfixLevel):
|
|
strreplace(pTitle, '\n', ' ');
|
|
strreplace(pSubtitle, '\n', ' ');
|
|
strreplace(pExtendedDescription, '\n', ' ');
|
|
|
|
if (Setup.EPGBugfixLevel == 0)
|
|
return;
|
|
|
|
// Some TV stations apparently have their own idea about how to fill in the
|
|
// EPG data. Let's fix their bugs as good as we can:
|
|
if (pTitle) {
|
|
|
|
// VOX puts too much information into the Subtitle and leaves the Extended
|
|
// Description empty:
|
|
//
|
|
// Title
|
|
// (NAT, Year Min')[ ["Subtitle". ]Extended Description]
|
|
//
|
|
if (pSubtitle && !pExtendedDescription) {
|
|
if (*pSubtitle == '(') {
|
|
char *e = strchr(pSubtitle + 1, ')');
|
|
if (e) {
|
|
if (*(e + 1)) {
|
|
if (*++e == ' ')
|
|
if (*(e + 1) == '"')
|
|
e++;
|
|
}
|
|
else
|
|
e = NULL;
|
|
char *s = e ? strdup(e) : NULL;
|
|
free(pSubtitle);
|
|
pSubtitle = s;
|
|
EpgBugFixStat(0, GetChannelID());
|
|
// now the fixes #1 and #2 below will handle the rest
|
|
}
|
|
}
|
|
}
|
|
|
|
// VOX and VIVA put the Subtitle in quotes and use either the Subtitle
|
|
// or the Extended Description field, depending on how long the string is:
|
|
//
|
|
// Title
|
|
// "Subtitle". Extended Description
|
|
//
|
|
if ((pSubtitle == NULL) != (pExtendedDescription == NULL)) {
|
|
char *p = pSubtitle ? pSubtitle : pExtendedDescription;
|
|
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(pSubtitle);
|
|
free(pExtendedDescription);
|
|
pSubtitle = s;
|
|
pExtendedDescription = d;
|
|
EpgBugFixStat(1, GetChannelID());
|
|
}
|
|
}
|
|
}
|
|
|
|
// VOX and VIVA put the Extended Description into the Subtitle (preceeded
|
|
// by a blank) if there is no actual Subtitle and the Extended Description
|
|
// is short enough:
|
|
//
|
|
// Title
|
|
// Extended Description
|
|
//
|
|
if (pSubtitle && !pExtendedDescription) {
|
|
if (*pSubtitle == ' ') {
|
|
memmove(pSubtitle, pSubtitle + 1, strlen(pSubtitle));
|
|
pExtendedDescription = pSubtitle;
|
|
pSubtitle = NULL;
|
|
EpgBugFixStat(2, GetChannelID());
|
|
}
|
|
}
|
|
|
|
// Pro7 sometimes repeats the Title in the Subtitle:
|
|
//
|
|
// Title
|
|
// Title
|
|
//
|
|
if (pSubtitle && strcmp(pTitle, pSubtitle) == 0) {
|
|
free(pSubtitle);
|
|
pSubtitle = NULL;
|
|
EpgBugFixStat(3, GetChannelID());
|
|
}
|
|
|
|
// ZDF.info puts the Subtitle between double quotes, which is nothing
|
|
// but annoying (some even put a '.' after the closing '"'):
|
|
//
|
|
// Title
|
|
// "Subtitle"[.]
|
|
//
|
|
if (pSubtitle && *pSubtitle == '"') {
|
|
int l = strlen(pSubtitle);
|
|
if (l > 2 && (pSubtitle[l - 1] == '"' || (pSubtitle[l - 1] == '.' && pSubtitle[l - 2] == '"'))) {
|
|
memmove(pSubtitle, pSubtitle + 1, l);
|
|
char *p = strrchr(pSubtitle, '"');
|
|
if (p)
|
|
*p = 0;
|
|
EpgBugFixStat(4, GetChannelID());
|
|
}
|
|
}
|
|
|
|
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:
|
|
pTitle = compactspace(pTitle);
|
|
pSubtitle = compactspace(pSubtitle);
|
|
pExtendedDescription = compactspace(pExtendedDescription);
|
|
// Remove superfluous hyphens:
|
|
if (pExtendedDescription) {
|
|
char *p = pExtendedDescription;
|
|
while (*p && *(p + 1) && *(p + 2)) {
|
|
if (*p == '-' && *(p + 1) == ' ' && *(p + 2) && islower(*(p - 1)) && islower(*(p + 2))) {
|
|
if (!startswith(p + 2, "und ")) { // special case in German, as in "Lach- und Sachgeschichten"
|
|
memmove(p, p + 2, strlen(p + 2) + 1);
|
|
EpgBugFixStat(5, GetChannelID());
|
|
}
|
|
}
|
|
p++;
|
|
}
|
|
}
|
|
|
|
// 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(pTitle, '`', '\'');
|
|
strreplace(pSubtitle, '`', '\'');
|
|
strreplace(pExtendedDescription, '`', '\'');
|
|
}
|
|
}
|
|
|
|
// --- cSchedule -------------------------------------------------------------
|
|
|
|
cSchedule::cSchedule(uint64 channelid)
|
|
{
|
|
pPresent = pFollowing = NULL;
|
|
uChannelID = channelid;
|
|
}
|
|
|
|
|
|
cSchedule::~cSchedule()
|
|
{
|
|
}
|
|
|
|
cEventInfo *cSchedule::AddEvent(cEventInfo *EventInfo)
|
|
{
|
|
Events.Add(EventInfo);
|
|
return EventInfo;
|
|
}
|
|
|
|
const cEventInfo *cSchedule::GetPresentEvent(void) const
|
|
{
|
|
return GetEventAround(time(NULL));
|
|
}
|
|
|
|
const cEventInfo *cSchedule::GetFollowingEvent(void) const
|
|
{
|
|
const cEventInfo *pe = NULL;
|
|
time_t now = time(NULL);
|
|
time_t delta = INT_MAX;
|
|
for (cEventInfo *p = Events.First(); p; p = Events.Next(p)) {
|
|
time_t dt = p->GetTime() - now;
|
|
if (dt > 0 && dt < delta) {
|
|
delta = dt;
|
|
pe = p;
|
|
}
|
|
}
|
|
return pe;
|
|
}
|
|
|
|
void cSchedule::SetChannelID(uint64 channelid)
|
|
{
|
|
uChannelID = channelid;
|
|
}
|
|
/** */
|
|
uint64 cSchedule::GetChannelID() const
|
|
{
|
|
return uChannelID;
|
|
}
|
|
/** */
|
|
const cEventInfo * cSchedule::GetEvent(unsigned short uEventID, time_t tTime) const
|
|
{
|
|
// Returns either the event info with the given uEventID or, if that one can't
|
|
// be found, the one with the given tTime (or NULL if neither can be found)
|
|
cEventInfo *pe = Events.First();
|
|
cEventInfo *pt = NULL;
|
|
while (pe != NULL)
|
|
{
|
|
if (pe->GetEventID() == uEventID)
|
|
return pe;
|
|
if (tTime > 0 && pe->GetTime() == tTime) // 'tTime < 0' is apparently used with NVOD channels
|
|
pt = pe;
|
|
|
|
pe = Events.Next(pe);
|
|
}
|
|
|
|
return pt;
|
|
}
|
|
|
|
const cEventInfo *cSchedule::GetEventAround(time_t Time) const
|
|
{
|
|
const cEventInfo *pe = NULL;
|
|
time_t delta = INT_MAX;
|
|
for (cEventInfo *p = Events.First(); p; p = Events.Next(p)) {
|
|
time_t dt = Time - p->GetTime();
|
|
if (dt >= 0 && dt < delta && p->GetTime() + p->GetDuration() >= Time) {
|
|
delta = dt;
|
|
pe = p;
|
|
}
|
|
}
|
|
return pe;
|
|
}
|
|
|
|
bool cSchedule::SetPresentEvent(cEventInfo *pEvent)
|
|
{
|
|
if (pPresent != NULL)
|
|
pPresent->SetPresent(false);
|
|
pPresent = pEvent;
|
|
pPresent->SetPresent(true);
|
|
|
|
return true;
|
|
}
|
|
|
|
/** */
|
|
bool cSchedule::SetFollowingEvent(cEventInfo *pEvent)
|
|
{
|
|
if (pFollowing != NULL)
|
|
pFollowing->SetFollowing(false);
|
|
pFollowing = pEvent;
|
|
pFollowing->SetFollowing(true);
|
|
|
|
return true;
|
|
}
|
|
|
|
/** */
|
|
void cSchedule::Cleanup()
|
|
{
|
|
Cleanup(time(NULL));
|
|
}
|
|
|
|
/** */
|
|
void cSchedule::Cleanup(time_t tTime)
|
|
{
|
|
cEventInfo *pEvent;
|
|
for (int a = 0; true ; a++)
|
|
{
|
|
pEvent = Events.Get(a);
|
|
if (pEvent == NULL)
|
|
break;
|
|
if (pEvent->GetTime() + pEvent->GetDuration() + 3600 < tTime) // adding one hour for safety
|
|
{
|
|
Events.Del(pEvent);
|
|
a--;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** */
|
|
void cSchedule::Dump(FILE *f, const char *Prefix) const
|
|
{
|
|
cChannel *channel = Channels.GetByChannelID(uChannelID);
|
|
if (channel)
|
|
{
|
|
fprintf(f, "%sC %s %s\n", Prefix, channel->GetChannelIDStr(), channel->Name());
|
|
for (cEventInfo *p = Events.First(); p; p = Events.Next(p))
|
|
p->Dump(f, Prefix);
|
|
fprintf(f, "%sc\n", Prefix);
|
|
}
|
|
}
|
|
|
|
bool cSchedule::Read(FILE *f, cSchedules *Schedules)
|
|
{
|
|
if (Schedules) {
|
|
char *s;
|
|
while ((s = readline(f)) != NULL) {
|
|
if (*s == 'C') {
|
|
s = skipspace(s + 1);
|
|
char *p = strchr(s, ' ');
|
|
if (p)
|
|
*p = 0; // strips optional channel name
|
|
if (*s) {
|
|
uint64 uChannelID = cChannel::StringToChannelID(s);
|
|
if (uChannelID) {
|
|
cSchedule *p = (cSchedule *)Schedules->AddChannelID(uChannelID);
|
|
if (p) {
|
|
if (!cEventInfo::Read(f, p))
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
esyslog("ERROR: illegal channel ID: %s", s);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
esyslog("ERROR: unexpected tag while reading EPG data: %s", s);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// --- cSchedules ------------------------------------------------------------
|
|
|
|
cSchedules::cSchedules()
|
|
{
|
|
pCurrentSchedule = NULL;
|
|
uCurrentChannelID = 0;
|
|
}
|
|
|
|
cSchedules::~cSchedules()
|
|
{
|
|
}
|
|
/** */
|
|
const cSchedule *cSchedules::AddChannelID(uint64 channelid)
|
|
{
|
|
const cSchedule *p = GetSchedule(channelid);
|
|
if (!p) {
|
|
Add(new cSchedule(channelid));
|
|
p = GetSchedule(channelid);
|
|
}
|
|
return p;
|
|
}
|
|
/** */
|
|
const cSchedule *cSchedules::SetCurrentChannelID(uint64 channelid)
|
|
{
|
|
pCurrentSchedule = AddChannelID(channelid);
|
|
if (pCurrentSchedule)
|
|
uCurrentChannelID = channelid;
|
|
return pCurrentSchedule;
|
|
}
|
|
/** */
|
|
const cSchedule * cSchedules::GetSchedule() const
|
|
{
|
|
return pCurrentSchedule;
|
|
}
|
|
/** */
|
|
const cSchedule * cSchedules::GetSchedule(uint64 channelid) const
|
|
{
|
|
cSchedule *p;
|
|
|
|
p = First();
|
|
while (p != NULL)
|
|
{
|
|
if (p->GetChannelID() == channelid)
|
|
return p;
|
|
p = Next(p);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/** */
|
|
void cSchedules::Cleanup()
|
|
{
|
|
cSchedule *p;
|
|
|
|
p = First();
|
|
while (p != NULL)
|
|
{
|
|
p->Cleanup(time(NULL));
|
|
p = Next(p);
|
|
}
|
|
}
|
|
|
|
/** */
|
|
void cSchedules::Dump(FILE *f, const char *Prefix) const
|
|
{
|
|
for (cSchedule *p = First(); p; p = Next(p))
|
|
p->Dump(f, Prefix);
|
|
}
|
|
|
|
/** */
|
|
bool cSchedules::Read(FILE *f)
|
|
{
|
|
cMutexLock MutexLock;
|
|
return cSchedule::Read(f, (cSchedules *)cSIProcessor::Schedules(MutexLock));
|
|
}
|
|
|
|
// --- cEIT ------------------------------------------------------------------
|
|
|
|
class cEIT {
|
|
private:
|
|
cSchedules *schedules;
|
|
public:
|
|
cEIT(unsigned char *buf, int length, cSchedules *Schedules);
|
|
~cEIT();
|
|
/** */
|
|
int ProcessEIT(unsigned char *buffer, int CurrentSource);
|
|
|
|
protected: // Protected methods
|
|
/** returns true if this EIT covers a
|
|
present/following information, false if it's
|
|
schedule information */
|
|
bool IsPresentFollowing();
|
|
protected: // Protected attributes
|
|
/** Table ID of this EIT struct */
|
|
u_char tid;
|
|
};
|
|
|
|
cEIT::cEIT(unsigned char * buf, int length, cSchedules *Schedules)
|
|
{
|
|
tid = buf[0];
|
|
schedules = Schedules;
|
|
}
|
|
|
|
cEIT::~cEIT()
|
|
{
|
|
}
|
|
|
|
/** */
|
|
int cEIT::ProcessEIT(unsigned char *buffer, int CurrentSource)
|
|
{
|
|
cEventInfo *pEvent, *rEvent = NULL;
|
|
cSchedule *pSchedule, *rSchedule = NULL;
|
|
struct LIST *VdrProgramInfos;
|
|
struct VdrProgramInfo *VdrProgramInfo;
|
|
|
|
if (!buffer)
|
|
return -1;
|
|
|
|
VdrProgramInfos = createVdrProgramInfos(buffer);
|
|
|
|
if (VdrProgramInfos) {
|
|
for (VdrProgramInfo = (struct VdrProgramInfo *) VdrProgramInfos->Head; VdrProgramInfo; VdrProgramInfo = (struct VdrProgramInfo *) xSucc (VdrProgramInfo)) {
|
|
//XXX TODO use complete channel ID
|
|
cChannel *channel = Channels.GetByServiceID(CurrentSource, VdrProgramInfo->ServiceID);
|
|
uint64 channelID = channel ? channel->GetChannelID() : (uint64(CurrentSource) << 48) | VdrProgramInfo->ServiceID;
|
|
//XXX
|
|
pSchedule = (cSchedule *)schedules->GetSchedule(channelID);
|
|
if (!pSchedule) {
|
|
schedules->Add(new cSchedule(channelID));
|
|
pSchedule = (cSchedule *)schedules->GetSchedule(channelID);
|
|
if (!pSchedule)
|
|
break;
|
|
}
|
|
if (VdrProgramInfo->ReferenceServiceID) {
|
|
rSchedule = (cSchedule *)schedules->GetSchedule((uint64(CurrentSource) << 48) | VdrProgramInfo->ReferenceServiceID);
|
|
if (!rSchedule)
|
|
break;
|
|
rEvent = (cEventInfo *)rSchedule->GetEvent((unsigned short)VdrProgramInfo->ReferenceEventID);
|
|
if (!rEvent)
|
|
break;
|
|
}
|
|
pEvent = (cEventInfo *)pSchedule->GetEvent((unsigned short)VdrProgramInfo->EventID, VdrProgramInfo->StartTime);
|
|
if (!pEvent) {
|
|
// If we don't have that event ID yet, we create a new one.
|
|
// Otherwise we copy the information into the existing event anyway, because the data might have changed.
|
|
pEvent = pSchedule->AddEvent(new cEventInfo(channelID, VdrProgramInfo->EventID));
|
|
if (!pEvent)
|
|
break;
|
|
pEvent->SetTableID(tid);
|
|
}
|
|
else {
|
|
// We have found an existing event, either through its event ID or its start time.
|
|
// If the existing event has a zero table ID it was defined externally and shall
|
|
// not be overwritten.
|
|
if (pEvent->GetTableID() == 0x00)
|
|
continue;
|
|
// If the new event comes from a table that belongs to an "other TS" and the existing
|
|
// one comes from an "actual TS" table, lets skip it.
|
|
if ((tid == 0x4F || tid == 0x60 || tid == 0x61) && (pEvent->GetTableID() == 0x4E || pEvent->GetTableID() == 0x50 || pEvent->GetTableID() == 0x51))
|
|
continue;
|
|
}
|
|
if (rEvent) {
|
|
pEvent->SetTitle(rEvent->GetTitle());
|
|
pEvent->SetSubtitle(rEvent->GetSubtitle());
|
|
pEvent->SetExtendedDescription(rEvent->GetExtendedDescription());
|
|
}
|
|
else {
|
|
pEvent->SetTableID(tid);
|
|
pEvent->SetTitle(VdrProgramInfo->ShortName);
|
|
pEvent->SetSubtitle(VdrProgramInfo->ShortText);
|
|
pEvent->SetExtendedDescription(VdrProgramInfo->ExtendedName);
|
|
//XXX kls 2001-09-22:
|
|
//XXX apparently this never occurred, so I have simpified ExtendedDescription handling
|
|
//XXX pEvent->AddExtendedDescription(VdrProgramInfo->ExtendedText);
|
|
}
|
|
pEvent->SetTime(VdrProgramInfo->StartTime);
|
|
pEvent->SetDuration(VdrProgramInfo->Duration);
|
|
pEvent->FixEpgBugs();
|
|
if (IsPresentFollowing()) {
|
|
if ((GetRunningStatus(VdrProgramInfo->Status) == RUNNING_STATUS_PAUSING) || (GetRunningStatus(VdrProgramInfo->Status) == RUNNING_STATUS_RUNNING))
|
|
pSchedule->SetPresentEvent(pEvent);
|
|
else if (GetRunningStatus(VdrProgramInfo->Status) == RUNNING_STATUS_AWAITING)
|
|
pSchedule->SetFollowingEvent(pEvent);
|
|
}
|
|
}
|
|
}
|
|
|
|
xMemFreeAll(NULL);
|
|
return 0;
|
|
}
|
|
|
|
/** returns true if this EIT covers a
|
|
present/following information, false if it's
|
|
schedule information */
|
|
bool cEIT::IsPresentFollowing()
|
|
{
|
|
if (tid == 0x4e || tid == 0x4f)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// --- cSIProcessor ----------------------------------------------------------
|
|
|
|
#define MAX_FILTERS 20
|
|
#define EPGDATAFILENAME "epg.data"
|
|
|
|
int cSIProcessor::numSIProcessors = 0;
|
|
cSchedules *cSIProcessor::schedules = NULL;
|
|
cMutex cSIProcessor::schedulesMutex;
|
|
const char *cSIProcessor::epgDataFileName = EPGDATAFILENAME;
|
|
time_t cSIProcessor::lastDump = time(NULL);
|
|
|
|
/** */
|
|
cSIProcessor::cSIProcessor(const char *FileName)
|
|
{
|
|
fileName = strdup(FileName);
|
|
masterSIProcessor = numSIProcessors == 0; // the first one becomes the 'master'
|
|
currentSource = 0;
|
|
currentTransponder = 0;
|
|
filters = NULL;
|
|
if (!numSIProcessors++) // the first one creates it
|
|
schedules = new cSchedules;
|
|
filters = (SIP_FILTER *)calloc(MAX_FILTERS, sizeof(SIP_FILTER));
|
|
SetStatus(true);
|
|
Start();
|
|
}
|
|
|
|
cSIProcessor::~cSIProcessor()
|
|
{
|
|
if (masterSIProcessor)
|
|
ReportEpgBugFixStats();
|
|
active = false;
|
|
Cancel(3);
|
|
ShutDownFilters();
|
|
free(filters);
|
|
if (!--numSIProcessors) // the last one deletes it
|
|
delete schedules;
|
|
free(fileName);
|
|
}
|
|
|
|
const cSchedules *cSIProcessor::Schedules(cMutexLock &MutexLock)
|
|
{
|
|
if (MutexLock.Lock(&schedulesMutex))
|
|
return schedules;
|
|
return NULL;
|
|
}
|
|
|
|
bool cSIProcessor::Read(FILE *f)
|
|
{
|
|
bool OwnFile = f == NULL;
|
|
if (OwnFile) {
|
|
const char *FileName = GetEpgDataFileName();
|
|
if (access(FileName, R_OK) == 0) {
|
|
dsyslog("reading EPG data from %s", FileName);
|
|
if ((f = fopen(FileName, "r")) == NULL) {
|
|
LOG_ERROR;
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
bool result = cSchedules::Read(f);
|
|
if (OwnFile)
|
|
fclose(f);
|
|
return result;
|
|
}
|
|
|
|
void cSIProcessor::Clear(void)
|
|
{
|
|
cMutexLock MutexLock(&schedulesMutex);
|
|
delete schedules;
|
|
schedules = new cSchedules;
|
|
}
|
|
|
|
void cSIProcessor::SetEpgDataFileName(const char *FileName)
|
|
{
|
|
epgDataFileName = NULL;
|
|
if (FileName)
|
|
epgDataFileName = strdup(DirectoryOk(FileName) ? AddDirectory(FileName, EPGDATAFILENAME) : FileName);
|
|
}
|
|
|
|
const char *cSIProcessor::GetEpgDataFileName(void)
|
|
{
|
|
if (epgDataFileName)
|
|
return *epgDataFileName == '/' ? epgDataFileName : AddDirectory(VideoDirectory, epgDataFileName);
|
|
return NULL;
|
|
}
|
|
|
|
void cSIProcessor::SetStatus(bool On)
|
|
{
|
|
ShutDownFilters();
|
|
if (On)
|
|
{
|
|
AddFilter(0x14, 0x70); // TDT
|
|
AddFilter(0x14, 0x73); // TOT
|
|
AddFilter(0x12, 0x4e); // event info, actual TS, present/following
|
|
AddFilter(0x12, 0x4f); // event info, other TS, present/following
|
|
AddFilter(0x12, 0x50); // event info, actual TS, schedule
|
|
AddFilter(0x12, 0x60); // event info, other TS, schedule
|
|
AddFilter(0x12, 0x51); // event info, actual TS, schedule for another 4 days
|
|
AddFilter(0x12, 0x61); // event info, other TS, schedule for another 4 days
|
|
}
|
|
}
|
|
|
|
/** use the vbi device to parse all relevant SI
|
|
information and let the classes corresponding
|
|
to the tables write their information to the disk */
|
|
void cSIProcessor::Action()
|
|
{
|
|
dsyslog("EIT processing thread started (pid=%d)%s", getpid(), masterSIProcessor ? " - master" : "");
|
|
|
|
time_t lastCleanup = time(NULL);
|
|
|
|
active = true;
|
|
|
|
while(active)
|
|
{
|
|
if (masterSIProcessor)
|
|
{
|
|
time_t now = time(NULL);
|
|
struct tm tm_r;
|
|
struct tm *ptm = localtime_r(&now, &tm_r);
|
|
if (now - lastCleanup > 3600 && ptm->tm_hour == 5)
|
|
{
|
|
cMutexLock MutexLock(&schedulesMutex);
|
|
isyslog("cleaning up schedules data");
|
|
schedules->Cleanup();
|
|
lastCleanup = now;
|
|
ReportEpgBugFixStats(true);
|
|
}
|
|
if (epgDataFileName && now - lastDump > 600)
|
|
{
|
|
cMutexLock MutexLock(&schedulesMutex);
|
|
cSafeFile f(GetEpgDataFileName());
|
|
if (f.Open()) {
|
|
schedules->Dump(f);
|
|
f.Close();
|
|
}
|
|
else
|
|
LOG_ERROR;
|
|
lastDump = now;
|
|
}
|
|
}
|
|
|
|
// set up pfd structures for all active filter
|
|
pollfd pfd[MAX_FILTERS];
|
|
int NumUsedFilters = 0;
|
|
for (int a = 0; a < MAX_FILTERS ; a++)
|
|
{
|
|
if (filters[a].inuse)
|
|
{
|
|
pfd[NumUsedFilters].fd = filters[a].handle;
|
|
pfd[NumUsedFilters].events = POLLIN;
|
|
NumUsedFilters++;
|
|
}
|
|
}
|
|
|
|
// wait until data becomes ready from the bitfilter
|
|
if (poll(pfd, NumUsedFilters, 1000) != 0)
|
|
{
|
|
for (int a = 0; a < NumUsedFilters ; a++)
|
|
{
|
|
if (pfd[a].revents & POLLIN)
|
|
{
|
|
/* read section */
|
|
unsigned char buf[4096+1]; // max. allowed size for any EIT section (+1 for safety ;-)
|
|
if (safe_read(filters[a].handle, buf, 3) == 3)
|
|
{
|
|
int seclen = ((buf[1] & 0x0F) << 8) | (buf[2] & 0xFF);
|
|
int pid = filters[a].pid;
|
|
int n = safe_read(filters[a].handle, buf + 3, seclen);
|
|
if (n == seclen)
|
|
{
|
|
seclen += 3;
|
|
//dsyslog("Received pid 0x%02x with table ID 0x%02x and length of %04d\n", pid, buf[0], seclen);
|
|
switch (pid)
|
|
{
|
|
case 0x14:
|
|
if (buf[0] == 0x70)
|
|
{
|
|
if (Setup.SetSystemTime && Setup.TimeTransponder && ISTRANSPONDER(currentTransponder, Setup.TimeTransponder))
|
|
{
|
|
cTDT ctdt((tdt_t *)buf);
|
|
ctdt.SetSystemTime();
|
|
}
|
|
}
|
|
/*XXX this comes pretty often:
|
|
else
|
|
dsyslog("Time packet was not 0x70 but 0x%02x\n", (int)buf[0]);
|
|
XXX*/
|
|
break;
|
|
|
|
case 0x12:
|
|
if (buf[0] != 0x72)
|
|
{
|
|
cMutexLock MutexLock(&schedulesMutex);
|
|
cEIT ceit(buf, seclen, schedules);
|
|
ceit.ProcessEIT(buf, currentSource);
|
|
}
|
|
else
|
|
dsyslog("Received stuffing section in EIT\n");
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
/*XXX this just fills up the log file - shouldn't we rather try to re-sync?
|
|
else
|
|
dsyslog("read incomplete section - seclen = %d, n = %d", seclen, n);
|
|
XXX*/
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
dsyslog("EIT processing thread ended (pid=%d)%s", getpid(), masterSIProcessor ? " - master" : "");
|
|
}
|
|
|
|
/** Add a filter with packet identifier pid and
|
|
table identifer tid */
|
|
bool cSIProcessor::AddFilter(u_char pid, u_char tid)
|
|
{
|
|
dmx_sct_filter_params sctFilterParams;
|
|
memset(&sctFilterParams, 0, sizeof(sctFilterParams));
|
|
sctFilterParams.pid = pid;
|
|
sctFilterParams.timeout = 0;
|
|
sctFilterParams.flags = DMX_IMMEDIATE_START;
|
|
sctFilterParams.filter.filter[0] = tid;
|
|
sctFilterParams.filter.mask[0] = 0xFF;
|
|
|
|
for (int a = 0; a < MAX_FILTERS; a++)
|
|
{
|
|
if (!filters[a].inuse)
|
|
{
|
|
filters[a].pid = pid;
|
|
filters[a].tid = tid;
|
|
if ((filters[a].handle = open(fileName, O_RDWR | O_NONBLOCK)) >= 0)
|
|
{
|
|
if (ioctl(filters[a].handle, DMX_SET_FILTER, &sctFilterParams) >= 0)
|
|
filters[a].inuse = true;
|
|
else
|
|
{
|
|
esyslog("ERROR: can't set filter");
|
|
close(filters[a].handle);
|
|
return false;
|
|
}
|
|
// dsyslog("Registered filter handle %04x, pid = %02d, tid = %02d", filters[a].handle, filters[a].pid, filters[a].tid);
|
|
}
|
|
else
|
|
{
|
|
esyslog("ERROR: can't open filter handle");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
esyslog("ERROR: too many filters");
|
|
|
|
return false;
|
|
}
|
|
|
|
/** */
|
|
bool cSIProcessor::ShutDownFilters(void)
|
|
{
|
|
for (int a = 0; a < MAX_FILTERS; a++)
|
|
{
|
|
if (filters[a].inuse)
|
|
{
|
|
close(filters[a].handle);
|
|
// dsyslog("Deregistered filter handle %04x, pid = %02d, tid = %02d", filters[a].handle, filters[a].pid, filters[a].tid);
|
|
filters[a].inuse = false;
|
|
}
|
|
}
|
|
|
|
return true; // there's no real 'boolean' to return here...
|
|
}
|
|
|
|
/** */
|
|
void cSIProcessor::SetCurrentTransponder(int CurrentSource, int CurrentTransponder)
|
|
{
|
|
currentSource = CurrentSource;
|
|
currentTransponder = CurrentTransponder;
|
|
}
|
|
|
|
/** */
|
|
bool cSIProcessor::SetCurrentChannelID(uint64 channelid)
|
|
{
|
|
cMutexLock MutexLock(&schedulesMutex);
|
|
return schedules ? schedules->SetCurrentChannelID(channelid) : false;
|
|
}
|
|
|
|
void cSIProcessor::TriggerDump(void)
|
|
{
|
|
cMutexLock MutexLock(&schedulesMutex);
|
|
lastDump = 0;
|
|
}
|