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