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