mirror of
https://github.com/vdr-projects/vdr.git
synced 2025-03-01 10:50:46 +00:00
VDR developer version 2.1.3 is now available at ftp://ftp.tvdr.de/vdr/Developer/vdr-2.1.3.tar.bz2 A 'diff' against the previous version is available at ftp://ftp.tvdr.de/vdr/Developer/vdr-2.1.2-2.1.3.diff MD5 checksums: 054f80e0045aa6fad118e9285b52f4f2 vdr-2.1.3.tar.bz2 3c5ab05d5c4d0b984b34e84190e80949 vdr-2.1.2-2.1.3.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. Originally I intended to release this version only after the new DiSEqC configuration dialog was finished. But in the meantime quite a few other things have come up, so I decided to postpone that dialog and first release what has piled up so far. From the HISTORY file: - Changed the return value of cPositioner::HorizonLongitude() to 0 in case the latitude of the antenna location is beyond +/-81 degrees. - Updated the Finnish OSD texts (thanks to Rolf Ahrenberg). - Fixed some compiler warnings with gcc-4.6.3 (thanks to Rolf Ahrenberg). - Changed the name of the SVDRP command RENR to MOVR (suggested by Rolf Ahrenberg). - When cutting a recording it is now checked whether there is already an edited version of this recording (with the same name, but starting with '%'), and the user is prompted for confirmation to overwrite it (suggested by Rolf Ahrenberg). - Revoked "Added maximum signal strength value for TechniSat SkyStar 2 DVB-S rev 2.3P" because it broke things for the "TechniSat AirStar 2" DVB-T card. - The LIRC remote control now connects to the socket even if it doesn't yet exist when VDR is started (thanks to Lars Hanisch). - Changed the absolute latitude limit for visible satellites to 81.2 degrees. - Added code for parsing LCN and AVC descriptors to libsi (thanks to Rolf Ahrenberg). - In the "Select folder" menu pressing Ok now selects the folder, even if this is a folder that contains sub folders (marked with "..."). To open such a folder you can press the Red key. - Fixed a possible access to uninitialized data in cEIT::cEIT() (reported by Dominik Strasser). - The new menu category mcRecordingEdit is now used to mark menus that edit recording properties (suggested by Stefan Braun). - Changes in the teletext PID no longer cause retuning (and thus interrupting a recording). - Removed '_' from the FileNameChars and CharMap translations in uk_UA.po. - Updated the Italian OSD texts (thanks to Diego Pierotto). - Fixed a missing initialization in the c'tor of cSkinLCARSDisplayChannel (thanks to Marko Mäkelä). - Simplified some conditional expressions in skinlcars.c and skinsttng.c (suggested by Marko Mäkelä). - Fixed uninitialized item area coordinates in cSkinLCARSDisplayMenu (reported by Marko Mäkelä). - Fixed a possible crash if the recordings list is updated externally while the Recordings menu is open (reported by Lars Hanisch). - Added a missing closing ')' in the help and man page entry of the --vfat option (reported by Lars Hanisch). - Fixed setting the name of the video directory to avoid a crash when using --genindex, and also to use the correct directory with --edit (the latter reported by Marko Mäkelä). - The Recordings menu can now be called with a cRecordingFilter, which allows the caller to have it display only a certain subset of the recordings (thanks to Lars Hanisch). - Added handling UTF-8 'umlaut' characters to cKbdRemote (thanks to Lars Hanisch). - Made it clear that the Data parameter in cDevice::StillPicture() may point to a series of packets, not just a single one (thanks to Thomas Reufer). - cDevice::TrickSpeed() now has an additional parameter named Forward, which indicates the direction in which replay is being done (suggested by Thomas Reufer). This information may be necessary for some output devices in order to properly implement trick modes. Authors of plugins that implement output devices will need to add this parameter to their derived cDevice class, regardless of whether they will make use of it or not. - Added a note to ePlayMode in device.h that VDR itself always uses pmAudioVideo when replaying a recording (suggested by Thomas Reufer). - Fixed some spellings in positioner.h and Doxyfile (thanks to Ville Skyttä). - Changed '%a' to the POSIX compliant '%m' in all scanf() calls (thanks to Ville Skyttä). - The new function cCamSlot::Decrypt() can be used by derived classes to implement a CAM slot that can be freely assigned to any device, without being directly inserted into the full TS data stream in hardware. A derived class that implements Decrypt() will also need to set the new parameter ReceiveCaPids in the call to the cCamSlot base class constructor to true, in order to receive the CA pid TS packets that contain data necessary for decrypting. - Many member functions of cCamSlot have been made virtual to allow for easier implementation of derived classes. - cTSBuffer now provides the number of available bytes in its Get() function. - cDvbDevice::GetTSPacket() now calls CamSlot()->Decrypt() in order to allow CAM slots that can be freely assigned to any device access to the TS data stream. - Added a check to avoid a possible NULL pointer dereference in cCiSession::SendData() (reported by Ville Skyttä). - Deleted a superfluous assignment in cPipe::Open() (reported by Ville Skyttä). - The script given to VDR with the '-r' option is now also called after the recording process has actually started (thanks to Christian Kaiser). - Avoiding unnecessary pkg-config warnings in plugin Makefiles (thanks to Ville Skyttä). Plugin authors may want to apply the following change to their Makefile: -PKGCFG = $(if $(VDRDIR),$(shell pkg-config --variable=$(1) $(VDRDIR)/vdr.pc),$(shell pkg-config --variable=$(1) vdr || pkg-config --variable=$(1) ../../../vdr.pc)) +PKGCFG = $(if $(VDRDIR),$(shell pkg-config --variable=$(1) $(VDRDIR)/vdr.pc),$(shell PKG_CONFIG_PATH="$$PKG_CONFIG_PATH:../../.." pkg-config --variable=$(1) vdr)) - Eliminated MAXDVBDEVICES (suggested by Oliver Endriss). - Channels that are no longer contained in the current SDT of a transponder are now marked with the keyword OBSOLETE in their name and provider fields. That way you can identify obsolete channels when you switch to them, and you can get the complete overview of all obsolete channels by sorting the Channels list by provider (by pressing the 0 key twice). Automatic deletion of obsolete channels may follow later.
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.1 2013/12/28 11:33:08 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 %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() 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 :%m[^:]:%m[^:]:%d :%d :%d :%d :%m[^:\n]:%m[^\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);
|
|
}
|