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