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