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