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