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