MythTV  master
scheduler.cpp
Go to the documentation of this file.
1 #include <iostream>
2 #include <algorithm>
3 #include <list>
4 #include <chrono> // for milliseconds
5 #include <thread> // for sleep_for
6 
7 using namespace std;
8 
9 #ifdef __linux__
10 # include <sys/vfs.h>
11 #else // if !__linux__
12 # include <sys/param.h>
13 # ifndef _WIN32
14 # include <sys/mount.h>
15 # endif // _WIN32
16 #endif // !__linux__
17 
18 #include <sys/stat.h>
19 #include <sys/time.h>
20 #include <sys/types.h>
21 
22 #include <QStringList>
23 #include <QDateTime>
24 #include <QString>
25 #include <QRegExp>
26 #include <QMutex>
27 #include <QFile>
28 #include <QMap>
29 
30 #include "mythmiscutil.h"
31 #include "mythsystemlegacy.h"
32 #include "scheduler.h"
33 #include "encoderlink.h"
34 #include "mainserver.h"
35 #include "remoteutil.h"
36 #include "mythdate.h"
37 #include "exitcodes.h"
38 #include "mythcontext.h"
39 #include "mythdb.h"
40 #include "compat.h"
41 #include "storagegroup.h"
42 #include "recordinginfo.h"
43 #include "recordingrule.h"
44 #include "scheduledrecording.h"
45 #include "cardutil.h"
46 #include "mythdb.h"
47 #include "mythsystemevent.h"
48 #include "mythlogging.h"
49 #include "tv_rec.h"
50 
51 #define LOC QString("Scheduler: ")
52 #define LOC_WARN QString("Scheduler, Warning: ")
53 #define LOC_ERR QString("Scheduler, Error: ")
54 
55 bool debugConflicts = false;
56 
57 Scheduler::Scheduler(bool runthread, QMap<int, EncoderLink *> *tvList,
58  QString tmptable, Scheduler *master_sched) :
59  MThread("Scheduler"),
60  recordTable(tmptable),
61  priorityTable("powerpriority"),
62  schedLock(),
63  reclist_changed(false),
64  specsched(master_sched),
65  schedulingEnabled(true),
66  m_tvList(tvList),
67  m_expirer(nullptr),
68  doRun(runthread),
69  m_mainServer(nullptr),
70  resetIdleTime(false),
71  m_isShuttingDown(false),
72  error(0),
73  livetvTime(QDateTime()),
74  lastPrepareTime(QDateTime()),
75  m_openEnd(openEndNever)
76 {
77 
78  tmLastLog = 0;
79  char *debug = getenv("DEBUG_CONFLICTS");
80  debugConflicts = (debug != nullptr);
81 
82  if (master_sched)
83  master_sched->GetAllPending(reclist);
84 
85  if (!doRun)
87 
88  if (tmptable == "powerpriority_tmp")
89  {
90  priorityTable = tmptable;
91  recordTable = "record";
92  }
93 
94  VerifyCards();
95 
97 
98  if (doRun)
99  {
101  {
102  QMutexLocker locker(&schedLock);
103  start(QThread::LowPriority);
104  while (doRun && !isRunning())
105  reschedWait.wait(&schedLock);
106  }
107  WakeUpSlaves();
108  }
109 }
110 
112 {
113  QMutexLocker locker(&schedLock);
114  if (doRun)
115  {
116  doRun = false;
117  reschedWait.wakeAll();
118  locker.unlock();
119  wait();
120  locker.relock();
121  }
122 
123  while (!reclist.empty())
124  {
125  delete reclist.back();
126  reclist.pop_back();
127  }
128 
129  while (!worklist.empty())
130  {
131  delete worklist.back();
132  worklist.pop_back();
133  }
134 
135  while (!conflictlists.empty())
136  {
137  delete conflictlists.back();
138  conflictlists.pop_back();
139  }
140 
141  sinputinfomap.clear();
142 
143  locker.unlock();
144  wait();
145 }
146 
147 void Scheduler::Stop(void)
148 {
149  QMutexLocker locker(&schedLock);
150  doRun = false;
151  reschedWait.wakeAll();
152 }
153 
155 {
156  m_mainServer = ms;
157 }
158 
160 {
161  resetIdleTime_lock.lock();
162  resetIdleTime = true;
163  resetIdleTime_lock.unlock();
164 }
165 
167 {
168  MSqlQuery query(MSqlQuery::InitCon());
169  if (!query.exec("SELECT count(*) FROM capturecard") || !query.next())
170  {
171  MythDB::DBError("verifyCards() -- main query 1", query);
172  return false;
173  }
174 
175  uint numcards = query.value(0).toUInt();
176  if (!numcards)
177  {
178  LOG(VB_GENERAL, LOG_ERR, LOC +
179  "No capture cards are defined in the database.\n\t\t\t"
180  "Perhaps you should re-read the installation instructions?");
181  return false;
182  }
183 
184  query.prepare("SELECT sourceid,name FROM videosource ORDER BY sourceid;");
185 
186  if (!query.exec())
187  {
188  MythDB::DBError("verifyCards() -- main query 2", query);
189  return false;
190  }
191 
192  uint numsources = 0;
193  MSqlQuery subquery(MSqlQuery::InitCon());
194  while (query.next())
195  {
196  subquery.prepare(
197  "SELECT cardid "
198  "FROM capturecard "
199  "WHERE sourceid = :SOURCEID "
200  "ORDER BY cardid;");
201  subquery.bindValue(":SOURCEID", query.value(0).toUInt());
202 
203  if (!subquery.exec())
204  {
205  MythDB::DBError("verifyCards() -- sub query", subquery);
206  }
207  else if (!subquery.next())
208  {
209  LOG(VB_GENERAL, LOG_WARNING, LOC +
210  QString("Listings source '%1' is defined, "
211  "but is not attached to a card input.")
212  .arg(query.value(1).toString()));
213  }
214  else
215  {
216  numsources++;
217  }
218  }
219 
220  if (!numsources)
221  {
222  LOG(VB_GENERAL, LOG_ERR, LOC +
223  "No channel sources defined in the database");
224  return false;
225  }
226 
227  return true;
228 }
229 
230 static inline bool Recording(const RecordingInfo *p)
231 {
232  return (p->GetRecordingStatus() == RecStatus::Recording ||
237 }
238 
240 {
241  if (a->GetScheduledStartTime() != b->GetScheduledStartTime())
242  return a->GetScheduledStartTime() < b->GetScheduledStartTime();
243  if (a->GetScheduledEndTime() != b->GetScheduledEndTime())
244  return a->GetScheduledEndTime() < b->GetScheduledEndTime();
245 
246  // Note: the PruneOverlaps logic depends on the following
247  if (a->GetTitle() != b->GetTitle())
248  return a->GetTitle() < b->GetTitle();
249  if (a->GetChanID() != b->GetChanID())
250  return a->GetChanID() < b->GetChanID();
251  if (a->GetInputID() != b->GetInputID())
252  return a->GetInputID() < b->GetInputID();
253 
254  // In cases where two recording rules match the same showing, one
255  // of them needs to take precedence. Penalize any entry that
256  // won't record except for those from kDontRecord rules. This
257  // will force them to yield to a rule that might record.
258  // Otherwise, more specific record type beats less specific.
259  int aprec = RecTypePrecedence(a->GetRecordingRuleType());
262  {
263  aprec += 100;
264  }
265  int bprec = RecTypePrecedence(b->GetRecordingRuleType());
266  if (b->GetRecordingStatus() != RecStatus::Unknown &&
267  b->GetRecordingStatus() != RecStatus::DontRecord)
268  {
269  bprec += 100;
270  }
271  if (aprec != bprec)
272  return aprec < bprec;
273 
274  if (a->GetFindID() != b->GetFindID())
275  return a->GetFindID() > b->GetFindID();
276  return a->GetRecordingRuleID() < b->GetRecordingRuleID();
277 }
278 
280 {
281  if (a->GetScheduledStartTime() != b->GetScheduledStartTime())
282  return a->GetScheduledStartTime() < b->GetScheduledStartTime();
283  if (a->GetScheduledEndTime() != b->GetScheduledEndTime())
284  return a->GetScheduledEndTime() < b->GetScheduledEndTime();
285 
286  // Note: the PruneRedundants logic depends on the following
287  int cmp = a->GetTitle().compare(b->GetTitle(), Qt::CaseInsensitive);
288  if (cmp != 0)
289  return cmp < 0;
290  if (a->GetRecordingRuleID() != b->GetRecordingRuleID())
291  return a->GetRecordingRuleID() < b->GetRecordingRuleID();
292  cmp = a->GetChannelSchedulingID().compare(b->GetChannelSchedulingID(),
293  Qt::CaseInsensitive);
294  if (cmp != 0)
295  return cmp < 0;
296  if (a->GetRecordingStatus() != b->GetRecordingStatus())
297  return a->GetRecordingStatus() < b->GetRecordingStatus();
298  cmp = a->GetChanNum().compare(b->GetChanNum(), Qt::CaseInsensitive);
299  return cmp < 0;
300 }
301 
303 {
304  if (a->GetRecordingStartTime() != b->GetRecordingStartTime())
305  return a->GetRecordingStartTime() < b->GetRecordingStartTime();
306  int cmp = a->GetChannelSchedulingID().compare(b->GetChannelSchedulingID(),
307  Qt::CaseInsensitive);
308  if (cmp != 0)
309  return cmp < 0;
310  if (a->GetRecordingEndTime() != b->GetRecordingEndTime())
311  return a->GetRecordingEndTime() < b->GetRecordingEndTime();
312  if (a->GetRecordingStatus() != b->GetRecordingStatus())
313  return a->GetRecordingStatus() < b->GetRecordingStatus();
314  if (a->GetChanNum() != b->GetChanNum())
315  return a->GetChanNum() < b->GetChanNum();
316  return a->GetChanID() < b->GetChanID();
317 }
318 
320 {
321  int arec = (a->GetRecordingStatus() != RecStatus::Recording &&
325  int brec = (b->GetRecordingStatus() != RecStatus::Recording &&
326  b->GetRecordingStatus() != RecStatus::Tuning &&
327  b->GetRecordingStatus() != RecStatus::Failing &&
328  b->GetRecordingStatus() != RecStatus::Pending);
329 
330  if (arec != brec)
331  return arec < brec;
332 
333  if (a->GetRecordingPriority() != b->GetRecordingPriority())
334  return a->GetRecordingPriority() > b->GetRecordingPriority();
335 
336  if (a->GetRecordingPriority2() != b->GetRecordingPriority2())
337  return a->GetRecordingPriority2() > b->GetRecordingPriority2();
338 
339  int atype = (a->GetRecordingRuleType() == kOverrideRecord ||
341  int btype = (b->GetRecordingRuleType() == kOverrideRecord ||
342  b->GetRecordingRuleType() == kSingleRecord);
343  if (atype != btype)
344  return atype > btype;
345 
346  QDateTime pasttime = MythDate::current().addSecs(-30);
347  int apast = (a->GetRecordingStartTime() < pasttime && !a->IsReactivated());
348  int bpast = (b->GetRecordingStartTime() < pasttime && !b->IsReactivated());
349  if (apast != bpast)
350  return apast < bpast;
351 
352  if (a->GetRecordingStartTime() != b->GetRecordingStartTime())
353  return a->GetRecordingStartTime() < b->GetRecordingStartTime();
354 
355  if (a->GetRecordingRuleID() != b->GetRecordingRuleID())
356  return a->GetRecordingRuleID() < b->GetRecordingRuleID();
357 
358  if (a->GetTitle() != b->GetTitle())
359  return a->GetTitle() < b->GetTitle();
360 
361  if (a->GetProgramID() != b->GetProgramID())
362  return a->GetProgramID() < b->GetProgramID();
363 
364  if (a->GetSubtitle() != b->GetSubtitle())
365  return a->GetSubtitle() < b->GetSubtitle();
366 
367  if (a->GetDescription() != b->GetDescription())
368  return a->GetDescription() < b->GetDescription();
369 
370  if (a->schedorder != b->schedorder)
371  return a->schedorder < b->schedorder;
372 
373  if (a->GetInputID() != b->GetInputID())
374  return a->GetInputID() < b->GetInputID();
375 
376  return a->GetChanID() < b->GetChanID();
377 }
378 
380 {
381  int arec = (a->GetRecordingStatus() != RecStatus::Recording &&
383  int brec = (b->GetRecordingStatus() != RecStatus::Recording &&
384  b->GetRecordingStatus() != RecStatus::Tuning);
385 
386  if (arec != brec)
387  return arec < brec;
388 
389  if (a->GetRecordingPriority() != b->GetRecordingPriority())
390  return a->GetRecordingPriority() > b->GetRecordingPriority();
391 
392  if (a->GetRecordingPriority2() != b->GetRecordingPriority2())
393  return a->GetRecordingPriority2() > b->GetRecordingPriority2();
394 
395  int atype = (a->GetRecordingRuleType() == kOverrideRecord ||
397  int btype = (b->GetRecordingRuleType() == kOverrideRecord ||
398  b->GetRecordingRuleType() == kSingleRecord);
399  if (atype != btype)
400  return atype > btype;
401 
402  QDateTime pasttime = MythDate::current().addSecs(-30);
403  int apast = (a->GetRecordingStartTime() < pasttime && !a->IsReactivated());
404  int bpast = (b->GetRecordingStartTime() < pasttime && !b->IsReactivated());
405  if (apast != bpast)
406  return apast < bpast;
407 
408  if (a->GetRecordingStartTime() != b->GetRecordingStartTime())
409  return a->GetRecordingStartTime() > b->GetRecordingStartTime();
410 
411  if (a->GetRecordingRuleID() != b->GetRecordingRuleID())
412  return a->GetRecordingRuleID() < b->GetRecordingRuleID();
413 
414  if (a->GetTitle() != b->GetTitle())
415  return a->GetTitle() < b->GetTitle();
416 
417  if (a->GetProgramID() != b->GetProgramID())
418  return a->GetProgramID() < b->GetProgramID();
419 
420  if (a->GetSubtitle() != b->GetSubtitle())
421  return a->GetSubtitle() < b->GetSubtitle();
422 
423  if (a->GetDescription() != b->GetDescription())
424  return a->GetDescription() < b->GetDescription();
425 
426  if (a->schedorder != b->schedorder)
427  return a->schedorder > b->schedorder;
428 
429  if (a->GetInputID() != b->GetInputID())
430  return a->GetInputID() > b->GetInputID();
431 
432  return a->GetChanID() > b->GetChanID();
433 }
434 
436 {
437  QReadLocker tvlocker(&TVRec::inputsLock);
438 
440 
441  LOG(VB_SCHEDULE, LOG_INFO, "BuildWorkList...");
442  BuildWorkList();
443 
444  schedLock.unlock();
445 
446  LOG(VB_SCHEDULE, LOG_INFO, "AddNewRecords...");
447  AddNewRecords();
448  LOG(VB_SCHEDULE, LOG_INFO, "AddNotListed...");
449  AddNotListed();
450 
451  LOG(VB_SCHEDULE, LOG_INFO, "Sort by time...");
453  LOG(VB_SCHEDULE, LOG_INFO, "PruneOverlaps...");
454  PruneOverlaps();
455 
456  LOG(VB_SCHEDULE, LOG_INFO, "Sort by priority...");
458  LOG(VB_SCHEDULE, LOG_INFO, "BuildListMaps...");
459  BuildListMaps();
460  LOG(VB_SCHEDULE, LOG_INFO, "SchedNewRecords...");
461  SchedNewRecords();
462  LOG(VB_SCHEDULE, LOG_INFO, "SchedLiveTV...");
463  SchedLiveTV();
464  LOG(VB_SCHEDULE, LOG_INFO, "ClearListMaps...");
465  ClearListMaps();
466 
467  schedLock.lock();
468 
469  LOG(VB_SCHEDULE, LOG_INFO, "Sort by time...");
471  LOG(VB_SCHEDULE, LOG_INFO, "PruneRedundants...");
472  PruneRedundants();
473 
474  LOG(VB_SCHEDULE, LOG_INFO, "Sort by time...");
476  LOG(VB_SCHEDULE, LOG_INFO, "ClearWorkList...");
477  bool res = ClearWorkList();
478 
479  return res;
480 }
481 
487 {
488  struct timeval fillstart, fillend;
489  float matchTime, checkTime, placeTime;
490 
491  MSqlQuery query(dbConn);
492  QString thequery;
493  QString where = "";
494 
495  // This will cause our temp copy of recordmatch to be empty
496  if (recordid == 0)
497  where = "WHERE recordid IS NULL ";
498 
499  thequery = QString("CREATE TEMPORARY TABLE recordmatch ") +
500  "SELECT * FROM recordmatch " + where + "; ";
501 
502  query.prepare(thequery);
503  recordmatchLock.lock();
504  bool ok = query.exec();
505  recordmatchLock.unlock();
506  if (!ok)
507  {
508  MythDB::DBError("FillRecordListFromDB", query);
509  return;
510  }
511 
512  thequery = "ALTER TABLE recordmatch "
513  " ADD UNIQUE INDEX (recordid, chanid, starttime); ";
514  query.prepare(thequery);
515  if (!query.exec())
516  {
517  MythDB::DBError("FillRecordListFromDB", query);
518  return;
519  }
520 
521  thequery = "ALTER TABLE recordmatch "
522  " ADD INDEX (chanid, starttime, manualid); ";
523  query.prepare(thequery);
524  if (!query.exec())
525  {
526  MythDB::DBError("FillRecordListFromDB", query);
527  return;
528  }
529 
530  QMutexLocker locker(&schedLock);
531 
532  gettimeofday(&fillstart, nullptr);
533  UpdateMatches(recordid, 0, 0, QDateTime());
534  gettimeofday(&fillend, nullptr);
535  matchTime = ((fillend.tv_sec - fillstart.tv_sec ) * 1000000 +
536  (fillend.tv_usec - fillstart.tv_usec)) / 1000000.0;
537 
538  LOG(VB_SCHEDULE, LOG_INFO, "CreateTempTables...");
540 
541  gettimeofday(&fillstart, nullptr);
542  LOG(VB_SCHEDULE, LOG_INFO, "UpdateDuplicates...");
544  gettimeofday(&fillend, nullptr);
545  checkTime = ((fillend.tv_sec - fillstart.tv_sec ) * 1000000 +
546  (fillend.tv_usec - fillstart.tv_usec)) / 1000000.0;
547 
548  gettimeofday(&fillstart, nullptr);
549  FillRecordList();
550  gettimeofday(&fillend, nullptr);
551  placeTime = ((fillend.tv_sec - fillstart.tv_sec ) * 1000000 +
552  (fillend.tv_usec - fillstart.tv_usec)) / 1000000.0;
553 
554  LOG(VB_SCHEDULE, LOG_INFO, "DeleteTempTables...");
556 
557  MSqlQuery queryDrop(dbConn);
558  queryDrop.prepare("DROP TABLE recordmatch;");
559  if (!queryDrop.exec())
560  {
561  MythDB::DBError("FillRecordListFromDB", queryDrop);
562  return;
563  }
564 
565  QString msg;
566  msg.sprintf("Speculative scheduled %d items in %.1f "
567  "= %.2f match + %.2f check + %.2f place",
568  (int)reclist.size(),
569  static_cast<double>(matchTime + checkTime + placeTime),
570  static_cast<double>(matchTime),
571  static_cast<double>(checkTime),
572  static_cast<double>(placeTime));
573  LOG(VB_GENERAL, LOG_INFO, msg);
574 }
575 
577 {
578  RecordingList schedList(false);
579  bool dummy;
580  LoadFromScheduler(schedList, dummy);
581 
582  QMutexLocker lockit(&schedLock);
583 
584  RecordingList::iterator it = schedList.begin();
585  for (; it != schedList.end(); ++it)
586  reclist.push_back(*it);
587 }
588 
589 void Scheduler::PrintList(RecList &list, bool onlyFutureRecordings)
590 {
591  if (!VERBOSE_LEVEL_CHECK(VB_SCHEDULE, LOG_DEBUG))
592  return;
593 
594  QDateTime now = MythDate::current();
595 
596  LOG(VB_SCHEDULE, LOG_INFO, "--- print list start ---");
597  LOG(VB_SCHEDULE, LOG_INFO, "Title - Subtitle Ch Station "
598  "Day Start End G I T N Pri");
599 
600  RecIter i = list.begin();
601  for ( ; i != list.end(); ++i)
602  {
603  RecordingInfo *first = (*i);
604 
605  if (onlyFutureRecordings &&
606  ((first->GetRecordingEndTime() < now &&
607  first->GetScheduledEndTime() < now) ||
608  (first->GetRecordingStartTime() < now && !Recording(first))))
609  continue;
610 
611  PrintRec(first);
612  }
613 
614  LOG(VB_SCHEDULE, LOG_INFO, "--- print list end ---");
615 }
616 
617 void Scheduler::PrintRec(const RecordingInfo *p, const QString &prefix)
618 {
619  if (!VERBOSE_LEVEL_CHECK(VB_SCHEDULE, LOG_DEBUG))
620  return;
621 
622  QString outstr = prefix;
623 
624  QString episode = p->toString(ProgramInfo::kTitleSubtitle, " - ", "")
625  .leftJustified(34 - prefix.length(), ' ', true);
626 
627  outstr += QString("%1 %2 %3 %4-%5 %6 %7 ")
628  .arg(episode)
629  .arg(p->GetChanNum().rightJustified(5, ' '))
630  .arg(p->GetChannelSchedulingID().leftJustified(7, ' ', true))
631  .arg(p->GetRecordingStartTime().toLocalTime().toString("dd hh:mm"))
632  .arg(p->GetRecordingEndTime().toLocalTime().toString("hh:mm"))
633  .arg(p->GetShortInputName().rightJustified(2, ' '))
634  .arg(QString::number(p->GetInputID()).rightJustified(2, ' '));
635  outstr += QString("%1 %2 %3")
636  .arg(toQChar(p->GetRecordingRuleType()))
637  .arg(RecStatus::toString(p->GetRecordingStatus(), p->GetInputID()).rightJustified(2, ' '))
638  .arg(p->GetRecordingPriority());
639  if (p->GetRecordingPriority2())
640  outstr += QString("/%1").arg(p->GetRecordingPriority2());
641 
642  LOG(VB_SCHEDULE, LOG_INFO, outstr);
643 }
644 
646 {
647  QMutexLocker lockit(&schedLock);
648 
649  RecIter dreciter = reclist.begin();
650  for (; dreciter != reclist.end(); ++dreciter)
651  {
652  RecordingInfo *p = *dreciter;
653  if (p->IsSameTitleTimeslotAndChannel(*pginfo))
654  {
655  // FIXME! If we are passed an RecStatus::Unknown recstatus, an
656  // in-progress recording might be being stopped. Try
657  // to handle it sensibly until a better fix can be
658  // made after the 0.25 code freeze.
659  if (pginfo->GetRecordingStatus() == RecStatus::Unknown)
660  {
664  else if (p->GetRecordingStatus() == RecStatus::Recording)
666  else
668  }
669 
670  if (p->GetRecordingStatus() != pginfo->GetRecordingStatus())
671  {
672  LOG(VB_GENERAL, LOG_INFO,
673  QString("Updating status for %1 on cardid [%2] (%3 => %4)")
675  .arg(p->GetInputID())
677  p->GetRecordingRuleType()))
679  p->GetRecordingRuleType())));
680  bool resched =
684  pginfo->GetRecordingStatus() != RecStatus::Tuning));
686  reclist_changed = true;
687  p->AddHistory(false);
688  if (resched)
689  {
690  EnqueueCheck(*p, "UpdateRecStatus1");
691  reschedWait.wakeOne();
692  }
693  else
694  {
695  MythEvent me("SCHEDULE_CHANGE");
696  gCoreContext->dispatch(me);
697  }
698  }
699  return;
700  }
701  }
702 }
703 
705  const QDateTime &startts,
706  RecStatus::Type recstatus,
707  const QDateTime &recendts)
708 {
709  QMutexLocker lockit(&schedLock);
710 
711  RecIter dreciter = reclist.begin();
712  for (; dreciter != reclist.end(); ++dreciter)
713  {
714  RecordingInfo *p = *dreciter;
715  if (p->GetInputID() == cardid && p->GetChanID() == chanid &&
716  p->GetScheduledStartTime() == startts)
717  {
718  p->SetRecordingEndTime(recendts);
719 
720  if (p->GetRecordingStatus() != recstatus)
721  {
722  LOG(VB_GENERAL, LOG_INFO,
723  QString("Updating status for %1 on cardid [%2] (%3 => %4)")
725  .arg(p->GetInputID())
727  p->GetRecordingRuleType()))
728  .arg(RecStatus::toString(recstatus,
729  p->GetRecordingRuleType())));
730  bool resched =
733  (recstatus != RecStatus::Recording &&
734  recstatus != RecStatus::Tuning));
735  p->SetRecordingStatus(recstatus);
736  reclist_changed = true;
737  p->AddHistory(false);
738  if (resched)
739  {
740  EnqueueCheck(*p, "UpdateRecStatus2");
741  reschedWait.wakeOne();
742  }
743  else
744  {
745  MythEvent me("SCHEDULE_CHANGE");
746  gCoreContext->dispatch(me);
747  }
748  }
749  return;
750  }
751  }
752 }
753 
755 {
756  QMutexLocker lockit(&schedLock);
757 
758  if (reclist_changed)
759  return false;
760 
761  RecordingType oldrectype = oldp->GetRecordingRuleType();
762  uint oldrecordid = oldp->GetRecordingRuleID();
763  QDateTime oldrecendts = oldp->GetRecordingEndTime();
764 
766  oldp->SetRecordingRuleID(newp->GetRecordingRuleID());
768 
769  if (specsched ||
771  {
772  if (newp->GetRecordingEndTime() < MythDate::current())
773  {
776  return false;
777  }
778  else
779  return true;
780  }
781 
782  EncoderLink *tv = (*m_tvList)[oldp->GetInputID()];
783  RecordingInfo tempold(*oldp);
784  lockit.unlock();
785  RecStatus::Type rs = tv->StartRecording(&tempold);
786  lockit.relock();
787  if (rs != RecStatus::Recording)
788  {
789  LOG(VB_GENERAL, LOG_ERR,
790  QString("Failed to change end time on card %1 to %2")
791  .arg(oldp->GetInputID())
793  oldp->SetRecordingRuleType(oldrectype);
794  oldp->SetRecordingRuleID(oldrecordid);
795  oldp->SetRecordingEndTime(oldrecendts);
796  }
797  else
798  {
799  RecordingInfo *foundp = nullptr;
800  RecIter i = reclist.begin();
801  for (; i != reclist.end(); ++i)
802  {
803  RecordingInfo *recp = *i;
804  if (recp->GetInputID() == oldp->GetInputID() &&
805  recp->IsSameTitleStartTimeAndChannel(*oldp))
806  {
807  *recp = *oldp;
808  foundp = *i;
809  break;
810  }
811  }
812 
813  // If any pending recordings are affected, set them to
814  // future conflicting and force a reschedule by marking
815  // reclist as changed.
816  RecConstIter j = reclist.begin();
817  while (FindNextConflict(reclist, foundp, j, openEndNever, nullptr))
818  {
819  RecordingInfo *recp = *j;
820  if (recp->GetRecordingStatus() == RecStatus::Pending)
821  {
822  QString schedid = recp->MakeUniqueSchedulerKey();
824  recp->AddHistory(false, false, true);
825  reclist_changed = true;
826  }
827  ++j;
828  }
829  }
830 
831  return rs == RecStatus::Recording;
832 }
833 
835 {
836  QMutexLocker lockit(&schedLock);
837  QReadLocker tvlocker(&TVRec::inputsLock);
838 
839  RecordingList::iterator it = slavelist.begin();
840  for (; it != slavelist.end(); ++it)
841  {
842  RecordingInfo *sp = *it;
843  bool found = false;
844 
845  RecIter ri = reclist.begin();
846  for ( ; ri != reclist.end(); ++ri)
847  {
848  RecordingInfo *rp = *ri;
849 
850  if (!sp->GetTitle().isEmpty() &&
852  sp->GetChannelSchedulingID().compare(
853  rp->GetChannelSchedulingID(), Qt::CaseInsensitive) == 0 &&
854  sp->GetTitle().compare(rp->GetTitle(),
855  Qt::CaseInsensitive) == 0)
856  {
857  if (sp->GetInputID() == rp->GetInputID() ||
858  sinputinfomap[sp->GetInputID()].sgroupid ==
859  rp->GetInputID())
860  {
861  found = true;
863  reclist_changed = true;
864  rp->AddHistory(false);
865  LOG(VB_GENERAL, LOG_INFO,
866  QString("setting %1/%2/\"%3\" as %4")
867  .arg(sp->GetInputID())
868  .arg(sp->GetChannelSchedulingID())
869  .arg(sp->GetTitle())
871  }
872  else
873  {
874  LOG(VB_GENERAL, LOG_NOTICE,
875  QString("%1/%2/\"%3\" is already recording on card %4")
876  .arg(sp->GetInputID())
877  .arg(sp->GetChannelSchedulingID())
878  .arg(sp->GetTitle())
879  .arg(rp->GetInputID()));
880  }
881  }
882  else if (sp->GetInputID() == rp->GetInputID() &&
886  {
888  reclist_changed = true;
889  rp->AddHistory(false);
890  LOG(VB_GENERAL, LOG_INFO,
891  QString("setting %1/%2/\"%3\" as aborted")
892  .arg(rp->GetInputID())
893  .arg(rp->GetChannelSchedulingID())
894  .arg(rp->GetTitle()));
895  }
896  }
897 
898  if (sp->GetInputID() && !found)
899  {
900  sp->mplexid = sp->QueryMplexID();
901  sp->sgroupid = sinputinfomap[sp->GetInputID()].sgroupid;
902  reclist.push_back(new RecordingInfo(*sp));
903  reclist_changed = true;
904  sp->AddHistory(false);
905  LOG(VB_GENERAL, LOG_INFO,
906  QString("adding %1/%2/\"%3\" as recording")
907  .arg(sp->GetInputID())
908  .arg(sp->GetChannelSchedulingID())
909  .arg(sp->GetTitle()));
910  }
911  }
912 }
913 
915 {
916  QMutexLocker lockit(&schedLock);
917 
918  RecIter ri = reclist.begin();
919  for ( ; ri != reclist.end(); ++ri)
920  {
921  RecordingInfo *rp = *ri;
922 
923  if (rp->GetInputID() == cardid &&
928  {
930  {
931  QString schedid = rp->MakeUniqueSchedulerKey();
933  rp->AddHistory(false, false, true);
934  }
935  else
936  {
938  rp->AddHistory(false);
939  }
940  reclist_changed = true;
941  LOG(VB_GENERAL, LOG_INFO, QString("setting %1/%2/\"%3\" as aborted")
942  .arg(rp->GetInputID()).arg(rp->GetChannelSchedulingID())
943  .arg(rp->GetTitle()));
944  }
945  }
946 }
947 
949 {
950  RecIter i = reclist.begin();
951  for (; i != reclist.end(); ++i)
952  {
953  RecordingInfo *p = *i;
958  worklist.push_back(new RecordingInfo(*p));
959  }
960 }
961 
963 {
964  RecordingInfo *p;
965 
966  if (reclist_changed)
967  {
968  while (!worklist.empty())
969  {
970  p = worklist.front();
971  delete p;
972  worklist.pop_front();
973  }
974 
975  return false;
976  }
977 
978  while (!reclist.empty())
979  {
980  p = reclist.front();
981  delete p;
982  reclist.pop_front();
983  }
984 
985  while (!worklist.empty())
986  {
987  p = worklist.front();
988  reclist.push_back(p);
989  worklist.pop_front();
990  }
991 
992  return true;
993 }
994 
995 static void erase_nulls(RecList &reclist)
996 {
997  RecIter it = reclist.begin();
998  uint dst = 0;
999  for (it = reclist.begin(); it != reclist.end(); ++it)
1000  {
1001  if (*it)
1002  {
1003  reclist[dst] = *it;
1004  dst++;
1005  }
1006  }
1007  reclist.resize(dst);
1008 }
1009 
1011 {
1012  RecordingInfo *lastp = nullptr;
1013 
1014  RecIter dreciter = worklist.begin();
1015  while (dreciter != worklist.end())
1016  {
1017  RecordingInfo *p = *dreciter;
1018  if (!lastp || lastp->GetRecordingRuleID() == p->GetRecordingRuleID() ||
1019  !lastp->IsSameTitleStartTimeAndChannel(*p))
1020  {
1021  lastp = p;
1022  ++dreciter;
1023  }
1024  else
1025  {
1026  delete p;
1027  *(dreciter++) = nullptr;
1028  }
1029  }
1030 
1032 }
1033 
1035 {
1036  QMap<uint, uint> badinputs;
1037 
1038  RecIter i = worklist.begin();
1039  for ( ; i != worklist.end(); ++i)
1040  {
1041  RecordingInfo *p = *i;
1048  {
1049  RecList *conflictlist =
1050  sinputinfomap[p->GetInputID()].conflictlist;
1051  if (!conflictlist)
1052  {
1053  ++badinputs[p->GetInputID()];
1054  continue;
1055  }
1056  conflictlist->push_back(p);
1057  titlelistmap[p->GetTitle().toLower()].push_back(p);
1058  recordidlistmap[p->GetRecordingRuleID()].push_back(p);
1059  }
1060  }
1061 
1062  QMap<uint, uint>::iterator it;
1063  for (it = badinputs.begin(); it != badinputs.end(); ++it)
1064  {
1065  LOG(VB_GENERAL, LOG_WARNING, LOC_WARN +
1066  QString("Ignored %1 entries for invalid input %2")
1067  .arg(badinputs[it.value()]).arg(it.key()));
1068  }
1069 }
1070 
1072 {
1073  for (uint i = 0; i < conflictlists.size(); ++i)
1074  conflictlists[i]->clear();
1075  titlelistmap.clear();
1076  recordidlistmap.clear();
1077  cache_is_same_program.clear();
1078 }
1079 
1081  const RecordingInfo *a, const RecordingInfo *b) const
1082 {
1083  IsSameKey X(a,b);
1084  IsSameCacheType::const_iterator it = cache_is_same_program.find(X);
1085  if (it != cache_is_same_program.end())
1086  return *it;
1087 
1088  IsSameKey Y(b,a);
1089  it = cache_is_same_program.find(Y);
1090  if (it != cache_is_same_program.end())
1091  return *it;
1092 
1093  return cache_is_same_program[X] = a->IsDuplicateProgram(*b);
1094 }
1095 
1097  const RecList &cardlist,
1098  const RecordingInfo *p,
1099  RecConstIter &j,
1100  OpenEndType openEnd,
1101  uint *paffinity) const
1102 {
1103  uint affinity = 0;
1104  for ( ; j != cardlist.end(); ++j)
1105  {
1106  const RecordingInfo *q = *j;
1107  QString msg;
1108 
1109  if (p == q)
1110  continue;
1111 
1112  if (!Recording(q))
1113  continue;
1114 
1115  if (debugConflicts)
1116  msg = QString("comparing with '%1' ").arg(q->GetTitle());
1117 
1118  if (p->GetInputID() != q->GetInputID())
1119  {
1120  const vector <uint> &conflicting_inputs =
1121  sinputinfomap[p->GetInputID()].conflicting_inputs;
1122  if (find(conflicting_inputs.begin(), conflicting_inputs.end(),
1123  q->GetInputID()) == conflicting_inputs.end())
1124  {
1125  if (debugConflicts)
1126  msg += " cardid== ";
1127  continue;
1128  }
1129  }
1130 
1131  if (p->GetRecordingEndTime() < q->GetRecordingStartTime() ||
1133  {
1134  if (debugConflicts)
1135  msg += " no-overlap ";
1136  continue;
1137  }
1138 
1139  bool mplexid_ok =
1140  (p->sgroupid != q->sgroupid ||
1141  sinputinfomap[p->sgroupid].schedgroup) &&
1142  ((p->mplexid && p->mplexid == q->mplexid) ||
1143  (!p->mplexid && p->GetChanID() == q->GetChanID()));
1144 
1145  if (p->GetRecordingEndTime() == q->GetRecordingStartTime() ||
1147  {
1148  if (openEnd == openEndNever ||
1149  (openEnd == openEndDiffChannel &&
1150  p->GetChanID() == q->GetChanID()) ||
1151  (openEnd == openEndAlways &&
1152  mplexid_ok))
1153  {
1154  if (debugConflicts)
1155  msg += " no-overlap ";
1156  if (mplexid_ok)
1157  ++affinity;
1158  continue;
1159  }
1160  }
1161 
1162  if (debugConflicts)
1163  {
1164  LOG(VB_SCHEDULE, LOG_INFO, msg);
1165  LOG(VB_SCHEDULE, LOG_INFO,
1166  QString(" cardid's: [%1], [%2] Share an input group"
1167  "mplexid's: %3, %4")
1168  .arg(p->GetInputID()).arg(q->GetInputID())
1169  .arg(p->mplexid).arg(q->mplexid));
1170  }
1171 
1172  // if two inputs are in the same input group we have a conflict
1173  // unless the programs are on the same multiplex.
1174  if (mplexid_ok)
1175  {
1176  ++affinity;
1177  continue;
1178  }
1179 
1180  if (debugConflicts)
1181  LOG(VB_SCHEDULE, LOG_INFO, "Found conflict");
1182 
1183  if (paffinity)
1184  *paffinity += affinity;
1185  return true;
1186  }
1187 
1188  if (debugConflicts)
1189  LOG(VB_SCHEDULE, LOG_INFO, "No conflict");
1190 
1191  if (paffinity)
1192  *paffinity += affinity;
1193  return false;
1194 }
1195 
1197  const RecordingInfo *p,
1198  OpenEndType openend,
1199  uint *affinity,
1200  bool checkAll) const
1201 {
1202  RecList &conflictlist = *sinputinfomap[p->GetInputID()].conflictlist;
1203  RecConstIter k = conflictlist.begin();
1204  if (FindNextConflict(conflictlist, p, k, openend, affinity))
1205  {
1206  RecordingInfo *firstConflict = *k;
1207  while (checkAll &&
1208  FindNextConflict(conflictlist, p, ++k, openend, affinity))
1209  ;
1210  return firstConflict;
1211  }
1212 
1213  return nullptr;
1214 }
1215 
1217 {
1218  RecList *showinglist;
1219 
1220  showinglist = &titlelistmap[p->GetTitle().toLower()];
1221  MarkShowingsList(*showinglist, p);
1222 
1223  if (p->GetRecordingRuleType() == kOneRecord ||
1226  {
1227  showinglist = &recordidlistmap[p->GetRecordingRuleID()];
1228  MarkShowingsList(*showinglist, p);
1229  }
1230  else if (p->GetRecordingRuleType() == kOverrideRecord && p->GetFindID())
1231  {
1232  showinglist = &recordidlistmap[p->GetParentRecordingRuleID()];
1233  MarkShowingsList(*showinglist, p);
1234  }
1235 }
1236 
1238 {
1239  RecIter i = showinglist.begin();
1240  for ( ; i != showinglist.end(); ++i)
1241  {
1242  RecordingInfo *q = *i;
1243  if (q == p)
1244  continue;
1245  if (q->GetRecordingStatus() != RecStatus::Unknown &&
1249  continue;
1250  if (q->IsSameTitleStartTimeAndChannel(*p))
1252  else if (q->GetRecordingRuleType() != kSingleRecord &&
1254  IsSameProgram(q,p))
1255  {
1258  else
1260  }
1261  }
1262 }
1263 
1265 {
1266  RecIter i = worklist.begin();
1267  for ( ; i != worklist.end(); ++i)
1268  {
1269  RecordingInfo *p = *i;
1271  }
1272 }
1273 
1275 {
1276  RecIter i = worklist.begin();
1277  for ( ; i != worklist.end(); ++i)
1278  {
1279  RecordingInfo *p = *i;
1281  }
1282 }
1283 
1284 bool Scheduler::TryAnotherShowing(RecordingInfo *p, bool samePriority,
1285  bool livetv)
1286 {
1287  PrintRec(p, " >");
1288 
1293  return false;
1294 
1295  RecList *showinglist = &recordidlistmap[p->GetRecordingRuleID()];
1296 
1297  RecStatus::Type oldstatus = p->GetRecordingStatus();
1299 
1300  RecordingInfo *best = nullptr;
1301  uint bestaffinity = 0;
1302 
1303  RecIter j = showinglist->begin();
1304  for ( ; j != showinglist->end(); ++j)
1305  {
1306  RecordingInfo *q = *j;
1307  if (q == p)
1308  continue;
1309 
1310  if (samePriority &&
1314  {
1315  continue;
1316  }
1317 
1321  {
1322  continue;
1323  }
1324 
1325  if (!p->IsSameTitleStartTimeAndChannel(*q))
1326  {
1327  if (!IsSameProgram(p,q))
1328  continue;
1329  if ((p->GetRecordingRuleType() == kSingleRecord ||
1331  continue;
1332  if (q->GetRecordingStartTime() < schedTime &&
1334  continue;
1335  }
1336 
1337  uint affinity = 0;
1338  const RecordingInfo *conflict = FindConflict(q, openEndNever,
1339  &affinity, false);
1340  if (conflict)
1341  {
1342  PrintRec(q, " #");
1343  PrintRec(conflict, " !");
1344  continue;
1345  }
1346 
1347  if (livetv)
1348  {
1349  // It is pointless to preempt another livetv session.
1350  // (the livetvlist contains dummy livetv pginfo's)
1351  RecConstIter k = livetvlist.begin();
1352  if (FindNextConflict(livetvlist, q, k))
1353  {
1354  PrintRec(q, " #");
1355  PrintRec(*k, " !");
1356  continue;
1357  }
1358  }
1359 
1360  PrintRec(q, QString(" %1:").arg(affinity));
1361  if (!best || affinity > bestaffinity)
1362  {
1363  best = q;
1364  bestaffinity = affinity;
1365  }
1366  }
1367 
1368  if (best)
1369  {
1370  if (livetv)
1371  {
1372  QString msg = QString(
1373  "Moved \"%1\" on chanid: %2 from card: %3 to %4 at %5 "
1374  "to avoid LiveTV conflict")
1375  .arg(p->GetTitle()).arg(p->GetChanID())
1376  .arg(p->GetInputID()).arg(best->GetInputID())
1377  .arg(best->GetScheduledStartTime().toLocalTime().toString());
1378  LOG(VB_GENERAL, LOG_INFO, msg);
1379  }
1380 
1382  MarkOtherShowings(best);
1383  if (best->GetRecordingStartTime() < livetvTime)
1385  PrintRec(p, " -");
1386  PrintRec(best, " +");
1387  return true;
1388  }
1389 
1390  p->SetRecordingStatus(oldstatus);
1391  return false;
1392 }
1393 
1395 {
1396  if (VERBOSE_LEVEL_CHECK(VB_SCHEDULE, LOG_DEBUG))
1397  {
1398  LOG(VB_SCHEDULE, LOG_DEBUG,
1399  "+ = schedule this showing to be recorded");
1400  LOG(VB_SCHEDULE, LOG_DEBUG,
1401  "n: = could schedule this showing with affinity");
1402  LOG(VB_SCHEDULE, LOG_DEBUG,
1403  "n# = could not schedule this showing, with affinity");
1404  LOG(VB_SCHEDULE, LOG_DEBUG,
1405  "! = conflict caused by this showing");
1406  LOG(VB_SCHEDULE, LOG_DEBUG,
1407  "/ = retry this showing, same priority pass");
1408  LOG(VB_SCHEDULE, LOG_DEBUG,
1409  "? = retry this showing, lower priority pass");
1410  LOG(VB_SCHEDULE, LOG_DEBUG,
1411  "> = try another showing for this program");
1412  LOG(VB_SCHEDULE, LOG_DEBUG,
1413  "- = unschedule a showing in favor of another one");
1414  }
1415 
1416  livetvTime = MythDate::current().addSecs(3600);
1417  m_openEnd =
1419 
1420  RecIter i = worklist.begin();
1421 
1422  for ( ; i != worklist.end(); ++i)
1423  {
1424  if ((*i)->GetRecordingStatus() != RecStatus::Recording &&
1425  (*i)->GetRecordingStatus() != RecStatus::Tuning &&
1426  (*i)->GetRecordingStatus() != RecStatus::Pending)
1427  break;
1428  MarkOtherShowings(*i);
1429  }
1430 
1431  while (i != worklist.end())
1432  {
1433  RecIter levelStart = i;
1434  int recpriority = (*i)->GetRecordingPriority();
1435 
1436  while (i != worklist.end())
1437  {
1438  if (i == worklist.end() ||
1439  (*i)->GetRecordingPriority() != recpriority)
1440  break;
1441 
1442  RecIter sublevelStart = i;
1443  int recpriority2 = (*i)->GetRecordingPriority2();
1444  LOG(VB_SCHEDULE, LOG_DEBUG, QString("Trying priority %1/%2...")
1445  .arg(recpriority).arg(recpriority2));
1446  // First pass for anything in this priority sublevel.
1447  SchedNewFirstPass(i, worklist.end(), recpriority, recpriority2);
1448 
1449  LOG(VB_SCHEDULE, LOG_DEBUG, QString("Retrying priority %1/%2...")
1450  .arg(recpriority).arg(recpriority2));
1451  SchedNewRetryPass(sublevelStart, i, true);
1452  }
1453 
1454  // Retry pass for anything in this priority level.
1455  LOG(VB_SCHEDULE, LOG_DEBUG, QString("Retrying priority %1/*...")
1456  .arg(recpriority));
1457  SchedNewRetryPass(levelStart, i, false);
1458  }
1459 }
1460 
1461 // Perform the first pass for scheduling new recordings for programs
1462 // in the same priority sublevel. For each program/starttime, choose
1463 // the first one with the highest affinity that doesn't conflict.
1465  int recpriority, int recpriority2)
1466 {
1467  while (i != end)
1468  {
1469  // Find the next unscheduled program in this sublevel.
1470  for ( ; i != end; ++i)
1471  {
1472  if ((*i)->GetRecordingPriority() != recpriority ||
1473  (*i)->GetRecordingPriority2() != recpriority2 ||
1474  (*i)->GetRecordingStatus() == RecStatus::Unknown)
1475  break;
1476  }
1477 
1478  // Stop if we don't find another program to schedule.
1479  if (i == end ||
1480  (*i)->GetRecordingPriority() != recpriority ||
1481  (*i)->GetRecordingPriority2() != recpriority2)
1482  break;
1483 
1484  RecordingInfo *first = *i;
1485  RecordingInfo *best = nullptr;
1486  uint bestaffinity = 0;
1487 
1488  // Try each showing of this program at this time.
1489  for ( ; i != end; ++i)
1490  {
1491  if ((*i)->GetRecordingPriority() != recpriority ||
1492  (*i)->GetRecordingPriority2() != recpriority2 ||
1493  (*i)->GetRecordingStartTime() !=
1494  first->GetRecordingStartTime() ||
1495  (*i)->GetRecordingRuleID() !=
1496  first->GetRecordingRuleID() ||
1497  (*i)->GetTitle() != first->GetTitle() ||
1498  (*i)->GetProgramID() != first->GetProgramID() ||
1499  (*i)->GetSubtitle() != first->GetSubtitle() ||
1500  (*i)->GetDescription() != first->GetDescription())
1501  break;
1502 
1503  // This shouldn't happen, but skip it just in case.
1504  if ((*i)->GetRecordingStatus() != RecStatus::Unknown)
1505  continue;
1506 
1507  uint affinity = 0;
1508  const RecordingInfo *conflict =
1509  FindConflict(*i, m_openEnd, &affinity, true);
1510  if (conflict)
1511  {
1512  PrintRec(*i, QString(" %1#").arg(affinity));
1513  PrintRec(conflict, " !");
1514  }
1515  else
1516  {
1517  PrintRec(*i, QString(" %1:").arg(affinity));
1518  if (!best || affinity > bestaffinity)
1519  {
1520  best = *i;
1521  bestaffinity = affinity;
1522  }
1523  }
1524  }
1525 
1526  // Schedule the best one.
1527  if (best)
1528  {
1529  PrintRec(best, " +");
1531  MarkOtherShowings(best);
1532  if (best->GetRecordingStartTime() < livetvTime)
1534  }
1535  }
1536 }
1537 
1538 // Perform the retry passes for scheduling new recordings. For each
1539 // unscheduled program, try to move the conflicting programs to
1540 // another time or tuner using the given constraints.
1542  bool samePriority, bool livetv)
1543 {
1544  RecList retry_list;
1545  for ( ; i != end; ++i)
1546  {
1547  if ((*i)->GetRecordingStatus() == RecStatus::Unknown)
1548  retry_list.push_back(*i);
1549  }
1550  SORT_RECLIST(retry_list, comp_retry);
1551 
1552  i = retry_list.begin();
1553  for ( ; i != retry_list.end(); ++i)
1554  {
1555  RecordingInfo *p = *i;
1557  continue;
1558 
1559  if (samePriority)
1560  PrintRec(p, " /");
1561  else
1562  PrintRec(p, " ?");
1563 
1564  // Assume we can successfully move all of the conflicts.
1565  BackupRecStatus();
1567  if (!livetv)
1568  MarkOtherShowings(p);
1569 
1570  // Try to move each conflict. Restore the old status if we
1571  // can't.
1572  RecList &conflictlist = *sinputinfomap[p->GetInputID()].conflictlist;
1573  RecConstIter k = conflictlist.begin();
1574  for ( ; FindNextConflict(conflictlist, p, k); ++k)
1575  {
1576  if (!TryAnotherShowing(*k, samePriority, livetv))
1577  {
1578  RestoreRecStatus();
1579  break;
1580  }
1581  }
1582 
1583  if (!livetv && p->GetRecordingStatus() == RecStatus::WillRecord)
1584  {
1585  if (p->GetRecordingStartTime() < livetvTime)
1587  PrintRec(p, " +");
1588  }
1589  }
1590 }
1591 
1593 {
1594  RecordingInfo *lastp = nullptr;
1595  int lastrecpri2 = 0;
1596 
1597  RecIter i = worklist.begin();
1598  while (i != worklist.end())
1599  {
1600  RecordingInfo *p = *i;
1601 
1602  // Delete anything that has already passed since we can't
1603  // change history, can we?
1608  p->GetScheduledEndTime() < schedTime &&
1610  {
1611  delete p;
1612  *(i++) = nullptr;
1613  continue;
1614  }
1615 
1616  // Check for RecStatus::Conflict
1619 
1620  // Restore the old status for some selected cases.
1628  {
1631  // Re-mark RecStatus::MissedFuture entries so non-future history
1632  // will be saved in the scheduler thread.
1633  if (rs == RecStatus::MissedFuture)
1635  }
1636 
1637  if (!Recording(p))
1638  {
1639  p->SetInputID(0);
1640  p->SetSourceID(0);
1641  p->ClearInputName();
1642  p->sgroupid = 0;
1643  }
1644 
1645  // Check for redundant against last non-deleted
1646  if (!lastp || lastp->GetRecordingRuleID() != p->GetRecordingRuleID() ||
1647  !lastp->IsSameTitleStartTimeAndChannel(*p))
1648  {
1649  lastp = p;
1650  lastrecpri2 = lastp->GetRecordingPriority2();
1651  lastp->SetRecordingPriority2(0);
1652  ++i;
1653  }
1654  else
1655  {
1656  // Flag lower priority showings that will be recorded so
1657  // we can warn the user about them
1658  if (lastp->GetRecordingStatus() == RecStatus::WillRecord &&
1659  p->GetRecordingPriority2() >
1660  lastrecpri2 - lastp->GetRecordingPriority2())
1661  {
1662  lastp->SetRecordingPriority2(
1663  lastrecpri2 - p->GetRecordingPriority2());
1664  }
1665  delete p;
1666  *(i++) = nullptr;
1667  }
1668  }
1669 
1671 }
1672 
1674 {
1675  if (specsched)
1676  return;
1677 
1678  QMap<int, QDateTime> nextRecMap;
1679 
1680  RecIter i = reclist.begin();
1681  while (i != reclist.end())
1682  {
1683  RecordingInfo *p = *i;
1686  nextRecMap[p->GetRecordingRuleID()].isNull())
1687  {
1688  nextRecMap[p->GetRecordingRuleID()] = p->GetRecordingStartTime();
1689  }
1690 
1691  if (p->GetRecordingRuleType() == kOverrideRecord &&
1692  p->GetParentRecordingRuleID() > 0 &&
1695  nextRecMap[p->GetParentRecordingRuleID()].isNull())
1696  {
1697  nextRecMap[p->GetParentRecordingRuleID()] =
1698  p->GetRecordingStartTime();
1699  }
1700  ++i;
1701  }
1702 
1703  MSqlQuery query(dbConn);
1704  query.prepare("SELECT recordid, next_record FROM record;");
1705 
1706  if (query.exec() && query.isActive())
1707  {
1708  MSqlQuery subquery(dbConn);
1709 
1710  while (query.next())
1711  {
1712  int recid = query.value(0).toInt();
1713  QDateTime next_record = MythDate::as_utc(query.value(1).toDateTime());
1714 
1715  if (next_record == nextRecMap[recid])
1716  continue;
1717 
1718  if (nextRecMap[recid].isValid())
1719  {
1720  subquery.prepare("UPDATE record SET next_record = :NEXTREC "
1721  "WHERE recordid = :RECORDID;");
1722  subquery.bindValue(":RECORDID", recid);
1723  subquery.bindValue(":NEXTREC", nextRecMap[recid]);
1724  if (!subquery.exec())
1725  MythDB::DBError("Update next_record", subquery);
1726  }
1727  else if (next_record.isValid())
1728  {
1729  subquery.prepare("UPDATE record "
1730  "SET next_record = NULL "
1731  "WHERE recordid = :RECORDID;");
1732  subquery.bindValue(":RECORDID", recid);
1733  if (!subquery.exec())
1734  MythDB::DBError("Clear next_record", subquery);
1735  }
1736  }
1737  }
1738 }
1739 
1740 void Scheduler::getConflicting(RecordingInfo *pginfo, QStringList &strlist)
1741 {
1742  RecList retlist;
1743  getConflicting(pginfo, &retlist);
1744 
1745  strlist << QString::number(retlist.size());
1746 
1747  while (!retlist.empty())
1748  {
1749  RecordingInfo *p = retlist.front();
1750  p->ToStringList(strlist);
1751  delete p;
1752  retlist.pop_front();
1753  }
1754 }
1755 
1757 {
1758  QMutexLocker lockit(&schedLock);
1759  QReadLocker tvlocker(&TVRec::inputsLock);
1760 
1761  RecConstIter i = reclist.begin();
1762  for (; FindNextConflict(reclist, pginfo, i); ++i)
1763  {
1764  const RecordingInfo *p = *i;
1765  retlist->push_back(new RecordingInfo(*p));
1766  }
1767 }
1768 
1769 bool Scheduler::GetAllPending(RecList &retList, int recRuleId) const
1770 {
1771  QMutexLocker lockit(&schedLock);
1772 
1773  bool hasconflicts = false;
1774 
1775  RecConstIter it = reclist.begin();
1776  for (; it != reclist.end(); ++it)
1777  {
1778  if (recRuleId > 0 &&
1779  (*it)->GetRecordingRuleID() != static_cast<uint>(recRuleId))
1780  continue;
1781  if ((*it)->GetRecordingStatus() == RecStatus::Conflict)
1782  hasconflicts = true;
1783  retList.push_back(new RecordingInfo(**it));
1784  }
1785 
1786  return hasconflicts;
1787 }
1788 
1789 bool Scheduler::GetAllPending(ProgramList &retList, int recRuleId) const
1790 {
1791  QMutexLocker lockit(&schedLock);
1792 
1793  bool hasconflicts = false;
1794 
1795  RecConstIter it = reclist.begin();
1796  for (; it != reclist.end(); ++it)
1797  {
1798  if (recRuleId > 0 &&
1799  (*it)->GetRecordingRuleID() != static_cast<uint>(recRuleId))
1800  continue;
1801 
1802  if ((*it)->GetRecordingStatus() == RecStatus::Conflict)
1803  hasconflicts = true;
1804  retList.push_back(new ProgramInfo(**it));
1805  }
1806 
1807  return hasconflicts;
1808 }
1809 
1810 QMap<QString,ProgramInfo*> Scheduler::GetRecording(void) const
1811 {
1812  QMutexLocker lockit(&schedLock);
1813 
1814  QMap<QString,ProgramInfo*> recMap;
1815  RecConstIter it = reclist.begin();
1816  for (; it != reclist.end(); ++it)
1817  {
1818  if (RecStatus::Recording == (*it)->GetRecordingStatus() ||
1819  RecStatus::Tuning == (*it)->GetRecordingStatus() ||
1820  RecStatus::Failing == (*it)->GetRecordingStatus())
1821  recMap[(*it)->MakeUniqueKey()] = new ProgramInfo(**it);
1822  }
1823 
1824  return recMap;
1825 }
1826 
1828 {
1829  QMutexLocker lockit(&schedLock);
1830 
1831  for (RecConstIter it = reclist.begin(); it != reclist.end(); ++it)
1832  {
1833  if (pginfo.IsSameRecording(**it))
1834  {
1835  return (RecStatus::Recording == (**it).GetRecordingStatus() ||
1836  RecStatus::Tuning == (**it).GetRecordingStatus() ||
1837  RecStatus::Failing == (**it).GetRecordingStatus() ||
1838  RecStatus::Pending == (**it).GetRecordingStatus()) ?
1839  (**it).GetRecordingStatus() : pginfo.GetRecordingStatus();
1840  }
1841  }
1842 
1843  return pginfo.GetRecordingStatus();
1844 }
1845 
1846 void Scheduler::GetAllPending(QStringList &strList) const
1847 {
1848  RecList retlist;
1849  bool hasconflicts = GetAllPending(retlist);
1850 
1851  strList << QString::number(hasconflicts);
1852  strList << QString::number(retlist.size());
1853 
1854  while (!retlist.empty())
1855  {
1856  RecordingInfo *p = retlist.front();
1857  p->ToStringList(strList);
1858  delete p;
1859  retlist.pop_front();
1860  }
1861 }
1862 
1864 void Scheduler::GetAllScheduled(QStringList &strList, SchedSortColumn sortBy,
1865  bool ascending)
1866 {
1867  RecList schedlist;
1868 
1869  GetAllScheduled(schedlist, sortBy, ascending);
1870 
1871  strList << QString::number(schedlist.size());
1872 
1873  while (!schedlist.empty())
1874  {
1875  RecordingInfo *pginfo = schedlist.front();
1876  pginfo->ToStringList(strList);
1877  delete pginfo;
1878  schedlist.pop_front();
1879  }
1880 }
1881 
1882 void Scheduler::Reschedule(const QStringList &request)
1883 {
1884  QMutexLocker locker(&schedLock);
1885  reschedQueue.enqueue(request);
1886  reschedWait.wakeOne();
1887 }
1888 
1890 {
1891  QMutexLocker lockit(&schedLock);
1892 
1893  LOG(VB_GENERAL, LOG_INFO, LOC + QString("AddRecording() recid: %1")
1894  .arg(pi.GetRecordingRuleID()));
1895 
1896  for (RecIter it = reclist.begin(); it != reclist.end(); ++it)
1897  {
1898  RecordingInfo *p = *it;
1901  {
1902  LOG(VB_GENERAL, LOG_INFO, LOC + "Not adding recording, " +
1903  QString("'%1' is already in reclist.")
1904  .arg(pi.GetTitle()));
1905  return;
1906  }
1907  }
1908 
1909  LOG(VB_SCHEDULE, LOG_INFO, LOC +
1910  QString("Adding '%1' to reclist.").arg(pi.GetTitle()));
1911 
1912  RecordingInfo * new_pi = new RecordingInfo(pi);
1913  new_pi->mplexid = new_pi->QueryMplexID();
1914  new_pi->sgroupid = sinputinfomap[new_pi->GetInputID()].sgroupid;
1915  reclist.push_back(new_pi);
1916  reclist_changed = true;
1917 
1918  // Save RecStatus::Recording recstatus to DB
1919  // This allows recordings to resume on backend restart
1920  new_pi->AddHistory(false);
1921 
1922  // Make sure we have a ScheduledRecording instance
1923  new_pi->GetRecordingRule();
1924 
1925  // Trigger reschedule..
1926  EnqueueMatch(pi.GetRecordingRuleID(), 0, 0, QDateTime(),
1927  QString("AddRecording %1").arg(pi.GetTitle()));
1928  reschedWait.wakeOne();
1929 }
1930 
1932 {
1933  if (!m_tvList || !rcinfo)
1934  {
1935  LOG(VB_GENERAL, LOG_ERR, LOC +
1936  "IsBusyRecording() -> true, no tvList or no rcinfo");
1937  return true;
1938  }
1939 
1940  if (!m_tvList->contains(rcinfo->GetInputID()))
1941  return true;
1942 
1943  InputInfo busy_input;
1944 
1945  EncoderLink *rctv1 = (*m_tvList)[rcinfo->GetInputID()];
1946  // first check the input we will be recording on...
1947  bool is_busy = rctv1->IsBusy(&busy_input, -1);
1948  if (is_busy &&
1949  (rcinfo->GetRecordingStatus() == RecStatus::Pending ||
1950  !sinputinfomap[rcinfo->GetInputID()].schedgroup ||
1951  ((!busy_input.mplexid || busy_input.mplexid != rcinfo->mplexid) &&
1952  (busy_input.mplexid || busy_input.chanid != rcinfo->GetChanID()))))
1953  {
1954  return true;
1955  }
1956 
1957  // now check other inputs in the same input group as the recording.
1958  uint inputid = rcinfo->GetInputID();
1959  vector<uint> &inputids = sinputinfomap[inputid].conflicting_inputs;
1960  vector<uint> &group_inputs = sinputinfomap[inputid].group_inputs;
1961  for (uint i = 0; i < inputids.size(); i++)
1962  {
1963  if (!m_tvList->contains(inputids[i]))
1964  {
1965 #if 0
1966  LOG(VB_SCHEDULE, LOG_ERR, LOC +
1967  QString("IsBusyRecording() -> true, rctv(NULL) for input %2")
1968  .arg(inputids[i]));
1969 #endif
1970  return true;
1971  }
1972 
1973  EncoderLink *rctv2 = (*m_tvList)[inputids[i]];
1974  if (rctv2->IsBusy(&busy_input, -1))
1975  {
1976  if ((!busy_input.mplexid ||
1977  busy_input.mplexid != rcinfo->mplexid) &&
1978  (busy_input.mplexid ||
1979  busy_input.chanid != rcinfo->GetChanID()))
1980  {
1981  // This conflicting input is busy on a different
1982  // multiplex than is desired. There is no way the
1983  // main input nor any of its children can be free.
1984  return true;
1985  }
1986  else if (!is_busy)
1987  {
1988  // This conflicting input is busy on the desired
1989  // multiplex and the main input is not busy. Nothing
1990  // else can conflict, so the main input is free.
1991  return false;
1992  }
1993  }
1994  else if (is_busy &&
1995  std::find(group_inputs.begin(), group_inputs.end(),
1996  inputids[i]) != group_inputs.end())
1997  {
1998  // This conflicting input is not busy, is also a child
1999  // input and the main input is busy on the desired
2000  // multiplex. This input is therefore considered free.
2001  return false;
2002  }
2003  }
2004 
2005  return is_busy;
2006 }
2007 
2009 {
2010  MSqlQuery query(dbConn);
2011 
2012  // Mark anything that was recording as aborted.
2013  query.prepare("UPDATE oldrecorded SET recstatus = :RSABORTED "
2014  " WHERE recstatus = :RSRECORDING OR "
2015  " recstatus = :RSTUNING OR "
2016  " recstatus = :RSFAILING");
2017  query.bindValue(":RSABORTED", RecStatus::Aborted);
2018  query.bindValue(":RSRECORDING", RecStatus::Recording);
2019  query.bindValue(":RSTUNING", RecStatus::Tuning);
2020  query.bindValue(":RSFAILING", RecStatus::Failing);
2021  if (!query.exec())
2022  MythDB::DBError("UpdateAborted", query);
2023 
2024  // Mark anything that was going to record as missed.
2025  query.prepare("UPDATE oldrecorded SET recstatus = :RSMISSED "
2026  "WHERE recstatus = :RSWILLRECORD OR "
2027  " recstatus = :RSPENDING");
2028  query.bindValue(":RSMISSED", RecStatus::Missed);
2029  query.bindValue(":RSWILLRECORD", RecStatus::WillRecord);
2030  query.bindValue(":RSPENDING", RecStatus::Pending);
2031  if (!query.exec())
2032  MythDB::DBError("UpdateMissed", query);
2033 
2034  // Mark anything that was set to RecStatus::CurrentRecording as
2035  // RecStatus::PreviousRecording.
2036  query.prepare("UPDATE oldrecorded SET recstatus = :RSPREVIOUS "
2037  "WHERE recstatus = :RSCURRENT");
2038  query.bindValue(":RSPREVIOUS", RecStatus::PreviousRecording);
2039  query.bindValue(":RSCURRENT", RecStatus::CurrentRecording);
2040  if (!query.exec())
2041  MythDB::DBError("UpdateCurrent", query);
2042 
2043  // Clear the "future" status of anything older than the maximum
2044  // endoffset. Anything more recent will bee handled elsewhere
2045  // during normal processing.
2046  query.prepare("UPDATE oldrecorded SET future = 0 "
2047  "WHERE future > 0 AND "
2048  " endtime < (NOW() - INTERVAL 475 MINUTE)");
2049  if (!query.exec())
2050  MythDB::DBError("UpdateFuture", query);
2051 }
2052 
2053 void Scheduler::run(void)
2054 {
2055  RunProlog();
2056 
2058 
2059  // Notify constructor that we're actually running
2060  {
2061  QMutexLocker lockit(&schedLock);
2062  reschedWait.wakeAll();
2063  }
2064 
2066 
2067  // wait for slaves to connect
2068  sleep(3);
2069 
2070  QMutexLocker lockit(&schedLock);
2071 
2073  EnqueueMatch(0, 0, 0, QDateTime(), "SchedulerInit");
2074 
2075  int prerollseconds = 0;
2076  int wakeThreshold = 300;
2077  int idleTimeoutSecs = 0;
2078  int idleWaitForRecordingTime = 15; // in minutes
2079  bool blockShutdown =
2080  gCoreContext->GetBoolSetting("blockSDWUwithoutClient", true);
2081  bool firstRun = true;
2082  QDateTime nextSleepCheck = MythDate::current();
2083  RecIter startIter = reclist.begin();
2084  QDateTime idleSince = QDateTime();
2085  int schedRunTime = 0; // max scheduler run time in seconds
2086  bool statuschanged = false;
2087  QDateTime nextStartTime = MythDate::current().addDays(14);
2088  QDateTime nextWakeTime = nextStartTime;
2089 
2090  while (doRun)
2091  {
2092  // If something changed, it might have short circuited a pass
2093  // through the list or changed the next run times. Start a
2094  // new pass immediately to take care of anything that still
2095  // needs attention right now and reset the run times.
2096  if (reclist_changed)
2097  {
2098  nextStartTime = MythDate::current();
2099  reclist_changed = false;
2100  }
2101 
2102  nextWakeTime = min(nextWakeTime, nextStartTime);
2103  QDateTime curtime = MythDate::current();
2104  int secs_to_next = curtime.secsTo(nextStartTime);
2105  int sched_sleep = max(curtime.msecsTo(nextWakeTime), qint64(0));
2106  if (idleTimeoutSecs > 0)
2107  sched_sleep = min(sched_sleep, 15000);
2108  bool haveRequests = HaveQueuedRequests();
2109  int const kSleepCheck = 300;
2110  bool checkSlaves = curtime >= nextSleepCheck;
2111 
2112  // If we're about to start a recording don't do any reschedules...
2113  // instead sleep for a bit
2114  if ((secs_to_next > -60 && secs_to_next < schedRunTime) ||
2115  (!haveRequests && !checkSlaves))
2116  {
2117  if (sched_sleep)
2118  {
2119  LOG(VB_SCHEDULE, LOG_INFO,
2120  QString("sleeping for %1 ms "
2121  "(s2n: %2 sr: %3 qr: %4 cs: %5)")
2122  .arg(sched_sleep).arg(secs_to_next).arg(schedRunTime)
2123  .arg(haveRequests).arg(checkSlaves));
2124  if (reschedWait.wait(&schedLock, sched_sleep))
2125  continue;
2126  }
2127  }
2128  else
2129  {
2130  if (haveRequests)
2131  {
2132  // The master backend is a long lived program, so
2133  // we reload some key settings on each reschedule.
2134  prerollseconds =
2135  gCoreContext->GetNumSetting("RecordPreRoll", 0);
2136  wakeThreshold =
2137  gCoreContext->GetNumSetting("WakeUpThreshold", 300);
2138  idleTimeoutSecs =
2139  gCoreContext->GetNumSetting("idleTimeoutSecs", 0);
2141  gCoreContext->GetNumSetting("idleWaitForRecordingTime",
2142  15);
2143 
2144  QTime t; t.start();
2145  if (HandleReschedule())
2146  {
2147  statuschanged = true;
2148  startIter = reclist.begin();
2149  }
2150  schedRunTime = max(int(((t.elapsed() + 999) / 1000) * 1.5 + 2),
2151  schedRunTime);
2152  }
2153 
2154  if (firstRun)
2155  {
2156  blockShutdown &= HandleRunSchedulerStartup(
2157  prerollseconds, idleWaitForRecordingTime);
2158  firstRun = false;
2159 
2160  // HandleRunSchedulerStartup releases the schedLock so the
2161  // reclist may have changed. If it has go to top of loop
2162  // and update secs_to_next...
2163  if (reclist_changed)
2164  continue;
2165  }
2166 
2167  if (checkSlaves)
2168  {
2169  // Check for slaves that can be put to sleep.
2171  nextSleepCheck = MythDate::current().addSecs(kSleepCheck);
2172  checkSlaves = false;
2173  }
2174  }
2175 
2176  nextStartTime = MythDate::current().addDays(14);
2177  // If checkSlaves is still set, choose a reasonable wake time
2178  // in the future instead of one that we know is in the past.
2179  if (checkSlaves)
2180  nextWakeTime = MythDate::current().addSecs(kSleepCheck);
2181  else
2182  nextWakeTime = nextSleepCheck;
2183 
2184  // Skip past recordings that are already history
2185  // (i.e. AddHistory() has been called setting oldrecstatus)
2186  for ( ; startIter != reclist.end(); ++startIter)
2187  {
2188  if ((*startIter)->GetRecordingStatus() !=
2189  (*startIter)->oldrecstatus)
2190  {
2191  break;
2192  }
2193  }
2194 
2195  // Start any recordings that are due to be started
2196  // & call RecordPending for recordings due to start in 30 seconds
2197  // & handle RecStatus::Tuning updates
2198  bool done = false;
2199  for (RecIter it = startIter; it != reclist.end() && !done; ++it)
2200  {
2201  done = HandleRecording(
2202  **it, statuschanged, nextStartTime, nextWakeTime,
2203  prerollseconds);
2204  }
2205 
2206  // HandleRecording() temporarily unlocks schedLock. If
2207  // anything changed, reclist iterators could be invalidated so
2208  // start over.
2209  if (reclist_changed)
2210  continue;
2211 
2213  curtime = MythDate::current();
2214  for (RecIter it = startIter; it != reclist.end(); ++it)
2215  {
2216  int secsleft = curtime.secsTo((*it)->GetRecordingStartTime());
2217  if ((secsleft - prerollseconds) <= wakeThreshold)
2218  HandleWakeSlave(**it, prerollseconds);
2219  else
2220  break;
2221  }
2222 
2223  if (statuschanged)
2224  {
2225  MythEvent me("SCHEDULE_CHANGE");
2226  gCoreContext->dispatch(me);
2227 // a scheduler run has nothing to do with the idle shutdown
2228 // idleSince = QDateTime();
2229  }
2230 
2231  // if idletimeout is 0, the user disabled the auto-shutdown feature
2232  if ((idleTimeoutSecs > 0) && (m_mainServer != nullptr))
2233  {
2234  HandleIdleShutdown(blockShutdown, idleSince, prerollseconds,
2236  statuschanged);
2237  if (idleSince.isValid())
2238  {
2239  nextWakeTime = MythDate::current().addSecs(
2240  (idleSince.addSecs(idleTimeoutSecs - 10) <= curtime) ? 1 :
2241  (idleSince.addSecs(idleTimeoutSecs - 30) <= curtime) ? 5 : 10);
2242  }
2243  }
2244 
2245  statuschanged = false;
2246  }
2247 
2248  RunEpilog();
2249 }
2250 
2251 void Scheduler::ResetDuplicates(uint recordid, uint findid,
2252  const QString &title, const QString &subtitle,
2253  const QString &descrip,
2254  const QString &programid)
2255 {
2256  MSqlQuery query(dbConn);
2257  QString filterClause;
2258  MSqlBindings bindings;
2259 
2260  if (!title.isEmpty())
2261  {
2262  filterClause += "AND p.title = :TITLE ";
2263  bindings[":TITLE"] = title;
2264  }
2265 
2266  // "**any**" is special value set in ProgLister::DeleteOldSeries()
2267  if (programid != "**any**")
2268  {
2269  filterClause += "AND (0 ";
2270  if (!subtitle.isEmpty())
2271  {
2272  // Need to check both for kDupCheckSubThenDesc
2273  filterClause += "OR p.subtitle = :SUBTITLE1 "
2274  "OR p.description = :SUBTITLE2 ";
2275  bindings[":SUBTITLE1"] = subtitle;
2276  bindings[":SUBTITLE2"] = subtitle;
2277  }
2278  if (!descrip.isEmpty())
2279  {
2280  // Need to check both for kDupCheckSubThenDesc
2281  filterClause += "OR p.description = :DESCRIP1 "
2282  "OR p.subtitle = :DESCRIP2 ";
2283  bindings[":DESCRIP1"] = descrip;
2284  bindings[":DESCRIP2"] = descrip;
2285  }
2286  if (!programid.isEmpty())
2287  {
2288  filterClause += "OR p.programid = :PROGRAMID ";
2289  bindings[":PROGRAMID"] = programid;
2290  }
2291  filterClause += ") ";
2292  }
2293 
2294  query.prepare(QString("UPDATE recordmatch rm "
2295  "INNER JOIN %1 r "
2296  " ON rm.recordid = r.recordid "
2297  "INNER JOIN program p "
2298  " ON rm.chanid = p.chanid "
2299  " AND rm.starttime = p.starttime "
2300  " AND rm.manualid = p.manualid "
2301  "SET oldrecduplicate = -1 "
2302  "WHERE p.generic = 0 "
2303  " AND r.type NOT IN (%2, %3, %4) ")
2304  .arg(recordTable)
2305  .arg(kSingleRecord)
2306  .arg(kOverrideRecord)
2307  .arg(kDontRecord)
2308  + filterClause);
2309  MSqlBindings::const_iterator it;
2310  for (it = bindings.begin(); it != bindings.end(); ++it)
2311  query.bindValue(it.key(), it.value());
2312  if (!query.exec())
2313  MythDB::DBError("ResetDuplicates1", query);
2314 
2315  if (findid && programid != "**any**")
2316  {
2317  query.prepare("UPDATE recordmatch rm "
2318  "SET oldrecduplicate = -1 "
2319  "WHERE rm.recordid = :RECORDID "
2320  " AND rm.findid = :FINDID");
2321  query.bindValue(":RECORDID", recordid);
2322  query.bindValue(":FINDID", findid);
2323  if (!query.exec())
2324  MythDB::DBError("ResetDuplicates2", query);
2325  }
2326  }
2327 
2329 {
2330  // We might have been inactive for a long time, so make
2331  // sure our DB connection is fresh before continuing.
2333 
2334  struct timeval fillstart, fillend;
2335  float matchTime, checkTime, placeTime;
2336 
2337  gettimeofday(&fillstart, nullptr);
2338  QString msg;
2339  bool deleteFuture = false;
2340  bool runCheck = false;
2341 
2342  while (HaveQueuedRequests())
2343  {
2344  QStringList request = reschedQueue.dequeue();
2345  QStringList tokens;
2346  if (request.size() >= 1)
2347  tokens = request[0].split(' ', QString::SkipEmptyParts);
2348 
2349  if (request.size() < 1 || tokens.size() < 1)
2350  {
2351  LOG(VB_GENERAL, LOG_ERR, "Empty Reschedule request received");
2352  continue;
2353  }
2354 
2355  LOG(VB_GENERAL, LOG_INFO, QString("Reschedule requested for %1")
2356  .arg(request.join(" | ")));
2357 
2358  if (tokens[0] == "MATCH")
2359  {
2360  if (tokens.size() < 5)
2361  {
2362  LOG(VB_GENERAL, LOG_ERR,
2363  QString("Invalid RescheduleMatch request received (%1)")
2364  .arg(request[0]));
2365  continue;
2366  }
2367 
2368  uint recordid = tokens[1].toUInt();
2369  uint sourceid = tokens[2].toUInt();
2370  uint mplexid = tokens[3].toUInt();
2371  QDateTime maxstarttime = MythDate::fromString(tokens[4]);
2372  deleteFuture = true;
2373  runCheck = true;
2374  schedLock.unlock();
2375  recordmatchLock.lock();
2376  UpdateMatches(recordid, sourceid, mplexid, maxstarttime);
2377  recordmatchLock.unlock();
2378  schedLock.lock();
2379  }
2380  else if (tokens[0] == "CHECK")
2381  {
2382  if (tokens.size() < 4 || request.size() < 5)
2383  {
2384  LOG(VB_GENERAL, LOG_ERR,
2385  QString("Invalid RescheduleCheck request received (%1)")
2386  .arg(request[0]));
2387  continue;
2388  }
2389 
2390  uint recordid = tokens[2].toUInt();
2391  uint findid = tokens[3].toUInt();
2392  QString title = request[1];
2393  QString subtitle = request[2];
2394  QString descrip = request[3];
2395  QString programid = request[4];
2396  runCheck = true;
2397  schedLock.unlock();
2398  recordmatchLock.lock();
2399  ResetDuplicates(recordid, findid, title, subtitle, descrip,
2400  programid);
2401  recordmatchLock.unlock();
2402  schedLock.lock();
2403  }
2404  else if (tokens[0] != "PLACE")
2405  {
2406  LOG(VB_GENERAL, LOG_ERR,
2407  QString("Unknown Reschedule request received (%1)")
2408  .arg(request[0]));
2409  }
2410  }
2411 
2412  // Delete future oldrecorded entries that no longer
2413  // match any potential recordings.
2414  if (deleteFuture)
2415  {
2416  MSqlQuery query(dbConn);
2417  query.prepare("DELETE oldrecorded FROM oldrecorded "
2418  "LEFT JOIN recordmatch ON "
2419  " recordmatch.chanid = oldrecorded.chanid AND "
2420  " recordmatch.starttime = oldrecorded.starttime "
2421  "WHERE oldrecorded.future > 0 AND "
2422  " recordmatch.recordid IS NULL");
2423  if (!query.exec())
2424  MythDB::DBError("DeleteFuture", query);
2425  }
2426 
2427  gettimeofday(&fillend, nullptr);
2428  matchTime = ((fillend.tv_sec - fillstart.tv_sec ) * 1000000 +
2429  (fillend.tv_usec - fillstart.tv_usec)) / 1000000.0;
2430 
2431  LOG(VB_SCHEDULE, LOG_INFO, "CreateTempTables...");
2432  CreateTempTables();
2433 
2434  gettimeofday(&fillstart, nullptr);
2435  if (runCheck)
2436  {
2437  LOG(VB_SCHEDULE, LOG_INFO, "UpdateDuplicates...");
2438  UpdateDuplicates();
2439  }
2440  gettimeofday(&fillend, nullptr);
2441  checkTime = ((fillend.tv_sec - fillstart.tv_sec ) * 1000000 +
2442  (fillend.tv_usec - fillstart.tv_usec)) / 1000000.0;
2443 
2444  gettimeofday(&fillstart, nullptr);
2445  bool worklistused = FillRecordList();
2446  gettimeofday(&fillend, nullptr);
2447  placeTime = ((fillend.tv_sec - fillstart.tv_sec ) * 1000000 +
2448  (fillend.tv_usec - fillstart.tv_usec)) / 1000000.0;
2449 
2450  LOG(VB_SCHEDULE, LOG_INFO, "DeleteTempTables...");
2451  DeleteTempTables();
2452 
2453  if (worklistused)
2454  {
2455  UpdateNextRecord();
2456  PrintList();
2457  }
2458  else
2459  {
2460  LOG(VB_GENERAL, LOG_INFO, "Reschedule interrupted, will retry");
2461  EnqueuePlace("Interrupted");
2462  return false;
2463  }
2464 
2465  msg.sprintf("Scheduled %d items in %.1f "
2466  "= %.2f match + %.2f check + %.2f place",
2467  (int)reclist.size(),
2468  static_cast<double>(matchTime + checkTime + placeTime),
2469  static_cast<double>(matchTime),
2470  static_cast<double>(checkTime),
2471  static_cast<double>(placeTime));
2472  LOG(VB_GENERAL, LOG_INFO, msg);
2473 
2474  // Write changed entries to oldrecorded.
2475  RecIter it = reclist.begin();
2476  for ( ; it != reclist.end(); ++it)
2477  {
2478  RecordingInfo *p = *it;
2479  if (p->GetRecordingStatus() != p->oldrecstatus)
2480  {
2481  if (p->GetRecordingEndTime() < schedTime)
2482  p->AddHistory(false, false, false);
2483  else if (p->GetRecordingStartTime() < schedTime &&
2486  p->AddHistory(false, false, false);
2487  else
2488  p->AddHistory(false, false, true);
2489  }
2490  else if (p->future)
2491  {
2492  // Force a non-future, oldrecorded entry to
2493  // get written when the time comes.
2495  }
2496  p->future = false;
2497  }
2498 
2499  gCoreContext->SendSystemEvent("SCHEDULER_RAN");
2500 
2501  return true;
2502 }
2503 
2505  int prerollseconds, int idleWaitForRecordingTime)
2506 {
2507  bool blockShutdown = true;
2508 
2509  // The parameter given to the startup_cmd. "user" means a user
2510  // probably started the backend process, "auto" means it was
2511  // started probably automatically.
2512  QString startupParam = "user";
2513 
2514  // find the first recording that WILL be recorded
2515  RecIter firstRunIter = reclist.begin();
2516  for ( ; firstRunIter != reclist.end(); ++firstRunIter)
2517  {
2518  if ((*firstRunIter)->GetRecordingStatus() == RecStatus::WillRecord ||
2519  (*firstRunIter)->GetRecordingStatus() == RecStatus::Pending)
2520  break;
2521  }
2522 
2523  // have we been started automatically?
2524  QDateTime curtime = MythDate::current();
2525  if (WasStartedAutomatically() ||
2526  ((firstRunIter != reclist.end()) &&
2527  ((curtime.secsTo((*firstRunIter)->GetRecordingStartTime()) -
2528  prerollseconds) < (idleWaitForRecordingTime * 60))))
2529  {
2530  LOG(VB_GENERAL, LOG_INFO, LOC + "AUTO-Startup assumed");
2531  startupParam = "auto";
2532 
2533  // Since we've started automatically, don't wait for
2534  // client to connect before allowing shutdown.
2535  blockShutdown = false;
2536  }
2537  else
2538  {
2539  LOG(VB_GENERAL, LOG_INFO, LOC + "Seem to be woken up by USER");
2540  }
2541 
2542  QString startupCommand = gCoreContext->GetSetting("startupCommand", "");
2543  if (!startupCommand.isEmpty())
2544  {
2545  startupCommand.replace("$status", startupParam);
2546  schedLock.unlock();
2548  schedLock.lock();
2549  }
2550 
2551  return blockShutdown;
2552 }
2553 
2554 // If a recording is about to start on a backend in a few minutes, wake it...
2555 void Scheduler::HandleWakeSlave(RecordingInfo &ri, int prerollseconds)
2556 {
2557  static const int sysEventSecs[5] = { 120, 90, 60, 30, 0 };
2558 
2559  QDateTime curtime = MythDate::current();
2560  QDateTime nextrectime = ri.GetRecordingStartTime();
2561  int secsleft = curtime.secsTo(nextrectime);
2562 
2563  QReadLocker tvlocker(&TVRec::inputsLock);
2564 
2565  QMap<int, EncoderLink*>::iterator tvit = m_tvList->find(ri.GetInputID());
2566  if (tvit == m_tvList->end())
2567  return;
2568 
2569  QString sysEventKey = ri.MakeUniqueKey();
2570 
2571  int i = 0;
2572  bool pendingEventSent = false;
2573  while (sysEventSecs[i] != 0)
2574  {
2575  if ((secsleft <= sysEventSecs[i]) &&
2576  (!sysEvents[i].contains(sysEventKey)))
2577  {
2578  if (!pendingEventSent)
2579  {
2581  QString("REC_PENDING SECS %1").arg(secsleft), &ri);
2582  }
2583 
2584  sysEvents[i].insert(sysEventKey);
2585  pendingEventSent = true;
2586  }
2587  i++;
2588  }
2589 
2590  // cleanup old sysEvents once in a while
2591  QSet<QString> keys;
2592  for (i = 0; sysEventSecs[i] != 0; i++)
2593  {
2594  if (sysEvents[i].size() < 20)
2595  continue;
2596 
2597  if (keys.empty())
2598  {
2599  RecConstIter it = reclist.begin();
2600  for ( ; it != reclist.end(); ++it)
2601  keys.insert((*it)->MakeUniqueKey());
2602  keys.insert("something");
2603  }
2604 
2605  QSet<QString>::iterator sit = sysEvents[i].begin();
2606  while (sit != sysEvents[i].end())
2607  {
2608  if (!keys.contains(*sit))
2609  sit = sysEvents[i].erase(sit);
2610  else
2611  ++sit;
2612  }
2613  }
2614 
2615  EncoderLink *nexttv = *tvit;
2616 
2617  if (nexttv->IsAsleep() && !nexttv->IsWaking())
2618  {
2619  LOG(VB_SCHEDULE, LOG_INFO, LOC +
2620  QString("Slave Backend %1 is being awakened to record: %2")
2621  .arg(nexttv->GetHostName()).arg(ri.GetTitle()));
2622 
2623  if (!WakeUpSlave(nexttv->GetHostName()))
2624  EnqueuePlace("HandleWakeSlave1");
2625  }
2626  else if ((nexttv->IsWaking()) &&
2627  ((secsleft - prerollseconds) < 210) &&
2628  (nexttv->GetSleepStatusTime().secsTo(curtime) < 300) &&
2629  (nexttv->GetLastWakeTime().secsTo(curtime) > 10))
2630  {
2631  LOG(VB_SCHEDULE, LOG_INFO, LOC +
2632  QString("Slave Backend %1 not available yet, "
2633  "trying to wake it up again.")
2634  .arg(nexttv->GetHostName()));
2635 
2636  if (!WakeUpSlave(nexttv->GetHostName(), false))
2637  EnqueuePlace("HandleWakeSlave2");
2638  }
2639  else if ((nexttv->IsWaking()) &&
2640  ((secsleft - prerollseconds) < 150) &&
2641  (nexttv->GetSleepStatusTime().secsTo(curtime) < 300))
2642  {
2643  LOG(VB_GENERAL, LOG_WARNING, LOC +
2644  QString("Slave Backend %1 has NOT come "
2645  "back from sleep yet in 150 seconds. Setting "
2646  "slave status to unknown and attempting "
2647  "to reschedule around its tuners.")
2648  .arg(nexttv->GetHostName()));
2649 
2650  QMap<int, EncoderLink*>::iterator it = m_tvList->begin();
2651  for (; it != m_tvList->end(); ++it)
2652  {
2653  if ((*it)->GetHostName() == nexttv->GetHostName())
2654  (*it)->SetSleepStatus(sStatus_Undefined);
2655  }
2656 
2657  EnqueuePlace("HandleWakeSlave3");
2658  }
2659 }
2660 
2662  RecordingInfo &ri, bool &statuschanged,
2663  QDateTime &nextStartTime, QDateTime &nextWakeTime,
2664  int prerollseconds)
2665 {
2666  if (ri.GetRecordingStatus() == ri.oldrecstatus)
2667  return false;
2668 
2669  QDateTime curtime = MythDate::current();
2670  QDateTime nextrectime = ri.GetRecordingStartTime();
2671  int origprerollseconds = prerollseconds;
2672 
2675  {
2676  // If this recording is sufficiently after nextWakeTime,
2677  // nothing later can shorten nextWakeTime, so stop scanning.
2678  if (nextWakeTime.secsTo(nextrectime) - prerollseconds > 300)
2679  {
2680  nextStartTime = min(nextStartTime, nextrectime);
2681  return true;
2682  }
2683 
2684  if (curtime < nextrectime)
2685  nextWakeTime = min(nextWakeTime, nextrectime);
2686  else
2687  ri.AddHistory(false);
2688  return false;
2689  }
2690 
2691  int secsleft = curtime.secsTo(nextrectime);
2692 
2693  // If we haven't reached this threshold yet, nothing later can
2694  // shorten nextWakeTime, so stop scanning. NOTE: this threshold
2695  // needs to be shorter than the related one in SchedLiveTV().
2696  if (secsleft - prerollseconds > 60)
2697  {
2698  nextStartTime = min(nextStartTime, nextrectime.addSecs(-30));
2699  nextWakeTime = min(nextWakeTime,
2700  nextrectime.addSecs(-prerollseconds - 60));
2701  return true;
2702  }
2703 
2704  QString schedid = ri.MakeUniqueSchedulerKey();
2705 
2707  {
2708  // If we haven't rescheduled in a while, do so now to
2709  // accomodate LiveTV.
2710  if (schedTime.secsTo(curtime) > 30)
2711  EnqueuePlace("PrepareToRecord");
2713  }
2714 
2715  if (secsleft - prerollseconds > 35)
2716  {
2717  nextStartTime = min(nextStartTime, nextrectime.addSecs(-30));
2718  nextWakeTime = min(nextWakeTime,
2719  nextrectime.addSecs(-prerollseconds - 35));
2720  return false;
2721  }
2722 
2723  QReadLocker tvlocker(&TVRec::inputsLock);
2724 
2725  QMap<int, EncoderLink*>::iterator tvit = m_tvList->find(ri.GetInputID());
2726  if (tvit == m_tvList->end())
2727  {
2728  QString msg = QString("Invalid cardid [%1] for %2")
2729  .arg(ri.GetInputID()).arg(ri.GetTitle());
2730  LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2731 
2733  ri.AddHistory(true);
2734  statuschanged = true;
2735  return false;
2736  }
2737 
2738  EncoderLink *nexttv = *tvit;
2739 
2740  if (nexttv->IsTunerLocked())
2741  {
2742  QString msg = QString("SUPPRESSED recording \"%1\" on channel: "
2743  "%2 on cardid: [%3], sourceid %4. Tuner "
2744  "is locked by an external application.")
2745  .arg(ri.GetTitle())
2746  .arg(ri.GetChanID())
2747  .arg(ri.GetInputID())
2748  .arg(ri.GetSourceID());
2749  LOG(VB_GENERAL, LOG_NOTICE, msg);
2750 
2752  ri.AddHistory(true);
2753  statuschanged = true;
2754  return false;
2755  }
2756 
2757  // Use this temporary copy of ri when schedLock is not held. Be
2758  // sure to update it as long as it is still needed whenever ri
2759  // changes.
2760  RecordingInfo tempri(ri);
2761 
2762  // Try to use preroll. If we can't do so right now, try again in
2763  // a little while in case the recorder frees up.
2764  if (prerollseconds > 0)
2765  {
2766  schedLock.unlock();
2767  bool isBusyRecording = IsBusyRecording(&tempri);
2768  schedLock.lock();
2769  if (reclist_changed)
2770  return reclist_changed;
2771 
2772  if (isBusyRecording)
2773  {
2774  if (secsleft > 5)
2775  nextWakeTime = min(nextWakeTime, curtime.addSecs(5));
2776  prerollseconds = 0;
2777  }
2778  }
2779 
2780  if (secsleft - prerollseconds > 30)
2781  {
2782  nextStartTime = min(nextStartTime, nextrectime.addSecs(-30));
2783  nextWakeTime = min(nextWakeTime,
2784  nextrectime.addSecs(-prerollseconds - 30));
2785  return false;
2786  }
2787 
2788  if (nexttv->IsWaking())
2789  {
2790  if (secsleft > 0)
2791  {
2792  LOG(VB_SCHEDULE, LOG_WARNING,
2793  QString("WARNING: Slave Backend %1 has NOT come "
2794  "back from sleep yet. Recording can "
2795  "not begin yet for: %2")
2796  .arg(nexttv->GetHostName())
2797  .arg(ri.GetTitle()));
2798  }
2799  else if (nexttv->GetLastWakeTime().secsTo(curtime) > 300)
2800  {
2801  LOG(VB_SCHEDULE, LOG_WARNING,
2802  QString("WARNING: Slave Backend %1 has NOT come "
2803  "back from sleep yet. Setting slave "
2804  "status to unknown and attempting "
2805  "to reschedule around its tuners.")
2806  .arg(nexttv->GetHostName()));
2807 
2808  QMap<int, EncoderLink *>::Iterator enciter =
2809  m_tvList->begin();
2810  for (; enciter != m_tvList->end(); ++enciter)
2811  {
2812  EncoderLink *enc = *enciter;
2813  if (enc->GetHostName() == nexttv->GetHostName())
2815  }
2816 
2817  EnqueuePlace("SlaveNotAwake");
2818  }
2819 
2820  nextStartTime = min(nextStartTime, nextrectime);
2821  nextWakeTime = min(nextWakeTime, curtime.addSecs(1));
2822  return false;
2823  }
2824 
2825  int fsID = -1;
2826  if (ri.GetPathname().isEmpty())
2827  {
2828  QString recording_dir;
2829  fsID = FillRecordingDir(ri.GetTitle(),
2830  ri.GetHostname(),
2831  ri.GetStorageGroup(),
2832  ri.GetRecordingStartTime(),
2833  ri.GetRecordingEndTime(),
2834  ri.GetInputID(),
2835  recording_dir,
2836  reclist);
2837  ri.SetPathname(recording_dir);
2838  tempri.SetPathname(recording_dir);
2839  }
2840 
2842  {
2843  if (!AssignGroupInput(tempri, origprerollseconds))
2844  {
2845  // We failed to assign an input. Keep asking the main
2846  // server to add one until we get one.
2847  MythEvent me(QString("ADD_CHILD_INPUT %1")
2848  .arg(tempri.GetInputID()));
2849  gCoreContext->dispatch(me);
2850  nextWakeTime = min(nextWakeTime, curtime.addSecs(1));
2851  return reclist_changed;
2852  }
2853  ri.SetInputID(tempri.GetInputID());
2854  nexttv = (*m_tvList)[ri.GetInputID()];
2855 
2858  ri.AddHistory(false, false, true);
2859  schedLock.unlock();
2860  nexttv->RecordPending(&tempri, max(secsleft, 0), false);
2861  schedLock.lock();
2862  if (reclist_changed)
2863  return reclist_changed;
2864  }
2865 
2866  if (secsleft - prerollseconds > 0)
2867  {
2868  nextStartTime = min(nextStartTime, nextrectime);
2869  nextWakeTime = min(nextWakeTime,
2870  nextrectime.addSecs(-prerollseconds));
2871  return false;
2872  }
2873 
2874  QDateTime recstartts = MythDate::current(true).addSecs(30);
2875  recstartts = QDateTime(
2876  recstartts.date(),
2877  QTime(recstartts.time().hour(), recstartts.time().minute()), Qt::UTC);
2878  ri.SetRecordingStartTime(recstartts);
2879  tempri.SetRecordingStartTime(recstartts);
2880 
2881  QString details = QString("%1: channel %2 on cardid [%3], sourceid %4")
2883  .arg(ri.GetChanID())
2884  .arg(ri.GetInputID())
2885  .arg(ri.GetSourceID());
2886 
2887  RecStatus::Type recStatus = RecStatus::Offline;
2888  if (schedulingEnabled && nexttv->IsConnected())
2889  {
2892  {
2893  schedLock.unlock();
2894  recStatus = nexttv->StartRecording(&tempri);
2895  schedLock.lock();
2896  ri.SetRecordingID(tempri.GetRecordingID());
2898 
2899  // activate auto expirer
2900  if (m_expirer && recStatus == RecStatus::Tuning)
2901  m_expirer->Update(ri.GetInputID(), fsID, false);
2902  }
2903  }
2904 
2905  HandleRecordingStatusChange(ri, recStatus, details);
2906  statuschanged = true;
2907 
2908  return reclist_changed;
2909 }
2910 
2912  RecordingInfo &ri, RecStatus::Type recStatus, const QString &details)
2913 {
2914  if (ri.GetRecordingStatus() == recStatus)
2915  return;
2916 
2917  ri.SetRecordingStatus(recStatus);
2918 
2919  bool doSchedAfterStart =
2920  ((recStatus != RecStatus::Tuning &&
2921  recStatus != RecStatus::Recording) ||
2923  (ri.GetParentRecordingRuleID() &&
2925  ri.AddHistory(doSchedAfterStart);
2926 
2927  QString msg = (RecStatus::Recording == recStatus) ?
2928  QString("Started recording") :
2929  ((RecStatus::Tuning == recStatus) ?
2930  QString("Tuning recording") :
2931  QString("Canceled recording (%1)")
2933 
2934  LOG(VB_GENERAL, LOG_INFO, QString("%1: %2").arg(msg).arg(details));
2935 
2936  if ((RecStatus::Recording == recStatus) || (RecStatus::Tuning == recStatus))
2937  {
2938  UpdateNextRecord();
2939  }
2940  else if (RecStatus::Failed == recStatus)
2941  {
2942  MythEvent me(QString("FORCE_DELETE_RECORDING %1 %2")
2943  .arg(ri.GetChanID())
2945  gCoreContext->dispatch(me);
2946  }
2947 }
2948 
2950  int prerollseconds)
2951 {
2952  if (!sinputinfomap[ri.GetInputID()].schedgroup)
2953  return true;
2954 
2955  LOG(VB_SCHEDULE, LOG_DEBUG,
2956  QString("Assigning input for %1/%2/\"%3\"")
2957  .arg(ri.GetInputID())
2958  .arg(ri.GetChannelSchedulingID())
2959  .arg(ri.GetTitle()));
2960 
2961  uint bestid = 0;
2962  uint betterid = 0;
2963  QDateTime now = MythDate::current();
2964 
2965  // Check each child input to find the best one to use.
2966  vector<uint> inputs = sinputinfomap[ri.GetInputID()].group_inputs;
2967  for (uint i = 0; !bestid && i < inputs.size(); ++i)
2968  {
2969  uint inputid = inputs[i];
2970  RecordingInfo *pend = nullptr;
2971  RecordingInfo *rec = nullptr;
2972 
2973  // First, see if anything is already pending or still
2974  // recording.
2975  RecIter j = reclist.begin();
2976  for ( ; j != reclist.end(); ++j)
2977  {
2978  RecordingInfo *p = (*j);
2979  if (now.secsTo(p->GetRecordingStartTime()) >
2980  prerollseconds + 60)
2981  break;
2982  if (p->GetInputID() != inputid)
2983  continue;
2985  {
2986  pend = p;
2987  break;
2988  }
2992  {
2993  rec = p;
2994  }
2995  }
2996 
2997  if (pend)
2998  {
2999  LOG(VB_SCHEDULE, LOG_DEBUG,
3000  QString("Input %1 has a pending recording").arg(inputid));
3001  continue;
3002  }
3003 
3004  if (rec)
3005  {
3006  if (rec->GetRecordingEndTime() >
3007  ri.GetRecordingStartTime())
3008  {
3009  LOG(VB_SCHEDULE, LOG_DEBUG,
3010  QString("Input %1 is recording").arg(inputid));
3011  }
3012  else if (rec->GetRecordingEndTime() <
3013  ri.GetRecordingStartTime())
3014  {
3015  LOG(VB_SCHEDULE, LOG_DEBUG,
3016  QString("Input %1 is recording but will be free")
3017  .arg(inputid));
3018  bestid = inputid;
3019  }
3020  else // rec->end == ri.start
3021  {
3022  if ((ri.mplexid && rec->mplexid != ri.mplexid) ||
3023  (!ri.mplexid && rec->GetChanID() != ri.GetChanID()))
3024  {
3025  LOG(VB_SCHEDULE, LOG_DEBUG,
3026  QString("Input %1 is recording but has to stop")
3027  .arg(inputid));
3028  bestid = inputid;
3029  }
3030  else
3031  {
3032  LOG(VB_SCHEDULE, LOG_DEBUG,
3033  QString("Input %1 is recording but could be free")
3034  .arg(inputid));
3035  if (!betterid)
3036  betterid = inputid;
3037  }
3038  }
3039  continue;
3040  }
3041 
3042  InputInfo busy_info;
3043  EncoderLink *rctv = (*m_tvList)[inputid];
3044  schedLock.unlock();
3045  bool isbusy = rctv->IsBusy(&busy_info, -1);
3046  schedLock.lock();
3047  if (reclist_changed)
3048  return false;
3049  if (!isbusy)
3050  {
3051  LOG(VB_SCHEDULE, LOG_DEBUG,
3052  QString("Input %1 is free").arg(inputid));
3053  bestid = inputid;
3054  }
3055  else if ((ri.mplexid && busy_info.mplexid != ri.mplexid) ||
3056  (!ri.mplexid && busy_info.chanid != ri.GetChanID()))
3057  {
3058  LOG(VB_SCHEDULE, LOG_DEBUG,
3059  QString("Input %1 is on livetv but has to stop")
3060  .arg(inputid));
3061  bestid = inputid;
3062  }
3063  }
3064 
3065  if (!bestid)
3066  bestid = betterid;
3067 
3068  if (bestid)
3069  {
3070  LOG(VB_SCHEDULE, LOG_INFO,
3071  QString("Assigned input %1 for %2/%3/\"%4\"")
3072  .arg(bestid).arg(ri.GetInputID()).arg(ri.GetChannelSchedulingID())
3073  .arg(ri.GetTitle()));
3074  ri.SetInputID(bestid);
3075  }
3076  else
3077  {
3078  LOG(VB_SCHEDULE, LOG_WARNING,
3079  QString("Failed to assign input for %1/%2/\"%3\"")
3080  .arg(ri.GetInputID()).arg(ri.GetChannelSchedulingID())
3081  .arg(ri.GetTitle()));
3082  }
3083 
3084  return bestid;
3085 }
3086 
3088  bool &blockShutdown, QDateTime &idleSince,
3089  int prerollseconds, int idleTimeoutSecs, int idleWaitForRecordingTime,
3090  bool &statuschanged)
3091 {
3092  // To ensure that one idle message is logged per 15 minutes
3093  uint logmask = VB_IDLE;
3094  int tm = QTime::currentTime().msecsSinceStartOfDay() / 900000;
3095  if (tm != tmLastLog)
3096  {
3097  logmask = VB_GENERAL;
3098  tmLastLog = tm;
3099  }
3100 
3101  if ((idleTimeoutSecs <= 0) || (m_mainServer == nullptr))
3102  return;
3103 
3104  // we release the block when a client connects
3105  // Allow the presence of a non-blocking client to release this,
3106  // the frontend may have connected then gone idle between scheduler runs
3107  if (blockShutdown)
3108  {
3110  {
3111  LOG(VB_GENERAL, LOG_NOTICE, "Client is connected, removing startup block on shutdown");
3112  blockShutdown = false;
3113  }
3114  }
3115  else
3116  {
3117  QDateTime curtime = MythDate::current();
3118 
3119  // find out, if we are currently recording (or LiveTV)
3120  bool recording = false;
3121  TVRec::inputsLock.lockForRead();
3122  QMap<int, EncoderLink *>::Iterator it;
3123  for (it = m_tvList->begin(); (it != m_tvList->end()) &&
3124  !recording; ++it)
3125  {
3126  if ((*it)->IsBusy())
3127  recording = true;
3128  }
3129  TVRec::inputsLock.unlock();
3130 
3131  // If there are BLOCKING clients, then we're not idle
3132  bool blocking = m_mainServer->isClientConnected(true);
3133  if (!blocking && !recording)
3134  {
3135  // have we received a RESET_IDLETIME message?
3136  resetIdleTime_lock.lock();
3137  if (resetIdleTime)
3138  {
3139  // yes - so reset the idleSince time
3140  if (idleSince.isValid())
3141  {
3142  MythEvent me(QString("SHUTDOWN_COUNTDOWN -1"));
3143  gCoreContext->dispatch(me);
3144  }
3145  idleSince = QDateTime();
3146  resetIdleTime = false;
3147  }
3148  resetIdleTime_lock.unlock();
3149 
3150  if (statuschanged || !idleSince.isValid())
3151  {
3152  bool wasValid = idleSince.isValid();
3153  if (!wasValid)
3154  idleSince = curtime;
3155 
3156  RecIter idleIter = reclist.begin();
3157  for ( ; idleIter != reclist.end(); ++idleIter)
3158  if ((*idleIter)->GetRecordingStatus() ==
3160  (*idleIter)->GetRecordingStatus() ==
3162  break;
3163 
3164  if (idleIter != reclist.end())
3165  {
3166  if ((curtime.secsTo((*idleIter)->GetRecordingStartTime()) -
3167  prerollseconds) <
3169  {
3170  LOG(logmask, LOG_NOTICE, "Blocking shutdown because "
3171  "a recording is due to "
3172  "start soon.");
3173  idleSince = QDateTime();
3174  }
3175  }
3176 
3177  // If we're due to grab guide data, then block shutdown
3178  if (gCoreContext->GetBoolSetting("MythFillGrabberSuggestsTime") &&
3179  gCoreContext->GetBoolSetting("MythFillEnabled"))
3180  {
3181  QString str = gCoreContext->GetSetting("MythFillSuggestedRunTime");
3182  QDateTime guideRunTime = MythDate::fromString(str);
3183 
3184  if (guideRunTime.isValid() &&
3185  (guideRunTime > MythDate::current()) &&
3186  (curtime.secsTo(guideRunTime) <
3187  (idleWaitForRecordingTime * 60)))
3188  {
3189  LOG(logmask, LOG_NOTICE, "Blocking shutdown because "
3190  "mythfilldatabase is due to "
3191  "run soon.");
3192  idleSince = QDateTime();
3193  }
3194  }
3195 
3196  // Before starting countdown check shutdown is OK
3197  if (idleSince.isValid())
3198  CheckShutdownServer(prerollseconds, idleSince, blockShutdown, logmask);
3199 
3200  if (wasValid && !idleSince.isValid())
3201  {
3202  MythEvent me(QString("SHUTDOWN_COUNTDOWN -1"));
3203  gCoreContext->dispatch(me);
3204  }
3205  }
3206 
3207  if (idleSince.isValid())
3208  {
3209  // is the machine already idling the timeout time?
3210  if (idleSince.addSecs(idleTimeoutSecs) < curtime)
3211  {
3212  // are we waiting for shutdown?
3213  if (m_isShuttingDown)
3214  {
3215  // if we have been waiting more that 60secs then assume
3216  // something went wrong so reset and try again
3217  if (idleSince.addSecs(idleTimeoutSecs + 60) <
3218  curtime)
3219  {
3220  LOG(VB_GENERAL, LOG_WARNING,
3221  "Waited more than 60"
3222  " seconds for shutdown to complete"
3223  " - resetting idle time");
3224  idleSince = QDateTime();
3225  m_isShuttingDown = false;
3226  }
3227  }
3228  else if (CheckShutdownServer(prerollseconds,
3229  idleSince,
3230  blockShutdown, logmask))
3231  {
3232  ShutdownServer(prerollseconds, idleSince);
3233  }
3234  else
3235  {
3236  MythEvent me(QString("SHUTDOWN_COUNTDOWN -1"));
3237  gCoreContext->dispatch(me);
3238  }
3239  }
3240  else
3241  {
3242  int itime = idleSince.secsTo(curtime);
3243  QString msg;
3244  if (itime <= 1)
3245  {
3246  msg = QString("I\'m idle now... shutdown will "
3247  "occur in %1 seconds.")
3248  .arg(idleTimeoutSecs);
3249  LOG(VB_GENERAL, LOG_NOTICE, msg);
3250  MythEvent me(QString("SHUTDOWN_COUNTDOWN %1")
3251  .arg(idleTimeoutSecs));
3252  gCoreContext->dispatch(me);
3253  }
3254  else
3255  {
3256  msg = QString("%1 secs left to system shutdown!")
3257  .arg(idleTimeoutSecs - itime);
3258  LOG(logmask, LOG_NOTICE, msg);
3259  MythEvent me(QString("SHUTDOWN_COUNTDOWN %1")
3260  .arg(idleTimeoutSecs - itime));
3261  gCoreContext->dispatch(me);
3262  }
3263  }
3264  }
3265  }
3266  else
3267  {
3268  if (recording)
3269  LOG(logmask, LOG_NOTICE, "Blocking shutdown because "
3270  "of an active encoder");
3271  if (blocking)
3272  LOG(logmask, LOG_NOTICE, "Blocking shutdown because "
3273  "of a connected client");
3274 
3275  // not idle, make the time invalid
3276  if (idleSince.isValid())
3277  {
3278  MythEvent me(QString("SHUTDOWN_COUNTDOWN -1"));
3279  gCoreContext->dispatch(me);
3280  }
3281  idleSince = QDateTime();
3282  }
3283  }
3284 }
3285 
3286 //returns true, if the shutdown is not blocked
3287 bool Scheduler::CheckShutdownServer(int prerollseconds, QDateTime &idleSince,
3288  bool &blockShutdown, uint logmask)
3289 {
3290  (void)prerollseconds;
3291  bool retval = false;
3292  QString preSDWUCheckCommand = gCoreContext->GetSetting("preSDWUCheckCommand",
3293  "");
3294  if (!preSDWUCheckCommand.isEmpty())
3295  {
3297 
3298  switch(state)
3299  {
3300  case 0:
3301  LOG(logmask, LOG_INFO,
3302  "CheckShutdownServer returned - OK to shutdown");
3303  retval = true;
3304  break;
3305  case 1:
3306  LOG(logmask, LOG_NOTICE,
3307  "CheckShutdownServer returned - Not OK to shutdown");
3308  // just reset idle'ing on retval == 1
3309  idleSince = QDateTime();
3310  break;
3311  case 2:
3312  LOG(logmask, LOG_NOTICE,
3313  "CheckShutdownServer returned - Not OK to shutdown, "
3314  "need reconnect");
3315  // reset shutdown status on retval = 2
3316  // (needs a clientconnection again,
3317  // before shutdown is executed)
3318  blockShutdown =
3319  gCoreContext->GetBoolSetting("blockSDWUwithoutClient",
3320  true);
3321  idleSince = QDateTime();
3322  break;
3323 #if 0
3324  case 3:
3325  //disable shutdown routine generally
3326  m_noAutoShutdown = true;
3327  break;
3328 #endif
3329  case GENERIC_EXIT_NOT_OK:
3330  LOG(VB_GENERAL, LOG_NOTICE,
3331  "CheckShutdownServer returned - Not OK");
3332  break;
3333  default:
3334  LOG(VB_GENERAL, LOG_NOTICE, QString(
3335  "CheckShutdownServer returned - Error %1").arg(state));
3336  break;
3337  }
3338  }
3339  else
3340  retval = true; // allow shutdown if now command is set.
3341 
3342  return retval;
3343 }
3344 
3345 void Scheduler::ShutdownServer(int prerollseconds, QDateTime &idleSince)
3346 {
3347  m_isShuttingDown = true;
3348 
3349  RecIter recIter = reclist.begin();
3350  for ( ; recIter != reclist.end(); ++recIter)
3351  if ((*recIter)->GetRecordingStatus() == RecStatus::WillRecord ||
3352  (*recIter)->GetRecordingStatus() == RecStatus::Pending)
3353  break;
3354 
3355  // set the wakeuptime if needed
3356  QDateTime restarttime;
3357  if (recIter != reclist.end())
3358  {
3359  RecordingInfo *nextRecording = (*recIter);
3360  restarttime = nextRecording->GetRecordingStartTime()
3361  .addSecs((-1) * prerollseconds);
3362  }
3363  // Check if we need to wake up to grab guide data
3364  QString str = gCoreContext->GetSetting("MythFillSuggestedRunTime");
3365  QDateTime guideRefreshTime = MythDate::fromString(str);
3366 
3367  if (gCoreContext->GetBoolSetting("MythFillEnabled")
3368  && gCoreContext->GetBoolSetting("MythFillGrabberSuggestsTime")
3369  && guideRefreshTime.isValid()
3370  && (guideRefreshTime > MythDate::current())
3371  && (restarttime.isNull() || guideRefreshTime < restarttime))
3372  restarttime = guideRefreshTime;
3373 
3374  if (restarttime.isValid())
3375  {
3376  int add = gCoreContext->GetNumSetting("StartupSecsBeforeRecording", 240);
3377  if (add)
3378  restarttime = restarttime.addSecs((-1) * add);
3379 
3380  QString wakeup_timeformat = gCoreContext->GetSetting("WakeupTimeFormat",
3381  "hh:mm yyyy-MM-dd");
3382  QString setwakeup_cmd = gCoreContext->GetSetting("SetWakeuptimeCommand",
3383  "echo \'Wakeuptime would "
3384  "be $time if command "
3385  "set.\'");
3386 
3387  if (setwakeup_cmd.isEmpty())
3388  {
3389  LOG(VB_GENERAL, LOG_NOTICE,
3390  "SetWakeuptimeCommand is empty, shutdown aborted");
3391  idleSince = QDateTime();
3392  m_isShuttingDown = false;
3393  return;
3394  }
3395  if (wakeup_timeformat == "time_t")
3396  {
3397  QString time_ts;
3398  setwakeup_cmd.replace("$time",
3399 #if QT_VERSION < QT_VERSION_CHECK(5,8,0)
3400  time_ts.setNum(restarttime.toTime_t())
3401 #else
3402  time_ts.setNum(restarttime.toSecsSinceEpoch())
3403 #endif
3404  );
3405  }
3406  else
3407  setwakeup_cmd.replace(
3408  "$time", restarttime.toLocalTime().toString(wakeup_timeformat));
3409 
3410  LOG(VB_GENERAL, LOG_NOTICE,
3411  QString("Running the command to set the next "
3412  "scheduled wakeup time :-\n\t\t\t\t") + setwakeup_cmd);
3413 
3414  // now run the command to set the wakeup time
3415  if (myth_system(setwakeup_cmd) != GENERIC_EXIT_OK)
3416  {
3417  LOG(VB_GENERAL, LOG_ERR,
3418  "SetWakeuptimeCommand failed, shutdown aborted");
3419  idleSince = QDateTime();
3420  m_isShuttingDown = false;
3421  return;
3422  }
3423 
3424  gCoreContext->SaveSettingOnHost("MythShutdownWakeupTime",
3426  nullptr);
3427  }
3428 
3429  // tell anyone who is listening the master server is going down now
3430  MythEvent me(QString("SHUTDOWN_NOW"));
3431  gCoreContext->dispatch(me);
3432 
3433  QString halt_cmd = gCoreContext->GetSetting("ServerHaltCommand",
3434  "sudo /sbin/halt -p");
3435 
3436  if (!halt_cmd.isEmpty())
3437  {
3438  // now we shut the slave backends down...
3440 
3441  LOG(VB_GENERAL, LOG_NOTICE,
3442  QString("Running the command to shutdown "
3443  "this computer :-\n\t\t\t\t") + halt_cmd);
3444 
3445  // and now shutdown myself
3446  schedLock.unlock();
3447  uint res = myth_system(halt_cmd);
3448  schedLock.lock();
3449  if (res != GENERIC_EXIT_OK)
3450  LOG(VB_GENERAL, LOG_ERR, "ServerHaltCommand failed, shutdown aborted");
3451  }
3452 
3453  // If we make it here then either the shutdown failed
3454  // OR we suspended or hibernated the OS instead
3455  idleSince = QDateTime();
3456  m_isShuttingDown = false;
3457 }
3458 
3460 {
3461  int prerollseconds = 0;
3462  int secsleft = 0;
3463 
3464  QReadLocker tvlocker(&TVRec::inputsLock);
3465 
3466  bool someSlavesCanSleep = false;
3467  QMap<int, EncoderLink *>::Iterator enciter = m_tvList->begin();
3468  for (; enciter != m_tvList->end(); ++enciter)
3469  {
3470  EncoderLink *enc = *enciter;
3471 
3472  if (enc->CanSleep())
3473  someSlavesCanSleep = true;
3474  }
3475 
3476  if (!someSlavesCanSleep)
3477  return;
3478 
3479  LOG(VB_SCHEDULE, LOG_INFO,
3480  "Scheduler, Checking for slaves that can be shut down");
3481 
3482  int sleepThreshold =
3483  gCoreContext->GetNumSetting( "SleepThreshold", 60 * 45);
3484 
3485  LOG(VB_SCHEDULE, LOG_DEBUG,
3486  QString(" Getting list of slaves that will be active in the "
3487  "next %1 minutes.") .arg(sleepThreshold / 60));
3488 
3489  LOG(VB_SCHEDULE, LOG_DEBUG, "Checking scheduler's reclist");
3490  RecIter recIter = reclist.begin();
3491  QDateTime curtime = MythDate::current();
3492  QStringList SlavesInUse;
3493  for ( ; recIter != reclist.end(); ++recIter)
3494  {
3495  RecordingInfo *pginfo = *recIter;
3496 
3497  if (pginfo->GetRecordingStatus() != RecStatus::Recording &&
3498  pginfo->GetRecordingStatus() != RecStatus::Tuning &&
3499  pginfo->GetRecordingStatus() != RecStatus::Failing &&
3502  continue;
3503 
3504  secsleft = curtime.secsTo(
3505  pginfo->GetRecordingStartTime()) - prerollseconds;
3506  if (secsleft > sleepThreshold)
3507  continue;
3508 
3509  if (m_tvList->find(pginfo->GetInputID()) != m_tvList->end())
3510  {
3511  EncoderLink *enc = (*m_tvList)[pginfo->GetInputID()];
3512  if ((!enc->IsLocal()) &&
3513  (!SlavesInUse.contains(enc->GetHostName())))
3514  {
3515  if (pginfo->GetRecordingStatus() == RecStatus::WillRecord ||
3517  LOG(VB_SCHEDULE, LOG_DEBUG,
3518  QString(" Slave %1 will be in use in %2 minutes")
3519  .arg(enc->GetHostName()) .arg(secsleft / 60));
3520  else
3521  LOG(VB_SCHEDULE, LOG_DEBUG,
3522  QString(" Slave %1 is in use currently "
3523  "recording '%1'")
3524  .arg(enc->GetHostName()).arg(pginfo->GetTitle()));
3525  SlavesInUse << enc->GetHostName();
3526  }
3527  }
3528  }
3529 
3530  LOG(VB_SCHEDULE, LOG_DEBUG, " Checking inuseprograms table:");
3531  QDateTime oneHourAgo = MythDate::current().addSecs(-61 * 60);
3532  MSqlQuery query(MSqlQuery::InitCon());
3533  query.prepare("SELECT DISTINCT hostname, recusage FROM inuseprograms "
3534  "WHERE lastupdatetime > :ONEHOURAGO ;");
3535  query.bindValue(":ONEHOURAGO", oneHourAgo);
3536  if (query.exec())
3537  {
3538  while(query.next()) {
3539  SlavesInUse << query.value(0).toString();
3540  LOG(VB_SCHEDULE, LOG_DEBUG,
3541  QString(" Slave %1 is marked as in use by a %2")
3542  .arg(query.value(0).toString())
3543  .arg(query.value(1).toString()));
3544  }
3545  }
3546 
3547  LOG(VB_SCHEDULE, LOG_DEBUG, QString(" Shutting down slaves which will "
3548  "be inactive for the next %1 minutes and can be put to sleep.")
3549  .arg(sleepThreshold / 60));
3550 
3551  enciter = m_tvList->begin();
3552  for (; enciter != m_tvList->end(); ++enciter)
3553  {
3554  EncoderLink *enc = *enciter;
3555 
3556  if ((!enc->IsLocal()) &&
3557  (enc->IsAwake()) &&
3558  (!SlavesInUse.contains(enc->GetHostName())) &&
3559  (!enc->IsFallingAsleep()))
3560  {
3561  QString sleepCommand =
3562  gCoreContext->GetSettingOnHost("SleepCommand",
3563  enc->GetHostName());
3564  QString wakeUpCommand =
3565  gCoreContext->GetSettingOnHost("WakeUpCommand",
3566  enc->GetHostName());
3567 
3568  if (!sleepCommand.isEmpty() && !wakeUpCommand.isEmpty())
3569  {
3570  QString thisHost = enc->GetHostName();
3571 
3572  LOG(VB_SCHEDULE, LOG_DEBUG,
3573  QString(" Commanding %1 to go to sleep.")
3574  .arg(thisHost));
3575 
3576  if (enc->GoToSleep())
3577  {
3578  QMap<int, EncoderLink *>::Iterator slviter =
3579  m_tvList->begin();
3580  for (; slviter != m_tvList->end(); ++slviter)
3581  {
3582  EncoderLink *slv = *slviter;
3583  if (slv->GetHostName() == thisHost)
3584  {
3585  LOG(VB_SCHEDULE, LOG_DEBUG,
3586  QString(" Marking card %1 on slave %2 "
3587  "as falling asleep.")
3588  .arg(slv->GetInputID())
3589  .arg(slv->GetHostName()));
3591  }
3592  }
3593  }
3594  else
3595  {
3596  LOG(VB_GENERAL, LOG_ERR, LOC +
3597  QString("Unable to shutdown %1 slave backend, setting "
3598  "sleep status to undefined.").arg(thisHost));
3599  QMap<int, EncoderLink *>::Iterator slviter =
3600  m_tvList->begin();
3601  for (; slviter != m_tvList->end(); ++slviter)
3602  {
3603  EncoderLink *slv = *slviter;
3604  if (slv->GetHostName() == thisHost)
3606  }
3607  }
3608  }
3609  }
3610  }
3611 }
3612 
3613 bool Scheduler::WakeUpSlave(QString slaveHostname, bool setWakingStatus)
3614 {
3615  if (slaveHostname == gCoreContext->GetHostName())
3616  {
3617  LOG(VB_GENERAL, LOG_NOTICE,
3618  QString("Tried to Wake Up %1, but this is the "
3619  "master backend and it is not asleep.")
3620  .arg(slaveHostname));
3621  return false;
3622  }
3623 
3624  QString wakeUpCommand = gCoreContext->GetSettingOnHost( "WakeUpCommand",
3625  slaveHostname);
3626 
3627  if (wakeUpCommand.isEmpty()) {
3628  LOG(VB_GENERAL, LOG_NOTICE,
3629  QString("Trying to Wake Up %1, but this slave "
3630  "does not have a WakeUpCommand set.").arg(slaveHostname));
3631 
3632  QMap<int, EncoderLink *>::Iterator enciter = m_tvList->begin();
3633  for (; enciter != m_tvList->end(); ++enciter)
3634  {
3635  EncoderLink *enc = *enciter;
3636  if (enc->GetHostName() == slaveHostname)
3638  }
3639 
3640  return false;
3641  }
3642 
3643  QDateTime curtime = MythDate::current();
3644  QMap<int, EncoderLink *>::Iterator enciter = m_tvList->begin();
3645  for (; enciter != m_tvList->end(); ++enciter)
3646  {
3647  EncoderLink *enc = *enciter;
3648  if (setWakingStatus && (enc->GetHostName() == slaveHostname))
3650  enc->SetLastWakeTime(curtime);
3651  }
3652 
3653  if (!IsMACAddress(wakeUpCommand))
3654  {
3655  LOG(VB_SCHEDULE, LOG_NOTICE, QString("Executing '%1' to wake up slave.")
3656  .arg(wakeUpCommand));
3657  myth_system(wakeUpCommand);
3658  return true;
3659  }
3660 
3661  return WakeOnLAN(wakeUpCommand);
3662 }
3663 
3665 {
3666  QReadLocker tvlocker(&TVRec::inputsLock);
3667 
3668  QStringList SlavesThatCanWake;
3669  QString thisSlave;
3670  QMap<int, EncoderLink *>::Iterator enciter = m_tvList->begin();
3671  for (; enciter != m_tvList->end(); ++enciter)
3672  {
3673  EncoderLink *enc = *enciter;
3674 
3675  if (enc->IsLocal())
3676  continue;
3677 
3678  thisSlave = enc->GetHostName();
3679 
3680  if ((!gCoreContext->GetSettingOnHost("WakeUpCommand", thisSlave)
3681  .isEmpty()) &&
3682  (!SlavesThatCanWake.contains(thisSlave)))
3683  SlavesThatCanWake << thisSlave;
3684  }
3685 
3686  int slave = 0;
3687  for (; slave < SlavesThatCanWake.count(); slave++)
3688  {
3689  thisSlave = SlavesThatCanWake[slave];
3690  LOG(VB_SCHEDULE, LOG_NOTICE,
3691  QString("Scheduler, Sending wakeup command to slave: %1")
3692  .arg(thisSlave));
3693  WakeUpSlave(thisSlave, false);
3694  }
3695 }
3696 
3698 {
3699  MSqlQuery query(dbConn);
3700 
3701  query.prepare(QString("SELECT type,title,station,startdate,starttime, "
3702  " enddate,endtime "
3703  "FROM %1 WHERE recordid = :RECORDID").arg(recordTable));
3704  query.bindValue(":RECORDID", recordid);
3705  if (!query.exec() || query.size() != 1)
3706  {
3707  MythDB::DBError("UpdateManuals", query);
3708  return;
3709  }
3710 
3711  if (!query.next())
3712  return;
3713 
3714  RecordingType rectype = RecordingType(query.value(0).toInt());
3715  QString title = query.value(1).toString();
3716  QString station = query.value(2).toString() ;
3717  QDateTime startdt = QDateTime(query.value(3).toDate(),
3718  query.value(4).toTime(), Qt::UTC);
3719  int duration = startdt.secsTo(
3720  QDateTime(query.value(5).toDate(),
3721  query.value(6).toTime(), Qt::UTC));
3722 
3723  query.prepare("SELECT chanid from channel "
3724  "WHERE callsign = :STATION");
3725  query.bindValue(":STATION", station);
3726  if (!query.exec())
3727  {
3728  MythDB::DBError("UpdateManuals", query);
3729  return;
3730  }
3731 
3732  vector<uint> chanidlist;
3733  while (query.next())
3734  chanidlist.push_back(query.value(0).toUInt());
3735 
3736  int progcount;
3737  int skipdays;
3738  bool weekday;
3739  int daysoff;
3740  QDateTime lstartdt = startdt.toLocalTime();
3741 
3742  switch (rectype)
3743  {
3744  case kSingleRecord:
3745  case kOverrideRecord:
3746  case kDontRecord:
3747  progcount = 1;
3748  skipdays = 1;
3749  weekday = false;
3750  daysoff = 0;
3751  break;
3752  case kDailyRecord:
3753  progcount = 13;
3754  skipdays = 1;
3755  weekday = (lstartdt.date().dayOfWeek() < 6);
3756  daysoff = lstartdt.date().daysTo(
3757  MythDate::current().toLocalTime().date());
3758  startdt = QDateTime(lstartdt.date().addDays(daysoff),
3759  lstartdt.time(), Qt::LocalTime).toUTC();
3760  break;
3761  case kWeeklyRecord:
3762  progcount = 2;
3763  skipdays = 7;
3764  weekday = false;
3765  daysoff = lstartdt.date().daysTo(
3766  MythDate::current().toLocalTime().date());
3767  daysoff = (daysoff + 6) / 7 * 7;
3768  startdt = QDateTime(lstartdt.date().addDays(daysoff),
3769  lstartdt.time(), Qt::LocalTime).toUTC();
3770  break;
3771  default:
3772  LOG(VB_GENERAL, LOG_ERR,
3773  QString("Invalid rectype for manual recordid %1").arg(recordid));
3774  return;
3775  }
3776 
3777  while (progcount--)
3778  {
3779  for (int i = 0; i < (int)chanidlist.size(); i++)
3780  {
3781  if (weekday && startdt.toLocalTime().date().dayOfWeek() >= 6)
3782  continue;
3783 
3784  query.prepare("REPLACE INTO program (chanid, starttime, endtime,"
3785  " title, subtitle, manualid, generic) "
3786  "VALUES (:CHANID, :STARTTIME, :ENDTIME, :TITLE,"
3787  " :SUBTITLE, :RECORDID, 1)");
3788  query.bindValue(":CHANID", chanidlist[i]);
3789  query.bindValue(":STARTTIME", startdt);
3790  query.bindValue(":ENDTIME", startdt.addSecs(duration));
3791  query.bindValue(":TITLE", title);
3792  query.bindValue(":SUBTITLE", startdt.toLocalTime());
3793  query.bindValue(":RECORDID", recordid);
3794  if (!query.exec())
3795  {
3796  MythDB::DBError("UpdateManuals", query);
3797  return;
3798  }
3799  }
3800 
3801  daysoff += skipdays;
3802  startdt = QDateTime(lstartdt.date().addDays(daysoff),
3803  lstartdt.time(), Qt::LocalTime).toUTC();
3804  }
3805 }
3806 
3807 void Scheduler::BuildNewRecordsQueries(uint recordid, QStringList &from,
3808  QStringList &where,
3809  MSqlBindings &bindings)
3810 {
3811  MSqlQuery result(dbConn);
3812  QString query;
3813  QString qphrase;
3814 
3815  query = QString("SELECT recordid,search,subtitle,description "
3816  "FROM %1 WHERE search <> %2 AND "
3817  "(recordid = %3 OR %4 = 0) ")
3818  .arg(recordTable).arg(kNoSearch).arg(recordid).arg(recordid);
3819 
3820  result.prepare(query);
3821 
3822  if (!result.exec() || !result.isActive())
3823  {
3824  MythDB::DBError("BuildNewRecordsQueries", result);
3825  return;
3826  }
3827 
3828  int count = 0;
3829  while (result.next())
3830  {
3831  QString prefix = QString(":NR%1").arg(count);
3832  qphrase = result.value(3).toString();
3833 
3834  RecSearchType searchtype = RecSearchType(result.value(1).toInt());
3835 
3836  if (qphrase.isEmpty() && searchtype != kManualSearch)
3837  {
3838  LOG(VB_GENERAL, LOG_ERR,
3839  QString("Invalid search key in recordid %1")
3840  .arg(result.value(0).toString()));
3841  continue;
3842  }
3843 
3844  QString bindrecid = prefix + "RECID";
3845  QString bindphrase = prefix + "PHRASE";
3846  QString bindlikephrase1 = prefix + "LIKEPHRASE1";
3847  QString bindlikephrase2 = prefix + "LIKEPHRASE2";
3848  QString bindlikephrase3 = prefix + "LIKEPHRASE3";
3849 
3850  bindings[bindrecid] = result.value(0).toString();
3851 
3852  switch (searchtype)
3853  {
3854  case kPowerSearch:
3855  qphrase.remove(QRegExp("^\\s*AND\\s+", Qt::CaseInsensitive));
3856  qphrase.remove(';');
3857  from << result.value(2).toString();
3858  where << (QString("%1.recordid = ").arg(recordTable) + bindrecid +
3859  QString(" AND program.manualid = 0 AND ( %2 )")
3860  .arg(qphrase));
3861  break;
3862  case kTitleSearch:
3863  bindings[bindlikephrase1] = QString("%") + qphrase + "%";
3864  from << "";
3865  where << (QString("%1.recordid = ").arg(recordTable) + bindrecid + " AND "
3866  "program.manualid = 0 AND "
3867  "program.title LIKE " + bindlikephrase1);
3868  break;
3869  case kKeywordSearch:
3870  bindings[bindlikephrase1] = QString("%") + qphrase + "%";
3871  bindings[bindlikephrase2] = QString("%") + qphrase + "%";
3872  bindings[bindlikephrase3] = QString("%") + qphrase + "%";
3873  from << "";
3874  where << (QString("%1.recordid = ").arg(recordTable) + bindrecid +
3875  " AND program.manualid = 0"
3876  " AND (program.title LIKE " + bindlikephrase1 +
3877  " OR program.subtitle LIKE " + bindlikephrase2 +
3878  " OR program.description LIKE " + bindlikephrase3 + ")");
3879  break;
3880  case kPeopleSearch:
3881  bindings[bindphrase] = qphrase;
3882  from << ", people, credits";
3883  where << (QString("%1.recordid = ").arg(recordTable) + bindrecid + " AND "
3884  "program.manualid = 0 AND "
3885  "people.name LIKE " + bindphrase + " AND "
3886  "credits.person = people.person AND "
3887  "program.chanid = credits.chanid AND "
3888  "program.starttime = credits.starttime");
3889  break;
3890  case kManualSearch:
3891  UpdateManuals(result.value(0).toInt());
3892  from << "";
3893  where << (QString("%1.recordid = ").arg(recordTable) + bindrecid +
3894  " AND " +
3895  QString("program.manualid = %1.recordid ")
3896  .arg(recordTable));
3897  break;
3898  default:
3899  LOG(VB_GENERAL, LOG_ERR,
3900  QString("Unknown RecSearchType (%1) for recordid %2")
3901  .arg(result.value(1).toInt())
3902  .arg(result.value(0).toString()));
3903  bindings.remove(bindrecid);
3904  break;
3905  }
3906 
3907  count++;
3908  }
3909 
3910  if (recordid == 0 || from.count() == 0)
3911  {
3912  QString recidmatch = "";
3913  if (recordid != 0)
3914  recidmatch = "RECTABLE.recordid = :NRRECORDID AND ";
3915  QString s1 = recidmatch +
3916  "RECTABLE.type <> :NRTEMPLATE AND "
3917  "RECTABLE.search = :NRST AND "
3918  "program.manualid = 0 AND "
3919  "program.title = RECTABLE.title ";
3920  s1.replace("RECTABLE", recordTable);
3921  QString s2 = recidmatch +
3922  "RECTABLE.type <> :NRTEMPLATE AND "
3923  "RECTABLE.search = :NRST AND "
3924  "program.manualid = 0 AND "
3925  "program.seriesid <> '' AND "
3926  "program.seriesid = RECTABLE.seriesid ";
3927  s2.replace("RECTABLE", recordTable);
3928 
3929  from << "";
3930  where << s1;
3931  from << "";
3932  where << s2;
3933  bindings[":NRTEMPLATE"] = kTemplateRecord;
3934  bindings[":NRST"] = kNoSearch;
3935  if (recordid != 0)
3936  bindings[":NRRECORDID"] = recordid;
3937  }
3938 }
3939 
3940 static QString progdupinit = QString(
3941 "(CASE "
3942 " WHEN RECTABLE.type IN (%1, %2, %3) THEN 0 "
3943 " WHEN RECTABLE.type IN (%4, %5, %6) THEN -1 "
3944 " ELSE (program.generic - 1) "
3945 " END) ")
3946  .arg(kSingleRecord).arg(kOverrideRecord).arg(kDontRecord)
3947  .arg(kOneRecord).arg(kDailyRecord).arg(kWeeklyRecord);
3948 
3949 static QString progfindid = QString(
3950 "(CASE RECTABLE.type "
3951 " WHEN %1 "
3952 " THEN RECTABLE.findid "
3953 " WHEN %2 "
3954 " THEN to_days(date_sub(convert_tz(program.starttime, 'UTC', 'SYSTEM'), "
3955 " interval time_format(RECTABLE.findtime, '%H:%i') hour_minute)) "
3956 " WHEN %3 "
3957 " THEN floor((to_days(date_sub(convert_tz(program.starttime, 'UTC', "
3958 " 'SYSTEM'), interval time_format(RECTABLE.findtime, '%H:%i') "
3959 " hour_minute)) - RECTABLE.findday)/7) * 7 + RECTABLE.findday "
3960 " WHEN %4 "
3961 " THEN RECTABLE.findid "
3962 " ELSE 0 "
3963 " END) ")
3964  .arg(kOneRecord)
3965  .arg(kDailyRecord)
3966  .arg(kWeeklyRecord)
3967  .arg(kOverrideRecord);
3968 
3969 void Scheduler::UpdateMatches(uint recordid, uint sourceid, uint mplexid,
3970  const QDateTime &maxstarttime)
3971 {
3972  struct timeval dbstart, dbend;
3973 
3974  MSqlQuery query(dbConn);
3975  MSqlBindings bindings;
3976  QString deleteClause;
3977  QString filterClause = QString(" AND program.endtime > "
3978  "(NOW() - INTERVAL 480 MINUTE)");
3979 
3980  if (recordid)
3981  {
3982  deleteClause += " AND recordmatch.recordid = :RECORDID";
3983  bindings[":RECORDID"] = recordid;
3984  }
3985  if (sourceid)
3986  {
3987  deleteClause += " AND channel.sourceid = :SOURCEID";
3988  filterClause += " AND channel.sourceid = :SOURCEID";
3989  bindings[":SOURCEID"] = sourceid;
3990  }
3991  if (mplexid)
3992  {
3993  deleteClause += " AND channel.mplexid = :MPLEXID";
3994  filterClause += " AND channel.mplexid = :MPLEXID";
3995  bindings[":MPLEXID"] = mplexid;
3996  }
3997  if (maxstarttime.isValid())
3998  {
3999  deleteClause += " AND recordmatch.starttime <= :MAXSTARTTIME";
4000  filterClause += " AND program.starttime <= :MAXSTARTTIME";
4001  bindings[":MAXSTARTTIME"] = maxstarttime;
4002  }
4003 
4004  query.prepare(QString("DELETE recordmatch FROM recordmatch, channel "
4005  "WHERE recordmatch.chanid = channel.chanid")
4006  + deleteClause);
4007  MSqlBindings::const_iterator it;
4008  for (it = bindings.begin(); it != bindings.end(); ++it)
4009  query.bindValue(it.key(), it.value());
4010  if (!query.exec())
4011  {
4012  MythDB::DBError("UpdateMatches1", query);
4013  return;
4014  }
4015  if (recordid)
4016  bindings.remove(":RECORDID");
4017 
4018  query.prepare("SELECT filterid, clause FROM recordfilter "
4019  "WHERE filterid >= 0 AND filterid < :NUMFILTERS AND "
4020  " TRIM(clause) <> ''");
4021  query.bindValue(":NUMFILTERS", RecordingRule::kNumFilters);
4022  if (!query.exec())
4023  {
4024  MythDB::DBError("UpdateMatches2", query);
4025  return;
4026  }
4027  while (query.next())
4028  {
4029  filterClause += QString(" AND (((RECTABLE.filter & %1) = 0) OR (%2))")
4030  .arg(1 << query.value(0).toInt()).arg(query.value(1).toString());
4031  }
4032 
4033  // Make sure all FindOne rules have a valid findid before scheduling.
4034  query.prepare("SELECT NULL from record "
4035  "WHERE type = :FINDONE AND findid <= 0;");
4036  query.bindValue(":FINDONE", kOneRecord);
4037  if (!query.exec())
4038  {
4039  MythDB::DBError("UpdateMatches3", query);
4040  return;
4041  }
4042  else if (query.size())
4043  {
4044  QDate epoch(1970, 1, 1);
4045  int findtoday =
4046  epoch.daysTo(MythDate::current().date()) + 719528;
4047  query.prepare("UPDATE record set findid = :FINDID "
4048  "WHERE type = :FINDONE AND findid <= 0;");
4049  query.bindValue(":FINDID", findtoday);
4050  query.bindValue(":FINDONE", kOneRecord);
4051  if (!query.exec())
4052  MythDB::DBError("UpdateMatches4", query);
4053  }
4054 
4055  int clause;
4056  QStringList fromclauses, whereclauses;
4057 
4058  BuildNewRecordsQueries(recordid, fromclauses, whereclauses, bindings);
4059 
4060  if (VERBOSE_LEVEL_CHECK(VB_SCHEDULE, LOG_INFO))
4061  {
4062  for (clause = 0; clause < fromclauses.count(); ++clause)
4063  {
4064  LOG(VB_SCHEDULE, LOG_INFO, QString("Query %1: %2/%3")
4065  .arg(clause).arg(fromclauses[clause])
4066  .arg(whereclauses[clause]));
4067  }
4068  }
4069 
4070  for (clause = 0; clause < fromclauses.count(); ++clause)
4071  {
4072  QString query2 = QString(
4073 "REPLACE INTO recordmatch (recordid, chanid, starttime, manualid, "
4074 " oldrecduplicate, findid) "
4075 "SELECT RECTABLE.recordid, program.chanid, program.starttime, "
4076 " IF(search = %1, RECTABLE.recordid, 0), ").arg(kManualSearch) +
4077  progdupinit + ", " + progfindid + QString(
4078 "FROM (RECTABLE, program INNER JOIN channel "
4079 " ON channel.chanid = program.chanid) ") + fromclauses[clause] + QString(
4080 " WHERE ") + whereclauses[clause] +
4081  QString(" AND channel.visible = 1 ") +
4082  filterClause + QString(" AND "
4083 
4084 "("
4085 " (RECTABLE.type = %1 " // all record
4086 " OR RECTABLE.type = %2 " // one record
4087 " OR RECTABLE.type = %3 " // daily record
4088 " OR RECTABLE.type = %4) " // weekly record
4089 " OR "
4090 " ((RECTABLE.type = %6 " // single record
4091 " OR RECTABLE.type = %7 " // override record
4092 " OR RECTABLE.type = %8)" // don't record
4093 " AND "
4094 " ADDTIME(RECTABLE.startdate, RECTABLE.starttime) = program.starttime " // date/time matches
4095 " AND "
4096 " RECTABLE.station = channel.callsign) " // channel matches
4097 ") ")
4098  .arg(kAllRecord)
4099  .arg(kOneRecord)
4100  .arg(kDailyRecord)
4101  .arg(kWeeklyRecord)
4102  .arg(kSingleRecord)
4103  .arg(kOverrideRecord)
4104  .arg(kDontRecord);
4105 
4106  query2.replace("RECTABLE", recordTable);
4107 
4108  LOG(VB_SCHEDULE, LOG_INFO, QString(" |-- Start DB Query %1...")
4109  .arg(clause));
4110 
4111  gettimeofday(&dbstart, nullptr);
4112  MSqlQuery result(dbConn);
4113  result.prepare(query2);
4114 
4115  for (it = bindings.begin(); it != bindings.end(); ++it)
4116  {
4117  if (query2.contains(it.key()))
4118  result.bindValue(it.key(), it.value());
4119  }
4120 
4121  bool ok = result.exec();
4122  gettimeofday(&dbend, nullptr);
4123 
4124  if (!ok)
4125  {
4126  MythDB::DBError("UpdateMatches3", result);
4127  continue;
4128  }
4129 
4130  LOG(VB_SCHEDULE, LOG_INFO, QString(" |-- %1 results in %2 sec.")
4131  .arg(result.size())
4132  .arg(((dbend.tv_sec - dbstart.tv_sec) * 1000000 +
4133  (dbend.tv_usec - dbstart.tv_usec)) / 1000000.0));
4134 
4135  }
4136 
4137  LOG(VB_SCHEDULE, LOG_INFO, " +-- Done.");
4138 }
4139 
4141 {
4142  MSqlQuery result(dbConn);
4143 
4144  if (recordTable == "record")
4145  {
4146  result.prepare("DROP TABLE IF EXISTS sched_temp_record;");
4147  if (!result.exec())
4148  {
4149  MythDB::DBError("Dropping sched_temp_record table", result);
4150  return;
4151  }
4152  result.prepare("CREATE TEMPORARY TABLE sched_temp_record "
4153  "LIKE record;");
4154  if (!result.exec())
4155  {
4156  MythDB::DBError("Creating sched_temp_record table", result);
4157  return;
4158  }
4159  result.prepare("INSERT sched_temp_record SELECT * from record;");
4160  if (!result.exec())
4161  {
4162  MythDB::DBError("Populating sched_temp_record table", result);
4163  return;
4164  }
4165  }
4166 
4167  result.prepare("DROP TABLE IF EXISTS sched_temp_recorded;");
4168  if (!result.exec())
4169  {
4170  MythDB::DBError("Dropping sched_temp_recorded table", result);
4171  return;
4172  }
4173  result.prepare("CREATE TEMPORARY TABLE sched_temp_recorded "
4174  "LIKE recorded;");
4175  if (!result.exec())
4176  {
4177  MythDB::DBError("Creating sched_temp_recorded table", result);
4178  return;
4179  }
4180  result.prepare("INSERT sched_temp_recorded SELECT * from recorded;");
4181  if (!result.exec())
4182  {
4183  MythDB::DBError("Populating sched_temp_recorded table", result);
4184  return;
4185  }
4186 }
4187 
4189 {
4190  MSqlQuery result(dbConn);
4191 
4192  if (recordTable == "record")
4193  {
4194  result.prepare("DROP TABLE IF EXISTS sched_temp_record;");
4195  if (!result.exec())
4196  MythDB::DBError("DeleteTempTables sched_temp_record", result);
4197  }
4198 
4199  result.prepare("DROP TABLE IF EXISTS sched_temp_recorded;");
4200  if (!result.exec())
4201  MythDB::DBError("DeleteTempTables drop table", result);
4202 }
4203 
4205 {
4206  QString schedTmpRecord = recordTable;
4207  if (schedTmpRecord == "record")
4208  schedTmpRecord = "sched_temp_record";
4209 
4210  QString rmquery = QString(
4211 "UPDATE recordmatch "
4212 " INNER JOIN RECTABLE ON (recordmatch.recordid = RECTABLE.recordid) "
4213 " INNER JOIN program p ON (recordmatch.chanid = p.chanid AND "
4214 " recordmatch.starttime = p.starttime AND "
4215 " recordmatch.manualid = p.manualid) "
4216 " LEFT JOIN oldrecorded ON "
4217 " ( "
4218 " RECTABLE.dupmethod > 1 AND "
4219 " oldrecorded.duplicate <> 0 AND "
4220 " p.title = oldrecorded.title AND "
4221 " p.generic = 0 "
4222 " AND "
4223 " ( "
4224 " (p.programid <> '' "
4225 " AND p.programid = oldrecorded.programid) "
4226 " OR "
4227 " ( ") +
4229 " (p.programid = '' OR oldrecorded.programid = '' OR "
4230 " LEFT(p.programid, LOCATE('/', p.programid)) <> "
4231 " LEFT(oldrecorded.programid, LOCATE('/', oldrecorded.programid))) " :
4232 " (p.programid = '' OR oldrecorded.programid = '') " )
4233  + QString(
4234 " AND "
4235 " (((RECTABLE.dupmethod & 0x02) = 0) OR (p.subtitle <> '' "
4236 " AND p.subtitle = oldrecorded.subtitle)) "
4237 " AND "
4238 " (((RECTABLE.dupmethod & 0x04) = 0) OR (p.description <> '' "
4239 " AND p.description = oldrecorded.description)) "
4240 " AND "
4241 " (((RECTABLE.dupmethod & 0x08) = 0) OR "
4242 " (p.subtitle <> '' AND "
4243 " (p.subtitle = oldrecorded.subtitle OR "
4244 " (oldrecorded.subtitle = '' AND "
4245 " p.subtitle = oldrecorded.description))) OR "
4246 " (p.subtitle = '' AND p.description <> '' AND "
4247 " (p.description = oldrecorded.subtitle OR "
4248 " (oldrecorded.subtitle = '' AND "
4249 " p.description = oldrecorded.description)))) "
4250 " ) "
4251 " ) "
4252 " ) "
4253 " LEFT JOIN sched_temp_recorded recorded ON "
4254 " ( "
4255 " RECTABLE.dupmethod > 1 AND "
4256 " recorded.duplicate <> 0 AND "
4257 " p.title = recorded.title AND "
4258 " p.generic = 0 AND "
4259 " recorded.recgroup NOT IN ('LiveTV','Deleted') "
4260 " AND "
4261 " ( "
4262 " (p.programid <> '' "
4263 " AND p.programid = recorded.programid) "
4264 " OR "
4265 " ( ") +
4267 " (p.programid = '' OR recorded.programid = '' OR "
4268 " LEFT(p.programid, LOCATE('/', p.programid)) <> "
4269 " LEFT(recorded.programid, LOCATE('/', recorded.programid))) " :
4270 " (p.programid = '' OR recorded.programid = '') ")
4271  + QString(
4272 " AND "
4273 " (((RECTABLE.dupmethod & 0x02) = 0) OR (p.subtitle <> '' "
4274 " AND p.subtitle = recorded.subtitle)) "
4275 " AND "
4276 " (((RECTABLE.dupmethod & 0x04) = 0) OR (p.description <> '' "
4277 " AND p.description = recorded.description)) "
4278 " AND "
4279 " (((RECTABLE.dupmethod & 0x08) = 0) OR "
4280 " (p.subtitle <> '' AND "
4281 " (p.subtitle = recorded.subtitle OR "
4282 " (recorded.subtitle = '' AND "
4283 " p.subtitle = recorded.description))) OR "
4284 " (p.subtitle = '' AND p.description <> '' AND "
4285 " (p.description = recorded.subtitle OR "
4286 " (recorded.subtitle = '' AND "
4287 " p.description = recorded.description)))) "
4288 " ) "
4289 " ) "
4290 " ) "
4291 " LEFT JOIN oldfind ON "
4292 " (oldfind.recordid = recordmatch.recordid AND "
4293 " oldfind.findid = recordmatch.findid) "
4294 " SET oldrecduplicate = (oldrecorded.endtime IS NOT NULL), "
4295 " recduplicate = (recorded.endtime IS NOT NULL), "
4296 " findduplicate = (oldfind.findid IS NOT NULL), "
4297 " oldrecstatus = oldrecorded.recstatus "
4298 " WHERE p.endtime >= (NOW() - INTERVAL 480 MINUTE) "
4299 " AND oldrecduplicate = -1 "
4300 );
4301  rmquery.replace("RECTABLE", schedTmpRecord);
4302 
4303  MSqlQuery result(dbConn);
4304  result.prepare(rmquery);
4305  if (!result.exec())
4306  {
4307  MythDB::DBError("UpdateDuplicates", result);
4308  return;
4309  }
4310 }
4311 
4313 {
4314  QString schedTmpRecord = recordTable;
4315  if (schedTmpRecord == "record")
4316  schedTmpRecord = "sched_temp_record";
4317 
4318  struct timeval dbstart, dbend;
4319 
4320  RecList tmpList;
4321 
4322  QMap<int, bool> cardMap;
4323  QMap<int, EncoderLink *>::Iterator enciter = m_tvList->begin();
4324  for (; enciter != m_tvList->end(); ++enciter)
4325  {
4326  EncoderLink *enc = *enciter;
4327  if (enc->IsConnected() || enc->IsAsleep())
4328  cardMap[enc->GetInputID()] = true;
4329  }
4330 
4331  QMap<int, bool> tooManyMap;
4332  bool checkTooMany = false;
4333  schedAfterStartMap.clear();
4334 
4335  MSqlQuery rlist(dbConn);
4336  rlist.prepare(QString("SELECT recordid, title, maxepisodes, maxnewest "
4337  "FROM %1").arg(schedTmpRecord));
4338 
4339  if (!rlist.exec())
4340  {
4341  MythDB::DBError("CheckTooMany", rlist);
4342  return;
4343  }
4344 
4345  while (rlist.next())
4346  {
4347  int recid = rlist.value(0).toInt();
4348  // QString qtitle = rlist.value(1).toString();
4349  int maxEpisodes = rlist.value(2).toInt();
4350  int maxNewest = rlist.value(3).toInt();
4351 
4352  tooManyMap[recid] = false;
4353  schedAfterStartMap[recid] = false;
4354 
4355  if (maxEpisodes && !maxNewest)
4356  {
4357  MSqlQuery epicnt(dbConn);
4358 
4359  epicnt.prepare("SELECT DISTINCT chanid, progstart, progend "
4360  "FROM recorded "
4361  "WHERE recordid = :RECID AND preserve = 0 "
4362  "AND recgroup NOT IN ('LiveTV','Deleted');");
4363  epicnt.bindValue(":RECID", recid);
4364 
4365  if (epicnt.exec())
4366  {
4367  if (epicnt.size() >= maxEpisodes - 1)
4368  {
4369  schedAfterStartMap[recid] = true;
4370  if (epicnt.size() >= maxEpisodes)
4371  {
4372  tooManyMap[recid] = true;
4373  checkTooMany = true;
4374  }
4375  }
4376  }
4377  }
4378  }
4379 
4380  int prefinputpri = gCoreContext->GetNumSetting("PrefInputPriority", 2);
4381  int hdtvpriority = gCoreContext->GetNumSetting("HDTVRecPriority", 0);
4382  int wspriority = gCoreContext->GetNumSetting("WSRecPriority", 0);
4383  int slpriority = gCoreContext->GetNumSetting("SignLangRecPriority", 0);
4384  int onscrpriority = gCoreContext->GetNumSetting("OnScrSubRecPriority", 0);
4385  int ccpriority = gCoreContext->GetNumSetting("CCRecPriority", 0);
4386  int hhpriority = gCoreContext->GetNumSetting("HardHearRecPriority", 0);
4387  int adpriority = gCoreContext->GetNumSetting("AudioDescRecPriority", 0);
4388 
4389  QString pwrpri = "channel.recpriority + capturecard.recpriority";
4390 
4391  if (prefinputpri)
4392  pwrpri += QString(" + "
4393  "(capturecard.cardid = RECTABLE.prefinput) * %1").arg(prefinputpri);
4394 
4395  if (hdtvpriority)
4396  pwrpri += QString(" + (program.hdtv > 0 OR "
4397  "FIND_IN_SET('HDTV', program.videoprop) > 0) * %1").arg(hdtvpriority);
4398 
4399  if (wspriority)
4400  pwrpri += QString(" + "
4401  "(FIND_IN_SET('WIDESCREEN', program.videoprop) > 0) * %1").arg(wspriority);
4402 
4403  if (slpriority)
4404  pwrpri += QString(" + "
4405  "(FIND_IN_SET('SIGNED', program.subtitletypes) > 0) * %1").arg(slpriority);
4406 
4407  if (onscrpriority)
4408  pwrpri += QString(" + "
4409  "(FIND_IN_SET('ONSCREEN', program.subtitletypes) > 0) * %1").arg(onscrpriority);
4410 
4411  if (ccpriority)
4412  pwrpri += QString(" + "
4413  "(FIND_IN_SET('NORMAL', program.subtitletypes) > 0 OR "
4414  "program.closecaptioned > 0 OR program.subtitled > 0) * %1").arg(ccpriority);
4415 
4416  if (hhpriority)
4417  pwrpri += QString(" + "
4418  "(FIND_IN_SET('HARDHEAR', program.subtitletypes) > 0 OR "
4419  "FIND_IN_SET('HARDHEAR', program.audioprop) > 0) * %1").arg(hhpriority);
4420 
4421  if (adpriority)
4422  pwrpri += QString(" + "
4423  "(FIND_IN_SET('VISUALIMPAIR', program.audioprop) > 0) * %1").arg(adpriority);
4424 
4425  MSqlQuery result(dbConn);
4426 
4427  result.prepare(QString("SELECT recpriority, selectclause FROM %1;")
4428  .arg(priorityTable));
4429 
4430  if (!result.exec())
4431  {
4432  MythDB::DBError("Power Priority", result);
4433  return;
4434  }
4435 
4436  while (result.next())
4437  {
4438  if (result.value(0).toInt())
4439  {
4440  QString sclause = result.value(1).toString();
4441  sclause.remove(QRegExp("^\\s*AND\\s+", Qt::CaseInsensitive));
4442  sclause.remove(';');
4443  pwrpri += QString(" + (%1) * %2").arg(sclause)
4444  .arg(result.value(0).toInt());
4445  }
4446  }
4447  pwrpri += QString(" AS powerpriority ");
4448 
4449  pwrpri.replace("program.","p.");
4450  pwrpri.replace("channel.","c.");
4451  QString query = QString(
4452  "SELECT "
4453  " c.chanid, c.sourceid, p.starttime, "// 0-2
4454  " p.endtime, p.title, p.subtitle, "// 3-5
4455  " p.description, c.channum, c.callsign, "// 6-8
4456  " c.name, oldrecduplicate, p.category, "// 9-11
4457  " RECTABLE.recpriority, RECTABLE.dupin, recduplicate, "//12-14
4458  " findduplicate, RECTABLE.type, RECTABLE.recordid, "//15-17
4459  " p.starttime - INTERVAL RECTABLE.startoffset "
4460  " minute AS recstartts, " //18
4461  " p.endtime + INTERVAL RECTABLE.endoffset "
4462  " minute AS recendts, " //19
4463  " p.previouslyshown, "//20
4464  " RECTABLE.recgroup, RECTABLE.dupmethod, c.commmethod, "//21-23
4465  " capturecard.cardid, 0, p.seriesid, "//24-26
4466  " p.programid, RECTABLE.inetref, p.category_type, "//27-29
4467  " p.airdate, p.stars, p.originalairdate, "//30-32
4468  " RECTABLE.inactive, RECTABLE.parentid, recordmatch.findid, "//33-35
4469  " RECTABLE.playgroup, oldrecstatus.recstatus, "//36-37
4470  " oldrecstatus.reactivate, p.videoprop+0, "//38-39
4471  " p.subtitletypes+0, p.audioprop+0, RECTABLE.storagegroup, "//40-42
4472  " capturecard.hostname, recordmatch.oldrecstatus, NULL, "//43-45
4473  " oldrecstatus.future, capturecard.schedorder, " //46-47
4474  " p.syndicatedepisodenumber, p.partnumber, p.parttotal, " //48-50
4475  " c.mplexid, capturecard.displayname, "//51-52
4476  " p.season, p.episode, p.totalepisodes, ") + //53-55
4477  pwrpri + QString( //56
4478  "FROM recordmatch "
4479  "INNER JOIN RECTABLE ON (recordmatch.recordid = RECTABLE.recordid) "
4480  "INNER JOIN program AS p "
4481  "ON ( recordmatch.chanid = p.chanid AND "
4482  " recordmatch.starttime = p.starttime AND "
4483  " recordmatch.manualid = p.manualid ) "
4484  "INNER JOIN channel AS c "
4485  "ON ( c.chanid = p.chanid ) "
4486  "INNER JOIN capturecard "
4487  "ON ( c.sourceid = capturecard.sourceid AND "
4488  " ( capturecard.schedorder <> 0 OR "
4489  " capturecard.parentid = 0 ) ) "
4490  "LEFT JOIN oldrecorded as oldrecstatus "
4491  "ON ( oldrecstatus.station = c.callsign AND "
4492  " oldrecstatus.starttime = p.starttime AND "
4493  " oldrecstatus.title = p.title ) "
4494  "WHERE p.endtime > (NOW() - INTERVAL 480 MINUTE) "
4495  "ORDER BY RECTABLE.recordid DESC, p.starttime, p.title, c.callsign, "
4496  " c.channum ");
4497  query.replace("RECTABLE", schedTmpRecord);
4498 
4499  LOG(VB_SCHEDULE, LOG_INFO, QString(" |-- Start DB Query..."));
4500 
4501  gettimeofday(&dbstart, nullptr);
4502  result.prepare(query);
4503  if (!result.exec())
4504  {
4505  MythDB::DBError("AddNewRecords", result);
4506  return;
4507  }
4508  gettimeofday(&dbend, nullptr);
4509 
4510  LOG(VB_SCHEDULE, LOG_INFO,
4511  QString(" |-- %1 results in %2 sec. Processing...")
4512  .arg(result.size())
4513  .arg(((dbend.tv_sec - dbstart.tv_sec) * 1000000 +
4514  (dbend.tv_usec - dbstart.tv_usec)) / 1000000.0));
4515 
4516  RecordingInfo *lastp = nullptr;
4517 
4518  while (result.next())
4519  {
4520  // If this is the same program we saw in the last pass and it
4521  // wasn't a viable candidate, then neither is this one so
4522  // don't bother with it. This is essentially an early call to
4523  // PruneRedundants().
4524  uint recordid = result.value(17).toUInt();
4525  QDateTime startts = MythDate::as_utc(result.value(2).toDateTime());
4526  QString title = result.value(4).toString();
4527  QString callsign = result.value(8).toString();
4528  if (lastp && lastp->GetRecordingStatus() != RecStatus::Unknown
4529  && lastp->GetRecordingStatus() != RecStatus::Offline
4530  && lastp->GetRecordingStatus() != RecStatus::DontRecord
4531  && recordid == lastp->GetRecordingRuleID()
4532  && startts == lastp->GetScheduledStartTime()
4533  && title == lastp->GetTitle()
4534  && callsign == lastp->GetChannelSchedulingID())
4535  continue;
4536 
4537  uint mplexid = result.value(51).toUInt();
4538  if (mplexid == 32767)
4539  mplexid = 0;
4540 
4541  QString inputname = result.value(52).toString();
4542  if (inputname.isEmpty())
4543  inputname = QString("Input %1").arg(result.value(24).toUInt());
4544 
4545  RecordingInfo *p = new RecordingInfo(
4546  title,
4547  QString(),//sorttitle
4548  result.value(5).toString(),//subtitle
4549  QString(),//sortsubtitle
4550  result.value(6).toString(),//description
4551  result.value(53).toInt(), // season
4552  result.value(54).toInt(), // episode
4553  result.value(55).toInt(), // total episodes
4554  result.value(48).toString(),//synidcatedepisode
4555  result.value(11).toString(),//category
4556 
4557  result.value(0).toUInt(),//chanid
4558  result.value(7).toString(),//channum
4559  callsign,
4560  result.value(9).toString(),//channame
4561 
4562  result.value(21).toString(),//recgroup
4563  result.value(36).toString(),//playgroup
4564 
4565  result.value(43).toString(),//hostname
4566  result.value(42).toString(),//storagegroup
4567 
4568  result.value(30).toUInt(),//year
4569  result.value(49).toUInt(),//partnumber
4570  result.value(50).toUInt(),//parttotal
4571 
4572  result.value(26).toString(),//seriesid
4573  result.value(27).toString(),//programid
4574  result.value(28).toString(),//inetref
4575  string_to_myth_category_type(result.value(29).toString()),//catType
4576 
4577  result.value(12).toInt(),//recpriority
4578 
4579  startts,
4580  MythDate::as_utc(result.value(3).toDateTime()),//endts
4581  MythDate::as_utc(result.value(18).toDateTime()),//recstartts
4582  MythDate::as_utc(result.value(19).toDateTime()),//recendts
4583 
4584  result.value(31).toDouble(),//stars
4585  (result.value(32).isNull()) ? QDate() :
4586  QDate::fromString(result.value(32).toString(), Qt::ISODate),
4587  //originalAirDate
4588 
4589  result.value(20).toInt(),//repeat
4590 
4591  RecStatus::Type(result.value(37).toInt()),//oldrecstatus
4592  result.value(38).toInt(),//reactivate
4593 
4594  recordid,
4595  result.value(34).toUInt(),//parentid
4596  RecordingType(result.value(16).toInt()),//rectype
4597  RecordingDupInType(result.value(13).toInt()),//dupin
4598  RecordingDupMethodType(result.value(22).toInt()),//dupmethod
4599 
4600  result.value(1).toUInt(),//sourceid
4601  result.value(24).toUInt(),//inputid
4602 
4603  result.value(35).toUInt(),//findid
4604 
4605  result.value(23).toInt() == COMM_DETECT_COMMFREE,//commfree
4606  result.value(40).toUInt(),//subtitleType
4607  result.value(39).toUInt(),//videoproperties
4608  result.value(41).toUInt(),//audioproperties
4609  result.value(46).toInt(),//future
4610  result.value(47).toInt(),//schedorder
4611  mplexid, //mplexid
4612  result.value(24).toUInt(), //sgroupid
4613  inputname); //inputname
4614 
4615  if (!p->future && !p->IsReactivated() &&
4618  {
4620  }
4621 
4622  p->SetRecordingPriority2(result.value(56).toInt());
4623 
4624  // Check to see if the program is currently recording and if
4625  // the end time was changed. Ideally, checking for a new end
4626  // time should be done after PruneOverlaps, but that would
4627  // complicate the list handling. Do it here unless it becomes
4628  // problematic.
4629  RecIter rec = worklist.begin();
4630  for ( ; rec != worklist.end(); ++rec)
4631  {
4632  RecordingInfo *r = *rec;
4634  {
4635  if (r->sgroupid == p->sgroupid &&
4636  r->GetRecordingEndTime() != p->GetRecordingEndTime() &&
4637  (r->GetRecordingRuleID() == p->GetRecordingRuleID() ||
4639  ChangeRecordingEnd(r, p);
4640  delete p;
4641  p = nullptr;
4642  break;
4643  }
4644  }
4645  if (p == nullptr)
4646  continue;
4647 
4648  lastp = p;
4649 
4651  {
4652  tmpList.push_back(p);
4653  continue;
4654  }
4655 
4656  RecStatus::Type newrecstatus = RecStatus::Unknown;
4657  // Check for RecStatus::Offline
4658  if ((doRun || specsched) &&
4659  (!cardMap.contains(p->GetInputID()) || !p->schedorder))
4660  {
4661  newrecstatus = RecStatus::Offline;
4662  if (p->schedorder == 0 &&
4663  m_schedorder_warned.find(p->GetInputID()) ==
4664  m_schedorder_warned.end())
4665  {
4666  LOG(VB_GENERAL, LOG_WARNING, LOC +
4667  QString("Channel %1, Title %2 %3 cardinput.schedorder = %4, "
4668  "it must be >0 to record from this input.")
4669  .arg(p->GetChannelName()).arg(p->GetTitle())
4670  .arg(p->GetScheduledStartTime().toString())
4671  .arg(p->schedorder));
4672  m_schedorder_warned.insert(p->GetInputID());
4673  }
4674  }
4675 
4676  // Check for RecStatus::TooManyRecordings
4677  if (checkTooMany && tooManyMap[p->GetRecordingRuleID()] &&
4678  !p->IsReactivated())
4679  {
4680  newrecstatus = RecStatus::TooManyRecordings;
4681  }
4682 
4683  // Check for RecStatus::CurrentRecording and RecStatus::PreviousRecording
4684  if (p->GetRecordingRuleType() == kDontRecord)
4685  newrecstatus = RecStatus::DontRecord;
4686  else if (result.value(15).toInt() && !p->IsReactivated())
4687  newrecstatus = RecStatus::PreviousRecording;
4688  else if (p->GetRecordingRuleType() != kSingleRecord &&
4690  !p->IsReactivated() &&
4692  {
4693  const RecordingDupInType dupin = p->GetDuplicateCheckSource();
4694 
4695  if ((dupin & kDupsNewEpi) && p->IsRepeat())
4696  newrecstatus = RecStatus::Repeat;
4697 
4698  if ((dupin & kDupsInOldRecorded) && result.value(10).toInt())
4699  {
4700  if (result.value(44).toInt() == RecStatus::NeverRecord)
4701  newrecstatus = RecStatus::NeverRecord;
4702  else
4703  newrecstatus = RecStatus::PreviousRecording;
4704  }
4705 
4706  if ((dupin & kDupsInRecorded) && result.value(14).toInt())
4707  newrecstatus = RecStatus::CurrentRecording;
4708  }
4709 
4710  bool inactive = result.value(33).toInt();
4711  if (inactive)
4712  newrecstatus = RecStatus::Inactive;
4713 
4714  // Mark anything that has already passed as some type of
4715  // missed. If it survives PruneOverlaps, it will get deleted
4716  // or have its old status restored in PruneRedundants.
4717  if (p->GetRecordingEndTime() < schedTime)
4718  {
4719  if (p->future)
4720  newrecstatus = RecStatus::MissedFuture;
4721  else
4722  newrecstatus = RecStatus::Missed;
4723  }
4724 
4725  p->SetRecordingStatus(newrecstatus);
4726 
4727  tmpList.push_back(p);
4728  }
4729 
4730  LOG(VB_SCHEDULE, LOG_INFO, " +-- Cleanup...");
4731  RecIter tmp = tmpList.begin();
4732  for ( ; tmp != tmpList.end(); ++tmp)
4733  worklist.push_back(*tmp);
4734 }
4735 
4737 
4738  struct timeval dbstart, dbend;
4739  RecList tmpList;
4740 
4741  QString query = QString(
4742  "SELECT RECTABLE.title, RECTABLE.subtitle, " // 0,1
4743  " RECTABLE.description, RECTABLE.season, " // 2,3
4744  " RECTABLE.episode, RECTABLE.category, " // 4,5
4745  " RECTABLE.chanid, channel.channum, " // 6,7
4746  " RECTABLE.station, channel.name, " // 8,9
4747  " RECTABLE.recgroup, RECTABLE.playgroup, " // 10,11
4748  " RECTABLE.seriesid, RECTABLE.programid, " // 12,13
4749  " RECTABLE.inetref, RECTABLE.recpriority, " // 14,15
4750  " RECTABLE.startdate, RECTABLE.starttime, " // 16,17
4751  " RECTABLE.enddate, RECTABLE.endtime, " // 18,19
4752  " RECTABLE.recordid, RECTABLE.type, " // 20,21
4753  " RECTABLE.dupin, RECTABLE.dupmethod, " // 22,23
4754  " RECTABLE.findid, " // 24
4755  " RECTABLE.startoffset, RECTABLE.endoffset, " // 25,26
4756  " channel.commmethod " // 27
4757  "FROM RECTABLE "
4758  "INNER JOIN channel ON (channel.chanid = RECTABLE.chanid) "
4759  "LEFT JOIN recordmatch on RECTABLE.recordid = recordmatch.recordid "
4760  "WHERE (type = %1 OR type = %2) AND "
4761  " recordmatch.chanid IS NULL")
4762  .arg(kSingleRecord)
4763  .arg(kOverrideRecord);
4764 
4765  query.replace("RECTABLE", recordTable);
4766 
4767  LOG(VB_SCHEDULE, LOG_INFO, QString(" |-- Start DB Query..."));
4768 
4769  gettimeofday(&dbstart, nullptr);
4770  MSqlQuery result(dbConn);
4771  result.prepare(query);
4772  bool ok = result.exec();
4773  gettimeofday(&dbend, nullptr);
4774 
4775  if (!ok)
4776  {
4777  MythDB::DBError("AddNotListed", result);
4778  return;
4779  }
4780 
4781  LOG(VB_SCHEDULE, LOG_INFO,
4782  QString(" |-- %1 results in %2 sec. Processing...")
4783  .arg(result.size())
4784  .arg(((dbend.tv_sec - dbstart.tv_sec) * 1000000 +
4785  (dbend.tv_usec - dbstart.tv_usec)) / 1000000.0));
4786 
4787  QDateTime now = MythDate::current();
4788 
4789  while (result.next())
4790  {
4791  RecordingType rectype = RecordingType(result.value(21).toInt());
4792  QDateTime startts(
4793  result.value(16).toDate(), result.value(17).toTime(), Qt::UTC);
4794  QDateTime endts(
4795  result.value(18).toDate(), result.value(19).toTime(), Qt::UTC);
4796 
4797  QDateTime recstartts = startts.addSecs(result.value(25).toInt() * -60);
4798  QDateTime recendts = endts.addSecs( result.value(26).toInt() * +60);
4799 
4800  if (recstartts >= recendts)
4801  {
4802  // start/end-offsets are invalid so ignore
4803  recstartts = startts;
4804  recendts = endts;
4805  }
4806 
4807  // Don't bother if the end time has already passed
4808  if (recendts < schedTime)
4809  continue;
4810 
4811  bool sor = (kSingleRecord == rectype) || (kOverrideRecord == rectype);
4812 
4813  RecordingInfo *p = new RecordingInfo(
4814  result.value(0).toString(), // Title
4815  QString(), // Title Sort
4816  (sor) ? result.value(1).toString() : QString(), // Subtitle
4817  QString(), // Subtitle Sort
4818  (sor) ? result.value(2).toString() : QString(), // Description
4819  result.value(3).toUInt(), // Season
4820  result.value(4).toUInt(), // Episode
4821  QString(), // Category
4822 
4823  result.value(6).toUInt(), // Chanid
4824  result.value(7).toString(), // Channel number
4825  result.value(8).toString(), // Call Sign
4826  result.value(9).toString(), // Channel name
4827 
4828  result.value(10).toString(), // Recgroup
4829  result.value(11).toString(), // Playgroup
4830 
4831  result.value(12).toString(), // Series ID
4832  result.value(13).toString(), // Program ID
4833  result.value(14).toString(), // Inetref
4834 
4835  result.value(15).toInt(), // Rec priority
4836 
4837  startts, endts,
4838  recstartts, recendts,
4839 
4840  RecStatus::NotListed, // Recording Status
4841 
4842  result.value(20).toUInt(), // Recording ID
4843  RecordingType(result.value(21).toInt()), // Recording type
4844 
4845  RecordingDupInType(result.value(22).toInt()), // DupIn type
4846  RecordingDupMethodType(result.value(23).toInt()), // Dup method
4847 
4848  result.value(24).toUInt(), // Find ID
4849 
4850  result.value(27).toInt() == COMM_DETECT_COMMFREE); // Comm Free
4851 
4852  tmpList.push_back(p);
4853  }
4854 
4855  RecIter tmp = tmpList.begin();
4856  for ( ; tmp != tmpList.end(); ++tmp)
4857  worklist.push_back(*tmp);
4858 }
4859 
4865  bool ascending)
4866 {
4867  QString sortColumn = "title";
4868  // Q: Why don't we use a string containing the column name instead?
4869  // A: It's too fragile, we'll refuse to compile if an invalid enum name is
4870  // used but not if an invalid column is specified. It also means that if
4871  // the column names change we only need to update one place not several
4872  switch (sortBy)
4873  {
4874  case kSortTitle:
4875  sortColumn = "record.title";
4876  break;
4877  case kSortPriority:
4878  sortColumn = "record.recpriority";
4879  break;
4880  case kSortLastRecorded:
4881  sortColumn = "record.last_record";
4882  break;
4883  case kSortNextRecording:
4884  // We want to shift the rules which have no upcoming recordings to
4885  // the back of the pack, most of the time the user won't be interested
4886  // in rules that aren't matching recordings at the present time.
4887  // We still want them available in the list however since vanishing rules
4888  // violates the principle of least surprise
4889  sortColumn = "record.next_record IS NULL, record.next_record";
4890  break;
4891  case kSortType:
4892  sortColumn = "record.type";
4893  break;
4894  }
4895 
4896  QString order = "ASC";
4897  if (!ascending)
4898  order = "DESC";
4899 
4900  QString query = QString(
4901  "SELECT record.title, record.subtitle, " // 0,1
4902  " record.description, record.season, " // 2,3
4903  " record.episode, record.category, " // 4,5
4904  " record.chanid, channel.channum, " // 6,7
4905  " record.station, channel.name, " // 8,9
4906  " record.recgroup, record.playgroup, " // 10,11
4907  " record.seriesid, record.programid, " // 12,13
4908  " record.inetref, record.recpriority, " // 14,15
4909  " record.startdate, record.starttime, " // 16,17
4910  " record.enddate, record.endtime, " // 18,19
4911  " record.recordid, record.type, " // 20,21
4912  " record.dupin, record.dupmethod, " // 22,23
4913  " record.findid, " // 24
4914  " channel.commmethod " // 25
4915  "FROM record "
4916  "LEFT JOIN channel ON channel.callsign = record.station "
4917  "GROUP BY recordid "
4918  "ORDER BY %1 %2");
4919 
4920  query = query.arg(sortColumn).arg(order);
4921 
4922  MSqlQuery result(MSqlQuery::InitCon());
4923  result.prepare(query);
4924 
4925  if (!result.exec())
4926  {
4927  MythDB::DBError("GetAllScheduled", result);
4928  return;
4929  }
4930 
4931  while (result.next())
4932  {
4933  RecordingType rectype = RecordingType(result.value(21).toInt());
4934  QDateTime startts = QDateTime(result.value(16).toDate(),
4935  result.value(17).toTime(), Qt::UTC);
4936  QDateTime endts = QDateTime(result.value(18).toDate(),
4937  result.value(19).toTime(), Qt::UTC);
4938  // Prevent invalid date/time warnings later
4939  if (!startts.isValid())
4940  startts = QDateTime(MythDate::current().date(), QTime(0,0),
4941  Qt::UTC);
4942  if (!endts.isValid())
4943  endts = startts;
4944 
4945  proglist.push_back(new RecordingInfo(
4946  result.value(0).toString(), QString(),
4947  result.value(1).toString(), QString(),
4948  result.value(2).toString(), result.value(3).toUInt(),
4949  result.value(4).toUInt(), result.value(5).toString(),
4950 
4951  result.value(6).toUInt(), result.value(7).toString(),
4952  result.value(8).toString(), result.value(9).toString(),
4953 
4954  result.value(10).toString(), result.value(11).toString(),
4955 
4956  result.value(12).toString(), result.value(13).toString(),
4957  result.value(14).toString(),
4958 
4959  result.value(15).toInt(),
4960 
4961  startts, endts,
4962  startts, endts,
4963 
4965 
4966  result.value(20).toUInt(), rectype,
4967  RecordingDupInType(result.value(22).toInt()),
4968  RecordingDupMethodType(result.value(23).toInt()),
4969 
4970  result.value(24).toUInt(),
4971 
4972  result.value(25).toInt() == COMM_DETECT_COMMFREE));
4973  }
4974 }
4975 
4977 // Storage Scheduler sort order routines
4978 // Sort mode-preferred to least-preferred (true == a more preferred than b)
4979 //
4980 // Prefer local over remote and to balance Disk I/O (weight), then free space
4982 {
4983  // local over remote
4984  if (a->isLocal() && !b->isLocal())
4985  {
4986  if (a->getWeight() <= b->getWeight())
4987  {
4988  return true;
4989  }
4990  }
4991  else if (a->isLocal() == b->isLocal())
4992  {
4993  if (a->getWeight() < b->getWeight())
4994  {
4995  return true;
4996  }
4997  else if (a->getWeight() > b->getWeight())
4998  {
4999  return false;
5000  }
5001  else if (a->getFreeSpace() > b->getFreeSpace())
5002  {
5003  return true;
5004  }
5005  }
5006  else if (!a->isLocal() && b->isLocal())
5007  {
5008  if (a->getWeight() < b->getWeight())
5009  {
5010  return true;
5011  }
5012  }
5013 
5014  return false;
5015 }
5016 
5017 // prefer dirs with more percentage free space over dirs with less
5019 {
5020  if (a->getTotalSpace() == 0)
5021  return false;
5022 
5023  if (b->getTotalSpace() == 0)
5024  return true;
5025 
5026  if ((a->getFreeSpace() * 100.0) / a->getTotalSpace() >
5027  (b->getFreeSpace() * 100.0) / b->getTotalSpace())
5028  return true;
5029 
5030  return false;
5031 }
5032 
5033 // prefer dirs with more absolute free space over dirs with less
5035 {
5036  if (a->getFreeSpace() > b->getFreeSpace())
5037  return true;
5038 
5039  return false;
5040 }
5041 
5042 // prefer dirs with less weight (disk I/O) over dirs with more weight.
5043 // if weights are equal, prefer dirs with more absolute free space over less
5045 {
5046  if (a->getWeight() < b->getWeight())
5047  {
5048  return true;
5049  }
5050  else if (a->getWeight() == b->getWeight())
5051  {
5052  if (a->getFreeSpace() > b->getFreeSpace())
5053  return true;
5054  }
5055 
5056  return false;
5057 }
5058 
5060 
5062 {
5063  QMutexLocker lockit(&schedLock);
5064  QReadLocker tvlocker(&TVRec::inputsLock);
5065 
5066  if (!m_tvList->contains(cardid))
5067  return;
5068 
5069  EncoderLink *tv = (*m_tvList)[cardid];
5070 
5071  QDateTime cur = MythDate::current(true);
5072  QString recording_dir;
5073  int fsID = FillRecordingDir(
5074  "LiveTV",
5075  (tv->IsLocal()) ? gCoreContext->GetHostName() : tv->GetHostName(),
5076  "LiveTV", cur, cur.addSecs(3600), cardid,
5077  recording_dir, reclist);
5078 
5079  tv->SetNextLiveTVDir(recording_dir);
5080 
5081  LOG(VB_FILE, LOG_INFO, LOC + QString("FindNextLiveTVDir: next dir is '%1'")
5082  .arg(recording_dir));
5083 
5084  if (m_expirer) // update auto expirer
5085  m_expirer->Update(cardid, fsID, true);
5086 }
5087 
5089  const QString &title,
5090  const QString &hostname,
5091  const QString &storagegroup,
5092  const QDateTime &recstartts,
5093  const QDateTime &recendts,
5094  uint cardid,
5095  QString &recording_dir,
5096  const RecList &reclist)
5097 {
5098  LOG(VB_SCHEDULE, LOG_INFO, LOC + "FillRecordingDir: Starting");
5099 
5100  uint cnt = 0;
5101  while (!m_mainServer)
5102  {
5103  if (cnt++ % 20 == 0)
5104  LOG(VB_SCHEDULE, LOG_WARNING, "Waiting for main server.");
5105  std::this_thread::sleep_for(std::chrono::milliseconds(50));
5106  }
5107 
5108  int fsID = -1;
5109  MSqlQuery query(MSqlQuery::InitCon());
5110  QMap<QString, FileSystemInfo>::Iterator fsit;
5111  QMap<QString, FileSystemInfo>::Iterator fsit2;
5112  StorageGroup mysgroup(storagegroup, hostname);
5113  QStringList dirlist = mysgroup.GetDirList();
5114  QStringList recsCounted;
5115  list<FileSystemInfo *> fsInfoList;
5116  list<FileSystemInfo *>::iterator fslistit;
5117 
5118  recording_dir.clear();
5119 
5120  if (dirlist.size() == 1)
5121  {
5122  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5123  QString("FillRecordingDir: The only directory in the %1 Storage "
5124  "Group is %2, so it will be used by default.")
5125  .arg(storagegroup) .arg(dirlist[0]));
5126  recording_dir = dirlist[0];
5127  LOG(VB_SCHEDULE, LOG_INFO, LOC + "FillRecordingDir: Finished");
5128 
5129  return -1;
5130  }
5131 
5132  int weightPerRecording =
5133  gCoreContext->GetNumSetting("SGweightPerRecording", 10);
5134  int weightPerPlayback =
5135  gCoreContext->GetNumSetting("SGweightPerPlayback", 5);
5136  int weightPerCommFlag =
5137  gCoreContext->GetNumSetting("SGweightPerCommFlag", 5);
5138  int weightPerTranscode =
5139  gCoreContext->GetNumSetting("SGweightPerTranscode", 5);
5140 
5141  QString storageScheduler =
5142  gCoreContext->GetSetting("StorageScheduler", "Combination");
5143  int localStartingWeight =
5144  gCoreContext->GetNumSetting("SGweightLocalStarting",
5145  (storageScheduler != "Combination") ? 0
5146  : (int)(-1.99 * weightPerRecording));
5147  int remoteStartingWeight =
5148  gCoreContext->GetNumSetting("SGweightRemoteStarting", 0);
5149  int maxOverlap = gCoreContext->GetNumSetting("SGmaxRecOverlapMins", 3) * 60;
5150 
5152 
5153  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5154  "FillRecordingDir: Calculating initial FS Weights.");
5155 
5156  for (fsit = fsInfoCache.begin(); fsit != fsInfoCache.end(); ++fsit)
5157  {
5158  FileSystemInfo *fs = &(*fsit);
5159  int tmpWeight = 0;
5160 
5161  QString msg = QString(" %1:%2").arg(fs->getHostname())
5162  .arg(fs->getPath());
5163  if (fs->isLocal())
5164  {
5165  tmpWeight = localStartingWeight;
5166  msg += " is local (" + QString::number(tmpWeight) + ")";
5167  }
5168  else
5169  {
5170  tmpWeight = remoteStartingWeight;
5171  msg += " is remote (+" + QString::number(tmpWeight) + ")";
5172  }
5173 
5174  fs->setWeight(tmpWeight);
5175 
5176  tmpWeight = gCoreContext->GetNumSetting(QString("SGweightPerDir:%1:%2")
5177  .arg(fs->getHostname()).arg(fs->getPath()), 0);
5178  fs->setWeight(fs->getWeight() + tmpWeight);
5179 
5180  if (tmpWeight)
5181  msg += ", has SGweightPerDir offset of "
5182  + QString::number(tmpWeight) + ")";
5183 
5184  msg += ". initial dir weight = " + QString::number(fs->getWeight());
5185  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, msg);
5186 
5187  fsInfoList.push_back(fs);
5188  }
5189 
5190  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5191  "FillRecordingDir: Adjusting FS Weights from inuseprograms.");
5192 
5193  MSqlQuery saveRecDir(MSqlQuery::InitCon());
5194  saveRecDir.prepare("UPDATE inuseprograms "
5195  "SET recdir = :RECDIR "
5196  "WHERE chanid = :CHANID AND "
5197  " starttime = :STARTTIME");
5198 
5199  query.prepare(
5200  "SELECT i.chanid, i.starttime, r.endtime, recusage, rechost, recdir "
5201  "FROM inuseprograms i, recorded r "
5202  "WHERE DATE_ADD(lastupdatetime, INTERVAL 16 MINUTE) > NOW() AND "
5203  " i.chanid = r.chanid AND "
5204  " i.starttime = r.starttime");
5205 
5206  if (!query.exec())
5207  {
5208  MythDB::DBError(LOC + "FillRecordingDir", query);
5209  }
5210  else
5211  {
5212  while (query.next())
5213  {
5214  uint recChanid = query.value(0).toUInt();
5215  QDateTime recStart( MythDate::as_utc(query.value(1).toDateTime()));
5216  QDateTime recEnd( MythDate::as_utc(query.value(2).toDateTime()));
5217  QString recUsage( query.value(3).toString());
5218  QString recHost( query.value(4).toString());
5219  QString recDir( query.value(5).toString());
5220 
5221  if (recDir.isEmpty())
5222  {
5223  ProgramInfo pginfo(recChanid, recStart);
5224  recDir = pginfo.DiscoverRecordingDirectory();
5225  recDir = recDir.isEmpty() ? "_UNKNOWN_" : recDir;
5226 
5227  saveRecDir.bindValue(":RECDIR", recDir);
5228  saveRecDir.bindValue(":CHANID", recChanid);
5229  saveRecDir.bindValue(":STARTTIME", recStart);
5230  if (!saveRecDir.exec())
5231  MythDB::DBError(LOC + "FillRecordingDir", saveRecDir);
5232  }
5233  if (recDir == "_UNKNOWN_")
5234  continue;
5235 
5236  for (fslistit = fsInfoList.begin();
5237  fslistit != fsInfoList.end(); ++fslistit)
5238  {
5239  FileSystemInfo *fs = *fslistit;
5240  if ((recHost == fs->getHostname()) &&
5241  (recDir == fs->getPath()))
5242  {
5243  int weightOffset = 0;
5244 
5245  if (recUsage == kRecorderInUseID)
5246  {
5247  if (recEnd > recstartts.addSecs(maxOverlap))
5248  {
5249  weightOffset += weightPerRecording;
5250  recsCounted << QString::number(recChanid) + ":" +
5251  recStart.toString(Qt::ISODate);
5252  }
5253  }
5254  else if (recUsage.contains(kPlayerInUseID))
5255  weightOffset += weightPerPlayback;
5256  else if (recUsage == kFlaggerInUseID)
5257  weightOffset += weightPerCommFlag;
5258  else if (recUsage == kTranscoderInUseID)
5259  weightOffset += weightPerTranscode;
5260 
5261  if (weightOffset)
5262  {
5263  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO,
5264  QString(" %1 @ %2 in use by '%3' on %4:%5, FSID "
5265  "#%6, FSID weightOffset +%7.")
5266  .arg(recChanid)
5267  .arg(recStart.toString(Qt::ISODate))
5268  .arg(recUsage).arg(recHost).arg(recDir)
5269  .arg(fs->getFSysID()).arg(weightOffset));
5270 
5271  // need to offset all directories on this filesystem
5272  for (fsit2 = fsInfoCache.begin();
5273  fsit2 != fsInfoCache.end(); ++fsit2)
5274  {
5275  FileSystemInfo *fs2 = &(*fsit2);
5276  if (fs2->getFSysID() == fs->getFSysID())
5277  {
5278  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO,
5279  QString(" %1:%2 => old weight %3 plus "
5280  "%4 = %5")
5281  .arg(fs2->getHostname())
5282  .arg(fs2->getPath())
5283  .arg(fs2->getWeight())
5284  .arg(weightOffset)
5285  .arg(fs2->getWeight() + weightOffset));
5286 
5287  fs2->setWeight(fs2->getWeight() + weightOffset);
5288  }
5289  }
5290  }
5291  break;
5292  }
5293  }
5294  }
5295  }
5296 
5297  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5298  "FillRecordingDir: Adjusting FS Weights from scheduler.");
5299 
5300  for (RecConstIter recIter = reclist.begin(); recIter != reclist.end(); ++recIter)
5301  {
5302  RecordingInfo *thispg = *recIter;
5303 
5304  if ((recendts < thispg->GetRecordingStartTime()) ||
5305  (recstartts > thispg->GetRecordingEndTime()) ||
5306  (thispg->GetRecordingStatus() != RecStatus::WillRecord &&
5307  thispg->GetRecordingStatus() != RecStatus::Pending) ||
5308  (thispg->GetInputID() == 0) ||
5309  (recsCounted.contains(QString("%1:%2").arg(thispg->GetChanID())
5310  .arg(thispg->GetRecordingStartTime(MythDate::ISODate)))) ||
5311  (thispg->GetPathname().isEmpty()))
5312  continue;
5313 
5314  for (fslistit = fsInfoList.begin();
5315  fslistit != fsInfoList.end(); ++fslistit)
5316  {
5317  FileSystemInfo *fs = *fslistit;
5318  if ((fs->getHostname() == thispg->GetHostname()) &&
5319  (fs->getPath() == thispg->GetPathname()))
5320  {
5321  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO,
5322  QString("%1 @ %2 will record on %3:%4, FSID #%5, "
5323  "weightPerRecording +%6.")
5324  .arg(thispg->GetChanID())
5326  .arg(fs->getHostname()).arg(fs->getPath())
5327  .arg(fs->getFSysID()).arg(weightPerRecording));
5328 
5329  for (fsit2 = fsInfoCache.begin();
5330  fsit2 != fsInfoCache.end(); ++fsit2)
5331  {
5332  FileSystemInfo *fs2 = &(*fsit2);
5333  if (fs2->getFSysID() == fs->getFSysID())
5334  {
5335  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO,
5336  QString(" %1:%2 => old weight %3 plus %4 = %5")
5337  .arg(fs2->getHostname()).arg(fs2->getPath())
5338  .arg(fs2->getWeight()).arg(weightPerRecording)
5339  .arg(fs2->getWeight() + weightPerRecording));
5340 
5341  fs2->setWeight(fs2->getWeight() + weightPerRecording);
5342  }
5343  }
5344  break;
5345  }
5346  }
5347  }
5348 
5349  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO,
5350  QString("Using '%1' Storage Scheduler directory sorting algorithm.")
5351  .arg(storageScheduler));
5352 
5353  if (storageScheduler == "BalancedFreeSpace")
5354  fsInfoList.sort(comp_storage_free_space);
5355  else if (storageScheduler == "BalancedPercFreeSpace")
5356  fsInfoList.sort(comp_storage_perc_free_space);
5357  else if (storageScheduler == "BalancedDiskIO")
5358  fsInfoList.sort(comp_storage_disk_io);
5359  else // default to using original method
5360  fsInfoList.sort(comp_storage_combination);
5361 
5362  if (VERBOSE_LEVEL_CHECK(VB_FILE | VB_SCHEDULE, LOG_INFO))
5363  {
5364  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO,
5365  "--- FillRecordingDir Sorted fsInfoList start ---");
5366  for (fslistit = fsInfoList.begin();fslistit != fsInfoList.end();
5367  ++fslistit)
5368  {
5369  FileSystemInfo *fs = *fslistit;
5370  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, QString("%1:%2")
5371  .arg(fs->getHostname()) .arg(fs->getPath()));
5372  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, QString(" Location : %1")
5373  .arg((fs->isLocal()) ? "local" : "remote"));
5374  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, QString(" weight : %1")
5375  .arg(fs->getWeight()));
5376  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, QString(" free space : %5")
5377  .arg(fs->getFreeSpace()));
5378  }
5379  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO,
5380  "--- FillRecordingDir Sorted fsInfoList end ---");
5381  }
5382 
5383  // This code could probably be expanded to check the actual bitrate the
5384  // recording will record at for analog broadcasts that are encoded locally.
5385  // maxSizeKB is 1/3 larger than required as this is what the auto expire
5386  // uses
5387  EncoderLink *nexttv = (*m_tvList)[cardid];
5388  long long maxByterate = nexttv->GetMaxBitrate() / 8;
5389  long long maxSizeKB = (maxByterate + maxByterate/3) *
5390  recstartts.secsTo(recendts) / 1024;
5391 
5392  bool simulateAutoExpire =
5393  ((gCoreContext->GetSetting("StorageScheduler") == "BalancedFreeSpace") &&
5394  (m_expirer) &&
5395  (fsInfoList.size() > 1));
5396 
5397  // Loop though looking for a directory to put the file in. The first time
5398  // through we look for directories with enough free space in them. If we
5399  // can't find a directory that way we loop through and pick the first good
5400  // one from the list no matter how much free space it has. We assume that
5401  // something will have to be expired for us to finish the recording.
5402  // pass 1: try to fit onto an existing file system with enough free space
5403  // pass 2: fit onto the file system with the lowest priority files to be
5404  // expired this is used only with multiple file systems
5405  // Estimates are made by simulating each expiry until one of
5406  // the file systems has enough sapce to fit the new file.
5407  // pass 3: fit onto the first file system that will take it with lowest
5408  // priority files on this file system expired
5409  for (unsigned int pass = 1; pass <= 3; pass++)
5410  {
5411  bool foundDir = false;
5412 
5413  if ((pass == 2) && simulateAutoExpire)
5414  {
5415  // setup a container of remaining space for all the file systems
5416  QMap <int , long long> remainingSpaceKB;
5417  for (fslistit = fsInfoList.begin();
5418  fslistit != fsInfoList.end(); ++fslistit)
5419  {
5420  remainingSpaceKB[(*fslistit)->getFSysID()] =
5421  (*fslistit)->getFreeSpace();
5422  }
5423 
5424  // get list of expirable programs
5425  pginfolist_t expiring;
5426  m_expirer->GetAllExpiring(expiring);
5427 
5428  for(pginfolist_t::iterator it=expiring.begin();
5429  it != expiring.end(); ++it)
5430  {
5431  // find the filesystem its on
5432  FileSystemInfo *fs = nullptr;
5433  for (fslistit = fsInfoList.begin();
5434  fslistit != fsInfoList.end(); ++fslistit)
5435  {
5436  // recording is not on this filesystem's host
5437  if ((*it)->GetHostname() != (*fslistit)->getHostname())
5438  continue;
5439 
5440  // directory is not in the Storage Group dir list
5441  if (!dirlist.contains((*fslistit)->getPath()))
5442  continue;
5443 
5444  QString filename =
5445  (*fslistit)->getPath() + "/" + (*it)->GetPathname();
5446 
5447  // recording is local
5448  if ((*it)->GetHostname() == gCoreContext->GetHostName())
5449  {
5450  QFile checkFile(filename);
5451 
5452  if (checkFile.exists())
5453  {
5454  fs = *fslistit;
5455  break;
5456  }
5457  }
5458  else // recording is remote
5459  {
5460  QString backuppath = (*it)->GetPathname();
5461  ProgramInfo *programinfo = *it;
5462  bool foundSlave = false;
5463 
5464  QMap<int, EncoderLink *>::Iterator enciter =
5465  m_tvList->begin();
5466  for (; enciter != m_tvList->end(); ++enciter)
5467  {
5468  if ((*enciter)->GetHostName() ==
5469  programinfo->GetHostname())
5470  {
5471  (*enciter)->CheckFile(programinfo);
5472  foundSlave = true;
5473  break;
5474  }
5475  }
5476  if (foundSlave &&
5477  programinfo->GetPathname() == filename)
5478  {
5479  fs = *fslistit;
5480  programinfo->SetPathname(backuppath);
5481  break;
5482  }
5483  programinfo->SetPathname(backuppath);
5484  }
5485  }
5486 
5487  if (!fs)
5488  {
5489  LOG(VB_GENERAL, LOG_ERR,
5490  QString("Unable to match '%1' "
5491  "to any file system. Ignoring it.")
5492  .arg((*it)->GetBasename()));
5493  continue;
5494  }
5495 
5496  // add this files size to the remaining free space
5497  remainingSpaceKB[fs->getFSysID()] +=
5498  (*it)->GetFilesize() / 1024;
5499 
5500  // check if we have enough space for new file
5501  long long desiredSpaceKB =
5503 
5504  if (remainingSpaceKB[fs->getFSysID()] >
5505  (desiredSpaceKB + maxSizeKB))
5506  {
5507  recording_dir = fs->getPath();
5508  fsID = fs->getFSysID();
5509 
5510  LOG(VB_FILE, LOG_INFO,
5511  QString("pass 2: '%1' will record in '%2' "
5512  "although there is only %3 MB free and the "
5513  "AutoExpirer wants at least %4 MB. This "
5514  "directory has the highest priority files "
5515  "to be expired from the AutoExpire list and "
5516  "there are enough that the Expirer should "
5517  "be able to free up space for this recording.")
5518  .arg(title).arg(recording_dir)
5519  .arg(fs->getFreeSpace() / 1024)
5520  .arg(desiredSpaceKB / 1024));
5521 
5522  foundDir = true;
5523  break;
5524  }
5525  }
5526 
5527  m_expirer->ClearExpireList(expiring);
5528  }
5529  else // passes 1 & 3 (or 1 & 2 if !simulateAutoExpire)
5530  {
5531  for (fslistit = fsInfoList.begin();
5532  fslistit != fsInfoList.end(); ++fslistit)
5533  {
5534  long long desiredSpaceKB = 0;
5535  FileSystemInfo *fs = *fslistit;
5536  if (m_expirer)
5537  desiredSpaceKB =
5539 
5540  if ((fs->getHostname() == hostname) &&
5541  (dirlist.contains(fs->getPath())) &&
5542  ((pass > 1) ||
5543  (fs->getFreeSpace() > (desiredSpaceKB + maxSizeKB))))
5544  {
5545  recording_dir = fs->getPath();
5546  fsID = fs->getFSysID();
5547 
5548  if (pass == 1)
5549  LOG(VB_FILE, LOG_INFO,
5550  QString("pass 1: '%1' will record in "
5551  "'%2' which has %3 MB free. This recording "
5552  "could use a max of %4 MB and the "
5553  "AutoExpirer wants to keep %5 MB free.")
5554  .arg(title)
5555  .arg(recording_dir)
5556  .arg(fs->getFreeSpace() / 1024)
5557  .arg(maxSizeKB / 1024)
5558  .arg(desiredSpaceKB / 1024));
5559  else
5560  LOG(VB_FILE, LOG_INFO,
5561  QString("pass %1: '%2' will record in "
5562  "'%3' although there is only %4 MB free and "
5563  "the AutoExpirer wants at least %5 MB. "
5564  "Something will have to be deleted or expired "
5565  "in order for this recording to complete "
5566  "successfully.")
5567  .arg(pass).arg(title)
5568  .arg(recording_dir)
5569  .arg(fs->getFreeSpace() / 1024)
5570  .arg(desiredSpaceKB / 1024));
5571 
5572  foundDir = true;
5573  break;
5574  }
5575  }
5576  }
5577 
5578  if (foundDir)
5579  break;
5580  }
5581 
5582  LOG(VB_SCHEDULE, LOG_INFO, LOC + "FillRecordingDir: Finished");
5583  return fsID;
5584 }
5585 
5587 {
5588  QList<FileSystemInfo> fsInfos;
5589 
5590  fsInfoCache.clear();
5591 
5592  if (m_mainServer)
5593  m_mainServer->GetFilesystemInfos(fsInfos, true);
5594 
5595  QMap <int, bool> fsMap;
5596  QList<FileSystemInfo>::iterator it1;
5597  for (it1 = fsInfos.begin(); it1 != fsInfos.end(); ++it1)
5598  {
5599  fsMap[it1->getFSysID()] = true;
5600  fsInfoCache[it1->getHostname() + ":" + it1->getPath()] = *it1;
5601  }
5602 
5603  LOG(VB_FILE, LOG_INFO, LOC +
5604  QString("FillDirectoryInfoCache: found %1 unique filesystems")
5605  .arg(fsMap.size()));
5606 }
5607 
5609 {
5610  int prerollseconds = gCoreContext->GetNumSetting("RecordPreRoll", 0);
5611  QDateTime curtime = MythDate::current();
5612  int secsleft = curtime.secsTo(livetvTime);
5613 
5614  // This check needs to be longer than the related one in
5615  // HandleRecording().
5616  if (secsleft - prerollseconds > 120)
5617  return;
5618 
5619  // Build a list of active livetv programs
5620  QMap<int, EncoderLink *>::Iterator enciter = m_tvList->begin();
5621  for (; enciter != m_tvList->end(); ++enciter)
5622  {
5623  EncoderLink *enc = *enciter;
5624 
5625  if (kState_WatchingLiveTV != enc->GetState())
5626  continue;
5627 
5628  InputInfo in;
5629  enc->IsBusy(&in);
5630 
5631  if (!in.inputid)
5632  continue;
5633 
5634  // Get the program that will be recording on this channel at
5635  // record start time and assume this LiveTV session continues
5636  // for at least another 30 minutes from now.
5637  RecordingInfo *dummy = new RecordingInfo(
5638  in.chanid, livetvTime, true, 4);
5640  if (schedTime.secsTo(dummy->GetRecordingEndTime()) < 1800)
5641  dummy->SetRecordingEndTime(schedTime.addSecs(1800));
5642  dummy->SetInputID(enc->GetInputID());
5643  dummy->mplexid = dummy->QueryMplexID();
5644  dummy->sgroupid = sinputinfomap[dummy->GetInputID()].sgroupid;
5646 
5647  livetvlist.push_front(dummy);
5648  }
5649 
5650  if (livetvlist.empty())
5651  return;
5652 
5653  SchedNewRetryPass(livetvlist.begin(), livetvlist.end(), false, true);
5654 
5655  while (!livetvlist.empty())
5656  {
5657  RecordingInfo *p = livetvlist.back();
5658  delete p;
5659  livetvlist.pop_back();
5660  }
5661 }
5662 
5663 /* Determines if the system was started by the auto-wakeup process */
5665 {
5666  bool autoStart = false;
5667 
5668  QDateTime startupTime = QDateTime();
5669  QString s = gCoreContext->GetSetting("MythShutdownWakeupTime", "");
5670  if (s.length())
5671  startupTime = MythDate::fromString(s);
5672 
5673  // if we don't have a valid startup time assume we were started manually
5674  if (startupTime.isValid())
5675  {
5676  int startupSecs = gCoreContext->GetNumSetting("StartupSecsBeforeRecording");
5677  // If we started within 'StartupSecsBeforeRecording' OR 15 minutes
5678  // of the saved wakeup time assume we either started automatically
5679  // to record, to obtain guide data or or for a
5680  // daily wakeup/shutdown period
5681  if (abs(startupTime.secsTo(MythDate::current())) <
5682  max(startupSecs, 15 * 60))
5683  {
5684  LOG(VB_GENERAL, LOG_INFO,
5685  "Close to auto-start time, AUTO-Startup assumed");
5686 
5687  QString str = gCoreContext->GetSetting("MythFillSuggestedRunTime");
5688  QDateTime guideRunTime = MythDate::fromString(str);
5689  if (guideRunTime.secsTo(MythDate::current()) <
5690  max(startupSecs, 15 * 60))
5691  {
5692  LOG(VB_GENERAL, LOG_INFO,
5693  "Close to MythFillDB suggested run time, AUTO-Startup to fetch guide data?");
5694  }
5695  autoStart = true;
5696  }
5697  else
5698  LOG(VB_GENERAL, LOG_DEBUG,
5699  "NOT close to auto-start time, USER-initiated startup assumed");
5700  }
5701  else if (!s.isEmpty())
5702  LOG(VB_GENERAL, LOG_ERR, LOC +
5703  QString("Invalid MythShutdownWakeupTime specified in database (%1)")
5704  .arg(s));
5705 
5706  return autoStart;
5707 }
5708 
5710 {
5711  // For each input, create a set containing all of the inputs
5712  // (including itself) that are grouped with it.
5713  MSqlQuery query(MSqlQuery::InitCon());
5714  QMap<uint, QSet<uint> > inputSets;
5715  query.prepare("SELECT DISTINCT ci1.cardid, ci2.cardid "
5716  "FROM capturecard ci1, capturecard ci2, "
5717  " inputgroup ig1, inputgroup ig2 "
5718  "WHERE ci1.cardid = ig1.cardinputid AND "
5719  " ci2.cardid = ig2.cardinputid AND"
5720  " ig1.inputgroupid = ig2.inputgroupid AND "
5721  " ci1.cardid <= ci2.cardid "
5722  "ORDER BY ci1.cardid, ci2.cardid");
5723  if (!query.exec())
5724  {
5725  MythDB::DBError("CreateConflictLists1", query);
5726  return false;
5727  }
5728  while (query.next())
5729  {
5730  uint id0 = query.value(0).toUInt();
5731  uint id1 = query.value(1).toUInt();
5732  inputSets[id0].insert(id1);
5733  inputSets[id1].insert(id0);
5734  }
5735 
5736  QMap<uint, QSet<uint> >::iterator mit;
5737  for (mit = inputSets.begin(); mit != inputSets.end(); ++mit)
5738  {
5739  uint inputid = mit.key();
5740  if (sinputinfomap[inputid].conflictlist)
5741  continue;
5742 
5743  // Find the union of all inputs grouped with those already in
5744  // the set. Keep doing so until no new inputs get added.
5745  // This might not be the most efficient way, but it's simple
5746  // and more than fast enough for our needs.
5747  QSet<uint> fullset = mit.value();
5748  QSet<uint> checkset;
5749  QSet<uint>::const_iterator sit;
5750  while (checkset != fullset)
5751  {
5752  checkset = fullset;
5753  for (sit = checkset.begin(); sit != checkset.end(); ++sit)
5754  fullset += inputSets[*sit];
5755  }
5756 
5757  // Create a new conflict list for the resulting set of inputs
5758  // and point each inputs list at it.
5759  RecList *conflictlist = new RecList();
5760  conflictlists.push_back(conflictlist);
5761  for (sit = checkset.begin(); sit != checkset.end(); ++sit)
5762  {
5763  LOG(VB_SCHEDULE, LOG_INFO,
5764  QString("Assigning input %1 to conflict set %2")