mirror of
https://github.com/vdr-projects/vdr.git
synced 2025-03-01 10:50:46 +00:00
Original announce message: VDR developer version 1.7.29 is now available at ftp://ftp.tvdr.de/vdr/Developer/vdr-1.7.29.tar.bz2 A 'diff' against the previous version is available at ftp://ftp.tvdr.de/vdr/Developer/vdr-1.7.28-1.7.29.diff MD5 checksums: a3f0ae42ba456aa1865c9ed065a64d80 vdr-1.7.29.tar.bz2 39db6b495210c293726126fbcba3e631 vdr-1.7.28-1.7.29.diff WARNING: ======== This is a developer version. Even though I use it in my productive environment. I strongly recommend that you only use it under controlled conditions and for testing and debugging. The default skin "LCARS" displays the signal strengths and qualities of all devices in its main menu. For devices that have an stb0899 frontend chip (like the TT-budget S2-3200) retrieving this information from the driver is rather slow, which results in a sluggish response to user input in the main menu. To speed this up you may want to apply the patches from ftp://ftp.tvdr.de/vdr/Developer/Driver-Patches to the LinuxDVB driver source. From the HISTORY file: - Added a missing template specification to the c'tor of cSortedTimers (thanks to Udo Richter). - Fixed the background color of the Transfer Mode indicator bitmap in the LCARS skin. - The LCARS skin now only displays devices that can actually receive channels, leaving out, for instance, pure replay devices (suggested by Reinhard Nissl). - Now scaling down the Transfer Mode indicator bitmap in the LCARS skin in case it doesn't fit with the selected font size (reported by Reinhard Nissl). - Fixed making LCARS the default skin. - Adjusted the default values for OSD and font sizes to better fit HDTV. - Updated the Finnish OSD texts (thanks to Rolf Ahrenberg). - Fixed the call to ChannelString() in cSkinLCARSDisplayChannel::SetChannel() (thanks to Rolf Ahrenberg). - Removed DeleteEvent() from the EPG handler interface (turned out not to be useful) and replaced it with HandledExternally() (thanks to Jörg Wendel). - Added SetComponents() to the EPG handler interface (thanks to Dirk Heiser). - Updated the Italian OSD texts (thanks to Diego Pierotto). - Changed the button colors in the LCARS skin to better fit with the rest of the theme. - Removed the gap from the main menu buttons in the LCARS skin. - Fixed some copy&paste errors in PLUGINS.html (thanks to Winfried Köhler). - The LCARS skin's main menu now only displays timers that are actually activated. - Within the "Recordings" menu, pressing the '0' key now toggles sorting between "by time" and "by name". The selected sort mode is stored separately for each folder (provided you have write access to that folder). If a folder is newly created by a repeating timer, the sort mode for that folder is initially set to "by time". - Fixed several spelling errors (thanks to Ville Skyttä). - Fixed handling recording with more than two bonded devices. - Fixed the type of MBperMinute in cVideoDiskUsage::HasChanged() (thanks to Andreas Mair). - Setting the "broken link" or "TEI" flags when cutting recordings is now suppressed if the editing point merges two seamlessly fitting parts of the same stream (thanks to Torsten Lang). - Fixed displaying messages in the LCARS skin. - Fixed checking for a visible live programme in case a menu or the channel display is currently open. - Changed some of the colors in the LCARS skin (you may need to delete the file lcars-default.theme from your themes directory to see these changes). - The new setup option "Miscellaneous/Show channel names with source" can be used to turn on adding the source character to channel names whenever they are displayed (suggested by Ludi Kaleni).
838 lines
23 KiB
C
838 lines
23 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 2.11 2012/06/09 14:37:24 kls Exp $
|
|
*/
|
|
|
|
#include "timers.h"
|
|
#include <ctype.h>
|
|
#include "channels.h"
|
|
#include "device.h"
|
|
#include "i18n.h"
|
|
#include "libsi/si.h"
|
|
#include "recording.h"
|
|
#include "remote.h"
|
|
#include "status.h"
|
|
|
|
#define VFAT_MAX_FILENAME 40 // same as MAX_SUBTITLE_LENGTH in recording.c
|
|
|
|
// 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, cChannel *Channel)
|
|
{
|
|
startTime = stopTime = 0;
|
|
lastSetEvent = 0;
|
|
deferred = 0;
|
|
recording = pending = inVpsMargin = false;
|
|
flags = tfNone;
|
|
if (Instant)
|
|
SetFlags(tfActive | tfInstant);
|
|
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 = 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;
|
|
aux = NULL;
|
|
event = NULL;
|
|
if (Instant && channel)
|
|
snprintf(file, sizeof(file), "%s%s", Setup.MarkInstantRecord ? "@" : "", *Setup.NameInstantRecord ? Setup.NameInstantRecord : channel->Name());
|
|
if (VfatFileSystem && (Utf8StrLen(file) > VFAT_MAX_FILENAME)) {
|
|
dsyslog("timer file name too long for VFAT file system: '%s'", file);
|
|
file[Utf8SymChars(file, VFAT_MAX_FILENAME)] = 0;
|
|
dsyslog("timer file name truncated to '%s'", file);
|
|
}
|
|
}
|
|
|
|
cTimer::cTimer(const cEvent *Event)
|
|
{
|
|
startTime = stopTime = 0;
|
|
lastSetEvent = 0;
|
|
deferred = 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 = 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 = Setup.DefaultPriority;
|
|
lifetime = Setup.DefaultLifetime;
|
|
*file = 0;
|
|
const char *Title = Event->Title();
|
|
if (!isempty(Title))
|
|
Utf8Strn0Cpy(file, Event->Title(), sizeof(file));
|
|
if (VfatFileSystem && (Utf8StrLen(file) > VFAT_MAX_FILENAME)) {
|
|
dsyslog("timer file name too long for VFAT file system: '%s'", file);
|
|
file[Utf8SymChars(file, VFAT_MAX_FILENAME)] = 0;
|
|
dsyslog("timer file name truncated to '%s'", file);
|
|
}
|
|
aux = NULL;
|
|
event = NULL; // let SetEvent() be called to get a log message
|
|
}
|
|
|
|
cTimer::cTimer(const cTimer &Timer)
|
|
{
|
|
channel = NULL;
|
|
aux = NULL;
|
|
event = NULL;
|
|
flags = tfNone;
|
|
*this = Timer;
|
|
}
|
|
|
|
cTimer::~cTimer()
|
|
{
|
|
free(aux);
|
|
}
|
|
|
|
cTimer& cTimer::operator= (const cTimer &Timer)
|
|
{
|
|
if (&Timer != this) {
|
|
uint OldFlags = flags & tfRecording;
|
|
startTime = Timer.startTime;
|
|
stopTime = Timer.stopTime;
|
|
lastSetEvent = 0;
|
|
deferred = 0;
|
|
recording = Timer.recording;
|
|
pending = Timer.pending;
|
|
inVpsMargin = Timer.inVpsMargin;
|
|
flags = Timer.flags | OldFlags;
|
|
channel = Timer.channel;
|
|
day = Timer.day;
|
|
weekdays = Timer.weekdays;
|
|
start = Timer.start;
|
|
stop = Timer.stop;
|
|
priority = Timer.priority;
|
|
lifetime = Timer.lifetime;
|
|
strncpy(file, Timer.file, sizeof(file));
|
|
free(aux);
|
|
aux = Timer.aux ? strdup(Timer.aux) : NULL;
|
|
event = NULL;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
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;
|
|
return r;
|
|
}
|
|
|
|
cString cTimer::ToText(bool UseChannelID) const
|
|
{
|
|
strreplace(file, ':', '|');
|
|
cString buffer = cString::sprintf("%u:%s:%s:%04d:%04d:%d:%d:%s:%s\n", flags, UseChannelID ? *Channel()->GetChannelID().ToString() : *itoa(Channel()->Number()), *PrintDay(day, weekdays, true), start, stop, priority, lifetime, file, aux ? aux : "");
|
|
strreplace(file, '|', ':');
|
|
return buffer;
|
|
}
|
|
|
|
cString cTimer::ToDescr(void) const
|
|
{
|
|
return cString::sprintf("%d (%d %04d-%04d %s'%s')", Index() + 1, Channel()->Number(), start, stop, HasFlags(tfVps) ? "VPS " : "", file);
|
|
}
|
|
|
|
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 %a 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() 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, "%u :%a[^:]:%a[^:]:%d :%d :%d :%d :%a[^:\n]:%a[^\n]", &flags, &channelbuffer, &daybuffer, &start, &stop, &priority, &lifetime, &filebuffer, &aux)) {
|
|
ClrFlags(tfRecording);
|
|
if (aux && !*skipspace(aux)) {
|
|
free(aux);
|
|
aux = NULL;
|
|
}
|
|
//TODO add more plausibility checks
|
|
result = ParseDay(daybuffer, day, weekdays);
|
|
if (VfatFileSystem) {
|
|
char *p = strrchr(filebuffer, FOLDERDELIMCHAR);
|
|
if (p)
|
|
p++;
|
|
else
|
|
p = filebuffer;
|
|
if (Utf8StrLen(p) > VFAT_MAX_FILENAME) {
|
|
dsyslog("timer file name too long for VFAT file system: '%s'", p);
|
|
p[Utf8SymChars(p, VFAT_MAX_FILENAME)] = 0;
|
|
dsyslog("timer file name truncated to '%s'", p);
|
|
}
|
|
}
|
|
Utf8Strn0Cpy(file, filebuffer, sizeof(file));
|
|
strreplace(file, '|', ':');
|
|
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 !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::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 length = TimeToInt(stop) - begin;
|
|
if (length < 0)
|
|
length += SECSINDAY;
|
|
|
|
if (IsSingleEvent()) {
|
|
startTime = SetTime(day, begin);
|
|
stopTime = startTime + length;
|
|
}
|
|
else {
|
|
for (int i = -1; i <= 7; i++) {
|
|
time_t t0 = IncDay(day ? max(day, t) : t, i);
|
|
if (DayMatches(t0)) {
|
|
time_t a = SetTime(t0, begin);
|
|
time_t b = a + length;
|
|
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 (t < deferred)
|
|
return false;
|
|
deferred = 0;
|
|
|
|
if (HasFlags(tfActive)) {
|
|
if (HasFlags(tfVps) && event && 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);
|
|
else
|
|
return startTime <= t && t < stopTime; // ...otherwise we fall back to normal timer handling
|
|
}
|
|
}
|
|
}
|
|
return startTime <= t + Margin && t < stopTime; // must stop *before* stopTime to allow adjacent timers
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#define FULLMATCH 1000
|
|
|
|
int 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 (HasFlags(tfActive) && channel->GetChannelID() == Event->ChannelID()) {
|
|
bool UseVps = HasFlags(tfVps) && Event->Vps();
|
|
Matches(UseVps ? Event->Vps() : Event->StartTime(), true);
|
|
int overlap = 0;
|
|
if (UseVps)
|
|
overlap = (startTime == Event->Vps()) ? FULLMATCH + (Event->IsRunning() ? 200 : 100) : 0;
|
|
if (!overlap) {
|
|
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);
|
|
}
|
|
startTime = stopTime = 0;
|
|
if (Overlap)
|
|
*Overlap = overlap;
|
|
if (UseVps)
|
|
return overlap > FULLMATCH ? tmFull : tmNone;
|
|
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
|
|
{
|
|
return IsSingleEvent() && !Recording() && StopTime() + EXPIRELATENCY <= time(NULL) && (!HasFlags(tfVps) || !event || !event->Vps());
|
|
}
|
|
|
|
time_t cTimer::StartTime(void) const
|
|
{
|
|
if (!startTime)
|
|
Matches();
|
|
return startTime;
|
|
}
|
|
|
|
time_t cTimer::StopTime(void) const
|
|
{
|
|
if (!stopTime)
|
|
Matches();
|
|
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::SetEventFromSchedule(const cSchedules *Schedules)
|
|
{
|
|
cSchedulesLock SchedulesLock;
|
|
if (!Schedules) {
|
|
lastSetEvent = 0; // forces setting the event, even if the schedule hasn't been modified
|
|
if (!(Schedules = cSchedules::Schedules(SchedulesLock)))
|
|
return;
|
|
}
|
|
const cSchedule *Schedule = Schedules->GetSchedule(Channel());
|
|
if (Schedule && Schedule->Events()->First()) {
|
|
time_t now = time(NULL);
|
|
if (!lastSetEvent || Schedule->Modified() >= lastSetEvent) {
|
|
lastSetEvent = now;
|
|
const cEvent *Event = NULL;
|
|
if (HasFlags(tfVps) && Schedule->Events()->First()->Vps()) {
|
|
if (event && Recording())
|
|
return; // let the recording end first
|
|
// 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() && e->RunningStatus() != SI::RunningStatusNotRunning) { // skip outdated events
|
|
int overlap = 0;
|
|
Matches(e, &overlap);
|
|
if (overlap > FULLMATCH) {
|
|
Event = e;
|
|
break; // take the first matching event
|
|
}
|
|
}
|
|
}
|
|
if (!Event && event && (now <= event->EndTime() || Matches(0, true)))
|
|
return; // stay with the old event until the timer has completely expired
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
SetEvent(Event);
|
|
}
|
|
}
|
|
}
|
|
|
|
void cTimer::SetEvent(const cEvent *Event)
|
|
{
|
|
if (event != Event) { //XXX TODO check event data, too???
|
|
if (Event)
|
|
isyslog("timer %s set to event %s", *ToDescr(), *Event->ToDescr());
|
|
else
|
|
isyslog("timer %s set to no event", *ToDescr());
|
|
event = Event;
|
|
}
|
|
}
|
|
|
|
void cTimer::SetRecording(bool Recording)
|
|
{
|
|
recording = 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 = strdup(Aux);
|
|
}
|
|
|
|
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())
|
|
InvFlags(tfActive);
|
|
else if (day) {
|
|
day = 0;
|
|
ClrFlags(tfActive);
|
|
}
|
|
else if (HasFlags(tfActive))
|
|
Skip();
|
|
else
|
|
SetFlags(tfActive);
|
|
SetEvent(NULL);
|
|
Matches(); // refresh start and end time
|
|
}
|
|
|
|
// --- cTimers ---------------------------------------------------------------
|
|
|
|
cTimers Timers;
|
|
|
|
cTimers::cTimers(void)
|
|
{
|
|
state = 0;
|
|
beingEdited = 0;;
|
|
lastSetEvents = 0;
|
|
lastDeleteExpired = 0;
|
|
}
|
|
|
|
cTimer *cTimers::GetTimer(cTimer *Timer)
|
|
{
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (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;
|
|
}
|
|
|
|
cTimer *cTimers::GetMatch(time_t t)
|
|
{
|
|
static int LastPending = -1;
|
|
cTimer *t0 = NULL;
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
if (!ti->Recording() && ti->Matches(t)) {
|
|
if (ti->Pending()) {
|
|
if (ti->Index() > LastPending)
|
|
LastPending = ti->Index();
|
|
else
|
|
continue;
|
|
}
|
|
if (!t0 || ti->Priority() > t0->Priority())
|
|
t0 = ti;
|
|
}
|
|
}
|
|
if (!t0)
|
|
LastPending = -1;
|
|
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)) {
|
|
ti->Matches();
|
|
if ((ti->HasFlags(tfActive)) && (!t0 || ti->StopTime() > time(NULL) && ti->Compare(*t0) < 0))
|
|
t0 = ti;
|
|
}
|
|
return t0;
|
|
}
|
|
|
|
void cTimers::SetModified(void)
|
|
{
|
|
cStatus::MsgTimerChange(NULL, tcMod);
|
|
state++;
|
|
}
|
|
|
|
void cTimers::Add(cTimer *Timer, cTimer *After)
|
|
{
|
|
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);
|
|
}
|
|
|
|
bool cTimers::Modified(int &State)
|
|
{
|
|
bool Result = state != State;
|
|
State = state;
|
|
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)) {
|
|
if (cRemote::HasKeys())
|
|
return; // react immediately on user input
|
|
ti->SetEventFromSchedule(Schedules);
|
|
}
|
|
}
|
|
}
|
|
lastSetEvents = time(NULL);
|
|
}
|
|
|
|
void cTimers::DeleteExpired(void)
|
|
{
|
|
if (time(NULL) - lastDeleteExpired < 30)
|
|
return;
|
|
cTimer *ti = First();
|
|
while (ti) {
|
|
cTimer *next = Next(ti);
|
|
if (ti->Expired()) {
|
|
isyslog("deleting timer %s", *ti->ToDescr());
|
|
Del(ti);
|
|
SetModified();
|
|
}
|
|
ti = next;
|
|
}
|
|
lastDeleteExpired = time(NULL);
|
|
}
|
|
|
|
// --- cSortedTimers ---------------------------------------------------------
|
|
|
|
static int CompareTimers(const void *a, const void *b)
|
|
{
|
|
return (*(const cTimer **)a)->Compare(**(const cTimer **)b);
|
|
}
|
|
|
|
cSortedTimers::cSortedTimers(void)
|
|
:cVector<const cTimer *>(Timers.Count())
|
|
{
|
|
for (const cTimer *Timer = Timers.First(); Timer; Timer = Timers.Next(Timer))
|
|
Append(Timer);
|
|
Sort(CompareTimers);
|
|
}
|