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