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