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()]))
203 LOG(VB_FILE, LOG_INFO,
LOC +
204 QString(
"Cardid %1: is not recording, removing it "
205 "from used list.").arg(cardid));
214 maxBitrate = 19500000LL;
215 thisKBperMin += (maxBitrate*((uint64_t)15))>>11;
216 LOG(VB_FILE, LOG_INFO, QString(
" Cardid %1: max bitrate "
217 "%2 Kb/sec, fsID %3 max is now %4 KB/min")
220 .arg(fsit->getFSysID())
224 fsMap[fsit->getFSysID()] = thisKBperMin;
226 if (thisKBperMin > maxKBperMin)
228 LOG(VB_FILE, LOG_INFO,
229 QString(
" Max of %1 KB/min for fsID %2 is higher "
230 "than the existing Max of %3 so we'll use this Max instead")
231 .arg(thisKBperMin).arg(fsit->getFSysID()).arg(maxKBperMin));
232 maxKBperMin = thisKBperMin;
238 uint expireFreq = 15;
242 expireFreq = std::max(3U, std::min(expireFreq, 15U));
245 double expireMinGB = ((maxKBperMin + maxKBperMin/3)
246 * expireFreq + extraKB) >> 20;
247 LOG(VB_GENERAL, LOG_NOTICE,
LOC +
248 QString(
"CalcParams(): Max required Free Space: %1 GB w/freq: %2 min")
249 .arg(expireMinGB, 0,
'f', 1).arg(expireFreq));
255 QMap<int, uint64_t>::iterator it = fsMap.begin();
256 while (it != fsMap.end())
291 if (curTime >= next_expire)
313 if ((curTime.time().minute() % 2) == 0)
317 if (curTime >= next_expire)
319 LOG(VB_FILE, LOG_INFO,
LOC +
"Running now!");
328 else if (maxAge == 0)
338 Sleep(60s - std::chrono::milliseconds(timer.elapsed()));
350 if (sleepTime <= 0ms)
354 std::chrono::milliseconds timeleft = sleepTime;
369 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireLiveTV(%1)").arg(
type));
382 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireOldDeleted()"));
395 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"ExpireQuickDeleted()"));
409 QList<FileSystemInfo> fsInfos;
410 QList<FileSystemInfo>::iterator fsit;
412 LOG(VB_FILE, LOG_INFO,
LOC +
"ExpireRecordings()");
419 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Filesystem Info cache is empty, unable "
420 "to determine what Recordings to expire");
427 QMap <int, bool> truncateMap;
429 query.
prepare(
"SELECT DISTINCT rechost, recdir "
430 "FROM inuseprograms "
431 "WHERE recusage = 'truncatingdelete' "
432 "AND lastupdatetime > DATE_ADD(NOW(), INTERVAL -2 MINUTE);");
442 QString rechost = query.
value(0).toString();
443 QString recdir = query.
value(1).toString();
445 LOG(VB_FILE, LOG_INFO,
LOC +
446 QString(
"%1:%2 has an in-progress truncating delete.")
447 .arg(rechost, recdir));
449 for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
451 if ((fsit->getHostname() == rechost) &&
452 (fsit->getPath() == recdir))
454 truncateMap[fsit->getFSysID()] =
true;
461 QMap <int, bool> fsMap;
462 for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
464 if (fsMap.contains(fsit->getFSysID()))
467 fsMap[fsit->getFSysID()] =
true;
469 LOG(VB_FILE, LOG_INFO,
470 QString(
"fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
471 .arg(fsit->getFSysID())
472 .arg(fsit->getTotalSpace() / 1024.0 / 1024.0, 7,
'f', 1)
473 .arg(fsit->getUsedSpace() / 1024.0 / 1024.0, 7,
'f', 1)
474 .arg(fsit->getFreeSpace() / 1024.0 / 1024.0, 7,
'f', 1));
476 if ((fsit->getTotalSpace() == -1) || (fsit->getUsedSpace() == -1))
478 LOG(VB_FILE, LOG_ERR,
LOC +
479 QString(
"fsID #%1 has invalid info, AutoExpire cannot run for "
480 "this filesystem. Continuing on to next...")
481 .arg(fsit->getFSysID()));
482 LOG(VB_FILE, LOG_INFO, QString(
"Directories on filesystem ID %1:")
483 .arg(fsit->getFSysID()));
484 QList<FileSystemInfo>::iterator fsit2;
485 for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); ++fsit2)
487 if (fsit2->getFSysID() == fsit->getFSysID())
489 LOG(VB_FILE, LOG_INFO, QString(
" %1:%2")
490 .arg(fsit2->getHostname(), fsit2->getPath()));
497 if (truncateMap.contains(fsit->getFSysID()))
499 LOG(VB_FILE, LOG_INFO,
500 QString(
" fsid %1 has a truncating delete in progress, "
501 "AutoExpire cannot run for this filesystem until the "
502 "delete has finished. Continuing on to next...")
503 .arg(fsit->getFSysID()));
507 if (std::max((int64_t)0LL, fsit->getFreeSpace()) <
510 LOG(VB_FILE, LOG_INFO,
511 QString(
" Not Enough Free Space! We want %1 MB")
514 QMap<QString, int> dirList;
515 QList<FileSystemInfo>::iterator fsit2;
517 LOG(VB_FILE, LOG_INFO,
518 QString(
" Directories on filesystem ID %1:")
519 .arg(fsit->getFSysID()));
521 for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); ++fsit2)
523 if (fsit2->getFSysID() == fsit->getFSysID())
525 LOG(VB_FILE, LOG_INFO, QString(
" %1:%2")
526 .arg(fsit2->getHostname(), fsit2->getPath()));
527 dirList[fsit2->getHostname() +
":" + fsit2->getPath()] = 1;
531 LOG(VB_FILE, LOG_INFO,
532 " Searching for files expirable in these directories");
534 auto it = expireList.begin();
535 while ((it != expireList.end()) &&
536 (std::max((int64_t)0LL, fsit->getFreeSpace()) <
542 LOG(VB_FILE, LOG_INFO, QString(
" Checking %1 => %2")
548 bool foundFile =
false;
556 ((
p->GetHostname() == myHostName) &&
566 if (!foundFile && (
p->GetHostname() != myHostName))
571 if (
file.startsWith(
"/"))
573 p->SetPathname(
file);
574 p->SetHostname(myHostName);
581 LOG(VB_FILE, LOG_ERR,
LOC +
582 QString(
" ERROR: Can't find file for %1")
588 QFileInfo vidFile(
p->GetPathname());
589 if (dirList.contains(
p->GetHostname() +
':' + vidFile.path()))
591 fsit->setUsedSpace(fsit->getUsedSpace()
592 - (
p->GetFilesize() / 1024));
593 deleteList.push_back(
p);
595 LOG(VB_FILE, LOG_INFO,
596 QString(
" FOUND file expirable. "
597 "%1 is located at %2 which is on fsID #%3. "
598 "Adding to deleteList. After deleting we "
599 "should have %4 MB free on this filesystem.")
601 p->GetPathname()).arg(fsit->getFSysID())
602 .arg(fsit->getFreeSpace() / 1024));
621 if (deleteList.empty())
623 LOG(VB_FILE, LOG_INFO,
LOC +
"SendDeleteMessages. Nothing to expire.");
627 LOG(VB_FILE, LOG_INFO,
LOC +
628 "SendDeleteMessages, cycling through deleteList.");
629 auto it = deleteList.begin();
630 while (it != deleteList.end())
632 msg = QString(
"%1Expiring %2 MB for %3 => %4")
634 QString::number((*it)->GetFilesize() >> 20),
638 LOG(VB_GENERAL, LOG_NOTICE, msg);
641 MythEvent me(QString(
"AUTO_EXPIRE %1 %2").arg((*it)->GetChanID())
656 QMap<QString, int> maxEpisodes;
657 QMap<QString, int>::Iterator maxIter;
658 QMap<QString, int> episodeParts;
662 query.
prepare(
"SELECT recordid, maxepisodes, title "
663 "FROM record WHERE maxepisodes > 0 "
664 "ORDER BY recordid ASC, maxepisodes DESC");
668 LOG(VB_FILE, LOG_INFO,
LOC +
669 QString(
"Found %1 record profiles using max episode expiration")
673 LOG(VB_FILE, LOG_INFO, QString(
" %1 (%2 for rec id %3)")
674 .arg(query.
value(2).toString())
675 .arg(query.
value(1).toInt())
676 .arg(query.
value(0).toInt()));
677 maxEpisodes[query.
value(0).toString()] = query.
value(1).toInt();
681 LOG(VB_FILE, LOG_INFO,
LOC +
682 "Checking episode count for each recording profile using max episodes");
683 for (maxIter = maxEpisodes.begin(); maxIter != maxEpisodes.end(); ++maxIter)
685 query.
prepare(
"SELECT chanid, starttime, title, progstart, progend, "
688 "WHERE recordid = :RECID AND preserve = 0 "
689 "AND recgroup NOT IN ('LiveTV', 'Deleted') "
690 "ORDER BY starttime DESC;");
691 query.
bindValue(
":RECID", maxIter.key());
699 LOG(VB_FILE, LOG_INFO, QString(
" Recordid %1 has %2 recordings.")
702 if (query.
size() > 0)
709 QString title = query.
value(2).toString();
712 int duplicate = query.
value(5).toInt();
714 episodeKey = QString(
"%1_%2_%3")
715 .arg(QString::number(chanid),
720 (!episodeParts.contains(episodeKey)) &&
724 QString(
"%1Deleting %2 at %3 => %4. "
725 "Too many episodes, we only want to keep %5.")
727 QString::number(chanid),
730 QString::number(*maxIter));
732 LOG(VB_GENERAL, LOG_NOTICE, msg);
741 msg = QString(
"DELETE_RECORDING %1 %2")
753 if (episodeParts.contains(episodeKey))
755 episodeParts[episodeKey] = episodeParts[episodeKey] + 1;
759 episodeParts[episodeKey] = 1;
801 QString msg =
"MythTV AutoExpire List ";
802 if (expHost !=
"ALL")
803 msg += QString(
"for '%1' ").arg(expHost);
804 msg +=
"(programs listed in order of expiration)";
805 std::cout << msg.toLocal8Bit().constData() << std::endl;
807 for (
auto *first : expireList)
809 if (expHost !=
"ALL" && first->GetHostname() != expHost)
813 title = title.leftJustified(39,
' ',
true);
815 QString outstr = QString(
"%1 %2 MB %3 [%4]")
817 QString::number(first->GetFilesize() >> 20)
818 .rightJustified(5,
' ',
true),
820 .leftJustified(24,
' ',
true),
821 QString::number(first->GetRecordingPriority())
822 .rightJustified(3,
' ',
true));
823 QByteArray out = outstr.toLocal8Bit();
825 std::cout << out.constData() << std::endl;
847 strList << QString::number(expireList.size());
849 for (
auto & info : expireList)
850 info->ToStringList(strList);
871 for (
auto & info : expireList)
883 while (!expireList.empty())
886 pginfo = expireList.back();
888 expireList.pop_back();
910 msg =
"Adding programs expirable in Oldest First order";
911 where =
"autoexpire > 0";
913 orderby =
"recorded.watched DESC, ";
914 orderby +=
"starttime ASC";
917 msg =
"Adding programs expirable in Lowest Priority First order";
918 where =
"autoexpire > 0";
920 orderby =
"recorded.watched DESC, ";
921 orderby +=
"recorded.recpriority ASC, starttime ASC";
924 msg =
"Adding programs expirable in Weighted Time Priority order";
925 where =
"autoexpire > 0";
927 orderby =
"recorded.watched DESC, ";
928 orderby += QString(
"DATE_ADD(starttime, INTERVAL '%1' * "
929 "recorded.recpriority DAY) ASC")
933 msg =
"Adding Short LiveTV programs in starttime order";
934 where =
"recgroup = 'LiveTV' "
935 "AND endtime < DATE_ADD(starttime, INTERVAL '30' SECOND) "
936 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ";
937 orderby =
"starttime ASC";
940 msg =
"Adding LiveTV programs in starttime order";
941 where = QString(
"recgroup = 'LiveTV' "
942 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
944 orderby =
"starttime ASC";
949 msg = QString(
"Adding programs deleted more than %1 days ago")
951 where = QString(
"recgroup = 'Deleted' "
952 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
954 orderby =
"starttime ASC";
959 msg = QString(
"Adding programs deleted more than 5 minutes ago");
960 where = QString(
"recgroup = 'Deleted' "
961 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ");
962 orderby =
"lastmodified ASC";
965 msg =
"Adding deleted programs in FIFO order";
966 where =
"recgroup = 'Deleted'";
967 orderby =
"lastmodified ASC";
971 LOG(VB_FILE, LOG_INFO,
LOC +
"FillDBOrdered: " + msg);
974 QString querystr = QString(
975 "SELECT recorded.chanid, starttime "
977 "LEFT JOIN channel ON recorded.chanid = channel.chanid "
978 "WHERE %1 AND deletepending = 0 "
979 "ORDER BY autoexpire DESC, %2").arg(where, orderby);
993 LOG(VB_FILE, LOG_INFO,
LOC +
994 QString(
" Skipping %1 at %2 because it is in Don't Expire "
996 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1000 LOG(VB_FILE, LOG_INFO,
LOC +
1001 QString(
" Skipping %1 at %2 because it is already in Expire "
1003 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1007 auto *pginfo =
new ProgramInfo(chanid, recstartts);
1008 if (pginfo->GetChanID())
1010 LOG(VB_FILE, LOG_INFO,
LOC + QString(
" Adding %1 at %2")
1011 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1012 expireList.push_back(pginfo);
1016 LOG(VB_FILE, LOG_INFO,
LOC +
1017 QString(
" Skipping %1 at %2 "
1018 "because it could not be loaded from the DB")
1019 .arg(chanid).arg(recstartts.toString(
Qt::ISODate)));
1044 QString msg = QString(
"Cardid %1: is starting a recording on")
1047 msg.append(
" an unknown fsID soon.");
1049 msg.append(QString(
" fsID %2 soon.").arg(fsID));
1050 LOG(VB_FILE, LOG_INFO,
LOC + msg);
1078 "SELECT chanid, starttime, lastupdatetime, recusage, hostname "
1079 "FROM inuseprograms");
1081 if (!query.
exec() || !query.
next())
1084 LOG(VB_FILE, LOG_INFO,
LOC +
"Adding Programs to 'Do Not Expire' List");
1095 QString key = QString(
"%1_%2")
1096 .arg(chanid).arg(recstartts.toString(
Qt::ISODate));
1098 LOG(VB_FILE, LOG_INFO, QString(
" %1 at %2 in use by %3 on %4")
1099 .arg(QString::number(chanid),
1101 query.
value(3).toString(),
1102 query.
value(4).toString()));
1105 while (query.
next());
1109 uint chanid,
const QDateTime &recstartts)
const
1111 QString key = QString(
"%1_%2")
1112 .arg(chanid).arg(recstartts.toString(
Qt::ISODate));
1120 return std::any_of(expireList.cbegin(), expireList.cend(),
1121 [chanid,&recstartts](
auto *info)
1122 { return ((info->GetChanID() == chanid) &&
1123 (info->GetRecordingStartTime() == recstartts)); } );