00001
00002 #include <sys/stat.h>
00003 #ifdef __linux__
00004 # include <sys/vfs.h>
00005 #else // if !__linux__
00006 # include <sys/param.h>
00007 # ifndef USING_MINGW
00008 # include <sys/mount.h>
00009 # endif // USING_MINGW
00010 #endif // !__linux__
00011
00012
00013 #include <unistd.h>
00014
00015
00016 #include <cstdlib>
00017
00018
00019 #include <iostream>
00020 #include <algorithm>
00021 using namespace std;
00022
00023
00024 #include <QDateTime>
00025 #include <QFileInfo>
00026 #include <QList>
00027
00028
00029 #include "filesysteminfo.h"
00030 #include "autoexpire.h"
00031 #include "programinfo.h"
00032 #include "mythcorecontext.h"
00033 #include "mythdb.h"
00034 #include "mythmiscutil.h"
00035 #include "storagegroup.h"
00036 #include "remoteutil.h"
00037 #include "remoteencoder.h"
00038 #include "encoderlink.h"
00039 #include "backendutil.h"
00040 #include "mainserver.h"
00041 #include "compat.h"
00042 #include "mythlogging.h"
00043
00044 #define LOC QString("AutoExpire: ")
00045 #define LOC_ERR QString("AutoExpire Error: ")
00046
00047 extern AutoExpire *expirer;
00048
00052 #define SPACE_TOO_BIG_KB 3*1024*1024
00053
00055 void ExpireThread::run(void)
00056 {
00057 RunProlog();
00058 m_parent->RunExpirer();
00059 RunEpilog();
00060 }
00061
00063 void UpdateThread::run(void)
00064 {
00065 RunProlog();
00066 m_parent->RunUpdate();
00067 RunEpilog();
00068 }
00069
00079 AutoExpire::AutoExpire(QMap<int, EncoderLink *> *tvList) :
00080 encoderList(tvList),
00081 expire_thread(new ExpireThread(this)),
00082 desired_freq(15),
00083 expire_thread_run(true),
00084 main_server(NULL),
00085 update_pending(false),
00086 update_thread(NULL)
00087 {
00088 expire_thread->start();
00089 gCoreContext->addListener(this);
00090 }
00091
00095 AutoExpire::AutoExpire() :
00096 encoderList(NULL),
00097 expire_thread(NULL),
00098 desired_freq(15),
00099 expire_thread_run(false),
00100 main_server(NULL),
00101 update_pending(false),
00102 update_thread(NULL)
00103 {
00104 }
00105
00109 AutoExpire::~AutoExpire()
00110 {
00111 {
00112 QMutexLocker locker(&instance_lock);
00113 expire_thread_run = false;
00114 instance_cond.wakeAll();
00115 }
00116
00117 {
00118 QMutexLocker locker(&instance_lock);
00119 while (update_pending)
00120 instance_cond.wait(&instance_lock);
00121 }
00122
00123 if (expire_thread)
00124 {
00125 gCoreContext->removeListener(this);
00126 expire_thread->wait();
00127 delete expire_thread;
00128 expire_thread = NULL;
00129 }
00130 }
00131
00137 uint64_t AutoExpire::GetDesiredSpace(int fsID) const
00138 {
00139 QMutexLocker locker(&instance_lock);
00140 if (desired_space.contains(fsID))
00141 return desired_space[fsID];
00142 return 0;
00143 }
00144
00148 void AutoExpire::CalcParams()
00149 {
00150 LOG(VB_FILE, LOG_INFO, LOC + "CalcParams()");
00151
00152 QList<FileSystemInfo> fsInfos;
00153
00154 instance_lock.lock();
00155 if (main_server)
00156 main_server->GetFilesystemInfos(fsInfos);
00157 instance_lock.unlock();
00158
00159 if (fsInfos.empty())
00160 {
00161 LOG(VB_GENERAL, LOG_ERR, LOC + "Filesystem Info cache is empty, unable " "to calculate necessary parameters.");
00162
00163 return;
00164 }
00165
00166 uint64_t maxKBperMin = 0;
00167 uint64_t extraKB = gCoreContext->GetNumSetting("AutoExpireExtraSpace", 0) <<
00168 20;
00169
00170 QMap<int, uint64_t> fsMap;
00171 QMap<int, vector<int> > fsEncoderMap;
00172
00173
00174
00175
00176 instance_lock.lock();
00177 QMap<int, int>::const_iterator ueit = used_encoders.begin();
00178 while (ueit != used_encoders.end())
00179 {
00180 fsEncoderMap[*ueit].push_back(ueit.key());
00181 ++ueit;
00182 }
00183 instance_lock.unlock();
00184
00185 QList<FileSystemInfo>::iterator fsit;
00186 for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
00187 {
00188 if (fsMap.contains(fsit->getFSysID()))
00189 continue;
00190
00191 fsMap[fsit->getFSysID()] = 0;
00192 uint64_t thisKBperMin = 0;
00193
00194
00195 vector<int>::iterator unknownfs_it = fsEncoderMap[-1].begin();
00196 for (; unknownfs_it != fsEncoderMap[-1].end(); ++unknownfs_it)
00197 fsEncoderMap[fsit->getFSysID()].push_back(*unknownfs_it);
00198
00199 if (fsEncoderMap.contains(fsit->getFSysID()))
00200 {
00201 LOG(VB_FILE, LOG_INFO,
00202 QString("fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
00203 .arg(fsit->getFSysID())
00204 .arg(fsit->getTotalSpace() / 1024.0 / 1024.0, 7, 'f', 1)
00205 .arg(fsit->getUsedSpace() / 1024.0 / 1024.0, 7, 'f', 1)
00206 .arg(fsit->getFreeSpace() / 1024.0 / 1024.0, 7, 'f', 1));
00207
00208 vector<int>::iterator encit =
00209 fsEncoderMap[fsit->getFSysID()].begin();
00210 for (; encit != fsEncoderMap[fsit->getFSysID()].end(); ++encit)
00211 {
00212 EncoderLink *enc = *(encoderList->find(*encit));
00213
00214 if (!enc->IsConnected() || !enc->IsBusy())
00215 {
00216
00217 LOG(VB_FILE, LOG_INFO, LOC +
00218 QString("Cardid %1: is not recoding, removing it "
00219 "from used list.").arg(*encit));
00220 instance_lock.lock();
00221 used_encoders.remove(*encit);
00222 instance_lock.unlock();
00223 continue;
00224 }
00225
00226 uint64_t maxBitrate = enc->GetMaxBitrate();
00227 if (maxBitrate<=0)
00228 maxBitrate = 19500000LL;
00229 thisKBperMin += (((uint64_t)maxBitrate)*((uint64_t)15))>>11;
00230 LOG(VB_FILE, LOG_INFO, QString(" Cardid %1: max bitrate "
00231 "%2 Kb/sec, fsID %3 max is now %4 KB/min")
00232 .arg(enc->GetCardID())
00233 .arg(enc->GetMaxBitrate() >> 10)
00234 .arg(fsit->getFSysID())
00235 .arg(thisKBperMin));
00236 }
00237 }
00238 fsMap[fsit->getFSysID()] = thisKBperMin;
00239
00240 if (thisKBperMin > maxKBperMin)
00241 {
00242 LOG(VB_FILE, LOG_INFO,
00243 QString(" Max of %1 KB/min for fsID %2 is higher "
00244 "than the existing Max of %3 so we'll use this Max instead")
00245 .arg(thisKBperMin).arg(fsit->getFSysID()).arg(maxKBperMin));
00246 maxKBperMin = thisKBperMin;
00247 }
00248 }
00249
00250
00251
00252 uint expireFreq = 15;
00253 if (maxKBperMin > 0)
00254 {
00255 expireFreq = SPACE_TOO_BIG_KB / (maxKBperMin + maxKBperMin/3);
00256 expireFreq = max(3U, min(expireFreq, 15U));
00257 }
00258
00259 double expireMinGB = ((maxKBperMin + maxKBperMin/3)
00260 * expireFreq + extraKB) >> 20;
00261 LOG(VB_GENERAL, LOG_NOTICE, LOC +
00262 QString("CalcParams(): Max required Free Space: %1 GB w/freq: %2 min")
00263 .arg(expireMinGB, 0, 'f', 1).arg(expireFreq));
00264
00265
00266 instance_lock.lock();
00267 desired_freq = expireFreq;
00268
00269 QMap<int, uint64_t>::iterator it = fsMap.begin();
00270 while (it != fsMap.end())
00271 {
00272 desired_space[it.key()] = (*it + *it/3) * expireFreq + extraKB;
00273 ++it;
00274 }
00275 instance_lock.unlock();
00276 }
00277
00286 void AutoExpire::RunExpirer(void)
00287 {
00288 QTime timer;
00289 QDateTime curTime;
00290 QDateTime next_expire = QDateTime::currentDateTime().addSecs(60);
00291
00292 QMutexLocker locker(&instance_lock);
00293
00294
00295 Sleep(20 * 1000);
00296
00297 timer.start();
00298
00299 while (expire_thread_run)
00300 {
00301 curTime = QDateTime::currentDateTime();
00302
00303 if (curTime >= next_expire)
00304 {
00305 locker.unlock();
00306 CalcParams();
00307 locker.relock();
00308 if (!expire_thread_run)
00309 break;
00310 }
00311 timer.restart();
00312
00313 UpdateDontExpireSet();
00314
00315
00316 if ((curTime.time().minute() % 2) == 0)
00317 ExpireLiveTV(emShortLiveTVPrograms);
00318
00319
00320 if (curTime >= next_expire)
00321 {
00322 LOG(VB_FILE, LOG_INFO, LOC + "Running now!");
00323 next_expire =
00324 QDateTime::currentDateTime().addSecs(desired_freq * 60);
00325
00326 ExpireLiveTV(emNormalLiveTVPrograms);
00327
00328 int maxAge = gCoreContext->GetNumSetting("DeletedMaxAge", 0);
00329 if (maxAge > 0)
00330 ExpireOldDeleted();
00331 else if (maxAge == 0)
00332 ExpireQuickDeleted();
00333
00334 ExpireEpisodesOverMax();
00335
00336 ExpireRecordings();
00337 }
00338
00339 Sleep(60 * 1000 - timer.elapsed());
00340 }
00341 }
00342
00349 void AutoExpire::Sleep(int sleepTime)
00350 {
00351 if (sleepTime <= 0)
00352 return;
00353
00354 QDateTime little_tm = QDateTime::currentDateTime().addMSecs(sleepTime);
00355 int timeleft = sleepTime;
00356 while (expire_thread_run && (timeleft > 0))
00357 {
00358 instance_cond.wait(&instance_lock, timeleft);
00359 timeleft = QDateTime::currentDateTime().secsTo(little_tm) * 1000;
00360 }
00361 }
00362
00366 void AutoExpire::ExpireLiveTV(int type)
00367 {
00368 pginfolist_t expireList;
00369
00370 LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireLiveTV(%1)").arg(type));
00371 FillDBOrdered(expireList, type);
00372 SendDeleteMessages(expireList);
00373 ClearExpireList(expireList);
00374 }
00375
00379 void AutoExpire::ExpireOldDeleted(void)
00380 {
00381 pginfolist_t expireList;
00382
00383 LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireOldDeleted()"));
00384 FillDBOrdered(expireList, emOldDeletedPrograms);
00385 SendDeleteMessages(expireList);
00386 ClearExpireList(expireList);
00387 }
00388
00392 void AutoExpire::ExpireQuickDeleted(void)
00393 {
00394 pginfolist_t expireList;
00395
00396 LOG(VB_FILE, LOG_INFO, LOC + QString("ExpireQuickDeleted()"));
00397 FillDBOrdered(expireList, emQuickDeletedPrograms);
00398 SendDeleteMessages(expireList);
00399 ClearExpireList(expireList);
00400 }
00401
00406 void AutoExpire::ExpireRecordings(void)
00407 {
00408 pginfolist_t expireList;
00409 pginfolist_t deleteList;
00410 QList<FileSystemInfo> fsInfos;
00411 QList<FileSystemInfo>::iterator fsit;
00412
00413 LOG(VB_FILE, LOG_INFO, LOC + "ExpireRecordings()");
00414
00415 if (main_server)
00416 main_server->GetFilesystemInfos(fsInfos);
00417
00418 if (fsInfos.empty())
00419 {
00420 LOG(VB_GENERAL, LOG_ERR, LOC + "Filesystem Info cache is empty, unable "
00421 "to determine what Recordings to expire");
00422
00423 return;
00424 }
00425
00426 FillExpireList(expireList);
00427
00428 QMap <int, bool> truncateMap;
00429 MSqlQuery query(MSqlQuery::InitCon());
00430 query.prepare("SELECT DISTINCT rechost, recdir "
00431 "FROM inuseprograms "
00432 "WHERE recusage = 'truncatingdelete' "
00433 "AND lastupdatetime > DATE_ADD(NOW(), INTERVAL -2 MINUTE);");
00434
00435 if (!query.exec())
00436 {
00437 MythDB::DBError(LOC + "ExpireRecordings", query);
00438 }
00439 else
00440 {
00441 while (query.next())
00442 {
00443 QString rechost = query.value(0).toString();
00444 QString recdir = query.value(1).toString();
00445
00446 LOG(VB_FILE, LOG_INFO, LOC +
00447 QString("%1:%2 has an in-progress truncating delete.")
00448 .arg(rechost).arg(recdir));
00449
00450 for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
00451 {
00452 if ((fsit->getHostname() == rechost) &&
00453 (fsit->getPath() == recdir))
00454 {
00455 truncateMap[fsit->getFSysID()] = true;
00456 break;
00457 }
00458 }
00459 }
00460 }
00461
00462 QMap <int, bool> fsMap;
00463 for (fsit = fsInfos.begin(); fsit != fsInfos.end(); ++fsit)
00464 {
00465 if (fsMap.contains(fsit->getFSysID()))
00466 continue;
00467
00468 fsMap[fsit->getFSysID()] = true;
00469
00470 LOG(VB_FILE, LOG_INFO,
00471 QString("fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
00472 .arg(fsit->getFSysID())
00473 .arg(fsit->getTotalSpace() / 1024.0 / 1024.0, 7, 'f', 1)
00474 .arg(fsit->getUsedSpace() / 1024.0 / 1024.0, 7, 'f', 1)
00475 .arg(fsit->getFreeSpace() / 1024.0 / 1024.0, 7, 'f', 1));
00476
00477 if ((fsit->getTotalSpace() == -1) || (fsit->getUsedSpace() == -1))
00478 {
00479 LOG(VB_FILE, LOG_ERR, LOC +
00480 QString("fsID #%1 has invalid info, AutoExpire cannot run for "
00481 "this filesystem. Continuing on to next...")
00482 .arg(fsit->getFSysID()));
00483 LOG(VB_FILE, LOG_INFO, QString("Directories on filesystem ID %1:")
00484 .arg(fsit->getFSysID()));
00485 QList<FileSystemInfo>::iterator fsit2;
00486 for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); ++fsit2)
00487 {
00488 if (fsit2->getFSysID() == fsit->getFSysID())
00489 {
00490 LOG(VB_FILE, LOG_INFO, QString(" %1:%2")
00491 .arg(fsit2->getHostname()).arg(fsit2->getPath()));
00492 }
00493 }
00494
00495 continue;
00496 }
00497
00498 if (truncateMap.contains(fsit->getFSysID()))
00499 {
00500 LOG(VB_FILE, LOG_INFO,
00501 QString(" fsid %1 has a truncating delete in progress, "
00502 "AutoExpire cannot run for this filesystem until the "
00503 "delete has finished. Continuing on to next...")
00504 .arg(fsit->getFSysID()));
00505 continue;
00506 }
00507
00508 if (max((int64_t)0LL, fsit->getFreeSpace()) <
00509 desired_space[fsit->getFSysID()])
00510 {
00511 LOG(VB_FILE, LOG_INFO,
00512 QString(" Not Enough Free Space! We want %1 MB")
00513 .arg(desired_space[fsit->getFSysID()] / 1024));
00514
00515 QMap<QString, int> dirList;
00516 QList<FileSystemInfo>::iterator fsit2;
00517
00518 LOG(VB_FILE, LOG_INFO,
00519 QString(" Directories on filesystem ID %1:")
00520 .arg(fsit->getFSysID()));
00521
00522 for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); ++fsit2)
00523 {
00524 if (fsit2->getFSysID() == fsit->getFSysID())
00525 {
00526 LOG(VB_FILE, LOG_INFO, QString(" %1:%2")
00527 .arg(fsit2->getHostname()).arg(fsit2->getPath()));
00528 dirList[fsit2->getHostname() + ":" + fsit2->getPath()] = 1;
00529 }
00530 }
00531
00532 LOG(VB_FILE, LOG_INFO,
00533 " Searching for files expirable in these directories");
00534 QString myHostName = gCoreContext->GetHostName();
00535 pginfolist_t::iterator it = expireList.begin();
00536 while ((it != expireList.end()) &&
00537 (max((int64_t)0LL, fsit->getFreeSpace()) <
00538 desired_space[fsit->getFSysID()]))
00539 {
00540 ProgramInfo *p = *it;
00541 ++it;
00542
00543 LOG(VB_FILE, LOG_INFO, QString(" Checking %1 => %2")
00544 .arg(p->toString(ProgramInfo::kRecordingKey))
00545 .arg(p->GetTitle()));
00546
00547 if (!p->IsLocal())
00548 {
00549 bool foundFile = false;
00550 QMap<int, EncoderLink *>::Iterator eit =
00551 encoderList->begin();
00552 while (eit != encoderList->end())
00553 {
00554 EncoderLink *el = *eit;
00555 eit++;
00556
00557 if ((p->GetHostname() == el->GetHostName()) ||
00558 ((p->GetHostname() == myHostName) &&
00559 (el->IsLocal())))
00560 {
00561 if (el->IsConnected())
00562 foundFile = el->CheckFile(p);
00563
00564 eit = encoderList->end();
00565 }
00566 }
00567
00568 if (!foundFile && (p->GetHostname() != myHostName))
00569 {
00570
00571 QString file = GetPlaybackURL(p);
00572
00573 if (file.left(1) == "/")
00574 {
00575 p->SetPathname(file);
00576 p->SetHostname(myHostName);
00577 foundFile = true;
00578 }
00579 }
00580
00581 if (!foundFile)
00582 {
00583 LOG(VB_FILE, LOG_ERR, LOC +
00584 QString(" ERROR: Can't find file for %1")
00585 .arg(p->toString(ProgramInfo::kRecordingKey)));
00586 continue;
00587 }
00588 }
00589
00590 QFileInfo vidFile(p->GetPathname());
00591 if (dirList.contains(p->GetHostname() + ':' + vidFile.path()))
00592 {
00593 fsit->setUsedSpace(fsit->getUsedSpace()
00594 - (p->GetFilesize() / 1024));
00595 deleteList.push_back(p);
00596
00597 LOG(VB_FILE, LOG_INFO,
00598 QString(" FOUND file expirable. "
00599 "%1 is located at %2 which is on fsID #%3. "
00600 "Adding to deleteList. After deleting we "
00601 "should have %4 MB free on this filesystem.")
00602 .arg(p->toString(ProgramInfo::kRecordingKey))
00603 .arg(p->GetPathname()).arg(fsit->getFSysID())
00604 .arg(fsit->getFreeSpace() / 1024));
00605 }
00606 }
00607 }
00608 }
00609
00610 SendDeleteMessages(deleteList);
00611
00612 ClearExpireList(deleteList, false);
00613 ClearExpireList(expireList);
00614 }
00615
00619 void AutoExpire::SendDeleteMessages(pginfolist_t &deleteList)
00620 {
00621 QString msg;
00622
00623 if (deleteList.empty())
00624 {
00625 LOG(VB_FILE, LOG_INFO, LOC + "SendDeleteMessages. Nothing to expire.");
00626 return;
00627 }
00628
00629 LOG(VB_FILE, LOG_INFO, LOC +
00630 "SendDeleteMessages, cycling through deleteList.");
00631 pginfolist_t::iterator it = deleteList.begin();
00632 while (it != deleteList.end())
00633 {
00634 msg = QString("%1Expiring %2 MB for %3 => %4")
00635 .arg(VERBOSE_LEVEL_CHECK(VB_FILE, LOG_ANY) ? " " : "")
00636 .arg(((*it)->GetFilesize() >> 20))
00637 .arg((*it)->toString(ProgramInfo::kRecordingKey))
00638 .arg((*it)->toString(ProgramInfo::kTitleSubtitle));
00639
00640 LOG(VB_GENERAL, LOG_NOTICE, msg);
00641
00642
00643 MythEvent me(QString("AUTO_EXPIRE %1 %2").arg((*it)->GetChanID())
00644 .arg((*it)->GetRecordingStartTime(ISODate)));
00645 gCoreContext->dispatch(me);
00646
00647 deleted_set.insert((*it)->MakeUniqueKey());
00648
00649 ++it;
00650 }
00651 }
00652
00658 void AutoExpire::ExpireEpisodesOverMax(void)
00659 {
00660 QMap<QString, int> maxEpisodes;
00661 QMap<QString, int>::Iterator maxIter;
00662 QMap<QString, int> episodeParts;
00663 QString episodeKey;
00664
00665 QString fileprefix = gCoreContext->GetFilePrefix();
00666
00667 MSqlQuery query(MSqlQuery::InitCon());
00668 query.prepare("SELECT recordid, maxepisodes, title "
00669 "FROM record WHERE maxepisodes > 0 "
00670 "ORDER BY recordid ASC, maxepisodes DESC");
00671
00672 if (query.exec() && query.isActive() && query.size() > 0)
00673 {
00674 LOG(VB_FILE, LOG_INFO, LOC +
00675 QString("Found %1 record profiles using max episode expiration")
00676 .arg(query.size()));
00677 while (query.next())
00678 {
00679 LOG(VB_FILE, LOG_INFO, QString(" %1 (%2 for rec id %3)")
00680 .arg(query.value(2).toString())
00681 .arg(query.value(1).toInt())
00682 .arg(query.value(0).toInt()));
00683 maxEpisodes[query.value(0).toString()] = query.value(1).toInt();
00684 }
00685 }
00686
00687 LOG(VB_FILE, LOG_INFO, LOC +
00688 "Checking episode count for each recording profile using max episodes");
00689 for (maxIter = maxEpisodes.begin(); maxIter != maxEpisodes.end(); ++maxIter)
00690 {
00691 query.prepare("SELECT chanid, starttime, title, progstart, progend, "
00692 "filesize, duplicate "
00693 "FROM recorded "
00694 "WHERE recordid = :RECID AND preserve = 0 "
00695 "AND recgroup NOT IN ('LiveTV', 'Deleted') "
00696 "ORDER BY starttime DESC;");
00697 query.bindValue(":RECID", maxIter.key());
00698
00699 if (!query.exec() || !query.isActive())
00700 {
00701 MythDB::DBError("AutoExpire query failed!", query);
00702 continue;
00703 }
00704
00705 LOG(VB_FILE, LOG_INFO, QString(" Recordid %1 has %2 recordings.")
00706 .arg(maxIter.key())
00707 .arg(query.size()));
00708 if (query.size() > 0)
00709 {
00710 int found = 1;
00711 while (query.next())
00712 {
00713 uint chanid = query.value(0).toUInt();
00714 QDateTime startts = query.value(1).toDateTime();
00715 QString title = query.value(2).toString();
00716 QDateTime progstart = query.value(3).toDateTime();
00717 QDateTime progend = query.value(4).toDateTime();
00718 int duplicate = query.value(6).toInt();
00719
00720 episodeKey = QString("%1_%2_%3")
00721 .arg(chanid)
00722 .arg(progstart.toString(Qt::ISODate))
00723 .arg(progend.toString(Qt::ISODate));
00724
00725 if ((!IsInDontExpireSet(chanid, startts)) &&
00726 (!episodeParts.contains(episodeKey)) &&
00727 (found > *maxIter))
00728 {
00729 uint64_t spaceFreed = query.value(5).toLongLong() >> 20;
00730 QString msg =
00731 QString("%1Expiring %2 MBytes for %3 at %4 => %5. "
00732 "Too many episodes, we only want to keep %6.")
00733 .arg(VERBOSE_LEVEL_CHECK(VB_FILE, LOG_ANY) ?
00734 " " : "")
00735 .arg(spaceFreed)
00736 .arg(chanid).arg(startts.toString())
00737 .arg(title).arg(*maxIter);
00738
00739 LOG(VB_GENERAL, LOG_NOTICE, msg);
00740
00741 msg = QString("AUTO_EXPIRE %1 %2")
00742 .arg(chanid)
00743 .arg(startts.toString(Qt::ISODate));
00744
00745 MythEvent me(msg);
00746 gCoreContext->dispatch(me);
00747 }
00748 else
00749 {
00750
00751
00752
00753 if (episodeParts.contains(episodeKey))
00754 {
00755 episodeParts[episodeKey] = episodeParts[episodeKey] + 1;
00756 }
00757 else
00758 {
00759 episodeParts[episodeKey] = 1;
00760 if( duplicate )
00761 found++;
00762 }
00763 }
00764 }
00765 }
00766 }
00767 }
00768
00773 void AutoExpire::FillExpireList(pginfolist_t &expireList)
00774 {
00775 int expMethod = gCoreContext->GetNumSetting("AutoExpireMethod", 1);
00776
00777 ClearExpireList(expireList);
00778
00779 FillDBOrdered(expireList, emNormalDeletedPrograms);
00780
00781 switch(expMethod)
00782 {
00783 case emOldestFirst:
00784 case emLowestPriorityFirst:
00785 case emWeightedTimePriority:
00786 FillDBOrdered(expireList, expMethod);
00787 break;
00788
00789 }
00790 }
00791
00795 void AutoExpire::PrintExpireList(QString expHost)
00796 {
00797 pginfolist_t expireList;
00798
00799 FillExpireList(expireList);
00800
00801 QString msg = "MythTV AutoExpire List ";
00802 if (expHost != "ALL")
00803 msg += QString("for '%1' ").arg(expHost);
00804 msg += "(programs listed in order of expiration)";
00805 cout << msg.toLocal8Bit().constData() << endl;
00806
00807 pginfolist_t::iterator i = expireList.begin();
00808 for (; i != expireList.end(); ++i)
00809 {
00810 ProgramInfo *first = (*i);
00811
00812 if (expHost != "ALL" && first->GetHostname() != expHost)
00813 continue;
00814
00815 QString title = first->toString(ProgramInfo::kTitleSubtitle);
00816 title = title.leftJustified(39, ' ', true);
00817
00818 QString outstr = QString("%1 %2 MB %3 [%4]")
00819 .arg(title)
00820 .arg(QString::number(first->GetFilesize() >> 20)
00821 .rightJustified(5, ' ', true))
00822 .arg(first->GetRecordingStartTime(ISODate)
00823 .leftJustified(24, ' ', true))
00824 .arg(QString::number(first->GetRecordingPriority())
00825 .rightJustified(3, ' ', true));
00826 QByteArray out = outstr.toLocal8Bit();
00827
00828 cout << out.constData() << endl;
00829 }
00830
00831 ClearExpireList(expireList);
00832 }
00833
00837 void AutoExpire::GetAllExpiring(QStringList &strList)
00838 {
00839 QMutexLocker lockit(&instance_lock);
00840 pginfolist_t expireList;
00841
00842 UpdateDontExpireSet();
00843
00844 FillDBOrdered(expireList, emShortLiveTVPrograms);
00845 FillDBOrdered(expireList, emNormalLiveTVPrograms);
00846 FillDBOrdered(expireList, emNormalDeletedPrograms);
00847 FillDBOrdered(expireList, gCoreContext->GetNumSetting("AutoExpireMethod",
00848 emOldestFirst));
00849
00850 strList << QString::number(expireList.size());
00851
00852 pginfolist_t::iterator it = expireList.begin();
00853 for (; it != expireList.end(); ++it)
00854 (*it)->ToStringList(strList);
00855
00856 ClearExpireList(expireList);
00857 }
00858
00862 void AutoExpire::GetAllExpiring(pginfolist_t &list)
00863 {
00864 QMutexLocker lockit(&instance_lock);
00865 pginfolist_t expireList;
00866
00867 UpdateDontExpireSet();
00868
00869 FillDBOrdered(expireList, emShortLiveTVPrograms);
00870 FillDBOrdered(expireList, emNormalLiveTVPrograms);
00871 FillDBOrdered(expireList, emNormalDeletedPrograms);
00872 FillDBOrdered(expireList, gCoreContext->GetNumSetting("AutoExpireMethod",
00873 emOldestFirst));
00874
00875 pginfolist_t::iterator it = expireList.begin();
00876 for (; it != expireList.end(); ++it)
00877 list.push_back( new ProgramInfo( *(*it) ));
00878
00879 ClearExpireList(expireList);
00880 }
00881
00885 void AutoExpire::ClearExpireList(pginfolist_t &expireList, bool deleteProg)
00886 {
00887 ProgramInfo *pginfo = NULL;
00888 while (!expireList.empty())
00889 {
00890 if (deleteProg)
00891 pginfo = expireList.back();
00892
00893 expireList.pop_back();
00894
00895 if (deleteProg)
00896 delete pginfo;
00897 }
00898 }
00899
00904 void AutoExpire::FillDBOrdered(pginfolist_t &expireList, int expMethod)
00905 {
00906 QString where;
00907 QString orderby;
00908 QString msg;
00909 int maxAge;
00910
00911 switch (expMethod)
00912 {
00913 default:
00914 case emOldestFirst:
00915 msg = "Adding programs expirable in Oldest First order";
00916 where = "autoexpire > 0";
00917 if (gCoreContext->GetNumSetting("AutoExpireWatchedPriority", 0))
00918 orderby = "recorded.watched DESC, ";
00919 orderby += "starttime ASC";
00920 break;
00921 case emLowestPriorityFirst:
00922 msg = "Adding programs expirable in Lowest Priority First order";
00923 where = "autoexpire > 0";
00924 if (gCoreContext->GetNumSetting("AutoExpireWatchedPriority", 0))
00925 orderby = "recorded.watched DESC, ";
00926 orderby += "recorded.recpriority ASC, starttime ASC";
00927 break;
00928 case emWeightedTimePriority:
00929 msg = "Adding programs expirable in Weighted Time Priority order";
00930 where = "autoexpire > 0";
00931 if (gCoreContext->GetNumSetting("AutoExpireWatchedPriority", 0))
00932 orderby = "recorded.watched DESC, ";
00933 orderby += QString("DATE_ADD(starttime, INTERVAL '%1' * "
00934 "recorded.recpriority DAY) ASC")
00935 .arg(gCoreContext->GetNumSetting("AutoExpireDayPriority", 3));
00936 break;
00937 case emShortLiveTVPrograms:
00938 msg = "Adding Short LiveTV programs in starttime order";
00939 where = "recgroup = 'LiveTV' "
00940 "AND endtime < DATE_ADD(starttime, INTERVAL '2' MINUTE) "
00941 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-1' MINUTE) ";
00942 orderby = "starttime ASC";
00943 break;
00944 case emNormalLiveTVPrograms:
00945 msg = "Adding LiveTV programs in starttime order";
00946 where = QString("recgroup = 'LiveTV' "
00947 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
00948 .arg(gCoreContext->GetNumSetting("AutoExpireLiveTVMaxAge", 1));
00949 orderby = "starttime ASC";
00950 break;
00951 case emOldDeletedPrograms:
00952 if ((maxAge = gCoreContext->GetNumSetting("DeletedMaxAge", 0)) <= 0)
00953 return;
00954 msg = QString("Adding programs deleted more than %1 days ago")
00955 .arg(maxAge);
00956 where = QString("recgroup = 'Deleted' "
00957 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
00958 .arg(maxAge);
00959 orderby = "starttime ASC";
00960 break;
00961 case emQuickDeletedPrograms:
00962 if (gCoreContext->GetNumSetting("DeletedMaxAge", 0) != 0)
00963 return;
00964 msg = QString("Adding programs deleted more than 5 minutes ago");
00965 where = QString("recgroup = 'Deleted' "
00966 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-5' MINUTE) ");
00967 orderby = "lastmodified ASC";
00968 break;
00969 case emNormalDeletedPrograms:
00970 msg = "Adding deleted programs in FIFO order";
00971 where = "recgroup = 'Deleted'";
00972 orderby = "lastmodified ASC";
00973 break;
00974 }
00975
00976 LOG(VB_FILE, LOG_INFO, LOC + "FillDBOrdered: " + msg);
00977
00978 MSqlQuery query(MSqlQuery::InitCon());
00979 QString querystr = QString(
00980 "SELECT recorded.chanid, starttime "
00981 "FROM recorded "
00982 "LEFT JOIN channel ON recorded.chanid = channel.chanid "
00983 "WHERE %1 AND deletepending = 0 "
00984 "ORDER BY autoexpire DESC, %2").arg(where).arg(orderby);
00985
00986 query.prepare(querystr);
00987
00988 if (!query.exec())
00989 return;
00990
00991 while (query.next())
00992 {
00993 uint chanid = query.value(0).toUInt();
00994 QDateTime recstartts = query.value(1).toDateTime();
00995
00996 if (IsInDontExpireSet(chanid, recstartts))
00997 {
00998 LOG(VB_FILE, LOG_INFO, LOC +
00999 QString(" Skipping %1 at %2 because it is in Don't Expire "
01000 "List")
01001 .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
01002 }
01003 else if (IsInExpireList(expireList, chanid, recstartts))
01004 {
01005 LOG(VB_FILE, LOG_INFO, LOC +
01006 QString(" Skipping %1 at %2 because it is already in Expire "
01007 "List")
01008 .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
01009 }
01010 else
01011 {
01012 ProgramInfo *pginfo = new ProgramInfo(chanid, recstartts);
01013 if (pginfo->GetChanID())
01014 {
01015 LOG(VB_FILE, LOG_INFO, LOC + QString(" Adding %1 at %2")
01016 .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
01017 expireList.push_back(pginfo);
01018 }
01019 else
01020 {
01021 LOG(VB_FILE, LOG_INFO, LOC +
01022 QString(" Skipping %1 at %2 "
01023 "because it could not be loaded from the DB")
01024 .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
01025 delete pginfo;
01026 }
01027 }
01028 }
01029 }
01030
01036 void AutoExpire::RunUpdate(void)
01037 {
01038 QMutexLocker locker(&instance_lock);
01039 Sleep(5 * 1000);
01040 locker.unlock();
01041 CalcParams();
01042 locker.relock();
01043 update_pending = false;
01044 update_thread->deleteLater();
01045 update_thread = NULL;
01046 instance_cond.wakeAll();
01047 }
01048
01060 void AutoExpire::Update(int encoder, int fsID, bool immediately)
01061 {
01062 if (!expirer)
01063 return;
01064
01065
01066 QMutexLocker locker(&expirer->instance_lock);
01067 while (expirer->update_pending)
01068 expirer->instance_cond.wait(&expirer->instance_lock);
01069 expirer->update_pending = true;
01070
01071 if (encoder > 0)
01072 {
01073 QString msg = QString("Cardid %1: is starting a recording on")
01074 .arg(encoder);
01075 if (fsID == -1)
01076 msg.append(" an unknown fsID soon.");
01077 else
01078 msg.append(QString(" fsID %2 soon.").arg(fsID));
01079
01080 LOG(VB_FILE, LOG_INFO, LOC + msg);
01081 expirer->used_encoders[encoder] = fsID;
01082 }
01083
01084
01085 if (immediately)
01086 {
01087 locker.unlock();
01088 expirer->CalcParams();
01089 locker.relock();
01090 expirer->update_pending = false;
01091 expirer->instance_cond.wakeAll();
01092 }
01093 else
01094 {
01095
01096 if (!expirer->update_thread)
01097 {
01098 expirer->update_thread = new UpdateThread(expirer);
01099 expirer->update_thread->start();
01100 }
01101 }
01102 }
01103
01104 void AutoExpire::UpdateDontExpireSet(void)
01105 {
01106 dont_expire_set = deleted_set;
01107
01108 MSqlQuery query(MSqlQuery::InitCon());
01109 query.prepare(
01110 "SELECT chanid, starttime, lastupdatetime, recusage, hostname "
01111 "FROM inuseprograms");
01112
01113 if (!query.exec() || !query.next())
01114 return;
01115
01116 LOG(VB_FILE, LOG_INFO, LOC + "Adding Programs to 'Do Not Expire' List");
01117 QDateTime curTime = QDateTime::currentDateTime();
01118
01119 do
01120 {
01121 uint chanid = query.value(0).toUInt();
01122 QDateTime recstartts = query.value(1).toDateTime();
01123 QDateTime lastupdate = query.value(2).toDateTime();
01124
01125 if (lastupdate.secsTo(curTime) < 2 * 60 * 60)
01126 {
01127 QString key = QString("%1_%2")
01128 .arg(chanid).arg(recstartts.toString(Qt::ISODate));
01129 dont_expire_set.insert(key);
01130 LOG(VB_FILE, LOG_INFO, QString(" %1 at %2 in use by %3 on %4")
01131 .arg(chanid)
01132 .arg(recstartts.toString(Qt::ISODate))
01133 .arg(query.value(3).toString())
01134 .arg(query.value(4).toString()));
01135 }
01136 }
01137 while (query.next());
01138 }
01139
01140 bool AutoExpire::IsInDontExpireSet(
01141 uint chanid, const QDateTime &recstartts) const
01142 {
01143 QString key = QString("%1_%2")
01144 .arg(chanid).arg(recstartts.toString(Qt::ISODate));
01145
01146 return (dont_expire_set.find(key) != dont_expire_set.end());
01147 }
01148
01149 bool AutoExpire::IsInExpireList(
01150 const pginfolist_t &expireList, uint chanid, const QDateTime &recstartts)
01151 {
01152 pginfolist_t::const_iterator it;
01153
01154 for (it = expireList.begin(); it != expireList.end(); ++it)
01155 {
01156 if (((*it)->GetChanID() == chanid) &&
01157 ((*it)->GetRecordingStartTime() == recstartts))
01158 {
01159 return true;
01160 }
01161 }
01162 return false;
01163 }
01164
01165