mirror of
https://github.com/VDR4Arch/vdr.git
synced 2023-10-10 13:36:52 +02:00
1455 lines
45 KiB
C
1455 lines
45 KiB
C
/*
|
|
* timers.c: Timer handling
|
|
*
|
|
* See the main source file 'vdr.c' for copyright information and
|
|
* how to reach the author.
|
|
*
|
|
* $Id: timers.c 5.16 2021/04/20 13:22:37 kls Exp $
|
|
*/
|
|
|
|
#include "timers.h"
|
|
#include <ctype.h>
|
|
#include "device.h"
|
|
#include "i18n.h"
|
|
#include "libsi/si.h"
|
|
#include "recording.h"
|
|
#include "remote.h"
|
|
#include "status.h"
|
|
#include "svdrp.h"
|
|
|
|
// IMPORTANT NOTE: in the 'sscanf()' calls there is a blank after the '%d'
|
|
// format characters in order to allow any number of blanks after a numeric
|
|
// value!
|
|
|
|
// --- cTimer ----------------------------------------------------------------
|
|
|
|
cTimer::cTimer(bool Instant, bool Pause, const cChannel *Channel)
|
|
{
|
|
id = 0;
|
|
startTime = stopTime = 0;
|
|
scheduleStateSet = scheduleStateSpawn = scheduleStateAdjust = -1;
|
|
deferred = 0;
|
|
pending = inVpsMargin = false;
|
|
flags = tfNone;
|
|
*pattern = 0;
|
|
*file = 0;
|
|
aux = NULL;
|
|
remote = NULL;
|
|
event = NULL;
|
|
if (Instant)
|
|
SetFlags(tfActive | tfInstant);
|
|
LOCK_CHANNELS_READ;
|
|
channel = Channel ? Channel : Channels->GetByNumber(cDevice::CurrentChannel());
|
|
time_t t = time(NULL);
|
|
struct tm tm_r;
|
|
struct tm *now = localtime_r(&t, &tm_r);
|
|
day = SetTime(t, 0);
|
|
weekdays = 0;
|
|
start = now->tm_hour * 100 + now->tm_min;
|
|
stop = 0;
|
|
if (!Setup.InstantRecordTime && channel && (Instant || Pause)) {
|
|
LOCK_SCHEDULES_READ;
|
|
if (const cSchedule *Schedule = Schedules->GetSchedule(channel)) {
|
|
if (const cEvent *Event = Schedule->GetPresentEvent()) {
|
|
time_t tstart = Event->StartTime();
|
|
time_t tstop = Event->EndTime();
|
|
if (Event->Vps() && Setup.UseVps) {
|
|
SetFlags(tfVps);
|
|
tstart = Event->Vps();
|
|
}
|
|
else {
|
|
int MarginStart = 0;
|
|
int MarginStop = 0;
|
|
CalcMargins(MarginStart, MarginStop, Event);
|
|
tstart -= MarginStart;
|
|
tstop += MarginStop;
|
|
}
|
|
day = SetTime(tstart, 0);
|
|
struct tm *time = localtime_r(&tstart, &tm_r);
|
|
start = time->tm_hour * 100 + time->tm_min;
|
|
time = localtime_r(&tstop, &tm_r);
|
|
stop = time->tm_hour * 100 + time->tm_min;
|
|
SetEvent(Event);
|
|
}
|
|
}
|
|
}
|
|
if (!stop) {
|
|
stop = now->tm_hour * 60 + now->tm_min + (Setup.InstantRecordTime ? Setup.InstantRecordTime : DEFINSTRECTIME);
|
|
stop = (stop / 60) * 100 + (stop % 60);
|
|
}
|
|
if (stop >= 2400)
|
|
stop -= 2400;
|
|
priority = Pause ? Setup.PausePriority : Setup.DefaultPriority;
|
|
lifetime = Pause ? Setup.PauseLifetime : Setup.DefaultLifetime;
|
|
if (Instant && channel)
|
|
snprintf(file, sizeof(file), "%s%s", Setup.MarkInstantRecord ? "@" : "", *Setup.NameInstantRecord ? Setup.NameInstantRecord : channel->Name());
|
|
}
|
|
|
|
static bool MatchPattern(const char *Pattern, const char *Title, cString *Before = NULL, cString *Match = NULL, cString *After = NULL)
|
|
{
|
|
if (Title) {
|
|
bool AvoidDuplicates = startswith(Pattern, TIMERPATTERN_AVOID);
|
|
if (AvoidDuplicates)
|
|
Pattern++;
|
|
if (strcmp(Pattern, "*") == 0) {
|
|
if (Before)
|
|
*Before = "";
|
|
if (Match)
|
|
*Match = Title;
|
|
if (After)
|
|
*After = "";
|
|
return true;
|
|
}
|
|
bool AnchorBegin = startswith(Pattern, TIMERPATTERN_BEGIN);
|
|
if (AnchorBegin)
|
|
Pattern++;
|
|
bool AnchorEnd = endswith(Pattern, TIMERPATTERN_END);
|
|
cNullTerminate nt;
|
|
if (AnchorEnd)
|
|
nt.Set(const_cast<char *>(Pattern + strlen(Pattern) - 1));
|
|
if (AnchorBegin && AnchorEnd) {
|
|
if (strcmp(Title, Pattern) == 0) {
|
|
if (Before)
|
|
*Before = "";
|
|
if (Match)
|
|
*Match = Title;
|
|
if (After)
|
|
*After = "";
|
|
return true;
|
|
}
|
|
}
|
|
else if (AnchorBegin) {
|
|
if (strstr(Title, Pattern) == Title) {
|
|
if (Before)
|
|
*Before = "";
|
|
if (Match)
|
|
*Match = Pattern;
|
|
if (After)
|
|
*After = cString(Title + strlen(Pattern));
|
|
return true;
|
|
}
|
|
}
|
|
else if (AnchorEnd) {
|
|
if (endswith(Title, Pattern)) {
|
|
if (Before)
|
|
*Before = cString(Title, Title + strlen(Title) - strlen(Pattern));
|
|
if (Match)
|
|
*Match = Pattern;
|
|
if (After)
|
|
*After = "";
|
|
return true;
|
|
}
|
|
}
|
|
else if (const char *p = strstr(Title, Pattern)) {
|
|
if (Before)
|
|
*Before = cString(Title, p);
|
|
if (Match)
|
|
*Match = Pattern;
|
|
if (After)
|
|
*After = cString(p + strlen(Pattern));
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static cString MakePatternFileName(const char *Pattern, const char *Title, const char *Episode, const char *File)
|
|
{
|
|
if (!Pattern || !Title || !File)
|
|
return NULL;
|
|
cString Before = "";
|
|
cString Match = "";
|
|
cString After = "";
|
|
if (MatchPattern(Pattern, Title, &Before, &Match, &After)) {
|
|
char *Result = strdup(File);
|
|
Result = strreplace(Result, TIMERMACRO_TITLE, Title);
|
|
if (!isempty(Episode)) // the event might not yet have a "short text", so we leave this to the actual recording
|
|
Result = strreplace(Result, TIMERMACRO_EPISODE, Episode);
|
|
Result = strreplace(Result, TIMERMACRO_BEFORE, Before);
|
|
Result = strreplace(Result, TIMERMACRO_MATCH, Match);
|
|
Result = strreplace(Result, TIMERMACRO_AFTER, After);
|
|
return cString(Result, true);;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
cTimer::cTimer(const cEvent *Event, const char *FileName, const cTimer *PatternTimer)
|
|
{
|
|
id = 0;
|
|
startTime = stopTime = 0;
|
|
scheduleStateSet = scheduleStateSpawn = scheduleStateAdjust = -1;
|
|
deferred = 0;
|
|
pending = inVpsMargin = false;
|
|
flags = tfActive;
|
|
*pattern = 0;
|
|
*file = 0;
|
|
aux = NULL;
|
|
remote = NULL;
|
|
event = NULL;
|
|
if (!PatternTimer || PatternTimer->HasFlags(tfVps)) {
|
|
if (Event->Vps() && (PatternTimer || Setup.UseVps))
|
|
SetFlags(tfVps);
|
|
}
|
|
LOCK_CHANNELS_READ;
|
|
channel = Channels->GetByChannelID(Event->ChannelID(), true);
|
|
time_t tstart = (flags & tfVps) ? Event->Vps() : Event->StartTime();
|
|
time_t tstop = tstart + Event->Duration();
|
|
if (!(HasFlags(tfVps))) {
|
|
int MarginStart = 0;
|
|
int MarginStop = 0;
|
|
CalcMargins(MarginStart, MarginStop, Event);
|
|
tstart -= MarginStart;
|
|
tstop += MarginStop;
|
|
}
|
|
struct tm tm_r;
|
|
struct tm *time = localtime_r(&tstart, &tm_r);
|
|
day = SetTime(tstart, 0);
|
|
weekdays = 0;
|
|
start = time->tm_hour * 100 + time->tm_min;
|
|
time = localtime_r(&tstop, &tm_r);
|
|
stop = time->tm_hour * 100 + time->tm_min;
|
|
if (stop >= 2400)
|
|
stop -= 2400;
|
|
priority = PatternTimer ? PatternTimer->Priority() : Setup.DefaultPriority;
|
|
lifetime = PatternTimer ? PatternTimer->Lifetime() : Setup.DefaultLifetime;
|
|
if (!FileName)
|
|
FileName = Event->Title();
|
|
if (!isempty(FileName))
|
|
Utf8Strn0Cpy(file, FileName, sizeof(file));
|
|
SetEvent(Event);
|
|
}
|
|
|
|
cTimer::cTimer(const cTimer &Timer)
|
|
{
|
|
channel = NULL;
|
|
aux = NULL;
|
|
remote = NULL;
|
|
event = NULL;
|
|
flags = tfNone;
|
|
*this = Timer;
|
|
}
|
|
|
|
cTimer::~cTimer()
|
|
{
|
|
if (event)
|
|
event->DecNumTimers();
|
|
free(aux);
|
|
free(remote);
|
|
}
|
|
|
|
cTimer& cTimer::operator= (const cTimer &Timer)
|
|
{
|
|
if (&Timer != this) {
|
|
id = Timer.id;
|
|
startTime = Timer.startTime;
|
|
stopTime = Timer.stopTime;
|
|
scheduleStateSet = scheduleStateSpawn = scheduleStateAdjust = -1;
|
|
deferred = 0;
|
|
pending = Timer.pending;
|
|
inVpsMargin = Timer.inVpsMargin;
|
|
flags = Timer.flags;
|
|
channel = Timer.channel;
|
|
day = Timer.day;
|
|
weekdays = Timer.weekdays;
|
|
start = Timer.start;
|
|
stop = Timer.stop;
|
|
priority = Timer.priority;
|
|
lifetime = Timer.lifetime;
|
|
strncpy(pattern, Timer.pattern, sizeof(pattern));
|
|
strncpy(file, Timer.file, sizeof(file));
|
|
free(aux);
|
|
aux = Timer.aux ? strdup(Timer.aux) : NULL;
|
|
free(remote);
|
|
remote = Timer.remote ? strdup(Timer.remote) : NULL;
|
|
if (event)
|
|
event->DecNumTimers();
|
|
event = Timer.event;
|
|
if (event)
|
|
event->IncNumTimers();
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
void cTimer::CalcMargins(int &MarginStart, int &MarginStop, const cEvent *Event)
|
|
{
|
|
MarginStart = Setup.MarginStart * 60;
|
|
MarginStop = Setup.MarginStop * 60;
|
|
// To make sure the timer gets assigned to the correct event, we must
|
|
// make sure that this is the only event that overlaps 100%:
|
|
if (const cEvent *e = dynamic_cast<const cEvent *>(Event->Prev()))
|
|
MarginStart = max(0, min(MarginStart, e->Duration() - 60));
|
|
if (const cEvent *e = dynamic_cast<const cEvent *>(Event->Next()))
|
|
MarginStop = max(0, min(MarginStop, e->Duration() - 60));
|
|
}
|
|
|
|
int cTimer::Compare(const cListObject &ListObject) const
|
|
{
|
|
const cTimer *ti = (const cTimer *)&ListObject;
|
|
time_t t1 = StartTime();
|
|
time_t t2 = ti->StartTime();
|
|
int r = t1 - t2;
|
|
if (r == 0)
|
|
r = ti->priority - priority;
|
|
if (IsPatternTimer() ^ ti->IsPatternTimer()) {
|
|
if (IsPatternTimer())
|
|
r = 1;
|
|
else
|
|
r = -1;
|
|
}
|
|
else if (IsPatternTimer() && ti->IsPatternTimer())
|
|
r = strcoll(Pattern(), ti->Pattern());
|
|
return r;
|
|
}
|
|
|
|
cString cTimer::PatternAndFile(void) const
|
|
{
|
|
if (IsPatternTimer())
|
|
return cString::sprintf("{%s}%s", pattern, file);
|
|
return file;
|
|
}
|
|
|
|
cString cTimer::ToText(bool UseChannelID) const
|
|
{
|
|
strreplace(pattern, ':', '|');
|
|
strreplace(file, ':', '|');
|
|
cString buffer = cString::sprintf("%u:%s:%s:%04d:%04d:%d:%d:%s:%s", flags, UseChannelID ? *Channel()->GetChannelID().ToString() : *itoa(Channel()->Number()), *PrintDay(day, weekdays, true), start, stop, priority, lifetime, *PatternAndFile(), aux ? aux : "");
|
|
strreplace(pattern, '|', ':');
|
|
strreplace(file, '|', ':');
|
|
return buffer;
|
|
}
|
|
|
|
cString cTimer::ToDescr(void) const
|
|
{
|
|
return cString::sprintf("%d%s%s (%d %04d-%04d %s'%s')", Id(), remote ? "@" : "", remote ? remote : "", Channel()->Number(), start, stop, HasFlags(tfVps) ? "VPS " : "", *PatternAndFile());
|
|
}
|
|
|
|
int cTimer::TimeToInt(int t)
|
|
{
|
|
return (t / 100 * 60 + t % 100) * 60;
|
|
}
|
|
|
|
bool cTimer::ParseDay(const char *s, time_t &Day, int &WeekDays)
|
|
{
|
|
// possible formats are:
|
|
// 19
|
|
// 2005-03-19
|
|
// MTWTFSS
|
|
// MTWTFSS@19
|
|
// MTWTFSS@2005-03-19
|
|
|
|
Day = 0;
|
|
WeekDays = 0;
|
|
s = skipspace(s);
|
|
if (!*s)
|
|
return false;
|
|
const char *a = strchr(s, '@');
|
|
const char *d = a ? a + 1 : isdigit(*s) ? s : NULL;
|
|
if (d) {
|
|
if (strlen(d) == 10) {
|
|
struct tm tm_r;
|
|
if (3 == sscanf(d, "%d-%d-%d", &tm_r.tm_year, &tm_r.tm_mon, &tm_r.tm_mday)) {
|
|
tm_r.tm_year -= 1900;
|
|
tm_r.tm_mon--;
|
|
tm_r.tm_hour = tm_r.tm_min = tm_r.tm_sec = 0;
|
|
tm_r.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
|
|
Day = mktime(&tm_r);
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
else {
|
|
// handle "day of month" for compatibility with older versions:
|
|
char *tail = NULL;
|
|
int day = strtol(d, &tail, 10);
|
|
if (tail && *tail || day < 1 || day > 31)
|
|
return false;
|
|
time_t t = time(NULL);
|
|
int DaysToCheck = 61; // 61 to handle months with 31/30/31
|
|
for (int i = -1; i <= DaysToCheck; i++) {
|
|
time_t t0 = IncDay(t, i);
|
|
if (GetMDay(t0) == day) {
|
|
Day = SetTime(t0, 0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (a || !isdigit(*s)) {
|
|
if ((a && a - s == 7) || strlen(s) == 7) {
|
|
for (const char *p = s + 6; p >= s; p--) {
|
|
WeekDays <<= 1;
|
|
WeekDays |= (*p != '-');
|
|
}
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
cString cTimer::PrintDay(time_t Day, int WeekDays, bool SingleByteChars)
|
|
{
|
|
#define DAYBUFFERSIZE 64
|
|
char buffer[DAYBUFFERSIZE];
|
|
char *b = buffer;
|
|
if (WeekDays) {
|
|
// TRANSLATORS: the first character of each weekday, beginning with monday
|
|
const char *w = trNOOP("MTWTFSS");
|
|
if (!SingleByteChars)
|
|
w = tr(w);
|
|
while (*w) {
|
|
int sl = Utf8CharLen(w);
|
|
if (WeekDays & 1) {
|
|
for (int i = 0; i < sl; i++)
|
|
b[i] = w[i];
|
|
b += sl;
|
|
}
|
|
else
|
|
*b++ = '-';
|
|
WeekDays >>= 1;
|
|
w += sl;
|
|
}
|
|
if (Day)
|
|
*b++ = '@';
|
|
}
|
|
if (Day) {
|
|
struct tm tm_r;
|
|
localtime_r(&Day, &tm_r);
|
|
b += strftime(b, DAYBUFFERSIZE - (b - buffer), "%Y-%m-%d", &tm_r);
|
|
}
|
|
*b = 0;
|
|
return buffer;
|
|
}
|
|
|
|
cString cTimer::PrintFirstDay(void) const
|
|
{
|
|
if (weekdays) {
|
|
cString s = PrintDay(day, weekdays, true);
|
|
if (strlen(s) == 18)
|
|
return *s + 8;
|
|
}
|
|
return ""; // not NULL, so the caller can always use the result
|
|
}
|
|
|
|
bool cTimer::Parse(const char *s)
|
|
{
|
|
char *channelbuffer = NULL;
|
|
char *daybuffer = NULL;
|
|
char *filebuffer = NULL;
|
|
free(aux);
|
|
aux = NULL;
|
|
//XXX Apparently sscanf() doesn't work correctly if the last %m argument
|
|
//XXX results in an empty string (this first occurred when the EIT gathering
|
|
//XXX was put into a separate thread - don't know why this happens...
|
|
//XXX As a cure we copy the original string and add a blank.
|
|
//XXX If anybody can shed some light on why sscanf() fails here, I'd love
|
|
//XXX to hear about that!
|
|
char *s2 = NULL;
|
|
int l2 = strlen(s);
|
|
while (l2 > 0 && isspace(s[l2 - 1]))
|
|
l2--;
|
|
if (s[l2 - 1] == ':') {
|
|
s2 = MALLOC(char, l2 + 3);
|
|
strcat(strn0cpy(s2, s, l2 + 1), " \n");
|
|
s = s2;
|
|
}
|
|
bool result = false;
|
|
if (8 <= sscanf(s, "%u :%m[^:]:%m[^:]:%d :%d :%d :%d :%m[^:\n]:%m[^\n]", &flags, &channelbuffer, &daybuffer, &start, &stop, &priority, &lifetime, &filebuffer, &aux)) {
|
|
if (aux && !*skipspace(aux)) {
|
|
free(aux);
|
|
aux = NULL;
|
|
}
|
|
//TODO add more plausibility checks
|
|
result = ParseDay(daybuffer, day, weekdays);
|
|
char *fb = filebuffer;
|
|
if (*fb == '{') {
|
|
if (char *p = strchr(fb, '}')) {
|
|
*p = 0;
|
|
Utf8Strn0Cpy(pattern, fb + 1, sizeof(pattern));
|
|
strreplace(pattern, '|', ':');
|
|
fb = p + 1;
|
|
}
|
|
}
|
|
else
|
|
*pattern = 0;
|
|
Utf8Strn0Cpy(file, fb, sizeof(file));
|
|
strreplace(file, '|', ':');
|
|
LOCK_CHANNELS_READ;
|
|
if (isnumber(channelbuffer))
|
|
channel = Channels->GetByNumber(atoi(channelbuffer));
|
|
else
|
|
channel = Channels->GetByChannelID(tChannelID::FromString(channelbuffer), true, true);
|
|
if (!channel) {
|
|
esyslog("ERROR: channel %s not defined", channelbuffer);
|
|
result = false;
|
|
}
|
|
}
|
|
free(channelbuffer);
|
|
free(daybuffer);
|
|
free(filebuffer);
|
|
free(s2);
|
|
return result;
|
|
}
|
|
|
|
bool cTimer::Save(FILE *f)
|
|
{
|
|
if (!Remote())
|
|
return fprintf(f, "%s\n", *ToText(true)) > 0;
|
|
return true;
|
|
}
|
|
|
|
bool cTimer::IsSingleEvent(void) const
|
|
{
|
|
return !weekdays;
|
|
}
|
|
|
|
int cTimer::GetMDay(time_t t)
|
|
{
|
|
struct tm tm_r;
|
|
return localtime_r(&t, &tm_r)->tm_mday;
|
|
}
|
|
|
|
int cTimer::GetWDay(time_t t)
|
|
{
|
|
struct tm tm_r;
|
|
int weekday = localtime_r(&t, &tm_r)->tm_wday;
|
|
return weekday == 0 ? 6 : weekday - 1; // we start with Monday==0!
|
|
}
|
|
|
|
bool cTimer::DayMatches(time_t t) const
|
|
{
|
|
return IsSingleEvent() ? SetTime(t, 0) == day : (weekdays & (1 << GetWDay(t))) != 0;
|
|
}
|
|
|
|
time_t cTimer::IncDay(time_t t, int Days)
|
|
{
|
|
struct tm tm_r;
|
|
tm tm = *localtime_r(&t, &tm_r);
|
|
tm.tm_mday += Days; // now tm_mday may be out of its valid range
|
|
int h = tm.tm_hour; // save original hour to compensate for DST change
|
|
tm.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
|
|
t = mktime(&tm); // normalize all values
|
|
tm.tm_hour = h; // compensate for DST change
|
|
return mktime(&tm); // calculate final result
|
|
}
|
|
|
|
time_t cTimer::SetTime(time_t t, int SecondsFromMidnight)
|
|
{
|
|
struct tm tm_r;
|
|
tm tm = *localtime_r(&t, &tm_r);
|
|
tm.tm_hour = SecondsFromMidnight / 3600;
|
|
tm.tm_min = (SecondsFromMidnight % 3600) / 60;
|
|
tm.tm_sec = SecondsFromMidnight % 60;
|
|
tm.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
|
|
return mktime(&tm);
|
|
}
|
|
|
|
void cTimer::SetPattern(const char *Pattern)
|
|
{
|
|
Utf8Strn0Cpy(pattern, Pattern, sizeof(pattern));
|
|
}
|
|
|
|
void cTimer::SetFile(const char *File)
|
|
{
|
|
if (!isempty(File))
|
|
Utf8Strn0Cpy(file, File, sizeof(file));
|
|
}
|
|
|
|
#define EITPRESENTFOLLOWINGRATE 10 // max. seconds between two occurrences of the "EIT present/following table for the actual multiplex" (2s by the standard, using some more for safety)
|
|
|
|
bool cTimer::Matches(time_t t, bool Directly, int Margin) const
|
|
{
|
|
startTime = stopTime = 0;
|
|
if (t == 0)
|
|
t = time(NULL);
|
|
|
|
int begin = TimeToInt(start); // seconds from midnight
|
|
int end = TimeToInt(stop);
|
|
int length = end - begin;
|
|
|
|
if (IsSingleEvent()) {
|
|
time_t t0 = day;
|
|
startTime = SetTime(t0, begin);
|
|
if (length < 0)
|
|
t0 = IncDay(day, 1);
|
|
stopTime = SetTime(t0, end);
|
|
}
|
|
else {
|
|
time_t d = day ? max(day, t) : t;
|
|
for (int i = -1; i <= 7; i++) {
|
|
time_t t0 = IncDay(d, i);
|
|
if (DayMatches(t0)) {
|
|
time_t a = SetTime(t0, begin);
|
|
if (length < 0)
|
|
t0 = IncDay(d, i + 1);
|
|
time_t b = SetTime(t0, end);
|
|
if ((!day || a >= day) && t < b) {
|
|
startTime = a;
|
|
stopTime = b;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!startTime)
|
|
startTime = IncDay(t, 7); // just to have something that's more than a week in the future
|
|
else if (!Directly && (t > startTime || t > day + SECSINDAY + 3600)) // +3600 in case of DST change
|
|
day = 0;
|
|
}
|
|
|
|
if (IsPatternTimer())
|
|
return false; // we only need to have start/stopTime initialized
|
|
|
|
if (t < deferred)
|
|
return false;
|
|
deferred = 0;
|
|
|
|
if (HasFlags(tfActive)) {
|
|
if (event) {
|
|
if (HasFlags(tfVps)) {
|
|
if (event->Vps()) {
|
|
if (Margin || !Directly) {
|
|
startTime = event->StartTime();
|
|
stopTime = event->EndTime();
|
|
if (!Margin) { // this is an actual check
|
|
if (event->Schedule()->PresentSeenWithin(EITPRESENTFOLLOWINGRATE)) // VPS control can only work with up-to-date events...
|
|
return event->IsRunning(true);
|
|
// ...otherwise we fall back to normal timer handling below (note: Margin == 0!)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (HasFlags(tfSpawned)) {
|
|
if (!Margin && !Directly) { // this is an actual check
|
|
// The spawned timer's start-/stopTimes are adjusted to the event's times in AdjustSpawnedTimer().
|
|
// However, in order to make sure the timer is set to the correct event, the margins at begin
|
|
// end end are limited by the durations of the events before and after this timer's event.
|
|
// The recording, though, shall always use the full start/stop margins, hence this calculation:
|
|
return event->StartTime() - Setup.MarginStart * 60 <= t && t < event->EndTime() + Setup.MarginStop * 60;
|
|
}
|
|
}
|
|
}
|
|
return startTime <= t + Margin && t < stopTime; // must stop *before* stopTime to allow adjacent timers
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#define FULLMATCH 1000
|
|
|
|
eTimerMatch cTimer::Matches(const cEvent *Event, int *Overlap) const
|
|
{
|
|
// Overlap is the percentage of the Event's duration that is covered by
|
|
// this timer (based on FULLMATCH for finer granularity than just 100).
|
|
// To make sure a VPS timer can be distinguished from a plain 100% overlap,
|
|
// it gets an additional 100 added, and a VPS event that is actually running
|
|
// gets 200 added to the FULLMATCH.
|
|
if (channel->GetChannelID() == Event->ChannelID()) {
|
|
bool UseVps = HasFlags(tfVps) && Event->Vps();
|
|
if (IsPatternTimer()) {
|
|
if (startswith(Pattern(), TIMERPATTERN_AVOID)) {
|
|
cString FileName = MakePatternFileName(Pattern(), Event->Title(), Event->ShortText(), File());
|
|
if (*FileName) {
|
|
const char *p = strgetlast(*FileName, FOLDERDELIMCHAR);
|
|
if (DoneRecordingsPattern.Contains(p))
|
|
return tmNone;
|
|
}
|
|
else
|
|
return tmNone;
|
|
}
|
|
else if (!MatchPattern(Pattern(), Event->Title()))
|
|
return tmNone;
|
|
UseVps = false;
|
|
}
|
|
Matches(UseVps ? Event->Vps() : Event->StartTime(), true);
|
|
int overlap = 0;
|
|
if (UseVps) {
|
|
if (startTime == Event->Vps()) {
|
|
overlap = FULLMATCH;
|
|
if (Event->IsRunning())
|
|
overlap += 200;
|
|
else if (Event->RunningStatus() != SI::RunningStatusNotRunning)
|
|
overlap += 100;
|
|
}
|
|
}
|
|
else {
|
|
if (startTime <= Event->StartTime() && Event->EndTime() <= stopTime)
|
|
overlap = FULLMATCH;
|
|
else if (stopTime <= Event->StartTime() || Event->EndTime() <= startTime)
|
|
overlap = 0;
|
|
else {
|
|
overlap = (min(stopTime, Event->EndTime()) - max(startTime, Event->StartTime())) * FULLMATCH / max(Event->Duration(), 1);
|
|
if (IsPatternTimer() && overlap > 0)
|
|
overlap = FULLMATCH;
|
|
}
|
|
}
|
|
startTime = stopTime = 0;
|
|
if (Overlap)
|
|
*Overlap = overlap;
|
|
return overlap >= FULLMATCH ? tmFull : overlap > 0 ? tmPartial : tmNone;
|
|
}
|
|
return tmNone;
|
|
}
|
|
|
|
#define EXPIRELATENCY 60 // seconds (just in case there's a short glitch in the VPS signal)
|
|
|
|
bool cTimer::Expired(void) const
|
|
{
|
|
if (IsSingleEvent() && !Recording()) {
|
|
time_t ExpireTime = StopTimeEvent();
|
|
if (HasFlags(tfVps))
|
|
ExpireTime += EXPIRELATENCY;
|
|
return ExpireTime <= time(NULL);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
time_t cTimer::StartTime(void) const
|
|
{
|
|
if (!startTime)
|
|
Matches();
|
|
return startTime;
|
|
}
|
|
|
|
time_t cTimer::StopTime(void) const
|
|
{
|
|
if (!stopTime)
|
|
Matches();
|
|
return stopTime;
|
|
}
|
|
|
|
time_t cTimer::StartTimeEvent(void) const
|
|
{
|
|
if (event) {
|
|
if (HasFlags(tfVps) && event->Vps())
|
|
return event->StartTime();
|
|
else if (HasFlags(tfSpawned))
|
|
return event->StartTime() - Setup.MarginStart * 60;
|
|
}
|
|
return StartTime();
|
|
}
|
|
|
|
time_t cTimer::StopTimeEvent(void) const
|
|
{
|
|
if (event) {
|
|
if (HasFlags(tfVps) && event->Vps())
|
|
return event->EndTime();
|
|
else if (HasFlags(tfSpawned))
|
|
return event->EndTime() + Setup.MarginStop * 60;
|
|
}
|
|
return StopTime();
|
|
}
|
|
|
|
#define EPGLIMITBEFORE (1 * 3600) // Time in seconds before a timer's start time and
|
|
#define EPGLIMITAFTER (1 * 3600) // after its stop time within which EPG events will be taken into consideration.
|
|
|
|
void cTimer::SetId(int Id)
|
|
{
|
|
id = Id;
|
|
}
|
|
|
|
cTimer *cTimer::SpawnPatternTimer(const cEvent *Event, cTimers *Timers)
|
|
{
|
|
cString FileName = MakePatternFileName(Pattern(), Event->Title(), Event->ShortText(), File());
|
|
isyslog("spawning timer %s for event %s", *ToDescr(), *Event->ToDescr());
|
|
cTimer *t = new cTimer(Event, FileName, this);
|
|
t->SetFlags(tfSpawned);
|
|
if (startswith(Pattern(), TIMERPATTERN_AVOID))
|
|
t->SetFlags(tfAvoid);
|
|
Timers->Add(t);
|
|
HandleRemoteTimerModifications(t);
|
|
return t;
|
|
}
|
|
|
|
bool cTimer::SpawnPatternTimers(const cSchedules *Schedules, cTimers *Timers)
|
|
{
|
|
bool TimersSpawned = false;
|
|
const cSchedule *Schedule = Schedules->GetSchedule(Channel());
|
|
if (Schedule && Schedule->Events()->First()) {
|
|
if (Schedule->Modified(scheduleStateSpawn)) {
|
|
time_t Now = time(NULL);
|
|
// Find the first event that matches this pattern timer and either already has a spawned
|
|
// timer, or has not yet ended:
|
|
for (const cEvent *e = Schedule->Events()->First(); e; e = Schedule->Events()->Next(e)) {
|
|
if (Matches(e) != tmNone) {
|
|
const cTimer *Timer = Timers->GetTimerForEvent(e, tfSpawned); // a matching event that already has a spawned timer
|
|
if (!Timer && e->EndTime() > Now) { // only look at events that have not yet ended
|
|
Timer = SpawnPatternTimer(e, Timers);
|
|
TimersSpawned = true;
|
|
}
|
|
if (Timer) {
|
|
// Check all following matching events that would start while the first timer
|
|
// is still recording:
|
|
bool UseVps = Timer->HasFlags(tfVps);
|
|
time_t Limit = Timer->StopTimeEvent();
|
|
if (UseVps)
|
|
Limit += EXPIRELATENCY;
|
|
else
|
|
Limit += Setup.MarginStart * 60;
|
|
for (e = Schedule->Events()->Next(e); e; e = Schedule->Events()->Next(e)) {
|
|
if (e->StartTime() <= Limit) {
|
|
if (!Timers->GetTimerForEvent(e, tfSpawned) && Matches(e) != tmNone) {
|
|
SpawnPatternTimer(e, Timers);
|
|
TimersSpawned = true;
|
|
}
|
|
if (UseVps)
|
|
break; // with VPS we only need to check the event immediately following the first one
|
|
}
|
|
else
|
|
break; // no need to check events that are too far in the future
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return TimersSpawned;
|
|
}
|
|
|
|
bool cTimer::AdjustSpawnedTimer(void)
|
|
{
|
|
if (Event()) {
|
|
if (const cSchedule *Schedule = Event()->Schedule()) { // events may be deleted from their schedule in cSchedule::DropOutdated()!
|
|
if (Schedule->Modified(scheduleStateAdjust)) {
|
|
// Adjust the timer to shifted start/stop times of the event if necessary:
|
|
time_t tstart = Event()->StartTime();
|
|
time_t tstop = Event()->EndTime();
|
|
int MarginStart = 0;
|
|
int MarginStop = 0;
|
|
CalcMargins(MarginStart, MarginStop, Event());
|
|
tstart -= MarginStart;
|
|
tstop += MarginStop;
|
|
// Event start/end times are given in "seconds since the epoch". Some broadcasters use values
|
|
// that result in full minutes (with zero seconds), while others use any values. VDR's timers
|
|
// use times given in full minutes, truncating any seconds. Thus we only react if the start/stop
|
|
// times of the timer are off by at least one minute:
|
|
if (abs(StartTime() - tstart) >= 60 || abs(StopTime() - tstop) >= 60) {
|
|
cString OldDescr = ToDescr();
|
|
struct tm tm_r;
|
|
struct tm *time = localtime_r(&tstart, &tm_r);
|
|
SetDay(cTimer::SetTime(tstart, 0));
|
|
SetStart(time->tm_hour * 100 + time->tm_min);
|
|
time = localtime_r(&tstop, &tm_r);
|
|
SetStop(time->tm_hour * 100 + time->tm_min);
|
|
Matches();
|
|
isyslog("timer %s times changed to %s-%s", *OldDescr, *TimeString(tstart), *TimeString(tstop));
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void cTimer::TriggerRespawn(void)
|
|
{
|
|
if (Local() && HasFlags(tfSpawned) || IsPatternTimer()) {
|
|
if (Channel()) {
|
|
LOCK_CHANNELS_READ;
|
|
if (const cSchedule *Schedule = Channel()->Schedule()) {
|
|
dsyslog("triggering respawn for timer %s", *ToDescr());
|
|
LOCK_SCHEDULES_WRITE;
|
|
const_cast<cSchedule *>(Schedule)->SetModified();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool cTimer::SetEventFromSchedule(const cSchedules *Schedules)
|
|
{
|
|
if (IsPatternTimer())
|
|
return SetEvent(NULL);
|
|
const cSchedule *Schedule = Schedules->GetSchedule(Channel());
|
|
if (Schedule && Schedule->Events()->First()) {
|
|
if (Schedule->Modified(scheduleStateSet)) {
|
|
const cEvent *Event = NULL;
|
|
if (HasFlags(tfVps) && Schedule->Events()->First()->Vps()) {
|
|
// VPS timers only match if their start time exactly matches the event's VPS time:
|
|
for (const cEvent *e = Schedule->Events()->First(); e; e = Schedule->Events()->Next(e)) {
|
|
if (e->StartTime()) {
|
|
int overlap = 0;
|
|
if (Matches(e, &overlap) == tmFull) {
|
|
Event = e;
|
|
if (overlap > FULLMATCH)
|
|
break; // take the first matching event
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// Normal timers match the event they have the most overlap with:
|
|
int Overlap = 0;
|
|
// Set up the time frame within which to check events:
|
|
Matches(0, true);
|
|
time_t TimeFrameBegin = StartTime() - EPGLIMITBEFORE;
|
|
time_t TimeFrameEnd = StopTime() + EPGLIMITAFTER;
|
|
for (const cEvent *e = Schedule->Events()->First(); e; e = Schedule->Events()->Next(e)) {
|
|
if (e->EndTime() < TimeFrameBegin)
|
|
continue; // skip events way before the timer starts
|
|
if (e->StartTime() > TimeFrameEnd)
|
|
break; // the rest is way after the timer ends
|
|
int overlap = 0;
|
|
Matches(e, &overlap);
|
|
if (overlap && overlap >= Overlap) {
|
|
if (Event && overlap == Overlap && e->Duration() <= Event->Duration())
|
|
continue; // if overlap is the same, we take the longer event
|
|
Overlap = overlap;
|
|
Event = e;
|
|
}
|
|
}
|
|
}
|
|
return SetEvent(Event);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool cTimer::SetEvent(const cEvent *Event)
|
|
{
|
|
if (event != Event) {
|
|
if (event)
|
|
event->DecNumTimers();
|
|
if (Event) {
|
|
isyslog("timer %s set to event %s", *ToDescr(), *Event->ToDescr());
|
|
Event->IncNumTimers();
|
|
Event->Schedule()->Modified(scheduleStateSet); // to get the current state
|
|
}
|
|
else {
|
|
isyslog("timer %s set to no event", *ToDescr());
|
|
scheduleStateSet = scheduleStateSpawn = scheduleStateAdjust = -1;
|
|
}
|
|
event = Event;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void cTimer::SetRecording(bool Recording)
|
|
{
|
|
if (Recording)
|
|
SetFlags(tfRecording);
|
|
else
|
|
ClrFlags(tfRecording);
|
|
isyslog("timer %s %s", *ToDescr(), Recording ? "start" : "stop");
|
|
}
|
|
|
|
void cTimer::SetPending(bool Pending)
|
|
{
|
|
pending = Pending;
|
|
}
|
|
|
|
void cTimer::SetInVpsMargin(bool InVpsMargin)
|
|
{
|
|
if (InVpsMargin && !inVpsMargin)
|
|
isyslog("timer %s entered VPS margin", *ToDescr());
|
|
inVpsMargin = InVpsMargin;
|
|
}
|
|
|
|
void cTimer::SetDay(time_t Day)
|
|
{
|
|
day = Day;
|
|
}
|
|
|
|
void cTimer::SetWeekDays(int WeekDays)
|
|
{
|
|
weekdays = WeekDays;
|
|
}
|
|
|
|
void cTimer::SetStart(int Start)
|
|
{
|
|
start = Start;
|
|
}
|
|
|
|
void cTimer::SetStop(int Stop)
|
|
{
|
|
stop = Stop;
|
|
}
|
|
|
|
void cTimer::SetPriority(int Priority)
|
|
{
|
|
priority = Priority;
|
|
}
|
|
|
|
void cTimer::SetLifetime(int Lifetime)
|
|
{
|
|
lifetime = Lifetime;
|
|
}
|
|
|
|
void cTimer::SetAux(const char *Aux)
|
|
{
|
|
free(aux);
|
|
aux = Aux ? strdup(Aux) : NULL;
|
|
}
|
|
|
|
void cTimer::SetRemote(const char *Remote)
|
|
{
|
|
free(remote);
|
|
remote = Remote ? strdup(Remote) : NULL;
|
|
}
|
|
|
|
void cTimer::SetDeferred(int Seconds)
|
|
{
|
|
deferred = time(NULL) + Seconds;
|
|
isyslog("timer %s deferred for %d seconds", *ToDescr(), Seconds);
|
|
}
|
|
|
|
void cTimer::SetFlags(uint Flags)
|
|
{
|
|
flags |= Flags;
|
|
}
|
|
|
|
void cTimer::ClrFlags(uint Flags)
|
|
{
|
|
flags &= ~Flags;
|
|
}
|
|
|
|
void cTimer::InvFlags(uint Flags)
|
|
{
|
|
flags ^= Flags;
|
|
}
|
|
|
|
bool cTimer::HasFlags(uint Flags) const
|
|
{
|
|
return (flags & Flags) == Flags;
|
|
}
|
|
|
|
void cTimer::Skip(void)
|
|
{
|
|
day = IncDay(SetTime(StartTime(), 0), 1);
|
|
startTime = 0;
|
|
SetEvent(NULL);
|
|
}
|
|
|
|
void cTimer::OnOff(void)
|
|
{
|
|
if (IsSingleEvent() || IsPatternTimer())
|
|
InvFlags(tfActive);
|
|
else if (day) {
|
|
day = 0;
|
|
ClrFlags(tfActive);
|
|
}
|
|
else if (HasFlags(tfActive))
|
|
Skip();
|
|
else
|
|
SetFlags(tfActive);
|
|
SetEvent(NULL);
|
|
if (HasFlags(tfActive))
|
|
TriggerRespawn(); // have pattern timers spawn if necessary
|
|
Matches(); // refresh start and end time
|
|
}
|
|
|
|
// --- cTimers ---------------------------------------------------------------
|
|
|
|
cTimers cTimers::timers;
|
|
int cTimers::lastTimerId = 0;
|
|
|
|
cTimers::cTimers(void)
|
|
:cConfig<cTimer>("1 Timers")
|
|
{
|
|
lastDeleteExpired = 0;
|
|
}
|
|
|
|
bool cTimers::Load(const char *FileName)
|
|
{
|
|
LOCK_TIMERS_WRITE;
|
|
Timers->SetExplicitModify();
|
|
if (timers.cConfig<cTimer>::Load(FileName)) {
|
|
for (cTimer *ti = timers.First(); ti; ti = timers.Next(ti)) {
|
|
ti->SetId(NewTimerId());
|
|
ti->ClrFlags(tfRecording);
|
|
Timers->SetModified();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int cTimers::NewTimerId(void)
|
|
{
|
|
return ++lastTimerId; // no need for locking, the caller must have a lock on the global Timers list
|
|
}
|
|
|
|
const cTimer *cTimers::GetById(int Id, const char *Remote) const
|
|
{
|
|
for (const cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (ti->Id() == Id) {
|
|
if (!Remote && !ti->Remote() || Remote && ti->Remote() && strcmp(Remote, ti->Remote()) == 0)
|
|
return ti;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
const cTimer *cTimers::GetTimer(const cTimer *Timer) const
|
|
{
|
|
for (const cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (!ti->Remote() &&
|
|
ti->Channel() == Timer->Channel() &&
|
|
(ti->WeekDays() && ti->WeekDays() == Timer->WeekDays() || !ti->WeekDays() && ti->Day() == Timer->Day()) &&
|
|
ti->Start() == Timer->Start() &&
|
|
ti->Stop() == Timer->Stop())
|
|
return ti;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
const cTimer *cTimers::GetMatch(time_t t) const
|
|
{
|
|
static int LastPending = -1;
|
|
const cTimer *t0 = NULL;
|
|
for (const cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (!ti->Remote() && !ti->Recording() && ti->Matches(t)) {
|
|
if (ti->Pending()) {
|
|
if (ti->Index() > LastPending) {
|
|
LastPending = ti->Index();
|
|
return ti;
|
|
}
|
|
else
|
|
continue;
|
|
}
|
|
if (!t0 || ti->Priority() > t0->Priority())
|
|
t0 = ti;
|
|
}
|
|
}
|
|
if (!t0)
|
|
LastPending = -1;
|
|
return t0;
|
|
}
|
|
|
|
const cTimer *cTimers::GetMatch(const cEvent *Event, eTimerMatch *Match) const
|
|
{
|
|
const cTimer *t = NULL;
|
|
eTimerMatch m = tmNone;
|
|
for (const cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
eTimerMatch tm = ti->Matches(Event);
|
|
if (tm > m || tm == tmFull && t && (t->Remote() && ti->Local() || t->IsPatternTimer() && ti->HasFlags(tfSpawned))) {
|
|
t = ti;
|
|
m = tm;
|
|
}
|
|
}
|
|
if (Match)
|
|
*Match = m;
|
|
return t;
|
|
}
|
|
|
|
const cTimer *cTimers::GetTimerForEvent(const cEvent *Event, eTimerFlags Flags)
|
|
{
|
|
if (Event && Event->HasTimer()) {
|
|
for (const cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (ti->Event() == Event && ti->Local() && ti->HasFlags(Flags))
|
|
return ti;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
int cTimers::GetMaxPriority(void) const
|
|
{
|
|
int n = -1;
|
|
for (const cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (!ti->Remote() && ti->Recording())
|
|
n = max(n, ti->Priority());
|
|
}
|
|
return n;
|
|
}
|
|
|
|
const cTimer *cTimers::GetNextActiveTimer(void) const
|
|
{
|
|
const cTimer *t0 = NULL;
|
|
for (const cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (!ti->Remote() && !ti->IsPatternTimer()) {
|
|
ti->Matches();
|
|
if ((ti->HasFlags(tfActive)) && (!t0 || ti->StopTime() > time(NULL) && ti->Compare(*t0) < 0))
|
|
t0 = ti;
|
|
}
|
|
}
|
|
return t0;
|
|
}
|
|
|
|
const cTimers *cTimers::GetTimersRead(cStateKey &StateKey, int TimeoutMs)
|
|
{
|
|
return timers.Lock(StateKey, false, TimeoutMs) ? &timers : NULL;
|
|
}
|
|
|
|
cTimers *cTimers::GetTimersWrite(cStateKey &StateKey, int TimeoutMs)
|
|
{
|
|
return timers.Lock(StateKey, true, TimeoutMs) ? &timers : NULL;
|
|
}
|
|
|
|
void cTimers::Add(cTimer *Timer, cTimer *After)
|
|
{
|
|
if (!Timer->Remote())
|
|
Timer->SetId(NewTimerId());
|
|
cConfig<cTimer>::Add(Timer, After);
|
|
cStatus::MsgTimerChange(Timer, tcAdd);
|
|
}
|
|
|
|
void cTimers::Ins(cTimer *Timer, cTimer *Before)
|
|
{
|
|
cConfig<cTimer>::Ins(Timer, Before);
|
|
cStatus::MsgTimerChange(Timer, tcAdd);
|
|
}
|
|
|
|
void cTimers::Del(cTimer *Timer, bool DeleteObject)
|
|
{
|
|
cStatus::MsgTimerChange(Timer, tcDel);
|
|
cConfig<cTimer>::Del(Timer, DeleteObject);
|
|
}
|
|
|
|
const cTimer *cTimers::UsesChannel(const cChannel *Channel) const
|
|
{
|
|
for (const cTimer *Timer = First(); Timer; Timer = Next(Timer)) {
|
|
if (Timer->Channel() == Channel)
|
|
return Timer;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
bool cTimers::SetEvents(const cSchedules *Schedules)
|
|
{
|
|
bool TimersModified = false;
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (!ti->IsPatternTimer())
|
|
TimersModified |= ti->SetEventFromSchedule(Schedules);
|
|
}
|
|
return TimersModified;
|
|
}
|
|
|
|
bool cTimers::SpawnPatternTimers(const cSchedules *Schedules)
|
|
{
|
|
bool TimersModified = false;
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (ti->IsPatternTimer() && ti->Local()) {
|
|
if (ti->HasFlags(tfActive))
|
|
TimersModified |= ti->SpawnPatternTimers(Schedules, this);
|
|
}
|
|
}
|
|
return TimersModified;
|
|
}
|
|
|
|
bool cTimers::AdjustSpawnedTimers(void)
|
|
{
|
|
bool TimersModified = false;
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (ti->Local()) {
|
|
if (ti->HasFlags(tfSpawned) && !ti->HasFlags(tfVps))
|
|
TimersModified |= ti->AdjustSpawnedTimer();
|
|
}
|
|
}
|
|
return TimersModified;
|
|
}
|
|
|
|
#define DELETE_EXPIRED_TIMEOUT 30 // seconds
|
|
|
|
bool cTimers::DeleteExpired(bool Force)
|
|
{
|
|
if (!Force && time(NULL) - lastDeleteExpired < DELETE_EXPIRED_TIMEOUT)
|
|
return false;
|
|
bool TimersModified = false;
|
|
cTimer *ti = First();
|
|
while (ti) {
|
|
cTimer *next = Next(ti);
|
|
if (!ti->Remote() && ti->Expired()) {
|
|
ti->SetEvent(NULL); // Del() doesn't call ~cTimer() right away, so this is necessary here
|
|
ti->TriggerRespawn(); // in case this is a spawned timer
|
|
isyslog("deleting timer %s", *ti->ToDescr());
|
|
Del(ti);
|
|
TimersModified = true;
|
|
}
|
|
ti = next;
|
|
}
|
|
lastDeleteExpired = time(NULL);
|
|
return TimersModified;
|
|
}
|
|
|
|
bool cTimers::StoreRemoteTimers(const char *ServerName, const cStringList *RemoteTimers)
|
|
{
|
|
bool Result = false;
|
|
if (!ServerName || !RemoteTimers || RemoteTimers->Size() == 0) {
|
|
// Remove remote timers from this list:
|
|
cTimer *Timer = First();
|
|
while (Timer) {
|
|
cTimer *t = Next(Timer);
|
|
if (Timer->Remote() && (!ServerName || strcmp(Timer->Remote(), ServerName) == 0)) {
|
|
Del(Timer);
|
|
Result = true;
|
|
}
|
|
Timer = t;
|
|
}
|
|
return Result;
|
|
}
|
|
// Collect all locally stored remote timers from ServerName:
|
|
cStringList tl;
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (ti->Remote() && strcmp(ti->Remote(), ServerName) == 0)
|
|
tl.Append(strdup(cString::sprintf("%d %s", ti->Id(), *ti->ToText(true))));
|
|
}
|
|
tl.SortNumerically(); // RemoteTimers is also sorted numerically!
|
|
// Compare the two lists and react accordingly:
|
|
int il = 0; // index into the local ("left") list of remote timers
|
|
int ir = 0; // index into the remote ("right") list of timers
|
|
int sl = tl.Size();
|
|
int sr = RemoteTimers->Size();
|
|
for (;;) {
|
|
int AddTimer = 0;
|
|
int DelTimer = 0;
|
|
if (il < sl) { // still have left entries
|
|
int nl = atoi(tl[il]);
|
|
if (ir < sr) { // still have right entries
|
|
// Compare timers:
|
|
int nr = atoi((*RemoteTimers)[ir]);
|
|
if (nl == nr) // same timer id
|
|
AddTimer = DelTimer = nl;
|
|
else if (nl < nr) // left entry not in right list
|
|
DelTimer = nl;
|
|
else // right entry not in left list
|
|
AddTimer = nr;
|
|
}
|
|
else // processed all right entries
|
|
DelTimer = nl;
|
|
}
|
|
else if (ir < sr) { // still have right entries
|
|
AddTimer = atoi((*RemoteTimers)[ir]);
|
|
if (!AddTimer) {
|
|
esyslog("ERROR: %s: error in timer settings: %s", ServerName, (*RemoteTimers)[ir]);
|
|
ir++;
|
|
continue; // let's see if we can process the rest
|
|
}
|
|
}
|
|
else // processed all left and right entries
|
|
break;
|
|
if (AddTimer && DelTimer) {
|
|
if (strcmp(tl[il], (*RemoteTimers)[ir]) != 0) {
|
|
// Overwrite timer:
|
|
char *v = (*RemoteTimers)[ir];
|
|
while (*v && *v != ' ')
|
|
v++; // skip id
|
|
if (cTimer *l = GetById(DelTimer, ServerName)) {
|
|
cTimer r;
|
|
if (r.Parse(v)) {
|
|
r.SetRemote(ServerName);
|
|
r.SetId(AddTimer);
|
|
*l = r;
|
|
Result = true;
|
|
}
|
|
else
|
|
esyslog("ERROR: %d@%s: error in timer settings: %s", DelTimer, ServerName, v);
|
|
}
|
|
}
|
|
else // identical timer, nothing to do
|
|
;
|
|
il++;
|
|
ir++;
|
|
}
|
|
else if (AddTimer) {
|
|
char *v = (*RemoteTimers)[ir];
|
|
while (*v && *v != ' ')
|
|
v++; // skip id
|
|
cTimer *Timer = new cTimer;
|
|
if (Timer->Parse(v)) {
|
|
Timer->SetRemote(ServerName);
|
|
Timer->SetId(AddTimer);
|
|
Add(Timer);
|
|
Result = true;
|
|
}
|
|
else {
|
|
esyslog("ERROR: %s: error in timer settings: %s", ServerName, v);
|
|
delete Timer;
|
|
}
|
|
ir++;
|
|
}
|
|
else if (DelTimer) {
|
|
if (cTimer *t = GetById(DelTimer, ServerName)) {
|
|
Del(t);
|
|
Result = true;
|
|
}
|
|
il++;
|
|
}
|
|
else {
|
|
esyslog("ERROR: oops while storing remote timers!");
|
|
break; // let's not get stuck here!
|
|
}
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
static bool RemoteTimerError(const cTimer *Timer, cString *Msg)
|
|
{
|
|
if (Msg)
|
|
*Msg = cString::sprintf("%s %d@%s!", tr("Error while accessing remote timer"), Timer->Id(), Timer->Remote());
|
|
return false; // convenience return code
|
|
}
|
|
|
|
bool HandleRemoteTimerModifications(cTimer *NewTimer, cTimer *OldTimer, cString *Msg)
|
|
{
|
|
cStringList Response;
|
|
if (!NewTimer) {
|
|
if (OldTimer) { // timer shall be deleted from remote machine
|
|
if (OldTimer->Remote() && OldTimer->Id()) {
|
|
if (!ExecSVDRPCommand(OldTimer->Remote(), cString::sprintf("DELT %d", OldTimer->Id()), &Response) || SVDRPCode(Response[0]) != 250)
|
|
return RemoteTimerError(OldTimer, Msg);
|
|
}
|
|
isyslog("deleted timer %s", *OldTimer->ToDescr());
|
|
}
|
|
}
|
|
else if (!OldTimer || OldTimer->Local() || !OldTimer->Id()) {
|
|
if (NewTimer->Local()) { // timer stays local, nothing to do
|
|
if (OldTimer && OldTimer->Id())
|
|
isyslog("modified timer %s", *NewTimer->ToDescr());
|
|
else
|
|
isyslog("added timer %s", *NewTimer->ToDescr());
|
|
}
|
|
else { // timer is new, or moved from local to remote
|
|
if (!ExecSVDRPCommand(NewTimer->Remote(), cString::sprintf("NEWT %s", *NewTimer->ToText(true)), &Response) || SVDRPCode(Response[0]) != 250)
|
|
return RemoteTimerError(NewTimer, Msg);
|
|
int RemoteId = atoi(SVDRPValue(Response[0]));
|
|
if (RemoteId <= 0)
|
|
return RemoteTimerError(NewTimer, Msg);
|
|
NewTimer->SetId(RemoteId);
|
|
if (OldTimer && OldTimer->Id()) {
|
|
isyslog("moved timer %d to %s", OldTimer->Id(), *NewTimer->ToDescr());
|
|
}
|
|
else
|
|
isyslog("added timer %s", *NewTimer->ToDescr());
|
|
}
|
|
}
|
|
else if (NewTimer->Local()) { // timer is moved from remote to local
|
|
if (!ExecSVDRPCommand(OldTimer->Remote(), cString::sprintf("DELT %d", OldTimer->Id()), &Response) || SVDRPCode(Response[0]) != 250)
|
|
return RemoteTimerError(OldTimer, Msg);
|
|
NewTimer->SetId(cTimers::NewTimerId());
|
|
NewTimer->ClrFlags(tfRecording); // in case it was recording on the remote machine
|
|
isyslog("moved timer %d@%s to %s", OldTimer->Id(), OldTimer->Remote(), *NewTimer->ToDescr());
|
|
}
|
|
else if (strcmp(OldTimer->Remote(), NewTimer->Remote()) == 0) { // timer stays remote on same machine
|
|
if (!ExecSVDRPCommand(OldTimer->Remote(), cString::sprintf("MODT %d %s", OldTimer->Id(), *NewTimer->ToText(true)), &Response) || SVDRPCode(Response[0]) != 250)
|
|
return RemoteTimerError(NewTimer, Msg);
|
|
isyslog("modified timer %s", *NewTimer->ToDescr());
|
|
}
|
|
else { // timer is moved from one remote machine to an other
|
|
if (!ExecSVDRPCommand(NewTimer->Remote(), cString::sprintf("NEWT %s", *NewTimer->ToText(true)), &Response) || SVDRPCode(Response[0]) != 250)
|
|
return RemoteTimerError(NewTimer, Msg);
|
|
int RemoteId = atoi(SVDRPValue(Response[0]));
|
|
if (RemoteId <= 0)
|
|
return RemoteTimerError(NewTimer, Msg);
|
|
NewTimer->SetId(RemoteId);
|
|
if (!ExecSVDRPCommand(OldTimer->Remote(), cString::sprintf("DELT %d", OldTimer->Id()), &Response) || SVDRPCode(Response[0]) != 250)
|
|
return RemoteTimerError(OldTimer, Msg);
|
|
isyslog("moved timer %d@%s to %s", OldTimer->Id(), OldTimer->Remote(), *NewTimer->ToDescr());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// --- cSortedTimers ---------------------------------------------------------
|
|
|
|
static int CompareTimers(const void *a, const void *b)
|
|
{
|
|
return (*(const cTimer **)a)->Compare(**(const cTimer **)b);
|
|
}
|
|
|
|
cSortedTimers::cSortedTimers(const cTimers *Timers)
|
|
:cVector<const cTimer *>(Timers->Count())
|
|
{
|
|
for (const cTimer *Timer = Timers->First(); Timer; Timer = Timers->Next(Timer))
|
|
Append(Timer);
|
|
Sort(CompareTimers);
|
|
}
|