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