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