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