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