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_expire_thread(new ExpireThread(this)),
75  m_expire_thread_run(true)
76 {
79 }
80 
85 {
86  {
87  QMutexLocker locker(&m_instance_lock);
88  m_expire_thread_run = false;
89  m_instance_cond.wakeAll();
90  }
91 
92  {
93  QMutexLocker locker(&m_update_lock);
94  m_update_queue.clear();
95  }
96 
97  if (m_expire_thread)
98  {
101  delete m_expire_thread;
102  m_expire_thread = nullptr;
103  }
104 }
105 
111 uint64_t AutoExpire::GetDesiredSpace(int fsID) const
112 {
113  QMutexLocker locker(&m_instance_lock);
114  if (m_desired_space.contains(fsID))
115  return m_desired_space[fsID];
116  return 0;
117 }
118 
123 {
124  LOG(VB_FILE, LOG_INFO, LOC + "CalcParams()");
125 
126  QList<FileSystemInfo> fsInfos;
127 
128  m_instance_lock.lock();
129  if (m_main_server)
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  // thrad can.
138  m_main_server->GetFilesystemInfos(fsInfos, false);
139  }
140  m_instance_lock.unlock();
141 
142  if (fsInfos.empty())
143  {
144  LOG(VB_GENERAL, LOG_ERR, LOC + "Filesystem Info cache is empty, unable " "to calculate necessary parameters.");
145 
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_instance_lock.lock();
161  QMap<int, int>::const_iterator ueit = m_used_encoders.begin();
162  while (ueit != m_used_encoders.end())
163  {
164  fsEncoderMap[*ueit].push_back(ueit.key());
165  ++ueit;
166  }
167  m_instance_lock.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  vector<int>::iterator unknownfs_it = fsEncoderMap[-1].begin();
180  for (; unknownfs_it != fsEncoderMap[-1].end(); ++unknownfs_it)
181  fsEncoderMap[fsit->getFSysID()].push_back(*unknownfs_it);
182 
183  if (fsEncoderMap.contains(fsit->getFSysID()))
184  {
185  LOG(VB_FILE, LOG_INFO,
186  QString("fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
187  .arg(fsit->getFSysID())
188  .arg(fsit->getTotalSpace() / 1024.0 / 1024.0, 7, 'f', 1)
189  .arg(fsit->getUsedSpace() / 1024.0 / 1024.0, 7, 'f', 1)
190  .arg(fsit->getFreeSpace() / 1024.0 / 1024.0, 7, 'f', 1));
191 
192  vector<int>::iterator encit =
193  fsEncoderMap[fsit->getFSysID()].begin();
194  for (; encit != fsEncoderMap[fsit->getFSysID()].end(); ++encit)
195  {
196  EncoderLink *enc = *(m_encoderList->find(*encit));
197 
198  if (!enc->IsConnected() || !enc->IsBusy())
199  {
200  // remove encoder since it can't write to any file system
201  LOG(VB_FILE, LOG_INFO, LOC +
202  QString("Cardid %1: is not recoding, removing it "
203  "from used list.").arg(*encit));
204  m_instance_lock.lock();
205  m_used_encoders.remove(*encit);
206  m_instance_lock.unlock();
207  continue;
208  }
209 
210  uint64_t maxBitrate = enc->GetMaxBitrate();
211  if (maxBitrate==0)
212  maxBitrate = 19500000LL;
213  thisKBperMin += (maxBitrate*((uint64_t)15))>>11;
214  LOG(VB_FILE, LOG_INFO, QString(" Cardid %1: max bitrate "
215  "%2 Kb/sec, fsID %3 max is now %4 KB/min")
216  .arg(enc->GetInputID())
217  .arg(enc->GetMaxBitrate() >> 10)
218  .arg(fsit->getFSysID())
219  .arg(thisKBperMin));
220  }
221  }
222  fsMap[fsit->getFSysID()] = thisKBperMin;
223 
224  if (thisKBperMin > maxKBperMin)
225  {
226  LOG(VB_FILE, LOG_INFO,
227  QString(" Max of %1 KB/min for fsID %2 is higher "
228  "than the existing Max of %3 so we'll use this Max instead")
229  .arg(thisKBperMin).arg(fsit->getFSysID()).arg(maxKBperMin));
230  maxKBperMin = thisKBperMin;
231  }
232  }
233 
234  // Determine frequency to run autoexpire so it doesn't have to free
235  // too much space
236  uint expireFreq = 15;
237  if (maxKBperMin > 0)
238  {
239  expireFreq = SPACE_TOO_BIG_KB / (maxKBperMin + maxKBperMin/3);
240  expireFreq = max(3U, min(expireFreq, 15U));
241  }
242 
243  double expireMinGB = ((maxKBperMin + maxKBperMin/3)
244  * expireFreq + extraKB) >> 20;
245  LOG(VB_GENERAL, LOG_NOTICE, LOC +
246  QString("CalcParams(): Max required Free Space: %1 GB w/freq: %2 min")
247  .arg(expireMinGB, 0, 'f', 1).arg(expireFreq));
248 
249  // lock class and save these parameters.
250  m_instance_lock.lock();
251  m_desired_freq = expireFreq;
252  // write per file system needed space back, use safety of 33%
253  QMap<int, uint64_t>::iterator it = fsMap.begin();
254  while (it != fsMap.end())
255  {
256  m_desired_space[it.key()] = (*it + *it/3) * expireFreq + extraKB;
257  ++it;
258  }
259  m_instance_lock.unlock();
260 }
261 
271 {
272  QTime timer;
273  QDateTime curTime;
274  QDateTime next_expire = MythDate::current().addSecs(60);
275 
276  QMutexLocker locker(&m_instance_lock);
277 
278  // wait a little for main server to come up and things to settle down
279  Sleep(20 * 1000);
280 
281  timer.start();
282 
283  while (m_expire_thread_run)
284  {
285  TVRec::s_inputsLock.lockForRead();
286 
287  curTime = MythDate::current();
288  // recalculate auto expire parameters
289  if (curTime >= next_expire)
290  {
291  m_update_lock.lock();
292  while (!m_update_queue.empty())
293  {
294  UpdateEntry ue = m_update_queue.dequeue();
295  if (ue.m_encoder > 0)
297  }
298  m_update_lock.unlock();
299 
300  locker.unlock();
301  CalcParams();
302  locker.relock();
303  if (!m_expire_thread_run)
304  break;
305  }
306  timer.restart();
307 
309 
310  // Expire Short LiveTV files for this backend every 2 minutes
311  if ((curTime.time().minute() % 2) == 0)
313 
314  // Expire normal recordings depending on frequency calculated
315  if (curTime >= next_expire)
316  {
317  LOG(VB_FILE, LOG_INFO, LOC + "Running now!");
318  next_expire =
319  MythDate::current().addSecs(m_desired_freq * 60);
320 
322 
323  int maxAge = gCoreContext->GetNumSetting("DeletedMaxAge", 0);
324  if (maxAge > 0)
326  else if (maxAge == 0)
328 
330 
332  }
333 
334  TVRec::s_inputsLock.unlock();
335 
336  Sleep(60 * 1000 - timer.elapsed());
337  }
338 }
339 
346 void AutoExpire::Sleep(int sleepTime)
347 {
348  if (sleepTime <= 0)
349  return;
350 
351  QDateTime little_tm = MythDate::current().addMSecs(sleepTime);
352  int timeleft = sleepTime;
353  while (m_expire_thread_run && (timeleft > 0))
354  {
355  m_instance_cond.wait(&m_instance_lock, timeleft);
356  timeleft = MythDate::current().secsTo(little_tm) * 1000;
357  }
358 }
359 
364 {
365  pginfolist_t expireList;
366 
367  LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireLiveTV(%1)").arg(type));
368  FillDBOrdered(expireList, type);
369  SendDeleteMessages(expireList);
370  ClearExpireList(expireList);
371 }
372 
377 {
378  pginfolist_t expireList;
379 
380  LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireOldDeleted()"));
381  FillDBOrdered(expireList, emOldDeletedPrograms);
382  SendDeleteMessages(expireList);
383  ClearExpireList(expireList);
384 }
385 
390 {
391  pginfolist_t expireList;
392 
393  LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireQuickDeleted()"));
395  SendDeleteMessages(expireList);
396  ClearExpireList(expireList);
397 }
398 
404 {
405  pginfolist_t expireList;
406  pginfolist_t deleteList;
407  QList<FileSystemInfo> fsInfos;
408  QList<FileSystemInfo>::iterator fsit;
409 
410  LOG(VB_FILE, LOG_INFO, LOC + "ExpireRecordings()");
411 
412  if (m_main_server)
413  m_main_server->GetFilesystemInfos(fsInfos, true);
414 
415  if (fsInfos.empty())
416  {
417  LOG(VB_GENERAL, LOG_ERR, LOC + "Filesystem Info cache is empty, unable "
418  "to determine what Recordings to expire");
419 
420  return;
421  }
422 
423  FillExpireList(expireList);
424 
425  QMap <int, bool> truncateMap;
426  MSqlQuery query(MSqlQuery::InitCon());
427  query.prepare("SELECT DISTINCT rechost, recdir "
428  "FROM inuseprograms "
429  "WHERE recusage = 'truncatingdelete' "
430  "AND lastupdatetime > DATE_ADD(NOW(), INTERVAL -2 MINUTE);");
431 
432  if (!query.exec())
433  {
434  MythDB::DBError(LOC + "ExpireRecordings", query);
435  }
436  else
437  {
438  while (query.next())
439  {
440  QString rechost = query.value(0).toString();
441  QString recdir = query.value(1).toString();
442 
443  LOG(VB_FILE, LOG_INFO, LOC +
444  QString("%1:%2 has an in-progress truncating delete.")
445  .arg(rechost).arg(recdir));
446 
447  for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
448  {
449  if ((fsit->getHostname() == rechost) &&
450  (fsit->getPath() == recdir))
451  {
452  truncateMap[fsit->getFSysID()] = true;
453  break;
454  }
455  }
456  }
457  }
458 
459  QMap <int, bool> fsMap;
460  for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
461  {
462  if (fsMap.contains(fsit->getFSysID()))
463  continue;
464 
465  fsMap[fsit->getFSysID()] = true;
466 
467  LOG(VB_FILE, LOG_INFO,
468  QString("fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
469  .arg(fsit->getFSysID())
470  .arg(fsit->getTotalSpace() / 1024.0 / 1024.0, 7, 'f', 1)
471  .arg(fsit->getUsedSpace() / 1024.0 / 1024.0, 7, 'f', 1)
472  .arg(fsit->getFreeSpace() / 1024.0 / 1024.0, 7, 'f', 1));
473 
474  if ((fsit->getTotalSpace() == -1) || (fsit->getUsedSpace() == -1))
475  {
476  LOG(VB_FILE, LOG_ERR, LOC +
477  QString("fsID #%1 has invalid info, AutoExpire cannot run for "
478  "this filesystem. Continuing on to next...")
479  .arg(fsit->getFSysID()));
480  LOG(VB_FILE, LOG_INFO, QString("Directories on filesystem ID %1:")
481  .arg(fsit->getFSysID()));
482  QList<FileSystemInfo>::iterator fsit2;
483  for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); ++fsit2)
484  {
485  if (fsit2->getFSysID() == fsit->getFSysID())
486  {
487  LOG(VB_FILE, LOG_INFO, QString(" %1:%2")
488  .arg(fsit2->getHostname()).arg(fsit2->getPath()));
489  }
490  }
491 
492  continue;
493  }
494 
495  if (truncateMap.contains(fsit->getFSysID()))
496  {
497  LOG(VB_FILE, LOG_INFO,
498  QString(" fsid %1 has a truncating delete in progress, "
499  "AutoExpire cannot run for this filesystem until the "
500  "delete has finished. Continuing on to next...")
501  .arg(fsit->getFSysID()));
502  continue;
503  }
504 
505  if (max((int64_t)0LL, fsit->getFreeSpace()) <
506  m_desired_space[fsit->getFSysID()])
507  {
508  LOG(VB_FILE, LOG_INFO,
509  QString(" Not Enough Free Space! We want %1 MB")
510  .arg(m_desired_space[fsit->getFSysID()] / 1024));
511 
512  QMap<QString, int> dirList;
513  QList<FileSystemInfo>::iterator fsit2;
514 
515  LOG(VB_FILE, LOG_INFO,
516  QString(" Directories on filesystem ID %1:")
517  .arg(fsit->getFSysID()));
518 
519  for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); ++fsit2)
520  {
521  if (fsit2->getFSysID() == fsit->getFSysID())
522  {
523  LOG(VB_FILE, LOG_INFO, QString(" %1:%2")
524  .arg(fsit2->getHostname()).arg(fsit2->getPath()));
525  dirList[fsit2->getHostname() + ":" + fsit2->getPath()] = 1;
526  }
527  }
528 
529  LOG(VB_FILE, LOG_INFO,
530  " Searching for files expirable in these directories");
531  QString myHostName = gCoreContext->GetHostName();
532  pginfolist_t::iterator it = expireList.begin();
533  while ((it != expireList.end()) &&
534  (max((int64_t)0LL, fsit->getFreeSpace()) <
535  m_desired_space[fsit->getFSysID()]))
536  {
537  ProgramInfo *p = *it;
538  ++it;
539 
540  LOG(VB_FILE, LOG_INFO, QString(" Checking %1 => %2")
542  .arg(p->GetTitle()));
543 
544  if (!p->IsLocal())
545  {
546  bool foundFile = false;
547  QMap<int, EncoderLink *>::Iterator eit =
548  m_encoderList->begin();
549  while (eit != m_encoderList->end())
550  {
551  EncoderLink *el = *eit;
552  eit++;
553 
554  if ((p->GetHostname() == el->GetHostName()) ||
555  ((p->GetHostname() == myHostName) &&
556  (el->IsLocal())))
557  {
558  if (el->IsConnected())
559  foundFile = el->CheckFile(p);
560 
561  eit = m_encoderList->end();
562  }
563  }
564 
565  if (!foundFile && (p->GetHostname() != myHostName))
566  {
567  // Wasn't found so check locally
568  QString file = GetPlaybackURL(p);
569 
570  if (file.startsWith("/"))
571  {
572  p->SetPathname(file);
573  p->SetHostname(myHostName);
574  foundFile = true;
575  }
576  }
577 
578  if (!foundFile)
579  {
580  LOG(VB_FILE, LOG_ERR, LOC +
581  QString(" ERROR: Can't find file for %1")
583  continue;
584  }
585  }
586 
587  QFileInfo vidFile(p->GetPathname());
588  if (dirList.contains(p->GetHostname() + ':' + vidFile.path()))
589  {
590  fsit->setUsedSpace(fsit->getUsedSpace()
591  - (p->GetFilesize() / 1024));
592  deleteList.push_back(p);
593 
594  LOG(VB_FILE, LOG_INFO,
595  QString(" FOUND file expirable. "
596  "%1 is located at %2 which is on fsID #%3. "
597  "Adding to deleteList. After deleting we "
598  "should have %4 MB free on this filesystem.")
600  .arg(p->GetPathname()).arg(fsit->getFSysID())
601  .arg(fsit->getFreeSpace() / 1024));
602  }
603  }
604  }
605  }
606 
607  SendDeleteMessages(deleteList);
608 
609  ClearExpireList(deleteList, false);
610  ClearExpireList(expireList);
611 }
612 
617 {
618  QString msg;
619 
620  if (deleteList.empty())
621  {
622  LOG(VB_FILE, LOG_INFO, LOC + "SendDeleteMessages. Nothing to expire.");
623  return;
624  }
625 
626  LOG(VB_FILE, LOG_INFO, LOC +
627  "SendDeleteMessages, cycling through deleteList.");
628  pginfolist_t::iterator it = deleteList.begin();
629  while (it != deleteList.end())
630  {
631  msg = QString("%1Expiring %2 MB for %3 => %4")
632  .arg(VERBOSE_LEVEL_CHECK(VB_FILE, LOG_ANY) ? " " : "")
633  .arg(((*it)->GetFilesize() >> 20))
634  .arg((*it)->toString(ProgramInfo::kRecordingKey))
635  .arg((*it)->toString(ProgramInfo::kTitleSubtitle));
636 
637  LOG(VB_GENERAL, LOG_NOTICE, msg);
638 
639  // send auto expire message to backend's event thread.
640  MythEvent me(QString("AUTO_EXPIRE %1 %2").arg((*it)->GetChanID())
641  .arg((*it)->GetRecordingStartTime(MythDate::ISODate)));
642  gCoreContext->dispatch(me);
643 
644  ++it; // move on to next program
645  }
646 }
647 
654 {
655  QMap<QString, int> maxEpisodes;
656  QMap<QString, int>::Iterator maxIter;
657  QMap<QString, int> episodeParts;
658  QString episodeKey;
659 
660  QString fileprefix = gCoreContext->GetFilePrefix();
661 
662  MSqlQuery query(MSqlQuery::InitCon());
663  query.prepare("SELECT recordid, maxepisodes, title "
664  "FROM record WHERE maxepisodes > 0 "
665  "ORDER BY recordid ASC, maxepisodes DESC");
666 
667  if (query.exec() && query.isActive() && query.size() > 0)
668  {
669  LOG(VB_FILE, LOG_INFO, LOC +
670  QString("Found %1 record profiles using max episode expiration")
671  .arg(query.size()));
672  while (query.next())
673  {
674  LOG(VB_FILE, LOG_INFO, QString(" %1 (%2 for rec id %3)")
675  .arg(query.value(2).toString())
676  .arg(query.value(1).toInt())
677  .arg(query.value(0).toInt()));
678  maxEpisodes[query.value(0).toString()] = query.value(1).toInt();
679  }
680  }
681 
682  LOG(VB_FILE, LOG_INFO, LOC +
683  "Checking episode count for each recording profile using max episodes");
684  for (maxIter = maxEpisodes.begin(); maxIter != maxEpisodes.end(); ++maxIter)
685  {
686  query.prepare("SELECT chanid, starttime, title, progstart, progend, "
687  "duplicate "
688  "FROM recorded "
689  "WHERE recordid = :RECID AND preserve = 0 "
690  "AND recgroup NOT IN ('LiveTV', 'Deleted') "
691  "ORDER BY starttime DESC;");
692  query.bindValue(":RECID", maxIter.key());
693 
694  if (!query.exec() || !query.isActive())
695  {
696  MythDB::DBError("AutoExpire query failed!", query);
697  continue;
698  }
699 
700  LOG(VB_FILE, LOG_INFO, QString(" Recordid %1 has %2 recordings.")
701  .arg(maxIter.key())
702  .arg(query.size()));
703  if (query.size() > 0)
704  {
705  int found = 1;
706  while (query.next())
707  {
708  uint chanid = query.value(0).toUInt();
709  QDateTime startts = MythDate::as_utc(query.value(1).toDateTime());
710  QString title = query.value(2).toString();
711  QDateTime progstart = MythDate::as_utc(query.value(3).toDateTime());
712  QDateTime progend = MythDate::as_utc(query.value(4).toDateTime());
713  int duplicate = query.value(5).toInt();
714 
715  episodeKey = QString("%1_%2_%3")
716  .arg(chanid)
717  .arg(progstart.toString(Qt::ISODate))
718  .arg(progend.toString(Qt::ISODate));
719 
720  if ((!IsInDontExpireSet(chanid, startts)) &&
721  (!episodeParts.contains(episodeKey)) &&
722  (found > *maxIter))
723  {
724  QString msg =
725  QString("%1Deleting %2 at %3 => %4. "
726  "Too many episodes, we only want to keep %5.")
727  .arg(VERBOSE_LEVEL_CHECK(VB_FILE, LOG_ANY) ?
728  " " : "")
729  .arg(chanid).arg(startts.toString(Qt::ISODate))
730  .arg(title).arg(*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  cout << msg.toLocal8Bit().constData() << endl;
806 
807  pginfolist_t::iterator i = expireList.begin();
808  for (; i != expireList.end(); ++i)
809  {
810  ProgramInfo *first = (*i);
811 
812  if (expHost != "ALL" && first->GetHostname() != expHost)
813  continue;
814 
815  QString title = first->toString(ProgramInfo::kTitleSubtitle);
816  title = title.leftJustified(39, ' ', true);
817 
818  QString outstr = QString("%1 %2 MB %3 [%4]")
819  .arg(title)
820  .arg(QString::number(first->GetFilesize() >> 20)
821  .rightJustified(5, ' ', true))
823  .leftJustified(24, ' ', true))
824  .arg(QString::number(first->GetRecordingPriority())
825  .rightJustified(3, ' ', true));
826  QByteArray out = outstr.toLocal8Bit();
827 
828  cout << out.constData() << endl;
829  }
830 
831  ClearExpireList(expireList);
832 }
833 
837 void AutoExpire::GetAllExpiring(QStringList &strList)
838 {
839  QMutexLocker lockit(&m_instance_lock);
840  pginfolist_t expireList;
841 
843 
847  FillDBOrdered(expireList, gCoreContext->GetNumSetting("AutoExpireMethod",
848  emOldestFirst));
849 
850  strList << QString::number(expireList.size());
851 
852  pginfolist_t::iterator it = expireList.begin();
853  for (; it != expireList.end(); ++it)
854  (*it)->ToStringList(strList);
855 
856  ClearExpireList(expireList);
857 }
858 
863 {
864  QMutexLocker lockit(&m_instance_lock);
865  pginfolist_t expireList;
866 
868 
872  FillDBOrdered(expireList, gCoreContext->GetNumSetting("AutoExpireMethod",
873  emOldestFirst));
874 
875  pginfolist_t::iterator it = expireList.begin();
876  for (; it != expireList.end(); ++it)
877  list.push_back( new ProgramInfo( *(*it) ));
878 
879  ClearExpireList(expireList);
880 }
881 
885 void AutoExpire::ClearExpireList(pginfolist_t &expireList, bool deleteProg)
886 {
887  ProgramInfo *pginfo = nullptr;
888  while (!expireList.empty())
889  {
890  if (deleteProg)
891  pginfo = expireList.back();
892 
893  expireList.pop_back();
894 
895  if (deleteProg)
896  delete pginfo;
897  }
898 }
899 
904 void AutoExpire::FillDBOrdered(pginfolist_t &expireList, int expMethod)
905 {
906  QString where;
907  QString orderby;
908  QString msg;
909  int maxAge;
910 
911  switch (expMethod)
912  {
913  default:
914  case emOldestFirst:
915  msg = "Adding programs expirable in Oldest First order";
916  where = "autoexpire > 0";
917  if (gCoreContext->GetBoolSetting("AutoExpireWatchedPriority", false))
918  orderby = "recorded.watched DESC, ";
919  orderby += "starttime ASC";
920  break;
922  msg = "Adding programs expirable in Lowest Priority First order";
923  where = "autoexpire > 0";
924  if (gCoreContext->GetBoolSetting("AutoExpireWatchedPriority", false))
925  orderby = "recorded.watched DESC, ";
926  orderby += "recorded.recpriority ASC, starttime ASC";
927  break;
929  msg = "Adding programs expirable in Weighted Time Priority order";
930  where = "autoexpire > 0";
931  if (gCoreContext->GetBoolSetting("AutoExpireWatchedPriority", false))
932  orderby = "recorded.watched DESC, ";
933  orderby += QString("DATE_ADD(starttime, INTERVAL '%1' * "
934  "recorded.recpriority DAY) ASC")
935  .arg(gCoreContext->GetNumSetting("AutoExpireDayPriority", 3));
936  break;
938  msg = "Adding Short LiveTV programs in starttime order";
939  where = "recgroup = 'LiveTV' "
940  "AND endtime < DATE_ADD(starttime, INTERVAL '30' SECOND) "
941  "AND endtime <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ";
942  orderby = "starttime ASC";
943  break;
945  msg = "Adding LiveTV programs in starttime order";
946  where = QString("recgroup = 'LiveTV' "
947  "AND endtime <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
948  .arg(gCoreContext->GetNumSetting("AutoExpireLiveTVMaxAge", 1));
949  orderby = "starttime ASC";
950  break;
952  if ((maxAge = gCoreContext->GetNumSetting("DeletedMaxAge", 0)) <= 0)
953  return;
954  msg = QString("Adding programs deleted more than %1 days ago")
955  .arg(maxAge);
956  where = QString("recgroup = 'Deleted' "
957  "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
958  .arg(maxAge);
959  orderby = "starttime ASC";
960  break;
962  if (gCoreContext->GetNumSetting("DeletedMaxAge", 0) != 0)
963  return;
964  msg = QString("Adding programs deleted more than 5 minutes ago");
965  where = QString("recgroup = 'Deleted' "
966  "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ");
967  orderby = "lastmodified ASC";
968  break;
970  msg = "Adding deleted programs in FIFO order";
971  where = "recgroup = 'Deleted'";
972  orderby = "lastmodified ASC";
973  break;
974  }
975 
976  LOG(VB_FILE, LOG_INFO, LOC + "FillDBOrdered: " + msg);
977 
978  MSqlQuery query(MSqlQuery::InitCon());
979  QString querystr = QString(
980  "SELECT recorded.chanid, starttime "
981  "FROM recorded "
982  "LEFT JOIN channel ON recorded.chanid = channel.chanid "
983  "WHERE %1 AND deletepending = 0 "
984  "ORDER BY autoexpire DESC, %2").arg(where).arg(orderby);
985 
986  query.prepare(querystr);
987 
988  if (!query.exec())
989  return;
990 
991  while (query.next())
992  {
993  uint chanid = query.value(0).toUInt();
994  QDateTime recstartts = MythDate::as_utc(query.value(1).toDateTime());
995 
996  if (IsInDontExpireSet(chanid, recstartts))
997  {
998  LOG(VB_FILE, LOG_INFO, LOC +
999  QString(" Skipping %1 at %2 because it is in Don't Expire "
1000  "List")
1001  .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1002  }
1003  else if (IsInExpireList(expireList, chanid, recstartts))
1004  {
1005  LOG(VB_FILE, LOG_INFO, LOC +
1006  QString(" Skipping %1 at %2 because it is already in Expire "
1007  "List")
1008  .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1009  }
1010  else
1011  {
1012  ProgramInfo *pginfo = new ProgramInfo(chanid, recstartts);
1013  if (pginfo->GetChanID())
1014  {
1015  LOG(VB_FILE, LOG_INFO, LOC + QString(" Adding %1 at %2")
1016  .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1017  expireList.push_back(pginfo);
1018  }
1019  else
1020  {
1021  LOG(VB_FILE, LOG_INFO, LOC +
1022  QString(" Skipping %1 at %2 "
1023  "because it could not be loaded from the DB")
1024  .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
1025  delete pginfo;
1026  }
1027  }
1028  }
1029 }
1030 
1042 void AutoExpire::Update(int encoder, int fsID, bool immediately)
1043 {
1044  if (!expirer)
1045  return;
1046 
1047  if (encoder > 0)
1048  {
1049  QString msg = QString("Cardid %1: is starting a recording on")
1050  .arg(encoder);
1051  if (fsID == -1)
1052  msg.append(" an unknown fsID soon.");
1053  else
1054  msg.append(QString(" fsID %2 soon.").arg(fsID));
1055  LOG(VB_FILE, LOG_INFO, LOC + msg);
1056  }
1057 
1058  if (immediately)
1059  {
1060  if (encoder > 0)
1061  {
1062  expirer->m_instance_lock.lock();
1063  expirer->m_used_encoders[encoder] = fsID;
1064  expirer->m_instance_lock.unlock();
1065  }
1066  expirer->CalcParams();
1067  expirer->m_instance_cond.wakeAll();
1068  }
1069  else
1070  {
1071  expirer->m_update_lock.lock();
1072  expirer->m_update_queue.append(UpdateEntry(encoder, fsID));
1073  expirer->m_update_lock.unlock();
1074  }
1075 }
1076 
1078 {
1079  m_dont_expire_set.clear();
1080 
1081  MSqlQuery query(MSqlQuery::InitCon());
1082  query.prepare(
1083  "SELECT chanid, starttime, lastupdatetime, recusage, hostname "
1084  "FROM inuseprograms");
1085 
1086  if (!query.exec() || !query.next())
1087  return;
1088 
1089  LOG(VB_FILE, LOG_INFO, LOC + "Adding Programs to 'Do Not Expire' List");
1090  QDateTime curTime = MythDate::current();
1091 
1092  do
1093  {
1094  uint chanid = query.value(0).toUInt();
1095  QDateTime recstartts = MythDate::as_utc(query.value(1).toDateTime());
1096  QDateTime lastupdate = MythDate::as_utc(query.value(2).toDateTime());
1097 
1098  if (lastupdate.secsTo(curTime) < 2 * 60 * 60)
1099  {
1100  QString key = QString("%1_%2")
1101  .arg(chanid).arg(recstartts.toString(Qt::ISODate));
1102  m_dont_expire_set.insert(key);
1103  LOG(VB_FILE, LOG_INFO, QString(" %1 at %2 in use by %3 on %4")
1104  .arg(chanid)
1105  .arg(recstartts.toString(Qt::ISODate))
1106  .arg(query.value(3).toString())
1107  .arg(query.value(4).toString()));
1108  }
1109  }
1110  while (query.next());
1111 }
1112 
1114  uint chanid, const QDateTime &recstartts) const
1115 {
1116  QString key = QString("%1_%2")
1117  .arg(chanid).arg(recstartts.toString(Qt::ISODate));
1118 
1119  return (m_dont_expire_set.find(key) != m_dont_expire_set.end());
1120 }
1121 
1123  const pginfolist_t &expireList, uint chanid, const QDateTime &recstartts)
1124 {
1125  pginfolist_t::const_iterator it;
1126 
1127  for (it = expireList.begin(); it != expireList.end(); ++it)
1128  {
1129  if (((*it)->GetChanID() == chanid) &&
1130  ((*it)->GetRecordingStartTime() == recstartts))
1131  {
1132  return true;
1133  }
1134  }
1135  return false;
1136 }
1137 
1138 /* vim: set expandtab tabstop=4 shiftwidth=4: */
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:782
void start(QThread::Priority=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:294
~AutoExpire()
AutoExpire destructor stops auto delete thread if it is running.
Definition: autoexpire.cpp:84
void FillDBOrdered(pginfolist_t &expireList, int expMethod)
Creates a list of programs to delete using the database to order list.
Definition: autoexpire.cpp:904
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:863
bool m_expire_thread_run
Definition: autoexpire.h:115
VERBOSE_PREAMBLE Most true
Definition: verbosedefs.h:91
virtual uint64_t GetFilesize(void) const
void ExpireLiveTV(int type)
This expires LiveTV programs.
Definition: autoexpire.cpp:363
void SetPathname(const QString &) const
#define LOC
Definition: autoexpire.cpp:45
void removeListener(QObject *listener)
Remove a listener to the observable.
QMap< int, EncoderLink * > * m_encoderList
Definition: autoexpire.h:89
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:125
bool wait(unsigned long time=ULONG_MAX)
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:311
void ExpireRecordings(void)
This expires normal recordings.
Definition: autoexpire.cpp:403
void ExpireQuickDeleted(void)
This expires deleted programs within a few minutes.
Definition: autoexpire.cpp:389
QSet< QString > m_dont_expire_set
Definition: autoexpire.h:112
QString GetTitle(void) const
Definition: programinfo.h:353
int size(void) const
Definition: mythdbcon.h:203
AutoExpire()=default
Holds information on a TV Program one might wish to record.
Definition: recordinginfo.h:34
QString toString(Verbosity v=kLongDescription, const QString &sep=":", const QString &grp="\"") const
bool IsInDontExpireSet(uint chanid, const QDateTime &recstartts) const
unsigned int uint
Definition: compat.h:140
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
QMutex m_update_lock
Definition: autoexpire.h:126
void addListener(QObject *listener)
Add a listener to the observable.
int GetRecordingPriority(void) const
Definition: programinfo.h:432
QWaitCondition m_instance_cond
Definition: autoexpire.h:121
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
Definition: mythdate.cpp:23
void CalcParams(void)
Calculates how much space needs to be cleared, and how often.
Definition: autoexpire.cpp:122
void SendDeleteMessages(pginfolist_t &deleteList)
This sends delete message to main event thread.
Definition: autoexpire.cpp:616
QQueue< UpdateEntry > m_update_queue
Definition: autoexpire.h:127
QMap< int, int64_t > m_desired_space
Definition: autoexpire.h:117
QVariant value(int i) const
Definition: mythdbcon.h:198
Holds information on recordings and videos.
Definition: programinfo.h:66
static QReadWriteLock s_inputsLock
Definition: tv_rec.h:433
This class is used as a container for messages.
Definition: mythevent.h:16
bool IsWatched(void) const
Definition: programinfo.h:473
void GetFilesystemInfos(QList< FileSystemInfo > &fsInfos, bool useCache=true)
void Sleep(int sleepTime)
Sleeps for sleepTime milliseconds; unless the expire thread is told to quit.
Definition: autoexpire.cpp:346
void ForgetHistory(void)
Forget the recording of a program so it will be recorded again.
#define VERBOSE_LEVEL_CHECK(_MASK_, _LEVEL_)
Definition: mythlogging.h:24
int m_encoder
Definition: autoexpire.h:55
#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
void ExpireOldDeleted(void)
This expires deleted programs older than DeletedMaxAge.
Definition: autoexpire.cpp:376
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:10
uint64_t GetDesiredSpace(int fsID) const
Used by the scheduler to select the next recording dir.
Definition: autoexpire.cpp:111
bool isActive(void) const
Definition: mythdbcon.h:204
bool IsLocal(void) const
Definition: programinfo.h:343
ExpireThread * m_expire_thread
Definition: autoexpire.h:113
static MSqlQueryInfo InitCon(ConnectionReuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:535
void GetAllExpiring(QStringList &strList)
Gets the full list of programs that can expire in expiration order.
Definition: autoexpire.cpp:837
uint m_desired_freq
Definition: autoexpire.h:114
void dispatch(const MythEvent &event)
int GetNumSetting(const QString &key, int defaultval=0)
void UpdateDontExpireSet(void)
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:807
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
QString GetFilePrefix(void)
bool GetBoolSetting(const QString &key, bool defaultval=false)
Used to expire recordings to make space for new recordings.
Definition: autoexpire.h:61
void SetHostname(const QString &host)
Definition: programinfo.h:514
static bool IsInExpireList(const pginfolist_t &expireList, uint chanid, const QDateTime &recstartts)
uint GetChanID(void) const
This is the unique key used in the database to locate tuning information.
Definition: programinfo.h:364
AutoExpire * expirer
QString GetPlaybackURL(ProgramInfo *pginfo, bool storePath)
static void Update(int encoder, int fsID, bool immediately)
This is used to update the global AutoExpire instance "expirer".
void run(void) override
This calls AutoExpire::RunExpirer() from within a new thread.
Definition: autoexpire.cpp:56
QMap< int, int > m_used_encoders
Definition: autoexpire.h:118
void PrintExpireList(const QString &expHost="ALL")
Prints a summary of the files that can be deleted.
Definition: autoexpire.cpp:795
QDateTime GetRecordingStartTime(void) const
Approximate time the recording started.
Definition: programinfo.h:396
void ClearExpireList(pginfolist_t &expireList, bool deleteProg=true)
Clears expireList, freeing any ProgramInfo's if necessary.
Definition: autoexpire.cpp:885
QString GetHostname(void) const
Definition: programinfo.h:413
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
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:603
vector< ProgramInfo * > pginfolist_t
Definition: autoexpire.h:23
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:179
void ExpireEpisodesOverMax(void)
This deletes programs exceeding the maximum number of episodes of that program desired.
Definition: autoexpire.cpp:653
void RunExpirer(void)
This contains the main loop for the auto expire process.
Definition: autoexpire.cpp:270
QString GetHostName(void)
QString GetPathname(void) const
Definition: programinfo.h:335
QMap< int, EncoderLink * > tvList
Default UTC.
Definition: mythdate.h:14
QMutex m_instance_lock
Definition: autoexpire.h:120
MainServer * m_main_server
Definition: autoexpire.h:123