45#define LOC QString("AutoExpire: ")
46#define LOC_ERR QString("AutoExpire Error: ")
75 m_encoderList(tvList),
77 m_expireThreadRun(
true)
126 LOG(VB_FILE, LOG_INFO,
LOC +
"CalcParams()");
146 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Filesystem Info cache is empty, unable "
147 "to calculate necessary parameters.");
151 uint64_t maxKBperMin = 0;
152 uint64_t extraKB =
static_cast<uint64_t
>
156 QMap<int, uint64_t> fsMap;
157 QMap<int, std::vector<int> > fsEncoderMap;
166 fsEncoderMap[*ueit].push_back(ueit.key());
171 for (
const auto& fs : std::as_const(fsInfos))
173 if (fsMap.contains(fs.getFSysID()))
176 fsMap[fs.getFSysID()] = 0;
177 uint64_t thisKBperMin = 0;
180 for (
auto unknownfs : std::as_const(fsEncoderMap[-1]))
181 fsEncoderMap[fs.getFSysID()].push_back(unknownfs);
183 if (fsEncoderMap.contains(fs.getFSysID()))
185 LOG(VB_FILE, LOG_INFO,
186 QString(
"fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
188 .arg(fs.getTotalSpace() / 1024.0 / 1024.0, 7,
'f', 1)
189 .arg(fs.getUsedSpace() / 1024.0 / 1024.0, 7,
'f', 1)
190 .arg(fs.getFreeSpace() / 1024.0 / 1024.0, 7,
'f', 1));
192 for (
auto cardid : std::as_const(fsEncoderMap[fs.getFSysID()]))
202 LOG(VB_FILE, LOG_INFO,
LOC +
203 QString(
"Cardid %1: is not recording, removing it "
204 "from used list.").arg(cardid));
213 maxBitrate = 19500000LL;
214 thisKBperMin += (maxBitrate*((uint64_t)15))>>11;
215 LOG(VB_FILE, LOG_INFO, QString(
" Cardid %1: max bitrate "
216 "%2 Kb/sec, fsID %3 max is now %4 KB/min")
223 fsMap[fs.getFSysID()] = thisKBperMin;
225 if (thisKBperMin > maxKBperMin)
227 LOG(VB_FILE, LOG_INFO,
228 QString(
" Max of %1 KB/min for fsID %2 is higher "
229 "than the existing Max of %3 so we'll use this Max instead")
230 .arg(thisKBperMin).arg(fs.getFSysID()).arg(maxKBperMin));
231 maxKBperMin = thisKBperMin;
237 uint expireFreq = 15;
244 double expireMinGB = ((maxKBperMin + maxKBperMin/3)
245 * expireFreq + extraKB) >> 20;
246 LOG(VB_GENERAL, LOG_NOTICE,
LOC +
247 QString(
"CalcParams(): Max required Free Space: %1 GB w/freq: %2 min")
248 .arg(expireMinGB, 0,
'f', 1).arg(expireFreq));
254 QMap<int, uint64_t>::iterator it = fsMap.begin();
255 while (it != fsMap.end())
257 m_desiredSpace[it.key()] = ((*it + *it/3) * expireFreq) + extraKB;
290 if (curTime >= next_expire)
312 if ((curTime.time().minute() % 2) == 0)
316 if (curTime >= next_expire)
318 LOG(VB_FILE, LOG_INFO,
LOC +
"Running now!");
327 else if (maxAge == 0)
337 Sleep(60s - std::chrono::milliseconds(timer.elapsed()));
349 if (sleepTime <= 0ms)
353 std::chrono::milliseconds timeleft = sleepTime;
368 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireLiveTV(%1)").arg(
type));
381 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireOldDeleted()"));
394 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireQuickDeleted()"));
410 LOG(VB_FILE, LOG_INFO,
LOC +
"ExpireRecordings()");
417 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Filesystem Info cache is empty, unable "
418 "to determine what Recordings to expire");
425 QMap <int, bool> truncateMap;
427 query.
prepare(
"SELECT DISTINCT rechost, recdir "
428 "FROM inuseprograms "
429 "WHERE recusage = 'truncatingdelete' "
430 "AND lastupdatetime > DATE_ADD(NOW(), INTERVAL -2 MINUTE);");
440 QString rechost = query.
value(0).toString();
441 QString recdir = query.
value(1).toString();
443 LOG(VB_FILE, LOG_INFO,
LOC +
444 QString(
"%1:%2 has an in-progress truncating delete.")
445 .arg(rechost, recdir));
447 for (
const auto& fs : std::as_const(fsInfos))
449 if ((fs.getHostname() == rechost) &&
450 (fs.getPath() == recdir))
452 truncateMap[fs.getFSysID()] =
true;
459 QMap <int, bool> fsMap;
461#
if QT_VERSION < QT_VERSION_CHECK(6,0,0)
462 auto* fsit = fsInfos.begin();
464 auto fsit = fsInfos.begin();
466fsit != fsInfos.end(); ++fsit)
468 if (fsMap.contains(fsit->getFSysID()))
471 fsMap[fsit->getFSysID()] =
true;
473 LOG(VB_FILE, LOG_INFO,
474 QString(
"fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
475 .arg(fsit->getFSysID())
476 .arg(fsit->getTotalSpace() / 1024.0 / 1024.0, 7,
'f', 1)
477 .arg(fsit->getUsedSpace() / 1024.0 / 1024.0, 7,
'f', 1)
478 .arg(fsit->getFreeSpace() / 1024.0 / 1024.0, 7,
'f', 1));
480 if ((fsit->getTotalSpace() == -1) || (fsit->getUsedSpace() == -1))
482 LOG(VB_FILE, LOG_ERR,
LOC +
483 QString(
"fsID #%1 has invalid info, AutoExpire cannot run for "
484 "this filesystem. Continuing on to next...")
485 .arg(fsit->getFSysID()));
486 LOG(VB_FILE, LOG_INFO, QString(
"Directories on filesystem ID %1:")
487 .arg(fsit->getFSysID()));
489#
if QT_VERSION < QT_VERSION_CHECK(6,0,0)
490 auto* fsit2 = fsInfos.begin();
492 auto fsit2 = fsInfos.begin();
494 fsit2 != fsInfos.end(); ++fsit2)
496 if (fsit2->getFSysID() == fsit->getFSysID())
498 LOG(VB_FILE, LOG_INFO, QString(
" %1:%2")
499 .arg(fsit2->getHostname(), fsit2->getPath()));
506 if (truncateMap.contains(fsit->getFSysID()))
508 LOG(VB_FILE, LOG_INFO,
509 QString(
" fsid %1 has a truncating delete in progress, "
510 "AutoExpire cannot run for this filesystem until the "
511 "delete has finished. Continuing on to next...")
512 .arg(fsit->getFSysID()));
516 if (std::max((int64_t)0LL, fsit->getFreeSpace()) <
519 LOG(VB_FILE, LOG_INFO,
520 QString(
" Not Enough Free Space! We want %1 MB")
523 QMap<QString, int> dirList;
525 LOG(VB_FILE, LOG_INFO,
526 QString(
" Directories on filesystem ID %1:")
527 .arg(fsit->getFSysID()));
529 for (
const auto&
fs2 : std::as_const(fsInfos))
531 if (
fs2.getFSysID() == fsit->getFSysID())
533 LOG(VB_FILE, LOG_INFO, QString(
" %1:%2")
534 .arg(
fs2.getHostname(),
fs2.getPath()));
535 dirList[
fs2.getHostname() +
":" +
fs2.getPath()] = 1;
539 LOG(VB_FILE, LOG_INFO,
540 " Searching for files expirable in these directories");
542 auto it = expireList.begin();
543 while ((it != expireList.end()) &&
544 (std::max((int64_t)0LL, fsit->getFreeSpace()) <
550 LOG(VB_FILE, LOG_INFO, QString(
" Checking %1 => %2")
556 bool foundFile =
false;
564 ((
p->GetHostname() == myHostName) &&
574 if (!foundFile && (
p->GetHostname() != myHostName))
579 if (
file.startsWith(
"/"))
581 p->SetPathname(
file);
582 p->SetHostname(myHostName);
589 LOG(VB_FILE, LOG_ERR,
LOC +
590 QString(
" ERROR: Can't find file for %1")
596 QFileInfo vidFile(
p->GetPathname());
597 if (dirList.contains(
p->GetHostname() +
':' + vidFile.path()))
599 fsit->setUsedSpace(fsit->getUsedSpace()
600 - (
p->GetFilesize() / 1024));
601 deleteList.push_back(
p);
603 LOG(VB_FILE, LOG_INFO,
604 QString(
" FOUND file expirable. "
605 "%1 is located at %2 which is on fsID #%3. "
606 "Adding to deleteList. After deleting we "
607 "should have %4 MB free on this filesystem.")
609 p->GetPathname()).arg(fsit->getFSysID())
610 .arg(fsit->getFreeSpace() / 1024));
629 if (deleteList.empty())
631 LOG(VB_FILE, LOG_INFO,
LOC +
"SendDeleteMessages. Nothing to expire.");
635 LOG(VB_FILE, LOG_INFO,
LOC +
636 "SendDeleteMessages, cycling through deleteList.");
637 auto it = deleteList.begin();
638 while (it != deleteList.end())
640 msg = QString(
"%1Expiring %2 MB for %3 => %4")
642 QString::number((*it)->GetFilesize() >> 20),
646 LOG(VB_GENERAL, LOG_NOTICE, msg);
649 MythEvent me(QString(
"AUTO_EXPIRE %1 %2").arg((*it)->GetChanID())
664 QMap<QString, int> maxEpisodes;
665 QMap<QString, int>::Iterator maxIter;
666 QMap<QString, int> episodeParts;
670 query.
prepare(
"SELECT recordid, maxepisodes, title "
671 "FROM record WHERE maxepisodes > 0 "
672 "ORDER BY recordid ASC, maxepisodes DESC");
676 LOG(VB_FILE, LOG_INFO,
LOC +
677 QString(
"Found %1 record profiles using max episode expiration")
681 LOG(VB_FILE, LOG_INFO, QString(
" %1 (%2 for rec id %3)")
682 .arg(query.
value(2).toString())
683 .arg(query.
value(1).toInt())
684 .arg(query.
value(0).toInt()));
685 maxEpisodes[query.
value(0).toString()] = query.
value(1).toInt();
689 LOG(VB_FILE, LOG_INFO,
LOC +
690 "Checking episode count for each recording profile using max episodes");
691 for (maxIter = maxEpisodes.begin(); maxIter != maxEpisodes.end(); ++maxIter)
693 query.
prepare(
"SELECT chanid, starttime, title, progstart, progend, "
696 "WHERE recordid = :RECID AND preserve = 0 "
697 "AND recgroup NOT IN ('LiveTV', 'Deleted') "
698 "ORDER BY starttime DESC;");
699 query.
bindValue(
":RECID", maxIter.key());
707 LOG(VB_FILE, LOG_INFO, QString(
" Recordid %1 has %2 recordings.")
710 if (query.
size() > 0)
717 QString title = query.
value(2).toString();
720 int duplicate = query.
value(5).toInt();
722 episodeKey = QString(
"%1_%2_%3")
723 .arg(QString::number(chanid),
728 (!episodeParts.contains(episodeKey)) &&
732 QString(
"%1Deleting %2 at %3 => %4. "
733 "Too many episodes, we only want to keep %5.")
735 QString::number(chanid),
738 QString::number(*maxIter));
740 LOG(VB_GENERAL, LOG_NOTICE, msg);
749 msg = QString(
"DELETE_RECORDING %1 %2")
761 if (episodeParts.contains(episodeKey))
763 episodeParts[episodeKey] = episodeParts[episodeKey] + 1;
767 episodeParts[episodeKey] = 1;
809 QString msg =
"MythTV AutoExpire List ";
810 if (expHost !=
"ALL")
811 msg += QString(
"for '%1' ").arg(expHost);
812 msg +=
"(programs listed in order of expiration)";
813 std::cout << msg.toLocal8Bit().constData() << std::endl;
815 for (
auto *first : expireList)
817 if (expHost !=
"ALL" && first->GetHostname() != expHost)
821 title = title.leftJustified(39,
' ',
true);
823 QString outstr = QString(
"%1 %2 MB %3 [%4]")
825 QString::number(first->GetFilesize() >> 20)
826 .rightJustified(5,
' ',
true),
828 .leftJustified(24,
' ',
true),
829 QString::number(first->GetRecordingPriority())
830 .rightJustified(3,
' ',
true));
831 QByteArray out = outstr.toLocal8Bit();
833 std::cout << out.constData() << std::endl;
855 strList << QString::number(expireList.size());
857 for (
auto &
info : expireList)
858 info->ToStringList(strList);
879 for (
auto &
info : expireList)
891 while (!expireList.empty())
894 pginfo = expireList.back();
896 expireList.pop_back();
918 msg =
"Adding programs expirable in Oldest First order";
919 where =
"autoexpire > 0";
921 orderby =
"recorded.watched DESC, ";
922 orderby +=
"starttime ASC";
925 msg =
"Adding programs expirable in Lowest Priority First order";
926 where =
"autoexpire > 0";
928 orderby =
"recorded.watched DESC, ";
929 orderby +=
"recorded.recpriority ASC, starttime ASC";
932 msg =
"Adding programs expirable in Weighted Time Priority order";
933 where =
"autoexpire > 0";
935 orderby =
"recorded.watched DESC, ";
936 orderby += QString(
"DATE_ADD(starttime, INTERVAL '%1' * "
937 "recorded.recpriority DAY) ASC")
941 msg =
"Adding Short LiveTV programs in starttime order";
942 where =
"recgroup = 'LiveTV' "
943 "AND endtime < DATE_ADD(starttime, INTERVAL '30' SECOND) "
944 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ";
945 orderby =
"starttime ASC";
948 msg =
"Adding LiveTV programs in starttime order";
949 where = QString(
"recgroup = 'LiveTV' "
950 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
952 orderby =
"starttime ASC";
958 msg = QString(
"Adding programs deleted more than %1 days ago")
960 where = QString(
"recgroup = 'Deleted' "
961 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
963 orderby =
"starttime ASC";
968 msg = QString(
"Adding programs deleted more than 5 minutes ago");
969 where = QString(
"recgroup = 'Deleted' "
970 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ");
971 orderby =
"lastmodified ASC";
974 msg =
"Adding deleted programs in FIFO order";
975 where =
"recgroup = 'Deleted'";
976 orderby =
"lastmodified ASC";
980 LOG(VB_FILE, LOG_INFO,
LOC +
"FillDBOrdered: " + msg);
983 QString querystr = QString(
984 "SELECT recorded.chanid, starttime "
986 "LEFT JOIN channel ON recorded.chanid = channel.chanid "
987 "WHERE %1 AND deletepending = 0 "
988 "ORDER BY autoexpire DESC, %2").arg(where, orderby);
1002 LOG(VB_FILE, LOG_INFO,
LOC +
1003 QString(
" Skipping %1 at %2 because it is in Don't Expire "
1005 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1009 LOG(VB_FILE, LOG_INFO,
LOC +
1010 QString(
" Skipping %1 at %2 because it is already in Expire "
1012 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1016 auto *pginfo =
new ProgramInfo(chanid, recstartts);
1017 if (pginfo->GetChanID())
1019 LOG(VB_FILE, LOG_INFO,
LOC + QString(
" Adding %1 at %2")
1020 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1021 expireList.push_back(pginfo);
1025 LOG(VB_FILE, LOG_INFO,
LOC +
1026 QString(
" Skipping %1 at %2 "
1027 "because it could not be loaded from the DB")
1028 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1053 QString msg = QString(
"Cardid %1: is starting a recording on")
1056 msg.append(
" an unknown fsID soon.");
1058 msg.append(QString(
" fsID %2 soon.").arg(fsID));
1059 LOG(VB_FILE, LOG_INFO,
LOC + msg);
1087 "SELECT chanid, starttime, lastupdatetime, recusage, hostname "
1088 "FROM inuseprograms");
1094 while (query.
next())
1096 if (query.
at() == 0)
1097 LOG(VB_FILE, LOG_INFO,
LOC +
"Adding Programs to 'Do Not Expire' List");
1104 QString key = QString(
"%1_%2")
1105 .arg(chanid).arg(recstartts.toString(
Qt::ISODate));
1107 LOG(VB_FILE, LOG_INFO, QString(
" %1 at %2 in use by %3 on %4")
1108 .arg(QString::number(chanid),
1110 query.
value(3).toString(),
1111 query.
value(4).toString()));
1117 uint chanid,
const QDateTime &recstartts)
const
1119 QString key = QString(
"%1_%2")
1120 .arg(chanid).arg(recstartts.toString(
Qt::ISODate));
1128 return std::any_of(expireList.cbegin(), expireList.cend(),
1129 [chanid,&recstartts](
auto *
info)
1130 { return ((info->GetChanID() == chanid) &&
1131 (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