00001 #include <cstdlib>
00002 #include <unistd.h>
00003 #include <signal.h>
00004
00005
00006 #include <qsqldatabase.h>
00007 #include <qsqlquery.h>
00008 #include <qregexp.h>
00009 #include <qstring.h>
00010 #include <qdatetime.h>
00011 #include <qfileinfo.h>
00012
00013 #include <iostream>
00014 #include <algorithm>
00015 using namespace std;
00016
00017 #include <sys/stat.h>
00018 #ifdef __linux__
00019 # include <sys/vfs.h>
00020 #else // if !__linux__
00021 # include <sys/param.h>
00022 # ifndef USING_MINGW
00023 # include <sys/mount.h>
00024 # endif // USING_MINGW
00025 #endif // !__linux__
00026
00027 #include "autoexpire.h"
00028 #include "programinfo.h"
00029 #include "libmyth/mythcontext.h"
00030 #include "libmyth/mythdbcon.h"
00031 #include "libmyth/util.h"
00032 #include "libmyth/storagegroup.h"
00033 #include "libmythtv/remoteutil.h"
00034 #include "libmythtv/remoteencoder.h"
00035 #include "encoderlink.h"
00036 #include "backendutil.h"
00037 #include "compat.h"
00038
00039 #define LOC QString("AutoExpire: ")
00040 #define LOC_ERR QString("AutoExpire Error: ")
00041
00042 extern AutoExpire *expirer;
00043
00047 #define SPACE_TOO_BIG_KB 3*1024*1024
00048
00058 AutoExpire::AutoExpire(QMap<int, EncoderLink *> *tvList)
00059 {
00060 encoderList = tvList;
00061 expire_thread_running = true;
00062
00063 Init();
00064
00065 pthread_create(&expire_thread, NULL, ExpirerThread, this);
00066 gContext->addListener(this);
00067 }
00068
00072 AutoExpire::AutoExpire(void)
00073 {
00074 encoderList = NULL;
00075 expire_thread_running = false;
00076 Init();
00077 }
00078
00082 void AutoExpire::Init(void)
00083 {
00084 desired_freq = 15;
00085 update_pending = false;
00086 }
00087
00091 AutoExpire::~AutoExpire()
00092 {
00093 instance_lock.lock();
00094 while (update_pending)
00095 instance_cond.wait(&instance_lock);
00096 instance_lock.unlock();
00097
00098 if (expire_thread_running)
00099 {
00100 gContext->removeListener(this);
00101 expire_thread_running = false;
00102 pthread_kill(expire_thread, SIGALRM);
00103 VERBOSE(VB_IMPORTANT, LOC + "Warning: Stopping auto expire thread "
00104 "can take several seconds. Please be patient.");
00105 pthread_join(expire_thread, NULL);
00106 }
00107 }
00108
00114 size_t AutoExpire::GetDesiredSpace(int fsID) const
00115 {
00116 if (desired_space.contains(fsID))
00117 return desired_space[fsID];
00118 return 0;
00119 }
00120
00124 void AutoExpire::CalcParams()
00125 {
00126 VERBOSE(VB_FILE, LOC + "CalcParams()");
00127
00128 vector<FileSystemInfo> fsInfos;
00129 GetFilesystemInfos(encoderList, fsInfos);
00130
00131 if (fsInfos.size() == 0)
00132 {
00133 QString msg = "ERROR: Filesystem Info cache is empty, unable to "
00134 "calculate necessary parameters.";
00135 VERBOSE(VB_IMPORTANT, LOC + msg);
00136 gContext->LogEntry("mythbackend", LP_WARNING,
00137 "Autoexpire CalcParams", msg);
00138
00139 return;
00140 }
00141
00142 size_t maxKBperMin = 0;
00143 size_t extraKB = gContext->GetNumSetting("AutoExpireExtraSpace", 0) << 20;
00144
00145 QMap<int, uint64_t> fsMap;
00146 QMap<int, vector<int> > fsEncoderMap;
00147
00148
00149
00150
00151 instance_lock.lock();
00152 QMap<int, int>::const_iterator ueit = used_encoders.begin();
00153 while (ueit != used_encoders.end())
00154 {
00155 fsEncoderMap[ueit.data()].push_back(ueit.key());
00156 ++ueit;
00157 }
00158 instance_lock.unlock();
00159
00160 vector<FileSystemInfo>::iterator fsit;
00161 for (fsit = fsInfos.begin(); fsit != fsInfos.end(); fsit++)
00162 {
00163 if (fsMap.contains(fsit->fsID))
00164 continue;
00165
00166 fsMap[fsit->fsID] = 0;
00167 size_t thisKBperMin = 0;
00168
00169
00170 vector<int>::iterator unknownfs_it = fsEncoderMap[-1].begin();
00171 for (; unknownfs_it != fsEncoderMap[-1].end(); ++unknownfs_it)
00172 fsEncoderMap[fsit->fsID].push_back(*unknownfs_it);
00173
00174 if (fsEncoderMap.contains(fsit->fsID))
00175 {
00176 VERBOSE(VB_FILE, QString(
00177 "fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
00178 .arg(fsit->fsID)
00179 .arg(fsit->totalSpaceKB / 1024.0 / 1024.0, 7, 'f', 1)
00180 .arg(fsit->usedSpaceKB / 1024.0 / 1024.0, 7, 'f', 1)
00181 .arg(fsit->freeSpaceKB / 1024.0 / 1024.0, 7, 'f', 1));
00182
00183
00184 vector<int>::iterator encit = fsEncoderMap[fsit->fsID].begin();
00185 for (; encit != fsEncoderMap[fsit->fsID].end(); ++encit)
00186 {
00187 EncoderLink *enc = *(encoderList->find(*encit));
00188
00189 if (!enc->IsConnected() || !enc->IsBusy())
00190 {
00191
00192 VERBOSE(VB_FILE, LOC
00193 + QString("Cardid %1: is not recoding, removing it "
00194 "from used list.").arg(*encit));
00195 instance_lock.lock();
00196 used_encoders.erase(*encit);
00197 instance_lock.unlock();
00198 continue;
00199 }
00200
00201 long long maxBitrate = enc->GetMaxBitrate();
00202 if (maxBitrate<=0)
00203 maxBitrate = 19500000LL;
00204 thisKBperMin += (((size_t)maxBitrate)*((size_t)15))>>11;
00205 VERBOSE(VB_FILE, QString(" Cardid %1: max bitrate "
00206 "%2 Kb/sec, fsID %3 max is now %4 KB/min")
00207 .arg(enc->GetCardID())
00208 .arg(enc->GetMaxBitrate() >> 10)
00209 .arg(fsit->fsID)
00210 .arg(thisKBperMin));
00211 }
00212 }
00213 fsMap[fsit->fsID] = thisKBperMin;
00214
00215 if (thisKBperMin > maxKBperMin)
00216 {
00217 VERBOSE(VB_FILE,
00218 QString(" Max of %1 KB/min for fsID %2 is higher "
00219 "than the existing Max of %3 so we'll use this Max instead")
00220 .arg(thisKBperMin).arg(fsit->fsID).arg(maxKBperMin));
00221 maxKBperMin = thisKBperMin;
00222 }
00223 }
00224
00225
00226
00227 uint expireFreq = 15;
00228 if (maxKBperMin > 0)
00229 {
00230 expireFreq = SPACE_TOO_BIG_KB / (maxKBperMin + maxKBperMin/3);
00231 expireFreq = max(3U, min(expireFreq, 15U));
00232 }
00233
00234 double expireMinGB = ((maxKBperMin + maxKBperMin/3)
00235 * expireFreq + extraKB) >> 20;
00236 VERBOSE(VB_IMPORTANT, LOC +
00237 QString("CalcParams(): Max required Free Space: %2 GB w/freq: "
00238 "%2 min").arg(expireMinGB, 0, 'f', 1).arg(expireFreq));
00239
00240
00241 instance_lock.lock();
00242 desired_freq = expireFreq;
00243
00244 QMap<int, uint64_t>::iterator it = fsMap.begin();
00245 while (it != fsMap.end())
00246 {
00247 desired_space[it.key()] = (it.data() + it.data()/3) * expireFreq + extraKB;
00248 ++it;
00249 }
00250 instance_lock.unlock();
00251 }
00252
00262 void AutoExpire::RunExpirer(void)
00263 {
00264 QTime timer;
00265 QDateTime curTime;
00266 QDateTime next_expire = QDateTime::currentDateTime().addSecs(60);
00267
00268
00269 sleep(20);
00270
00271 timer.start();
00272
00273 while (expire_thread_running)
00274 {
00275 curTime = QDateTime::currentDateTime();
00276
00277 if (curTime >= next_expire)
00278 CalcParams();
00279
00280 timer.restart();
00281
00282 instance_lock.lock();
00283
00284 UpdateDontExpireSet();
00285
00286
00287 if ((curTime.time().minute() % 2) == 0)
00288 ExpireLiveTV(emShortLiveTVPrograms);
00289
00290
00291 if (curTime >= next_expire)
00292 {
00293 VERBOSE(VB_FILE, LOC + "Running now!");
00294 next_expire =
00295 QDateTime::currentDateTime().addSecs(desired_freq * 60);
00296
00297 ExpireLiveTV(emNormalLiveTVPrograms);
00298
00299 if (gContext->GetNumSetting("DeletedMaxAge", 0))
00300 ExpireOldDeleted();
00301
00302 ExpireEpisodesOverMax();
00303
00304 ExpireRecordings();
00305 }
00306
00307 instance_lock.unlock();
00308
00309 Sleep(60 - (timer.elapsed() / 1000));
00310 }
00311 }
00312
00317 void AutoExpire::Sleep(int sleepTime)
00318 {
00319 int minSleep = 5, timeExpended = 0;
00320 while (expire_thread_running && timeExpended < sleepTime)
00321 {
00322 if (timeExpended > (sleepTime - minSleep))
00323 minSleep = sleepTime - timeExpended;
00324 timeExpended += minSleep - (int)sleep(minSleep);
00325 }
00326 }
00327
00331 void AutoExpire::ExpireLiveTV(int type)
00332 {
00333 pginfolist_t expireList;
00334
00335 VERBOSE(VB_FILE, LOC + QString("ExpireLiveTV(%1)").arg(type));
00336 FillDBOrdered(expireList, type);
00337 SendDeleteMessages(expireList);
00338 ClearExpireList(expireList);
00339 }
00340
00344 void AutoExpire::ExpireOldDeleted(void)
00345 {
00346 pginfolist_t expireList;
00347
00348 VERBOSE(VB_FILE, LOC + QString("ExpireOldDeleted()"));
00349 FillDBOrdered(expireList, emOldDeletedPrograms);
00350 SendDeleteMessages(expireList);
00351 ClearExpireList(expireList);
00352 }
00353
00358 void AutoExpire::ExpireRecordings(void)
00359 {
00360 pginfolist_t expireList;
00361 pginfolist_t deleteList;
00362 vector<FileSystemInfo> fsInfos;
00363 vector<FileSystemInfo>::iterator fsit;
00364
00365 VERBOSE(VB_FILE, LOC + "ExpireRecordings()");
00366
00367 GetFilesystemInfos(encoderList, fsInfos);
00368
00369 if (fsInfos.size() == 0)
00370 {
00371 QString msg = "ERROR: Filesystem Info cache is empty, unable to "
00372 "determine what Recordings to expire";
00373 VERBOSE(VB_IMPORTANT, LOC + msg);
00374 gContext->LogEntry("mythbackend", LP_WARNING,
00375 "Autoexpire Recording", msg);
00376
00377 return;
00378 }
00379
00380 FillExpireList(expireList);
00381
00382 QMap <int, bool> truncateMap;
00383 MSqlQuery query(MSqlQuery::InitCon());
00384 query.prepare("SELECT DISTINCT rechost, recdir "
00385 "FROM inuseprograms "
00386 "WHERE recusage = 'truncatingdelete' "
00387 "AND lastupdatetime > DATE_ADD(NOW(), INTERVAL -2 MINUTE);");
00388
00389 if (query.exec() && query.isActive() && query.size() > 0)
00390 {
00391 while (query.next())
00392 {
00393 QString rechost = query.value(0).toString();
00394 QString recdir = query.value(1).toString();
00395
00396 VERBOSE(VB_FILE, LOC + QString(
00397 "%1:%2 has an in-progress truncating delete.")
00398 .arg(rechost).arg(recdir));
00399
00400 for (fsit = fsInfos.begin(); fsit != fsInfos.end(); fsit++)
00401 {
00402 if ((fsit->hostname == rechost) &&
00403 (fsit->directory == recdir))
00404 {
00405 truncateMap[fsit->fsID] = true;
00406 break;
00407 }
00408 }
00409 }
00410 }
00411
00412 QMap <int, bool> fsMap;
00413 for (fsit = fsInfos.begin(); fsit != fsInfos.end(); fsit++)
00414 {
00415 if (fsMap.contains(fsit->fsID))
00416 continue;
00417
00418 fsMap[fsit->fsID] = true;
00419
00420 VERBOSE(VB_FILE, QString(
00421 "fsID #%1: Total: %2 GB Used: %3 GB Free: %4 GB")
00422 .arg(fsit->fsID)
00423 .arg(fsit->totalSpaceKB / 1024.0 / 1024.0, 7, 'f', 1)
00424 .arg(fsit->usedSpaceKB / 1024.0 / 1024.0, 7, 'f', 1)
00425 .arg(fsit->freeSpaceKB / 1024.0 / 1024.0, 7, 'f', 1));
00426
00427 if ((fsit->totalSpaceKB == -1) || (fsit->usedSpaceKB == -1))
00428 {
00429 VERBOSE(VB_FILE, LOC_ERR + QString("fsID #%1 has invalid info, "
00430 "AutoExpire can not run for this filesystem. "
00431 "Continuing on to next...").arg(fsit->fsID));
00432 VERBOSE(VB_FILE, QString("Directories on filesystem ID %1:")
00433 .arg(fsit->fsID));
00434 vector<FileSystemInfo>::iterator fsit2;
00435 for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); fsit2++)
00436 {
00437 if (fsit2->fsID == fsit->fsID)
00438 {
00439 VERBOSE(VB_FILE, QString(" %1:%2")
00440 .arg(fsit2->hostname).arg(fsit2->directory));
00441 }
00442 }
00443
00444 continue;
00445 }
00446
00447 if (truncateMap.contains(fsit->fsID))
00448 {
00449 VERBOSE(VB_FILE, QString(
00450 " fsid %1 has a truncating delete in progress, AutoExpire "
00451 "can not run for this filesystem until the delete has "
00452 "finished. Continuing on to next...").arg(fsit->fsID));
00453 continue;
00454 }
00455
00456 if ((size_t)max(0LL, fsit->freeSpaceKB) < desired_space[fsit->fsID])
00457 {
00458 VERBOSE(VB_FILE,
00459 QString(" Not Enough Free Space! We want %1 MB")
00460 .arg(desired_space[fsit->fsID] / 1024));
00461
00462 QMap<QString, int> dirList;
00463 vector<FileSystemInfo>::iterator fsit2;
00464
00465 VERBOSE(VB_FILE, QString(" Directories on filesystem ID %1:")
00466 .arg(fsit->fsID));
00467
00468 for (fsit2 = fsInfos.begin(); fsit2 != fsInfos.end(); fsit2++)
00469 {
00470 if (fsit2->fsID == fsit->fsID)
00471 {
00472 VERBOSE(VB_FILE, QString(" %1:%2")
00473 .arg(fsit2->hostname).arg(fsit2->directory));
00474 dirList[fsit2->hostname + ":" + fsit2->directory] = 1;
00475 }
00476 }
00477
00478 VERBOSE(VB_FILE,
00479 " Searching for expireable files in these directories");
00480 QString myHostName = gContext->GetHostName();
00481 pginfolist_t::iterator it = expireList.begin();
00482 while ((it != expireList.end()) &&
00483 ((size_t)max(0LL, fsit->freeSpaceKB) < desired_space[fsit->fsID]))
00484 {
00485 ProgramInfo *p = *it;
00486 it++;
00487
00488 VERBOSE(VB_FILE, QString(" Checking %1 @ %2 => %3")
00489 .arg(p->chanid).arg(p->recstartts.toString(Qt::ISODate))
00490 .arg(p->title));
00491
00492 if (p->pathname.left(1) != "/")
00493 {
00494 bool foundFile = false;
00495 QMap<int, EncoderLink *>::Iterator eit =
00496 encoderList->begin();
00497 while (eit != encoderList->end())
00498 {
00499 EncoderLink *el = eit.data();
00500 eit++;
00501
00502 if ((p->hostname == el->GetHostName()) ||
00503 ((p->hostname == myHostName) &&
00504 (el->IsLocal())))
00505 {
00506 if (el->IsConnected())
00507 foundFile = el->CheckFile(p);
00508
00509 eit = encoderList->end();
00510 }
00511 }
00512
00513 if (!foundFile && (p->hostname != myHostName))
00514 {
00515
00516 QString file = GetPlaybackURL(p);
00517
00518 if (file.left(1) == "/")
00519 {
00520 p->pathname = file;
00521 p->hostname = myHostName;
00522 foundFile = true;
00523 }
00524 }
00525
00526 if (!foundFile)
00527 {
00528 VERBOSE(VB_FILE, QString(" ERROR: Can't find "
00529 "file for %1 @ %2").arg(p->chanid)
00530 .arg(p->recstartts.toString(Qt::ISODate)));
00531 continue;
00532 }
00533 }
00534
00535 QFileInfo vidFile(p->pathname);
00536 if (dirList.contains(p->hostname + ":" + vidFile.dirPath()))
00537 {
00538 fsit->freeSpaceKB += (p->filesize / 1024);
00539 deleteList.push_back(p);
00540
00541 VERBOSE(VB_FILE, QString(" FOUND Expireable file. "
00542 "%1 @ %2 is located at %3 which is on fsID #%4. "
00543 "Adding to deleteList. After deleting we should "
00544 "have %5 MB free on this filesystem.")
00545 .arg(p->chanid)
00546 .arg(p->recstartts.toString(Qt::ISODate))
00547 .arg(p->pathname).arg(fsit->fsID)
00548 .arg(fsit->freeSpaceKB / 1024));
00549 }
00550 }
00551 }
00552 }
00553
00554 SendDeleteMessages(deleteList);
00555
00556 ClearExpireList(deleteList, false);
00557 ClearExpireList(expireList);
00558 }
00559
00563 void AutoExpire::SendDeleteMessages(pginfolist_t &deleteList)
00564 {
00565 QString msg;
00566
00567 if (deleteList.size() == 0)
00568 {
00569 VERBOSE(VB_FILE, LOC + "SendDeleteMessages. Nothing to expire.");
00570 return;
00571 }
00572
00573 VERBOSE(VB_FILE, LOC + "SendDeleteMessages, cycling through deleteList.");
00574 pginfolist_t::iterator it = deleteList.begin();
00575 while (it != deleteList.end())
00576 {
00577 QString titlestr = (*it)->title;
00578 if (!(*it)->subtitle.isEmpty())
00579 titlestr += " \"" + (*it)->subtitle + "\"";
00580 msg = QString("Expiring %1 MBytes for %2 @ %3 => %4")
00581 .arg((int)((*it)->filesize >> 20))
00582 .arg((*it)->chanid).arg((*it)->startts.toString())
00583 .arg(titlestr);
00584
00585 if (print_verbose_messages & VB_IMPORTANT)
00586 VERBOSE(VB_IMPORTANT, msg);
00587 else
00588 VERBOSE(VB_FILE, QString(" ") + msg);
00589
00590 gContext->LogEntry("autoexpire", LP_NOTICE,
00591 "Expiring Program", msg);
00592
00593
00594 MythEvent me(QString("AUTO_EXPIRE %1 %2").arg((*it)->chanid)
00595 .arg((*it)->recstartts.toString(Qt::ISODate)));
00596 gContext->dispatch(me);
00597
00598 ++it;
00599 }
00600 }
00601
00605 void *AutoExpire::ExpirerThread(void *param)
00606 {
00607 AutoExpire *expirer = (AutoExpire *)param;
00608 expirer->RunExpirer();
00609
00610 return NULL;
00611 }
00612
00618 void AutoExpire::ExpireEpisodesOverMax(void)
00619 {
00620 QMap<QString, int> maxEpisodes;
00621 QMap<QString, int>::Iterator maxIter;
00622 QMap<QString, int> episodeParts;
00623 QString episodeKey;
00624
00625 QString fileprefix = gContext->GetFilePrefix();
00626
00627 MSqlQuery query(MSqlQuery::InitCon());
00628 query.prepare("SELECT recordid, maxepisodes, title "
00629 "FROM record WHERE maxepisodes > 0 "
00630 "ORDER BY recordid ASC, maxepisodes DESC");
00631
00632 if (query.exec() && query.isActive() && query.size() > 0)
00633 {
00634 VERBOSE(VB_FILE, LOC + QString("Found %1 record profiles using max "
00635 "episode expiration")
00636 .arg(query.size()));
00637 while (query.next())
00638 {
00639 VERBOSE(VB_FILE, QString(" %1 (%2 for rec id %3)")
00640 .arg(query.value(2).toString())
00641 .arg(query.value(1).toInt())
00642 .arg(query.value(0).toInt()));
00643 maxEpisodes[query.value(0).toString()] = query.value(1).toInt();
00644 }
00645 }
00646
00647 VERBOSE(VB_FILE, LOC + "Checking episode count for each recording "
00648 "profile using max episodes");
00649 for (maxIter = maxEpisodes.begin(); maxIter != maxEpisodes.end(); maxIter++)
00650 {
00651 query.prepare("SELECT chanid, starttime, title, progstart, progend, "
00652 "filesize, duplicate "
00653 "FROM recorded "
00654 "WHERE recordid = :RECID AND preserve = 0 "
00655 "AND recgroup <> 'LiveTV' "
00656 "ORDER BY starttime DESC;");
00657 query.bindValue(":RECID", maxIter.key());
00658
00659 if (!query.exec() || !query.isActive())
00660 {
00661 MythContext::DBError("AutoExpire query failed!", query);
00662 continue;
00663 }
00664
00665 VERBOSE(VB_FILE, QString(" Recordid %1 has %2 recordings.")
00666 .arg(maxIter.key())
00667 .arg(query.size()));
00668 if (query.size() > 0)
00669 {
00670 int found = 1;
00671 while (query.next())
00672 {
00673 QString chanid = query.value(0).toString();
00674 QDateTime startts = query.value(1).toDateTime();
00675 QString title = QString::fromUtf8(query.value(2).toString());
00676 QDateTime progstart = query.value(3).toDateTime();
00677 QDateTime progend = query.value(4).toDateTime();
00678 int duplicate = query.value(6).toInt();
00679
00680 episodeKey = QString("%1_%2_%3")
00681 .arg(chanid)
00682 .arg(progstart.toString(Qt::ISODate))
00683 .arg(progend.toString(Qt::ISODate));
00684
00685 if ((!IsInDontExpireSet(chanid, startts)) &&
00686 (!episodeParts.contains(episodeKey)) &&
00687 (found > maxIter.data()))
00688 {
00689 long long spaceFreed =
00690 stringToLongLong(query.value(5).toString()) >> 20;
00691 QString msg =
00692 QString("Expiring %1 MBytes for %2 @ %3 => %4. Too "
00693 "many episodes, we only want to keep %5.")
00694 .arg(spaceFreed)
00695 .arg(chanid).arg(startts.toString())
00696 .arg(title).arg(maxIter.data());
00697
00698 if (print_verbose_messages & VB_IMPORTANT)
00699 VERBOSE(VB_IMPORTANT, msg);
00700 else
00701 VERBOSE(VB_FILE, QString(" ") + msg);
00702
00703 gContext->LogEntry("autoexpire", LP_NOTICE,
00704 "Expired program", msg);
00705
00706 msg = QString("AUTO_EXPIRE %1 %2")
00707 .arg(chanid)
00708 .arg(startts.toString(Qt::ISODate));
00709
00710 MythEvent me(msg);
00711 gContext->dispatchNow(me);
00712 }
00713 else
00714 {
00715
00716
00717
00718 if (episodeParts.contains(episodeKey))
00719 {
00720 episodeParts[episodeKey] = episodeParts[episodeKey] + 1;
00721 }
00722 else
00723 {
00724 episodeParts[episodeKey] = 1;
00725 if( duplicate )
00726 found++;
00727 }
00728 }
00729 }
00730 }
00731 }
00732 }
00733
00738 void AutoExpire::FillExpireList(pginfolist_t &expireList)
00739 {
00740 int expMethod = gContext->GetNumSetting("AutoExpireMethod", 1);
00741
00742 ClearExpireList(expireList);
00743
00744 FillDBOrdered(expireList, emNormalDeletedPrograms);
00745
00746 switch(expMethod)
00747 {
00748 case emOldestFirst:
00749 case emLowestPriorityFirst:
00750 case emWeightedTimePriority:
00751 FillDBOrdered(expireList, expMethod);
00752 break;
00753
00754 }
00755 }
00756
00760 void AutoExpire::PrintExpireList(QString expHost)
00761 {
00762 pginfolist_t expireList;
00763
00764 FillExpireList(expireList);
00765
00766 cout << "MythTV AutoExpire List ";
00767 if (expHost != "ALL")
00768 cout << "for '" << expHost << "' ";
00769 cout << "(programs listed in order of expiration)\n";
00770
00771 pginfolist_t::iterator i = expireList.begin();
00772 for (; i != expireList.end(); i++)
00773 {
00774 ProgramInfo *first = (*i);
00775
00776 if (expHost != "ALL" && first->hostname != expHost)
00777 continue;
00778
00779 QString title = first->title;
00780
00781 if (first->subtitle != "")
00782 title += ": \"" + first->subtitle + "\"";
00783
00784 cout << title.local8Bit().leftJustify(39, ' ', true) << " "
00785 << QString("%1").arg(first->filesize >> 20).local8Bit()
00786 .rightJustify(5, ' ', true) << "MB "
00787 << first->startts.toString().local8Bit().leftJustify(24, ' ', true)
00788 << " [" << QString("%1").arg(first->recpriority).local8Bit()
00789 .rightJustify(3, ' ', true) << "]"
00790 << endl;
00791 }
00792
00793 ClearExpireList(expireList);
00794 }
00795
00799 void AutoExpire::GetAllExpiring(QStringList &strList)
00800 {
00801 QMutexLocker lockit(&instance_lock);
00802 pginfolist_t expireList;
00803
00804 UpdateDontExpireSet();
00805
00806 FillDBOrdered(expireList, emShortLiveTVPrograms);
00807 FillDBOrdered(expireList, emNormalLiveTVPrograms);
00808 FillDBOrdered(expireList, emNormalDeletedPrograms);
00809 FillDBOrdered(expireList, gContext->GetNumSetting("AutoExpireMethod",
00810 emOldestFirst));
00811
00812 strList << QString::number(expireList.size());
00813
00814 pginfolist_t::iterator it = expireList.begin();
00815 for (; it != expireList.end(); it++)
00816 (*it)->ToStringList(strList);
00817
00818 ClearExpireList(expireList);
00819 }
00820
00824 void AutoExpire::GetAllExpiring(pginfolist_t &list)
00825 {
00826 QMutexLocker lockit(&instance_lock);
00827 pginfolist_t expireList;
00828
00829 UpdateDontExpireSet();
00830
00831 FillDBOrdered(expireList, emShortLiveTVPrograms);
00832 FillDBOrdered(expireList, emNormalLiveTVPrograms);
00833 FillDBOrdered(expireList, emNormalDeletedPrograms);
00834 FillDBOrdered(expireList, gContext->GetNumSetting("AutoExpireMethod",
00835 emOldestFirst));
00836
00837 pginfolist_t::iterator it = expireList.begin();
00838 for (; it != expireList.end(); it++)
00839 list.push_back( new ProgramInfo( *(*it) ));
00840
00841 ClearExpireList(expireList);
00842 }
00843
00847 void AutoExpire::ClearExpireList(pginfolist_t &expireList, bool deleteProg)
00848 {
00849 ProgramInfo *pginfo = NULL;
00850 while (expireList.size() > 0)
00851 {
00852 if (deleteProg)
00853 pginfo = expireList.back();
00854
00855 expireList.pop_back();
00856
00857 if (deleteProg)
00858 delete pginfo;
00859 }
00860 }
00861
00866 void AutoExpire::FillDBOrdered(pginfolist_t &expireList, int expMethod)
00867 {
00868 QString where;
00869 QString orderby;
00870 QString msg;
00871 int maxAge;
00872
00873 switch (expMethod)
00874 {
00875 default:
00876 case emOldestFirst:
00877 msg = "Adding expirable programs in Oldest First order";
00878 where = "autoexpire > 0";
00879 if (gContext->GetNumSetting("AutoExpireWatchedPriority", 0))
00880 orderby = "recorded.watched DESC, ";
00881 orderby += "starttime ASC";
00882 break;
00883 case emLowestPriorityFirst:
00884 msg = "Adding expirable programs in Lowest Priority First order";
00885 where = "autoexpire > 0";
00886 if (gContext->GetNumSetting("AutoExpireWatchedPriority", 0))
00887 orderby = "recorded.watched DESC, ";
00888 orderby += "recorded.recpriority ASC, starttime ASC";
00889 break;
00890 case emWeightedTimePriority:
00891 msg = "Adding expirable programs in Weighted Time Priority order";
00892 where = "autoexpire > 0";
00893 if (gContext->GetNumSetting("AutoExpireWatchedPriority", 0))
00894 orderby = "recorded.watched DESC, ";
00895 orderby += QString("DATE_ADD(starttime, INTERVAL '%1' * "
00896 "recorded.recpriority DAY) ASC")
00897 .arg(gContext->GetNumSetting("AutoExpireDayPriority", 3));
00898 break;
00899 case emShortLiveTVPrograms:
00900 msg = "Adding Short LiveTV programs in starttime order";
00901 where = "recgroup = 'LiveTV' "
00902 "AND endtime < DATE_ADD(starttime, INTERVAL '2' MINUTE) "
00903 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-1' MINUTE) ";
00904 orderby = "starttime ASC";
00905 break;
00906 case emNormalLiveTVPrograms:
00907 msg = "Adding LiveTV programs in starttime order";
00908 where = QString("recgroup = 'LiveTV' "
00909 "AND endtime <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
00910 .arg(gContext->GetNumSetting("AutoExpireLiveTVMaxAge", 1));
00911 orderby = "starttime ASC";
00912 break;
00913 case emOldDeletedPrograms:
00914 if ((maxAge = gContext->GetNumSetting("DeletedMaxAge", 0)) == 0)
00915 return;
00916 msg = QString("Adding programs deleted more than %1 days ago")
00917 .arg(maxAge);
00918 where = QString("recgroup = 'Deleted' "
00919 "AND lastmodified <= DATE_ADD(NOW(), INTERVAL '-%1' DAY) ")
00920 .arg(maxAge);
00921 orderby = "starttime ASC";
00922 break;
00923 case emNormalDeletedPrograms:
00924 if (gContext->GetNumSetting("DeletedFifoOrder", 0) == 0)
00925 return;
00926 msg = "Adding deleted programs in FIFO order";
00927 where = "recgroup = 'Deleted'";
00928 orderby = "lastmodified ASC";
00929 break;
00930 }
00931
00932 VERBOSE(VB_FILE, LOC + "FillDBOrdered: " + msg);
00933
00934 MSqlQuery query(MSqlQuery::InitCon());
00935 QString querystr = QString(
00936 "SELECT recorded.chanid, starttime, endtime, "
00937 " title, subtitle, description, "
00938 " hostname, channum, name, "
00939 " callsign, seriesid, programid, "
00940 " recorded.recpriority, progstart, "
00941 " progend, filesize, recgroup, "
00942 " storagegroup, basename "
00943 "FROM recorded "
00944 "LEFT JOIN channel ON recorded.chanid = channel.chanid "
00945 "WHERE %1 AND deletepending = 0 "
00946 "ORDER BY autoexpire DESC, %2").arg(where).arg(orderby);
00947
00948 query.prepare(querystr);
00949
00950 if (!query.exec() || !query.isActive() || !query.size())
00951 return;
00952
00953 while (query.next())
00954 {
00955 QString m_chanid = query.value(0).toString();
00956 QDateTime m_recstartts = query.value(1).toDateTime();
00957
00958 if (IsInDontExpireSet(m_chanid, m_recstartts))
00959 {
00960 VERBOSE(VB_FILE, LOC + QString(" Skipping "
00961 "%1 @ %2 because it is in Don't Expire List")
00962 .arg(m_chanid).arg(m_recstartts.toString()));
00963 continue;
00964 }
00965 else if (IsInExpireList(expireList, m_chanid, m_recstartts))
00966 {
00967 VERBOSE(VB_FILE, LOC + QString(" Skipping "
00968 "%1 @ %2 because it is already in Expire List")
00969 .arg(m_chanid) .arg(m_recstartts.toString()));
00970 continue;
00971 }
00972
00973 ProgramInfo *proginfo = new ProgramInfo;
00974
00975 proginfo->chanid = m_chanid;
00976 proginfo->startts = query.value(13).toDateTime();
00977 proginfo->endts = query.value(14).toDateTime();
00978 proginfo->recstartts = m_recstartts;
00979 proginfo->recendts = query.value(2).toDateTime();
00980 proginfo->title = QString::fromUtf8(query.value(3).toString());
00981 proginfo->subtitle = QString::fromUtf8(query.value(4).toString());
00982 proginfo->description = QString::fromUtf8(query.value(5).toString());
00983 proginfo->hostname = query.value(6).toString();
00984
00985 if (!query.value(7).toString().isEmpty())
00986 {
00987 proginfo->chanstr = query.value(7).toString();
00988 proginfo->channame = QString::fromUtf8(query.value(8).toString());
00989 proginfo->chansign = QString::fromUtf8(query.value(9).toString());
00990 }
00991 else
00992 {
00993 proginfo->chanstr = "#" + proginfo->chanid;
00994 proginfo->channame = "#" + proginfo->chanid;
00995 proginfo->chansign = "#" + proginfo->chanid;
00996 }
00997
00998 proginfo->seriesid = query.value(10).toString();
00999 proginfo->programid = query.value(11).toString();
01000 proginfo->recpriority = query.value(12).toInt();
01001 proginfo->filesize = stringToLongLong(query.value(15).toString());
01002 proginfo->recgroup = QString::fromUtf8(query.value(16).toString());
01003 proginfo->storagegroup = QString::fromUtf8(query.value(17).toString());
01004 proginfo->pathname = query.value(18).toString();
01005
01006 VERBOSE(VB_FILE, LOC + QString(" Adding "
01007 "%1 @ %2")
01008 .arg(proginfo->chanid)
01009 .arg(proginfo->recstartts.toString()));
01010 expireList.push_back(proginfo);
01011 }
01012 }
01013
01020 void *SpawnUpdateThread(void *autoExpireInstance)
01021 {
01022 sleep(5);
01023 AutoExpire *ae = (AutoExpire*) autoExpireInstance;
01024 ae->CalcParams();
01025 ae->instance_lock.lock();
01026 ae->update_pending = false;
01027 ae->instance_cond.wakeAll();
01028 ae->instance_lock.unlock();
01029 return NULL;
01030 }
01031
01043 void AutoExpire::Update(int encoder, int fsID, bool immediately)
01044 {
01045 if (!expirer)
01046 return;
01047
01048
01049 expirer->instance_lock.lock();
01050 while (expirer->update_pending)
01051 expirer->instance_cond.wait(&expirer->instance_lock);
01052 expirer->update_pending = true;
01053
01054 if (encoder > 0)
01055 {
01056 QString msg = QString("Cardid %1: is starting a recording on").arg(encoder);
01057 if (fsID == -1)
01058 msg.append(" an unknown fsID soon.");
01059 else
01060 msg.append(QString(" fsID %2 soon.").arg(fsID));
01061
01062 VERBOSE(VB_FILE, LOC + msg);
01063 expirer->used_encoders[encoder] = fsID;
01064 }
01065
01066 expirer->instance_lock.unlock();
01067
01068
01069 if (immediately)
01070 {
01071 expirer->CalcParams();
01072 expirer->instance_lock.lock();
01073 expirer->update_pending = false;
01074 expirer->instance_cond.wakeAll();
01075 expirer->instance_lock.unlock();
01076 }
01077 else
01078 {
01079
01080 pthread_attr_t attr;
01081 pthread_attr_init(&attr);
01082 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
01083 pthread_create(&expirer->update_thread, &attr,
01084 SpawnUpdateThread, expirer);
01085 pthread_attr_destroy(&attr);
01086 }
01087 }
01088
01089 void AutoExpire::UpdateDontExpireSet(void)
01090 {
01091 dont_expire_set.clear();
01092
01093 MSqlQuery query(MSqlQuery::InitCon());
01094 query.prepare("SELECT chanid, starttime, lastupdatetime, recusage, "
01095 " hostname "
01096 "FROM inuseprograms;");
01097
01098 if (!query.exec() || !query.isActive() || !query.size())
01099 return;
01100
01101 QDateTime curTime = QDateTime::currentDateTime();
01102
01103 VERBOSE(VB_FILE, LOC + "Adding Programs to 'Do Not Expire' List");
01104 while (query.next())
01105 {
01106 QString chanid = query.value(0).toString();
01107 QDateTime startts = query.value(1).toDateTime();
01108 QDateTime lastupdate = query.value(2).toDateTime();
01109
01110 if (lastupdate.secsTo(curTime) < 2 * 60 * 60)
01111 {
01112 QString key = chanid + startts.toString(Qt::ISODate);
01113 dont_expire_set.insert(key);
01114 VERBOSE(VB_FILE, QString(" %1 @ %2 in use by %3 on %4")
01115 .arg(chanid)
01116 .arg(startts.toString(Qt::ISODate))
01117 .arg(query.value(3).toString())
01118 .arg(query.value(4).toString()));
01119 }
01120 }
01121 }
01122
01123 bool AutoExpire::IsInDontExpireSet(QString chanid, QDateTime starttime)
01124 {
01125 QString key = chanid + starttime.toString(Qt::ISODate);
01126
01127 return (dont_expire_set.count(key));
01128 }
01129
01130 bool AutoExpire::IsInExpireList(pginfolist_t &expireList, QString chanid,
01131 QDateTime starttime)
01132 {
01133 pginfolist_t::iterator it;
01134
01135 for (it = expireList.begin(); it != expireList.end(); ++it)
01136 {
01137 if (((*it)->chanid == chanid) &&
01138 ((*it)->recstartts == starttime))
01139 return true;
01140 }
01141 return false;
01142 }
01143
01144