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