2 * Copyright (C) 2012-2019 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
9 #include "PVRGUIDirectory.h"
12 #include "FileItemList.h"
13 #include "ServiceBroker.h"
14 #include "guilib/LocalizeStrings.h"
15 #include "guilib/WindowIDs.h"
16 #include "input/WindowTranslator.h"
17 #include "pvr/PVRManager.h"
18 #include "pvr/addons/PVRClients.h"
19 #include "pvr/channels/PVRChannel.h"
20 #include "pvr/channels/PVRChannelGroupMember.h"
21 #include "pvr/channels/PVRChannelGroups.h"
22 #include "pvr/channels/PVRChannelGroupsContainer.h"
23 #include "pvr/channels/PVRChannelsPath.h"
24 #include "pvr/epg/EpgContainer.h"
25 #include "pvr/epg/EpgSearchFilter.h"
26 #include "pvr/epg/EpgSearchPath.h"
27 #include "pvr/recordings/PVRRecording.h"
28 #include "pvr/recordings/PVRRecordings.h"
29 #include "pvr/recordings/PVRRecordingsPath.h"
30 #include "pvr/timers/PVRTimerInfoTag.h"
31 #include "pvr/timers/PVRTimers.h"
32 #include "pvr/timers/PVRTimersPath.h"
33 #include "settings/Settings.h"
34 #include "settings/SettingsComponent.h"
35 #include "utils/StringUtils.h"
36 #include "utils/URIUtils.h"
37 #include "utils/log.h"
46 bool CPVRGUIDirectory::Exists() const
48 if (!CServiceBroker::GetPVRManager().IsStarted())
51 return m_url
.IsProtocol("pvr") && StringUtils::StartsWith(m_url
.GetFileName(), "recordings");
54 bool CPVRGUIDirectory::SupportsWriteFileOperations() const
56 if (!CServiceBroker::GetPVRManager().IsStarted())
59 const std::string filename
= m_url
.GetFileName();
60 return URIUtils::IsPVRRecording(filename
);
66 bool GetRootDirectory(bool bRadio
, CFileItemList
& results
)
68 std::shared_ptr
<CFileItem
> item
;
70 const std::shared_ptr
<const CPVRClients
> clients
= CServiceBroker::GetPVRManager().Clients();
73 const bool bAnyClientSupportingEPG
= clients
->AnyClientSupportingEPG();
74 if (bAnyClientSupportingEPG
)
76 item
= std::make_shared
<CFileItem
>(
77 StringUtils::Format("pvr://guide/{}/", bRadio
? "radio" : "tv"), true);
78 item
->SetLabel(g_localizeStrings
.Get(19069)); // Guide
79 item
->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio
? WINDOW_RADIO_GUIDE
81 item
->SetArt("icon", "DefaultPVRGuide.png");
86 item
= std::make_shared
<CFileItem
>(
87 bRadio
? CPVRChannelsPath::PATH_RADIO_CHANNELS
: CPVRChannelsPath::PATH_TV_CHANNELS
, true);
88 item
->SetLabel(g_localizeStrings
.Get(19019)); // Channels
89 item
->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio
? WINDOW_RADIO_CHANNELS
90 : WINDOW_TV_CHANNELS
));
91 item
->SetArt("icon", "DefaultPVRChannels.png");
95 if (clients
->AnyClientSupportingRecordings())
97 item
= std::make_shared
<CFileItem
>(bRadio
? CPVRRecordingsPath::PATH_ACTIVE_RADIO_RECORDINGS
98 : CPVRRecordingsPath::PATH_ACTIVE_TV_RECORDINGS
,
100 item
->SetLabel(g_localizeStrings
.Get(19017)); // Recordings
101 item
->SetProperty("node.target", CWindowTranslator::TranslateWindow(
102 bRadio
? WINDOW_RADIO_RECORDINGS
: WINDOW_TV_RECORDINGS
));
103 item
->SetArt("icon", "DefaultPVRRecordings.png");
107 // Timers/Timer rules
108 // - always present, because Reminders are always available, no client support needed for this
109 item
= std::make_shared
<CFileItem
>(
110 bRadio
? CPVRTimersPath::PATH_RADIO_TIMERS
: CPVRTimersPath::PATH_TV_TIMERS
, true);
111 item
->SetLabel(g_localizeStrings
.Get(19040)); // Timers
112 item
->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio
? WINDOW_RADIO_TIMERS
113 : WINDOW_TV_TIMERS
));
114 item
->SetArt("icon", "DefaultPVRTimers.png");
117 item
= std::make_shared
<CFileItem
>(
118 bRadio
? CPVRTimersPath::PATH_RADIO_TIMER_RULES
: CPVRTimersPath::PATH_TV_TIMER_RULES
, true);
119 item
->SetLabel(g_localizeStrings
.Get(19138)); // Timer rules
120 item
->SetProperty("node.target", CWindowTranslator::TranslateWindow(
121 bRadio
? WINDOW_RADIO_TIMER_RULES
: WINDOW_TV_TIMER_RULES
));
122 item
->SetArt("icon", "DefaultPVRTimerRules.png");
126 if (bAnyClientSupportingEPG
)
128 item
= std::make_shared
<CFileItem
>(
129 bRadio
? CPVREpgSearchPath::PATH_RADIO_SEARCH
: CPVREpgSearchPath::PATH_TV_SEARCH
, true);
130 item
->SetLabel(g_localizeStrings
.Get(137)); // Search
131 item
->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio
? WINDOW_RADIO_SEARCH
132 : WINDOW_TV_SEARCH
));
133 item
->SetArt("icon", "DefaultPVRSearch.png");
140 } // unnamed namespace
142 bool CPVRGUIDirectory::GetDirectory(CFileItemList
& results
) const
144 std::string base
= m_url
.Get();
145 URIUtils::RemoveSlashAtEnd(base
);
147 std::string fileName
= m_url
.GetFileName();
148 URIUtils::RemoveSlashAtEnd(fileName
);
150 results
.SetCacheToDisc(CFileItemList::CACHE_NEVER
);
152 if (fileName
.empty())
154 if (CServiceBroker::GetPVRManager().IsStarted())
156 std::shared_ptr
<CFileItem
> item
;
158 item
= std::make_shared
<CFileItem
>(base
+ "channels/", true);
159 item
->SetLabel(g_localizeStrings
.Get(19019)); // Channels
160 item
->SetLabelPreformatted(true);
163 item
= std::make_shared
<CFileItem
>(base
+ "recordings/active/", true);
164 item
->SetLabel(g_localizeStrings
.Get(19017)); // Recordings
165 item
->SetLabelPreformatted(true);
168 item
= std::make_shared
<CFileItem
>(base
+ "recordings/deleted/", true);
169 item
->SetLabel(g_localizeStrings
.Get(19184)); // Deleted recordings
170 item
->SetLabelPreformatted(true);
173 // Sort by name only. Labels are preformatted.
174 results
.AddSortMethod(SortByLabel
, 551 /* Name */, LABEL_MASKS("%L", "", "%L", ""));
178 else if (StringUtils::StartsWith(fileName
, "tv"))
180 if (CServiceBroker::GetPVRManager().IsStarted())
182 return GetRootDirectory(false, results
);
186 else if (StringUtils::StartsWith(fileName
, "radio"))
188 if (CServiceBroker::GetPVRManager().IsStarted())
190 return GetRootDirectory(true, results
);
194 else if (StringUtils::StartsWith(fileName
, "recordings"))
196 if (CServiceBroker::GetPVRManager().IsStarted())
198 return GetRecordingsDirectory(results
);
202 else if (StringUtils::StartsWith(fileName
, "channels"))
204 if (CServiceBroker::GetPVRManager().IsStarted())
206 return GetChannelsDirectory(results
);
210 else if (StringUtils::StartsWith(fileName
, "timers"))
212 if (CServiceBroker::GetPVRManager().IsStarted())
214 return GetTimersDirectory(results
);
219 const CPVREpgSearchPath
path(m_url
.Get());
222 if (CServiceBroker::GetPVRManager().IsStarted())
224 if (path
.IsSavedSearchesRoot())
225 return GetSavedSearchesDirectory(path
.IsRadio(), results
);
233 bool CPVRGUIDirectory::HasTVRecordings()
235 return CServiceBroker::GetPVRManager().IsStarted() &&
236 CServiceBroker::GetPVRManager().Recordings()->GetNumTVRecordings() > 0;
239 bool CPVRGUIDirectory::HasDeletedTVRecordings()
241 return CServiceBroker::GetPVRManager().IsStarted() &&
242 CServiceBroker::GetPVRManager().Recordings()->HasDeletedTVRecordings();
245 bool CPVRGUIDirectory::HasRadioRecordings()
247 return CServiceBroker::GetPVRManager().IsStarted() &&
248 CServiceBroker::GetPVRManager().Recordings()->GetNumRadioRecordings() > 0;
251 bool CPVRGUIDirectory::HasDeletedRadioRecordings()
253 return CServiceBroker::GetPVRManager().IsStarted() &&
254 CServiceBroker::GetPVRManager().Recordings()->HasDeletedRadioRecordings();
260 std::string
TrimSlashes(const std::string
& strOrig
)
262 std::string strReturn
= strOrig
;
263 while (strReturn
[0] == '/')
264 strReturn
.erase(0, 1);
266 URIUtils::RemoveSlashAtEnd(strReturn
);
270 bool IsDirectoryMember(const std::string
& strDirectory
,
271 const std::string
& strEntryDirectory
,
274 const std::string strUseDirectory
= TrimSlashes(strDirectory
);
275 const std::string strUseEntryDirectory
= TrimSlashes(strEntryDirectory
);
277 // Case-insensitive comparison since sub folders are created with case-insensitive matching (GetSubDirectories)
279 return StringUtils::EqualsNoCase(strUseDirectory
, strUseEntryDirectory
);
281 return StringUtils::StartsWithNoCase(strUseEntryDirectory
, strUseDirectory
);
284 void GetSubDirectories(const CPVRRecordingsPath
& recParentPath
,
285 const std::vector
<std::shared_ptr
<CPVRRecording
>>& recordings
,
286 CFileItemList
& results
)
288 // Only active recordings are fetched to provide sub directories.
289 // Not applicable for deleted view which is supposed to be flattened.
290 std::set
<std::shared_ptr
<CFileItem
>> unwatchedFolders
;
291 bool bRadio
= recParentPath
.IsRadio();
293 for (const auto& recording
: recordings
)
295 if (recording
->IsDeleted())
298 if (recording
->IsRadio() != bRadio
)
301 const std::string strCurrent
=
302 recParentPath
.GetUnescapedSubDirectoryPath(recording
->Directory());
303 if (strCurrent
.empty())
306 CPVRRecordingsPath
recChildPath(recParentPath
);
307 recChildPath
.AppendSegment(strCurrent
);
308 const std::string strFilePath
= recChildPath
;
310 std::shared_ptr
<CFileItem
> item
;
311 if (!results
.Contains(strFilePath
))
313 item
= std::make_shared
<CFileItem
>(strCurrent
, true);
314 item
->SetPath(strFilePath
);
315 item
->SetLabel(strCurrent
);
316 item
->SetLabelPreformatted(true);
317 item
->m_dateTime
= recording
->RecordingTimeAsLocalTime();
318 item
->SetProperty("totalepisodes", 0);
319 item
->SetProperty("watchedepisodes", 0);
320 item
->SetProperty("unwatchedepisodes", 0);
321 item
->SetProperty("inprogressepisodes", 0);
322 item
->SetProperty("sizeinbytes", UINT64_C(0));
324 // Assume all folders are watched, we'll change the overlay later
325 item
->SetOverlayImage(CGUIListItem::ICON_OVERLAY_WATCHED
);
330 item
= results
.Get(strFilePath
);
331 if (item
->m_dateTime
< recording
->RecordingTimeAsLocalTime())
332 item
->m_dateTime
= recording
->RecordingTimeAsLocalTime();
335 item
->IncrementProperty("totalepisodes", 1);
336 if (recording
->GetPlayCount() == 0)
338 unwatchedFolders
.insert(item
);
339 item
->IncrementProperty("unwatchedepisodes", 1);
343 item
->IncrementProperty("watchedepisodes", 1);
345 if (recording
->GetResumePoint().IsPartWay())
347 item
->IncrementProperty("inprogressepisodes", 1);
349 item
->IncrementProperty("sizeinbytes", recording
->GetSizeInBytes());
352 // Replace the incremental size of the recordings with a string equivalent
353 for (auto& item
: results
.GetList())
355 int64_t size
= item
->GetProperty("sizeinbytes").asInteger();
356 item
->ClearProperty("sizeinbytes");
357 item
->m_dwSize
= size
; // We'll also sort recording folders by size
359 item
->SetProperty("recordingsize", StringUtils::SizeToString(size
));
362 // Change the watched overlay to unwatched for folders containing unwatched entries
363 for (auto& item
: unwatchedFolders
)
364 item
->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED
);
367 } // unnamed namespace
369 bool CPVRGUIDirectory::GetRecordingsDirectory(CFileItemList
& results
) const
371 results
.SetContent("recordings");
373 bool bGrouped
= false;
374 const std::vector
<std::shared_ptr
<CPVRRecording
>> recordings
=
375 CServiceBroker::GetPVRManager().Recordings()->GetAll();
377 if (m_url
.HasOption("view"))
379 const std::string view
= m_url
.GetOption("view");
380 if (view
== "grouped")
382 else if (view
== "flat")
386 CLog::LogF(LOGERROR
, "Unsupported value '{}' for url parameter 'view'", view
);
392 bGrouped
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
393 CSettings::SETTING_PVRRECORD_GROUPRECORDINGS
);
396 const CPVRRecordingsPath
recPath(m_url
.GetWithoutOptions());
397 if (recPath
.IsValid())
399 // Get the directory structure if in non-flatten mode
400 // Deleted view is always flatten. So only for an active view
401 const std::string strDirectory
= recPath
.GetUnescapedDirectoryPath();
402 if (!recPath
.IsDeleted() && bGrouped
)
403 GetSubDirectories(recPath
, recordings
, results
);
405 // get all files of the current directory or recursively all files starting at the current directory if in flatten mode
406 std::shared_ptr
<CFileItem
> item
;
407 for (const auto& recording
: recordings
)
409 // Omit recordings not matching criteria
410 if (recording
->IsDeleted() != recPath
.IsDeleted() ||
411 recording
->IsRadio() != recPath
.IsRadio() ||
412 !IsDirectoryMember(strDirectory
, recording
->Directory(), bGrouped
))
415 item
= std::make_shared
<CFileItem
>(recording
);
416 item
->SetOverlayImage(recording
->GetPlayCount() > 0 ? CGUIListItem::ICON_OVERLAY_WATCHED
417 : CGUIListItem::ICON_OVERLAY_UNWATCHED
);
422 return recPath
.IsValid();
425 bool CPVRGUIDirectory::GetSavedSearchesDirectory(bool bRadio
, CFileItemList
& results
) const
427 const std::vector
<std::shared_ptr
<CPVREpgSearchFilter
>> searches
=
428 CServiceBroker::GetPVRManager().EpgContainer().GetSavedSearches(bRadio
);
430 for (const auto& search
: searches
)
432 results
.Add(std::make_shared
<CFileItem
>(search
));
437 bool CPVRGUIDirectory::GetChannelGroupsDirectory(bool bRadio
,
439 CFileItemList
& results
)
441 const CPVRChannelGroups
* channelGroups
=
442 CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio
);
445 std::shared_ptr
<CFileItem
> item
;
446 const std::vector
<std::shared_ptr
<CPVRChannelGroup
>> groups
=
447 channelGroups
->GetMembers(bExcludeHidden
);
448 for (const auto& group
: groups
)
450 item
= std::make_shared
<CFileItem
>(group
->GetPath(), true);
451 item
->m_strTitle
= group
->GroupName();
452 item
->SetLabel(group
->GroupName());
462 std::shared_ptr
<CPVRChannelGroupMember
> GetLastWatchedChannelGroupMember(
463 const std::shared_ptr
<CPVRChannel
>& channel
)
465 const int lastGroupId
{channel
->LastWatchedGroupId()};
466 if (lastGroupId
!= PVR_GROUP_ID_UNNKOWN
)
468 const std::shared_ptr
<const CPVRChannelGroup
> lastGroup
{
469 CServiceBroker::GetPVRManager().ChannelGroups()->GetByIdFromAll(lastGroupId
)};
470 if (lastGroup
&& !lastGroup
->IsHidden() && !lastGroup
->IsDeleted())
471 return lastGroup
->GetByUniqueID(channel
->StorageId());
476 std::shared_ptr
<CPVRChannelGroupMember
> GetFirstMatchingGroupMember(
477 const std::shared_ptr
<CPVRChannel
>& channel
)
479 CPVRChannelGroups
* groups
{
480 CServiceBroker::GetPVRManager().ChannelGroups()->Get(channel
->IsRadio())};
483 const std::vector
<std::shared_ptr
<CPVRChannelGroup
>> channelGroups
{
484 groups
->GetMembers(true /* exclude hidden */)};
486 for (const auto& channelGroup
: channelGroups
)
488 if (channelGroup
->IsDeleted())
491 std::shared_ptr
<CPVRChannelGroupMember
> groupMember
{
492 channelGroup
->GetByUniqueID(channel
->StorageId())};
500 std::vector
<std::shared_ptr
<CPVRChannelGroupMember
>> GetChannelGroupMembers(
501 const CPVRChannelsPath
& path
)
503 const std::string
& groupName
{path
.GetGroupName()};
505 std::shared_ptr
<CPVRChannelGroup
> group
;
506 if (path
.IsHiddenChannelGroup()) // hidden channels from the 'all channels' group
508 group
= CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(path
.IsRadio());
510 else if (groupName
== "*") // all channels across all groups
512 group
= CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(path
.IsRadio());
515 std::vector
<std::shared_ptr
<CPVRChannelGroupMember
>> result
;
517 const std::vector
<std::shared_ptr
<CPVRChannelGroupMember
>> allGroupMembers
{
518 group
->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE
)};
519 for (const auto& allGroupMember
: allGroupMembers
)
521 std::shared_ptr
<CPVRChannelGroupMember
> member
{
522 GetLastWatchedChannelGroupMember(allGroupMember
->Channel())};
525 result
.emplace_back(member
);
526 continue; // Process next 'All channels' group member.
529 if (group
->IsHidden())
531 // Very special case. 'All channels' group is hidden. Let's see what we get iterating all
532 // non-hidden / non-deleted groups. We must not return any 'All channels' group members,
533 // because their path is invalid (it contains the group).
534 member
= GetFirstMatchingGroupMember(allGroupMember
->Channel());
536 result
.emplace_back(member
);
540 // Use the 'All channels' group member.
541 result
.emplace_back(allGroupMember
);
549 group
= CServiceBroker::GetPVRManager()
551 ->Get(path
.IsRadio())
552 ->GetByName(groupName
, path
.GetGroupClientID());
556 return group
->GetMembers(CPVRChannelGroup::Include::ALL
);
558 CLog::LogF(LOGERROR
, "Unable to obtain members for channel group '{}'", groupName
);
561 } // unnamed namespace
563 bool CPVRGUIDirectory::GetChannelsDirectory(CFileItemList
& results
) const
565 const CPVRChannelsPath
path(m_url
.GetWithoutOptions());
570 std::shared_ptr
<CFileItem
> item
;
573 item
= std::make_shared
<CFileItem
>(CPVRChannelsPath::PATH_TV_CHANNELS
, true);
574 item
->SetLabel(g_localizeStrings
.Get(19020)); // TV
575 item
->SetLabelPreformatted(true);
578 // all radio channels
579 item
= std::make_shared
<CFileItem
>(CPVRChannelsPath::PATH_RADIO_CHANNELS
, true);
580 item
->SetLabel(g_localizeStrings
.Get(19021)); // Radio
581 item
->SetLabelPreformatted(true);
586 else if (path
.IsChannelsRoot())
588 return GetChannelGroupsDirectory(path
.IsRadio(), true, results
);
590 else if (path
.IsChannelGroup())
592 const bool playedOnly
{(m_url
.HasOption("view") && (m_url
.GetOption("view") == "lastplayed"))};
593 const bool showHiddenChannels
{path
.IsHiddenChannelGroup()};
594 const std::vector
<std::shared_ptr
<CPVRChannelGroupMember
>> groupMembers
{
595 GetChannelGroupMembers(path
)};
596 for (const auto& groupMember
: groupMembers
)
598 if (showHiddenChannels
!= groupMember
->Channel()->IsHidden())
601 if (playedOnly
&& !groupMember
->Channel()->LastWatched())
604 results
.Add(std::make_shared
<CFileItem
>(groupMember
));
615 bool GetTimersRootDirectory(const CPVRTimersPath
& path
,
617 const std::vector
<std::shared_ptr
<CPVRTimerInfoTag
>>& timers
,
618 CFileItemList
& results
)
620 bool bRadio
= path
.IsRadio();
621 bool bRules
= path
.IsRules();
623 for (const auto& timer
: timers
)
625 if ((bRadio
== timer
->IsRadio() ||
626 (bRules
&& timer
->ClientChannelUID() == PVR_TIMER_ANY_CHANNEL
)) &&
627 (bRules
== timer
->IsTimerRule()) && (!bHideDisabled
|| !timer
->IsDisabled()))
629 const auto item
= std::make_shared
<CFileItem
>(timer
);
630 const CPVRTimersPath
timersPath(path
.GetPath(), timer
->ClientID(), timer
->ClientIndex());
631 item
->SetPath(timersPath
.GetPath());
638 bool GetTimersSubDirectory(const CPVRTimersPath
& path
,
640 const std::vector
<std::shared_ptr
<CPVRTimerInfoTag
>>& timers
,
641 CFileItemList
& results
)
643 bool bRadio
= path
.IsRadio();
644 int iParentId
= path
.GetParentId();
645 int iClientId
= path
.GetClientId();
647 std::shared_ptr
<CFileItem
> item
;
649 for (const auto& timer
: timers
)
651 if ((timer
->IsRadio() == bRadio
) && timer
->HasParent() && (timer
->ClientID() == iClientId
) &&
652 (timer
->ParentClientIndex() == iParentId
) && (!bHideDisabled
|| !timer
->IsDisabled()))
654 item
= std::make_shared
<CFileItem
>(timer
);
655 const CPVRTimersPath
timersPath(path
.GetPath(), timer
->ClientID(), timer
->ClientIndex());
656 item
->SetPath(timersPath
.GetPath());
663 } // unnamed namespace
665 bool CPVRGUIDirectory::GetTimersDirectory(CFileItemList
& results
) const
667 const CPVRTimersPath
path(m_url
.GetWithoutOptions());
668 if (path
.IsValid() && (path
.IsTimersRoot() || path
.IsTimerRule()))
670 bool bHideDisabled
= false;
671 if (m_url
.HasOption("view"))
673 const std::string view
= m_url
.GetOption("view");
674 if (view
== "hidedisabled")
676 bHideDisabled
= true;
680 CLog::LogF(LOGERROR
, "Unsupported value '{}' for url parameter 'view'", view
);
686 bHideDisabled
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
687 CSettings::SETTING_PVRTIMERS_HIDEDISABLEDTIMERS
);
690 const std::vector
<std::shared_ptr
<CPVRTimerInfoTag
>> timers
=
691 CServiceBroker::GetPVRManager().Timers()->GetAll();
693 if (path
.IsTimersRoot())
695 /* Root folder containing either timer rules or timers. */
696 return GetTimersRootDirectory(path
, bHideDisabled
, timers
, results
);
698 else if (path
.IsTimerRule())
700 /* Sub folder containing the timers scheduled by the given timer rule. */
701 return GetTimersSubDirectory(path
, bHideDisabled
, timers
, results
);