MythTV  master
autoexpire.cpp
Go to the documentation of this file.
1 // System headers
2 #include <sys/stat.h>
3 #ifdef __linux__
4 # include <sys/vfs.h>
5 #else // if !__linux__
6 # include <sys/param.h>
7 # ifndef _WIN32
8 # include <sys/mount.h>
9 # endif // _WIN32
10 #endif // !__linux__
11 
12 // POSIX headers
13 #include <unistd.h>
14 
15 // C++ headers
16 #include <algorithm>
17 #include <cstdlib>
18 #include <iostream>
19 
20 // Qt headers
21 #include <QDateTime>
22 #include <QFileInfo>
23 #include <QList>
24 
25 // MythTV headers
26 #include "libmythbase/compat.h"
29 #include "libmythbase/mythdate.h"
30 #include "libmythbase/mythdb.h"
33 #include "libmythbase/remoteutil.h"
37 #include "libmythtv/tv_rec.h"
38 
39 // MythBackend
40 #include "autoexpire.h"
41 #include "backendcontext.h"
42 #include "encoderlink.h"
43 #include "mainserver.h"
44 
45 #define LOC QString("AutoExpire: ")
46 #define LOC_ERR QString("AutoExpire Error: ")
47 
51 static constexpr uint64_t kSpaceTooBigKB { 3ULL * 1024 * 1024 };
52 
53 // Consider recordings within the last two hours to be too recent to
54 // add to the autoexpire list.
55 static constexpr int64_t kRecentInterval { 2LL * 60 * 60 };
56 
59 {
60  RunProlog();
61  m_parent->RunExpirer();
62  RunEpilog();
63 }
64 
74 AutoExpire::AutoExpire(QMap<int, EncoderLink *> *tvList) :
75  m_encoderList(tvList),
76  m_expireThread(new ExpireThread(this)),
77  m_expireThreadRun(true)
78 {
81 }
82 
87 {
88  {
89  QMutexLocker locker(&m_instanceLock);
90  m_expireThreadRun = false;
91  m_instanceCond.wakeAll();
92  }
93 
94  {
95  QMutexLocker locker(&m_updateLock);
96  m_updateQueue.clear();
97  }
98 
99  if (m_expireThread)
100  {
102  m_expireThread->wait();
103  delete m_expireThread;
104  m_expireThread = nullptr;
105  }
106 }
107 
113 uint64_t AutoExpire::GetDesiredSpace(int fsID) const
114 {
115  QMutexLocker locker(&m_instanceLock);
116  if (m_desiredSpace.contains(fsID))
117  return m_desiredSpace[fsID];
118  return 0;
119 }
120 
125 {
126  LOG(VB_FILE, LOG_INFO, LOC + "CalcParams()");
127 
128  QList<FileSystemInfo> fsInfos;
129 
130  m_instanceLock.lock();
131  if (m_mainServer)
132  {
133  // The scheduler relies on something forcing the mainserver
134  // fsinfos cache to get updated periodically. Currently, that
135  // is done here. Don't remove or change this invocation
136  // without handling that issue too. It is done this way
137  // because the scheduler thread can't afford to be blocked by
138  // an unresponsive, remote filesystem and the autoexpirer
139  // thread can.
140  m_mainServer->GetFilesystemInfos(fsInfos, false);
141  }
142  m_instanceLock.unlock();
143 
144  if (fsInfos.empty())
145  {
146  LOG(VB_GENERAL, LOG_ERR, LOC + "Filesystem Info cache is empty, unable "
147  "to calculate necessary parameters.");
148  return;
149  }
150 
151  uint64_t maxKBperMin = 0;
152  uint64_t extraKB = static_cast<uint64_t>
153  (gCoreContext->GetNumSetting("AutoExpireExtraSpace", 0))
154  << 20;
155 
156  QMap<int, uint64_t> fsMap;
157  QMap<int, std::vector<int> > fsEncoderMap;
158 
159  // We use this copying on purpose. The used_encoders map ensures
160  // that every encoder writes only to one fs.
161  // Copying the data minimizes the time the lock is held.
162  m_instanceLock.lock();
163  QMap<int, int>::const_iterator ueit = m_usedEncoders.cbegin();
164  while (ueit != m_usedEncoders.cend())
165  {
166  fsEncoderMap[*ueit].push_back(ueit.key());
167  ++ueit;
168  }
169  m_instanceLock.unlock();
170 
171  QList<FileSystemInfo>::iterator fsit;
172  for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
173  {
174  if (fsMap.contains(fsit->getFSysID()))
175  continue;
176 
177  fsMap[fsit->getFSysID()] = 0;
178  uint64_t thisKBperMin = 0;
179 
180  // append unknown recordings to all fsIDs
181  for (auto unknownfs : std::as_const(fsEncoderMap[-1]))
182  fsEncoderMap[fsit->getFSysID()].push_back(unknownfs);
183 
184  if (fsEncoderMap.contains(fsit->getFSysID()))
185  {
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));
192 
193  for (auto cardid : std::as_const(fsEncoderMap[fsit->getFSysID()]))
194  {
195  auto iter = m_encoderList->constFind(cardid);
196  if (iter == m_encoderList->constEnd())
197  continue;
198  EncoderLink *enc = *iter;
199 
200  if (!enc->IsConnected() || !enc->IsBusy())
201  {
202  // remove encoder since it can't write to any file system
203  LOG(VB_FILE, LOG_INFO, LOC +
204  QString("Cardid %1: is not recording, removing it "
205  "from used list.").arg(cardid));
206  m_instanceLock.lock();
207  m_usedEncoders.remove(cardid);
208  m_instanceLock.unlock();
209  continue;
210  }
211 
212  uint64_t maxBitrate = enc->GetMaxBitrate();
213  if (maxBitrate==0)
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")
218  .arg(enc->GetInputID())
219  .arg(enc->GetMaxBitrate() >> 10)
220  .arg(fsit->getFSysID())
221  .arg(thisKBperMin));
222  }
223  }
224  fsMap[fsit->getFSysID()] = thisKBperMin;
225 
226  if (thisKBperMin > maxKBperMin)
227  {
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;
233  }
234  }
235 
236  // Determine frequency to run autoexpire so it doesn't have to free
237  // too much space
238  uint expireFreq = 15;
239  if (maxKBperMin > 0)
240  {
241  expireFreq = kSpaceTooBigKB / (maxKBperMin + maxKBperMin/3);
242  expireFreq = std::max(3U, std::min(expireFreq, 15U));
243  }
244 
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));
250 
251  // lock class and save these parameters.
252  m_instanceLock.lock();
253  m_desiredFreq = expireFreq;
254  // write per file system needed space back, use safety of 33%
255  QMap<int, uint64_t>::iterator it = fsMap.begin();
256  while (it != fsMap.end())
257  {
258  m_desiredSpace[it.key()] = (*it + *it/3) * expireFreq + extraKB;
259  ++it;
260  }
261  m_instanceLock.unlock();
262 }
263 
273 {
274  QElapsedTimer timer;
275  QDateTime curTime;
276  QDateTime next_expire = MythDate::current().addSecs(60);
277 
278  QMutexLocker locker(&m_instanceLock);
279 
280  // wait a little for main server to come up and things to settle down
281  Sleep(20s);
282 
283  timer.start();
284 
285  while (m_expireThreadRun)
286  {
287  TVRec::s_inputsLock.lockForRead();
288 
289  curTime = MythDate::current();
290  // recalculate auto expire parameters
291  if (curTime >= next_expire)
292  {
293  m_updateLock.lock();
294  while (!m_updateQueue.empty())
295  {
296  UpdateEntry ue = m_updateQueue.dequeue();
297  if (ue.m_encoder > 0)
299  }
300  m_updateLock.unlock();
301 
302  locker.unlock();
303  CalcParams();
304  locker.relock();
305  if (!m_expireThreadRun)
306  break;
307  }
308  timer.restart();
309 
311 
312  // Expire Short LiveTV files for this backend every 2 minutes
313  if ((curTime.time().minute() % 2) == 0)
315 
316  // Expire normal recordings depending on frequency calculated
317  if (curTime >= next_expire)
318  {
319  LOG(VB_FILE, LOG_INFO, LOC + "Running now!");
320  next_expire =
321  MythDate::current().addSecs(m_desiredFreq * 60LL);
322 
324 
325  int maxAge = gCoreContext->GetNumSetting("DeletedMaxAge", 0);
326  if (maxAge > 0)
328  else if (maxAge == 0)
330 
332 
334  }
335 
336  TVRec::s_inputsLock.unlock();
337 
338  Sleep(60s - std::chrono::milliseconds(timer.elapsed()));
339  }
340 }
341 
348 void AutoExpire::Sleep(std::chrono::milliseconds sleepTime)
349 {
350  if (sleepTime <= 0ms)
351  return;
352 
353  QDateTime little_tm = MythDate::current().addMSecs(sleepTime.count());
354  std::chrono::milliseconds timeleft = sleepTime;
355  while (m_expireThreadRun && (timeleft > 0ms))
356  {
357  m_instanceCond.wait(&m_instanceLock, timeleft.count());
358  timeleft = MythDate::secsInFuture(little_tm);
359  }
360 }
361 
366 {
367  pginfolist_t expireList;
368 
369  LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireLiveTV(%1)").arg(type));
370  FillDBOrdered(expireList, type);
371  SendDeleteMessages(expireList);
372  ClearExpireList(expireList);
373 }
374 
379 {
380  pginfolist_t expireList;
381 
382  LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireOldDeleted()"));
383  FillDBOrdered(expireList, emOldDeletedPrograms);
384  SendDeleteMessages(expireList);
385  ClearExpireList(expireList);
386 }
387 
392 {
393  pginfolist_t expireList;
394 
395  LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireQuickDeleted()"));
397  SendDeleteMessages(expireList);
398  ClearExpireList(expireList);
399 }
400 
406 {
407  pginfolist_t expireList;
408  pginfolist_t deleteList;
409  QList<FileSystemInfo> fsInfos;
410  QList<FileSystemInfo>::iterator fsit;
411 
412  LOG(VB_FILE, LOG_INFO, LOC + "ExpireRecordings()");
413 
414  if (m_mainServer)
415  m_mainServer->GetFilesystemInfos(fsInfos, true);
416 
417  if (fsInfos.empty())
418  {
419  LOG(VB_GENERAL, LOG_ERR, LOC + "Filesystem Info cache is empty, unable "
420  "to determine what Recordings to expire");
421 
422  return;
423  }
424 
425  FillExpireList(expireList);
426 
427  QMap <int, bool> truncateMap;
428  MSqlQuery query(MSqlQuery::InitCon());
429  query.prepare("SELECT DISTINCT rechost, recdir "
430  "FROM inuseprograms "
431  "WHERE recusage = 'truncatingdelete' "
432  "AND lastupdatetime > DATE_ADD(NOW(), INTERVAL -2 MINUTE);");
433 
434  if (!query.exec())
435  {
436  MythDB::DBError(LOC + "ExpireRecordings", query);
437  }
438  else
439  {
440  while (query.next())
441  {
442  QString rechost = query.value(0).toString();
443  QString recdir = query.value(1).toString();
444 
445  LOG(VB_FILE, LOG_INFO, LOC +
446  QString("%1:%2 has an in-progress truncating delete.")
447  .arg(rechost, recdir));
448 
449  for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
450  {
451  if ((fsit->getHostname() == rechost) &&
452  (fsit->getPath() == recdir))
453  {
454  truncateMap[fsit->getFSysID()] = true;
455  break;
456  }
457  }
458  }
459  }
460 
461  QMap <int, bool> fsMap;
462  for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
463  {
464  if (fsMap.contains(fsit->getFSysID()))
465  continue;
466 
467  fsMap[fsit->getFSysID()] = true;
468 
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));
475 
476  if ((fsit->getTotalSpace() == -1) || (fsit->getUsedSpace() == -1))
477  {
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)
486  {
487  if (fsit2->getFSysID() == fsit->getFSysID())
488  {
489  LOG(VB_FILE, LOG_INFO, QString(" %1:%2")
490  .arg(fsit2->getHostname(), fsit2->getPath()));
491  }
492  }
493 
494  continue;
495  }
496 
497  if (truncateMap.contains(fsit->getFSysID()))
498  {
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()));
504  continue;
505  }
506 
507  if (std::max((int64_t)0LL, fsit->getFreeSpace()) <
508  m_desiredSpace[fsit->getFSysID()])
509  {
510  LOG(VB_FILE, LOG_INFO,
511  QString(" Not Enough Free Space! We want %1 MB")
512  .arg(m_desiredSpace[fsit->getFSysID()] / 1024));
513 
514  QMap<QString, int> dirList;
515  QList<FileSystemInfo>::iterator fsit2;
516 
517  LOG(VB_FILE, LOG_INFO,
518  QString(" Directories on filesystem ID %1:")
519  .arg(fsit->getFSysID()));
520 
521  for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); ++fsit2)
522  {
523  if (fsit2->getFSysID() == fsit->getFSysID())
524  {
525  LOG(VB_FILE, LOG_INFO, QString(" %1:%2")
526  .arg(fsit2->getHostname(), fsit2->getPath()));
527  dirList[fsit2->getHostname() + ":" + fsit2->getPath()] = 1;
528  }
529  }
530 
531  LOG(VB_FILE, LOG_INFO,
532  " Searching for files expirable in these directories");
533  QString myHostName = gCoreContext->GetHostName();
534  auto it = expireList.begin();
535  while ((it != expireList.end()) &&
536  (std::max((int64_t)0LL, fsit->getFreeSpace()) <
537  m_desiredSpace[fsit->getFSysID()]))
538  {
539  ProgramInfo *p = *it;
540  ++it;
541 
542  LOG(VB_FILE, LOG_INFO, QString(" Checking %1 => %2")
543  .arg(p->toString(ProgramInfo::kRecordingKey),
544  p->GetTitle()));
545 
546  if (!p->IsLocal())
547  {
548  bool foundFile = false;
549  auto eit = m_encoderList->constBegin();
550  while (eit != m_encoderList->constEnd())
551  {
552  EncoderLink *el = *eit;
553  eit++;
554 
555  if ((p->GetHostname() == el->GetHostName()) ||
556  ((p->GetHostname() == myHostName) &&
557  (el->IsLocal())))
558  {
559  if (el->IsConnected())
560  foundFile = el->CheckFile(p);
561 
562  eit = m_encoderList->constEnd();
563  }
564  }
565 
566  if (!foundFile && (p->GetHostname() != myHostName))
567  {
568  // Wasn't found so check locally
569  QString file = GetPlaybackURL(p);
570 
571  if (file.startsWith("/"))
572  {
573  p->SetPathname(file);
574  p->SetHostname(myHostName);
575  foundFile = true;
576  }
577  }
578 
579  if (!foundFile)
580  {
581  LOG(VB_FILE, LOG_ERR, LOC +
582  QString(" ERROR: Can't find file for %1")
583  .arg(p->toString(ProgramInfo::kRecordingKey)));
584  continue;
585  }
586  }
587 
588  QFileInfo vidFile(p->GetPathname());
589  if (dirList.contains(p->GetHostname() + ':' + vidFile.path()))
590  {
591  fsit->setUsedSpace(fsit->getUsedSpace()
592  - (p->GetFilesize() / 1024));
593  deleteList.push_back(p);
594 
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.")
600  .arg(p->toString(ProgramInfo::kRecordingKey),
601  p->GetPathname()).arg(fsit->getFSysID())
602  .arg(fsit->getFreeSpace() / 1024));
603  }
604  }
605  }
606  }
607 
608  SendDeleteMessages(deleteList);
609 
610  ClearExpireList(deleteList, false);
611  ClearExpireList(expireList);
612 }
613 
618 {
619  QString msg;
620 
621  if (deleteList.empty())
622  {
623  LOG(VB_FILE, LOG_INFO, LOC + "SendDeleteMessages. Nothing to expire.");
624  return;
625  }
626 
627  LOG(VB_FILE, LOG_INFO, LOC +
628  "SendDeleteMessages, cycling through deleteList.");
629  auto it = deleteList.begin();
630  while (it != deleteList.end())
631  {
632  msg = QString("%1Expiring %2 MB for %3 => %4")
633  .arg(VERBOSE_LEVEL_CHECK(VB_FILE, LOG_ANY) ? " " : "",
634  QString::number((*it)->GetFilesize() >> 20),
635  (*it)->toString(ProgramInfo::kRecordingKey),
636  (*it)->toString(ProgramInfo::kTitleSubtitle));
637 
638  LOG(VB_GENERAL, LOG_NOTICE, msg);
639 
640  // send auto expire message to backend's event thread.
641  MythEvent me(QString("AUTO_EXPIRE %1 %2").arg((*it)->GetChanID())
642  .arg((*it)->GetRecordingStartTime(MythDate::ISODate)));
643  gCoreContext->dispatch(me);
644 
645  ++it; // move on to next program
646  }
647 }
648 
655 {
656  QMap<QString, int> maxEpisodes;
657  QMap<QString, int>::Iterator maxIter;
658  QMap<QString, int> episodeParts;
659  QString episodeKey;
660 
661  MSqlQuery query(MSqlQuery::InitCon());
662  query.prepare("SELECT recordid, maxepisodes, title "
663  "FROM record WHERE maxepisodes > 0 "
664  "ORDER BY recordid ASC, maxepisodes DESC");
665 
666  if (query.exec() && query.isActive() && query.size() > 0)
667  {
668  LOG(VB_FILE, LOG_INFO, LOC +
669  QString("Found %1 record profiles using max episode expiration")
670  .arg(query.size()));
671  while (query.next())
672  {
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();
678  }
679  }
680 
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)
684  {
685  query.prepare("SELECT chanid, starttime, title, progstart, progend, "
686  "duplicate "
687  "FROM recorded "
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());
692 
693  if (!query.exec() || !query.isActive())
694  {
695  MythDB::DBError("AutoExpire query failed!", query);
696  continue;
697  }
698 
699  LOG(VB_FILE, LOG_INFO, QString(" Recordid %1 has %2 recordings.")
700  .arg(maxIter.key())
701  .arg(query.size()));
702  if (query.size() > 0)
703  {
704  int found = 1;
705  while (query.next())
706  {
707  uint chanid = query.value(0).toUInt();
708  QDateTime startts = MythDate::as_utc(query.value(1).toDateTime());
709  QString title = query.value(2).toString();
710  QDateTime progstart = MythDate::as_utc(query.value(3).toDateTime());
711  QDateTime progend = MythDate::as_utc(query.value(4).toDateTime());
712  int duplicate = query.value(5).toInt();
713 
714  episodeKey = QString("%1_%2_%3")
715  .arg(QString::number(chanid),
716  progstart.toString(Qt::ISODate),
717  progend.toString(Qt::ISODate));
718 
719  if ((!IsInDontExpireSet(chanid, startts)) &&
720  (!episodeParts.contains(episodeKey)) &&
721  (found > *maxIter))
722  {
723  QString msg =
724  QString("%1Deleting %2 at %3 => %4. "
725  "Too many episodes, we only want to keep %5.")
726  .arg(VERBOSE_LEVEL_CHECK(VB_FILE, LOG_ANY) ? " " : "",
727  QString::number(chanid),
728  startts.toString(Qt::ISODate),
729  title,
730  QString::number(*maxIter));
731 
732  LOG(VB_GENERAL, LOG_NOTICE, msg);
733 
734  // allow re-record if auto expired
735  RecordingInfo recInfo(chanid, startts);
736  if (gCoreContext->GetBoolSetting("RerecordWatched", false) ||
737  !recInfo.IsWatched())
738  {
739  recInfo.ForgetHistory();
740  }
741  msg = QString("DELETE_RECORDING %1 %2")
742  .arg(chanid)
743  .arg(startts.toString(Qt::ISODate));
744 
745  MythEvent me(msg);
746  gCoreContext->dispatch(me);
747  }
748  else
749  {
750  // keep track of shows we haven't expired so we can
751  // make sure we don't expire another part of the same
752  // episode.
753  if (episodeParts.contains(episodeKey))
754  {
755  episodeParts[episodeKey] = episodeParts[episodeKey] + 1;
756  }
757  else
758  {
759  episodeParts[episodeKey] = 1;
760  if( duplicate )
761  found++;
762  }
763  }
764  }
765  }
766  }
767 }
768 
774 {
775  int expMethod = gCoreContext->GetNumSetting("AutoExpireMethod", 1);
776 
777  ClearExpireList(expireList);
778 
780 
781  switch(expMethod)
782  {
783  case emOldestFirst:
786  FillDBOrdered(expireList, expMethod);
787  break;
788  // default falls through so list is empty so no AutoExpire
789  }
790 }
791 
795 void AutoExpire::PrintExpireList(const QString& expHost)
796 {
797  pginfolist_t expireList;
798 
799  FillExpireList(expireList);
800 
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;
806 
807  for (auto *first : expireList)
808  {
809  if (expHost != "ALL" && first->GetHostname() != expHost)
810  continue;
811 
812  QString title = first->toString(ProgramInfo::kTitleSubtitle);
813  title = title.leftJustified(39, ' ', true);
814 
815  QString outstr = QString("%1 %2 MB %3 [%4]")
816  .arg(title,
817  QString::number(first->GetFilesize() >> 20)
818  .rightJustified(5, ' ', true),
819  first->GetRecordingStartTime(MythDate::ISODate)
820  .leftJustified(24, ' ', true),
821  QString::number(first->GetRecordingPriority())
822  .rightJustified(3, ' ', true));
823  QByteArray out = outstr.toLocal8Bit();
824 
825  std::cout << out.constData() << std::endl;
826  }
827 
828  ClearExpireList(expireList);
829 }
830 
834 void AutoExpire::GetAllExpiring(QStringList &strList)
835 {
836  QMutexLocker lockit(&m_instanceLock);
837  pginfolist_t expireList;
838 
840 
844  FillDBOrdered(expireList, gCoreContext->GetNumSetting("AutoExpireMethod",
845  emOldestFirst));
846 
847  strList << QString::number(expireList.size());
848 
849  for (auto & info : expireList)
850  info->ToStringList(strList);
851 
852  ClearExpireList(expireList);
853 }
854 
859 {
860  QMutexLocker lockit(&m_instanceLock);
861  pginfolist_t expireList;
862 
864 
868  FillDBOrdered(expireList, gCoreContext->GetNumSetting("AutoExpireMethod",
869  emOldestFirst));
870 
871  for (auto & info : expireList)
872  list.push_back( new ProgramInfo( *info ));
873 
874  ClearExpireList(expireList);
875 }
876 
880 void AutoExpire::ClearExpireList(pginfolist_t &expireList, bool deleteProg)
881 {
882  ProgramInfo *pginfo = nullptr;
883  while (!expireList.empty())
884  {
885  if (deleteProg)
886  pginfo = expireList.back();
887 
888  expireList.pop_back();
889 
890  if (deleteProg)
891  delete pginfo;
892  }
893 }
894 
899 void AutoExpire::FillDBOrdered(pginfolist_t &expireList, int expMethod)
900 {
901  QString where;
902  QString orderby;
903  QString msg;
904  int maxAge = 0;
905 
906  switch (expMethod)
907  {
908  default:
909  case emOldestFirst:
910  msg = "Adding programs expirable in Oldest First order";
911  where = "autoexpire > 0";
912  if (gCoreContext->GetBoolSetting("AutoExpireWatchedPriority", false))
913  orderby = "recorded.watched DESC, ";
914  orderby += "starttime ASC";
915  break;
917  msg = "Adding programs expirable in Lowest Priority First order";
918  where = "autoexpire > 0";
919  if (gCoreContext->GetBoolSetting("AutoExpireWatchedPriority", false))
920  orderby = "recorded.watched DESC, ";
921  orderby += "recorded.recpriority ASC, starttime ASC";
922  break;
924  msg = "Adding programs expirable in Weighted Time Priority order";
925  where = "autoexpire > 0";
926  if (gCoreContext->GetBoolSetting("AutoExpireWatchedPriority", false))
927  orderby = "recorded.watched DESC, ";
928  orderby += QString("DATE_ADD(starttime, INTERVAL '%1' * "
929  "recorded.recpriority DAY) ASC")
930  .arg(gCoreContext->GetNumSetting("AutoExpireDayPriority", 3));
931  break;
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";
938  break;
940  msg = "Adding LiveTV programs in starttime order";
941  where = QString("recgroup = 'LiveTV' "
942  "AND endtime <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
943  .arg(gCoreContext->GetNumSetting("AutoExpireLiveTVMaxAge", 1));
944  orderby = "starttime ASC";
945  break;
947  maxAge = gCoreContext->GetNumSetting("DeletedMaxAge", 0);
948  if (maxAge <= 0)
949  return;
950  msg = QString("Adding programs deleted more than %1 days ago")
951  .arg(maxAge);
952  where = QString("recgroup = 'Deleted' "
953  "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
954  .arg(maxAge);
955  orderby = "starttime ASC";
956  break;
958  if (gCoreContext->GetNumSetting("DeletedMaxAge", 0) != 0)
959  return;
960  msg = QString("Adding programs deleted more than 5 minutes ago");
961  where = QString("recgroup = 'Deleted' "
962  "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ");
963  orderby = "lastmodified ASC";
964  break;
966  msg = "Adding deleted programs in FIFO order";
967  where = "recgroup = 'Deleted'";
968  orderby = "lastmodified ASC";
969  break;
970  }
971 
972  LOG(VB_FILE, LOG_INFO, LOC + "FillDBOrdered: " + msg);
973 
974  MSqlQuery query(MSqlQuery::InitCon());
975  QString querystr = QString(
976  "SELECT recorded.chanid, starttime "
977  "FROM recorded "
978  "LEFT JOIN channel ON recorded.chanid = channel.chanid "
979  "WHERE %1 AND deletepending = 0 "
980  "ORDER BY autoexpire DESC, %2").arg(where, orderby);
981 
982  query.prepare(querystr);
983 
984  if (!query.exec())
985  return;
986 
987  while (query.next())
988  {
989  uint chanid = query.value(0).toUInt();
990  QDateTime recstartts = MythDate::as_utc(query.value(1).toDateTime());
991 
992  if (IsInDontExpireSet(chanid, recstartts))
993  {
994  LOG(VB_FILE, LOG_INFO, LOC +
995  QString(" Skipping %1 at %2 because it is in Don't Expire "
996  "List")
997  .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
998  }
999  else if (IsInExpireList(expireList, chanid, recstartts))
1000  {
1001  LOG(VB_FILE, LOG_INFO, LOC +
1002  QString(" Skipping %1 at %2 because it is already in Expire "
1003  "List")
1004  .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1005  }
1006  else
1007  {
1008  auto *pginfo = new ProgramInfo(chanid, recstartts);
1009  if (pginfo->GetChanID())
1010  {
1011  LOG(VB_FILE, LOG_INFO, LOC + QString(" Adding %1 at %2")
1012  .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1013  expireList.push_back(pginfo);
1014  }
1015  else
1016  {
1017  LOG(VB_FILE, LOG_INFO, LOC +
1018  QString(" Skipping %1 at %2 "
1019  "because it could not be loaded from the DB")
1020  .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1021  delete pginfo;
1022  }
1023  }
1024  }
1025 }
1026 
1038 void AutoExpire::Update(int encoder, int fsID, bool immediately)
1039 {
1040  if (!gExpirer)
1041  return;
1042 
1043  if (encoder > 0)
1044  {
1045  QString msg = QString("Cardid %1: is starting a recording on")
1046  .arg(encoder);
1047  if (fsID == -1)
1048  msg.append(" an unknown fsID soon.");
1049  else
1050  msg.append(QString(" fsID %2 soon.").arg(fsID));
1051  LOG(VB_FILE, LOG_INFO, LOC + msg);
1052  }
1053 
1054  if (immediately)
1055  {
1056  if (encoder > 0)
1057  {
1058  gExpirer->m_instanceLock.lock();
1059  gExpirer->m_usedEncoders[encoder] = fsID;
1060  gExpirer->m_instanceLock.unlock();
1061  }
1062  gExpirer->CalcParams();
1063  gExpirer->m_instanceCond.wakeAll();
1064  }
1065  else
1066  {
1067  gExpirer->m_updateLock.lock();
1068  gExpirer->m_updateQueue.append(UpdateEntry(encoder, fsID));
1069  gExpirer->m_updateLock.unlock();
1070  }
1071 }
1072 
1074 {
1075  m_dontExpireSet.clear();
1076 
1077  MSqlQuery query(MSqlQuery::InitCon());
1078  query.prepare(
1079  "SELECT chanid, starttime, lastupdatetime, recusage, hostname "
1080  "FROM inuseprograms");
1081 
1082  if (!query.exec() || !query.next())
1083  return;
1084 
1085  LOG(VB_FILE, LOG_INFO, LOC + "Adding Programs to 'Do Not Expire' List");
1086  QDateTime curTime = MythDate::current();
1087 
1088  do
1089  {
1090  uint chanid = query.value(0).toUInt();
1091  QDateTime recstartts = MythDate::as_utc(query.value(1).toDateTime());
1092  QDateTime lastupdate = MythDate::as_utc(query.value(2).toDateTime());
1093 
1094  if (lastupdate.secsTo(curTime) < kRecentInterval)
1095  {
1096  QString key = QString("%1_%2")
1097  .arg(chanid).arg(recstartts.toString(Qt::ISODate));
1098  m_dontExpireSet.insert(key);
1099  LOG(VB_FILE, LOG_INFO, QString(" %1 at %2 in use by %3 on %4")
1100  .arg(QString::number(chanid),
1101  recstartts.toString(Qt::ISODate),
1102  query.value(3).toString(),
1103  query.value(4).toString()));
1104  }
1105  }
1106  while (query.next());
1107 }
1108 
1110  uint chanid, const QDateTime &recstartts) const
1111 {
1112  QString key = QString("%1_%2")
1113  .arg(chanid).arg(recstartts.toString(Qt::ISODate));
1114 
1115  return (m_dontExpireSet.find(key) != m_dontExpireSet.end());
1116 }
1117 
1119  const pginfolist_t &expireList, uint chanid, const QDateTime &recstartts)
1120 {
1121  return std::any_of(expireList.cbegin(), expireList.cend(),
1122  [chanid,&recstartts](auto *info)
1123  { return ((info->GetChanID() == chanid) &&
1124  (info->GetRecordingStartTime() == recstartts)); } );
1125 }
1126 
1127 /* vim: set expandtab tabstop=4 shiftwidth=4: */
filesysteminfo.h
AutoExpire::m_instanceCond
QWaitCondition m_instanceCond
Definition: autoexpire.h:120
MSqlQuery::isActive
bool isActive(void) const
Definition: mythdbcon.h:215
MSqlQuery::next
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:813
MSqlQuery
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:127
MSqlQuery::size
int size(void) const
Definition: mythdbcon.h:214
MThread::start
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:283
AutoExpire::PrintExpireList
void PrintExpireList(const QString &expHost="ALL")
Prints a summary of the files that can be deleted.
Definition: autoexpire.cpp:795
backendcontext.h
mythdb.h
MainServer::GetFilesystemInfos
void GetFilesystemInfos(QList< FileSystemInfo > &fsInfos, bool useCache=true)
Definition: mainserver.cpp:5345
MythDate::as_utc
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
Definition: mythdate.cpp:27
AutoExpire::IsInExpireList
static bool IsInExpireList(const pginfolist_t &expireList, uint chanid, const QDateTime &recstartts)
Definition: autoexpire.cpp:1118
AutoExpire::Update
static void Update(int encoder, int fsID, bool immediately)
This is used to update the global AutoExpire instance "expirer".
Definition: autoexpire.cpp:1038
MThread::wait
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:300
ExpireThread::m_parent
QPointer< AutoExpire > m_parent
Definition: autoexpire.h:47
RecordingInfo
Holds information on a TV Program one might wish to record.
Definition: recordinginfo.h:35
gExpirer
AutoExpire * gExpirer
Definition: backendcontext.cpp:8
AutoExpire::ClearExpireList
static void ClearExpireList(pginfolist_t &expireList, bool deleteProg=true)
Clears expireList, freeing any ProgramInfo's if necessary.
Definition: autoexpire.cpp:880
RecordingInfo::ForgetHistory
void ForgetHistory(void)
Forget the recording of a program so it will be recorded again.
Definition: recordinginfo.cpp:1459
MythEvent
This class is used as a container for messages.
Definition: mythevent.h:16
ProgramInfo::kTitleSubtitle
@ kTitleSubtitle
Definition: programinfo.h:509
VERBOSE_LEVEL_CHECK
static bool VERBOSE_LEVEL_CHECK(uint64_t mask, LogLevel_t level)
Definition: mythlogging.h:29
MSqlQuery::value
QVariant value(int i) const
Definition: mythdbcon.h:204
MSqlQuery::exec
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:619
AutoExpire::ExpireOldDeleted
void ExpireOldDeleted(void)
This expires deleted programs older than DeletedMaxAge.
Definition: autoexpire.cpp:378
emLowestPriorityFirst
@ emLowestPriorityFirst
Definition: autoexpire.h:29
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MThread::RunProlog
void RunProlog(void)
Sets up a thread, call this if you reimplement run().
Definition: mthread.cpp:196
AutoExpire::ExpireEpisodesOverMax
void ExpireEpisodesOverMax(void)
This deletes programs exceeding the maximum number of episodes of that program desired....
Definition: autoexpire.cpp:654
AutoExpire::GetAllExpiring
void GetAllExpiring(QStringList &strList)
Gets the full list of programs that can expire in expiration order.
Definition: autoexpire.cpp:834
ExpireThread::run
void run(void) override
This calls AutoExpire::RunExpirer() from within a new thread.
Definition: autoexpire.cpp:58
AutoExpire::m_desiredSpace
QMap< int, int64_t > m_desiredSpace
Definition: autoexpire.h:116
build_compdb.file
file
Definition: build_compdb.py:55
AutoExpire::m_updateLock
QMutex m_updateLock
Definition: autoexpire.h:125
remoteutil.h
AutoExpire::m_desiredFreq
uint m_desiredFreq
Definition: autoexpire.h:113
MythDate::current
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:14
true
VERBOSE_PREAMBLE Most true
Definition: verbosedefs.h:95
emOldDeletedPrograms
@ emOldDeletedPrograms
Definition: autoexpire.h:33
AutoExpire::m_instanceLock
QMutex m_instanceLock
Definition: autoexpire.h:119
ProgramInfo::IsWatched
bool IsWatched(void) const
Definition: programinfo.h:482
kSpaceTooBigKB
static constexpr uint64_t kSpaceTooBigKB
If calculated desired space for 10 min freq is > kSpaceTooBigKB then we use 5 min expire frequency.
Definition: autoexpire.cpp:51
MythObservable::addListener
void addListener(QObject *listener)
Add a listener to the observable.
Definition: mythobservable.cpp:38
kRecentInterval
static constexpr int64_t kRecentInterval
Definition: autoexpire.cpp:55
MythDate::secsInFuture
std::chrono::seconds secsInFuture(const QDateTime &future)
Definition: mythdate.cpp:208
fileserverutil.h
mythdate.h
autoexpire.h
programinfo.h
mythlogging.h
AutoExpire::m_expireThreadRun
bool m_expireThreadRun
Definition: autoexpire.h:114
UpdateEntry::m_encoder
int m_encoder
Definition: autoexpire.h:54
AutoExpire::Sleep
void Sleep(std::chrono::milliseconds sleepTime)
Sleeps for sleepTime milliseconds; unless the expire thread is told to quit.
Definition: autoexpire.cpp:348
hardwareprofile.config.p
p
Definition: config.py:33
AutoExpire::m_usedEncoders
QMap< int, int > m_usedEncoders
Definition: autoexpire.h:117
MSqlQuery::InitCon
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:551
compat.h
AutoExpire::ExpireRecordings
void ExpireRecordings(void)
This expires normal recordings.
Definition: autoexpire.cpp:405
MythDB::DBError
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:226
AutoExpire::IsInDontExpireSet
bool IsInDontExpireSet(uint chanid, const QDateTime &recstartts) const
Definition: autoexpire.cpp:1109
GetPlaybackURL
QString GetPlaybackURL(ProgramInfo *pginfo, bool storePath)
Definition: fileserverutil.cpp:46
pginfolist_t
std::vector< ProgramInfo * > pginfolist_t
Definition: autoexpire.h:24
emNormalLiveTVPrograms
@ emNormalLiveTVPrograms
Definition: autoexpire.h:32
MThread::RunEpilog
void RunEpilog(void)
Cleans up a thread's resources, call this if you reimplement run().
Definition: mthread.cpp:209
ExpireThread
Definition: autoexpire.h:40
storagegroup.h
TVRec::s_inputsLock
static QReadWriteLock s_inputsLock
Definition: tv_rec.h:432
LOC
#define LOC
Definition: autoexpire.cpp:45
AutoExpire::SendDeleteMessages
static void SendDeleteMessages(pginfolist_t &deleteList)
This sends delete message to main event thread.
Definition: autoexpire.cpp:617
uint
unsigned int uint
Definition: compat.h:81
AutoExpire::AutoExpire
AutoExpire()=default
gCoreContext
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: mythcorecontext.cpp:55
AutoExpire::FillExpireList
void FillExpireList(pginfolist_t &expireList)
Uses the "AutoExpireMethod" setting in the database to fill the list of files that are deletable.
Definition: autoexpire.cpp:773
AutoExpire::m_dontExpireSet
QSet< QString > m_dontExpireSet
Definition: autoexpire.h:111
emNormalDeletedPrograms
@ emNormalDeletedPrograms
Definition: autoexpire.h:34
MythCoreContext::GetNumSetting
int GetNumSetting(const QString &key, int defaultval=0)
Definition: mythcorecontext.cpp:912
UpdateEntry
Definition: autoexpire.h:50
AutoExpire::RunExpirer
void RunExpirer(void)
This contains the main loop for the auto expire process.
Definition: autoexpire.cpp:272
MythCoreContext::GetBoolSetting
bool GetBoolSetting(const QString &key, bool defaultval=false)
Definition: mythcorecontext.cpp:906
AutoExpire::CalcParams
void CalcParams(void)
Definition: autoexpire.cpp:124
ProgramInfo
Holds information on recordings and videos.
Definition: programinfo.h:67
AutoExpire::FillDBOrdered
void FillDBOrdered(pginfolist_t &expireList, int expMethod)
Creates a list of programs to delete using the database to order list.
Definition: autoexpire.cpp:899
AutoExpire::m_expireThread
ExpireThread * m_expireThread
Definition: autoexpire.h:112
mythcorecontext.h
MSqlQuery::bindValue
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:889
MythDate::ISODate
@ ISODate
Default UTC.
Definition: mythdate.h:17
emOldestFirst
@ emOldestFirst
Definition: autoexpire.h:28
AutoExpire::~AutoExpire
~AutoExpire() override
AutoExpire destructor stops auto delete thread if it is running.
Definition: autoexpire.cpp:86
emShortLiveTVPrograms
@ emShortLiveTVPrograms
Definition: autoexpire.h:31
tv_rec.h
AutoExpire::m_mainServer
MainServer * m_mainServer
Definition: autoexpire.h:122
emQuickDeletedPrograms
@ emQuickDeletedPrograms
Definition: autoexpire.h:35
remoteencoder.h
mainserver.h
AutoExpire::ExpireQuickDeleted
void ExpireQuickDeleted(void)
This expires deleted programs within a few minutes.
Definition: autoexpire.cpp:391
MythCoreContext::GetHostName
QString GetHostName(void)
Definition: mythcorecontext.cpp:838
AutoExpire::ExpireLiveTV
void ExpireLiveTV(int type)
This expires LiveTV programs.
Definition: autoexpire.cpp:365
AutoExpire::GetDesiredSpace
uint64_t GetDesiredSpace(int fsID) const
Used by the scheduler to select the next recording dir.
Definition: autoexpire.cpp:113
AutoExpire::m_encoderList
QMap< int, EncoderLink * > * m_encoderList
Definition: autoexpire.h:88
AutoExpire::m_updateQueue
QQueue< UpdateEntry > m_updateQueue
Definition: autoexpire.h:126
MythCoreContext::dispatch
void dispatch(const MythEvent &event)
Definition: mythcorecontext.cpp:1719
MythObservable::removeListener
void removeListener(QObject *listener)
Remove a listener to the observable.
Definition: mythobservable.cpp:55
AutoExpire::UpdateDontExpireSet
void UpdateDontExpireSet(void)
Definition: autoexpire.cpp:1073
emWeightedTimePriority
@ emWeightedTimePriority
Definition: autoexpire.h:30
ProgramInfo::kRecordingKey
@ kRecordingKey
Definition: programinfo.h:510
MSqlQuery::prepare
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:838
UpdateEntry::m_fsID
int m_fsID
Definition: autoexpire.h:57