2#if QT_VERSION >= QT_VERSION_CHECK(6,5,0)
3#include <QtSystemDetection>
11# include <sys/param.h>
13# include <sys/mount.h>
50#define LOC QString("AutoExpire: ")
51#define LOC_ERR QString("AutoExpire Error: ")
80 m_encoderList(tvList),
82 m_expireThreadRun(
true)
131 LOG(VB_FILE, LOG_INFO,
LOC +
"CalcParams()");
151 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Filesystem Info cache is empty, unable "
152 "to calculate necessary parameters.");
156 uint64_t maxKBperMin = 0;
157 uint64_t extraKB =
static_cast<uint64_t
>
161 QMap<int, uint64_t> fsMap;
162 QMap<int, std::vector<int> > fsEncoderMap;
171 fsEncoderMap[*ueit].push_back(ueit.key());
176 for (
const auto& fs : std::as_const(fsInfos))
178 if (fsMap.contains(fs.getFSysID()))
181 fsMap[fs.getFSysID()] = 0;
182 uint64_t thisKBperMin = 0;
185 for (
auto unknownfs : std::as_const(fsEncoderMap[-1]))
186 fsEncoderMap[fs.getFSysID()].push_back(unknownfs);
188 if (fsEncoderMap.contains(fs.getFSysID()))
190 LOG(VB_FILE, LOG_INFO,
191 QString(
"fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
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));
197 for (
auto cardid : std::as_const(fsEncoderMap[fs.getFSysID()]))
207 LOG(VB_FILE, LOG_INFO,
LOC +
208 QString(
"Cardid %1: is not recording, removing it "
209 "from used list.").arg(cardid));
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")
228 fsMap[fs.getFSysID()] = thisKBperMin;
230 if (thisKBperMin > maxKBperMin)
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;
242 uint expireFreq = 15;
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));
259 QMap<int, uint64_t>::iterator it = fsMap.begin();
260 while (it != fsMap.end())
262 m_desiredSpace[it.key()] = ((*it + (*it/3)) * expireFreq) + extraKB;
295 if (curTime >= next_expire)
317 if ((curTime.time().minute() % 2) == 0)
321 if (curTime >= next_expire)
323 LOG(VB_FILE, LOG_INFO,
LOC +
"Running now!");
332 else if (maxAge == 0)
342 Sleep(60s - std::chrono::milliseconds(timer.elapsed()));
354 if (sleepTime <= 0ms)
358 std::chrono::milliseconds timeleft = sleepTime;
373 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireLiveTV(%1)").arg(
type));
386 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireOldDeleted()"));
399 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireQuickDeleted()"));
415 LOG(VB_FILE, LOG_INFO,
LOC +
"ExpireRecordings()");
422 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Filesystem Info cache is empty, unable "
423 "to determine what Recordings to expire");
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);");
445 QString rechost = query.
value(0).toString();
446 QString recdir = query.
value(1).toString();
448 LOG(VB_FILE, LOG_INFO,
LOC +
449 QString(
"%1:%2 has an in-progress truncating delete.")
450 .arg(rechost, recdir));
452 for (
const auto& fs : std::as_const(fsInfos))
454 if ((fs.getHostname() == rechost) &&
455 (fs.getPath() == recdir))
457 truncateMap[fs.getFSysID()] =
true;
464 QMap <int, bool> fsMap;
466#
if QT_VERSION < QT_VERSION_CHECK(6,0,0)
467 auto* fsit = fsInfos.begin();
469 auto fsit = fsInfos.begin();
471fsit != fsInfos.end(); ++fsit)
473 if (fsMap.contains(fsit->getFSysID()))
476 fsMap[fsit->getFSysID()] =
true;
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));
485 if ((fsit->getTotalSpace() == -1) || (fsit->getUsedSpace() == -1))
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()));
494#
if QT_VERSION < QT_VERSION_CHECK(6,0,0)
495 auto* fsit2 = fsInfos.begin();
497 auto fsit2 = fsInfos.begin();
499 fsit2 != fsInfos.end(); ++fsit2)
501 if (fsit2->getFSysID() == fsit->getFSysID())
503 LOG(VB_FILE, LOG_INFO, QString(
" %1:%2")
504 .arg(fsit2->getHostname(), fsit2->getPath()));
511 if (truncateMap.contains(fsit->getFSysID()))
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()));
521 if (std::max((int64_t)0LL, fsit->getFreeSpace()) <
524 LOG(VB_FILE, LOG_INFO,
525 QString(
" Not Enough Free Space! We want %1 MB")
528 QMap<QString, int> dirList;
530 LOG(VB_FILE, LOG_INFO,
531 QString(
" Directories on filesystem ID %1:")
532 .arg(fsit->getFSysID()));
534 for (
const auto&
fs2 : std::as_const(fsInfos))
536 if (
fs2.getFSysID() == fsit->getFSysID())
538 LOG(VB_FILE, LOG_INFO, QString(
" %1:%2")
539 .arg(
fs2.getHostname(),
fs2.getPath()));
540 dirList[
fs2.getHostname() +
":" +
fs2.getPath()] = 1;
544 LOG(VB_FILE, LOG_INFO,
545 " Searching for files expirable in these directories");
547 auto it = expireList.begin();
548 while ((it != expireList.end()) &&
549 (std::max((int64_t)0LL, fsit->getFreeSpace()) <
555 LOG(VB_FILE, LOG_INFO, QString(
" Checking %1 => %2")
561 bool foundFile =
false;
569 ((
p->GetHostname() == myHostName) &&
579 if (!foundFile && (
p->GetHostname() != myHostName))
584 if (
file.startsWith(
"/"))
586 p->SetPathname(
file);
587 p->SetHostname(myHostName);
594 LOG(VB_FILE, LOG_ERR,
LOC +
595 QString(
" ERROR: Can't find file for %1")
601 QFileInfo vidFile(
p->GetPathname());
602 if (dirList.contains(
p->GetHostname() +
':' + vidFile.path()))
604 fsit->setUsedSpace(fsit->getUsedSpace()
605 - (
p->GetFilesize() / 1024));
606 deleteList.push_back(
p);
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.")
614 p->GetPathname()).arg(fsit->getFSysID())
615 .arg(fsit->getFreeSpace() / 1024));
634 if (deleteList.empty())
636 LOG(VB_FILE, LOG_INFO,
LOC +
"SendDeleteMessages. Nothing to expire.");
640 LOG(VB_FILE, LOG_INFO,
LOC +
641 "SendDeleteMessages, cycling through deleteList.");
642 auto it = deleteList.begin();
643 while (it != deleteList.end())
645 msg = QString(
"%1Expiring %2 MB for %3 => %4")
647 QString::number((*it)->GetFilesize() >> 20),
651 LOG(VB_GENERAL, LOG_NOTICE, msg);
654 MythEvent me(QString(
"AUTO_EXPIRE %1 %2").arg((*it)->GetChanID())
669 QMap<QString, int> maxEpisodes;
670 QMap<QString, int>::Iterator maxIter;
671 QMap<QString, int> episodeParts;
675 query.
prepare(
"SELECT recordid, maxepisodes, title "
676 "FROM record WHERE maxepisodes > 0 "
677 "ORDER BY recordid ASC, maxepisodes DESC");
681 LOG(VB_FILE, LOG_INFO,
LOC +
682 QString(
"Found %1 record profiles using max episode expiration")
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();
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)
698 query.
prepare(
"SELECT chanid, starttime, title, progstart, progend, "
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());
712 LOG(VB_FILE, LOG_INFO, QString(
" Recordid %1 has %2 recordings.")
715 if (query.
size() > 0)
722 QString title = query.
value(2).toString();
725 int duplicate = query.
value(5).toInt();
727 episodeKey = QString(
"%1_%2_%3")
728 .arg(QString::number(chanid),
733 (!episodeParts.contains(episodeKey)) &&
737 QString(
"%1Deleting %2 at %3 => %4. "
738 "Too many episodes, we only want to keep %5.")
740 QString::number(chanid),
743 QString::number(*maxIter));
745 LOG(VB_GENERAL, LOG_NOTICE, msg);
754 msg = QString(
"DELETE_RECORDING %1 %2")
766 if (episodeParts.contains(episodeKey))
768 episodeParts[episodeKey] = episodeParts[episodeKey] + 1;
772 episodeParts[episodeKey] = 1;
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';
820 for (
auto *first : expireList)
822 if (expHost !=
"ALL" && first->GetHostname() != expHost)
826 title = title.leftJustified(39,
' ',
true);
828 QString outstr = QString(
"%1 %2 MB %3 [%4]")
830 QString::number(first->GetFilesize() >> 20)
831 .rightJustified(5,
' ',
true),
833 .leftJustified(24,
' ',
true),
834 QString::number(first->GetRecordingPriority())
835 .rightJustified(3,
' ',
true));
836 QByteArray out = outstr.toLocal8Bit();
838 std::cout << out.constData() <<
'\n';
860 strList << QString::number(expireList.size());
862 for (
auto &
info : expireList)
863 info->ToStringList(strList);
884 for (
auto &
info : expireList)
896 while (!expireList.empty())
899 pginfo = expireList.back();
901 expireList.pop_back();
923 msg =
"Adding programs expirable in Oldest First order";
924 where =
"autoexpire > 0";
926 orderby =
"recorded.watched DESC, ";
927 orderby +=
"starttime ASC";
930 msg =
"Adding programs expirable in Lowest Priority First order";
931 where =
"autoexpire > 0";
933 orderby =
"recorded.watched DESC, ";
934 orderby +=
"recorded.recpriority ASC, starttime ASC";
937 msg =
"Adding programs expirable in Weighted Time Priority order";
938 where =
"autoexpire > 0";
940 orderby =
"recorded.watched DESC, ";
941 orderby += QString(
"DATE_ADD(starttime, INTERVAL '%1' * "
942 "recorded.recpriority DAY) ASC")
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";
953 msg =
"Adding LiveTV programs in starttime order";
954 where = QString(
"recgroup = 'LiveTV' "
955 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
957 orderby =
"starttime ASC";
963 msg = QString(
"Adding programs deleted more than %1 days ago")
965 where = QString(
"recgroup = 'Deleted' "
966 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
968 orderby =
"starttime ASC";
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";
979 msg =
"Adding deleted programs in FIFO order";
980 where =
"recgroup = 'Deleted'";
981 orderby =
"lastmodified ASC";
985 LOG(VB_FILE, LOG_INFO,
LOC +
"FillDBOrdered: " + msg);
988 QString querystr = QString(
989 "SELECT recorded.chanid, starttime "
991 "LEFT JOIN channel ON recorded.chanid = channel.chanid "
992 "WHERE %1 AND deletepending = 0 "
993 "ORDER BY autoexpire DESC, %2").arg(where, orderby);
1000 while (query.
next())
1007 LOG(VB_FILE, LOG_INFO,
LOC +
1008 QString(
" Skipping %1 at %2 because it is in Don't Expire "
1010 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1014 LOG(VB_FILE, LOG_INFO,
LOC +
1015 QString(
" Skipping %1 at %2 because it is already in Expire "
1017 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1021 auto *pginfo =
new ProgramInfo(chanid, recstartts);
1022 if (pginfo->GetChanID())
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);
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)));
1058 QString msg = QString(
"Cardid %1: is starting a recording on")
1061 msg.append(
" an unknown fsID soon.");
1063 msg.append(QString(
" fsID %2 soon.").arg(fsID));
1064 LOG(VB_FILE, LOG_INFO,
LOC + msg);
1092 "SELECT chanid, starttime, lastupdatetime, recusage, hostname "
1093 "FROM inuseprograms");
1099 while (query.
next())
1101 if (query.
at() == 0)
1102 LOG(VB_FILE, LOG_INFO,
LOC +
"Adding Programs to 'Do Not Expire' List");
1109 QString key = QString(
"%1_%2")
1110 .arg(chanid).arg(recstartts.toString(
Qt::ISODate));
1112 LOG(VB_FILE, LOG_INFO, QString(
" %1 at %2 in use by %3 on %4")
1113 .arg(QString::number(chanid),
1115 query.
value(3).toString(),
1116 query.
value(4).toString()));
1122 uint chanid,
const QDateTime &recstartts)
const
1124 QString key = QString(
"%1_%2")
1125 .arg(chanid).arg(recstartts.toString(
Qt::ISODate));
1133 return std::ranges::any_of(expireList,
1134 [chanid,&recstartts](
auto *
info)
1135 {
return ((
info->GetChanID() == chanid) &&
1136 (
info->GetRecordingStartTime() == recstartts)); } );
1139#include "moc_autoexpire.cpp"
static constexpr uint64_t kSpaceTooBigKB
If calculated desired space for 10 min freq is > kSpaceTooBigKB then we use 5 min expire frequency.
static constexpr int64_t kRecentInterval
std::vector< ProgramInfo * > pginfolist_t
@ emNormalDeletedPrograms
void FillDBOrdered(pginfolist_t &expireList, int expMethod)
Creates a list of programs to delete using the database to order list.
void ExpireRecordings(void)
This expires normal recordings.
QWaitCondition m_instanceCond
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.
void ExpireEpisodesOverMax(void)
This deletes programs exceeding the maximum number of episodes of that program desired.
void GetAllExpiring(QStringList &strList)
Gets the full list of programs that can expire in expiration order.
void UpdateDontExpireSet(void)
QMap< int, EncoderLink * > * m_encoderList
void ExpireQuickDeleted(void)
This expires deleted programs within a few minutes.
static bool IsInExpireList(const pginfolist_t &expireList, uint chanid, const QDateTime &recstartts)
QMap< int, int64_t > m_desiredSpace
void RunExpirer(void)
This contains the main loop for the auto expire process.
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.
void PrintExpireList(const QString &expHost="ALL")
Prints a summary of the files that can be deleted.
ExpireThread * m_expireThread
void CalcParams(void)
Calculates how much space needs to be cleared, and how often.
QMap< int, int > m_usedEncoders
MainServer * m_mainServer
QSet< QString > m_dontExpireSet
static void SendDeleteMessages(pginfolist_t &deleteList)
This sends delete message to main event thread.
uint64_t GetDesiredSpace(int fsID) const
Used by the scheduler to select the next recording dir.
void Sleep(std::chrono::milliseconds sleepTime)
Sleeps for sleepTime milliseconds; unless the expire thread is told to quit.
static void ClearExpireList(pginfolist_t &expireList, bool deleteProg=true)
Clears expireList, freeing any ProgramInfo's if necessary.
~AutoExpire() override
AutoExpire destructor stops auto delete thread if it is running.
void ExpireLiveTV(int type)
This expires LiveTV programs.
QQueue< UpdateEntry > m_updateQueue
Provides an interface to both local and remote TVRec's for the mythbackend.
bool IsConnected(void) const
Returns true if the EncoderLink instance is usable.
long long GetMaxBitrate(void)
Returns maximum bits per second this recorder might output.
bool CheckFile(ProgramInfo *pginfo)
Checks if program is stored locally.
bool IsLocal(void) const
Returns true for a local EncoderLink.
int GetInputID(void) const
Returns the inputid used to refer to the recorder in the DB.
bool IsBusy(InputInfo *busy_input=nullptr, std::chrono::seconds time_buffer=5s)
Returns true if the recorder is busy, or will be within the next time_buffer seconds.
QString GetHostName(void) const
Returns the remote host for a non-local EncoderLink.
QPointer< AutoExpire > m_parent
void run(void) override
This calls AutoExpire::RunExpirer() from within a new thread.
QSqlQuery wrapper that fetches a DB connection from the connection pool.
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
QVariant value(int i) const
bool isActive(void) const
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
void RunProlog(void)
Sets up a thread, call this if you reimplement run().
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
void RunEpilog(void)
Cleans up a thread's resources, call this if you reimplement run().
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
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)
This class is used as a container for messages.
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.
bool IsWatched(void) const
Holds information on a TV Program one might wish to record.
void ForgetHistory(void)
Forget the recording of a program so it will be recorded again.
static QReadWriteLock s_inputsLock
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)
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
std::chrono::seconds secsInFuture(const QDateTime &future)
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
static eu8 clamp(eu8 value, eu8 low, eu8 high)
static QString fs2(QT_TRANSLATE_NOOP("SchedFilterEditor", "First showing"))
VERBOSE_PREAMBLE Most true