MythTV  master
playbackbox.cpp
Go to the documentation of this file.
1 #include "playbackbox.h"
2 
3 // C++
4 #include <array>
5 
6 // QT
7 #include <QCoreApplication>
8 #include <QDateTime>
9 #include <QLocale>
10 #include <QTimer>
11 #include <QMap>
12 
13 // MythTV
14 #ifdef _MSC_VER
15 # include "libmythbase/compat.h" // for random
16 #endif
19 #include "libmythbase/mythdate.h"
20 #include "libmythbase/mythdb.h"
21 #include "libmythbase/mythdbcon.h"
22 #include "libmythbase/mythevent.h" // for MythEvent, etc
24 #include "libmythbase/mythrandom.h"
26 #include "libmythbase/programtypes.h" // for AudioProps, SubtitleTypes, etc
28 #include "libmythbase/stringutil.h"
30 #include "libmythtv/playgroup.h"
34 #include "libmythtv/tv.h"
35 #include "libmythtv/tv_actions.h" // for ACTION_LISTRECORDEDEPISODES, etc
37 #include "libmythui/mythmainwindow.h" // for GetMythMainWindow, etc
38 #include "libmythui/mythnotificationcenter.h" // for ShowNotificationError, etc
40 #include "libmythui/mythscreenstack.h" // for MythScreenStack
41 #include "libmythui/mythuiactions.h" // for ACTION_1
42 #include "libmythui/mythuibutton.h"
45 #include "libmythui/mythuiimage.h"
49 #include "libmythui/mythuitext.h"
51 
52 // MythFrontend
53 #include "playbackboxlistitem.h"
54 
55 #define LOC QString("PlaybackBox: ")
56 #define LOC_WARN QString("PlaybackBox Warning: ")
57 #define LOC_ERR QString("PlaybackBox Error: ")
58 
59 static const QString sLocation = "Playback Box";
60 
61 static int comp_programid(const ProgramInfo *a, const ProgramInfo *b)
62 {
63  if (a->GetProgramID() == b->GetProgramID())
64  return (a->GetRecordingStartTime() <
65  b->GetRecordingStartTime() ? 1 : -1);
66  return (a->GetProgramID() < b->GetProgramID() ? 1 : -1);
67 }
68 
69 static int comp_programid_rev(const ProgramInfo *a, const ProgramInfo *b)
70 {
71  if (a->GetProgramID() == b->GetProgramID())
72  return (a->GetRecordingStartTime() >
73  b->GetRecordingStartTime() ? 1 : -1);
74  return (a->GetProgramID() > b->GetProgramID() ? 1 : -1);
75 }
76 
77 static int comp_originalAirDate(const ProgramInfo *a, const ProgramInfo *b)
78 {
79  QDate dt1 = (a->GetOriginalAirDate().isValid()) ?
80  a->GetOriginalAirDate() : a->GetScheduledStartTime().date();
81  QDate dt2 = (b->GetOriginalAirDate().isValid()) ?
82  b->GetOriginalAirDate() : b->GetScheduledStartTime().date();
83 
84  if (dt1 == dt2)
85  return (a->GetRecordingStartTime() <
86  b->GetRecordingStartTime() ? 1 : -1);
87  return (dt1 < dt2 ? 1 : -1);
88 }
89 
90 static int comp_originalAirDate_rev(const ProgramInfo *a, const ProgramInfo *b)
91 {
92  QDate dt1 = (a->GetOriginalAirDate().isValid()) ?
93  a->GetOriginalAirDate() : a->GetScheduledStartTime().date();
94  QDate dt2 = (b->GetOriginalAirDate().isValid()) ?
95  b->GetOriginalAirDate() : b->GetScheduledStartTime().date();
96 
97  if (dt1 == dt2)
98  return (a->GetRecordingStartTime() >
99  b->GetRecordingStartTime() ? 1 : -1);
100  return (dt1 > dt2 ? 1 : -1);
101 }
102 
103 static int comp_recpriority2(const ProgramInfo *a, const ProgramInfo *b)
104 {
106  return (a->GetRecordingStartTime() <
107  b->GetRecordingStartTime() ? 1 : -1);
108  return (a->GetRecordingPriority2() <
109  b->GetRecordingPriority2() ? 1 : -1);
110 }
111 
112 static int comp_recordDate(const ProgramInfo *a, const ProgramInfo *b)
113 {
114  if (a->GetScheduledStartTime().date() == b->GetScheduledStartTime().date())
115  return (a->GetRecordingStartTime() <
116  b->GetRecordingStartTime() ? 1 : -1);
117  return (a->GetScheduledStartTime().date() <
118  b->GetScheduledStartTime().date() ? 1 : -1);
119 }
120 
121 static int comp_recordDate_rev(const ProgramInfo *a, const ProgramInfo *b)
122 {
123  if (a->GetScheduledStartTime().date() == b->GetScheduledStartTime().date())
124  return (a->GetRecordingStartTime() >
125  b->GetRecordingStartTime() ? 1 : -1);
126  return (a->GetScheduledStartTime().date() >
127  b->GetScheduledStartTime().date() ? 1 : -1);
128 }
129 
130 static int comp_season(const ProgramInfo *a, const ProgramInfo *b)
131 {
132  if (a->GetSeason() == 0 || b->GetSeason() == 0)
133  return comp_originalAirDate(a, b);
134  if (a->GetSeason() != b->GetSeason())
135  return (a->GetSeason() < b->GetSeason() ? 1 : -1);
136  if (a->GetEpisode() == 0 && b->GetEpisode() == 0)
137  return comp_originalAirDate(a, b);
138  return (a->GetEpisode() < b->GetEpisode() ? 1 : -1);
139 }
140 
141 static int comp_season_rev(const ProgramInfo *a, const ProgramInfo *b)
142 {
143  if (a->GetSeason() == 0 || b->GetSeason() == 0)
144  return comp_originalAirDate_rev(a, b);
145  if (a->GetSeason() != b->GetSeason())
146  return (a->GetSeason() > b->GetSeason() ? 1 : -1);
147  if (a->GetEpisode() == 0 && b->GetEpisode() == 0)
148  return comp_originalAirDate_rev(a, b);
149  return (a->GetEpisode() > b->GetEpisode() ? 1 : -1);
150 }
151 
153  const ProgramInfo *a, const ProgramInfo *b)
154 {
155  return comp_programid(a, b) < 0;
156 }
157 
159  const ProgramInfo *a, const ProgramInfo *b)
160 {
161  return comp_programid_rev(a, b) < 0;
162 }
163 
165  const ProgramInfo *a, const ProgramInfo *b)
166 {
167  return comp_originalAirDate(a, b) < 0;
168 }
169 
171  const ProgramInfo *a, const ProgramInfo *b)
172 {
173  return comp_originalAirDate_rev(a, b) < 0;
174 }
175 
177  const ProgramInfo *a, const ProgramInfo *b)
178 {
179  return comp_recpriority2(a, b) < 0;
180 }
181 
183  const ProgramInfo *a, const ProgramInfo *b)
184 {
185  return comp_recordDate(a, b) < 0;
186 }
187 
189  const ProgramInfo *a, const ProgramInfo *b)
190 {
191  return comp_recordDate_rev(a, b) < 0;
192 }
193 
195  const ProgramInfo *a, const ProgramInfo *b)
196 {
197  return comp_season(a, b) < 0;
198 }
199 
201  const ProgramInfo *a, const ProgramInfo *b)
202 {
203  return comp_season_rev(a, b) < 0;
204 }
205 
206 static const std::array<const uint,3> s_artDelay
208 
210  PlaybackBox::ViewMask toggle)
211 {
212  // can only toggle a single bit at a time
213  if ((mask & toggle))
214  return (PlaybackBox::ViewMask)(mask & ~toggle);
215  return (PlaybackBox::ViewMask)(mask | toggle);
216 }
217 
218 static QString construct_sort_title(
219  QString title, const QString& sortTitle, PlaybackBox::ViewMask viewmask,
220  PlaybackBox::ViewTitleSort sortType, int recpriority)
221 {
222  if (title.isEmpty())
223  return title;
224 
225  QString sTitle = sortTitle.isEmpty()
226  ? title : sortTitle + " - " + title;
227 
228  if (viewmask == PlaybackBox::VIEW_TITLES &&
230  {
231  // Also incorporate recpriority (reverse numeric sort). In
232  // case different episodes of a recording schedule somehow
233  // have different recpriority values (e.g., manual fiddling
234  // with database), the title will appear once for each
235  // distinct recpriority value among its episodes.
236  //
237  // Deal with QMap sorting. Positive recpriority values have a
238  // '+' prefix (QMap alphabetically sorts before '-'). Positive
239  // recpriority values are "inverted" by subtracting them from
240  // 1000, so that high recpriorities are sorted first (QMap
241  // alphabetically). For example:
242  //
243  // recpriority => sort key
244  // 95 +905
245  // 90 +910
246  // 89 +911
247  // 1 +999
248  // 0 -000
249  // -5 -005
250  // -10 -010
251  // -99 -099
252 
253  QString sortprefix;
254  if (recpriority > 0)
255  sortprefix = QString("+%1").arg(1000 - recpriority, 3, 10, QChar('0'));
256  else
257  sortprefix = QString("-%1").arg(-recpriority, 3, 10, QChar('0'));
258 
259  sTitle = sortprefix + '-' + sTitle;
260  }
261  return sTitle;
262 }
263 
264 static QString extract_main_state(const ProgramInfo &pginfo, const TV *player)
265 {
266  QString state("normal");
267  if (pginfo.GetFilesize() == 0)
268  state = "error";
269  else if (pginfo.GetRecordingStatus() == RecStatus::Recording ||
272  state = "running";
273 
274  if (((pginfo.GetRecordingStatus() != RecStatus::Recording) &&
275  (pginfo.GetAvailableStatus() != asAvailable) &&
276  (pginfo.GetAvailableStatus() != asNotYetAvailable)) ||
277  (player && player->IsSameProgram(&pginfo)))
278  {
279  state = "disabled";
280  }
281 
282  if ((state == "normal" || state == "running") &&
283  pginfo.GetVideoProperties() & VID_DAMAGED)
284  {
285  state = "warning";
286  }
287 
288  return state;
289 }
290 
292 {
293  QString job = "default";
294 
295  if (pginfo.GetRecordingStatus() == RecStatus::Recording ||
298  job = "recording";
300  JOB_TRANSCODE, pginfo.GetChanID(),
301  pginfo.GetRecordingStartTime()))
302  job = "transcoding";
304  JOB_COMMFLAG, pginfo.GetChanID(),
305  pginfo.GetRecordingStartTime()))
306  job = "commflagging";
307 
308  return job;
309 }
310 
312 {
313  // commflagged can be yes, no or processing
315  pginfo.GetRecordingStartTime()))
316  return "running";
318  pginfo.GetRecordingStartTime()))
319  return "queued";
320 
321  return ((pginfo.GetProgramFlags() & FL_COMMFLAG) ? "yes" : "no");
322 }
323 
324 
325 static QString extract_subtitle(
326  const ProgramInfo &pginfo, const QString &groupname)
327 {
328  QString subtitle;
329  if (groupname != pginfo.GetTitle().toLower())
330  {
331  subtitle = pginfo.toString(ProgramInfo::kTitleSubtitle, " - ");
332  }
333  else
334  {
335  subtitle = pginfo.GetSubtitle();
336  if (subtitle.trimmed().isEmpty())
337  subtitle = pginfo.GetTitle();
338  }
339  return subtitle;
340 }
341 
342 static void push_onto_del(QStringList &list, const ProgramInfo &pginfo)
343 {
344  list.clear();
345  list.push_back(QString::number(pginfo.GetRecordingID()));
346  list.push_back(QString() /* force Delete */);
347  list.push_back(QString()); /* forget history */
348 }
349 
350 static bool extract_one_del(QStringList &list, uint &recordingID)
351 {
352  if (list.size() < 3)
353  {
354  list.clear();
355  return false;
356  }
357 
358  recordingID = list[0].toUInt();
359 
360  list.pop_front();
361  list.pop_front();
362  list.pop_front();
363 
364  if (recordingID == 0U) {
365  LOG(VB_GENERAL, LOG_ERR, LOC + "extract_one_del() invalid entry");
366  return false;
367  }
368  return true;
369 }
370 
371 void * PlaybackBox::RunPlaybackBox(void * player, bool showTV)
372 {
374 
375  auto *pbb = new PlaybackBox(mainStack,"playbackbox", (TV *)player, showTV);
376 
377  if (pbb->Create())
378  mainStack->AddScreen(pbb);
379  else
380  delete pbb;
381 
382  return nullptr;
383 }
384 
385 PlaybackBox::PlaybackBox(MythScreenStack *parent, const QString& name,
386  TV *player, bool /*showTV*/)
387  : ScheduleCommon(parent, name),
388  // Recording Group settings
389  m_groupDisplayName(ProgramInfo::i18n("All Programs")),
390  m_recGroup("All Programs"),
391  m_watchGroupName(tr("Watch List")),
392  m_watchGroupLabel(m_watchGroupName.toLower()),
393 
394  // Other state
395  m_programInfoCache(this),
396  // Other
397  m_helper(this)
398 {
399  for (size_t i = 0; i < kNumArtImages; i++)
400  {
401  m_artImage[i] = nullptr;
402  m_artTimer[i] = new QTimer(this);
403  m_artTimer[i]->setSingleShot(true);
404  }
405 
406  m_recGroup = gCoreContext->GetSetting("DisplayRecGroup",
407  "All Programs");
408  int pbOrder = gCoreContext->GetNumSetting("PlayBoxOrdering", 3);
409  // Split out sort order modes, wacky order for backward compatibility
410  m_listOrder = (pbOrder >> 1) ^ (m_allOrder = pbOrder & 1);
411  m_watchListStart = gCoreContext->GetBoolSetting("PlaybackWLStart", false);
412 
413  m_watchListAutoExpire= gCoreContext->GetBoolSetting("PlaybackWLAutoExpire", false);
414  m_watchListMaxAge = gCoreContext->GetNumSetting("PlaybackWLMaxAge", 60);
416  std::chrono::days(2));
417 
418  bool displayCat = gCoreContext->GetBoolSetting("DisplayRecGroupIsCategory", false);
419 
421  "DisplayGroupDefaultViewMask",
423 
424  // Translate these external settings into mask values
425  if (gCoreContext->GetBoolSetting("PlaybackWatchList", true) &&
426  ((m_viewMask & VIEW_WATCHLIST) == 0))
427  {
429  gCoreContext->SaveSetting("DisplayGroupDefaultViewMask", (int)m_viewMask);
430  }
431  else if (! gCoreContext->GetBoolSetting("PlaybackWatchList", true) &&
432  ((m_viewMask & VIEW_WATCHLIST) != 0))
433  {
435  gCoreContext->SaveSetting("DisplayGroupDefaultViewMask", (int)m_viewMask);
436  }
437 
438  // This setting is deprecated in favour of viewmask, this just ensures the
439  // that it is converted over when upgrading from earlier versions
440  if (gCoreContext->GetBoolSetting("LiveTVInAllPrograms",false) &&
441  ((m_viewMask & VIEW_LIVETVGRP) == 0))
442  {
444  gCoreContext->SaveSetting("DisplayGroupDefaultViewMask", (int)m_viewMask);
445  }
446 
447  if (gCoreContext->GetBoolSetting("MasterBackendOverride", false))
449 
450  if (player)
451  {
452  m_player = player;
453  m_player->IncrRef();
454  QString tmp = m_player->GetRecordingGroup();
455  if (!tmp.isEmpty())
456  m_recGroup = tmp;
457  }
458 
459  // recording group stuff
460  m_recGroupIdx = -1;
461  m_recGroupType.clear();
463  (displayCat && m_recGroup != "All Programs") ? "category" : "recgroup";
465 
467 
468  // misc setup
469  gCoreContext->addListener(this);
470 
471  m_popupStack = GetMythMainWindow()->GetStack("popup stack");
472 }
473 
475 {
478 
479  for (size_t i = 0; i < kNumArtImages; i++)
480  {
481  m_artTimer[i]->disconnect(this);
482  m_artTimer[i] = nullptr;
483  m_artImage[i] = nullptr;
484  }
485 
486  if (m_player)
487  {
489  m_player->DecrRef();
490  }
491 }
492 
494 {
495  if (!LoadWindowFromXML("recordings-ui.xml", "watchrecordings", this))
496  return false;
497 
498  m_recgroupList = dynamic_cast<MythUIButtonList *> (GetChild("recgroups"));
499  m_groupList = dynamic_cast<MythUIButtonList *> (GetChild("groups"));
500  m_recordingList = dynamic_cast<MythUIButtonList *> (GetChild("recordings"));
501 
502  m_noRecordingsText = dynamic_cast<MythUIText *> (GetChild("norecordings"));
503 
504  m_previewImage = dynamic_cast<MythUIImage *>(GetChild("preview"));
505  m_recordedProgress = dynamic_cast<MythUIProgressBar *>(GetChild("recordedprogressbar"));
506  m_watchedProgress = dynamic_cast<MythUIProgressBar *>(GetChild("watchedprogressbar"));
507  m_artImage[kArtworkFanart] = dynamic_cast<MythUIImage*>(GetChild("fanart"));
508  m_artImage[kArtworkBanner] = dynamic_cast<MythUIImage*>(GetChild("banner"));
509  m_artImage[kArtworkCoverart]= dynamic_cast<MythUIImage*>(GetChild("coverart"));
510 
511  if (!m_recordingList || !m_groupList)
512  {
513  LOG(VB_GENERAL, LOG_ERR, LOC +
514  "Theme is missing critical theme elements.");
515  return false;
516  }
517 
518  if (m_recgroupList)
519  {
520  if (gCoreContext->GetBoolSetting("RecGroupsFocusable", false))
521  {
524  }
525  else
526  {
528  }
529  }
530 
534  this, &PlaybackBox::SwitchList);
538  this, qOverload<>(&PlaybackBox::PlayFromAnyMark));
540  this, &PlaybackBox::ItemVisible);
542  this, &PlaybackBox::ItemLoaded);
543 
544  // connect up timers...
548 
549  BuildFocusList();
552 
553  if (m_player)
554  emit m_player->RequestEmbedding(true);
555  return true;
556 }
557 
559 {
562 }
563 
565 {
566  m_groupList->SetLCDTitles(tr("Groups"));
567  m_recordingList->SetLCDTitles(tr("Recordings"),
568  "titlesubtitle|shortdate|starttime");
569 
570  m_recordingList->SetSearchFields("titlesubtitle");
571 
572  if (gCoreContext->GetNumSetting("QueryInitialFilter", 0) == 1)
573  showGroupFilter();
574  else if (!m_player)
576  else
577  {
578  UpdateUILists();
579 
580  if ((m_titleList.size() <= 1) && (m_progsInDB > 0))
581  {
582  m_recGroup.clear();
583  showGroupFilter();
584  }
585  }
586 
587  if (!gCoreContext->GetBoolSetting("PlaybackBoxStartInTitle", false))
589 }
590 
592 {
593  if (GetFocusWidget() == m_groupList)
595  else if (GetFocusWidget() == m_recordingList)
597 }
598 
599 void PlaybackBox::displayRecGroup(const QString &newRecGroup)
600 {
601  m_groupSelected = true;
602 
603  QString password = getRecGroupPassword(newRecGroup);
604 
605  m_newRecGroup = newRecGroup;
606  if (m_curGroupPassword != password && !password.isEmpty())
607  {
608  MythScreenStack *popupStack =
609  GetMythMainWindow()->GetStack("popup stack");
610 
611  QString label = tr("Password for group '%1':").arg(newRecGroup);
612 
613  auto *pwd = new MythTextInputDialog(popupStack, label, FilterNone, true);
614 
615  connect(pwd, &MythTextInputDialog::haveResult,
617  connect(pwd, &MythScreenType::Exiting,
619 
620  m_passwordEntered = false;
621 
622  if (pwd->Create())
623  popupStack->AddScreen(pwd, false);
624 
625  return;
626  }
627 
628  setGroupFilter(newRecGroup);
629 }
630 
631 void PlaybackBox::checkPassword(const QString &password)
632 {
633  if (password == getRecGroupPassword(m_newRecGroup))
634  {
635  m_curGroupPassword = password;
636  m_passwordEntered = true;
638  }
639 }
640 
642 {
643  if (!m_passwordEntered &&
645  showGroupFilter();
646 }
647 
648 void PlaybackBox::updateGroupInfo(const QString &groupname,
649  const QString &grouplabel)
650 {
651  InfoMap infoMap;
652  QString desc;
653 
654  infoMap["group"] = m_groupDisplayName;
655  infoMap["title"] = grouplabel;
656  infoMap["show"] =
657  groupname.isEmpty() ? ProgramInfo::i18n("All Programs") : grouplabel;
658  int countInGroup = m_progLists[groupname].size();
659 
661  {
662  if (!groupname.isEmpty() && !m_progLists[groupname].empty())
663  {
664  ProgramInfo *pginfo = *m_progLists[groupname].begin();
665 
666  QString fn = m_helper.LocateArtwork(
667  pginfo->GetInetRef(), pginfo->GetSeason(), kArtworkFanart, nullptr, groupname);
668 
669  if (fn.isEmpty())
670  {
671  m_artTimer[kArtworkFanart]->stop();
672  m_artImage[kArtworkFanart]->Reset();
673  }
674  else if (m_artImage[kArtworkFanart]->GetFilename() != fn)
675  {
676  m_artImage[kArtworkFanart]->SetFilename(fn);
678  }
679  }
680  else
681  {
682  m_artImage[kArtworkFanart]->Reset();
683  }
684  }
685 
686 
687  if (countInGroup >= 1)
688  {
689  ProgramList group = m_progLists[groupname];
690  float groupSize = 0.0;
691 
692  for (auto *info : group)
693  {
694  if (info)
695  {
696  uint64_t filesize = info->GetFilesize();
697 // This query should be unnecessary if the ProgramInfo Updater is working
698 // if (filesize == 0 || info->GetRecordingStatus() == RecStatus::Recording)
699 // {
700 // filesize = info->QueryFilesize();
701 // info->SetFilesize(filesize);
702 // }
703  groupSize += filesize;
704  }
705  }
706 
707  desc = tr("There is/are %n recording(s) in this display "
708  "group, which consume(s) %1 GiB.", "", countInGroup)
709  .arg(groupSize / 1024.0F / 1024.0F / 1024.0F, 0, 'f', 2);
710  }
711  else
712  {
713  desc = tr("There is no recording in this display group.");
714  }
715 
716  infoMap["description"] = desc;
717  infoMap["rec_count"] = QString("%1").arg(countInGroup);
718 
720  SetTextFromMap(infoMap);
721  m_currentMap = infoMap;
722 
723  MythUIStateType *ratingState = dynamic_cast<MythUIStateType*>
724  (GetChild("ratingstate"));
725  if (ratingState)
726  ratingState->Reset();
727 
728  MythUIStateType *jobState = dynamic_cast<MythUIStateType*>
729  (GetChild("jobstate"));
730  if (jobState)
731  jobState->Reset();
732 
733  if (m_previewImage)
735 
737  m_artImage[kArtworkBanner]->Reset();
738 
740  m_artImage[kArtworkCoverart]->Reset();
741 
742  updateIcons();
743 }
744 
746  bool force_preview_reload)
747 {
748  if (!pginfo)
749  return;
750 
751  MythUIButtonListItem *item =
752  m_recordingList->GetItemByData(QVariant::fromValue(pginfo));
753 
754  if (item)
755  {
756  MythUIButtonListItem *sel_item =
758  UpdateUIListItem(item, item == sel_item, force_preview_reload);
759  }
760  else
761  {
762  LOG(VB_GENERAL, LOG_DEBUG, LOC +
763  QString("UpdateUIListItem called with a title unknown "
764  "to us in m_recordingList\n\t\t\t%1")
765  .arg(pginfo->toString(ProgramInfo::kTitleSubtitle)));
766  }
767 }
768 
769 static const std::array<const std::string,9> disp_flags
770 {
771  "playlist", "watched", "preserve",
772  "cutlist", "autoexpire", "editing",
773  "bookmark", "inuse", "transcoded"
774 };
775 
777 {
778  std::array<bool,disp_flags.size()> disp_flag_stat {};
779 
780  disp_flag_stat[0] = m_playList.contains(pginfo->GetRecordingID());
781  disp_flag_stat[1] = pginfo->IsWatched();
782  disp_flag_stat[2] = pginfo->IsPreserved();
783  disp_flag_stat[3] = pginfo->HasCutlist();
784  disp_flag_stat[4] = pginfo->IsAutoExpirable();
785  disp_flag_stat[5] = ((pginfo->GetProgramFlags() & FL_EDITING) != 0U);
786  disp_flag_stat[6] = pginfo->IsBookmarkSet();
787  disp_flag_stat[7] = pginfo->IsInUsePlaying();
788  disp_flag_stat[8] = ((pginfo->GetProgramFlags() & FL_TRANSCODED) != 0U);
789 
790  for (size_t i = 0; i < disp_flags.size(); ++i)
791  item->DisplayState(disp_flag_stat[i] ? "yes" : "no",
792  QString::fromStdString(disp_flags[i]));
793 }
794 
796  bool is_sel, bool force_preview_reload)
797 {
798  if (!item)
799  return;
800 
801  auto *pginfo = item->GetData().value<ProgramInfo *>();
802 
803  if (!pginfo)
804  return;
805 
806  QString state = extract_main_state(*pginfo, m_player);
807 
808  // Update the text, e.g. Title or subtitle may have been changed on another
809  // frontend
811  {
812  InfoMap infoMap;
813  pginfo->ToMap(infoMap);
814  item->SetTextFromMap(infoMap);
815 
816  QString groupname =
817  m_groupList->GetItemCurrent()->GetData().toString();
818 
819  QString tempSubTitle = extract_subtitle(*pginfo, groupname);
820 
821  if (groupname == pginfo->GetTitle().toLower())
822  {
823  item->SetText(tempSubTitle, "titlesubtitle");
824  // titlesubtitle will just have the subtitle, so put the full
825  // string in titlesubtitlefull, when a theme can then "depend" on.
826  item->SetText(pginfo->toString(ProgramInfo::kTitleSubtitle, " - "),
827  "titlesubtitlefull");
828  }
829  }
830 
831  // Recording and availability status
832  item->SetFontState(state);
833  item->DisplayState(state, "status");
834 
835  // Job status (recording, transcoding, flagging)
836  QString job = extract_job_state(*pginfo);
837  item->DisplayState(job, "jobstate");
838 
839  // Flagging status (queued, running, no, yes)
840  item->DisplayState(extract_commflag_state(*pginfo), "commflagged");
841 
842  SetItemIcons(item, pginfo);
843 
844  QString rating = QString::number(pginfo->GetStars(10));
845 
846  item->DisplayState(rating, "ratingstate");
847 
848  QString oldimgfile = item->GetImageFilename("preview");
849  if (oldimgfile.isEmpty() || force_preview_reload)
850  m_previewTokens.insert(m_helper.GetPreviewImage(*pginfo));
851 
852  if ((GetFocusWidget() == m_recordingList) && is_sel)
853  {
854  InfoMap infoMap;
855 
856  pginfo->CalculateProgress(pginfo->QueryLastPlayPos());
857 
858  pginfo->ToMap(infoMap);
859  infoMap["group"] = m_groupDisplayName;
861  SetTextFromMap(infoMap);
862  m_currentMap = infoMap;
863 
864  MythUIStateType *ratingState = dynamic_cast<MythUIStateType*>
865  (GetChild("ratingstate"));
866  if (ratingState)
867  ratingState->DisplayState(rating);
868 
869  MythUIStateType *jobState = dynamic_cast<MythUIStateType*>
870  (GetChild("jobstate"));
871  if (jobState)
872  jobState->DisplayState(job);
873 
874  if (m_previewImage)
875  {
876  m_previewImage->SetFilename(oldimgfile);
877  m_previewImage->Load(true, true);
878  }
879 
880  if (m_recordedProgress)
881  m_recordedProgress->Set(0, 100, pginfo->GetRecordedPercent());
882  if (m_watchedProgress)
883  m_watchedProgress->Set(0, 100, pginfo->GetWatchedPercent());
884 
885  // Handle artwork
886  QString arthost;
887  for (size_t i = 0; i < kNumArtImages; i++)
888  {
889  if (!m_artImage[i])
890  continue;
891 
892  if (arthost.isEmpty())
893  {
894  arthost = (!m_artHostOverride.isEmpty()) ?
895  m_artHostOverride : pginfo->GetHostname();
896  }
897 
898  QString fn = m_helper.LocateArtwork(
899  pginfo->GetInetRef(), pginfo->GetSeason(),
900  (VideoArtworkType)i, pginfo);
901 
902  if (fn.isEmpty())
903  {
904  m_artTimer[i]->stop();
905  m_artImage[i]->Reset();
906  }
907  else if (m_artImage[i]->GetFilename() != fn)
908  {
909  m_artImage[i]->SetFilename(fn);
910  m_artTimer[i]->start(s_artDelay[i]);
911  }
912  }
913 
914  updateIcons(pginfo);
915  }
916 }
917 
919 {
920  auto *pginfo = item->GetData().value<ProgramInfo*>();
921  if (item->GetText("is_item_initialized").isNull())
922  {
923  QMap<AudioProps, QString> audioFlags;
924  audioFlags[AUD_DOLBY] = "dolby";
925  audioFlags[AUD_SURROUND] = "surround";
926  audioFlags[AUD_STEREO] = "stereo";
927  audioFlags[AUD_MONO] = "mono";
928 
929  QMap<VideoProps, QString> codecFlags;
930  codecFlags[VID_MPEG2] = "mpeg2";
931  codecFlags[VID_AVC] = "avc";
932  codecFlags[VID_HEVC] = "hevc";
933 
934  QMap<SubtitleProps, QString> subtitleFlags;
935  subtitleFlags[SUB_SIGNED] = "deafsigned";
936  subtitleFlags[SUB_ONSCREEN] = "onscreensub";
937  subtitleFlags[SUB_NORMAL] = "subtitles";
938  subtitleFlags[SUB_HARDHEAR] = "cc";
939 
940  QString groupname =
941  m_groupList->GetItemCurrent()->GetData().toString();
942 
943  QString state = extract_main_state(*pginfo, m_player);
944 
945  item->SetFontState(state);
946 
947  InfoMap infoMap;
948  pginfo->ToMap(infoMap);
949  item->SetTextFromMap(infoMap);
950 
951  QString tempSubTitle = extract_subtitle(*pginfo, groupname);
952 
953  if (groupname == pginfo->GetTitle().toLower())
954  {
955  item->SetText(tempSubTitle, "titlesubtitle");
956  // titlesubtitle will just have the subtitle, so put the full
957  // string in titlesubtitlefull, when a theme can then "depend" on.
958  item->SetText(pginfo->toString(ProgramInfo::kTitleSubtitle, " - "),
959  "titlesubtitlefull");
960  }
961 
962  item->DisplayState(state, "status");
963 
964  item->DisplayState(QString::number(pginfo->GetStars(10)),
965  "ratingstate");
966 
967  SetItemIcons(item, pginfo);
968 
969  QMap<AudioProps, QString>::iterator ait;
970  for (ait = audioFlags.begin(); ait != audioFlags.end(); ++ait)
971  {
972  if (pginfo->GetAudioProperties() & ait.key())
973  item->DisplayState(ait.value(), "audioprops");
974  }
975 
976  uint props = pginfo->GetVideoProperties();
977 
978  QMap<VideoProps, QString>::iterator cit;
979  for (cit = codecFlags.begin(); cit != codecFlags.end(); ++cit)
980  {
981  if (props & cit.key())
982  {
983  item->DisplayState(cit.value(), "videoprops");
984  item->DisplayState(cit.value(), "codecprops");
985  }
986  }
987 
988  if (props & VID_PROGRESSIVE)
989  {
990  item->DisplayState("progressive", "videoprops");
991  if (props & VID_4K)
992  item->DisplayState("uhd4Kp", "videoprops");
993  if (props & VID_1080)
994  item->DisplayState("hd1080p", "videoprops");
995  }
996  else
997  {
998  if (props & VID_4K)
999  item->DisplayState("uhd4Ki", "videoprops");
1000  if (props & VID_1080)
1001  item->DisplayState("hd1080i", "videoprops");
1002  }
1003  if (props & VID_720)
1004  item->DisplayState("hd720", "videoprops");
1005  if (!(props & (VID_4K | VID_1080 | VID_720)))
1006  {
1007  if (props & VID_HDTV)
1008  item->DisplayState("hdtv", "videoprops");
1009  else if (props & VID_WIDESCREEN)
1010  item->DisplayState("widescreen", "videoprops");
1011  else
1012  item->DisplayState("sd", "videoprops");
1013  }
1014 
1015  QMap<SubtitleProps, QString>::iterator sit;
1016  for (sit = subtitleFlags.begin(); sit != subtitleFlags.end(); ++sit)
1017  {
1018  if (pginfo->GetSubtitleType() & sit.key())
1019  item->DisplayState(sit.value(), "subtitletypes");
1020  }
1021 
1022  item->DisplayState(pginfo->GetCategoryTypeString(), "categorytype");
1023 
1024  // Mark this button list item as initialized.
1025  item->SetText("yes", "is_item_initialized");
1026  }
1027 
1028 }
1029 
1031 {
1032  auto *pginfo = item->GetData().value<ProgramInfo*>();
1033 
1034  ItemLoaded(item);
1035  // Job status (recording, transcoding, flagging)
1036  QString job = extract_job_state(*pginfo);
1037  item->DisplayState(job, "jobstate");
1038 
1039  // Flagging status (queued, running, no, yes)
1040  item->DisplayState(extract_commflag_state(*pginfo), "commflagged");
1041 
1042  item->SetProgress1(0, pginfo->IsWatched() ? 0 : 100,
1043  pginfo->GetWatchedPercent());
1044  item->SetProgress2(0, 100, pginfo->GetRecordedPercent());
1045 
1046  MythUIButtonListItem *sel_item = item->parent()->GetItemCurrent();
1047  if ((item != sel_item) && item->GetImageFilename("preview").isEmpty() &&
1048  (asAvailable == pginfo->GetAvailableStatus()))
1049  {
1050  QString token = m_helper.GetPreviewImage(*pginfo, true);
1051  if (token.isEmpty())
1052  return;
1053 
1054  m_previewTokens.insert(token);
1055  // now make sure selected item is still at the top of the queue
1056  auto *sel_pginfo = sel_item->GetData().value<ProgramInfo*>();
1057  if (sel_pginfo && sel_item->GetImageFilename("preview").isEmpty() &&
1058  (asAvailable == sel_pginfo->GetAvailableStatus()))
1059  {
1060  m_previewTokens.insert(m_helper.GetPreviewImage(*sel_pginfo, false));
1061  }
1062  }
1063 }
1064 
1065 
1072 void PlaybackBox::HandlePreviewEvent(const QStringList &list)
1073 {
1074  if (list.size() < 5)
1075  {
1076  LOG(VB_GENERAL, LOG_ERR, "HandlePreviewEvent() -- too few args");
1077  for (uint i = 0; i < (uint) list.size(); i++)
1078  {
1079  LOG(VB_GENERAL, LOG_INFO, QString("%1: %2")
1080  .arg(i).arg(list[i]));
1081  }
1082  return;
1083  }
1084 
1085  uint recordingID = list[0].toUInt();
1086  const QString& previewFile = list[1];
1087  const QString& message = list[2];
1088 
1089  bool found = false;
1090  for (uint i = 4; i < (uint) list.size(); i++)
1091  {
1092  const QString& token = list[i];
1093  QSet<QString>::iterator it = m_previewTokens.find(token);
1094  if (it != m_previewTokens.end())
1095  {
1096  found = true;
1097  m_previewTokens.erase(it);
1098  }
1099  }
1100 
1101  if (!found)
1102  {
1103  QString tokens("\n\t\t\ttokens: ");
1104  for (uint i = 4; i < (uint) list.size(); i++)
1105  tokens += list[i] + ", ";
1106  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1107  "Ignoring PREVIEW_SUCCESS, no matcing token" + tokens);
1108  return;
1109  }
1110 
1111  if (previewFile.isEmpty())
1112  {
1113  LOG(VB_GENERAL, LOG_ERR, LOC +
1114  "Ignoring PREVIEW_SUCCESS, no preview file.");
1115  return;
1116  }
1117 
1118  ProgramInfo *info = m_programInfoCache.GetRecordingInfo(recordingID);
1119  MythUIButtonListItem *item = nullptr;
1120 
1121  if (info)
1122  item = m_recordingList->GetItemByData(QVariant::fromValue(info));
1123 
1124  if (!item)
1125  {
1126  LOG(VB_GENERAL, LOG_DEBUG, LOC +
1127  "Ignoring PREVIEW_SUCCESS, item no longer on screen.");
1128  }
1129 
1130  if (item)
1131  {
1132  LOG(VB_GUI, LOG_INFO, LOC + QString("Loading preview %1,\n\t\t\tmsg %2")
1133  .arg(previewFile, message));
1134 
1135  item->SetImage(previewFile, "preview", true);
1136 
1137  if ((GetFocusWidget() == m_recordingList) &&
1138  (m_recordingList->GetItemCurrent() == item) &&
1140  {
1141  m_previewImage->SetFilename(previewFile);
1142  m_previewImage->Load(true, true);
1143  }
1144  }
1145 }
1146 
1148 {
1149  uint32_t flags = FL_NONE;
1150 
1151  if (pginfo)
1152  flags = pginfo->GetProgramFlags();
1153 
1154  QMap <QString, int>::iterator it;
1155  QMap <QString, int> iconMap;
1156 
1157  iconMap["commflagged"] = FL_COMMFLAG;
1158  iconMap["cutlist"] = FL_CUTLIST;
1159  iconMap["autoexpire"] = FL_AUTOEXP;
1160  iconMap["processing"] = FL_COMMPROCESSING;
1161  iconMap["editing"] = FL_EDITING;
1162  iconMap["bookmark"] = FL_BOOKMARK;
1163  iconMap["inuse"] = (FL_INUSERECORDING |
1164  FL_INUSEPLAYING |
1165  FL_INUSEOTHER);
1166  iconMap["transcoded"] = FL_TRANSCODED;
1167  iconMap["watched"] = FL_WATCHED;
1168  iconMap["preserved"] = FL_PRESERVED;
1169 
1170  MythUIImage *iconImage = nullptr;
1171  MythUIStateType *iconState = nullptr;
1172  for (it = iconMap.begin(); it != iconMap.end(); ++it)
1173  {
1174  iconImage = dynamic_cast<MythUIImage *>(GetChild(it.key()));
1175  if (iconImage)
1176  iconImage->SetVisible((flags & (*it)) != 0U);
1177 
1178  iconState = dynamic_cast<MythUIStateType *>(GetChild(it.key()));
1179  if (iconState)
1180  {
1181  if (flags & (*it))
1182  iconState->DisplayState("yes");
1183  else
1184  iconState->DisplayState("no");
1185  }
1186  }
1187 
1188  iconMap.clear();
1189  // Add prefix to ensure iteration order in case 2 or more properties set
1190  iconMap["1dolby"] = AUD_DOLBY;
1191  iconMap["2surround"] = AUD_SURROUND;
1192  iconMap["3stereo"] = AUD_STEREO;
1193  iconMap["4mono"] = AUD_MONO;
1194 
1195  iconState = dynamic_cast<MythUIStateType *>(GetChild("audioprops"));
1196  bool haveIcon = false;
1197  if (pginfo && iconState)
1198  {
1199  for (it = iconMap.begin(); it != iconMap.end(); ++it)
1200  {
1201  if (pginfo->GetAudioProperties() & (*it))
1202  {
1203  if (iconState->DisplayState(it.key().mid(1)))
1204  {
1205  haveIcon = true;
1206  break;
1207  }
1208  }
1209  }
1210  }
1211 
1212  if (iconState && !haveIcon)
1213  iconState->Reset();
1214 
1215  iconState = dynamic_cast<MythUIStateType *>(GetChild("videoprops"));
1216  if (pginfo && iconState)
1217  {
1218  haveIcon = false;
1219  uint props = pginfo->GetVideoProperties();
1220 
1221  iconMap.clear();
1222  if (props & VID_PROGRESSIVE)
1223  {
1224  iconMap["uhd4Kp"] = VID_4K;
1225  iconMap["hd1080p"] = VID_1080;
1226  }
1227  else
1228  {
1229  iconMap["uhd4Ki"] = VID_4K;
1230  iconMap["hd1080i"] = VID_1080;
1231  }
1232  iconMap["hd1080"] = VID_1080;
1233  iconMap["hd720"] = VID_720;
1234  iconMap["hdtv"] = VID_HDTV;
1235  iconMap["widescreen"] = VID_WIDESCREEN;
1236 
1237  for (it = iconMap.begin(); it != iconMap.end(); ++it)
1238  {
1239  if (props & (*it))
1240  {
1241  if (iconState->DisplayState(it.key()))
1242  {
1243  haveIcon = true;
1244  break;
1245  }
1246  }
1247  }
1248 
1249  if (!haveIcon)
1250  iconState->Reset();
1251  }
1252 
1253  iconMap.clear();
1254  iconMap["damaged"] = VID_DAMAGED;
1255 
1256  iconState = dynamic_cast<MythUIStateType *>(GetChild("videoquality"));
1257  haveIcon = false;
1258  if (pginfo && iconState)
1259  {
1260  for (it = iconMap.begin(); it != iconMap.end(); ++it)
1261  {
1262  if (pginfo->GetVideoProperties() & (*it))
1263  {
1264  if (iconState->DisplayState(it.key()))
1265  {
1266  haveIcon = true;
1267  break;
1268  }
1269  }
1270  }
1271  }
1272 
1273  if (iconState && !haveIcon)
1274  iconState->Reset();
1275  iconMap.clear();
1276  iconMap["deafsigned"] = SUB_SIGNED;
1277  iconMap["onscreensub"] = SUB_ONSCREEN;
1278  iconMap["subtitles"] = SUB_NORMAL;
1279  iconMap["cc"] = SUB_HARDHEAR;
1280 
1281  iconState = dynamic_cast<MythUIStateType *>(GetChild("subtitletypes"));
1282  haveIcon = false;
1283  if (pginfo && iconState)
1284  {
1285  for (it = iconMap.begin(); it != iconMap.end(); ++it)
1286  {
1287  if (pginfo->GetSubtitleType() & (*it))
1288  {
1289  if (iconState->DisplayState(it.key()))
1290  {
1291  haveIcon = true;
1292  break;
1293  }
1294  }
1295  }
1296  }
1297 
1298  if (iconState && !haveIcon)
1299  iconState->Reset();
1300 
1301  iconState = dynamic_cast<MythUIStateType *>(GetChild("categorytype"));
1302  if (iconState)
1303  {
1304  if (!(pginfo && iconState->DisplayState(pginfo->GetCategoryTypeString())))
1305  iconState->Reset();
1306  }
1307 }
1308 
1310 {
1311  return GetChild("freereport") || GetChild("usedbar");
1312 }
1313 
1315 {
1316  MythUIText *freereportText =
1317  dynamic_cast<MythUIText*>(GetChild("freereport"));
1318  MythUIProgressBar *usedProgress =
1319  dynamic_cast<MythUIProgressBar *>(GetChild("usedbar"));
1320 
1321  // If the theme doesn't have these widgets,
1322  // don't waste time querying the backend...
1323  if (!freereportText && !usedProgress && !GetChild("diskspacetotal") &&
1324  !GetChild("diskspaceused") && !GetChild("diskspacefree") &&
1325  !GetChild("diskspacepercentused") && !GetChild("diskspacepercentfree"))
1326  return;
1327 
1328  auto freeSpaceTotal = (double) m_helper.GetFreeSpaceTotalMB();
1329  auto freeSpaceUsed = (double) m_helper.GetFreeSpaceUsedMB();
1330 
1331  QLocale locale = gCoreContext->GetQLocale();
1332  InfoMap usageMap;
1333  usageMap["diskspacetotal"] = locale.toString((freeSpaceTotal / 1024.0),
1334  'f', 2);
1335  usageMap["diskspaceused"] = locale.toString((freeSpaceUsed / 1024.0),
1336  'f', 2);
1337  usageMap["diskspacefree"] = locale.toString(
1338  ((freeSpaceTotal - freeSpaceUsed) / 1024.0),
1339  'f', 2);
1340 
1341  double perc = 0.0;
1342  if (freeSpaceTotal > 0.0)
1343  perc = (100.0 * freeSpaceUsed) / freeSpaceTotal;
1344 
1345  usageMap["diskspacepercentused"] = QString::number((int)perc);
1346  usageMap["diskspacepercentfree"] = QString::number(100 - (int)perc);
1347 
1348  QString size = locale.toString(((freeSpaceTotal - freeSpaceUsed) / 1024.0),
1349  'f', 2);
1350 
1351  QString usestr = tr("%1% used, %2 GB free", "Diskspace")
1352  .arg(QString::number((int)perc),
1353  size);
1354 
1355  if (freereportText)
1356  freereportText->SetText(usestr);
1357 
1358  if (usedProgress)
1359  {
1360  usedProgress->SetTotal((int)freeSpaceTotal);
1361  usedProgress->SetUsed((int)freeSpaceUsed);
1362  }
1363 
1364  SetTextFromMap(usageMap);
1365 }
1366 
1367 /*
1368  * \fn PlaybackBox::updateUIRecGroupList(void)
1369  * \brief called when the list of recording groups may have changed
1370  */
1372 {
1373  if (m_recGroupIdx < 0 || !m_recgroupList || m_recGroups.size() < 2)
1374  return;
1375 
1376  QSignalBlocker blocker(m_recgroupList);
1377 
1378  m_recgroupList->Reset();
1379 
1380  int idx = 0;
1381  QStringList::iterator it = m_recGroups.begin();
1382  for (; it != m_recGroups.end(); (++it), (++idx))
1383  {
1384  QString key = (*it);
1385  QString tmp = (key == "All Programs") ? "All" : key;
1386  QString name = ProgramInfo::i18n(tmp);
1387 
1388  if (m_recGroups.size() == 2 && key == "Default")
1389  continue; // All and Default will be the same, so only show All
1390 
1391  auto *item = new MythUIButtonListItem(m_recgroupList, name,
1392  QVariant::fromValue(key));
1393 
1394  if (idx == m_recGroupIdx)
1396  item->SetText(name);
1397  }
1398 }
1399 
1400 void PlaybackBox::UpdateUIGroupList(const QStringList &groupPreferences)
1401 {
1402  m_groupList->Reset();
1403 
1404  if (!m_titleList.isEmpty())
1405  {
1406  int best_pref = INT_MAX;
1407  int sel_idx = 0;
1408  QStringList::iterator it;
1409  for (it = m_titleList.begin(); it != m_titleList.end(); ++it)
1410  {
1411  QString groupname = (*it);
1412 
1413  auto *item = new MythUIButtonListItem(m_groupList, "",
1414  QVariant::fromValue(groupname.toLower()));
1415 
1416  int pref = groupPreferences.indexOf(groupname.toLower());
1417  if ((pref >= 0) && (pref < best_pref))
1418  {
1419  best_pref = pref;
1420  sel_idx = m_groupList->GetItemPos(item);
1421  m_currentGroup = groupname.toLower();
1422  }
1423 
1424  QString displayName = groupname;
1425  if (displayName.isEmpty())
1426  {
1427  if (m_recGroup == "All Programs")
1428  displayName = ProgramInfo::i18n("All Programs");
1429  else
1430  displayName = ProgramInfo::i18n("All Programs - %1")
1431  .arg(m_groupDisplayName);
1432  }
1433 
1434  item->SetText(groupname, "groupname");
1435  item->SetText(displayName, "name");
1436  item->SetText(displayName);
1437 
1438  int count = m_progLists[groupname.toLower()].size();
1439  item->SetText(QString::number(count), "reccount");
1440  }
1441 
1442  m_needUpdate = true;
1443  m_groupList->SetItemCurrent(sel_idx);
1444  // We need to explicitly call updateRecList in this case,
1445  // since 0 is selected by default, and we need updateRecList
1446  // to be called with m_needUpdate set.
1447  if (!sel_idx)
1449  }
1450 }
1451 
1453 {
1454  QString newRecGroup = sel_item->GetData().toString();
1455  displayRecGroup(newRecGroup);
1456 }
1457 
1459 {
1460  QString nextGroup;
1461  m_recGroupsLock.lock();
1462  if (m_recGroupIdx >= 0 && !m_recGroups.empty())
1463  {
1464  if (++m_recGroupIdx >= m_recGroups.size())
1465  m_recGroupIdx = 0;
1466  nextGroup = m_recGroups[m_recGroupIdx];
1467  }
1468  m_recGroupsLock.unlock();
1469 
1470  if (!nextGroup.isEmpty())
1471  displayRecGroup(nextGroup);
1472 }
1473 
1475 {
1476  if (!sel_item)
1477  return;
1478 
1479  QString groupname = sel_item->GetData().toString();
1480  QString grouplabel = sel_item->GetText();
1481 
1482  updateGroupInfo(groupname, grouplabel);
1483 
1484  if (((m_currentGroup == groupname) && !m_needUpdate) ||
1486  return;
1487 
1488  m_needUpdate = false;
1489 
1490  if (!m_isFilling)
1491  m_currentGroup = groupname;
1492 
1494 
1495  ProgramMap::iterator pmit = m_progLists.find(groupname);
1496  if (pmit == m_progLists.end())
1497  return;
1498 
1499  ProgramList &progList = *pmit;
1500 
1501  for (auto & prog : progList)
1502  {
1503  if (prog->GetAvailableStatus() == asPendingDelete ||
1504  prog->GetAvailableStatus() == asDeleted)
1505  continue;
1506 
1507  new PlaybackBoxListItem(this, m_recordingList, prog);
1508  }
1510 
1511  if (m_noRecordingsText)
1512  {
1513  if (!progList.empty())
1515  else
1516  {
1517  QString txt = m_programInfoCache.empty() ?
1518  tr("There are no recordings available") :
1519  tr("There are no recordings in your current view");
1522  }
1523  }
1524 }
1525 
1526 static bool save_position(
1527  const MythUIButtonList *groupList, const MythUIButtonList *recordingList,
1528  QStringList &groupSelPref, QStringList &itemSelPref,
1529  QStringList &itemTopPref)
1530 {
1531  MythUIButtonListItem *prefSelGroup = groupList->GetItemCurrent();
1532  if (!prefSelGroup)
1533  return false;
1534 
1535  groupSelPref.push_back(prefSelGroup->GetData().toString());
1536  for (int i = groupList->GetCurrentPos();
1537  i < groupList->GetCount(); i++)
1538  {
1539  prefSelGroup = groupList->GetItemAt(i);
1540  if (prefSelGroup)
1541  groupSelPref.push_back(prefSelGroup->GetData().toString());
1542  }
1543 
1544  int curPos = recordingList->GetCurrentPos();
1545  for (int i = curPos; (i >= 0) && (i < recordingList->GetCount()); i++)
1546  {
1547  MythUIButtonListItem *item = recordingList->GetItemAt(i);
1548  auto *pginfo = item->GetData().value<ProgramInfo*>();
1549  itemSelPref.push_back(groupSelPref.front());
1550  itemSelPref.push_back(QString::number(pginfo->GetRecordingID()));
1551  }
1552  for (int i = curPos; (i >= 0) && (i < recordingList->GetCount()); i--)
1553  {
1554  MythUIButtonListItem *item = recordingList->GetItemAt(i);
1555  auto *pginfo = item->GetData().value<ProgramInfo*>();
1556  itemSelPref.push_back(groupSelPref.front());
1557  itemSelPref.push_back(QString::number(pginfo->GetRecordingID()));
1558  }
1559 
1560  int topPos = recordingList->GetTopItemPos();
1561  for (int i = topPos + 1; i >= topPos - 1; i--)
1562  {
1563  if (i >= 0 && i < recordingList->GetCount())
1564  {
1565  MythUIButtonListItem *item = recordingList->GetItemAt(i);
1566  auto *pginfo = item->GetData().value<ProgramInfo*>();
1567  if (i == topPos)
1568  {
1569  itemTopPref.push_front(QString::number(pginfo->GetRecordingID()));
1570  itemTopPref.push_front(groupSelPref.front());
1571  }
1572  else
1573  {
1574  itemTopPref.push_back(groupSelPref.front());
1575  itemTopPref.push_back(QString::number(pginfo->GetRecordingID()));
1576  }
1577  }
1578  }
1579 
1580  return true;
1581 }
1582 
1583 static void restore_position(
1584  MythUIButtonList *groupList, MythUIButtonList *recordingList,
1585  const QStringList &groupSelPref, const QStringList &itemSelPref,
1586  const QStringList &itemTopPref)
1587 {
1588  // If possible reselect the item selected before,
1589  // otherwise select the nearest available item.
1590  MythUIButtonListItem *prefSelGroup = groupList->GetItemCurrent();
1591  if (!prefSelGroup ||
1592  !groupSelPref.contains(prefSelGroup->GetData().toString()) ||
1593  !itemSelPref.contains(prefSelGroup->GetData().toString()))
1594  {
1595  return;
1596  }
1597 
1598  // the group is selected in UpdateUIGroupList()
1599  QString groupname = prefSelGroup->GetData().toString();
1600 
1601  // find best selection
1602  int sel = -1;
1603  for (uint i = 0; i+1 < (uint)itemSelPref.size(); i+=2)
1604  {
1605  if (itemSelPref[i] != groupname)
1606  continue;
1607 
1608  uint recordingID = itemSelPref[i+1].toUInt();
1609  for (uint j = 0; j < (uint)recordingList->GetCount(); j++)
1610  {
1611  MythUIButtonListItem *item = recordingList->GetItemAt(j);
1612  auto *pginfo = item->GetData().value<ProgramInfo*>();
1613  if (pginfo && (pginfo->GetRecordingID() == recordingID))
1614  {
1615  sel = j;
1616  i = itemSelPref.size();
1617  break;
1618  }
1619  }
1620  }
1621 
1622  // find best top item
1623  int top = -1;
1624  for (uint i = 0; i+1 < (uint)itemTopPref.size(); i+=2)
1625  {
1626  if (itemTopPref[i] != groupname)
1627  continue;
1628 
1629  uint recordingID = itemTopPref[i+1].toUInt();
1630  for (uint j = 0; j < (uint)recordingList->GetCount(); j++)
1631  {
1632  MythUIButtonListItem *item = recordingList->GetItemAt(j);
1633  auto *pginfo = item->GetData().value<ProgramInfo*>();
1634  if (pginfo && (pginfo->GetRecordingID() == recordingID))
1635  {
1636  top = j;
1637  i = itemTopPref.size();
1638  break;
1639  }
1640  }
1641  }
1642 
1643  if (sel >= 0)
1644  {
1645 #if 0
1646  LOG(VB_GENERAL, LOG_DEBUG, QString("Reselect success (%1,%2)")
1647  .arg(sel).arg(top));
1648 #endif
1649  recordingList->SetItemCurrent(sel, top);
1650  }
1651  else
1652  {
1653 #if 0
1654  LOG(VB_GENERAL, LOG_DEBUG, QString("Reselect failure (%1,%2)")
1655  .arg(sel).arg(top));
1656 #endif
1657  }
1658 }
1659 
1661 {
1662  m_isFilling = true;
1663 
1664  // Save selection, including next few items & groups
1665  QStringList groupSelPref;
1666  QStringList itemSelPref;
1667  QStringList itemTopPref;
1669  groupSelPref, itemSelPref, itemTopPref))
1670  {
1671  // If user wants to start in watchlist and watchlist is displayed, then
1672  // make it the current group
1674  groupSelPref.push_back(m_watchGroupLabel);
1675  }
1676 
1677  // Cache available status for later restoration
1678  QMap<uint, AvailableStatusType> asCache;
1679 
1680  if (!m_progLists.isEmpty())
1681  {
1682  for (auto & prog : m_progLists[""])
1683  {
1684  uint asRecordingID = prog->GetRecordingID();
1685  asCache[asRecordingID] = prog->GetAvailableStatus();
1686  }
1687  }
1688 
1689  m_progsInDB = 0;
1690  m_titleList.clear();
1691  m_progLists.clear();
1693  m_groupList->Reset();
1694  if (m_recgroupList)
1695  m_recgroupList->Reset();
1696  // Clear autoDelete for the "all" list since it will share the
1697  // objects with the title lists.
1698  m_progLists[""] = ProgramList(false);
1699  m_progLists[""].setAutoDelete(false);
1700 
1702  "DisplayGroupTitleSort", TitleSortAlphabetical);
1703 
1704  bool isAllProgsGroup = (m_recGroup == "All Programs");
1705  QMap<QString, QString> sortedList;
1706  QMap<int, QString> searchRule;
1707  QMap<int, QDateTime> recidLastEventTime;
1708  QMap<int, ProgramInfo*> recidWatchListProgram;
1709 
1711 
1712  if (!m_programInfoCache.empty())
1713  {
1714  QString sTitle;
1715 
1716  if ((m_viewMask & VIEW_SEARCHES))
1717  {
1718  MSqlQuery query(MSqlQuery::InitCon());
1719  query.prepare("SELECT recordid,title FROM record "
1720  "WHERE search > 0 AND search != :MANUAL;");
1721  query.bindValue(":MANUAL", kManualSearch);
1722 
1723  if (query.exec())
1724  {
1725  while (query.next())
1726  {
1727  QString tmpTitle = query.value(1).toString();
1728  tmpTitle.remove(RecordingInfo::kReSearchTypeName);
1729  searchRule[query.value(0).toInt()] = tmpTitle;
1730  }
1731  }
1732  }
1733 
1734  bool isCategoryFilter = (m_recGroupType[m_recGroup] == "category");
1735  bool isUnknownCategory = (m_recGroup == tr("Unknown"));
1736  bool isDeletedGroup = (m_recGroup == "Deleted");
1737  bool isLiveTvGroup = (m_recGroup == "LiveTV");
1738 
1739  std::vector<ProgramInfo*> list;
1740  bool newest_first = (0==m_allOrder);
1741  m_programInfoCache.GetOrdered(list, newest_first);
1742  for (auto *p : list)
1743  {
1744  if (p->IsDeletePending())
1745  continue;
1746 
1747  m_progsInDB++;
1748 
1749  const QString& pRecgroup(p->GetRecordingGroup());
1750  const bool isLiveTVProg(pRecgroup == "LiveTV");
1751 
1752  // Never show anything from unauthorised passworded groups
1753  QString password = getRecGroupPassword(pRecgroup);
1754  if (m_curGroupPassword != password && !password.isEmpty())
1755  continue;
1756 
1757  if (pRecgroup == "Deleted")
1758  {
1759  // Filter nothing from Deleted group
1760  // Never show Deleted recs anywhere else
1761  if (!isDeletedGroup)
1762  continue;
1763  }
1764  // Optionally ignore LiveTV programs if not viewing LiveTV group
1765  else if (!(m_viewMask & VIEW_LIVETVGRP) &&
1766  !isLiveTvGroup && isLiveTVProg)
1767  { // NOLINT(bugprone-branch-clone)
1768  continue;
1769  }
1770  // Optionally ignore watched
1771  else if (!(m_viewMask & VIEW_WATCHED) && p->IsWatched())
1772  {
1773  continue;
1774  }
1775  else if (isCategoryFilter)
1776  {
1777  // Filter by category
1778  if (isUnknownCategory ? !p->GetCategory().isEmpty()
1779  : p->GetCategory() != m_recGroup)
1780  continue;
1781  }
1782  // Filter by recgroup
1783  else if (!isAllProgsGroup && pRecgroup != m_recGroup)
1784  continue;
1785 
1786  if (p->GetTitle().isEmpty())
1787  p->SetTitle(tr("_NO_TITLE_"));
1788 
1789  if (m_viewMask != VIEW_NONE && (!isLiveTVProg || isLiveTvGroup))
1790  {
1791  m_progLists[""].push_front(p);
1792  }
1793 
1794  uint asRecordingID = p->GetRecordingID();
1795  if (asCache.contains(asRecordingID))
1796  p->SetAvailableStatus(asCache[asRecordingID], "UpdateUILists");
1797  else
1798  p->SetAvailableStatus(asAvailable, "UpdateUILists");
1799 
1800  if (!isLiveTvGroup && isLiveTVProg && (m_viewMask & VIEW_LIVETVGRP))
1801  {
1802  QString tmpTitle = tr("Live TV");
1803  sortedList[tmpTitle.toLower()] = tmpTitle;
1804  m_progLists[tmpTitle.toLower()].push_front(p);
1805  m_progLists[tmpTitle.toLower()].setAutoDelete(false);
1806  continue;
1807  }
1808 
1809  // Show titles
1810  if ((m_viewMask & VIEW_TITLES) && (!isLiveTVProg || isLiveTvGroup))
1811  {
1812  sTitle = construct_sort_title(
1813  p->GetTitle(), p->GetSortTitle(), m_viewMask, titleSort,
1814  p->GetRecordingPriority());
1815  sTitle = sTitle.toLower();
1816 
1817  if (!sortedList.contains(sTitle))
1818  sortedList[sTitle] = p->GetTitle();
1819  m_progLists[sortedList[sTitle].toLower()].push_front(p);
1820  m_progLists[sortedList[sTitle].toLower()].setAutoDelete(false);
1821  }
1822 
1823  // Show recording groups
1824  if ((m_viewMask & VIEW_RECGROUPS) &&
1825  !pRecgroup.isEmpty() && !isLiveTVProg)
1826  {
1827  sortedList[pRecgroup.toLower()] = pRecgroup;
1828  m_progLists[pRecgroup.toLower()].push_front(p);
1829  m_progLists[pRecgroup.toLower()].setAutoDelete(false);
1830  }
1831 
1832  // Show categories
1833  if (((m_viewMask & VIEW_CATEGORIES) != 0) && !p->GetCategory().isEmpty())
1834  {
1835  QString catl = p->GetCategory().toLower();
1836  sortedList[catl] = p->GetCategory();
1837  m_progLists[catl].push_front(p);
1838  m_progLists[catl].setAutoDelete(false);
1839  }
1840 
1841  if (((m_viewMask & VIEW_SEARCHES) != 0) &&
1842  !searchRule[p->GetRecordingRuleID()].isEmpty() &&
1843  p->GetTitle() != searchRule[p->GetRecordingRuleID()])
1844  { // Show search rules
1845  QString tmpTitle = QString("(%1)")
1846  .arg(searchRule[p->GetRecordingRuleID()]);
1847  sortedList[tmpTitle.toLower()] = tmpTitle;
1848  m_progLists[tmpTitle.toLower()].push_front(p);
1849  m_progLists[tmpTitle.toLower()].setAutoDelete(false);
1850  }
1851 
1852  if ((m_viewMask & VIEW_WATCHLIST) &&
1853  !isLiveTVProg && pRecgroup != "Deleted")
1854  {
1855  int rid = p->GetRecordingRuleID();
1856  auto letIt = recidLastEventTime.find(rid);
1857  if (letIt == recidLastEventTime.end() || *letIt < p->GetLastModifiedTime())
1858  {
1859  recidLastEventTime[rid] = p->GetLastModifiedTime();
1860  }
1861 
1862  if (m_watchListAutoExpire && !p->IsAutoExpirable())
1863  {
1864  p->SetRecordingPriority2(wlExpireOff);
1865  LOG(VB_FILE, LOG_INFO, QString("Auto-expire off: %1")
1866  .arg(p->GetTitle()));
1867  }
1868  else if (p->IsWatched())
1869  {
1870  p->SetRecordingPriority2(wlWatched);
1871  LOG(VB_FILE, LOG_INFO,
1872  QString("Marked as 'watched': %1")
1873  .arg(p->GetTitle()));
1874  }
1875  else
1876  {
1877  auto wlpIt = recidWatchListProgram.find(rid);
1878  if (wlpIt == recidWatchListProgram.end())
1879  {
1880  recidWatchListProgram[rid] = p;
1881  }
1882  else if(comp_season(p, *wlpIt) > 0)
1883  {
1884  (*wlpIt)->SetRecordingPriority2(wlEarlier);
1885  LOG(VB_FILE, LOG_INFO,
1886  QString("Not the earliest: %1")
1887  .arg((*wlpIt)->GetTitle()));
1888 
1889  recidWatchListProgram[rid] = p;
1890  }
1891  else
1892  {
1893  p->SetRecordingPriority2(wlEarlier);
1894  LOG(VB_FILE, LOG_INFO,
1895  QString("Not the earliest: %1")
1896  .arg(p->GetTitle()));
1897  }
1898  }
1899  }
1900  }
1901 
1902  if ((m_viewMask & VIEW_WATCHLIST) && !recidWatchListProgram.empty())
1903  {
1904  for (auto *p : qAsConst(recidWatchListProgram))
1905  {
1906  m_progLists[m_watchGroupLabel].push_back(p);
1907  }
1908 
1909  m_progLists[m_watchGroupLabel].setAutoDelete(false);
1910  }
1911  }
1912 
1913  if (sortedList.empty())
1914  {
1915  LOG(VB_GENERAL, LOG_WARNING, LOC + "SortedList is Empty");
1916  m_progLists[""];
1917  m_titleList << "";
1918  m_playList.clear();
1919  if (!isAllProgsGroup)
1921 
1923  UpdateUIGroupList(groupSelPref);
1924 
1925  m_isFilling = false;
1926  return false;
1927  }
1928 
1929  QString episodeSort = gCoreContext->GetSetting("PlayBoxEpisodeSort", "Date");
1930 
1931  if (episodeSort == "OrigAirDate")
1932  {
1933  QMap<QString, ProgramList>::Iterator Iprog;
1934  for (Iprog = m_progLists.begin(); Iprog != m_progLists.end(); ++Iprog)
1935  {
1936  if (!Iprog.key().isEmpty())
1937  {
1938  std::stable_sort((*Iprog).begin(), (*Iprog).end(),
1939  (m_listOrder == 0) ?
1942  }
1943  }
1944  }
1945  else if (episodeSort == "Id")
1946  {
1947  QMap<QString, ProgramList>::Iterator Iprog;
1948  for (Iprog = m_progLists.begin(); Iprog != m_progLists.end(); ++Iprog)
1949  {
1950  if (!Iprog.key().isEmpty())
1951  {
1952  std::stable_sort((*Iprog).begin(), (*Iprog).end(),
1953  (m_listOrder == 0) ?
1956  }
1957  }
1958  }
1959  else if (episodeSort == "Date")
1960  {
1961  QMap<QString, ProgramList>::iterator it;
1962  for (it = m_progLists.begin(); it != m_progLists.end(); ++it)
1963  {
1964  if (!it.key().isEmpty())
1965  {
1966  std::stable_sort((*it).begin(), (*it).end(),
1967  (!m_listOrder) ?
1970  }
1971  }
1972  }
1973  else if (episodeSort == "Season")
1974  {
1975  QMap<QString, ProgramList>::iterator it;
1976  for (it = m_progLists.begin(); it != m_progLists.end(); ++it)
1977  {
1978  if (!it.key().isEmpty())
1979  {
1980  std::stable_sort((*it).begin(), (*it).end(),
1981  (!m_listOrder) ?
1984  }
1985  }
1986  }
1987 
1988  if (!m_progLists[m_watchGroupLabel].empty())
1989  {
1990  MSqlQuery query(MSqlQuery::InitCon());
1991  query.prepare("SELECT recordid, last_delete FROM record;");
1992 
1993  if (query.exec())
1994  {
1995  while (query.next())
1996  {
1997  int recid = query.value(0).toInt();
1998 
1999  QDateTime last_delete =
2000  MythDate::as_utc(query.value(1).toDateTime());
2001 
2002  if (last_delete.isValid())
2003  {
2004  auto it = recidLastEventTime.find(recid);
2005  if (it != recidLastEventTime.end() && last_delete > *it)
2006  {
2007  recidLastEventTime[recid] = last_delete;
2008  }
2009  }
2010  }
2011  }
2012 
2013  auto pit = m_progLists[m_watchGroupLabel].begin();
2014  while (pit != m_progLists[m_watchGroupLabel].end())
2015  {
2016  int recid = (*pit)->GetRecordingRuleID();
2017 
2018  (*pit)->SetRecordingPriority2(recidLastEventTime[recid].toSecsSinceEpoch()/60);
2019 
2020  LOG(VB_FILE, LOG_INFO, QString(" %1 %2 %3")
2021  .arg(MythDate::toString((*pit)->GetScheduledStartTime(),
2023  .arg((*pit)->GetRecordingPriority2())
2024  .arg((*pit)->GetTitle()));
2025 
2026  ++pit;
2027  }
2028 
2029  std::stable_sort(m_progLists[m_watchGroupLabel].begin(),
2032  }
2033 
2034  m_titleList = QStringList("");
2035  if (!m_progLists[m_watchGroupLabel].empty())
2037  if ((!m_progLists["livetv"].empty()) &&
2038  (std::find(sortedList.cbegin(), sortedList.cend(), tr("Live TV"))
2039  == sortedList.cend()))
2040  m_titleList << tr("Live TV");
2041  m_titleList << sortedList.values();
2042 
2043  // Populate list of recording groups
2044  if (!m_programInfoCache.empty())
2045  {
2046  QMutexLocker locker(&m_recGroupsLock);
2047 
2048  m_recGroups.clear();
2049  m_recGroupIdx = -1;
2050 
2051  m_recGroups.append("All Programs");
2052 
2053  MSqlQuery query(MSqlQuery::InitCon());
2054 
2055  query.prepare("SELECT distinct recgroup from recorded WHERE "
2056  "deletepending = 0 ORDER BY recgroup");
2057  if (query.exec())
2058  {
2059  QString name;
2060  while (query.next())
2061  {
2062  name = query.value(0).toString();
2063  if (name != "Deleted" && name != "LiveTV" && !name.startsWith('.'))
2064  {
2065  m_recGroups.append(name);
2066  m_recGroupType[name] = "recgroup";
2067  }
2068  }
2069 
2071  if (m_recGroupIdx < 0)
2072  m_recGroupIdx = 0;
2073  }
2074  }
2075 
2077  UpdateUIGroupList(groupSelPref);
2078  UpdateUsageUI();
2079 
2080  for (uint id : qAsConst(m_playList))
2081  {
2082  ProgramInfo *pginfo = FindProgramInUILists(id);
2083  if (!pginfo)
2084  continue;
2085  MythUIButtonListItem *item =
2086  m_recordingList->GetItemByData(QVariant::fromValue(pginfo));
2087  if (item)
2088  item->DisplayState("yes", "playlist");
2089  }
2090 
2092  groupSelPref, itemSelPref, itemTopPref);
2093 
2094  m_isFilling = false;
2095 
2096  return true;
2097 }
2098 
2100 {
2101  if (Random)
2102  {
2103  m_playListPlay.clear();
2104  QList<uint> tmp = m_playList;
2105  while (!tmp.isEmpty())
2106  {
2107  unsigned int i = MythRandom(0, tmp.size() - 1);
2108  m_playListPlay.append(tmp[i]);
2109  tmp.removeAll(tmp[i]);
2110  }
2111  }
2112  else
2113  {
2115  }
2116 
2117  QCoreApplication::postEvent(
2118  this, new MythEvent("PLAY_PLAYLIST"));
2119 }
2120 
2122 {
2123  if (!item)
2124  item = m_recordingList->GetItemCurrent();
2125 
2126  if (!item)
2127  return;
2128 
2129  auto *pginfo = item->GetData().value<ProgramInfo *>();
2130 
2131  const bool ignoreBookmark = false;
2132  const bool ignoreProgStart = false;
2133  const bool ignoreLastPlayPos = false;
2134  const bool underNetworkControl = false;
2135  if (pginfo)
2136  PlayX(*pginfo, ignoreBookmark, ignoreProgStart, ignoreLastPlayPos,
2137  underNetworkControl);
2138 }
2139 
2141 {
2142  if (!item)
2143  item = m_recordingList->GetItemCurrent();
2144 
2145  if (!item)
2146  return;
2147 
2148  auto *pginfo = item->GetData().value<ProgramInfo *>();
2149 
2150  const bool ignoreBookmark = false;
2151  const bool ignoreProgStart = true;
2152  const bool ignoreLastPlayPos = true;
2153  const bool underNetworkControl = false;
2154  if (pginfo)
2155  PlayX(*pginfo, ignoreBookmark, ignoreProgStart, ignoreLastPlayPos,
2156  underNetworkControl);
2157 }
2158 
2160 {
2161  if (!item)
2162  item = m_recordingList->GetItemCurrent();
2163 
2164  if (!item)
2165  return;
2166 
2167  auto *pginfo = item->GetData().value<ProgramInfo *>();
2168 
2169  const bool ignoreBookmark = true;
2170  const bool ignoreProgStart = true;
2171  const bool ignoreLastPlayPos = true;
2172  const bool underNetworkControl = false;
2173  if (pginfo)
2174  PlayX(*pginfo, ignoreBookmark, ignoreProgStart, ignoreLastPlayPos,
2175  underNetworkControl);
2176 }
2177 
2179 {
2180  if (!item)
2181  item = m_recordingList->GetItemCurrent();
2182 
2183  if (!item)
2184  return;
2185 
2186  auto *pginfo = item->GetData().value<ProgramInfo *>();
2187 
2188  const bool ignoreBookmark = true;
2189  const bool ignoreProgStart = true;
2190  const bool ignoreLastPlayPos = false;
2191  const bool underNetworkControl = false;
2192  if (pginfo)
2193  PlayX(*pginfo, ignoreBookmark, ignoreProgStart, ignoreLastPlayPos,
2194  underNetworkControl);
2195 }
2196 
2197 void PlaybackBox::PlayX(const ProgramInfo &pginfo,
2198  bool ignoreBookmark,
2199  bool ignoreProgStart,
2200  bool ignoreLastPlayPos,
2201  bool underNetworkControl)
2202 {
2203  if (!m_player)
2204  {
2205  Play(pginfo, false, ignoreBookmark, ignoreProgStart, ignoreLastPlayPos, underNetworkControl);
2206  return;
2207  }
2208 
2209  if (!m_player->IsSameProgram(&pginfo))
2210  {
2212  m_playerSelectedNewShow.push_back(ignoreBookmark ? "1" : "0");
2213  m_playerSelectedNewShow.push_back(underNetworkControl ? "1" : "0");
2214  // XXX add anything for ignoreProgStart and ignoreLastPlayPos?
2215  }
2216  Close();
2217 }
2218 
2220 {
2221  ProgramInfo *pginfo = GetCurrentProgram();
2222  if (pginfo)
2223  pginfo->SaveBookmark(0);
2224 }
2225 
2227 {
2228  ProgramInfo *pginfo = GetCurrentProgram();
2229  if (pginfo)
2230  pginfo->SaveLastPlayPos(0);
2231 }
2232 
2234 {
2235  ProgramInfo *pginfo = GetCurrentProgram();
2236  if (pginfo)
2237  m_helper.StopRecording(*pginfo);
2238 }
2239 
2241 {
2242  if (!item)
2243  return;
2244 
2245  auto *pginfo = item->GetData().value<ProgramInfo *>();
2246 
2247  if (!pginfo)
2248  return;
2249 
2250  if (pginfo->GetAvailableStatus() == asPendingDelete)
2251  {
2252  LOG(VB_GENERAL, LOG_ERR, QString("deleteSelected(%1) -- failed ")
2253  .arg(pginfo->toString(ProgramInfo::kTitleSubtitle)) +
2254  QString("availability status: %1 ")
2255  .arg(pginfo->GetAvailableStatus()));
2256 
2257  ShowOkPopup(tr("Cannot delete\n") +
2258  tr("This recording is already being deleted"));
2259  }
2260  else if (!pginfo->QueryIsDeleteCandidate())
2261  {
2262  QString byWho;
2263  pginfo->QueryIsInUse(byWho);
2264 
2265  LOG(VB_GENERAL, LOG_ERR, QString("deleteSelected(%1) -- failed ")
2266  .arg(pginfo->toString(ProgramInfo::kTitleSubtitle)) +
2267  QString("delete candidate: %1 in use by %2")
2268  .arg(pginfo->QueryIsDeleteCandidate()).arg(byWho));
2269 
2270  if (byWho.isEmpty())
2271  {
2272  ShowOkPopup(tr("Cannot delete\n") +
2273  tr("This recording is already being deleted"));
2274  }
2275  else
2276  {
2277  ShowOkPopup(tr("Cannot delete\n") +
2278  tr("This recording is currently in use by:") + "\n" +
2279  byWho);
2280  }
2281  }
2282  else
2283  {
2284  push_onto_del(m_delList, *pginfo);
2286  }
2287 }
2288 
2290 {
2291  ProgramInfo *pginfo = nullptr;
2292 
2294 
2295  if (!item)
2296  return nullptr;
2297 
2298  pginfo = item->GetData().value<ProgramInfo *>();
2299 
2300  if (!pginfo)
2301  return nullptr;
2302 
2303  return pginfo;
2304 }
2305 
2307 {
2308  if (!item)
2309  return;
2310 
2311  PlayFromAnyMark(item);
2312 }
2313 
2314 void PlaybackBox::popupClosed(const QString& which, int result)
2315 {
2316  m_menuDialog = nullptr;
2317 
2318  if (result == -2)
2319  {
2320  if (!m_doToggleMenu)
2321  {
2322  m_doToggleMenu = true;
2323  return;
2324  }
2325 
2326  if (which == "groupmenu")
2327  {
2328  ProgramInfo *pginfo = GetCurrentProgram();
2329  if (pginfo)
2330  {
2332 
2333  if ((asPendingDelete == pginfo->GetAvailableStatus()) ||
2334  (asDeleted == pginfo->GetAvailableStatus()) ||
2335  (asNotYetAvailable == pginfo->GetAvailableStatus()))
2336  {
2337  ShowAvailabilityPopup(*pginfo);
2338  }
2339  else
2340  {
2341  ShowActionPopup(*pginfo);
2342  m_doToggleMenu = false;
2343  }
2344  }
2345  }
2346  else if (which == "actionmenu")
2347  {
2348  ShowGroupPopup();
2349  m_doToggleMenu = false;
2350  }
2351  }
2352  else
2353  m_doToggleMenu = true;
2354 }
2355 
2357 {
2358  QString label = tr("Group List Menu");
2359 
2360  ProgramInfo *pginfo = GetCurrentProgram();
2361 
2362  m_popupMenu = new MythMenu(label, this, "groupmenu");
2363 
2364  m_popupMenu->AddItem(tr("Change Group Filter"),
2366 
2367  m_popupMenu->AddItem(tr("Change Group View"),
2369 
2370  if (m_recGroupType[m_recGroup] == "recgroup")
2371  m_popupMenu->AddItem(tr("Change Group Password"),
2373 
2374  if (!m_playList.isEmpty())
2375  {
2376  m_popupMenu->AddItem(tr("Playlist Options"), nullptr, createPlaylistMenu());
2377  }
2378  else if (!m_player)
2379  {
2380  if (GetFocusWidget() == m_groupList)
2381  {
2382  m_popupMenu->AddItem(tr("Add this Group to Playlist"),
2384  }
2385  else if (pginfo)
2386  {
2387  m_popupMenu->AddItem(tr("Add this recording to Playlist"),
2388  qOverload<>(&PlaybackBox::togglePlayListItem));
2389  }
2390  }
2391 
2392  m_popupMenu->AddItem(tr("Help (Status Icons)"), &PlaybackBox::showIconHelp);
2393 
2394  DisplayPopupMenu();
2395 }
2396 
2398  const ProgramInfo &rec,
2399  bool inPlaylist, bool ignoreBookmark, bool ignoreProgStart,
2400  bool ignoreLastPlayPos, bool underNetworkControl)
2401 {
2402  bool playCompleted = false;
2403 
2404  if (m_player)
2405  return true;
2406 
2407  if ((asAvailable != rec.GetAvailableStatus()) || !rec.GetFilesize() ||
2408  !rec.IsPathSet())
2409  {
2411  rec, (inPlaylist) ? kCheckForPlaylistAction : kCheckForPlayAction);
2412  return false;
2413  }
2414 
2415  for (size_t i = 0; i < kNumArtImages; i++)
2416  {
2417  if (!m_artImage[i])
2418  continue;
2419 
2420  m_artTimer[i]->stop();
2421  m_artImage[i]->Reset();
2422  }
2423 
2424  ProgramInfo tvrec(rec);
2425 
2426  m_playingSomething = true;
2427  int initIndex = m_recordingList->StopLoad();
2428 
2429  if (!gCoreContext->GetBoolSetting("UseProgStartMark", false))
2430  ignoreProgStart = true;
2431 
2432  uint flags =
2433  (inPlaylist ? kStartTVInPlayList : kStartTVNoFlags) |
2434  (underNetworkControl ? kStartTVByNetworkCommand : kStartTVNoFlags) |
2435  (ignoreLastPlayPos ? kStartTVIgnoreLastPlayPos : kStartTVNoFlags) |
2436  (ignoreProgStart ? kStartTVIgnoreProgStart : kStartTVNoFlags) |
2437  (ignoreBookmark ? kStartTVIgnoreBookmark : kStartTVNoFlags);
2438 
2439  playCompleted = TV::StartTV(&tvrec, flags);
2440 
2441  m_playingSomething = false;
2442  m_recordingList->LoadInBackground(initIndex);
2443 
2444  if (inPlaylist && !m_playListPlay.empty())
2445  {
2446  QCoreApplication::postEvent(
2447  this, new MythEvent("PLAY_PLAYLIST"));
2448  }
2449 
2450  if (m_needUpdate)
2452 
2453  return playCompleted;
2454 }
2455 
2456 void PlaybackBox::RemoveProgram( uint recordingID, bool forgetHistory,
2457  bool forceMetadataDelete)
2458 {
2459  ProgramInfo *delItem = FindProgramInUILists(recordingID);
2460 
2461  if (!delItem)
2462  return;
2463 
2464  if (!forceMetadataDelete &&
2465  ((delItem->GetAvailableStatus() == asPendingDelete) ||
2466  !delItem->QueryIsDeleteCandidate()))
2467  {
2468  return;
2469  }
2470 
2471  if (m_playList.contains(delItem->GetRecordingID()))
2472  togglePlayListItem(delItem);
2473 
2474  if (!forceMetadataDelete)
2475  delItem->UpdateLastDelete(true);
2476 
2477  delItem->SetAvailableStatus(asPendingDelete, "RemoveProgram");
2479  forceMetadataDelete, forgetHistory);
2480 
2481  // if the item is in the current recording list UI then delete it.
2482  MythUIButtonListItem *uiItem =
2483  m_recordingList->GetItemByData(QVariant::fromValue(delItem));
2484  if (uiItem)
2485  m_recordingList->RemoveItem(uiItem);
2486 }
2487 
2489 {
2490  m_artImage[kArtworkFanart]->Load();
2491 }
2492 
2494 {
2495  m_artImage[kArtworkBanner]->Load();
2496 }
2497 
2499 {
2500  m_artImage[kArtworkCoverart]->Load();
2501 }
2502 
2504 {
2505  QString label;
2506  switch (type)
2507  {
2508  case kDeleteRecording:
2509  label = tr("Are you sure you want to delete:"); break;
2510  case kForceDeleteRecording:
2511  label = tr("Recording file does not exist.\n"
2512  "Are you sure you want to delete:");
2513  break;
2514  case kStopRecording:
2515  label = tr("Are you sure you want to stop:"); break;
2516  }
2517 
2518  ProgramInfo *delItem = nullptr;
2519  if (m_delList.empty() && (delItem = GetCurrentProgram()))
2520  {
2521  push_onto_del(m_delList, *delItem);
2522  }
2523  else if (m_delList.size() >= 3)
2524  {
2525  delItem = FindProgramInUILists(m_delList[0].toUInt());
2526  }
2527 
2528  if (!delItem)
2529  return;
2530 
2531  uint other_delete_cnt = (m_delList.size() / 3) - 1;
2532 
2533  label += CreateProgramInfoString(*delItem);
2534 
2535  m_popupMenu = new MythMenu(label, this, "deletemenu");
2536 
2537  if ((kDeleteRecording == type) &&
2538  delItem->GetRecordingGroup() != "Deleted" &&
2539  delItem->GetRecordingGroup() != "LiveTV")
2540  {
2541  m_popupMenu->AddItem(tr("Yes, and allow re-record"),
2543  }
2544 
2545  bool defaultIsYes =
2546  ((kDeleteRecording != type) &&
2547  (kForceDeleteRecording != type) &&
2548  (delItem->QueryAutoExpire() != kDisableAutoExpire));
2549 
2550  switch (type)
2551  {
2552  case kDeleteRecording:
2553  m_popupMenu->AddItem(tr("Yes, delete it"),
2554  qOverload<>(&PlaybackBox::Delete), nullptr, defaultIsYes);
2555  break;
2556  case kForceDeleteRecording:
2557  m_popupMenu->AddItem(tr("Yes, delete it"),
2558  &PlaybackBox::DeleteForce, nullptr, defaultIsYes);
2559  break;
2560  case kStopRecording:
2561  m_popupMenu->AddItem(tr("Yes, stop recording"),
2562  &PlaybackBox::StopSelected, nullptr, defaultIsYes);
2563  break;
2564  }
2565 
2566 
2567  if ((kForceDeleteRecording == type) && other_delete_cnt)
2568  {
2570  tr("Yes, delete it and the remaining %1 list items")
2571  .arg(other_delete_cnt), &PlaybackBox::DeleteForceAllRemaining);
2572  }
2573 
2574  switch (type)
2575  {
2576  case kDeleteRecording:
2577  case kForceDeleteRecording:
2578  m_popupMenu->AddItem(tr("No, keep it"), &PlaybackBox::DeleteIgnore,
2579  nullptr, !defaultIsYes);
2580  break;
2581  case kStopRecording:
2582  m_popupMenu->AddItem(tr("No, continue recording"), &PlaybackBox::DeleteIgnore,
2583  nullptr, !defaultIsYes);
2584  break;
2585  }
2586 
2587  if ((type == kForceDeleteRecording) && other_delete_cnt)
2588  {
2590  tr("No, and keep the remaining %1 list items")
2591  .arg(other_delete_cnt),
2593  }
2594 
2595  DisplayPopupMenu();
2596 }
2597 
2599 {
2600  QString msg = pginfo.toString(ProgramInfo::kTitleSubtitle, " ");
2601  msg += "\n";
2602 
2603  QString byWho;
2604  switch (pginfo.GetAvailableStatus())
2605  {
2606  case asAvailable:
2607  if (pginfo.QueryIsInUse(byWho))
2608  {
2609  ShowNotification(tr("Recording Available\n"),
2610  sLocation, msg +
2611  tr("This recording is currently in "
2612  "use by:") + "\n" + byWho);
2613  }
2614  else
2615  {
2616  ShowNotification(tr("Recording Available\n"),
2617  sLocation, msg +
2618  tr("This recording is currently "
2619  "Available"));
2620  }
2621  break;
2622  case asPendingDelete:
2623  ShowNotificationError(tr("Recording Unavailable\n"),
2624  sLocation, msg +
2625  tr("This recording is currently being "
2626  "deleted and is unavailable"));
2627  break;
2628  case asDeleted:
2629  ShowNotificationError(tr("Recording Unavailable\n"),
2630  sLocation, msg +
2631  tr("This recording has been "
2632  "deleted and is unavailable"));
2633  break;
2634  case asFileNotFound:
2635  ShowNotificationError(tr("Recording Unavailable\n"),
2636  sLocation, msg +
2637  tr("The file for this recording can "
2638  "not be found"));
2639  break;
2640  case asZeroByte:
2641  ShowNotificationError(tr("Recording Unavailable\n"),
2642  sLocation, msg +
2643  tr("The file for this recording is "
2644  "empty."));
2645  break;
2646  case asNotYetAvailable:
2647  ShowNotificationError(tr("Recording Unavailable\n"),
2648  sLocation, msg +
2649  tr("This recording is not yet "
2650  "available."));
2651  }
2652 }
2653 
2655 {
2656  QString label = tr("There is %n item(s) in the playlist. Actions affect "
2657  "all items in the playlist", "", m_playList.size());
2658 
2659  auto *menu = new MythMenu(label, this, "slotmenu");
2660 
2661  menu->AddItem(tr("Play"), &PlaybackBox::doPlayList);
2662  menu->AddItem(tr("Shuffle Play"), &PlaybackBox::doPlayListRandom);
2663  menu->AddItem(tr("Clear Playlist"), &PlaybackBox::doClearPlaylist);
2664 
2665  if (GetFocusWidget() == m_groupList)
2666  {
2667  if ((m_viewMask & VIEW_TITLES))
2668  {
2669  menu->AddItem(tr("Toggle playlist for this Category/Title"),
2671  }
2672  else
2673  {
2674  menu->AddItem(tr("Toggle playlist for this Group"),
2676  }
2677  }
2678  else
2679  menu->AddItem(tr("Toggle playlist for this recording"),
2680  qOverload<>(&PlaybackBox::togglePlayListItem));
2681 
2682  menu->AddItem(tr("Storage Options"), nullptr, createPlaylistStorageMenu());
2683  menu->AddItem(tr("Job Options"), nullptr, createPlaylistJobMenu());
2684  menu->AddItem(tr("Delete"), &PlaybackBox::PlaylistDeleteKeepHistory);
2685  menu->AddItem(tr("Delete, and allow re-record"),
2687 
2688  return menu;
2689 }
2690 
2692 {
2693  QString label = tr("There is %n item(s) in the playlist. Actions affect "
2694  "all items in the playlist", "", m_playList.size());
2695 
2696  auto *menu = new MythMenu(label, this, "slotmenu");
2697 
2698  menu->AddItem(tr("Change Recording Group"), &PlaybackBox::ShowRecGroupChangerUsePlaylist);
2699  menu->AddItem(tr("Change Playback Group"), &PlaybackBox::ShowPlayGroupChangerUsePlaylist);
2700  menu->AddItem(tr("Disable Auto Expire"), &PlaybackBox::doPlaylistExpireSetOff);
2701  menu->AddItem(tr("Enable Auto Expire"), &PlaybackBox::doPlaylistExpireSetOn);
2702  menu->AddItem(tr("Mark as Watched"), &PlaybackBox::doPlaylistWatchedSetOn);
2703  menu->AddItem(tr("Mark as Unwatched"), &PlaybackBox::doPlaylistWatchedSetOff);
2704  menu->AddItem(tr("Allow Re-record"), &PlaybackBox::doPlaylistAllowRerecord);
2705 
2706  return menu;
2707 }
2708 
2710 {
2711  QString label = tr("There is %n item(s) in the playlist. Actions affect "
2712  "all items in the playlist", "", m_playList.size());
2713 
2714  auto *menu = new MythMenu(label, this, "slotmenu");
2715 
2716  QString jobTitle;
2717  QString command;
2718  QList<uint>::Iterator it;
2719  bool isTranscoding = true;
2720  bool isFlagging = true;
2721  bool isMetadataLookup = true;
2722  bool isRunningUserJob1 = true;
2723  bool isRunningUserJob2 = true;
2724  bool isRunningUserJob3 = true;
2725  bool isRunningUserJob4 = true;
2726 
2727  for(it = m_playList.begin(); it != m_playList.end(); ++it)
2728  {
2729  ProgramInfo *tmpItem = FindProgramInUILists(*it);
2730  if (tmpItem)
2731  {
2733  JOB_TRANSCODE,
2734  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2735  isTranscoding = false;
2737  JOB_COMMFLAG,
2738  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2739  isFlagging = false;
2741  JOB_METADATA,
2742  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2743  isMetadataLookup = false;
2745  JOB_USERJOB1,
2746  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2747  isRunningUserJob1 = false;
2749  JOB_USERJOB2,
2750  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2751  isRunningUserJob2 = false;
2753  JOB_USERJOB3,
2754  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2755  isRunningUserJob3 = false;
2757  JOB_USERJOB4,
2758  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2759  isRunningUserJob4 = false;
2760  if (!isTranscoding && !isFlagging && !isRunningUserJob1 &&
2761  !isRunningUserJob2 && !isRunningUserJob3 && !isRunningUserJob4)
2762  break;
2763  }
2764  }
2765 
2766  if (!isTranscoding)
2767  menu->AddItem(tr("Begin Transcoding"), &PlaybackBox::doPlaylistBeginTranscoding);
2768  else
2769  menu->AddItem(tr("Stop Transcoding"), &PlaybackBox::stopPlaylistTranscoding);
2770 
2771  if (!isFlagging)
2772  menu->AddItem(tr("Begin Commercial Detection"), &PlaybackBox::doPlaylistBeginFlagging);
2773  else
2774  menu->AddItem(tr("Stop Commercial Detection"), &PlaybackBox::stopPlaylistFlagging);
2775 
2776  if (!isMetadataLookup)
2777  menu->AddItem(tr("Begin Metadata Lookup"), &PlaybackBox::doPlaylistBeginLookup);
2778  else
2779  menu->AddItem(tr("Stop Metadata Lookup"), &PlaybackBox::stopPlaylistLookup);
2780 
2781  command = gCoreContext->GetSetting("UserJob1", "");
2782  if (!command.isEmpty())
2783  {
2784  jobTitle = gCoreContext->GetSetting("UserJobDesc1");
2785 
2786  if (!isRunningUserJob1)
2787  {
2788  menu->AddItem(tr("Begin") + ' ' + jobTitle,
2790  }
2791  else
2792  {
2793  menu->AddItem(tr("Stop") + ' ' + jobTitle,
2795  }
2796  }
2797 
2798  command = gCoreContext->GetSetting("UserJob2", "");
2799  if (!command.isEmpty())
2800  {
2801  jobTitle = gCoreContext->GetSetting("UserJobDesc2");
2802 
2803  if (!isRunningUserJob2)
2804  {
2805  menu->AddItem(tr("Begin") + ' ' + jobTitle,
2807  }
2808  else
2809  {
2810  menu->AddItem(tr("Stop") + ' ' + jobTitle,
2812  }
2813  }
2814 
2815  command = gCoreContext->GetSetting("UserJob3", "");
2816  if (!command.isEmpty())
2817  {
2818  jobTitle = gCoreContext->GetSetting("UserJobDesc3");
2819 
2820  if (!isRunningUserJob3)
2821  {
2822  menu->AddItem(tr("Begin") + ' ' + jobTitle,
2824  }
2825  else
2826  {
2827  menu->AddItem(tr("Stop") + ' ' + jobTitle,
2829  }
2830  }
2831 
2832  command = gCoreContext->GetSetting("UserJob4", "");
2833  if (!command.isEmpty())
2834  {
2835  jobTitle = gCoreContext->GetSetting("UserJobDesc4");
2836 
2837  if (!isRunningUserJob4)
2838  {
2839  menu->AddItem(QString("%1 %2").arg(tr("Begin"), jobTitle),
2841  }
2842  else
2843  {
2844  menu->AddItem(QString("%1 %2").arg(tr("Stop"), jobTitle),
2846  }
2847  }
2848 
2849  return menu;
2850 }
2851 
2853 {
2854  if (m_menuDialog || !m_popupMenu)
2855  return;
2856 
2857  m_menuDialog = new MythDialogBox(m_popupMenu, m_popupStack, "pbbmainmenupopup");
2858 
2859  if (m_menuDialog->Create())
2860  {
2863  }
2864  else
2865  delete m_menuDialog;
2866 }
2867 
2869 {
2870  if (m_menuDialog)
2871  return;
2872 
2873  if (GetFocusWidget() == m_groupList)
2874  ShowGroupPopup();
2875  else
2876  {
2877  ProgramInfo *pginfo = GetCurrentProgram();
2878  if (pginfo)
2879  {
2881  *pginfo, kCheckForMenuAction);
2882 
2883  if ((asPendingDelete == pginfo->GetAvailableStatus()) ||
2884  (asDeleted == pginfo->GetAvailableStatus()) ||
2885  (asNotYetAvailable == pginfo->GetAvailableStatus()))
2886  {
2887  ShowAvailabilityPopup(*pginfo);
2888  }
2889  else
2890  {
2891  ShowActionPopup(*pginfo);
2892  }
2893  }
2894  else
2895  ShowGroupPopup();
2896  }
2897 }
2898 
2900 {
2901  ProgramInfo *pginfo = GetCurrentProgram();
2902  if (!pginfo)
2903  return nullptr;
2904 
2905  QString title = tr("Play Options") + CreateProgramInfoString(*pginfo);
2906 
2907  auto *menu = new MythMenu(title, this, "slotmenu");
2908  bool hasLastPlay = pginfo->IsLastPlaySet();
2909  bool hasBookMark = pginfo->IsBookmarkSet();
2910  if (hasLastPlay)
2911  menu->AddItem(tr("Play from last played position"),
2912  qOverload<>(&PlaybackBox::PlayFromLastPlayPos));
2913  if (hasBookMark)
2914  menu->AddItem(tr("Play from bookmark"),
2915  qOverload<>(&PlaybackBox::PlayFromBookmark));
2916  menu->AddItem(tr("Play from beginning"),
2917  qOverload<>(&PlaybackBox::PlayFromBeginning));
2918  if (hasLastPlay)
2919  menu->AddItem(tr("Clear last played position"),
2921  if (hasBookMark)
2922  menu->AddItem(tr("Clear bookmark"), &PlaybackBox::ClearBookmark);
2923 
2924  return menu;
2925 }
2926 
2928 {
2929  ProgramInfo *pginfo = GetCurrentProgram();
2930  if (!pginfo)
2931  return nullptr;
2932 
2933  QString title = tr("Storage Options") + CreateProgramInfoString(*pginfo);
2934  QString autoExpireText = (pginfo->IsAutoExpirable()) ?
2935  tr("Disable Auto Expire") : tr("Enable Auto Expire");
2936  QString preserveText = (pginfo->IsPreserved()) ?
2937  tr("Do not preserve this episode") : tr("Preserve this episode");
2938 
2939  auto *menu = new MythMenu(title, this, "slotmenu");
2940  menu->AddItem(tr("Change Recording Group"), &PlaybackBox::ShowRecGroupChangerNoPlaylist);
2941  menu->AddItem(tr("Change Playback Group"), &PlaybackBox::ShowPlayGroupChangerNoPlaylist);
2942  menu->AddItem(autoExpireText, &PlaybackBox::toggleAutoExpire);
2943  menu->AddItem(preserveText, &PlaybackBox::togglePreserveEpisode);
2944 
2945  return menu;
2946 }
2947 
2949 {
2950  ProgramInfo *pginfo = GetCurrentProgram();
2951  if (!pginfo)
2952  return nullptr;
2953 
2954  QString title = tr("Scheduling Options") + CreateProgramInfoString(*pginfo);
2955 
2956  auto *menu = new MythMenu(title, this, "slotmenu");
2957 
2958  menu->AddItem(tr("Edit Recording Schedule"),
2959  qOverload<>(&PlaybackBox::EditScheduled));
2960 
2961  menu->AddItem(tr("Allow this episode to re-record"), &PlaybackBox::doAllowRerecord);
2962 
2963  menu->AddItem(tr("Show Recording Details"), &PlaybackBox::ShowDetails);
2964 
2965  menu->AddItem(tr("Change Recording Metadata"), &PlaybackBox::showMetadataEditor);
2966 
2967  menu->AddItem(tr("Custom Edit"), &PlaybackBox::EditCustom);
2968 
2969  return menu;
2970 }
2971 
2972 static const std::array<const int,kMaxJobs> kJobs
2974  JOB_TRANSCODE,
2975  JOB_COMMFLAG,
2976  JOB_METADATA,
2977  JOB_USERJOB1,
2978  JOB_USERJOB2,
2979  JOB_USERJOB3,
2980  JOB_USERJOB4,
2981 };
2982 std::array<PlaybackBoxCb,kMaxJobs*2> PlaybackBox::kMySlots
2983 { // stop start
2991 };
2992 
2994 {
2995  ProgramInfo *pginfo = GetCurrentProgram();
2996  if (!pginfo)
2997  return nullptr;
2998 
2999  QString title = tr("Job Options") + CreateProgramInfoString(*pginfo);
3000 
3001  auto *menu = new MythMenu(title, this, "slotmenu");
3002 
3003  const std::array<const bool,kMaxJobs> add
3004  {
3005  true,
3006  true,
3007  true,
3008  !gCoreContext->GetSetting("UserJob1", "").isEmpty(),
3009  !gCoreContext->GetSetting("UserJob2", "").isEmpty(),
3010  !gCoreContext->GetSetting("UserJob3", "").isEmpty(),
3011  !gCoreContext->GetSetting("UserJob4", "").isEmpty(),
3012  };
3013  const std::array<const QString,kMaxJobs*2> desc
3014  {
3015  // stop start
3016  tr("Stop Transcoding"), tr("Begin Transcoding"),
3017  tr("Stop Commercial Detection"), tr("Begin Commercial Detection"),
3018  tr("Stop Metadata Lookup"), tr("Begin Metadata Lookup"),
3019  "1", "1",
3020  "2", "2",
3021  "3", "3",
3022  "4", "4",
3023  };
3024 
3025  for (size_t i = 0; i < kMaxJobs; i++)
3026  {
3027  if (!add[i])
3028  continue;
3029 
3030  QString stop_desc = desc[i*2+0];
3031  QString start_desc = desc[i*2+1];
3032 
3033  if (start_desc.toUInt())
3034  {
3035  QString jobTitle = gCoreContext->GetSetting(
3036  "UserJobDesc"+start_desc, tr("User Job") + " #" + start_desc);
3037  stop_desc = tr("Stop") + ' ' + jobTitle;
3038  start_desc = tr("Begin") + ' ' + jobTitle;
3039  }
3040 
3041  bool running = JobQueue::IsJobQueuedOrRunning(
3042  kJobs[i], pginfo->GetChanID(), pginfo->GetRecordingStartTime());
3043 
3044  MythMenu *submenu = ((kJobs[i] == JOB_TRANSCODE) && !running)
3045  ? createTranscodingProfilesMenu() : nullptr;
3046  menu->AddItem((running) ? stop_desc : start_desc,
3047  kMySlots[i * 2 + (running ? 0 : 1)], submenu);
3048  }
3049 
3050  return menu;
3051 }
3052 
3054 {
3055  QString label = tr("Transcoding profiles");
3056 
3057  auto *menu = new MythMenu(label, this, "transcode");
3058 
3059  menu->AddItemV(tr("Default"), QVariant::fromValue(-1));
3060  menu->AddItemV(tr("Autodetect"), QVariant::fromValue(0));
3061 
3062  MSqlQuery query(MSqlQuery::InitCon());
3063  query.prepare("SELECT r.name, r.id "
3064  "FROM recordingprofiles r, profilegroups p "
3065  "WHERE p.name = 'Transcoders' "
3066  "AND r.profilegroup = p.id "
3067  "AND r.name != 'RTjpeg/MPEG4' "
3068  "AND r.name != 'MPEG2' ");
3069 
3070  if (!query.exec())
3071  {
3072  MythDB::DBError(LOC + "unable to query transcoders", query);
3073  return nullptr;
3074  }
3075 
3076  while (query.next())
3077  {
3078  QString transcoder_name = query.value(0).toString();
3079  int transcoder_id = query.value(1).toInt();
3080 
3081  // Translatable strings for known profiles
3082  if (transcoder_name == "High Quality")
3083  transcoder_name = tr("High Quality");
3084  else if (transcoder_name == "Medium Quality")
3085  transcoder_name = tr("Medium Quality");
3086  else if (transcoder_name == "Low Quality")
3087  transcoder_name = tr("Low Quality");
3088 
3089  menu->AddItemV(transcoder_name, QVariant::fromValue(transcoder_id));
3090  }
3091 
3092  return menu;
3093 }
3094 
3096 {
3097  ProgramInfo *pginfo = GetCurrentProgram();
3098 
3099  if (!pginfo)
3100  return;
3101 
3102  if (id >= 0)
3103  {
3104  RecordingInfo ri(*pginfo);
3106  }
3108 }
3109 
3111 {
3112  QString label =
3113  (asFileNotFound == pginfo.GetAvailableStatus()) ?
3114  tr("Recording file cannot be found") :
3115  (asZeroByte == pginfo.GetAvailableStatus()) ?
3116  tr("Recording file contains no data") :
3117  tr("Recording Options");
3118 
3119  m_popupMenu = new MythMenu(label + CreateProgramInfoString(pginfo), this, "actionmenu");
3120 
3121  if ((asFileNotFound == pginfo.GetAvailableStatus()) ||
3122  (asZeroByte == pginfo.GetAvailableStatus()))
3123  {
3124  if (m_playList.contains(pginfo.GetRecordingID()))
3125  {
3126  m_popupMenu->AddItem(tr("Remove from Playlist"),
3127  qOverload<>(&PlaybackBox::togglePlayListItem));
3128  }
3129  else
3130  {
3131  m_popupMenu->AddItem(tr("Add to Playlist"),
3132  qOverload<>(&PlaybackBox::togglePlayListItem));
3133  }
3134 
3135  if (!m_playList.isEmpty())
3136  m_popupMenu->AddItem(tr("Playlist Options"), nullptr, createPlaylistMenu());
3137 
3138  m_popupMenu->AddItem(tr("Recording Options"), nullptr, createRecordingMenu());
3139 
3142  {
3143  m_popupMenu->AddItem(tr("List Recorded Episodes"),
3145  }
3146  else
3147  {
3148  m_popupMenu->AddItem(tr("List All Recordings"),
3150  }
3151 
3152  m_popupMenu->AddItem(tr("Delete"), &PlaybackBox::askDelete);
3153 
3154  DisplayPopupMenu();
3155 
3156  return;
3157  }
3158 
3159  bool sameProgram = false;
3160 
3161  if (m_player)
3162  sameProgram = m_player->IsSameProgram(&pginfo);
3163 
3164  TVState tvstate = kState_None;
3165 
3166  if (!sameProgram)
3167  {
3168  if (pginfo.IsBookmarkSet() || pginfo.IsLastPlaySet())
3169  m_popupMenu->AddItem(tr("Play from..."), nullptr, createPlayFromMenu());
3170  else
3171  m_popupMenu->AddItem(tr("Play"),
3172  qOverload<>(&PlaybackBox::PlayFromAnyMark));
3173  }
3174 
3175  if (!m_player)
3176  {
3177  if (m_playList.contains(pginfo.GetRecordingID()))
3178  {
3179  m_popupMenu->AddItem(tr("Remove from Playlist"),
3180  qOverload<>(&PlaybackBox::togglePlayListItem));
3181  }
3182  else
3183  {
3184  m_popupMenu->AddItem(tr("Add to Playlist"),
3185  qOverload<>(&PlaybackBox::togglePlayListItem));
3186  }
3187  if (!m_playList.isEmpty())
3188  {
3189  m_popupMenu->AddItem(tr("Playlist Options"), nullptr, createPlaylistMenu());
3190  }
3191  }
3192 
3193  if ((pginfo.GetRecordingStatus() == RecStatus::Recording ||
3194  pginfo.GetRecordingStatus() == RecStatus::Tuning ||
3195  pginfo.GetRecordingStatus() == RecStatus::Failing) &&
3196  (!sameProgram ||
3197  (tvstate != kState_WatchingLiveTV &&
3198  tvstate != kState_WatchingRecording)))
3199  {
3200  m_popupMenu->AddItem(tr("Stop Recording"), &PlaybackBox::askStop);
3201  }
3202 
3203  if (pginfo.IsWatched())
3204  m_popupMenu->AddItem(tr("Mark as Unwatched"), &PlaybackBox::toggleWatched);
3205  else
3206  m_popupMenu->AddItem(tr("Mark as Watched"), &PlaybackBox::toggleWatched);
3207 
3208  m_popupMenu->AddItem(tr("Storage Options"), nullptr, createStorageMenu());
3209  m_popupMenu->AddItem(tr("Recording Options"), nullptr, createRecordingMenu());
3210  m_popupMenu->AddItem(tr("Job Options"), nullptr, createJobMenu());
3211 
3214  {
3215  m_popupMenu->AddItem(tr("List Recorded Episodes"),
3217  }
3218  else
3219  {
3220  m_popupMenu->AddItem(tr("List All Recordings"),
3222  }
3223 
3224  if (!sameProgram)
3225  {
3226  if (pginfo.GetRecordingGroup() == "Deleted")
3227  {
3228  push_onto_del(m_delList, pginfo);
3229  m_popupMenu->AddItem(tr("Undelete"), &PlaybackBox::Undelete);
3230  m_popupMenu->AddItem(tr("Delete Forever"), qOverload<>(&PlaybackBox::Delete));
3231  }
3232  else
3233  {
3234  m_popupMenu->AddItem(tr("Delete"), &PlaybackBox::askDelete);
3235  }
3236  }
3237 
3238  DisplayPopupMenu();
3239 }
3240 
3242 {
3243  QDateTime recstartts = pginfo.GetRecordingStartTime();
3244  QDateTime recendts = pginfo.GetRecordingEndTime();
3245 
3246  QString timedate = QString("%1 - %2")
3247  .arg(MythDate::toString(
3249  MythDate::toString(recendts, MythDate::kTime));
3250 
3251  QString title = pginfo.GetTitle();
3252 
3253  QString extra;
3254 
3255  if (!pginfo.GetSubtitle().isEmpty())
3256  {
3257  extra = QString('\n') + pginfo.GetSubtitle();
3258  }
3259 
3260  return QString("\n%1%2\n%3").arg(title, extra, timedate);
3261 }
3262 
3264 {
3265  QList<uint>::Iterator it;
3266  for (it = m_playList.begin(); it != m_playList.end(); ++it)
3267  {
3268  ProgramInfo *tmpItem = FindProgramInUILists(*it);
3269 
3270  if (!tmpItem)
3271  continue;
3272 
3273  MythUIButtonListItem *item =
3274  m_recordingList->GetItemByData(QVariant::fromValue(tmpItem));
3275 
3276  if (item)
3277  item->DisplayState("no", "playlist");
3278  }
3279  m_playList.clear();
3280 }
3281 
3283 {
3284  playSelectedPlaylist(false);
3285 }
3286 
3287 
3289 {
3290  playSelectedPlaylist(true);
3291 }
3292 
3294 {
3295  ProgramInfo *pginfo = GetCurrentProgram();
3296  if (pginfo)
3297  {
3298  push_onto_del(m_delList, *pginfo);
3300  }
3301 }
3302 
3310 {
3311  ProgramInfo *pginfo = GetCurrentProgram();
3312 
3313  if (!pginfo)
3314  return;
3315 
3316  RecordingInfo ri(*pginfo);
3317  ri.ForgetHistory();
3318  *pginfo = ri;
3319 }
3320 
3322 {
3323  QList<uint>::Iterator it;
3324 
3325  for (it = m_playList.begin(); it != m_playList.end(); ++it)
3326  {
3327  ProgramInfo *pginfo = FindProgramInUILists(*it);
3328  if (pginfo != nullptr)
3329  {
3330  RecordingInfo ri(*pginfo);
3331  ri.ForgetHistory();
3332  *pginfo = ri;
3333  }
3334  }
3335 
3336  doClearPlaylist();
3337  UpdateUILists();
3338 }
3339 
3340 void PlaybackBox::doJobQueueJob(int jobType, int jobFlags)
3341 {
3342  ProgramInfo *pginfo = GetCurrentProgram();
3343 
3344  if (!pginfo)
3345  return;
3346 
3347  ProgramInfo *tmpItem = FindProgramInUILists(*pginfo);
3348 
3350  jobType, pginfo->GetChanID(), pginfo->GetRecordingStartTime()))
3351  {
3353  jobType, pginfo->GetChanID(), pginfo->GetRecordingStartTime(),
3354  JOB_STOP);
3355  if ((jobType & JOB_COMMFLAG) && (tmpItem))
3356  {
3357  tmpItem->SetEditing(false);
3358  tmpItem->SetFlagging(false);
3359  }
3360  }
3361  else
3362  {
3363  QString jobHost;
3364  if (gCoreContext->GetBoolSetting("JobsRunOnRecordHost", false))
3365  jobHost = pginfo->GetHostname();
3366 
3367  JobQueue::QueueJob(jobType, pginfo->GetChanID(),
3368  pginfo->GetRecordingStartTime(), "", "", jobHost,
3369  jobFlags);
3370  }
3371 }
3372 
3374 {
3376 }
3377 
3379 {
3381 }
3382 
3383 void PlaybackBox::doPlaylistJobQueueJob(int jobType, int jobFlags)
3384 {
3385  for (const uint pbs : qAsConst(m_playList))
3386  {
3387  ProgramInfo *tmpItem = FindProgramInUILists(pbs);
3388  if (tmpItem &&
3390  jobType,
3391  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime())))
3392  {
3393  QString jobHost;
3394  if (gCoreContext->GetBoolSetting("JobsRunOnRecordHost", false))
3395  jobHost = tmpItem->GetHostname();
3396 
3397  JobQueue::QueueJob(jobType, tmpItem->GetChanID(),
3398  tmpItem->GetRecordingStartTime(),
3399  "", "", jobHost, jobFlags);
3400  }
3401  }
3402 }
3403 
3405 {
3406  QList<uint>::Iterator it;
3407 
3408  for (it = m_playList.begin(); it != m_playList.end(); ++it)
3409  {
3410  ProgramInfo *tmpItem = FindProgramInUILists(*it);
3411  if (tmpItem &&
3413  jobType,
3414  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime())))
3415  {
3417  jobType, tmpItem->GetChanID(),
3418  tmpItem->GetRecordingStartTime(), JOB_STOP);
3419 
3420  if (jobType & JOB_COMMFLAG)
3421  {
3422  tmpItem->SetEditing(false);
3423  tmpItem->SetFlagging(false);
3424  }
3425  }
3426  }
3427 }
3428 
3430 {
3431  ProgramInfo *pginfo = GetCurrentProgram();
3432  if (pginfo)
3433  {
3434  push_onto_del(m_delList, *pginfo);
3436  }
3437 }
3438 
3439 void PlaybackBox::PlaylistDelete(bool forgetHistory)
3440 {
3441  QString forceDeleteStr("0");
3442 
3443  QStringList list;
3444  for (int id : qAsConst(m_playList))
3445  {
3446  ProgramInfo *tmpItem = FindProgramInUILists(id);
3447  if (tmpItem && tmpItem->QueryIsDeleteCandidate())
3448  {
3449  tmpItem->SetAvailableStatus(asPendingDelete, "PlaylistDelete");
3450  list.push_back(QString::number(tmpItem->GetRecordingID()));
3451  list.push_back(forceDeleteStr);
3452  list.push_back(forgetHistory ? "1" : "0");
3453 
3454  // if the item is in the current recording list UI then delete it.
3455  MythUIButtonListItem *uiItem =
3456  m_recordingList->GetItemByData(QVariant::fromValue(tmpItem));
3457  if (uiItem)
3458  m_recordingList->RemoveItem(uiItem);
3459  }
3460  }
3461  m_playList.clear();
3462 
3463  if (!list.empty())
3464  m_helper.DeleteRecordings(list);
3465 
3466  doClearPlaylist();
3467 }
3468 
3469 // FIXME: Huh? This doesn't specify which recording to undelete, it just
3470 // undeletes the first one on the list
3472 {
3473  uint recordingID = 0;
3474  if (extract_one_del(m_delList, recordingID))
3475  m_helper.UndeleteRecording(recordingID);
3476 }
3477 
3479 {
3480  uint recordingID = 0;
3481  while (extract_one_del(m_delList, recordingID))
3482  {
3483  if (flags & kIgnore)
3484  continue;
3485 
3486  RemoveProgram(recordingID, (flags & kForgetHistory) != 0, (flags & kForce) != 0);
3487 
3488  if (!(flags & kAllRemaining))
3489  break;
3490  }
3491 
3492  if (!m_delList.empty())
3493  {
3494  auto *e = new MythEvent("DELETE_FAILURES", m_delList);
3495  m_delList.clear();
3496  QCoreApplication::postEvent(this, e);
3497  }
3498 }
3499 
3501 {
3502  ProgramInfo *pginfo = GetCurrentProgram();
3503  if (pginfo) {
3504  QString title = pginfo->GetTitle().toLower();
3505  MythUIButtonListItem* group = m_groupList->GetItemByData(QVariant::fromValue(title));
3506  if (group)
3507  {
3508  m_groupList->SetItemCurrent(group);
3509  // set focus back to previous item
3510  MythUIButtonListItem *previousItem = m_recordingList->GetItemByData(QVariant::fromValue(pginfo));
3511  m_recordingList->SetItemCurrent(previousItem);
3512  }
3513  }
3514 }
3515 
3517 {
3518  ProgramInfo *pginfo = GetCurrentProgram();
3520  if (pginfo)
3521  {
3522  // set focus back to previous item
3523  MythUIButtonListItem *previousitem =
3524  m_recordingList->GetItemByData(QVariant::fromValue(pginfo));
3525  m_recordingList->SetItemCurrent(previousitem);
3526  }
3527 }
3528 
3530 {
3531  return FindProgramInUILists( pginfo.GetRecordingID(),
3532  pginfo.GetRecordingGroup());
3533 }
3534 
3536  const QString& recgroup)
3537 {
3538  // LiveTV ProgramInfo's are not in the aggregated list
3539  std::array<ProgramList::iterator,2> _it {
3540  m_progLists[tr("Live TV").toLower()].begin(), m_progLists[""].begin() };
3541  std::array<ProgramList::iterator,2> _end {
3542  m_progLists[tr("Live TV").toLower()].end(), m_progLists[""].end() };
3543 
3544  if (recgroup != "LiveTV")
3545  {
3546  swap( _it[0], _it[1]);
3547  swap(_end[0], _end[1]);
3548  }
3549 
3550  for (uint i = 0; i < 2; i++)
3551  {
3552  auto it = _it[i];
3553  auto end = _end[i];
3554  for (; it != end; ++it)
3555  {
3556  if ((*it)->GetRecordingID() == recordingID)
3557  {
3558  return *it;
3559  }
3560  }
3561  }
3562 
3563  return nullptr;
3564 }
3565 
3567 {
3569 
3570  if (!item)
3571  return;
3572 
3573  auto *pginfo = item->GetData().value<ProgramInfo *>();
3574 
3575  if (!pginfo)
3576  return;
3577 
3578  bool on = !pginfo->IsWatched();
3579  pginfo->SaveWatched(on);
3580  item->DisplayState((on)?"yes":"on", "watched");
3581  updateIcons(pginfo);
3582 
3583  // A refill affects the responsiveness of the UI and we only
3584  // need to rebuild the list if the watch list is displayed
3585  if (m_viewMask & VIEW_WATCHLIST)
3586  UpdateUILists();
3587 }
3588 
3590 {
3592 
3593  if (!item)
3594  return;
3595 
3596  auto *pginfo = item->GetData().value<ProgramInfo *>();
3597 
3598  if (!pginfo)
3599  return;
3600 
3601  bool on = !pginfo->IsAutoExpirable();
3602  pginfo->SaveAutoExpire((on) ? kNormalAutoExpire : kDisableAutoExpire, true);
3603  item->DisplayState((on)?"yes":"no", "autoexpire");
3604  updateIcons(pginfo);
3605 }
3606 
3608 {
3610 
3611  if (!item)
3612  return;
3613 
3614  auto *pginfo = item->GetData().value<ProgramInfo *>();
3615 
3616  if (!pginfo)
3617  return;
3618 
3619  bool on = !pginfo->IsPreserved();
3620  pginfo->SavePreserve(on);
3621  item->DisplayState(on?"yes":"no", "preserve");
3622  updateIcons(pginfo);
3623 }
3624 
3625 void PlaybackBox::toggleView(ViewMask itemMask, bool setOn)
3626 {
3627  if (setOn)
3628  m_viewMask = (ViewMask)(m_viewMask | itemMask);
3629  else
3630  m_viewMask = (ViewMask)(m_viewMask & ~itemMask);
3631 
3632  UpdateUILists();
3633 }
3634 
3636 {
3637  QString groupname = m_groupList->GetItemCurrent()->GetData().toString();
3638 
3639  for (auto *pl : qAsConst(m_progLists[groupname]))
3640  {
3641  if (pl && (pl->GetAvailableStatus() == asAvailable))
3642  togglePlayListItem(pl);
3643  }
3644 }
3645 
3647 {
3649 
3650  if (!item)
3651  return;
3652 
3653  auto *pginfo = item->GetData().value<ProgramInfo *>();
3654 
3655  if (!pginfo)
3656  return;
3657 
3658  togglePlayListItem(pginfo);
3659 
3662 }
3663 
3665 {
3666  if (!pginfo)
3667  return;
3668 
3669  uint recordingID = pginfo->GetRecordingID();
3670 
3671  MythUIButtonListItem *item =
3672  m_recordingList->GetItemByData(QVariant::fromValue(pginfo));
3673 
3674  if (m_playList.contains(recordingID))
3675  {
3676  if (item)
3677  item->DisplayState("no", "playlist");
3678 
3679  m_playList.removeAll(recordingID);
3680  }
3681  else
3682  {
3683  if (item)
3684  item->DisplayState("yes", "playlist");
3685  m_playList.append(recordingID);
3686  }
3687 }
3688 
3690 {
3691  int commands = 0;
3692  QString command;
3693 
3694  m_ncLock.lock();
3695  commands = m_networkControlCommands.size();
3696  m_ncLock.unlock();
3697 
3698  while (commands)
3699  {
3700  m_ncLock.lock();
3701  command = m_networkControlCommands.front();
3702  m_networkControlCommands.pop_front();
3703  m_ncLock.unlock();
3704 
3706 
3707  m_ncLock.lock();
3708  commands = m_networkControlCommands.size();
3709  m_ncLock.unlock();
3710  }
3711 }
3712 
3713 void PlaybackBox::processNetworkControlCommand(const QString &command)
3714 {
3715  QStringList tokens = command.simplified().split(" ");
3716 
3717  if (tokens.size() >= 4 && (tokens[1] == "PLAY" || tokens[1] == "RESUME"))
3718  {
3719  if (tokens.size() == 6 && tokens[2] == "PROGRAM")
3720  {
3721  int clientID = tokens[5].toInt();
3722 
3723  LOG(VB_GENERAL, LOG_INFO, LOC +
3724  QString("NetworkControl: Trying to %1 program '%2' @ '%3'")
3725  .arg(tokens[1], tokens[3], tokens[4]));
3726 
3727  if (m_playingSomething)
3728  {
3729  LOG(VB_GENERAL, LOG_ERR, LOC +
3730  "NetworkControl: Already playing");
3731 
3732  QString msg = QString(
3733  "NETWORK_CONTROL RESPONSE %1 ERROR: Unable to play, "
3734  "player is already playing another recording.")
3735  .arg(clientID);
3736 
3737  MythEvent me(msg);
3738  gCoreContext->dispatch(me);
3739  return;
3740  }
3741 
3742  uint chanid = tokens[3].toUInt();
3743  QDateTime recstartts = MythDate::fromString(tokens[4]);
3744  ProgramInfo pginfo(chanid, recstartts);
3745 
3746  if (pginfo.GetChanID())
3747  {
3748  QString msg = QString("NETWORK_CONTROL RESPONSE %1 OK")
3749  .arg(clientID);
3750  MythEvent me(msg);
3751  gCoreContext->dispatch(me);
3752 
3753  pginfo.SetPathname(pginfo.GetPlaybackURL());
3754 
3755  const bool ignoreBookmark = (tokens[1] == "PLAY");
3756  const bool ignoreProgStart = true;
3757  const bool ignoreLastPlayPos = true;
3758  const bool underNetworkControl = true;
3759  PlayX(pginfo, ignoreBookmark, ignoreProgStart,
3760  ignoreLastPlayPos, underNetworkControl);
3761  }
3762  else
3763  {
3764  QString message = QString("NETWORK_CONTROL RESPONSE %1 "
3765  "ERROR: Could not find recording for "
3766  "chanid %2 @ %3")
3767  .arg(tokens[5], tokens[3], tokens[4]);
3768  MythEvent me(message);
3769  gCoreContext->dispatch(me);
3770  }
3771  }
3772  }
3773 }
3774 
3775 bool PlaybackBox::keyPressEvent(QKeyEvent *event)
3776 {
3777  // This should be an impossible keypress we've simulated
3778  if ((event->key() == Qt::Key_LaunchMedia) &&
3779  (event->modifiers() ==
3780  (Qt::ShiftModifier |
3781  Qt::ControlModifier |
3782  Qt::AltModifier |
3783  Qt::MetaModifier |
3784  Qt::KeypadModifier)))
3785  {
3786  event->accept();
3787  m_ncLock.lock();
3788  int commands = m_networkControlCommands.size();
3789  m_ncLock.unlock();
3790  if (commands)
3792  return true;
3793  }
3794 
3795  if (GetFocusWidget()->keyPressEvent(event))
3796  return true;
3797 
3798  QStringList actions;
3799  bool handled = GetMythMainWindow()->TranslateKeyPress("TV Frontend",
3800  event, actions);
3801 
3802  for (int i = 0; i < actions.size() && !handled; ++i)
3803  {
3804  QString action = actions[i];
3805  handled = true;
3806 
3807  if (action == ACTION_1 || action == "HELP")
3808  showIconHelp();
3809  else if (action == "MENU")
3810  {
3811  ShowMenu();
3812  }
3813  else if (action == "NEXTFAV")
3814  {
3815  if (GetFocusWidget() == m_groupList)
3817  else
3819  }
3820  else if (action == "TOGGLEFAV")
3821  {
3822  m_playList.clear();
3823  UpdateUILists();
3824  }
3825  else if (action == ACTION_TOGGLERECORD)
3826  {
3828  UpdateUILists();
3829  }
3830  else if (action == ACTION_PAGERIGHT)
3831  {
3833  }
3834  else if (action == ACTION_PAGELEFT)
3835  {
3836  QString nextGroup;
3837  m_recGroupsLock.lock();
3838  if (m_recGroupIdx >= 0 && !m_recGroups.empty())
3839  {
3840  if (--m_recGroupIdx < 0)
3841  m_recGroupIdx = m_recGroups.size() - 1;
3842  nextGroup = m_recGroups[m_recGroupIdx];
3843  }
3844  m_recGroupsLock.unlock();
3845 
3846  if (!nextGroup.isEmpty())
3847  displayRecGroup(nextGroup);
3848  }
3849  else if (action == "NEXTVIEW")
3850  {
3852  if (++curpos >= m_groupList->GetCount())
3853  curpos = 0;
3854  m_groupList->SetItemCurrent(curpos);
3855  }
3856  else if (action == "PREVVIEW")
3857  {
3859  if (--curpos < 0)
3860  curpos = m_groupList->GetCount() - 1;
3861  m_groupList->SetItemCurrent(curpos);
3862  }
3863  else if (action == ACTION_LISTRECORDEDEPISODES)
3864  {
3868  else
3870  }
3871  else if (action == "CHANGERECGROUP")
3872  showGroupFilter();
3873  else if (action == "CHANGEGROUPVIEW")
3874  showViewChanger();
3875  else if (action == "EDIT")
3876  EditScheduled();
3877  else if (m_titleList.size() > 1)
3878  {
3879  if (action == "DELETE")
3881  else if (action == ACTION_PLAYBACK)
3882  PlayFromAnyMark();
3883  else if (action == "DETAILS" || action == "INFO")
3884  ShowDetails();
3885  else if (action == "CUSTOMEDIT")
3886  EditCustom();
3887  else if (action == "GUIDE")
3888  ShowGuide();
3889  else if (action == "UPCOMING")
3890  ShowUpcoming();
3891  else if (action == ACTION_VIEWSCHEDULED)
3893  else if (action == ACTION_PREVRECORDED)
3894  ShowPrevious();
3895  else
3896  handled = false;
3897  }
3898  else
3899  handled = false;
3900  }
3901 
3902  if (!handled && MythScreenType::keyPressEvent(event))
3903  handled = true;
3904 
3905  return handled;
3906 }
3907 
3908 void PlaybackBox::customEvent(QEvent *event)
3909 {
3910  if (event->type() == DialogCompletionEvent::kEventType)
3911  {
3912  auto *dce = dynamic_cast<DialogCompletionEvent*>(event);
3913  if (!dce)
3914  return;
3915 
3916  QString resultid = dce->GetId();
3917 
3918  if (resultid == "transcode" && dce->GetResult() >= 0)
3919  changeProfileAndTranscode(dce->GetData().toInt());
3920  }
3921  else if (event->type() == MythEvent::kMythEventMessage)
3922  {
3923  auto *me = dynamic_cast<MythEvent *>(event);
3924  if (me == nullptr)
3925  return;
3926 
3927  const QString& message = me->Message();
3928 
3929  if (message.startsWith("RECORDING_LIST_CHANGE"))
3930  {
3931  QStringList tokens = message.simplified().split(" ");
3932  uint recordingID = 0;
3933  if (tokens.size() >= 3)
3934  recordingID = tokens[2].toUInt();
3935 
3936  if ((tokens.size() >= 2) && tokens[1] == "UPDATE")
3937  {
3938  ProgramInfo evinfo(me->ExtraDataList());
3939  if (evinfo.HasPathname() || evinfo.GetChanID())
3940  {
3941  uint32_t flags = m_programInfoCache.Update(evinfo);
3942  if (flags != ProgramInfoCache::PIC_NO_ACTION)
3943  HandleUpdateItemEvent(evinfo.GetRecordingID(), flags);
3944  }
3945  }
3946  else if (recordingID && (tokens[1] == "ADD"))
3947  {
3948  ProgramInfo evinfo(recordingID);
3949  if (evinfo.GetChanID())
3950  {
3952  HandleRecordingAddEvent(evinfo);
3953  }
3954  }
3955  else if (recordingID && (tokens[1] == "DELETE"))
3956  {
3957  HandleRecordingRemoveEvent(recordingID);
3958  }
3959  else
3960  {
3962  }
3963  }
3964  else if (message.startsWith("NETWORK_CONTROL"))
3965  {
3966  QStringList tokens = message.simplified().split(" ");
3967  if ((tokens[1] != "ANSWER") && (tokens[1] != "RESPONSE"))
3968  {
3969  m_ncLock.lock();
3970  m_networkControlCommands.push_back(message);
3971  m_ncLock.unlock();
3972 
3973  // This should be an impossible keypress we're simulating
3974  Qt::KeyboardModifiers modifiers =
3975  Qt::ShiftModifier |
3976  Qt::ControlModifier |
3977  Qt::AltModifier |
3978  Qt::MetaModifier |
3979  Qt::KeypadModifier;
3980  auto *keyevent = new QKeyEvent(QEvent::KeyPress,
3981  Qt::Key_LaunchMedia, modifiers);
3982  QCoreApplication::postEvent(GetMythMainWindow(), keyevent);
3983 
3984  keyevent = new QKeyEvent(QEvent::KeyRelease,
3985  Qt::Key_LaunchMedia, modifiers);
3986  QCoreApplication::postEvent(GetMythMainWindow(), keyevent);
3987  }
3988  }
3989  else if (message.startsWith("UPDATE_FILE_SIZE"))
3990  {
3991  QStringList tokens = message.simplified().split(" ");
3992  if (tokens.size() >= 3)
3993  {
3994  bool ok = false;
3995  uint recordingID = tokens[1].toUInt();
3996  uint64_t filesize = tokens[2].toLongLong(&ok);
3997  if (ok)
3998  {
3999  // Delegate to background thread
4000  MConcurrent::run("UpdateFileSize", &m_programInfoCache,
4002  recordingID, filesize,
4004  }
4005  }
4006  }
4007  else if (message == "UPDATE_UI_LIST")
4008  {
4009  if (m_playingSomething)
4010  m_needUpdate = true;
4011  else
4012  {
4013  UpdateUILists();
4015  }
4016  }
4017  else if (message.startsWith("UPDATE_UI_ITEM"))
4018  {
4019  QStringList tokens = message.simplified().split(" ");
4020  if (tokens.size() < 3)
4021  return;
4022 
4023  uint recordingID = tokens[1].toUInt();
4024  auto flags = static_cast<ProgramInfoCache::UpdateState>(tokens[2].toUInt());
4025 
4026  if (flags != ProgramInfoCache::PIC_NO_ACTION)
4027  HandleUpdateItemEvent(recordingID, flags);
4028  }
4029  else if (message == "UPDATE_USAGE_UI")
4030  {
4031  UpdateUsageUI();
4032  }
4033  else if (message == "RECONNECT_SUCCESS")
4034  {
4036  }
4037  else if (message == "LOCAL_PBB_DELETE_RECORDINGS")
4038  {
4039  QStringList list;
4040  for (uint i = 0; i+2 < (uint)me->ExtraDataList().size(); i+=3)
4041  {
4042  uint recordingID = me->ExtraDataList()[i+0].toUInt();
4043  ProgramInfo *pginfo =
4044  m_programInfoCache.GetRecordingInfo(recordingID);
4045 
4046  if (!pginfo)
4047  {
4048  LOG(VB_GENERAL, LOG_WARNING, LOC +
4049  QString("LOCAL_PBB_DELETE_RECORDINGS - "
4050  "No matching recording %1")
4051  .arg(recordingID));
4052  continue;
4053  }
4054 
4055  QString forceDeleteStr = me->ExtraDataList()[i+1];
4056  QString forgetHistoryStr = me->ExtraDataList()[i+2];
4057 
4058  list.push_back(QString::number(pginfo->GetRecordingID()));
4059  list.push_back(forceDeleteStr);
4060  list.push_back(forgetHistoryStr);
4062  "LOCAL_PBB_DELETE_RECORDINGS");
4063 
4064  // if the item is in the current recording list UI
4065  // then delete it.
4066  MythUIButtonListItem *uiItem =
4067  m_recordingList->GetItemByData(QVariant::fromValue(pginfo));
4068  if (uiItem)
4069  m_recordingList->RemoveItem(uiItem);
4070  }
4071  if (!list.empty())
4072  m_helper.DeleteRecordings(list);
4073  }
4074  else if (message == "DELETE_SUCCESSES")
4075  {
4077  }
4078  else if (message == "DELETE_FAILURES")
4079  {
4080  if (me->ExtraDataList().size() < 3)
4081  return;
4082 
4083  for (uint i = 0; i+2 < (uint)me->ExtraDataList().size(); i += 3)
4084  {
4086  me->ExtraDataList()[i+0].toUInt());
4087  if (pginfo)
4088  {
4089  pginfo->SetAvailableStatus(asAvailable, "DELETE_FAILURES");
4091  }
4092  }
4093 
4094  bool forceDelete = me->ExtraDataList()[1].toUInt() != 0U;
4095  if (!forceDelete)
4096  {
4097  m_delList = me->ExtraDataList();
4098  if (!m_menuDialog)
4099  {
4101  return;
4102  }
4103  LOG(VB_GENERAL, LOG_WARNING, LOC +
4104  "Delete failures not handled due to "
4105  "pre-existing popup.");
4106  }
4107 
4108  // Since we deleted items from the UI after we set
4109  // asPendingDelete, we need to put them back now..
4111  }
4112  else if (message == "PREVIEW_SUCCESS")
4113  {
4114  HandlePreviewEvent(me->ExtraDataList());
4115  }
4116  else if (message == "PREVIEW_FAILED" && me->ExtraDataCount() >= 5)
4117  {
4118  for (uint i = 4; i < (uint) me->ExtraDataCount(); i++)
4119  {
4120  const QString& token = me->ExtraData(i);
4121  QSet<QString>::iterator it = m_previewTokens.find(token);
4122  if (it != m_previewTokens.end())
4123  m_previewTokens.erase(it);
4124  }
4125  }
4126  else if (message == "AVAILABILITY" && me->ExtraDataCount() == 8)
4127  {
4128  static constexpr std::chrono::milliseconds kMaxUIWaitTime = 10s;
4129  QStringList list = me->ExtraDataList();
4130  uint recordingID = list[0].toUInt();
4131  auto cat = (CheckAvailabilityType) list[1].toInt();
4132  auto availableStatus = (AvailableStatusType) list[2].toInt();
4133  uint64_t fs = list[3].toULongLong();
4134  QTime tm;
4135  tm.setHMS(list[4].toUInt(), list[5].toUInt(),
4136  list[6].toUInt(), list[7].toUInt());
4137  QTime now = QTime::currentTime();
4138  auto time_elapsed = std::chrono::milliseconds(tm.msecsTo(now));
4139  if (time_elapsed < 0ms)
4140  time_elapsed += 24h;
4141 
4142  AvailableStatusType old_avail = availableStatus;
4143  ProgramInfo *pginfo = FindProgramInUILists(recordingID);
4144  if (pginfo)
4145  {
4146  pginfo->SetFilesize(std::max(pginfo->GetFilesize(), fs));
4147  old_avail = pginfo->GetAvailableStatus();
4148  pginfo->SetAvailableStatus(availableStatus, "AVAILABILITY");
4149  }
4150 
4151  if (time_elapsed >= kMaxUIWaitTime)
4152  m_playListPlay.clear();
4153 
4154  bool playnext = ((kCheckForPlaylistAction == cat) &&
4155  !m_playListPlay.empty());
4156 
4157 
4158  if (((kCheckForPlayAction == cat) ||
4159  (kCheckForPlaylistAction == cat)) &&
4160  (time_elapsed < kMaxUIWaitTime))
4161  {
4162  if (asAvailable != availableStatus)
4163  {
4164  if (kCheckForPlayAction == cat && pginfo)
4165  ShowAvailabilityPopup(*pginfo);
4166  }
4167  else if (pginfo)
4168  {
4169  playnext = false;
4170  const bool ignoreBookmark = false;
4171  const bool ignoreProgStart = false;
4172  const bool ignoreLastPlayPos = true;
4173  const bool underNetworkControl = false;
4174  Play(*pginfo, kCheckForPlaylistAction == cat,
4175  ignoreBookmark, ignoreProgStart, ignoreLastPlayPos,
4176  underNetworkControl);
4177  }
4178  }
4179 
4180  if (playnext)
4181  {
4182  // failed to play this item, instead
4183  // play the next item on the list..
4184  QCoreApplication::postEvent(
4185  this, new MythEvent("PLAY_PLAYLIST"));
4186  }
4187 
4188  if (old_avail != availableStatus)
4189  UpdateUIListItem(pginfo, true);
4190  }
4191  else if ((message == "PLAY_PLAYLIST") && !m_playListPlay.empty())
4192  {
4193  uint recordingID = m_playListPlay.front();
4194  m_playListPlay.pop_front();
4195 
4196  if (!m_playListPlay.empty())
4197  {
4198  const ProgramInfo *pginfo =
4200  if (pginfo)
4202  }
4203 
4204  ProgramInfo *pginfo = FindProgramInUILists(recordingID);
4205  const bool ignoreBookmark = false;
4206  const bool ignoreProgStart = true;
4207  const bool ignoreLastPlayPos = true;
4208  const bool underNetworkControl = false;
4209  if (pginfo)
4210  Play(*pginfo, true, ignoreBookmark, ignoreProgStart,
4211  ignoreLastPlayPos, underNetworkControl);
4212  }
4213  else if ((message == "SET_PLAYBACK_URL") && (me->ExtraDataCount() == 2))
4214  {
4215  uint recordingID = me->ExtraData(0).toUInt();
4216  ProgramInfo *info = m_programInfoCache.GetRecordingInfo(recordingID);
4217  if (info)
4218  info->SetPathname(me->ExtraData(1));
4219  }
4220  else if ((message == "FOUND_ARTWORK") && (me->ExtraDataCount() >= 5))
4221  {
4222  auto type = (VideoArtworkType) me->ExtraData(2).toInt();
4223  uint recordingID = me->ExtraData(3).toUInt();
4224  const QString& group = me->ExtraData(4);
4225  const QString& fn = me->ExtraData(5);
4226 
4227  if (recordingID)
4228  {
4229  ProgramInfo *pginfo = m_programInfoCache.GetRecordingInfo(recordingID);
4230  if (pginfo &&
4231  m_recordingList->GetItemByData(QVariant::fromValue(pginfo)) ==
4233  m_artImage[(uint)type]->GetFilename() != fn)
4234  {
4235  m_artImage[(uint)type]->SetFilename(fn);
4236  m_artTimer[(uint)type]->start(s_artDelay[(uint)type]);
4237  }
4238  }
4239  else if (!group.isEmpty() &&
4240  (m_currentGroup == group) &&
4241  m_artImage[type] &&
4243  m_artImage[(uint)type]->GetFilename() != fn)
4244  {
4245  m_artImage[(uint)type]->SetFilename(fn);
4246  m_artTimer[(uint)type]->start(s_artDelay[(uint)type]);
4247  }
4248  }
4249  else if (message == "EXIT_TO_MENU" ||
4250  message == "CANCEL_PLAYLIST")
4251  {
4252  m_playListPlay.clear();
4253  }
4254  }
4255  else
4257 }
4258 
4260 {
4261  if (!m_programInfoCache.Remove(recordingID))
4262  {
4263  LOG(VB_GENERAL, LOG_WARNING, LOC +
4264  QString("Failed to remove %1, reloading list")
4265  .arg(recordingID));
4267  return;
4268  }
4269 
4271  QString groupname;
4272  if (sel_item)
4273  groupname = sel_item->GetData().toString();
4274 
4275  ProgramMap::iterator git = m_progLists.begin();
4276  while (git != m_progLists.end())
4277  {
4278  auto pit = (*git).begin();
4279  while (pit != (*git).end())
4280  {
4281  if ((*pit)->GetRecordingID() == recordingID)
4282  {
4283  if (!git.key().isEmpty() && git.key() == groupname)
4284  {
4285  MythUIButtonListItem *item_by_data =
4287  QVariant::fromValue(*pit));
4288  MythUIButtonListItem *item_cur =
4290 
4291  if (item_cur && (item_by_data == item_cur))
4292  {
4293  MythUIButtonListItem *item_next =
4294  m_recordingList->GetItemNext(item_cur);
4295  if (item_next)
4296  m_recordingList->SetItemCurrent(item_next);
4297  }
4298 
4299  m_recordingList->RemoveItem(item_by_data);
4300  }
4301  pit = (*git).erase(pit);
4302  }
4303  else
4304  {
4305  ++pit;
4306  }
4307  }
4308 
4309  if ((*git).empty())
4310  {
4311  if (!groupname.isEmpty() && (git.key() == groupname))
4312  {
4313  MythUIButtonListItem *next_item =
4314  m_groupList->GetItemNext(sel_item);
4315  if (next_item)
4316  m_groupList->SetItemCurrent(next_item);
4317 
4318  m_groupList->RemoveItem(sel_item);
4319 
4320  sel_item = next_item;
4321  groupname = "";
4322  if (sel_item)
4323  groupname = sel_item->GetData().toString();
4324  }
4325  git = m_progLists.erase(git);
4326  }
4327  else
4328  {
4329  ++git;
4330  }
4331  }
4332 
4334 }
4335 
4337 {
4338  m_programInfoCache.Add(evinfo);
4340 }
4341 
4343 {
4344  // Changing recording group full reload
4346  {
4348  }
4349  else
4350  {
4351  ProgramInfo *pginfo = FindProgramInUILists(recordingID);
4352  if (pginfo == nullptr)
4353  return;
4354  bool genPreview = (flags & ProgramInfoCache::PIC_MARK_CHANGED);
4355  UpdateUIListItem(pginfo, genPreview);
4356  }
4357 }
4358 
4360 {
4362  QCoreApplication::postEvent(this, new MythEvent("UPDATE_UI_LIST"));
4363 }
4364 
4366 {
4367  auto *helpPopup = new HelpPopup(m_popupStack);
4368 
4369  if (helpPopup->Create())
4370  m_popupStack->AddScreen(helpPopup);
4371  else
4372  delete helpPopup;
4373 }
4374 
4376 {
4377  auto *viewPopup = new ChangeView(m_popupStack, this, m_viewMask);
4378 
4379  if (viewPopup->Create())
4380  {
4381  connect(viewPopup, &ChangeView::save, this, &PlaybackBox::saveViewChanges);
4382  m_popupStack->AddScreen(viewPopup);
4383  }
4384  else
4385  delete viewPopup;
4386 }
4387 
4389 {
4390  if (m_viewMask == VIEW_NONE)
4392  gCoreContext->SaveSetting("DisplayGroupDefaultViewMask", (int)m_viewMask);
4393  gCoreContext->SaveBoolSetting("PlaybackWatchList",
4394  (m_viewMask & VIEW_WATCHLIST) != 0);
4395 }
4396 
4398 {
4399  QStringList groupNames;
4400  QStringList displayNames;
4401  QStringList groups;
4402  QStringList displayGroups;
4403 
4404  MSqlQuery query(MSqlQuery::InitCon());
4405 
4406  m_recGroupType.clear();
4407 
4408  uint totalItems = 0;
4409 
4410  // Add the group entries
4411  displayNames.append(QString("------- %1 -------").arg(tr("Groups")));
4412  groupNames.append("");
4413 
4414  // Find each recording group, and the number of recordings in each
4415  query.prepare("SELECT recgroup, COUNT(title) FROM recorded "
4416  "WHERE deletepending = 0 AND watched <= :WATCHED "
4417  "GROUP BY recgroup");
4418  query.bindValue(":WATCHED", (m_viewMask & VIEW_WATCHED));
4419  if (query.exec())
4420  {
4421  while (query.next())
4422  {
4423  QString dispGroup = query.value(0).toString();
4424  uint items = query.value(1).toInt();
4425 
4426  if ((dispGroup != "LiveTV" || (m_viewMask & VIEW_LIVETVGRP)) &&
4427  (dispGroup != "Deleted"))
4428  totalItems += items;
4429 
4430  groupNames.append(dispGroup);
4431 
4432  dispGroup = (dispGroup == "Default") ? tr("Default") : dispGroup;
4433  dispGroup = (dispGroup == "Deleted") ? tr("Deleted") : dispGroup;
4434  dispGroup = (dispGroup == "LiveTV") ? tr("Live TV") : dispGroup;
4435 
4436  displayNames.append(tr("%1 [%n item(s)]", nullptr, items).arg(dispGroup));
4437 
4438  m_recGroupType[query.value(0).toString()] = "recgroup";
4439  }
4440  }
4441 
4442  // Create and add the "All Programs" entry
4443  displayNames.push_front(tr("%1 [%n item(s)]", nullptr, totalItems)
4444  .arg(ProgramInfo::i18n("All Programs")));
4445  groupNames.push_front("All Programs");
4446  m_recGroupType["All Programs"] = "recgroup";
4447 
4448  // Find each category, and the number of recordings in each
4449  query.prepare("SELECT DISTINCT category, COUNT(title) FROM recorded "
4450  "WHERE deletepending = 0 AND watched <= :WATCHED "
4451  "GROUP BY category");
4452  query.bindValue(":WATCHED", (m_viewMask & VIEW_WATCHED));
4453  if (query.exec())
4454  {
4455  int unknownCount = 0;
4456  while (query.next())
4457  {
4458  uint items = query.value(1).toInt();
4459  QString dispGroup = query.value(0).toString();
4460  if (dispGroup.isEmpty())
4461  {
4462  unknownCount += items;
4463  dispGroup = tr("Unknown");
4464  }
4465  else if (dispGroup == tr("Unknown"))
4466  unknownCount += items;
4467 
4468  if ((!m_recGroupType.contains(dispGroup)) &&
4469  (dispGroup != tr("Unknown")))
4470  {
4471  displayGroups += tr("%1 [%n item(s)]", nullptr, items).arg(dispGroup);
4472  groups += dispGroup;
4473 
4474  m_recGroupType[dispGroup] = "category";
4475  }
4476  }
4477 
4478  if (unknownCount > 0)
4479  {
4480  QString dispGroup = tr("Unknown");
4481  uint items = unknownCount;
4482  displayGroups += tr("%1 [%n item(s)]", nullptr, items).arg(dispGroup);
4483  groups += dispGroup;
4484 
4485  m_recGroupType[dispGroup] = "category";
4486  }
4487  }
4488 
4489  // Add the category entries
4490  displayNames.append(QString("------- %1 -------").arg(tr("Categories")));
4491  groupNames.append("");
4492  groups.sort();
4493  displayGroups.sort();
4494  QStringList::iterator it;
4495  for (it = displayGroups.begin(); it != displayGroups.end(); ++it)
4496  displayNames.append(*it);
4497  for (it = groups.begin(); it != groups.end(); ++it)
4498  groupNames.append(*it);
4499 
4500  QString label = tr("Change Filter");
4501 
4502  auto *recGroupPopup = new GroupSelector(m_popupStack, label, displayNames,
4503  groupNames, m_recGroup);
4504 
4505  if (recGroupPopup->Create())
4506  {
4507  m_usingGroupSelector = true;
4508  m_groupSelected = false;
4509  connect(recGroupPopup, &GroupSelector::result,
4511  connect(recGroupPopup, &MythScreenType::Exiting,
4513  m_popupStack->AddScreen(recGroupPopup);
4514  }
4515  else
4516  delete recGroupPopup;
4517 }
4518 
4520 {
4521  if (m_groupSelected)
4522  return;
4523 
4524  if (m_firstGroup)
4525  Close();
4526 
4527  m_usingGroupSelector = false;
4528 }
4529 
4530 void PlaybackBox::setGroupFilter(const QString &recGroup)
4531 {
4532  QString newRecGroup = recGroup;
4533 
4534  if (newRecGroup.isEmpty())
4535  return;
4536 
4537  m_firstGroup = false;
4538  m_usingGroupSelector = false;
4539 
4540  if (newRecGroup == ProgramInfo::i18n("Default"))
4541  newRecGroup = "Default";
4542  else if (newRecGroup == ProgramInfo::i18n("All Programs"))
4543  newRecGroup = "All Programs";
4544  else if (newRecGroup == ProgramInfo::i18n("LiveTV"))
4545  newRecGroup = "LiveTV";
4546  else if (newRecGroup == ProgramInfo::i18n("Deleted"))
4547  newRecGroup = "Deleted";
4548 
4549  m_recGroup = newRecGroup;
4550 
4552 
4553  // Since the group filter is changing, the current position in the lists
4554  // is meaningless -- so reset the lists so the position won't be saved.
4556  m_groupList->Reset();
4557 
4558  UpdateUILists();
4559 
4560  if (gCoreContext->GetBoolSetting("RememberRecGroup",true))
4561  gCoreContext->SaveSetting("DisplayRecGroup", m_recGroup);
4562 
4563  if (m_recGroupType[m_recGroup] == "recgroup")
4564  gCoreContext->SaveSetting("DisplayRecGroupIsCategory", 0);
4565  else
4566  gCoreContext->SaveSetting("DisplayRecGroupIsCategory", 1);
4567 }
4568 
4569 QString PlaybackBox::getRecGroupPassword(const QString &group)
4570 {
4571  return m_recGroupPwCache.value(group);
4572 }
4573 
4575 {
4576  m_recGroupPwCache.clear();
4577 
4578  MSqlQuery query(MSqlQuery::InitCon());
4579  query.prepare("SELECT recgroup, password FROM recgroups "
4580  "WHERE password IS NOT NULL AND password <> '';");
4581 
4582  if (query.exec())
4583  {
4584  while (query.next())
4585  {
4586  QString recgroup = query.value(0).toString();
4587 
4588  if (recgroup == ProgramInfo::i18n("Default"))
4589  recgroup = "Default";
4590  else if (recgroup == ProgramInfo::i18n("All Programs"))
4591  recgroup = "All Programs";
4592  else if (recgroup == ProgramInfo::i18n("LiveTV"))
4593  recgroup = "LiveTV";
4594  else if (recgroup == ProgramInfo::i18n("Deleted"))
4595  recgroup = "Deleted";
4596 
4597  m_recGroupPwCache.insert(recgroup, query.value(1).toString());
4598  }
4599  }
4600 }
4601 
4603 void PlaybackBox::ShowRecGroupChanger(bool use_playlist)
4604 {
4605  m_opOnPlaylist = use_playlist;
4606 
4607  ProgramInfo *pginfo = nullptr;
4608  if (use_playlist)
4609  {
4610  if (!m_playList.empty())
4611  pginfo = FindProgramInUILists(m_playList[0]);
4612  }
4613  else
4614  pginfo = GetCurrentProgram();
4615 
4616  if (!pginfo)
4617  return;
4618 
4619  MSqlQuery query(MSqlQuery::InitCon());
4620  query.prepare(
4621  "SELECT g.recgroup, COUNT(r.title) FROM recgroups g "
4622  "LEFT JOIN recorded r ON g.recgroupid=r.recgroupid AND r.deletepending = 0 "
4623  "WHERE g.recgroupid != 2 AND g.recgroupid != 3 "
4624  "GROUP BY g.recgroupid ORDER BY g.recgroup");
4625 
4626  QStringList displayNames(tr("Add New"));
4627  QStringList groupNames("addnewgroup");
4628 
4629  if (!query.exec())
4630  return;
4631 
4632  while (query.next())
4633  {
4634  QString dispGroup = query.value(0).toString();
4635  groupNames.push_back(dispGroup);
4636 
4637  if (dispGroup == "Default")
4638  dispGroup = tr("Default");
4639  else if (dispGroup == "LiveTV")
4640  dispGroup = tr("Live TV");
4641  else if (dispGroup == "Deleted")
4642  dispGroup = tr("Deleted");
4643 
4644  displayNames.push_back(tr("%1 [%n item(s)]", "", query.value(1).toInt())
4645  .arg(dispGroup));
4646  }
4647 
4648  QString label = tr("Select Recording Group") +
4649  CreateProgramInfoString(*pginfo);
4650 
4651  auto *rgChanger = new GroupSelector(m_popupStack, label, displayNames,
4652  groupNames, pginfo->GetRecordingGroup());
4653 
4654  if (rgChanger->Create())
4655  {
4656  connect(rgChanger, &GroupSelector::result, this, &PlaybackBox::setRecGroup);
4657  m_popupStack->AddScreen(rgChanger);
4658  }
4659  else
4660  delete rgChanger;
4661 }
4662 
4664 void PlaybackBox::ShowPlayGroupChanger(bool use_playlist)
4665 {
4666  m_opOnPlaylist = use_playlist;
4667 
4668  ProgramInfo *pginfo = nullptr;
4669  if (use_playlist)
4670  {
4671  if (!m_playList.empty())
4672  pginfo = FindProgramInUILists(m_playList[0]);
4673  }
4674  else
4675  pginfo = GetCurrentProgram();
4676 
4677  if (!pginfo)
4678  return;
4679 
4680  QStringList groupNames(tr("Default"));
4681  QStringList displayNames("Default");
4682 
4683  QStringList list = PlayGroup::GetNames();
4684  for (const auto& name : qAsConst(list))
4685  {
4686  displayNames.push_back(name);
4687  groupNames.push_back(name);
4688  }
4689 
4690  QString label = tr("Select Playback Group") +
4691  CreateProgramInfoString(*pginfo);
4692 
4693  auto *pgChanger = new GroupSelector(m_popupStack, label,displayNames,
4694  groupNames, pginfo->GetPlaybackGroup());
4695 
4696  if (pgChanger->Create())
4697  {
4698  connect(pgChanger, &GroupSelector::result,
4699  this, &PlaybackBox::setPlayGroup);
4700  m_popupStack->AddScreen(pgChanger);
4701  }
4702  else
4703  delete pgChanger;
4704 }
4705 
4707 {
4708  QList<uint>::Iterator it;
4709 
4710  for (it = m_playList.begin(); it != m_playList.end(); ++it)
4711  {
4712  ProgramInfo *tmpItem = FindProgramInUILists(*it);
4713  if (tmpItem != nullptr)
4714  {
4715  if (!tmpItem->IsAutoExpirable() && turnOn)
4716  tmpItem->SaveAutoExpire(kNormalAutoExpire, true);
4717  else if (tmpItem->IsAutoExpirable() && !turnOn)
4718  tmpItem->SaveAutoExpire(kDisableAutoExpire, true);
4719  }
4720  }
4721 }
4722 
4724 {
4725  QList<uint>::Iterator it;
4726 
4727  for (it = m_playList.begin(); it != m_playList.end(); ++it)
4728  {
4729  ProgramInfo *tmpItem = FindProgramInUILists(*it);
4730  if (tmpItem != nullptr)
4731  {
4732  tmpItem->SaveWatched(turnOn);
4733  }
4734  }
4735 
4736  doClearPlaylist();
4737  UpdateUILists();
4738 }
4739 
4741 {
4742  ProgramInfo *pgInfo = GetCurrentProgram();
4743 
4745 
4746  auto *editMetadata = new RecMetadataEdit(mainStack, pgInfo);
4747 
4748  if (editMetadata->Create())
4749  {
4750  connect(editMetadata, &RecMetadataEdit::result,
4752  mainStack->AddScreen(editMetadata);
4753  }
4754  else
4755  delete editMetadata;
4756 }
4757 
4758 void PlaybackBox::saveRecMetadata(const QString &newTitle,
4759  const QString &newSubtitle,
4760  const QString &newDescription,
4761  const QString &newInetref,
4762  uint newSeason,
4763  uint newEpisode)
4764 {
4766 
4767  if (!item)
4768  return;
4769 
4770  auto *pginfo = item->GetData().value<ProgramInfo *>();
4771 
4772  if (!pginfo)
4773  return;
4774 
4775  QString groupname = m_groupList->GetItemCurrent()->GetData().toString();
4776 
4777  if (groupname == pginfo->GetTitle().toLower() &&
4778  newTitle != pginfo->GetTitle())
4779  {
4780  m_recordingList->RemoveItem(item);
4781  }
4782  else
4783  {
4784  QString tempSubTitle = newTitle;
4785  if (!newSubtitle.trimmed().isEmpty())
4786  tempSubTitle = QString("%1 - \"%2\"")
4787  .arg(tempSubTitle, newSubtitle);
4788 
4789  QString seasone;
4790  QString seasonx;
4791  QString season;
4792  QString episode;
4793  if (newSeason > 0 || newEpisode > 0)
4794  {
4795  season = StringUtil::intToPaddedString(newSeason, 1);
4796  episode = StringUtil::intToPaddedString(newEpisode, 1);
4797  seasone = QString("s%1e%2")
4798  .arg(StringUtil::intToPaddedString(newSeason, 2),
4799  StringUtil::intToPaddedString(newEpisode, 2));
4800  seasonx = QString("%1x%2")
4801  .arg(StringUtil::intToPaddedString(newSeason, 1),
4802  StringUtil::intToPaddedString(newEpisode, 2));
4803  }
4804 
4805  item->SetText(tempSubTitle, "titlesubtitle");
4806  item->SetText(newTitle, "title");
4807  item->SetText(newSubtitle, "subtitle");
4808  item->SetText(newInetref, "inetref");
4809  item->SetText(seasonx, "00x00");
4810  item->SetText(seasone, "s00e00");
4811  item->SetText(season, "season");
4812  item->SetText(episode, "episode");
4813  if (newDescription != nullptr)
4814  item->SetText(newDescription, "description");
4815  }
4816 
4817  pginfo->SaveInetRef(newInetref);
4818  pginfo->SaveSeasonEpisode(newSeason, newEpisode);
4819 
4820  RecordingInfo ri(*pginfo);
4821  ri.ApplyRecordRecTitleChange(newTitle, newSubtitle, newDescription);
4822  *pginfo = ri;
4823 }
4824 
4825 void PlaybackBox::setRecGroup(QString newRecGroup)
4826 {
4827  newRecGroup = newRecGroup.simplified();
4828 
4829  if (newRecGroup.isEmpty())
4830  return;
4831 
4832  if (newRecGroup == "addnewgroup")
4833  {
4834  MythScreenStack *popupStack =
4835  GetMythMainWindow()->GetStack("popup stack");
4836 
4837  auto *newgroup = new MythTextInputDialog(popupStack,
4838  tr("New Recording Group"));
4839 
4840  connect(newgroup, &MythTextInputDialog::haveResult,
4841  this, &PlaybackBox::setRecGroup);
4842 
4843  if (newgroup->Create())
4844  popupStack->AddScreen(newgroup, false);
4845  else
4846  delete newgroup;
4847  return;
4848  }
4849 
4850  RecordingRule record;
4851  record.LoadTemplate("Default");
4852  AutoExpireType defaultAutoExpire =
4854 
4855  if (m_opOnPlaylist)
4856  {
4857  for (int id : qAsConst(m_playList))
4858  {
4860  if (!p)
4861  continue;
4862 
4863  if ((p->GetRecordingGroup() == "LiveTV") &&
4864  (newRecGroup != "LiveTV"))
4865  {
4866  p->SaveAutoExpire(defaultAutoExpire);
4867  }
4868  else if ((p->GetRecordingGroup() != "LiveTV") &&
4869  (newRecGroup == "LiveTV"))
4870  {
4871  p->SaveAutoExpire(kLiveTVAutoExpire);
4872  }
4873 
4874  RecordingInfo ri(*p);
4875  ri.ApplyRecordRecGroupChange(newRecGroup);
4876  *p = ri;
4877  }
4878  doClearPlaylist();
4879  UpdateUILists();
4880  return;
4881  }
4882 
4884  if (!p)
4885  return;
4886 
4887  if ((p->GetRecordingGroup() == "LiveTV") && (newRecGroup != "LiveTV"))
4888  p->SaveAutoExpire(defaultAutoExpire);
4889  else if ((p->GetRecordingGroup() != "LiveTV") && (newRecGroup == "LiveTV"))
4890  p->SaveAutoExpire(kLiveTVAutoExpire);
4891 
4892  RecordingInfo ri(*p);
4893  ri.ApplyRecordRecGroupChange(newRecGroup);
4894  *p = ri;
4895  UpdateUILists();
4896 }
4897 
4898 void PlaybackBox::setPlayGroup(QString newPlayGroup)
4899 {
4900  ProgramInfo *tmpItem = GetCurrentProgram();
4901 
4902  if (newPlayGroup.isEmpty() || !tmpItem)
4903  return;
4904 
4905  if (newPlayGroup == tr("Default"))
4906  newPlayGroup = "Default";
4907 
4908  if (m_opOnPlaylist)
4909  {
4910  QList<uint>::Iterator it;
4911 
4912  for (it = m_playList.begin(); it != m_playList.end(); ++it )
4913  {
4914  tmpItem = FindProgramInUILists(*it);
4915  if (tmpItem)
4916  {
4917  RecordingInfo ri(*tmpItem);
4918  ri.ApplyRecordPlayGroupChange(newPlayGroup);
4919  *tmpItem = ri;
4920  }
4921  }
4922  doClearPlaylist();
4923  }
4924  else
4925  {
4926  RecordingInfo ri(*tmpItem);
4927  ri.ApplyRecordPlayGroupChange(newPlayGroup);
4928  *tmpItem = ri;
4929  }
4930 }
4931 
4933 {
4935 
4936  if (!item)
4937  return;
4938 
4939  QString currentPassword = getRecGroupPassword(m_recGroup);
4940 
4941  auto *pwChanger = new PasswordChange(m_popupStack, currentPassword);
4942 
4943  if (pwChanger->Create())
4944  {
4945  connect(pwChanger, &PasswordChange::result,
4947  m_popupStack->AddScreen(pwChanger);
4948  }
4949  else
4950  delete pwChanger;
4951 }
4952 
4953 void PlaybackBox::SetRecGroupPassword(const QString &newPassword)
4954 {
4955  MSqlQuery query(MSqlQuery::InitCon());
4956 
4957  query.prepare("UPDATE recgroups SET password = :PASSWD WHERE "
4958  "recgroup = :RECGROUP");
4959  query.bindValue(":RECGROUP", m_recGroup);
4960  query.bindValue(":PASSWD", newPassword);
4961 
4962  if (!query.exec())
4963  MythDB::DBError("PlaybackBox::SetRecGroupPassword",
4964  query);
4965 
4966  if (newPassword.isEmpty())
4967  m_recGroupPwCache.remove(m_recGroup);
4968  else
4969  m_recGroupPwCache.insert(m_recGroup, newPassword);
4970 }
4971 
4973 
4975 {
4976  if (!LoadWindowFromXML("recordings-ui.xml", "groupselector", this))
4977  return false;
4978 
4979  MythUIText *labelText = dynamic_cast<MythUIText*> (GetChild("label"));
4980  MythUIButtonList *groupList = dynamic_cast<MythUIButtonList*>
4981  (GetChild("groups"));
4982 
4983  if (!groupList)
4984  {
4985  LOG(VB_GENERAL, LOG_ERR, LOC +
4986  "Theme is missing 'groups' button list.");
4987  return false;
4988  }
4989 
4990  if (labelText)
4991  labelText->SetText(m_label);
4992 
4993  for (int i = 0; i < m_list.size(); ++i)
4994  {
4995  new MythUIButtonListItem(groupList, m_list.at(i),
4996  QVariant::fromValue(m_data.at(i)));
4997  }
4998 
4999  // Set the current position in the list
5000  groupList->SetValueByData(QVariant::fromValue(m_selected));
5001 
5002  BuildFocusList();
5003 
5004  connect(groupList, &MythUIButtonList::itemClicked,
5005  this, &GroupSelector::AcceptItem);
5006 
5007  return true;
5008 }
5009 
5011 {
5012  if (!item)
5013  return;
5014 
5015  // ignore the dividers
5016  if (item->GetData().toString().isEmpty())
5017  return;
5018 
5019  QString group = item->GetData().toString();
5020  emit result(group);
5021  Close();
5022 }
5023 
5025 
5027 {
5028  if (!LoadWindowFromXML("recordings-ui.xml", "changeview", this))
5029  return false;
5030 
5031  MythUICheckBox *checkBox = dynamic_cast<MythUICheckBox*>(GetChild("titles"));
5032  if (checkBox)
5033  {
5036  connect(checkBox, &MythUICheckBox::toggled,
5038  }
5039 
5040  checkBox = dynamic_cast<MythUICheckBox*>(GetChild("categories"));
5041  if (checkBox)
5042  {
5045  connect(checkBox, &MythUICheckBox::toggled,
5047  }
5048 
5049  checkBox = dynamic_cast<MythUICheckBox*>(GetChild("recgroups"));
5050  if (checkBox)
5051  {
5054  connect(checkBox, &MythUICheckBox::toggled,
5056  }
5057 
5058  // TODO Do we need two separate settings to determine whether the watchlist
5059  // is shown? The filter setting be enough?
5060  checkBox = dynamic_cast<MythUICheckBox*>(GetChild("watchlist"));
5061  if (checkBox)
5062  {
5065  connect(checkBox, &MythUICheckBox::toggled,
5067  }
5068  //
5069 
5070  checkBox = dynamic_cast<MythUICheckBox*>(GetChild("searches"));
5071  if (checkBox)
5072  {
5075  connect(checkBox, &MythUICheckBox::toggled,
5077  }
5078 
5079  // TODO Do we need two separate settings to determine whether livetv
5080  // recordings are shown? Same issue as the watchlist above
5081  checkBox = dynamic_cast<MythUICheckBox*>(GetChild("livetv"));
5082  if (checkBox)
5083  {
5086  connect(checkBox, &MythUICheckBox::toggled,
5088  }
5089  //
5090 
5091  checkBox = dynamic_cast<MythUICheckBox*>(GetChild("watched"));
5092  if (checkBox)
5093  {
5096  connect(checkBox, &MythUICheckBox::toggled,
5098  }
5099 
5100  MythUIButton *savebutton = dynamic_cast<MythUIButton*>(GetChild("save"));
5101  connect(savebutton, &MythUIButton::Clicked, this, &ChangeView::SaveChanges);
5102 
5103  BuildFocusList();
5104 
5105  return true;
5106 }
5107 
5109 {
5110  emit save();
5111  Close();
5112 }
5113 
5115 
5117 {
5118  if (!LoadWindowFromXML("recordings-ui.xml", "passwordchanger", this))
5119  return false;
5120 
5121  m_oldPasswordEdit = dynamic_cast<MythUITextEdit *>(GetChild("oldpassword"));
5122  m_newPasswordEdit = dynamic_cast<MythUITextEdit *>(GetChild("newpassword"));
5123  m_okButton = dynamic_cast<MythUIButton *>(GetChild("ok"));
5124 
5126  {
5127  LOG(VB_GENERAL, LOG_ERR, LOC +
5128  "Window 'passwordchanger' is missing required elements.");
5129  return false;
5130  }
5131 
5134 // if (m_oldPassword.isEmpty())
5135 // m_oldPasswordEdit->SetDisabled(true);
5138 
5139  BuildFocusList();
5140 
5144 
5145  return true;
5146 }
5147 
5149 {
5150  QString newText = m_oldPasswordEdit->GetText();
5151  bool ok = (newText == m_oldPassword);
5152  m_okButton->SetEnabled(ok);
5153 }
5154 
5155 
5157 {
5158  emit result(m_newPasswordEdit->GetText());
5159  Close();
5160 }
5161 
5163 
5165  : MythScreenType(lparent, "recmetadataedit"),
5166  m_progInfo(pginfo)
5167 {
5168  m_popupStack = GetMythMainWindow()->GetStack("popup stack");
5169  m_metadataFactory = new MetadataFactory(this);
5170 }
5171 
5173 {
5174  if (!LoadWindowFromXML("recordings-ui.xml", "editmetadata", this))
5175  return false;
5176 
5177  m_titleEdit = dynamic_cast<MythUITextEdit*>(GetChild("title"));
5178  m_subtitleEdit = dynamic_cast<MythUITextEdit*>(GetChild("subtitle"));
5179  m_descriptionEdit = dynamic_cast<MythUITextEdit*>(GetChild("description"));
5180  m_inetrefEdit = dynamic_cast<MythUITextEdit*>(GetChild("inetref"));
5181  MythUIButton *inetrefClear = dynamic_cast<MythUIButton*>
5182  (GetChild("inetref_clear"));
5183  m_seasonSpin = dynamic_cast<MythUISpinBox*>(GetChild("season"));
5184  m_episodeSpin = dynamic_cast<MythUISpinBox*>(GetChild("episode"));
5185  MythUIButton *okButton = dynamic_cast<MythUIButton*>(GetChild("ok"));
5186  m_queryButton = dynamic_cast<MythUIButton*>(GetChild("query_button"));
5187 
5189  !m_episodeSpin || !okButton)
5190  {
5191  LOG(VB_GENERAL, LOG_ERR, LOC +
5192  "Window 'editmetadata' is missing required elements.");
5193  return false;
5194  }
5195 
5197  m_titleEdit->SetMaxLength(128);
5200  if (m_descriptionEdit)
5201  {
5204  }
5207  m_seasonSpin->SetRange(0,9999,1,5);
5209  m_episodeSpin->SetRange(0,9999,1,10);
5211 
5212  connect(inetrefClear, &MythUIButton::Clicked, this, &RecMetadataEdit::ClearInetref);
5213  connect(okButton, &MythUIButton::Clicked, this, &RecMetadataEdit::SaveChanges);
5214  if (m_queryButton)
5215  {
5217  }
5218 
5219  BuildFocusList();
5220 
5221  return true;
5222 }
5223 
5225 {
5226  m_inetrefEdit->SetText("");
5227 }
5228 
5230 {
5231  QString newRecTitle = m_titleEdit->GetText();
5232  QString newRecSubtitle = m_subtitleEdit->GetText();
5233  QString newRecDescription = nullptr;
5234  QString newRecInetref = nullptr;
5235  uint newRecSeason = 0;
5236  uint newRecEpisode = 0;
5237  if (m_descriptionEdit)
5238  newRecDescription = m_descriptionEdit->GetText();
5239  newRecInetref = m_inetrefEdit->GetText();
5240  newRecSeason = m_seasonSpin->GetIntValue();
5241  newRecEpisode = m_episodeSpin->GetIntValue();
5242 
5243  if (newRecTitle.isEmpty())
5244  return;
5245 
5246  emit result(newRecTitle, newRecSubtitle, newRecDescription,
5247  newRecInetref, newRecSeason, newRecEpisode);
5248  Close();
5249 }
5250 
5252 {
5253  if (m_busyPopup)
5254  return;
5255 
5256  m_busyPopup = new MythUIBusyDialog(tr("Trying to manually find this "
5257  "recording online..."),
5258  m_popupStack,
5259  "metaoptsdialog");
5260 
5261  if (m_busyPopup->Create())
5263 
5264  auto *lookup = new MetadataLookup();
5265  lookup->SetStep(kLookupSearch);
5266  lookup->SetType(kMetadataRecording);
5268 
5269  if (type == kUnknownVideo)
5270  {
5271  if (m_seasonSpin->GetIntValue() == 0 &&
5272  m_episodeSpin->GetIntValue() == 0 &&
5273  m_subtitleEdit->GetText().isEmpty())
5274  {
5275  lookup->SetSubtype(kProbableMovie);
5276  }
5277  else
5278  {
5279  lookup->SetSubtype(kProbableTelevision);
5280  }
5281  }
5282  else
5283  {
5284  // we could determine the type from the inetref
5285  lookup->SetSubtype(type);
5286  }
5287  lookup->SetAllowGeneric(true);
5288  lookup->SetHandleImages(false);
5289  lookup->SetHost(gCoreContext->GetMasterHostName());
5290  lookup->SetTitle(m_titleEdit->GetText());
5291  lookup->SetSubtitle(m_subtitleEdit->GetText());
5292  lookup->SetInetref(m_inetrefEdit->GetText());
5293  lookup->SetCollectionref(m_inetrefEdit->GetText());
5294  lookup->SetSeason(m_seasonSpin->GetIntValue());
5295  lookup->SetEpisode(m_episodeSpin->GetIntValue());
5296  lookup->SetAutomatic(false);
5297 
5298  m_metadataFactory->Lookup(lookup);
5299 }
5300 
5302 {
5303  if (!lookup)
5304  return;
5305 
5306  m_inetrefEdit->SetText(lookup->GetInetref());
5307  m_seasonSpin->SetValue(lookup->GetSeason());
5308  m_episodeSpin->SetValue(lookup->GetEpisode());
5309  if (!lookup->GetSubtitle().isEmpty())
5310  {
5311  m_subtitleEdit->SetText(lookup->GetSubtitle());
5312  }
5313  if (!lookup->GetDescription().isEmpty())
5314  {
5316  }
5317 }
5318 
5320 {
5321  QueryComplete(lookup);
5322 }
5323 
5324 void RecMetadataEdit::customEvent(QEvent *levent)
5325 {
5326  if (levent->type() == MetadataFactoryMultiResult::kEventType)
5327  {
5328  if (m_busyPopup)
5329  {
5330  m_busyPopup->Close();
5331  m_busyPopup = nullptr;
5332  }
5333 
5334  auto *mfmr = dynamic_cast<MetadataFactoryMultiResult*>(levent);
5335 
5336  if (!mfmr)
5337  return;
5338 
5339  MetadataLookupList list = mfmr->m_results;
5340 
5341  auto *resultsdialog = new MetadataResultsDialog(m_popupStack, list);
5342 
5343  connect(resultsdialog, &MetadataResultsDialog::haveResult,
5345  Qt::QueuedConnection);
5346 
5347  if (resultsdialog->Create())
5348  m_popupStack->AddScreen(resultsdialog);
5349  }
5350  else if (levent->type() == MetadataFactorySingleResult::kEventType)
5351  {
5352  if (m_busyPopup)
5353  {
5354  m_busyPopup->Close();
5355  m_busyPopup = nullptr;
5356  }
5357 
5358  auto *mfsr = dynamic_cast<MetadataFactorySingleResult*>(levent);
5359 
5360  if (!mfsr || !mfsr->m_result)
5361  return;
5362 
5363  QueryComplete(mfsr->m_result);
5364  }
5365  else if (levent->type() == MetadataFactoryNoResult::kEventType)
5366  {
5367  if (m_busyPopup)
5368  {
5369  m_busyPopup->Close();
5370  m_busyPopup = nullptr;
5371  }
5372 
5373  auto *mfnr = dynamic_cast<MetadataFactoryNoResult*>(levent);
5374 
5375  if (!mfnr)
5376  return;
5377 
5378  QString title = tr("No match found for this recording. You can "
5379  "try entering a TVDB/TMDB number, season, and "
5380  "episode manually.");
5381 
5382  auto *okPopup = new MythConfirmationDialog(m_popupStack, title, false);
5383 
5384  if (okPopup->Create())
5385  m_popupStack->AddScreen(okPopup);
5386  }
5387 }
5388 
5390 
5392 {
5393  if (!LoadWindowFromXML("recordings-ui.xml", "iconhelp", this))
5394  return false;
5395 
5396  m_iconList = dynamic_cast<MythUIButtonList*>(GetChild("iconlist"));
5397 
5398  if (!m_iconList)
5399  {
5400  LOG(VB_GENERAL, LOG_ERR, LOC +
5401  "Window 'iconhelp' is missing required elements.");
5402  return false;
5403  }
5404 
5405  BuildFocusList();
5406 
5407  addItem("watched", tr("Recording has been watched"));
5408  addItem("commflagged", tr("Commercials are flagged"));
5409  addItem("cutlist", tr("An editing cutlist is present"));
5410  addItem("autoexpire", tr("The program is able to auto-expire"));
5411  addItem("processing", tr("Commercials are being flagged"));
5412  addItem("bookmark", tr("A bookmark is set"));
5413 #if 0
5414  addItem("inuse", tr("Recording is in use"));
5415  addItem("transcoded", tr("Recording has been transcoded"));
5416 #endif
5417 
5418  addItem("mono", tr("Recording is in Mono"));
5419  addItem("stereo", tr("Recording is in Stereo"));
5420  addItem("surround", tr("Recording is in Surround Sound"));
5421  addItem("dolby", tr("Recording is in Dolby Surround Sound"));
5422 
5423  addItem("cc", tr("Recording is Closed Captioned"));
5424  addItem("subtitles", tr("Recording has Subtitles Available"));
5425  addItem("onscreensub", tr("Recording is Subtitled"));
5426 
5427  addItem("SD", tr("Recording is in Standard Definition"));
5428  addItem("widescreen", tr("Recording is Widescreen"));
5429  addItem("hdtv", tr("Recording is in High Definition"));
5430  addItem("hd720", tr("Recording is in 720p High Definition"));
5431  addItem("hd1080i", tr("Recording is in 1080i High Definition"));
5432  addItem("hd1080p", tr("Recording is in 1080p High Definition"));
5433  addItem("uhd4Ki", tr("Recording is in 4k(interlaced) UHD resolution"));
5434  addItem("uhd4Kp", tr("Recording is in 4k UHD resolution"));
5435  addItem("mpeg2", tr("Recording is using MPEG-2 codec"));
5436  addItem("avchd", tr("Recording is using AVC/H.264 codec"));
5437  addItem("hevc", tr("Recording is using HEVC/H.265 codec"));
5438 // addItem("preserved", tr("Recording is preserved"));
5439 
5440  return true;
5441 }
5442 
5443 void HelpPopup::addItem(const QString &state, const QString &text)
5444 {
5445  auto *item = new MythUIButtonListItem(m_iconList, text);
5446  item->DisplayState(state, "icons");
5447 }
5448 
5450 {
5451  QDateTime now = QDateTime::currentDateTime();
5452  if (!m_lastUpdated.isValid() ||
5453  m_lastUpdated.msecsTo(now) >= kInvalidateTimeMs.count())
5454  {
5455  QMap<int, JobQueueEntry> jobs;
5457  m_jobs.clear();
5458  for (const auto& job : qAsConst(jobs))
5459  {
5460  m_jobs.insert(qMakePair(job.chanid, job.recstartts), job);
5461  }
5462  m_lastUpdated = now;
5463  }
5464 }
5465 
5467  const QDateTime &recstartts)
5468 {
5469  Update();
5470  QList<JobQueueEntry> values = m_jobs.values(qMakePair(chanid, recstartts));
5471  auto end = values.cend();
5472  for (auto iter = values.cbegin(); iter != end; ++iter)
5473  {
5474  if (iter->type == jobType)
5475  return JobQueue::IsJobStatusQueued(iter->status);
5476  }
5477  return false;
5478 }
5479 
5481  const QDateTime &recstartts)
5482 {
5483  Update();
5484  QList<JobQueueEntry> values = m_jobs.values(qMakePair(chanid, recstartts));
5485  auto end = values.cend();
5486  for (auto iter = values.cbegin(); iter != end; ++iter)
5487  {
5488  if (iter->type == jobType)
5489  return JobQueue::IsJobStatusRunning(iter->status);
5490  }
5491  return false;
5492 }
5493 
5495  const QDateTime &recstartts)
5496 {
5497  return IsJobQueued(jobType, chanid, recstartts) ||
5498  IsJobRunning(jobType, chanid, recstartts);
5499 }
5500 
5501 /* vim: set expandtab tabstop=4 shiftwidth=4: */
MetadataFactory::Lookup
void Lookup(ProgramInfo *pginfo, bool automatic=true, bool getimages=true, bool allowgeneric=false)
Definition: metadatafactory.cpp:139
ProgramInfoCache::Add
void Add(const ProgramInfo &pginfo)
Adds a ProgramInfo to the cache.
Definition: programinfocache.cpp:272
PlaybackBoxHelper::DeleteRecording
void DeleteRecording(uint recordingID, bool forceDelete, bool forgetHistory)
Definition: playbackboxhelper.cpp:375
RecordingInfo::ApplyRecordPlayGroupChange
void ApplyRecordPlayGroupChange(const QString &newplaygroup)
Sets the recording group, both in this RecordingInfo and in the database.
Definition: recordinginfo.cpp:740
PlaybackBox::toggleRecGroupView
void toggleRecGroupView(bool setOn)
Definition: playbackbox.h:218
PlaybackBox::updateIcons
void updateIcons(const ProgramInfo *pginfo=nullptr)
Definition: playbackbox.cpp:1147
MythUIButton::Clicked
void Clicked()
JOB_USERJOB3
@ JOB_USERJOB3
Definition: jobqueue.h:88
kLiveTVAutoExpire
@ kLiveTVAutoExpire
Definition: