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