mirror of
https://github.com/VDR4Arch/vdr.git
synced 2023-10-10 13:36:52 +02:00
573 lines
15 KiB
C
573 lines
15 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 1.21 2004/12/26 12:35:33 kls Exp $
|
|
*/
|
|
|
|
#include "timers.h"
|
|
#include <ctype.h>
|
|
#include "channels.h"
|
|
#include "i18n.h"
|
|
#include "remote.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)
|
|
{
|
|
startTime = stopTime = 0;
|
|
recording = pending = inVpsMargin = false;
|
|
flags = tfNone;
|
|
if (Instant)
|
|
SetFlags(tfActive | tfInstant);
|
|
channel = Channels.GetByNumber(cDevice::CurrentChannel());
|
|
time_t t = time(NULL);
|
|
struct tm tm_r;
|
|
struct tm *now = localtime_r(&t, &tm_r);
|
|
day = now->tm_mday;
|
|
start = now->tm_hour * 100 + now->tm_min;
|
|
stop = now->tm_hour * 60 + now->tm_min + Setup.InstantRecordTime;
|
|
stop = (stop / 60) * 100 + (stop % 60);
|
|
if (stop >= 2400)
|
|
stop -= 2400;
|
|
priority = Pause ? Setup.PausePriority : Setup.DefaultPriority;
|
|
lifetime = Pause ? Setup.PauseLifetime : Setup.DefaultLifetime;
|
|
*file = 0;
|
|
firstday = 0;
|
|
summary = NULL;
|
|
event = NULL;
|
|
if (Instant && channel)
|
|
snprintf(file, sizeof(file), "%s%s", Setup.MarkInstantRecord ? "@" : "", *Setup.NameInstantRecord ? Setup.NameInstantRecord : channel->Name());
|
|
}
|
|
|
|
cTimer::cTimer(const cEvent *Event)
|
|
{
|
|
startTime = stopTime = 0;
|
|
recording = pending = inVpsMargin = false;
|
|
flags = tfActive;
|
|
if (Event->Vps() && Setup.UseVps)
|
|
SetFlags(tfVps);
|
|
channel = Channels.GetByChannelID(Event->ChannelID(), true);
|
|
time_t tstart = (flags & tfVps) ? Event->Vps() : Event->StartTime();
|
|
time_t tstop = tstart + Event->Duration();
|
|
if (!(HasFlags(tfVps))) {
|
|
tstop += Setup.MarginStop * 60;
|
|
tstart -= Setup.MarginStart * 60;
|
|
}
|
|
struct tm tm_r;
|
|
struct tm *time = localtime_r(&tstart, &tm_r);
|
|
day = time->tm_mday;
|
|
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 = Setup.DefaultPriority;
|
|
lifetime = Setup.DefaultLifetime;
|
|
*file = 0;
|
|
const char *Title = Event->Title();
|
|
if (!isempty(Title))
|
|
strn0cpy(file, Event->Title(), sizeof(file));
|
|
firstday = 0;
|
|
summary = NULL;
|
|
event = Event;
|
|
}
|
|
|
|
cTimer::~cTimer()
|
|
{
|
|
free(summary);
|
|
}
|
|
|
|
cTimer& cTimer::operator= (const cTimer &Timer)
|
|
{
|
|
memcpy(this, &Timer, sizeof(*this));
|
|
if (summary)
|
|
summary = strdup(summary);
|
|
event = NULL;
|
|
return *this;
|
|
}
|
|
|
|
int cTimer::Compare(const cListObject &ListObject) const
|
|
{
|
|
cTimer *ti = (cTimer *)&ListObject;
|
|
time_t t1 = StartTime();
|
|
time_t t2 = ti->StartTime();
|
|
int r = t1 - t2;
|
|
if (r == 0)
|
|
r = ti->priority - priority;
|
|
return r;
|
|
}
|
|
|
|
cString cTimer::ToText(bool UseChannelID)
|
|
{
|
|
char *buffer;
|
|
strreplace(file, ':', '|');
|
|
strreplace(summary, '\n', '|');
|
|
asprintf(&buffer, "%d:%s:%s:%04d:%04d:%d:%d:%s:%s\n", flags, UseChannelID ? *Channel()->GetChannelID().ToString() : *itoa(Channel()->Number()), *PrintDay(day, firstday), start, stop, priority, lifetime, file, summary ? summary : "");
|
|
strreplace(summary, '|', '\n');
|
|
strreplace(file, '|', ':');
|
|
return buffer;
|
|
}
|
|
|
|
int cTimer::TimeToInt(int t)
|
|
{
|
|
return (t / 100 * 60 + t % 100) * 60;
|
|
}
|
|
|
|
int cTimer::ParseDay(const char *s, time_t *FirstDay)
|
|
{
|
|
char *tail;
|
|
int d = strtol(s, &tail, 10);
|
|
if (FirstDay)
|
|
*FirstDay = 0;
|
|
if (tail && *tail) {
|
|
d = 0;
|
|
if (tail == s) {
|
|
const char *first = strchr(s, '@');
|
|
int l = first ? first - s : strlen(s);
|
|
if (l == 7) {
|
|
for (const char *p = s + 6; p >= s; p--) {
|
|
d <<= 1;
|
|
d |= (*p != '-');
|
|
}
|
|
d |= 0x80000000;
|
|
}
|
|
if (FirstDay && first) {
|
|
++first;
|
|
if (strlen(first) == 10) {
|
|
struct tm tm_r;
|
|
if (3 == sscanf(first, "%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
|
|
*FirstDay = mktime(&tm_r);
|
|
}
|
|
}
|
|
else
|
|
d = 0;
|
|
}
|
|
}
|
|
}
|
|
else if (d < 1 || d > 31)
|
|
d = 0;
|
|
return d;
|
|
}
|
|
|
|
cString cTimer::PrintDay(int d, time_t FirstDay)
|
|
{
|
|
#define DAYBUFFERSIZE 32
|
|
char buffer[DAYBUFFERSIZE];
|
|
if ((d & 0x80000000) != 0) {
|
|
char *b = buffer;
|
|
const char *w = tr("MTWTFSS");
|
|
while (*w) {
|
|
*b++ = (d & 1) ? *w : '-';
|
|
d >>= 1;
|
|
w++;
|
|
}
|
|
if (FirstDay) {
|
|
struct tm tm_r;
|
|
localtime_r(&FirstDay, &tm_r);
|
|
b += strftime(b, DAYBUFFERSIZE - (b - buffer), "@%Y-%m-%d", &tm_r);
|
|
}
|
|
*b = 0;
|
|
}
|
|
else
|
|
sprintf(buffer, "%d", d);
|
|
return buffer;
|
|
}
|
|
|
|
cString cTimer::PrintFirstDay(void)
|
|
{
|
|
if (firstday) {
|
|
cString s = PrintDay(day, firstday);
|
|
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(summary);
|
|
summary = NULL;
|
|
//XXX Apparently sscanf() doesn't work correctly if the last %a argument
|
|
//XXX results in an empty string (this first occured 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() failes 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, "%d :%a[^:]:%a[^:]:%d :%d :%d :%d :%a[^:\n]:%a[^\n]", &flags, &channelbuffer, &daybuffer, &start, &stop, &priority, &lifetime, &filebuffer, &summary)) {
|
|
if (summary && !*skipspace(summary)) {
|
|
free(summary);
|
|
summary = NULL;
|
|
}
|
|
//TODO add more plausibility checks
|
|
day = ParseDay(daybuffer, &firstday);
|
|
result = day != 0;
|
|
strn0cpy(file, filebuffer, MaxFileName);
|
|
strreplace(file, '|', ':');
|
|
strreplace(summary, '|', '\n');
|
|
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)
|
|
{
|
|
return fprintf(f, "%s", *ToText(true)) > 0;
|
|
}
|
|
|
|
bool cTimer::IsSingleEvent(void) const
|
|
{
|
|
return (day & 0x80000000) == 0;
|
|
}
|
|
|
|
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!
|
|
}
|
|
|
|
int cTimer::GetWDayFromMDay(int MDay)
|
|
{
|
|
time_t now = time(NULL);
|
|
for (int i = -1; i <= 28; i++) { // looking 4 weeks into the future should be enough
|
|
time_t t0 = IncDay(now, i);
|
|
if (GetMDay(t0) == MDay)
|
|
return GetWDay(t0);
|
|
}
|
|
return GetWDay(now); // just to return something
|
|
}
|
|
|
|
bool cTimer::DayMatches(time_t t) const
|
|
{
|
|
return IsSingleEvent() ? GetMDay(t) == day : (day & (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);
|
|
}
|
|
|
|
char *cTimer::SetFile(const char *File)
|
|
{
|
|
if (!isempty(File))
|
|
strn0cpy(file, File, sizeof(file));
|
|
return file;
|
|
}
|
|
|
|
bool cTimer::Matches(time_t t, bool Directly) const
|
|
{
|
|
startTime = stopTime = 0;
|
|
if (t == 0)
|
|
t = time(NULL);
|
|
|
|
int begin = TimeToInt(start); // seconds from midnight
|
|
int length = TimeToInt(stop) - begin;
|
|
if (length < 0)
|
|
length += SECSINDAY;
|
|
|
|
int DaysToCheck = IsSingleEvent() ? 61 : 7; // 61 to handle months with 31/30/31
|
|
for (int i = -1; i <= DaysToCheck; i++) {
|
|
time_t t0 = IncDay(t, i);
|
|
if (DayMatches(t0)) {
|
|
time_t a = SetTime(t0, begin);
|
|
time_t b = a + length;
|
|
if ((!firstday || a >= firstday) && t <= b) {
|
|
startTime = a;
|
|
stopTime = b;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!startTime)
|
|
startTime = firstday; // just to have something that's more than a week in the future
|
|
else if (!Directly && (t > startTime || t > firstday + SECSINDAY + 3600)) // +3600 in case of DST change
|
|
firstday = 0;
|
|
|
|
if (HasFlags(tfActive)) {
|
|
if (HasFlags(tfVps) && !Directly && event && event->Vps()) {
|
|
startTime = event->StartTime();
|
|
stopTime = event->EndTime();
|
|
return event->IsRunning(true);
|
|
}
|
|
return startTime <= t && t < stopTime; // must stop *before* stopTime to allow adjacent timers
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int cTimer::Matches(const cEvent *Event)
|
|
{
|
|
if (channel->GetChannelID() == Event->ChannelID()) {
|
|
bool UseVps = HasFlags(tfVps) && Event->Vps();
|
|
time_t t1 = UseVps ? Event->Vps() : Event->StartTime();
|
|
time_t t2 = t1 + Event->Duration();
|
|
bool m1 = Matches(t1, true);
|
|
bool m2 = UseVps ? m1 : Matches(t2, true);
|
|
startTime = stopTime = 0;
|
|
if (m1 && m2) {
|
|
if (UseVps && Event->IsRunning(true))
|
|
return tmFull;
|
|
if (time(NULL) > Event->EndTime())
|
|
return tmNone;
|
|
return tmFull;
|
|
}
|
|
if ((m1 || m2) && time(NULL) <= Event->EndTime())
|
|
return tmPartial;
|
|
}
|
|
return tmNone;
|
|
}
|
|
|
|
time_t cTimer::StartTime(void) const
|
|
{
|
|
if (!startTime)
|
|
Matches();
|
|
return startTime;
|
|
}
|
|
|
|
time_t cTimer::StopTime(void) const
|
|
{
|
|
if (!stopTime)
|
|
Matches();
|
|
return stopTime;
|
|
}
|
|
|
|
void cTimer::SetEvent(const cEvent *Event)
|
|
{
|
|
if (event != Event) { //XXX TODO check event data, too???
|
|
if (Event) {
|
|
char vpsbuf[64] = "";
|
|
if (Event->Vps())
|
|
sprintf(vpsbuf, "(VPS: %s) ", *Event->GetVpsString());
|
|
isyslog("timer %d (%d %04d-%04d '%s') set to event %s %s-%s %s'%s'", Index() + 1, Channel()->Number(), start, stop, file, *Event->GetDateString(), *Event->GetTimeString(), *Event->GetEndTimeString(), vpsbuf, Event->Title());
|
|
}
|
|
else
|
|
isyslog("timer %d (%d %04d-%04d '%s') set to no event", Index() + 1, Channel()->Number(), start, stop, file);
|
|
event = Event;
|
|
}
|
|
}
|
|
|
|
void cTimer::SetRecording(bool Recording)
|
|
{
|
|
recording = Recording;
|
|
isyslog("timer %d (%d %04d-%04d '%s') %s", Index() + 1, Channel()->Number(), start, stop, file, recording ? "start" : "stop");
|
|
}
|
|
|
|
void cTimer::SetPending(bool Pending)
|
|
{
|
|
pending = Pending;
|
|
}
|
|
|
|
void cTimer::SetInVpsMargin(bool InVpsMargin)
|
|
{
|
|
if (InVpsMargin && !inVpsMargin)
|
|
isyslog("timer %d (%d %04d-%04d '%s') entered VPS margin", Index() + 1, Channel()->Number(), start, stop, file);
|
|
inVpsMargin = InVpsMargin;
|
|
}
|
|
|
|
void cTimer::SetFlags(int Flags)
|
|
{
|
|
flags |= Flags;
|
|
}
|
|
|
|
void cTimer::ClrFlags(int Flags)
|
|
{
|
|
flags &= ~Flags;
|
|
}
|
|
|
|
void cTimer::InvFlags(int Flags)
|
|
{
|
|
flags ^= Flags;
|
|
}
|
|
|
|
bool cTimer::HasFlags(int Flags) const
|
|
{
|
|
return (flags & Flags) == Flags;
|
|
}
|
|
|
|
void cTimer::Skip(void)
|
|
{
|
|
firstday = IncDay(SetTime(StartTime(), 0), 1);
|
|
event = NULL;
|
|
}
|
|
|
|
void cTimer::OnOff(void)
|
|
{
|
|
if (IsSingleEvent())
|
|
InvFlags(tfActive);
|
|
else if (firstday) {
|
|
firstday = 0;
|
|
ClrFlags(tfActive);
|
|
}
|
|
else if (HasFlags(tfActive))
|
|
Skip();
|
|
else
|
|
SetFlags(tfActive);
|
|
event = NULL;
|
|
Matches(); // refresh start and end time
|
|
}
|
|
|
|
// -- cTimers ----------------------------------------------------------------
|
|
|
|
cTimers Timers;
|
|
|
|
cTimers::cTimers(void)
|
|
{
|
|
modified = false;
|
|
beingEdited = 0;;
|
|
lastSetEvents = 0;
|
|
}
|
|
|
|
cTimer *cTimers::GetTimer(cTimer *Timer)
|
|
{
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (ti->Channel() == Timer->Channel() && ti->Day() == Timer->Day() && ti->Start() == Timer->Start() && ti->Stop() == Timer->Stop())
|
|
return ti;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
cTimer *cTimers::GetMatch(time_t t)
|
|
{
|
|
cTimer *t0 = NULL;
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (!ti->Recording() && ti->Matches(t)) {
|
|
if (!t0 || ti->Priority() > t0->Priority())
|
|
t0 = ti;
|
|
}
|
|
}
|
|
return t0;
|
|
}
|
|
|
|
cTimer *cTimers::GetMatch(const cEvent *Event, int *Match)
|
|
{
|
|
cTimer *t = NULL;
|
|
int m = tmNone;
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
int tm = ti->Matches(Event);
|
|
if (tm > m) {
|
|
t = ti;
|
|
m = tm;
|
|
if (m == tmFull)
|
|
break;
|
|
}
|
|
}
|
|
if (Match)
|
|
*Match = m;
|
|
return t;
|
|
}
|
|
|
|
cTimer *cTimers::GetNextActiveTimer(void)
|
|
{
|
|
cTimer *t0 = NULL;
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if ((ti->HasFlags(tfActive)) && (!t0 || ti->Compare(*t0) < 0))
|
|
t0 = ti;
|
|
}
|
|
return t0;
|
|
}
|
|
|
|
void cTimers::SetModified(void)
|
|
{
|
|
modified = true;
|
|
}
|
|
|
|
bool cTimers::Modified(void)
|
|
{
|
|
bool Result = modified;
|
|
modified = false;
|
|
return Result;
|
|
}
|
|
|
|
void cTimers::SetEvents(void)
|
|
{
|
|
if (time(NULL) - lastSetEvents < 5)
|
|
return;
|
|
cSchedulesLock SchedulesLock(false, 100);
|
|
const cSchedules *Schedules = cSchedules::Schedules(SchedulesLock);
|
|
if (Schedules) {
|
|
if (!lastSetEvents || Schedules->Modified() >= lastSetEvents) {
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
const cSchedule *Schedule = Schedules->GetSchedule(ti->Channel()->GetChannelID());
|
|
if (Schedule) {
|
|
if (!lastSetEvents || Schedule->Modified() >= lastSetEvents) {
|
|
const cEvent *Event = NULL;
|
|
int Match = tmNone;
|
|
for (const cEvent *e = Schedule->Events()->First(); e; e = Schedule->Events()->Next(e)) {
|
|
if (cRemote::HasKeys())
|
|
return; // react immediately on user input
|
|
int m = ti->Matches(e);
|
|
if (m > Match) {
|
|
Match = m;
|
|
Event = e;
|
|
if (Match == tmFull)
|
|
break;
|
|
//XXX what if there's another event with the same VPS time???
|
|
}
|
|
}
|
|
ti->SetEvent(Event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
lastSetEvents = time(NULL);
|
|
}
|