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