mirror of
https://github.com/vdr-projects/vdr.git
synced 2025-03-01 10:50:46 +00:00
VDR developer version 2.1.1 is now available at ftp://ftp.tvdr.de/vdr/Developer/vdr-2.1.1.tar.bz2 A 'diff' against the previous version is available at ftp://ftp.tvdr.de/vdr/Developer/vdr-2.0.0-2.1.1.diff MD5 checksums: b17f9838bb8ddee9620f838fea7a171d vdr-2.1.1.tar.bz2 8b8ca593885c380cd370e6d19a5b16a1 vdr-2.0.0-2.1.1.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 main focus of this version is on adding basic support for positioners to control steerable satellite dishes. Manually controlling the dish position and storing individual positions will follow later. The fixes contained in this version will be released in a stable version 2.0.3 later, if there are no problems. From the HISTORY file: - Fixed initializing cDevice::keepTracks. - Fixed an endless loop in cTextWrapper::Set() in case the given Width is smaller than one character (reported by Stefan Braun). - Removed all "modified since version 1.6" markers from PLUGINS.html. - Added definitions for older DVB API versions, back until 5.0 (based on a patch from Udo Richter). - Changed cThread::SetIOPriority() from "best effort class" to "idle class" in order to improve overall performance when an editing process is running (thanks to Jochen Dolze). - Fixed handling '/' and '~' in recording file names in case DirectoryEncoding is used (thanks to Lars Hanisch). - Changed the sign of the satellite position value in cSource to reflect the standard of western values being negative. The new member function cSource::Position() can be used to retrieve the orbital position of a satellite. - Fixed multiple occurrences of the same directory in the recordings list in case there are directories that only differ in non-alphanumeric characters (was broken by "Fixed selecting the last replayed recording in the Recordings menu in case there are folders and plain recordings with names that differ only in non-alphanumeric characters" in version 1.7.36). - Fixed displaying the frame number when setting an editing mark (thanks to Thomas Günther). - Fixed no longer generating any editing marks if the edited recording results in just one single sequence (reported by Halim Sahin). - Fixed an error message when parsing SCR values in diseqc.conf. - Fixed an unexpected RCS version tag in the newplugin script. - Fixed an endless loop in the DrawEllipse() functions for very small ellipses (reported by Stefan Braun). - Fixed a crash in the LCARS skin's main menu in case there is no current channel (reported by Dominique Dumont). - Added basic support for positioners to control steerable satellite dishes (based on a patch from Seppo Ingalsuo and Ales Jurik). + Supports GotoN (aka "DiSEqC 1.2") and GotoX (aka "USALS"). + The new DiSEqC command code 'P' can be used to instruct a positioner to move the dish to the required satellite position. When a 'P' code is processed, further execution of the remaining DiSEqC sequence (if any) is postponed until the positioner has reached the new satellite position. + The new special source value of "S360E" can be used in diseqc.conf to indicate that an entry using a positioner can move the dish to any requested position within its range. Think of it as "full circle". + The devices a particular cDiseqc or cScr applies to are now stored directly in each cDiseqc or cScr, respectively. + A plugin can implement a custom positioner control (see PLUGINS.html, section "Positioners"). + The new function cSkinDisplayChannel::SetPositioner() can be implemented by skins to show the user a progress display when the dish is being moved. The default implementation calls SetMessage() with a string indicating the new position the dish is being moved to. The LCARS skin shows a progress bar indicating the movement of the dish. + The new parameters "Site latitude", "Site longitude", "Positioner speed", and "Positioner swing" in the "Setup/LNB" menu can be used to configure the necessary values for a steerable dish. + The cDvbTuner now has a new status tsPositioning, in which it waits until a steerable dish has reached its target position. Parsing SI data is paused until the target position has been reached. - The LCARS skin now shows the source value of the current channel in its channel display. - Fixed asserting free disk space in the cutter. - No longer trying to delete old recordings in AssertFreeDiskSpace() if the given Priority is less than 1. - Fixed handling LIRC events in case repeated events are lost. - Fixed a possible crash when shutting down VDR while subtitles are being displayed (reported by Ville Skyttä). - cDevice::IsPrimaryDevice() now also checks whether the primary device actually has a decoder and returns false otherwise. This should improve device allocation on systems that are only used as a receiver and don't actually display anything. - Increased the value of MAXRETRIES to 20 to reduce the probability of disturbances in transfer mode. - All bonded devices (except for the master) now turn off their LNB power completely to avoid problems when receiving vertically polarized transponders (suggested by Manfred Völkel and Oliver Endriss). - Reverted the change from version 1.5.7 that made all logging go to LOG_ERR (thanks to Christopher Reimer). - Added Begin/EndSegmentTransfer() to the EPG handler interface (thanks to Jörg Wendel). - The code for distributing recordings over several video directories is now deprecated and disabled by default. You can re-enable this feature by removing the comment sign ('//') from the beginning of the line //#define DEPRECATED_DISTRIBUTED_VIDEODIR // Code enclosed with this macro is ... in the file videodir.c. Note, though, that this can only be a temporary workaround. This feature will be completely removed in one of the next developer versions. Distributing the video directory over several disks was a useful feature in times when disks were still relatively small, but it also caused serious problems in case one of the disks failed. Nowadays hard disks come in sizes measured in terabytes, and tools like "mhddfs" can be used to combine several disks to form one large volume. A recommended method for a relatively safe disk setup in a VDR system is to use two 1TB (or larger) disks and use them as a RAID-1 (mirrored). That way, if one disk fails, you can replace it without data loss.
848 lines
23 KiB
C
848 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 3.0 2013/03/29 15:37:16 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"
|
|
|
|
// 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;
|
|
*file = 0;
|
|
aux = NULL;
|
|
event = NULL;
|
|
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 = 0;
|
|
if (!Setup.InstantRecordTime && channel && (Instant || Pause)) {
|
|
cSchedulesLock SchedulesLock;
|
|
if (const cSchedules *Schedules = cSchedules::Schedules(SchedulesLock)) {
|
|
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 {
|
|
tstop += Setup.MarginStop * 60;
|
|
tstart -= Setup.MarginStart * 60;
|
|
}
|
|
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());
|
|
}
|
|
|
|
cTimer::cTimer(const cEvent *Event)
|
|
{
|
|
startTime = stopTime = 0;
|
|
lastSetEvent = 0;
|
|
deferred = 0;
|
|
recording = pending = inVpsMargin = false;
|
|
flags = tfActive;
|
|
*file = 0;
|
|
aux = NULL;
|
|
event = NULL;
|
|
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;
|
|
const char *Title = Event->Title();
|
|
if (!isempty(Title))
|
|
Utf8Strn0Cpy(file, Event->Title(), sizeof(file));
|
|
SetEvent(Event);
|
|
}
|
|
|
|
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);
|
|
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...
|
|
if (event->StartTime() > 0) // checks for "phased out" events
|
|
return event->IsRunning(true);
|
|
}
|
|
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
|
|
|
|
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 (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 && event->StartTime() > 0) { // checks for "phased out" events
|
|
if (Recording())
|
|
return; // let the recording end first
|
|
if (now <= event->EndTime() || Matches(0, true))
|
|
return; // stay with the old event until the timer has completely expired
|
|
}
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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();
|
|
return ti;
|
|
}
|
|
else
|
|
continue;
|
|
}
|
|
if (!t0 || ti->Priority() > t0->Priority())
|
|
t0 = ti;
|
|
}
|
|
}
|
|
if (!t0)
|
|
LastPending = -1;
|
|
return t0;
|
|
}
|
|
|
|
cTimer *cTimers::GetMatch(const cEvent *Event, eTimerMatch *Match)
|
|
{
|
|
cTimer *t = NULL;
|
|
eTimerMatch m = tmNone;
|
|
for (cTimer *ti = First(); ti; ti = Next(ti)) {
|
|
eTimerMatch 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);
|
|
}
|