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