|
MythTV
0.26-pre
|
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: */
1.7.6.1