44#define LOC QString("AutoExpire: ")
45#define LOC_ERR QString("AutoExpire Error: ")
74 m_encoderList(tvList),
76 m_expireThreadRun(
true)
125 LOG(VB_FILE, LOG_INFO,
LOC +
"CalcParams()");
145 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Filesystem Info cache is empty, unable "
146 "to calculate necessary parameters.");
150 uint64_t maxKBperMin = 0;
151 uint64_t extraKB =
static_cast<uint64_t
>
155 QMap<int, uint64_t> fsMap;
156 QMap<int, std::vector<int> > fsEncoderMap;
165 fsEncoderMap[*ueit].push_back(ueit.key());
170 for (
const auto& fs : std::as_const(fsInfos))
172 if (fsMap.contains(fs.getFSysID()))
175 fsMap[fs.getFSysID()] = 0;
176 uint64_t thisKBperMin = 0;
179 for (
auto unknownfs : std::as_const(fsEncoderMap[-1]))
180 fsEncoderMap[fs.getFSysID()].push_back(unknownfs);
182 if (fsEncoderMap.contains(fs.getFSysID()))
184 LOG(VB_FILE, LOG_INFO,
185 QString(
"fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
187 .arg(fs.getTotalSpace() / 1024.0 / 1024.0, 7,
'f', 1)
188 .arg(fs.getUsedSpace() / 1024.0 / 1024.0, 7,
'f', 1)
189 .arg(fs.getFreeSpace() / 1024.0 / 1024.0, 7,
'f', 1));
191 for (
auto cardid : std::as_const(fsEncoderMap[fs.getFSysID()]))
201 LOG(VB_FILE, LOG_INFO,
LOC +
202 QString(
"Cardid %1: is not recording, removing it "
203 "from used list.").arg(cardid));
212 maxBitrate = 19500000LL;
213 thisKBperMin += (maxBitrate*((uint64_t)15))>>11;
214 LOG(VB_FILE, LOG_INFO, QString(
" Cardid %1: max bitrate "
215 "%2 Kb/sec, fsID %3 max is now %4 KB/min")
222 fsMap[fs.getFSysID()] = thisKBperMin;
224 if (thisKBperMin > maxKBperMin)
226 LOG(VB_FILE, LOG_INFO,
227 QString(
" Max of %1 KB/min for fsID %2 is higher "
228 "than the existing Max of %3 so we'll use this Max instead")
229 .arg(thisKBperMin).arg(fs.getFSysID()).arg(maxKBperMin));
230 maxKBperMin = thisKBperMin;
236 uint expireFreq = 15;
243 double expireMinGB = ((maxKBperMin + maxKBperMin/3)
244 * expireFreq + extraKB) >> 20;
245 LOG(VB_GENERAL, LOG_NOTICE,
LOC +
246 QString(
"CalcParams(): Max required Free Space: %1 GB w/freq: %2 min")
247 .arg(expireMinGB, 0,
'f', 1).arg(expireFreq));
253 QMap<int, uint64_t>::iterator it = fsMap.begin();
254 while (it != fsMap.end())
256 m_desiredSpace[it.key()] = ((*it + *it/3) * expireFreq) + extraKB;
289 if (curTime >= next_expire)
311 if ((curTime.time().minute() % 2) == 0)
315 if (curTime >= next_expire)
317 LOG(VB_FILE, LOG_INFO,
LOC +
"Running now!");
326 else if (maxAge == 0)
336 Sleep(60s - std::chrono::milliseconds(timer.elapsed()));
348 if (sleepTime <= 0ms)
352 std::chrono::milliseconds timeleft = sleepTime;
367 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireLiveTV(%1)").arg(
type));
380 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireOldDeleted()"));
393 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireQuickDeleted()"));
409 LOG(VB_FILE, LOG_INFO,
LOC +
"ExpireRecordings()");
416 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Filesystem Info cache is empty, unable "
417 "to determine what Recordings to expire");
424 QMap <int, bool> truncateMap;
426 query.
prepare(
"SELECT DISTINCT rechost, recdir "
427 "FROM inuseprograms "
428 "WHERE recusage = 'truncatingdelete' "
429 "AND lastupdatetime > DATE_ADD(NOW(), INTERVAL -2 MINUTE);");
439 QString rechost = query.
value(0).toString();
440 QString recdir = query.
value(1).toString();
442 LOG(VB_FILE, LOG_INFO,
LOC +
443 QString(
"%1:%2 has an in-progress truncating delete.")
444 .arg(rechost, recdir));
446 for (
const auto& fs : std::as_const(fsInfos))
448 if ((fs.getHostname() == rechost) &&
449 (fs.getPath() == recdir))
451 truncateMap[fs.getFSysID()] =
true;
458 QMap <int, bool> fsMap;
460#
if QT_VERSION < QT_VERSION_CHECK(6,0,0)
461 auto* fsit = fsInfos.begin();
463 auto fsit = fsInfos.begin();
465fsit != fsInfos.end(); ++fsit)
467 if (fsMap.contains(fsit->getFSysID()))
470 fsMap[fsit->getFSysID()] =
true;
472 LOG(VB_FILE, LOG_INFO,
473 QString(
"fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
474 .arg(fsit->getFSysID())
475 .arg(fsit->getTotalSpace() / 1024.0 / 1024.0, 7,
'f', 1)
476 .arg(fsit->getUsedSpace() / 1024.0 / 1024.0, 7,
'f', 1)
477 .arg(fsit->getFreeSpace() / 1024.0 / 1024.0, 7,
'f', 1));
479 if ((fsit->getTotalSpace() == -1) || (fsit->getUsedSpace() == -1))
481 LOG(VB_FILE, LOG_ERR,
LOC +
482 QString(
"fsID #%1 has invalid info, AutoExpire cannot run for "
483 "this filesystem. Continuing on to next...")
484 .arg(fsit->getFSysID()));
485 LOG(VB_FILE, LOG_INFO, QString(
"Directories on filesystem ID %1:")
486 .arg(fsit->getFSysID()));
488#
if QT_VERSION < QT_VERSION_CHECK(6,0,0)
489 auto* fsit2 = fsInfos.begin();
491 auto fsit2 = fsInfos.begin();
493 fsit2 != fsInfos.end(); ++fsit2)
495 if (fsit2->getFSysID() == fsit->getFSysID())
497 LOG(VB_FILE, LOG_INFO, QString(
" %1:%2")
498 .arg(fsit2->getHostname(), fsit2->getPath()));
505 if (truncateMap.contains(fsit->getFSysID()))
507 LOG(VB_FILE, LOG_INFO,
508 QString(
" fsid %1 has a truncating delete in progress, "
509 "AutoExpire cannot run for this filesystem until the "
510 "delete has finished. Continuing on to next...")
511 .arg(fsit->getFSysID()));
515 if (std::max((int64_t)0LL, fsit->getFreeSpace()) <
518 LOG(VB_FILE, LOG_INFO,
519 QString(
" Not Enough Free Space! We want %1 MB")
522 QMap<QString, int> dirList;
524 LOG(VB_FILE, LOG_INFO,
525 QString(
" Directories on filesystem ID %1:")
526 .arg(fsit->getFSysID()));
528 for (
const auto&
fs2 : std::as_const(fsInfos))
530 if (
fs2.getFSysID() == fsit->getFSysID())
532 LOG(VB_FILE, LOG_INFO, QString(
" %1:%2")
533 .arg(
fs2.getHostname(),
fs2.getPath()));
534 dirList[
fs2.getHostname() +
":" +
fs2.getPath()] = 1;
538 LOG(VB_FILE, LOG_INFO,
539 " Searching for files expirable in these directories");
541 auto it = expireList.begin();
542 while ((it != expireList.end()) &&
543 (std::max((int64_t)0LL, fsit->getFreeSpace()) <
549 LOG(VB_FILE, LOG_INFO, QString(
" Checking %1 => %2")
555 bool foundFile =
false;
563 ((
p->GetHostname() == myHostName) &&
573 if (!foundFile && (
p->GetHostname() != myHostName))
578 if (
file.startsWith(
"/"))
580 p->SetPathname(
file);
581 p->SetHostname(myHostName);
588 LOG(VB_FILE, LOG_ERR,
LOC +
589 QString(
" ERROR: Can't find file for %1")
595 QFileInfo vidFile(
p->GetPathname());
596 if (dirList.contains(
p->GetHostname() +
':' + vidFile.path()))
598 fsit->setUsedSpace(fsit->getUsedSpace()
599 - (
p->GetFilesize() / 1024));
600 deleteList.push_back(
p);
602 LOG(VB_FILE, LOG_INFO,
603 QString(
" FOUND file expirable. "
604 "%1 is located at %2 which is on fsID #%3. "
605 "Adding to deleteList. After deleting we "
606 "should have %4 MB free on this filesystem.")
608 p->GetPathname()).arg(fsit->getFSysID())
609 .arg(fsit->getFreeSpace() / 1024));
628 if (deleteList.empty())
630 LOG(VB_FILE, LOG_INFO,
LOC +
"SendDeleteMessages. Nothing to expire.");
634 LOG(VB_FILE, LOG_INFO,
LOC +
635 "SendDeleteMessages, cycling through deleteList.");
636 auto it = deleteList.begin();
637 while (it != deleteList.end())
639 msg = QString(
"%1Expiring %2 MB for %3 => %4")
641 QString::number((*it)->GetFilesize() >> 20),
645 LOG(VB_GENERAL, LOG_NOTICE, msg);
648 MythEvent me(QString(
"AUTO_EXPIRE %1 %2").arg((*it)->GetChanID())
663 QMap<QString, int> maxEpisodes;
664 QMap<QString, int>::Iterator maxIter;
665 QMap<QString, int> episodeParts;
669 query.
prepare(
"SELECT recordid, maxepisodes, title "
670 "FROM record WHERE maxepisodes > 0 "
671 "ORDER BY recordid ASC, maxepisodes DESC");
675 LOG(VB_FILE, LOG_INFO,
LOC +
676 QString(
"Found %1 record profiles using max episode expiration")
680 LOG(VB_FILE, LOG_INFO, QString(
" %1 (%2 for rec id %3)")
681 .arg(query.
value(2).toString())
682 .arg(query.
value(1).toInt())
683 .arg(query.
value(0).toInt()));
684 maxEpisodes[query.
value(0).toString()] = query.
value(1).toInt();
688 LOG(VB_FILE, LOG_INFO,
LOC +
689 "Checking episode count for each recording profile using max episodes");
690 for (maxIter = maxEpisodes.begin(); maxIter != maxEpisodes.end(); ++maxIter)
692 query.
prepare(
"SELECT chanid, starttime, title, progstart, progend, "
695 "WHERE recordid = :RECID AND preserve = 0 "
696 "AND recgroup NOT IN ('LiveTV', 'Deleted') "
697 "ORDER BY starttime DESC;");
698 query.
bindValue(
":RECID", maxIter.key());
706 LOG(VB_FILE, LOG_INFO, QString(
" Recordid %1 has %2 recordings.")
709 if (query.
size() > 0)
716 QString title = query.
value(2).toString();
719 int duplicate = query.
value(5).toInt();
721 episodeKey = QString(
"%1_%2_%3")
722 .arg(QString::number(chanid),
727 (!episodeParts.contains(episodeKey)) &&
731 QString(
"%1Deleting %2 at %3 => %4. "
732 "Too many episodes, we only want to keep %5.")
734 QString::number(chanid),
737 QString::number(*maxIter));
739 LOG(VB_GENERAL, LOG_NOTICE, msg);
748 msg = QString(
"DELETE_RECORDING %1 %2")
760 if (episodeParts.contains(episodeKey))
762 episodeParts[episodeKey] = episodeParts[episodeKey] + 1;
766 episodeParts[episodeKey] = 1;
808 QString msg =
"MythTV AutoExpire List ";
809 if (expHost !=
"ALL")
810 msg += QString(
"for '%1' ").arg(expHost);
811 msg +=
"(programs listed in order of expiration)";
812 std::cout << msg.toLocal8Bit().constData() << std::endl;
814 for (
auto *first : expireList)
816 if (expHost !=
"ALL" && first->GetHostname() != expHost)
820 title = title.leftJustified(39,
' ',
true);
822 QString outstr = QString(
"%1 %2 MB %3 [%4]")
824 QString::number(first->GetFilesize() >> 20)
825 .rightJustified(5,
' ',
true),
827 .leftJustified(24,
' ',
true),
828 QString::number(first->GetRecordingPriority())
829 .rightJustified(3,
' ',
true));
830 QByteArray out = outstr.toLocal8Bit();
832 std::cout << out.constData() << std::endl;
854 strList << QString::number(expireList.size());
856 for (
auto &
info : expireList)
857 info->ToStringList(strList);
878 for (
auto &
info : expireList)
890 while (!expireList.empty())
893 pginfo = expireList.back();
895 expireList.pop_back();
917 msg =
"Adding programs expirable in Oldest First order";
918 where =
"autoexpire > 0";
920 orderby =
"recorded.watched DESC, ";
921 orderby +=
"starttime ASC";
924 msg =
"Adding programs expirable in Lowest Priority First order";
925 where =
"autoexpire > 0";
927 orderby =
"recorded.watched DESC, ";
928 orderby +=
"recorded.recpriority ASC, starttime ASC";
931 msg =
"Adding programs expirable in Weighted Time Priority order";
932 where =
"autoexpire > 0";
934 orderby =
"recorded.watched DESC, ";
935 orderby += QString(
"DATE_ADD(starttime, INTERVAL '%1' * "
936 "recorded.recpriority DAY) ASC")
940 msg =
"Adding Short LiveTV programs in starttime order";
941 where =
"recgroup = 'LiveTV' "
942 "AND endtime < DATE_ADD(starttime, INTERVAL '30' SECOND) "
943 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ";
944 orderby =
"starttime ASC";
947 msg =
"Adding LiveTV programs in starttime order";
948 where = QString(
"recgroup = 'LiveTV' "
949 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
951 orderby =
"starttime ASC";
957 msg = QString(
"Adding programs deleted more than %1 days ago")
959 where = QString(
"recgroup = 'Deleted' "
960 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
962 orderby =
"starttime ASC";
967 msg = QString(
"Adding programs deleted more than 5 minutes ago");
968 where = QString(
"recgroup = 'Deleted' "
969 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ");
970 orderby =
"lastmodified ASC";
973 msg =
"Adding deleted programs in FIFO order";
974 where =
"recgroup = 'Deleted'";
975 orderby =
"lastmodified ASC";
979 LOG(VB_FILE, LOG_INFO,
LOC +
"FillDBOrdered: " + msg);
982 QString querystr = QString(
983 "SELECT recorded.chanid, starttime "
985 "LEFT JOIN channel ON recorded.chanid = channel.chanid "
986 "WHERE %1 AND deletepending = 0 "
987 "ORDER BY autoexpire DESC, %2").arg(where, orderby);
1001 LOG(VB_FILE, LOG_INFO,
LOC +
1002 QString(
" Skipping %1 at %2 because it is in Don't Expire "
1004 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1008 LOG(VB_FILE, LOG_INFO,
LOC +
1009 QString(
" Skipping %1 at %2 because it is already in Expire "
1011 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1015 auto *pginfo =
new ProgramInfo(chanid, recstartts);
1016 if (pginfo->GetChanID())
1018 LOG(VB_FILE, LOG_INFO,
LOC + QString(
" Adding %1 at %2")
1019 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1020 expireList.push_back(pginfo);
1024 LOG(VB_FILE, LOG_INFO,
LOC +
1025 QString(
" Skipping %1 at %2 "
1026 "because it could not be loaded from the DB")
1027 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1052 QString msg = QString(
"Cardid %1: is starting a recording on")
1055 msg.append(
" an unknown fsID soon.");
1057 msg.append(QString(
" fsID %2 soon.").arg(fsID));
1058 LOG(VB_FILE, LOG_INFO,
LOC + msg);
1086 "SELECT chanid, starttime, lastupdatetime, recusage, hostname "
1087 "FROM inuseprograms");
1093 while (query.
next())
1095 if (query.
at() == 0)
1096 LOG(VB_FILE, LOG_INFO,
LOC +
"Adding Programs to 'Do Not Expire' List");
1103 QString key = QString(
"%1_%2")
1104 .arg(chanid).arg(recstartts.toString(
Qt::ISODate));
1106 LOG(VB_FILE, LOG_INFO, QString(
" %1 at %2 in use by %3 on %4")
1107 .arg(QString::number(chanid),
1109 query.
value(3).toString(),
1110 query.
value(4).toString()));
1116 uint chanid,
const QDateTime &recstartts)
const
1118 QString key = QString(
"%1_%2")
1119 .arg(chanid).arg(recstartts.toString(
Qt::ISODate));
1127 return std::any_of(expireList.cbegin(), expireList.cend(),
1128 [chanid,&recstartts](
auto *
info)
1129 { return ((info->GetChanID() == chanid) &&
1130 (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