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