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