MythTV master
jobqueue.cpp
Go to the documentation of this file.
1
2#include <unistd.h>
3#include <sys/types.h>
4#include <sys/stat.h>
5#include <iostream>
6#include <cstdlib>
7#include <thread>
8#include <fcntl.h>
9#include <pthread.h>
10
11#include <QtGlobal> // for Q_OS_XXX
12#if QT_VERSION >= QT_VERSION_CHECK(6,5,0)
13#include <QtSystemDetection>
14#endif
15#ifdef Q_OS_BSD4
16static constexpr pthread_t PTHREAD_NULL { nullptr };
17#else
18static constexpr int PTHREAD_NULL { 0 };
19#endif
20#include <QDateTime>
21#include <QFileInfo>
22#include <QEvent>
23#include <QCoreApplication>
24#include <QTimeZone>
25
26#include "libmythbase/compat.h"
28#include "libmythbase/mthread.h"
30#include "libmythbase/mythconfig.h"
33#include "libmythbase/mythdb.h"
38
39#include "jobqueue.h"
40#include "previewgenerator.h"
41#include "programinfo.h"
42#include "recordinginfo.h"
43#include "recordingprofile.h"
44
45#define LOC QString("JobQueue: ")
46
47// Consider anything less than 4 hours as a "recent" job.
48static constexpr int64_t kRecentInterval {4LL * 60 * 60};
49
50JobQueue::JobQueue(bool master) :
51 m_hostname(gCoreContext->GetHostName()),
52 m_runningJobsLock(new QRecursiveMutex()),
53 m_isMaster(master),
54 m_queueThread(new MThread("JobQueue", this))
55{
56 m_jobQueueCPU = gCoreContext->GetNumSetting("JobQueueCPU", 0);
57
58#if !CONFIG_VALGRIND
59 QMutexLocker locker(&m_queueThreadCondLock);
60 //NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)
61 m_processQueue = true;
63#else
64 LOG(VB_GENERAL, LOG_ERR, LOC +
65 "The JobQueue has been disabled because "
66 "you compiled with the --enable-valgrind option.");
67#endif // CONFIG_VALGRIND
68
70}
71
73{
75 m_processQueue = false;
76 m_queueThreadCond.wakeAll();
77 m_queueThreadCondLock.unlock();
78
80 delete m_queueThread;
81 m_queueThread = nullptr;
82
84
85 delete m_runningJobsLock;
86}
87
88void JobQueue::customEvent(QEvent *e)
89{
90 if (e->type() == MythEvent::kMythEventMessage)
91 {
92 auto *me = dynamic_cast<MythEvent *>(e);
93 if (me == nullptr)
94 return;
95 QString message = me->Message();
96
97 if (message.startsWith("LOCAL_JOB"))
98 {
99 // LOCAL_JOB action ID jobID
100 // LOCAL_JOB action type chanid recstartts hostname
101 QString msg;
102 message = message.simplified();
103 QStringList tokens = message.split(" ", Qt::SkipEmptyParts);
104 const QString& action = tokens[1];
105 int jobID = -1;
106
107 if (tokens[2] == "ID")
108 {
109 jobID = tokens[3].toInt();
110 }
111 else
112 {
113 jobID = GetJobID(
114 tokens[2].toInt(),
115 tokens[3].toUInt(),
116 MythDate::fromString(tokens[4]));
117 }
118
119 m_runningJobsLock->lock();
120 if (!m_runningJobs.contains(jobID))
121 {
122 msg = QString("Unable to determine jobID for message: "
123 "%1. Program will not be flagged.")
124 .arg(message);
125 LOG(VB_GENERAL, LOG_ERR, LOC + msg);
126 m_runningJobsLock->unlock();
127 return;
128 }
129 m_runningJobsLock->unlock();
130
131 msg = QString("Received message '%1'").arg(message);
132 LOG(VB_JOBQUEUE, LOG_INFO, LOC + msg);
133
134 if ((action == "STOP") ||
135 (action == "PAUSE") ||
136 (action == "RESTART") ||
137 (action == "RESUME" ))
138 {
139 m_runningJobsLock->lock();
140
141 if (action == "STOP")
143 else if (action == "PAUSE")
145 else if (action == "RESUME")
147 else if (action == "RESTART")
149
150 m_runningJobsLock->unlock();
151 }
152 }
153 }
154}
155
157{
159 m_queueThreadCond.wakeAll();
160 m_queueThreadCondLock.unlock();
161
162 RecoverQueue();
163
165 m_queueThreadCond.wait(&m_queueThreadCondLock, 10 * 1000UL);
166 m_queueThreadCondLock.unlock();
167
168 ProcessQueue();
169}
170
172{
173 LOG(VB_JOBQUEUE, LOG_INFO, LOC + "ProcessQueue() started");
174
175 QString logInfo;
176 //int flags;
177 QString hostname;
178
179 QMap<int, int> jobStatus;
180 QString message;
181 QMap<int, JobQueueEntry> jobs;
182 bool atMax = false;
183 QMap<int, RunningJobInfo>::Iterator rjiter;
184
185 QMutexLocker locker(&m_queueThreadCondLock);
186 while (m_processQueue)
187 {
188 locker.unlock();
189
190 bool startedJobAlready = false;
191 auto sleepTime = gCoreContext->GetDurSetting<std::chrono::seconds>("JobQueueCheckFrequency", 30s);
192 int maxJobs = gCoreContext->GetNumSetting("JobQueueMaxSimultaneousJobs", 3);
193 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
194 QString("Currently set to run up to %1 job(s) max.")
195 .arg(maxJobs));
196
197 jobStatus.clear();
198
199 m_runningJobsLock->lock();
200 for (rjiter = m_runningJobs.begin(); rjiter != m_runningJobs.end();
201 ++rjiter)
202 {
203 if ((*rjiter).pginfo)
204 (*rjiter).pginfo->UpdateInUseMark();
205 }
206 m_runningJobsLock->unlock();
207
208 m_jobsRunning = 0;
209 GetJobsInQueue(jobs);
210
211 if (!jobs.empty())
212 {
213 bool inTimeWindow = InJobRunWindow();
214 for (const auto & job : std::as_const(jobs))
215 {
216 int status = job.status;
217 hostname = job.hostname;
218
219 if (((status == JOB_RUNNING) ||
220 (status == JOB_STARTING) ||
221 (status == JOB_PAUSED)) &&
222 (hostname == m_hostname))
224 }
225
226 message = QString("Currently Running %1 jobs.")
227 .arg(m_jobsRunning);
228 if (!inTimeWindow)
229 {
230 message += QString(" Jobs in Queue, but we are outside of the "
231 "Job Queue time window, no new jobs can be "
232 "started.");
233 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
234 }
235 else if (m_jobsRunning >= maxJobs)
236 {
237 message += " (At Maximum, no new jobs can be started until "
238 "a running job completes)";
239
240 if (!atMax)
241 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
242
243 atMax = true;
244 }
245 else
246 {
247 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
248 atMax = false;
249 }
250
251
252 for ( int x = 0;
253 (x < jobs.size()) && (m_jobsRunning < maxJobs); x++)
254 {
255 int jobID = jobs[x].id;
256 int cmds = jobs[x].cmds;
257 //flags = jobs[x].flags;
258 int status = jobs[x].status;
259 hostname = jobs[x].hostname;
260
261 if (!jobs[x].chanid)
262 logInfo = QString("jobID #%1").arg(jobID);
263 else
264 logInfo = QString("chanid %1 @ %2").arg(jobs[x].chanid)
265 .arg(jobs[x].startts);
266
267 // Should we even be looking at this job?
268 if (inTimeWindow &&
269 (!hostname.isEmpty()) &&
270 (hostname != m_hostname))
271 {
272 // Setting the status here will prevent us from processing
273 // any other jobs for this recording until this one is
274 // completed on the remote host.
275 jobStatus[jobID] = status;
276
277 message = QString("Skipping '%1' job for %2, "
278 "should run on '%3' instead")
279 .arg(JobText(jobs[x].type), logInfo,
280 hostname);
281 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
282 continue;
283 }
284
285 // Check to see if there was a previous job that is not done
286 if (inTimeWindow)
287 {
288 int otherJobID = GetRunningJobID(jobs[x].chanid,
289 jobs[x].recstartts);
290 if (otherJobID && (jobStatus.contains(otherJobID)) &&
291 (!(jobStatus[otherJobID] & JOB_DONE)))
292 {
293 message =
294 QString("Skipping '%1' job for %2, "
295 "Job ID %3 is already running for "
296 "this recording with a status of '%4'")
297 .arg(JobText(jobs[x].type), logInfo,
298 QString::number(otherJobID),
299 StatusText(jobStatus[otherJobID]));
300 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
301 continue;
302 }
303 }
304
305 jobStatus[jobID] = status;
306
307 // Are we allowed to run this job?
308 if (inTimeWindow && (!AllowedToRun(jobs[x])))
309 {
310 message = QString("Skipping '%1' job for %2, "
311 "not allowed to run on this backend.")
312 .arg(JobText(jobs[x].type), logInfo);
313 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
314 continue;
315 }
316
317 // Is this job scheduled for the future
318 if (jobs[x].schedruntime > MythDate::current())
319 {
320 message = QString("Skipping '%1' job for %2, this job is "
321 "not scheduled to run until %3.")
322 .arg(JobText(jobs[x].type), logInfo,
323 jobs[x].schedruntime
325 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
326 continue;
327 }
328
329 if (cmds & JOB_STOP)
330 {
331 // if we're trying to stop a job and it's not queued
332 // then lets send a STOP command
333 if (status != JOB_QUEUED) {
334 message = QString("Stopping '%1' job for %2")
335 .arg(JobText(jobs[x].type), logInfo);
336 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
337
338 m_runningJobsLock->lock();
339 if (m_runningJobs.contains(jobID))
341 m_runningJobsLock->unlock();
342
343 // ChangeJobCmds(m_db, jobID, JOB_RUN);
344 continue;
345
346 // if we're trying to stop a job and it's still queued
347 // then let's just change the status to cancelled so
348 // we don't try to run it from the queue
349 }
350
351 message = QString("Cancelling '%1' job for %2")
352 .arg(JobText(jobs[x].type), logInfo);
353 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
354
355 // at the bottom of this loop we requeue any jobs that
356 // are not currently queued and also not associated
357 // with a hostname so we must claim this job before we
358 // can cancel it
360 {
361 message = QString("Unable to claim '%1' job for %2")
362 .arg(JobText(jobs[x].type), logInfo);
363 LOG(VB_JOBQUEUE, LOG_ERR, LOC + message);
364 continue;
365 }
366
367 ChangeJobStatus(jobID, JOB_CANCELLED, "");
369 continue;
370 }
371
372 if ((cmds & JOB_PAUSE) && (status != JOB_QUEUED))
373 {
374 message = QString("Pausing '%1' job for %2")
375 .arg(JobText(jobs[x].type), logInfo);
376 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
377
378 m_runningJobsLock->lock();
379 if (m_runningJobs.contains(jobID))
381 m_runningJobsLock->unlock();
382
384 continue;
385 }
386
387 if ((cmds & JOB_RESTART) && (status != JOB_QUEUED))
388 {
389 message = QString("Restart '%1' job for %2")
390 .arg(JobText(jobs[x].type), logInfo);
391 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
392
393 m_runningJobsLock->lock();
394 if (m_runningJobs.contains(jobID))
396 m_runningJobsLock->unlock();
397
399 continue;
400 }
401
402 if (status != JOB_QUEUED)
403 {
404
405 if (hostname.isEmpty())
406 {
407 message = QString("Resetting '%1' job for %2 to %3 "
408 "status, because no hostname is set.")
409 .arg(JobText(jobs[x].type),
410 logInfo,
411 StatusText(JOB_QUEUED));
412 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
413
414 ChangeJobStatus(jobID, JOB_QUEUED, "");
416 }
417 else if (inTimeWindow)
418 {
419 message = QString("Skipping '%1' job for %2, "
420 "current job status is '%3'")
421 .arg(JobText(jobs[x].type),
422 logInfo,
423 StatusText(status));
424 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
425 }
426 continue;
427 }
428
429 // never start or claim more than one job in a single run
430 if (startedJobAlready)
431 continue;
432
433 if (inTimeWindow &&
434 (hostname.isEmpty()) &&
436 {
437 message = QString("Unable to claim '%1' job for %2")
438 .arg(JobText(jobs[x].type), logInfo);
439 LOG(VB_JOBQUEUE, LOG_ERR, LOC + message);
440 continue;
441 }
442
443 if (!inTimeWindow)
444 {
445 message = QString("Skipping '%1' job for %2, "
446 "current time is outside of the "
447 "Job Queue processing window.")
448 .arg(JobText(jobs[x].type), logInfo);
449 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
450 continue;
451 }
452
453 message = QString("Processing '%1' job for %2, "
454 "current status is '%3'")
455 .arg(JobText(jobs[x].type), logInfo,
456 StatusText(status));
457 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
458
459 ProcessJob(jobs[x]);
460
461 startedJobAlready = true;
462 }
463 }
464
465 if (QCoreApplication::applicationName() == MYTH_APPNAME_MYTHJOBQUEUE)
466 {
467 if (m_jobsRunning > 0)
468 {
470 {
472 LOG(VB_JOBQUEUE, LOG_INFO, QString("%1 jobs running. "
473 "Blocking shutdown.").arg(m_jobsRunning));
474 }
475 }
476 else
477 {
479 {
481 LOG(VB_JOBQUEUE, LOG_INFO, "No jobs running. "
482 "Allowing shutdown.");
483 }
484 }
485 }
486
487
488 locker.relock();
489 if (m_processQueue)
490 {
491 std::chrono::milliseconds st = startedJobAlready ? 5s : sleepTime;
492 if (st > 0ms)
493 m_queueThreadCond.wait(locker.mutex(), st.count());
494 }
495 }
496}
497
498bool JobQueue::QueueRecordingJobs(const RecordingInfo &recinfo, int jobTypes)
499{
500 if (jobTypes == JOB_NONE)
501 jobTypes = recinfo.GetAutoRunJobs();
502
503 if (recinfo.IsCommercialFree())
504 jobTypes &= (~JOB_COMMFLAG);
505
506 if (jobTypes != JOB_NONE)
507 {
508 QString jobHost = QString("");
509
510 if (gCoreContext->GetBoolSetting("JobsRunOnRecordHost", false))
511 jobHost = recinfo.GetHostname();
512
513 return JobQueue::QueueJobs(
514 jobTypes, recinfo.GetChanID(), recinfo.GetRecordingStartTime(),
515 "", "", jobHost);
516 }
517 return false;
518}
519
520bool JobQueue::QueueJob(int jobType, uint chanid, const QDateTime &recstartts,
521 const QString& args, const QString& comment, QString host,
522 int flags, int status, QDateTime schedruntime)
523{
524 int tmpStatus = JOB_UNKNOWN;
525 int tmpCmd = JOB_UNKNOWN;
526 int chanidInt = -1;
527
528 if(!schedruntime.isValid())
529 schedruntime = MythDate::current();
530
532
533 // In order to replace a job, we must have a chanid/recstartts combo
534 if (chanid)
535 {
536 int jobID = -1;
537 query.prepare("SELECT status, id, cmds FROM jobqueue "
538 "WHERE chanid = :CHANID AND starttime = :STARTTIME "
539 "AND type = :JOBTYPE;");
540 query.bindValue(":CHANID", chanid);
541 query.bindValue(":STARTTIME", recstartts);
542 query.bindValue(":JOBTYPE", jobType);
543
544 if (!query.exec())
545 {
546 MythDB::DBError("Error in JobQueue::QueueJob()", query);
547 return false;
548 }
549 if (query.next())
550 {
551 tmpStatus = query.value(0).toInt();
552 jobID = query.value(1).toInt();
553 tmpCmd = query.value(2).toInt();
554 }
555 switch (tmpStatus)
556 {
557 case JOB_UNKNOWN:
558 break;
559 case JOB_STARTING:
560 case JOB_RUNNING:
561 case JOB_PAUSED:
562 case JOB_STOPPING:
563 case JOB_ERRORING:
564 case JOB_ABORTING:
565 return false;
566 default:
568 break;
569 }
570 if (! (tmpStatus & JOB_DONE) && (tmpCmd & JOB_STOP))
571 return false;
572
573 chanidInt = chanid;
574 }
575
576 if (host.isNull())
577 host = QString("");
578
579 query.prepare("INSERT INTO jobqueue (chanid, starttime, inserttime, type, "
580 "status, statustime, schedruntime, hostname, args, comment, "
581 "flags) "
582 "VALUES (:CHANID, :STARTTIME, now(), :JOBTYPE, :STATUS, "
583 "now(), :SCHEDRUNTIME, :HOST, :ARGS, :COMMENT, :FLAGS);");
584
585 query.bindValue(":CHANID", chanidInt);
586 query.bindValue(":STARTTIME", recstartts);
587 query.bindValue(":JOBTYPE", jobType);
588 query.bindValue(":STATUS", status);
589 query.bindValue(":SCHEDRUNTIME", schedruntime);
590 query.bindValue(":HOST", host);
591 query.bindValue(":ARGS", args);
592 query.bindValue(":COMMENT", comment);
593 query.bindValue(":FLAGS", flags);
594
595 if (!query.exec())
596 {
597 MythDB::DBError("Error in JobQueue::StartJob()", query);
598 return false;
599 }
600
601 return true;
602}
603
604bool JobQueue::QueueJobs(int jobTypes, uint chanid, const QDateTime &recstartts,
605 const QString& args, const QString& comment, const QString& host)
606{
607 if (gCoreContext->GetBoolSetting("AutoTranscodeBeforeAutoCommflag", false))
608 {
609 if (jobTypes & JOB_METADATA)
610 QueueJob(JOB_METADATA, chanid, recstartts, args, comment, host);
611 if (jobTypes & JOB_TRANSCODE)
612 QueueJob(JOB_TRANSCODE, chanid, recstartts, args, comment, host);
613 if (jobTypes & JOB_COMMFLAG)
614 QueueJob(JOB_COMMFLAG, chanid, recstartts, args, comment, host);
615 }
616 else
617 {
618 if (jobTypes & JOB_METADATA)
619 QueueJob(JOB_METADATA, chanid, recstartts, args, comment, host);
620 if (jobTypes & JOB_COMMFLAG)
621 QueueJob(JOB_COMMFLAG, chanid, recstartts, args, comment, host);
622 if (jobTypes & JOB_TRANSCODE)
623 {
624 QDateTime schedruntime = MythDate::current();
625
626 int defer = gCoreContext->GetNumSetting("DeferAutoTranscodeDays", 0);
627 if (defer)
628 {
629#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
630 schedruntime = QDateTime(schedruntime.addDays(defer).date(),
631 QTime(0,0,0), Qt::UTC);
632#else
633 schedruntime = QDateTime(schedruntime.addDays(defer).date(),
634 QTime(0,0,0),
635 QTimeZone(QTimeZone::UTC));
636#endif
637 }
638
639 QueueJob(JOB_TRANSCODE, chanid, recstartts, args, comment, host,
640 0, JOB_QUEUED, schedruntime);
641 }
642 }
643
644 if (jobTypes & JOB_USERJOB1)
645 QueueJob(JOB_USERJOB1, chanid, recstartts, args, comment, host);
646 if (jobTypes & JOB_USERJOB2)
647 QueueJob(JOB_USERJOB2, chanid, recstartts, args, comment, host);
648 if (jobTypes & JOB_USERJOB3)
649 QueueJob(JOB_USERJOB3, chanid, recstartts, args, comment, host);
650 if (jobTypes & JOB_USERJOB4)
651 QueueJob(JOB_USERJOB4, chanid, recstartts, args, comment, host);
652
653 return true;
654}
655
656int JobQueue::GetJobID(int jobType, uint chanid, const QDateTime &recstartts)
657{
659
660 query.prepare("SELECT id FROM jobqueue "
661 "WHERE chanid = :CHANID AND starttime = :STARTTIME "
662 "AND type = :JOBTYPE;");
663 query.bindValue(":CHANID", chanid);
664 query.bindValue(":STARTTIME", recstartts);
665 query.bindValue(":JOBTYPE", jobType);
666
667 if (!query.exec())
668 {
669 MythDB::DBError("Error in JobQueue::GetJobID()", query);
670 return -1;
671 }
672 if (query.next())
673 return query.value(0).toInt();
674 return -1;
675}
676
678 int jobID, int &jobType, uint &chanid, QDateTime &recstartts)
679{
681
682 query.prepare("SELECT type, chanid, starttime FROM jobqueue "
683 "WHERE id = :ID;");
684
685 query.bindValue(":ID", jobID);
686
687 if (!query.exec())
688 {
689 MythDB::DBError("Error in JobQueue::GetJobInfoFromID()", query);
690 return false;
691 }
692 if (query.next())
693 {
694 jobType = query.value(0).toInt();
695 chanid = query.value(1).toUInt();
696 recstartts = MythDate::as_utc(query.value(2).toDateTime());
697 return true;
698 }
699 return false;
700}
701
703 int jobID, int &jobType, uint &chanid, QString &recstartts)
704{
705 QDateTime tmpStarttime;
706
707 bool result = JobQueue::GetJobInfoFromID(
708 jobID, jobType, chanid, tmpStarttime);
709
710 if (result)
711 recstartts = MythDate::toString(tmpStarttime, MythDate::kFilename);
712
713 return result;
714}
715
716int JobQueue::GetJobTypeFromName(const QString &name)
717{
718 if (!JobNameToType.contains(name))
719 {
720 LOG(VB_GENERAL, LOG_ERR, QString("'%1' is an invalid Job Name.")
721 .arg(name));
722 return JOB_NONE;
723 }
724 return JobNameToType[name];
725}
726
728{
729 QString message = QString("GLOBAL_JOB PAUSE ID %1").arg(jobID);
730 MythEvent me(message);
732
734}
735
737{
738 QString message = QString("GLOBAL_JOB RESUME ID %1").arg(jobID);
739 MythEvent me(message);
741
743}
744
746{
747 QString message = QString("GLOBAL_JOB RESTART ID %1").arg(jobID);
748 MythEvent me(message);
750
752}
753
755{
756 QString message = QString("GLOBAL_JOB STOP ID %1").arg(jobID);
757 MythEvent me(message);
759
761}
762
763bool JobQueue::DeleteAllJobs(uint chanid, const QDateTime &recstartts)
764{
766 QString message;
767
768 query.prepare("UPDATE jobqueue SET status = :CANCELLED "
769 "WHERE chanid = :CHANID AND starttime = :STARTTIME "
770 "AND status = :QUEUED;");
771
772 query.bindValue(":CANCELLED", JOB_CANCELLED);
773 query.bindValue(":CHANID", chanid);
774 query.bindValue(":STARTTIME", recstartts);
775 query.bindValue(":QUEUED", JOB_QUEUED);
776
777 if (!query.exec())
778 MythDB::DBError("Cancel Pending Jobs", query);
779
780 query.prepare("UPDATE jobqueue SET cmds = :CMD "
781 "WHERE chanid = :CHANID AND starttime = :STARTTIME "
782 "AND status <> :CANCELLED;");
783 query.bindValue(":CMD", JOB_STOP);
784 query.bindValue(":CHANID", chanid);
785 query.bindValue(":STARTTIME", recstartts);
786 query.bindValue(":CANCELLED", JOB_CANCELLED);
787
788 if (!query.exec())
789 {
790 MythDB::DBError("Stop Unfinished Jobs", query);
791 return false;
792 }
793
794 // wait until running job(s) are done
795 bool jobsAreRunning = true;
796 std::chrono::seconds totalSlept = 0s;
797 std::chrono::seconds maxSleep = 90s;
798 while (jobsAreRunning && totalSlept < maxSleep)
799 {
800 std::this_thread::sleep_for(1ms);
801 query.prepare("SELECT id FROM jobqueue "
802 "WHERE chanid = :CHANID and starttime = :STARTTIME "
803 "AND status NOT IN "
804 "(:FINISHED,:ABORTED,:ERRORED,:CANCELLED);");
805 query.bindValue(":CHANID", chanid);
806 query.bindValue(":STARTTIME", recstartts);
807 query.bindValue(":FINISHED", JOB_FINISHED);
808 query.bindValue(":ABORTED", JOB_ABORTED);
809 query.bindValue(":ERRORED", JOB_ERRORED);
810 query.bindValue(":CANCELLED", JOB_CANCELLED);
811
812 if (!query.exec())
813 {
814 MythDB::DBError("Stop Unfinished Jobs", query);
815 return false;
816 }
817
818 if (query.size() == 0)
819 {
820 jobsAreRunning = false;
821 continue;
822 }
823 if ((totalSlept % 5s) == 0s)
824 {
825 message = QString("Waiting on %1 jobs still running for "
826 "chanid %2 @ %3").arg(query.size())
827 .arg(chanid).arg(recstartts.toString(Qt::ISODate));
828 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
829 }
830
831 std::this_thread::sleep_for(1s);
832 totalSlept++;
833 }
834
835 if (totalSlept <= maxSleep)
836 {
837 query.prepare("DELETE FROM jobqueue "
838 "WHERE chanid = :CHANID AND starttime = :STARTTIME;");
839 query.bindValue(":CHANID", chanid);
840 query.bindValue(":STARTTIME", recstartts);
841
842 if (!query.exec())
843 MythDB::DBError("Delete All Jobs", query);
844 }
845 else
846 {
847 query.prepare("SELECT id, type, status, comment FROM jobqueue "
848 "WHERE chanid = :CHANID AND starttime = :STARTTIME "
849 "AND status <> :CANCELLED ORDER BY id;");
850
851 query.bindValue(":CHANID", chanid);
852 query.bindValue(":STARTTIME", recstartts);
853 query.bindValue(":CANCELLED", JOB_CANCELLED);
854
855 if (!query.exec())
856 {
857 MythDB::DBError("Error in JobQueue::DeleteAllJobs(), Unable "
858 "to query list of Jobs left in Queue.", query);
859 return false;
860 }
861
862 LOG(VB_GENERAL, LOG_ERR, LOC +
863 QString( "In DeleteAllJobs: There are Jobs "
864 "left in the JobQueue that are still running for "
865 "chanid %1 @ %2.").arg(chanid)
866 .arg(recstartts.toString(Qt::ISODate)));
867
868 while (query.next())
869 {
870 LOG(VB_GENERAL, LOG_ERR, LOC +
871 QString("Job ID %1: '%2' with status '%3' and comment '%4'")
872 .arg(query.value(0).toString(),
873 JobText(query.value(1).toInt()),
874 StatusText(query.value(2).toInt()),
875 query.value(3).toString()));
876 }
877
878 return false;
879 }
880
881 return true;
882}
883
885{
886 return JobQueue::SafeDeleteJob(jobID, 0, 0, QDateTime());
887}
888
889bool JobQueue::SafeDeleteJob(int jobID, int jobType, int chanid,
890 const QDateTime& recstartts)
891{
892 if (jobID < 0)
893 return false;
894
895 if (chanid)
896 {
897
898 int thisJob = GetJobID(jobType, chanid, recstartts);
899 QString msg;
900
901 if( thisJob != jobID)
902 {
903 msg = QString("JobType, chanid and starttime don't match jobID %1");
904 LOG(VB_JOBQUEUE, LOG_ERR, LOC + msg.arg(jobID));
905 return false;
906 }
907
908 if (JobQueue::IsJobRunning(jobType, chanid, recstartts))
909 {
910 msg = QString("Can't remove running JobID %1");
911 LOG(VB_GENERAL, LOG_ERR, LOC + msg.arg(jobID));
912 return false;
913 }
914 }
915
917
918 query.prepare("DELETE FROM jobqueue WHERE id = :ID;");
919
920 query.bindValue(":ID", jobID);
921
922 if (!query.exec())
923 {
924 MythDB::DBError("Error in JobQueue::SafeDeleteJob()", query);
925 return false;
926 }
927
928 return true;
929}
930
931bool JobQueue::ChangeJobCmds(int jobID, int newCmds)
932{
933 if (jobID < 0)
934 return false;
935
937
938 query.prepare("UPDATE jobqueue SET cmds = :CMDS WHERE id = :ID;");
939
940 query.bindValue(":CMDS", newCmds);
941 query.bindValue(":ID", jobID);
942
943 if (!query.exec())
944 {
945 MythDB::DBError("Error in JobQueue::ChangeJobCmds()", query);
946 return false;
947 }
948
949 return true;
950}
951
952bool JobQueue::ChangeJobCmds(int jobType, uint chanid,
953 const QDateTime &recstartts, int newCmds)
954{
956
957 query.prepare("UPDATE jobqueue SET cmds = :CMDS WHERE type = :TYPE "
958 "AND chanid = :CHANID AND starttime = :STARTTIME;");
959
960 query.bindValue(":CMDS", newCmds);
961 query.bindValue(":TYPE", jobType);
962 query.bindValue(":CHANID", chanid);
963 query.bindValue(":STARTTIME", recstartts);
964
965 if (!query.exec())
966 {
967 MythDB::DBError("Error in JobQueue::ChangeJobCmds()", query);
968 return false;
969 }
970
971 return true;
972}
973
974bool JobQueue::ChangeJobFlags(int jobID, int newFlags)
975{
976 if (jobID < 0)
977 return false;
978
980
981 query.prepare("UPDATE jobqueue SET flags = :FLAGS WHERE id = :ID;");
982
983 query.bindValue(":FLAGS", newFlags);
984 query.bindValue(":ID", jobID);
985
986 if (!query.exec())
987 {
988 MythDB::DBError("Error in JobQueue::ChangeJobFlags()", query);
989 return false;
990 }
991
992 return true;
993}
994
995bool JobQueue::ChangeJobStatus(int jobID, int newStatus, const QString& comment)
996{
997 if (jobID < 0)
998 return false;
999
1000 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("ChangeJobStatus(%1, %2, '%3')")
1001 .arg(jobID).arg(StatusText(newStatus), comment));
1002
1004
1005 query.prepare("UPDATE jobqueue SET status = :STATUS, comment = :COMMENT "
1006 "WHERE id = :ID AND status <> :NEWSTATUS;");
1007
1008 query.bindValue(":STATUS", newStatus);
1009 query.bindValue(":COMMENT", comment);
1010 query.bindValue(":ID", jobID);
1011 query.bindValue(":NEWSTATUS", newStatus);
1012
1013 if (!query.exec())
1014 {
1015 MythDB::DBError("Error in JobQueue::ChangeJobStatus()", query);
1016 return false;
1017 }
1018
1019 return true;
1020}
1021
1022bool JobQueue::ChangeJobComment(int jobID, const QString& comment)
1023{
1024 if (jobID < 0)
1025 return false;
1026
1027 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("ChangeJobComment(%1, '%2')")
1028 .arg(jobID).arg(comment));
1029
1031
1032 query.prepare("UPDATE jobqueue SET comment = :COMMENT "
1033 "WHERE id = :ID;");
1034
1035 query.bindValue(":COMMENT", comment);
1036 query.bindValue(":ID", jobID);
1037
1038 if (!query.exec())
1039 {
1040 MythDB::DBError("Error in JobQueue::ChangeJobComment()", query);
1041 return false;
1042 }
1043
1044 return true;
1045}
1046
1047bool JobQueue::ChangeJobArgs(int jobID, const QString& args)
1048{
1049 if (jobID < 0)
1050 return false;
1051
1053
1054 query.prepare("UPDATE jobqueue SET args = :ARGS "
1055 "WHERE id = :ID;");
1056
1057 query.bindValue(":ARGS", args);
1058 query.bindValue(":ID", jobID);
1059
1060 if (!query.exec())
1061 {
1062 MythDB::DBError("Error in JobQueue::ChangeJobArgs()", query);
1063 return false;
1064 }
1065
1066 return true;
1067}
1068
1069int JobQueue::GetRunningJobID(uint chanid, const QDateTime &recstartts)
1070{
1071 m_runningJobsLock->lock();
1072 for (const auto& jInfo : std::as_const(m_runningJobs))
1073 {
1074 if ((jInfo.pginfo->GetChanID() == chanid) &&
1075 (jInfo.pginfo->GetRecordingStartTime() == recstartts))
1076 {
1077 m_runningJobsLock->unlock();
1078
1079 return jInfo.id;
1080 }
1081 }
1082 m_runningJobsLock->unlock();
1083
1084 return 0;
1085}
1086
1088{
1089 return (status == JOB_QUEUED);
1090}
1091
1093{
1094 return ((status != JOB_UNKNOWN) && (status != JOB_QUEUED) &&
1095 ((status & JOB_DONE) == 0));
1096}
1097
1098bool JobQueue::IsJobRunning(int jobType,
1099 uint chanid, const QDateTime &recstartts)
1100{
1101 return IsJobStatusRunning(GetJobStatus(jobType, chanid, recstartts));
1102}
1103
1104bool JobQueue::IsJobRunning(int jobType, const ProgramInfo &pginfo)
1105{
1107 jobType, pginfo.GetChanID(), pginfo.GetRecordingStartTime());
1108}
1109
1111 int jobType, uint chanid, const QDateTime &recstartts)
1112{
1113 int tmpStatus = GetJobStatus(jobType, chanid, recstartts);
1114
1115 return (tmpStatus != JOB_UNKNOWN) && ((tmpStatus & JOB_DONE) == 0);
1116}
1117
1119 int jobType, uint chanid, const QDateTime &recstartts)
1120{
1121 return IsJobStatusQueued(GetJobStatus(jobType, chanid, recstartts));
1122}
1123
1124QString JobQueue::JobText(int jobType)
1125{
1126 switch (jobType)
1127 {
1128 case JOB_TRANSCODE: return tr("Transcode");
1129 case JOB_COMMFLAG: return tr("Flag Commercials");
1130 case JOB_METADATA: return tr("Look up Metadata");
1131 case JOB_PREVIEW: return tr("Preview Generation");
1132 }
1133
1134 if (jobType & JOB_USERJOB)
1135 {
1136 QString settingName =
1137 QString("UserJobDesc%1").arg(UserJobTypeToIndex(jobType));
1138 return gCoreContext->GetSetting(settingName, settingName);
1139 }
1140
1141 return tr("Unknown Job");
1142}
1143
1144// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
1145#define JOBSTATUS_STATUSTEXT(A,B,C) case A: return C;
1146
1147QString JobQueue::StatusText(int status)
1148{
1149 switch (status)
1150 {
1152 default: break;
1153 }
1154 return tr("Undefined");
1155}
1156
1157bool JobQueue::InJobRunWindow(std::chrono::minutes orStartsWithinMins)
1158{
1159 QString queueStartTimeStr;
1160 QString queueEndTimeStr;
1161 QTime queueStartTime;
1162 QTime queueEndTime;
1163 QTime curTime = QTime::currentTime();
1164 bool inTimeWindow = false;
1165 orStartsWithinMins = orStartsWithinMins < 0min ? 0min : orStartsWithinMins;
1166
1167 queueStartTimeStr = gCoreContext->GetSetting("JobQueueWindowStart", "00:00");
1168 queueEndTimeStr = gCoreContext->GetSetting("JobQueueWindowEnd", "23:59");
1169
1170 queueStartTime = QTime::fromString(queueStartTimeStr, "hh:mm");
1171 if (!queueStartTime.isValid())
1172 {
1173 LOG(VB_GENERAL, LOG_ERR,
1174 QString("Invalid JobQueueWindowStart time '%1', using 00:00")
1175 .arg(queueStartTimeStr));
1176 queueStartTime = QTime(0, 0);
1177 }
1178
1179 queueEndTime = QTime::fromString(queueEndTimeStr, "hh:mm");
1180 if (!queueEndTime.isValid())
1181 {
1182 LOG(VB_GENERAL, LOG_ERR,
1183 QString("Invalid JobQueueWindowEnd time '%1', using 23:59")
1184 .arg(queueEndTimeStr));
1185 queueEndTime = QTime(23, 59);
1186 }
1187
1188 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1189 QString("Currently set to run new jobs from %1 to %2")
1190 .arg(queueStartTimeStr, queueEndTimeStr));
1191
1192 if ((queueStartTime <= curTime) && (curTime < queueEndTime))
1193 { // NOLINT(bugprone-branch-clone)
1194 inTimeWindow = true;
1195 }
1196 else if ((queueStartTime > queueEndTime) &&
1197 ((curTime < queueEndTime) || (queueStartTime <= curTime)))
1198 {
1199 inTimeWindow = true;
1200 }
1201 else if (orStartsWithinMins > 0min)
1202 {
1203 // Check if the window starts soon
1204 if (curTime <= queueStartTime)
1205 {
1206 // Start time hasn't passed yet today
1207 if (queueStartTime.secsTo(curTime) <= duration_cast<std::chrono::seconds>(orStartsWithinMins).count())
1208 {
1209 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1210 QString("Job run window will start within %1 minutes")
1211 .arg(orStartsWithinMins.count()));
1212 inTimeWindow = true;
1213 }
1214 }
1215 else
1216 {
1217 // We passed the start time for today, try tomorrow
1218 QDateTime curDateTime = MythDate::current();
1219#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1220 QDateTime startDateTime = QDateTime(
1221 curDateTime.date(), queueStartTime, Qt::UTC).addDays(1);
1222#else
1223 QDateTime startDateTime =
1224 QDateTime(curDateTime.date(), queueStartTime,
1225 QTimeZone(QTimeZone::UTC)).addDays(1);
1226#endif
1227
1228 if (curDateTime.secsTo(startDateTime) <= duration_cast<std::chrono::seconds>(orStartsWithinMins).count())
1229 {
1230 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1231 QString("Job run window will start "
1232 "within %1 minutes (tomorrow)")
1233 .arg(orStartsWithinMins.count()));
1234 inTimeWindow = true;
1235 }
1236 }
1237 }
1238
1239 return inTimeWindow;
1240}
1241
1242bool JobQueue::HasRunningOrPendingJobs(std::chrono::minutes startingWithinMins)
1243{
1244 /* startingWithinMins <= 0 - look for any pending jobs
1245 > 0 - only consider pending starting within this time */
1246 QMap<int, JobQueueEntry> jobs;
1247 QMap<int, JobQueueEntry>::Iterator it;
1248 QDateTime maxSchedRunTime = MythDate::current();
1249 bool checkForQueuedJobs = (startingWithinMins <= 0min
1250 || InJobRunWindow(startingWithinMins));
1251
1252 if (checkForQueuedJobs && startingWithinMins > 0min) {
1253 maxSchedRunTime = maxSchedRunTime.addSecs(duration_cast<std::chrono::seconds>(startingWithinMins).count());
1254 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1255 QString("HasRunningOrPendingJobs: checking for jobs "
1256 "starting before: %1")
1257 .arg(maxSchedRunTime.toString(Qt::ISODate)));
1258 }
1259
1261
1262 if (!jobs.empty()) {
1263 for (it = jobs.begin(); it != jobs.end(); ++it)
1264 {
1265 int tmpStatus = (*it).status;
1266 if (tmpStatus == JOB_RUNNING) {
1267 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1268 QString("HasRunningOrPendingJobs: found running job"));
1269 return true;
1270 }
1271
1272 if (checkForQueuedJobs) {
1273 if ((tmpStatus != JOB_UNKNOWN) && (!(tmpStatus & JOB_DONE))) {
1274 if (startingWithinMins <= 0min) {
1275 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1276 "HasRunningOrPendingJobs: found pending job");
1277 return true;
1278 }
1279 if ((*it).schedruntime <= maxSchedRunTime) {
1280 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1281 QString("HasRunningOrPendingJobs: found pending "
1282 "job scheduled to start at: %1")
1283 .arg((*it).schedruntime.toString(Qt::ISODate)));
1284 return true;
1285 }
1286 }
1287 }
1288 }
1289 }
1290 return false;
1291}
1292
1293
1294int JobQueue::GetJobsInQueue(QMap<int, JobQueueEntry> &jobs, int findJobs)
1295{
1296 JobQueueEntry thisJob;
1298 QDateTime recentDate = MythDate::current().addSecs(-kRecentInterval);
1299 QString logInfo;
1300 int jobCount = 0;
1301 bool commflagWhileRecording =
1302 gCoreContext->GetBoolSetting("AutoCommflagWhileRecording", false);
1303
1304 jobs.clear();
1305
1306 query.prepare("SELECT j.id, j.chanid, j.starttime, j.inserttime, j.type, "
1307 "j.cmds, j.flags, j.status, j.statustime, j.hostname, "
1308 "j.args, j.comment, r.endtime, j.schedruntime "
1309 "FROM jobqueue j "
1310 "LEFT JOIN recorded r "
1311 " ON j.chanid = r.chanid AND j.starttime = r.starttime "
1312 "ORDER BY j.schedruntime, j.id;");
1313
1314 if (!query.exec())
1315 {
1316 MythDB::DBError("Error in JobQueue::GetJobs(), Unable to "
1317 "query list of Jobs in Queue.", query);
1318 return 0;
1319 }
1320
1321 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1322 QString("GetJobsInQueue: findJobs search bitmask %1, "
1323 "found %2 total jobs")
1324 .arg(findJobs).arg(query.size()));
1325
1326 while (query.next())
1327 {
1328 bool wantThisJob = false;
1329
1330 thisJob.id = query.value(0).toInt();
1331 thisJob.recstartts = MythDate::as_utc(query.value(2).toDateTime());
1332 thisJob.schedruntime = MythDate::as_utc(query.value(13).toDateTime());
1333 thisJob.type = query.value(4).toInt();
1334 thisJob.status = query.value(7).toInt();
1335 thisJob.statustime = MythDate::as_utc(query.value(8).toDateTime());
1336 thisJob.startts = MythDate::toString(
1338
1339 // -1 indicates the chanid is empty
1340 if (query.value(1).toInt() == -1)
1341 {
1342 thisJob.chanid = 0;
1343 logInfo = QString("jobID #%1").arg(thisJob.id);
1344 }
1345 else
1346 {
1347 thisJob.chanid = query.value(1).toUInt();
1348 logInfo = QString("chanid %1 @ %2").arg(thisJob.chanid)
1349 .arg(thisJob.startts);
1350 }
1351
1352 if ((MythDate::as_utc(query.value(12).toDateTime()) > MythDate::current()) &&
1353 ((!commflagWhileRecording) ||
1354 ((thisJob.type != JOB_COMMFLAG) &&
1355 (thisJob.type != JOB_METADATA))))
1356 {
1357 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1358 QString("GetJobsInQueue: Ignoring '%1' Job "
1359 "for %2 in %3 state. Endtime in future.")
1360 .arg(JobText(thisJob.type),
1361 logInfo, StatusText(thisJob.status)));
1362 continue;
1363 }
1364
1365 if ((findJobs & JOB_LIST_ALL) ||
1366 ((findJobs & JOB_LIST_DONE) &&
1367 (thisJob.status & JOB_DONE)) ||
1368 ((findJobs & JOB_LIST_NOT_DONE) &&
1369 (!(thisJob.status & JOB_DONE))) ||
1370 ((findJobs & JOB_LIST_ERROR) &&
1371 (thisJob.status == JOB_ERRORED)) ||
1372 ((findJobs & JOB_LIST_RECENT) &&
1373 (thisJob.statustime > recentDate)))
1374 wantThisJob = true;
1375
1376 if (!wantThisJob)
1377 {
1378 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1379 QString("GetJobsInQueue: Ignore '%1' Job for %2 in %3 state.")
1380 .arg(JobText(thisJob.type),
1381 logInfo, StatusText(thisJob.status)));
1382 continue;
1383 }
1384
1385 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1386 QString("GetJobsInQueue: Found '%1' Job for %2 in %3 state.")
1387 .arg(JobText(thisJob.type),
1388 logInfo, StatusText(thisJob.status)));
1389
1390 thisJob.inserttime = MythDate::as_utc(query.value(3).toDateTime());
1391 thisJob.cmds = query.value(5).toInt();
1392 thisJob.flags = query.value(6).toInt();
1393 thisJob.hostname = query.value(9).toString();
1394 thisJob.args = query.value(10).toString();
1395 thisJob.comment = query.value(11).toString();
1396
1397 if ((thisJob.type & JOB_USERJOB) &&
1398 (UserJobTypeToIndex(thisJob.type) == 0))
1399 {
1400 thisJob.type = JOB_NONE;
1401 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1402 QString("GetJobsInQueue: Unknown Job Type: %1")
1403 .arg(thisJob.type));
1404 }
1405
1406 if (thisJob.type != JOB_NONE)
1407 jobs[jobCount++] = thisJob;
1408 }
1409
1410 return jobCount;
1411}
1412
1413bool JobQueue::ChangeJobHost(int jobID, const QString& newHostname)
1414{
1416
1417 if (!newHostname.isEmpty())
1418 {
1419 query.prepare("UPDATE jobqueue SET hostname = :NEWHOSTNAME "
1420 "WHERE hostname = :EMPTY AND id = :ID;");
1421 query.bindValue(":NEWHOSTNAME", newHostname);
1422 query.bindValue(":EMPTY", "");
1423 query.bindValue(":ID", jobID);
1424 }
1425 else
1426 {
1427 query.prepare("UPDATE jobqueue SET hostname = :EMPTY "
1428 "WHERE id = :ID;");
1429 query.bindValue(":EMPTY", "");
1430 query.bindValue(":ID", jobID);
1431 }
1432
1433 if (!query.exec())
1434 {
1435 MythDB::DBError(QString("Error in JobQueue::ChangeJobHost(), "
1436 "Unable to set hostname to '%1' for "
1437 "job %2.").arg(newHostname).arg(jobID),
1438 query);
1439 return false;
1440 }
1441
1442 return query.numRowsAffected() > 0;
1443}
1444
1446{
1447 QString allowSetting;
1448
1449 if ((!job.hostname.isEmpty()) &&
1450 (job.hostname != m_hostname))
1451 return false;
1452
1453 if (job.type & JOB_USERJOB)
1454 {
1455 allowSetting =
1456 QString("JobAllowUserJob%1").arg(UserJobTypeToIndex(job.type));
1457 }
1458 else
1459 {
1460 switch (job.type)
1461 {
1462 case JOB_TRANSCODE: allowSetting = "JobAllowTranscode";
1463 break;
1464 case JOB_COMMFLAG: allowSetting = "JobAllowCommFlag";
1465 break;
1466 case JOB_METADATA: allowSetting = "JobAllowMetadata";
1467 break;
1468 case JOB_PREVIEW: allowSetting = "JobAllowPreview";
1469 break;
1470 default: return false;
1471 }
1472 }
1473
1474 return gCoreContext->GetBoolSetting(allowSetting, true);
1475}
1476
1478{
1480
1481 query.prepare("SELECT cmds FROM jobqueue WHERE id = :ID;");
1482
1483 query.bindValue(":ID", jobID);
1484
1485 if (query.exec())
1486 {
1487 if (query.next())
1488 return (enum JobCmds)query.value(0).toInt();
1489 }
1490 else
1491 {
1492 MythDB::DBError("Error in JobQueue::GetJobCmd()", query);
1493 }
1494
1495 return JOB_RUN;
1496}
1497
1499{
1501
1502 query.prepare("SELECT args FROM jobqueue WHERE id = :ID;");
1503
1504 query.bindValue(":ID", jobID);
1505
1506 if (query.exec())
1507 {
1508 if (query.next())
1509 return query.value(0).toString();
1510 }
1511 else
1512 {
1513 MythDB::DBError("Error in JobQueue::GetJobArgs()", query);
1514 }
1515
1516 return {""};
1517}
1518
1520{
1522
1523 query.prepare("SELECT flags FROM jobqueue WHERE id = :ID;");
1524
1525 query.bindValue(":ID", jobID);
1526
1527 if (query.exec())
1528 {
1529 if (query.next())
1530 return (enum JobFlags)query.value(0).toInt();
1531 }
1532 else
1533 {
1534 MythDB::DBError("Error in JobQueue::GetJobFlags()", query);
1535 }
1536
1537 return JOB_NO_FLAGS;
1538}
1539
1541{
1543
1544 query.prepare("SELECT status FROM jobqueue WHERE id = :ID;");
1545
1546 query.bindValue(":ID", jobID);
1547
1548 if (query.exec())
1549 {
1550 if (query.next())
1551 return (enum JobStatus)query.value(0).toInt();
1552 }
1553 else
1554 {
1555 MythDB::DBError("Error in JobQueue::GetJobStatus()", query);
1556 }
1557 return JOB_UNKNOWN;
1558}
1559
1561 int jobType, uint chanid, const QDateTime &recstartts)
1562{
1564
1565 query.prepare("SELECT status FROM jobqueue WHERE type = :TYPE "
1566 "AND chanid = :CHANID AND starttime = :STARTTIME;");
1567
1568 query.bindValue(":TYPE", jobType);
1569 query.bindValue(":CHANID", chanid);
1570 query.bindValue(":STARTTIME", recstartts);
1571
1572 if (query.exec())
1573 {
1574 if (query.next())
1575 return (enum JobStatus)query.value(0).toInt();
1576 }
1577 else
1578 {
1579 MythDB::DBError("Error in JobQueue::GetJobStatus()", query);
1580 }
1581 return JOB_UNKNOWN;
1582}
1583
1584void JobQueue::RecoverQueue(bool justOld)
1585{
1586 QMap<int, JobQueueEntry> jobs;
1587 QString msg;
1588 QString logInfo;
1589
1590 msg = QString("RecoverQueue: Checking for unfinished jobs to "
1591 "recover.");
1592 LOG(VB_JOBQUEUE, LOG_INFO, LOC + msg);
1593
1594 GetJobsInQueue(jobs);
1595
1596 if (!jobs.empty())
1597 {
1598 QMap<int, JobQueueEntry>::Iterator it;
1599 QDateTime oldDate = MythDate::current().addDays(-1);
1600 QString hostname = gCoreContext->GetHostName();
1601
1602 for (it = jobs.begin(); it != jobs.end(); ++it)
1603 {
1604 int tmpCmds = (*it).cmds;
1605 int tmpStatus = (*it).status;
1606
1607 if (!(*it).chanid)
1608 logInfo = QString("jobID #%1").arg((*it).id);
1609 else
1610 logInfo = QString("chanid %1 @ %2").arg((*it).chanid)
1611 .arg((*it).startts);
1612
1613 if (((tmpStatus == JOB_STARTING) ||
1614 (tmpStatus == JOB_RUNNING) ||
1615 (tmpStatus == JOB_PAUSED) ||
1616 (tmpCmds & JOB_STOP) ||
1617 (tmpStatus == JOB_STOPPING)) &&
1618 (((!justOld) &&
1619 ((*it).hostname == hostname)) ||
1620 ((*it).statustime < oldDate)))
1621 {
1622 msg = QString("RecoverQueue: Recovering '%1' for %2 "
1623 "from '%3' state.")
1624 .arg(JobText((*it).type),
1625 logInfo, StatusText((*it).status));
1626 LOG(VB_JOBQUEUE, LOG_INFO, LOC + msg);
1627
1628 ChangeJobStatus((*it).id, JOB_QUEUED, "");
1629 ChangeJobCmds((*it).id, JOB_RUN);
1630 if (!gCoreContext->GetBoolSetting("JobsRunOnRecordHost", false))
1631 ChangeJobHost((*it).id, "");
1632 }
1633 else
1634 {
1635#if 0
1636 msg = QString("RecoverQueue: Ignoring '%1' for %2 "
1637 "in '%3' state.")
1638 .arg(JobText((*it).type))
1639 .arg(logInfo).arg(StatusText((*it).status));
1640 LOG(VB_JOBQUEUE, LOG_INFO, LOC + msg);
1641#endif
1642 }
1643 }
1644 }
1645}
1646
1648{
1649 MSqlQuery delquery(MSqlQuery::InitCon());
1650 QDateTime donePurgeDate = MythDate::current().addDays(-2);
1651 QDateTime errorsPurgeDate = MythDate::current().addDays(-4);
1652
1653 delquery.prepare("DELETE FROM jobqueue "
1654 "WHERE (status in (:FINISHED, :ABORTED, :CANCELLED) "
1655 "AND statustime < :DONEPURGEDATE) "
1656 "OR (status in (:ERRORED) "
1657 "AND statustime < :ERRORSPURGEDATE) ");
1658 delquery.bindValue(":FINISHED", JOB_FINISHED);
1659 delquery.bindValue(":ABORTED", JOB_ABORTED);
1660 delquery.bindValue(":CANCELLED", JOB_CANCELLED);
1661 delquery.bindValue(":ERRORED", JOB_ERRORED);
1662 delquery.bindValue(":DONEPURGEDATE", donePurgeDate);
1663 delquery.bindValue(":ERRORSPURGEDATE", errorsPurgeDate);
1664
1665 if (!delquery.exec())
1666 {
1667 MythDB::DBError("JobQueue::CleanupOldJobsInQueue: Error deleting "
1668 "old finished jobs.", delquery);
1669 }
1670}
1671
1672bool JobQueue::InJobRunWindow(QDateTime jobstarttsRaw)
1673{
1674 if (!jobstarttsRaw.isValid())
1675 {
1676 jobstarttsRaw = QDateTime::currentDateTime();
1677 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("Invalid date/time passed, "
1678 "using %1").arg(
1679 jobstarttsRaw.toString()));
1680 }
1681
1682 QString hostname(gCoreContext->GetHostName());
1683
1685 "JobQueueWindowStart", hostname, "00:00")));
1686
1688 "JobQueueWindowEnd", hostname, "23:59")));
1689
1690 QTime scheduleTime(QTime::fromString(jobstarttsRaw.toString("hh:mm")));
1691
1692 if (scheduleTime < windowStart || scheduleTime > windowEnd)
1693 {
1694 LOG(VB_JOBQUEUE, LOG_ERR, LOC + "Time not within job queue window, " +
1695 "job not queued");
1696 return false;
1697 }
1698
1699 return true;
1700}
1701
1703{
1704 int jobID = job.id;
1705
1707 {
1708 LOG(VB_JOBQUEUE, LOG_ERR, LOC +
1709 "ProcessJob(): Unable to open database connection");
1710 return;
1711 }
1712
1713 ChangeJobStatus(jobID, JOB_PENDING);
1714 ProgramInfo *pginfo = nullptr;
1715
1716 if (job.chanid)
1717 {
1718 pginfo = new ProgramInfo(job.chanid, job.recstartts);
1719
1720 if (!pginfo->GetChanID())
1721 {
1722 LOG(VB_JOBQUEUE, LOG_ERR, LOC +
1723 QString("Unable to retrieve program info for chanid %1 @ %2")
1724 .arg(job.chanid)
1725 .arg(job.recstartts.toString(Qt::ISODate)));
1726
1727 ChangeJobStatus(jobID, JOB_ERRORED,
1728 tr("Unable to retrieve program info from database"));
1729
1730 delete pginfo;
1731
1732 return;
1733 }
1734
1735 pginfo->SetPathname(pginfo->GetPlaybackURL());
1736 }
1737
1738
1739 m_runningJobsLock->lock();
1740
1741 ChangeJobStatus(jobID, JOB_STARTING);
1742 RunningJobInfo jInfo;
1743 jInfo.type = job.type;
1744 jInfo.id = jobID;
1745 jInfo.flag = JOB_RUN;
1746 jInfo.desc = GetJobDescription(job.type);
1747 jInfo.command = GetJobCommand(jobID, job.type, pginfo);
1748 jInfo.pginfo = pginfo;
1749
1750 m_runningJobs[jobID] = jInfo;
1751
1752 if (pginfo)
1753 pginfo->MarkAsInUse(true, kJobQueueInUseID);
1754
1755 if (pginfo && pginfo->GetRecordingGroup() == "Deleted")
1756 {
1757 ChangeJobStatus(jobID, JOB_CANCELLED,
1758 tr("Program has been deleted"));
1760 }
1761 else if ((job.type == JOB_TRANSCODE) ||
1762 (m_runningJobs[jobID].command == "mythtranscode"))
1763 {
1765 }
1766 else if ((job.type == JOB_COMMFLAG) ||
1767 (m_runningJobs[jobID].command == "mythcommflag"))
1768 {
1770 }
1771 else if ((job.type == JOB_METADATA) ||
1772 (m_runningJobs[jobID].command == "mythmetadatalookup"))
1773 {
1775 }
1776 else if (job.type & JOB_USERJOB)
1777 {
1779 }
1780 else
1781 {
1782 ChangeJobStatus(jobID, JOB_ERRORED,
1783 tr("UNKNOWN JobType, unable to process!"));
1785 }
1786
1787 m_runningJobsLock->unlock();
1788}
1789
1790void JobQueue::StartChildJob(void *(*ChildThreadRoutine)(void *), int jobID)
1791{
1792 auto *jts = new JobThreadStruct;
1793 jts->jq = this;
1794 jts->jobID = jobID;
1795
1796 pthread_t childThread = PTHREAD_NULL;
1797 pthread_attr_t attr;
1798 pthread_attr_init(&attr);
1799 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
1800 pthread_create(&childThread, &attr, ChildThreadRoutine, jts);
1801 pthread_attr_destroy(&attr);
1802}
1803
1805{
1806 if (jobType == JOB_TRANSCODE)
1807 return "Transcode";
1808 if (jobType == JOB_COMMFLAG)
1809 return "Commercial Detection";
1810 if (!(jobType & JOB_USERJOB))
1811 return "Unknown Job";
1812
1813 QString descSetting =
1814 QString("UserJobDesc%1").arg(UserJobTypeToIndex(jobType));
1815
1816 return gCoreContext->GetSetting(descSetting, "Unknown Job");
1817}
1818
1819QString JobQueue::GetJobCommand(int id, int jobType, ProgramInfo *tmpInfo)
1820{
1821 QString command;
1823
1824 if (jobType == JOB_TRANSCODE)
1825 {
1826 command = gCoreContext->GetSetting("JobQueueTranscodeCommand");
1827 if (command.trimmed().isEmpty())
1828 command = "mythtranscode";
1829
1830 if (command == "mythtranscode")
1831 return command;
1832 }
1833 else if (jobType == JOB_COMMFLAG)
1834 {
1835 command = gCoreContext->GetSetting("JobQueueCommFlagCommand");
1836 if (command.trimmed().isEmpty())
1837 command = "mythcommflag";
1838
1839 if (command == "mythcommflag")
1840 return command;
1841 }
1842 else if (jobType & JOB_USERJOB)
1843 {
1844 command = gCoreContext->GetSetting(
1845 QString("UserJob%1").arg(UserJobTypeToIndex(jobType)), "");
1846 }
1847
1848 if (!command.isEmpty())
1849 {
1850 command.replace("%JOBID%", QString("%1").arg(id));
1851 }
1852
1853 if (!command.isEmpty() && tmpInfo)
1854 {
1855 tmpInfo->SubstituteMatches(command);
1856
1857 command.replace("%VERBOSELEVEL%", QString("%1").arg(verboseMask));
1858 command.replace("%VERBOSEMODE%", QString("%1").arg(logPropagateArgs));
1859
1860 uint transcoder = tmpInfo->QueryTranscoderID();
1861 command.replace("%TRANSPROFILE%",
1863 "autodetect" : QString::number(transcoder));
1864 }
1865
1866 return command;
1867}
1868
1870{
1871 m_runningJobsLock->lock();
1872
1873 if (m_runningJobs.contains(id))
1874 {
1875 ProgramInfo *pginfo = m_runningJobs[id].pginfo;
1876 if (pginfo)
1877 {
1878 pginfo->MarkAsInUse(false, kJobQueueInUseID);
1879 delete pginfo;
1880 }
1881
1882 m_runningJobs.remove(id);
1883 }
1884
1885 m_runningJobsLock->unlock();
1886}
1887
1889{
1890 // Pretty print "bytes" as KB, MB, GB, TB, etc., subject to the desired
1891 // number of units
1892 struct PpTab_t {
1893 const char *m_suffix;
1894 unsigned int m_max;
1895 int m_precision;
1896 };
1897 static constexpr std::array<const PpTab_t,9> kPpTab {{
1898 { .m_suffix="bytes", .m_max=9999, .m_precision=0 },
1899 { .m_suffix="kB", .m_max=999, .m_precision=0 },
1900 { .m_suffix="MB", .m_max=999, .m_precision=1 },
1901 { .m_suffix="GB", .m_max=999, .m_precision=1 },
1902 { .m_suffix="TB", .m_max=999, .m_precision=1 },
1903 { .m_suffix="PB", .m_max=999, .m_precision=1 },
1904 { .m_suffix="EB", .m_max=999, .m_precision=1 },
1905 { .m_suffix="ZB", .m_max=999, .m_precision=1 },
1906 { .m_suffix="YB", .m_max=0, .m_precision=0 },
1907 }};
1908 float fbytes = bytes;
1909
1910 unsigned int ii = 0;
1911 while (kPpTab[ii].m_max && fbytes > kPpTab[ii].m_max) {
1912 fbytes /= 1024;
1913 ii++;
1914 }
1915
1916 return QString("%1 %2")
1917 .arg(fbytes, 0, 'f', kPpTab[ii].m_precision)
1918 .arg(kPpTab[ii].m_suffix);
1919}
1920
1922{
1923 auto *jts = (JobThreadStruct *)param;
1924 JobQueue *jq = jts->jq;
1925
1926 MThread::ThreadSetup(QString("Transcode_%1").arg(jts->jobID));
1927 jq->DoTranscodeThread(jts->jobID);
1929
1930 delete jts;
1931
1932 return nullptr;
1933}
1934
1936{
1937 // We can't currently transcode non-recording files w/o a ProgramInfo
1938 m_runningJobsLock->lock();
1939 if (!m_runningJobs[jobID].pginfo)
1940 {
1941 LOG(VB_JOBQUEUE, LOG_ERR, LOC +
1942 "The JobQueue cannot currently transcode files that do not "
1943 "have a chanid/starttime in the recorded table.");
1944 ChangeJobStatus(jobID, JOB_ERRORED, "ProgramInfo data not found");
1946 m_runningJobsLock->unlock();
1947 return;
1948 }
1949
1950 ProgramInfo *program_info = m_runningJobs[jobID].pginfo;
1951 m_runningJobsLock->unlock();
1952
1953 ChangeJobStatus(jobID, JOB_RUNNING);
1954
1955 // make sure flags are up to date
1956 program_info->Reload();
1957
1958 bool useCutlist = program_info->HasCutlist() &&
1959 ((GetJobFlags(jobID) & JOB_USE_CUTLIST) != 0);
1960
1961 uint transcoder = program_info->QueryTranscoderID();
1962 QString profilearg =
1964 "autodetect" : QString::number(transcoder);
1965
1966 QString path;
1967 QString command;
1968
1969 m_runningJobsLock->lock();
1970 if (m_runningJobs[jobID].command == "mythtranscode")
1971 {
1972 path = GetAppBinDir() + "mythtranscode";
1973 command = QString("%1 -j %2 --profile %3")
1974 .arg(path).arg(jobID).arg(profilearg);
1975 if (useCutlist)
1976 command += " --honorcutlist";
1977 command += logPropagateArgs;
1978 }
1979 else
1980 {
1981 command = m_runningJobs[jobID].command;
1982
1983 QStringList tokens = command.split(" ", Qt::SkipEmptyParts);
1984 if (!tokens.empty())
1985 path = tokens[0];
1986 }
1987 m_runningJobsLock->unlock();
1988
1989 if (m_jobQueueCPU < 2)
1990 {
1991 myth_nice(17);
1992 myth_ioprio((0 == m_jobQueueCPU) ? 8 : 7);
1993 }
1994
1995 QString transcoderName;
1997 {
1998 transcoderName = "Autodetect";
1999 }
2000 else
2001 {
2003 query.prepare("SELECT name FROM recordingprofiles WHERE id = :ID;");
2004 query.bindValue(":ID", transcoder);
2005 if (query.exec() && query.next())
2006 {
2007 transcoderName = query.value(0).toString();
2008 }
2009 else
2010 {
2011 /* Unexpected value; log it. */
2012 transcoderName = QString("Autodetect(%1)").arg(transcoder);
2013 }
2014 }
2015
2016 bool retry = true;
2017 int retrylimit = 3;
2018 while (retry)
2019 {
2020 retry = false;
2021
2022 ChangeJobStatus(jobID, JOB_STARTING);
2024
2025 QString filename = program_info->GetPlaybackURL(false, true);
2026
2027 long long filesize = 0;
2028 long long origfilesize = QFileInfo(filename).size();
2029
2030 QString msg = QString("Transcode %1")
2032
2033 QString details = QString("%1: %2 (%3)")
2034 .arg(program_info->toString(ProgramInfo::kTitleSubtitle),
2035 transcoderName, PrettyPrint(origfilesize));
2036
2037 LOG(VB_GENERAL, LOG_INFO, LOC + QString("%1 for %2")
2038 .arg(msg, details));
2039
2040 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("Running command: '%1'")
2041 .arg(command));
2042
2043 GetMythDB()->GetDBManager()->CloseDatabases();
2044 uint result = myth_system(command);
2045 int status = GetJobStatus(jobID);
2046
2047 if ((result == GENERIC_EXIT_DAEMONIZING_ERROR) ||
2048 (result == GENERIC_EXIT_CMD_NOT_FOUND))
2049 {
2050 ChangeJobStatus(jobID, JOB_ERRORED,
2051 tr("ERROR: Unable to find mythtranscode, check backend logs."));
2053
2054 msg = QString("Transcode %1").arg(StatusText(GetJobStatus(jobID)));
2055 details = QString("%1: %2 does not exist or is not executable")
2056 .arg(program_info->toString(ProgramInfo::kTitleSubtitle),path);
2057
2058 LOG(VB_GENERAL, LOG_ERR, LOC +
2059 QString("%1 for %2").arg(msg, details));
2060 }
2061 else if (result == GENERIC_EXIT_RESTART && retrylimit > 0)
2062 {
2063 LOG(VB_JOBQUEUE, LOG_INFO, LOC + "Transcode command restarting");
2064 retry = true;
2065 retrylimit--;
2066
2068 }
2069 else
2070 {
2071 if (status == JOB_FINISHED)
2072 {
2073 ChangeJobStatus(jobID, JOB_FINISHED, tr("Finished."));
2074 retry = false;
2075
2076 program_info->Reload(); // Refresh, the basename may have changed
2077 filename = program_info->GetPlaybackURL(false, true);
2078 QFileInfo st(filename);
2079
2080 if (st.exists())
2081 {
2082 filesize = st.size();
2083 /*: %1 is transcoder name, %2 is the original file size
2084 and %3 is the current file size */
2085 QString comment = tr("%1: %2 => %3")
2086 .arg(transcoderName,
2087 PrettyPrint(origfilesize),
2088 PrettyPrint(filesize));
2089 ChangeJobComment(jobID, comment);
2090
2091 if (filesize > 0)
2092 program_info->SaveFilesize(filesize);
2093
2094 details = QString("%1: %2 (%3)")
2095 .arg(program_info->toString(
2097 transcoderName,
2098 PrettyPrint(filesize));
2099 }
2100 else
2101 {
2102 QString comment =
2103 QString("could not stat '%1'").arg(filename);
2104
2105 ChangeJobStatus(jobID, JOB_FINISHED, comment);
2106
2107 details = QString("%1: %2")
2108 .arg(program_info->toString(
2110 comment);
2111 }
2112
2114 }
2115 else
2116 {
2118
2119 QString comment = tr("exit status %1, job status was \"%2\"")
2120 .arg(result)
2121 .arg(StatusText(status));
2122
2123 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2124
2125 details = QString("%1: %2 (%3)")
2126 .arg(program_info->toString(
2128 transcoderName,
2129 comment);
2130 }
2131
2132 msg = QString("Transcode %1").arg(StatusText(GetJobStatus(jobID)));
2133 LOG(VB_GENERAL, LOG_INFO, LOC + msg + ": " + details);
2134 }
2135 }
2136
2137 if (retrylimit == 0)
2138 {
2139 LOG(VB_JOBQUEUE, LOG_ERR, LOC + "Retry limit exceeded for transcoder, "
2140 "setting job status to errored.");
2141 ChangeJobStatus(jobID, JOB_ERRORED, tr("Retry limit exceeded"));
2142 }
2143
2145}
2146
2148{
2149 auto *jts = (JobThreadStruct *)param;
2150 JobQueue *jq = jts->jq;
2151
2152 MThread::ThreadSetup(QString("Metadata_%1").arg(jts->jobID));
2153 jq->DoMetadataLookupThread(jts->jobID);
2155
2156 delete jts;
2157
2158 return nullptr;
2159}
2160
2162{
2163 // We can't currently lookup non-recording files w/o a ProgramInfo
2164 m_runningJobsLock->lock();
2165 if (!m_runningJobs[jobID].pginfo)
2166 {
2167 LOG(VB_JOBQUEUE, LOG_ERR, LOC +
2168 "The JobQueue cannot currently perform lookups for items which do "
2169 "not have a chanid/starttime in the recorded table.");
2170 ChangeJobStatus(jobID, JOB_ERRORED, "ProgramInfo data not found");
2172 m_runningJobsLock->unlock();
2173 return;
2174 }
2175
2176 ProgramInfo *program_info = m_runningJobs[jobID].pginfo;
2177 m_runningJobsLock->unlock();
2178
2179 QString details = QString("%1 recorded from channel %3")
2180 .arg(program_info->toString(ProgramInfo::kTitleSubtitle),
2181 program_info->toString(ProgramInfo::kRecordingKey));
2182
2184 {
2185 QString msg = QString("Metadata Lookup failed. Could not open "
2186 "new database connection for %1. "
2187 "Program cannot be looked up.")
2188 .arg(details);
2189 LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2190
2191 ChangeJobStatus(jobID, JOB_ERRORED,
2192 tr("Could not open new database connection for "
2193 "metadata lookup."));
2194
2195 delete program_info;
2196 return;
2197 }
2198
2199 LOG(VB_GENERAL, LOG_INFO,
2200 LOC + "Metadata Lookup Starting for " + details);
2201
2202 uint retVal = 0;
2203 QString path;
2204 QString command;
2205
2206 path = GetAppBinDir() + "mythmetadatalookup";
2207 command = QString("%1 -j %2")
2208 .arg(path).arg(jobID);
2209 command += logPropagateArgs;
2210
2211 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("Running command: '%1'")
2212 .arg(command));
2213
2214 GetMythDB()->GetDBManager()->CloseDatabases();
2215 retVal = myth_system(command);
2216 int priority = LOG_NOTICE;
2217 QString comment;
2218
2219 m_runningJobsLock->lock();
2220
2221 if ((retVal == GENERIC_EXIT_DAEMONIZING_ERROR) ||
2222 (retVal == GENERIC_EXIT_CMD_NOT_FOUND))
2223 {
2224 comment = tr("Unable to find mythmetadatalookup");
2225 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2226 priority = LOG_WARNING;
2227 }
2228 else if (m_runningJobs[jobID].flag == JOB_STOP)
2229 {
2230 comment = tr("Aborted by user");
2231 ChangeJobStatus(jobID, JOB_ABORTED, comment);
2232 priority = LOG_WARNING;
2233 }
2234 else if (retVal == GENERIC_EXIT_NO_RECORDING_DATA)
2235 {
2236 comment = tr("Unable to open file or init decoder");
2237 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2238 priority = LOG_WARNING;
2239 }
2240 else if (retVal >= GENERIC_EXIT_NOT_OK) // 256 or above - error
2241 {
2242 comment = tr("Failed with exit status %1").arg(retVal);
2243 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2244 priority = LOG_WARNING;
2245 }
2246 else
2247 {
2248 comment = tr("Metadata Lookup Complete.");
2249 ChangeJobStatus(jobID, JOB_FINISHED, comment);
2250
2251 program_info->SendUpdateEvent();
2252 }
2253
2254 QString msg = tr("Metadata Lookup %1", "Job ID")
2256
2257 if (!comment.isEmpty())
2258 details += QString(" (%1)").arg(comment);
2259
2260 if (priority <= LOG_WARNING)
2261 LOG(VB_GENERAL, LOG_ERR, LOC + msg + ": " + details);
2262
2264 m_runningJobsLock->unlock();
2265}
2266
2268{
2269 auto *jts = (JobThreadStruct *)param;
2270 JobQueue *jq = jts->jq;
2271
2272 MThread::ThreadSetup(QString("Commflag_%1").arg(jts->jobID));
2273 jq->DoFlagCommercialsThread(jts->jobID);
2275
2276 delete jts;
2277
2278 return nullptr;
2279}
2280
2282{
2283 // We can't currently commflag non-recording files w/o a ProgramInfo
2284 m_runningJobsLock->lock();
2285 if (!m_runningJobs[jobID].pginfo)
2286 {
2287 LOG(VB_JOBQUEUE, LOG_ERR, LOC +
2288 "The JobQueue cannot currently commflag files that do not "
2289 "have a chanid/starttime in the recorded table.");
2290 ChangeJobStatus(jobID, JOB_ERRORED, "ProgramInfo data not found");
2292 m_runningJobsLock->unlock();
2293 return;
2294 }
2295
2296 ProgramInfo *program_info = m_runningJobs[jobID].pginfo;
2297 m_runningJobsLock->unlock();
2298
2299 QString details = QString("%1 recorded from channel %3")
2300 .arg(program_info->toString(ProgramInfo::kTitleSubtitle),
2301 program_info->toString(ProgramInfo::kRecordingKey));
2302
2304 {
2305 QString msg = QString("Commercial Detection failed. Could not open "
2306 "new database connection for %1. "
2307 "Program cannot be flagged.")
2308 .arg(details);
2309 LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2310
2311 ChangeJobStatus(jobID, JOB_ERRORED,
2312 tr("Could not open new database connection for "
2313 "commercial detector."));
2314
2315 delete program_info;
2316 return;
2317 }
2318
2319 LOG(VB_GENERAL, LOG_INFO,
2320 LOC + "Commercial Detection Starting for " + details);
2321
2322 uint breaksFound = 0;
2323 QString path;
2324 QString command;
2325
2326 m_runningJobsLock->lock();
2327 if (m_runningJobs[jobID].command == "mythcommflag")
2328 {
2329 path = GetAppBinDir() + "mythcommflag";
2330 command = QString("%1 -j %2 --noprogress")
2331 .arg(path).arg(jobID);
2332 command += logPropagateArgs;
2333 }
2334 else
2335 {
2336 command = m_runningJobs[jobID].command;
2337 QStringList tokens = command.split(" ", Qt::SkipEmptyParts);
2338 if (!tokens.empty())
2339 path = tokens[0];
2340 }
2341 m_runningJobsLock->unlock();
2342
2343 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("Running command: '%1'")
2344 .arg(command));
2345
2346 GetMythDB()->GetDBManager()->CloseDatabases();
2347 breaksFound = myth_system(command, kMSLowExitVal);
2348 int priority = LOG_NOTICE;
2349 QString comment;
2350
2351 m_runningJobsLock->lock();
2352
2353 if ((breaksFound == GENERIC_EXIT_DAEMONIZING_ERROR) ||
2354 (breaksFound == GENERIC_EXIT_CMD_NOT_FOUND))
2355 {
2356 comment = tr("Unable to find mythcommflag");
2357 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2358 priority = LOG_WARNING;
2359 }
2360 else if (m_runningJobs[jobID].flag == JOB_STOP)
2361 {
2362 comment = tr("Aborted by user");
2363 ChangeJobStatus(jobID, JOB_ABORTED, comment);
2364 priority = LOG_WARNING;
2365 }
2366 else if (breaksFound == GENERIC_EXIT_NO_RECORDING_DATA)
2367 {
2368 comment = tr("Unable to open file or init decoder");
2369 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2370 priority = LOG_WARNING;
2371 }
2372 else if (breaksFound >= GENERIC_EXIT_NOT_OK) // 256 or above - error
2373 {
2374 comment = tr("Failed with exit status %1").arg(breaksFound);
2375 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2376 priority = LOG_WARNING;
2377 }
2378 else
2379 {
2380 comment = tr("%n commercial break(s)", "", breaksFound);
2381 ChangeJobStatus(jobID, JOB_FINISHED, comment);
2382
2383 program_info->SendUpdateEvent();
2384
2385 if (!program_info->IsLocal())
2386 program_info->SetPathname(program_info->GetPlaybackURL(false,true));
2387 if (program_info->IsLocal())
2388 {
2389 auto *pg = new PreviewGenerator(program_info, QString(),
2391 pg->Run();
2392 pg->deleteLater();
2393 }
2394 }
2395
2396 QString msg = tr("Commercial Detection %1", "Job ID")
2398
2399 if (!comment.isEmpty())
2400 details += QString(" (%1)").arg(comment);
2401
2402 if (priority <= LOG_WARNING)
2403 LOG(VB_GENERAL, LOG_ERR, LOC + msg + ": " + details);
2404
2406 m_runningJobsLock->unlock();
2407}
2408
2409void *JobQueue::UserJobThread(void *param)
2410{
2411 auto *jts = (JobThreadStruct *)param;
2412 JobQueue *jq = jts->jq;
2413
2414 MThread::ThreadSetup(QString("UserJob_%1").arg(jts->jobID));
2415 jq->DoUserJobThread(jts->jobID);
2417
2418 delete jts;
2419
2420 return nullptr;
2421}
2422
2424{
2425 m_runningJobsLock->lock();
2426 ProgramInfo *pginfo = m_runningJobs[jobID].pginfo;
2427 QString jobDesc = m_runningJobs[jobID].desc;
2428 QString command = m_runningJobs[jobID].command;
2429 m_runningJobsLock->unlock();
2430
2431 ChangeJobStatus(jobID, JOB_RUNNING);
2432
2433 QString msg;
2434
2435 if (pginfo)
2436 {
2437 msg = QString("Started %1 for %2 recorded from channel %3")
2438 .arg(jobDesc,
2441 }
2442 else
2443 {
2444 msg = QString("Started %1 for jobID %2").arg(jobDesc).arg(jobID);
2445 }
2446
2447 LOG(VB_GENERAL, LOG_INFO, LOC + QString(msg.toLocal8Bit().constData()));
2448
2449 switch (m_jobQueueCPU)
2450 {
2451 case 0: myth_nice(17);
2452 myth_ioprio(8);
2453 break;
2454 case 1: myth_nice(10);
2455 myth_ioprio(7);
2456 break;
2457 case 2:
2458 default: break;
2459 }
2460
2461 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("Running command: '%1'")
2462 .arg(command));
2463 GetMythDB()->GetDBManager()->CloseDatabases();
2464 uint result = myth_system(command);
2465
2466 if ((result == GENERIC_EXIT_DAEMONIZING_ERROR) ||
2467 (result == GENERIC_EXIT_CMD_NOT_FOUND))
2468 {
2469 msg = QString("User Job '%1' failed, unable to find "
2470 "executable, check your PATH and backend logs.")
2471 .arg(command);
2472 LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2473 LOG(VB_GENERAL, LOG_NOTICE, LOC + QString("Current PATH: '%1'")
2474 .arg(qEnvironmentVariable("PATH")));
2475
2476 ChangeJobStatus(jobID, JOB_ERRORED,
2477 tr("ERROR: Unable to find executable, check backend logs."));
2478 }
2479 else if (result != 0)
2480 {
2481 msg = QString("User Job '%1' failed.").arg(command);
2482 LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2483
2484 ChangeJobStatus(jobID, JOB_ERRORED,
2485 tr("ERROR: User Job returned non-zero, check logs."));
2486 }
2487 else
2488 {
2489 if (pginfo)
2490 {
2491 msg = QString("Finished %1 for %2 recorded from channel %3")
2492 .arg(jobDesc,
2495 }
2496 else
2497 {
2498 msg = QString("Finished %1 for jobID %2").arg(jobDesc).arg(jobID);
2499 }
2500
2501 LOG(VB_GENERAL, LOG_INFO, LOC + QString(msg.toLocal8Bit().constData()));
2502
2503 ChangeJobStatus(jobID, JOB_FINISHED, tr("Successfully Completed."));
2504
2505 if (pginfo)
2506 pginfo->SendUpdateEvent();
2507 }
2508
2510}
2511
2513{
2514 if (jobType & JOB_USERJOB)
2515 {
2516 int x = ((jobType & JOB_USERJOB)>> 8);
2517 int bits = 1;
2518 while ((x != 0) && ((x & 0x01) == 0))
2519 {
2520 bits++;
2521 x = x >> 1;
2522 }
2523 if ( bits > 4 )
2524 return JOB_NONE;
2525
2526 return bits;
2527 }
2528 return JOB_NONE;
2529}
2530
2531/* vim: set expandtab tabstop=4 shiftwidth=4: */
static bool QueueRecordingJobs(const RecordingInfo &recinfo, int jobTypes=JOB_NONE)
Definition: jobqueue.cpp:498
QMap< int, RunningJobInfo > m_runningJobs
Definition: jobqueue.h:268
static bool ChangeJobHost(int jobID, const QString &newHostname)
Definition: jobqueue.cpp:1413
static bool ChangeJobFlags(int jobID, int newFlags)
Definition: jobqueue.cpp:974
static void RecoverQueue(bool justOld=false)
Definition: jobqueue.cpp:1584
static QString GetJobCommand(int id, int jobType, ProgramInfo *tmpInfo)
Definition: jobqueue.cpp:1819
static bool RestartJob(int jobID)
Definition: jobqueue.cpp:745
static bool SafeDeleteJob(int jobID, int jobType, int chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:889
static bool ChangeJobCmds(int jobID, int newCmds)
Definition: jobqueue.cpp:931
static QString GetJobArgs(int jobID)
Definition: jobqueue.cpp:1498
QWaitCondition m_queueThreadCond
Definition: jobqueue.h:273
static void CleanupOldJobsInQueue()
Definition: jobqueue.cpp:1647
void DoUserJobThread(int jobID)
Definition: jobqueue.cpp:2423
bool AllowedToRun(const JobQueueEntry &job)
Definition: jobqueue.cpp:1445
static bool GetJobInfoFromID(int jobID, int &jobType, uint &chanid, QDateTime &recstartts)
Definition: jobqueue.cpp:677
static void * TranscodeThread(void *param)
Definition: jobqueue.cpp:1921
static void * UserJobThread(void *param)
Definition: jobqueue.cpp:2409
static bool InJobRunWindow(QDateTime jobstarttsRaw)
Definition: jobqueue.cpp:1672
static int GetJobsInQueue(QMap< int, JobQueueEntry > &jobs, int findJobs=JOB_LIST_NOT_DONE)
Definition: jobqueue.cpp:1294
void DoFlagCommercialsThread(int jobID)
Definition: jobqueue.cpp:2281
static enum JobFlags GetJobFlags(int jobID)
Definition: jobqueue.cpp:1519
static QString JobText(int jobType)
Definition: jobqueue.cpp:1124
QRecursiveMutex * m_runningJobsLock
Definition: jobqueue.h:267
static bool ChangeJobArgs(int jobID, const QString &args="")
Definition: jobqueue.cpp:1047
static enum JobCmds GetJobCmd(int jobID)
Definition: jobqueue.cpp:1477
int m_jobQueueCPU
Definition: jobqueue.h:260
MThread * m_queueThread
Definition: jobqueue.h:272
static bool DeleteAllJobs(uint chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:763
static bool DeleteJob(int jobID)
Definition: jobqueue.cpp:884
int m_jobsRunning
Definition: jobqueue.h:259
static bool QueueJob(int jobType, uint chanid, const QDateTime &recstartts, const QString &args="", const QString &comment="", QString host="", int flags=0, int status=JOB_QUEUED, QDateTime schedruntime=QDateTime())
Definition: jobqueue.cpp:520
static bool IsJobStatusQueued(int status)
Definition: jobqueue.cpp:1087
static int GetJobID(int jobType, uint chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:656
void StartChildJob(void *(*ChildThreadRoutine)(void *), int jobID)
Definition: jobqueue.cpp:1790
static bool StopJob(int jobID)
Definition: jobqueue.cpp:754
QMutex m_queueThreadCondLock
Definition: jobqueue.h:274
static bool ResumeJob(int jobID)
Definition: jobqueue.cpp:736
static bool ChangeJobComment(int jobID, const QString &comment="")
Definition: jobqueue.cpp:1022
void ProcessJob(const JobQueueEntry &job)
Definition: jobqueue.cpp:1702
static bool IsJobQueuedOrRunning(int jobType, uint chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:1110
static void * FlagCommercialsThread(void *param)
Definition: jobqueue.cpp:2267
void ProcessQueue(void)
Definition: jobqueue.cpp:171
static bool IsJobRunning(int jobType, uint chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:1098
static QString GetJobDescription(int jobType)
Definition: jobqueue.cpp:1804
static bool ChangeJobStatus(int jobID, int newStatus, const QString &comment="")
Definition: jobqueue.cpp:995
static void * MetadataLookupThread(void *param)
Definition: jobqueue.cpp:2147
static QString PrettyPrint(off_t bytes)
Definition: jobqueue.cpp:1888
static enum JobStatus GetJobStatus(int jobID)
Definition: jobqueue.cpp:1540
bool m_processQueue
Definition: jobqueue.h:275
void RemoveRunningJob(int id)
Definition: jobqueue.cpp:1869
void run(void) override
Definition: jobqueue.cpp:156
int GetRunningJobID(uint chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:1069
static bool PauseJob(int jobID)
Definition: jobqueue.cpp:727
static bool IsJobQueued(int jobType, uint chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:1118
void DoMetadataLookupThread(int jobID)
Definition: jobqueue.cpp:2161
void DoTranscodeThread(int jobID)
Definition: jobqueue.cpp:1935
static bool QueueJobs(int jobTypes, uint chanid, const QDateTime &recstartts, const QString &args="", const QString &comment="", const QString &host="")
Definition: jobqueue.cpp:604
static QString StatusText(int status)
Definition: jobqueue.cpp:1147
~JobQueue(void) override
Definition: jobqueue.cpp:72
static int GetJobTypeFromName(const QString &name)
Definition: jobqueue.cpp:716
QString m_hostname
Definition: jobqueue.h:257
JobQueue(bool master)
Definition: jobqueue.cpp:50
static bool HasRunningOrPendingJobs(std::chrono::minutes startingWithinMins=0min)
Definition: jobqueue.cpp:1242
static int UserJobTypeToIndex(int JobType)
Definition: jobqueue.cpp:2512
static bool IsJobStatusRunning(int status)
Definition: jobqueue.cpp:1092
void customEvent(QEvent *e) override
Definition: jobqueue.cpp:88
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:128
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:838
QVariant value(int i) const
Definition: mythdbcon.h:204
int size(void) const
Definition: mythdbcon.h:214
static bool testDBConnection()
Checks DB connection + login (login info via Mythcontext)
Definition: mythdbcon.cpp:877
int numRowsAffected() const
Definition: mythdbcon.h:217
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:619
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:889
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:813
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:551
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:49
static void ThreadCleanup(void)
This is to be called on exit in those few threads that haven't been ported to MThread.
Definition: mthread.cpp:210
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:267
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:284
static void ThreadSetup(const QString &name)
This is to be called on startup in those few threads that haven't been ported to MThread.
Definition: mthread.cpp:205
QString GetHostName(void)
QString GetSetting(const QString &key, const QString &defaultval="")
QString GetSettingOnHost(const QString &key, const QString &host, const QString &defaultval="")
T GetDurSetting(const QString &key, T defaultval=T::zero())
void BlockShutdown(void)
void dispatch(const MythEvent &event)
int GetNumSetting(const QString &key, int defaultval=0)
bool IsBlockingClient(void) const
is this client blocking shutdown
void AllowShutdown(void)
bool GetBoolSetting(const QString &key, bool defaultval=false)
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:225
This class is used as a container for messages.
Definition: mythevent.h:17
const QString & Message() const
Definition: mythevent.h:65
static const Type kMythEventMessage
Definition: mythevent.h:79
void addListener(QObject *listener)
Add a listener to the observable.
void removeListener(QObject *listener)
Remove a listener to the observable.
This class creates a preview image of a recording.
Holds information on recordings and videos.
Definition: programinfo.h:74
uint GetChanID(void) const
This is the unique key used in the database to locate tuning information.
Definition: programinfo.h:380
void SaveTranscodeStatus(TranscodingStatus trans)
Set "transcoded" field in "recorded" table to "trans".
QString toString(Verbosity v=kLongDescription, const QString &sep=":", const QString &grp="\"") const
virtual void SaveFilesize(uint64_t fsize)
Sets recording file size in database, and sets "filesize" field.
QString GetRecordingGroup(void) const
Definition: programinfo.h:427
uint QueryTranscoderID(void) const
bool HasCutlist(void) const
Definition: programinfo.h:491
QString GetHostname(void) const
Definition: programinfo.h:429
void MarkAsInUse(bool inuse, const QString &usedFor="")
Tracks a recording's in use status, to prevent deletion and to allow the storage scheduler to perform...
QDateTime GetRecordingStartTime(void) const
Approximate time the recording started.
Definition: programinfo.h:412
bool IsLocal(void) const
Definition: programinfo.h:358
bool Reload(void)
bool IsCommercialFree(void) const
Definition: programinfo.h:489
virtual void SubstituteMatches(QString &str)
Subsitute MATCH% type variable names in the given string.
QString GetPlaybackURL(bool checkMaster=false, bool forceCheckLocal=false)
Returns filename or URL to be used to play back this recording.
void SendUpdateEvent(void) const
Sends event out that the ProgramInfo should be reloaded.
void SetPathname(const QString &pn)
Holds information on a TV Program one might wish to record.
Definition: recordinginfo.h:36
int GetAutoRunJobs(void) const
Returns a bitmap of which jobs are attached to this RecordingInfo.
static const uint kTranscoderAutodetect
sentinel value
unsigned int uint
Definition: compat.h:60
@ GENERIC_EXIT_RESTART
Need to restart transcoding.
Definition: exitcodes.h:34
@ GENERIC_EXIT_CMD_NOT_FOUND
Command not found.
Definition: exitcodes.h:15
@ GENERIC_EXIT_DAEMONIZING_ERROR
Error daemonizing or execl.
Definition: exitcodes.h:31
@ GENERIC_EXIT_NO_RECORDING_DATA
No program/recording data.
Definition: exitcodes.h:32
@ GENERIC_EXIT_NOT_OK
Exited with error.
Definition: exitcodes.h:14
#define LOC
Definition: jobqueue.cpp:45
static constexpr int64_t kRecentInterval
Definition: jobqueue.cpp:48
static constexpr int PTHREAD_NULL
Definition: jobqueue.cpp:18
#define JOBSTATUS_STATUSTEXT(A, B, C)
Definition: jobqueue.cpp:1145
@ JOB_LIST_DONE
Definition: jobqueue.h:68
@ JOB_LIST_ALL
Definition: jobqueue.h:67
@ JOB_LIST_RECENT
Definition: jobqueue.h:71
@ JOB_LIST_NOT_DONE
Definition: jobqueue.h:69
@ JOB_LIST_ERROR
Definition: jobqueue.h:70
#define JOBSTATUS_MAP(F)
Definition: jobqueue.h:25
@ JOB_USERJOB3
Definition: jobqueue.h:86
@ JOB_METADATA
Definition: jobqueue.h:80
@ JOB_USERJOB1
Definition: jobqueue.h:84
@ JOB_USERJOB
Definition: jobqueue.h:83
@ JOB_USERJOB2
Definition: jobqueue.h:85
@ JOB_PREVIEW
Definition: jobqueue.h:81
@ JOB_NONE
Definition: jobqueue.h:75
@ JOB_COMMFLAG
Definition: jobqueue.h:79
@ JOB_USERJOB4
Definition: jobqueue.h:87
@ JOB_TRANSCODE
Definition: jobqueue.h:78
static QMap< QString, int > JobNameToType
Definition: jobqueue.h:90
JobStatus
Definition: jobqueue.h:44
JobCmds
Definition: jobqueue.h:50
@ JOB_STOP
Definition: jobqueue.h:54
@ JOB_RESTART
Definition: jobqueue.h:55
@ JOB_RESUME
Definition: jobqueue.h:53
@ JOB_RUN
Definition: jobqueue.h:51
@ JOB_PAUSE
Definition: jobqueue.h:52
JobFlags
Definition: jobqueue.h:58
@ JOB_USE_CUTLIST
Definition: jobqueue.h:60
@ JOB_NO_FLAGS
Definition: jobqueue.h:59
uint64_t verboseMask
Definition: logging.cpp:101
QString logPropagateArgs
Definition: logging.cpp:86
static constexpr const char * MYTH_APPNAME_MYTHJOBQUEUE
Definition: mythappname.h:5
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
MythDB * GetMythDB(void)
Definition: mythdb.cpp:50
QString GetAppBinDir(void)
Definition: mythdirs.cpp:282
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
bool myth_nice(int val)
bool myth_ioprio(int)
Allows setting the I/O priority of the current process/thread.
@ kMSLowExitVal
allow exit values 0-127 only
Definition: mythsystem.h:47
uint myth_system(const QString &command, uint flags, std::chrono::seconds timeout)
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
Definition: mythdate.cpp:28
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:93
@ kFilename
Default UTC, "yyyyMMddhhmmss".
Definition: mythdate.h:18
@ ISODate
Default UTC.
Definition: mythdate.h:17
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:39
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
string hostname
Definition: caa.py:17
const QString kJobQueueInUseID
@ TRANSCODING_COMPLETE
Definition: programtypes.h:158
@ TRANSCODING_RUNNING
Definition: programtypes.h:159
@ TRANSCODING_NOT_TRANSCODED
Definition: programtypes.h:157
QDateTime schedruntime
Definition: jobqueue.h:104
QString hostname
Definition: jobqueue.h:112
QDateTime statustime
Definition: jobqueue.h:111
QString comment
Definition: jobqueue.h:114
QDateTime recstartts
Definition: jobqueue.h:103
QString args
Definition: jobqueue.h:113
QString startts
Definition: jobqueue.h:105
QDateTime inserttime
Definition: jobqueue.h:106
ProgramInfo * pginfo
Definition: jobqueue.h:123
QString command
Definition: jobqueue.h:122
QString desc
Definition: jobqueue.h:121