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