6 # include <sys/param.h>
8 # include <sys/mount.h>
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()");
128 QList<FileSystemInfo> fsInfos;
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 QList<FileSystemInfo>::iterator fsit;
172 for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
174 if (fsMap.contains(fsit->getFSysID()))
177 fsMap[fsit->getFSysID()] = 0;
178 uint64_t thisKBperMin = 0;
181 for (
auto unknownfs : qAsConst(fsEncoderMap[-1]))
182 fsEncoderMap[fsit->getFSysID()].push_back(unknownfs);
184 if (fsEncoderMap.contains(fsit->getFSysID()))
186 LOG(VB_FILE, LOG_INFO,
187 QString(
"fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
188 .arg(fsit->getFSysID())
189 .arg(fsit->getTotalSpace() / 1024.0 / 1024.0, 7,
'f', 1)
190 .arg(fsit->getUsedSpace() / 1024.0 / 1024.0, 7,
'f', 1)
191 .arg(fsit->getFreeSpace() / 1024.0 / 1024.0, 7,
'f', 1));
193 for (
auto cardid : qAsConst(fsEncoderMap[fsit->getFSysID()]))
200 LOG(VB_FILE, LOG_INFO,
LOC +
201 QString(
"Cardid %1: is not recording, removing it "
202 "from used list.").arg(cardid));
211 maxBitrate = 19500000LL;
212 thisKBperMin += (maxBitrate*((uint64_t)15))>>11;
213 LOG(VB_FILE, LOG_INFO, QString(
" Cardid %1: max bitrate "
214 "%2 Kb/sec, fsID %3 max is now %4 KB/min")
217 .arg(fsit->getFSysID())
221 fsMap[fsit->getFSysID()] = thisKBperMin;
223 if (thisKBperMin > maxKBperMin)
225 LOG(VB_FILE, LOG_INFO,
226 QString(
" Max of %1 KB/min for fsID %2 is higher "
227 "than the existing Max of %3 so we'll use this Max instead")
228 .arg(thisKBperMin).arg(fsit->getFSysID()).arg(maxKBperMin));
229 maxKBperMin = thisKBperMin;
235 uint expireFreq = 15;
239 expireFreq = std::max(3U, std::min(expireFreq, 15U));
242 double expireMinGB = ((maxKBperMin + maxKBperMin/3)
243 * expireFreq + extraKB) >> 20;
244 LOG(VB_GENERAL, LOG_NOTICE,
LOC +
245 QString(
"CalcParams(): Max required Free Space: %1 GB w/freq: %2 min")
246 .arg(expireMinGB, 0,
'f', 1).arg(expireFreq));
252 QMap<int, uint64_t>::iterator it = fsMap.begin();
253 while (it != fsMap.end())
288 if (curTime >= next_expire)
310 if ((curTime.time().minute() % 2) == 0)
314 if (curTime >= next_expire)
316 LOG(VB_FILE, LOG_INFO,
LOC +
"Running now!");
325 else if (maxAge == 0)
335 Sleep(60s - std::chrono::milliseconds(timer.elapsed()));
347 if (sleepTime <= 0ms)
351 std::chrono::milliseconds timeleft = sleepTime;
366 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireLiveTV(%1)").arg(
type));
379 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireOldDeleted()"));
392 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireQuickDeleted()"));
406 QList<FileSystemInfo> fsInfos;
407 QList<FileSystemInfo>::iterator fsit;
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 (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
448 if ((fsit->getHostname() == rechost) &&
449 (fsit->getPath() == recdir))
451 truncateMap[fsit->getFSysID()] =
true;
458 QMap <int, bool> fsMap;
459 for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
461 if (fsMap.contains(fsit->getFSysID()))
464 fsMap[fsit->getFSysID()] =
true;
466 LOG(VB_FILE, LOG_INFO,
467 QString(
"fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
468 .arg(fsit->getFSysID())
469 .arg(fsit->getTotalSpace() / 1024.0 / 1024.0, 7,
'f', 1)
470 .arg(fsit->getUsedSpace() / 1024.0 / 1024.0, 7,
'f', 1)
471 .arg(fsit->getFreeSpace() / 1024.0 / 1024.0, 7,
'f', 1));
473 if ((fsit->getTotalSpace() == -1) || (fsit->getUsedSpace() == -1))
475 LOG(VB_FILE, LOG_ERR,
LOC +
476 QString(
"fsID #%1 has invalid info, AutoExpire cannot run for "
477 "this filesystem. Continuing on to next...")
478 .arg(fsit->getFSysID()));
479 LOG(VB_FILE, LOG_INFO, QString(
"Directories on filesystem ID %1:")
480 .arg(fsit->getFSysID()));
481 QList<FileSystemInfo>::iterator fsit2;
482 for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); ++fsit2)
484 if (fsit2->getFSysID() == fsit->getFSysID())
486 LOG(VB_FILE, LOG_INFO, QString(
" %1:%2")
487 .arg(fsit2->getHostname(), fsit2->getPath()));
494 if (truncateMap.contains(fsit->getFSysID()))
496 LOG(VB_FILE, LOG_INFO,
497 QString(
" fsid %1 has a truncating delete in progress, "
498 "AutoExpire cannot run for this filesystem until the "
499 "delete has finished. Continuing on to next...")
500 .arg(fsit->getFSysID()));
504 if (std::max((int64_t)0LL, fsit->getFreeSpace()) <
507 LOG(VB_FILE, LOG_INFO,
508 QString(
" Not Enough Free Space! We want %1 MB")
511 QMap<QString, int> dirList;
512 QList<FileSystemInfo>::iterator fsit2;
514 LOG(VB_FILE, LOG_INFO,
515 QString(
" Directories on filesystem ID %1:")
516 .arg(fsit->getFSysID()));
518 for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); ++fsit2)
520 if (fsit2->getFSysID() == fsit->getFSysID())
522 LOG(VB_FILE, LOG_INFO, QString(
" %1:%2")
523 .arg(fsit2->getHostname(), fsit2->getPath()));
524 dirList[fsit2->getHostname() +
":" + fsit2->getPath()] = 1;
528 LOG(VB_FILE, LOG_INFO,
529 " Searching for files expirable in these directories");
531 auto it = expireList.begin();
532 while ((it != expireList.end()) &&
533 (std::max((int64_t)0LL, fsit->getFreeSpace()) <
539 LOG(VB_FILE, LOG_INFO, QString(
" Checking %1 => %2")
545 bool foundFile =
false;
553 ((
p->GetHostname() == myHostName) &&
563 if (!foundFile && (
p->GetHostname() != myHostName))
568 if (
file.startsWith(
"/"))
570 p->SetPathname(
file);
571 p->SetHostname(myHostName);
578 LOG(VB_FILE, LOG_ERR,
LOC +
579 QString(
" ERROR: Can't find file for %1")
585 QFileInfo vidFile(
p->GetPathname());
586 if (dirList.contains(
p->GetHostname() +
':' + vidFile.path()))
588 fsit->setUsedSpace(fsit->getUsedSpace()
589 - (
p->GetFilesize() / 1024));
590 deleteList.push_back(
p);
592 LOG(VB_FILE, LOG_INFO,
593 QString(
" FOUND file expirable. "
594 "%1 is located at %2 which is on fsID #%3. "
595 "Adding to deleteList. After deleting we "
596 "should have %4 MB free on this filesystem.")
598 p->GetPathname()).arg(fsit->getFSysID())
599 .arg(fsit->getFreeSpace() / 1024));
618 if (deleteList.empty())
620 LOG(VB_FILE, LOG_INFO,
LOC +
"SendDeleteMessages. Nothing to expire.");
624 LOG(VB_FILE, LOG_INFO,
LOC +
625 "SendDeleteMessages, cycling through deleteList.");
626 auto it = deleteList.begin();
627 while (it != deleteList.end())
629 msg = QString(
"%1Expiring %2 MB for %3 => %4")
631 QString::number((*it)->GetFilesize() >> 20),
635 LOG(VB_GENERAL, LOG_NOTICE, msg);
638 MythEvent me(QString(
"AUTO_EXPIRE %1 %2").arg((*it)->GetChanID())
653 QMap<QString, int> maxEpisodes;
654 QMap<QString, int>::Iterator maxIter;
655 QMap<QString, int> episodeParts;
659 query.
prepare(
"SELECT recordid, maxepisodes, title "
660 "FROM record WHERE maxepisodes > 0 "
661 "ORDER BY recordid ASC, maxepisodes DESC");
665 LOG(VB_FILE, LOG_INFO,
LOC +
666 QString(
"Found %1 record profiles using max episode expiration")
670 LOG(VB_FILE, LOG_INFO, QString(
" %1 (%2 for rec id %3)")
671 .arg(query.
value(2).toString())
672 .arg(query.
value(1).toInt())
673 .arg(query.
value(0).toInt()));
674 maxEpisodes[query.
value(0).toString()] = query.
value(1).toInt();
678 LOG(VB_FILE, LOG_INFO,
LOC +
679 "Checking episode count for each recording profile using max episodes");
680 for (maxIter = maxEpisodes.begin(); maxIter != maxEpisodes.end(); ++maxIter)
682 query.
prepare(
"SELECT chanid, starttime, title, progstart, progend, "
685 "WHERE recordid = :RECID AND preserve = 0 "
686 "AND recgroup NOT IN ('LiveTV', 'Deleted') "
687 "ORDER BY starttime DESC;");
688 query.
bindValue(
":RECID", maxIter.key());
696 LOG(VB_FILE, LOG_INFO, QString(
" Recordid %1 has %2 recordings.")
699 if (query.
size() > 0)
706 QString title = query.
value(2).toString();
709 int duplicate = query.
value(5).toInt();
711 episodeKey = QString(
"%1_%2_%3")
712 .arg(QString::number(chanid),
717 (!episodeParts.contains(episodeKey)) &&
721 QString(
"%1Deleting %2 at %3 => %4. "
722 "Too many episodes, we only want to keep %5.")
724 QString::number(chanid),
727 QString::number(*maxIter));
729 LOG(VB_GENERAL, LOG_NOTICE, msg);
738 msg = QString(
"DELETE_RECORDING %1 %2")
750 if (episodeParts.contains(episodeKey))
752 episodeParts[episodeKey] = episodeParts[episodeKey] + 1;
756 episodeParts[episodeKey] = 1;
798 QString msg =
"MythTV AutoExpire List ";
799 if (expHost !=
"ALL")
800 msg += QString(
"for '%1' ").arg(expHost);
801 msg +=
"(programs listed in order of expiration)";
802 std::cout << msg.toLocal8Bit().constData() << std::endl;
804 for (
auto *first : expireList)
806 if (expHost !=
"ALL" && first->GetHostname() != expHost)
810 title = title.leftJustified(39,
' ',
true);
812 QString outstr = QString(
"%1 %2 MB %3 [%4]")
814 QString::number(first->GetFilesize() >> 20)
815 .rightJustified(5,
' ',
true),
817 .leftJustified(24,
' ',
true),
818 QString::number(first->GetRecordingPriority())
819 .rightJustified(3,
' ',
true));
820 QByteArray out = outstr.toLocal8Bit();
822 std::cout << out.constData() << std::endl;
844 strList << QString::number(expireList.size());
846 for (
auto & info : expireList)
847 info->ToStringList(strList);
868 for (
auto & info : expireList)
880 while (!expireList.empty())
883 pginfo = expireList.back();
885 expireList.pop_back();
907 msg =
"Adding programs expirable in Oldest First order";
908 where =
"autoexpire > 0";
910 orderby =
"recorded.watched DESC, ";
911 orderby +=
"starttime ASC";
914 msg =
"Adding programs expirable in Lowest Priority First order";
915 where =
"autoexpire > 0";
917 orderby =
"recorded.watched DESC, ";
918 orderby +=
"recorded.recpriority ASC, starttime ASC";
921 msg =
"Adding programs expirable in Weighted Time Priority order";
922 where =
"autoexpire > 0";
924 orderby =
"recorded.watched DESC, ";
925 orderby += QString(
"DATE_ADD(starttime, INTERVAL '%1' * "
926 "recorded.recpriority DAY) ASC")
930 msg =
"Adding Short LiveTV programs in starttime order";
931 where =
"recgroup = 'LiveTV' "
932 "AND endtime < DATE_ADD(starttime, INTERVAL '30' SECOND) "
933 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ";
934 orderby =
"starttime ASC";
937 msg =
"Adding LiveTV programs in starttime order";
938 where = QString(
"recgroup = 'LiveTV' "
939 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
941 orderby =
"starttime ASC";
946 msg = QString(
"Adding programs deleted more than %1 days ago")
948 where = QString(
"recgroup = 'Deleted' "
949 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
951 orderby =
"starttime ASC";
956 msg = QString(
"Adding programs deleted more than 5 minutes ago");
957 where = QString(
"recgroup = 'Deleted' "
958 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ");
959 orderby =
"lastmodified ASC";
962 msg =
"Adding deleted programs in FIFO order";
963 where =
"recgroup = 'Deleted'";
964 orderby =
"lastmodified ASC";
968 LOG(VB_FILE, LOG_INFO,
LOC +
"FillDBOrdered: " + msg);
971 QString querystr = QString(
972 "SELECT recorded.chanid, starttime "
974 "LEFT JOIN channel ON recorded.chanid = channel.chanid "
975 "WHERE %1 AND deletepending = 0 "
976 "ORDER BY autoexpire DESC, %2").arg(where, orderby);
990 LOG(VB_FILE, LOG_INFO,
LOC +
991 QString(
" Skipping %1 at %2 because it is in Don't Expire "
993 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
997 LOG(VB_FILE, LOG_INFO,
LOC +
998 QString(
" Skipping %1 at %2 because it is already in Expire "
1000 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1004 auto *pginfo =
new ProgramInfo(chanid, recstartts);
1005 if (pginfo->GetChanID())
1007 LOG(VB_FILE, LOG_INFO,
LOC + QString(
" Adding %1 at %2")
1008 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1009 expireList.push_back(pginfo);
1013 LOG(VB_FILE, LOG_INFO,
LOC +
1014 QString(
" Skipping %1 at %2 "
1015 "because it could not be loaded from the DB")
1016 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1041 QString msg = QString(
"Cardid %1: is starting a recording on")
1044 msg.append(
" an unknown fsID soon.");
1046 msg.append(QString(
" fsID %2 soon.").arg(fsID));
1047 LOG(VB_FILE, LOG_INFO,
LOC + msg);
1075 "SELECT chanid, starttime, lastupdatetime, recusage, hostname "
1076 "FROM inuseprograms");
1078 if (!query.
exec() || !query.
next())
1081 LOG(VB_FILE, LOG_INFO,
LOC +
"Adding Programs to 'Do Not Expire' List");
1092 QString key = QString(
"%1_%2")
1093 .arg(chanid).arg(recstartts.toString(
Qt::ISODate));
1095 LOG(VB_FILE, LOG_INFO, QString(
" %1 at %2 in use by %3 on %4")
1096 .arg(QString::number(chanid),
1098 query.
value(3).toString(),
1099 query.
value(4).toString()));
1102 while (query.
next());
1106 uint chanid,
const QDateTime &recstartts)
const
1108 QString key = QString(
"%1_%2")
1109 .arg(chanid).arg(recstartts.toString(
Qt::ISODate));
1117 return std::any_of(expireList.cbegin(), expireList.cend(),
1118 [chanid,&recstartts](
auto *info)
1119 { return ((info->GetChanID() == chanid) &&
1120 (info->GetRecordingStartTime() == recstartts)); } );