/* * timers.c: Timer handling * * See the main source file 'vdr.c' for copyright information and * how to reach the author. * * $Id: timers.c 5.18 2022/11/20 10:57:31 kls Exp $ */ #include "timers.h" #include #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(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(Event->Prev())) MarginStart = max(0, min(MarginStart, e->Duration() - 60)); if (const cEvent *e = dynamic_cast(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(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("1 Timers") { lastDeleteExpired = 0; } bool cTimers::Load(const char *FileName) { LOCK_TIMERS_WRITE; Timers->SetExplicitModify(); if (timers.cConfig::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) const { 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::Add(Timer, After); cStatus::MsgTimerChange(Timer, tcAdd); } void cTimers::Ins(cTimer *Timer, cTimer *Before) { cConfig::Ins(Timer, Before); cStatus::MsgTimerChange(Timer, tcAdd); } void cTimers::Del(cTimer *Timer, bool DeleteObject) { cStatus::MsgTimerChange(Timer, tcDel); cConfig::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(Timers->Count()) { for (const cTimer *Timer = Timers->First(); Timer; Timer = Timers->Next(Timer)) Append(Timer); Sort(CompareTimers); }