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