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