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