2#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
3#include <QtSystemDetection>
11# include <sys/param.h>
13# include <sys/mount.h>
49#define LOC QString("AutoExpire: ")
50#define LOC_ERR QString("AutoExpire Error: ")
79 m_encoderList(tvList),
81 m_expireThreadRun(
true)
130 LOG(VB_FILE, LOG_INFO,
LOC +
"CalcParams()");
150 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Filesystem Info cache is empty, unable "
151 "to calculate necessary parameters.");
155 uint64_t maxKBperMin = 0;
156 uint64_t extraKB =
static_cast<uint64_t
>
160 QMap<int, uint64_t> fsMap;
161 QMap<int, std::vector<int> > fsEncoderMap;
170 fsEncoderMap[*ueit].push_back(ueit.key());
175 for (
const auto& fs : std::as_const(fsInfos))
177 if (fsMap.contains(fs.getFSysID()))
180 fsMap[fs.getFSysID()] = 0;
181 uint64_t thisKBperMin = 0;
184 for (
auto unknownfs : std::as_const(fsEncoderMap[-1]))
185 fsEncoderMap[fs.getFSysID()].push_back(unknownfs);
187 if (fsEncoderMap.contains(fs.getFSysID()))
189 LOG(VB_FILE, LOG_INFO,
190 QString(
"fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
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));
196 for (
auto cardid : std::as_const(fsEncoderMap[fs.getFSysID()]))
206 LOG(VB_FILE, LOG_INFO,
LOC +
207 QString(
"Cardid %1: is not recording, removing it "
208 "from used list.").arg(cardid));
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")
227 fsMap[fs.getFSysID()] = thisKBperMin;
229 if (thisKBperMin > maxKBperMin)
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;
241 uint expireFreq = 15;
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));
258 QMap<int, uint64_t>::iterator it = fsMap.begin();
259 while (it != fsMap.end())
261 m_desiredSpace[it.key()] = ((*it + *it/3) * expireFreq) + extraKB;
294 if (curTime >= next_expire)
316 if ((curTime.time().minute() % 2) == 0)
320 if (curTime >= next_expire)
322 LOG(VB_FILE, LOG_INFO,
LOC +
"Running now!");
331 else if (maxAge == 0)
341 Sleep(60s - std::chrono::milliseconds(timer.elapsed()));
353 if (sleepTime <= 0ms)
357 std::chrono::milliseconds timeleft = sleepTime;
372 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireLiveTV(%1)").arg(
type));
385 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireOldDeleted()"));
398 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireQuickDeleted()"));
414 LOG(VB_FILE, LOG_INFO,
LOC +
"ExpireRecordings()");
421 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Filesystem Info cache is empty, unable "
422 "to determine what Recordings to expire");
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);");
444 QString rechost = query.
value(0).toString();
445 QString recdir = query.
value(1).toString();
447 LOG(VB_FILE, LOG_INFO,
LOC +
448 QString(
"%1:%2 has an in-progress truncating delete.")
449 .arg(rechost, recdir));
451 for (
const auto& fs : std::as_const(fsInfos))
453 if ((fs.getHostname() == rechost) &&
454 (fs.getPath() == recdir))
456 truncateMap[fs.getFSysID()] =
true;
463 QMap <int, bool> fsMap;
465#
if QT_VERSION < QT_VERSION_CHECK(6,0,0)
466 auto* fsit = fsInfos.begin();
468 auto fsit = fsInfos.begin();
470fsit != fsInfos.end(); ++fsit)
472 if (fsMap.contains(fsit->getFSysID()))
475 fsMap[fsit->getFSysID()] =
true;
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));
484 if ((fsit->getTotalSpace() == -1) || (fsit->getUsedSpace() == -1))
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()));
493#
if QT_VERSION < QT_VERSION_CHECK(6,0,0)
494 auto* fsit2 = fsInfos.begin();
496 auto fsit2 = fsInfos.begin();
498 fsit2 != fsInfos.end(); ++fsit2)
500 if (fsit2->getFSysID() == fsit->getFSysID())
502 LOG(VB_FILE, LOG_INFO, QString(
" %1:%2")
503 .arg(fsit2->getHostname(), fsit2->getPath()));
510 if (truncateMap.contains(fsit->getFSysID()))
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()));
520 if (std::max((int64_t)0LL, fsit->getFreeSpace()) <
523 LOG(VB_FILE, LOG_INFO,
524 QString(
" Not Enough Free Space! We want %1 MB")
527 QMap<QString, int> dirList;
529 LOG(VB_FILE, LOG_INFO,
530 QString(
" Directories on filesystem ID %1:")
531 .arg(fsit->getFSysID()));
533 for (
const auto&
fs2 : std::as_const(fsInfos))
535 if (
fs2.getFSysID() == fsit->getFSysID())
537 LOG(VB_FILE, LOG_INFO, QString(
" %1:%2")
538 .arg(
fs2.getHostname(),
fs2.getPath()));
539 dirList[
fs2.getHostname() +
":" +
fs2.getPath()] = 1;
543 LOG(VB_FILE, LOG_INFO,
544 " Searching for files expirable in these directories");
546 auto it = expireList.begin();
547 while ((it != expireList.end()) &&
548 (std::max((int64_t)0LL, fsit->getFreeSpace()) <
554 LOG(VB_FILE, LOG_INFO, QString(
" Checking %1 => %2")
560 bool foundFile =
false;
568 ((
p->GetHostname() == myHostName) &&
578 if (!foundFile && (
p->GetHostname() != myHostName))
583 if (
file.startsWith(
"/"))
585 p->SetPathname(
file);
586 p->SetHostname(myHostName);
593 LOG(VB_FILE, LOG_ERR,
LOC +
594 QString(
" ERROR: Can't find file for %1")
600 QFileInfo vidFile(
p->GetPathname());
601 if (dirList.contains(
p->GetHostname() +
':' + vidFile.path()))
603 fsit->setUsedSpace(fsit->getUsedSpace()
604 - (
p->GetFilesize() / 1024));
605 deleteList.push_back(
p);
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.")
613 p->GetPathname()).arg(fsit->getFSysID())
614 .arg(fsit->getFreeSpace() / 1024));
633 if (deleteList.empty())
635 LOG(VB_FILE, LOG_INFO,
LOC +
"SendDeleteMessages. Nothing to expire.");
639 LOG(VB_FILE, LOG_INFO,
LOC +
640 "SendDeleteMessages, cycling through deleteList.");
641 auto it = deleteList.begin();
642 while (it != deleteList.end())
644 msg = QString(
"%1Expiring %2 MB for %3 => %4")
646 QString::number((*it)->GetFilesize() >> 20),
650 LOG(VB_GENERAL, LOG_NOTICE, msg);
653 MythEvent me(QString(
"AUTO_EXPIRE %1 %2").arg((*it)->GetChanID())
668 QMap<QString, int> maxEpisodes;
669 QMap<QString, int>::Iterator maxIter;
670 QMap<QString, int> episodeParts;
674 query.
prepare(
"SELECT recordid, maxepisodes, title "
675 "FROM record WHERE maxepisodes > 0 "
676 "ORDER BY recordid ASC, maxepisodes DESC");
680 LOG(VB_FILE, LOG_INFO,
LOC +
681 QString(
"Found %1 record profiles using max episode expiration")
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();
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)
697 query.
prepare(
"SELECT chanid, starttime, title, progstart, progend, "
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());
711 LOG(VB_FILE, LOG_INFO, QString(
" Recordid %1 has %2 recordings.")
714 if (query.
size() > 0)
721 QString title = query.
value(2).toString();
724 int duplicate = query.
value(5).toInt();
726 episodeKey = QString(
"%1_%2_%3")
727 .arg(QString::number(chanid),
732 (!episodeParts.contains(episodeKey)) &&
736 QString(
"%1Deleting %2 at %3 => %4. "
737 "Too many episodes, we only want to keep %5.")
739 QString::number(chanid),
742 QString::number(*maxIter));
744 LOG(VB_GENERAL, LOG_NOTICE, msg);
753 msg = QString(
"DELETE_RECORDING %1 %2")
765 if (episodeParts.contains(episodeKey))
767 episodeParts[episodeKey] = episodeParts[episodeKey] + 1;
771 episodeParts[episodeKey] = 1;
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;
819 for (
auto *first : expireList)
821 if (expHost !=
"ALL" && first->GetHostname() != expHost)
825 title = title.leftJustified(39,
' ',
true);
827 QString outstr = QString(
"%1 %2 MB %3 [%4]")
829 QString::number(first->GetFilesize() >> 20)
830 .rightJustified(5,
' ',
true),
832 .leftJustified(24,
' ',
true),
833 QString::number(first->GetRecordingPriority())
834 .rightJustified(3,
' ',
true));
835 QByteArray out = outstr.toLocal8Bit();
837 std::cout << out.constData() << std::endl;
859 strList << QString::number(expireList.size());
861 for (
auto &
info : expireList)
862 info->ToStringList(strList);
883 for (
auto &
info : expireList)
895 while (!expireList.empty())
898 pginfo = expireList.back();
900 expireList.pop_back();
922 msg =
"Adding programs expirable in Oldest First order";
923 where =
"autoexpire > 0";
925 orderby =
"recorded.watched DESC, ";
926 orderby +=
"starttime ASC";
929 msg =
"Adding programs expirable in Lowest Priority First order";
930 where =
"autoexpire > 0";
932 orderby =
"recorded.watched DESC, ";
933 orderby +=
"recorded.recpriority ASC, starttime ASC";
936 msg =
"Adding programs expirable in Weighted Time Priority order";
937 where =
"autoexpire > 0";
939 orderby =
"recorded.watched DESC, ";
940 orderby += QString(
"DATE_ADD(starttime, INTERVAL '%1' * "
941 "recorded.recpriority DAY) ASC")
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";
952 msg =
"Adding LiveTV programs in starttime order";
953 where = QString(
"recgroup = 'LiveTV' "
954 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
956 orderby =
"starttime ASC";
962 msg = QString(
"Adding programs deleted more than %1 days ago")
964 where = QString(
"recgroup = 'Deleted' "
965 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
967 orderby =
"starttime ASC";
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";
978 msg =
"Adding deleted programs in FIFO order";
979 where =
"recgroup = 'Deleted'";
980 orderby =
"lastmodified ASC";
984 LOG(VB_FILE, LOG_INFO,
LOC +
"FillDBOrdered: " + msg);
987 QString querystr = QString(
988 "SELECT recorded.chanid, starttime "
990 "LEFT JOIN channel ON recorded.chanid = channel.chanid "
991 "WHERE %1 AND deletepending = 0 "
992 "ORDER BY autoexpire DESC, %2").arg(where, orderby);
1006 LOG(VB_FILE, LOG_INFO,
LOC +
1007 QString(
" Skipping %1 at %2 because it is in Don't Expire "
1009 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1013 LOG(VB_FILE, LOG_INFO,
LOC +
1014 QString(
" Skipping %1 at %2 because it is already in Expire "
1016 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1020 auto *pginfo =
new ProgramInfo(chanid, recstartts);
1021 if (pginfo->GetChanID())
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);
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)));
1057 QString msg = QString(
"Cardid %1: is starting a recording on")
1060 msg.append(
" an unknown fsID soon.");
1062 msg.append(QString(
" fsID %2 soon.").arg(fsID));
1063 LOG(VB_FILE, LOG_INFO,
LOC + msg);
1091 "SELECT chanid, starttime, lastupdatetime, recusage, hostname "
1092 "FROM inuseprograms");
1098 while (query.
next())
1100 if (query.
at() == 0)
1101 LOG(VB_FILE, LOG_INFO,
LOC +
"Adding Programs to 'Do Not Expire' List");
1108 QString key = QString(
"%1_%2")
1109 .arg(chanid).arg(recstartts.toString(
Qt::ISODate));
1111 LOG(VB_FILE, LOG_INFO, QString(
" %1 at %2 in use by %3 on %4")
1112 .arg(QString::number(chanid),
1114 query.
value(3).toString(),
1115 query.
value(4).toString()));
1121 uint chanid,
const QDateTime &recstartts)
const
1123 QString key = QString(
"%1_%2")
1124 .arg(chanid).arg(recstartts.toString(
Qt::ISODate));
1132 return std::any_of(expireList.cbegin(), expireList.cend(),
1133 [chanid,&recstartts](
auto *
info)
1134 { return ((info->GetChanID() == chanid) &&
1135 (info->GetRecordingStartTime() == recstartts)); } );
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