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  // Artwork Variables
386  m_artHostOverride(),
387  // Recording Group settings
388  m_groupDisplayName(ProgramInfo::i18n("All Programs")),
389  m_recGroup("All Programs"),
390  m_watchGroupName(tr("Watch List")),
391  m_watchGroupLabel(m_watchGroupName.toLower()),
392 
393  // Other state
394  m_programInfoCache(this),
395  // Other
396  m_helper(this)
397 {
398  for (size_t i = 0; i < kNumArtImages; i++)
399  {
400  m_artImage[i] = nullptr;
401  m_artTimer[i] = new QTimer(this);
402  m_artTimer[i]->setSingleShot(true);
403  }
404 
405  m_recGroup = gCoreContext->GetSetting("DisplayRecGroup",
406  "All Programs");
407  int pbOrder = gCoreContext->GetNumSetting("PlayBoxOrdering", 3);
408  // Split out sort order modes, wacky order for backward compatibility
409  m_listOrder = (pbOrder >> 1) ^ (m_allOrder = pbOrder & 1);
410  m_watchListStart = gCoreContext->GetBoolSetting("PlaybackWLStart", false);
411 
412  m_watchListAutoExpire= gCoreContext->GetBoolSetting("PlaybackWLAutoExpire", false);
413  m_watchListMaxAge = gCoreContext->GetNumSetting("PlaybackWLMaxAge", 60);
415  std::chrono::days(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);
535  this, qOverload<>(&PlaybackBox::PlayFromBookmarkOrProgStart));
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")
762  .arg(pginfo->toString(ProgramInfo::kTitleSubtitle)));
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<SubtitleProps, 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<SubtitleProps, 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, 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  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  {
1703  MSqlQuery query(MSqlQuery::InitCon());
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(RecordingInfo::kReSearchTypeName);
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 
1962  MSqlQuery query(MSqlQuery::InitCon());
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.count() / 6)
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.count() - 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.count() * 2) - 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 
2203  MSqlQuery query(MSqlQuery::InitCon());
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 = 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"),
2509 
2510  m_popupMenu->AddItem(tr("Change Group View"),
2512 
2513  if (m_recGroupType[m_recGroup] == "recgroup")
2514  m_popupMenu->AddItem(tr("Change Group Password"),
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"),
2527  }
2528  else if (pginfo)
2529  {
2530  m_popupMenu->AddItem(tr("Add this recording to Playlist"),
2531  qOverload<>(&PlaybackBox::togglePlayListItem));
2532  }
2533  }
2534 
2535  m_popupMenu->AddItem(tr("Help (Status Icons)"), &PlaybackBox::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  if ((kDeleteRecording == type) &&
2689  delItem->GetRecordingGroup() != "Deleted" &&
2690  delItem->GetRecordingGroup() != "LiveTV")
2691  {
2692  m_popupMenu->AddItem(tr("Yes, and allow re-record"),
2694  }
2695 
2696  bool defaultIsYes =
2697  ((kDeleteRecording != type) &&
2698  (kForceDeleteRecording != type) &&
2699  (delItem->QueryAutoExpire() != kDisableAutoExpire));
2700 
2701  switch (type)
2702  {
2703  case kDeleteRecording:
2704  m_popupMenu->AddItem(tr("Yes, delete it"),
2705  qOverload<>(&PlaybackBox::Delete), nullptr, defaultIsYes);
2706  break;
2707  case kForceDeleteRecording:
2708  m_popupMenu->AddItem(tr("Yes, delete it"),
2709  &PlaybackBox::DeleteForce, nullptr, defaultIsYes);
2710  break;
2711  case kStopRecording:
2712  m_popupMenu->AddItem(tr("Yes, stop recording"),
2713  &PlaybackBox::StopSelected, nullptr, defaultIsYes);
2714  break;
2715  }
2716 
2717 
2718  if ((kForceDeleteRecording == type) && other_delete_cnt)
2719  {
2721  tr("Yes, delete it and the remaining %1 list items")
2722  .arg(other_delete_cnt), &PlaybackBox::DeleteForceAllRemaining);
2723  }
2724 
2725  switch (type)
2726  {
2727  case kDeleteRecording:
2728  case kForceDeleteRecording:
2729  m_popupMenu->AddItem(tr("No, keep it"), &PlaybackBox::DeleteIgnore,
2730  nullptr, !defaultIsYes);
2731  break;
2732  case kStopRecording:
2733  m_popupMenu->AddItem(tr("No, continue recording"), &PlaybackBox::DeleteIgnore,
2734  nullptr, !defaultIsYes);
2735  break;
2736  }
2737 
2738  if ((type == kForceDeleteRecording) && other_delete_cnt)
2739  {
2741  tr("No, and keep the remaining %1 list items")
2742  .arg(other_delete_cnt),
2744  }
2745 
2746  DisplayPopupMenu();
2747 }
2748 
2750 {
2751  QString msg = pginfo.toString(ProgramInfo::kTitleSubtitle, " ");
2752  msg += "\n";
2753 
2754  QString byWho;
2755  switch (pginfo.GetAvailableStatus())
2756  {
2757  case asAvailable:
2758  if (pginfo.QueryIsInUse(byWho))
2759  {
2760  ShowNotification(tr("Recording Available\n"),
2761  sLocation, msg +
2762  tr("This recording is currently in "
2763  "use by:") + "\n" + byWho);
2764  }
2765  else
2766  {
2767  ShowNotification(tr("Recording Available\n"),
2768  sLocation, msg +
2769  tr("This recording is currently "
2770  "Available"));
2771  }
2772  break;
2773  case asPendingDelete:
2774  ShowNotificationError(tr("Recording Unavailable\n"),
2775  sLocation, msg +
2776  tr("This recording is currently being "
2777  "deleted and is unavailable"));
2778  break;
2779  case asDeleted:
2780  ShowNotificationError(tr("Recording Unavailable\n"),
2781  sLocation, msg +
2782  tr("This recording has been "
2783  "deleted and is unavailable"));
2784  break;
2785  case asFileNotFound:
2786  ShowNotificationError(tr("Recording Unavailable\n"),
2787  sLocation, msg +
2788  tr("The file for this recording can "
2789  "not be found"));
2790  break;
2791  case asZeroByte:
2792  ShowNotificationError(tr("Recording Unavailable\n"),
2793  sLocation, msg +
2794  tr("The file for this recording is "
2795  "empty."));
2796  break;
2797  case asNotYetAvailable:
2798  ShowNotificationError(tr("Recording Unavailable\n"),
2799  sLocation, msg +
2800  tr("This recording is not yet "
2801  "available."));
2802  }
2803 }
2804 
2806 {
2807  QString label = tr("There is %n item(s) in the playlist. Actions affect "
2808  "all items in the playlist", "", m_playList.size());
2809 
2810  auto *menu = new MythMenu(label, this, "slotmenu");
2811 
2812  menu->AddItem(tr("Play"), &PlaybackBox::doPlayList);
2813  menu->AddItem(tr("Shuffle Play"), &PlaybackBox::doPlayListRandom);
2814  menu->AddItem(tr("Clear Playlist"), &PlaybackBox::doClearPlaylist);
2815 
2816  if (GetFocusWidget() == m_groupList)
2817  {
2818  if ((m_viewMask & VIEW_TITLES))
2819  {
2820  menu->AddItem(tr("Toggle playlist for this Category/Title"),
2822  }
2823  else
2824  {
2825  menu->AddItem(tr("Toggle playlist for this Group"),
2827  }
2828  }
2829  else
2830  menu->AddItem(tr("Toggle playlist for this recording"),
2831  qOverload<>(&PlaybackBox::togglePlayListItem));
2832 
2833  menu->AddItem(tr("Storage Options"), nullptr, createPlaylistStorageMenu());
2834  menu->AddItem(tr("Job Options"), nullptr, createPlaylistJobMenu());
2835  menu->AddItem(tr("Delete"), &PlaybackBox::PlaylistDeleteKeepHistory);
2836  menu->AddItem(tr("Delete, and allow re-record"),
2838 
2839  return menu;
2840 }
2841 
2843 {
2844  QString label = tr("There is %n item(s) in the playlist. Actions affect "
2845  "all items in the playlist", "", m_playList.size());
2846 
2847  auto *menu = new MythMenu(label, this, "slotmenu");
2848 
2849  menu->AddItem(tr("Change Recording Group"), &PlaybackBox::ShowRecGroupChangerUsePlaylist);
2850  menu->AddItem(tr("Change Playback Group"), &PlaybackBox::ShowPlayGroupChangerUsePlaylist);
2851  menu->AddItem(tr("Disable Auto Expire"), &PlaybackBox::doPlaylistExpireSetOff);
2852  menu->AddItem(tr("Enable Auto Expire"), &PlaybackBox::doPlaylistExpireSetOn);
2853  menu->AddItem(tr("Mark as Watched"), &PlaybackBox::doPlaylistWatchedSetOn);
2854  menu->AddItem(tr("Mark as Unwatched"), &PlaybackBox::doPlaylistWatchedSetOff);
2855  menu->AddItem(tr("Allow Re-record"), &PlaybackBox::doPlaylistAllowRerecord);
2856 
2857  return menu;
2858 }
2859 
2861 {
2862  QString label = tr("There is %n item(s) in the playlist. Actions affect "
2863  "all items in the playlist", "", m_playList.size());
2864 
2865  auto *menu = new MythMenu(label, this, "slotmenu");
2866 
2867  QString jobTitle;
2868  QString command;
2869  QList<uint>::Iterator it;
2870  bool isTranscoding = true;
2871  bool isFlagging = true;
2872  bool isMetadataLookup = true;
2873  bool isRunningUserJob1 = true;
2874  bool isRunningUserJob2 = true;
2875  bool isRunningUserJob3 = true;
2876  bool isRunningUserJob4 = true;
2877 
2878  for(it = m_playList.begin(); it != m_playList.end(); ++it)
2879  {
2880  ProgramInfo *tmpItem = FindProgramInUILists(*it);
2881  if (tmpItem)
2882  {
2884  JOB_TRANSCODE,
2885  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2886  isTranscoding = false;
2888  JOB_COMMFLAG,
2889  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2890  isFlagging = false;
2892  JOB_METADATA,
2893  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2894  isMetadataLookup = false;
2896  JOB_USERJOB1,
2897  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2898  isRunningUserJob1 = false;
2900  JOB_USERJOB2,
2901  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2902  isRunningUserJob2 = false;
2904  JOB_USERJOB3,
2905  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2906  isRunningUserJob3 = false;
2908  JOB_USERJOB4,
2909  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime()))
2910  isRunningUserJob4 = false;
2911  if (!isTranscoding && !isFlagging && !isRunningUserJob1 &&
2912  !isRunningUserJob2 && !isRunningUserJob3 && !isRunningUserJob4)
2913  break;
2914  }
2915  }
2916 
2917  if (!isTranscoding)
2918  menu->AddItem(tr("Begin Transcoding"), &PlaybackBox::doPlaylistBeginTranscoding);
2919  else
2920  menu->AddItem(tr("Stop Transcoding"), &PlaybackBox::stopPlaylistTranscoding);
2921 
2922  if (!isFlagging)
2923  menu->AddItem(tr("Begin Commercial Detection"), &PlaybackBox::doPlaylistBeginFlagging);
2924  else
2925  menu->AddItem(tr("Stop Commercial Detection"), &PlaybackBox::stopPlaylistFlagging);
2926 
2927  if (!isMetadataLookup)
2928  menu->AddItem(tr("Begin Metadata Lookup"), &PlaybackBox::doPlaylistBeginLookup);
2929  else
2930  menu->AddItem(tr("Stop Metadata Lookup"), &PlaybackBox::stopPlaylistLookup);
2931 
2932  command = gCoreContext->GetSetting("UserJob1", "");
2933  if (!command.isEmpty())
2934  {
2935  jobTitle = gCoreContext->GetSetting("UserJobDesc1");
2936 
2937  if (!isRunningUserJob1)
2938  {
2939  menu->AddItem(tr("Begin") + ' ' + jobTitle,
2941  }
2942  else
2943  {
2944  menu->AddItem(tr("Stop") + ' ' + jobTitle,
2946  }
2947  }
2948 
2949  command = gCoreContext->GetSetting("UserJob2", "");
2950  if (!command.isEmpty())
2951  {
2952  jobTitle = gCoreContext->GetSetting("UserJobDesc2");
2953 
2954  if (!isRunningUserJob2)
2955  {
2956  menu->AddItem(tr("Begin") + ' ' + jobTitle,
2958  }
2959  else
2960  {
2961  menu->AddItem(tr("Stop") + ' ' + jobTitle,
2963  }
2964  }
2965 
2966  command = gCoreContext->GetSetting("UserJob3", "");
2967  if (!command.isEmpty())
2968  {
2969  jobTitle = gCoreContext->GetSetting("UserJobDesc3");
2970 
2971  if (!isRunningUserJob3)
2972  {
2973  menu->AddItem(tr("Begin") + ' ' + jobTitle,
2975  }
2976  else
2977  {
2978  menu->AddItem(tr("Stop") + ' ' + jobTitle,
2980  }
2981  }
2982 
2983  command = gCoreContext->GetSetting("UserJob4", "");
2984  if (!command.isEmpty())
2985  {
2986  jobTitle = gCoreContext->GetSetting("UserJobDesc4");
2987 
2988  if (!isRunningUserJob4)
2989  {
2990  menu->AddItem(QString("%1 %2").arg(tr("Begin"), jobTitle),
2992  }
2993  else
2994  {
2995  menu->AddItem(QString("%1 %2").arg(tr("Stop"), jobTitle),
2997  }
2998  }
2999 
3000  return menu;
3001 }
3002 
3004 {
3005  if (m_menuDialog || !m_popupMenu)
3006  return;
3007 
3008  m_menuDialog = new MythDialogBox(m_popupMenu, m_popupStack, "pbbmainmenupopup");
3009 
3010  if (m_menuDialog->Create())
3011  {
3014  }
3015  else
3016  delete m_menuDialog;
3017 }
3018 
3020 {
3021  if (m_menuDialog)
3022  return;
3023 
3024  if (GetFocusWidget() == m_groupList)
3025  ShowGroupPopup();
3026  else
3027  {
3028  ProgramInfo *pginfo = GetCurrentProgram();
3029  if (pginfo)
3030  {
3032  *pginfo, kCheckForMenuAction);
3033 
3034  if ((asPendingDelete == pginfo->GetAvailableStatus()) ||
3035  (asDeleted == pginfo->GetAvailableStatus()) ||
3036  (asNotYetAvailable == pginfo->GetAvailableStatus()))
3037  {
3038  ShowAvailabilityPopup(*pginfo);
3039  }
3040  else
3041  {
3042  ShowActionPopup(*pginfo);
3043  }
3044  }
3045  else
3046  ShowGroupPopup();
3047  }
3048 }
3049 
3051 {
3052  ProgramInfo *pginfo = GetCurrentProgram();
3053  if (!pginfo)
3054  return nullptr;
3055 
3056  QString title = tr("Play Options") + CreateProgramInfoString(*pginfo);
3057 
3058  auto *menu = new MythMenu(title, this, "slotmenu");
3059 
3060  if (pginfo->IsBookmarkSet())
3061  menu->AddItem(tr("Play from bookmark"),
3062  qOverload<>(&PlaybackBox::PlayFromBookmark));
3063 
3064  if (pginfo->QueryLastPlayPos())
3065  menu->AddItem(tr("Play from last played position"),
3066  qOverload<>(&PlaybackBox::PlayFromLastPlayPos));
3067 
3068  menu->AddItem(tr("Play from beginning"),
3069  qOverload<>(&PlaybackBox::PlayFromBeginning));
3070 
3071  if (pginfo->IsBookmarkSet())
3072  menu->AddItem(tr("Clear bookmark"), &PlaybackBox::ClearBookmark);
3073 
3074  return menu;
3075 }
3076 
3078 {
3079  ProgramInfo *pginfo = GetCurrentProgram();
3080  if (!pginfo)
3081  return nullptr;
3082 
3083  QString title = tr("Storage Options") + CreateProgramInfoString(*pginfo);
3084  QString autoExpireText = (pginfo->IsAutoExpirable()) ?
3085  tr("Disable Auto Expire") : tr("Enable Auto Expire");
3086  QString preserveText = (pginfo->IsPreserved()) ?
3087  tr("Do not preserve this episode") : tr("Preserve this episode");
3088 
3089  auto *menu = new MythMenu(title, this, "slotmenu");
3090  menu->AddItem(tr("Change Recording Group"), &PlaybackBox::ShowRecGroupChangerNoPlaylist);
3091  menu->AddItem(tr("Change Playback Group"), &PlaybackBox::ShowPlayGroupChangerNoPlaylist);
3092  menu->AddItem(autoExpireText, &PlaybackBox::toggleAutoExpire);
3093  menu->AddItem(preserveText, &PlaybackBox::togglePreserveEpisode);
3094 
3095  return menu;
3096 }
3097 
3099 {
3100  ProgramInfo *pginfo = GetCurrentProgram();
3101  if (!pginfo)
3102  return nullptr;
3103 
3104  QString title = tr("Scheduling Options") + CreateProgramInfoString(*pginfo);
3105 
3106  auto *menu = new MythMenu(title, this, "slotmenu");
3107 
3108  menu->AddItem(tr("Edit Recording Schedule"),
3109  qOverload<>(&PlaybackBox::EditScheduled));
3110 
3111  menu->AddItem(tr("Allow this episode to re-record"), &PlaybackBox::doAllowRerecord);
3112 
3113  menu->AddItem(tr("Show Recording Details"), &PlaybackBox::ShowDetails);
3114 
3115  menu->AddItem(tr("Change Recording Metadata"), &PlaybackBox::showMetadataEditor);
3116 
3117  menu->AddItem(tr("Custom Edit"), &PlaybackBox::EditCustom);
3118 
3119  return menu;
3120 }
3121 
3122 static const std::array<const int,kMaxJobs> kJobs
3124  JOB_TRANSCODE,
3125  JOB_COMMFLAG,
3126  JOB_METADATA,
3127  JOB_USERJOB1,
3128  JOB_USERJOB2,
3129  JOB_USERJOB3,
3130  JOB_USERJOB4,
3131 };
3132 std::array<PlaybackBoxCb,kMaxJobs*2> PlaybackBox::kMySlots
3133 { // stop start
3141 };
3142 
3144 {
3145  ProgramInfo *pginfo = GetCurrentProgram();
3146  if (!pginfo)
3147  return nullptr;
3148 
3149  QString title = tr("Job Options") + CreateProgramInfoString(*pginfo);
3150 
3151  auto *menu = new MythMenu(title, this, "slotmenu");
3152 
3153  const std::array<const bool,kMaxJobs> add
3154  {
3155  true,
3156  true,
3157  true,
3158  !gCoreContext->GetSetting("UserJob1", "").isEmpty(),
3159  !gCoreContext->GetSetting("UserJob2", "").isEmpty(),
3160  !gCoreContext->GetSetting("UserJob3", "").isEmpty(),
3161  !gCoreContext->GetSetting("UserJob4", "").isEmpty(),
3162  };
3163  const std::array<const QString,kMaxJobs*2> desc
3164  {
3165  // stop start
3166  tr("Stop Transcoding"), tr("Begin Transcoding"),
3167  tr("Stop Commercial Detection"), tr("Begin Commercial Detection"),
3168  tr("Stop Metadata Lookup"), tr("Begin Metadata Lookup"),
3169  "1", "1",
3170  "2", "2",
3171  "3", "3",
3172  "4", "4",
3173  };
3174 
3175  for (size_t i = 0; i < kMaxJobs; i++)
3176  {
3177  if (!add[i])
3178  continue;
3179 
3180  QString stop_desc = desc[i*2+0];
3181  QString start_desc = desc[i*2+1];
3182 
3183  if (start_desc.toUInt())
3184  {
3185  QString jobTitle = gCoreContext->GetSetting(
3186  "UserJobDesc"+start_desc, tr("User Job") + " #" + start_desc);
3187  stop_desc = tr("Stop") + ' ' + jobTitle;
3188  start_desc = tr("Begin") + ' ' + jobTitle;
3189  }
3190 
3191  bool running = JobQueue::IsJobQueuedOrRunning(
3192  kJobs[i], pginfo->GetChanID(), pginfo->GetRecordingStartTime());
3193 
3194  MythMenu *submenu = ((kJobs[i] == JOB_TRANSCODE) && running)
3195  ? createTranscodingProfilesMenu() : nullptr;
3196  menu->AddItem((running) ? stop_desc : start_desc,
3197  kMySlots[i * 2 + (running ? 0 : 1)], submenu);
3198  }
3199 
3200  return menu;
3201 }
3202 
3204 {
3205  QString label = tr("Transcoding profiles");
3206 
3207  auto *menu = new MythMenu(label, this, "transcode");
3208 
3209  menu->AddItemV(tr("Default"), QVariant::fromValue(-1));
3210  menu->AddItemV(tr("Autodetect"), QVariant::fromValue(0));
3211 
3212  MSqlQuery query(MSqlQuery::InitCon());
3213  query.prepare("SELECT r.name, r.id "
3214  "FROM recordingprofiles r, profilegroups p "
3215  "WHERE p.name = 'Transcoders' "
3216  "AND r.profilegroup = p.id "
3217  "AND r.name != 'RTjpeg/MPEG4' "
3218  "AND r.name != 'MPEG2' ");
3219 
3220  if (!query.exec())
3221  {
3222  MythDB::DBError(LOC + "unable to query transcoders", query);
3223  return nullptr;
3224  }
3225 
3226  while (query.next())
3227  {
3228  QString transcoder_name = query.value(0).toString();
3229  int transcoder_id = query.value(1).toInt();
3230 
3231  // Translatable strings for known profiles
3232  if (transcoder_name == "High Quality")
3233  transcoder_name = tr("High Quality");
3234  else if (transcoder_name == "Medium Quality")
3235  transcoder_name = tr("Medium Quality");
3236  else if (transcoder_name == "Low Quality")
3237  transcoder_name = tr("Low Quality");
3238 
3239  menu->AddItemV(transcoder_name, QVariant::fromValue(transcoder_id));
3240  }
3241 
3242  return menu;
3243 }
3244 
3246 {
3247  ProgramInfo *pginfo = GetCurrentProgram();
3248 
3249  if (!pginfo)
3250  return;
3251 
3252  if (id >= 0)
3253  {
3254  RecordingInfo ri(*pginfo);
3256  }
3258 }
3259 
3261 {
3262  QString label =
3263  (asFileNotFound == pginfo.GetAvailableStatus()) ?
3264  tr("Recording file cannot be found") :
3265  (asZeroByte == pginfo.GetAvailableStatus()) ?
3266  tr("Recording file contains no data") :
3267  tr("Recording Options");
3268 
3269  m_popupMenu = new MythMenu(label + CreateProgramInfoString(pginfo), this, "actionmenu");
3270 
3271  if ((asFileNotFound == pginfo.GetAvailableStatus()) ||
3272  (asZeroByte == pginfo.GetAvailableStatus()))
3273  {
3274  if (m_playList.contains(pginfo.GetRecordingID()))
3275  {
3276  m_popupMenu->AddItem(tr("Remove from Playlist"),
3277  qOverload<>(&PlaybackBox::togglePlayListItem));
3278  }
3279  else
3280  {
3281  m_popupMenu->AddItem(tr("Add to Playlist"),
3282  qOverload<>(&PlaybackBox::togglePlayListItem));
3283  }
3284 
3285  if (!m_playList.isEmpty())
3286  m_popupMenu->AddItem(tr("Playlist Options"), nullptr, createPlaylistMenu());
3287 
3288  m_popupMenu->AddItem(tr("Recording Options"), nullptr, createRecordingMenu());
3289 
3291  {
3292  m_popupMenu->AddItem(tr("List Recorded Episodes"),
3294  }
3295  else
3296  {
3297  m_popupMenu->AddItem(tr("List All Recordings"),
3299  }
3300 
3301  m_popupMenu->AddItem(tr("Delete"), &PlaybackBox::askDelete);
3302 
3303  DisplayPopupMenu();
3304 
3305  return;
3306  }
3307 
3308  bool sameProgram = false;
3309 
3310  if (m_player)
3311  sameProgram = m_player->IsSameProgram(&pginfo);
3312 
3313  TVState tvstate = kState_None;
3314 
3315  if (!sameProgram)
3316  {
3317  if (pginfo.IsBookmarkSet() || pginfo.QueryLastPlayPos())
3318  m_popupMenu->AddItem(tr("Play from..."), nullptr, createPlayFromMenu());
3319  else
3320  m_popupMenu->AddItem(tr("Play"),
3322  }
3323 
3324  if (!m_player)
3325  {
3326  if (m_playList.contains(pginfo.GetRecordingID()))
3327  {
3328  m_popupMenu->AddItem(tr("Remove from Playlist"),
3329  qOverload<>(&PlaybackBox::togglePlayListItem));
3330  }
3331  else
3332  {
3333  m_popupMenu->AddItem(tr("Add to Playlist"),
3334  qOverload<>(&PlaybackBox::togglePlayListItem));
3335  }
3336  if (!m_playList.isEmpty())
3337  {
3338  m_popupMenu->AddItem(tr("Playlist Options"), nullptr, createPlaylistMenu());
3339  }
3340  }
3341 
3342  if ((pginfo.GetRecordingStatus() == RecStatus::Recording ||
3343  pginfo.GetRecordingStatus() == RecStatus::Tuning ||
3344  pginfo.GetRecordingStatus() == RecStatus::Failing) &&
3345  (!(sameProgram &&
3346  (tvstate == kState_WatchingLiveTV ||
3347  tvstate == kState_WatchingRecording))))
3348  {
3349  m_popupMenu->AddItem(tr("Stop Recording"), &PlaybackBox::askStop);
3350  }
3351 
3352  if (pginfo.IsWatched())
3353  m_popupMenu->AddItem(tr("Mark as Unwatched"), &PlaybackBox::toggleWatched);
3354  else
3355  m_popupMenu->AddItem(tr("Mark as Watched"), &PlaybackBox::toggleWatched);
3356 
3357  m_popupMenu->AddItem(tr("Storage Options"), nullptr, createStorageMenu());
3358  m_popupMenu->AddItem(tr("Recording Options"), nullptr, createRecordingMenu());
3359  m_popupMenu->AddItem(tr("Job Options"), nullptr, createJobMenu());
3360 
3362  {
3363  m_popupMenu->AddItem(tr("List Recorded Episodes"),
3365  }
3366  else
3367  {
3368  m_popupMenu->AddItem(tr("List All Recordings"),
3370  }
3371 
3372  if (!sameProgram)
3373  {
3374  if (pginfo.GetRecordingGroup() == "Deleted")
3375  {
3376  push_onto_del(m_delList, pginfo);
3377  m_popupMenu->AddItem(tr("Undelete"), &PlaybackBox::Undelete);
3378  m_popupMenu->AddItem(tr("Delete Forever"), qOverload<>(&PlaybackBox::Delete));
3379  }
3380  else
3381  {
3382  m_popupMenu->AddItem(tr("Delete"), &PlaybackBox::askDelete);
3383  }
3384  }
3385 
3386  DisplayPopupMenu();
3387 }
3388 
3390 {
3391  QDateTime recstartts = pginfo.GetRecordingStartTime();
3392  QDateTime recendts = pginfo.GetRecordingEndTime();
3393 
3394  QString timedate = QString("%1 - %2")
3395  .arg(MythDate::toString(
3397  MythDate::toString(recendts, MythDate::kTime));
3398 
3399  QString title = pginfo.GetTitle();
3400 
3401  QString extra;
3402 
3403  if (!pginfo.GetSubtitle().isEmpty())
3404  {
3405  extra = QString('\n') + pginfo.GetSubtitle();
3406  }
3407 
3408  return QString("\n%1%2\n%3").arg(title, extra, timedate);
3409 }
3410 
3412 {
3413  QList<uint>::Iterator it;
3414  for (it = m_playList.begin(); it != m_playList.end(); ++it)
3415  {
3416  ProgramInfo *tmpItem = FindProgramInUILists(*it);
3417 
3418  if (!tmpItem)
3419  continue;
3420 
3421  MythUIButtonListItem *item =
3422  m_recordingList->GetItemByData(QVariant::fromValue(tmpItem));
3423 
3424  if (item)
3425  item->DisplayState("no", "playlist");
3426  }
3427  m_playList.clear();
3428 }
3429 
3431 {
3432  playSelectedPlaylist(false);
3433 }
3434 
3435 
3437 {
3438  playSelectedPlaylist(true);
3439 }
3440 
3442 {
3443  ProgramInfo *pginfo = GetCurrentProgram();
3444  if (pginfo)
3445  {
3446  push_onto_del(m_delList, *pginfo);
3448  }
3449 }
3450 
3458 {
3459  ProgramInfo *pginfo = GetCurrentProgram();
3460 
3461  if (!pginfo)
3462  return;
3463 
3464  RecordingInfo ri(*pginfo);
3465  ri.ForgetHistory();
3466  *pginfo = ri;
3467 }
3468 
3470 {
3471  QList<uint>::Iterator it;
3472 
3473  for (it = m_playList.begin(); it != m_playList.end(); ++it)
3474  {
3475  ProgramInfo *pginfo = FindProgramInUILists(*it);
3476  if (pginfo != nullptr)
3477  {
3478  RecordingInfo ri(*pginfo);
3479  ri.ForgetHistory();
3480  *pginfo = ri;
3481  }
3482  }
3483 
3484  doClearPlaylist();
3485  UpdateUILists();
3486 }
3487 
3488 void PlaybackBox::doJobQueueJob(int jobType, int jobFlags)
3489 {
3490  ProgramInfo *pginfo = GetCurrentProgram();
3491 
3492  if (!pginfo)
3493  return;
3494 
3495  ProgramInfo *tmpItem = FindProgramInUILists(*pginfo);
3496 
3498  jobType, pginfo->GetChanID(), pginfo->GetRecordingStartTime()))
3499  {
3501  jobType, pginfo->GetChanID(), pginfo->GetRecordingStartTime(),
3502  JOB_STOP);
3503  if ((jobType & JOB_COMMFLAG) && (tmpItem))
3504  {
3505  tmpItem->SetEditing(false);
3506  tmpItem->SetFlagging(false);
3507  }
3508  }
3509  else
3510  {
3511  QString jobHost;
3512  if (gCoreContext->GetBoolSetting("JobsRunOnRecordHost", false))
3513  jobHost = pginfo->GetHostname();
3514 
3515  JobQueue::QueueJob(jobType, pginfo->GetChanID(),
3516  pginfo->GetRecordingStartTime(), "", "", jobHost,
3517  jobFlags);
3518  }
3519 }
3520 
3522 {
3524 }
3525 
3527 {
3529 }
3530 
3531 void PlaybackBox::doPlaylistJobQueueJob(int jobType, int jobFlags)
3532 {
3533  for (const uint pbs : qAsConst(m_playList))
3534  {
3535  ProgramInfo *tmpItem = FindProgramInUILists(pbs);
3536  if (tmpItem &&
3538  jobType,
3539  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime())))
3540  {
3541  QString jobHost;
3542  if (gCoreContext->GetBoolSetting("JobsRunOnRecordHost", false))
3543  jobHost = tmpItem->GetHostname();
3544 
3545  JobQueue::QueueJob(jobType, tmpItem->GetChanID(),
3546  tmpItem->GetRecordingStartTime(),
3547  "", "", jobHost, jobFlags);
3548  }
3549  }
3550 }
3551 
3553 {
3554  QList<uint>::Iterator it;
3555 
3556  for (it = m_playList.begin(); it != m_playList.end(); ++it)
3557  {
3558  ProgramInfo *tmpItem = FindProgramInUILists(*it);
3559  if (tmpItem &&
3561  jobType,
3562  tmpItem->GetChanID(), tmpItem->GetRecordingStartTime())))
3563  {
3565  jobType, tmpItem->GetChanID(),
3566  tmpItem->GetRecordingStartTime(), JOB_STOP);
3567 
3568  if ((jobType & JOB_COMMFLAG) && (tmpItem))
3569  {
3570  tmpItem->SetEditing(false);
3571  tmpItem->SetFlagging(false);
3572  }
3573  }
3574  }
3575 }
3576 
3578 {
3579  ProgramInfo *pginfo = GetCurrentProgram();
3580  if (pginfo)
3581  {
3582  push_onto_del(m_delList, *pginfo);
3584  }
3585 }
3586 
3587 void PlaybackBox::PlaylistDelete(bool forgetHistory)
3588 {
3589  QString forceDeleteStr("0");
3590 
3591  QStringList list;
3592  for (int id : qAsConst(m_playList))
3593  {
3594  ProgramInfo *tmpItem = FindProgramInUILists(id);
3595  if (tmpItem && tmpItem->QueryIsDeleteCandidate())
3596  {
3597  tmpItem->SetAvailableStatus(asPendingDelete, "PlaylistDelete");
3598  list.push_back(QString::number(tmpItem->GetRecordingID()));
3599  list.push_back(forceDeleteStr);
3600  list.push_back(forgetHistory ? "1" : "0");
3601 
3602  // if the item is in the current recording list UI then delete it.
3603  MythUIButtonListItem *uiItem =
3604  m_recordingList->GetItemByData(QVariant::fromValue(tmpItem));
3605  if (uiItem)
3606  m_recordingList->RemoveItem(uiItem);
3607  }
3608  }
3609  m_playList.clear();
3610 
3611  if (!list.empty())
3612  m_helper.DeleteRecordings(list);
3613 
3614  doClearPlaylist();
3615 }
3616 
3617 // FIXME: Huh? This doesn't specify which recording to undelete, it just
3618 // undeletes the first one on the list
3620 {
3621  uint recordingID = 0;
3622  if (extract_one_del(m_delList, recordingID))
3623  m_helper.UndeleteRecording(recordingID);
3624 }
3625 
3627 {
3628  uint recordingID = 0;
3629  while (extract_one_del(m_delList, recordingID))
3630  {
3631  if (flags & kIgnore)
3632  continue;
3633 
3634  RemoveProgram(recordingID, (flags & kForgetHistory) != 0, (flags & kForce) != 0);
3635 
3636  if (!(flags & kAllRemaining))
3637  break;
3638  }
3639 
3640  if (!m_delList.empty())
3641  {
3642  auto *e = new MythEvent("DELETE_FAILURES", m_delList);
3643  m_delList.clear();
3644  QCoreApplication::postEvent(this, e);
3645  }
3646 }
3647 
3649 {
3650  ProgramInfo *pginfo = GetCurrentProgram();
3651  if (pginfo) {
3652  QString title = pginfo->GetTitle().toLower();
3653  MythUIButtonListItem* group = m_groupList->GetItemByData(QVariant::fromValue(title));
3654  if (group)
3655  {
3656  m_groupList->SetItemCurrent(group);
3657  // set focus back to previous item
3658  MythUIButtonListItem *previousItem = m_recordingList->GetItemByData(QVariant::fromValue(pginfo));
3659  m_recordingList->SetItemCurrent(previousItem);
3660  }
3661  }
3662 }
3663 
3665 {
3666  ProgramInfo *pginfo = GetCurrentProgram();
3668  if (pginfo)
3669  {
3670  // set focus back to previous item
3671  MythUIButtonListItem *previousitem =
3672  m_recordingList->GetItemByData(QVariant::fromValue(pginfo));
3673  m_recordingList->SetItemCurrent(previousitem);
3674  }
3675 }
3676 
3678 {
3679  return FindProgramInUILists( pginfo.GetRecordingID(),
3680  pginfo.GetRecordingGroup());
3681 }
3682 
3684  const QString& recgroup)
3685 {
3686  // LiveTV ProgramInfo's are not in the aggregated list
3687  std::array<ProgramList::iterator,2> _it {
3688  m_progLists[tr("Live TV").toLower()].begin(), m_progLists[""].begin() };
3689  std::array<ProgramList::iterator,2> _end {
3690  m_progLists[tr("Live TV").toLower()].end(), m_progLists[""].end() };
3691 
3692  if (recgroup != "LiveTV")
3693  {
3694  swap( _it[0], _it[1]);
3695  swap(_end[0], _end[1]);
3696  }
3697 
3698  for (uint i = 0; i < 2; i++)
3699  {
3700  auto it = _it[i];
3701  auto end = _end[i];
3702  for (; it != end; ++it)
3703  {
3704  if ((*it)->GetRecordingID() == recordingID)
3705  {
3706  return *it;
3707  }
3708  }
3709  }
3710 
3711  return nullptr;
3712 }
3713 
3715 {
3717 
3718  if (!item)
3719  return;
3720 
3721  auto *pginfo = item->GetData().value<ProgramInfo *>();
3722 
3723  if (!pginfo)
3724  return;
3725 
3726  bool on = !pginfo->IsWatched();
3727  pginfo->SaveWatched(on);
3728  item->DisplayState((on)?"yes":"on", "watched");
3729  updateIcons(pginfo);
3730 
3731  // A refill affects the responsiveness of the UI and we only
3732  // need to rebuild the list if the watch list is displayed
3733  if (m_viewMask & VIEW_WATCHLIST)
3734  UpdateUILists();
3735 }
3736 
3738 {
3740 
3741  if (!item)
3742  return;
3743 
3744  auto *pginfo = item->GetData().value<ProgramInfo *>();
3745 
3746  if (!pginfo)
3747  return;
3748 
3749  bool on = !pginfo->IsAutoExpirable();
3750  pginfo->SaveAutoExpire((on) ? kNormalAutoExpire : kDisableAutoExpire, true);
3751  item->DisplayState((on)?"yes":"no", "autoexpire");
3752  updateIcons(pginfo);
3753 }
3754 
3756 {
3758 
3759  if (!item)
3760  return;
3761 
3762  auto *pginfo = item->GetData().value<ProgramInfo *>();
3763 
3764  if (!pginfo)
3765  return;
3766 
3767  bool on = !pginfo->IsPreserved();
3768  pginfo->SavePreserve(on);
3769  item->DisplayState(on?"yes":"no", "preserve");
3770  updateIcons(pginfo);
3771 }
3772 
3773 void PlaybackBox::toggleView(ViewMask itemMask, bool setOn)
3774 {
3775  if (setOn)
3776  m_viewMask = (ViewMask)(m_viewMask | itemMask);
3777  else
3778  m_viewMask = (ViewMask)(m_viewMask & ~itemMask);
3779 
3780  UpdateUILists();
3781 }
3782 
3784 {
3785  QString groupname = m_groupList->GetItemCurrent()->GetData().toString();
3786 
3787  for (auto *pl : qAsConst(m_progLists[groupname]))
3788  {
3789  if (pl && (pl->GetAvailableStatus() == asAvailable))
3790  togglePlayListItem(pl);
3791  }
3792 }
3793 
3795 {
3797 
3798  if (!item)
3799  return;
3800 
3801  auto *pginfo = item->GetData().value<ProgramInfo *>();
3802 
3803  if (!pginfo)
3804  return;
3805 
3806  togglePlayListItem(pginfo);
3807 
3810 }
3811 
3813 {
3814  if (!pginfo)
3815  return;
3816 
3817  uint recordingID = pginfo->GetRecordingID();
3818 
3819  MythUIButtonListItem *item =
3820  m_recordingList->GetItemByData(QVariant::fromValue(pginfo));
3821 
3822  if (m_playList.contains(recordingID))
3823  {
3824  if (item)
3825  item->DisplayState("no", "playlist");
3826 
3827  m_playList.removeAll(recordingID);
3828  }
3829  else
3830  {
3831  if (item)
3832  item->DisplayState("yes", "playlist");
3833  m_playList.append(recordingID);
3834  }
3835 }
3836 
3838 {
3839  int commands = 0;
3840  QString command;
3841 
3842  m_ncLock.lock();
3843  commands = m_networkControlCommands.size();
3844  m_ncLock.unlock();
3845 
3846  while (commands)
3847  {
3848  m_ncLock.lock();
3849  command = m_networkControlCommands.front();
3850  m_networkControlCommands.pop_front();
3851  m_ncLock.unlock();
3852 
3854 
3855  m_ncLock.lock();
3856  commands = m_networkControlCommands.size();
3857  m_ncLock.unlock();
3858  }
3859 }
3860 
3861 void PlaybackBox::processNetworkControlCommand(const QString &command)
3862 {
3863  QStringList tokens = command.simplified().split(" ");
3864 
3865  if (tokens.size() >= 4 && (tokens[1] == "PLAY" || tokens[1] == "RESUME"))
3866  {
3867  if (tokens.size() == 6 && tokens[2] == "PROGRAM")
3868  {
3869  int clientID = tokens[5].toInt();
3870 
3871  LOG(VB_GENERAL, LOG_INFO, LOC +
3872  QString("NetworkControl: Trying to %1 program '%2' @ '%3'")
3873  .arg(tokens[1], tokens[3], tokens[4]));
3874 
3875  if (m_playingSomething)
3876  {
3877  LOG(VB_GENERAL, LOG_ERR, LOC +
3878  "NetworkControl: Already playing");
3879 
3880  QString msg = QString(
3881  "NETWORK_CONTROL RESPONSE %1 ERROR: Unable to play, "
3882  "player is already playing another recording.")
3883  .arg(clientID);
3884 
3885  MythEvent me(msg);
3886  gCoreContext->dispatch(me);
3887  return;
3888  }
3889 
3890  uint chanid = tokens[3].toUInt();
3891  QDateTime recstartts = MythDate::fromString(tokens[4]);
3892  ProgramInfo pginfo(chanid, recstartts);
3893 
3894  if (pginfo.GetChanID())
3895  {
3896  QString msg = QString("NETWORK_CONTROL RESPONSE %1 OK")
3897  .arg(clientID);
3898  MythEvent me(msg);
3899  gCoreContext->dispatch(me);
3900 
3901  pginfo.SetPathname(pginfo.GetPlaybackURL());
3902 
3903  const bool ignoreBookmark = (tokens[1] == "PLAY");
3904  const bool ignoreProgStart = true;
3905  const bool ignoreLastPlayPos = true;
3906  const bool underNetworkControl = true;
3907  PlayX(pginfo, ignoreBookmark, ignoreProgStart,
3908  ignoreLastPlayPos, underNetworkControl);
3909  }
3910  else
3911  {
3912  QString message = QString("NETWORK_CONTROL RESPONSE %1 "
3913  "ERROR: Could not find recording for "
3914  "chanid %2 @ %3")
3915  .arg(tokens[5], tokens[3], tokens[4]);
3916  MythEvent me(message);
3917  gCoreContext->dispatch(me);
3918  }
3919  }
3920  }
3921 }
3922 
3923 bool PlaybackBox::keyPressEvent(QKeyEvent *event)
3924 {
3925  // This should be an impossible keypress we've simulated
3926  if ((event->key() == Qt::Key_LaunchMedia) &&
3927  (event->modifiers() ==
3928  (Qt::ShiftModifier |
3929  Qt::ControlModifier |
3930  Qt::AltModifier |
3931  Qt::MetaModifier |
3932  Qt::KeypadModifier)))
3933  {
3934  event->accept();
3935  m_ncLock.lock();
3936  int commands = m_networkControlCommands.size();
3937  m_ncLock.unlock();
3938  if (commands)
3940  return true;
3941  }
3942 
3943  if (GetFocusWidget()->keyPressEvent(event))
3944  return true;
3945 
3946  QStringList actions;
3947  bool handled = GetMythMainWindow()->TranslateKeyPress("TV Frontend",
3948  event, actions);
3949 
3950  for (int i = 0; i < actions.size() && !handled; ++i)
3951  {
3952  QString action = actions[i];
3953  handled = true;
3954 
3955  if (action == ACTION_1 || action == "HELP")
3956  showIconHelp();
3957  else if (action == "MENU")
3958  {
3959  ShowMenu();
3960  }
3961  else if (action == "NEXTFAV")
3962  {
3963  if (GetFocusWidget() == m_groupList)
3965  else
3967  }
3968  else if (action == "TOGGLEFAV")
3969  {
3970  m_playList.clear();
3971  UpdateUILists();
3972  }
3973  else if (action == ACTION_TOGGLERECORD)
3974  {
3976  UpdateUILists();
3977  }
3978  else if (action == ACTION_PAGERIGHT)
3979  {
3981  }
3982  else if (action == ACTION_PAGELEFT)
3983  {
3984  QString nextGroup;
3985  m_recGroupsLock.lock();
3986  if (m_recGroupIdx >= 0 && !m_recGroups.empty())
3987  {
3988  if (--m_recGroupIdx < 0)
3989  m_recGroupIdx = m_recGroups.size() - 1;
3990  nextGroup = m_recGroups[m_recGroupIdx];
3991  }
3992  m_recGroupsLock.unlock();
3993 
3994  if (!nextGroup.isEmpty())
3995  displayRecGroup(nextGroup);
3996  }
3997  else if (action == "NEXTVIEW")
3998  {
4000  if (++curpos >= m_groupList->GetCount())
4001  curpos = 0;
4002  m_groupList->SetItemCurrent(curpos);
4003  }
4004  else if (action == "PREVVIEW")
4005  {
4007  if (--curpos < 0)
4008  curpos = m_groupList->GetCount() - 1;
4009  m_groupList->SetItemCurrent(curpos);
4010  }
4011  else if (action == ACTION_LISTRECORDEDEPISODES)
4012  {
4015  else
4017  }
4018  else if (action == "CHANGERECGROUP")
4019  showGroupFilter();
4020  else if (action == "CHANGEGROUPVIEW")
4021  showViewChanger();
4022  else if (action == "EDIT")
4023  EditScheduled();
4024  else if (m_titleList.size() > 1)
4025  {
4026  if (action == "DELETE")
4028  else if (action == ACTION_PLAYBACK)
4030  else if (action == "DETAILS" || action == "INFO")
4031  ShowDetails();
4032  else if (action == "CUSTOMEDIT")
4033  EditCustom();
4034  else if (action == "GUIDE")
4035  ShowGuide();
4036  else if (action == "UPCOMING")
4037  ShowUpcoming();
4038  else if (action == ACTION_VIEWSCHEDULED)
4040  else if (action == ACTION_PREVRECORDED)
4041  ShowPrevious();
4042  else
4043  handled = false;
4044  }
4045  else
4046  handled = false;
4047  }
4048 
4049  if (!handled && MythScreenType::keyPressEvent(event))
4050  handled = true;
4051 
4052  return handled;
4053 }
4054 
4055 void PlaybackBox::customEvent(QEvent *event)
4056 {
4057  if (event->type() == DialogCompletionEvent::kEventType)
4058  {
4059  auto *dce = dynamic_cast<DialogCompletionEvent*>(event);
4060  if (!dce)
4061  return;
4062 
4063  QString resultid = dce->GetId();
4064 
4065  if (resultid == "transcode" && dce->GetResult() >= 0)
4066  changeProfileAndTranscode(dce->GetData().toInt());
4067  }
4068  else if (event->type() == MythEvent::MythEventMessage)
4069  {
4070  auto *me = dynamic_cast<MythEvent *>(event);
4071  if (me == nullptr)
4072  return;
4073 
4074  const QString& message = me->Message();
4075 
4076  if (message.startsWith("RECORDING_LIST_CHANGE"))
4077  {
4078  QStringList tokens = message.simplified().split(" ");
4079  uint recordingID = 0;
4080  if (tokens.size() >= 3)
4081  recordingID = tokens[2].toUInt();
4082 
4083  if ((tokens.size() >= 2) && tokens[1] == "UPDATE")
4084  {
4085  ProgramInfo evinfo(me->ExtraDataList());
4086  if (evinfo.HasPathname() || evinfo.GetChanID())
4088  }
4089  else if (recordingID && (tokens[1] == "ADD"))
4090  {
4091  ProgramInfo evinfo(recordingID);
4092  if (evinfo.GetChanID())
4093  {
4095  HandleRecordingAddEvent(evinfo);
4096  }
4097  }
4098  else if (recordingID && (tokens[1] == "DELETE"))
4099  {
4100  HandleRecordingRemoveEvent(recordingID);
4101  }
4102  else
4103  {
4105  }
4106  }
4107  else if (message.startsWith("NETWORK_CONTROL"))
4108  {
4109  QStringList tokens = message.simplified().split(" ");
4110  if ((tokens[1] != "ANSWER") && (tokens[1] != "RESPONSE"))
4111  {
4112  m_ncLock.lock();
4113  m_networkControlCommands.push_back(message);
4114  m_ncLock.unlock();
4115 
4116  // This should be an impossible keypress we're simulating
4117  Qt::KeyboardModifiers modifiers =
4118  Qt::ShiftModifier |
4119  Qt::ControlModifier |
4120  Qt::AltModifier |
4121  Qt::MetaModifier |
4122  Qt::KeypadModifier;
4123  auto *keyevent = new QKeyEvent(QEvent::KeyPress,
4124  Qt::Key_LaunchMedia, modifiers);
4125  QCoreApplication::postEvent(GetMythMainWindow(), keyevent);
4126 
4127  keyevent = new QKeyEvent(QEvent::KeyRelease,
4128  Qt::Key_LaunchMedia, modifiers);
4129  QCoreApplication::postEvent(GetMythMainWindow(), keyevent);
4130  }
4131  }
4132  else if (message.startsWith("UPDATE_FILE_SIZE"))
4133  {
4134  QStringList tokens = message.simplified().split(" ");
4135  bool ok = false;
4136  uint recordingID = 0;
4137  uint64_t filesize = 0ULL;
4138  if (tokens.size() >= 3)
4139  {
4140  recordingID = tokens[1].toUInt();
4141  filesize = tokens[2].toLongLong(&ok);
4142  }
4143  if (recordingID && ok)
4144  {
4145 
4146  HandleUpdateProgramInfoFileSizeEvent(recordingID, filesize);
4147  }
4148  }
4149  else if (message == "UPDATE_UI_LIST")
4150  {
4151  if (m_playingSomething)
4152  m_needUpdate = true;
4153  else
4154  {
4155  UpdateUILists();
4157  }
4158  }
4159  else if (message == "UPDATE_USAGE_UI")
4160  {
4161  UpdateUsageUI();
4162  }
4163  else if (message == "RECONNECT_SUCCESS")
4164  {
4166  }
4167  else if (message == "LOCAL_PBB_DELETE_RECORDINGS")
4168  {
4169  QStringList list;
4170  for (uint i = 0; i+2 < (uint)me->ExtraDataList().size(); i+=3)
4171  {
4172  uint recordingID = me->ExtraDataList()[i+0].toUInt();
4173  ProgramInfo *pginfo =
4174  m_programInfoCache.GetRecordingInfo(recordingID);
4175 
4176  if (!pginfo)
4177  {
4178  LOG(VB_GENERAL, LOG_WARNING, LOC +
4179  QString("LOCAL_PBB_DELETE_RECORDINGS - "
4180  "No matching recording %1")
4181  .arg(recordingID));
4182  continue;
4183  }
4184 
4185  QString forceDeleteStr = me->ExtraDataList()[i+1];
4186  QString forgetHistoryStr = me->ExtraDataList()[i+2];
4187 
4188  list.push_back(QString::number(pginfo->GetRecordingID()));
4189  list.push_back(forceDeleteStr);
4190  list.push_back(forgetHistoryStr);
4192  "LOCAL_PBB_DELETE_RECORDINGS");
4193 
4194  // if the item is in the current recording list UI
4195  // then delete it.
4196  MythUIButtonListItem *uiItem =
4197  m_recordingList->GetItemByData(QVariant::fromValue(pginfo));
4198  if (uiItem)
4199  m_recordingList->RemoveItem(uiItem);
4200  }
4201  if (!list.empty())
4202  m_helper.DeleteRecordings(list);
4203  }
4204  else if (message == "DELETE_SUCCESSES")
4205  {
4207  }
4208  else if (message == "DELETE_FAILURES")
4209  {
4210  if (me->ExtraDataList().size() < 3)
4211  return;
4212 
4213  for (uint i = 0; i+2 < (uint)me->ExtraDataList().size(); i += 3)
4214  {
4216  me->ExtraDataList()[i+0].toUInt());
4217  if (pginfo)
4218  {
4219  pginfo->SetAvailableStatus(asAvailable, "DELETE_FAILURES");
4221  }
4222  }
4223 
4224  bool forceDelete = me->ExtraDataList()[1].toUInt() != 0U;
4225  if (!forceDelete)
4226  {
4227  m_delList = me->ExtraDataList();
4228  if (!m_menuDialog)
4229  {
4231  return;
4232  }
4233  LOG(VB_GENERAL, LOG_WARNING, LOC +
4234  "Delete failures not handled due to "
4235  "pre-existing popup.");
4236  }
4237 
4238  // Since we deleted items from the UI after we set
4239  // asPendingDelete, we need to put them back now..
4241  }
4242  else if (message == "PREVIEW_SUCCESS")
4243  {
4244  HandlePreviewEvent(me->ExtraDataList());
4245  }
4246  else if (message == "PREVIEW_FAILED" && me->ExtraDataCount() >= 5)
4247  {
4248  for (uint i = 4; i < (uint) me->ExtraDataCount(); i++)
4249  {
4250  const QString& token = me->ExtraData(i);
4251  QSet<QString>::iterator it = m_previewTokens.find(token);
4252  if (it != m_previewTokens.end())
4253  m_previewTokens.erase(it);
4254  }
4255  }
4256  else if (message == "AVAILABILITY" && me->ExtraDataCount() == 8)
4257  {
4258  static constexpr std::chrono::milliseconds kMaxUIWaitTime = 10s;
4259  QStringList list = me->ExtraDataList();
4260  uint recordingID = list[0].toUInt();
4261  auto cat = (CheckAvailabilityType) list[1].toInt();
4262  auto availableStatus = (AvailableStatusType) list[2].toInt();
4263  uint64_t fs = list[3].toULongLong();
4264  QTime tm;
4265  tm.setHMS(list[4].toUInt(), list[5].toUInt(),
4266  list[6].toUInt(), list[7].toUInt());
4267  QTime now = QTime::currentTime();
4268  auto time_elapsed = std::chrono::milliseconds(tm.msecsTo(now));
4269  if (time_elapsed < 0ms)
4270  time_elapsed += 24h;
4271 
4272  AvailableStatusType old_avail = availableStatus;
4273  ProgramInfo *pginfo = FindProgramInUILists(recordingID);
4274  if (pginfo)
4275  {
4276  pginfo->SetFilesize(std::max(pginfo->GetFilesize(), fs));
4277  old_avail = pginfo->GetAvailableStatus();
4278  pginfo->SetAvailableStatus(availableStatus, "AVAILABILITY");
4279  }
4280 
4281  if (time_elapsed >= kMaxUIWaitTime)
4282  m_playListPlay.clear();
4283 
4284  bool playnext = ((kCheckForPlaylistAction == cat) &&
4285  !m_playListPlay.empty());
4286 
4287 
4288  if (((kCheckForPlayAction == cat) ||
4289  (kCheckForPlaylistAction == cat)) &&
4290  (time_elapsed < kMaxUIWaitTime))
4291  {
4292  if (asAvailable != availableStatus)
4293  {
4294  if (kCheckForPlayAction == cat && pginfo)
4295  ShowAvailabilityPopup(*pginfo);
4296  }
4297  else if (pginfo)
4298  {
4299  playnext = false;
4300  const bool ignoreBookmark = false;
4301  const bool ignoreProgStart = false;
4302  const bool ignoreLastPlayPos = true;
4303  const bool underNetworkControl = false;
4304  Play(*pginfo, kCheckForPlaylistAction == cat,
4305  ignoreBookmark, ignoreProgStart, ignoreLastPlayPos,
4306  underNetworkControl);
4307  }
4308  }
4309 
4310  if (playnext)
4311  {
4312  // failed to play this item, instead
4313  // play the next item on the list..
4314  QCoreApplication::postEvent(
4315  this, new MythEvent("PLAY_PLAYLIST"));
4316  }
4317 
4318  if (old_avail != availableStatus)
4319  UpdateUIListItem(pginfo, true);
4320  }
4321  else if ((message == "PLAY_PLAYLIST") && !m_playListPlay.empty())
4322  {
4323  uint recordingID = m_playListPlay.front();
4324  m_playListPlay.pop_front();
4325 
4326  if (!m_playListPlay.empty())
4327  {
4328  const ProgramInfo *pginfo =
4330  if (pginfo)
4332  }
4333 
4334  ProgramInfo *pginfo = FindProgramInUILists(recordingID);
4335  const bool ignoreBookmark = false;
4336  const bool ignoreProgStart = true;
4337  const bool ignoreLastPlayPos = true;
4338  const bool underNetworkControl = false;
4339  if (pginfo)
4340  Play(*pginfo, true, ignoreBookmark, ignoreProgStart,
4341  ignoreLastPlayPos, underNetworkControl);
4342  }
4343  else if ((message == "SET_PLAYBACK_URL") && (me->ExtraDataCount() == 2))
4344  {
4345  uint recordingID = me->ExtraData(0).toUInt();
4346  ProgramInfo *info = m_programInfoCache.GetRecordingInfo(recordingID);
4347  if (info)
4348  info->SetPathname(me->ExtraData(1));
4349  }
4350  else if ((message == "FOUND_ARTWORK") && (me->ExtraDataCount() >= 5))
4351  {
4352  auto type = (VideoArtworkType) me->ExtraData(2).toInt();
4353  uint recordingID = me->ExtraData(3).toUInt();
4354  const QString& group = me->ExtraData(4);
4355  const QString& fn = me->ExtraData(5);
4356 
4357  if (recordingID)
4358  {
4359  ProgramInfo *pginfo = m_programInfoCache.GetRecordingInfo(recordingID);
4360  if (pginfo &&
4361  m_recordingList->GetItemByData(QVariant::fromValue(pginfo)) ==
4363  m_artImage[(uint)type]->GetFilename() != fn)
4364  {
4365  m_artImage[(uint)type]->SetFilename(fn);
4366  m_artTimer[(uint)type]->start(s_artDelay[(uint)type]);
4367  }
4368  }
4369  else if (!group.isEmpty() &&
4370  (m_currentGroup == group) &&
4371  m_artImage[type] &&
4373  m_artImage[(uint)type]->GetFilename() != fn)
4374  {
4375  m_artImage[(uint)type]->SetFilename(fn);
4376  m_artTimer[(uint)type]->start(s_artDelay[(uint)type]);
4377  }
4378  }
4379  else if (message == "EXIT_TO_MENU" ||
4380  message == "CANCEL_PLAYLIST")
4381  {
4382  m_playListPlay.clear();
4383  }
4384  }
4385  else
4387 }
4388 
4390 {
4391  if (!m_programInfoCache.Remove(recordingID))
4392  {
4393  LOG(VB_GENERAL, LOG_WARNING, LOC +
4394  QString("Failed to remove %1, reloading list")
4395  .arg(recordingID));
4397  return;
4398  }
4399 
4401  QString groupname;
4402  if (sel_item)
4403  groupname = sel_item->GetData().toString();
4404 
4405  ProgramMap::iterator git = m_progLists.begin();
4406  while (git != m_progLists.end())
4407  {
4408  auto pit = (*git).begin();
4409  while (pit != (*git).end())
4410  {
4411  if ((*pit)->GetRecordingID() == recordingID)
4412  {
4413  if (!git.key().isEmpty() && git.key() == groupname)
4414  {
4415  MythUIButtonListItem *item_by_data =
4417  QVariant::fromValue(*pit));
4418  MythUIButtonListItem *item_cur =
4420 
4421  if (item_cur && (item_by_data == item_cur))
4422  {
4423  MythUIButtonListItem *item_next =
4424  m_recordingList->GetItemNext(item_cur);
4425  if (item_next)
4426  m_recordingList->SetItemCurrent(item_next);
4427  }
4428 
4429  m_recordingList->RemoveItem(item_by_data);
4430  }
4431  pit = (*git).erase(pit);
4432  }
4433  else
4434  {
4435  ++pit;
4436  }
4437  }
4438 
4439  if ((*git).empty())
4440  {
4441  if (!groupname.isEmpty() && (git.key() == groupname))
4442  {
4443  MythUIButtonListItem *next_item =
4444  m_groupList->GetItemNext(sel_item);
4445  if (next_item)
4446  m_groupList->SetItemCurrent(next_item);
4447 
4448  m_groupList->RemoveItem(sel_item);
4449 
4450  sel_item = next_item;
4451  groupname = "";
4452  if (sel_item)
4453  groupname = sel_item->GetData().toString();
4454  }
4455  git = m_progLists.erase(git);
4456  }
4457  else
4458  {
4459  ++git;
4460  }
4461  }
4462 
4464 }
4465 
4467 {
4468  m_programInfoCache.Add(evinfo);
4470 }
4471 
4473 {
4474  QString old_recgroup = m_programInfoCache.GetRecGroup(
4475  evinfo.GetRecordingID());
4476 
4477  if (!m_programInfoCache.Update(evinfo))
4478  return;
4479 
4480  // If the recording group has changed, reload lists from the recently
4481  // updated cache; if not, only update UI for the updated item
4482  if (evinfo.GetRecordingGroup() == old_recgroup)
4483  {
4484  ProgramInfo *dst = FindProgramInUILists(evinfo);
4485  if (dst)
4486  UpdateUIListItem(dst, true);
4487  return;
4488  }
4489 
4491 }
4492 
4494  uint64_t filesize)
4495 {
4496  m_programInfoCache.UpdateFileSize(recordingID, filesize);
4497 
4498  ProgramInfo *dst = FindProgramInUILists(recordingID);
4499  if (dst)
4500  UpdateUIListItem(dst, false);
4501 }
4502 
4504 {
4506  QCoreApplication::postEvent(this, new MythEvent("UPDATE_UI_LIST"));
4507 }
4508 
4510 {
4511  auto *helpPopup = new HelpPopup(m_popupStack);
4512 
4513  if (helpPopup->Create())
4514  m_popupStack->AddScreen(helpPopup);
4515  else
4516  delete helpPopup;
4517 }
4518 
4520 {
4521  auto *viewPopup = new ChangeView(m_popupStack, this, m_viewMask);
4522 
4523  if (viewPopup->Create())
4524  {
4525  connect(viewPopup, &ChangeView::save, this, &PlaybackBox::saveViewChanges);
4526  m_popupStack->AddScreen(viewPopup);
4527  }
4528  else
4529  delete viewPopup;
4530 }
4531 
4533 {
4534  if (m_viewMask == VIEW_NONE)
4536  gCoreContext->SaveSetting("DisplayGroupDefaultViewMask", (int)m_viewMask);
4537  gCoreContext->SaveBoolSetting("PlaybackWatchList",
4538  (m_viewMask & VIEW_WATCHLIST) != 0);
4539 }
4540 
4542 {
4543  QStringList groupNames;
4544  QStringList displayNames;
4545  QStringList groups;
4546  QStringList displayGroups;
4547 
4548  MSqlQuery query(MSqlQuery::InitCon());
4549 
4550  m_recGroupType.clear();
4551 
4552  uint totalItems = 0;
4553 
4554  // Add the group entries
4555  displayNames.append(QString("------- %1 -------").arg(tr("Groups")));
4556  groupNames.append("");
4557 
4558  // Find each recording group, and the number of recordings in each
4559  query.prepare("SELECT recgroup, COUNT(title) FROM recorded "
4560  "WHERE deletepending = 0 AND watched <= :WATCHED "
4561  "GROUP BY recgroup");
4562  query.bindValue(":WATCHED", (m_viewMask & VIEW_WATCHED));
4563  if (query.exec())
4564  {
4565  while (query.next())
4566  {
4567  QString dispGroup = query.value(0).toString();
4568  uint items = query.value(1).toInt();
4569 
4570  if ((dispGroup != "LiveTV" || (m_viewMask & VIEW_LIVETVGRP)) &&
4571  (dispGroup != "Deleted"))
4572  totalItems += items;
4573 
4574  groupNames.append(dispGroup);
4575 
4576  dispGroup = (dispGroup == "Default") ? tr("Default") : dispGroup;
4577  dispGroup = (dispGroup == "Deleted") ? tr("Deleted") : dispGroup;
4578  dispGroup = (dispGroup == "LiveTV") ? tr("Live TV") : dispGroup;
4579 
4580  displayNames.append(tr("%1 [%n item(s)]", nullptr, items).arg(dispGroup));
4581 
4582  m_recGroupType[query.value(0).toString()] = "recgroup";
4583  }
4584  }
4585 
4586  // Create and add the "All Programs" entry
4587  displayNames.push_front(tr("%1 [%n item(s)]", nullptr, totalItems)
4588  .arg(ProgramInfo::i18n("All Programs")));
4589  groupNames.push_front("All Programs");
4590  m_recGroupType["All Programs"] = "recgroup";
4591 
4592  // Find each category, and the number of recordings in each
4593  query.prepare("SELECT DISTINCT category, COUNT(title) FROM recorded "
4594  "WHERE deletepending = 0 AND watched <= :WATCHED "
4595  "GROUP BY category");
4596  query.bindValue(":WATCHED", (m_viewMask & VIEW_WATCHED));
4597  if (query.exec())
4598  {
4599  int unknownCount = 0;
4600  while (query.next())
4601  {
4602  uint items = query.value(1).toInt();
4603  QString dispGroup = query.value(0).toString();
4604  if (dispGroup.isEmpty())
4605  {
4606  unknownCount += items;
4607  dispGroup = tr("Unknown");
4608  }
4609  else if (dispGroup == tr("Unknown"))
4610  unknownCount += items;
4611 
4612  if ((!m_recGroupType.contains(dispGroup)) &&
4613  (dispGroup != tr("Unknown")))
4614  {
4615  displayGroups += tr("%1 [%n item(s)]", nullptr, items).arg(dispGroup);
4616  groups += dispGroup;
4617 
4618  m_recGroupType[dispGroup] = "category";
4619  }
4620  }
4621 
4622  if (unknownCount > 0)
4623  {
4624  QString dispGroup = tr("Unknown");
4625  uint items = unknownCount;
4626  displayGroups += tr("%1 [%n item(s)]", nullptr, items).arg(dispGroup);
4627  groups += dispGroup;
4628 
4629  m_recGroupType[dispGroup] = "category";
4630  }
4631  }
4632 
4633  // Add the category entries
4634  displayNames.append(QString("------- %1 -------").arg(tr("Categories")));
4635  groupNames.append("");
4636  groups.sort();
4637  displayGroups.sort();
4638  QStringList::iterator it;
4639  for (it = displayGroups.begin(); it != displayGroups.end(); ++it)
4640  displayNames.append(*it);
4641  for (it = groups.begin(); it != groups.end(); ++it)
4642  groupNames.append(*it);
4643 
4644  QString label = tr("Change Filter");
4645 
4646  auto *recGroupPopup = new GroupSelector(m_popupStack, label, displayNames,
4647  groupNames, m_recGroup);
4648 
4649  if (recGroupPopup->Create())
4650  {
4651  m_usingGroupSelector = true;
4652  m_groupSelected = false;
4653  connect(recGroupPopup, &GroupSelector::result,
4655  connect(recGroupPopup, &MythScreenType::Exiting,
4657  m_popupStack->AddScreen(recGroupPopup);
4658  }
4659  else
4660  delete recGroupPopup;
4661 }
4662 
4664 {
4665  if (m_groupSelected)
4666  return;
4667 
4668  if (m_firstGroup)
4669  Close();
4670 
4671  m_usingGroupSelector = false;
4672 }
4673 
4674 void PlaybackBox::setGroupFilter(const QString &recGroup)
4675 {
4676  QString newRecGroup = recGroup;
4677 
4678  if (newRecGroup.isEmpty())
4679  return;
4680 
4681  m_firstGroup = false;
4682  m_usingGroupSelector = false;
4683 
4684  if (newRecGroup == ProgramInfo::i18n("Default"))
4685  newRecGroup = "Default";
4686  else if (newRecGroup == ProgramInfo::i18n("All Programs"))
4687  newRecGroup = "All Programs";
4688  else if (newRecGroup == ProgramInfo::i18n("LiveTV"))
4689  newRecGroup = "LiveTV";
4690  else if (newRecGroup == ProgramInfo::i18n("Deleted"))
4691  newRecGroup = "Deleted";
4692 
4693  m_recGroup = newRecGroup;
4694 
4696 
4697  // Since the group filter is changing, the current position in the lists
4698  // is meaningless -- so reset the lists so the position won't be saved.
4700  m_groupList->Reset();
4701 
4702  UpdateUILists();
4703 
4704  if (gCoreContext->GetBoolSetting("RememberRecGroup",true))
4705  gCoreContext->SaveSetting("DisplayRecGroup", m_recGroup);
4706 
4707  if (m_recGroupType[m_recGroup] == "recgroup")
4708  gCoreContext->SaveSetting("DisplayRecGroupIsCategory", 0);
4709  else
4710  gCoreContext->SaveSetting("DisplayRecGroupIsCategory", 1);
4711 }
4712 
4713 QString PlaybackBox::getRecGroupPassword(const QString &group)
4714 {
4715  return m_recGroupPwCache.value(group);
4716 }
4717 
4719 {
4720  m_recGroupPwCache.clear();
4721 
4722  MSqlQuery query(MSqlQuery::InitCon());
4723  query.prepare("SELECT recgroup, password FROM recgroups "
4724  "WHERE password IS NOT NULL AND password <> '';");
4725 
4726  if (query.exec())
4727  {
4728  while (query.next())
4729  {
4730  QString recgroup = query.value(0).toString();
4731 
4732  if (recgroup == ProgramInfo::i18n("Default"))
4733  recgroup = "Default";
4734  else if (recgroup == ProgramInfo::i18n("All Programs"))
4735  recgroup = "All Programs";
4736  else if (recgroup == ProgramInfo::i18n("LiveTV"))
4737  recgroup = "LiveTV";
4738  else if (recgroup == ProgramInfo::i18n("Deleted"))
4739  recgroup = "Deleted";
4740 
4741  m_recGroupPwCache.insert(recgroup, query.value(1).toString());
4742  }
4743  }
4744 }
4745 
4747 void PlaybackBox::ShowRecGroupChanger(bool use_playlist)
4748 {
4749  m_opOnPlaylist = use_playlist;
4750 
4751  ProgramInfo *pginfo = nullptr;
4752  if (use_playlist)
4753  {
4754  if (!m_playList.empty())
4755  pginfo = FindProgramInUILists(m_playList[0]);
4756  }
4757  else
4758  pginfo = GetCurrentProgram();
4759 
4760  if (!pginfo)
4761  return;
4762 
4763  MSqlQuery query(MSqlQuery::InitCon());
4764  query.prepare(
4765  "SELECT g.recgroup, COUNT(r.title) FROM recgroups g "
4766  "LEFT JOIN recorded r ON g.recgroupid=r.recgroupid AND r.deletepending = 0 "
4767  "WHERE g.recgroupid != 2 AND g.recgroupid != 3 "
4768  "GROUP BY g.recgroupid ORDER BY g.recgroup");
4769 
4770  QStringList displayNames(tr("Add New"));
4771  QStringList groupNames("addnewgroup");
4772 
4773  if (!query.exec())
4774  return;
4775 
4776  while (query.next())
4777  {
4778  QString dispGroup = query.value(0).toString();
4779  groupNames.push_back(dispGroup);
4780 
4781  if (dispGroup == "Default")
4782  dispGroup = tr("Default");
4783  else if (dispGroup == "LiveTV")
4784  dispGroup = tr("Live TV");
4785  else if (dispGroup == "Deleted")
4786  dispGroup = tr("Deleted");
4787 
4788  displayNames.push_back(tr("%1 [%n item(s)]", "", query.value(1).toInt())
4789  .arg(dispGroup));
4790  }
4791 
4792  QString label = tr("Select Recording Group") +
4793  CreateProgramInfoString(*pginfo);
4794 
4795  auto *rgChanger = new GroupSelector(m_popupStack, label, displayNames,
4796  groupNames, pginfo->GetRecordingGroup());
4797 
4798  if (rgChanger->Create())
4799  {
4800  connect(rgChanger, &GroupSelector::result, this, &PlaybackBox::setRecGroup);
4801  m_popupStack->AddScreen(rgChanger);
4802  }
4803  else
4804  delete rgChanger;
4805 }
4806 
4808 void PlaybackBox::ShowPlayGroupChanger(bool use_playlist)
4809 {
4810  m_opOnPlaylist = use_playlist;
4811 
4812  ProgramInfo *pginfo = nullptr;
4813  if (use_playlist)
4814  {
4815  if (!m_playList.empty())
4816  pginfo = FindProgramInUILists(m_playList[0]);
4817  }
4818  else
4819  pginfo = GetCurrentProgram();
4820 
4821  if (!pginfo)
4822  return;
4823 
4824  QStringList groupNames(tr("Default"));
4825  QStringList displayNames("Default");
4826 
4827  QStringList list = PlayGroup::GetNames();
4828  for (const auto& name : qAsConst(list))
4829  {
4830  displayNames.push_back(name);
4831  groupNames.push_back(name);
4832  }
4833 
4834  QString label = tr("Select Playback Group") +
4835  CreateProgramInfoString(*pginfo);
4836 
4837  auto *pgChanger = new GroupSelector(m_popupStack, label,displayNames,
4838  groupNames, pginfo->GetPlaybackGroup());
4839 
4840  if (pgChanger->Create())
4841  {
4842  connect(pgChanger, &GroupSelector::result,
4843  this, &PlaybackBox::setPlayGroup);
4844  m_popupStack->AddScreen(pgChanger);
4845  }
4846  else
4847  delete pgChanger;
4848 }
4849 
4851 {
4852  QList<uint>::Iterator it;
4853 
4854  for (it = m_playList.begin(); it != m_playList.end(); ++it)
4855  {
4856  ProgramInfo *tmpItem = FindProgramInUILists(*it);
4857  if (tmpItem != nullptr)
4858  {
4859  if (!tmpItem->IsAutoExpirable() && turnOn)
4860  tmpItem->SaveAutoExpire(kNormalAutoExpire, true);
4861  else if (tmpItem->IsAutoExpirable() && !turnOn)
4862  tmpItem->SaveAutoExpire(kDisableAutoExpire, true);
4863  }
4864  }
4865 }
4866 
4868 {
4869  QList<uint>::Iterator it;
4870 
4871  for (it = m_playList.begin(); it != m_playList.end(); ++it)
4872  {
4873  ProgramInfo *tmpItem = FindProgramInUILists(*it);
4874  if (tmpItem != nullptr)
4875  {
4876  tmpItem->SaveWatched(turnOn);
4877  }
4878  }
4879 
4880  doClearPlaylist();
4881  UpdateUILists();
4882 }
4883 
4885 {
4886  ProgramInfo *pgInfo = GetCurrentProgram();
4887 
4889 
4890  auto *editMetadata = new RecMetadataEdit(mainStack, pgInfo);
4891 
4892  if (editMetadata->Create())
4893  {
4894  connect(editMetadata, &RecMetadataEdit::result,
4896  mainStack->AddScreen(editMetadata);
4897  }
4898  else
4899  delete editMetadata;
4900 }
4901 
4902 void PlaybackBox::saveRecMetadata(const QString &newTitle,
4903  const QString &newSubtitle,
4904  const QString &newDescription,
4905  const QString &newInetref,
4906  uint newSeason,
4907  uint newEpisode)
4908 {
4910 
4911  if (!item)
4912  return;
4913 
4914  auto *pginfo = item->GetData().value<ProgramInfo *>();
4915 
4916  if (!pginfo)
4917  return;
4918 
4919  QString groupname = m_groupList->GetItemCurrent()->GetData().toString();
4920 
4921  if (groupname == pginfo->GetTitle().toLower() &&
4922  newTitle != pginfo->GetTitle())
4923  {
4924  m_recordingList->RemoveItem(item);
4925  }
4926  else
4927  {
4928  QString tempSubTitle = newTitle;
4929  if (!newSubtitle.trimmed().isEmpty())
4930  tempSubTitle = QString("%1 - \"%2\"")
4931  .arg(tempSubTitle, newSubtitle);
4932 
4933  QString seasone;
4934  QString seasonx;
4935  QString season;
4936  QString episode;
4937  if (newSeason > 0 || newEpisode > 0)
4938  {
4939  season = format_season_and_episode(newSeason, 1);
4940  episode = format_season_and_episode(newEpisode, 1);
4941  seasone = QString("s%1e%2")
4942  .arg(format_season_and_episode(newSeason, 2),
4943  format_season_and_episode(newEpisode, 2));
4944  seasonx = QString("%1x%2")
4945  .arg(format_season_and_episode(newSeason, 1),
4946  format_season_and_episode(newEpisode, 2));
4947  }
4948 
4949  item->SetText(tempSubTitle, "titlesubtitle");
4950  item->SetText(newTitle, "title");
4951  item->SetText(newSubtitle, "subtitle");
4952  item->SetText(newInetref, "inetref");
4953  item->SetText(seasonx, "00x00");
4954  item->SetText(seasone, "s00e00");
4955  item->SetText(season, "season");
4956  item->SetText(episode, "episode");
4957  if (newDescription != nullptr)
4958  item->SetText(newDescription, "description");
4959  }
4960 
4961  pginfo->SaveInetRef(newInetref);
4962  pginfo->SaveSeasonEpisode(newSeason, newEpisode);
4963 
4964  RecordingInfo ri(*pginfo);
4965  ri.ApplyRecordRecTitleChange(newTitle, newSubtitle, newDescription);
4966  *pginfo = ri;
4967 }
4968 
4969 void PlaybackBox::setRecGroup(QString newRecGroup)
4970 {
4971  newRecGroup = newRecGroup.simplified();
4972 
4973  if (newRecGroup.isEmpty())
4974  return;
4975 
4976  if (newRecGroup == "addnewgroup")
4977  {
4978  MythScreenStack *popupStack =
4979  GetMythMainWindow()->GetStack("popup stack");
4980 
4981  auto *newgroup = new MythTextInputDialog(popupStack,
4982  tr("New Recording Group"));
4983 
4984  connect(newgroup, &MythTextInputDialog::haveResult,
4985  this, &PlaybackBox::setRecGroup);
4986 
4987  if (newgroup->Create())
4988  popupStack->AddScreen(newgroup, false);
4989  else
4990  delete newgroup;
4991  return;
4992  }
4993 
4994  RecordingRule record;
4995  record.LoadTemplate("Default");
4996  AutoExpireType defaultAutoExpire =
4998 
4999  if (m_opOnPlaylist)
5000  {
5001  for (int id : qAsConst(m_playList))
5002  {
5004  if (!p)
5005  continue;
5006 
5007  if ((p->GetRecordingGroup() == "LiveTV") &&
5008  (newRecGroup != "LiveTV"))
5009  {
5010  p->SaveAutoExpire(defaultAutoExpire);
5011  }
5012  else if ((p->GetRecordingGroup() != "LiveTV") &&
5013  (newRecGroup == "LiveTV"))
5014  {
5015  p->SaveAutoExpire(kLiveTVAutoExpire);
5016  }
5017 
5018  RecordingInfo ri(*p);
5019  ri.ApplyRecordRecGroupChange(newRecGroup);
5020  *p = ri;
5021  }
5022  doClearPlaylist();
5023  UpdateUILists();
5024  return;
5025  }
5026 
5028  if (!p)
5029  return;
5030 
5031  if ((p->GetRecordingGroup() == "LiveTV") && (newRecGroup != "LiveTV"))
5032  p->SaveAutoExpire(defaultAutoExpire);
5033  else if ((p->GetRecordingGroup() != "LiveTV") && (newRecGroup == "LiveTV"))
5034  p->SaveAutoExpire(kLiveTVAutoExpire);
5035 
5036  RecordingInfo ri(*p);
5037  ri.ApplyRecordRecGroupChange(newRecGroup);
5038  *p = ri;
5039  UpdateUILists();
5040 }
5041 
5042 void PlaybackBox::setPlayGroup(QString newPlayGroup)
5043 {
5044  ProgramInfo *tmpItem = GetCurrentProgram();
5045 
5046  if (newPlayGroup.isEmpty() || !tmpItem)
5047  return;
5048 
5049  if (newPlayGroup == tr("Default"))
5050  newPlayGroup = "Default";
5051 
5052  if (m_opOnPlaylist)
5053  {
5054  QList<uint>::Iterator it;
5055 
5056  for (it = m_playList.begin(); it != m_playList.end(); ++it )
5057  {
5058  tmpItem = FindProgramInUILists(*it);
5059  if (tmpItem)
5060  {
5061  RecordingInfo ri(*tmpItem);
5062  ri.ApplyRecordPlayGroupChange(newPlayGroup);
5063  *tmpItem = ri;
5064  }
5065  }
5066  doClearPlaylist();
5067  }
5068  else
5069  {
5070  RecordingInfo ri(*tmpItem);
5071  ri.ApplyRecordPlayGroupChange(newPlayGroup);
5072  *tmpItem = ri;
5073  }
5074 }
5075 
5077 {
5079 
5080  if (!item)
5081  return;
5082 
5083  QString currentPassword = getRecGroupPassword(m_recGroup);
5084 
5085  auto *pwChanger = new PasswordChange(m_popupStack, currentPassword);
5086 
5087  if (pwChanger->Create())
5088  {
5089  connect(pwChanger, &PasswordChange::result,
5091  m_popupStack->AddScreen(pwChanger);
5092  }
5093  else
5094  delete pwChanger;
5095 }
5096 
5097 void PlaybackBox::SetRecGroupPassword(const QString &newPassword)
5098 {
5099  MSqlQuery query(MSqlQuery::InitCon());
5100 
5101  query.prepare("UPDATE recgroups SET password = :PASSWD WHERE "
5102  "recgroup = :RECGROUP");
5103  query.bindValue(":RECGROUP", m_recGroup);
5104  query.bindValue(":PASSWD", newPassword);
5105 
5106  if (!query.exec())
5107  MythDB::DBError("PlaybackBox::SetRecGroupPassword",
5108  query);
5109 
5110  if (newPassword.isEmpty())
5111  m_recGroupPwCache.remove(m_recGroup);
5112  else
5113  m_recGroupPwCache.insert(m_recGroup, newPassword);
5114 }
5115 
5117 
5119 {
5120  if (!LoadWindowFromXML("recordings-ui.xml", "groupselector", this))
5121  return false;
5122 
5123  MythUIText *labelText = dynamic_cast<MythUIText*> (GetChild("label"));
5124  MythUIButtonList *groupList = dynamic_cast<MythUIButtonList*>
5125  (GetChild("groups"));
5126 
5127  if (!groupList)
5128  {
5129  LOG(VB_GENERAL, LOG_ERR, LOC +
5130  "Theme is missing 'groups' button list.");
5131  return false;
5132  }
5133 
5134  if (labelText)
5135  labelText->SetText(m_label);
5136 
5137  for (int i = 0; i < m_list.size(); ++i)
5138  {
5139  new MythUIButtonListItem(groupList, m_list.at(i),
5140  QVariant::fromValue(m_data.at(i)));
5141  }
5142 
5143  // Set the current position in the list
5144  groupList->SetValueByData(QVariant::fromValue(m_selected));
5145 
5146  BuildFocusList();
5147 
5148  connect(groupList, &MythUIButtonList::itemClicked,
5149  this, &GroupSelector::AcceptItem);
5150 
5151  return true;
5152 }
5153 
5155 {
5156  if (!item)
5157  return;
5158 
5159  // ignore the dividers
5160  if (item->GetData().toString().isEmpty())
5161  return;
5162 
5163  QString group = item->GetData().toString();
5164  emit result(group);
5165  Close();
5166 }
5167 
5169 
5171 {
5172  if (!LoadWindowFromXML("recordings-ui.xml", "changeview", this))
5173  return false;
5174 
5175  MythUICheckBox *checkBox = dynamic_cast<MythUICheckBox*>(GetChild("titles"));
5176  if (checkBox)
5177  {
5180  connect(checkBox, &MythUICheckBox::toggled,
5182  }
5183 
5184  checkBox = dynamic_cast<MythUICheckBox*>(GetChild("categories"));
5185  if (checkBox)
5186  {
5189  connect(checkBox, &MythUICheckBox::toggled,
5191  }
5192 
5193  checkBox = dynamic_cast<MythUICheckBox*>(GetChild("recgroups"));
5194  if (checkBox)
5195  {
5198  connect(checkBox, &MythUICheckBox::toggled,
5200  }
5201 
5202  // TODO Do we need two separate settings to determine whether the watchlist
5203  // is shown? The filter setting be enough?
5204  checkBox = dynamic_cast<MythUICheckBox*>(GetChild("watchlist"));
5205  if (checkBox)
5206  {
5209  connect(checkBox, &MythUICheckBox::toggled,
5211  }
5212  //
5213 
5214  checkBox = dynamic_cast<MythUICheckBox*>(GetChild("searches"));
5215  if (checkBox)
5216  {
5219  connect(checkBox, &MythUICheckBox::toggled,
5221  }
5222 
5223  // TODO Do we need two separate settings to determine whether livetv
5224  // recordings are shown? Same issue as the watchlist above
5225  checkBox = dynamic_cast<MythUICheckBox*>(GetChild("livetv"));
5226  if (checkBox)
5227  {
5230  connect(checkBox, &MythUICheckBox::toggled,
5232  }
5233  //
5234 
5235  checkBox = dynamic_cast<MythUICheckBox*>(GetChild("watched"));
5236  if (checkBox)
5237  {
5240  connect(checkBox, &MythUICheckBox::toggled,
5242  }
5243 
5244  MythUIButton *savebutton = dynamic_cast<MythUIButton*>(GetChild("save"));
5245  connect(savebutton, &MythUIButton::Clicked, this, &ChangeView::SaveChanges);
5246 
5247  BuildFocusList();
5248 
5249  return true;
5250 }
5251 
5253 {
5254  emit save();
5255  Close();
5256 }
5257 
5259 
5261 {
5262  if (!LoadWindowFromXML("recordings-ui.xml", "passwordchanger", this))
5263  return false;
5264 
5265  m_oldPasswordEdit = dynamic_cast<MythUITextEdit *>(GetChild("oldpassword"));
5266  m_newPasswordEdit = dynamic_cast<MythUITextEdit *>(GetChild("newpassword"));
5267  m_okButton = dynamic_cast<MythUIButton *>(GetChild("ok"));
5268 
5270  {
5271  LOG(VB_GENERAL, LOG_ERR, LOC +
5272  "Window 'passwordchanger' is missing required elements.");
5273  return false;
5274  }
5275 
5278 // if (m_oldPassword.isEmpty())
5279 // m_oldPasswordEdit->SetDisabled(true);
5282 
5283  BuildFocusList();
5284 
5288 
5289  return true;
5290 }
5291 
5293 {
5294  QString newText = m_oldPasswordEdit->GetText();
5295  bool ok = (newText == m_oldPassword);
5296  m_okButton->SetEnabled(ok);
5297 }
5298 
5299 
5301 {
5302  emit result(m_newPasswordEdit->GetText());
5303  Close();
5304 }
5305 
5307 
5309  : MythScreenType(lparent, "recmetadataedit"),
5310  m_progInfo(pginfo)
5311 {
5312  m_popupStack = GetMythMainWindow()->GetStack("popup stack");
5313  m_metadataFactory = new MetadataFactory(this);
5314 }
5315 
5317 {
5318  if (!LoadWindowFromXML("recordings-ui.xml", "editmetadata", this))
5319  return false;
5320 
5321  m_titleEdit = dynamic_cast<MythUITextEdit*>(GetChild("title"));
5322  m_subtitleEdit = dynamic_cast<MythUITextEdit*>(GetChild("subtitle"));
5323  m_descriptionEdit = dynamic_cast<MythUITextEdit*>(GetChild("description"));
5324  m_inetrefEdit = dynamic_cast<MythUITextEdit*>(GetChild("inetref"));
5325  MythUIButton *inetrefClear = dynamic_cast<MythUIButton*>
5326  (GetChild("inetref_clear"));
5327  m_seasonSpin = dynamic_cast<MythUISpinBox*>(GetChild("season"));
5328  m_episodeSpin = dynamic_cast<MythUISpinBox*>(GetChild("episode"));
5329  MythUIButton *okButton = dynamic_cast<MythUIButton*>(GetChild("ok"));
5330  m_queryButton = dynamic_cast<MythUIButton*>(GetChild("query_button"));
5331 
5333  !m_episodeSpin || !okButton)
5334  {
5335  LOG(VB_GENERAL, LOG_ERR, LOC +
5336  "Window 'editmetadata' is missing required elements.");
5337  return false;
5338  }
5339 
5341  m_titleEdit->SetMaxLength(128);
5344  if (m_descriptionEdit)
5345  {
5348  }
5351  m_seasonSpin->SetRange(0,9999,1,5);
5353  m_episodeSpin->SetRange(0,9999,1,10);
5355 
5356  connect(inetrefClear, &MythUIButton::Clicked, this, &RecMetadataEdit::ClearInetref);
5357  connect(okButton, &MythUIButton::Clicked, this, &RecMetadataEdit::SaveChanges);
5358  if (m_queryButton)
5359  {
5361  }
5362 
5363  BuildFocusList();
5364 
5365  return true;
5366 }
5367 
5369 {
5370  m_inetrefEdit->SetText("");
5371 }
5372 
5374 {
5375  QString newRecTitle = m_titleEdit->GetText();
5376  QString newRecSubtitle = m_subtitleEdit->GetText();
5377  QString newRecDescription = nullptr;
5378  QString newRecInetref = nullptr;
5379  uint newRecSeason = 0;
5380  uint newRecEpisode = 0;
5381  if (m_descriptionEdit)
5382  newRecDescription = m_descriptionEdit->GetText();
5383  newRecInetref = m_inetrefEdit->GetText();
5384  newRecSeason = m_seasonSpin->GetIntValue();
5385  newRecEpisode = m_episodeSpin->GetIntValue();
5386 
5387  if (newRecTitle.isEmpty())
5388  return;
5389 
5390  emit result(newRecTitle, newRecSubtitle, newRecDescription,
5391  newRecInetref, newRecSeason, newRecEpisode);
5392  Close();
5393 }
5394 
5396 {
5397  if (m_busyPopup)
5398  return;
5399 
5400  m_busyPopup = new MythUIBusyDialog(tr("Trying to manually find this "
5401  "recording online..."),
5402  m_popupStack,
5403  "metaoptsdialog");
5404 
5405  if (m_busyPopup->Create())
5407 
5408  auto *lookup = new MetadataLookup();
5409  lookup->SetStep(kLookupSearch);
5410  lookup->SetType(kMetadataRecording);
5412 
5413  if (type == kUnknownVideo)
5414  {
5415  if (m_seasonSpin->GetIntValue() == 0 &&
5416  m_episodeSpin->GetIntValue() == 0 &&
5417  m_subtitleEdit->GetText().isEmpty())
5418  {
5419  lookup->SetSubtype(kProbableMovie);
5420  }
5421  else
5422  {
5423  lookup->SetSubtype(kProbableTelevision);
5424  }
5425  }
5426  else
5427  {
5428  // we could determine the type from the inetref
5429  lookup->SetSubtype(type);
5430  }
5431  lookup->SetAllowGeneric(true);
5432  lookup->SetHandleImages(false);
5433  lookup->SetHost(gCoreContext->GetMasterHostName());
5434  lookup->SetTitle(m_titleEdit->GetText());
5435  lookup->SetSubtitle(m_subtitleEdit->GetText());
5436  lookup->SetInetref(m_inetrefEdit->GetText());
5437  lookup->SetCollectionref(m_inetrefEdit->GetText());
5438  lookup->SetSeason(m_seasonSpin->GetIntValue());
5439  lookup->SetEpisode(m_episodeSpin->GetIntValue());
5440  lookup->SetAutomatic(false);
5441 
5442  m_metadataFactory->Lookup(lookup);
5443 }
5444 
5446 {
5447  if (!lookup)
5448  return;
5449 
5450  m_inetrefEdit->SetText(lookup->GetInetref());
5451  m_seasonSpin->SetValue(lookup->GetSeason());
5452  m_episodeSpin->SetValue(lookup->GetEpisode());
5453  if (!lookup->GetSubtitle().isEmpty())
5454  {
5456  }
5457  if (!lookup->GetDescription().isEmpty())
5458  {
5460  }
5461 }
5462 
5464 {
5465  QueryComplete(lookup);
5466 }
5467 
5468 void RecMetadataEdit::customEvent(QEvent *levent)
5469 {
5470  if (levent->type() == MetadataFactoryMultiResult::kEventType)
5471  {
5472  if (m_busyPopup)
5473  {
5474  m_busyPopup->Close();
5475  m_busyPopup = nullptr;
5476  }
5477 
5478  auto *mfmr = dynamic_cast<MetadataFactoryMultiResult*>(levent);
5479 
5480  if (!mfmr)
5481  return;
5482 
5483  MetadataLookupList list = mfmr->m_results;
5484 
5485  auto *resultsdialog = new MetadataResultsDialog(m_popupStack, list);
5486 
5487  connect(resultsdialog, &MetadataResultsDialog::haveResult,
5489  Qt::QueuedConnection);
5490 
5491  if (resultsdialog->Create())
5492  m_popupStack->AddScreen(resultsdialog);
5493  }
5494  else if (levent->type() == MetadataFactorySingleResult::kEventType)
5495  {
5496  if (m_busyPopup)
5497  {
5498  m_busyPopup->Close();
5499  m_busyPopup = nullptr;
5500  }
5501 
5502  auto *mfsr = dynamic_cast<MetadataFactorySingleResult*>(levent);
5503 
5504  if (!mfsr || !mfsr->m_result)
5505  return;
5506 
5507  QueryComplete(mfsr->m_result);
5508  }
5509  else if (levent->type() == MetadataFactoryNoResult::kEventType)
5510  {
5511  if (m_busyPopup)
5512  {
5513  m_busyPopup->Close();
5514  m_busyPopup = nullptr;
5515  }
5516 
5517  auto *mfnr = dynamic_cast<MetadataFactoryNoResult*>(levent);
5518 
5519  if (!mfnr)
5520  return;
5521 
5522  QString title = tr("No match found for this recording. You can "
5523  "try entering a TVDB/TMDB number, season, and "
5524  "episode manually.");
5525 
5526  auto *okPopup = new MythConfirmationDialog(m_popupStack, title, false);
5527 
5528  if (okPopup->Create())
5529  m_popupStack->AddScreen(okPopup);
5530  }
5531 }
5532 
5534 
5536 {
5537  if (!LoadWindowFromXML("recordings-ui.xml", "iconhelp", this))
5538  return false;
5539 
5540  m_iconList = dynamic_cast<MythUIButtonList*>(GetChild("iconlist"));
5541 
5542  if (!m_iconList)
5543  {
5544  LOG(VB_GENERAL, LOG_ERR, LOC +
5545  "Window 'iconhelp' is missing required elements.");
5546  return false;
5547  }
5548 
5549  BuildFocusList();
5550 
5551  addItem("watched", tr("Recording has been watched"));
5552  addItem("commflagged", tr("Commercials are flagged"));
5553  addItem("cutlist", tr("An editing cutlist is present"));
5554  addItem("autoexpire", tr("The program is able to auto-expire"));
5555  addItem("processing", tr("Commercials are being flagged"));
5556  addItem("bookmark", tr("A bookmark is set"));
5557 #if 0
5558  addItem("inuse", tr("Recording is in use"));
5559  addItem("transcoded", tr("Recording has been transcoded"));
5560 #endif
5561 
5562  addItem("mono", tr("Recording is in Mono"));
5563  addItem("stereo", tr("Recording is in Stereo"));
5564  addItem("surround", tr("Recording is in Surround Sound"));
5565  addItem("dolby", tr("Recording is in Dolby Surround Sound"));
5566 
5567  addItem("cc", tr("Recording is Closed Captioned"));
5568  addItem("subtitles", tr("Recording has Subtitles Available"));
5569  addItem("onscreensub", tr("Recording is Subtitled"));
5570 
5571  addItem("SD", tr(