MythTV  0.26-pre
autoexpire.cpp
Go to the documentation of this file.
00001 // System headers
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 // POSIX headers
00013 #include <unistd.h>
00014 
00015 // C headers
00016 #include <cstdlib>
00017 
00018 // C++ headers
00019 #include <iostream>
00020 #include <algorithm>
00021 using namespace std;
00022 
00023 // Qt headers
00024 #include <QDateTime>
00025 #include <QFileInfo>
00026 #include <QList>
00027 
00028 // MythTV headers
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     // we use this copying on purpose. The used_encoders map ensures
00174     // that every encoder writes only to one fs.
00175     // Copying the data minimizes the time the lock is held
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         // append unknown recordings to all fsIDs
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                     // remove encoder since it can't write to any file system
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     // Determine frequency to run autoexpire so it doesn't have to free
00251     // too much space
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     // lock class and save these parameters.
00266     instance_lock.lock();
00267     desired_freq = expireFreq;
00268     // write per file system needed space back, use safety of 33%
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     // wait a little for main server to come up and things to settle down
00295     Sleep(20 * 1000);
00296 
00297     timer.start();
00298 
00299     while (expire_thread_run)
00300     {
00301         curTime = QDateTime::currentDateTime();
00302         // recalculate auto expire parameters
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         // Expire Short LiveTV files for this backend every 2 minutes
00316         if ((curTime.time().minute() % 2) == 0)
00317             ExpireLiveTV(emShortLiveTVPrograms);
00318 
00319         // Expire normal recordings depending on frequency calculated
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                         // Wasn't found so check locally
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         // send auto expire message to backend's event thread.
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; // move on to next program
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                     // keep track of shows we haven't expired so we can
00751                     // make sure we don't expire another part of the same
00752                     // episode.
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         // default falls through so list is empty so no AutoExpire
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             if (gCoreContext->GetNumSetting("DeletedFifoOrder", 0) == 0)
00971                 return;
00972             msg = "Adding deleted programs in FIFO order";
00973             where = "recgroup = 'Deleted'";
00974             orderby = "lastmodified ASC";
00975             break;
00976     }
00977 
00978     LOG(VB_FILE, LOG_INFO, LOC + "FillDBOrdered: " + msg);
00979 
00980     MSqlQuery query(MSqlQuery::InitCon());
00981     QString querystr = QString(
00982         "SELECT recorded.chanid, starttime "
00983         "FROM recorded "
00984         "LEFT JOIN channel ON recorded.chanid = channel.chanid "
00985         "WHERE %1 AND deletepending = 0 "
00986         "ORDER BY autoexpire DESC, %2").arg(where).arg(orderby);
00987 
00988     query.prepare(querystr);
00989 
00990     if (!query.exec())
00991         return;
00992 
00993     while (query.next())
00994     {
00995         uint chanid = query.value(0).toUInt();
00996         QDateTime recstartts = query.value(1).toDateTime();
00997 
00998         if (IsInDontExpireSet(chanid, recstartts))
00999         {
01000             LOG(VB_FILE, LOG_INFO, LOC +
01001                 QString("    Skipping %1 at %2 because it is in Don't Expire "
01002                         "List")
01003                     .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
01004         }
01005         else if (IsInExpireList(expireList, chanid, recstartts))
01006         {
01007             LOG(VB_FILE, LOG_INFO, LOC +
01008                 QString("    Skipping %1 at %2 because it is already in Expire "
01009                         "List")
01010                     .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
01011         }
01012         else
01013         {
01014             ProgramInfo *pginfo = new ProgramInfo(chanid, recstartts);
01015             if (pginfo->GetChanID())
01016             {
01017                 LOG(VB_FILE, LOG_INFO, LOC + QString("    Adding   %1 at %2")
01018                         .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
01019                 expireList.push_back(pginfo);
01020             }
01021             else
01022             {
01023                 LOG(VB_FILE, LOG_INFO, LOC +
01024                     QString("    Skipping %1 at %2 "
01025                             "because it could not be loaded from the DB")
01026                         .arg(chanid).arg(recstartts.toString(Qt::ISODate)));
01027                 delete pginfo;
01028             }
01029         }
01030     }
01031 }
01032 
01038 void AutoExpire::RunUpdate(void)
01039 {
01040     QMutexLocker locker(&instance_lock);
01041     Sleep(5 * 1000);
01042     locker.unlock();
01043     CalcParams();
01044     locker.relock();
01045     update_pending = false;
01046     update_thread->deleteLater();
01047     update_thread = NULL;
01048     instance_cond.wakeAll();
01049 }
01050 
01062 void AutoExpire::Update(int encoder, int fsID, bool immediately)
01063 {
01064     if (!expirer)
01065         return;
01066 
01067     // make sure there is only one update pending
01068     QMutexLocker locker(&expirer->instance_lock);
01069     while (expirer->update_pending)
01070         expirer->instance_cond.wait(&expirer->instance_lock);
01071     expirer->update_pending = true;
01072 
01073     if (encoder > 0)
01074     {
01075         QString msg = QString("Cardid %1: is starting a recording on")
01076                       .arg(encoder);
01077         if (fsID == -1)
01078             msg.append(" an unknown fsID soon.");
01079         else
01080             msg.append(QString(" fsID %2 soon.").arg(fsID));
01081 
01082         LOG(VB_FILE, LOG_INFO, LOC + msg);
01083         expirer->used_encoders[encoder] = fsID;
01084     }
01085 
01086     // do it..
01087     if (immediately)
01088     {
01089         locker.unlock();
01090         expirer->CalcParams();
01091         locker.relock();
01092         expirer->update_pending = false;
01093         expirer->instance_cond.wakeAll();
01094     }
01095     else
01096     {
01097         // create thread to do work, unless one is running still
01098         if (!expirer->update_thread)
01099         {
01100             expirer->update_thread = new UpdateThread(expirer);
01101             expirer->update_thread->start();
01102         }
01103     }
01104 }
01105 
01106 void AutoExpire::UpdateDontExpireSet(void)
01107 {
01108     dont_expire_set = deleted_set;
01109 
01110     MSqlQuery query(MSqlQuery::InitCon());
01111     query.prepare(
01112         "SELECT chanid, starttime, lastupdatetime, recusage, hostname "
01113         "FROM inuseprograms");
01114 
01115     if (!query.exec() || !query.next())
01116         return;
01117 
01118     LOG(VB_FILE, LOG_INFO, LOC + "Adding Programs to 'Do Not Expire' List");
01119     QDateTime curTime = QDateTime::currentDateTime();
01120 
01121     do
01122     {
01123         uint chanid = query.value(0).toUInt();
01124         QDateTime recstartts = query.value(1).toDateTime();
01125         QDateTime lastupdate = query.value(2).toDateTime();
01126 
01127         if (lastupdate.secsTo(curTime) < 2 * 60 * 60)
01128         {
01129             QString key = QString("%1_%2")
01130                 .arg(chanid).arg(recstartts.toString(Qt::ISODate));
01131             dont_expire_set.insert(key);
01132             LOG(VB_FILE, LOG_INFO, QString("    %1 at %2 in use by %3 on %4")
01133                     .arg(chanid)
01134                     .arg(recstartts.toString(Qt::ISODate))
01135                     .arg(query.value(3).toString())
01136                     .arg(query.value(4).toString()));
01137         }
01138     }
01139     while (query.next());
01140 }
01141 
01142 bool AutoExpire::IsInDontExpireSet(
01143     uint chanid, const QDateTime &recstartts) const
01144 {
01145     QString key = QString("%1_%2")
01146         .arg(chanid).arg(recstartts.toString(Qt::ISODate));
01147 
01148     return (dont_expire_set.find(key) != dont_expire_set.end());
01149 }
01150 
01151 bool AutoExpire::IsInExpireList(
01152     const pginfolist_t &expireList, uint chanid, const QDateTime &recstartts)
01153 {
01154     pginfolist_t::const_iterator it;
01155 
01156     for (it = expireList.begin(); it != expireList.end(); ++it)
01157     {
01158         if (((*it)->GetChanID()             == chanid) &&
01159             ((*it)->GetRecordingStartTime() == recstartts))
01160         {
01161             return true;
01162         }
01163     }
01164     return false;
01165 }
01166 
01167 /* vim: set expandtab tabstop=4 shiftwidth=4: */
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends