MythTV  master
programinfo.cpp
Go to the documentation of this file.
1 // -*- Mode: c++ -*-
2 
3 // POSIX headers
4 #include <unistd.h> // for getpid()
5 
6 // C++ headers
7 #include <algorithm>
8 
9 // Qt headers
10 #include <QMap>
11 #include <QUrl>
12 #include <QFile>
13 #include <QFileInfo>
14 #include <QDir>
15 
16 // MythTV headers
17 #include "libmythbase/compat.h"
18 #include "libmythbase/mythcdrom.h"
20 #include "libmythbase/mythdb.h"
25 #include "libmythbase/remotefile.h"
27 #include "libmythbase/stringutil.h"
28 
29 #include "programinfo.h"
30 #include "programinfoupdater.h"
31 #include "remoteutil.h"
32 
33 #define LOC QString("ProgramInfo(%1): ").arg(GetBasename())
34 
35 //#define DEBUG_IN_USE
36 
37 static int init_tr(void);
38 
44 
45 const static uint kInvalidDateTime = UINT_MAX;
46 
47 #define DEFINE_FLAGS_NAMES
48 #include "programtypeflags.h"
49 #undef DEFINE_FLAGS_NAMES
50 
51 const QString ProgramInfo::kFromRecordedQuery =
52  "SELECT r.title, r.subtitle, r.description, "// 0-2
53  " r.season, r.episode, r.category, "// 3-5
54  " r.chanid, c.channum, c.callsign, "// 6-8
55  " c.name, c.outputfilters,r.recgroup, "// 9-11
56  " r.playgroup, r.storagegroup, r.basename, "//12-14
57  " r.hostname, r.recpriority, r.seriesid, "//15-17
58  " r.programid, r.inetref, r.filesize, "//18-20
59  " r.progstart, r.progend, r.stars, "//21-23
60  " r.starttime, r.endtime, p.airdate+0, "//24-26
61  " r.originalairdate, r.lastmodified, r.recordid, "//27-29
62  " c.commmethod, r.commflagged, r.previouslyshown, "//30-32
63  " r.transcoder, r.transcoded, r.deletepending, "//33-35
64  " r.preserve, r.cutlist, r.autoexpire, "//36-38
65  " r.editing, r.bookmark, r.watched, "//39-41
66  " p.audioprop+0, p.videoprop+0, p.subtitletypes+0, "//42-44
67  " r.findid, rec.dupin, rec.dupmethod, "//45-47
68  " p.syndicatedepisodenumber, p.partnumber, p.parttotal, "//48-50
69  " p.season, p.episode, p.totalepisodes, "//51-53
70  " p.category_type, r.recordedid, r.inputname, "//54-56
71  " r.bookmarkupdate, r.lastplay "//57-58
72  "FROM recorded AS r "
73  "LEFT JOIN channel AS c "
74  "ON (r.chanid = c.chanid) "
75  "LEFT JOIN recordedprogram AS p "
76  "ON (r.chanid = p.chanid AND "
77  " r.progstart = p.starttime) "
78  "LEFT JOIN record AS rec "
79  "ON (r.recordid = rec.recordid) ";
80 
81 static void set_flag(uint32_t &flags, int flag_to_set, bool is_set)
82 {
83  flags &= ~flag_to_set;
84  if (is_set)
85  flags |= flag_to_set;
86 }
87 
88 static QString determineURLType(const QString& url)
89 {
90  QString result = url;
91 
92  if (!url.startsWith("dvd:") && !url.startsWith("bd:"))
93  {
94  if(url.endsWith(".img", Qt::CaseInsensitive) ||
95  url.endsWith(".iso", Qt::CaseInsensitive))
96  {
97  switch (MythCDROM::inspectImage(url))
98  {
99  case MythCDROM::kBluray:
100  result = "bd:" + url;
101  break;
102 
103  case MythCDROM::kDVD:
104  result = "dvd:" + url;
105  break;
106 
107  case MythCDROM::kUnknown:
108  // Quiet compiler warning.
109  break;
110  }
111  }
112  else
113  {
114  if (QDir(url + "/BDMV").exists())
115  result = "bd:" + url;
116  else if (QDir(url + "/VIDEO_TS").exists())
117  result = "dvd:" + url;
118  }
119  }
120 
121  return result;
122 }
123 
124 static const std::array<const QString,ProgramInfo::kNumCatTypes> kCatName
125 { "", "movie", "series", "sports", "tvshow" };
126 
128 {
129  if ((category_type > ProgramInfo::kCategoryNone) &&
130  (category_type < kCatName.size()))
131  return kCatName[category_type];
132 
133  return "";
134 }
135 
137 {
138  for (size_t i = 1; i < kCatName.size(); i++)
139  if (category_type == kCatName[i])
140  return (ProgramInfo::CategoryType) i;
142 }
143 
148  m_title(other.m_title),
149  m_sortTitle(other.m_sortTitle),
150  m_subtitle(other.m_subtitle),
151  m_sortSubtitle(other.m_sortSubtitle),
152  m_description(other.m_description),
153  m_season(other.m_season),
154  m_episode(other.m_episode),
155  m_totalEpisodes(other.m_totalEpisodes),
156  m_syndicatedEpisode(other.m_syndicatedEpisode),
157  m_category(other.m_category),
158  m_director(other.m_director),
159 
160  m_recPriority(other.m_recPriority),
161 
162  m_chanId(other.m_chanId),
163  m_chanStr(other.m_chanStr),
164  m_chanSign(other.m_chanSign),
165  m_chanName(other.m_chanName),
166  m_chanPlaybackFilters(other.m_chanPlaybackFilters),
167 
168  m_recGroup(other.m_recGroup),
169  m_playGroup(other.m_playGroup),
170 
171  m_pathname(other.m_pathname),
172 
173  m_hostname(other.m_hostname),
174  m_storageGroup(other.m_storageGroup),
175 
176  m_seriesId(other.m_seriesId),
177  m_programId(other.m_programId),
178  m_inetRef(other.m_inetRef),
179  m_catType(other.m_catType),
180 
181  m_fileSize(other.m_fileSize),
182 
183  m_startTs(other.m_startTs),
184  m_endTs(other.m_endTs),
185  m_recStartTs(other.m_recStartTs),
186  m_recEndTs(other.m_recEndTs),
187 
188  m_stars(other.m_stars),
189 
190  m_originalAirDate(other.m_originalAirDate),
191  m_lastModified(other.m_lastModified),
192  m_lastInUseTime(MythDate::current().addSecs(-4 * 60 * 60)),
193 
194  m_recPriority2(other.m_recPriority2),
195  m_recordId(other.m_recordId),
196  m_parentId(other.m_parentId),
197 
198  m_sourceId(other.m_sourceId),
199  m_inputId(other.m_inputId),
200 
201  m_findId(other.m_findId),
202  m_programFlags(other.m_programFlags),
203  m_videoProperties(other.m_videoProperties),
204  m_audioProperties(other.m_audioProperties),
205  m_subtitleProperties(other.m_subtitleProperties),
206  m_year(other.m_year),
207  m_partNumber(other.m_partNumber),
208  m_partTotal(other.m_partTotal),
209 
210  m_recStatus(other.m_recStatus),
211  m_recType(other.m_recType),
212  m_dupIn(other.m_dupIn),
213  m_dupMethod(other.m_dupMethod),
214 
215  m_recordedId(other.m_recordedId),
216  m_inputName(other.m_inputName),
217  m_bookmarkUpdate(other.m_bookmarkUpdate),
218 
219  // everything below this line is not serialized
220  m_availableStatus(other.m_availableStatus),
221  m_spread(other.m_spread),
222  m_startCol(other.m_startCol),
223 
224  // Private
225  m_positionMapDBReplacement(other.m_positionMapDBReplacement)
226 {
228 }
229 
234 {
236 
237  MSqlQuery query(MSqlQuery::InitCon());
238  query.prepare(
239  "SELECT chanid, starttime "
240  "FROM recorded "
241  "WHERE recordedid = :RECORDEDID");
242  query.bindValue(":RECORDEDID", _recordedid);
243 
244  if (query.exec() && query.next())
245  {
246  uint _chanid = query.value(0).toUInt();
247  QDateTime _recstartts = MythDate::as_utc(query.value(1).toDateTime());
248  LoadProgramFromRecorded(_chanid, _recstartts);
249  }
250  else
251  {
252  LOG(VB_GENERAL, LOG_CRIT, LOC +
253  QString("Failed to find recorded entry for %1.")
254  .arg(_recordedid));
256  }
257 
259 }
260 
264 ProgramInfo::ProgramInfo(uint _chanid, const QDateTime &_recstartts)
265 {
267 
268  LoadProgramFromRecorded(_chanid, _recstartts);
270 }
271 
276  uint _recordedid,
277  QString _title,
278  QString _sortTitle,
279  QString _subtitle,
280  QString _sortSubtitle,
281  QString _description,
282  uint _season,
283  uint _episode,
284  uint _totalepisodes,
285  QString _syndicatedepisode,
286  QString _category,
287 
288  uint _chanid,
289  QString _channum,
290  QString _chansign,
291  QString _channame,
292  QString _chanplaybackfilters,
293 
294  QString _recgroup,
295  QString _playgroup,
296 
297  const QString &_pathname,
298 
299  QString _hostname,
300  QString _storagegroup,
301 
302  QString _seriesid,
303  QString _programid,
304  QString _inetref,
305  CategoryType _catType,
306 
307  int _recpriority,
308 
309  uint64_t _filesize,
310 
311  QDateTime _startts,
312  QDateTime _endts,
313  QDateTime _recstartts,
314  QDateTime _recendts,
315 
316  float _stars,
317 
318  uint _year,
319  uint _partnumber,
320  uint _parttotal,
321  QDate _originalAirDate,
322  QDateTime _lastmodified,
323 
324  RecStatus::Type _recstatus,
325 
326  uint _recordid,
327 
328  RecordingDupInType _dupin,
329  RecordingDupMethodType _dupmethod,
330 
331  uint _findid,
332 
333  uint _programflags,
334  uint _audioproperties,
335  uint _videoproperties,
336  uint _subtitleType,
337  QString _inputname,
338  QDateTime _bookmarkupdate) :
339  m_title(std::move(_title)),
340  m_sortTitle(std::move(_sortTitle)),
341  m_subtitle(std::move(_subtitle)),
342  m_sortSubtitle(std::move(_sortSubtitle)),
343  m_description(std::move(_description)),
344  m_season(_season),
345  m_episode(_episode),
346  m_totalEpisodes(_totalepisodes),
347  m_syndicatedEpisode(std::move(_syndicatedepisode)),
348  m_category(std::move(_category)),
349 
350  m_recPriority(_recpriority),
351 
352  m_chanId(_chanid),
353  m_chanStr(std::move(_channum)),
354  m_chanSign(std::move(_chansign)),
355  m_chanName(std::move(_channame)),
356  m_chanPlaybackFilters(std::move(_chanplaybackfilters)),
357 
358  m_recGroup(std::move(_recgroup)),
359  m_playGroup(std::move(_playgroup)),
360 
361  m_pathname(_pathname),
362 
363  m_hostname(std::move(_hostname)),
364  m_storageGroup(std::move(_storagegroup)),
365 
366  m_seriesId(std::move(_seriesid)),
367  m_programId(std::move(_programid)),
368  m_inetRef(std::move(_inetref)),
369  m_catType(_catType),
370 
371  m_fileSize(_filesize),
372 
373  m_startTs(std::move(_startts)),
374  m_endTs(std::move(_endts)),
375  m_recStartTs(std::move(_recstartts)),
376  m_recEndTs(std::move(_recendts)),
377 
378  m_stars(std::clamp(_stars, 0.0F, 1.0F)),
379 
380  m_originalAirDate(_originalAirDate),
381  m_lastModified(std::move(_lastmodified)),
382  m_lastInUseTime(MythDate::current().addSecs(-4 * 60 * 60)),
383 
384  m_recordId(_recordid),
385  m_findId(_findid),
386 
387  m_programFlags(_programflags),
388  m_videoProperties(_videoproperties),
389  m_audioProperties(_audioproperties),
390  m_subtitleProperties(_subtitleType),
391  m_year(_year),
392  m_partNumber(_partnumber),
393  m_partTotal(_parttotal),
394 
395  m_recStatus(_recstatus),
396  m_dupIn(_dupin),
397  m_dupMethod(_dupmethod),
398 
399  m_recordedId(_recordedid),
400  m_inputName(std::move(_inputname)),
401  m_bookmarkUpdate(std::move(_bookmarkupdate))
402 {
403  if (m_originalAirDate.isValid() && m_originalAirDate < QDate(1895, 12, 28))
404  m_originalAirDate = QDate();
405 
406  SetPathname(_pathname);
408 }
409 
414  QString _title,
415  QString _sortTitle,
416  QString _subtitle,
417  QString _sortSubtitle,
418  QString _description,
419  uint _season,
420  uint _episode,
421  QString _category,
422 
423  uint _chanid,
424  QString _channum,
425  QString _chansign,
426  QString _channame,
427 
428  QString _seriesid,
429  QString _programid,
430  QString _inetref,
431 
432  QDateTime _startts,
433  QDateTime _endts,
434  QDateTime _recstartts,
435  QDateTime _recendts,
436 
437  RecStatus::Type _recstatus,
438 
439  uint _recordid,
440 
441  RecordingType _rectype,
442 
443  uint _findid,
444 
445  bool duplicate) :
446  m_title(std::move(_title)),
447  m_sortTitle(std::move(_sortTitle)),
448  m_subtitle(std::move(_subtitle)),
449  m_sortSubtitle(std::move(_sortSubtitle)),
450  m_description(std::move(_description)),
451  m_season(_season),
452  m_episode(_episode),
453  m_category(std::move(_category)),
454 
455  m_chanId(_chanid),
456  m_chanStr(std::move(_channum)),
457  m_chanSign(std::move(_chansign)),
458  m_chanName(std::move(_channame)),
459 
460  m_seriesId(std::move(_seriesid)),
461  m_programId(std::move(_programid)),
462  m_inetRef(std::move(_inetref)),
463 
464  m_startTs(std::move(_startts)),
465  m_endTs(std::move(_endts)),
466  m_recStartTs(std::move(_recstartts)),
467  m_recEndTs(std::move(_recendts)),
468 
469  m_lastModified(m_startTs),
470  m_lastInUseTime(MythDate::current().addSecs(-4 * 60 * 60)),
471 
472  m_recordId(_recordid),
473  m_findId(_findid),
474 
475  m_programFlags((duplicate) ? FL_DUPLICATE : 0),
476 
477  m_recStatus(_recstatus),
478  m_recType(_rectype),
479  m_dupIn(kDupsUnset),
480  m_dupMethod(kDupCheckUnset)
481 {
483 }
484 
489  QString _title,
490  QString _sortTitle,
491  QString _subtitle,
492  QString _sortSubtitle,
493  QString _description,
494  QString _syndicatedepisode,
495  QString _category,
496 
497  uint _chanid,
498  QString _channum,
499  QString _chansign,
500  QString _channame,
501  QString _chanplaybackfilters,
502 
503  QDateTime _startts,
504  QDateTime _endts,
505  QDateTime _recstartts,
506  QDateTime _recendts,
507 
508  QString _seriesid,
509  QString _programid,
510  const CategoryType _catType,
511 
512  float _stars,
513  uint _year,
514  uint _partnumber,
515  uint _parttotal,
516 
517  QDate _originalAirDate,
518  RecStatus::Type _recstatus,
519  uint _recordid,
520  RecordingType _rectype,
521  uint _findid,
522 
523  bool commfree,
524  bool repeat,
525 
526  uint _videoproperties,
527  uint _audioproperties,
528  uint _subtitleType,
529 
530  uint _season,
531  uint _episode,
532  uint _totalepisodes,
533 
534  const ProgramList &schedList) :
535  m_title(std::move(_title)),
536  m_sortTitle(std::move(_sortTitle)),
537  m_subtitle(std::move(_subtitle)),
538  m_sortSubtitle(std::move(_sortSubtitle)),
539  m_description(std::move(_description)),
540  m_season(_season),
541  m_episode(_episode),
542  m_totalEpisodes(_totalepisodes),
543  m_syndicatedEpisode(std::move(_syndicatedepisode)),
544  m_category(std::move(_category)),
545 
546  m_chanId(_chanid),
547  m_chanStr(std::move(_channum)),
548  m_chanSign(std::move(_chansign)),
549  m_chanName(std::move(_channame)),
550  m_chanPlaybackFilters(std::move(_chanplaybackfilters)),
551 
552  m_seriesId(std::move(_seriesid)),
553  m_programId(std::move(_programid)),
554  m_catType(_catType),
555 
556  m_startTs(std::move(_startts)),
557  m_endTs(std::move(_endts)),
558  m_recStartTs(std::move(_recstartts)),
559  m_recEndTs(std::move(_recendts)),
560 
561  m_stars(std::clamp(_stars, 0.0F, 1.0F)),
562 
563  m_originalAirDate(_originalAirDate),
564  m_lastModified(m_startTs),
565  m_lastInUseTime(m_startTs.addSecs(-4 * 60 * 60)),
566 
567  m_recordId(_recordid),
568  m_findId(_findid),
569 
570  m_videoProperties(_videoproperties),
571  m_audioProperties(_audioproperties),
572  m_subtitleProperties(_subtitleType),
573  m_year(_year),
574  m_partNumber(_partnumber),
575  m_partTotal(_parttotal),
576 
577  m_recStatus(_recstatus),
578  m_recType(_rectype)
579 {
580  m_programFlags |= (commfree) ? FL_CHANCOMMFREE : 0;
581  m_programFlags |= (repeat) ? FL_REPEAT : 0;
582 
583  if (m_originalAirDate.isValid() && m_originalAirDate < QDate(1895, 12, 28))
584  m_originalAirDate = QDate();
585 
586  for (auto *it : schedList)
587  {
588  // If this showing is scheduled to be recorded, then we need to copy
589  // some of the information from the scheduler
590  //
591  // This applies even if the showing may be on a different channel
592  // to the one which is actually being recorded e.g. A regional or HD
593  // variant of the same channel
594  if (!IsSameProgramAndStartTime(*it))
595  continue;
596 
597  const ProgramInfo &s = *it;
599  m_recType = s.m_recType;
603  m_inputId = s.m_inputId;
604  m_dupIn = s.m_dupIn;
606  m_findId = s.m_findId;
610 
611  // This is the exact showing (same chanid or callsign)
612  // which will be recorded
613  if (IsSameChannel(s))
614  {
616  break;
617  }
618 
625  }
627 }
628 
633  QString _title,
634  QString _sortTitle,
635  QString _subtitle,
636  QString _sortSubtitle,
637  QString _description,
638  uint _season,
639  uint _episode,
640  uint _totalepisodes,
641  QString _category,
642 
643  uint _chanid,
644  QString _channum,
645  QString _chansign,
646  QString _channame,
647  QString _chanplaybackfilters,
648 
649  QString _recgroup,
650  QString _playgroup,
651 
652  QDateTime _startts,
653  QDateTime _endts,
654  QDateTime _recstartts,
655  QDateTime _recendts,
656 
657  QString _seriesid,
658  QString _programid,
659  QString _inetref,
660  QString _inputname) :
661  m_title(std::move(_title)),
662  m_sortTitle(std::move(_sortTitle)),
663  m_subtitle(std::move(_subtitle)),
664  m_sortSubtitle(std::move(_sortSubtitle)),
665  m_description(std::move(_description)),
666  m_season(_season),
667  m_episode(_episode),
668  m_totalEpisodes(_totalepisodes),
669  m_category(std::move(_category)),
670 
671  m_chanId(_chanid),
672  m_chanStr(std::move(_channum)),
673  m_chanSign(std::move(_chansign)),
674  m_chanName(std::move(_channame)),
675  m_chanPlaybackFilters(std::move(_chanplaybackfilters)),
676 
677  m_recGroup(std::move(_recgroup)),
678  m_playGroup(std::move(_playgroup)),
679 
680  m_seriesId(std::move(_seriesid)),
681  m_programId(std::move(_programid)),
682  m_inetRef(std::move(_inetref)),
683 
684  m_startTs(std::move(_startts)),
685  m_endTs(std::move(_endts)),
686  m_recStartTs(std::move(_recstartts)),
687  m_recEndTs(std::move(_recendts)),
688 
689  m_lastModified(MythDate::current()),
690  m_lastInUseTime(m_lastModified.addSecs(-4 * 60 * 60)),
691 
692  m_inputName(std::move(_inputname))
693 {
695 }
696 
700 ProgramInfo::ProgramInfo(const QString &_pathname)
701 {
703  if (_pathname.isEmpty())
704  {
705  return;
706  }
707 
708  uint _chanid = 0;
709  QDateTime _recstartts;
711  QueryKeyFromPathname(_pathname, _chanid, _recstartts) &&
712  LoadProgramFromRecorded(_chanid, _recstartts))
713  {
714  return;
715  }
716 
718 
719  QDateTime cur = MythDate::current();
720  m_recStartTs = m_startTs = cur.addSecs(-4 * 60 * 60 - 1);
721  m_recEndTs = m_endTs = cur.addSecs(-1);
722 
723  QString basename = _pathname.section('/', -1);
724  if (_pathname == basename)
725  SetPathname(QDir::currentPath() + '/' + _pathname);
726  else if (_pathname.contains("./") && !_pathname.contains(":"))
727  SetPathname(QFileInfo(_pathname).absoluteFilePath());
728  else
729  SetPathname(_pathname);
731 }
732 
736 ProgramInfo::ProgramInfo(const QString &_pathname,
737  const QString &_plot,
738  const QString &_title,
739  const QString &_sortTitle,
740  const QString &_subtitle,
741  const QString &_sortSubtitle,
742  const QString &_director,
743  int _season, int _episode,
744  const QString &_inetref,
745  std::chrono::minutes length_in_minutes,
746  uint _year,
747  const QString &_programid)
748 {
750 
751  m_title = _title;
752  m_sortTitle = _sortTitle;
753  m_subtitle = _subtitle;
754  m_sortSubtitle = _sortSubtitle;
755  m_description = _plot;
756  m_season = _season;
757  m_episode = _episode;
758  m_director = _director;
759  m_programId = _programid;
760  m_inetRef = _inetref;
761  m_year = _year;
762 
763  QDateTime cur = MythDate::current();
764  int64_t minutes = length_in_minutes.count();
765  m_recStartTs = cur.addSecs((minutes + 1) * -60);
766  m_recEndTs = m_recStartTs.addSecs(minutes * 60);
767  m_startTs = QDateTime(QDate(m_year,1,1),QTime(0,0,0), Qt::UTC);
768  m_endTs = m_startTs.addSecs(minutes * 60);
769 
770  QString pn = _pathname;
771  if (!_pathname.startsWith("myth://"))
772  pn = determineURLType(_pathname);
773 
774  SetPathname(pn);
776 }
777 
781 ProgramInfo::ProgramInfo(const QString &_title, uint _chanid,
782  const QDateTime &_startts,
783  const QDateTime &_endts)
784 {
786 
787  MSqlQuery query(MSqlQuery::InitCon());
788  query.prepare(
789  "SELECT chanid, channum, callsign, name, outputfilters, commmethod "
790  "FROM channel "
791  "WHERE chanid=:CHANID");
792  query.bindValue(":CHANID", _chanid);
793  if (query.exec() && query.next())
794  {
795  m_chanStr = query.value(1).toString();
796  m_chanSign = query.value(2).toString();
797  m_chanName = query.value(3).toString();
798  m_chanPlaybackFilters = query.value(4).toString();
799  set_flag(m_programFlags, FL_CHANCOMMFREE,
800  query.value(5).toInt() == COMM_DETECT_COMMFREE);
801  }
802 
803  m_chanId = _chanid;
804  m_startTs = _startts;
805  m_endTs = _endts;
806 
807  m_title = _title;
808  if (m_title.isEmpty())
809  {
810  QString channelFormat =
811  gCoreContext->GetSetting("ChannelFormat", "<num> <sign>");
812 
813  m_title = QString("%1 - %2").arg(ChannelText(channelFormat),
815  }
816 
818  QString("%1 (%2)").arg(m_title, QObject::tr("Manual Record"));
820 }
821 
826 {
827  if (this == &other)
828  return *this;
829 
830  clone(other);
831  return *this;
832 }
833 
835 void ProgramInfo::clone(const ProgramInfo &other,
836  bool ignore_non_serialized_data)
837 {
838  bool is_same =
839  ((m_chanId != 0U) && m_recStartTs.isValid() && m_startTs.isValid() &&
840  m_chanId == other.m_chanId && m_recStartTs == other.m_recStartTs &&
841  m_startTs == other.m_startTs);
842 
843  m_title = other.m_title;
844  m_sortTitle = other.m_sortTitle;
845  m_subtitle = other.m_subtitle;
848  m_season = other.m_season;
849  m_episode = other.m_episode;
852  m_category = other.m_category;
853  m_director = other.m_director;
854 
855  m_chanId = other.m_chanId;
856  m_chanStr = other.m_chanStr;
857  m_chanSign = other.m_chanSign;
858  m_chanName = other.m_chanName;
860 
861  m_recGroup = other.m_recGroup;
862  m_playGroup = other.m_playGroup;
863 
864  if (!ignore_non_serialized_data || !is_same ||
865  (GetBasename() != other.GetBasename()))
866  {
867  m_pathname = other.m_pathname;
868  }
869 
870  m_hostname = other.m_hostname;
872 
873  m_seriesId = other.m_seriesId;
874  m_programId = other.m_programId;
875  m_inetRef = other.m_inetRef;
876  m_catType = other.m_catType;
877 
879 
880  m_fileSize = other.m_fileSize;
881 
882  m_startTs = other.m_startTs;
883  m_endTs = other.m_endTs;
884  m_recStartTs = other.m_recStartTs;
885  m_recEndTs = other.m_recEndTs;
886 
887  m_stars = other.m_stars;
888 
889  m_year = other.m_year;
890  m_partNumber = other.m_partNumber;
891  m_partTotal = other.m_partTotal;
892 
895  m_lastInUseTime = MythDate::current().addSecs(-4 * 60 * 60);
896 
897  m_recStatus = other.m_recStatus;
898 
900  m_recordId = other.m_recordId;
901  m_parentId = other.m_parentId;
902 
903  m_recType = other.m_recType;
904  m_dupIn = other.m_dupIn;
905  m_dupMethod = other.m_dupMethod;
906 
907  m_recordedId = other.m_recordedId;
908  m_inputName = other.m_inputName;
910 
911  m_sourceId = other.m_sourceId;
912  m_inputId = other.m_inputId;
913 
914  m_findId = other.m_findId;
919 
920  if (!ignore_non_serialized_data)
921  {
922  m_spread = other.m_spread;
923  m_startCol = other.m_startCol;
925 
928  }
929 }
930 
932 {
933  m_title.clear();
934  m_sortTitle.clear();
935  m_subtitle.clear();
936  m_sortSubtitle.clear();
937  m_description.clear();
938  m_season = 0;
939  m_episode = 0;
940  m_totalEpisodes = 0;
941  m_syndicatedEpisode.clear();
942  m_category.clear();
943  m_director.clear();
944 
945  m_chanId = 0;
946  m_chanStr.clear();
947  m_chanSign.clear();
948  m_chanName.clear();
949  m_chanPlaybackFilters.clear();
950 
951  m_recGroup = "Default";
952  m_playGroup = "Default";
953 
954  m_pathname.clear();
955 
956  m_hostname.clear();
957  m_storageGroup = "Default";
958 
959  m_year = 0;
960  m_partNumber = 0;
961  m_partTotal = 0;
962 
963  m_seriesId.clear();
964  m_programId.clear();
965  m_inetRef.clear();
967 
968  m_recPriority = 0;
969 
970  m_fileSize = 0ULL;
971 
973  m_endTs = m_startTs;
976 
977  m_stars = 0.0F;
978 
979  m_originalAirDate = QDate();
981  m_lastInUseTime = m_startTs.addSecs(-4 * 60 * 60);
982 
984 
985  m_recPriority2 = 0;
986  m_recordId = 0;
987  m_parentId = 0;
988 
992 
993  m_recordedId = 0;
994  m_inputName.clear();
995  m_bookmarkUpdate = QDateTime();
996 
997  m_sourceId = 0;
998  m_inputId = 0;
999 
1000  m_findId = 0;
1001 
1002  m_programFlags = FL_NONE;
1003  m_videoProperties = VID_UNKNOWN;
1004  m_audioProperties = AUD_UNKNOWN;
1005  m_subtitleProperties = SUB_UNKNOWN;
1006 
1007  // everything below this line is not serialized
1008  m_spread = -1;
1009  m_startCol = -1;
1011 
1012  // Private
1013  m_inUseForWhat.clear();
1014  m_positionMapDBReplacement = nullptr;
1015 }
1016 
1021 bool qstringEqualOrDefault(const QString& a, const QString& b);
1022 bool qstringEqualOrDefault(const QString& a, const QString& b)
1023 {
1024  if (a == b)
1025  return true;
1026  if (a.isEmpty() and (b == "Default"))
1027  return true;
1028  if ((a == "Default") and b.isEmpty())
1029  return true;
1030  return false;
1031 }
1032 
1044 {
1045  if ((m_title != rhs.m_title) ||
1046  (m_subtitle != rhs.m_subtitle) ||
1047  (m_description != rhs.m_description) ||
1048  (m_season != rhs.m_season) ||
1049  (m_episode != rhs.m_episode) ||
1050  (m_totalEpisodes != rhs.m_totalEpisodes) ||
1052  (m_category != rhs.m_category)
1053 #if 0
1054  || (m_director != rhs.m_director)
1055 #endif
1056  )
1057  return false;
1058 
1059  if (m_recPriority != rhs.m_recPriority)
1060  return false;
1061 
1062  if ((m_chanId != rhs.m_chanId) ||
1063  (m_chanStr != rhs.m_chanStr) ||
1064  (m_chanSign != rhs.m_chanSign) ||
1065  (m_chanName != rhs.m_chanName) ||
1067  return false;
1068 
1071  return false;
1072 
1073  if (m_pathname != rhs.m_pathname)
1074  return false;
1075 
1076  if ((m_hostname != rhs.m_hostname) ||
1078  return false;
1079 
1080  if ((m_seriesId != rhs.m_seriesId) ||
1081  (m_programId != rhs.m_programId) ||
1082  (m_inetRef != rhs.m_inetRef) ||
1083  (m_catType != rhs.m_catType))
1084  return false;
1085 
1086  if (m_fileSize != rhs.m_fileSize)
1087  return false;
1088 
1089  if ((m_startTs != rhs.m_startTs) ||
1090  (m_endTs != rhs.m_endTs) ||
1091  (m_recStartTs != rhs.m_recStartTs) ||
1092  (m_recEndTs != rhs.m_recEndTs))
1093  return false;
1094 
1095  if ((m_stars != rhs.m_stars) ||
1098 #if 0
1099  || (m_lastInUseTime != rhs.m_lastInUseTime)
1100 #endif
1101  )
1102  return false;
1103 
1104  if (m_recPriority2 != rhs.m_recPriority2)
1105  return false;
1106 
1107  if ((m_recordId != rhs.m_recordId) ||
1108  (m_parentId != rhs.m_parentId))
1109  return false;
1110 
1111  if ((m_sourceId != rhs.m_sourceId) ||
1112  (m_inputId != rhs.m_inputId) ||
1113  (m_findId != rhs.m_findId))
1114  return false;
1115 
1116  if ((m_programFlags != rhs.m_programFlags) ||
1120  (m_year != rhs.m_year) ||
1121  (m_partNumber != rhs.m_partNumber) ||
1122  (m_partTotal != rhs.m_partTotal))
1123  return false;
1124 
1125  if ((m_recStatus != rhs.m_recStatus) ||
1126  (m_recType != rhs.m_recType) ||
1127  (m_dupIn != rhs.m_dupIn) ||
1128  (m_dupMethod != rhs.m_dupMethod))
1129  return false;
1130 
1131  if ((m_recordedId != rhs.m_recordedId) ||
1132  (m_inputName != rhs.m_inputName) ||
1134  return false;
1135 
1136  return true;
1137 }
1138 
1143 {
1144  std::shared_ptr<MythSortHelper>sh = getMythSortHelper();
1145 
1146  if (m_sortTitle.isEmpty() and not m_title.isEmpty())
1147  m_sortTitle = sh->doTitle(m_title);
1148  if (m_sortSubtitle.isEmpty() and not m_subtitle.isEmpty())
1149  m_sortSubtitle = sh->doTitle(m_subtitle);
1150 }
1151 
1152 void ProgramInfo::SetTitle(const QString &t, const QString &st)
1153 {
1154  m_title = t;
1155  m_sortTitle = st;
1156  ensureSortFields();
1157 }
1158 
1159 void ProgramInfo::SetSubtitle(const QString &st, const QString &sst)
1160 {
1161  m_subtitle = st;
1162  m_sortSubtitle = sst;
1163  ensureSortFields();
1164 }
1165 
1168  uint chanid, const QDateTime &recstartts)
1169 {
1170  return QString("%1_%2").arg(chanid).arg(recstartts.toString(Qt::ISODate));
1171 }
1172 
1176 bool ProgramInfo::ExtractKey(const QString &uniquekey,
1177  uint &chanid, QDateTime &recstartts)
1178 {
1179  QStringList keyParts = uniquekey.split('_');
1180  if (keyParts.size() != 2)
1181  return false;
1182  chanid = keyParts[0].toUInt();
1183  recstartts = MythDate::fromString(keyParts[1]);
1184  return (chanid != 0U) && recstartts.isValid();
1185 }
1186 
1188  const QString &pathname, uint &chanid, QDateTime &recstartts)
1189 {
1190  QString basename = pathname.section('/', -1);
1191  if (basename.isEmpty())
1192  return false;
1193 
1194  QStringList lr = basename.split("_");
1195  if (lr.size() == 2)
1196  {
1197  chanid = lr[0].toUInt();
1198  QStringList ts = lr[1].split(".");
1199  if (chanid && !ts.empty())
1200  {
1201  recstartts = MythDate::fromString(ts[0]);
1202  return recstartts.isValid();
1203  }
1204  }
1205 
1206  return false;
1207 }
1208 
1210  const QString &pathname, uint &chanid, QDateTime &recstartts)
1211 {
1212  QString basename = pathname.section('/', -1);
1213  if (basename.isEmpty())
1214  return false;
1215 
1216  MSqlQuery query(MSqlQuery::InitCon());
1217  query.prepare(
1218  "SELECT chanid, starttime "
1219  "FROM recorded "
1220  "WHERE basename = :BASENAME");
1221  query.bindValue(":BASENAME", basename);
1222  if (query.exec() && query.next())
1223  {
1224  chanid = query.value(0).toUInt();
1225  recstartts = MythDate::as_utc(query.value(1).toDateTime());
1226  return true;
1227  }
1228 
1229  return ExtractKeyFromPathname(pathname, chanid, recstartts);
1230 }
1231 
1232 bool ProgramInfo::QueryRecordedIdFromPathname(const QString &pathname,
1233  uint &recordedid)
1234 {
1235  QString basename = pathname.section('/', -1);
1236  if (basename.isEmpty())
1237  return false;
1238 
1239  MSqlQuery query(MSqlQuery::InitCon());
1240  query.prepare(
1241  "SELECT recordedid "
1242  "FROM recorded "
1243  "WHERE basename = :BASENAME");
1244  query.bindValue(":BASENAME", basename);
1245  if (query.exec() && query.next())
1246  {
1247  recordedid = query.value(0).toUInt();
1248  return true;
1249  }
1250 
1251  return false;
1252 }
1253 
1254 #define INT_TO_LIST(x) do { list << QString::number(x); } while (false)
1255 
1256 #define DATETIME_TO_LIST(x) do { \
1257  if ((x).isValid()) { \
1258  INT_TO_LIST((x).toSecsSinceEpoch()); \
1259  } else { \
1260  INT_TO_LIST(kInvalidDateTime); \
1261  } \
1262  } while (false)
1263 
1264 #define LONGLONG_TO_LIST(x) do { list << QString::number(x); } while (false)
1265 
1266 #define STR_TO_LIST(x) do { list << (x); } while (false)
1267 #define DATE_TO_LIST(x) do { list << (x).toString(Qt::ISODate); } while (false)
1268 
1269 #define FLOAT_TO_LIST(x) do { list << QString("%1").arg(x); } while (false)
1270 
1277 void ProgramInfo::ToStringList(QStringList &list) const
1278 {
1279  STR_TO_LIST(m_title); // 0
1280  STR_TO_LIST(m_subtitle); // 1
1281  STR_TO_LIST(m_description); // 2
1282  INT_TO_LIST(m_season); // 3
1283  INT_TO_LIST(m_episode); // 4
1286  STR_TO_LIST(m_category); // 7
1287  INT_TO_LIST(m_chanId); // 8
1288  STR_TO_LIST(m_chanStr); // 9
1289  STR_TO_LIST(m_chanSign); // 10
1290  STR_TO_LIST(m_chanName); // 11
1291  STR_TO_LIST(m_pathname); // 12
1292  INT_TO_LIST(m_fileSize); // 13
1293 
1294  DATETIME_TO_LIST(m_startTs); // 14
1295  DATETIME_TO_LIST(m_endTs); // 15
1296  INT_TO_LIST(m_findId); // 16
1297  STR_TO_LIST(m_hostname); // 17
1298  INT_TO_LIST(m_sourceId); // 18
1299  INT_TO_LIST(m_inputId); // 19 (m_formerly cardid)
1300  INT_TO_LIST(m_inputId); // 20
1301  INT_TO_LIST(m_recPriority); // 21
1302  INT_TO_LIST(m_recStatus); // 22
1303  INT_TO_LIST(m_recordId); // 23
1304 
1305  INT_TO_LIST(m_recType); // 24
1306  INT_TO_LIST(m_dupIn); // 25
1307  INT_TO_LIST(m_dupMethod); // 26
1310  INT_TO_LIST(m_programFlags); // 29
1311  STR_TO_LIST(!m_recGroup.isEmpty() ? m_recGroup : "Default"); // 30
1313  STR_TO_LIST(m_seriesId); // 32
1314  STR_TO_LIST(m_programId); // 33
1315  STR_TO_LIST(m_inetRef); // 34
1316 
1318  FLOAT_TO_LIST(m_stars); // 36
1320  STR_TO_LIST((!m_playGroup.isEmpty()) ? m_playGroup : "Default"); // 38
1321  INT_TO_LIST(m_recPriority2); // 39
1322  INT_TO_LIST(m_parentId); // 40
1323  STR_TO_LIST((!m_storageGroup.isEmpty()) ? m_storageGroup : "Default"); // 41
1327 
1328  INT_TO_LIST(m_year); // 45
1329  INT_TO_LIST(m_partNumber); // 46
1330  INT_TO_LIST(m_partTotal); // 47
1331  INT_TO_LIST(m_catType); // 48
1332 
1333  INT_TO_LIST(m_recordedId); // 49
1334  STR_TO_LIST(m_inputName); // 50
1336 /* do not forget to update the NUMPROGRAMLINES defines! */
1337 }
1338 
1339 // QStringList::const_iterator it = list.begin()+offset;
1340 
1341 #define NEXT_STR() do { if (it == listend) \
1342  { \
1343  LOG(VB_GENERAL, LOG_ERR, listerror); \
1344  clear(); \
1345  return false; \
1346  } \
1347  ts = *it++; } while (false)
1348 
1349 #define INT_FROM_LIST(x) do { NEXT_STR(); (x) = ts.toLongLong(); } while (false)
1350 #define ENUM_FROM_LIST(x, y) do { NEXT_STR(); (x) = ((y)ts.toInt()); } while (false)
1351 
1352 #define DATETIME_FROM_LIST(x) \
1353  do { NEXT_STR(); \
1354  if (ts.isEmpty() or (ts.toUInt() == kInvalidDateTime)) { \
1355  (x) = QDateTime(); \
1356  } else { \
1357  (x) = MythDate::fromSecsSinceEpoch(ts.toLongLong()); \
1358  } \
1359  } while (false)
1360 #define DATE_FROM_LIST(x) \
1361  do { NEXT_STR(); (x) = ((ts.isEmpty()) || (ts == "0000-00-00")) ? \
1362  QDate() : QDate::fromString(ts, Qt::ISODate); \
1363  } while (false)
1364 
1365 #define STR_FROM_LIST(x) do { NEXT_STR(); (x) = ts; } while (false)
1366 
1367 #define FLOAT_FROM_LIST(x) do { NEXT_STR(); (x) = ts.toFloat(); } while (false)
1368 
1380 bool ProgramInfo::FromStringList(QStringList::const_iterator &it,
1381  const QStringList::const_iterator& listend)
1382 {
1383  QString listerror = LOC + "FromStringList, not enough items in list.";
1384  QString ts;
1385 
1386  uint origChanid = m_chanId;
1387  QDateTime origRecstartts = m_recStartTs;
1388 
1389  STR_FROM_LIST(m_title); // 0
1390  STR_FROM_LIST(m_subtitle); // 1
1392  INT_FROM_LIST(m_season); // 3
1393  INT_FROM_LIST(m_episode); // 4
1396  STR_FROM_LIST(m_category); // 7
1397  INT_FROM_LIST(m_chanId); // 8
1398  STR_FROM_LIST(m_chanStr); // 9
1399  STR_FROM_LIST(m_chanSign); // 10
1400  STR_FROM_LIST(m_chanName); // 11
1401  STR_FROM_LIST(m_pathname); // 12
1402  INT_FROM_LIST(m_fileSize); // 13
1403 
1405  DATETIME_FROM_LIST(m_endTs); // 15
1406  INT_FROM_LIST(m_findId); // 16
1407  STR_FROM_LIST(m_hostname); // 17
1408  INT_FROM_LIST(m_sourceId); // 18
1409  NEXT_STR(); // 19 (formerly cardid)
1410  INT_FROM_LIST(m_inputId); // 20
1413  INT_FROM_LIST(m_recordId); // 23
1414 
1421  STR_FROM_LIST(m_recGroup); // 30
1423  STR_FROM_LIST(m_seriesId); // 32
1424  STR_FROM_LIST(m_programId); // 33
1425  STR_FROM_LIST(m_inetRef); // 34
1426 
1428  FLOAT_FROM_LIST(m_stars); // 36
1430  STR_FROM_LIST(m_playGroup); // 38
1432  INT_FROM_LIST(m_parentId); // 40
1437 
1438  INT_FROM_LIST(m_year); // 45
1439  INT_FROM_LIST(m_partNumber); // 46
1440  INT_FROM_LIST(m_partTotal); // 47
1442 
1443  INT_FROM_LIST(m_recordedId); // 49
1444  STR_FROM_LIST(m_inputName); // 50
1446 
1447  if (!origChanid || !origRecstartts.isValid() ||
1448  (origChanid != m_chanId) || (origRecstartts != m_recStartTs))
1449  {
1451  m_spread = -1;
1452  m_startCol = -1;
1453  m_inUseForWhat = QString();
1454  m_positionMapDBReplacement = nullptr;
1455  }
1456 
1457  ensureSortFields();
1458 
1459  return true;
1460 }
1461 
1462 template <typename T>
1463 QString propsValueToString (const QString& name, QMap<T,QString> propNames,
1464  T props)
1465 {
1466  if (props == 0)
1467  return propNames[0];
1468 
1469  QStringList result;
1470  for (uint i = 0; i < sizeof(T)*8 - 1; ++i)
1471  {
1472  uint bit = 1<<i;
1473  if ((props & bit) == 0)
1474  continue;
1475  if (propNames.contains(bit))
1476  {
1477  result += propNames[bit];
1478  continue;
1479  }
1480  QString tmp = QString("0x%1").arg(bit, sizeof(T)*2,16,QChar('0'));
1481  LOG(VB_GENERAL, LOG_ERR, QString("Unknown name for %1 flag 0x%2.")
1482  .arg(name, tmp));
1483  result += tmp;
1484  }
1485  return result.join('|');
1486 }
1487 
1488 template <typename T>
1489 uint propsValueFromString (const QString& name, QMap<T,QString> propNames,
1490  const QString& props)
1491 {
1492  if (props.isEmpty())
1493  return 0;
1494 
1495  uint result = 0;
1496 
1497  QStringList names = props.split('|');
1498  for ( const auto& n : names )
1499  {
1500  uint bit = propNames.key(n, 0);
1501  if (bit == 0)
1502  {
1503  LOG(VB_GENERAL, LOG_ERR, QString("Unknown flag for %1 %2")
1504  .arg(name, n));
1505  }
1506  else
1507  result |= bit;
1508  }
1509  return result;
1510 }
1511 
1513 {
1514  return propsValueToString("program", ProgramFlagNames, m_programFlags);
1515 }
1516 
1518 {
1519  return propsValueToString("subtitle", SubtitlePropsNames,
1521 }
1522 
1524 {
1525  return propsValueToString("video", VideoPropsNames, m_videoProperties);
1526 }
1527 
1529 {
1530  return propsValueToString("audio", AudioPropsNames, m_audioProperties);
1531 }
1532 
1534 {
1535  return propsValueFromString("subtitle", SubtitlePropsNames, names);
1536 }
1537 
1539 {
1540  return propsValueFromString("video", VideoPropsNames, names);
1541 }
1542 
1544 {
1545  return propsValueFromString("audio", AudioPropsNames, names);
1546 }
1547 
1548 void ProgramInfo::ProgramFlagsFromNames(const QString & names)
1549 {
1550  m_programFlags = propsValueFromString("program", ProgramFlagNames, names);
1551 }
1552 
1557  bool showrerecord,
1558  uint star_range,
1559  uint date_format) const
1560 {
1561  QLocale locale = gCoreContext->GetQLocale();
1562  // NOTE: Format changes and relevant additions made here should be
1563  // reflected in RecordingRule
1564  QString channelFormat =
1565  gCoreContext->GetSetting("ChannelFormat", "<num> <sign>");
1566  QString longChannelFormat =
1567  gCoreContext->GetSetting("LongChannelFormat", "<num> <name>");
1568 
1569  QDateTime timeNow = MythDate::current();
1570 
1571  progMap["title"] = m_title;
1572  progMap["subtitle"] = m_subtitle;
1573  progMap["sorttitle"] = m_sortTitle;
1574  progMap["sortsubtitle"] = m_sortSubtitle;
1575 
1576  QString tempSubTitle = m_title;
1577  QString tempSortSubtitle = m_sortTitle;
1578  if (!m_subtitle.trimmed().isEmpty())
1579  {
1580  tempSubTitle = QString("%1 - \"%2\"")
1581  .arg(tempSubTitle, m_subtitle);
1582  tempSortSubtitle = QString("%1 - \"%2\"")
1583  .arg(tempSortSubtitle, m_sortSubtitle);
1584  }
1585 
1586  progMap["titlesubtitle"] = tempSubTitle;
1587  progMap["sorttitlesubtitle"] = tempSortSubtitle;
1588 
1589  progMap["description"] = progMap["description0"] = m_description;
1590 
1591  if (m_season > 0 || m_episode > 0)
1592  {
1593  progMap["season"] = StringUtil::intToPaddedString(m_season, 1);
1594  progMap["episode"] = StringUtil::intToPaddedString(m_episode, 1);
1595  progMap["totalepisodes"] = StringUtil::intToPaddedString(m_totalEpisodes, 1);
1596  progMap["s00e00"] = QString("s%1e%2")
1599  progMap["00x00"] = QString("%1x%2")
1602  }
1603  else
1604  {
1605  progMap["season"] = progMap["episode"] = "";
1606  progMap["totalepisodes"] = "";
1607  progMap["s00e00"] = progMap["00x00"] = "";
1608  }
1609  progMap["syndicatedepisode"] = m_syndicatedEpisode;
1610 
1611  progMap["category"] = m_category;
1612  progMap["director"] = m_director;
1613 
1614  progMap["callsign"] = m_chanSign;
1615  progMap["commfree"] = (m_programFlags & FL_CHANCOMMFREE) ? "1" : "0";
1616  progMap["outputfilters"] = m_chanPlaybackFilters;
1617  if (IsVideo())
1618  {
1619  progMap["starttime"] = "";
1620  progMap["startdate"] = "";
1621  progMap["endtime"] = "";
1622  progMap["enddate"] = "";
1623  progMap["recstarttime"] = "";
1624  progMap["recstartdate"] = "";
1625  progMap["recendtime"] = "";
1626  progMap["recenddate"] = "";
1627 
1628  if (m_startTs.date().year() == 1895)
1629  {
1630  progMap["startdate"] = "";
1631  progMap["recstartdate"] = "";
1632  }
1633  else
1634  {
1635  progMap["startdate"] = m_startTs.toLocalTime().toString("yyyy");
1636  progMap["recstartdate"] = m_startTs.toLocalTime().toString("yyyy");
1637  }
1638  }
1639  else // if (IsRecording())
1640  {
1641  using namespace MythDate;
1642  progMap["starttime"] = MythDate::toString(m_startTs, date_format | kTime);
1643  progMap["startdate"] =
1644  MythDate::toString(m_startTs, date_format | kDateFull | kSimplify);
1645  progMap["shortstartdate"] = MythDate::toString(m_startTs, date_format | kDateShort);
1646  progMap["endtime"] = MythDate::toString(m_endTs, date_format | kTime);
1647  progMap["enddate"] = MythDate::toString(m_endTs, date_format | kDateFull | kSimplify);
1648  progMap["shortenddate"] = MythDate::toString(m_endTs, date_format | kDateShort);
1649  progMap["recstarttime"] = MythDate::toString(m_recStartTs, date_format | kTime);
1650  progMap["recstartdate"] = MythDate::toString(m_recStartTs, date_format | kDateShort);
1651  progMap["recendtime"] = MythDate::toString(m_recEndTs, date_format | kTime);
1652  progMap["recenddate"] = MythDate::toString(m_recEndTs, date_format | kDateShort);
1653  progMap["startts"] = QString::number(m_startTs.toSecsSinceEpoch());
1654  progMap["endts"] = QString::number(m_endTs.toSecsSinceEpoch());
1655  if (timeNow.toLocalTime().date().year() !=
1656  m_startTs.toLocalTime().date().year())
1657  progMap["startyear"] = m_startTs.toLocalTime().toString("yyyy");
1658  if (timeNow.toLocalTime().date().year() !=
1659  m_endTs.toLocalTime().date().year())
1660  progMap["endyear"] = m_endTs.toLocalTime().toString("yyyy");
1661  }
1662 
1663  using namespace MythDate;
1664  progMap["timedate"] =
1665  MythDate::toString(m_recStartTs, date_format | kDateTimeFull | kSimplify) + " - " +
1666  MythDate::toString(m_recEndTs, date_format | kTime);
1667 
1668  progMap["shorttimedate"] =
1669  MythDate::toString(m_recStartTs, date_format | kDateTimeShort | kSimplify) + " - " +
1670  MythDate::toString(m_recEndTs, date_format | kTime);
1671 
1672  progMap["starttimedate"] =
1674 
1675  progMap["shortstarttimedate"] =
1677 
1678  progMap["lastmodifiedtime"] = MythDate::toString(m_lastModified, date_format | kTime);
1679  progMap["lastmodifieddate"] =
1681  progMap["lastmodified"] =
1683 
1684  if (m_recordedId)
1685  progMap["recordedid"] = QString::number(m_recordedId);
1686 
1687  progMap["channum"] = m_chanStr;
1688  progMap["chanid"] = QString::number(m_chanId);
1689  progMap["channame"] = m_chanName;
1690  progMap["channel"] = ChannelText(channelFormat);
1691  progMap["longchannel"] = ChannelText(longChannelFormat);
1692 
1693  QString tmpSize = locale.toString(m_fileSize * (1.0 / (1024.0 * 1024.0 * 1024.0)), 'f', 2);
1694  progMap["filesize_str"] = QObject::tr("%1 GB", "GigaBytes").arg(tmpSize);
1695 
1696  progMap["filesize"] = locale.toString((quint64)m_fileSize);
1697 
1698  int seconds = m_recStartTs.secsTo(m_recEndTs);
1699  int minutes = seconds / 60;
1700 
1701  QString min_str = QObject::tr("%n minute(s)","",minutes);
1702 
1703  progMap["lenmins"] = min_str;
1704  int hours = minutes / 60;
1705  minutes = minutes % 60;
1706 
1707  progMap["lentime"] = min_str;
1708  if (hours > 0 && minutes > 0)
1709  {
1710  min_str = QObject::tr("%n minute(s)","",minutes);
1711  progMap["lentime"] = QString("%1 %2")
1712  .arg(QObject::tr("%n hour(s)","", hours), min_str);
1713  }
1714  else if (hours > 0)
1715  {
1716  progMap["lentime"] = QObject::tr("%n hour(s)","", hours);
1717  }
1718 
1719  progMap["recordedpercent"] =
1720  (m_recordedPercent >= 0)
1721  ? QString::number(m_recordedPercent) : QString();
1722  progMap["watchedpercent"] =
1723  ((m_watchedPercent > 0) && !IsWatched())
1724  ? QString::number(m_watchedPercent) : QString();
1725 
1726  // This is calling toChar from recordingtypes.cpp, not the QChar
1727  // constructor.
1728  progMap["rectypechar"] = toQChar(GetRecordingRuleType());
1729  progMap["rectype"] = ::toString(GetRecordingRuleType());
1730  QString tmp_rec = progMap["rectype"];
1732  {
1733  if (((m_recEndTs > timeNow) && (m_recStatus <= RecStatus::WillRecord)) ||
1735  {
1736  tmp_rec += QString(" %1%2").arg(m_recPriority > 0 ? "+" : "").arg(m_recPriority);
1737  if (m_recPriority2)
1738  tmp_rec += QString("/%1%2").arg(m_recPriority2 > 0 ? "+" : "").arg(m_recPriority2);
1739  tmp_rec += " ";
1740  }
1741  else
1742  {
1743  tmp_rec += " -- ";
1744  }
1745  if (showrerecord && (GetRecordingStatus() == RecStatus::Recorded) &&
1746  !IsDuplicate())
1747  {
1748  tmp_rec += QObject::tr("Re-Record");
1749  }
1750  else
1751  {
1753  }
1754  }
1755  progMap["rectypestatus"] = tmp_rec;
1756 
1757  progMap["card"] = RecStatus::toString(GetRecordingStatus(), m_inputId);
1758  progMap["input"] = RecStatus::toString(GetRecordingStatus(),
1759  GetShortInputName());
1760  progMap["inputname"] = m_inputName;
1761  // Don't add bookmarkupdate to progMap, for now.
1762 
1763  progMap["recpriority"] = QString::number(m_recPriority);
1764  progMap["recpriority2"] = QString::number(m_recPriority2);
1765  progMap["recordinggroup"] = (m_recGroup == "Default")
1766  ? QObject::tr("Default") : m_recGroup;
1767  progMap["playgroup"] = m_playGroup;
1768 
1769  if (m_storageGroup == "Default")
1770  progMap["storagegroup"] = QObject::tr("Default");
1771  else if (StorageGroup::kSpecialGroups.contains(m_storageGroup))
1772  {
1773  // This relies upon the translation established in the
1774  // definition of StorageGroup::kSpecialGroups.
1775  // clazy:exclude tr-non-literal
1776  progMap["storagegroup"] = QObject::tr(m_storageGroup.toUtf8().constData());
1777  }
1778  else
1779  {
1780  progMap["storagegroup"] = m_storageGroup;
1781  }
1782 
1783  progMap["programflags"] = QString::number(m_programFlags);
1784  progMap["audioproperties"] = QString::number(m_audioProperties);
1785  progMap["videoproperties"] = QString::number(m_videoProperties);
1786  progMap["subtitleType"] = QString::number(m_subtitleProperties);
1787  progMap["programflags_names"] = GetProgramFlagNames();
1788  progMap["audioproperties_names"] = GetAudioPropertyNames();
1789  progMap["videoproperties_names"] = GetVideoPropertyNames();
1790  progMap["subtitleType_names"] = GetSubtitleTypeNames();
1791 
1792  progMap["recstatus"] = RecStatus::toString(GetRecordingStatus(),
1794  progMap["recstatuslong"] = RecStatus::toDescription(GetRecordingStatus(),
1797 
1798  if (IsRepeat())
1799  {
1800  progMap["repeat"] = QString("(%1) ").arg(QObject::tr("Repeat"));
1801  progMap["longrepeat"] = progMap["repeat"];
1802  if (m_originalAirDate.isValid())
1803  {
1804  progMap["longrepeat"] = QString("(%1 %2) ")
1805  .arg(QObject::tr("Repeat"),
1808  date_format | MythDate::kDateFull | MythDate::kAddYear));
1809  }
1810  }
1811  else
1812  {
1813  progMap["repeat"] = "";
1814  progMap["longrepeat"] = "";
1815  }
1816 
1817  progMap["seriesid"] = m_seriesId;
1818  progMap["programid"] = m_programId;
1819  progMap["inetref"] = m_inetRef;
1820  progMap["catType"] = myth_category_type_to_string(m_catType);
1821 
1822  progMap["year"] = m_year > 1895 ? QString::number(m_year) : "";
1823 
1824  progMap["partnumber"] = m_partNumber ? QString::number(m_partNumber) : "";
1825  progMap["parttotal"] = m_partTotal ? QString::number(m_partTotal) : "";
1826 
1827  QString star_str = (m_stars != 0.0F) ?
1828  QObject::tr("%n star(s)", "", GetStars(star_range)) : "";
1829  progMap["stars"] = star_str;
1830  progMap["numstars"] = QString::number(GetStars(star_range));
1831 
1832  if (m_stars != 0.0F && m_year)
1833  progMap["yearstars"] = QString("(%1, %2)").arg(m_year).arg(star_str);
1834  else if (m_stars != 0.0F)
1835  progMap["yearstars"] = QString("(%1)").arg(star_str);
1836  else if (m_year)
1837  progMap["yearstars"] = QString("(%1)").arg(m_year);
1838  else
1839  progMap["yearstars"] = "";
1840 
1841  if (!m_originalAirDate.isValid() ||
1842  (!m_programId.isEmpty() && m_programId.startsWith("MV")))
1843  {
1844  progMap["originalairdate"] = "";
1845  progMap["shortoriginalairdate"] = "";
1846  }
1847  else
1848  {
1849  progMap["originalairdate"] = MythDate::toString(
1850  m_originalAirDate, date_format | MythDate::kDateFull);
1851  progMap["shortoriginalairdate"] = MythDate::toString(
1852  m_originalAirDate, date_format | MythDate::kDateShort);
1853  }
1854 
1855  // 'mediatype' for a statetype, so untranslated
1856  // 'mediatypestring' for textarea, so translated
1857  // TODO Move to a dedicated ToState() method?
1858  QString mediaType;
1859  QString mediaTypeString;
1860  switch (GetProgramInfoType())
1861  {
1863  mediaType = "video";
1864  mediaTypeString = QObject::tr("Video");
1865  break;
1867  mediaType = "dvd";
1868  mediaTypeString = QObject::tr("DVD");
1869  break;
1871  mediaType = "httpstream";
1872  mediaTypeString = QObject::tr("HTTP Streaming");
1873  break;
1875  mediaType = "rtspstream";
1876  mediaTypeString = QObject::tr("RTSP Streaming");
1877  break;
1879  mediaType = "bluraydisc";
1880  mediaTypeString = QObject::tr("Blu-ray Disc");
1881  break;
1882  case kProgramInfoTypeRecording : // Fall through
1883  default :
1884  mediaType = "recording";
1885  mediaTypeString = QObject::tr("Recording",
1886  "Recorded file, object not action");
1887  }
1888  progMap["mediatype"] = mediaType;
1889  progMap["mediatypestring"] = mediaTypeString;
1890 }
1891 
1893 std::chrono::seconds ProgramInfo::GetSecondsInRecording(void) const
1894 {
1895  auto recsecs = std::chrono::seconds(m_recStartTs.secsTo(m_endTs));
1896  auto duration = std::chrono::seconds(m_startTs.secsTo(m_endTs));
1897  return (recsecs > 0s) ? recsecs : std::max(duration, 0s);
1898 }
1899 
1902 {
1904 }
1905 
1908 {
1909  uint64_t last_frame = 0;
1910  frm_pos_map_t posMap;
1912  if (posMap.empty())
1913  {
1915  if (posMap.empty())
1917  }
1918  if (!posMap.empty())
1919  {
1920  frm_pos_map_t::const_iterator it = posMap.constEnd();
1921  --it;
1922  last_frame = it.key();
1923  }
1924  return last_frame;
1925 }
1926 
1927 bool ProgramInfo::IsGeneric(void) const
1928 {
1929  return
1930  (m_programId.isEmpty() && m_subtitle.isEmpty() &&
1931  m_description.isEmpty()) ||
1932  (!m_programId.isEmpty() && m_programId.endsWith("0000")
1933  && m_catType == kCategorySeries);
1934 }
1935 
1936 QString ProgramInfo::toString(const Verbosity v, const QString& sep, const QString& grp)
1937  const
1938 {
1939  QString str;
1940  switch (v)
1941  {
1942  case kLongDescription:
1943  str = LOC + "channame(" + m_chanName + ")\n";
1944  str += " startts(" +
1945  m_startTs.toString() + ") endts(" + m_endTs.toString() + ")\n";
1946  str += " recstartts(" + m_recStartTs.toString() +
1947  ") recendts(" + m_recEndTs.toString() + ")\n";
1948  str += " title(" + m_title + ")";
1949  break;
1950  case kTitleSubtitle:
1951  str = m_title.contains(' ') ?
1952  QString("%1%2%3").arg(grp, m_title, grp) : m_title;
1953  if (!m_subtitle.isEmpty())
1954  {
1955  str += m_subtitle.contains(' ') ?
1956  QString("%1%2%3%4").arg(sep, grp, m_subtitle, grp) :
1957  QString("%1%2").arg(sep, m_subtitle);
1958  }
1959  break;
1960  case kRecordingKey:
1961  str = QString("%1 at %2")
1963  break;
1964  case kSchedulingKey:
1965  str = QString("%1 @ %2")
1967  break;
1968  }
1969 
1970  return str;
1971 }
1972 
1974 {
1976  if (test.GetChanID())
1977  {
1978  clone(test, true);
1979  return true;
1980  }
1981  return false;
1982 }
1983 
1988  const uint _chanid, const QDateTime &_recstartts)
1989 {
1990  if (!_chanid || !_recstartts.isValid())
1991  {
1993  return false;
1994  }
1995 
1996  MSqlQuery query(MSqlQuery::InitCon());
1997  query.prepare(
1999  "WHERE r.chanid = :CHANID AND "
2000  " r.starttime = :RECSTARTTS");
2001  query.bindValue(":CHANID", _chanid);
2002  query.bindValue(":RECSTARTTS", _recstartts);
2003 
2004  if (!query.exec())
2005  {
2006  MythDB::DBError("LoadProgramFromRecorded", query);
2008  return false;
2009  }
2010 
2011  if (!query.next())
2012  {
2014  return false;
2015  }
2016 
2017  bool is_reload = (m_chanId == _chanid) && (m_recStartTs == _recstartts);
2018  if (!is_reload)
2019  {
2020  // These items are not initialized below so they need to be cleared
2021  // if we're loading in a different program into this ProgramInfo
2023  m_lastInUseTime = MythDate::current().addSecs(-4 * 60 * 60);
2025  m_recPriority2 = 0;
2026  m_parentId = 0;
2027  m_sourceId = 0;
2028  m_inputId = 0;
2029 
2030  // everything below this line (in context) is not serialized
2031  m_spread = m_startCol = -1;
2033  m_inUseForWhat.clear();
2034  m_positionMapDBReplacement = nullptr;
2035  }
2036 
2037  m_title = query.value(0).toString();
2038  m_subtitle = query.value(1).toString();
2039  m_description = query.value(2).toString();
2040  m_season = query.value(3).toUInt();
2041  if (m_season == 0)
2042  m_season = query.value(51).toUInt();
2043  m_episode = query.value(4).toUInt();
2044  if (m_episode == 0)
2045  m_episode = query.value(52).toUInt();
2046  m_totalEpisodes = query.value(53).toUInt();
2047  m_syndicatedEpisode = query.value(48).toString();
2048  m_category = query.value(5).toString();
2049 
2050  m_chanId = _chanid;
2051  m_chanStr = QString("#%1").arg(m_chanId);
2054  m_chanPlaybackFilters.clear();
2055  if (!query.value(7).toString().isEmpty())
2056  {
2057  m_chanStr = query.value(7).toString();
2058  m_chanSign = query.value(8).toString();
2059  m_chanName = query.value(9).toString();
2060  m_chanPlaybackFilters = query.value(10).toString();
2061  }
2062 
2063  m_recGroup = query.value(11).toString();
2064  m_playGroup = query.value(12).toString();
2065 
2066  // We don't want to update the pathname if the basename is
2067  // the same as we may have already expanded pathname from
2068  // a simple basename to a localized path.
2069  QString new_basename = query.value(14).toString();
2070  if ((GetBasename() != new_basename) || !is_reload)
2071  {
2072  if (is_reload)
2073  {
2074  LOG(VB_FILE, LOG_INFO, LOC +
2075  QString("Updated pathname '%1':'%2' -> '%3'")
2076  .arg(m_pathname, GetBasename(), new_basename));
2077  }
2078  SetPathname(new_basename);
2079  }
2080 
2081  m_hostname = query.value(15).toString();
2082  m_storageGroup = query.value(13).toString();
2083 
2084  m_seriesId = query.value(17).toString();
2085  m_programId = query.value(18).toString();
2086  m_inetRef = query.value(19).toString();
2087  m_catType = string_to_myth_category_type(query.value(54).toString());
2088 
2089  m_recPriority = query.value(16).toInt();
2090 
2091  m_fileSize = query.value(20).toULongLong();
2092 
2093  m_startTs = MythDate::as_utc(query.value(21).toDateTime());
2094  m_endTs = MythDate::as_utc(query.value(22).toDateTime());
2095  m_recStartTs = MythDate::as_utc(query.value(24).toDateTime());
2096  m_recEndTs = MythDate::as_utc(query.value(25).toDateTime());
2097 
2098  m_stars = std::clamp((float)query.value(23).toDouble(), 0.0F, 1.0F);
2099 
2100  m_year = query.value(26).toUInt();
2101  m_partNumber = query.value(49).toUInt();
2102  m_partTotal = query.value(50).toUInt();
2103 
2104  m_originalAirDate = query.value(27).toDate();
2105  m_lastModified = MythDate::as_utc(query.value(28).toDateTime());
2106  //m_lastInUseTime;
2107 
2109 
2110  //m_recPriority2;
2111 
2112  m_recordId = query.value(29).toUInt();
2113  //m_parentId;
2114 
2115  //m_sourcid;
2116  //m_inputId;
2117  //m_cardid;
2118  m_findId = query.value(45).toUInt();
2119 
2120  //m_recType;
2121  m_dupIn = RecordingDupInType(query.value(46).toInt());
2122  m_dupMethod = RecordingDupMethodType(query.value(47).toInt());
2123 
2124  m_recordedId = query.value(55).toUInt();
2125  m_inputName = query.value(56).toString();
2126  m_bookmarkUpdate = MythDate::as_utc(query.value(57).toDateTime());
2127 
2128  // ancillary data -- begin
2129  m_programFlags = FL_NONE;
2130  set_flag(m_programFlags, FL_CHANCOMMFREE,
2131  query.value(30).toInt() == COMM_DETECT_COMMFREE);
2132  set_flag(m_programFlags, FL_COMMFLAG,
2133  query.value(31).toInt() == COMM_FLAG_DONE);
2134  set_flag(m_programFlags, FL_COMMPROCESSING ,
2135  query.value(31).toInt() == COMM_FLAG_PROCESSING);
2136  set_flag(m_programFlags, FL_REPEAT, query.value(32).toBool());
2137  set_flag(m_programFlags, FL_TRANSCODED,
2138  query.value(34).toInt() == TRANSCODING_COMPLETE);
2139  set_flag(m_programFlags, FL_DELETEPENDING, query.value(35).toBool());
2140  set_flag(m_programFlags, FL_PRESERVED, query.value(36).toBool());
2141  set_flag(m_programFlags, FL_CUTLIST, query.value(37).toBool());
2142  set_flag(m_programFlags, FL_AUTOEXP, query.value(38).toBool());
2143  set_flag(m_programFlags, FL_REALLYEDITING, query.value(39).toBool());
2144  set_flag(m_programFlags, FL_BOOKMARK, query.value(40).toBool());
2145  set_flag(m_programFlags, FL_WATCHED, query.value(41).toBool());
2146  set_flag(m_programFlags, FL_LASTPLAYPOS, query.value(58).toBool());
2147  set_flag(m_programFlags, FL_EDITING,
2148  ((m_programFlags & FL_REALLYEDITING) != 0U) ||
2149  ((m_programFlags & FL_COMMPROCESSING) != 0U));
2150 
2151  m_audioProperties = query.value(42).toUInt();
2152  m_videoProperties = query.value(43).toUInt();
2153  m_subtitleProperties = query.value(44).toUInt();
2154  // ancillary data -- end
2155 
2156  if (m_originalAirDate.isValid() && m_originalAirDate < QDate(1895, 12, 28))
2157  m_originalAirDate = QDate();
2158 
2159  // Extra stuff which is not serialized and may get lost.
2160  // m_spread
2161  // m_startCol
2162  // m_availableStatus
2163  // m_inUseForWhat
2164  // m_postitionMapDBReplacement
2165 
2166  return true;
2167 }
2168 
2174 {
2175  return (m_title == other.m_title &&
2176  m_chanId == other.m_chanId &&
2177  m_startTs == other.m_startTs);
2178 }
2179 
2185 {
2187  return m_recordId == other.m_recordId;
2188 
2189  if (m_findId && m_findId == other.m_findId &&
2190  (m_recordId == other.m_recordId || m_recordId == other.m_parentId))
2191  return true;
2192 
2193  if (m_dupMethod & kDupCheckNone)
2194  return false;
2195 
2196  if (m_title.compare(other.m_title, Qt::CaseInsensitive) != 0)
2197  return false;
2198 
2199  if (m_catType == kCategorySeries)
2200  {
2201  if (m_programId.endsWith("0000"))
2202  return false;
2203  }
2204 
2205  if (!m_programId.isEmpty() && !other.m_programId.isEmpty())
2206  {
2207  if (s_usingProgIDAuth)
2208  {
2209  int index = m_programId.indexOf('/');
2210  int oindex = other.m_programId.indexOf('/');
2211  if (index == oindex && (index < 0 ||
2212  m_programId.left(index) == other.m_programId.left(oindex)))
2213  return m_programId == other.m_programId;
2214  }
2215  else
2216  {
2217  return m_programId == other.m_programId;
2218  }
2219  }
2220 
2221  if ((m_dupMethod & kDupCheckSub) &&
2222  ((m_subtitle.isEmpty()) ||
2223  (m_subtitle.compare(other.m_subtitle, Qt::CaseInsensitive) != 0)))
2224  return false;
2225 
2226  if ((m_dupMethod & kDupCheckDesc) &&
2227  ((m_description.isEmpty()) ||
2228  (m_description.compare(other.m_description, Qt::CaseInsensitive) != 0)))
2229  return false;
2230 
2232  ((m_subtitle.isEmpty() &&
2233  ((!other.m_subtitle.isEmpty() &&
2234  m_description.compare(other.m_subtitle, Qt::CaseInsensitive) != 0) ||
2235  (other.m_subtitle.isEmpty() &&
2236  m_description.compare(other.m_description, Qt::CaseInsensitive) != 0))) ||
2237  (!m_subtitle.isEmpty() &&
2238  ((other.m_subtitle.isEmpty() &&
2239  m_subtitle.compare(other.m_description, Qt::CaseInsensitive) != 0) ||
2240  (!other.m_subtitle.isEmpty() &&
2241  m_subtitle.compare(other.m_subtitle, Qt::CaseInsensitive) != 0)))))
2242  return false;
2243 
2244  return true;
2245 }
2246 
2253 bool ProgramInfo::IsSameProgram(const ProgramInfo& other) const
2254 {
2255  if (m_title.compare(other.m_title, Qt::CaseInsensitive) != 0)
2256  return false;
2257 
2258  if (!m_programId.isEmpty() && !other.m_programId.isEmpty())
2259  {
2260  if (m_catType == kCategorySeries)
2261  {
2262  if (m_programId.endsWith("0000"))
2263  return false;
2264  }
2265 
2266  if (s_usingProgIDAuth)
2267  {
2268  int index = m_programId.indexOf('/');
2269  int oindex = other.m_programId.indexOf('/');
2270  if (index == oindex && (index < 0 ||
2271  m_programId.left(index) == other.m_programId.left(oindex)))
2272  return m_programId == other.m_programId;
2273  }
2274  else
2275  {
2276  return m_programId == other.m_programId;
2277  }
2278  }
2279 
2280  if ((m_dupMethod & kDupCheckSub) &&
2281  ((m_subtitle.isEmpty()) ||
2282  (m_subtitle.compare(other.m_subtitle, Qt::CaseInsensitive) != 0)))
2283  return false;
2284 
2285  if ((m_dupMethod & kDupCheckDesc) &&
2286  ((m_description.isEmpty()) ||
2287  (m_description.compare(other.m_description, Qt::CaseInsensitive) != 0)))
2288  return false;
2289 
2291  ((m_subtitle.isEmpty() &&
2292  ((!other.m_subtitle.isEmpty() &&
2293  m_description.compare(other.m_subtitle, Qt::CaseInsensitive) != 0) ||
2294  (other.m_subtitle.isEmpty() &&
2295  m_description.compare(other.m_description, Qt::CaseInsensitive) != 0))) ||
2296  (!m_subtitle.isEmpty() &&
2297  ((other.m_subtitle.isEmpty() &&
2298  m_subtitle.compare(other.m_description, Qt::CaseInsensitive) != 0) ||
2299  (!other.m_subtitle.isEmpty() &&
2300  m_subtitle.compare(other.m_subtitle, Qt::CaseInsensitive) != 0)))))
2301  return false;
2302 
2303  return true;
2304 }
2305 
2312 {
2313  if (m_startTs != other.m_startTs)
2314  return false;
2315  if (IsSameChannel(other))
2316  return true;
2317  if (!IsSameProgram(other))
2318  return false;
2319  return true;
2320 }
2321 
2328 {
2329  if (m_title.compare(other.m_title, Qt::CaseInsensitive) != 0)
2330  return false;
2331  return m_startTs == other.m_startTs &&
2332  IsSameChannel(other);
2333 }
2334 
2342 {
2343  if (m_title.compare(other.m_title, Qt::CaseInsensitive) != 0)
2344  return false;
2345  return IsSameChannel(other) &&
2346  m_startTs < other.m_endTs &&
2347  m_endTs > other.m_startTs;
2348 }
2349 
2356 bool ProgramInfo::IsSameChannel(const ProgramInfo& other) const
2357 {
2358  return m_chanId == other.m_chanId ||
2359  (!m_chanSign.isEmpty() &&
2360  m_chanSign.compare(other.m_chanSign, Qt::CaseInsensitive) == 0);
2361 }
2362 
2364 {
2365  QMap<QString, int> authMap;
2366  std::array<QString,3> tables { "program", "recorded", "oldrecorded" };
2367  MSqlQuery query(MSqlQuery::InitCon());
2368 
2369  for (const QString& table : tables)
2370  {
2371  query.prepare(QString(
2372  "SELECT DISTINCT LEFT(programid, LOCATE('/', programid)) "
2373  "FROM %1 WHERE programid <> ''").arg(table));
2374  if (!query.exec())
2375  MythDB::DBError("CheckProgramIDAuthorities", query);
2376  else
2377  {
2378  while (query.next())
2379  authMap[query.value(0).toString()] = 1;
2380  }
2381  }
2382 
2383  int numAuths = authMap.count();
2384  LOG(VB_GENERAL, LOG_INFO,
2385  QString("Found %1 distinct programid authorities").arg(numAuths));
2386 
2387  s_usingProgIDAuth = (numAuths > 1);
2388 }
2389 
2394 QString ProgramInfo::CreateRecordBasename(const QString &ext) const
2395 {
2397 
2398  QString retval = QString("%1_%2.%3")
2399  .arg(QString::number(m_chanId), starts, ext);
2400 
2401  return retval;
2402 }
2403 
2405  uint chanid, const QString &pathname, bool use_remote)
2406 {
2407  QString fn_lower = pathname.toLower();
2409  if (chanid)
2411  else if (fn_lower.startsWith("http:"))
2413  else if (fn_lower.startsWith("rtsp:"))
2415  else
2416  {
2417  fn_lower = determineURLType(pathname);
2418 
2419  if (fn_lower.startsWith("dvd:"))
2420  {
2422  }
2423  else if (fn_lower.startsWith("bd:"))
2424  {
2426  }
2427  else if (use_remote && fn_lower.startsWith("myth://"))
2428  {
2429  QString tmpFileDVD = pathname + "/VIDEO_TS";
2430  QString tmpFileBD = pathname + "/BDMV";
2431  if (RemoteFile::Exists(tmpFileDVD))
2433  else if (RemoteFile::Exists(tmpFileBD))
2435  }
2436  }
2437  return pit;
2438 }
2439 
2440 void ProgramInfo::SetPathname(const QString &pn)
2441 {
2442  m_pathname = pn;
2443 
2445  SetProgramInfoType(pit);
2446 }
2447 
2449 {
2451 }
2452 
2454  AvailableStatusType status, const QString &where)
2455 {
2456  if (status != m_availableStatus)
2457  {
2458  LOG(VB_GUI, LOG_INFO,
2459  toString(kTitleSubtitle) + QString(": %1 -> %2 in %3")
2461  ::toString(status),
2462  where));
2463  }
2464  m_availableStatus = status;
2465 }
2466 
2470 bool ProgramInfo::SaveBasename(const QString &basename)
2471 {
2472  MSqlQuery query(MSqlQuery::InitCon());
2473  query.prepare("UPDATE recorded "
2474  "SET basename = :BASENAME "
2475  "WHERE recordedid = :RECORDEDID;");
2476  query.bindValue(":RECORDEDID", m_recordedId);
2477  query.bindValue(":BASENAME", basename);
2478 
2479  if (!query.exec())
2480  {
2481  MythDB::DBError("SetRecordBasename", query);
2482  return false;
2483  }
2484 
2485  query.prepare("UPDATE recordedfile "
2486  "SET basename = :BASENAME "
2487  "WHERE recordedid = :RECORDEDID;");
2488  query.bindValue(":RECORDEDID", m_recordedId);
2489  query.bindValue(":BASENAME", basename);
2490 
2491  if (!query.exec())
2492  {
2493  MythDB::DBError("SetRecordBasename", query);
2494  return false;
2495  }
2496 
2497  SetPathname(basename);
2498 
2499  SendUpdateEvent();
2500  return true;
2501 }
2502 
2510 QString ProgramInfo::QueryBasename(void) const
2511 {
2512  QString bn = GetBasename();
2513  if (!bn.isEmpty())
2514  return bn;
2515 
2516  MSqlQuery query(MSqlQuery::InitCon());
2517  query.prepare(
2518  "SELECT basename "
2519  "FROM recordedfile "
2520  "WHERE recordedid = :RECORDEDID;");
2521  query.bindValue(":RECORDEDID", m_recordedId);
2522 
2523  if (!query.exec())
2524  {
2525  MythDB::DBError("QueryBasename", query);
2526  }
2527  else if (query.next())
2528  {
2529  return query.value(0).toString();
2530  }
2531  else
2532  {
2533  LOG(VB_GENERAL, LOG_INFO,
2534  QString("QueryBasename found no entry for recording ID %1")
2535  .arg(m_recordedId));
2536  }
2537 
2538  return QString();
2539 }
2540 
2549  bool checkMaster, bool forceCheckLocal)
2550 {
2551  // return the original path if BD or DVD URI
2552  if (IsVideoBD() || IsVideoDVD())
2553  return GetPathname();
2554 
2555  QString basename = QueryBasename();
2556  if (basename.isEmpty())
2557  return "";
2558 
2559  bool checklocal = !gCoreContext->GetBoolSetting("AlwaysStreamFiles", false) ||
2560  forceCheckLocal;
2561 
2562  if (IsVideo())
2563  {
2564  QString fullpath = GetPathname();
2565  if (!fullpath.startsWith("myth://", Qt::CaseInsensitive) || !checklocal)
2566  return fullpath;
2567 
2568  QUrl url = QUrl(fullpath);
2569  QString path = url.path();
2570  QString host = url.toString(QUrl::RemovePath).mid(7);
2571 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
2572  QStringList list = host.split(":", QString::SkipEmptyParts);
2573 #else
2574  QStringList list = host.split(":", Qt::SkipEmptyParts);
2575 #endif
2576  if (!list.empty())
2577  {
2578  host = list[0];
2579 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
2580  list = host.split("@", QString::SkipEmptyParts);
2581 #else
2582  list = host.split("@", Qt::SkipEmptyParts);
2583 #endif
2584  QString group;
2585  if (!list.empty() && list.size() < 3)
2586  {
2587  host = list.size() == 1 ? list[0] : list[1];
2588  group = list.size() == 1 ? QString() : list[0];
2589  StorageGroup sg = StorageGroup(group, host);
2590  QString local = sg.FindFile(path);
2591  if (!local.isEmpty() && sg.FileExists(local))
2592  return local;
2593  }
2594  }
2595  return fullpath;
2596  }
2597 
2598  QString tmpURL;
2599  if (checklocal)
2600  {
2601  // Check to see if the file exists locally
2602  StorageGroup sgroup(m_storageGroup);
2603 #if 0
2604  LOG(VB_FILE, LOG_DEBUG, LOC +
2605  QString("GetPlaybackURL: CHECKING SG : %1 : ").arg(tmpURL));
2606 #endif
2607  tmpURL = sgroup.FindFile(basename);
2608 
2609  if (!tmpURL.isEmpty())
2610  {
2611  LOG(VB_FILE, LOG_INFO, LOC +
2612  QString("GetPlaybackURL: File is local: '%1'") .arg(tmpURL));
2613  return tmpURL;
2614  }
2616  {
2617  LOG(VB_GENERAL, LOG_ERR, LOC +
2618  QString("GetPlaybackURL: '%1' should be local, but it can "
2619  "not be found.").arg(basename));
2620  // Note do not preceed with "/" that will cause existing code
2621  // to look for a local file with this name...
2622  return QString("GetPlaybackURL/UNABLE/TO/FIND/LOCAL/FILE/ON/%1/%2")
2623  .arg(m_hostname, basename);
2624  }
2625  }
2626 
2627  // Check to see if we should stream from the master backend
2628  if ((checkMaster) &&
2629  (gCoreContext->GetBoolSetting("MasterBackendOverride", false)) &&
2630  (RemoteCheckFile(this, false)))
2631  {
2634  basename);
2635 
2636  LOG(VB_FILE, LOG_INFO, LOC +
2637  QString("GetPlaybackURL: Found @ '%1'").arg(tmpURL));
2638  return tmpURL;
2639  }
2640 
2641  // Fallback to streaming from the backend the recording was created on
2644  basename);
2645 
2646  LOG(VB_FILE, LOG_INFO, LOC +
2647  QString("GetPlaybackURL: Using default of: '%1'") .arg(tmpURL));
2648 
2649  return tmpURL;
2650 }
2651 
2655 {
2656  uint ret = 0U;
2657  if (m_chanId)
2658  {
2659  MSqlQuery query(MSqlQuery::InitCon());
2660 
2661  query.prepare("SELECT mplexid FROM channel "
2662  "WHERE chanid = :CHANID");
2663  query.bindValue(":CHANID", m_chanId);
2664 
2665  if (!query.exec())
2666  MythDB::DBError("QueryMplexID", query);
2667  else if (query.next())
2668  ret = query.value(0).toUInt();
2669 
2670  // clear out bogus mplexid's
2671  ret = (32767 == ret) ? 0 : ret;
2672  }
2673 
2674  return ret;
2675 }
2676 
2679 void ProgramInfo::SaveBookmark(uint64_t frame)
2680 {
2682 
2683  bool is_valid = (frame > 0);
2684  if (is_valid)
2685  {
2686  frm_dir_map_t bookmarkmap;
2687  bookmarkmap[frame] = MARK_BOOKMARK;
2688  SaveMarkupMap(bookmarkmap);
2689  }
2690 
2691  set_flag(m_programFlags, FL_BOOKMARK, is_valid);
2692 
2693  UpdateMarkTimeStamp(is_valid);
2694  SendUpdateEvent();
2695 }
2696 
2697 void ProgramInfo::UpdateMarkTimeStamp(bool bookmarked) const
2698 {
2699  if (IsRecording())
2700  {
2701  MSqlQuery query(MSqlQuery::InitCon());
2702  query.prepare(
2703  "UPDATE recorded "
2704  "SET bookmarkupdate = CURRENT_TIMESTAMP, "
2705  " bookmark = :BOOKMARKFLAG "
2706  "WHERE recordedid = :RECORDEDID");
2707 
2708  query.bindValue(":BOOKMARKFLAG", bookmarked);
2709  query.bindValue(":RECORDEDID", m_recordedId);
2710 
2711  if (!query.exec())
2712  MythDB::DBError("bookmark flag update", query);
2713  }
2714 }
2715 
2716 void ProgramInfo::SaveLastPlayPos(uint64_t frame)
2717 {
2719 
2720  bool isValid = frame > 0;
2721  if (isValid)
2722  {
2723  frm_dir_map_t lastPlayPosMap;
2724  lastPlayPosMap[frame] = MARK_UTIL_LASTPLAYPOS;
2725  SaveMarkupMap(lastPlayPosMap, MARK_UTIL_LASTPLAYPOS);
2726  }
2727 
2728  set_flag(m_programFlags, FL_LASTPLAYPOS, isValid);
2729 
2730  UpdateLastPlayTimeStamp(isValid);
2731  SendUpdateEvent();
2732 }
2733 
2734 // This function overloads the 'bookmarkupdate' field to force the UI
2735 // to update when the last play timestamp is updated. The alternative
2736 // is adding another field to the database and to the programinfo
2737 // serialization.
2738 void ProgramInfo::UpdateLastPlayTimeStamp(bool hasLastPlay) const
2739 {
2740  if (IsRecording())
2741  {
2742  MSqlQuery query(MSqlQuery::InitCon());
2743  query.prepare(
2744  "UPDATE recorded "
2745  "SET bookmarkupdate = CURRENT_TIMESTAMP, "
2746  " lastplay = :LASTPLAYFLAG "
2747  "WHERE recordedid = :RECORDEDID");
2748 
2749  query.bindValue(":LASTPLAYFLAG", hasLastPlay);
2750  query.bindValue(":RECORDEDID", m_recordedId);
2751 
2752  if (!query.exec())
2753  MythDB::DBError("lastplay flag update", query);
2754  }
2755 }
2756 
2758 {
2759  if (IsRecording())
2761 }
2762 
2764 {
2766 }
2767 
2769 {
2771 }
2772 
2777 {
2778  MSqlQuery query(MSqlQuery::InitCon());
2779  query.prepare(
2780  "SELECT bookmarkupdate "
2781  "FROM recorded "
2782  "WHERE chanid = :CHANID AND"
2783  " starttime = :STARTTIME");
2784  query.bindValue(":CHANID", m_chanId);
2785  query.bindValue(":STARTTIME", m_recStartTs);
2786 
2787  QDateTime ts;
2788 
2789  if (!query.exec())
2790  MythDB::DBError("ProgramInfo::GetBookmarkTimeStamp()", query);
2791  else if (query.next())
2792  ts = MythDate::as_utc(query.value(0).toDateTime());
2793 
2794  return ts;
2795 }
2796 
2803 uint64_t ProgramInfo::QueryBookmark(void) const
2804 {
2805  if (m_programFlags & FL_IGNOREBOOKMARK)
2806  return 0;
2807 
2808  frm_dir_map_t bookmarkmap;
2809  QueryMarkupMap(bookmarkmap, MARK_BOOKMARK);
2810 
2811  return (bookmarkmap.isEmpty()) ? 0 : bookmarkmap.begin().key();
2812 }
2813 
2814 uint64_t ProgramInfo::QueryBookmark(uint chanid, const QDateTime &recstartts)
2815 {
2816  frm_dir_map_t bookmarkmap;
2818  chanid, recstartts,
2819  bookmarkmap, MARK_BOOKMARK);
2820 
2821  return (bookmarkmap.isEmpty()) ? 0 : bookmarkmap.begin().key();
2822 }
2823 
2831 {
2832  if (m_programFlags & FL_IGNORELASTPLAYPOS)
2833  return 0;
2834 
2835  frm_dir_map_t bookmarkmap;
2836  QueryMarkupMap(bookmarkmap, MARK_UTIL_LASTPLAYPOS);
2837 
2838  return (bookmarkmap.isEmpty()) ? 0 : bookmarkmap.begin().key();
2839 }
2840 
2847 uint64_t ProgramInfo::QueryProgStart(void) const
2848 {
2849  if (m_programFlags & FL_IGNOREPROGSTART)
2850  return 0;
2851 
2852  frm_dir_map_t bookmarkmap;
2853  QueryMarkupMap(bookmarkmap, MARK_UTIL_PROGSTART);
2854 
2855  return (bookmarkmap.isEmpty()) ? 0 : bookmarkmap.begin().key();
2856 }
2857 
2858 uint64_t ProgramInfo::QueryStartMark(void) const
2859 {
2860  uint64_t start = 0;
2861  if ((start = QueryLastPlayPos()) > 0)
2862  {
2863  LOG(VB_PLAYBACK, LOG_INFO, QString("Using last position @ %1").arg(start));
2864  }
2865  else if ((start = QueryBookmark()) > 0)
2866  {
2867  LOG(VB_PLAYBACK, LOG_INFO, QString("Using bookmark @ %1").arg(start));
2868  }
2869  else if (HasCutlist())
2870  {
2871  // Disable progstart if the program has a cutlist.
2872  LOG(VB_PLAYBACK, LOG_INFO, "Ignoring progstart as cutlist exists");
2873  }
2874  else if ((start = QueryProgStart()) > 0)
2875  {
2876  LOG(VB_PLAYBACK, LOG_INFO, QString("Using progstart @ %1").arg(start));
2877  }
2878  else
2879  {
2880  LOG(VB_PLAYBACK, LOG_INFO, "Using file start");
2881  }
2882  return start;
2883 }
2884 
2891  const QString &serialid) const
2892 {
2893  QStringList fields = QStringList();
2894  MSqlQuery query(MSqlQuery::InitCon());
2895 
2896  if (!(m_programFlags & FL_IGNOREBOOKMARK))
2897  {
2898  query.prepare(" SELECT dvdstate, title, framenum, audionum, subtitlenum "
2899  " FROM dvdbookmark "
2900  " WHERE serialid = :SERIALID ");
2901  query.bindValue(":SERIALID", serialid);
2902 
2903  if (query.exec() && query.next())
2904  {
2905  QString dvdstate = query.value(0).toString();
2906 
2907  if (!dvdstate.isEmpty())
2908  {
2909  fields.append(dvdstate);
2910  }
2911  else
2912  {
2913  // Legacy bookmark
2914  for(int i = 1; i < 5; i++)
2915  fields.append(query.value(i).toString());
2916  }
2917  }
2918  }
2919 
2920  return fields;
2921 }
2922 
2923 void ProgramInfo::SaveDVDBookmark(const QStringList &fields)
2924 {
2925  QStringList::const_iterator it = fields.begin();
2926  MSqlQuery query(MSqlQuery::InitCon());
2927 
2928  QString serialid = *(it);
2929  QString name = *(++it);
2930 
2931  if( fields.count() == 3 )
2932  {
2933  // We have a state field, so update/create the bookmark
2934  QString state = *(++it);
2935 
2936  query.prepare("INSERT IGNORE INTO dvdbookmark "
2937  " (serialid, name)"
2938  " VALUES ( :SERIALID, :NAME );");
2939  query.bindValue(":SERIALID", serialid);
2940  query.bindValue(":NAME", name);
2941 
2942  if (!query.exec())
2943  MythDB::DBError("SetDVDBookmark inserting", query);
2944 
2945  query.prepare(" UPDATE dvdbookmark "
2946  " SET dvdstate = :STATE , "
2947  " timestamp = NOW() "
2948  " WHERE serialid = :SERIALID");
2949  query.bindValue(":STATE",state);
2950  query.bindValue(":SERIALID",serialid);
2951  }
2952  else
2953  {
2954  // No state field, delete the bookmark
2955  query.prepare("DELETE FROM dvdbookmark "
2956  "WHERE serialid = :SERIALID");
2957  query.bindValue(":SERIALID",serialid);
2958  }
2959 
2960  if (!query.exec())
2961  MythDB::DBError("SetDVDBookmark updating", query);
2962 }
2963 
2967 QStringList ProgramInfo::QueryBDBookmark(const QString &serialid) const
2968 {
2969  QStringList fields = QStringList();
2970  MSqlQuery query(MSqlQuery::InitCon());
2971 
2972  if (!(m_programFlags & FL_IGNOREBOOKMARK))
2973  {
2974  query.prepare(" SELECT bdstate FROM bdbookmark "
2975  " WHERE serialid = :SERIALID ");
2976  query.bindValue(":SERIALID", serialid);
2977 
2978  if (query.exec() && query.next())
2979  fields.append(query.value(0).toString());
2980  }
2981 
2982  return fields;
2983 }
2984 
2985 void ProgramInfo::SaveBDBookmark(const QStringList &fields)
2986 {
2987  QStringList::const_iterator it = fields.begin();
2988  MSqlQuery query(MSqlQuery::InitCon());
2989 
2990  QString serialid = *(it);
2991  QString name = *(++it);
2992 
2993  if( fields.count() == 3 )
2994  {
2995  // We have a state field, so update/create the bookmark
2996  QString state = *(++it);
2997 
2998  query.prepare("INSERT IGNORE INTO bdbookmark "
2999  " (serialid, name)"
3000  " VALUES ( :SERIALID, :NAME );");
3001  query.bindValue(":SERIALID", serialid);
3002  query.bindValue(":NAME", name);
3003 
3004  if (!query.exec())
3005  MythDB::DBError("SetBDBookmark inserting", query);
3006 
3007  query.prepare(" UPDATE bdbookmark "
3008  " SET bdstate = :STATE , "
3009  " timestamp = NOW() "
3010  " WHERE serialid = :SERIALID");
3011  query.bindValue(":STATE",state);
3012  query.bindValue(":SERIALID",serialid);
3013  }
3014  else
3015  {
3016  // No state field, delete the bookmark
3017  query.prepare("DELETE FROM bdbookmark "
3018  "WHERE serialid = :SERIALID");
3019  query.bindValue(":SERIALID",serialid);
3020  }
3021 
3022  if (!query.exec())
3023  MythDB::DBError("SetBDBookmark updating", query);
3024 }
3025 
3032 {
3034 
3035  MSqlQuery query(MSqlQuery::InitCon());
3036 
3037  query.prepare(" SELECT category_type "
3038  " FROM recordedprogram "
3039  " WHERE chanid = :CHANID "
3040  " AND starttime = :STARTTIME;");
3041 
3042  query.bindValue(":CHANID", m_chanId);
3043  query.bindValue(":STARTTIME", m_startTs);
3044 
3045  if (query.exec() && query.next())
3046  {
3047  ret = string_to_myth_category_type(query.value(0).toString());
3048  }
3049 
3050  return ret;
3051 }
3052 
3054 void ProgramInfo::SaveWatched(bool watched)
3055 {
3056  if (IsRecording())
3057  {
3058  MSqlQuery query(MSqlQuery::InitCon());
3059 
3060  query.prepare("UPDATE recorded"
3061  " SET watched = :WATCHEDFLAG"
3062  " WHERE chanid = :CHANID"
3063  " AND starttime = :STARTTIME ;");
3064  query.bindValue(":CHANID", m_chanId);
3065  query.bindValue(":STARTTIME", m_recStartTs);
3066  query.bindValue(":WATCHEDFLAG", watched);
3067 
3068  if (!query.exec())
3069  MythDB::DBError("Set watched flag", query);
3070  else
3071  UpdateLastDelete(watched);
3072 
3073  SendUpdateEvent();
3074  }
3075  else if (IsVideoFile())
3076  {
3077  QString url = m_pathname;
3078  if (url.startsWith("myth://"))
3079  {
3080  url = QUrl(url).path();
3081  url.remove(0,1);
3082  }
3083 
3084  MSqlQuery query(MSqlQuery::InitCon());
3085  query.prepare("UPDATE videometadata"
3086  " SET watched = :WATCHEDFLAG"
3087  " WHERE title = :TITLE"
3088  " AND subtitle = :SUBTITLE"
3089  " AND filename = :FILENAME ;");
3090  query.bindValue(":TITLE", m_title);
3091  query.bindValue(":SUBTITLE", m_subtitle);
3092  query.bindValue(":FILENAME", url);
3093  query.bindValue(":WATCHEDFLAG", watched);
3094 
3095  if (!query.exec())
3096  MythDB::DBError("Set watched flag", query);
3097  }
3098 
3099  set_flag(m_programFlags, FL_WATCHED, watched);
3100 }
3101 
3107 {
3108  bool editing = (m_programFlags & FL_REALLYEDITING) != 0U;
3109 
3110  MSqlQuery query(MSqlQuery::InitCon());
3111 
3112  query.prepare("SELECT editing FROM recorded"
3113  " WHERE chanid = :CHANID"
3114  " AND starttime = :STARTTIME ;");
3115  query.bindValue(":CHANID", m_chanId);
3116  query.bindValue(":STARTTIME", m_recStartTs);
3117 
3118  if (query.exec() && query.next())
3119  editing = query.value(0).toBool();
3120 
3121  /*
3122  set_flag(programflags, FL_REALLYEDITING, editing);
3123  set_flag(programflags, FL_EDITING, ((programflags & FL_REALLYEDITING) ||
3124  (programflags & COMM_FLAG_PROCESSING)));
3125  */
3126  return editing;
3127 }
3128 
3133 {
3134  MSqlQuery query(MSqlQuery::InitCon());
3135 
3136  query.prepare("UPDATE recorded"
3137  " SET editing = :EDIT"
3138  " WHERE chanid = :CHANID"
3139  " AND starttime = :STARTTIME ;");
3140  query.bindValue(":EDIT", edit);
3141  query.bindValue(":CHANID", m_chanId);
3142  query.bindValue(":STARTTIME", m_recStartTs);
3143 
3144  if (!query.exec())
3145  MythDB::DBError("Edit status update", query);
3146 
3147  set_flag(m_programFlags, FL_REALLYEDITING, edit);
3148  set_flag(m_programFlags, FL_EDITING, (((m_programFlags & FL_REALLYEDITING) != 0U) ||
3149  ((m_programFlags & COMM_FLAG_PROCESSING) != 0U)));
3150 
3151  SendUpdateEvent();
3152 }
3153 
3158 {
3159  MSqlQuery query(MSqlQuery::InitCon());
3160 
3161  query.prepare("UPDATE recorded"
3162  " SET deletepending = :DELETEFLAG, "
3163  " duplicate = 0 "
3164  " WHERE chanid = :CHANID"
3165  " AND starttime = :STARTTIME ;");
3166  query.bindValue(":CHANID", m_chanId);
3167  query.bindValue(":STARTTIME", m_recStartTs);
3168  query.bindValue(":DELETEFLAG", deleteFlag);
3169 
3170  if (!query.exec())
3171  MythDB::DBError("SaveDeletePendingFlag", query);
3172 
3173  set_flag(m_programFlags, FL_DELETEPENDING, deleteFlag);
3174 
3175  if (!deleteFlag)
3176  SendAddedEvent();
3177 
3178  SendUpdateEvent();
3179 }
3180 
3185 bool ProgramInfo::QueryIsInUse(QStringList &byWho) const
3186 {
3187  if (!IsRecording())
3188  return false;
3189 
3190  QDateTime oneHourAgo = MythDate::current().addSecs(-61 * 60);
3191  MSqlQuery query(MSqlQuery::InitCon());
3192 
3193  query.prepare("SELECT hostname, recusage FROM inuseprograms "
3194  " WHERE chanid = :CHANID"
3195  " AND starttime = :STARTTIME "
3196  " AND lastupdatetime > :ONEHOURAGO ;");
3197  query.bindValue(":CHANID", m_chanId);
3198  query.bindValue(":STARTTIME", m_recStartTs);
3199  query.bindValue(":ONEHOURAGO", oneHourAgo);
3200 
3201  byWho.clear();
3202  if (query.exec() && query.size() > 0)
3203  {
3204  QString usageStr;
3205  QString recusage;
3206  while (query.next())
3207  {
3208  usageStr = QObject::tr("Unknown");
3209  recusage = query.value(1).toString();
3210 
3211  if (recusage == kPlayerInUseID)
3212  usageStr = QObject::tr("Playing");
3213  else if (recusage == kPIPPlayerInUseID)
3214  usageStr = QObject::tr("PIP");
3215  else if (recusage == kPBPPlayerInUseID)
3216  usageStr = QObject::tr("PBP");
3217  else if ((recusage == kRecorderInUseID) ||
3218  (recusage == kImportRecorderInUseID))
3219  usageStr = QObject::tr("Recording");
3220  else if (recusage == kFileTransferInUseID)
3221  usageStr = QObject::tr("File transfer");
3222  else if (recusage == kTruncatingDeleteInUseID)
3223  usageStr = QObject::tr("Delete");
3224  else if (recusage == kFlaggerInUseID)
3225  usageStr = QObject::tr("Commercial Detection");
3226  else if (recusage == kTranscoderInUseID)
3227  usageStr = QObject::tr("Transcoding");
3228  else if (recusage == kPreviewGeneratorInUseID)
3229  usageStr = QObject::tr("Preview Generation");
3230  else if (recusage == kJobQueueInUseID)
3231  usageStr = QObject::tr("User Job");
3232 
3233  byWho.push_back(recusage);
3234  byWho.push_back(query.value(0).toString());
3235  byWho.push_back(query.value(0).toString() + " (" + usageStr + ")");
3236  }
3237 
3238  return true;
3239  }
3240 
3241  return false;
3242 }
3243 
3248 bool ProgramInfo::QueryIsInUse(QString &byWho) const
3249 {
3250  QStringList users;
3251  bool inuse = QueryIsInUse(users);
3252  byWho.clear();
3253  for (int i = 0; i+2 < users.size(); i+=3)
3254  byWho += users[i+2] + "\n";
3255  return inuse;
3256 }
3257 
3258 
3265 bool ProgramInfo::QueryIsDeleteCandidate(bool one_playback_allowed) const
3266 {
3267  if (!IsRecording())
3268  return false;
3269 
3270  // gCoreContext->GetNumSetting("AutoExpireInsteadOfDelete", 0) &&
3271  if (GetRecordingGroup() != "Deleted" && GetRecordingGroup() != "LiveTV")
3272  return true;
3273 
3274  bool ok = true;
3275  QStringList byWho;
3276  if (QueryIsInUse(byWho) && !byWho.isEmpty())
3277  {
3278  uint play_cnt = 0;
3279  uint ft_cnt = 0;
3280  uint jq_cnt = 0;
3281  for (uint i = 0; (i+2 < (uint)byWho.size()) && ok; i+=3)
3282  {
3283  play_cnt += byWho[i].contains(kPlayerInUseID) ? 1 : 0;
3284  ft_cnt += (byWho[i].contains(kFlaggerInUseID) ||
3285  byWho[i].contains(kTranscoderInUseID)) ? 1 : 0;
3286  jq_cnt += (byWho[i].contains(kJobQueueInUseID)) ? 1 : 0;
3287  ok = ok && (byWho[i].contains(kRecorderInUseID) ||
3288  byWho[i].contains(kFlaggerInUseID) ||
3289  byWho[i].contains(kTranscoderInUseID) ||
3290  byWho[i].contains(kJobQueueInUseID) ||
3291  (one_playback_allowed && (play_cnt <= 1)));
3292  }
3293  ok = ok && (ft_cnt == jq_cnt);
3294  }
3295 
3296  return ok;
3297 }
3298 
3301 {
3302  MSqlQuery query(MSqlQuery::InitCon());
3303 
3304  query.prepare("SELECT transcoded FROM recorded"
3305  " WHERE chanid = :CHANID"
3306  " AND starttime = :STARTTIME ;");
3307  query.bindValue(":CHANID", m_chanId);
3308  query.bindValue(":STARTTIME", m_recStartTs);
3309 
3310  if (query.exec() && query.next())
3311  return (TranscodingStatus) query.value(0).toUInt();
3313 }
3314 
3321 {
3322  MSqlQuery query(MSqlQuery::InitCon());
3323 
3324  query.prepare(
3325  "UPDATE recorded "
3326  "SET transcoded = :VALUE "
3327  "WHERE chanid = :CHANID AND"
3328  " starttime = :STARTTIME");
3329  query.bindValue(":VALUE", (uint)trans);
3330  query.bindValue(":CHANID", m_chanId);
3331  query.bindValue(":STARTTIME", m_recStartTs);
3332 
3333  if (!query.exec())
3334  MythDB::DBError("Transcoded status update", query);
3335 
3336  set_flag(m_programFlags, FL_TRANSCODED, TRANSCODING_COMPLETE == trans);
3337  SendUpdateEvent();
3338 }
3339 
3344 {
3345  MSqlQuery query(MSqlQuery::InitCon());
3346 
3347  query.prepare("UPDATE recorded"
3348  " SET commflagged = :FLAG"
3349  " WHERE chanid = :CHANID"
3350  " AND starttime = :STARTTIME ;");
3351  query.bindValue(":FLAG", (int)flag);
3352  query.bindValue(":CHANID", m_chanId);
3353  query.bindValue(":STARTTIME", m_recStartTs);
3354 
3355  if (!query.exec())
3356  MythDB::DBError("Commercial Flagged status update", query);
3357 
3358  set_flag(m_programFlags, FL_COMMFLAG, COMM_FLAG_DONE == flag);
3359  set_flag(m_programFlags, FL_COMMPROCESSING, COMM_FLAG_PROCESSING == flag);
3360  set_flag(m_programFlags, FL_EDITING, (((m_programFlags & FL_REALLYEDITING) != 0U) ||
3361  ((m_programFlags & COMM_FLAG_PROCESSING) != 0U)));
3362  SendUpdateEvent();
3363 }
3364 
3365 
3369 void ProgramInfo::SavePreserve(bool preserveEpisode)
3370 {
3371  MSqlQuery query(MSqlQuery::InitCon());
3372 
3373  query.prepare("UPDATE recorded"
3374  " SET preserve = :PRESERVE"
3375  " WHERE chanid = :CHANID"
3376  " AND starttime = :STARTTIME ;");
3377  query.bindValue(":PRESERVE", preserveEpisode);
3378  query.bindValue(":CHANID", m_chanId);
3379  query.bindValue(":STARTTIME", m_recStartTs);
3380 
3381  if (!query.exec())
3382  MythDB::DBError("PreserveEpisode update", query);
3383  else
3384  UpdateLastDelete(false);
3385 
3386  set_flag(m_programFlags, FL_PRESERVED, preserveEpisode);
3387 
3388  SendUpdateEvent();
3389 }
3390 
3396 void ProgramInfo::SaveAutoExpire(AutoExpireType autoExpire, bool updateDelete)
3397 {
3398  MSqlQuery query(MSqlQuery::InitCon());
3399 
3400  query.prepare("UPDATE recorded"
3401  " SET autoexpire = :AUTOEXPIRE"
3402  " WHERE chanid = :CHANID"
3403  " AND starttime = :STARTTIME ;");
3404  query.bindValue(":AUTOEXPIRE", (uint)autoExpire);
3405  query.bindValue(":CHANID", m_chanId);
3406  query.bindValue(":STARTTIME", m_recStartTs);
3407 
3408  if (!query.exec())
3409  MythDB::DBError("AutoExpire update", query);
3410  else if (updateDelete)
3411  UpdateLastDelete(true);
3412 
3413  set_flag(m_programFlags, FL_AUTOEXP, autoExpire != kDisableAutoExpire);
3414 
3415  SendUpdateEvent();
3416 }
3417 
3422 void ProgramInfo::UpdateLastDelete(bool setTime) const
3423 {
3424  MSqlQuery query(MSqlQuery::InitCon());
3425 
3426  if (setTime)
3427  {
3428  QDateTime timeNow = MythDate::current();
3429  auto delay_secs = std::chrono::seconds(m_recStartTs.secsTo(timeNow));
3430  auto delay = duration_cast<std::chrono::hours>(delay_secs);
3431  delay = std::clamp(delay, 1h, 200h);
3432 
3433  query.prepare("UPDATE record SET last_delete = :TIME, "
3434  "avg_delay = (avg_delay * 3 + :DELAY) / 4 "
3435  "WHERE recordid = :RECORDID");
3436  query.bindValue(":TIME", timeNow);
3437  query.bindValue(":DELAY", static_cast<qint64>(delay.count()));
3438  }
3439  else
3440  {
3441  query.prepare("UPDATE record SET last_delete = NULL "
3442  "WHERE recordid = :RECORDID");
3443  }
3444  query.bindValue(":RECORDID", m_recordId);
3445 
3446  if (!query.exec())
3447  MythDB::DBError("Update last_delete", query);
3448 }
3449 
3452 {
3453  MSqlQuery query(MSqlQuery::InitCon());
3454 
3455  query.prepare("SELECT autoexpire FROM recorded"
3456  " WHERE chanid = :CHANID"
3457  " AND starttime = :STARTTIME ;");
3458  query.bindValue(":CHANID", m_chanId);
3459  query.bindValue(":STARTTIME", m_recStartTs);
3460 
3461  if (query.exec() && query.next())
3462  return (AutoExpireType) query.value(0).toInt();
3463 
3464  return kDisableAutoExpire;
3465 }
3466 
3467 bool ProgramInfo::QueryCutList(frm_dir_map_t &delMap, bool loadAutoSave) const
3468 {
3469  if (loadAutoSave)
3470  {
3471  frm_dir_map_t autosaveMap;
3472  QueryMarkupMap(autosaveMap, MARK_TMP_CUT_START);
3473  QueryMarkupMap(autosaveMap, MARK_TMP_CUT_END, true);
3474  QueryMarkupMap(autosaveMap, MARK_PLACEHOLDER, true);
3475  // Convert the temporary marks into regular marks.
3476  delMap.clear();
3477  // NOLINTNEXTLINE(modernize-loop-convert)
3478  for (auto i = autosaveMap.constBegin(); i != autosaveMap.constEnd(); ++i)
3479  {
3480  uint64_t frame = i.key();
3481  MarkTypes mark = i.value();
3482  if (mark == MARK_TMP_CUT_START)
3483  mark = MARK_CUT_START;
3484  else if (mark == MARK_TMP_CUT_END)
3485  mark = MARK_CUT_END;
3486  delMap[frame] = mark;
3487  }
3488  }
3489  else
3490  {
3491  QueryMarkupMap(delMap, MARK_CUT_START);
3492  QueryMarkupMap(delMap, MARK_CUT_END, true);
3493  QueryMarkupMap(delMap, MARK_PLACEHOLDER, true);
3494  }
3495 
3496  return !delMap.isEmpty();
3497 }
3498 
3499 void ProgramInfo::SaveCutList(frm_dir_map_t &delMap, bool isAutoSave) const
3500 {
3501  if (!isAutoSave)
3502  {
3505  }
3509 
3510  frm_dir_map_t tmpDelMap;
3511  // NOLINTNEXTLINE(modernize-loop-convert)
3512  for (auto i = delMap.constBegin(); i != delMap.constEnd(); ++i)
3513  {
3514  uint64_t frame = i.key();
3515  MarkTypes mark = i.value();
3516  if (isAutoSave)
3517  {
3518  if (mark == MARK_CUT_START)
3520  else if (mark == MARK_CUT_END)
3522  }
3523  tmpDelMap[frame] = mark;
3524  }
3525  SaveMarkupMap(tmpDelMap);
3526 
3527  if (IsRecording())
3528  {
3529  MSqlQuery query(MSqlQuery::InitCon());
3530 
3531  // Flag the existence of a cutlist
3532  query.prepare("UPDATE recorded"
3533  " SET cutlist = :CUTLIST"
3534  " WHERE chanid = :CHANID"
3535  " AND starttime = :STARTTIME ;");
3536 
3537  query.bindValue(":CUTLIST", delMap.isEmpty() ? 0 : 1);
3538  query.bindValue(":CHANID", m_chanId);
3539  query.bindValue(":STARTTIME", m_recStartTs);
3540 
3541  if (!query.exec())
3542  MythDB::DBError("cutlist flag update", query);
3543  }
3544 }
3545 
3547 {
3550  SaveMarkupMap(frames);
3551 }
3552 
3554 {
3556  QueryMarkupMap(frames, MARK_COMM_END, true);
3557 }
3558 
3560  MarkTypes type, int64_t min_frame, int64_t max_frame) const
3561 {
3562  MSqlQuery query(MSqlQuery::InitCon());
3563  QString comp;
3564 
3565  if (min_frame >= 0)
3566  comp += QString(" AND mark >= %1 ").arg(min_frame);
3567 
3568  if (max_frame >= 0)
3569  comp += QString(" AND mark <= %1 ").arg(max_frame);
3570 
3571  if (type != MARK_ALL)
3572  comp += QString(" AND type = :TYPE ");
3573 
3574  if (IsVideo())
3575  {
3576  query.prepare("DELETE FROM filemarkup"
3577  " WHERE filename = :PATH "
3578  + comp + ";");
3580  }
3581  else if (IsRecording())
3582  {
3583  query.prepare("DELETE FROM recordedmarkup"
3584  " WHERE chanid = :CHANID"
3585  " AND STARTTIME = :STARTTIME"
3586  + comp + ';');
3587  query.bindValue(":CHANID", m_chanId);
3588  query.bindValue(":STARTTIME", m_recStartTs);
3589  }
3590  else
3591  {
3592  return;
3593  }
3594  query.bindValue(":TYPE", type);
3595 
3596  if (!query.exec())
3597  MythDB::DBError("ClearMarkupMap deleting", query);
3598 }
3599 
3602  int64_t min_frame, int64_t max_frame) const
3603 {
3604  MSqlQuery query(MSqlQuery::InitCon());
3605  QString videoPath;
3606 
3607  if (IsVideo())
3608  {
3610  }
3611  else if (IsRecording())
3612  {
3613  // check to make sure the show still exists before saving markups
3614  query.prepare("SELECT starttime FROM recorded"
3615  " WHERE chanid = :CHANID"
3616  " AND starttime = :STARTTIME ;");
3617  query.bindValue(":CHANID", m_chanId);
3618  query.bindValue(":STARTTIME", m_recStartTs);
3619 
3620  if (!query.exec())
3621  MythDB::DBError("SaveMarkupMap checking record table", query);
3622 
3623  if (!query.next())
3624  return;
3625  }
3626  else
3627  {
3628  return;
3629  }
3630 
3631  frm_dir_map_t::const_iterator it;
3632  for (it = marks.begin(); it != marks.end(); ++it)
3633  {
3634  uint64_t frame = it.key();
3635 
3636  if ((min_frame >= 0) && (frame < (uint64_t)min_frame))
3637  continue;
3638 
3639  if ((max_frame >= 0) && (frame > (uint64_t)max_frame))
3640  continue;
3641 
3642  int mark_type = (type != MARK_ALL) ? type : *it;
3643 
3644  if (IsVideo())
3645  {
3646  query.prepare("INSERT INTO filemarkup (filename, mark, type)"
3647  " VALUES ( :PATH , :MARK , :TYPE );");
3648  query.bindValue(":PATH", videoPath);
3649  }
3650  else // if (IsRecording())
3651  {
3652  query.prepare("INSERT INTO recordedmarkup"
3653  " (chanid, starttime, mark, type)"
3654  " VALUES ( :CHANID , :STARTTIME , :MARK , :TYPE );");
3655  query.bindValue(":CHANID", m_chanId);
3656  query.bindValue(":STARTTIME", m_recStartTs);
3657  }
3658  query.bindValue(":MARK", (quint64)frame);
3659  query.bindValue(":TYPE", mark_type);
3660 
3661  if (!query.exec())
3662  MythDB::DBError("SaveMarkupMap inserting", query);
3663  }
3664 }
3665 
3667  frm_dir_map_t &marks, MarkTypes type, bool merge) const
3668 {
3669  if (!merge)
3670  marks.clear();
3671 
3672  if (IsVideo())
3673  {
3675  marks, type, merge);
3676  }
3677  else if (IsRecording())
3678  {
3680  }
3681 }
3682 
3684  const QString &video_pathname,
3686  MarkTypes type, bool mergeIntoMap)
3687 {
3688  if (!mergeIntoMap)
3689  marks.clear();
3690 
3691  MSqlQuery query(MSqlQuery::InitCon());
3692 
3693  query.prepare("SELECT mark, type "
3694  "FROM filemarkup "
3695  "WHERE filename = :PATH AND "
3696  " type = :TYPE "
3697  "ORDER BY mark");
3698  query.bindValue(":PATH", video_pathname);
3699  query.bindValue(":TYPE", type);
3700 
3701  if (!query.exec())
3702  {
3703  MythDB::DBError("QueryMarkupMap", query);
3704  return;
3705  }
3706 
3707  while (query.next())
3708  {
3709  marks[query.value(0).toLongLong()] =
3710  (MarkTypes) query.value(1).toInt();
3711  }
3712 }
3713 
3715  uint chanid, const QDateTime &recstartts,
3717  MarkTypes type, bool mergeIntoMap)
3718 {
3719  if (!mergeIntoMap)
3720  marks.clear();
3721 
3722  MSqlQuery query(MSqlQuery::InitCon());
3723  query.prepare("SELECT mark, type "
3724  "FROM recordedmarkup "
3725  "WHERE chanid = :CHANID AND "
3726  " starttime = :STARTTIME AND"
3727  " type = :TYPE "
3728  "ORDER BY mark");
3729  query.bindValue(":CHANID", chanid);
3730  query.bindValue(":STARTTIME", recstartts);
3731  query.bindValue(":TYPE", type);
3732 
3733  if (!query.exec())
3734  {
3735  MythDB::DBError("QueryMarkupMap", query);
3736  return;
3737  }
3738 
3739  while (query.next())
3740  {
3741  marks[query.value(0).toULongLong()] =
3742  (MarkTypes) query.value(1).toInt();
3743  }
3744 }
3745 
3748 {
3749  frm_dir_map_t flagMap;
3750 
3751  QueryMarkupMap(flagMap, type);
3752 
3753  return flagMap.contains(0);
3754 }
3755 
3758 {
3760  frm_dir_map_t flagMap;
3761  flagMap[0] = type;
3762  SaveMarkupMap(flagMap, type);
3763 }
3764 
3766  frm_pos_map_t &posMap, MarkTypes type) const
3767 {
3769  {
3770  QMutexLocker locker(m_positionMapDBReplacement->lock);
3771  posMap = m_positionMapDBReplacement->map[type];
3772 
3773  return;
3774  }
3775 
3776  posMap.clear();
3777  MSqlQuery query(MSqlQuery::InitCon());
3778 
3779  if (IsVideo())
3780  {
3781  query.prepare("SELECT mark, `offset` FROM filemarkup"
3782  " WHERE filename = :PATH"
3783  " AND type = :TYPE ;");
3785  }
3786  else if (IsRecording())
3787  {
3788  query.prepare("SELECT mark, `offset` FROM recordedseek"
3789  " WHERE chanid = :CHANID"
3790  " AND starttime = :STARTTIME"
3791  " AND type = :TYPE ;");
3792  query.bindValue(":CHANID", m_chanId);
3793  query.bindValue(":STARTTIME", m_recStartTs);
3794  }
3795  else
3796  {
3797  return;
3798  }
3799  query.bindValue(":TYPE", type);
3800 
3801  if (!query.exec())
3802  {
3803  MythDB::DBError("QueryPositionMap", query);
3804  return;
3805  }
3806 
3807  while (query.next())
3808  posMap[query.value(0).toULongLong()] = query.value(1).toULongLong();
3809 }
3810 
3812 {
3814  {
3815  QMutexLocker locker(m_positionMapDBReplacement->lock);
3817  return;
3818  }
3819 
3820  MSqlQuery query(MSqlQuery::InitCon());
3821 
3822  if (IsVideo())
3823  {
3824  query.prepare("DELETE FROM filemarkup"
3825  " WHERE filename = :PATH"
3826  " AND type = :TYPE ;");
3828  }
3829  else if (IsRecording())
3830  {
3831  query.prepare("DELETE FROM recordedseek"
3832  " WHERE chanid = :CHANID"
3833  " AND starttime = :STARTTIME"
3834  " AND type = :TYPE ;");
3835  query.bindValue(":CHANID", m_chanId);
3836  query.bindValue(":STARTTIME", m_recStartTs);
3837  }
3838  else
3839  {
3840  return;
3841  }
3842 
3843  query.bindValue(":TYPE", type);
3844 
3845  if (!query.exec())
3846  MythDB::DBError("clear position map", query);
3847 }
3848 
3850  frm_pos_map_t &posMap, MarkTypes type,
3851  int64_t min_frame, int64_t max_frame) const
3852 {
3854  {
3855  QMutexLocker locker(m_positionMapDBReplacement->lock);
3856 
3857  if ((min_frame >= 0) || (max_frame >= 0))
3858  {
3859  frm_pos_map_t new_map;
3860  // NOLINTNEXTLINE(modernize-loop-convert)
3861  for (auto it = m_positionMapDBReplacement->map[type].begin();
3862  it != m_positionMapDBReplacement->map[type].end();
3863  ++it)
3864  {
3865  uint64_t frame = it.key();
3866  if ((min_frame >= 0) && (frame >= (uint64_t)min_frame))
3867  continue;
3868  if ((min_frame >= 0) && (frame <= (uint64_t)max_frame))
3869  continue;
3870  new_map.insert(it.key(), *it);
3871  }
3872  m_positionMapDBReplacement->map[type] = new_map;
3873  }
3874  else
3875  {
3877  }
3878 
3879  // NOLINTNEXTLINE(modernize-loop-convert)
3880  for (auto it = posMap.cbegin(); it != posMap.cend(); ++it)
3881  {
3882  uint64_t frame = it.key();
3883  if ((min_frame >= 0) && (frame >= (uint64_t)min_frame))
3884  continue;
3885  if ((min_frame >= 0) && (frame <= (uint64_t)max_frame))
3886  continue;
3887 
3889  .insert(frame, *it);
3890  }
3891 
3892  return;
3893  }
3894 
3895  MSqlQuery query(MSqlQuery::InitCon());
3896  QString comp;
3897 
3898  if (min_frame >= 0)
3899  comp += " AND mark >= :MIN_FRAME ";
3900  if (max_frame >= 0)
3901  comp += " AND mark <= :MAX_FRAME ";
3902 
3903  QString videoPath;
3904  if (IsVideo())
3905  {
3907 
3908  query.prepare("DELETE FROM filemarkup"
3909  " WHERE filename = :PATH"
3910  " AND type = :TYPE"
3911  + comp + ';');
3912  query.bindValue(":PATH", videoPath);
3913  }
3914  else if (IsRecording())
3915  {
3916  query.prepare("DELETE FROM recordedseek"
3917  " WHERE chanid = :CHANID"
3918  " AND starttime = :STARTTIME"
3919  " AND type = :TYPE"
3920  + comp + ';');
3921  query.bindValue(":CHANID", m_chanId);
3922  query.bindValue(":STARTTIME", m_recStartTs);
3923  }
3924  else
3925  {
3926  return;
3927  }
3928 
3929  query.bindValue(":TYPE", type);
3930  if (min_frame >= 0)
3931  query.bindValue(":MIN_FRAME", (quint64)min_frame);
3932  if (max_frame >= 0)
3933  query.bindValue(":MAX_FRAME", (quint64)max_frame);
3934 
3935  if (!query.exec())
3936  MythDB::DBError("position map clear", query);
3937 
3938  if (posMap.isEmpty())
3939  return;
3940 
3941  // Use the multi-value insert syntax to reduce database I/O
3942  QStringList q("INSERT INTO ");
3943  QString qfields;
3944  if (IsVideo())
3945  {
3946  q << "filemarkup (filename, type, mark, `offset`)";
3947  qfields = QString("('%1',%2,") .
3948  // ideally, this should be escaped
3949  arg(videoPath) .
3950  arg(type);
3951  }
3952  else // if (IsRecording())
3953  {
3954  q << "recordedseek (chanid, starttime, type, mark, `offset`)";
3955  qfields = QString("(%1,'%2',%3,") .
3956  arg(m_chanId) .
3957  arg(m_recStartTs.toString(Qt::ISODate)) .
3958  arg(type);
3959  }
3960  q << " VALUES ";
3961 
3962  bool add_comma = false;
3963  frm_pos_map_t::iterator it;
3964  for (it = posMap.begin(); it != posMap.end(); ++it)
3965  {
3966  uint64_t frame = it.key();
3967 
3968  if ((min_frame >= 0) && (frame < (uint64_t)min_frame))
3969  continue;
3970 
3971  if ((max_frame >= 0) && (frame > (uint64_t)max_frame))
3972  continue;
3973 
3974  uint64_t offset = *it;
3975 
3976  if (add_comma)
3977  {
3978  q << ",";
3979  }
3980  else
3981  {
3982  add_comma = true;
3983  }
3984  q << qfields << QString("%1,%2)").arg(frame).arg(offset);
3985  }
3986  query.prepare(q.join(""));
3987  if (!query.exec())
3988  {
3989  MythDB::DBError("position map insert", query);
3990  }
3991 }
3992 
3994  frm_pos_map_t &posMap, MarkTypes type) const
3995 {
3996  if (posMap.isEmpty())
3997  return;
3998 
4000  {
4001  QMutexLocker locker(m_positionMapDBReplacement->lock);
4002 
4003  for (auto it = posMap.cbegin(); it != posMap.cend(); ++it)
4004  m_positionMapDBReplacement->map[type].insert(it.key(), *it);
4005 
4006  return;
4007  }
4008 
4009  // Use the multi-value insert syntax to reduce database I/O
4010  QStringList q("INSERT INTO ");
4011  QString qfields;
4012  if (IsVideo())
4013  {
4014  q << "filemarkup (filename, type, mark, `offset`)";
4015  qfields = QString("('%1',%2,") .
4016  // ideally, this should be escaped
4018  arg(type);
4019  }
4020  else if (IsRecording())
4021  {
4022  q << "recordedseek (chanid, starttime, type, mark, `offset`)";
4023  qfields = QString("(%1,'%2',%3,") .
4024  arg(m_chanId) .
4025  arg(m_recStartTs.toString(Qt::ISODate)) .
4026  arg(type);
4027  }
4028  else
4029  {
4030  return;
4031  }
4032  q << " VALUES ";
4033 
4034  bool add_comma = false;
4035  frm_pos_map_t::iterator it;
4036  for (it = posMap.begin(); it != posMap.end(); ++it)
4037  {
4038  uint64_t frame = it.key();
4039  uint64_t offset = *it;
4040 
4041  if (add_comma)
4042  {
4043  q << ",";
4044  }
4045  else
4046  {
4047  add_comma = true;
4048  }
4049  q << qfields << QString("%1,%2)").arg(frame).arg(offset);
4050  }
4051 
4052  MSqlQuery query(MSqlQuery::InitCon());
4053  query.prepare(q.join(""));
4054  if (!query.exec())
4055  {
4056  MythDB::DBError("delta position map insert", query);
4057  }
4058 }
4059 
4060 static const char *from_filemarkup_offset_asc =
4061  "SELECT mark, `offset` FROM filemarkup"
4062  " WHERE filename = :PATH"
4063  " AND type = :TYPE"
4064  " AND mark >= :QUERY_ARG"
4065  " ORDER BY filename ASC, type ASC, mark ASC LIMIT 1;";
4066 static const char *from_filemarkup_offset_desc =
4067  "SELECT mark, `offset` FROM filemarkup"
4068  " WHERE filename = :PATH"
4069  " AND type = :TYPE"
4070  " AND mark <= :QUERY_ARG"
4071  " ORDER BY filename DESC, type DESC, mark DESC LIMIT 1;";
4072 static const char *from_recordedseek_offset_asc =
4073  "SELECT mark, `offset` FROM recordedseek"
4074  " WHERE chanid = :CHANID"
4075  " AND starttime = :STARTTIME"
4076  " AND type = :TYPE"
4077  " AND mark >= :QUERY_ARG"
4078  " ORDER BY chanid ASC, starttime ASC, type ASC, mark ASC LIMIT 1;";
4079 static const char *from_recordedseek_offset_desc =
4080  "SELECT mark, `offset` FROM recordedseek"
4081  " WHERE chanid = :CHANID"
4082  " AND starttime = :STARTTIME"
4083  " AND type = :TYPE"
4084  " AND mark <= :QUERY_ARG"
4085  " ORDER BY chanid DESC, starttime DESC, type DESC, mark DESC LIMIT 1;";
4086 static const char *from_filemarkup_mark_asc =
4087  "SELECT `offset`,mark FROM filemarkup"
4088  " WHERE filename = :PATH"
4089  " AND type = :TYPE"
4090  " AND `offset` >= :QUERY_ARG"
4091  " ORDER BY filename ASC, type ASC, mark ASC LIMIT 1;";
4092 static const char *from_filemarkup_mark_desc =
4093  "SELECT `offset`,mark FROM filemarkup"
4094  " WHERE filename = :PATH"
4095  " AND type = :TYPE"
4096  " AND `offset` <= :QUERY_ARG"
4097  " ORDER BY filename DESC, type DESC, mark DESC LIMIT 1;";
4098 static const char *from_recordedseek_mark_asc =
4099  "SELECT `offset`,mark FROM recordedseek"
4100  " WHERE chanid = :CHANID"
4101  " AND starttime = :STARTTIME"
4102  " AND type = :TYPE"
4103  " AND `offset` >= :QUERY_ARG"
4104  " ORDER BY chanid ASC, starttime ASC, type ASC, mark ASC LIMIT 1;";
4105 static const char *from_recordedseek_mark_desc =
4106  "SELECT `offset`,mark FROM recordedseek"
4107  " WHERE chanid = :CHANID"
4108  " AND starttime = :STARTTIME"
4109  " AND type = :TYPE"
4110  " AND `offset` <= :QUERY_ARG"
4111  " ORDER BY chanid DESC, starttime DESC, type DESC, mark DESC LIMIT 1;";
4112 
4113 bool ProgramInfo::QueryKeyFrameInfo(uint64_t * result,
4114  uint64_t position_or_keyframe,
4115  bool backwards,
4116  MarkTypes type,
4117  const char *from_filemarkup_asc,
4118  const char *from_filemarkup_desc,
4119  const char *from_recordedseek_asc,
4120  const char *from_recordedseek_desc) const
4121 {
4122  MSqlQuery query(MSqlQuery::InitCon());
4123 
4124  if (IsVideo())
4125  {
4126  if (backwards)
4127  query.prepare(from_filemarkup_desc);
4128  else
4129  query.prepare(from_filemarkup_asc);
4131  }
4132  else if (IsRecording())
4133  {
4134  if (backwards)
4135  query.prepare(from_recordedseek_desc);
4136  else
4137  query.prepare(from_recordedseek_asc);
4138  query.bindValue(":CHANID", m_chanId);
4139  query.bindValue(":STARTTIME", m_recStartTs);
4140  }
4141  query.bindValue(":TYPE", type);
4142  query.bindValue(":QUERY_ARG", (unsigned long long)position_or_keyframe);
4143 
4144  if (!query.exec())
4145  {
4146  MythDB::DBError("QueryKeyFrameInfo", query);
4147  return false;
4148  }
4149 
4150  if (query.next())
4151  {
4152  *result = query.value(1).toULongLong();
4153  return true;
4154  }
4155 
4156  if (IsVideo())
4157  {
4158  if (backwards)
4159  query.prepare(from_filemarkup_asc);
4160  else
4161  query.prepare(from_filemarkup_desc);
4163  }
4164  else if (IsRecording())
4165  {
4166  if (backwards)
4167  query.prepare(from_recordedseek_asc);
4168  else
4169  query.prepare(from_recordedseek_desc);
4170  query.bindValue(":CHANID", m_chanId);
4171  query.bindValue(":STARTTIME", m_recStartTs);
4172  }
4173  query.bindValue(":TYPE", type);
4174  query.bindValue(":QUERY_ARG", (unsigned long long)position_or_keyframe);
4175 
4176  if (!query.exec())
4177  {
4178  MythDB::DBError("QueryKeyFrameInfo", query);
4179  return false;
4180  }
4181 
4182  if (query.next())
4183  {
4184  *result = query.value(1).toULongLong();
4185  return true;
4186  }
4187 
4188  return false;
4189 
4190 }
4191 
4192 bool ProgramInfo::QueryPositionKeyFrame(uint64_t *keyframe, uint64_t position,
4193  bool backwards) const
4194 {
4195  return QueryKeyFrameInfo(keyframe, position, backwards, MARK_GOP_BYFRAME,
4200 }
4201 bool ProgramInfo::QueryKeyFramePosition(uint64_t *position, uint64_t keyframe,
4202  bool backwards) const
4203 {
4204  return QueryKeyFrameInfo(position, keyframe, backwards, MARK_GOP_BYFRAME,
4209 }
4210 bool ProgramInfo::QueryDurationKeyFrame(uint64_t *keyframe, uint64_t duration,
4211  bool backwards) const
4212 {
4213  return QueryKeyFrameInfo(keyframe, duration, backwards, MARK_DURATION_MS,
4218 }
4219 bool ProgramInfo::QueryKeyFrameDuration(uint64_t *duration, uint64_t keyframe,
4220  bool backwards) const
4221 {
4222  return QueryKeyFrameInfo(duration, keyframe, backwards, MARK_DURATION_MS,
4227 }
4228 
4233  uint64_t frame, MarkTypes type, uint customAspect)
4234 {
4235  if (!IsRecording())
4236  return;
4237 
4238  MSqlQuery query(MSqlQuery::InitCon());
4239 
4240  query.prepare("INSERT INTO recordedmarkup"
4241  " (chanid, starttime, mark, type, data)"
4242  " VALUES"
4243  " ( :CHANID, :STARTTIME, :MARK, :TYPE, :DATA);");
4244  query.bindValue(":CHANID", m_chanId);
4245  query.bindValue(":STARTTIME", m_recStartTs);
4246 
4247  query.bindValue(":MARK", (quint64)frame);
4248  query.bindValue(":TYPE", type);
4249 
4250  if (type == MARK_ASPECT_CUSTOM)
4251  query.bindValue(":DATA", customAspect);
4252  else
4253  {
4254  // create NULL value
4255 #if QT_VERSION < QT_VERSION_CHECK(6,0,0)
4256  query.bindValue(":DATA", QVariant(QVariant::UInt));
4257 #else
4258  query.bindValue(":DATA", QVariant(QMetaType(QMetaType::UInt)));
4259 #endif
4260  }
4261 
4262  if (!query.exec())
4263  MythDB::DBError("aspect ratio change insert", query);
4264 }
4265 
4269 void ProgramInfo::SaveFrameRate(uint64_t frame, uint framerate)
4270 {
4271  if (!IsRecording())
4272  return;
4273 
4274  MSqlQuery query(MSqlQuery::InitCon());
4275 
4276  query.prepare("INSERT INTO recordedmarkup"
4277  " (chanid, starttime, mark, type, data)"
4278  " VALUES"
4279  " ( :CHANID, :STARTTIME, :MARK, :TYPE, :DATA);");
4280  query.bindValue(":CHANID", m_chanId);
4281  query.bindValue(":STARTTIME", m_recStartTs);
4282  query.bindValue(":MARK", (quint64)frame);
4283  query.bindValue(":TYPE", MARK_VIDEO_RATE);
4284  query.bindValue(":DATA", framerate);
4285 
4286  if (!query.exec())
4287  MythDB::DBError("Frame rate insert", query);
4288 }
4289 
4290 
4294 void ProgramInfo::SaveVideoScanType(uint64_t frame, bool progressive)
4295 {
4296  if (!IsRecording())
4297  return;
4298 
4299  MSqlQuery query(MSqlQuery::InitCon());
4300 
4301  query.prepare("INSERT INTO recordedmarkup"
4302  " (chanid, starttime, mark, type, data)"
4303  " VALUES"
4304  " ( :CHANID, :STARTTIME, :MARK, :TYPE, :DATA);");
4305  query.bindValue(":CHANID", m_chanId);
4306  query.bindValue(":STARTTIME", m_recStartTs);
4307  query.bindValue(":MARK", (quint64)frame);
4308  query.bindValue(":TYPE", MARK_SCAN_PROGRESSIVE);
4309  query.bindValue(":DATA", progressive);
4310 
4311  if (!query.exec())
4312  MythDB::DBError("Video scan type insert", query);
4313 }
4314 
4315 
4317 void ProgramInfo::SaveTotalDuration(std::chrono::milliseconds duration)
4318 {
4319  if (!IsRecording())
4320  return;
4321 
4322  MSqlQuery query(MSqlQuery::InitCon());
4323 
4324  query.prepare("DELETE FROM recordedmarkup "
4325  " WHERE chanid=:CHANID "
4326  " AND starttime=:STARTTIME "
4327  " AND type=:TYPE");
4328  query.bindValue(":CHANID", m_chanId);
4329  query.bindValue(":STARTTIME", m_recStartTs);
4330  query.bindValue(":TYPE", MARK_DURATION_MS);
4331 
4332  if (!query.exec())
4333  MythDB::DBError("Duration delete", query);
4334 
4335  query.prepare("INSERT INTO recordedmarkup"
4336  " (chanid, starttime, mark, type, data)"
4337  " VALUES"
4338  " ( :CHANID, :STARTTIME, 0, :TYPE, :DATA);");
4339  query.bindValue(":CHANID", m_chanId);
4340  query.bindValue(":STARTTIME", m_recStartTs);
4341  query.bindValue(":TYPE", MARK_DURATION_MS);
4342  query.bindValue(":DATA", (uint)(duration.count()));
4343 
4344  if (!query.exec())
4345  MythDB::DBError("Duration insert", query);
4346 }
4347 
4349 void ProgramInfo::SaveTotalFrames(int64_t frames)
4350 {
4351  if (!IsRecording())
4352  return;
4353 
4354  MSqlQuery query(MSqlQuery::InitCon());
4355 
4356  query.prepare("DELETE FROM recordedmarkup "
4357  " WHERE chanid=:CHANID "
4358  " AND starttime=:STARTTIME "
4359  " AND type=:TYPE");
4360  query.bindValue(":CHANID", m_chanId);
4361  query.bindValue(":STARTTIME", m_recStartTs);
4362  query.bindValue(":TYPE", MARK_TOTAL_FRAMES);
4363 
4364  if (!query.exec())
4365  MythDB::DBError("Frames delete", query);
4366 
4367  query.prepare("INSERT INTO recordedmarkup"
4368  " (chanid, starttime, mark, type, data)"
4369  " VALUES"
4370  " ( :CHANID, :STARTTIME, 0, :TYPE, :DATA);");
4371  query.bindValue(":CHANID", m_chanId);
4372  query.bindValue(":STARTTIME", m_recStartTs);
4373  query.bindValue(":TYPE", MARK_TOTAL_FRAMES);
4374  query.bindValue(":DATA", (uint)(frames));
4375 
4376  if (!query.exec())
4377  MythDB::DBError("Total Frames insert", query);
4378 }
4379 
4383 void ProgramInfo::SaveResolution(uint64_t frame, uint width, uint height)
4384 {
4385  if (!IsRecording())
4386  return;
4387 
4388  MSqlQuery query(MSqlQuery::InitCon());
4389 
4390  query.prepare("INSERT INTO recordedmarkup"
4391  " (chanid, starttime, mark, type, data)"
4392  " VALUES"
4393  " ( :CHANID, :STARTTIME, :MARK, :TYPE, :DATA);");
4394  query.bindValue(":CHANID", m_chanId);
4395  query.bindValue(":STARTTIME", m_recStartTs);
4396  query.bindValue(":MARK", (quint64)frame);
4397  query.bindValue(":TYPE", MARK_VIDEO_WIDTH);
4398  query.bindValue(":DATA", width);
4399 
4400  if (!query.exec())
4401  MythDB::DBError("Resolution insert", query);
4402 
4403  query.prepare("INSERT INTO recordedmarkup"
4404  " (chanid, starttime, mark, type, data)"
4405  " VALUES"
4406  " ( :CHANID, :STARTTIME, :MARK, :TYPE, :DATA);");
4407  query.bindValue(":CHANID", m_chanId);
4408  query.bindValue(":STARTTIME", m_recStartTs);
4409  query.bindValue(":MARK", (quint64)frame);
4410  query.bindValue(":TYPE", MARK_VIDEO_HEIGHT);
4411  query.bindValue(":DATA", height);
4412 
4413  if (!query.exec())
4414  MythDB::DBError("Resolution insert", query);
4415 }
4416 
4418  MarkTypes type, uint chanid, const QDateTime &recstartts)
4419 {
4420  QString qstr = QString(
4421  "SELECT recordedmarkup.data "
4422  "FROM recordedmarkup "
4423  "WHERE recordedmarkup.chanid = :CHANID AND "
4424  " recordedmarkup.starttime = :STARTTIME AND "
4425  " recordedmarkup.type = :TYPE "
4426  "GROUP BY recordedmarkup.data "
4427  "ORDER BY SUM( ( SELECT IFNULL(rm.mark, recordedmarkup.mark)"
4428  " FROM recordedmarkup AS rm "
4429  " WHERE rm.chanid = recordedmarkup.chanid AND "
4430  " rm.starttime = recordedmarkup.starttime AND "
4431  " rm.type = recordedmarkup.type AND "
4432  " rm.mark > recordedmarkup.mark "
4433  " ORDER BY rm.mark ASC LIMIT 1 "
4434  " ) - recordedmarkup.mark "
4435  " ) DESC "
4436  "LIMIT 1");
4437 
4438  MSqlQuery query(MSqlQuery::InitCon());
4439  query.prepare(qstr);
4440  query.bindValue(":TYPE", (int)type);
4441  query.bindValue(":CHANID", chanid);
4442  query.bindValue(":STARTTIME", recstartts);
4443 
4444  if (!query.exec())
4445  {
4446  MythDB::DBError("load_markup_datum", query);
4447  return 0;
4448  }
4449 
4450  return (query.next()) ? query.value(0).toUInt() : 0;
4451 }
4452 
4458 {
4460 }
4461 
4467 {
4469 }
4470 
4476 {
4478 }
4479 
4481 {
4482  MSqlQuery query(MSqlQuery::InitCon());
4483  query.prepare("SELECT recordedmarkup.type "
4484  "FROM recordedmarkup "
4485  "WHERE recordedmarkup.chanid = :CHANID AND "
4486  " recordedmarkup.starttime = :STARTTIME AND "
4487  " recordedmarkup.type >= :ASPECTSTART AND "
4488  " recordedmarkup.type <= :ASPECTEND "
4489  "GROUP BY recordedmarkup.type "
4490  "ORDER BY SUM( ( SELECT IFNULL(rm.mark, ( "
4491  " SELECT MAX(rmmax.mark) "
4492  " FROM recordedmarkup AS rmmax "
4493  " WHERE rmmax.chanid = recordedmarkup.chanid "
4494  " AND rmmax.starttime = recordedmarkup.starttime "
4495  " ) ) "
4496  " FROM recordedmarkup AS rm "
4497  " WHERE rm.chanid = recordedmarkup.chanid AND "
4498  " rm.starttime = recordedmarkup.starttime AND "
4499  " rm.type >= :ASPECTSTART2 AND "
4500  " rm.type <= :ASPECTEND2 AND "
4501  " rm.mark > recordedmarkup.mark "
4502  " ORDER BY rm.mark ASC LIMIT 1 "
4503  " ) - recordedmarkup.mark "
4504  " ) DESC "
4505  "LIMIT 1");
4506  query.bindValue(":CHANID", m_chanId);
4507  query.bindValue(":STARTTIME", m_recStartTs);
4508  query.bindValue(":ASPECTSTART", MARK_ASPECT_4_3); // 11
4509  query.bindValue(":ASPECTEND", MARK_ASPECT_CUSTOM); // 14
4510  query.bindValue(":ASPECTSTART2", MARK_ASPECT_4_3); // 11
4511  query.bindValue(":ASPECTEND2", MARK_ASPECT_CUSTOM); // 14
4512 
4513  if (!query.exec())
4514  {
4515  MythDB::DBError("QueryAverageAspectRatio", query);
4516  return MARK_UNSET;
4517  }
4518 
4519  if (!query.next())
4520  return MARK_UNSET;
4521 
4522  return static_cast<MarkTypes>(query.value(0).toInt());
4523 }
4524 
4531 {
4532  return static_cast<bool>(load_markup_datum(MARK_SCAN_PROGRESSIVE,
4534 }
4535 
4541 std::chrono::milliseconds ProgramInfo::QueryTotalDuration(void) const
4542 {
4544  return 0ms;
4545 
4546  auto msec = std::chrono::milliseconds(load_markup_datum(MARK_DURATION_MS, m_chanId, m_recStartTs));
4547 
4548 // Impossible condition, load_markup_datum returns an unsigned int
4549 // if (msec < 0ms)
4550 // return 0ms;
4551 
4552  return msec;
4553 }
4554 
4559 {
4561  return frames;
4562 }
4563 
4564 void ProgramInfo::QueryMarkup(QVector<MarkupEntry> &mapMark,
4565  QVector<MarkupEntry> &mapSeek) const
4566 {
4567  MSqlQuery query(MSqlQuery::InitCon());
4568  // Get the markup
4569  if (IsVideo())
4570  {
4571  query.prepare("SELECT type, mark, `offset` FROM filemarkup"
4572  " WHERE filename = :PATH"
4573  " AND type NOT IN (:KEYFRAME,:DURATION)"
4574  " ORDER BY mark, type;");
4576  query.bindValue(":KEYFRAME", MARK_GOP_BYFRAME);
4577  query.bindValue(":DURATION", MARK_DURATION_MS);
4578  }
4579  else if (IsRecording())
4580  {
4581  query.prepare("SELECT type, mark, data FROM recordedmarkup"
4582  " WHERE chanid = :CHANID"
4583  " AND STARTTIME = :STARTTIME"
4584  " ORDER BY mark, type");
4585  query.bindValue(":CHANID", m_chanId);
4586  query.bindValue(":STARTTIME", m_recStartTs);
4587  }
4588  else
4589  {
4590  return;
4591  }
4592  if (!query.exec())
4593  {
4594  MythDB::DBError("QueryMarkup markup data", query);
4595  return;
4596  }
4597  while (query.next())
4598  {
4599  int type = query.value(0).toInt();
4600  uint64_t frame = query.value(1).toLongLong();
4601  uint64_t data = 0;
4602  bool isDataNull = query.value(2).isNull();
4603  if (!isDataNull)
4604  data = query.value(2).toLongLong();
4605  mapMark.append(MarkupEntry(type, frame, data, isDataNull));
4606  }
4607 
4608  // Get the seektable
4609  if (IsVideo())
4610  {
4611  query.prepare("SELECT type, mark, `offset` FROM filemarkup"
4612  " WHERE filename = :PATH"
4613  " AND type IN (:KEYFRAME,:DURATION)"
4614  " ORDER BY mark, type;");
4616  query.bindValue(":KEYFRAME", MARK_GOP_BYFRAME);
4617  query.bindValue(":DURATION", MARK_DURATION_MS);
4618  }
4619  else if (IsRecording())
4620  {
4621  query.prepare("SELECT type, mark, `offset` FROM recordedseek"
4622  " WHERE chanid = :CHANID"
4623  " AND STARTTIME = :STARTTIME"
4624  " ORDER BY mark, type");
4625  query.bindValue(":CHANID", m_chanId);
4626  query.bindValue(":STARTTIME", m_recStartTs);
4627  }
4628  if (!query.exec())
4629  {
4630  MythDB::DBError("QueryMarkup seektable data", query);
4631  return;
4632  }
4633  while (query.next())
4634  {
4635  int type = query.value(0).toInt();
4636  uint64_t frame = query.value(1).toLongLong();
4637  uint64_t data = 0;
4638  bool isDataNull = query.value(2).isNull();
4639  if (!isDataNull)
4640  data = query.value(2).toLongLong();
4641  mapSeek.append(MarkupEntry(type, frame, data, isDataNull));
4642  }
4643 }
4644 
4645 void ProgramInfo::SaveMarkup(const QVector<MarkupEntry> &mapMark,
4646  const QVector<MarkupEntry> &mapSeek) const
4647 {
4648  MSqlQuery query(MSqlQuery::InitCon());
4649  if (IsVideo())
4650  {
4652  if (mapMark.isEmpty())
4653  {
4654  LOG(VB_GENERAL, LOG_INFO,
4655  QString("No mark entries in input, "
4656  "not removing marks from DB"));
4657  }
4658  else
4659  {
4660  query.prepare("DELETE FROM filemarkup"
4661  " WHERE filename = :PATH"
4662  " AND type NOT IN (:KEYFRAME,:DURATION)");
4663  query.bindValue(":PATH", path);
4664  query.bindValue(":KEYFRAME", MARK_GOP_BYFRAME);
4665  query.bindValue(":DURATION", MARK_DURATION_MS);
4666  if (!query.exec())
4667  {
4668  MythDB::DBError("SaveMarkup seektable data", query);
4669  return;
4670  }
4671  for (const auto& entry : qAsConst(mapMark))
4672  {
4673  if (entry.type == MARK_DURATION_MS)
4674  continue;
4675  if (entry.isDataNull)
4676  {
4677  query.prepare("INSERT INTO filemarkup"
4678  " (filename,type,mark)"
4679  " VALUES (:PATH,:TYPE,:MARK)");
4680  }
4681  else
4682  {
4683  query.prepare("INSERT INTO filemarkup"
4684  " (filename,type,mark,`offset`)"
4685  " VALUES (:PATH,:TYPE,:MARK,:OFFSET)");
4686  query.bindValue(":OFFSET", (quint64)entry.data);
4687  }
4688  query.bindValue(":PATH", path);
4689  query.bindValue(":TYPE", entry.type);
4690  query.bindValue(":MARK", (quint64)entry.frame);
4691  if (!query.exec())
4692  {
4693  MythDB::DBError("SaveMarkup seektable data", query);
4694  return;
4695  }
4696  }
4697  }
4698  if (mapSeek.isEmpty())
4699  {
4700  LOG(VB_GENERAL, LOG_INFO,
4701  QString("No seek entries in input, "
4702  "not removing marks from DB"));
4703  }
4704  else
4705  {
4706  query.prepare("DELETE FROM filemarkup"
4707  " WHERE filename = :PATH"
4708  " AND type IN (:KEYFRAME,:DURATION)");
4709  query.bindValue(":PATH", path);
4710  query.bindValue(":KEYFRAME", MARK_GOP_BYFRAME);
4711  query.bindValue(":DURATION", MARK_DURATION_MS);
4712  if (!query.exec())
4713  {
4714  MythDB::DBError("SaveMarkup seektable data", query);
4715  return;
4716  }
4717  for (int i = 0; i < mapSeek.size(); ++i)
4718  {
4719  if (i > 0 && (i % 1000 == 0))
4720  {
4721  LOG(VB_GENERAL, LOG_INFO,
4722  QString("Inserted %1 of %2 records")
4723  .arg(i).arg(mapSeek.size()));
4724  }
4725  const MarkupEntry &entry = mapSeek[i];
4726  query.prepare("INSERT INTO filemarkup"
4727  " (filename,type,mark,`offset`)"
4728  " VALUES (:PATH,:TYPE,:MARK,:OFFSET)");
4729  query.bindValue(":PATH", path);
4730  query.bindValue(":TYPE", entry.type);
4731  query.bindValue(":MARK", (quint64)entry.frame);
4732  query.bindValue(":OFFSET", (quint64)entry.data);
4733  if (!query.exec())
4734  {
4735  MythDB::DBError("SaveMarkup seektable data", query);
4736  return;
4737  }
4738  }
4739  }
4740  }
4741  else if (IsRecording())
4742  {
4743  if (mapMark.isEmpty())
4744  {
4745  LOG(VB_GENERAL, LOG_INFO,
4746  QString("No mark entries in input, "
4747  "not removing marks from DB"));
4748  }
4749  else
4750  {
4751  query.prepare("DELETE FROM recordedmarkup"
4752  " WHERE chanid = :CHANID"
4753  " AND starttime = :STARTTIME");
4754  query.bindValue(":CHANID", m_chanId);
4755  query.bindValue(":STARTTIME", m_recStartTs);
4756  if (!query.exec())
4757  {
4758  MythDB::DBError("SaveMarkup seektable data", query);
4759  return;
4760  }
4761  for (const auto& entry : qAsConst(mapMark))
4762  {
4763  if (entry.isDataNull)
4764  {
4765  query.prepare("INSERT INTO recordedmarkup"
4766  " (chanid,starttime,type,mark)"
4767  " VALUES (:CHANID,:STARTTIME,:TYPE,:MARK)");
4768  }
4769  else
4770  {
4771  query.prepare("INSERT INTO recordedmarkup"
4772  " (chanid,starttime,type,mark,data)"
4773  " VALUES (:CHANID,:STARTTIME,"
4774  " :TYPE,:MARK,:OFFSET)");
4775  query.bindValue(":OFFSET", (quint64)entry.data);
4776  }
4777  query.bindValue(":CHANID", m_chanId);
4778  query.bindValue(":STARTTIME", m_recStartTs);
4779  query.bindValue(":TYPE", entry.type);
4780  query.bindValue(":MARK", (quint64)entry.frame);
4781  if (!query.exec())
4782  {
4783  MythDB::DBError("SaveMarkup seektable data", query);
4784  return;
4785  }
4786  }
4787  }
4788  if (mapSeek.isEmpty())
4789  {
4790  LOG(VB_GENERAL, LOG_INFO,
4791  QString("No seek entries in input, "
4792  "not removing marks from DB"));
4793  }
4794  else
4795  {
4796  query.prepare("DELETE FROM recordedseek"
4797  " WHERE chanid = :CHANID"
4798  " AND starttime = :STARTTIME");
4799  query.bindValue(":CHANID", m_chanId);
4800  query.bindValue(":STARTTIME", m_recStartTs);
4801  if (!query.exec())
4802  {
4803  MythDB::DBError("SaveMarkup seektable data", query);
4804  return;
4805  }
4806  for (int i = 0; i < mapSeek.size(); ++i)
4807  {
4808  if (i > 0 && (i % 1000 == 0))
4809  {
4810  LOG(VB_GENERAL, LOG_INFO,
4811  QString("Inserted %1 of %2 records")
4812  .arg(i).arg(mapSeek.size()));
4813  }
4814  const MarkupEntry &entry = mapSeek[i];
4815  query.prepare("INSERT INTO recordedseek"
4816  " (chanid,starttime,type,mark,`offset`)"
4817  " VALUES (:CHANID,:STARTTIME,"
4818  " :TYPE,:MARK,:OFFSET)");
4819  query.bindValue(":CHANID", m_chanId);
4820  query.bindValue(":STARTTIME", m_recStartTs);
4821  query.bindValue(":TYPE", entry.type);
4822  query.bindValue(":MARK", (quint64)entry.frame);
4823  query.bindValue(":OFFSET", (quint64)entry.data);
4824  if (!query.exec())
4825  {
4826  MythDB::DBError("SaveMarkup seektable data", query);
4827  return;
4828  }
4829  }
4830  }
4831  }
4832 }
4833 
4834 void ProgramInfo::SaveVideoProperties(uint mask, uint video_property_flags)
4835 {
4836  MSqlQuery query(MSqlQuery::InitCon());
4837 
4838  LOG(VB_RECORD, LOG_INFO,
4839  QString("SaveVideoProperties(0x%1, 0x%2)")
4840  .arg(mask,2,16,QChar('0')).arg(video_property_flags,2,16,QChar('0')));
4841 
4842  query.prepare(
4843  "UPDATE recordedprogram "
4844  "SET videoprop = ((videoprop+0) & :OTHERFLAGS) | :FLAGS "
4845  "WHERE chanid = :CHANID AND starttime = :STARTTIME");
4846 
4847  query.bindValue(":OTHERFLAGS", ~mask);
4848  query.bindValue(":FLAGS", video_property_flags);
4849  query.bindValue(":CHANID", m_chanId);
4850  query.bindValue(":STARTTIME", m_startTs);
4851  if (!query.exec())
4852  {
4853  MythDB::DBError("SaveVideoProperties", query);
4854  return;
4855  }
4856 
4857  uint videoproperties = m_videoProperties;
4858  videoproperties &= ~mask;
4859  videoproperties |= video_property_flags;
4860  m_videoProperties = videoproperties;
4861 
4862  SendUpdateEvent();
4863 }
4864 
4875 QString ProgramInfo::ChannelText(const QString &format) const
4876 {
4877  QString chan(format);
4878  chan.replace("<num>", m_chanStr)
4879  .replace("<sign>", m_chanSign)
4880  .replace("<name>", m_chanName);
4881  return chan;
4882 }
4883 
4885 {
4886 #ifdef DEBUG_IN_USE
4887  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("UpdateInUseMark(%1) '%2'")
4888  .arg(force?"force":"no force").arg(m_inUseForWhat));
4889 #endif
4890 
4891  if (!IsRecording())
4892  return;
4893 
4894  if (m_inUseForWhat.isEmpty())
4895  return;
4896 
4897  if (force || MythDate::secsInPast(m_lastInUseTime) > 15min)
4898  MarkAsInUse(true);
4899 }
4900 
4902 {
4903  MSqlQuery query(MSqlQuery::InitCon());
4904 
4905  query.prepare(
4906  "UPDATE recorded "
4907  "SET season = :SEASON, episode = :EPISODE "
4908  "WHERE chanid = :CHANID AND starttime = :STARTTIME "
4909  "AND recordid = :RECORDID");
4910 
4911  query.bindValue(":SEASON", seas);
4912  query.bindValue(":EPISODE", ep);
4913  query.bindValue(":CHANID", m_chanId);
4914  query.bindValue(":STARTTIME", m_recStartTs);
4915  query.bindValue(":RECORDID", m_recordId);
4916  if (!query.exec())
4917  {
4918  MythDB::DBError("SaveSeasonEpisode", query);
4919  return;
4920  }
4921 
4922  SendUpdateEvent();
4923 }
4924 
4925 void ProgramInfo::SaveInetRef(const QString &inet)
4926 {
4927  MSqlQuery query(MSqlQuery::InitCon());
4928 
4929  query.prepare(
4930  "UPDATE recorded "
4931  "SET inetref = :INETREF "
4932  "WHERE chanid = :CHANID AND starttime = :STARTTIME "
4933  "AND recordid = :RECORDID");
4934 
4935  query.bindValue(":INETREF", inet);
4936  query.bindValue(":CHANID", m_chanId);
4937  query.bindValue(":STARTTIME", m_recStartTs);
4938  query.bindValue(":RECORDID", m_recordId);
4939  query.exec();
4940 
4941  SendUpdateEvent();
4942 }
4943 
4951 {
4952  if (IsLocal() && QFileInfo(m_pathname).isReadable())
4953  return true;
4954 
4955  if (!IsMythStream())
4956  m_pathname = GetPlaybackURL(true, false);
4957 
4958  if (IsMythStream())
4959  return RemoteCheckFile(this);
4960 
4961  if (IsLocal())
4962  return QFileInfo(m_pathname).isReadable();
4963 
4964  return false;
4965 }
4966 
4967 QString ProgramInfo::QueryRecordingGroupPassword(const QString &group)
4968 {
4969  QString result;
4970 
4971  MSqlQuery query(MSqlQuery::InitCon());
4972  query.prepare("SELECT password FROM recgroups "
4973  "WHERE recgroup = :GROUP");
4974  query.bindValue(":GROUP", group);
4975 
4976  if (query.exec() && query.next())
4977  result = query.value(0).toString();
4978 
4979  return(result);
4980 }
4981 
4985 {
4986  MSqlQuery query(MSqlQuery::InitCon());
4987  query.prepare("SELECT recgroup FROM recorded "
4988  "WHERE chanid = :CHANID AND "
4989  " starttime = :START");
4990  query.bindValue(":START", m_recStartTs);
4991  query.bindValue(":CHANID", m_chanId);
4992 
4993  QString grp = m_recGroup;
4994  if (query.exec() && query.next())
4995  grp = query.value(0).toString();
4996 
4997  return grp;
4998 }
4999 
5001 {
5002  MSqlQuery query(MSqlQuery::InitCon());
5003  query.prepare("SELECT transcoder FROM recorded "
5004  "WHERE chanid = :CHANID AND "
5005  " starttime = :START");
5006  query.bindValue(":CHANID", m_chanId);
5007  query.bindValue(":START", m_recStartTs);
5008 
5009  if (query.exec() && query.next())
5010  return query.value(0).toUInt();
5011 
5012  return 0;
5013 }
5014 
5021 {
5022  if (!IsLocal())
5023  {
5024  if (!gCoreContext->IsBackend())
5025  return "";
5026 
5027  QString path = GetPlaybackURL(false, true);
5028  if (path.startsWith("/"))
5029  {
5030  QFileInfo testFile(path);
5031  return testFile.path();
5032  }
5033 
5034  return "";
5035  }
5036 
5037  QFileInfo testFile(m_pathname);
5038  if (testFile.exists() || (gCoreContext->GetHostName() == m_hostname))
5039  {
5040  // we may be recording this file and it may not exist yet so we need
5041  // to do some checking to see what is in pathname
5042  if (testFile.exists())
5043  {
5044  if (testFile.isSymLink())
5045  testFile.setFile(getSymlinkTarget(m_pathname));
5046 
5047  if (testFile.isFile())
5048  return testFile.path();
5049  if (testFile.isDir())
5050  return testFile.filePath();
5051  }
5052  else
5053  {
5054  testFile.setFile(testFile.absolutePath());
5055  if (testFile.exists())
5056  {
5057  if (testFile.isSymLink())
5058  testFile.setFile(getSymlinkTarget(testFile.path()));
5059 
5060  if (testFile.isDir())
5061  return testFile.filePath();
5062  }
5063  }
5064  }
5065 
5066  return "";
5067 }
5068 
5075 void ProgramInfo::MarkAsInUse(bool inuse, const QString& usedFor)
5076 {
5077  if (!IsRecording())
5078  return;
5079 
5080  bool notifyOfChange = false;
5081 
5082  if (inuse &&
5083  (m_inUseForWhat.isEmpty() ||
5084  (!usedFor.isEmpty() && usedFor != m_inUseForWhat)))
5085  {
5086  if (!usedFor.isEmpty())
5087  {
5088 
5089 #ifdef DEBUG_IN_USE
5090  if (!m_inUseForWhat.isEmpty())
5091  {
5092  LOG(VB_GENERAL, LOG_INFO, LOC +
5093  QString("MarkAsInUse(true, '%1'->'%2')")
5094  .arg(m_inUseForWhat).arg(usedFor) +
5095  " -- use has changed");
5096  }
5097  else
5098  {
5099  LOG(VB_GENERAL, LOG_INFO, LOC +
5100  QString("MarkAsInUse(true, ''->'%1')").arg(usedFor));
5101  }
5102 #endif // DEBUG_IN_USE
5103 
5104  m_inUseForWhat = usedFor;
5105  }
5106  else if (m_inUseForWhat.isEmpty())
5107  {
5108  m_inUseForWhat = QString("%1 [%2]")
5109  .arg(QObject::tr("Unknown")).arg(getpid());
5110  LOG(VB_GENERAL, LOG_WARNING, LOC +
5111  QString("MarkAsInUse(true, ''->'%1')").arg(m_inUseForWhat) +
5112  " -- use was not explicitly set");
5113  }
5114 
5115  notifyOfChange = true;
5116  }
5117 
5118  if (!inuse && !m_inUseForWhat.isEmpty() && usedFor != m_inUseForWhat)
5119  {
5120  LOG(VB_GENERAL, LOG_WARNING, LOC +
5121  QString("MarkAsInUse(false, '%1'->'%2')")
5122  .arg(m_inUseForWhat, usedFor) +
5123  " -- use has changed since first setting as in use.");
5124  }
5125 #ifdef DEBUG_IN_USE
5126  else if (!inuse)
5127  {
5128  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("MarkAsInUse(false, '%1')")
5129  .arg(m_inUseForWhat));
5130  }
5131 #endif // DEBUG_IN_USE
5132 
5133  if (!inuse && m_inUseForWhat.isEmpty())
5134  m_inUseForWhat = usedFor;
5135 
5136  if (!inuse && m_inUseForWhat.isEmpty())
5137  {
5138  LOG(VB_GENERAL, LOG_WARNING, LOC +
5139  "MarkAsInUse requires a key to delete in use mark");
5140  return; // can't delete if we don't have a key
5141  }
5142 
5143  if (!inuse)
5144  {
5145  MSqlQuery query(MSqlQuery::InitCon());
5146  query.prepare(
5147  "DELETE FROM inuseprograms "
5148  "WHERE chanid = :CHANID AND starttime = :STARTTIME AND "
5149  " hostname = :HOSTNAME AND recusage = :RECUSAGE");
5150  query.bindValue(":CHANID", m_chanId);
5151  query.bindValue(":STARTTIME", m_recStartTs);
5152  query.bindValue(":HOSTNAME", gCoreContext->GetHostName());
5153  query.bindValue(":RECUSAGE", m_inUseForWhat);
5154 
5155  if (!query.exec())
5156  MythDB::DBError("MarkAsInUse -- delete", query);
5157 
5158  m_inUseForWhat.clear();
5159  m_lastInUseTime = MythDate::current(true).addSecs(-4 * 60 * 60);
5160  SendUpdateEvent();
5161  return;
5162  }
5163 
5164  if (m_pathname == GetBasename())
5165  m_pathname = GetPlaybackURL(false, true);
5166 
5167  QString recDir = DiscoverRecordingDirectory();
5168 
5169  QDateTime inUseTime = MythDate::current(true);
5170 
5171  MSqlQuery query(MSqlQuery::InitCon());
5172  query.prepare(
5173  "SELECT count(*) "
5174  "FROM inuseprograms "
5175  "WHERE chanid = :CHANID AND starttime = :STARTTIME AND "
5176  " hostname = :HOSTNAME AND recusage = :RECUSAGE");
5177  query.bindValue(":CHANID", m_chanId);
5178  query.bindValue(":STARTTIME", m_recStartTs);
5179  query.bindValue(":HOSTNAME", gCoreContext->GetHostName());
5180  query.bindValue(":RECUSAGE", m_inUseForWhat);
5181 
5182  if (!query.exec())
5183  {
5184  MythDB::DBError("MarkAsInUse -- select", query);
5185  }
5186  else if (!query.next())
5187  {
5188  LOG(VB_GENERAL, LOG_ERR, LOC + "MarkAsInUse -- select query failed");
5189  }
5190  else if (query.value(0).toBool())
5191  {
5192  query.prepare(
5193  "UPDATE inuseprograms "
5194  "SET lastupdatetime = :UPDATETIME "
5195  "WHERE chanid = :CHANID AND starttime = :STARTTIME AND "
5196  " hostname = :HOSTNAME AND recusage = :RECUSAGE");
5197  query.bindValue(":CHANID", m_chanId);
5198  query.bindValue(":STARTTIME", m_recStartTs);
5199  query.bindValue(":HOSTNAME", gCoreContext->GetHostName());
5200  query.bindValue(":RECUSAGE", m_inUseForWhat);
5201  query.bindValue(":UPDATETIME", inUseTime);
5202 
5203  if (!query.exec())
5204  MythDB::DBError("MarkAsInUse -- update failed", query);
5205  else
5206  m_lastInUseTime = inUseTime;
5207  }
5208  else // if (!query.value(0).toUInt())
5209  {
5210  query.prepare(
5211  "INSERT INTO inuseprograms "
5212  " (chanid, starttime, recusage, hostname, "
5213  " lastupdatetime, rechost, recdir) "
5214  "VALUES "
5215  " (:CHANID, :STARTTIME, :RECUSAGE, :HOSTNAME, "
5216  " :UPDATETIME, :RECHOST, :RECDIR)");
5217  query.bindValue(":CHANID", m_chanId);
5218  query.bindValue(":STARTTIME", m_recStartTs);
5219  query.bindValue(":HOSTNAME", gCoreContext->GetHostName());
5220  query.bindValue(":RECUSAGE", m_inUseForWhat);
5221  query.bindValue(":UPDATETIME", inUseTime);
5222  query.bindValue(":RECHOST",
5223  m_hostname.isEmpty() ? gCoreContext->GetHostName()
5224  : m_hostname);
5225  query.bindValue(":RECDIR", recDir);
5226 
5227  if (!query.exec())
5228  MythDB::DBError("MarkAsInUse -- insert failed", query);
5229  else
5230  m_lastInUseTime = inUseTime;
5231  }
5232 
5233  if (!notifyOfChange)
5234  return;
5235 
5236  // Let others know we changed status
5237  QDateTime oneHourAgo = MythDate::current().addSecs(-61 * 60);
5238  query.prepare("SELECT DISTINCT recusage "
5239  "FROM inuseprograms "
5240  "WHERE lastupdatetime >= :ONEHOURAGO AND "
5241  " chanid = :CHANID AND "
5242  " starttime = :STARTTIME");
5243  query.bindValue(":CHANID", m_chanId);
5244  query.bindValue(":STARTTIME", m_recStartTs);
5245  query.bindValue(":ONEHOURAGO", oneHourAgo);
5246  if (!query.exec())
5247  return; // not safe to send update event...
5248 
5249  m_programFlags &= ~(FL_INUSEPLAYING | FL_INUSERECORDING | FL_INUSEOTHER);
5250  while (query.next())
5251  {
5252  QString inUseForWhat = query.value(0).toString();
5253  if (inUseForWhat.contains(kPlayerInUseID))
5254  m_programFlags |= FL_INUSEPLAYING;
5255  else if (inUseForWhat == kRecorderInUseID)
5256  m_programFlags |= FL_INUSERECORDING;
5257  else
5258  m_programFlags |= FL_INUSEOTHER;
5259  }
5260  SendUpdateEvent();
5261 }
5262 
5268 bool ProgramInfo::QueryTuningInfo(QString &channum, QString &input) const
5269 {
5270  channum.clear();
5271  input.clear();
5272  MSqlQuery query(MSqlQuery::InitCon());
5273 
5274  query.prepare("SELECT channel.channum, capturecard.inputname "
5275  "FROM channel, capturecard "
5276  "WHERE channel.chanid = :CHANID AND "
5277  " capturecard.sourceid = :SOURCEID AND "
5278  " capturecard.cardid = :INPUTID");
5279  query.bindValue(":CHANID", m_chanId);
5280  query.bindValue(":SOURCEID", m_sourceId);
5281  query.bindValue(":INPUTID", m_inputId);
5282 
5283  if (query.exec() && query.next())
5284  {
5285  channum = query.value(0).toString();
5286  input = query.value(1).toString();
5287  return true;
5288  }
5289  MythDB::DBError("GetChannel(ProgInfo...)", query);
5290  return false;
5291 }
5292 
5293 static int init_tr(void)
5294 {
5295  static bool s_done = false;
5296  static QMutex s_initTrLock;
5297  QMutexLocker locker(&s_initTrLock);
5298  if (s_done)
5299  return 1;
5300 
5301  QString rec_profile_names =
5302  QObject::tr("Default", "Recording Profile Default") +
5303  QObject::tr("High Quality", "Recording Profile High Quality") +
5304  QObject::tr("Live TV", "Recording Profile Live TV") +
5305  QObject::tr("Low Quality", "Recording Profile Low Quality") +
5306  QObject::tr("Medium Quality", "Recording Profile Medium Quality") +
5307  QObject::tr("MPEG-2", "Recording Profile MPEG-2") +
5308  QObject::tr("RTjpeg/MPEG-4", "Recording Profile RTjpeg/MPEG-4");
5309 
5310 
5311  QString rec_profile_groups =
5312  QObject::tr("CRC IP Recorders",
5313  "Recording Profile Group Name") +
5314  QObject::tr("FireWire Input",
5315  "Recording Profile Group Name") +
5316  QObject::tr("Freebox Input",
5317  "Recording Profile Group Name") +
5318  QObject::tr("Hardware DVB Encoders",
5319  "Recording Profile Group Name") +
5320  QObject::tr("Hardware HDTV",
5321  "Recording Profile Group Name") +
5322  QObject::tr("Hardware MJPEG Encoders (Matrox G200-TV, Miro DC10, etc)",
5323  "Recording Profile Group Name") +
5324  QObject::tr("HD-PVR Recorders",
5325  "Recording Profile Group Name") +
5326  QObject::tr("HDHomeRun Recorders",
5327  "Recording Profile Group Name") +
5328  QObject::tr("MPEG-2 Encoders (PVR-x50, PVR-500)",
5329  "Recording Profile Group Name") +
5330  QObject::tr("Software Encoders (V4L based)",
5331  "Recording Profile Group Name") +
5332  QObject::tr("Transcoders",
5333  "Recording Profile Group Name") +
5334  QObject::tr("USB MPEG-4 Encoder (Plextor ConvertX, etc)",
5335  "Recording Profile Group Name") +
5336  QObject::tr("V4L2 Encoders",
5337  "Recording Profile Group Name");
5338 
5339  QString display_rec_groups =
5340  QObject::tr("All Programs", "Recording Group All Programs") +
5341  QObject::tr("All", "Recording Group All Programs -- short form") +
5342  QObject::tr("Live TV", "Recording Group Live TV") +
5343  QObject::tr("Default", "Recording Group Default") +
5344  QObject::tr("Deleted", "Recording Group Deleted");
5345 
5346  QString special_program_groups =
5347  QObject::tr("All Programs - %1",
5348  "Show all programs from a specific recording group");
5349 
5350  QString storage_groups =
5351  QObject::tr("Default", "Storage Group Name") +
5352  QObject::tr("Live TV", "Storage Group Name") +
5353  QObject::tr("Thumbnails", "Storage Group Name") +
5354  QObject::tr("DB Backups", "Storage Group Name");
5355 
5356  QString play_groups =
5357  QObject::tr("Default", "Playback Group Name");
5358 
5359  s_done = true;
5360  return (rec_profile_names.length() +
5361  rec_profile_groups.length() +
5362  display_rec_groups.length() +
5363  special_program_groups.length() +
5364  storage_groups.length() +
5365  play_groups.length());
5366 }
5367 
5369 {
5370  QMutexLocker locker(&s_staticDataLock);
5371  if (!s_updater)
5372  s_updater = new ProgramInfoUpdater();
5373  return 1;
5374 }
5375 
5377 QString ProgramInfo::i18n(const QString &msg)
5378 {
5379  init_tr();
5380  QByteArray msg_arr = msg.toLatin1();
5381  QString msg_i18n = QObject::tr(msg_arr.constData());
5382  QByteArray msg_i18n_arr = msg_i18n.toLatin1();
5383  return (msg_arr == msg_i18n_arr) ? msg : msg_i18n;
5384 }
5385 
5393 {
5394  QString pburl = GetPlaybackURL(false, true);
5395  if (pburl.startsWith("myth://"))
5396  {
5397  str.replace(QString("%DIR%"), pburl);
5398  }
5399  else
5400  {
5401  QFileInfo dirInfo(pburl);
5402  str.replace(QString("%DIR%"), dirInfo.path());
5403  }
5404 
5405  str.replace(QString("%FILE%"), GetBasename());
5406  str.replace(QString("%TITLE%"), m_title);
5407  str.replace(QString("%SUBTITLE%"), m_subtitle);
5408  str.replace(QString("%SEASON%"), QString::number(m_season));
5409  str.replace(QString("%EPISODE%"), QString::number(m_episode));
5410  str.replace(QString("%TOTALEPISODES%"), QString::number(m_totalEpisodes));
5411  str.replace(QString("%SYNDICATEDEPISODE%"), m_syndicatedEpisode);
5412  str.replace(QString("%DESCRIPTION%"), m_description);
5413  str.replace(QString("%HOSTNAME%"), m_hostname);
5414  str.replace(QString("%CATEGORY%"), m_category);
5415  str.replace(QString("%RECGROUP%"), m_recGroup);
5416  str.replace(QString("%PLAYGROUP%"), m_playGroup);
5417  str.replace(QString("%CHANID%"), QString::number(m_chanId));
5418  str.replace(QString("%INETREF%"), m_inetRef);
5419  str.replace(QString("%PARTNUMBER%"), QString::number(m_partNumber));
5420  str.replace(QString("%PARTTOTAL%"), QString::number(m_partTotal));
5421  str.replace(QString("%ORIGINALAIRDATE%"),
5422  m_originalAirDate.toString(Qt::ISODate));
5423  static const std::array<const QString,4> s_timeStr
5424  { "STARTTIME", "ENDTIME", "PROGSTART", "PROGEND", };
5425  const std::array<const QDateTime *,4> time_dtr
5427  for (size_t i = 0; i < s_timeStr.size(); i++)
5428  {
5429  str.replace(QString("%%1%").arg(s_timeStr[i]),
5430  (time_dtr[i]->toLocalTime()).toString("yyyyMMddhhmmss"));
5431  str.replace(QString("%%1ISO%").arg(s_timeStr[i]),
5432  (time_dtr[i]->toLocalTime()).toString(Qt::ISODate));
5433  str.replace(QString("%%1UTC%").arg(s_timeStr[i]),
5434  time_dtr[i]->toString("yyyyMMddhhmmss"));
5435  str.replace(QString("%%1ISOUTC%").arg(s_timeStr[i]),
5436  time_dtr[i]->toString(Qt::ISODate));
5437  }
5438  str.replace(QString("%RECORDEDID%"), QString::number(m_recordedId));
5439 }
5440 
5441 QMap<QString,uint32_t> ProgramInfo::QueryInUseMap(void)
5442 {
5443  QMap<QString, uint32_t> inUseMap;
5444  QDateTime oneHourAgo = MythDate::current().addSecs(-61 * 60);
5445 
5446  MSqlQuery query(MSqlQuery::InitCon());
5447 
5448  query.prepare("SELECT DISTINCT chanid, starttime, recusage "
5449  "FROM inuseprograms WHERE lastupdatetime >= :ONEHOURAGO");
5450  query.bindValue(":ONEHOURAGO", oneHourAgo);
5451 
5452  if (!query.exec())
5453  return inUseMap;
5454 
5455  while (query.next())
5456  {
5457  QString inUseKey = ProgramInfo::MakeUniqueKey(
5458  query.value(0).toUInt(),
5459  MythDate::as_utc(query.value(1).toDateTime()));
5460 
5461  QString inUseForWhat = query.value(2).toString();
5462 
5463  if (!inUseMap.contains(inUseKey))
5464  inUseMap[inUseKey] = 0;
5465 
5466  if (inUseForWhat.contains(kPlayerInUseID))
5467  inUseMap[inUseKey] |= FL_INUSEPLAYING;
5468  else if (inUseForWhat == kRecorderInUseID)
5469  inUseMap[inUseKey] |= FL_INUSERECORDING;
5470  else
5471  inUseMap[inUseKey] |= FL_INUSEOTHER;
5472  }
5473 
5474  return inUseMap;
5475 }
5476 
5477 QMap<QString,bool> ProgramInfo::QueryJobsRunning(int type)
5478 {
5479  QMap<QString,bool> is_job_running;
5480 
5481  MSqlQuery query(MSqlQuery::InitCon());
5482  query.prepare("SELECT chanid, starttime, status FROM jobqueue "
5483  "WHERE type = :TYPE");
5484  query.bindValue(":TYPE", type);
5485  if (!query.exec())
5486  return is_job_running;
5487 
5488  while (query.next())
5489  {
5490  uint chanid = query.value(0).toUInt();
5491  QDateTime recstartts = MythDate::as_utc(query.value(1).toDateTime());
5492  int tmpStatus = query.value(2).toInt();
5493  if ((tmpStatus != /*JOB_UNKNOWN*/ 0x0000) &&
5494  (tmpStatus != /*JOB_QUEUED*/ 0x0001) &&
5495  (!(tmpStatus & /*JOB_DONE*/ 0x0100)))
5496  {
5497  is_job_running[
5498  ProgramInfo::MakeUniqueKey(chanid,recstartts)] = true;
5499  }
5500  }
5501 
5502  return is_job_running;
5503 }
5504 
5506  const QString &tmptable, int recordid)
5507 {
5508  QStringList slist;
5509 
5511  if (sched && tmptable.isEmpty())
5512  {
5513  sched->GetAllPending(slist);
5514  return slist;
5515  }
5516 
5517  if (sched)
5518  {
5519  LOG(VB_GENERAL, LOG_ERR,
5520  "Called from master backend\n\t\t\t"
5521  "with recordid or tmptable, this is not currently supported");
5522  return slist;
5523  }
5524 
5525  slist.push_back(
5526  (tmptable.isEmpty()) ?
5527  QString("QUERY_GETALLPENDING") :
5528  QString("QUERY_GETALLPENDING %1 %2").arg(tmptable).arg(recordid));
5529 
5530  if (!gCoreContext->SendReceiveStringList(slist) || slist.size() < 2)
5531  {
5532  LOG(VB_GENERAL, LOG_ALERT,
5533  "LoadFromScheduler(): Error querying master.");
5534  slist.clear();
5535  }
5536 
5537  return slist;
5538 }
5539 
5540 // NOTE: This may look ugly, and in some ways it is, but it's designed this
5541 // way for with two purposes in mind - Consistent behaviour and speed
5542 // So if you do make changes then carefully test that it doesn't result
5543 // in any regressions in total speed of execution or adversely affect the
5544 // results returned for any of it's users.
5545 static bool FromProgramQuery(const QString &sql, const MSqlBindings &bindings,
5546  MSqlQuery &query, const uint start,
5547  const uint limit, uint &count)
5548 {
5549  count = 0;
5550 
5551  QString columns = QString(
5552  "program.chanid, program.starttime, program.endtime, " // 0-2
5553  "program.title, program.subtitle, program.description, " // 3-5
5554  "program.category, channel.channum, channel.callsign, " // 6-8
5555  "channel.name, program.previouslyshown, channel.commmethod, " // 9-11
5556  "channel.outputfilters, program.seriesid, program.programid, " // 12-14
5557  "program.airdate, program.stars, program.originalairdate, " // 15-17
5558  "program.category_type, oldrecstatus.recordid, " // 18-19
5559  "oldrecstatus.rectype, oldrecstatus.recstatus, " // 20-21
5560  "oldrecstatus.findid, program.videoprop+0, program.audioprop+0, " // 22-24
5561  "program.subtitletypes+0, program.syndicatedepisodenumber, " // 25-26
5562  "program.partnumber, program.parttotal, " // 27-28
5563  "program.season, program.episode, program.totalepisodes "); // 29-31
5564 
5565  QString querystr = QString(
5566  "SELECT %1 "
5567  "FROM program "
5568  "LEFT JOIN channel ON program.chanid = channel.chanid "
5569  "LEFT JOIN oldrecorded AS oldrecstatus ON "
5570  " oldrecstatus.future = 0 AND "
5571  " program.title = oldrecstatus.title AND "
5572  " channel.callsign = oldrecstatus.station AND "
5573  " program.starttime = oldrecstatus.starttime "
5574  ) + sql;
5575 
5576  // If a limit arg was given then append the LIMIT, otherwise set a hard
5577  // limit of 20000.
5578  if (limit > 0)
5579  querystr += QString("LIMIT %1 ").arg(limit);
5580  else if (!querystr.contains(" LIMIT "))
5581  querystr += " LIMIT 20000 "; // For performance reasons we have to have an upper limit
5582 
5583  MSqlBindings::const_iterator it;
5584  // Some end users need to know the total number of matching records,
5585  // irrespective of any LIMIT clause
5586  //
5587  // SQL_CALC_FOUND_ROWS is better than COUNT(*), as COUNT(*) won't work
5588  // with any GROUP BY clauses. COUNT is marginally faster but not enough to
5589  // matter
5590  //
5591  // It's considerably faster in my tests to do a separate query which returns
5592  // no data using SQL_CALC_FOUND_ROWS than it is to use SQL_CALC_FOUND_ROWS
5593  // with the full query. Unfortunate but true.
5594  //
5595  // e.g. Fetching all programs for the next 14 days with a LIMIT of 10 - 220ms
5596  // Same query with SQL_CALC_FOUND_ROWS - 1920ms
5597  // Same query but only one column and with SQL_CALC_FOUND_ROWS - 370ms
5598  // Total to fetch both the count and the data = 590ms vs 1920ms
5599  // Therefore two queries is 1.4 seconds faster than one query.
5600  if (start > 0 || limit > 0)
5601  {
5602  QString countStr = querystr.arg("SQL_CALC_FOUND_ROWS program.chanid");
5603  query.prepare(countStr);
5604  for (it = bindings.begin(); it != bindings.end(); ++it)
5605  {
5606  if (countStr.contains(it.key()))
5607  query.bindValue(it.key(), it.value());
5608  }
5609 
5610  if (!query.exec())
5611  {
5612  MythDB::DBError("LoadFromProgramQuery", query);
5613  return false;
5614  }
5615 
5616  if (query.exec("SELECT FOUND_ROWS()") && query.next())
5617  count = query.value(0).toUInt();
5618  }
5619 
5620  if (start > 0)
5621  querystr += QString("OFFSET %1 ").arg(start);
5622 
5623  querystr = querystr.arg(columns);
5624  query.prepare(querystr);
5625  for (it = bindings.begin(); it != bindings.end(); ++it)
5626  {
5627  if (querystr.contains(it.key()))
5628  query.bindValue(it.key(), it.value());
5629  }
5630 
5631  if (!query.exec())
5632  {
5633  MythDB::DBError("LoadFromProgramQuery", query);
5634  return false;
5635  }
5636 
5637  return true;
5638 }
5639 
5640 bool LoadFromProgram(ProgramList &destination, const QString &where,
5641  const QString &groupBy, const QString &orderBy,
5642  const MSqlBindings &bindings, const ProgramList &schedList)
5643 {
5644  uint count = 0;
5645 
5646  QString queryStr = "";
5647 
5648  if (!where.isEmpty())
5649  queryStr.append(QString("WHERE %1 ").arg(where));
5650 
5651  if (!groupBy.isEmpty())
5652  queryStr.append(QString("GROUP BY %1 ").arg(groupBy));
5653 
5654  if (!orderBy.isEmpty())
5655  queryStr.append(QString("ORDER BY %1 ").arg(orderBy));
5656 
5657  // ------------------------------------------------------------------------
5658 
5659  return LoadFromProgram(destination, queryStr, bindings, schedList, 0, 0, count);
5660 }
5661 
5662 bool LoadFromProgram(ProgramList &destination,
5663  const QString &sql, const MSqlBindings &bindings,
5664  const ProgramList &schedList)
5665 {
5666  uint count = 0;
5667 
5668  QString queryStr = sql;
5669  // ------------------------------------------------------------------------
5670  // FIXME: Remove the following. These all make assumptions about the content
5671  // of the sql passed in, they can end up breaking that query by
5672  // inserting a WHERE clause after a GROUP BY, or a GROUP BY after
5673  // an ORDER BY. These should be part of the sql passed in otherwise
5674  // the caller isn't getting what they asked for and to fix that they
5675  // are forced to include a GROUP BY, ORDER BY or WHERE that they
5676  // do not want
5677  //
5678  // See the new version of this method above. The plan is to convert existing
5679  // users of this method and have them supply all of their own data for the
5680  // queries (no defaults.)
5681 
5682  if (!queryStr.contains("WHERE"))
5683  queryStr += " WHERE deleted IS NULL AND visible > 0 ";
5684 
5685  // NOTE: Any GROUP BY clause with a LIMIT is slow, adding at least
5686  // a couple of seconds to the query execution time
5687 
5688  // TODO: This one seems to be dealing with eliminating duplicate channels (same
5689  // programming, different source), but using GROUP BY for that isn't very
5690  // efficient so another approach is required
5691  if (!queryStr.contains("GROUP BY"))
5692  queryStr += " GROUP BY program.starttime, channel.channum, "
5693  " channel.callsign, program.title ";
5694 
5695  if (!queryStr.contains("ORDER BY"))
5696  {
5697  queryStr += " ORDER BY program.starttime, ";
5698  QString chanorder =
5699  gCoreContext->GetSetting("ChannelOrdering", "channum");
5700  if (chanorder != "channum")
5701  queryStr += chanorder + " ";
5702  else // approximation which the DB can handle
5703  queryStr += "atsc_major_chan,atsc_minor_chan,channum,callsign ";
5704  }
5705 
5706  // ------------------------------------------------------------------------
5707 
5708  return LoadFromProgram(destination, queryStr, bindings, schedList, 0, 0, count);
5709 }
5710 
5711 bool LoadFromProgram( ProgramList &destination,
5712  const QString &sql, const MSqlBindings &bindings,
5713  const ProgramList &schedList,
5714  const uint start, const uint limit, uint &count)
5715 {
5716  destination.clear();
5717 
5718  if (sql.contains(" OFFSET", Qt::CaseInsensitive))
5719  {
5720  LOG(VB_GENERAL, LOG_WARNING, "LoadFromProgram(): SQL contains OFFSET "
5721  "clause, caller should be updated to use "
5722  "start parameter instead");
5723  }
5724 
5725  if (sql.contains(" LIMIT", Qt::CaseInsensitive))
5726  {
5727  LOG(VB_GENERAL, LOG_WARNING, "LoadFromProgram(): SQL contains LIMIT "
5728  "clause, caller should be updated to use "
5729  "limit parameter instead");
5730  }
5731 
5732  MSqlQuery query(MSqlQuery::InitCon());
5733  query.setForwardOnly(true);
5734  if (!FromProgramQuery(sql, bindings, query, start, limit, count))
5735  return false;
5736 
5737  if (count == 0)
5738  count = query.size();
5739 
5740  while (query.next())
5741  {
5742  destination.push_back(
5743  new ProgramInfo(
5744  query.value(3).toString(), // title
5745  QString(), // sortTitle
5746  query.value(4).toString(), // subtitle
5747  QString(), // sortSubtitle
5748  query.value(5).toString(), // description
5749  query.value(26).toString(), // syndicatedepisodenumber
5750  query.value(6).toString(), // category
5751 
5752  query.value(0).toUInt(), // chanid
5753  query.value(7).toString(), // channum
5754  query.value(8).toString(), // chansign
5755  query.value(9).toString(), // channame
5756  query.value(12).toString(), // chanplaybackfilters
5757 
5758  MythDate::as_utc(query.value(1).toDateTime()), // startts
5759  MythDate::as_utc(query.value(2).toDateTime()), // endts
5760  MythDate::as_utc(query.value(1).toDateTime()), // recstartts
5761  MythDate::as_utc(query.value(2).toDateTime()), // recendts
5762 
5763  query.value(13).toString(), // seriesid
5764  query.value(14).toString(), // programid
5765  string_to_myth_category_type(query.value(18).toString()), // catType
5766 
5767  query.value(16).toFloat(), // stars
5768  query.value(15).toUInt(), // year
5769  query.value(27).toUInt(), // partnumber
5770  query.value(28).toUInt(), // parttotal
5771  query.value(17).toDate(), // originalAirDate
5772  RecStatus::Type(query.value(21).toInt()), // recstatus
5773  query.value(19).toUInt(), // recordid
5774  RecordingType(query.value(20).toInt()), // rectype
5775  query.value(22).toUInt(), // findid
5776 
5777  query.value(11).toInt() == COMM_DETECT_COMMFREE, // commfree
5778  query.value(10).toBool(), // repeat
5779  query.