|
MythTV
0.26-pre
|
00001 #include <unistd.h> 00002 00003 #include <iostream> 00004 #include <algorithm> 00005 #include <list> 00006 using namespace std; 00007 00008 #ifdef __linux__ 00009 # include <sys/vfs.h> 00010 #else // if !__linux__ 00011 # include <sys/param.h> 00012 # ifndef USING_MINGW 00013 # include <sys/mount.h> 00014 # endif // USING_MINGW 00015 #endif // !__linux__ 00016 00017 #include <sys/stat.h> 00018 #include <sys/time.h> 00019 #include <sys/types.h> 00020 00021 #include <QStringList> 00022 #include <QDateTime> 00023 #include <QString> 00024 #include <QRegExp> 00025 #include <QMutex> 00026 #include <QFile> 00027 #include <QMap> 00028 00029 #include "scheduler.h" 00030 #include "encoderlink.h" 00031 #include "mainserver.h" 00032 #include "remoteutil.h" 00033 #include "backendutil.h" 00034 #include "mythmiscutil.h" 00035 #include "exitcodes.h" 00036 #include "mythcontext.h" 00037 #include "mythdb.h" 00038 #include "compat.h" 00039 #include "storagegroup.h" 00040 #include "recordinginfo.h" 00041 #include "recordingrule.h" 00042 #include "scheduledrecording.h" 00043 #include "cardutil.h" 00044 #include "mythdb.h" 00045 #include "mythsystemevent.h" 00046 #include "mythlogging.h" 00047 00048 #define LOC QString("Scheduler: ") 00049 #define LOC_WARN QString("Scheduler, Warning: ") 00050 #define LOC_ERR QString("Scheduler, Error: ") 00051 00052 bool debugConflicts = false; 00053 00054 Scheduler::Scheduler(bool runthread, QMap<int, EncoderLink *> *tvList, 00055 QString tmptable, Scheduler *master_sched) : 00056 MThread("Scheduler"), 00057 recordTable(tmptable), 00058 priorityTable("powerpriority"), 00059 schedLock(), 00060 reclist_changed(false), 00061 specsched(master_sched), 00062 schedMoveHigher(false), 00063 schedulingEnabled(true), 00064 m_tvList(tvList), 00065 m_expirer(NULL), 00066 doRun(runthread), 00067 m_mainServer(NULL), 00068 resetIdleTime(false), 00069 m_isShuttingDown(false), 00070 error(0), 00071 livetvTime(QDateTime()), 00072 livetvpriority(0), 00073 prefinputpri(0) 00074 { 00075 char *debug = getenv("DEBUG_CONFLICTS"); 00076 debugConflicts = (debug != NULL); 00077 00078 if (master_sched) 00079 master_sched->GetAllPending(reclist); 00080 00081 if (!doRun) 00082 dbConn = MSqlQuery::DDCon(); 00083 00084 if (tmptable == "powerpriority_tmp") 00085 { 00086 priorityTable = tmptable; 00087 recordTable = "record"; 00088 } 00089 00090 if (!VerifyCards()) 00091 { 00092 error = true; 00093 return; 00094 } 00095 00096 fsInfoCacheFillTime = QDateTime::currentDateTime().addSecs(-1000); 00097 00098 if (doRun) 00099 { 00100 ProgramInfo::CheckProgramIDAuthorities(); 00101 { 00102 QMutexLocker locker(&schedLock); 00103 start(QThread::LowPriority); 00104 while (doRun && !isRunning()) 00105 reschedWait.wait(&schedLock); 00106 } 00107 WakeUpSlaves(); 00108 } 00109 } 00110 00111 Scheduler::~Scheduler() 00112 { 00113 QMutexLocker locker(&schedLock); 00114 if (doRun) 00115 { 00116 doRun = false; 00117 reschedWait.wakeAll(); 00118 locker.unlock(); 00119 wait(); 00120 locker.relock(); 00121 } 00122 00123 while (!reclist.empty()) 00124 { 00125 delete reclist.back(); 00126 reclist.pop_back(); 00127 } 00128 00129 while (!worklist.empty()) 00130 { 00131 delete worklist.back(); 00132 worklist.pop_back(); 00133 } 00134 00135 locker.unlock(); 00136 wait(); 00137 } 00138 00139 void Scheduler::Stop(void) 00140 { 00141 QMutexLocker locker(&schedLock); 00142 doRun = false; 00143 reschedWait.wakeAll(); 00144 } 00145 00146 void Scheduler::SetMainServer(MainServer *ms) 00147 { 00148 m_mainServer = ms; 00149 } 00150 00151 void Scheduler::ResetIdleTime(void) 00152 { 00153 resetIdleTime_lock.lock(); 00154 resetIdleTime = true; 00155 resetIdleTime_lock.unlock(); 00156 } 00157 00158 bool Scheduler::VerifyCards(void) 00159 { 00160 MSqlQuery query(MSqlQuery::InitCon()); 00161 if (!query.exec("SELECT count(*) FROM capturecard") || !query.next()) 00162 { 00163 MythDB::DBError("verifyCards() -- main query 1", query); 00164 error = GENERIC_EXIT_DB_ERROR; 00165 return false; 00166 } 00167 00168 uint numcards = query.value(0).toUInt(); 00169 if (!numcards) 00170 { 00171 LOG(VB_GENERAL, LOG_ERR, LOC + 00172 "No capture cards are defined in the database.\n\t\t\t" 00173 "Perhaps you should re-read the installation instructions?"); 00174 error = GENERIC_EXIT_SETUP_ERROR; 00175 return false; 00176 } 00177 00178 query.prepare("SELECT sourceid,name FROM videosource ORDER BY sourceid;"); 00179 00180 if (!query.exec()) 00181 { 00182 MythDB::DBError("verifyCards() -- main query 2", query); 00183 error = GENERIC_EXIT_DB_ERROR; 00184 return false; 00185 } 00186 00187 uint numsources = 0; 00188 MSqlQuery subquery(MSqlQuery::InitCon()); 00189 while (query.next()) 00190 { 00191 subquery.prepare( 00192 "SELECT cardinputid " 00193 "FROM cardinput " 00194 "WHERE sourceid = :SOURCEID " 00195 "ORDER BY cardinputid;"); 00196 subquery.bindValue(":SOURCEID", query.value(0).toUInt()); 00197 00198 if (!subquery.exec()) 00199 { 00200 MythDB::DBError("verifyCards() -- sub query", subquery); 00201 } 00202 else if (!subquery.next()) 00203 { 00204 LOG(VB_GENERAL, LOG_WARNING, LOC + 00205 QString("Listings source '%1' is defined, " 00206 "but is not attached to a card input.") 00207 .arg(query.value(1).toString())); 00208 } 00209 else 00210 { 00211 numsources++; 00212 } 00213 } 00214 00215 if (!numsources) 00216 { 00217 LOG(VB_GENERAL, LOG_ERR, LOC + 00218 "No channel sources defined in the database"); 00219 error = GENERIC_EXIT_SETUP_ERROR; 00220 return false; 00221 } 00222 00223 return true; 00224 } 00225 00226 static inline bool Recording(const RecordingInfo *p) 00227 { 00228 return (p->GetRecordingStatus() == rsRecording || 00229 p->GetRecordingStatus() == rsTuning || 00230 p->GetRecordingStatus() == rsWillRecord); 00231 } 00232 00233 static bool comp_overlap(RecordingInfo *a, RecordingInfo *b) 00234 { 00235 if (a->GetScheduledStartTime() != b->GetScheduledStartTime()) 00236 return a->GetScheduledStartTime() < b->GetScheduledStartTime(); 00237 if (a->GetScheduledEndTime() != b->GetScheduledEndTime()) 00238 return a->GetScheduledEndTime() < b->GetScheduledEndTime(); 00239 00240 // Note: the PruneOverlaps logic depends on the following 00241 if (a->GetTitle() != b->GetTitle()) 00242 return a->GetTitle() < b->GetTitle(); 00243 if (a->GetChanID() != b->GetChanID()) 00244 return a->GetChanID() < b->GetChanID(); 00245 if (a->GetInputID() != b->GetInputID()) 00246 return a->GetInputID() < b->GetInputID(); 00247 00248 // In cases where two recording rules match the same showing, one 00249 // of them needs to take precedence. Penalize any entry that 00250 // won't record except for those from kDontRecord rules. This 00251 // will force them to yield to a rule that might record. 00252 // Otherwise, more specific record type beats less specific. 00253 int apri = RecTypePriority(a->GetRecordingRuleType()); 00254 if (a->GetRecordingStatus() != rsUnknown && 00255 a->GetRecordingStatus() != rsDontRecord) 00256 { 00257 apri += 100; 00258 } 00259 int bpri = RecTypePriority(b->GetRecordingRuleType()); 00260 if (b->GetRecordingStatus() != rsUnknown && 00261 b->GetRecordingStatus() != rsDontRecord) 00262 { 00263 bpri += 100; 00264 } 00265 if (apri != bpri) 00266 return apri < bpri; 00267 00268 if (a->GetFindID() != b->GetFindID()) 00269 return a->GetFindID() > b->GetFindID(); 00270 return a->GetRecordingRuleID() < b->GetRecordingRuleID(); 00271 } 00272 00273 static bool comp_redundant(RecordingInfo *a, RecordingInfo *b) 00274 { 00275 if (a->GetScheduledStartTime() != b->GetScheduledStartTime()) 00276 return a->GetScheduledStartTime() < b->GetScheduledStartTime(); 00277 if (a->GetScheduledEndTime() != b->GetScheduledEndTime()) 00278 return a->GetScheduledEndTime() < b->GetScheduledEndTime(); 00279 00280 // Note: the PruneRedundants logic depends on the following 00281 int cmp = a->GetTitle().compare(b->GetTitle(), Qt::CaseInsensitive); 00282 if (cmp != 0) 00283 return cmp < 0; 00284 if (a->GetRecordingRuleID() != b->GetRecordingRuleID()) 00285 return a->GetRecordingRuleID() < b->GetRecordingRuleID(); 00286 cmp = a->GetChannelSchedulingID().compare(b->GetChannelSchedulingID(), 00287 Qt::CaseInsensitive); 00288 if (cmp != 0) 00289 return cmp < 0; 00290 if (a->GetRecordingStatus() != b->GetRecordingStatus()) 00291 return a->GetRecordingStatus() < b->GetRecordingStatus(); 00292 cmp = a->GetChanNum().compare(b->GetChanNum(), Qt::CaseInsensitive); 00293 return cmp < 0; 00294 } 00295 00296 static bool comp_recstart(RecordingInfo *a, RecordingInfo *b) 00297 { 00298 if (a->GetRecordingStartTime() != b->GetRecordingStartTime()) 00299 return a->GetRecordingStartTime() < b->GetRecordingStartTime(); 00300 if (a->GetRecordingEndTime() != b->GetRecordingEndTime()) 00301 return a->GetRecordingEndTime() < b->GetRecordingEndTime(); 00302 int cmp = a->GetChannelSchedulingID().compare(b->GetChannelSchedulingID(), 00303 Qt::CaseInsensitive); 00304 if (cmp != 0) 00305 return cmp < 0; 00306 if (a->GetRecordingStatus() != b->GetRecordingStatus()) 00307 return a->GetRecordingStatus() < b->GetRecordingStatus(); 00308 if (a->GetChanNum() != b->GetChanNum()) 00309 return a->GetChanNum() < b->GetChanNum(); 00310 return a->GetChanID() < b->GetChanID(); 00311 } 00312 00313 static bool comp_priority(RecordingInfo *a, RecordingInfo *b) 00314 { 00315 int arec = (a->GetRecordingStatus() != rsRecording && 00316 a->GetRecordingStatus() != rsTuning); 00317 int brec = (b->GetRecordingStatus() != rsRecording && 00318 b->GetRecordingStatus() != rsTuning); 00319 00320 if (arec != brec) 00321 return arec < brec; 00322 00323 if (a->GetRecordingPriority() != b->GetRecordingPriority()) 00324 return a->GetRecordingPriority() > b->GetRecordingPriority(); 00325 00326 QDateTime pasttime = QDateTime::currentDateTime().addSecs(-30); 00327 int apast = (a->GetRecordingStartTime() < pasttime && 00328 !a->IsReactivated()); 00329 int bpast = (b->GetRecordingStartTime() < pasttime && 00330 !b->IsReactivated()); 00331 00332 if (apast != bpast) 00333 return apast < bpast; 00334 00335 int apri = RecTypePriority(a->GetRecordingRuleType()); 00336 int bpri = RecTypePriority(b->GetRecordingRuleType()); 00337 00338 if (apri != bpri) 00339 return apri < bpri; 00340 00341 if (a->GetRecordingStartTime() != b->GetRecordingStartTime()) 00342 { 00343 if (apast) 00344 return a->GetRecordingStartTime() > b->GetRecordingStartTime(); 00345 else 00346 return a->GetRecordingStartTime() < b->GetRecordingStartTime(); 00347 } 00348 00349 if (a->GetRecordingPriority2() != b->GetRecordingPriority2()) 00350 return a->GetRecordingPriority2() < b->GetRecordingPriority2(); 00351 00352 if (a->GetInputID() != b->GetInputID()) 00353 return a->GetInputID() < b->GetInputID(); 00354 00355 return a->GetRecordingRuleID() < b->GetRecordingRuleID(); 00356 } 00357 00358 bool Scheduler::FillRecordList(void) 00359 { 00360 schedMoveHigher = (bool)gCoreContext->GetNumSetting("SchedMoveHigher"); 00361 schedTime = QDateTime::currentDateTime(); 00362 00363 LOG(VB_SCHEDULE, LOG_INFO, "BuildWorkList..."); 00364 BuildWorkList(); 00365 00366 schedLock.unlock(); 00367 00368 LOG(VB_SCHEDULE, LOG_INFO, "AddNewRecords..."); 00369 AddNewRecords(); 00370 LOG(VB_SCHEDULE, LOG_INFO, "AddNotListed..."); 00371 AddNotListed(); 00372 00373 LOG(VB_SCHEDULE, LOG_INFO, "Sort by time..."); 00374 SORT_RECLIST(worklist, comp_overlap); 00375 LOG(VB_SCHEDULE, LOG_INFO, "PruneOverlaps..."); 00376 PruneOverlaps(); 00377 00378 LOG(VB_SCHEDULE, LOG_INFO, "Sort by priority..."); 00379 SORT_RECLIST(worklist, comp_priority); 00380 LOG(VB_SCHEDULE, LOG_INFO, "BuildListMaps..."); 00381 BuildListMaps(); 00382 LOG(VB_SCHEDULE, LOG_INFO, "SchedNewRecords..."); 00383 SchedNewRecords(); 00384 LOG(VB_SCHEDULE, LOG_INFO, "SchedPreserveLiveTV..."); 00385 SchedPreserveLiveTV(); 00386 LOG(VB_SCHEDULE, LOG_INFO, "ClearListMaps..."); 00387 ClearListMaps(); 00388 00389 schedLock.lock(); 00390 00391 LOG(VB_SCHEDULE, LOG_INFO, "Sort by time..."); 00392 SORT_RECLIST(worklist, comp_redundant); 00393 LOG(VB_SCHEDULE, LOG_INFO, "PruneRedundants..."); 00394 PruneRedundants(); 00395 00396 LOG(VB_SCHEDULE, LOG_INFO, "Sort by time..."); 00397 SORT_RECLIST(worklist, comp_recstart); 00398 LOG(VB_SCHEDULE, LOG_INFO, "ClearWorkList..."); 00399 bool res = ClearWorkList(); 00400 00401 return res; 00402 } 00403 00408 void Scheduler::FillRecordListFromDB(uint recordid) 00409 { 00410 struct timeval fillstart, fillend; 00411 float matchTime, checkTime, placeTime; 00412 00413 MSqlQuery query(dbConn); 00414 QString thequery; 00415 QString where = ""; 00416 00417 // This will cause our temp copy of recordmatch to be empty 00418 if (recordid == 0) 00419 where = "WHERE recordid IS NULL "; 00420 00421 thequery = QString("CREATE TEMPORARY TABLE recordmatch ") + 00422 "SELECT * FROM recordmatch " + where + "; "; 00423 00424 query.prepare(thequery); 00425 recordmatchLock.lock(); 00426 bool ok = query.exec(); 00427 recordmatchLock.unlock(); 00428 if (!ok) 00429 { 00430 MythDB::DBError("FillRecordListFromDB", query); 00431 return; 00432 } 00433 00434 thequery = "ALTER TABLE recordmatch " 00435 " ADD UNIQUE INDEX (recordid, chanid, starttime); "; 00436 query.prepare(thequery); 00437 if (!query.exec()) 00438 { 00439 MythDB::DBError("FillRecordListFromDB", query); 00440 return; 00441 } 00442 00443 thequery = "ALTER TABLE recordmatch " 00444 " ADD INDEX (chanid, starttime, manualid); "; 00445 query.prepare(thequery); 00446 if (!query.exec()) 00447 { 00448 MythDB::DBError("FillRecordListFromDB", query); 00449 return; 00450 } 00451 00452 QMutexLocker locker(&schedLock); 00453 00454 gettimeofday(&fillstart, NULL); 00455 UpdateMatches(recordid, 0, 0, QDateTime()); 00456 gettimeofday(&fillend, NULL); 00457 matchTime = ((fillend.tv_sec - fillstart.tv_sec ) * 1000000 + 00458 (fillend.tv_usec - fillstart.tv_usec)) / 1000000.0; 00459 00460 LOG(VB_SCHEDULE, LOG_INFO, "CreateTempTables..."); 00461 CreateTempTables(); 00462 00463 gettimeofday(&fillstart, NULL); 00464 LOG(VB_SCHEDULE, LOG_INFO, "UpdateDuplicates..."); 00465 UpdateDuplicates(); 00466 gettimeofday(&fillend, NULL); 00467 checkTime = ((fillend.tv_sec - fillstart.tv_sec ) * 1000000 + 00468 (fillend.tv_usec - fillstart.tv_usec)) / 1000000.0; 00469 00470 gettimeofday(&fillstart, NULL); 00471 FillRecordList(); 00472 gettimeofday(&fillend, NULL); 00473 placeTime = ((fillend.tv_sec - fillstart.tv_sec ) * 1000000 + 00474 (fillend.tv_usec - fillstart.tv_usec)) / 1000000.0; 00475 00476 LOG(VB_SCHEDULE, LOG_INFO, "DeleteTempTables..."); 00477 DeleteTempTables(); 00478 00479 MSqlQuery queryDrop(dbConn); 00480 queryDrop.prepare("DROP TABLE recordmatch;"); 00481 if (!queryDrop.exec()) 00482 { 00483 MythDB::DBError("FillRecordListFromDB", queryDrop); 00484 return; 00485 } 00486 00487 QString msg; 00488 msg.sprintf("Speculative scheduled %d items in %.1f " 00489 "= %.2f match + %.2f check + %.2f place", 00490 (int)reclist.size(), 00491 matchTime + checkTime + placeTime, 00492 matchTime, checkTime, placeTime); 00493 LOG(VB_GENERAL, LOG_INFO, msg); 00494 } 00495 00496 void Scheduler::FillRecordListFromMaster(void) 00497 { 00498 RecordingList schedList(false); 00499 bool dummy; 00500 LoadFromScheduler(schedList, dummy); 00501 00502 QMutexLocker lockit(&schedLock); 00503 00504 RecordingList::iterator it = schedList.begin(); 00505 for (; it != schedList.end(); ++it) 00506 reclist.push_back(*it); 00507 } 00508 00509 void Scheduler::PrintList(RecList &list, bool onlyFutureRecordings) 00510 { 00511 if (!VERBOSE_LEVEL_CHECK(VB_SCHEDULE, LOG_DEBUG)) 00512 return; 00513 00514 QDateTime now = QDateTime::currentDateTime(); 00515 00516 LOG(VB_SCHEDULE, LOG_INFO, "--- print list start ---"); 00517 LOG(VB_SCHEDULE, LOG_INFO, "Title - Subtitle Ch Station " 00518 "Day Start End S C I T N Pri"); 00519 00520 RecIter i = list.begin(); 00521 for ( ; i != list.end(); ++i) 00522 { 00523 RecordingInfo *first = (*i); 00524 00525 if (onlyFutureRecordings && 00526 ((first->GetRecordingEndTime() < now && 00527 first->GetScheduledEndTime() < now) || 00528 (first->GetRecordingStartTime() < now && !Recording(first)))) 00529 continue; 00530 00531 PrintRec(first); 00532 } 00533 00534 LOG(VB_SCHEDULE, LOG_INFO, "--- print list end ---"); 00535 } 00536 00537 void Scheduler::PrintRec(const RecordingInfo *p, const char *prefix) 00538 { 00539 if (!VERBOSE_LEVEL_CHECK(VB_SCHEDULE, LOG_DEBUG)) 00540 return; 00541 00542 QString outstr; 00543 00544 if (prefix) 00545 outstr = QString(prefix); 00546 00547 QString episode = p->toString(ProgramInfo::kTitleSubtitle, " - ", ""); 00548 episode = episode.leftJustified(34 - (prefix ? strlen(prefix) : 0), 00549 ' ', true); 00550 00551 outstr += QString("%1 %2 %3 %4-%5 %6 %7 %8 ") 00552 .arg(episode) 00553 .arg(p->GetChanNum().rightJustified(4, ' ')) 00554 .arg(p->GetChannelSchedulingID().leftJustified(7, ' ', true)) 00555 .arg(p->GetRecordingStartTime().toString("dd hh:mm")) 00556 .arg(p->GetRecordingEndTime().toString("hh:mm")) 00557 .arg(p->GetSourceID()) 00558 .arg(p->GetCardID()) 00559 .arg(p->GetInputID()); 00560 outstr += QString("%1 %2 %3") 00561 .arg(toQChar(p->GetRecordingRuleType())) 00562 .arg(toString(p->GetRecordingStatus(), p->GetCardID())) 00563 .arg(p->GetRecordingPriority()); 00564 if (p->GetRecordingPriority2()) 00565 outstr += QString("/%1").arg(p->GetRecordingPriority2()); 00566 00567 LOG(VB_SCHEDULE, LOG_INFO, outstr); 00568 } 00569 00570 void Scheduler::UpdateRecStatus(RecordingInfo *pginfo) 00571 { 00572 QMutexLocker lockit(&schedLock); 00573 00574 RecIter dreciter = reclist.begin(); 00575 for (; dreciter != reclist.end(); ++dreciter) 00576 { 00577 RecordingInfo *p = *dreciter; 00578 if (p->IsSameProgramTimeslot(*pginfo)) 00579 { 00580 // FIXME! If we are passed an rsUnknown recstatus, an 00581 // in-progress recording might be being stopped. Try 00582 // to handle it sensibly until a better fix can be 00583 // made after the 0.25 code freeze. 00584 if (pginfo->GetRecordingStatus() == rsUnknown) 00585 { 00586 if (p->GetRecordingStatus() == rsTuning) 00587 pginfo->SetRecordingStatus(rsFailed); 00588 else if (p->GetRecordingStatus() == rsRecording) 00589 pginfo->SetRecordingStatus(rsRecorded); 00590 else 00591 pginfo->SetRecordingStatus(p->GetRecordingStatus()); 00592 } 00593 00594 if (p->GetRecordingStatus() != pginfo->GetRecordingStatus()) 00595 { 00596 LOG(VB_GENERAL, LOG_INFO, 00597 QString("Updating status for %1 on cardid %2 (%3 => %4)") 00598 .arg(p->toString(ProgramInfo::kTitleSubtitle)) 00599 .arg(p->GetCardID()) 00600 .arg(toString(p->GetRecordingStatus(), 00601 p->GetRecordingRuleType())) 00602 .arg(toString(pginfo->GetRecordingStatus(), 00603 p->GetRecordingRuleType()))); 00604 bool resched = 00605 ((p->GetRecordingStatus() != rsRecording && 00606 p->GetRecordingStatus() != rsTuning) || 00607 (pginfo->GetRecordingStatus() != rsRecording && 00608 pginfo->GetRecordingStatus() != rsTuning)); 00609 p->SetRecordingStatus(pginfo->GetRecordingStatus()); 00610 reclist_changed = true; 00611 p->AddHistory(false); 00612 if (resched) 00613 { 00614 EnqueueCheck(*p, "UpdateRecStatus1"); 00615 reschedWait.wakeOne(); 00616 } 00617 else 00618 { 00619 MythEvent me("SCHEDULE_CHANGE"); 00620 gCoreContext->dispatch(me); 00621 } 00622 } 00623 return; 00624 } 00625 } 00626 } 00627 00628 void Scheduler::UpdateRecStatus(uint cardid, uint chanid, 00629 const QDateTime &startts, 00630 RecStatusType recstatus, 00631 const QDateTime &recendts) 00632 { 00633 QMutexLocker lockit(&schedLock); 00634 00635 RecIter dreciter = reclist.begin(); 00636 for (; dreciter != reclist.end(); ++dreciter) 00637 { 00638 RecordingInfo *p = *dreciter; 00639 if (p->GetCardID() == cardid && p->GetChanID() == chanid && 00640 p->GetScheduledStartTime() == startts) 00641 { 00642 p->SetRecordingEndTime(recendts); 00643 00644 if (p->GetRecordingStatus() != recstatus) 00645 { 00646 LOG(VB_GENERAL, LOG_INFO, 00647 QString("Updating status for %1 on cardid %2 (%3 => %4)") 00648 .arg(p->toString(ProgramInfo::kTitleSubtitle)) 00649 .arg(p->GetCardID()) 00650 .arg(toString(p->GetRecordingStatus(), 00651 p->GetRecordingRuleType())) 00652 .arg(toString(recstatus, 00653 p->GetRecordingRuleType()))); 00654 bool resched = 00655 ((p->GetRecordingStatus() != rsRecording && 00656 p->GetRecordingStatus() != rsTuning) || 00657 (recstatus != rsRecording && 00658 recstatus != rsTuning)); 00659 p->SetRecordingStatus(recstatus); 00660 reclist_changed = true; 00661 p->AddHistory(false); 00662 if (resched) 00663 { 00664 EnqueueCheck(*p, "UpdateRecStatus2"); 00665 reschedWait.wakeOne(); 00666 } 00667 else 00668 { 00669 MythEvent me("SCHEDULE_CHANGE"); 00670 gCoreContext->dispatch(me); 00671 } 00672 } 00673 return; 00674 } 00675 } 00676 } 00677 00678 bool Scheduler::ChangeRecordingEnd(RecordingInfo *oldp, RecordingInfo *newp) 00679 { 00680 QMutexLocker lockit(&schedLock); 00681 00682 if (reclist_changed) 00683 return false; 00684 00685 RecordingType oldrectype = oldp->GetRecordingRuleType(); 00686 uint oldrecordid = oldp->GetRecordingRuleID(); 00687 QDateTime oldrecendts = oldp->GetRecordingEndTime(); 00688 00689 oldp->SetRecordingRuleType(newp->GetRecordingRuleType()); 00690 oldp->SetRecordingRuleID(newp->GetRecordingRuleID()); 00691 oldp->SetRecordingEndTime(newp->GetRecordingEndTime()); 00692 00693 if (specsched) 00694 { 00695 if (newp->GetRecordingEndTime() < QDateTime::currentDateTime()) 00696 { 00697 oldp->SetRecordingStatus(rsRecorded); 00698 newp->SetRecordingStatus(rsRecorded); 00699 return false; 00700 } 00701 else 00702 return true; 00703 } 00704 00705 EncoderLink *tv = (*m_tvList)[oldp->GetCardID()]; 00706 RecStatusType rs = tv->StartRecording(oldp); 00707 if (rs != rsRecording) 00708 { 00709 LOG(VB_GENERAL, LOG_ERR, 00710 QString("Failed to change end time on card %1 to %2") 00711 .arg(oldp->GetCardID()) 00712 .arg(newp->GetRecordingEndTime(ISODate))); 00713 oldp->SetRecordingRuleType(oldrectype); 00714 oldp->SetRecordingRuleID(oldrecordid); 00715 oldp->SetRecordingEndTime(oldrecendts); 00716 } 00717 else 00718 { 00719 RecIter i = reclist.begin(); 00720 for (; i != reclist.end(); ++i) 00721 { 00722 RecordingInfo *recp = *i; 00723 if (recp->IsSameTimeslot(*oldp)) 00724 { 00725 *recp = *oldp; 00726 break; 00727 } 00728 } 00729 } 00730 00731 return rs == rsRecording; 00732 } 00733 00734 void Scheduler::SlaveConnected(RecordingList &slavelist) 00735 { 00736 QMutexLocker lockit(&schedLock); 00737 00738 RecordingList::iterator it = slavelist.begin(); 00739 for (; it != slavelist.end(); ++it) 00740 { 00741 RecordingInfo *sp = *it; 00742 bool found = false; 00743 00744 RecIter ri = reclist.begin(); 00745 for ( ; ri != reclist.end(); ++ri) 00746 { 00747 RecordingInfo *rp = *ri; 00748 00749 if (sp->GetInputID() && 00750 sp->GetScheduledStartTime() == rp->GetScheduledStartTime() && 00751 sp->GetChannelSchedulingID().compare( 00752 rp->GetChannelSchedulingID(), Qt::CaseInsensitive) == 0 && 00753 sp->GetTitle().compare(rp->GetTitle(), 00754 Qt::CaseInsensitive) == 0) 00755 { 00756 if (sp->GetCardID() == rp->GetCardID()) 00757 { 00758 found = true; 00759 rp->SetRecordingStatus(sp->GetRecordingStatus()); 00760 reclist_changed = true; 00761 rp->AddHistory(false); 00762 LOG(VB_GENERAL, LOG_INFO, 00763 QString("setting %1/%2/\"%3\" as %4") 00764 .arg(sp->GetCardID()) 00765 .arg(sp->GetChannelSchedulingID()) 00766 .arg(sp->GetTitle()) 00767 .arg(toUIState(sp->GetRecordingStatus()))); 00768 } 00769 else 00770 { 00771 LOG(VB_GENERAL, LOG_NOTICE, 00772 QString("%1/%2/\"%3\" is already recording on card %4") 00773 .arg(sp->GetCardID()) 00774 .arg(sp->GetChannelSchedulingID()) 00775 .arg(sp->GetTitle()) 00776 .arg(rp->GetCardID())); 00777 } 00778 } 00779 else if (sp->GetCardID() == rp->GetCardID() && 00780 (rp->GetRecordingStatus() == rsRecording || 00781 rp->GetRecordingStatus() == rsTuning)) 00782 { 00783 rp->SetRecordingStatus(rsAborted); 00784 reclist_changed = true; 00785 rp->AddHistory(false); 00786 LOG(VB_GENERAL, LOG_INFO, 00787 QString("setting %1/%2/\"%3\" as aborted") 00788 .arg(rp->GetCardID()) 00789 .arg(rp->GetChannelSchedulingID()) 00790 .arg(rp->GetTitle())); 00791 } 00792 } 00793 00794 if (sp->GetInputID() && !found) 00795 { 00796 reclist.push_back(new RecordingInfo(*sp)); 00797 reclist_changed = true; 00798 sp->AddHistory(false); 00799 LOG(VB_GENERAL, LOG_INFO, 00800 QString("adding %1/%2/\"%3\" as recording") 00801 .arg(sp->GetCardID()) 00802 .arg(sp->GetChannelSchedulingID()) 00803 .arg(sp->GetTitle())); 00804 } 00805 } 00806 } 00807 00808 void Scheduler::SlaveDisconnected(uint cardid) 00809 { 00810 QMutexLocker lockit(&schedLock); 00811 00812 RecIter ri = reclist.begin(); 00813 for ( ; ri != reclist.end(); ++ri) 00814 { 00815 RecordingInfo *rp = *ri; 00816 00817 if (rp->GetCardID() == cardid && 00818 (rp->GetRecordingStatus() == rsRecording || 00819 rp->GetRecordingStatus() == rsTuning)) 00820 { 00821 rp->SetRecordingStatus(rsAborted); 00822 reclist_changed = true; 00823 rp->AddHistory(false); 00824 LOG(VB_GENERAL, LOG_INFO, QString("setting %1/%2/\"%3\" as aborted") 00825 .arg(rp->GetCardID()).arg(rp->GetChannelSchedulingID()) 00826 .arg(rp->GetTitle())); 00827 } 00828 } 00829 } 00830 00831 void Scheduler::BuildWorkList(void) 00832 { 00833 reclist_changed = false; 00834 00835 RecIter i = reclist.begin(); 00836 for (; i != reclist.end(); ++i) 00837 { 00838 RecordingInfo *p = *i; 00839 if (p->GetRecordingStatus() == rsRecording || 00840 p->GetRecordingStatus() == rsTuning) 00841 worklist.push_back(new RecordingInfo(*p)); 00842 } 00843 } 00844 00845 bool Scheduler::ClearWorkList(void) 00846 { 00847 RecordingInfo *p; 00848 00849 if (reclist_changed) 00850 { 00851 while (!worklist.empty()) 00852 { 00853 p = worklist.front(); 00854 delete p; 00855 worklist.pop_front(); 00856 } 00857 00858 return false; 00859 } 00860 00861 while (!reclist.empty()) 00862 { 00863 p = reclist.front(); 00864 delete p; 00865 reclist.pop_front(); 00866 } 00867 00868 while (!worklist.empty()) 00869 { 00870 p = worklist.front(); 00871 reclist.push_back(p); 00872 worklist.pop_front(); 00873 } 00874 00875 return true; 00876 } 00877 00878 static void erase_nulls(RecList &reclist) 00879 { 00880 RecIter it = reclist.begin(); 00881 uint dst = 0; 00882 for (it = reclist.begin(); it != reclist.end(); ++it) 00883 { 00884 if (*it) 00885 { 00886 reclist[dst] = *it; 00887 dst++; 00888 } 00889 } 00890 reclist.resize(dst); 00891 } 00892 00893 void Scheduler::PruneOverlaps(void) 00894 { 00895 RecordingInfo *lastp = NULL; 00896 00897 RecIter dreciter = worklist.begin(); 00898 while (dreciter != worklist.end()) 00899 { 00900 RecordingInfo *p = *dreciter; 00901 if (!lastp || lastp->GetRecordingRuleID() == p->GetRecordingRuleID() || 00902 !lastp->IsSameTimeslot(*p)) 00903 { 00904 lastp = p; 00905 ++dreciter; 00906 } 00907 else 00908 { 00909 delete p; 00910 *(dreciter++) = NULL; 00911 } 00912 } 00913 00914 erase_nulls(worklist); 00915 } 00916 00917 void Scheduler::BuildListMaps(void) 00918 { 00919 RecIter i = worklist.begin(); 00920 for ( ; i != worklist.end(); ++i) 00921 { 00922 RecordingInfo *p = *i; 00923 if (p->GetRecordingStatus() == rsRecording || 00924 p->GetRecordingStatus() == rsTuning || 00925 p->GetRecordingStatus() == rsWillRecord || 00926 p->GetRecordingStatus() == rsUnknown) 00927 { 00928 conflictlist.push_back(p); 00929 titlelistmap[p->GetTitle().toLower()].push_back(p); 00930 recordidlistmap[p->GetRecordingRuleID()].push_back(p); 00931 } 00932 } 00933 } 00934 00935 void Scheduler::ClearListMaps(void) 00936 { 00937 conflictlist.clear(); 00938 titlelistmap.clear(); 00939 recordidlistmap.clear(); 00940 cache_is_same_program.clear(); 00941 } 00942 00943 bool Scheduler::IsSameProgram( 00944 const RecordingInfo *a, const RecordingInfo *b) const 00945 { 00946 IsSameKey X(a,b); 00947 IsSameCacheType::const_iterator it = cache_is_same_program.find(X); 00948 if (it != cache_is_same_program.end()) 00949 return *it; 00950 00951 IsSameKey Y(b,a); 00952 it = cache_is_same_program.find(Y); 00953 if (it != cache_is_same_program.end()) 00954 return *it; 00955 00956 return cache_is_same_program[X] = a->IsSameProgram(*b); 00957 } 00958 00959 bool Scheduler::FindNextConflict( 00960 const RecList &cardlist, 00961 const RecordingInfo *p, 00962 RecConstIter &j, 00963 int openEnd) const 00964 { 00965 for ( ; j != cardlist.end(); ++j) 00966 { 00967 const RecordingInfo *q = *j; 00968 QString msg; 00969 00970 if (p == q) 00971 continue; 00972 00973 if (!Recording(q)) 00974 continue; 00975 00976 if (debugConflicts) 00977 msg = QString("comparing with '%1' ").arg(q->GetTitle()); 00978 00979 if (p->GetCardID() != q->GetCardID() && 00980 !igrp.GetSharedInputGroup(p->GetInputID(), q->GetInputID())) 00981 { 00982 if (debugConflicts) 00983 msg += " cardid== "; 00984 continue; 00985 } 00986 00987 if (openEnd == 2 || (openEnd == 1 && p->GetChanID() != q->GetChanID())) 00988 { 00989 if (p->GetRecordingEndTime() < q->GetRecordingStartTime() || 00990 p->GetRecordingStartTime() > q->GetRecordingEndTime()) 00991 { 00992 if (debugConflicts) 00993 msg += " no-overlap "; 00994 continue; 00995 } 00996 } 00997 else 00998 { 00999 if (p->GetRecordingEndTime() <= q->GetRecordingStartTime() || 01000 p->GetRecordingStartTime() >= q->GetRecordingEndTime()) 01001 { 01002 if (debugConflicts) 01003 msg += " no-overlap "; 01004 continue; 01005 } 01006 } 01007 01008 if (debugConflicts) 01009 { 01010 LOG(VB_SCHEDULE, LOG_INFO, msg); 01011 LOG(VB_SCHEDULE, LOG_INFO, 01012 QString(" cardid's: %1, %2 Shared input group: %3 " 01013 "mplexid's: %4, %5") 01014 .arg(p->GetCardID()).arg(q->GetCardID()) 01015 .arg(igrp.GetSharedInputGroup( 01016 p->GetInputID(), q->GetInputID())) 01017 .arg(p->QueryMplexID()).arg(q->QueryMplexID())); 01018 } 01019 01020 // if two inputs are in the same input group we have a conflict 01021 // unless the programs are on the same multiplex. 01022 if (p->GetCardID() != q->GetCardID()) 01023 { 01024 uint p_mplexid = p->QueryMplexID(); 01025 if (p_mplexid && (p_mplexid == q->QueryMplexID())) 01026 continue; 01027 } 01028 01029 if (debugConflicts) 01030 LOG(VB_SCHEDULE, LOG_INFO, "Found conflict"); 01031 01032 return true; 01033 } 01034 01035 if (debugConflicts) 01036 LOG(VB_SCHEDULE, LOG_INFO, "No conflict"); 01037 01038 return false; 01039 } 01040 01041 const RecordingInfo *Scheduler::FindConflict( 01042 const RecordingInfo *p, 01043 int openend) const 01044 { 01045 RecConstIter k = conflictlist.begin(); 01046 if (FindNextConflict(conflictlist, p, k, openend)) 01047 return *k; 01048 01049 return NULL; 01050 } 01051 01052 void Scheduler::MarkOtherShowings(RecordingInfo *p) 01053 { 01054 RecList *showinglist; 01055 01056 showinglist = &titlelistmap[p->GetTitle().toLower()]; 01057 MarkShowingsList(*showinglist, p); 01058 01059 if (p->GetRecordingRuleType() == kFindOneRecord || 01060 p->GetRecordingRuleType() == kFindDailyRecord || 01061 p->GetRecordingRuleType() == kFindWeeklyRecord) 01062 { 01063 showinglist = &recordidlistmap[p->GetRecordingRuleID()]; 01064 MarkShowingsList(*showinglist, p); 01065 } 01066 else if (p->GetRecordingRuleType() == kOverrideRecord && p->GetFindID()) 01067 { 01068 showinglist = &recordidlistmap[p->GetParentRecordingRuleID()]; 01069 MarkShowingsList(*showinglist, p); 01070 } 01071 } 01072 01073 void Scheduler::MarkShowingsList(RecList &showinglist, RecordingInfo *p) 01074 { 01075 RecIter i = showinglist.begin(); 01076 for ( ; i != showinglist.end(); ++i) 01077 { 01078 RecordingInfo *q = *i; 01079 if (q == p) 01080 continue; 01081 if (q->GetRecordingStatus() != rsUnknown && 01082 q->GetRecordingStatus() != rsWillRecord && 01083 q->GetRecordingStatus() != rsEarlierShowing && 01084 q->GetRecordingStatus() != rsLaterShowing) 01085 continue; 01086 if (q->IsSameTimeslot(*p)) 01087 q->SetRecordingStatus(rsLaterShowing); 01088 else if (q->GetRecordingRuleType() != kSingleRecord && 01089 q->GetRecordingRuleType() != kOverrideRecord && 01090 IsSameProgram(q,p)) 01091 { 01092 if (q->GetRecordingStartTime() < p->GetRecordingStartTime()) 01093 q->SetRecordingStatus(rsLaterShowing); 01094 else 01095 q->SetRecordingStatus(rsEarlierShowing); 01096 } 01097 } 01098 } 01099 01100 void Scheduler::BackupRecStatus(void) 01101 { 01102 RecIter i = worklist.begin(); 01103 for ( ; i != worklist.end(); ++i) 01104 { 01105 RecordingInfo *p = *i; 01106 p->savedrecstatus = p->GetRecordingStatus(); 01107 } 01108 } 01109 01110 void Scheduler::RestoreRecStatus(void) 01111 { 01112 RecIter i = worklist.begin(); 01113 for ( ; i != worklist.end(); ++i) 01114 { 01115 RecordingInfo *p = *i; 01116 p->SetRecordingStatus(p->savedrecstatus); 01117 } 01118 } 01119 01120 bool Scheduler::TryAnotherShowing(RecordingInfo *p, bool samePriority, 01121 bool preserveLive) 01122 { 01123 PrintRec(p, " >"); 01124 01125 if (p->GetRecordingStatus() == rsRecording || 01126 p->GetRecordingStatus() == rsTuning) 01127 return false; 01128 01129 RecList *showinglist = &recordidlistmap[p->GetRecordingRuleID()]; 01130 01131 RecStatusType oldstatus = p->GetRecordingStatus(); 01132 p->SetRecordingStatus(rsLaterShowing); 01133 01134 bool hasLaterShowing = false; 01135 01136 RecIter j = showinglist->begin(); 01137 for ( ; j != showinglist->end(); ++j) 01138 { 01139 RecordingInfo *q = *j; 01140 if (q == p) 01141 continue; 01142 01143 if (samePriority && 01144 (q->GetRecordingPriority() < p->GetRecordingPriority())) 01145 { 01146 continue; 01147 } 01148 01149 hasLaterShowing = false; 01150 01151 if (q->GetRecordingStatus() != rsEarlierShowing && 01152 q->GetRecordingStatus() != rsLaterShowing && 01153 q->GetRecordingStatus() != rsUnknown) 01154 { 01155 continue; 01156 } 01157 01158 if (!p->IsSameTimeslot(*q)) 01159 { 01160 if (!IsSameProgram(p,q)) 01161 continue; 01162 if ((p->GetRecordingRuleType() == kSingleRecord || 01163 p->GetRecordingRuleType() == kOverrideRecord)) 01164 continue; 01165 if (q->GetRecordingStartTime() < schedTime && 01166 p->GetRecordingStartTime() >= schedTime) 01167 continue; 01168 01169 hasLaterShowing |= preserveLive; 01170 } 01171 01172 if (samePriority) 01173 PrintRec(q, " %"); 01174 else 01175 PrintRec(q, " $"); 01176 01177 bool failedLiveCheck = false; 01178 if (preserveLive) 01179 { 01180 failedLiveCheck |= 01181 (!livetvpriority || 01182 p->GetRecordingPriority() - prefinputpri > 01183 q->GetRecordingPriority()); 01184 01185 // It is pointless to preempt another livetv session. 01186 // (the retrylist contains dummy livetv pginfo's) 01187 RecConstIter k = retrylist.begin(); 01188 if (FindNextConflict(retrylist, q, k)) 01189 { 01190 PrintRec(*k, " L!"); 01191 continue; 01192 } 01193 } 01194 01195 const RecordingInfo *conflict = FindConflict(q); 01196 if (conflict) 01197 { 01198 PrintRec(conflict, " !"); 01199 continue; 01200 } 01201 01202 if (hasLaterShowing) 01203 { 01204 QString id = p->MakeUniqueSchedulerKey(); 01205 hasLaterList[id] = true; 01206 continue; 01207 } 01208 01209 if (failedLiveCheck) 01210 { 01211 // Failed the priority check or "Move scheduled shows to 01212 // avoid LiveTV feature" is turned off. 01213 // However, there is no conflict so if this alternate showing 01214 // is on an equivalent virtual card, allow the move. 01215 bool equiv = (p->GetSourceID() == q->GetSourceID() && 01216 igrp.GetSharedInputGroup( 01217 p->GetInputID(), q->GetInputID())); 01218 01219 if (!equiv) 01220 continue; 01221 } 01222 01223 if (preserveLive) 01224 { 01225 QString msg = QString( 01226 "Moved \"%1\" on chanid: %2 from card: %3 to %4 " 01227 "to avoid LiveTV conflict") 01228 .arg(p->GetTitle()).arg(p->GetChanID()) 01229 .arg(p->GetCardID()).arg(q->GetCardID()); 01230 LOG(VB_SCHEDULE, LOG_INFO, msg); 01231 } 01232 01233 q->SetRecordingStatus(rsWillRecord); 01234 MarkOtherShowings(q); 01235 PrintRec(p, " -"); 01236 PrintRec(q, " +"); 01237 return true; 01238 } 01239 01240 p->SetRecordingStatus(oldstatus); 01241 return false; 01242 } 01243 01244 void Scheduler::SchedNewRecords(void) 01245 { 01246 if (VERBOSE_LEVEL_CHECK(VB_SCHEDULE, LOG_DEBUG)) 01247 { 01248 LOG(VB_SCHEDULE, LOG_DEBUG, 01249 "+ = schedule this showing to be recorded"); 01250 LOG(VB_SCHEDULE, LOG_DEBUG, 01251 "# = could not schedule this showing, retry later"); 01252 LOG(VB_SCHEDULE, LOG_DEBUG, 01253 "! = conflict caused by this showing"); 01254 LOG(VB_SCHEDULE, LOG_DEBUG, 01255 "/ = retry this showing, same priority pass"); 01256 LOG(VB_SCHEDULE, LOG_DEBUG, 01257 "? = retry this showing, lower priority pass"); 01258 LOG(VB_SCHEDULE, LOG_DEBUG, 01259 "> = try another showing for this program"); 01260 LOG(VB_SCHEDULE, LOG_DEBUG, 01261 "% = found another showing, same priority required"); 01262 LOG(VB_SCHEDULE, LOG_DEBUG, 01263 "$ = found another showing, lower priority allowed"); 01264 LOG(VB_SCHEDULE, LOG_DEBUG, 01265 "- = unschedule a showing in favor of another one"); 01266 } 01267 01268 int openEnd = gCoreContext->GetNumSetting("SchedOpenEnd", 0); 01269 01270 RecIter i = worklist.begin(); 01271 while (i != worklist.end()) 01272 { 01273 RecordingInfo *p = *i; 01274 if (p->GetRecordingStatus() == rsRecording || 01275 p->GetRecordingStatus() == rsTuning) 01276 MarkOtherShowings(p); 01277 else if (p->GetRecordingStatus() == rsUnknown) 01278 { 01279 const RecordingInfo *conflict = FindConflict(p, openEnd); 01280 if (!conflict) 01281 { 01282 p->SetRecordingStatus(rsWillRecord); 01283 01284 if (p->GetRecordingStartTime() < schedTime.addSecs(90)) 01285 { 01286 QString id = p->MakeUniqueSchedulerKey(); 01287 if (!recPendingList.contains(id)) 01288 recPendingList[id] = false; 01289 01290 livetvTime = (livetvTime < schedTime) ? 01291 schedTime : livetvTime; 01292 } 01293 01294 MarkOtherShowings(p); 01295 PrintRec(p, " +"); 01296 } 01297 else 01298 { 01299 retrylist.push_front(p); 01300 PrintRec(p, " #"); 01301 PrintRec(conflict, " !"); 01302 } 01303 } 01304 01305 int lastpri = p->GetRecordingPriority(); 01306 ++i; 01307 if (i == worklist.end() || lastpri != (*i)->GetRecordingPriority()) 01308 { 01309 MoveHigherRecords(); 01310 retrylist.clear(); 01311 } 01312 } 01313 } 01314 01315 void Scheduler::MoveHigherRecords(bool move_this) 01316 { 01317 RecIter i = retrylist.begin(); 01318 for ( ; move_this && i != retrylist.end(); ++i) 01319 { 01320 RecordingInfo *p = *i; 01321 if (p->GetRecordingStatus() != rsUnknown) 01322 continue; 01323 01324 PrintRec(p, " /"); 01325 01326 BackupRecStatus(); 01327 p->SetRecordingStatus(rsWillRecord); 01328 MarkOtherShowings(p); 01329 01330 RecConstIter k = conflictlist.begin(); 01331 for ( ; FindNextConflict(conflictlist, p, k); ++k) 01332 { 01333 if (!TryAnotherShowing(*k, true)) 01334 { 01335 RestoreRecStatus(); 01336 break; 01337 } 01338 } 01339 01340 if (p->GetRecordingStatus() == rsWillRecord) 01341 PrintRec(p, " +"); 01342 } 01343 01344 i = retrylist.begin(); 01345 for ( ; i != retrylist.end(); ++i) 01346 { 01347 RecordingInfo *p = *i; 01348 if (p->GetRecordingStatus() != rsUnknown) 01349 continue; 01350 01351 PrintRec(p, " ?"); 01352 01353 if (move_this && TryAnotherShowing(p, false)) 01354 continue; 01355 01356 BackupRecStatus(); 01357 p->SetRecordingStatus(rsWillRecord); 01358 if (move_this) 01359 MarkOtherShowings(p); 01360 01361 RecConstIter k = conflictlist.begin(); 01362 for ( ; FindNextConflict(conflictlist, p, k); ++k) 01363 { 01364 if ((p->GetRecordingPriority() < (*k)->GetRecordingPriority() && 01365 !schedMoveHigher && move_this) || 01366 !TryAnotherShowing(*k, false, !move_this)) 01367 { 01368 RestoreRecStatus(); 01369 break; 01370 } 01371 } 01372 01373 if (move_this && p->GetRecordingStatus() == rsWillRecord) 01374 PrintRec(p, " +"); 01375 } 01376 } 01377 01378 void Scheduler::PruneRedundants(void) 01379 { 01380 RecordingInfo *lastp = NULL; 01381 01382 RecIter i = worklist.begin(); 01383 while (i != worklist.end()) 01384 { 01385 RecordingInfo *p = *i; 01386 01387 // Delete anything that has already passed since we can't 01388 // change history, can we? 01389 if (p->GetRecordingStatus() != rsRecording && 01390 p->GetRecordingStatus() != rsTuning && 01391 p->GetRecordingStatus() != rsMissedFuture && 01392 p->GetScheduledEndTime() < schedTime && 01393 p->GetRecordingEndTime() < schedTime) 01394 { 01395 delete p; 01396 *(i++) = NULL; 01397 continue; 01398 } 01399 01400 // Check for rsConflict 01401 if (p->GetRecordingStatus() == rsUnknown) 01402 p->SetRecordingStatus(rsConflict); 01403 01404 // Restore the old status for some selected cases. 01405 if (p->GetRecordingStatus() == rsMissedFuture || 01406 (p->GetRecordingStatus() == rsMissed && 01407 p->oldrecstatus != rsUnknown) || 01408 (p->GetRecordingStatus() == rsCurrentRecording && 01409 p->oldrecstatus == rsPreviousRecording && !p->future) || 01410 (p->GetRecordingStatus() != rsWillRecord && 01411 p->oldrecstatus == rsAborted)) 01412 { 01413 RecStatusType rs = p->GetRecordingStatus(); 01414 p->SetRecordingStatus(p->oldrecstatus); 01415 // Re-mark rsMissedFuture entries so non-future history 01416 // will be saved in the scheduler thread. 01417 if (rs == rsMissedFuture) 01418 p->oldrecstatus = rsMissedFuture; 01419 } 01420 01421 if (!Recording(p)) 01422 { 01423 p->SetCardID(0); 01424 p->SetInputID(0); 01425 } 01426 01427 // Check for redundant against last non-deleted 01428 if (!lastp || lastp->GetRecordingRuleID() != p->GetRecordingRuleID() || 01429 !lastp->IsSameTimeslot(*p)) 01430 { 01431 lastp = p; 01432 lastp->SetRecordingPriority2(0); 01433 ++i; 01434 } 01435 else 01436 { 01437 // Flag lower priority showings that will recorded so we 01438 // can warn the user about them 01439 if (lastp->GetRecordingStatus() == rsWillRecord && 01440 p->GetRecordingPriority() > 01441 lastp->GetRecordingPriority() - lastp->GetRecordingPriority2()) 01442 { 01443 lastp->SetRecordingPriority2( 01444 lastp->GetRecordingPriority() - p->GetRecordingPriority()); 01445 } 01446 delete p; 01447 *(i++) = NULL; 01448 } 01449 } 01450 01451 erase_nulls(worklist); 01452 } 01453 01454 void Scheduler::UpdateNextRecord(void) 01455 { 01456 if (specsched) 01457 return; 01458 01459 QMap<int, QDateTime> nextRecMap; 01460 01461 RecIter i = reclist.begin(); 01462 while (i != reclist.end()) 01463 { 01464 RecordingInfo *p = *i; 01465 if (p->GetRecordingStatus() == rsWillRecord && 01466 nextRecMap[p->GetRecordingRuleID()].isNull()) 01467 { 01468 nextRecMap[p->GetRecordingRuleID()] = p->GetRecordingStartTime(); 01469 } 01470 01471 if (p->GetRecordingRuleType() == kOverrideRecord && 01472 p->GetParentRecordingRuleID() > 0 && 01473 p->GetRecordingStatus() == rsWillRecord && 01474 nextRecMap[p->GetParentRecordingRuleID()].isNull()) 01475 { 01476 nextRecMap[p->GetParentRecordingRuleID()] = 01477 p->GetRecordingStartTime(); 01478 } 01479 ++i; 01480 } 01481 01482 MSqlQuery query(dbConn); 01483 query.prepare("SELECT recordid, next_record FROM record;"); 01484 01485 if (query.exec() && query.isActive()) 01486 { 01487 MSqlQuery subquery(dbConn); 01488 01489 while (query.next()) 01490 { 01491 int recid = query.value(0).toInt(); 01492 QDateTime next_record = query.value(1).toDateTime(); 01493 01494 if (next_record == nextRecMap[recid]) 01495 continue; 01496 01497 if (nextRecMap[recid].isValid()) 01498 { 01499 subquery.prepare("UPDATE record SET next_record = :NEXTREC " 01500 "WHERE recordid = :RECORDID;"); 01501 subquery.bindValue(":RECORDID", recid); 01502 subquery.bindValue(":NEXTREC", nextRecMap[recid]); 01503 if (!subquery.exec()) 01504 MythDB::DBError("Update next_record", subquery); 01505 } 01506 else if (next_record.isValid()) 01507 { 01508 subquery.prepare("UPDATE record " 01509 "SET next_record = '0000-00-00 00:00:00' " 01510 "WHERE recordid = :RECORDID;"); 01511 subquery.bindValue(":RECORDID", recid); 01512 if (!subquery.exec()) 01513 MythDB::DBError("Clear next_record", subquery); 01514 } 01515 } 01516 } 01517 } 01518 01519 void Scheduler::getConflicting(RecordingInfo *pginfo, QStringList &strlist) 01520 { 01521 RecList retlist; 01522 getConflicting(pginfo, &retlist); 01523 01524 strlist << QString::number(retlist.size()); 01525 01526 while (!retlist.empty()) 01527 { 01528 RecordingInfo *p = retlist.front(); 01529 p->ToStringList(strlist); 01530 delete p; 01531 retlist.pop_front(); 01532 } 01533 } 01534 01535 void Scheduler::getConflicting(RecordingInfo *pginfo, RecList *retlist) 01536 { 01537 QMutexLocker lockit(&schedLock); 01538 01539 RecConstIter i = reclist.begin(); 01540 for (; FindNextConflict(reclist, pginfo, i); ++i) 01541 { 01542 const RecordingInfo *p = *i; 01543 retlist->push_back(new RecordingInfo(*p)); 01544 } 01545 } 01546 01547 bool Scheduler::GetAllPending(RecList &retList) const 01548 { 01549 QMutexLocker lockit(&schedLock); 01550 01551 bool hasconflicts = false; 01552 01553 RecConstIter it = reclist.begin(); 01554 for (; it != reclist.end(); ++it) 01555 { 01556 if ((*it)->GetRecordingStatus() == rsConflict) 01557 hasconflicts = true; 01558 retList.push_back(new RecordingInfo(**it)); 01559 } 01560 01561 return hasconflicts; 01562 } 01563 01564 QMap<QString,ProgramInfo*> Scheduler::GetRecording(void) const 01565 { 01566 QMutexLocker lockit(&schedLock); 01567 01568 QMap<QString,ProgramInfo*> recMap; 01569 RecConstIter it = reclist.begin(); 01570 for (; it != reclist.end(); ++it) 01571 { 01572 if (rsRecording == (*it)->GetRecordingStatus() || 01573 rsTuning == (*it)->GetRecordingStatus()) 01574 recMap[(*it)->MakeUniqueKey()] = new ProgramInfo(**it); 01575 } 01576 01577 return recMap; 01578 } 01579 01580 RecStatusType Scheduler::GetRecStatus(const ProgramInfo &pginfo) 01581 { 01582 QMutexLocker lockit(&schedLock); 01583 01584 for (RecConstIter it = reclist.begin(); it != reclist.end(); ++it) 01585 { 01586 if (pginfo.IsSameRecording(**it)) 01587 { 01588 return (rsRecording == (**it).GetRecordingStatus() || 01589 rsTuning == (**it).GetRecordingStatus()) ? 01590 (**it).GetRecordingStatus() : pginfo.GetRecordingStatus(); 01591 } 01592 } 01593 01594 return pginfo.GetRecordingStatus(); 01595 } 01596 01597 void Scheduler::GetAllPending(QStringList &strList) const 01598 { 01599 RecList retlist; 01600 bool hasconflicts = GetAllPending(retlist); 01601 01602 strList << QString::number(hasconflicts); 01603 strList << QString::number(retlist.size()); 01604 01605 while (!retlist.empty()) 01606 { 01607 RecordingInfo *p = retlist.front(); 01608 p->ToStringList(strList); 01609 delete p; 01610 retlist.pop_front(); 01611 } 01612 } 01613 01615 void Scheduler::GetAllScheduled(QStringList &strList) 01616 { 01617 RecList schedlist; 01618 01619 GetAllScheduled(schedlist); 01620 01621 strList << QString::number(schedlist.size()); 01622 01623 while (!schedlist.empty()) 01624 { 01625 RecordingInfo *pginfo = schedlist.front(); 01626 pginfo->ToStringList(strList); 01627 delete pginfo; 01628 schedlist.pop_front(); 01629 } 01630 } 01631 01632 void Scheduler::Reschedule(const QStringList &request) 01633 { 01634 QMutexLocker locker(&schedLock); 01635 reschedQueue.enqueue(request); 01636 reschedWait.wakeOne(); 01637 } 01638 01639 void Scheduler::AddRecording(const RecordingInfo &pi) 01640 { 01641 QMutexLocker lockit(&schedLock); 01642 01643 LOG(VB_GENERAL, LOG_INFO, LOC + QString("AddRecording() recid: %1") 01644 .arg(pi.GetRecordingRuleID())); 01645 01646 for (RecIter it = reclist.begin(); it != reclist.end(); ++it) 01647 { 01648 RecordingInfo *p = *it; 01649 if (p->GetRecordingStatus() == rsRecording && 01650 p->IsSameProgramTimeslot(pi)) 01651 { 01652 LOG(VB_GENERAL, LOG_INFO, LOC + "Not adding recording, " + 01653 QString("'%1' is already in reclist.") 01654 .arg(pi.GetTitle())); 01655 return; 01656 } 01657 } 01658 01659 LOG(VB_SCHEDULE, LOG_INFO, LOC + 01660 QString("Adding '%1' to reclist.").arg(pi.GetTitle())); 01661 01662 RecordingInfo * new_pi = new RecordingInfo(pi); 01663 reclist.push_back(new_pi); 01664 reclist_changed = true; 01665 01666 // Save rsRecording recstatus to DB 01667 // This allows recordings to resume on backend restart 01668 new_pi->AddHistory(false); 01669 01670 // Make sure we have a ScheduledRecording instance 01671 new_pi->GetRecordingRule(); 01672 01673 // Trigger reschedule.. 01674 EnqueueMatch(pi.GetRecordingRuleID(), 0, 0, QDateTime(), 01675 QString("AddRecording %1").arg(pi.GetTitle())); 01676 reschedWait.wakeOne(); 01677 } 01678 01679 bool Scheduler::IsBusyRecording(const RecordingInfo *rcinfo) 01680 { 01681 if (!m_tvList || !rcinfo) 01682 { 01683 LOG(VB_GENERAL, LOG_ERR, LOC + 01684 "IsBusyRecording() -> true, no tvList or no rcinfo"); 01685 01686 return true; 01687 } 01688 01689 EncoderLink *rctv = (*m_tvList)[rcinfo->GetCardID()]; 01690 // first check the card we will be recording on... 01691 if (!rctv || rctv->IsBusyRecording()) 01692 return true; 01693 01694 // now check other cards in the same input group as the recording. 01695 TunedInputInfo busy_input; 01696 uint inputid = rcinfo->GetInputID(); 01697 vector<uint> cardids = CardUtil::GetConflictingCards( 01698 inputid, rcinfo->GetCardID()); 01699 for (uint i = 0; i < cardids.size(); i++) 01700 { 01701 rctv = (*m_tvList)[cardids[i]]; 01702 if (!rctv) 01703 { 01704 #if 0 01705 LOG(VB_SCHEDULE, LOG_ERR, LOC + 01706 QString("IsBusyRecording() -> true, rctv(NULL) for card %2") 01707 .arg(cardids[i])); 01708 #endif 01709 return true; 01710 } 01711 01712 if (rctv->IsBusy(&busy_input, -1) && 01713 igrp.GetSharedInputGroup(busy_input.inputid, inputid)) 01714 { 01715 return true; 01716 } 01717 } 01718 01719 return false; 01720 } 01721 01722 void Scheduler::OldRecordedFixups(void) 01723 { 01724 MSqlQuery query(dbConn); 01725 01726 // Mark anything that was recording as aborted. 01727 query.prepare("UPDATE oldrecorded SET recstatus = :RSABORTED " 01728 " WHERE recstatus = :RSRECORDING OR recstatus = :RSTUNING"); 01729 query.bindValue(":RSABORTED", rsAborted); 01730 query.bindValue(":RSRECORDING", rsRecording); 01731 query.bindValue(":RSTUNING", rsTuning); 01732 if (!query.exec()) 01733 MythDB::DBError("UpdateAborted", query); 01734 01735 // Mark anything that was going to record as missed. 01736 query.prepare("UPDATE oldrecorded SET recstatus = :RSMISSED " 01737 "WHERE recstatus = :RSWILLRECORD"); 01738 query.bindValue(":RSMISSED", rsMissed); 01739 query.bindValue(":RSWILLRECORD", rsWillRecord); 01740 if (!query.exec()) 01741 MythDB::DBError("UpdateMissed", query); 01742 01743 // Mark anything that was set to rsCurrentRecording as 01744 // rsPreviousRecording. 01745 query.prepare("UPDATE oldrecorded SET recstatus = :RSPREVIOUS " 01746 "WHERE recstatus = :RSCURRENT"); 01747 query.bindValue(":RSPREVIOUS", rsPreviousRecording); 01748 query.bindValue(":RSCURRENT", rsCurrentRecording); 01749 if (!query.exec()) 01750 MythDB::DBError("UpdateCurrent", query); 01751 01752 // Clear the "future" status of anything older than the maximum 01753 // endoffset. Anything more recent will bee handled elsewhere 01754 // during normal processing. 01755 query.prepare("UPDATE oldrecorded SET future = 0 " 01756 "WHERE future > 0 AND " 01757 " endtime < (NOW() - INTERVAL 475 MINUTE)"); 01758 if (!query.exec()) 01759 MythDB::DBError("UpdateFuture", query); 01760 } 01761 01762 void Scheduler::run(void) 01763 { 01764 RunProlog(); 01765 01766 dbConn = MSqlQuery::SchedCon(); 01767 01768 // Notify constructor that we're actually running 01769 { 01770 QMutexLocker lockit(&schedLock); 01771 reschedWait.wakeAll(); 01772 } 01773 01774 OldRecordedFixups(); 01775 01776 // wait for slaves to connect 01777 sleep(3); 01778 01779 QMutexLocker lockit(&schedLock); 01780 01781 reschedQueue.clear(); 01782 EnqueueMatch(0, 0, 0, QDateTime(), "SchedulerInit"); 01783 01784 int prerollseconds = 0; 01785 int wakeThreshold = 300; 01786 int idleTimeoutSecs = 0; 01787 int idleWaitForRecordingTime = 15; // in minutes 01788 int tuningTimeout = 180; // in seconds 01789 bool blockShutdown = 01790 gCoreContext->GetNumSetting("blockSDWUwithoutClient", 1); 01791 bool firstRun = true; 01792 QDateTime lastSleepCheck = QDateTime::currentDateTime().addDays(-1); 01793 RecIter startIter = reclist.begin(); 01794 QDateTime idleSince = QDateTime(); 01795 int maxSleep = 60000; // maximum sleep time in milliseconds 01796 int schedRunTime = 30; // max scheduler run time in seconds 01797 01798 while (doRun) 01799 { 01800 QDateTime curtime = QDateTime::currentDateTime(); 01801 bool statuschanged = false; 01802 int secs_to_next = (startIter != reclist.end()) ? 01803 curtime.secsTo((*startIter)->GetRecordingStartTime()) : 60*60; 01804 01805 // If we're about to start a recording don't do any reschedules... 01806 // instead sleep for a bit 01807 if (secs_to_next < (schedRunTime + 2)) 01808 { 01809 int msecs = CalcTimeToNextHandleRecordingEvent( 01810 curtime, startIter, reclist, prerollseconds, maxSleep); 01811 LOG(VB_SCHEDULE, LOG_INFO, 01812 QString("sleeping for %1 ms (s2n: %2 sr: %3)") 01813 .arg(msecs).arg(secs_to_next).arg(schedRunTime)); 01814 if (msecs < 100) 01815 (void) ::usleep(msecs * 1000); 01816 else 01817 reschedWait.wait(&schedLock, msecs); 01818 } 01819 else 01820 { 01821 if (reschedQueue.empty()) 01822 { 01823 int sched_sleep = (secs_to_next - schedRunTime - 1) * 1000; 01824 sched_sleep = min(sched_sleep, maxSleep); 01825 if (secs_to_next < prerollseconds + (maxSleep/1000)) 01826 sched_sleep = min(sched_sleep, 5000); 01827 LOG(VB_SCHEDULE, LOG_INFO, 01828 QString("sleeping for %1 ms (interuptable)") 01829 .arg(sched_sleep)); 01830 reschedWait.wait(&schedLock, sched_sleep); 01831 if (!doRun) 01832 break; 01833 } 01834 01835 QTime t; t.start(); 01836 if (!reschedQueue.empty() && HandleReschedule()) 01837 { 01838 statuschanged = true; 01839 startIter = reclist.begin(); 01840 01841 // The master backend is a long lived program, so 01842 // we reload some key settings on each reschedule. 01843 prerollseconds = 01844 gCoreContext->GetNumSetting("RecordPreRoll", 0); 01845 wakeThreshold = 01846 gCoreContext->GetNumSetting("WakeUpThreshold", 300); 01847 idleTimeoutSecs = 01848 gCoreContext->GetNumSetting("idleTimeoutSecs", 0); 01849 idleWaitForRecordingTime = 01850 gCoreContext->GetNumSetting("idleWaitForRecordingTime", 15); 01851 tuningTimeout = 01852 gCoreContext->GetNumSetting("tuningTimeout", 180); 01853 } 01854 int e = t.elapsed(); 01855 if (e > 0) 01856 { 01857 schedRunTime = (firstRun) ? 0 : schedRunTime; 01858 schedRunTime = 01859 max((int)(((e + 999) / 1000) * 1.5f), schedRunTime); 01860 } 01861 01862 if (firstRun) 01863 { 01864 blockShutdown &= HandleRunSchedulerStartup( 01865 prerollseconds, idleWaitForRecordingTime); 01866 firstRun = false; 01867 01868 // HandleRunSchedulerStartup releases the schedLock so the 01869 // reclist may have changed. If it has go to top of loop 01870 // and update secs_to_next... 01871 if (reclist_changed) 01872 continue; 01873 } 01874 01875 // Unless a recording is about to start, check for slaves 01876 // that can be put to sleep if it has been at least five 01877 // minutes since we last put slaves to sleep. 01878 curtime = QDateTime::currentDateTime(); 01879 secs_to_next = (startIter != reclist.end()) ? 01880 curtime.secsTo((*startIter)->GetRecordingStartTime()) : 60*60; 01881 if ((secs_to_next > schedRunTime * 1.5f) && 01882 (lastSleepCheck.secsTo(curtime) > 300)) 01883 { 01884 PutInactiveSlavesToSleep(); 01885 lastSleepCheck = QDateTime::currentDateTime(); 01886 } 01887 } 01888 01889 // Skip past recordings that are already history 01890 // (i.e. AddHistory() has been called setting oldrecstatus) 01891 for ( ; startIter != reclist.end(); ++startIter) 01892 { 01893 if ((*startIter)->GetRecordingStatus() != 01894 (*startIter)->oldrecstatus) 01895 { 01896 break; 01897 } 01898 } 01899 01900 // Start any recordings that are due to be started 01901 // & call RecordPending for recordings due to start in 30 seconds 01902 // & handle rsTuning updates 01903 bool done = false; 01904 for (RecIter it = startIter; it != reclist.end() && !done; ++it) 01905 { 01906 done = HandleRecording( 01907 **it, statuschanged, prerollseconds, tuningTimeout); 01908 } 01909 01911 curtime = QDateTime::currentDateTime(); 01912 for (RecIter it = startIter; it != reclist.end(); ++it) 01913 { 01914 int secsleft = curtime.secsTo((*it)->GetRecordingStartTime()); 01915 if ((secsleft - prerollseconds) <= wakeThreshold) 01916 HandleWakeSlave(**it, prerollseconds); 01917 else 01918 break; 01919 } 01920 01921 if (statuschanged) 01922 { 01923 MythEvent me("SCHEDULE_CHANGE"); 01924 gCoreContext->dispatch(me); 01925 idleSince = QDateTime(); 01926 } 01927 01928 // if idletimeout is 0, the user disabled the auto-shutdown feature 01929 if ((idleTimeoutSecs > 0) && (m_mainServer != NULL)) 01930 { 01931 HandleIdleShutdown(blockShutdown, idleSince, prerollseconds, 01932 idleTimeoutSecs, idleWaitForRecordingTime, 01933 statuschanged); 01934 } 01935 } 01936 01937 RunEpilog(); 01938 } 01939 01940 int Scheduler::CalcTimeToNextHandleRecordingEvent( 01941 const QDateTime &curtime, 01942 RecConstIter startIter, const RecList &reclist, 01943 int prerollseconds, int max_sleep /*ms*/) 01944 { 01945 if (startIter == reclist.end()) 01946 return max_sleep; 01947 01948 int msecs = max_sleep; 01949 for (RecConstIter i = startIter; i != reclist.end() && (msecs > 0); ++i) 01950 { 01951 // Check on recordings that we've told to start, but have 01952 // not yet started every second or so. 01953 if ((*i)->GetRecordingStatus() == rsTuning) 01954 { 01955 msecs = min(msecs, 1000); 01956 continue; 01957 } 01958 01959 // These recordings have already been handled.. 01960 if ((*i)->GetRecordingStatus() == (*i)->oldrecstatus) 01961 continue; 01962 01963 int secs_to_next = curtime.secsTo((*i)->GetRecordingStartTime()); 01964 01965 if (!recPendingList[(*i)->MakeUniqueSchedulerKey()]) 01966 secs_to_next -= 30; 01967 01968 if (secs_to_next < 0) 01969 { 01970 msecs = 0; 01971 break; 01972 } 01973 01974 // This is what normally breaks us out of the loop... 01975 if (secs_to_next > max_sleep) 01976 { 01977 msecs = min(msecs, max_sleep); 01978 break; 01979 } 01980 01981 if (secs_to_next > 31) 01982 { 01983 msecs = min(msecs, 30 * 1000); 01984 continue; 01985 } 01986 01987 if ((secs_to_next-1) * 1000 > msecs) 01988 continue; 01989 01990 if (secs_to_next < 15) 01991 { 01992 QTime st = (*i)->GetRecordingStartTime().time(); 01993 int tmp = curtime.time().msecsTo(st); 01994 tmp = (tmp < 0) ? tmp + 86400000 : tmp; 01995 msecs = (tmp > 15*1000) ? 0 : min(msecs, tmp); 01996 } 01997 else 01998 { 01999 msecs = min(msecs, (secs_to_next-1) * 1000); 02000 } 02001 } 02002 02003 return min(msecs, max_sleep); 02004 } 02005 02006 void Scheduler::ResetDuplicates(uint recordid, uint findid, 02007 const QString &title, const QString &subtitle, 02008 const QString &descrip, 02009 const QString &programid) 02010 { 02011 MSqlQuery query(dbConn); 02012 QString filterClause; 02013 MSqlBindings bindings; 02014 02015 if (!title.isEmpty()) 02016 { 02017 filterClause += "AND p.title = :TITLE "; 02018 bindings[":TITLE"] = title; 02019 } 02020 02021 // "**any**" is special value set in ProgLister::DeleteOldSeries() 02022 if (programid != "**any**") 02023 { 02024 filterClause += "AND (0 "; 02025 if (!subtitle.isEmpty()) 02026 { 02027 // Need to check both for kDupCheckSubThenDesc 02028 filterClause += "OR p.subtitle = :SUBTITLE " 02029 "OR p.description = :SUBTITLE "; 02030 bindings[":SUBTITLE"] = subtitle; 02031 } 02032 if (!descrip.isEmpty()) 02033 { 02034 // Need to check both for kDupCheckSubThenDesc 02035 filterClause += "OR p.description = :DESCRIP " 02036 "OR p.subtitle = :DESCRIP "; 02037 bindings[":DESCRIP"] = descrip; 02038 } 02039 if (!programid.isEmpty()) 02040 { 02041 filterClause += "OR p.programid = :PROGRAMID "; 02042 bindings[":PROGRAMID"] = programid; 02043 } 02044 filterClause += ") "; 02045 } 02046 02047 query.prepare(QString("UPDATE recordmatch rm " 02048 "INNER JOIN %1 r " 02049 " ON rm.recordid = r.recordid " 02050 "INNER JOIN program p " 02051 " ON rm.chanid = p.chanid " 02052 " AND rm.starttime = p.starttime " 02053 " AND rm.manualid = p.manualid " 02054 "SET oldrecduplicate = -1 " 02055 "WHERE p.generic = 0 " 02056 " AND r.type NOT IN (%2, %3, %4) ") 02057 .arg(recordTable) 02058 .arg(kSingleRecord) 02059 .arg(kOverrideRecord) 02060 .arg(kDontRecord) 02061 + filterClause); 02062 MSqlBindings::const_iterator it; 02063 for (it = bindings.begin(); it != bindings.end(); ++it) 02064 query.bindValue(it.key(), it.value()); 02065 if (!query.exec()) 02066 MythDB::DBError("ResetDuplicates1", query); 02067 02068 if (findid && programid != "**any**") 02069 { 02070 query.prepare("UPDATE recordmatch rm " 02071 "SET oldrecduplicate = -1 " 02072 "WHERE rm.recordid = :RECORDID " 02073 " AND rm.findid = :FINDID"); 02074 query.bindValue("RECORDID", recordid); 02075 query.bindValue("FINDID", findid); 02076 if (!query.exec()) 02077 MythDB::DBError("ResetDuplicates2", query); 02078 } 02079 } 02080 02081 bool Scheduler::HandleReschedule(void) 02082 { 02083 // We might have been inactive for a long time, so make 02084 // sure our DB connection is fresh before continuing. 02085 dbConn = MSqlQuery::SchedCon(); 02086 02087 struct timeval fillstart, fillend; 02088 float matchTime, checkTime, placeTime; 02089 02090 gettimeofday(&fillstart, NULL); 02091 QString msg; 02092 bool deleteFuture = false; 02093 bool runCheck = false; 02094 02095 while (!reschedQueue.empty()) 02096 { 02097 QStringList request = reschedQueue.dequeue(); 02098 QStringList tokens; 02099 if (request.size() >= 1) 02100 tokens = request[0].split(' ', QString::SkipEmptyParts); 02101 02102 if (request.size() < 1 || tokens.size() < 1) 02103 { 02104 LOG(VB_GENERAL, LOG_ERR, "Empty Reschedule request received"); 02105 return false; 02106 } 02107 02108 LOG(VB_GENERAL, LOG_INFO, QString("Reschedule requested for %1") 02109 .arg(request.join(" | "))); 02110 02111 if (tokens[0] == "MATCH") 02112 { 02113 if (tokens.size() < 5) 02114 { 02115 LOG(VB_GENERAL, LOG_ERR, 02116 QString("Invalid RescheduleMatch request received (%1)") 02117 .arg(request[0])); 02118 continue; 02119 } 02120 02121 uint recordid = tokens[1].toUInt(); 02122 uint sourceid = tokens[2].toUInt(); 02123 uint mplexid = tokens[3].toUInt(); 02124 QDateTime maxstarttime = 02125 QDateTime::fromString(tokens[4], Qt::ISODate); 02126 deleteFuture = true; 02127 runCheck = true; 02128 schedLock.unlock(); 02129 recordmatchLock.lock(); 02130 UpdateMatches(recordid, sourceid, mplexid, maxstarttime); 02131 recordmatchLock.unlock(); 02132 schedLock.lock(); 02133 } 02134 else if (tokens[0] == "CHECK") 02135 { 02136 if (tokens.size() < 4 || request.size() < 5) 02137 { 02138 LOG(VB_GENERAL, LOG_ERR, 02139 QString("Invalid RescheduleCheck request received (%1)") 02140 .arg(request[0])); 02141 continue; 02142 } 02143 02144 uint recordid = tokens[2].toUInt(); 02145 uint findid = tokens[3].toUInt(); 02146 QString title = request[1]; 02147 QString subtitle = request[2]; 02148 QString descrip = request[3]; 02149 QString programid = request[4]; 02150 runCheck = true; 02151 schedLock.unlock(); 02152 recordmatchLock.lock(); 02153 ResetDuplicates(recordid, findid, title, subtitle, descrip, 02154 programid); 02155 recordmatchLock.unlock(); 02156 schedLock.lock(); 02157 } 02158 else if (tokens[0] != "PLACE") 02159 { 02160 LOG(VB_GENERAL, LOG_ERR, 02161 QString("Unknown Reschedule request received (%1)") 02162 .arg(request[0])); 02163 } 02164 } 02165 02166 // Delete future oldrecorded entries that no longer 02167 // match any potential recordings. 02168 if (deleteFuture) 02169 { 02170 MSqlQuery query(dbConn); 02171 query.prepare("DELETE oldrecorded FROM oldrecorded " 02172 "LEFT JOIN recordmatch ON " 02173 " recordmatch.chanid = oldrecorded.chanid AND " 02174 " recordmatch.starttime = oldrecorded.starttime " 02175 "WHERE oldrecorded.future > 0 AND " 02176 " recordmatch.recordid IS NULL"); 02177 if (!query.exec()) 02178 MythDB::DBError("DeleteFuture", query); 02179 } 02180 02181 gettimeofday(&fillend, NULL); 02182 matchTime = ((fillend.tv_sec - fillstart.tv_sec ) * 1000000 + 02183 (fillend.tv_usec - fillstart.tv_usec)) / 1000000.0; 02184 02185 LOG(VB_SCHEDULE, LOG_INFO, "CreateTempTables..."); 02186 CreateTempTables(); 02187 02188 gettimeofday(&fillstart, NULL); 02189 if (runCheck) 02190 { 02191 LOG(VB_SCHEDULE, LOG_INFO, "UpdateDuplicates..."); 02192 UpdateDuplicates(); 02193 } 02194 gettimeofday(&fillend, NULL); 02195 checkTime = ((fillend.tv_sec - fillstart.tv_sec ) * 1000000 + 02196 (fillend.tv_usec - fillstart.tv_usec)) / 1000000.0; 02197 02198 gettimeofday(&fillstart, NULL); 02199 bool worklistused = FillRecordList(); 02200 gettimeofday(&fillend, NULL); 02201 placeTime = ((fillend.tv_sec - fillstart.tv_sec ) * 1000000 + 02202 (fillend.tv_usec - fillstart.tv_usec)) / 1000000.0; 02203 02204 LOG(VB_SCHEDULE, LOG_INFO, "DeleteTempTables..."); 02205 DeleteTempTables(); 02206 02207 if (worklistused) 02208 { 02209 UpdateNextRecord(); 02210 PrintList(); 02211 } 02212 else 02213 { 02214 LOG(VB_GENERAL, LOG_INFO, "Reschedule interrupted, will retry"); 02215 EnqueuePlace("Interrupted"); 02216 return false; 02217 } 02218 02219 msg.sprintf("Scheduled %d items in %.1f " 02220 "= %.2f match + %.2f check + %.2f place", 02221 (int)reclist.size(), matchTime + checkTime + placeTime, 02222 matchTime, checkTime, placeTime); 02223 LOG(VB_GENERAL, LOG_INFO, msg); 02224 02225 fsInfoCacheFillTime = QDateTime::currentDateTime().addSecs(-1000); 02226 02227 // Write changed entries to oldrecorded. 02228 RecIter it = reclist.begin(); 02229 for ( ; it != reclist.end(); ++it) 02230 { 02231 RecordingInfo *p = *it; 02232 if (p->GetRecordingStatus() != p->oldrecstatus) 02233 { 02234 if (p->GetRecordingEndTime() < schedTime) 02235 p->AddHistory(false, false, false); 02236 else if (p->GetRecordingStartTime() < schedTime && 02237 p->GetRecordingStatus() != rsWillRecord) 02238 p->AddHistory(false, false, false); 02239 else 02240 p->AddHistory(false, false, true); 02241 } 02242 else if (p->future) 02243 { 02244 // Force a non-future, oldrecorded entry to 02245 // get written when the time comes. 02246 p->oldrecstatus = rsUnknown; 02247 } 02248 p->future = false; 02249 } 02250 02251 gCoreContext->SendSystemEvent("SCHEDULER_RAN"); 02252 02253 return true; 02254 } 02255 02256 bool Scheduler::HandleRunSchedulerStartup( 02257 int prerollseconds, int idleWaitForRecordingTime) 02258 { 02259 bool blockShutdown = true; 02260 02261 // The parameter given to the startup_cmd. "user" means a user 02262 // probably started the backend process, "auto" means it was 02263 // started probably automatically. 02264 QString startupParam = "user"; 02265 02266 // find the first recording that WILL be recorded 02267 RecIter firstRunIter = reclist.begin(); 02268 for ( ; firstRunIter != reclist.end(); ++firstRunIter) 02269 { 02270 if ((*firstRunIter)->GetRecordingStatus() == rsWillRecord) 02271 break; 02272 } 02273 02274 // have we been started automatically? 02275 QDateTime curtime = QDateTime::currentDateTime(); 02276 if (WasStartedAutomatically() || 02277 ((firstRunIter != reclist.end()) && 02278 ((curtime.secsTo((*firstRunIter)->GetRecordingStartTime()) - 02279 prerollseconds) < (idleWaitForRecordingTime * 60)))) 02280 { 02281 LOG(VB_GENERAL, LOG_INFO, LOC + "AUTO-Startup assumed"); 02282 startupParam = "auto"; 02283 02284 // Since we've started automatically, don't wait for 02285 // client to connect before allowing shutdown. 02286 blockShutdown = false; 02287 } 02288 else 02289 { 02290 LOG(VB_GENERAL, LOG_INFO, LOC + "Seem to be woken up by USER"); 02291 } 02292 02293 QString startupCommand = gCoreContext->GetSetting("startupCommand", ""); 02294 if (!startupCommand.isEmpty()) 02295 { 02296 startupCommand.replace("$status", startupParam); 02297 schedLock.unlock(); 02298 myth_system(startupCommand); 02299 schedLock.lock(); 02300 } 02301 02302 return blockShutdown; 02303 } 02304 02305 // If a recording is about to start on a backend in a few minutes, wake it... 02306 void Scheduler::HandleWakeSlave(RecordingInfo &ri, int prerollseconds) 02307 { 02308 static const int sysEventSecs[5] = { 120, 90, 60, 30, 0 }; 02309 02310 QDateTime curtime = QDateTime::currentDateTime(); 02311 QDateTime nextrectime = ri.GetRecordingStartTime(); 02312 int secsleft = curtime.secsTo(nextrectime); 02313 02314 QMap<int, EncoderLink*>::iterator tvit = m_tvList->find(ri.GetCardID()); 02315 if (tvit == m_tvList->end()) 02316 return; 02317 02318 QString sysEventKey = ri.MakeUniqueKey(); 02319 02320 int i = 0; 02321 bool pendingEventSent = false; 02322 while (sysEventSecs[i] != 0) 02323 { 02324 if ((secsleft <= sysEventSecs[i]) && 02325 (!sysEvents[i].contains(sysEventKey))) 02326 { 02327 if (!pendingEventSent) 02328 { 02329 SendMythSystemRecEvent( 02330 QString("REC_PENDING SECS %1").arg(secsleft), &ri); 02331 } 02332 02333 sysEvents[i].insert(sysEventKey); 02334 pendingEventSent = true; 02335 } 02336 i++; 02337 } 02338 02339 // cleanup old sysEvents once in a while 02340 QSet<QString> keys; 02341 for (i = 0; sysEventSecs[i] != 0; i++) 02342 { 02343 if (sysEvents[i].size() < 20) 02344 continue; 02345 02346 if (keys.empty()) 02347 { 02348 RecConstIter it = reclist.begin(); 02349 for ( ; it != reclist.end(); ++it) 02350 keys.insert((*it)->MakeUniqueKey()); 02351 keys.insert("something"); 02352 } 02353 02354 QSet<QString>::iterator sit = sysEvents[i].begin(); 02355 while (sit != sysEvents[i].end()) 02356 { 02357 if (!keys.contains(*sit)) 02358 sit = sysEvents[i].erase(sit); 02359 else 02360 ++sit; 02361 } 02362 } 02363 02364 EncoderLink *nexttv = *tvit; 02365 02366 if (nexttv->IsAsleep() && !nexttv->IsWaking()) 02367 { 02368 LOG(VB_SCHEDULE, LOG_INFO, LOC + 02369 QString("Slave Backend %1 is being awakened to record: %2") 02370 .arg(nexttv->GetHostName()).arg(ri.GetTitle())); 02371 02372 if (!WakeUpSlave(nexttv->GetHostName())) 02373 EnqueuePlace("HandleWakeSlave1"); 02374 } 02375 else if ((nexttv->IsWaking()) && 02376 ((secsleft - prerollseconds) < 210) && 02377 (nexttv->GetSleepStatusTime().secsTo(curtime) < 300) && 02378 (nexttv->GetLastWakeTime().secsTo(curtime) > 10)) 02379 { 02380 LOG(VB_SCHEDULE, LOG_INFO, LOC + 02381 QString("Slave Backend %1 not available yet, " 02382 "trying to wake it up again.") 02383 .arg(nexttv->GetHostName())); 02384 02385 if (!WakeUpSlave(nexttv->GetHostName(), false)) 02386 EnqueuePlace("HandleWakeSlave2"); 02387 } 02388 else if ((nexttv->IsWaking()) && 02389 ((secsleft - prerollseconds) < 150) && 02390 (nexttv->GetSleepStatusTime().secsTo(curtime) < 300)) 02391 { 02392 LOG(VB_GENERAL, LOG_WARNING, LOC + 02393 QString("Slave Backend %1 has NOT come " 02394 "back from sleep yet in 150 seconds. Setting " 02395 "slave status to unknown and attempting " 02396 "to reschedule around its tuners.") 02397 .arg(nexttv->GetHostName())); 02398 02399 QMap<int, EncoderLink*>::iterator it = m_tvList->begin(); 02400 for (; it != m_tvList->end(); ++it) 02401 { 02402 if ((*it)->GetHostName() == nexttv->GetHostName()) 02403 (*it)->SetSleepStatus(sStatus_Undefined); 02404 } 02405 02406 EnqueuePlace("HandleWakeSlave3"); 02407 } 02408 } 02409 02410 bool Scheduler::HandleRecording( 02411 RecordingInfo &ri, bool &statuschanged, 02412 int prerollseconds, int tuningTimeout) 02413 { 02414 if (ri.GetRecordingStatus() == rsTuning) 02415 { 02416 HandleTuning(ri, statuschanged, tuningTimeout); 02417 return false; 02418 } 02419 02420 if (ri.GetRecordingStatus() != rsWillRecord) 02421 { 02422 if (ri.GetRecordingStatus() != ri.oldrecstatus && 02423 ri.GetRecordingStartTime() <= QDateTime::currentDateTime()) 02424 { 02425 ri.AddHistory(false); 02426 } 02427 return false; 02428 } 02429 02430 QDateTime nextrectime = ri.GetRecordingStartTime(); 02431 QDateTime curtime = QDateTime::currentDateTime(); 02432 int secsleft = curtime.secsTo(nextrectime); 02433 QString schedid = ri.MakeUniqueSchedulerKey(); 02434 02435 if (secsleft - prerollseconds < 60) 02436 { 02437 if (!recPendingList.contains(schedid)) 02438 { 02439 recPendingList[schedid] = false; 02440 02441 livetvTime = (livetvTime < nextrectime) ? 02442 nextrectime : livetvTime; 02443 02444 EnqueuePlace("PrepareToRecord"); 02445 } 02446 } 02447 02448 if (secsleft - prerollseconds > 35) 02449 return true; 02450 02451 QMap<int, EncoderLink*>::iterator tvit = m_tvList->find(ri.GetCardID()); 02452 if (tvit == m_tvList->end()) 02453 { 02454 QString msg = QString("Invalid cardid (%1) for %2") 02455 .arg(ri.GetCardID()).arg(ri.GetTitle()); 02456 LOG(VB_GENERAL, LOG_ERR, LOC + msg); 02457 02458 ri.SetRecordingStatus(rsTunerBusy); 02459 ri.AddHistory(true); 02460 statuschanged = true; 02461 return false; 02462 } 02463 02464 EncoderLink *nexttv = *tvit; 02465 02466 if (nexttv->IsTunerLocked()) 02467 { 02468 QString msg = QString("SUPPRESSED recording \"%1\" on channel: " 02469 "%2 on cardid: %3, sourceid %4. Tuner " 02470 "is locked by an external application.") 02471 .arg(ri.GetTitle()) 02472 .arg(ri.GetChanID()) 02473 .arg(ri.GetCardID()) 02474 .arg(ri.GetSourceID()); 02475 LOG(VB_GENERAL, LOG_NOTICE, msg); 02476 02477 ri.SetRecordingStatus(rsTunerBusy); 02478 ri.AddHistory(true); 02479 statuschanged = true; 02480 return false; 02481 } 02482 02483 if ((prerollseconds > 0) && !IsBusyRecording(&ri)) 02484 { 02485 // Will use pre-roll settings only if no other 02486 // program is currently being recorded 02487 secsleft -= prerollseconds; 02488 } 02489 02490 #if 0 02491 LOG(VB_GENERAL, LOG_DEBUG, QString("%1 seconds until %2"). 02492 .arg(secsleft) .arg(ri.GetTitle())); 02493 #endif 02494 02495 if (secsleft > 30) 02496 return false; 02497 02498 if (nexttv->IsWaking()) 02499 { 02500 if (secsleft > 0) 02501 { 02502 LOG(VB_SCHEDULE, LOG_WARNING, 02503 QString("WARNING: Slave Backend %1 has NOT come " 02504 "back from sleep yet. Recording can " 02505 "not begin yet for: %2") 02506 .arg(nexttv->GetHostName()) 02507 .arg(ri.GetTitle())); 02508 } 02509 else if (nexttv->GetLastWakeTime().secsTo(curtime) > 300) 02510 { 02511 LOG(VB_SCHEDULE, LOG_WARNING, 02512 QString("WARNING: Slave Backend %1 has NOT come " 02513 "back from sleep yet. Setting slave " 02514 "status to unknown and attempting " 02515 "to reschedule around its tuners.") 02516 .arg(nexttv->GetHostName())); 02517 02518 QMap<int, EncoderLink *>::Iterator enciter = 02519 m_tvList->begin(); 02520 for (; enciter != m_tvList->end(); ++enciter) 02521 { 02522 EncoderLink *enc = *enciter; 02523 if (enc->GetHostName() == nexttv->GetHostName()) 02524 enc->SetSleepStatus(sStatus_Undefined); 02525 } 02526 02527 EnqueuePlace("SlaveNotAwake"); 02528 } 02529 02530 return false; 02531 } 02532 02533 int fsID = -1; 02534 if (ri.GetPathname().isEmpty()) 02535 { 02536 QString recording_dir; 02537 fsID = FillRecordingDir(ri.GetTitle(), 02538 ri.GetHostname(), 02539 ri.GetStorageGroup(), 02540 ri.GetRecordingStartTime(), 02541 ri.GetRecordingEndTime(), 02542 ri.GetCardID(), 02543 recording_dir, 02544 reclist); 02545 ri.SetPathname(recording_dir); 02546 } 02547 02548 if (!recPendingList[schedid]) 02549 { 02550 nexttv->RecordPending(&ri, max(secsleft, 0), 02551 hasLaterList.contains(schedid)); 02552 recPendingList[schedid] = true; 02553 } 02554 02555 if (secsleft > 0) 02556 return false; 02557 02558 QDateTime recstartts = mythCurrentDateTime().addSecs(30); 02559 recstartts.setTime( 02560 QTime(recstartts.time().hour(), recstartts.time().minute())); 02561 ri.SetRecordingStartTime(recstartts); 02562 02563 QString details = QString("%1: channel %2 on cardid %3, sourceid %4") 02564 .arg(ri.toString(ProgramInfo::kTitleSubtitle)) 02565 .arg(ri.GetChanID()) 02566 .arg(ri.GetCardID()) 02567 .arg(ri.GetSourceID()); 02568 02569 RecStatusTypes recStatus = rsOffLine; 02570 if (schedulingEnabled && nexttv->IsConnected()) 02571 { 02572 if (ri.GetRecordingStatus() == rsWillRecord) 02573 { 02574 recStatus = nexttv->StartRecording(&ri); 02575 ri.AddHistory(false); 02576 02577 // activate auto expirer 02578 if (m_expirer) 02579 m_expirer->Update(ri.GetCardID(), fsID, true); 02580 } 02581 } 02582 02583 HandleRecordingStatusChange(ri, recStatus, details); 02584 statuschanged = true; 02585 02586 return false; 02587 } 02588 02589 void Scheduler::HandleRecordingStatusChange( 02590 RecordingInfo &ri, RecStatusTypes recStatus, const QString &details) 02591 { 02592 if (ri.GetRecordingStatus() == recStatus) 02593 return; 02594 02595 ri.SetRecordingStatus(recStatus); 02596 02597 if (rsTuning != recStatus) 02598 { 02599 bool doSchedAfterStart = 02600 ((rsRecording != recStatus) && (rsTuning != recStatus)) || 02601 schedAfterStartMap[ri.GetRecordingRuleID()] || 02602 (ri.GetParentRecordingRuleID() && 02603 schedAfterStartMap[ri.GetParentRecordingRuleID()]); 02604 ri.AddHistory(doSchedAfterStart); 02605 } 02606 02607 QString msg = (rsRecording == recStatus) ? 02608 QString("Started recording") : 02609 ((rsTuning == recStatus) ? 02610 QString("Tuning recording") : 02611 QString("Canceled recording (%1)") 02612 .arg(toString(ri.GetRecordingStatus(), ri.GetRecordingRuleType()))); 02613 02614 LOG(VB_GENERAL, LOG_INFO, QString("%1: %2").arg(msg).arg(details)); 02615 02616 if ((rsRecording == recStatus) || (rsTuning == recStatus)) 02617 { 02618 UpdateNextRecord(); 02619 } 02620 else if (rsFailed == recStatus) 02621 { 02622 MythEvent me(QString("FORCE_DELETE_RECORDING %1 %2") 02623 .arg(ri.GetChanID()) 02624 .arg(ri.GetRecordingStartTime(ISODate))); 02625 gCoreContext->dispatch(me); 02626 } 02627 } 02628 02629 void Scheduler::HandleTuning( 02630 RecordingInfo &ri, bool &statuschanged, int tuningTimeout) 02631 { 02632 if (rsTuning != ri.GetRecordingStatus()) 02633 return; 02634 02635 // Determine current recording status 02636 QMap<int, EncoderLink*>::iterator tvit = m_tvList->find(ri.GetCardID()); 02637 RecStatusTypes recStatus = rsTunerBusy; 02638 if (tvit == m_tvList->end()) 02639 { 02640 QString msg = QString("Invalid cardid (%1) for %2") 02641 .arg(ri.GetCardID()).arg(ri.GetTitle()); 02642 LOG(VB_GENERAL, LOG_ERR, LOC + msg); 02643 } 02644 else if (tuningTimeout > 0) 02645 { 02646 recStatus = (*tvit)->GetRecordingStatus(); 02647 if (rsTuning == recStatus) 02648 { 02649 // If tuning is still taking place this long after we 02650 // started give up on it so the scheduler can try to 02651 // find another broadcast of the same material. 02652 QDateTime curtime = QDateTime::currentDateTime(); 02653 if ((ri.GetRecordingStartTime().secsTo(curtime) > tuningTimeout) && 02654 (ri.GetScheduledStartTime().secsTo(curtime) > tuningTimeout)) 02655 { 02656 recStatus = rsFailed; 02657 LOG(VB_GENERAL, LOG_INFO, 02658 QString("Canceling recording since tuning timeout, " 02659 "%1 seconds, has been exceeded.") 02660 .arg(tuningTimeout)); 02661 } 02662 } 02663 } 02664 02665 // If the status has changed, handle it 02666 if (rsTuning != recStatus) 02667 { 02668 QString details = QString("%1: channel %2 on cardid %3, sourceid %4") 02669 .arg(ri.toString(ProgramInfo::kTitleSubtitle)) 02670 .arg(ri.GetChanID()).arg(ri.GetCardID()).arg(ri.GetSourceID()); 02671 HandleRecordingStatusChange(ri, recStatus, details); 02672 statuschanged = true; 02673 } 02674 } 02675 02676 void Scheduler::HandleIdleShutdown( 02677 bool &blockShutdown, QDateTime &idleSince, 02678 int prerollseconds, int idleTimeoutSecs, int idleWaitForRecordingTime, 02679 bool &statuschanged) 02680 { 02681 if ((idleTimeoutSecs <= 0) || (m_mainServer == NULL)) 02682 return; 02683 02684 // we release the block when a client connects 02685 if (blockShutdown) 02686 blockShutdown &= !m_mainServer->isClientConnected(); 02687 else 02688 { 02689 QDateTime curtime = QDateTime::currentDateTime(); 02690 02691 // find out, if we are currently recording (or LiveTV) 02692 bool recording = false; 02693 QMap<int, EncoderLink *>::Iterator it; 02694 for (it = m_tvList->begin(); (it != m_tvList->end()) && 02695 !recording; ++it) 02696 { 02697 if ((*it)->IsBusy()) 02698 recording = true; 02699 } 02700 02701 if (!(m_mainServer->isClientConnected()) && !recording) 02702 { 02703 // have we received a RESET_IDLETIME message? 02704 resetIdleTime_lock.lock(); 02705 if (resetIdleTime) 02706 { 02707 // yes - so reset the idleSince time 02708 idleSince = QDateTime(); 02709 resetIdleTime = false; 02710 } 02711 resetIdleTime_lock.unlock(); 02712 02713 if (statuschanged || !idleSince.isValid()) 02714 { 02715 if (!idleSince.isValid()) 02716 idleSince = curtime; 02717 02718 RecIter idleIter = reclist.begin(); 02719 for ( ; idleIter != reclist.end(); ++idleIter) 02720 if ((*idleIter)->GetRecordingStatus() == 02721 rsWillRecord) 02722 break; 02723 02724 if (idleIter != reclist.end()) 02725 { 02726 if (curtime.secsTo 02727 ((*idleIter)->GetRecordingStartTime()) - 02728 prerollseconds < 02729 (idleWaitForRecordingTime * 60) + 02730 idleTimeoutSecs) 02731 { 02732 idleSince = QDateTime(); 02733 } 02734 } 02735 } 02736 02737 if (idleSince.isValid()) 02738 { 02739 // is the machine already idling the timeout time? 02740 if (idleSince.addSecs(idleTimeoutSecs) < curtime) 02741 { 02742 // are we waiting for shutdown? 02743 if (m_isShuttingDown) 02744 { 02745 // if we have been waiting more that 60secs then assume 02746 // something went wrong so reset and try again 02747 if (idleSince.addSecs(idleTimeoutSecs + 60) < 02748 curtime) 02749 { 02750 LOG(VB_GENERAL, LOG_WARNING, 02751 "Waited more than 60" 02752 " seconds for shutdown to complete" 02753 " - resetting idle time"); 02754 idleSince = QDateTime(); 02755 m_isShuttingDown = false; 02756 } 02757 } 02758 else if (!m_isShuttingDown && 02759 CheckShutdownServer(prerollseconds, 02760 idleSince, 02761 blockShutdown)) 02762 { 02763 ShutdownServer(prerollseconds, idleSince); 02764 } 02765 } 02766 else 02767 { 02768 int itime = idleSince.secsTo(curtime); 02769 QString msg; 02770 if (itime == 1) 02771 { 02772 msg = QString("I\'m idle now... shutdown will " 02773 "occur in %1 seconds.") 02774 .arg(idleTimeoutSecs); 02775 LOG(VB_GENERAL, LOG_NOTICE, msg); 02776 MythEvent me(QString("SHUTDOWN_COUNTDOWN %1") 02777 .arg(idleTimeoutSecs)); 02778 gCoreContext->dispatch(me); 02779 } 02780 else if (itime % 10 == 0) 02781 { 02782 msg = QString("%1 secs left to system shutdown!") 02783 .arg(idleTimeoutSecs - itime); 02784 LOG(VB_IDLE, LOG_NOTICE, msg); 02785 MythEvent me(QString("SHUTDOWN_COUNTDOWN %1") 02786 .arg(idleTimeoutSecs - itime)); 02787 gCoreContext->dispatch(me); 02788 } 02789 } 02790 } 02791 } 02792 else 02793 { 02794 // not idle, make the time invalid 02795 if (idleSince.isValid()) 02796 { 02797 MythEvent me(QString("SHUTDOWN_COUNTDOWN -1")); 02798 gCoreContext->dispatch(me); 02799 } 02800 idleSince = QDateTime(); 02801 } 02802 } 02803 } 02804 02805 //returns true, if the shutdown is not blocked 02806 bool Scheduler::CheckShutdownServer(int prerollseconds, QDateTime &idleSince, 02807 bool &blockShutdown) 02808 { 02809 (void)prerollseconds; 02810 bool retval = false; 02811 QString preSDWUCheckCommand = gCoreContext->GetSetting("preSDWUCheckCommand", 02812 ""); 02813 if (!preSDWUCheckCommand.isEmpty()) 02814 { 02815 uint state = myth_system(preSDWUCheckCommand); 02816 02817 if (state != GENERIC_EXIT_NOT_OK) 02818 { 02819 retval = false; 02820 switch(state) 02821 { 02822 case 0: 02823 LOG(VB_GENERAL, LOG_INFO, 02824 "CheckShutdownServer returned - OK to shutdown"); 02825 retval = true; 02826 break; 02827 case 1: 02828 LOG(VB_IDLE, LOG_NOTICE, 02829 "CheckShutdownServer returned - Not OK to shutdown"); 02830 // just reset idle'ing on retval == 1 02831 idleSince = QDateTime(); 02832 break; 02833 case 2: 02834 LOG(VB_IDLE, LOG_NOTICE, 02835 "CheckShutdownServer returned - Not OK to shutdown, " 02836 "need reconnect"); 02837 // reset shutdown status on retval = 2 02838 // (needs a clientconnection again, 02839 // before shutdown is executed) 02840 blockShutdown = 02841 gCoreContext->GetNumSetting("blockSDWUwithoutClient", 02842 1); 02843 idleSince = QDateTime(); 02844 break; 02845 #if 0 02846 case 3: 02847 //disable shutdown routine generally 02848 m_noAutoShutdown = true; 02849 break; 02850 #endif 02851 default: 02852 break; 02853 } 02854 } 02855 } 02856 else 02857 retval = true; // allow shutdown if now command is set. 02858 02859 return retval; 02860 } 02861 02862 void Scheduler::ShutdownServer(int prerollseconds, QDateTime &idleSince) 02863 { 02864 m_isShuttingDown = true; 02865 02866 RecIter recIter = reclist.begin(); 02867 for ( ; recIter != reclist.end(); ++recIter) 02868 if ((*recIter)->GetRecordingStatus() == rsWillRecord) 02869 break; 02870 02871 // set the wakeuptime if needed 02872 if (recIter != reclist.end()) 02873 { 02874 RecordingInfo *nextRecording = (*recIter); 02875 QDateTime restarttime = nextRecording->GetRecordingStartTime() 02876 .addSecs((-1) * prerollseconds); 02877 02878 int add = gCoreContext->GetNumSetting("StartupSecsBeforeRecording", 240); 02879 if (add) 02880 restarttime = restarttime.addSecs((-1) * add); 02881 02882 QString wakeup_timeformat = gCoreContext->GetSetting("WakeupTimeFormat", 02883 "hh:mm yyyy-MM-dd"); 02884 QString setwakeup_cmd = gCoreContext->GetSetting("SetWakeuptimeCommand", 02885 "echo \'Wakeuptime would " 02886 "be $time if command " 02887 "set.\'"); 02888 02889 if (setwakeup_cmd.isEmpty()) 02890 { 02891 LOG(VB_GENERAL, LOG_NOTICE, 02892 "SetWakeuptimeCommand is empty, shutdown aborted"); 02893 idleSince = QDateTime(); 02894 m_isShuttingDown = false; 02895 return; 02896 } 02897 if (wakeup_timeformat == "time_t") 02898 { 02899 QString time_ts; 02900 setwakeup_cmd.replace("$time", 02901 time_ts.setNum(restarttime.toTime_t())); 02902 } 02903 else 02904 setwakeup_cmd.replace("$time", 02905 restarttime.toString(wakeup_timeformat)); 02906 02907 LOG(VB_GENERAL, LOG_NOTICE, 02908 QString("Running the command to set the next " 02909 "scheduled wakeup time :-\n\t\t\t\t\t\t") + setwakeup_cmd); 02910 02911 // now run the command to set the wakeup time 02912 if (myth_system(setwakeup_cmd) != GENERIC_EXIT_OK) 02913 { 02914 LOG(VB_GENERAL, LOG_ERR, 02915 "SetWakeuptimeCommand failed, shutdown aborted"); 02916 idleSince = QDateTime(); 02917 m_isShuttingDown = false; 02918 return; 02919 } 02920 } 02921 02922 // tell anyone who is listening the master server is going down now 02923 MythEvent me(QString("SHUTDOWN_NOW")); 02924 gCoreContext->dispatch(me); 02925 02926 QString halt_cmd = gCoreContext->GetSetting("ServerHaltCommand", 02927 "sudo /sbin/halt -p"); 02928 02929 if (!halt_cmd.isEmpty()) 02930 { 02931 // now we shut the slave backends down... 02932 m_mainServer->ShutSlaveBackendsDown(halt_cmd); 02933 02934 LOG(VB_GENERAL, LOG_NOTICE, 02935 QString("Running the command to shutdown " 02936 "this computer :-\n\t\t\t\t\t\t") + halt_cmd); 02937 02938 // and now shutdown myself 02939 schedLock.unlock(); 02940 uint res = myth_system(halt_cmd); 02941 schedLock.lock(); 02942 if (res == GENERIC_EXIT_OK) 02943 return; 02944 02945 LOG(VB_GENERAL, LOG_ERR, "ServerHaltCommand failed, shutdown aborted"); 02946 } 02947 02948 // If we make it here then either the shutdown failed 02949 // OR we suspended or hibernated the OS instead 02950 idleSince = QDateTime(); 02951 m_isShuttingDown = false; 02952 } 02953 02954 void Scheduler::PutInactiveSlavesToSleep(void) 02955 { 02956 int prerollseconds = 0; 02957 int secsleft = 0; 02958 EncoderLink *enc = NULL; 02959 02960 bool someSlavesCanSleep = false; 02961 QMap<int, EncoderLink *>::Iterator enciter = m_tvList->begin(); 02962 for (; enciter != m_tvList->end(); ++enciter) 02963 { 02964 EncoderLink *enc = *enciter; 02965 02966 if (enc->CanSleep()) 02967 someSlavesCanSleep = true; 02968 } 02969 02970 if (!someSlavesCanSleep) 02971 return; 02972 02973 LOG(VB_SCHEDULE, LOG_INFO, 02974 "Scheduler, Checking for slaves that can be shut down"); 02975 02976 int sleepThreshold = 02977 gCoreContext->GetNumSetting( "SleepThreshold", 60 * 45); 02978 02979 LOG(VB_SCHEDULE, LOG_DEBUG, 02980 QString(" Getting list of slaves that will be active in the " 02981 "next %1 minutes.") .arg(sleepThreshold / 60)); 02982 02983 LOG(VB_SCHEDULE, LOG_DEBUG, "Checking scheduler's reclist"); 02984 RecIter recIter = reclist.begin(); 02985 QDateTime curtime = QDateTime::currentDateTime(); 02986 QStringList SlavesInUse; 02987 for ( ; recIter != reclist.end(); ++recIter) 02988 { 02989 RecordingInfo *pginfo = *recIter; 02990 02991 if ((pginfo->GetRecordingStatus() != rsRecording) && 02992 (pginfo->GetRecordingStatus() != rsTuning) && 02993 (pginfo->GetRecordingStatus() != rsWillRecord)) 02994 continue; 02995 02996 secsleft = curtime.secsTo( 02997 pginfo->GetRecordingStartTime()) - prerollseconds; 02998 if (secsleft > sleepThreshold) 02999 continue; 03000 03001 if (m_tvList->find(pginfo->GetCardID()) != m_tvList->end()) 03002 { 03003 enc = (*m_tvList)[pginfo->GetCardID()]; 03004 if ((!enc->IsLocal()) && 03005 (!SlavesInUse.contains(enc->GetHostName()))) 03006 { 03007 if (pginfo->GetRecordingStatus() == rsWillRecord) 03008 LOG(VB_SCHEDULE, LOG_DEBUG, 03009 QString(" Slave %1 will be in use in %2 minutes") 03010 .arg(enc->GetHostName()) .arg(secsleft / 60)); 03011 else 03012 LOG(VB_SCHEDULE, LOG_DEBUG, 03013 QString(" Slave %1 is in use currently " 03014 "recording '%1'") 03015 .arg(enc->GetHostName()).arg(pginfo->GetTitle())); 03016 SlavesInUse << enc->GetHostName(); 03017 } 03018 } 03019 } 03020 03021 LOG(VB_SCHEDULE, LOG_DEBUG, " Checking inuseprograms table:"); 03022 QDateTime oneHourAgo = QDateTime::currentDateTime().addSecs(-61 * 60); 03023 MSqlQuery query(MSqlQuery::InitCon()); 03024 query.prepare("SELECT DISTINCT hostname, recusage FROM inuseprograms " 03025 "WHERE lastupdatetime > :ONEHOURAGO ;"); 03026 query.bindValue(":ONEHOURAGO", oneHourAgo); 03027 if (query.exec()) 03028 { 03029 while(query.next()) { 03030 SlavesInUse << query.value(0).toString(); 03031 LOG(VB_SCHEDULE, LOG_DEBUG, 03032 QString(" Slave %1 is marked as in use by a %2") 03033 .arg(query.value(0).toString()) 03034 .arg(query.value(1).toString())); 03035 } 03036 } 03037 03038 LOG(VB_SCHEDULE, LOG_DEBUG, QString(" Shutting down slaves which will " 03039 "be inactive for the next %1 minutes and can be put to sleep.") 03040 .arg(sleepThreshold / 60)); 03041 03042 enciter = m_tvList->begin(); 03043 for (; enciter != m_tvList->end(); ++enciter) 03044 { 03045 enc = *enciter; 03046 03047 if ((!enc->IsLocal()) && 03048 (enc->IsAwake()) && 03049 (!SlavesInUse.contains(enc->GetHostName())) && 03050 (!enc->IsFallingAsleep())) 03051 { 03052 QString sleepCommand = 03053 gCoreContext->GetSettingOnHost("SleepCommand", 03054 enc->GetHostName()); 03055 QString wakeUpCommand = 03056 gCoreContext->GetSettingOnHost("WakeUpCommand", 03057 enc->GetHostName()); 03058 03059 if (!sleepCommand.isEmpty() && !wakeUpCommand.isEmpty()) 03060 { 03061 QString thisHost = enc->GetHostName(); 03062 03063 LOG(VB_SCHEDULE, LOG_DEBUG, 03064 QString(" Commanding %1 to go to sleep.") 03065 .arg(thisHost)); 03066 03067 if (enc->GoToSleep()) 03068 { 03069 QMap<int, EncoderLink *>::Iterator slviter = 03070 m_tvList->begin(); 03071 for (; slviter != m_tvList->end(); ++slviter) 03072 { 03073 EncoderLink *slv = *slviter; 03074 if (slv->GetHostName() == thisHost) 03075 { 03076 LOG(VB_SCHEDULE, LOG_DEBUG, 03077 QString(" Marking card %1 on slave %2 " 03078 "as falling asleep.") 03079 .arg(slv->GetCardID()) 03080 .arg(slv->GetHostName())); 03081 slv->SetSleepStatus(sStatus_FallingAsleep); 03082 } 03083 } 03084 } 03085 else 03086 { 03087 LOG(VB_GENERAL, LOG_ERR, LOC + 03088 QString("Unable to shutdown %1 slave backend, setting " 03089 "sleep status to undefined.").arg(thisHost)); 03090 QMap<int, EncoderLink *>::Iterator slviter = 03091 m_tvList->begin(); 03092 for (; slviter != m_tvList->end(); ++slviter) 03093 { 03094 EncoderLink *slv = *slviter; 03095 if (slv->GetHostName() == thisHost) 03096 slv->SetSleepStatus(sStatus_Undefined); 03097 } 03098 } 03099 } 03100 } 03101 } 03102 } 03103 03104 bool Scheduler::WakeUpSlave(QString slaveHostname, bool setWakingStatus) 03105 { 03106 if (slaveHostname == gCoreContext->GetHostName()) 03107 { 03108 LOG(VB_GENERAL, LOG_NOTICE, 03109 QString("Tried to Wake Up %1, but this is the " 03110 "master backend and it is not asleep.") 03111 .arg(slaveHostname)); 03112 return false; 03113 } 03114 03115 QString wakeUpCommand = gCoreContext->GetSettingOnHost( "WakeUpCommand", 03116 slaveHostname); 03117 03118 if (wakeUpCommand.isEmpty()) { 03119 LOG(VB_GENERAL, LOG_NOTICE, 03120 QString("Trying to Wake Up %1, but this slave " 03121 "does not have a WakeUpCommand set.").arg(slaveHostname)); 03122 03123 QMap<int, EncoderLink *>::Iterator enciter = m_tvList->begin(); 03124 for (; enciter != m_tvList->end(); ++enciter) 03125 { 03126 EncoderLink *enc = *enciter; 03127 if (enc->GetHostName() == slaveHostname) 03128 enc->SetSleepStatus(sStatus_Undefined); 03129 } 03130 03131 return false; 03132 } 03133 03134 QDateTime curtime = QDateTime::currentDateTime(); 03135 QMap<int, EncoderLink *>::Iterator enciter = m_tvList->begin(); 03136 for (; enciter != m_tvList->end(); ++enciter) 03137 { 03138 EncoderLink *enc = *enciter; 03139 if (setWakingStatus && (enc->GetHostName() == slaveHostname)) 03140 enc->SetSleepStatus(sStatus_Waking); 03141 enc->SetLastWakeTime(curtime); 03142 } 03143 03144 if (!IsMACAddress(wakeUpCommand)) 03145 { 03146 LOG(VB_SCHEDULE, LOG_NOTICE, QString("Executing '%1' to wake up slave.") 03147 .arg(wakeUpCommand)); 03148 myth_system(wakeUpCommand); 03149 return true; 03150 } 03151 03152 return WakeOnLAN(wakeUpCommand); 03153 } 03154 03155 void Scheduler::WakeUpSlaves(void) 03156 { 03157 QStringList SlavesThatCanWake; 03158 QString thisSlave; 03159 QMap<int, EncoderLink *>::Iterator enciter = m_tvList->begin(); 03160 for (; enciter != m_tvList->end(); ++enciter) 03161 { 03162 EncoderLink *enc = *enciter; 03163 03164 if (enc->IsLocal()) 03165 continue; 03166 03167 thisSlave = enc->GetHostName(); 03168 03169 if ((!gCoreContext->GetSettingOnHost("WakeUpCommand", thisSlave) 03170 .isEmpty()) && 03171 (!SlavesThatCanWake.contains(thisSlave))) 03172 SlavesThatCanWake << thisSlave; 03173 } 03174 03175 int slave = 0; 03176 for (; slave < SlavesThatCanWake.count(); slave++) 03177 { 03178 thisSlave = SlavesThatCanWake[slave]; 03179 LOG(VB_SCHEDULE, LOG_NOTICE, 03180 QString("Scheduler, Sending wakeup command to slave: %1") 03181 .arg(thisSlave)); 03182 WakeUpSlave(thisSlave, false); 03183 } 03184 } 03185 03186 void Scheduler::UpdateManuals(uint recordid) 03187 { 03188 MSqlQuery query(dbConn); 03189 03190 query.prepare(QString("SELECT type,title,station,startdate,starttime, " 03191 " enddate,endtime " 03192 "FROM %1 WHERE recordid = :RECORDID").arg(recordTable)); 03193 query.bindValue(":RECORDID", recordid); 03194 if (!query.exec() || query.size() != 1) 03195 { 03196 MythDB::DBError("UpdateManuals", query); 03197 return; 03198 } 03199 03200 if (!query.next()) 03201 return; 03202 03203 RecordingType rectype = RecordingType(query.value(0).toInt()); 03204 QString title = query.value(1).toString(); 03205 QString station = query.value(2).toString() ; 03206 QDateTime startdt = QDateTime(query.value(3).toDate(), 03207 query.value(4).toTime()); 03208 int duration = startdt.secsTo(QDateTime(query.value(5).toDate(), 03209 query.value(6).toTime())) / 60; 03210 03211 query.prepare("SELECT chanid from channel " 03212 "WHERE callsign = :STATION"); 03213 query.bindValue(":STATION", station); 03214 if (!query.exec()) 03215 { 03216 MythDB::DBError("UpdateManuals", query); 03217 return; 03218 } 03219 03220 vector<uint> chanidlist; 03221 while (query.next()) 03222 chanidlist.push_back(query.value(0).toUInt()); 03223 03224 int progcount; 03225 int skipdays; 03226 bool weekday; 03227 int weeksoff; 03228 03229 switch (rectype) 03230 { 03231 case kSingleRecord: 03232 case kOverrideRecord: 03233 case kDontRecord: 03234 progcount = 1; 03235 skipdays = 1; 03236 weekday = false; 03237 break; 03238 case kTimeslotRecord: 03239 progcount = 13; 03240 skipdays = 1; 03241 if (startdt.date().dayOfWeek() < 6) 03242 weekday = true; 03243 else 03244 weekday = false; 03245 startdt.setDate(QDate::currentDate()); 03246 break; 03247 case kWeekslotRecord: 03248 progcount = 2; 03249 skipdays = 7; 03250 weekday = false; 03251 weeksoff = (startdt.date().daysTo(QDate::currentDate()) + 6) / 7; 03252 startdt = startdt.addDays(weeksoff * 7); 03253 break; 03254 default: 03255 LOG(VB_GENERAL, LOG_ERR, 03256 QString("Invalid rectype for manual recordid %1").arg(recordid)); 03257 return; 03258 } 03259 03260 while (progcount--) 03261 { 03262 for (int i = 0; i < (int)chanidlist.size(); i++) 03263 { 03264 if (weekday && startdt.date().dayOfWeek() >= 6) 03265 continue; 03266 03267 query.prepare("REPLACE INTO program (chanid, starttime, endtime," 03268 " title, subtitle, manualid, generic) " 03269 "VALUES (:CHANID, :STARTTIME, :ENDTIME, :TITLE," 03270 " :SUBTITLE, :RECORDID, 1)"); 03271 query.bindValue(":CHANID", chanidlist[i]); 03272 query.bindValue(":STARTTIME", startdt); 03273 query.bindValue(":ENDTIME", startdt.addSecs(duration * 60)); 03274 query.bindValue(":TITLE", title); 03275 query.bindValue(":SUBTITLE", startdt.toString()); 03276 query.bindValue(":RECORDID", recordid); 03277 if (!query.exec()) 03278 { 03279 MythDB::DBError("UpdateManuals", query); 03280 return; 03281 } 03282 } 03283 startdt = startdt.addDays(skipdays); 03284 } 03285 } 03286 03287 void Scheduler::BuildNewRecordsQueries(uint recordid, QStringList &from, 03288 QStringList &where, 03289 MSqlBindings &bindings) 03290 { 03291 MSqlQuery result(dbConn); 03292 QString query; 03293 QString qphrase; 03294 03295 query = QString("SELECT recordid,search,subtitle,description " 03296 "FROM %1 WHERE search <> %2 AND " 03297 "(recordid = %3 OR %4 = 0) ") 03298 .arg(recordTable).arg(kNoSearch).arg(recordid).arg(recordid); 03299 03300 result.prepare(query); 03301 03302 if (!result.exec() || !result.isActive()) 03303 { 03304 MythDB::DBError("BuildNewRecordsQueries", result); 03305 return; 03306 } 03307 03308 int count = 0; 03309 while (result.next()) 03310 { 03311 QString prefix = QString(":NR%1").arg(count); 03312 qphrase = result.value(3).toString(); 03313 03314 RecSearchType searchtype = RecSearchType(result.value(1).toInt()); 03315 03316 if (qphrase.isEmpty() && searchtype != kManualSearch) 03317 { 03318 LOG(VB_GENERAL, LOG_ERR, 03319 QString("Invalid search key in recordid %1") 03320 .arg(result.value(0).toString())); 03321 continue; 03322 } 03323 03324 QString bindrecid = prefix + "RECID"; 03325 QString bindphrase = prefix + "PHRASE"; 03326 QString bindlikephrase1 = prefix + "LIKEPHRASE1"; 03327 QString bindlikephrase2 = prefix + "LIKEPHRASE2"; 03328 QString bindlikephrase3 = prefix + "LIKEPHRASE3"; 03329 03330 bindings[bindrecid] = result.value(0).toString(); 03331 03332 switch (searchtype) 03333 { 03334 case kPowerSearch: 03335 qphrase.remove(QRegExp("^\\s*AND\\s+", Qt::CaseInsensitive)); 03336 qphrase.remove(';'); 03337 from << result.value(2).toString(); 03338 where << (QString("%1.recordid = ").arg(recordTable) + bindrecid + 03339 QString(" AND program.manualid = 0 AND ( %2 )") 03340 .arg(qphrase)); 03341 break; 03342 case kTitleSearch: 03343 bindings[bindlikephrase1] = QString(QString("%") + qphrase + "%"); 03344 from << ""; 03345 where << (QString("%1.recordid = ").arg(recordTable) + bindrecid + " AND " 03346 "program.manualid = 0 AND " 03347 "program.title LIKE " + bindlikephrase1); 03348 break; 03349 case kKeywordSearch: 03350 bindings[bindlikephrase1] = QString(QString("%") + qphrase + "%"); 03351 bindings[bindlikephrase2] = QString(QString("%") + qphrase + "%"); 03352 bindings[bindlikephrase3] = QString(QString("%") + qphrase + "%"); 03353 from << ""; 03354 where << (QString("%1.recordid = ").arg(recordTable) + bindrecid + 03355 " AND program.manualid = 0" 03356 " AND (program.title LIKE " + bindlikephrase1 + 03357 " OR program.subtitle LIKE " + bindlikephrase2 + 03358 " OR program.description LIKE " + bindlikephrase3 + ")"); 03359 break; 03360 case kPeopleSearch: 03361 bindings[bindphrase] = qphrase; 03362 from << ", people, credits"; 03363 where << (QString("%1.recordid = ").arg(recordTable) + bindrecid + " AND " 03364 "program.manualid = 0 AND " 03365 "people.name LIKE " + bindphrase + " AND " 03366 "credits.person = people.person AND " 03367 "program.chanid = credits.chanid AND " 03368 "program.starttime = credits.starttime"); 03369 break; 03370 case kManualSearch: 03371 UpdateManuals(result.value(0).toInt()); 03372 from << ""; 03373 where << (QString("%1.recordid = ").arg(recordTable) + bindrecid + 03374 " AND " + 03375 QString("program.manualid = %1.recordid ") 03376 .arg(recordTable)); 03377 break; 03378 default: 03379 LOG(VB_GENERAL, LOG_ERR, 03380 QString("Unknown RecSearchType (%1) for recordid %2") 03381 .arg(result.value(1).toInt()) 03382 .arg(result.value(0).toString())); 03383 bindings.remove(bindrecid); 03384 break; 03385 } 03386 03387 count++; 03388 } 03389 03390 if (recordid == 0 || from.count() == 0) 03391 { 03392 QString recidmatch = ""; 03393 if (recordid != 0) 03394 recidmatch = "RECTABLE.recordid = :NRRECORDID AND "; 03395 QString s1 = recidmatch + 03396 "RECTABLE.type <> :NRTEMPLATE AND " 03397 "RECTABLE.search = :NRST AND " 03398 "program.manualid = 0 AND " 03399 "program.title = RECTABLE.title "; 03400 s1.replace("RECTABLE", recordTable); 03401 QString s2 = recidmatch + 03402 "RECTABLE.type <> :NRTEMPLATE AND " 03403 "RECTABLE.search = :NRST AND " 03404 "program.manualid = 0 AND " 03405 "program.seriesid <> '' AND " 03406 "program.seriesid = RECTABLE.seriesid "; 03407 s2.replace("RECTABLE", recordTable); 03408 03409 from << ""; 03410 where << s1; 03411 from << ""; 03412 where << s2; 03413 bindings[":NRTEMPLATE"] = kTemplateRecord; 03414 bindings[":NRST"] = kNoSearch; 03415 if (recordid != 0) 03416 bindings[":NRRECORDID"] = recordid; 03417 } 03418 } 03419 03420 static QString progdupinit = QString( 03421 "(CASE " 03422 " WHEN RECTABLE.type IN (%1, %2, %3) THEN 0 " 03423 " WHEN RECTABLE.type IN (%4, %5, %6) THEN -1 " 03424 " ELSE (program.generic - 1) " 03425 " END) ") 03426 .arg(kSingleRecord).arg(kOverrideRecord).arg(kDontRecord) 03427 .arg(kFindOneRecord).arg(kFindDailyRecord).arg(kFindWeeklyRecord); 03428 03429 static QString progfindid = QString( 03430 "(CASE RECTABLE.type " 03431 " WHEN %1 " 03432 " THEN RECTABLE.findid " 03433 " WHEN %2 " 03434 " THEN to_days(date_sub(program.starttime, interval " 03435 " time_format(RECTABLE.findtime, '%H:%i') hour_minute)) " 03436 " WHEN %3 " 03437 " THEN floor((to_days(date_sub(program.starttime, interval " 03438 " time_format(RECTABLE.findtime, '%H:%i') hour_minute)) - " 03439 " RECTABLE.findday)/7) * 7 + RECTABLE.findday " 03440 " WHEN %4 " 03441 " THEN RECTABLE.findid " 03442 " ELSE 0 " 03443 " END) ") 03444 .arg(kFindOneRecord) 03445 .arg(kFindDailyRecord) 03446 .arg(kFindWeeklyRecord) 03447 .arg(kOverrideRecord); 03448 03449 void Scheduler::UpdateMatches(uint recordid, uint sourceid, uint mplexid, 03450 const QDateTime maxstarttime) 03451 { 03452 struct timeval dbstart, dbend; 03453 03454 MSqlQuery query(dbConn); 03455 MSqlBindings bindings; 03456 QString deleteClause; 03457 QString filterClause = QString(" AND program.endtime > " 03458 "(NOW() - INTERVAL 480 MINUTE)"); 03459 03460 if (recordid) 03461 { 03462 deleteClause += " AND recordmatch.recordid = :RECORDID"; 03463 bindings[":RECORDID"] = recordid; 03464 } 03465 if (sourceid) 03466 { 03467 deleteClause += " AND channel.sourceid = :SOURCEID"; 03468 filterClause += " AND channel.sourceid = :SOURCEID"; 03469 bindings[":SOURCEID"] = sourceid; 03470 } 03471 if (mplexid) 03472 { 03473 deleteClause += " AND channel.mplexid = :MPLEXID"; 03474 filterClause += " AND channel.mplexid = :MPLEXID"; 03475 bindings[":MPLEXID"] = mplexid; 03476 } 03477 if (maxstarttime.isValid()) 03478 { 03479 deleteClause += " AND recordmatch.starttime <= :MAXSTARTTIME"; 03480 filterClause += " AND program.starttime <= :MAXSTARTTIME"; 03481 bindings[":MAXSTARTTIME"] = maxstarttime; 03482 } 03483 03484 query.prepare(QString("DELETE recordmatch FROM recordmatch, channel " 03485 "WHERE recordmatch.chanid = channel.chanid") 03486 + deleteClause); 03487 MSqlBindings::const_iterator it; 03488 for (it = bindings.begin(); it != bindings.end(); ++it) 03489 query.bindValue(it.key(), it.value()); 03490 if (!query.exec()) 03491 { 03492 MythDB::DBError("UpdateMatches1", query); 03493 return; 03494 } 03495 if (recordid) 03496 bindings.remove(":RECORDID"); 03497 03498 query.prepare("SELECT filterid, clause FROM recordfilter " 03499 "WHERE filterid >= 0 AND filterid < :NUMFILTERS AND " 03500 " TRIM(clause) <> ''"); 03501 query.bindValue(":NUMFILTERS", RecordingRule::kNumFilters); 03502 if (!query.exec()) 03503 { 03504 MythDB::DBError("UpdateMatches2", query); 03505 return; 03506 } 03507 while (query.next()) 03508 { 03509 filterClause += QString(" AND (((RECTABLE.filter & %1) = 0) OR (%2))") 03510 .arg(1 << query.value(0).toInt()).arg(query.value(1).toString()); 03511 } 03512 03513 // Make sure all FindOne rules have a valid findid before scheduling. 03514 query.prepare("SELECT NULL from record " 03515 "WHERE type = :FINDONE AND findid <= 0;"); 03516 query.bindValue(":FINDONE", kFindOneRecord); 03517 if (!query.exec()) 03518 { 03519 MythDB::DBError("UpdateMatches3", query); 03520 return; 03521 } 03522 else if (query.size()) 03523 { 03524 QDate epoch(1970, 1, 1); 03525 int findtoday = epoch.daysTo(QDate::currentDate()) + 719528; 03526 query.prepare("UPDATE record set findid = :FINDID " 03527 "WHERE type = :FINDONE AND findid <= 0;"); 03528 query.bindValue(":FINDID", findtoday); 03529 query.bindValue(":FINDONE", kFindOneRecord); 03530 if (!query.exec()) 03531 MythDB::DBError("UpdateMatches4", query); 03532 } 03533 03534 int clause; 03535 QStringList fromclauses, whereclauses; 03536 03537 BuildNewRecordsQueries(recordid, fromclauses, whereclauses, bindings); 03538 03539 if (VERBOSE_LEVEL_CHECK(VB_SCHEDULE, LOG_INFO)) 03540 { 03541 for (clause = 0; clause < fromclauses.count(); ++clause) 03542 { 03543 LOG(VB_SCHEDULE, LOG_INFO, QString("Query %1: %2/%3") 03544 .arg(clause).arg(fromclauses[clause]) 03545 .arg(whereclauses[clause])); 03546 } 03547 } 03548 03549 for (clause = 0; clause < fromclauses.count(); ++clause) 03550 { 03551 QString query = QString( 03552 "REPLACE INTO recordmatch (recordid, chanid, starttime, manualid, " 03553 " oldrecduplicate, findid) " 03554 "SELECT RECTABLE.recordid, program.chanid, program.starttime, " 03555 " IF(search = %1, RECTABLE.recordid, 0), ").arg(kManualSearch) + 03556 progdupinit + ", " + progfindid + QString( 03557 "FROM (RECTABLE, program INNER JOIN channel " 03558 " ON channel.chanid = program.chanid) ") + fromclauses[clause] + QString( 03559 " WHERE ") + whereclauses[clause] + 03560 QString(" AND channel.visible = 1 ") + 03561 filterClause + QString(" AND " 03562 03563 "((RECTABLE.type = %1 " // allrecord 03564 "OR RECTABLE.type = %2 " // findonerecord 03565 "OR RECTABLE.type = %3 " // finddailyrecord 03566 "OR RECTABLE.type = %4) " // findweeklyrecord 03567 " OR " 03568 " ((RECTABLE.station = channel.callsign) " // channel matches 03569 " AND " 03570 " ((RECTABLE.type = %5) " // channelrecord 03571 " OR" 03572 " ((TIME_TO_SEC(RECTABLE.starttime) = TIME_TO_SEC(program.starttime)) " // timeslot matches 03573 " AND " 03574 " ((RECTABLE.type = %6) " // timeslotrecord 03575 " OR" 03576 " ((DAYOFWEEK(RECTABLE.startdate) = DAYOFWEEK(program.starttime) " 03577 " AND " 03578 " ((RECTABLE.type = %7) " // weekslotrecord 03579 " OR" 03580 " ((TO_DAYS(RECTABLE.startdate) = TO_DAYS(program.starttime)) " // date matches 03581 " AND (RECTABLE.type <> %8)" // single,override,don't,etc. 03582 " )" 03583 " )" 03584 " )" 03585 " )" 03586 " )" 03587 " )" 03588 " )" 03589 " )" 03590 ") ") 03591 .arg(kAllRecord) 03592 .arg(kFindOneRecord) 03593 .arg(kFindDailyRecord) 03594 .arg(kFindWeeklyRecord) 03595 .arg(kChannelRecord) 03596 .arg(kTimeslotRecord) 03597 .arg(kWeekslotRecord) 03598 .arg(kNotRecording); 03599 03600 query.replace("RECTABLE", recordTable); 03601 03602 LOG(VB_SCHEDULE, LOG_INFO, QString(" |-- Start DB Query %1...") 03603 .arg(clause)); 03604 03605 gettimeofday(&dbstart, NULL); 03606 MSqlQuery result(dbConn); 03607 result.prepare(query); 03608 03609 for (it = bindings.begin(); it != bindings.end(); ++it) 03610 { 03611 if (query.contains(it.key())) 03612 result.bindValue(it.key(), it.value()); 03613 } 03614 03615 bool ok = result.exec(); 03616 gettimeofday(&dbend, NULL); 03617 03618 if (!ok) 03619 { 03620 MythDB::DBError("UpdateMatches3", result); 03621 continue; 03622 } 03623 03624 LOG(VB_SCHEDULE, LOG_INFO, QString(" |-- %1 results in %2 sec.") 03625 .arg(result.size()) 03626 .arg(((dbend.tv_sec - dbstart.tv_sec) * 1000000 + 03627 (dbend.tv_usec - dbstart.tv_usec)) / 1000000.0)); 03628 03629 } 03630 03631 LOG(VB_SCHEDULE, LOG_INFO, " +-- Done."); 03632 } 03633 03634 void Scheduler::CreateTempTables(void) 03635 { 03636 MSqlQuery result(dbConn); 03637 03638 if (recordTable == "record") 03639 { 03640 result.prepare("DROP TABLE IF EXISTS sched_temp_record;"); 03641 if (!result.exec()) 03642 { 03643 MythDB::DBError("Dropping sched_temp_record table", result); 03644 return; 03645 } 03646 result.prepare("CREATE TEMPORARY TABLE sched_temp_record " 03647 "LIKE record;"); 03648 if (!result.exec()) 03649 { 03650 MythDB::DBError("Creating sched_temp_record table", result); 03651 return; 03652 } 03653 result.prepare("INSERT sched_temp_record SELECT * from record;"); 03654 if (!result.exec()) 03655 { 03656 MythDB::DBError("Populating sched_temp_record table", result); 03657 return; 03658 } 03659 } 03660 03661 result.prepare("DROP TABLE IF EXISTS sched_temp_recorded;"); 03662 if (!result.exec()) 03663 { 03664 MythDB::DBError("Dropping sched_temp_recorded table", result); 03665 return; 03666 } 03667 result.prepare("CREATE TEMPORARY TABLE sched_temp_recorded " 03668 "LIKE recorded;"); 03669 if (!result.exec()) 03670 { 03671 MythDB::DBError("Creating sched_temp_recorded table", result); 03672 return; 03673 } 03674 result.prepare("INSERT sched_temp_recorded SELECT * from recorded;"); 03675 if (!result.exec()) 03676 { 03677 MythDB::DBError("Populating sched_temp_recorded table", result); 03678 return; 03679 } 03680 } 03681 03682 void Scheduler::DeleteTempTables(void) 03683 { 03684 MSqlQuery result(dbConn); 03685 03686 if (recordTable == "record") 03687 { 03688 result.prepare("DROP TABLE IF EXISTS sched_temp_record;"); 03689 if (!result.exec()) 03690 MythDB::DBError("DeleteTempTables sched_temp_record", result); 03691 } 03692 03693 result.prepare("DROP TABLE IF EXISTS sched_temp_recorded;"); 03694 if (!result.exec()) 03695 MythDB::DBError("DeleteTempTables drop table", result); 03696 } 03697 03698 void Scheduler::UpdateDuplicates(void) 03699 { 03700 QString schedTmpRecord = recordTable; 03701 if (schedTmpRecord == "record") 03702 schedTmpRecord = "sched_temp_record"; 03703 03704 QString rmquery = QString( 03705 "UPDATE recordmatch " 03706 " INNER JOIN RECTABLE ON (recordmatch.recordid = RECTABLE.recordid) " 03707 " INNER JOIN program p ON (recordmatch.chanid = p.chanid AND " 03708 " recordmatch.starttime = p.starttime AND " 03709 " recordmatch.manualid = p.manualid) " 03710 " LEFT JOIN oldrecorded ON " 03711 " ( " 03712 " RECTABLE.dupmethod > 1 AND " 03713 " oldrecorded.duplicate <> 0 AND " 03714 " p.title = oldrecorded.title AND " 03715 " p.generic = 0 " 03716 " AND " 03717 " ( " 03718 " (p.programid <> '' " 03719 " AND p.programid = oldrecorded.programid) " 03720 " OR " 03721 " ( ") + 03722 (ProgramInfo::UsingProgramIDAuthority() ? 03723 " (p.programid = '' OR oldrecorded.programid = '' OR " 03724 " LEFT(p.programid, LOCATE('/', p.programid)) <> " 03725 " LEFT(oldrecorded.programid, LOCATE('/', oldrecorded.programid))) " : 03726 " (p.programid = '' OR oldrecorded.programid = '') " ) 03727 + QString( 03728 " AND " 03729 " (((RECTABLE.dupmethod & 0x02) = 0) OR (p.subtitle <> '' " 03730 " AND p.subtitle = oldrecorded.subtitle)) " 03731 " AND " 03732 " (((RECTABLE.dupmethod & 0x04) = 0) OR (p.description <> '' " 03733 " AND p.description = oldrecorded.description)) " 03734 " AND " 03735 " (((RECTABLE.dupmethod & 0x08) = 0) OR " 03736 " (p.subtitle <> '' AND " 03737 " (p.subtitle = oldrecorded.subtitle OR " 03738 " (oldrecorded.subtitle = '' AND " 03739 " p.subtitle = oldrecorded.description))) OR " 03740 " (p.subtitle = '' AND p.description <> '' AND " 03741 " (p.description = oldrecorded.subtitle OR " 03742 " (oldrecorded.subtitle = '' AND " 03743 " p.description = oldrecorded.description)))) " 03744 " ) " 03745 " ) " 03746 " ) " 03747 " LEFT JOIN sched_temp_recorded recorded ON " 03748 " ( " 03749 " RECTABLE.dupmethod > 1 AND " 03750 " recorded.duplicate <> 0 AND " 03751 " p.title = recorded.title AND " 03752 " p.generic = 0 AND " 03753 " recorded.recgroup NOT IN ('LiveTV','Deleted') " 03754 " AND " 03755 " ( " 03756 " (p.programid <> '' " 03757 " AND p.programid = recorded.programid) " 03758 " OR " 03759 " ( ") + 03760 (ProgramInfo::UsingProgramIDAuthority() ? 03761 " (p.programid = '' OR recorded.programid = '' OR " 03762 " LEFT(p.programid, LOCATE('/', p.programid)) <> " 03763 " LEFT(recorded.programid, LOCATE('/', recorded.programid))) " : 03764 " (p.programid = '' OR recorded.programid = '') ") 03765 + QString( 03766 " AND " 03767 " (((RECTABLE.dupmethod & 0x02) = 0) OR (p.subtitle <> '' " 03768 " AND p.subtitle = recorded.subtitle)) " 03769 " AND " 03770 " (((RECTABLE.dupmethod & 0x04) = 0) OR (p.description <> '' " 03771 " AND p.description = recorded.description)) " 03772 " AND " 03773 " (((RECTABLE.dupmethod & 0x08) = 0) OR " 03774 " (p.subtitle <> '' AND " 03775 " (p.subtitle = recorded.subtitle OR " 03776 " (recorded.subtitle = '' AND " 03777 " p.subtitle = recorded.description))) OR " 03778 " (p.subtitle = '' AND p.description <> '' AND " 03779 " (p.description = recorded.subtitle OR " 03780 " (recorded.subtitle = '' AND " 03781 " p.description = recorded.description)))) " 03782 " ) " 03783 " ) " 03784 " ) " 03785 " LEFT JOIN oldfind ON " 03786 " (oldfind.recordid = recordmatch.recordid AND " 03787 " oldfind.findid = recordmatch.findid) " 03788 " SET oldrecduplicate = (oldrecorded.endtime IS NOT NULL), " 03789 " recduplicate = (recorded.endtime IS NOT NULL), " 03790 " findduplicate = (oldfind.findid IS NOT NULL), " 03791 " oldrecstatus = oldrecorded.recstatus " 03792 " WHERE p.endtime >= (NOW() - INTERVAL 480 MINUTE) " 03793 " AND oldrecduplicate = -1 " 03794 ); 03795 rmquery.replace("RECTABLE", schedTmpRecord); 03796 03797 MSqlQuery result(dbConn); 03798 result.prepare(rmquery); 03799 if (!result.exec()) 03800 { 03801 MythDB::DBError("UpdateDuplicates", result); 03802 return; 03803 } 03804 } 03805 03806 void Scheduler::AddNewRecords(void) 03807 { 03808 QString schedTmpRecord = recordTable; 03809 if (schedTmpRecord == "record") 03810 schedTmpRecord = "sched_temp_record"; 03811 03812 struct timeval dbstart, dbend; 03813 03814 QMap<RecordingType, int> recTypeRecPriorityMap; 03815 RecList tmpList; 03816 03817 QMap<int, bool> cardMap; 03818 QMap<int, EncoderLink *>::Iterator enciter = m_tvList->begin(); 03819 for (; enciter != m_tvList->end(); ++enciter) 03820 { 03821 EncoderLink *enc = *enciter; 03822 if (enc->IsConnected() || enc->IsAsleep()) 03823 cardMap[enc->GetCardID()] = true; 03824 } 03825 03826 QMap<int, bool> tooManyMap; 03827 bool checkTooMany = false; 03828 schedAfterStartMap.clear(); 03829 03830 MSqlQuery rlist(dbConn); 03831 rlist.prepare(QString("SELECT recordid, title, maxepisodes, maxnewest " 03832 "FROM %1").arg(schedTmpRecord)); 03833 03834 if (!rlist.exec()) 03835 { 03836 MythDB::DBError("CheckTooMany", rlist); 03837 return; 03838 } 03839 03840 while (rlist.next()) 03841 { 03842 int recid = rlist.value(0).toInt(); 03843 QString qtitle = rlist.value(1).toString(); 03844 int maxEpisodes = rlist.value(2).toInt(); 03845 int maxNewest = rlist.value(3).toInt(); 03846 03847 tooManyMap[recid] = false; 03848 schedAfterStartMap[recid] = false; 03849 03850 if (maxEpisodes && !maxNewest) 03851 { 03852 MSqlQuery epicnt(dbConn); 03853 03854 epicnt.prepare("SELECT DISTINCT chanid, progstart, progend " 03855 "FROM recorded " 03856 "WHERE recordid = :RECID AND preserve = 0 " 03857 "AND recgroup NOT IN ('LiveTV','Deleted');"); 03858 epicnt.bindValue(":RECID", recid); 03859 03860 if (epicnt.exec()) 03861 { 03862 if (epicnt.size() >= maxEpisodes - 1) 03863 { 03864 schedAfterStartMap[recid] = true; 03865 if (epicnt.size() >= maxEpisodes) 03866 { 03867 tooManyMap[recid] = true; 03868 checkTooMany = true; 03869 } 03870 } 03871 } 03872 } 03873 } 03874 03875 prefinputpri = gCoreContext->GetNumSetting("PrefInputPriority", 2); 03876 int hdtvpriority = gCoreContext->GetNumSetting("HDTVRecPriority", 0); 03877 int wspriority = gCoreContext->GetNumSetting("WSRecPriority", 0); 03878 int autopriority = gCoreContext->GetNumSetting("AutoRecPriority", 0); 03879 int slpriority = gCoreContext->GetNumSetting("SignLangRecPriority", 0); 03880 int onscrpriority = gCoreContext->GetNumSetting("OnScrSubRecPriority", 0); 03881 int ccpriority = gCoreContext->GetNumSetting("CCRecPriority", 0); 03882 int hhpriority = gCoreContext->GetNumSetting("HardHearRecPriority", 0); 03883 int adpriority = gCoreContext->GetNumSetting("AudioDescRecPriority", 0); 03884 03885 int autostrata = autopriority * 2 + 1; 03886 03887 QString pwrpri = "channel.recpriority + cardinput.recpriority"; 03888 03889 if (prefinputpri) 03890 pwrpri += QString(" + " 03891 "(cardinput.cardinputid = RECTABLE.prefinput) * %1").arg(prefinputpri); 03892 03893 if (hdtvpriority) 03894 pwrpri += QString(" + (program.hdtv > 0 OR " 03895 "FIND_IN_SET('HDTV', program.videoprop) > 0) * %1").arg(hdtvpriority); 03896 03897 if (wspriority) 03898 pwrpri += QString(" + " 03899 "(FIND_IN_SET('WIDESCREEN', program.videoprop) > 0) * %1").arg(wspriority); 03900 03901 if (slpriority) 03902 pwrpri += QString(" + " 03903 "(FIND_IN_SET('SIGNED', program.subtitletypes) > 0) * %1").arg(slpriority); 03904 03905 if (onscrpriority) 03906 pwrpri += QString(" + " 03907 "(FIND_IN_SET('ONSCREEN', program.subtitletypes) > 0) * %1").arg(onscrpriority); 03908 03909 if (ccpriority) 03910 pwrpri += QString(" + " 03911 "(FIND_IN_SET('NORMAL', program.subtitletypes) > 0 OR " 03912 "program.closecaptioned > 0 OR program.subtitled > 0) * %1").arg(ccpriority); 03913 03914 if (hhpriority) 03915 pwrpri += QString(" + " 03916 "(FIND_IN_SET('HARDHEAR', program.subtitletypes) > 0 OR " 03917 "FIND_IN_SET('HARDHEAR', program.audioprop) > 0) * %1").arg(hhpriority); 03918 03919 if (adpriority) 03920 pwrpri += QString(" + " 03921 "(FIND_IN_SET('VISUALIMPAIR', program.audioprop) > 0) * %1").arg(adpriority); 03922 03923 MSqlQuery result(dbConn); 03924 03925 result.prepare(QString("SELECT recpriority, selectclause FROM %1;") 03926 .arg(priorityTable)); 03927 03928 if (!result.exec()) 03929 { 03930 MythDB::DBError("Power Priority", result); 03931 return; 03932 } 03933 03934 while (result.next()) 03935 { 03936 if (result.value(0).toInt()) 03937 { 03938 QString sclause = result.value(1).toString(); 03939 sclause.remove(QRegExp("^\\s*AND\\s+", Qt::CaseInsensitive)); 03940 sclause.remove(';'); 03941 pwrpri += QString(" + (%1) * %2").arg(sclause) 03942 .arg(result.value(0).toInt()); 03943 } 03944 } 03945 pwrpri += QString(" AS powerpriority "); 03946 03947 pwrpri.replace("program.","p."); 03948 pwrpri.replace("channel.","c."); 03949 QString query = QString( 03950 "SELECT " 03951 " c.chanid, c.sourceid, p.starttime, "// 0-2 03952 " p.endtime, p.title, p.subtitle, "// 3-5 03953 " p.description, c.channum, c.callsign, "// 6-8 03954 " c.name, oldrecduplicate, p.category, "// 9-11 03955 " RECTABLE.recpriority, RECTABLE.dupin, recduplicate, "//12-14 03956 " findduplicate, RECTABLE.type, RECTABLE.recordid, "//15-17 03957 " p.starttime - INTERVAL RECTABLE.startoffset " 03958 " minute AS recstartts, " //18 03959 " p.endtime + INTERVAL RECTABLE.endoffset " 03960 " minute AS recendts, " //19 03961 " p.previouslyshown, "//20 03962 " RECTABLE.recgroup, RECTABLE.dupmethod, c.commmethod, "//21-23 03963 " capturecard.cardid, cardinput.cardinputid,p.seriesid, "//24-26 03964 " p.programid, RECTABLE.inetref, p.category_type, "//27-29 03965 " p.airdate, p.stars, p.originalairdate, "//30-32 03966 " RECTABLE.inactive, RECTABLE.parentid, recordmatch.findid, "//33-35 03967 " RECTABLE.playgroup, oldrecstatus.recstatus, "//36-37 03968 " oldrecstatus.reactivate, p.videoprop+0, "//38-39 03969 " p.subtitletypes+0, p.audioprop+0, RECTABLE.storagegroup, "//40-42 03970 " capturecard.hostname, recordmatch.oldrecstatus, " 03971 " RECTABLE.avg_delay, "//43-45 03972 " oldrecstatus.future, cardinput.schedorder, ") + //46-47 03973 pwrpri + QString( 03974 "FROM recordmatch " 03975 "INNER JOIN RECTABLE ON (recordmatch.recordid = RECTABLE.recordid) " 03976 "INNER JOIN program AS p " 03977 "ON ( recordmatch.chanid = p.chanid AND " 03978 " recordmatch.starttime = p.starttime AND " 03979 " recordmatch.manualid = p.manualid ) " 03980 "INNER JOIN channel AS c " 03981 "ON ( c.chanid = p.chanid ) " 03982 "INNER JOIN cardinput ON (c.sourceid = cardinput.sourceid) " 03983 "INNER JOIN capturecard ON (capturecard.cardid = cardinput.cardid) " 03984 "LEFT JOIN oldrecorded as oldrecstatus " 03985 "ON ( oldrecstatus.station = c.callsign AND " 03986 " oldrecstatus.starttime = p.starttime AND " 03987 " oldrecstatus.title = p.title ) " 03988 "WHERE p.endtime > (NOW() - INTERVAL 480 MINUTE) " 03989 "ORDER BY RECTABLE.recordid DESC, p.starttime, p.title, c.callsign, " 03990 " c.channum "); 03991 query.replace("RECTABLE", schedTmpRecord); 03992 03993 LOG(VB_SCHEDULE, LOG_INFO, QString(" |-- Start DB Query...")); 03994 03995 gettimeofday(&dbstart, NULL); 03996 result.prepare(query); 03997 if (!result.exec()) 03998 { 03999 MythDB::DBError("AddNewRecords", result); 04000 return; 04001 } 04002 gettimeofday(&dbend, NULL); 04003 04004 LOG(VB_SCHEDULE, LOG_INFO, 04005 QString(" |-- %1 results in %2 sec. Processing...") 04006 .arg(result.size()) 04007 .arg(((dbend.tv_sec - dbstart.tv_sec) * 1000000 + 04008 (dbend.tv_usec - dbstart.tv_usec)) / 1000000.0)); 04009 04010 RecordingInfo *lastp = NULL; 04011 04012 while (result.next()) 04013 { 04014 // If this is the same program we saw in the last pass and it 04015 // wasn't a viable candidate, then neither is this one so 04016 // don't bother with it. This is essentially an early call to 04017 // PruneRedundants(). 04018 uint recordid = result.value(17).toUInt(); 04019 QDateTime startts = result.value(2).toDateTime(); 04020 QString title = result.value(4).toString(); 04021 QString callsign = result.value(8).toString(); 04022 if (lastp && lastp->GetRecordingStatus() != rsUnknown 04023 && lastp->GetRecordingStatus() != rsOffLine 04024 && lastp->GetRecordingStatus() != rsDontRecord 04025 && recordid == lastp->GetRecordingRuleID() 04026 && startts == lastp->GetScheduledStartTime() 04027 && title == lastp->GetTitle() 04028 && callsign == lastp->GetChannelSchedulingID()) 04029 continue; 04030 04031 RecordingInfo *p = new RecordingInfo( 04032 title, 04033 result.value(5).toString(),//subtitle 04034 result.value(6).toString(),//description 04035 0, // season 04036 0, // episode 04037 result.value(11).toString(),//category 04038 04039 result.value(0).toUInt(),//chanid 04040 result.value(7).toString(),//channum 04041 callsign, 04042 result.value(9).toString(),//channame 04043 04044 result.value(21).toString(),//recgroup 04045 result.value(36).toString(),//playgroup 04046 04047 result.value(43).toString(),//hostname 04048 result.value(42).toString(),//storagegroup 04049 04050 result.value(30).toUInt(),//year 04051 04052 result.value(26).toString(),//seriesid 04053 result.value(27).toString(),//programid 04054 result.value(28).toString(),//inetref 04055 result.value(29).toString(),//catType 04056 04057 result.value(12).toInt(),//recpriority 04058 04059 startts, 04060 result.value(3).toDateTime(),//endts 04061 result.value(18).toDateTime(),//recstartts 04062 result.value(19).toDateTime(),//recendts 04063 04064 result.value(31).toDouble(),//stars 04065 (result.value(32).isNull()) ? QDate() : 04066 QDate::fromString(result.value(32).toString(), Qt::ISODate), 04067 //originalAirDate 04068 04069 result.value(20).toInt(),//repeat 04070 04071 RecStatusType(result.value(37).toInt()),//oldrecstatus 04072 result.value(38).toInt(),//reactivate 04073 04074 recordid, 04075 result.value(34).toUInt(),//parentid 04076 RecordingType(result.value(16).toInt()),//rectype 04077 RecordingDupInType(result.value(13).toInt()),//dupin 04078 RecordingDupMethodType(result.value(22).toInt()),//dupmethod 04079 04080 result.value(1).toUInt(),//sourceid 04081 result.value(25).toUInt(),//inputid 04082 result.value(24).toUInt(),//cardid 04083 04084 result.value(35).toUInt(),//findid 04085 04086 result.value(23).toInt() == COMM_DETECT_COMMFREE,//commfree 04087 result.value(40).toUInt(),//subtitleType 04088 result.value(39).toUInt(),//videoproperties 04089 result.value(41).toUInt(),//audioproperties 04090 result.value(46).toInt());//future 04091 p->SetRecordingPriority2(result.value(47).toInt()); // schedorder 04092 04093 if (!p->future && !p->IsReactivated() && 04094 p->oldrecstatus != rsAborted && 04095 p->oldrecstatus != rsNotListed) 04096 { 04097 p->SetRecordingStatus(p->oldrecstatus); 04098 } 04099 04100 if (!recTypeRecPriorityMap.contains(p->GetRecordingRuleType())) 04101 { 04102 recTypeRecPriorityMap[p->GetRecordingRuleType()] = 04103 p->GetRecordingTypeRecPriority(p->GetRecordingRuleType()); 04104 } 04105 04106 p->SetRecordingPriority( 04107 p->GetRecordingPriority() + recTypeRecPriorityMap[p->GetRecordingRuleType()] + 04108 result.value(48).toInt() + 04109 ((autopriority) ? 04110 autopriority - (result.value(45).toInt() * autostrata / 200) : 0)); 04111 04112 // Check to see if the program is currently recording and if 04113 // the end time was changed. Ideally, checking for a new end 04114 // time should be done after PruneOverlaps, but that would 04115 // complicate the list handling. Do it here unless it becomes 04116 // problematic. 04117 RecIter rec = worklist.begin(); 04118 for ( ; rec != worklist.end(); ++rec) 04119 { 04120 RecordingInfo *r = *rec; 04121 if (p->IsSameTimeslot(*r)) 04122 { 04123 if (r->GetInputID() == p->GetInputID() && 04124 r->GetRecordingEndTime() != p->GetRecordingEndTime() && 04125 (r->GetRecordingRuleID() == p->GetRecordingRuleID() || 04126 p->GetRecordingRuleType() == kOverrideRecord)) 04127 ChangeRecordingEnd(r, p); 04128 delete p; 04129 p = NULL; 04130 break; 04131 } 04132 } 04133 if (p == NULL) 04134 continue; 04135 04136 lastp = p; 04137 04138 if (p->GetRecordingStatus() != rsUnknown) 04139 { 04140 tmpList.push_back(p); 04141 continue; 04142 } 04143 04144 RecStatusType newrecstatus = rsUnknown; 04145 // Check for rsOffLine 04146 if ((doRun || specsched) && 04147 (!cardMap.contains(p->GetCardID()) || !p->GetRecordingPriority2())) 04148 newrecstatus = rsOffLine; 04149 04150 // Check for rsTooManyRecordings 04151 if (checkTooMany && tooManyMap[p->GetRecordingRuleID()] && 04152 !p->IsReactivated()) 04153 { 04154 newrecstatus = rsTooManyRecordings; 04155 } 04156 04157 // Check for rsCurrentRecording and rsPreviousRecording 04158 if (p->GetRecordingRuleType() == kDontRecord) 04159 newrecstatus = rsDontRecord; 04160 else if (result.value(15).toInt() && !p->IsReactivated()) 04161 newrecstatus = rsPreviousRecording; 04162 else if (p->GetRecordingRuleType() != kSingleRecord && 04163 p->GetRecordingRuleType() != kOverrideRecord && 04164 !p->IsReactivated() && 04165 !(p->GetDuplicateCheckMethod() & kDupCheckNone)) 04166 { 04167 const RecordingDupInType dupin = p->GetDuplicateCheckSource(); 04168 04169 if ((dupin & kDupsNewEpi) && p->IsRepeat()) 04170 newrecstatus = rsRepeat; 04171 04172 if ((dupin & kDupsInOldRecorded) && result.value(10).toInt()) 04173 { 04174 if (result.value(44).toInt() == rsNeverRecord) 04175 newrecstatus = rsNeverRecord; 04176 else 04177 newrecstatus = rsPreviousRecording; 04178 } 04179 04180 if ((dupin & kDupsInRecorded) && result.value(14).toInt()) 04181 newrecstatus = rsCurrentRecording; 04182 } 04183 04184 bool inactive = result.value(33).toInt(); 04185 if (inactive) 04186 newrecstatus = rsInactive; 04187 04188 // Mark anything that has already passed as some type of 04189 // missed. If it survives PruneOverlaps, it will get deleted 04190 // or have its old status restored in PruneRedundants. 04191 if (p->GetRecordingEndTime() < schedTime) 04192 { 04193 if (p->future) 04194 newrecstatus = rsMissedFuture; 04195 else 04196 newrecstatus = rsMissed; 04197 } 04198 04199 p->SetRecordingStatus(newrecstatus); 04200 04201 tmpList.push_back(p); 04202 } 04203 04204 LOG(VB_SCHEDULE, LOG_INFO, " +-- Cleanup..."); 04205 RecIter tmp = tmpList.begin(); 04206 for ( ; tmp != tmpList.end(); ++tmp) 04207 worklist.push_back(*tmp); 04208 } 04209 04210 void Scheduler::AddNotListed(void) { 04211 04212 struct timeval dbstart, dbend; 04213 RecList tmpList; 04214 04215 QString query = QString( 04216 "SELECT RECTABLE.title, RECTABLE.subtitle, " // 0,1 04217 " RECTABLE.description, RECTABLE.season, " // 2,3 04218 " RECTABLE.episode, RECTABLE.category, " // 4,5 04219 " RECTABLE.chanid, channel.channum, " // 6,7 04220 " RECTABLE.station, channel.name, " // 8,9 04221 " RECTABLE.recgroup, RECTABLE.playgroup, " // 10,11 04222 " RECTABLE.seriesid, RECTABLE.programid, " // 12,13 04223 " RECTABLE.inetref, RECTABLE.recpriority, " // 14,15 04224 " RECTABLE.startdate, RECTABLE.starttime, " // 16,17 04225 " RECTABLE.enddate, RECTABLE.endtime, " // 18,19 04226 " RECTABLE.recordid, RECTABLE.type, " // 20,21 04227 " RECTABLE.dupin, RECTABLE.dupmethod, " // 22,23 04228 " RECTABLE.findid, " // 24 04229 " RECTABLE.startoffset, RECTABLE.endoffset, " // 25,26 04230 " channel.commmethod " // 27 04231 "FROM RECTABLE " 04232 "INNER JOIN channel ON (channel.chanid = RECTABLE.chanid) " 04233 "LEFT JOIN recordmatch on RECTABLE.recordid = recordmatch.recordid " 04234 "WHERE (type = %1 OR type = %2 OR type = %3 OR type = %4) AND " 04235 " recordmatch.chanid IS NULL") 04236 .arg(kSingleRecord) 04237 .arg(kTimeslotRecord) 04238 .arg(kWeekslotRecord) 04239 .arg(kOverrideRecord); 04240 04241 query.replace("RECTABLE", recordTable); 04242 04243 LOG(VB_SCHEDULE, LOG_INFO, QString(" |-- Start DB Query...")); 04244 04245 gettimeofday(&dbstart, NULL); 04246 MSqlQuery result(dbConn); 04247 result.prepare(query); 04248 bool ok = result.exec(); 04249 gettimeofday(&dbend, NULL); 04250 04251 if (!ok) 04252 { 04253 MythDB::DBError("AddNotListed", result); 04254 return; 04255 } 04256 04257 LOG(VB_SCHEDULE, LOG_INFO, 04258 QString(" |-- %1 results in %2 sec. Processing...") 04259 .arg(result.size()) 04260 .arg(((dbend.tv_sec - dbstart.tv_sec) * 1000000 + 04261 (dbend.tv_usec - dbstart.tv_usec)) / 1000000.0)); 04262 04263 QDateTime now = QDateTime::currentDateTime(); 04264 04265 while (result.next()) 04266 { 04267 RecordingType rectype = RecordingType(result.value(21).toInt()); 04268 QDateTime startts(result.value(16).toDate(), result.value(17).toTime()); 04269 QDateTime endts( result.value(18).toDate(), result.value(19).toTime()); 04270 04271 if (rectype == kTimeslotRecord) 04272 { 04273 int days = startts.daysTo(now); 04274 04275 startts = startts.addDays(days); 04276 endts = endts.addDays(days); 04277 04278 if (endts < now) 04279 { 04280 startts = startts.addDays(1); 04281 endts = endts.addDays(1); 04282 } 04283 } 04284 else if (rectype == kWeekslotRecord) 04285 { 04286 int weeks = (startts.daysTo(now) + 6) / 7; 04287 04288 startts = startts.addDays(weeks * 7); 04289 endts = endts.addDays(weeks * 7); 04290 04291 if (endts < now) 04292 { 04293 startts = startts.addDays(7); 04294 endts = endts.addDays(7); 04295 } 04296 } 04297 04298 QDateTime recstartts = startts.addSecs(result.value(25).toInt() * -60); 04299 QDateTime recendts = endts.addSecs( result.value(26).toInt() * +60); 04300 04301 if (recstartts >= recendts) 04302 { 04303 // start/end-offsets are invalid so ignore 04304 recstartts = startts; 04305 recendts = endts; 04306 } 04307 04308 // Don't bother if the end time has already passed 04309 if (recendts < schedTime) 04310 continue; 04311 04312 bool sor = (kSingleRecord == rectype) || (kOverrideRecord == rectype); 04313 04314 RecordingInfo *p = new RecordingInfo( 04315 result.value(0).toString(), // Title 04316 (sor) ? result.value(1).toString() : QString(), // Subtitle 04317 (sor) ? result.value(2).toString() : QString(), // Description 04318 result.value(3).toUInt(), // Season 04319 result.value(4).toUInt(), // Episode 04320 QString(), // Category 04321 04322 result.value(6).toUInt(), // Chanid 04323 result.value(7).toString(), // Channel number 04324 result.value(8).toString(), // Call Sign 04325 result.value(9).toString(), // Channel name 04326 04327 result.value(10).toString(), // Recgroup 04328 result.value(11).toString(), // Playgroup 04329 04330 result.value(12).toString(), // Series ID 04331 result.value(13).toString(), // Program ID 04332 result.value(14).toString(), // Inetref 04333 04334 result.value(15).toInt(), // Rec priority 04335 04336 startts, endts, 04337 recstartts, recendts, 04338 04339 rsNotListed, // Recording Status 04340 04341 result.value(20).toUInt(), // Recording ID 04342 RecordingType(result.value(21).toInt()), // Recording type 04343 04344 RecordingDupInType(result.value(22).toInt()), // DupIn type 04345 RecordingDupMethodType(result.value(23).toInt()), // Dup method 04346 04347 result.value(24).toUInt(), // Find ID 04348 04349 result.value(27).toInt() == COMM_DETECT_COMMFREE); // Comm Free 04350 04351 tmpList.push_back(p); 04352 } 04353 04354 RecIter tmp = tmpList.begin(); 04355 for ( ; tmp != tmpList.end(); ++tmp) 04356 worklist.push_back(*tmp); 04357 } 04358 04363 void Scheduler::GetAllScheduled(RecList &proglist) 04364 { 04365 QString query = QString( 04366 "SELECT record.title, record.subtitle, " // 0,1 04367 " record.description, record.season, " // 2,3 04368 " record.episode, record.category, " // 4,5 04369 " record.chanid, channel.channum, " // 6,7 04370 " record.station, channel.name, " // 8,9 04371 " record.recgroup, record.playgroup, " // 10,11 04372 " record.seriesid, record.programid, " // 12,13 04373 " record.inetref, record.recpriority, " // 14,15 04374 " record.startdate, record.starttime, " // 16,17 04375 " record.enddate, record.endtime, " // 18,19 04376 " record.recordid, record.type, " // 20,21 04377 " record.dupin, record.dupmethod, " // 22,23 04378 " record.findid, " // 24 04379 " channel.commmethod " // 25 04380 "FROM record " 04381 "LEFT JOIN channel ON channel.callsign = record.station " 04382 "GROUP BY recordid " 04383 "ORDER BY title ASC"); 04384 04385 MSqlQuery result(MSqlQuery::InitCon()); 04386 result.prepare(query); 04387 04388 if (!result.exec()) 04389 { 04390 MythDB::DBError("GetAllScheduled", result); 04391 return; 04392 } 04393 04394 while (result.next()) 04395 { 04396 RecordingType rectype = RecordingType(result.value(21).toInt()); 04397 QDateTime startts; 04398 QDateTime endts; 04399 if (rectype == kSingleRecord || 04400 rectype == kDontRecord || 04401 rectype == kOverrideRecord || 04402 rectype == kTimeslotRecord || 04403 rectype == kWeekslotRecord) 04404 { 04405 startts = QDateTime(result.value(16).toDate(), 04406 result.value(17).toTime()); 04407 endts = QDateTime(result.value(18).toDate(), 04408 result.value(19).toTime()); 04409 } 04410 else 04411 { 04412 // put currentDateTime() in time fields to prevent 04413 // Invalid date/time warnings later 04414 startts = mythCurrentDateTime(); 04415 startts.setTime(QTime(0,0)); 04416 endts = startts; 04417 } 04418 04419 proglist.push_back(new RecordingInfo( 04420 result.value(0).toString(), result.value(1).toString(), 04421 result.value(2).toString(), result.value(3).toUInt(), 04422 result.value(4).toUInt(), result.value(5).toString(), 04423 04424 result.value(6).toUInt(), result.value(7).toString(), 04425 result.value(8).toString(), result.value(9).toString(), 04426 04427 result.value(10).toString(), result.value(11).toString(), 04428 04429 result.value(12).toString(), result.value(13).toString(), 04430 result.value(14).toString(), 04431 04432 result.value(15).toInt(), 04433 04434 startts, endts, 04435 startts, endts, 04436 04437 rsUnknown, 04438 04439 result.value(20).toUInt(), rectype, 04440 RecordingDupInType(result.value(22).toInt()), 04441 RecordingDupMethodType(result.value(23).toInt()), 04442 04443 result.value(24).toUInt(), 04444 04445 result.value(25).toInt() == COMM_DETECT_COMMFREE)); 04446 } 04447 } 04448 04450 // Storage Scheduler sort order routines 04451 // Sort mode-preferred to least-preferred (true == a more preferred than b) 04452 // 04453 // Prefer local over remote and to balance Disk I/O (weight), then free space 04454 static bool comp_storage_combination(FileSystemInfo *a, FileSystemInfo *b) 04455 { 04456 // local over remote 04457 if (a->isLocal() && !b->isLocal()) 04458 { 04459 if (a->getWeight() <= b->getWeight()) 04460 { 04461 return true; 04462 } 04463 } 04464 else if (a->isLocal() == b->isLocal()) 04465 { 04466 if (a->getWeight() < b->getWeight()) 04467 { 04468 return true; 04469 } 04470 else if (a->getWeight() > b->getWeight()) 04471 { 04472 return false; 04473 } 04474 else if (a->getFreeSpace() > b->getFreeSpace()) 04475 { 04476 return true; 04477 } 04478 } 04479 else if (!a->isLocal() && b->isLocal()) 04480 { 04481 if (a->getWeight() < b->getWeight()) 04482 { 04483 return true; 04484 } 04485 } 04486 04487 return false; 04488 } 04489 04490 // prefer dirs with more percentage free space over dirs with less 04491 static bool comp_storage_perc_free_space(FileSystemInfo *a, FileSystemInfo *b) 04492 { 04493 if (a->getTotalSpace() == 0) 04494 return false; 04495 04496 if (b->getTotalSpace() == 0) 04497 return true; 04498 04499 if ((a->getFreeSpace() * 100.0) / a->getTotalSpace() > 04500 (b->getFreeSpace() * 100.0) / b->getTotalSpace()) 04501 return true; 04502 04503 return false; 04504 } 04505 04506 // prefer dirs with more absolute free space over dirs with less 04507 static bool comp_storage_free_space(FileSystemInfo *a, FileSystemInfo *b) 04508 { 04509 if (a->getFreeSpace() > b->getFreeSpace()) 04510 return true; 04511 04512 return false; 04513 } 04514 04515 // prefer dirs with less weight (disk I/O) over dirs with more weight 04516 static bool comp_storage_disk_io(FileSystemInfo *a, FileSystemInfo *b) 04517 { 04518 if (a->getWeight() < b->getWeight()) 04519 return true; 04520 04521 return false; 04522 } 04523 04525 04526 void Scheduler::GetNextLiveTVDir(uint cardid) 04527 { 04528 QMutexLocker lockit(&schedLock); 04529 04530 EncoderLink *tv = (*m_tvList)[cardid]; 04531 04532 if (!tv) 04533 return; 04534 04535 QDateTime cur = mythCurrentDateTime(); 04536 QString recording_dir; 04537 int fsID = FillRecordingDir( 04538 "LiveTV", 04539 (tv->IsLocal()) ? gCoreContext->GetHostName() : tv->GetHostName(), 04540 "LiveTV", cur, cur.addSecs(3600), cardid, 04541 recording_dir, reclist); 04542 04543 tv->SetNextLiveTVDir(recording_dir); 04544 04545 LOG(VB_FILE, LOG_INFO, LOC + QString("FindNextLiveTVDir: next dir is '%1'") 04546 .arg(recording_dir)); 04547 04548 if (m_expirer) // update auto expirer 04549 m_expirer->Update(cardid, fsID, true); 04550 } 04551 04552 int Scheduler::FillRecordingDir( 04553 const QString &title, 04554 const QString &hostname, 04555 const QString &storagegroup, 04556 const QDateTime &recstartts, 04557 const QDateTime &recendts, 04558 uint cardid, 04559 QString &recording_dir, 04560 const RecList &reclist) 04561 { 04562 LOG(VB_SCHEDULE, LOG_INFO, LOC + "FillRecordingDir: Starting"); 04563 04564 int fsID = -1; 04565 MSqlQuery query(MSqlQuery::InitCon()); 04566 QMap<QString, FileSystemInfo>::Iterator fsit; 04567 QMap<QString, FileSystemInfo>::Iterator fsit2; 04568 QString dirKey; 04569 QStringList strlist; 04570 RecordingInfo *thispg; 04571 StorageGroup mysgroup(storagegroup, hostname); 04572 QStringList dirlist = mysgroup.GetDirList(); 04573 QStringList recsCounted; 04574 list<FileSystemInfo *> fsInfoList; 04575 list<FileSystemInfo *>::iterator fslistit; 04576 04577 recording_dir.clear(); 04578 04579 if (dirlist.size() == 1) 04580 { 04581 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC + 04582 QString("FillRecordingDir: The only directory in the %1 Storage " 04583 "Group is %2, so it will be used by default.") 04584 .arg(storagegroup) .arg(dirlist[0])); 04585 recording_dir = dirlist[0]; 04586 LOG(VB_SCHEDULE, LOG_INFO, LOC + "FillRecordingDir: Finished"); 04587 04588 return -1; 04589 } 04590 04591 int weightPerRecording = 04592 gCoreContext->GetNumSetting("SGweightPerRecording", 10); 04593 int weightPerPlayback = 04594 gCoreContext->GetNumSetting("SGweightPerPlayback", 5); 04595 int weightPerCommFlag = 04596 gCoreContext->GetNumSetting("SGweightPerCommFlag", 5); 04597 int weightPerTranscode = 04598 gCoreContext->GetNumSetting("SGweightPerTranscode", 5); 04599 04600 QString storageScheduler = 04601 gCoreContext->GetSetting("StorageScheduler", "Combination"); 04602 int localStartingWeight = 04603 gCoreContext->GetNumSetting("SGweightLocalStarting", 04604 (storageScheduler != "Combination") ? 0 04605 : (int)(-1.99 * weightPerRecording)); 04606 int remoteStartingWeight = 04607 gCoreContext->GetNumSetting("SGweightRemoteStarting", 0); 04608 int maxOverlap = gCoreContext->GetNumSetting("SGmaxRecOverlapMins", 3) * 60; 04609 04610 FillDirectoryInfoCache(); 04611 04612 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC + 04613 "FillRecordingDir: Calculating initial FS Weights."); 04614 04615 for (fsit = fsInfoCache.begin(); fsit != fsInfoCache.end(); ++fsit) 04616 { 04617 FileSystemInfo *fs = &(*fsit); 04618 int tmpWeight = 0; 04619 04620 QString msg = QString(" %1:%2").arg(fs->getHostname()) 04621 .arg(fs->getPath()); 04622 if (fs->isLocal()) 04623 { 04624 tmpWeight = localStartingWeight; 04625 msg += " is local (" + QString::number(tmpWeight) + ")"; 04626 } 04627 else 04628 { 04629 tmpWeight = remoteStartingWeight; 04630 msg += " is remote (+" + QString::number(tmpWeight) + ")"; 04631 } 04632 04633 fs->setWeight(tmpWeight); 04634 04635 tmpWeight = gCoreContext->GetNumSetting(QString("SGweightPerDir:%1:%2") 04636 .arg(fs->getHostname()).arg(fs->getPath()), 0); 04637 fs->setWeight(fs->getWeight() + tmpWeight); 04638 04639 if (tmpWeight) 04640 msg += ", has SGweightPerDir offset of " 04641 + QString::number(tmpWeight) + ")"; 04642 04643 msg += ". initial dir weight = " + QString::number(fs->getWeight()); 04644 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, msg); 04645 04646 fsInfoList.push_back(fs); 04647 } 04648 04649 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC + 04650 "FillRecordingDir: Adjusting FS Weights from inuseprograms."); 04651 04652 MSqlQuery saveRecDir(MSqlQuery::InitCon()); 04653 saveRecDir.prepare("UPDATE inuseprograms " 04654 "SET recdir = :RECDIR " 04655 "WHERE chanid = :CHANID AND " 04656 " starttime = :STARTTIME"); 04657 04658 query.prepare( 04659 "SELECT i.chanid, i.starttime, r.endtime, recusage, rechost, recdir " 04660 "FROM inuseprograms i, recorded r " 04661 "WHERE DATE_ADD(lastupdatetime, INTERVAL 16 MINUTE) > NOW() AND " 04662 " i.chanid = r.chanid AND " 04663 " i.starttime = r.starttime"); 04664 04665 if (!query.exec()) 04666 { 04667 MythDB::DBError(LOC + "FillRecordingDir", query); 04668 } 04669 else 04670 { 04671 while (query.next()) 04672 { 04673 uint recChanid = query.value(0).toUInt(); 04674 QDateTime recStart( query.value(1).toDateTime()); 04675 QDateTime recEnd( query.value(2).toDateTime()); 04676 QString recUsage( query.value(3).toString()); 04677 QString recHost( query.value(4).toString()); 04678 QString recDir( query.value(5).toString()); 04679 04680 if (recDir.isEmpty()) 04681 { 04682 ProgramInfo pginfo(recChanid, recStart); 04683 recDir = pginfo.DiscoverRecordingDirectory(); 04684 recDir = recDir.isEmpty() ? "_UNKNOWN_" : recDir; 04685 04686 saveRecDir.bindValue(":RECDIR", recDir); 04687 saveRecDir.bindValue(":CHANID", recChanid); 04688 saveRecDir.bindValue(":STARTTIME", recStart); 04689 if (!saveRecDir.exec()) 04690 MythDB::DBError(LOC + "FillRecordingDir", saveRecDir); 04691 } 04692 if (recDir == "_UNKNOWN_") 04693 continue; 04694 04695 for (fslistit = fsInfoList.begin(); 04696 fslistit != fsInfoList.end(); ++fslistit) 04697 { 04698 FileSystemInfo *fs = *fslistit; 04699 if ((recHost == fs->getHostname()) && 04700 (recDir == fs->getPath())) 04701 { 04702 int weightOffset = 0; 04703 04704 if (recUsage == kRecorderInUseID) 04705 { 04706 if (recEnd > recstartts.addSecs(maxOverlap)) 04707 { 04708 weightOffset += weightPerRecording; 04709 recsCounted << QString::number(recChanid) + ":" + 04710 recStart.toString(Qt::ISODate); 04711 } 04712 } 04713 else if (recUsage.contains(kPlayerInUseID)) 04714 weightOffset += weightPerPlayback; 04715 else if (recUsage == kFlaggerInUseID) 04716 weightOffset += weightPerCommFlag; 04717 else if (recUsage == kTranscoderInUseID) 04718 weightOffset += weightPerTranscode; 04719 04720 if (weightOffset) 04721 { 04722 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, 04723 QString(" %1 @ %2 in use by '%3' on %4:%5, FSID " 04724 "#%6, FSID weightOffset +%7.") 04725 .arg(recChanid) 04726 .arg(recStart.toString(Qt::ISODate)) 04727 .arg(recUsage).arg(recHost).arg(recDir) 04728 .arg(fs->getFSysID()).arg(weightOffset)); 04729 04730 // need to offset all directories on this filesystem 04731 for (fsit2 = fsInfoCache.begin(); 04732 fsit2 != fsInfoCache.end(); ++fsit2) 04733 { 04734 FileSystemInfo *fs2 = &(*fsit2); 04735 if (fs2->getFSysID() == fs->getFSysID()) 04736 { 04737 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, 04738 QString(" %1:%2 => old weight %3 plus " 04739 "%4 = %5") 04740 .arg(fs2->getHostname()) 04741 .arg(fs2->getPath()) 04742 .arg(fs2->getWeight()) 04743 .arg(weightOffset) 04744 .arg(fs2->getWeight() + weightOffset)); 04745 04746 fs2->setWeight(fs2->getWeight() + weightOffset); 04747 } 04748 } 04749 } 04750 break; 04751 } 04752 } 04753 } 04754 } 04755 04756 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC + 04757 "FillRecordingDir: Adjusting FS Weights from scheduler."); 04758 04759 RecConstIter recIter; 04760 for (recIter = reclist.begin(); recIter != reclist.end(); ++recIter) 04761 { 04762 thispg = *recIter; 04763 04764 if ((recendts < thispg->GetRecordingStartTime()) || 04765 (recstartts > thispg->GetRecordingEndTime()) || 04766 (thispg->GetRecordingStatus() != rsWillRecord) || 04767 (thispg->GetCardID() == 0) || 04768 (recsCounted.contains(QString("%1:%2").arg(thispg->GetChanID()) 04769 .arg(thispg->GetRecordingStartTime(ISODate)))) || 04770 (thispg->GetPathname().isEmpty())) 04771 continue; 04772 04773 for (fslistit = fsInfoList.begin(); 04774 fslistit != fsInfoList.end(); ++fslistit) 04775 { 04776 FileSystemInfo *fs = *fslistit; 04777 if ((fs->getHostname() == thispg->GetHostname()) && 04778 (fs->getPath() == thispg->GetPathname())) 04779 { 04780 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, 04781 QString("%1 @ %2 will record on %3:%4, FSID #%5, " 04782 "weightPerRecording +%6.") 04783 .arg(thispg->GetChanID()) 04784 .arg(thispg->GetRecordingStartTime(ISODate)) 04785 .arg(fs->getHostname()).arg(fs->getPath()) 04786 .arg(fs->getFSysID()).arg(weightPerRecording)); 04787 04788 for (fsit2 = fsInfoCache.begin(); 04789 fsit2 != fsInfoCache.end(); ++fsit2) 04790 { 04791 FileSystemInfo *fs2 = &(*fsit2); 04792 if (fs2->getFSysID() == fs->getFSysID()) 04793 { 04794 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, 04795 QString(" %1:%2 => old weight %3 plus %4 = %5") 04796 .arg(fs2->getHostname()).arg(fs2->getPath()) 04797 .arg(fs2->getWeight()).arg(weightPerRecording) 04798 .arg(fs2->getWeight() + weightPerRecording)); 04799 04800 fs2->setWeight(fs2->getWeight() + weightPerRecording); 04801 } 04802 } 04803 break; 04804 } 04805 } 04806 } 04807 04808 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, 04809 QString("Using '%1' Storage Scheduler directory sorting algorithm.") 04810 .arg(storageScheduler)); 04811 04812 if (storageScheduler == "BalancedFreeSpace") 04813 fsInfoList.sort(comp_storage_free_space); 04814 else if (storageScheduler == "BalancedPercFreeSpace") 04815 fsInfoList.sort(comp_storage_perc_free_space); 04816 else if (storageScheduler == "BalancedDiskIO") 04817 fsInfoList.sort(comp_storage_disk_io); 04818 else // default to using original method 04819 fsInfoList.sort(comp_storage_combination); 04820 04821 if (VERBOSE_LEVEL_CHECK(VB_FILE | VB_SCHEDULE, LOG_INFO)) 04822 { 04823 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, 04824 "--- FillRecordingDir Sorted fsInfoList start ---"); 04825 for (fslistit = fsInfoList.begin();fslistit != fsInfoList.end(); 04826 ++fslistit) 04827 { 04828 FileSystemInfo *fs = *fslistit; 04829 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, QString("%1:%2") 04830 .arg(fs->getHostname()) .arg(fs->getPath())); 04831 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, QString(" Location : %1") 04832 .arg((fs->isLocal()) ? "local" : "remote")); 04833 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, QString(" weight : %1") 04834 .arg(fs->getWeight())); 04835 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, QString(" free space : %5") 04836 .arg(fs->getFreeSpace())); 04837 } 04838 LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, 04839 "--- FillRecordingDir Sorted fsInfoList end ---"); 04840 } 04841 04842 // This code could probably be expanded to check the actual bitrate the 04843 // recording will record at for analog broadcasts that are encoded locally. 04844 // maxSizeKB is 1/3 larger than required as this is what the auto expire 04845 // uses 04846 EncoderLink *nexttv = (*m_tvList)[cardid]; 04847 long long maxByterate = nexttv->GetMaxBitrate() / 8; 04848 long long maxSizeKB = (maxByterate + maxByterate/3) * 04849 recstartts.secsTo(recendts) / 1024; 04850 04851 bool simulateAutoExpire = 04852 ((gCoreContext->GetSetting("StorageScheduler") == "BalancedFreeSpace") && 04853 (m_expirer) && 04854 (fsInfoList.size() > 1)); 04855 04856 // Loop though looking for a directory to put the file in. The first time 04857 // through we look for directories with enough free space in them. If we 04858 // can't find a directory that way we loop through and pick the first good 04859 // one from the list no matter how much free space it has. We assume that 04860 // something will have to be expired for us to finish the recording. 04861 // pass 1: try to fit onto an existing file system with enough free space 04862 // pass 2: fit onto the file system with the lowest priority files to be 04863 // expired this is used only with multiple file systems 04864 // Estimates are made by simulating each expiry until one of 04865 // the file systems has enough sapce to fit the new file. 04866 // pass 3: fit onto the first file system that will take it with lowest 04867 // priority files on this file system expired 04868 for (unsigned int pass = 1; pass <= 3; pass++) 04869 { 04870 bool foundDir = false; 04871 04872 if ((pass == 2) && simulateAutoExpire) 04873 { 04874 // setup a container of remaining space for all the file systems 04875 QMap <int , long long> remainingSpaceKB; 04876 for (fslistit = fsInfoList.begin(); 04877 fslistit != fsInfoList.end(); ++fslistit) 04878 { 04879 remainingSpaceKB[(*fslistit)->getFSysID()] = 04880 (*fslistit)->getFreeSpace(); 04881 } 04882 04883 // get list of expirable programs 04884 pginfolist_t expiring; 04885 m_expirer->GetAllExpiring(expiring); 04886 04887 for(pginfolist_t::iterator it=expiring.begin(); 04888 it != expiring.end(); ++it) 04889 { 04890 // find the filesystem its on 04891 FileSystemInfo *fs = NULL; 04892 for (fslistit = fsInfoList.begin(); 04893 fslistit != fsInfoList.end(); ++fslistit) 04894 { 04895 // recording is not on this filesystem's host 04896 if ((*it)->GetHostname() != (*fslistit)->getHostname()) 04897 continue; 04898 04899 // directory is not in the Storage Group dir list 04900 if (!dirlist.contains((*fslistit)->getPath())) 04901 continue; 04902 04903 QString filename = 04904 (*fslistit)->getPath() + "/" + (*it)->GetPathname(); 04905 04906 // recording is local 04907 if ((*it)->GetHostname() == gCoreContext->GetHostName()) 04908 { 04909 QFile checkFile(filename); 04910 04911 if (checkFile.exists()) 04912 { 04913 fs = *fslistit; 04914 break; 04915 } 04916 } 04917 else // recording is remote 04918 { 04919 QString backuppath = (*it)->GetPathname(); 04920 ProgramInfo *programinfo = *it; 04921 bool foundSlave = false; 04922 04923 QMap<int, EncoderLink *>::Iterator enciter = 04924 m_tvList->begin(); 04925 for (; enciter != m_tvList->end(); ++enciter) 04926 { 04927 if ((*enciter)->GetHostName() == 04928 programinfo->GetHostname()) 04929 { 04930 (*enciter)->CheckFile(programinfo); 04931 foundSlave = true; 04932 break; 04933 } 04934 } 04935 if (foundSlave && 04936 programinfo->GetPathname() == filename) 04937 { 04938 fs = *fslistit; 04939 programinfo->SetPathname(backuppath); 04940 break; 04941 } 04942 programinfo->SetPathname(backuppath); 04943 } 04944 } 04945 04946 if (!fs) 04947 { 04948 LOG(VB_GENERAL, LOG_ERR, 04949 QString("Unable to match '%1' " 04950 "to any file system. Ignoring it.") 04951 .arg((*it)->GetBasename())); 04952 continue; 04953 } 04954 04955 // add this files size to the remaining free space 04956 remainingSpaceKB[fs->getFSysID()] += 04957 (*it)->GetFilesize() / 1024; 04958 04959 // check if we have enough space for new file 04960 long long desiredSpaceKB = 04961 m_expirer->GetDesiredSpace(fs->getFSysID()); 04962 04963 if (remainingSpaceKB[fs->getFSysID()] > 04964 (desiredSpaceKB + maxSizeKB)) 04965 { 04966 recording_dir = fs->getPath(); 04967 fsID = fs->getFSysID(); 04968 04969 LOG(VB_FILE, LOG_INFO, 04970 QString("pass 2: '%1' will record in '%2' " 04971 "although there is only %3 MB free and the " 04972 "AutoExpirer wants at least %4 MB. This " 04973 "directory has the highest priority files " 04974 "to be expired from the AutoExpire list and " 04975 "there are enough that the Expirer should " 04976 "be able to free up space for this recording.") 04977 .arg(title).arg(recording_dir) 04978 .arg(fs->getFreeSpace() / 1024) 04979 .arg(desiredSpaceKB / 1024)); 04980 04981 foundDir = true; 04982 break; 04983 } 04984 } 04985 04986 m_expirer->ClearExpireList(expiring); 04987 } 04988 else // passes 1 & 3 (or 1 & 2 if !simulateAutoExpire) 04989 { 04990 for (fslistit = fsInfoList.begin(); 04991 fslistit != fsInfoList.end(); ++fslistit) 04992 { 04993 long long desiredSpaceKB = 0; 04994 FileSystemInfo *fs = *fslistit; 04995 if (m_expirer) 04996 desiredSpaceKB = 04997 m_expirer->GetDesiredSpace(fs->getFSysID()); 04998 04999 if ((fs->getHostname() == hostname) && 05000 (dirlist.contains(fs->getPath())) && 05001 ((pass > 1) || 05002 (fs->getFreeSpace() > (desiredSpaceKB + maxSizeKB)))) 05003 { 05004 recording_dir = fs->getPath(); 05005 fsID = fs->getFSysID(); 05006 05007 if (pass == 1) 05008 LOG(VB_FILE, LOG_INFO, 05009 QString("pass 1: '%1' will record in " 05010 "'%2' which has %3 MB free. This recording " 05011 "could use a max of %4 MB and the " 05012 "AutoExpirer wants to keep %5 MB free.") 05013 .arg(title) 05014 .arg(recording_dir) 05015 .arg(fs->getFreeSpace() / 1024) 05016 .arg(maxSizeKB / 1024) 05017 .arg(desiredSpaceKB / 1024)); 05018 else 05019 LOG(VB_FILE, LOG_INFO, 05020 QString("pass %1: '%2' will record in " 05021 "'%3' although there is only %4 MB free and " 05022 "the AutoExpirer wants at least %5 MB. " 05023 "Something will have to be deleted or expired " 05024 "in order for this recording to complete " 05025 "successfully.") 05026 .arg(pass).arg(title) 05027 .arg(recording_dir) 05028 .arg(fs->getFreeSpace() / 1024) 05029 .arg(desiredSpaceKB / 1024)); 05030 05031 foundDir = true; 05032 break; 05033 } 05034 } 05035 } 05036 05037 if (foundDir) 05038 break; 05039 } 05040 05041 LOG(VB_SCHEDULE, LOG_INFO, LOC + "FillRecordingDir: Finished"); 05042 return fsID; 05043 } 05044 05045 void Scheduler::FillDirectoryInfoCache(bool force) 05046 { 05047 if ((!force) && 05048 (fsInfoCacheFillTime > QDateTime::currentDateTime().addSecs(-180))) 05049 return; 05050 05051 QList<FileSystemInfo> fsInfos; 05052 05053 fsInfoCache.clear(); 05054 05055 if (m_mainServer) 05056 m_mainServer->GetFilesystemInfos(fsInfos); 05057 05058 QMap <int, bool> fsMap; 05059 QList<FileSystemInfo>::iterator it1; 05060 for (it1 = fsInfos.begin(); it1 != fsInfos.end(); ++it1) 05061 { 05062 fsMap[it1->getFSysID()] = true; 05063 fsInfoCache[it1->getHostname() + ":" + it1->getPath()] = *it1; 05064 } 05065 05066 LOG(VB_FILE, LOG_INFO, LOC + 05067 QString("FillDirectoryInfoCache: found %1 unique filesystems") 05068 .arg(fsMap.size())); 05069 05070 fsInfoCacheFillTime = QDateTime::currentDateTime(); 05071 } 05072 05073 void Scheduler::SchedPreserveLiveTV(void) 05074 { 05075 if (!livetvTime.isValid()) 05076 return; 05077 05078 if (livetvTime < schedTime) 05079 { 05080 livetvTime = QDateTime(); 05081 return; 05082 } 05083 05084 livetvpriority = gCoreContext->GetNumSetting("LiveTVPriority", 0); 05085 05086 // Build a list of active livetv programs 05087 QMap<int, EncoderLink *>::Iterator enciter = m_tvList->begin(); 05088 for (; enciter != m_tvList->end(); ++enciter) 05089 { 05090 EncoderLink *enc = *enciter; 05091 05092 if (kState_WatchingLiveTV != enc->GetState()) 05093 continue; 05094 05095 TunedInputInfo in; 05096 enc->IsBusy(&in); 05097 05098 if (!in.inputid) 05099 continue; 05100 05101 // Get the program that will be recording on this channel 05102 // at record start time, if this LiveTV session continues. 05103 RecordingInfo *dummy = new RecordingInfo( 05104 in.chanid, livetvTime, true, 4); 05105 05106 dummy->SetCardID(enc->GetCardID()); 05107 dummy->SetInputID(in.inputid); 05108 dummy->SetRecordingStatus(rsUnknown); 05109 05110 retrylist.push_front(dummy); 05111 } 05112 05113 if (retrylist.empty()) 05114 return; 05115 05116 MoveHigherRecords(false); 05117 05118 while (!retrylist.empty()) 05119 { 05120 RecordingInfo *p = retrylist.back(); 05121 delete p; 05122 retrylist.pop_back(); 05123 } 05124 } 05125 05126 /* Determines if the system was started by the auto-wakeup process */ 05127 bool Scheduler::WasStartedAutomatically() 05128 { 05129 bool autoStart = false; 05130 05131 QDateTime startupTime = QDateTime(); 05132 QString s = gCoreContext->GetSetting("MythShutdownWakeupTime", ""); 05133 if (s.length()) 05134 startupTime = QDateTime::fromString(s, Qt::ISODate); 05135 05136 // if we don't have a valid startup time assume we were started manually 05137 if (startupTime.isValid()) 05138 { 05139 // if we started within 15mins of the saved wakeup time assume we 05140 // started automatically to record or for a daily wakeup/shutdown period 05141 05142 if (abs(startupTime.secsTo(QDateTime::currentDateTime())) < (15 * 60)) 05143 { 05144 LOG(VB_SCHEDULE, LOG_INFO, 05145 "Close to auto-start time, AUTO-Startup assumed"); 05146 autoStart = true; 05147 } 05148 else 05149 LOG(VB_SCHEDULE, LOG_DEBUG, 05150 "NOT close to auto-start time, USER-initiated startup assumed"); 05151 } 05152 else if (!s.isEmpty()) 05153 LOG(VB_GENERAL, LOG_ERR, LOC + 05154 QString("Invalid MythShutdownWakeupTime specified in database (%1)") 05155 .arg(s)); 05156 05157 return autoStart; 05158 } 05159 05160 /* vim: set expandtab tabstop=4 shiftwidth=4: */
1.7.6.1