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 "libmyth/programinfo.h"
27 #include "libmyth/remoteutil.h"
28 #include "libmythbase/compat.h"
31 #include "libmythbase/mythdate.h"
32 #include "libmythbase/mythdb.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 #define SPACE_TOO_BIG_KB (3*1024*1024)
52 
55 {
56  RunProlog();
57  m_parent->RunExpirer();
58  RunEpilog();
59 }
60 
70 AutoExpire::AutoExpire(QMap<int, EncoderLink *> *tvList) :
71  m_encoderList(tvList),
72  m_expireThread(new ExpireThread(this)),
73  m_expireThreadRun(true)
74 {
77 }
78 
83 {
84  {
85  QMutexLocker locker(&m_instanceLock);
86  m_expireThreadRun = false;
87  m_instanceCond.wakeAll();
88  }
89 
90  {
91  QMutexLocker locker(&m_updateLock);
92  m_updateQueue.clear();
93  }
94 
95  if (m_expireThread)
96  {
99  delete m_expireThread;
100  m_expireThread = nullptr;
101  }
102 }
103 
109 uint64_t AutoExpire::GetDesiredSpace(int fsID) const
110 {
111  QMutexLocker locker(&m_instanceLock);
112  if (m_desiredSpace.contains(fsID))
113  return m_desiredSpace[fsID];
114  return 0;
115 }
116 
121 {
122  LOG(VB_FILE, LOG_INFO, LOC + "CalcParams()");
123 
124  QList<FileSystemInfo> fsInfos;
125 
126  m_instanceLock.lock();
127  if (m_mainServer)
128  {
129  // The scheduler relies on something forcing the mainserver
130  // fsinfos cache to get updated periodically. Currently, that
131  // is done here. Don't remove or change this invocation
132  // without handling that issue too. It is done this way
133  // because the scheduler thread can't afford to be blocked by
134  // an unresponsive, remote filesystem and the autoexpirer
135  // thread can.
136  m_mainServer->GetFilesystemInfos(fsInfos, false);
137  }
138  m_instanceLock.unlock();
139 
140  if (fsInfos.empty())
141  {
142  LOG(VB_GENERAL, LOG_ERR, LOC + "Filesystem Info cache is empty, unable "
143  "to calculate necessary parameters.");
144  return;
145  }
146 
147  uint64_t maxKBperMin = 0;
148  uint64_t extraKB = static_cast<uint64_t>
149  (gCoreContext->GetNumSetting("AutoExpireExtraSpace", 0))
150  << 20;
151 
152  QMap<int, uint64_t> fsMap;
153  QMap<int, std::vector<int> > fsEncoderMap;
154 
155  // We use this copying on purpose. The used_encoders map ensures
156  // that every encoder writes only to one fs.
157  // Copying the data minimizes the time the lock is held.
158  m_instanceLock.lock();
159  QMap<int, int>::const_iterator ueit = m_usedEncoders.cbegin();
160  while (ueit != m_usedEncoders.cend())
161  {
162  fsEncoderMap[*ueit].push_back(ueit.key());
163  ++ueit;
164  }
165  m_instanceLock.unlock();
166 
167  QList<FileSystemInfo>::iterator fsit;
168  for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
169  {
170  if (fsMap.contains(fsit->getFSysID()))
171  continue;
172 
173  fsMap[fsit->getFSysID()] = 0;
174  uint64_t thisKBperMin = 0;
175 
176  // append unknown recordings to all fsIDs
177  for (auto unknownfs : qAsConst(fsEncoderMap[-1]))
178  fsEncoderMap[fsit->getFSysID()].push_back(unknownfs);
179 
180  if (fsEncoderMap.contains(fsit->getFSysID()))
181  {
182  LOG(VB_FILE, LOG_INFO,
183  QString("fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
184  .arg(fsit->getFSysID())
185  .arg(fsit->getTotalSpace() / 1024.0 / 1024.0, 7, 'f', 1)
186  .arg(fsit->getUsedSpace() / 1024.0 / 1024.0, 7, 'f', 1)
187  .arg(fsit->getFreeSpace() / 1024.0 / 1024.0, 7, 'f', 1));
188 
189  for (auto cardid : qAsConst(fsEncoderMap[fsit->getFSysID()]))
190  {
191  EncoderLink *enc = *(m_encoderList->constFind(cardid));
192 
193  if (!enc->IsConnected() || !enc->IsBusy())
194  {
195  // remove encoder since it can't write to any file system
196  LOG(VB_FILE, LOG_INFO, LOC +
197  QString("Cardid %1: is not recording, removing it "
198  "from used list.").arg(cardid));
199  m_instanceLock.lock();
200  m_usedEncoders.remove(cardid);
201  m_instanceLock.unlock();
202  continue;
203  }
204 
205  uint64_t maxBitrate = enc->GetMaxBitrate();
206  if (maxBitrate==0)
207  maxBitrate = 19500000LL;
208  thisKBperMin += (maxBitrate*((uint64_t)15))>>11;
209  LOG(VB_FILE, LOG_INFO, QString(" Cardid %1: max bitrate "
210  "%2 Kb/sec, fsID %3 max is now %4 KB/min")
211  .arg(enc->GetInputID())
212  .arg(enc->GetMaxBitrate() >> 10)
213  .arg(fsit->getFSysID())
214  .arg(thisKBperMin));
215  }
216  }
217  fsMap[fsit->getFSysID()] = thisKBperMin;
218 
219  if (thisKBperMin > maxKBperMin)
220  {
221  LOG(VB_FILE, LOG_INFO,
222  QString(" Max of %1 KB/min for fsID %2 is higher "
223  "than the existing Max of %3 so we'll use this Max instead")
224  .arg(thisKBperMin).arg(fsit->getFSysID()).arg(maxKBperMin));
225  maxKBperMin = thisKBperMin;
226  }
227  }
228 
229  // Determine frequency to run autoexpire so it doesn't have to free
230  // too much space
231  uint expireFreq = 15;
232  if (maxKBperMin > 0)
233  {
234  expireFreq = SPACE_TOO_BIG_KB / (maxKBperMin + maxKBperMin/3);
235  expireFreq = std::max(3U, std::min(expireFreq, 15U));
236  }
237 
238  double expireMinGB = ((maxKBperMin + maxKBperMin/3)
239  * expireFreq + extraKB) >> 20;
240  LOG(VB_GENERAL, LOG_NOTICE, LOC +
241  QString("CalcParams(): Max required Free Space: %1 GB w/freq: %2 min")
242  .arg(expireMinGB, 0, 'f', 1).arg(expireFreq));
243 
244  // lock class and save these parameters.
245  m_instanceLock.lock();
246  m_desiredFreq = expireFreq;
247  // write per file system needed space back, use safety of 33%
248  QMap<int, uint64_t>::iterator it = fsMap.begin();
249  while (it != fsMap.end())
250  {
251  m_desiredSpace[it.key()] = (*it + *it/3) * expireFreq + extraKB;
252  ++it;
253  }
254  m_instanceLock.unlock();
255 }
256 
266 {
267  QElapsedTimer timer;
268  QDateTime curTime;
269  QDateTime next_expire = MythDate::current().addSecs(60);
270 
271  QMutexLocker locker(&m_instanceLock);
272 
273  // wait a little for main server to come up and things to settle down
274  Sleep(20s);
275 
276  timer.start();
277 
278  while (m_expireThreadRun)
279  {
280  TVRec::s_inputsLock.lockForRead();
281 
282  curTime = MythDate::current();
283  // recalculate auto expire parameters
284  if (curTime >= next_expire)
285  {
286  m_updateLock.lock();
287  while (!m_updateQueue.empty())
288  {
289  UpdateEntry ue = m_updateQueue.dequeue();
290  if (ue.m_encoder > 0)
292  }
293  m_updateLock.unlock();
294 
295  locker.unlock();
296  CalcParams();
297  locker.relock();
298  if (!m_expireThreadRun)
299  break;
300  }
301  timer.restart();
302 
304 
305  // Expire Short LiveTV files for this backend every 2 minutes
306  if ((curTime.time().minute() % 2) == 0)
308 
309  // Expire normal recordings depending on frequency calculated
310  if (curTime >= next_expire)
311  {
312  LOG(VB_FILE, LOG_INFO, LOC + "Running now!");
313  next_expire =
314  MythDate::current().addSecs(m_desiredFreq * 60);
315 
317 
318  int maxAge = gCoreContext->GetNumSetting("DeletedMaxAge", 0);
319  if (maxAge > 0)
321  else if (maxAge == 0)
323 
325 
327  }
328 
329  TVRec::s_inputsLock.unlock();
330 
331  Sleep(60s - std::chrono::milliseconds(timer.elapsed()));
332  }
333 }
334 
341 void AutoExpire::Sleep(std::chrono::milliseconds sleepTime)
342 {
343  if (sleepTime <= 0ms)
344  return;
345 
346  QDateTime little_tm = MythDate::current().addMSecs(sleepTime.count());
347  std::chrono::milliseconds timeleft = sleepTime;
348  while (m_expireThreadRun && (timeleft > 0ms))
349  {
350  m_instanceCond.wait(&m_instanceLock, timeleft.count());
351  timeleft = MythDate::secsInFuture(little_tm);
352  }
353 }
354 
359 {
360  pginfolist_t expireList;
361 
362  LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireLiveTV(%1)").arg(type));
363  FillDBOrdered(expireList, type);
364  SendDeleteMessages(expireList);
365  ClearExpireList(expireList);
366 }
367 
372 {
373  pginfolist_t expireList;
374 
375  LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireOldDeleted()"));
376  FillDBOrdered(expireList, emOldDeletedPrograms);
377  SendDeleteMessages(expireList);
378  ClearExpireList(expireList);
379 }
380 
385 {
386  pginfolist_t expireList;
387 
388  LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireQuickDeleted()"));
390  SendDeleteMessages(expireList);
391  ClearExpireList(expireList);
392 }
393 
399 {
400  pginfolist_t expireList;
401  pginfolist_t deleteList;
402  QList<FileSystemInfo> fsInfos;
403  QList<FileSystemInfo>::iterator fsit;
404 
405  LOG(VB_FILE, LOG_INFO, LOC + "ExpireRecordings()");
406 
407  if (m_mainServer)
408  m_mainServer->GetFilesystemInfos(fsInfos, true);
409 
410  if (fsInfos.empty())
411  {
412  LOG(VB_GENERAL, LOG_ERR, LOC + "Filesystem Info cache is empty, unable "
413  "to determine what Recordings to expire");
414 
415  return;
416  }
417 
418  FillExpireList(expireList);
419 
420  QMap <int, bool> truncateMap;
421  MSqlQuery query(MSqlQuery::InitCon());
422  query.prepare("SELECT DISTINCT rechost, recdir "
423  "FROM inuseprograms "
424  "WHERE recusage = 'truncatingdelete' "
425  "AND lastupdatetime > DATE_ADD(NOW(), INTERVAL -2 MINUTE);");
426 
427  if (!query.exec())
428  {
429  MythDB::DBError(LOC + "ExpireRecordings", query);
430  }
431  else
432  {
433  while (query.next())
434  {
435  QString rechost = query.value(0).toString();
436  QString recdir = query.value(1).toString();
437 
438  LOG(VB_FILE, LOG_INFO, LOC +
439  QString("%1:%2 has an in-progress truncating delete.")
440  .arg(rechost, recdir));
441 
442  for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
443  {
444  if ((fsit->getHostname() == rechost) &&
445  (fsit->getPath() == recdir))
446  {
447  truncateMap[fsit->getFSysID()] = true;
448  break;
449  }
450  }
451  }
452  }
453 
454  QMap <int, bool> fsMap;
455  for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
456  {
457  if (fsMap.contains(fsit->getFSysID()))
458  continue;
459 
460  fsMap[fsit->getFSysID()] = true;
461 
462  LOG(VB_FILE, LOG_INFO,
463  QString("fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
464  .arg(fsit->getFSysID())
465  .arg(fsit->getTotalSpace() / 1024.0 / 1024.0, 7, 'f', 1)
466  .arg(fsit->getUsedSpace() / 1024.0 / 1024.0, 7, 'f', 1)
467  .arg(fsit->getFreeSpace() / 1024.0 / 1024.0, 7, 'f', 1));
468 
469  if ((fsit->getTotalSpace() == -1) || (fsit->getUsedSpace() == -1))
470  {
471  LOG(VB_FILE, LOG_ERR, LOC +
472  QString("fsID #%1 has invalid info, AutoExpire cannot run for "
473  "this filesystem. Continuing on to next...")
474  .arg(fsit->getFSysID()));
475  LOG(VB_FILE, LOG_INFO, QString("Directories on filesystem ID %1:")
476  .arg(fsit->getFSysID()));
477  QList<FileSystemInfo>::iterator fsit2;
478  for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); ++fsit2)
479  {
480  if (fsit2->getFSysID() == fsit->getFSysID())
481  {
482  LOG(VB_FILE, LOG_INFO, QString(" %1:%2")
483  .arg(fsit2->getHostname(), fsit2->getPath()));
484  }
485  }
486 
487  continue;
488  }
489 
490  if (truncateMap.contains(fsit->getFSysID()))
491  {
492  LOG(VB_FILE, LOG_INFO,
493  QString(" fsid %1 has a truncating delete in progress, "
494  "AutoExpire cannot run for this filesystem until the "
495  "delete has finished. Continuing on to next...")
496  .arg(fsit->getFSysID()));
497  continue;
498  }
499 
500  if (std::max((int64_t)0LL, fsit->getFreeSpace()) <
501  m_desiredSpace[fsit->getFSysID()])
502  {
503  LOG(VB_FILE, LOG_INFO,
504  QString(" Not Enough Free Space! We want %1 MB")
505  .arg(m_desiredSpace[fsit->getFSysID()] / 1024));
506 
507  QMap<QString, int> dirList;
508  QList<FileSystemInfo>::iterator fsit2;
509 
510  LOG(VB_FILE, LOG_INFO,
511  QString(" Directories on filesystem ID %1:")
512  .arg(fsit->getFSysID()));
513 
514  for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); ++fsit2)
515  {
516  if (fsit2->getFSysID() == fsit->getFSysID())
517  {
518  LOG(VB_FILE, LOG_INFO, QString(" %1:%2")
519  .arg(fsit2->getHostname(), fsit2->getPath()));
520  dirList[fsit2->getHostname() + ":" + fsit2->getPath()] = 1;
521  }
522  }
523 
524  LOG(VB_FILE, LOG_INFO,
525  " Searching for files expirable in these directories");
526  QString myHostName = gCoreContext->GetHostName();
527  auto it = expireList.begin();
528  while ((it != expireList.end()) &&
529  (std::max((int64_t)0LL, fsit->getFreeSpace()) <
530  m_desiredSpace[fsit->getFSysID()]))
531  {
532  ProgramInfo *p = *it;
533  ++it;
534 
535  LOG(VB_FILE, LOG_INFO, QString(" Checking %1 => %2")
536  .arg(p->toString(ProgramInfo::kRecordingKey),
537  p->GetTitle()));
538 
539  if (!p->IsLocal())
540  {
541  bool foundFile = false;
542  auto eit = m_encoderList->constBegin();
543  while (eit != m_encoderList->constEnd())
544  {
545  EncoderLink *el = *eit;
546  eit++;
547 
548  if ((p->GetHostname() == el->GetHostName()) ||
549  ((p->GetHostname() == myHostName) &&
550  (el->IsLocal())))
551  {
552  if (el->IsConnected())
553  foundFile = el->CheckFile(p);
554 
555  eit = m_encoderList->constEnd();
556  }
557  }
558 
559  if (!foundFile && (p->GetHostname() != myHostName))
560  {
561  // Wasn't found so check locally
562  QString file = GetPlaybackURL(p);
563 
564  if (file.startsWith("/"))
565  {
566  p->SetPathname(file);
567  p->SetHostname(myHostName);
568  foundFile = true;
569  }
570  }
571 
572  if (!foundFile)
573  {
574  LOG(VB_FILE, LOG_ERR, LOC +
575  QString(" ERROR: Can't find file for %1")
576  .arg(p->toString(ProgramInfo::kRecordingKey)));
577  continue;
578  }
579  }
580 
581  QFileInfo vidFile(p->GetPathname());
582  if (dirList.contains(p->GetHostname() + ':' + vidFile.path()))
583  {
584  fsit->setUsedSpace(fsit->getUsedSpace()
585  - (p->GetFilesize() / 1024));
586  deleteList.push_back(p);
587 
588  LOG(VB_FILE, LOG_INFO,
589  QString(" FOUND file expirable. "
590  "%1 is located at %2 which is on fsID #%3. "
591  "Adding to deleteList. After deleting we "
592  "should have %4 MB free on this filesystem.")
593  .arg(p->toString(ProgramInfo::kRecordingKey),
594  p->GetPathname()).arg(fsit->getFSysID())
595  .arg(fsit->getFreeSpace() / 1024));
596  }
597  }
598  }
599  }
600 
601  SendDeleteMessages(deleteList);
602 
603  ClearExpireList(deleteList, false);
604  ClearExpireList(expireList);
605 }
606 
611 {
612  QString msg;
613 
614  if (deleteList.empty())
615  {
616  LOG(VB_FILE, LOG_INFO, LOC + "SendDeleteMessages. Nothing to expire.");
617  return;
618  }
619 
620  LOG(VB_FILE, LOG_INFO, LOC +
621  "SendDeleteMessages, cycling through deleteList.");
622  auto it = deleteList.begin();
623  while (it != deleteList.end())
624  {
625  msg = QString("%1Expiring %2 MB for %3 => %4")
626  .arg(VERBOSE_LEVEL_CHECK(VB_FILE, LOG_ANY) ? " " : "",
627  QString::number((*it)->GetFilesize() >> 20),
628  (*it)->toString(ProgramInfo::kRecordingKey),
629  (*it)->toString(ProgramInfo::kTitleSubtitle));
630 
631  LOG(VB_GENERAL, LOG_NOTICE, msg);
632 
633  // send auto expire message to backend's event thread.
634  MythEvent me(QString("AUTO_EXPIRE %1 %2").arg((*it)->GetChanID())
635  .arg((*it)->GetRecordingStartTime(MythDate::ISODate)));
636  gCoreContext->dispatch(me);
637 
638  ++it; // move on to next program
639  }
640 }
641 
648 {
649  QMap<QString, int> maxEpisodes;
650  QMap<QString, int>::Iterator maxIter;
651  QMap<QString, int> episodeParts;
652  QString episodeKey;
653 
654  MSqlQuery query(MSqlQuery::InitCon());
655  query.prepare("SELECT recordid, maxepisodes, title "
656  "FROM record WHERE maxepisodes > 0 "
657  "ORDER BY recordid ASC, maxepisodes DESC");
658 
659  if (query.exec() && query.isActive() && query.size() > 0)
660  {
661  LOG(VB_FILE, LOG_INFO, LOC +
662  QString("Found %1 record profiles using max episode expiration")
663  .arg(query.size()));
664  while (query.next())
665  {
666  LOG(VB_FILE, LOG_INFO, QString(" %1 (%2 for rec id %3)")
667  .arg(query.value(2).toString())
668  .arg(query.value(1).toInt())
669  .arg(query.value(0).toInt()));
670  maxEpisodes[query.value(0).toString()] = query.value(1).toInt();
671  }
672  }
673 
674  LOG(VB_FILE, LOG_INFO, LOC +
675  "Checking episode count for each recording profile using max episodes");
676  for (maxIter = maxEpisodes.begin(); maxIter != maxEpisodes.end(); ++maxIter)
677  {
678  query.prepare("SELECT chanid, starttime, title, progstart, progend, "
679  "duplicate "
680  "FROM recorded "
681  "WHERE recordid = :RECID AND preserve = 0 "
682  "AND recgroup NOT IN ('LiveTV', 'Deleted') "
683  "ORDER BY starttime DESC;");
684  query.bindValue(":RECID", maxIter.key());
685 
686  if (!query.exec() || !query.isActive())
687  {
688  MythDB::DBError("AutoExpire query failed!", query);
689  continue;
690  }
691 
692  LOG(VB_FILE, LOG_INFO, QString(" Recordid %1 has %2 recordings.")
693  .arg(maxIter.key())
694  .arg(query.size()));
695  if (query.size() > 0)
696  {
697  int found = 1;
698  while (query.next())
699  {
700  uint chanid = query.value(0).toUInt();
701  QDateTime startts = MythDate::as_utc(query.value(1).toDateTime());
702  QString title = query.value(2).toString();
703  QDateTime progstart = MythDate::as_utc(query.value(3).toDateTime());
704  QDateTime progend = MythDate::as_utc(query.value(4).toDateTime());
705  int duplicate = query.value(5).toInt();
706 
707  episodeKey = QString("%1_%2_%3")
708  .arg(QString::number(chanid),
709  progstart.toString(Qt::ISODate),
710  progend.toString(Qt::ISODate));
711 
712  if ((!IsInDontExpireSet(chanid, startts)) &&
713  (!episodeParts.contains(episodeKey)) &&
714  (found > *maxIter))
715  {
716  QString msg =
717  QString("%1Deleting %2 at %3 => %4. "
718  "Too many episodes, we only want to keep %5.")
719  .arg(VERBOSE_LEVEL_CHECK(VB_FILE, LOG_ANY) ? " " : "",
720  QString::number(chanid),
721  startts.toString(Qt::ISODate),
722  title,
723  QString::number(*maxIter));
724 
725  LOG(VB_GENERAL, LOG_NOTICE, msg);
726 
727  // allow re-record if auto expired
728  RecordingInfo recInfo(chanid, startts);
729  if (gCoreContext->GetBoolSetting("RerecordWatched", false) ||
730  !recInfo.IsWatched())
731  {
732  recInfo.ForgetHistory();
733  }
734  msg = QString("DELETE_RECORDING %1 %2")
735  .arg(chanid)
736  .arg(startts.toString(Qt::ISODate));
737 
738  MythEvent me(msg);
739  gCoreContext->dispatch(me);
740  }
741  else
742  {
743  // keep track of shows we haven't expired so we can
744  // make sure we don't expire another part of the same
745  // episode.
746  if (episodeParts.contains(episodeKey))
747  {
748  episodeParts[episodeKey] = episodeParts[episodeKey] + 1;
749  }
750  else
751  {
752  episodeParts[episodeKey] = 1;
753  if( duplicate )
754  found++;
755  }
756  }
757  }
758  }
759  }
760 }
761 
767 {
768  int expMethod = gCoreContext->GetNumSetting("AutoExpireMethod", 1);
769 
770  ClearExpireList(expireList);
771 
773 
774  switch(expMethod)
775  {
776  case emOldestFirst:
779  FillDBOrdered(expireList, expMethod);
780  break;
781  // default falls through so list is empty so no AutoExpire
782  }
783 }
784 
788 void AutoExpire::PrintExpireList(const QString& expHost)
789 {
790  pginfolist_t expireList;
791 
792  FillExpireList(expireList);
793 
794  QString msg = "MythTV AutoExpire List ";
795  if (expHost != "ALL")
796  msg += QString("for '%1' ").arg(expHost);
797  msg += "(programs listed in order of expiration)";
798  std::cout << msg.toLocal8Bit().constData() << std::endl;
799 
800  for (auto *first : expireList)
801  {
802  if (expHost != "ALL" && first->GetHostname() != expHost)
803  continue;
804 
805  QString title = first->toString(ProgramInfo::kTitleSubtitle);
806  title = title.leftJustified(39, ' ', true);
807 
808  QString outstr = QString("%1 %2 MB %3 [%4]")
809  .arg(title,
810  QString::number(first->GetFilesize() >> 20)
811  .rightJustified(5, ' ', true),
812  first->GetRecordingStartTime(MythDate::ISODate)
813  .leftJustified(24, ' ', true),
814  QString::number(first->GetRecordingPriority())
815  .rightJustified(3, ' ', true));
816  QByteArray out = outstr.toLocal8Bit();
817 
818  std::cout << out.constData() << std::endl;
819  }
820 
821  ClearExpireList(expireList);
822 }
823 
827 void AutoExpire::GetAllExpiring(QStringList &strList)
828 {
829  QMutexLocker lockit(&m_instanceLock);
830  pginfolist_t expireList;
831 
833 
837  FillDBOrdered(expireList, gCoreContext->GetNumSetting("AutoExpireMethod",
838  emOldestFirst));
839 
840  strList << QString::number(expireList.size());
841 
842  for (auto & info : expireList)
843  info->ToStringList(strList);
844 
845  ClearExpireList(expireList);
846 }
847 
852 {
853  QMutexLocker lockit(&m_instanceLock);
854  pginfolist_t expireList;
855 
857 
861  FillDBOrdered(expireList, gCoreContext->GetNumSetting("AutoExpireMethod",
862  emOldestFirst));
863 
864  for (auto & info : expireList)
865  list.push_back( new ProgramInfo( *info ));
866 
867  ClearExpireList(expireList);
868 }
869 
873 void AutoExpire::ClearExpireList(pginfolist_t &expireList, bool deleteProg)
874 {
875  ProgramInfo *pginfo = nullptr;
876  while (!expireList.empty())
877  {
878  if (deleteProg)
879  pginfo = expireList.back();
880 
881  expireList.pop_back();
882 
883  if (deleteProg)
884  delete pginfo;
885  }
886 }
887 
892 void AutoExpire::FillDBOrdered(pginfolist_t &expireList, int expMethod)
893 {
894  QString where;
895  QString orderby;
896  QString msg;
897  int maxAge = 0;
898 
899  switch (expMethod)
900  {
901  default:
902  case emOldestFirst:
903  msg = "Adding programs expirable in Oldest First order";
904  where = "autoexpire > 0";
905  if (gCoreContext->GetBoolSetting("AutoExpireWatchedPriority", false))
906  orderby = "recorded.watched DESC, ";
907  orderby += "starttime ASC";
908  break;
910  msg = "Adding programs expirable in Lowest Priority First order";
911  where = "autoexpire > 0";
912  if (gCoreContext->GetBoolSetting("AutoExpireWatchedPriority", false))
913  orderby = "recorded.watched DESC, ";
914  orderby += "recorded.recpriority ASC, starttime ASC";
915  break;
917  msg = "Adding programs expirable in Weighted Time Priority order";
918  where = "autoexpire > 0";
919  if (gCoreContext->GetBoolSetting("AutoExpireWatchedPriority", false))
920  orderby = "recorded.watched DESC, ";
921  orderby += QString("DATE_ADD(starttime, INTERVAL '%1' * "
922  "recorded.recpriority DAY) ASC")
923  .arg(gCoreContext->GetNumSetting("AutoExpireDayPriority", 3));
924  break;
926  msg = "Adding Short LiveTV programs in starttime order";
927  where = "recgroup = 'LiveTV' "
928  "AND endtime < DATE_ADD(starttime, INTERVAL '30' SECOND) "
929  "AND endtime <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ";
930  orderby = "starttime ASC";
931  break;
933  msg = "Adding LiveTV programs in starttime order";
934  where = QString("recgroup = 'LiveTV' "
935  "AND endtime <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
936  .arg(gCoreContext->GetNumSetting("AutoExpireLiveTVMaxAge", 1));
937  orderby = "starttime ASC";
938  break;
940  if ((maxAge = gCoreContext->GetNumSetting("DeletedMaxAge", 0)) <= 0)
941  return;
942  msg = QString("Adding programs deleted more than %1 days ago")
943  .arg(maxAge);
944  where = QString("recgroup = 'Deleted' "
945  "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
946  .arg(maxAge);
947  orderby = "starttime ASC";
948  break;
950  if (gCoreContext->GetNumSetting("DeletedMaxAge", 0) != 0)
951  return;
952  msg = QString("Adding programs deleted more than 5 minutes ago");
953  where = QString("recgroup = 'Deleted' "
954  "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ");
955  orderby = "lastmodified ASC";
956  break;
958  msg = "Adding deleted programs in FIFO order";
959  where = "recgroup = 'Deleted'";
960  orderby = "lastmodified ASC";
961  break;
962  }
963 
964  LOG(VB_FILE, LOG_INFO, LOC + "FillDBOrdered: " + msg);
965 
966  MSqlQuery query(MSqlQuery::InitCon());
967  QString querystr = QString(
968  "SELECT recorded.chanid, starttime "
969  "FROM recorded "
970  "LEFT JOIN channel ON recorded.chanid = channel.chanid "
971  "WHERE %1 AND deletepending = 0 "
972  "ORDER BY autoexpire DESC, %2").arg(where, orderby);
973 
974  query.prepare(querystr);
975 
976  if (!query.exec())
977  return;
978 
979  while (query.next())
980  {
981  uint chanid = query.value(0).toUInt();
982  QDateTime recstartts = MythDate::as_utc(query.value(1).toDateTime());
983 
984  if (IsInDontExpireSet(chanid, recstartts))
985  {
986  LOG(VB_FILE, LOG_INFO, LOC +
987  QString(" Skipping %1 at %2 because it is in Don't Expire "
988  "List")
989  .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
990  }
991  else if (IsInExpireList(expireList, chanid, recstartts))
992  {
993  LOG(VB_FILE, LOG_INFO, LOC +
994  QString(" Skipping %1 at %2 because it is already in Expire "
995  "List")
996  .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
997  }
998  else
999  {
1000  auto *pginfo = new ProgramInfo(chanid, recstartts);
1001  if (pginfo->GetChanID())
1002  {
1003  LOG(VB_FILE, LOG_INFO, LOC + QString(" Adding %1 at %2")
1004  .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1005  expireList.push_back(pginfo);
1006  }
1007  else
1008  {
1009  LOG(VB_FILE, LOG_INFO, LOC +
1010  QString(" Skipping %1 at %2 "
1011  "because it could not be loaded from the DB")
1012  .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1013  delete pginfo;
1014  }
1015  }
1016  }
1017 }
1018 
1030 void AutoExpire::Update(int encoder, int fsID, bool immediately)
1031 {
1032  if (!gExpirer)
1033  return;
1034 
1035  if (encoder > 0)
1036  {
1037  QString msg = QString("Cardid %1: is starting a recording on")
1038  .arg(encoder);
1039  if (fsID == -1)
1040  msg.append(" an unknown fsID soon.");
1041  else
1042  msg.append(QString(" fsID %2 soon.").arg(fsID));
1043  LOG(VB_FILE, LOG_INFO, LOC + msg);
1044  }
1045 
1046  if (immediately)
1047  {
1048  if (encoder > 0)
1049  {
1050  gExpirer->m_instanceLock.lock();
1051  gExpirer->m_usedEncoders[encoder] = fsID;
1052  gExpirer->m_instanceLock.unlock();
1053  }
1054  gExpirer->CalcParams();
1055  gExpirer->m_instanceCond.wakeAll();
1056  }
1057  else
1058  {
1059  gExpirer->m_updateLock.lock();
1060  gExpirer->m_updateQueue.append(UpdateEntry(encoder, fsID));
1061  gExpirer->m_updateLock.unlock();
1062  }
1063 }
1064 
1066 {
1067  m_dontExpireSet.clear();
1068 
1069  MSqlQuery query(MSqlQuery::InitCon());
1070  query.prepare(
1071  "SELECT chanid, starttime, lastupdatetime, recusage, hostname "
1072  "FROM inuseprograms");
1073 
1074  if (!query.exec() || !query.next())
1075  return;
1076 
1077  LOG(VB_FILE, LOG_INFO, LOC + "Adding Programs to 'Do Not Expire' List");
1078  QDateTime curTime = MythDate::current();
1079 
1080  do
1081  {
1082  uint chanid = query.value(0).toUInt();
1083  QDateTime recstartts = MythDate::as_utc(query.value(1).toDateTime());
1084  QDateTime lastupdate = MythDate::as_utc(query.value(2).toDateTime());
1085 
1086  if (lastupdate.secsTo(curTime) < 2 * 60 * 60)
1087  {
1088  QString key = QString("%1_%2")
1089  .arg(chanid).arg(recstartts.toString(Qt::ISODate));
1090  m_dontExpireSet.insert(key);
1091  LOG(VB_FILE, LOG_INFO, QString(" %1 at %2 in use by %3 on %4")
1092  .arg(QString::number(chanid),
1093  recstartts.toString(Qt::ISODate),
1094  query.value(3).toString(),
1095  query.value(4).toString()));
1096  }
1097  }
1098  while (query.next());
1099 }
1100 
1102  uint chanid, const QDateTime &recstartts) const
1103 {
1104  QString key = QString("%1_%2")
1105  .arg(chanid).arg(recstartts.toString(Qt::ISODate));
1106 
1107  return (m_dontExpireSet.find(key) != m_dontExpireSet.end());
1108 }
1109 
1111  const pginfolist_t &expireList, uint chanid, const QDateTime &recstartts)
1112 {
1113  return std::any_of(expireList.cbegin(), expireList.cend(),
1114  [chanid,&recstartts](auto *info)
1115  { return ((info->GetChanID() == chanid) &&
1116  (info->GetRecordingStartTime() == recstartts)); } );
1117 }
1118 
1119 /* 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:213
MSqlQuery::next
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:807
MSqlQuery
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:125
MSqlQuery::size
int size(void) const
Definition: mythdbcon.h:212
MThread::start
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:286
AutoExpire::PrintExpireList
void PrintExpireList(const QString &expHost="ALL")
Prints a summary of the files that can be deleted.
Definition: autoexpire.cpp:788
backendcontext.h
mythdb.h
MainServer::GetFilesystemInfos
void GetFilesystemInfos(QList< FileSystemInfo > &fsInfos, bool useCache=true)
Definition: mainserver.cpp:5376
MythDate::as_utc
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
Definition: mythdate.cpp:28
AutoExpire::IsInExpireList
static bool IsInExpireList(const pginfolist_t &expireList, uint chanid, const QDateTime &recstartts)
Definition: autoexpire.cpp:1110
AutoExpire::Update
static void Update(int encoder, int fsID, bool immediately)
This is used to update the global AutoExpire instance "expirer".
Definition: autoexpire.cpp:1030
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:303
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:873
RecordingInfo::ForgetHistory
void ForgetHistory(void)
Forget the recording of a program so it will be recorded again.
Definition: recordinginfo.cpp:1412
MythEvent
This class is used as a container for messages.
Definition: mythevent.h:16
ProgramInfo::kTitleSubtitle
@ kTitleSubtitle
Definition: programinfo.h:511
MSqlQuery::value
QVariant value(int i) const
Definition: mythdbcon.h:202
MSqlQuery::exec
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:608
AutoExpire::ExpireOldDeleted
void ExpireOldDeleted(void)
This expires deleted programs older than DeletedMaxAge.
Definition: autoexpire.cpp:371
emLowestPriorityFirst
@ emLowestPriorityFirst
Definition: autoexpire.h:29
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:23
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:647
AutoExpire::GetAllExpiring
void GetAllExpiring(QStringList &strList)
Gets the full list of programs that can expire in expiration order.
Definition: autoexpire.cpp:827
ExpireThread::run
void run(void) override
This calls AutoExpire::RunExpirer() from within a new thread.
Definition: autoexpire.cpp:54
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:15
true
VERBOSE_PREAMBLE Most true
Definition: verbosedefs.h:91
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:484
MythObservable::addListener
void addListener(QObject *listener)
Add a listener to the observable.
Definition: mythobservable.cpp:38
MythDate::secsInFuture
std::chrono::seconds secsInFuture(const QDateTime &future)
Definition: mythdate.cpp:209
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:341
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:540
compat.h
AutoExpire::ExpireRecordings
void ExpireRecordings(void)
This expires normal recordings.
Definition: autoexpire.cpp:398
MythDB::DBError
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:227
AutoExpire::IsInDontExpireSet
bool IsInDontExpireSet(uint chanid, const QDateTime &recstartts) const
Definition: autoexpire.cpp:1101
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:610
uint
unsigned int uint
Definition: compat.h:79
AutoExpire::AutoExpire
AutoExpire()=default
gCoreContext
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: mythcorecontext.cpp:61
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:766
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:935
UpdateEntry
Definition: autoexpire.h:50
AutoExpire::RunExpirer
void RunExpirer(void)
This contains the main loop for the auto expire process.
Definition: autoexpire.cpp:265
MythCoreContext::GetBoolSetting
bool GetBoolSetting(const QString &key, bool defaultval=false)
Definition: mythcorecontext.cpp:929
AutoExpire::CalcParams
void CalcParams(void)
Definition: autoexpire.cpp:120
VERBOSE_LEVEL_CHECK
#define VERBOSE_LEVEL_CHECK(_MASK_, _LEVEL_)
Definition: mythlogging.h:14
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:892
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:883
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:82
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:384
SPACE_TOO_BIG_KB
#define SPACE_TOO_BIG_KB
If calculated desired space for 10 min freq is > SPACE_TOO_BIG_KB then we use 5 min expire frequency.
Definition: autoexpire.cpp:51
MythCoreContext::GetHostName
QString GetHostName(void)
Definition: mythcorecontext.cpp:861
AutoExpire::ExpireLiveTV
void ExpireLiveTV(int type)
This expires LiveTV programs.
Definition: autoexpire.cpp:358
AutoExpire::GetDesiredSpace
uint64_t GetDesiredSpace(int fsID) const
Used by the scheduler to select the next recording dir.
Definition: autoexpire.cpp:109
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:1748
MythObservable::removeListener
void removeListener(QObject *listener)
Remove a listener to the observable.
Definition: mythobservable.cpp:55
AutoExpire::UpdateDontExpireSet
void UpdateDontExpireSet(void)
Definition: autoexpire.cpp:1065
emWeightedTimePriority
@ emWeightedTimePriority
Definition: autoexpire.h:30
ProgramInfo::kRecordingKey
@ kRecordingKey
Definition: programinfo.h:512
MSqlQuery::prepare
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:832
UpdateEntry::m_fsID
int m_fsID
Definition: autoexpire.h:57