MythTV master
autoexpire.cpp
Go to the documentation of this file.
1// System headers
2#include <sys/stat.h>
3#ifdef __linux__
4# include <sys/vfs.h>
5#else // if !__linux__
6# include <sys/param.h>
7# ifndef _WIN32
8# include <sys/mount.h>
9# endif // _WIN32
10#endif // !__linux__
11
12// POSIX headers
13#include <unistd.h>
14
15// C++ headers
16#include <algorithm>
17#include <cstdlib>
18#include <iostream>
19
20// Qt headers
21#include <QDateTime>
22#include <QFileInfo>
23#include <QList>
24
25// MythTV headers
26#include "libmythbase/compat.h"
30#include "libmythbase/mythdb.h"
36#include "libmythtv/tv_rec.h"
37
38// MythBackend
39#include "autoexpire.h"
40#include "backendcontext.h"
41#include "encoderlink.h"
42#include "mainserver.h"
43
44#define LOC QString("AutoExpire: ")
45#define LOC_ERR QString("AutoExpire Error: ")
46
50static constexpr uint64_t kSpaceTooBigKB { 3ULL * 1024 * 1024 };
51
52// Consider recordings within the last two hours to be too recent to
53// add to the autoexpire list.
54static constexpr int64_t kRecentInterval { 2LL * 60 * 60 };
55
58{
59 RunProlog();
60 m_parent->RunExpirer();
61 RunEpilog();
62}
63
73AutoExpire::AutoExpire(QMap<int, EncoderLink *> *tvList) :
74 m_encoderList(tvList),
75 m_expireThread(new ExpireThread(this)),
76 m_expireThreadRun(true)
77{
80}
81
86{
87 {
88 QMutexLocker locker(&m_instanceLock);
89 m_expireThreadRun = false;
90 m_instanceCond.wakeAll();
91 }
92
93 {
94 QMutexLocker locker(&m_updateLock);
95 m_updateQueue.clear();
96 }
97
99 {
102 delete m_expireThread;
103 m_expireThread = nullptr;
104 }
105}
106
112uint64_t AutoExpire::GetDesiredSpace(int fsID) const
113{
114 QMutexLocker locker(&m_instanceLock);
115 if (m_desiredSpace.contains(fsID))
116 return m_desiredSpace[fsID];
117 return 0;
118}
119
124{
125 LOG(VB_FILE, LOG_INFO, LOC + "CalcParams()");
126
127 FileSystemInfoList fsInfos;
128
129 m_instanceLock.lock();
130 if (m_mainServer)
131 {
132 // The scheduler relies on something forcing the mainserver
133 // fsinfos cache to get updated periodically. Currently, that
134 // is done here. Don't remove or change this invocation
135 // without handling that issue too. It is done this way
136 // because the scheduler thread can't afford to be blocked by
137 // an unresponsive, remote filesystem and the autoexpirer
138 // thread can.
139 m_mainServer->GetFilesystemInfos(fsInfos, false);
140 }
141 m_instanceLock.unlock();
142
143 if (fsInfos.empty())
144 {
145 LOG(VB_GENERAL, LOG_ERR, LOC + "Filesystem Info cache is empty, unable "
146 "to calculate necessary parameters.");
147 return;
148 }
149
150 uint64_t maxKBperMin = 0;
151 uint64_t extraKB = static_cast<uint64_t>
152 (gCoreContext->GetNumSetting("AutoExpireExtraSpace", 0))
153 << 20;
154
155 QMap<int, uint64_t> fsMap;
156 QMap<int, std::vector<int> > fsEncoderMap;
157
158 // We use this copying on purpose. The used_encoders map ensures
159 // that every encoder writes only to one fs.
160 // Copying the data minimizes the time the lock is held.
161 m_instanceLock.lock();
162 QMap<int, int>::const_iterator ueit = m_usedEncoders.cbegin();
163 while (ueit != m_usedEncoders.cend())
164 {
165 fsEncoderMap[*ueit].push_back(ueit.key());
166 ++ueit;
167 }
168 m_instanceLock.unlock();
169
170 for (const auto& fs : std::as_const(fsInfos))
171 {
172 if (fsMap.contains(fs.getFSysID()))
173 continue;
174
175 fsMap[fs.getFSysID()] = 0;
176 uint64_t thisKBperMin = 0;
177
178 // append unknown recordings to all fsIDs
179 for (auto unknownfs : std::as_const(fsEncoderMap[-1]))
180 fsEncoderMap[fs.getFSysID()].push_back(unknownfs);
181
182 if (fsEncoderMap.contains(fs.getFSysID()))
183 {
184 LOG(VB_FILE, LOG_INFO,
185 QString("fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
186 .arg(fs.getFSysID())
187 .arg(fs.getTotalSpace() / 1024.0 / 1024.0, 7, 'f', 1)
188 .arg(fs.getUsedSpace() / 1024.0 / 1024.0, 7, 'f', 1)
189 .arg(fs.getFreeSpace() / 1024.0 / 1024.0, 7, 'f', 1));
190
191 for (auto cardid : std::as_const(fsEncoderMap[fs.getFSysID()]))
192 {
193 auto iter = m_encoderList->constFind(cardid);
194 if (iter == m_encoderList->constEnd())
195 continue;
196 EncoderLink *enc = *iter;
197
198 if (!enc->IsConnected() || !enc->IsBusy())
199 {
200 // remove encoder since it can't write to any file system
201 LOG(VB_FILE, LOG_INFO, LOC +
202 QString("Cardid %1: is not recording, removing it "
203 "from used list.").arg(cardid));
204 m_instanceLock.lock();
205 m_usedEncoders.remove(cardid);
206 m_instanceLock.unlock();
207 continue;
208 }
209
210 uint64_t maxBitrate = enc->GetMaxBitrate();
211 if (maxBitrate==0)
212 maxBitrate = 19500000LL;
213 thisKBperMin += (maxBitrate*((uint64_t)15))>>11;
214 LOG(VB_FILE, LOG_INFO, QString(" Cardid %1: max bitrate "
215 "%2 Kb/sec, fsID %3 max is now %4 KB/min")
216 .arg(enc->GetInputID())
217 .arg(enc->GetMaxBitrate() >> 10)
218 .arg(fs.getFSysID())
219 .arg(thisKBperMin));
220 }
221 }
222 fsMap[fs.getFSysID()] = thisKBperMin;
223
224 if (thisKBperMin > maxKBperMin)
225 {
226 LOG(VB_FILE, LOG_INFO,
227 QString(" Max of %1 KB/min for fsID %2 is higher "
228 "than the existing Max of %3 so we'll use this Max instead")
229 .arg(thisKBperMin).arg(fs.getFSysID()).arg(maxKBperMin));
230 maxKBperMin = thisKBperMin;
231 }
232 }
233
234 // Determine frequency to run autoexpire so it doesn't have to free
235 // too much space
236 uint expireFreq = 15;
237 if (maxKBperMin > 0)
238 {
239 expireFreq = kSpaceTooBigKB / (maxKBperMin + maxKBperMin/3);
240 expireFreq = std::clamp(expireFreq, 3U, 15U);
241 }
242
243 double expireMinGB = ((maxKBperMin + maxKBperMin/3)
244 * expireFreq + extraKB) >> 20;
245 LOG(VB_GENERAL, LOG_NOTICE, LOC +
246 QString("CalcParams(): Max required Free Space: %1 GB w/freq: %2 min")
247 .arg(expireMinGB, 0, 'f', 1).arg(expireFreq));
248
249 // lock class and save these parameters.
250 m_instanceLock.lock();
251 m_desiredFreq = expireFreq;
252 // write per file system needed space back, use safety of 33%
253 QMap<int, uint64_t>::iterator it = fsMap.begin();
254 while (it != fsMap.end())
255 {
256 m_desiredSpace[it.key()] = ((*it + *it/3) * expireFreq) + extraKB;
257 ++it;
258 }
259 m_instanceLock.unlock();
260}
261
271{
272 QElapsedTimer timer;
273 QDateTime curTime;
274 QDateTime next_expire = MythDate::current().addSecs(60);
275
276 QMutexLocker locker(&m_instanceLock);
277
278 // wait a little for main server to come up and things to settle down
279 Sleep(20s);
280
281 timer.start();
282
283 while (m_expireThreadRun)
284 {
285 TVRec::s_inputsLock.lockForRead();
286
287 curTime = MythDate::current();
288 // recalculate auto expire parameters
289 if (curTime >= next_expire)
290 {
291 m_updateLock.lock();
292 while (!m_updateQueue.empty())
293 {
294 UpdateEntry ue = m_updateQueue.dequeue();
295 if (ue.m_encoder > 0)
296 m_usedEncoders[ue.m_encoder] = ue.m_fsID; // clazy:exclude=readlock-detaching
297 }
298 m_updateLock.unlock();
299
300 locker.unlock();
301 CalcParams();
302 locker.relock();
304 break;
305 }
306 timer.restart();
307
309
310 // Expire Short LiveTV files for this backend every 2 minutes
311 if ((curTime.time().minute() % 2) == 0)
313
314 // Expire normal recordings depending on frequency calculated
315 if (curTime >= next_expire)
316 {
317 LOG(VB_FILE, LOG_INFO, LOC + "Running now!");
318 next_expire =
319 MythDate::current().addSecs(m_desiredFreq * 60LL);
320
322
323 int maxAge = gCoreContext->GetNumSetting("DeletedMaxAge", 0);
324 if (maxAge > 0)
326 else if (maxAge == 0)
328
330
332 }
333
334 TVRec::s_inputsLock.unlock();
335
336 Sleep(60s - std::chrono::milliseconds(timer.elapsed()));
337 }
338}
339
346void AutoExpire::Sleep(std::chrono::milliseconds sleepTime)
347{
348 if (sleepTime <= 0ms)
349 return;
350
351 QDateTime little_tm = MythDate::current().addMSecs(sleepTime.count());
352 std::chrono::milliseconds timeleft = sleepTime;
353 while (m_expireThreadRun && (timeleft > 0ms))
354 {
355 m_instanceCond.wait(&m_instanceLock, timeleft.count());
356 timeleft = MythDate::secsInFuture(little_tm);
357 }
358}
359
364{
365 pginfolist_t expireList;
366
367 LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireLiveTV(%1)").arg(type));
368 FillDBOrdered(expireList, type);
369 SendDeleteMessages(expireList);
370 ClearExpireList(expireList);
371}
372
377{
378 pginfolist_t expireList;
379
380 LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireOldDeleted()"));
382 SendDeleteMessages(expireList);
383 ClearExpireList(expireList);
384}
385
390{
391 pginfolist_t expireList;
392
393 LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireQuickDeleted()"));
395 SendDeleteMessages(expireList);
396 ClearExpireList(expireList);
397}
398
404{
405 pginfolist_t expireList;
406 pginfolist_t deleteList;
407 FileSystemInfoList fsInfos;
408
409 LOG(VB_FILE, LOG_INFO, LOC + "ExpireRecordings()");
410
411 if (m_mainServer)
412 m_mainServer->GetFilesystemInfos(fsInfos, true);
413
414 if (fsInfos.empty())
415 {
416 LOG(VB_GENERAL, LOG_ERR, LOC + "Filesystem Info cache is empty, unable "
417 "to determine what Recordings to expire");
418
419 return;
420 }
421
422 FillExpireList(expireList);
423
424 QMap <int, bool> truncateMap;
426 query.prepare("SELECT DISTINCT rechost, recdir "
427 "FROM inuseprograms "
428 "WHERE recusage = 'truncatingdelete' "
429 "AND lastupdatetime > DATE_ADD(NOW(), INTERVAL -2 MINUTE);");
430
431 if (!query.exec())
432 {
433 MythDB::DBError(LOC + "ExpireRecordings", query);
434 }
435 else
436 {
437 while (query.next())
438 {
439 QString rechost = query.value(0).toString();
440 QString recdir = query.value(1).toString();
441
442 LOG(VB_FILE, LOG_INFO, LOC +
443 QString("%1:%2 has an in-progress truncating delete.")
444 .arg(rechost, recdir));
445
446 for (const auto& fs : std::as_const(fsInfos))
447 {
448 if ((fs.getHostname() == rechost) &&
449 (fs.getPath() == recdir))
450 {
451 truncateMap[fs.getFSysID()] = true;
452 break;
453 }
454 }
455 }
456 }
457
458 QMap <int, bool> fsMap;
459 for (
460#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
461 auto* fsit = fsInfos.begin();
462#else
463 auto fsit = fsInfos.begin();
464#endif
465fsit != fsInfos.end(); ++fsit)
466 {
467 if (fsMap.contains(fsit->getFSysID()))
468 continue;
469
470 fsMap[fsit->getFSysID()] = true;
471
472 LOG(VB_FILE, LOG_INFO,
473 QString("fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
474 .arg(fsit->getFSysID())
475 .arg(fsit->getTotalSpace() / 1024.0 / 1024.0, 7, 'f', 1)
476 .arg(fsit->getUsedSpace() / 1024.0 / 1024.0, 7, 'f', 1)
477 .arg(fsit->getFreeSpace() / 1024.0 / 1024.0, 7, 'f', 1));
478
479 if ((fsit->getTotalSpace() == -1) || (fsit->getUsedSpace() == -1))
480 {
481 LOG(VB_FILE, LOG_ERR, LOC +
482 QString("fsID #%1 has invalid info, AutoExpire cannot run for "
483 "this filesystem. Continuing on to next...")
484 .arg(fsit->getFSysID()));
485 LOG(VB_FILE, LOG_INFO, QString("Directories on filesystem ID %1:")
486 .arg(fsit->getFSysID()));
487 for (
488#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
489 auto* fsit2 = fsInfos.begin();
490#else
491 auto fsit2 = fsInfos.begin();
492#endif
493 fsit2 != fsInfos.end(); ++fsit2)
494 {
495 if (fsit2->getFSysID() == fsit->getFSysID())
496 {
497 LOG(VB_FILE, LOG_INFO, QString(" %1:%2")
498 .arg(fsit2->getHostname(), fsit2->getPath()));
499 }
500 }
501
502 continue;
503 }
504
505 if (truncateMap.contains(fsit->getFSysID()))
506 {
507 LOG(VB_FILE, LOG_INFO,
508 QString(" fsid %1 has a truncating delete in progress, "
509 "AutoExpire cannot run for this filesystem until the "
510 "delete has finished. Continuing on to next...")
511 .arg(fsit->getFSysID()));
512 continue;
513 }
514
515 if (std::max((int64_t)0LL, fsit->getFreeSpace()) <
516 m_desiredSpace[fsit->getFSysID()])
517 {
518 LOG(VB_FILE, LOG_INFO,
519 QString(" Not Enough Free Space! We want %1 MB")
520 .arg(m_desiredSpace[fsit->getFSysID()] / 1024));
521
522 QMap<QString, int> dirList;
523
524 LOG(VB_FILE, LOG_INFO,
525 QString(" Directories on filesystem ID %1:")
526 .arg(fsit->getFSysID()));
527
528 for (const auto& fs2 : std::as_const(fsInfos))
529 {
530 if (fs2.getFSysID() == fsit->getFSysID())
531 {
532 LOG(VB_FILE, LOG_INFO, QString(" %1:%2")
533 .arg(fs2.getHostname(), fs2.getPath()));
534 dirList[fs2.getHostname() + ":" + fs2.getPath()] = 1;
535 }
536 }
537
538 LOG(VB_FILE, LOG_INFO,
539 " Searching for files expirable in these directories");
540 QString myHostName = gCoreContext->GetHostName();
541 auto it = expireList.begin();
542 while ((it != expireList.end()) &&
543 (std::max((int64_t)0LL, fsit->getFreeSpace()) <
544 m_desiredSpace[fsit->getFSysID()]))
545 {
546 ProgramInfo *p = *it;
547 ++it;
548
549 LOG(VB_FILE, LOG_INFO, QString(" Checking %1 => %2")
550 .arg(p->toString(ProgramInfo::kRecordingKey),
551 p->GetTitle()));
552
553 if (!p->IsLocal())
554 {
555 bool foundFile = false;
556 auto eit = m_encoderList->constBegin();
557 while (eit != m_encoderList->constEnd())
558 {
559 EncoderLink *el = *eit;
560 eit++;
561
562 if ((p->GetHostname() == el->GetHostName()) ||
563 ((p->GetHostname() == myHostName) &&
564 (el->IsLocal())))
565 {
566 if (el->IsConnected())
567 foundFile = el->CheckFile(p);
568
569 eit = m_encoderList->constEnd();
570 }
571 }
572
573 if (!foundFile && (p->GetHostname() != myHostName))
574 {
575 // Wasn't found so check locally
576 QString file = GetPlaybackURL(p);
577
578 if (file.startsWith("/"))
579 {
580 p->SetPathname(file);
581 p->SetHostname(myHostName);
582 foundFile = true;
583 }
584 }
585
586 if (!foundFile)
587 {
588 LOG(VB_FILE, LOG_ERR, LOC +
589 QString(" ERROR: Can't find file for %1")
590 .arg(p->toString(ProgramInfo::kRecordingKey)));
591 continue;
592 }
593 }
594
595 QFileInfo vidFile(p->GetPathname());
596 if (dirList.contains(p->GetHostname() + ':' + vidFile.path()))
597 {
598 fsit->setUsedSpace(fsit->getUsedSpace()
599 - (p->GetFilesize() / 1024));
600 deleteList.push_back(p);
601
602 LOG(VB_FILE, LOG_INFO,
603 QString(" FOUND file expirable. "
604 "%1 is located at %2 which is on fsID #%3. "
605 "Adding to deleteList. After deleting we "
606 "should have %4 MB free on this filesystem.")
607 .arg(p->toString(ProgramInfo::kRecordingKey),
608 p->GetPathname()).arg(fsit->getFSysID())
609 .arg(fsit->getFreeSpace() / 1024));
610 }
611 }
612 }
613 }
614
615 SendDeleteMessages(deleteList);
616
617 ClearExpireList(deleteList, false);
618 ClearExpireList(expireList);
619}
620
625{
626 QString msg;
627
628 if (deleteList.empty())
629 {
630 LOG(VB_FILE, LOG_INFO, LOC + "SendDeleteMessages. Nothing to expire.");
631 return;
632 }
633
634 LOG(VB_FILE, LOG_INFO, LOC +
635 "SendDeleteMessages, cycling through deleteList.");
636 auto it = deleteList.begin();
637 while (it != deleteList.end())
638 {
639 msg = QString("%1Expiring %2 MB for %3 => %4")
640 .arg(VERBOSE_LEVEL_CHECK(VB_FILE, LOG_ANY) ? " " : "",
641 QString::number((*it)->GetFilesize() >> 20),
642 (*it)->toString(ProgramInfo::kRecordingKey),
643 (*it)->toString(ProgramInfo::kTitleSubtitle));
644
645 LOG(VB_GENERAL, LOG_NOTICE, msg);
646
647 // send auto expire message to backend's event thread.
648 MythEvent me(QString("AUTO_EXPIRE %1 %2").arg((*it)->GetChanID())
649 .arg((*it)->GetRecordingStartTime(MythDate::ISODate)));
651
652 ++it; // move on to next program
653 }
654}
655
662{
663 QMap<QString, int> maxEpisodes;
664 QMap<QString, int>::Iterator maxIter;
665 QMap<QString, int> episodeParts;
666 QString episodeKey;
667
669 query.prepare("SELECT recordid, maxepisodes, title "
670 "FROM record WHERE maxepisodes > 0 "
671 "ORDER BY recordid ASC, maxepisodes DESC");
672
673 if (query.exec() && query.isActive() && query.size() > 0)
674 {
675 LOG(VB_FILE, LOG_INFO, LOC +
676 QString("Found %1 record profiles using max episode expiration")
677 .arg(query.size()));
678 while (query.next())
679 {
680 LOG(VB_FILE, LOG_INFO, QString(" %1 (%2 for rec id %3)")
681 .arg(query.value(2).toString())
682 .arg(query.value(1).toInt())
683 .arg(query.value(0).toInt()));
684 maxEpisodes[query.value(0).toString()] = query.value(1).toInt();
685 }
686 }
687
688 LOG(VB_FILE, LOG_INFO, LOC +
689 "Checking episode count for each recording profile using max episodes");
690 for (maxIter = maxEpisodes.begin(); maxIter != maxEpisodes.end(); ++maxIter)
691 {
692 query.prepare("SELECT chanid, starttime, title, progstart, progend, "
693 "duplicate "
694 "FROM recorded "
695 "WHERE recordid = :RECID AND preserve = 0 "
696 "AND recgroup NOT IN ('LiveTV', 'Deleted') "
697 "ORDER BY starttime DESC;");
698 query.bindValue(":RECID", maxIter.key());
699
700 if (!query.exec() || !query.isActive())
701 {
702 MythDB::DBError("AutoExpire query failed!", query);
703 continue;
704 }
705
706 LOG(VB_FILE, LOG_INFO, QString(" Recordid %1 has %2 recordings.")
707 .arg(maxIter.key())
708 .arg(query.size()));
709 if (query.size() > 0)
710 {
711 int found = 1;
712 while (query.next())
713 {
714 uint chanid = query.value(0).toUInt();
715 QDateTime startts = MythDate::as_utc(query.value(1).toDateTime());
716 QString title = query.value(2).toString();
717 QDateTime progstart = MythDate::as_utc(query.value(3).toDateTime());
718 QDateTime progend = MythDate::as_utc(query.value(4).toDateTime());
719 int duplicate = query.value(5).toInt();
720
721 episodeKey = QString("%1_%2_%3")
722 .arg(QString::number(chanid),
723 progstart.toString(Qt::ISODate),
724 progend.toString(Qt::ISODate));
725
726 if ((!IsInDontExpireSet(chanid, startts)) &&
727 (!episodeParts.contains(episodeKey)) &&
728 (found > *maxIter))
729 {
730 QString msg =
731 QString("%1Deleting %2 at %3 => %4. "
732 "Too many episodes, we only want to keep %5.")
733 .arg(VERBOSE_LEVEL_CHECK(VB_FILE, LOG_ANY) ? " " : "",
734 QString::number(chanid),
735 startts.toString(Qt::ISODate),
736 title,
737 QString::number(*maxIter));
738
739 LOG(VB_GENERAL, LOG_NOTICE, msg);
740
741 // allow re-record if auto expired
742 RecordingInfo recInfo(chanid, startts);
743 if (gCoreContext->GetBoolSetting("RerecordWatched", false) ||
744 !recInfo.IsWatched())
745 {
746 recInfo.ForgetHistory();
747 }
748 msg = QString("DELETE_RECORDING %1 %2")
749 .arg(chanid)
750 .arg(startts.toString(Qt::ISODate));
751
752 MythEvent me(msg);
754 }
755 else
756 {
757 // keep track of shows we haven't expired so we can
758 // make sure we don't expire another part of the same
759 // episode.
760 if (episodeParts.contains(episodeKey))
761 {
762 episodeParts[episodeKey] = episodeParts[episodeKey] + 1;
763 }
764 else
765 {
766 episodeParts[episodeKey] = 1;
767 if( duplicate )
768 found++;
769 }
770 }
771 }
772 }
773 }
774}
775
781{
782 int expMethod = gCoreContext->GetNumSetting("AutoExpireMethod", 1);
783
784 ClearExpireList(expireList);
785
787
788 switch(expMethod)
789 {
790 case emOldestFirst:
793 FillDBOrdered(expireList, expMethod);
794 break;
795 // default falls through so list is empty so no AutoExpire
796 }
797}
798
802void AutoExpire::PrintExpireList(const QString& expHost)
803{
804 pginfolist_t expireList;
805
806 FillExpireList(expireList);
807
808 QString msg = "MythTV AutoExpire List ";
809 if (expHost != "ALL")
810 msg += QString("for '%1' ").arg(expHost);
811 msg += "(programs listed in order of expiration)";
812 std::cout << msg.toLocal8Bit().constData() << std::endl;
813
814 for (auto *first : expireList)
815 {
816 if (expHost != "ALL" && first->GetHostname() != expHost)
817 continue;
818
819 QString title = first->toString(ProgramInfo::kTitleSubtitle);
820 title = title.leftJustified(39, ' ', true);
821
822 QString outstr = QString("%1 %2 MB %3 [%4]")
823 .arg(title,
824 QString::number(first->GetFilesize() >> 20)
825 .rightJustified(5, ' ', true),
826 first->GetRecordingStartTime(MythDate::ISODate)
827 .leftJustified(24, ' ', true),
828 QString::number(first->GetRecordingPriority())
829 .rightJustified(3, ' ', true));
830 QByteArray out = outstr.toLocal8Bit();
831
832 std::cout << out.constData() << std::endl;
833 }
834
835 ClearExpireList(expireList);
836}
837
841void AutoExpire::GetAllExpiring(QStringList &strList)
842{
843 QMutexLocker lockit(&m_instanceLock);
844 pginfolist_t expireList;
845
847
851 FillDBOrdered(expireList, gCoreContext->GetNumSetting("AutoExpireMethod",
853
854 strList << QString::number(expireList.size());
855
856 for (auto & info : expireList)
857 info->ToStringList(strList);
858
859 ClearExpireList(expireList);
860}
861
866{
867 QMutexLocker lockit(&m_instanceLock);
868 pginfolist_t expireList;
869
871
875 FillDBOrdered(expireList, gCoreContext->GetNumSetting("AutoExpireMethod",
877
878 for (auto & info : expireList)
879 list.push_back( new ProgramInfo( *info ));
880
881 ClearExpireList(expireList);
882}
883
887void AutoExpire::ClearExpireList(pginfolist_t &expireList, bool deleteProg)
888{
889 ProgramInfo *pginfo = nullptr;
890 while (!expireList.empty())
891 {
892 if (deleteProg)
893 pginfo = expireList.back();
894
895 expireList.pop_back();
896
897 if (deleteProg)
898 delete pginfo;
899 }
900}
901
906void AutoExpire::FillDBOrdered(pginfolist_t &expireList, int expMethod)
907{
908 QString where;
909 QString orderby;
910 QString msg;
911 int maxAge = 0;
912
913 switch (expMethod)
914 {
915 default:
916 case emOldestFirst:
917 msg = "Adding programs expirable in Oldest First order";
918 where = "autoexpire > 0";
919 if (gCoreContext->GetBoolSetting("AutoExpireWatchedPriority", false))
920 orderby = "recorded.watched DESC, ";
921 orderby += "starttime ASC";
922 break;
924 msg = "Adding programs expirable in Lowest Priority First order";
925 where = "autoexpire > 0";
926 if (gCoreContext->GetBoolSetting("AutoExpireWatchedPriority", false))
927 orderby = "recorded.watched DESC, ";
928 orderby += "recorded.recpriority ASC, starttime ASC";
929 break;
931 msg = "Adding programs expirable in Weighted Time Priority order";
932 where = "autoexpire > 0";
933 if (gCoreContext->GetBoolSetting("AutoExpireWatchedPriority", false))
934 orderby = "recorded.watched DESC, ";
935 orderby += QString("DATE_ADD(starttime, INTERVAL '%1' * "
936 "recorded.recpriority DAY) ASC")
937 .arg(gCoreContext->GetNumSetting("AutoExpireDayPriority", 3));
938 break;
940 msg = "Adding Short LiveTV programs in starttime order";
941 where = "recgroup = 'LiveTV' "
942 "AND endtime < DATE_ADD(starttime, INTERVAL '30' SECOND) "
943 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ";
944 orderby = "starttime ASC";
945 break;
947 msg = "Adding LiveTV programs in starttime order";
948 where = QString("recgroup = 'LiveTV' "
949 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
950 .arg(gCoreContext->GetNumSetting("AutoExpireLiveTVMaxAge", 1));
951 orderby = "starttime ASC";
952 break;
954 maxAge = gCoreContext->GetNumSetting("DeletedMaxAge", 0);
955 if (maxAge <= 0)
956 return;
957 msg = QString("Adding programs deleted more than %1 days ago")
958 .arg(maxAge);
959 where = QString("recgroup = 'Deleted' "
960 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
961 .arg(maxAge);
962 orderby = "starttime ASC";
963 break;
965 if (gCoreContext->GetNumSetting("DeletedMaxAge", 0) != 0)
966 return;
967 msg = QString("Adding programs deleted more than 5 minutes ago");
968 where = QString("recgroup = 'Deleted' "
969 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ");
970 orderby = "lastmodified ASC";
971 break;
973 msg = "Adding deleted programs in FIFO order";
974 where = "recgroup = 'Deleted'";
975 orderby = "lastmodified ASC";
976 break;
977 }
978
979 LOG(VB_FILE, LOG_INFO, LOC + "FillDBOrdered: " + msg);
980
982 QString querystr = QString(
983 "SELECT recorded.chanid, starttime "
984 "FROM recorded "
985 "LEFT JOIN channel ON recorded.chanid = channel.chanid "
986 "WHERE %1 AND deletepending = 0 "
987 "ORDER BY autoexpire DESC, %2").arg(where, orderby);
988
989 query.prepare(querystr);
990
991 if (!query.exec())
992 return;
993
994 while (query.next())
995 {
996 uint chanid = query.value(0).toUInt();
997 QDateTime recstartts = MythDate::as_utc(query.value(1).toDateTime());
998
999 if (IsInDontExpireSet(chanid, recstartts))
1000 {
1001 LOG(VB_FILE, LOG_INFO, LOC +
1002 QString(" Skipping %1 at %2 because it is in Don't Expire "
1003 "List")
1004 .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1005 }
1006 else if (IsInExpireList(expireList, chanid, recstartts))
1007 {
1008 LOG(VB_FILE, LOG_INFO, LOC +
1009 QString(" Skipping %1 at %2 because it is already in Expire "
1010 "List")
1011 .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1012 }
1013 else
1014 {
1015 auto *pginfo = new ProgramInfo(chanid, recstartts);
1016 if (pginfo->GetChanID())
1017 {
1018 LOG(VB_FILE, LOG_INFO, LOC + QString(" Adding %1 at %2")
1019 .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1020 expireList.push_back(pginfo);
1021 }
1022 else
1023 {
1024 LOG(VB_FILE, LOG_INFO, LOC +
1025 QString(" Skipping %1 at %2 "
1026 "because it could not be loaded from the DB")
1027 .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1028 delete pginfo;
1029 }
1030 }
1031 }
1032}
1033
1045void AutoExpire::Update(int encoder, int fsID, bool immediately)
1046{
1047 if (!gExpirer)
1048 return;
1049
1050 if (encoder > 0)
1051 {
1052 QString msg = QString("Cardid %1: is starting a recording on")
1053 .arg(encoder);
1054 if (fsID == -1)
1055 msg.append(" an unknown fsID soon.");
1056 else
1057 msg.append(QString(" fsID %2 soon.").arg(fsID));
1058 LOG(VB_FILE, LOG_INFO, LOC + msg);
1059 }
1060
1061 if (immediately)
1062 {
1063 if (encoder > 0)
1064 {
1065 gExpirer->m_instanceLock.lock();
1066 gExpirer->m_usedEncoders[encoder] = fsID;
1067 gExpirer->m_instanceLock.unlock();
1068 }
1070 gExpirer->m_instanceCond.wakeAll();
1071 }
1072 else
1073 {
1074 gExpirer->m_updateLock.lock();
1075 gExpirer->m_updateQueue.append(UpdateEntry(encoder, fsID));
1076 gExpirer->m_updateLock.unlock();
1077 }
1078}
1079
1081{
1082 m_dontExpireSet.clear();
1083
1085 query.prepare(
1086 "SELECT chanid, starttime, lastupdatetime, recusage, hostname "
1087 "FROM inuseprograms");
1088
1089 if (!query.exec())
1090 return;
1091
1092 QDateTime curTime = MythDate::current();
1093 while (query.next())
1094 {
1095 if (query.at() == 0)
1096 LOG(VB_FILE, LOG_INFO, LOC + "Adding Programs to 'Do Not Expire' List");
1097 uint chanid = query.value(0).toUInt();
1098 QDateTime recstartts = MythDate::as_utc(query.value(1).toDateTime());
1099 QDateTime lastupdate = MythDate::as_utc(query.value(2).toDateTime());
1100
1101 if (lastupdate.secsTo(curTime) < kRecentInterval)
1102 {
1103 QString key = QString("%1_%2")
1104 .arg(chanid).arg(recstartts.toString(Qt::ISODate));
1105 m_dontExpireSet.insert(key);
1106 LOG(VB_FILE, LOG_INFO, QString(" %1 at %2 in use by %3 on %4")
1107 .arg(QString::number(chanid),
1108 recstartts.toString(Qt::ISODate),
1109 query.value(3).toString(),
1110 query.value(4).toString()));
1111 }
1112 }
1113}
1114
1116 uint chanid, const QDateTime &recstartts) const
1117{
1118 QString key = QString("%1_%2")
1119 .arg(chanid).arg(recstartts.toString(Qt::ISODate));
1120
1121 return (m_dontExpireSet.contains(key));
1122}
1123
1125 const pginfolist_t &expireList, uint chanid, const QDateTime &recstartts)
1126{
1127 return std::any_of(expireList.cbegin(), expireList.cend(),
1128 [chanid,&recstartts](auto *info)
1129 { return ((info->GetChanID() == chanid) &&
1130 (info->GetRecordingStartTime() == recstartts)); } );
1131}
1132
1133/* vim: set expandtab tabstop=4 shiftwidth=4: */
#define LOC
Definition: autoexpire.cpp:44
static constexpr uint64_t kSpaceTooBigKB
If calculated desired space for 10 min freq is > kSpaceTooBigKB then we use 5 min expire frequency.
Definition: autoexpire.cpp:50
static constexpr int64_t kRecentInterval
Definition: autoexpire.cpp:54
std::vector< ProgramInfo * > pginfolist_t
Definition: autoexpire.h:23
@ emNormalLiveTVPrograms
Definition: autoexpire.h:31
@ emQuickDeletedPrograms
Definition: autoexpire.h:34
@ emLowestPriorityFirst
Definition: autoexpire.h:28
@ emOldDeletedPrograms
Definition: autoexpire.h:32
@ emNormalDeletedPrograms
Definition: autoexpire.h:33
@ emShortLiveTVPrograms
Definition: autoexpire.h:30
@ emOldestFirst
Definition: autoexpire.h:27
@ emWeightedTimePriority
Definition: autoexpire.h:29
AutoExpire * gExpirer
void FillDBOrdered(pginfolist_t &expireList, int expMethod)
Creates a list of programs to delete using the database to order list.
Definition: autoexpire.cpp:906
void ExpireRecordings(void)
This expires normal recordings.
Definition: autoexpire.cpp:403
QWaitCondition m_instanceCond
Definition: autoexpire.h:119
AutoExpire()=default
bool m_expireThreadRun
Definition: autoexpire.h:113
static void Update(int encoder, int fsID, bool immediately)
This is used to update the global AutoExpire instance "expirer".
void ExpireOldDeleted(void)
This expires deleted programs older than DeletedMaxAge.
Definition: autoexpire.cpp:376
void ExpireEpisodesOverMax(void)
This deletes programs exceeding the maximum number of episodes of that program desired.
Definition: autoexpire.cpp:661
void GetAllExpiring(QStringList &strList)
Gets the full list of programs that can expire in expiration order.
Definition: autoexpire.cpp:841
void UpdateDontExpireSet(void)
QMap< int, EncoderLink * > * m_encoderList
Definition: autoexpire.h:87
void ExpireQuickDeleted(void)
This expires deleted programs within a few minutes.
Definition: autoexpire.cpp:389
static bool IsInExpireList(const pginfolist_t &expireList, uint chanid, const QDateTime &recstartts)
uint m_desiredFreq
Definition: autoexpire.h:112
QMap< int, int64_t > m_desiredSpace
Definition: autoexpire.h:115
void RunExpirer(void)
This contains the main loop for the auto expire process.
Definition: autoexpire.cpp:270
bool IsInDontExpireSet(uint chanid, const QDateTime &recstartts) const
void FillExpireList(pginfolist_t &expireList)
Uses the "AutoExpireMethod" setting in the database to fill the list of files that are deletable.
Definition: autoexpire.cpp:780
void PrintExpireList(const QString &expHost="ALL")
Prints a summary of the files that can be deleted.
Definition: autoexpire.cpp:802
ExpireThread * m_expireThread
Definition: autoexpire.h:111
void CalcParams(void)
Calculates how much space needs to be cleared, and how often.
Definition: autoexpire.cpp:123
QMutex m_instanceLock
Definition: autoexpire.h:118
QMap< int, int > m_usedEncoders
Definition: autoexpire.h:116
MainServer * m_mainServer
Definition: autoexpire.h:121
QSet< QString > m_dontExpireSet
Definition: autoexpire.h:110
QMutex m_updateLock
Definition: autoexpire.h:124
static void SendDeleteMessages(pginfolist_t &deleteList)
This sends delete message to main event thread.
Definition: autoexpire.cpp:624
uint64_t GetDesiredSpace(int fsID) const
Used by the scheduler to select the next recording dir.
Definition: autoexpire.cpp:112
void Sleep(std::chrono::milliseconds sleepTime)
Sleeps for sleepTime milliseconds; unless the expire thread is told to quit.
Definition: autoexpire.cpp:346
static void ClearExpireList(pginfolist_t &expireList, bool deleteProg=true)
Clears expireList, freeing any ProgramInfo's if necessary.
Definition: autoexpire.cpp:887
~AutoExpire() override
AutoExpire destructor stops auto delete thread if it is running.
Definition: autoexpire.cpp:85
void ExpireLiveTV(int type)
This expires LiveTV programs.
Definition: autoexpire.cpp:363
QQueue< UpdateEntry > m_updateQueue
Definition: autoexpire.h:125
QPointer< AutoExpire > m_parent
Definition: autoexpire.h:46
void run(void) override
This calls AutoExpire::RunExpirer() from within a new thread.
Definition: autoexpire.cpp:57
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
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
int at(void) const
Definition: mythdbcon.h:221
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
void RunProlog(void)
Sets up a thread, call this if you reimplement run().
Definition: mthread.cpp:196
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 GetFilesystemInfos(FileSystemInfoList &fsInfos, bool useCache=true)
QString GetHostName(void)
void dispatch(const MythEvent &event)
int GetNumSetting(const QString &key, int defaultval=0)
bool GetBoolSetting(const QString &key, bool defaultval=false)
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:226
This class is used as a container for messages.
Definition: mythevent.h:17
void addListener(QObject *listener)
Add a listener to the observable.
void removeListener(QObject *listener)
Remove a listener to the observable.
Holds information on recordings and videos.
Definition: programinfo.h:70
bool IsWatched(void) const
Definition: programinfo.h:489
Holds information on a TV Program one might wish to record.
Definition: recordinginfo.h:36
void ForgetHistory(void)
Forget the recording of a program so it will be recorded again.
static QReadWriteLock s_inputsLock
Definition: tv_rec.h:434
int m_encoder
Definition: autoexpire.h:55
unsigned int uint
Definition: compat.h:68
QString GetPlaybackURL(ProgramInfo *pginfo, bool storePath)
QVector< FileSystemInfo > FileSystemInfoList
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
static bool VERBOSE_LEVEL_CHECK(uint64_t mask, LogLevel_t level)
Definition: mythlogging.h:29
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
Definition: mythdate.cpp:28
@ ISODate
Default UTC.
Definition: mythdate.h:17
std::chrono::seconds secsInFuture(const QDateTime &future)
Definition: mythdate.cpp:217
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
dictionary info
Definition: azlyrics.py:7
static eu8 clamp(eu8 value, eu8 low, eu8 high)
Definition: pxsup2dast.c:206
static QString fs2(QT_TRANSLATE_NOOP("SchedFilterEditor", "First showing"))
VERBOSE_PREAMBLE Most true
Definition: verbosedefs.h:95