summaryrefslogtreecommitdiffstats
path: root/xbmc/pvr
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/pvr')
-rw-r--r--xbmc/pvr/CMakeLists.txt30
-rw-r--r--xbmc/pvr/IPVRComponent.h18
-rw-r--r--xbmc/pvr/PVRCachedImage.cpp71
-rw-r--r--xbmc/pvr/PVRCachedImage.h43
-rw-r--r--xbmc/pvr/PVRCachedImages.cpp94
-rw-r--r--xbmc/pvr/PVRCachedImages.h49
-rw-r--r--xbmc/pvr/PVRChannelNumberInputHandler.cpp190
-rw-r--r--xbmc/pvr/PVRChannelNumberInputHandler.h118
-rw-r--r--xbmc/pvr/PVRComponentRegistration.cpp52
-rw-r--r--xbmc/pvr/PVRComponentRegistration.h22
-rw-r--r--xbmc/pvr/PVRContextMenus.cpp780
-rw-r--r--xbmc/pvr/PVRContextMenus.h65
-rw-r--r--xbmc/pvr/PVRDatabase.cpp1141
-rw-r--r--xbmc/pvr/PVRDatabase.h320
-rw-r--r--xbmc/pvr/PVREdl.cpp68
-rw-r--r--xbmc/pvr/PVREdl.h34
-rw-r--r--xbmc/pvr/PVREventLogJob.cpp56
-rw-r--r--xbmc/pvr/PVREventLogJob.h62
-rw-r--r--xbmc/pvr/PVRItem.cpp167
-rw-r--r--xbmc/pvr/PVRItem.h41
-rw-r--r--xbmc/pvr/PVRManager.cpp1029
-rw-r--r--xbmc/pvr/PVRManager.h482
-rw-r--r--xbmc/pvr/PVRPlaybackState.cpp566
-rw-r--r--xbmc/pvr/PVRPlaybackState.h284
-rw-r--r--xbmc/pvr/PVRStreamProperties.cpp40
-rw-r--r--xbmc/pvr/PVRStreamProperties.h43
-rw-r--r--xbmc/pvr/PVRThumbLoader.cpp136
-rw-r--r--xbmc/pvr/PVRThumbLoader.h41
-rw-r--r--xbmc/pvr/addons/CMakeLists.txt13
-rw-r--r--xbmc/pvr/addons/PVRClient.cpp2023
-rw-r--r--xbmc/pvr/addons/PVRClient.h1062
-rw-r--r--xbmc/pvr/addons/PVRClientCapabilities.cpp85
-rw-r--r--xbmc/pvr/addons/PVRClientCapabilities.h277
-rw-r--r--xbmc/pvr/addons/PVRClientMenuHooks.cpp185
-rw-r--r--xbmc/pvr/addons/PVRClientMenuHooks.h73
-rw-r--r--xbmc/pvr/addons/PVRClientUID.cpp33
-rw-r--r--xbmc/pvr/addons/PVRClientUID.h42
-rw-r--r--xbmc/pvr/addons/PVRClients.cpp977
-rw-r--r--xbmc/pvr/addons/PVRClients.h485
-rw-r--r--xbmc/pvr/channels/CMakeLists.txt23
-rw-r--r--xbmc/pvr/channels/PVRChannel.cpp843
-rw-r--r--xbmc/pvr/channels/PVRChannel.h550
-rw-r--r--xbmc/pvr/channels/PVRChannelGroup.cpp1176
-rw-r--r--xbmc/pvr/channels/PVRChannelGroup.h548
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupInternal.cpp245
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupInternal.h116
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupMember.cpp137
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupMember.h101
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupSettings.cpp120
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupSettings.h62
-rw-r--r--xbmc/pvr/channels/PVRChannelGroups.cpp621
-rw-r--r--xbmc/pvr/channels/PVRChannelGroups.h244
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupsContainer.cpp167
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupsContainer.h175
-rw-r--r--xbmc/pvr/channels/PVRChannelNumber.cpp36
-rw-r--r--xbmc/pvr/channels/PVRChannelNumber.h88
-rw-r--r--xbmc/pvr/channels/PVRChannelsPath.cpp190
-rw-r--r--xbmc/pvr/channels/PVRChannelsPath.h73
-rw-r--r--xbmc/pvr/channels/PVRRadioRDSInfoTag.cpp736
-rw-r--r--xbmc/pvr/channels/PVRRadioRDSInfoTag.h208
-rw-r--r--xbmc/pvr/channels/test/CMakeLists.txt4
-rw-r--r--xbmc/pvr/channels/test/TestPVRChannelsPath.cpp404
-rw-r--r--xbmc/pvr/dialogs/CMakeLists.txt29
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.cpp76
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.h34
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRChannelManager.cpp1076
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRChannelManager.h95
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.cpp299
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.h61
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.cpp102
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.h41
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGroupManager.cpp546
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGroupManager.h75
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGuideControls.cpp19
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGuideControls.h21
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.cpp255
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.h44
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.cpp391
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.h64
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.cpp154
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.h47
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.cpp216
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.h60
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.cpp102
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.h37
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.cpp235
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.h58
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.cpp1513
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.h196
-rw-r--r--xbmc/pvr/epg/CMakeLists.txt22
-rw-r--r--xbmc/pvr/epg/Epg.cpp554
-rw-r--r--xbmc/pvr/epg/Epg.h327
-rw-r--r--xbmc/pvr/epg/EpgChannelData.cpp107
-rw-r--r--xbmc/pvr/epg/EpgChannelData.h59
-rw-r--r--xbmc/pvr/epg/EpgContainer.cpp1028
-rw-r--r--xbmc/pvr/epg/EpgContainer.h360
-rw-r--r--xbmc/pvr/epg/EpgDatabase.cpp1494
-rw-r--r--xbmc/pvr/epg/EpgDatabase.h377
-rw-r--r--xbmc/pvr/epg/EpgInfoTag.cpp681
-rw-r--r--xbmc/pvr/epg/EpgInfoTag.h513
-rw-r--r--xbmc/pvr/epg/EpgSearchData.h44
-rw-r--r--xbmc/pvr/epg/EpgSearchFilter.cpp403
-rw-r--r--xbmc/pvr/epg/EpgSearchFilter.h171
-rw-r--r--xbmc/pvr/epg/EpgSearchPath.cpp64
-rw-r--r--xbmc/pvr/epg/EpgSearchPath.h49
-rw-r--r--xbmc/pvr/epg/EpgTagsCache.cpp179
-rw-r--r--xbmc/pvr/epg/EpgTagsCache.h61
-rw-r--r--xbmc/pvr/epg/EpgTagsContainer.cpp668
-rw-r--r--xbmc/pvr/epg/EpgTagsContainer.h227
-rw-r--r--xbmc/pvr/filesystem/CMakeLists.txt5
-rw-r--r--xbmc/pvr/filesystem/PVRGUIDirectory.cpp625
-rw-r--r--xbmc/pvr/filesystem/PVRGUIDirectory.h107
-rw-r--r--xbmc/pvr/guilib/CMakeLists.txt35
-rw-r--r--xbmc/pvr/guilib/GUIEPGGridContainer.cpp2549
-rw-r--r--xbmc/pvr/guilib/GUIEPGGridContainer.dox245
-rw-r--r--xbmc/pvr/guilib/GUIEPGGridContainer.h274
-rw-r--r--xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp723
-rw-r--r--xbmc/pvr/guilib/GUIEPGGridContainerModel.h178
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionListener.cpp420
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionListener.h46
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsChannels.cpp424
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsChannels.h182
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsClients.cpp88
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsClients.h38
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp341
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsDatabase.h40
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsEPG.cpp217
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsEPG.h84
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp74
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsParentalControl.h59
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp564
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsPlayback.h150
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp190
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h55
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp361
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsRecordings.h114
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsTimers.cpp1009
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsTimers.h234
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsUtils.cpp36
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsUtils.h40
-rw-r--r--xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp101
-rw-r--r--xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h46
-rw-r--r--xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp373
-rw-r--r--xbmc/pvr/guilib/PVRGUIChannelNavigator.h189
-rw-r--r--xbmc/pvr/guilib/PVRGUIProgressHandler.cpp97
-rw-r--r--xbmc/pvr/guilib/PVRGUIProgressHandler.h59
-rw-r--r--xbmc/pvr/guilib/guiinfo/CMakeLists.txt9
-rw-r--r--xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp2017
-rw-r--r--xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h217
-rw-r--r--xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp270
-rw-r--r--xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h111
-rw-r--r--xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp424
-rw-r--r--xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h87
-rw-r--r--xbmc/pvr/providers/CMakeLists.txt7
-rw-r--r--xbmc/pvr/providers/PVRProvider.cpp393
-rw-r--r--xbmc/pvr/providers/PVRProvider.h245
-rw-r--r--xbmc/pvr/providers/PVRProviders.cpp380
-rw-r--r--xbmc/pvr/providers/PVRProviders.h139
-rw-r--r--xbmc/pvr/recordings/CMakeLists.txt9
-rw-r--r--xbmc/pvr/recordings/PVRRecording.cpp730
-rw-r--r--xbmc/pvr/recordings/PVRRecording.h541
-rw-r--r--xbmc/pvr/recordings/PVRRecordings.cpp361
-rw-r--r--xbmc/pvr/recordings/PVRRecordings.h154
-rw-r--r--xbmc/pvr/recordings/PVRRecordingsPath.cpp258
-rw-r--r--xbmc/pvr/recordings/PVRRecordingsPath.h68
-rw-r--r--xbmc/pvr/settings/CMakeLists.txt5
-rw-r--r--xbmc/pvr/settings/PVRSettings.cpp232
-rw-r--r--xbmc/pvr/settings/PVRSettings.h76
-rw-r--r--xbmc/pvr/timers/CMakeLists.txt13
-rw-r--r--xbmc/pvr/timers/PVRTimerInfoTag.cpp1389
-rw-r--r--xbmc/pvr/timers/PVRTimerInfoTag.h644
-rw-r--r--xbmc/pvr/timers/PVRTimerRuleMatcher.cpp193
-rw-r--r--xbmc/pvr/timers/PVRTimerRuleMatcher.h47
-rw-r--r--xbmc/pvr/timers/PVRTimerType.cpp418
-rw-r--r--xbmc/pvr/timers/PVRTimerType.h411
-rw-r--r--xbmc/pvr/timers/PVRTimers.cpp1338
-rw-r--r--xbmc/pvr/timers/PVRTimers.h331
-rw-r--r--xbmc/pvr/timers/PVRTimersPath.cpp82
-rw-r--r--xbmc/pvr/timers/PVRTimersPath.h50
-rw-r--r--xbmc/pvr/windows/CMakeLists.txt21
-rw-r--r--xbmc/pvr/windows/GUIViewStatePVR.cpp182
-rw-r--r--xbmc/pvr/windows/GUIViewStatePVR.h78
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRBase.cpp563
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRBase.h138
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRChannels.cpp414
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRChannels.h65
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRGuide.cpp973
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRGuide.h129
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRRecordings.cpp440
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRRecordings.h71
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRSearch.cpp544
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRSearch.h90
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRTimerRules.cpp47
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRTimerRules.h38
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRTimers.cpp47
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRTimers.h36
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRTimersBase.cpp204
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRTimersBase.h36
198 files changed, 57535 insertions, 0 deletions
diff --git a/xbmc/pvr/CMakeLists.txt b/xbmc/pvr/CMakeLists.txt
new file mode 100644
index 0000000..e7a95a6
--- /dev/null
+++ b/xbmc/pvr/CMakeLists.txt
@@ -0,0 +1,30 @@
+set(SOURCES PVRCachedImage.cpp
+ PVRCachedImages.cpp
+ PVRChannelNumberInputHandler.cpp
+ PVRComponentRegistration.cpp
+ PVRContextMenus.cpp
+ PVRDatabase.cpp
+ PVREdl.cpp
+ PVREventLogJob.cpp
+ PVRItem.cpp
+ PVRManager.cpp
+ PVRPlaybackState.cpp
+ PVRStreamProperties.cpp
+ PVRThumbLoader.cpp)
+
+set(HEADERS IPVRComponent.h
+ PVRCachedImage.h
+ PVRCachedImages.h
+ PVRChannelNumberInputHandler.h
+ PVRComponentRegistration.h
+ PVRContextMenus.h
+ PVRDatabase.h
+ PVREdl.h
+ PVREventLogJob.h
+ PVRItem.h
+ PVRManager.h
+ PVRPlaybackState.h
+ PVRStreamProperties.h
+ PVRThumbLoader.h)
+
+core_add_library(pvr)
diff --git a/xbmc/pvr/IPVRComponent.h b/xbmc/pvr/IPVRComponent.h
new file mode 100644
index 0000000..8695095
--- /dev/null
+++ b/xbmc/pvr/IPVRComponent.h
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2022 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+namespace PVR
+{
+class IPVRComponent
+{
+public:
+ virtual ~IPVRComponent() = default;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/PVRCachedImage.cpp b/xbmc/pvr/PVRCachedImage.cpp
new file mode 100644
index 0000000..e336563
--- /dev/null
+++ b/xbmc/pvr/PVRCachedImage.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2005-2021 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRCachedImage.h"
+
+#include "TextureDatabase.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+using namespace PVR;
+
+CPVRCachedImage::CPVRCachedImage(const std::string& owner) : m_owner(owner)
+{
+}
+
+CPVRCachedImage::CPVRCachedImage(const std::string& clientImage, const std::string& owner)
+ : m_clientImage(clientImage), m_owner(owner)
+{
+ UpdateLocalImage();
+}
+
+bool CPVRCachedImage::operator==(const CPVRCachedImage& right) const
+{
+ return (this == &right) || (m_clientImage == right.m_clientImage &&
+ m_localImage == right.m_localImage && m_owner == right.m_owner);
+}
+
+bool CPVRCachedImage::operator!=(const CPVRCachedImage& right) const
+{
+ return !(*this == right);
+}
+
+void CPVRCachedImage::SetClientImage(const std::string& image)
+{
+ if (StringUtils::StartsWith(image, "image://"))
+ {
+ CLog::LogF(LOGERROR, "Not allowed to call this method with an image URL");
+ return;
+ }
+
+ if (m_owner.empty())
+ {
+ CLog::LogF(LOGERROR, "Empty owner");
+ return;
+ }
+
+ m_clientImage = image;
+ UpdateLocalImage();
+}
+
+void CPVRCachedImage::SetOwner(const std::string& owner)
+{
+ if (m_owner != owner)
+ {
+ m_owner = owner;
+ UpdateLocalImage();
+ }
+}
+
+void CPVRCachedImage::UpdateLocalImage()
+{
+ if (m_clientImage.empty())
+ m_localImage.clear();
+ else
+ m_localImage = CTextureUtils::GetWrappedImageURL(m_clientImage, m_owner);
+}
diff --git a/xbmc/pvr/PVRCachedImage.h b/xbmc/pvr/PVRCachedImage.h
new file mode 100644
index 0000000..c8f0210
--- /dev/null
+++ b/xbmc/pvr/PVRCachedImage.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2005-2021 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace PVR
+{
+
+class CPVRCachedImage
+{
+public:
+ CPVRCachedImage() = delete;
+ virtual ~CPVRCachedImage() = default;
+
+ explicit CPVRCachedImage(const std::string& owner);
+ CPVRCachedImage(const std::string& clientImage, const std::string& owner);
+
+ bool operator==(const CPVRCachedImage& right) const;
+ bool operator!=(const CPVRCachedImage& right) const;
+
+ const std::string& GetClientImage() const { return m_clientImage; }
+ const std::string& GetLocalImage() const { return m_localImage; }
+
+ void SetClientImage(const std::string& image);
+
+ void SetOwner(const std::string& owner);
+
+private:
+ void UpdateLocalImage();
+
+ std::string m_clientImage;
+ std::string m_localImage;
+ std::string m_owner;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRCachedImages.cpp b/xbmc/pvr/PVRCachedImages.cpp
new file mode 100644
index 0000000..ea17fd7
--- /dev/null
+++ b/xbmc/pvr/PVRCachedImages.cpp
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2005-2021 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRCachedImages.h"
+
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "TextureDatabase.h"
+#include "URL.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace PVR;
+
+int CPVRCachedImages::Cleanup(const std::vector<PVRImagePattern>& urlPatterns,
+ const std::vector<std::string>& urlsToCheck,
+ bool clearTextureForPath /* = false */)
+{
+ int iCleanedImages = 0;
+
+ if (urlPatterns.empty())
+ {
+ CLog::LogFC(LOGERROR, LOGPVR, "No URL patterns given");
+ return iCleanedImages;
+ }
+
+ CTextureDatabase db;
+ if (!db.Open())
+ {
+ CLog::LogFC(LOGERROR, LOGPVR, "Failed to open texture database");
+ return iCleanedImages;
+ }
+
+ CDatabase::Filter filter;
+
+ for (const auto& pattern : urlPatterns)
+ {
+ const std::string encodedPattern =
+ StringUtils::Format("{}@{}", pattern.owner, CURL::Encode(pattern.path));
+
+ std::string escapedPattern;
+ for (size_t i = 0; i < encodedPattern.size(); ++i)
+ {
+ if (encodedPattern[i] == '%' || encodedPattern[i] == '^')
+ escapedPattern += '^';
+
+ escapedPattern += encodedPattern[i];
+ }
+
+ const std::string where =
+ StringUtils::Format("url LIKE 'image://{}%' ESCAPE '^'", escapedPattern);
+ filter.AppendWhere(where, false); // logical OR
+ }
+
+ CVariant items;
+ if (!db.GetTextures(items, filter))
+ {
+ CLog::LogFC(LOGERROR, LOGPVR, "Failed to get items from texture database");
+ return iCleanedImages;
+ }
+
+ for (unsigned int i = 0; i < items.size(); ++i)
+ {
+ // Unwrap the image:// URL returned from texture db.
+ const std::string textureURL = UnwrapImageURL(items[i]["url"].asString());
+
+ if (std::none_of(urlsToCheck.cbegin(), urlsToCheck.cend(),
+ [&textureURL](const std::string& url) { return url == textureURL; }))
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Removing stale cached image: '{}'", textureURL);
+ CServiceBroker::GetTextureCache()->ClearCachedImage(items[i]["textureid"].asInteger());
+
+ if (clearTextureForPath)
+ db.ClearTextureForPath(textureURL, "thumb");
+
+ iCleanedImages++;
+ }
+ }
+
+ return iCleanedImages;
+}
+
+std::string CPVRCachedImages::UnwrapImageURL(const std::string& url)
+{
+ return StringUtils::StartsWith(url, "image://") ? CURL(url).GetHostName() : url;
+}
diff --git a/xbmc/pvr/PVRCachedImages.h b/xbmc/pvr/PVRCachedImages.h
new file mode 100644
index 0000000..9dcdd24
--- /dev/null
+++ b/xbmc/pvr/PVRCachedImages.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2005-2021 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+
+struct PVRImagePattern
+{
+ PVRImagePattern(const std::string& _owner, const std::string& _path) : owner(_owner), path(_path)
+ {
+ }
+
+ std::string owner;
+ std::string path;
+};
+
+class CPVRCachedImages
+{
+public:
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @param urlPatterns The URL patterns to fetch from texture database.
+ * @param urlsToCheck The URLs to check for still being present in the texture db.
+ * @param clearTextureForPath Whether to clear the path in texture database.
+ * @return number of cleaned up images.
+ */
+ static int Cleanup(const std::vector<PVRImagePattern>& urlPatterns,
+ const std::vector<std::string>& urlsToCheck,
+ bool clearTextureForPath = false);
+
+ /*!
+ * @brief Extract the wrapped URL from an image URL.
+ * @param url The URL to unwrap.
+ * @return The unwrapped URL if url is an image URL, url otherwise.
+ */
+ static std::string UnwrapImageURL(const std::string& url);
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRChannelNumberInputHandler.cpp b/xbmc/pvr/PVRChannelNumberInputHandler.cpp
new file mode 100644
index 0000000..a57c774
--- /dev/null
+++ b/xbmc/pvr/PVRChannelNumberInputHandler.cpp
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRChannelNumberInputHandler.h"
+
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+#include <cstdlib>
+#include <mutex>
+#include <string>
+
+using namespace PVR;
+using namespace std::chrono_literals;
+
+CPVRChannelNumberInputHandler::CPVRChannelNumberInputHandler()
+ : CPVRChannelNumberInputHandler(CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iPVRNumericChannelSwitchTimeout,
+ CHANNEL_NUMBER_INPUT_MAX_DIGITS)
+{
+}
+
+CPVRChannelNumberInputHandler::CPVRChannelNumberInputHandler(
+ int iDelay, int iMaxDigits /* = CHANNEL_NUMBER_INPUT_MAX_DIGITS */)
+ : m_iDelay(iDelay), m_iMaxDigits(iMaxDigits), m_timer(this)
+{
+}
+
+void CPVRChannelNumberInputHandler::OnTimeout()
+{
+ if (m_inputBuffer.empty())
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ SetLabel("");
+ }
+ else
+ {
+ // call the overridden worker method
+ OnInputDone();
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ // erase input buffer immediately , but...
+ m_inputBuffer.erase();
+
+ // ... display the label for another .5 secs if we stopped the timer before regular timeout.
+ if (m_timer.IsRunning())
+ SetLabel("");
+ else
+ m_timer.Start(500ms);
+ }
+}
+
+void CPVRChannelNumberInputHandler::ExecuteAction()
+{
+ m_timer.Stop(true /* wait until worker thread ended */);
+ OnTimeout();
+}
+
+bool CPVRChannelNumberInputHandler::CheckInputAndExecuteAction()
+{
+ const CPVRChannelNumber channelNumber = GetChannelNumber();
+ if (channelNumber.IsValid())
+ {
+ // we have a valid channel number; execute the associated action now.
+ ExecuteAction();
+ return true;
+ }
+ return false;
+}
+
+void CPVRChannelNumberInputHandler::AppendChannelNumberCharacter(char cCharacter)
+{
+ if (cCharacter != CPVRChannelNumber::SEPARATOR && (cCharacter < '0' || cCharacter > '9'))
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (cCharacter == CPVRChannelNumber::SEPARATOR)
+ {
+ // no leading separator
+ if (m_inputBuffer.empty())
+ return;
+
+ // max one separator
+ if (m_inputBuffer.find(CPVRChannelNumber::SEPARATOR) != std::string::npos)
+ return;
+ }
+
+ if (m_inputBuffer.size() == static_cast<size_t>(m_iMaxDigits))
+ {
+ m_inputBuffer.erase(m_inputBuffer.begin());
+ SetLabel(m_inputBuffer);
+ }
+ else if (m_inputBuffer.empty())
+ {
+ m_sortedChannelNumbers.clear();
+ GetChannelNumbers(m_sortedChannelNumbers);
+
+ std::sort(m_sortedChannelNumbers.begin(), m_sortedChannelNumbers.end());
+ }
+
+ m_inputBuffer.append(&cCharacter, 1);
+ SetLabel(m_inputBuffer);
+
+ for (auto it = m_sortedChannelNumbers.begin(); it != m_sortedChannelNumbers.end();)
+ {
+ const std::string channel = *it;
+ ++it;
+
+ if (StringUtils::StartsWith(channel, m_inputBuffer))
+ {
+ if (it != m_sortedChannelNumbers.end() && StringUtils::StartsWith(*it, m_inputBuffer))
+ {
+ // there are alternative numbers; wait for more input
+ break;
+ }
+
+ // no alternatives; complete the number and fire immediately
+ m_inputBuffer = channel;
+ SetLabel(m_inputBuffer);
+ ExecuteAction();
+ return;
+ }
+ }
+
+ if (!m_timer.IsRunning())
+ m_timer.Start(std::chrono::milliseconds(m_iDelay));
+ else
+ m_timer.Restart();
+}
+
+CPVRChannelNumber CPVRChannelNumberInputHandler::GetChannelNumber() const
+{
+ int iChannelNumber = 0;
+ int iSubChannelNumber = 0;
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ size_t pos = m_inputBuffer.find(CPVRChannelNumber::SEPARATOR);
+ if (pos != std::string::npos)
+ {
+ // main + sub
+ if (pos != 0)
+ {
+ iChannelNumber = std::atoi(m_inputBuffer.substr(0, pos).c_str());
+ if (pos != m_inputBuffer.size() - 1)
+ iSubChannelNumber = std::atoi(m_inputBuffer.substr(pos + 1).c_str());
+ }
+ }
+ else
+ {
+ // only main
+ iChannelNumber = std::atoi(m_inputBuffer.c_str());
+ }
+
+ return CPVRChannelNumber(iChannelNumber, iSubChannelNumber);
+}
+
+bool CPVRChannelNumberInputHandler::HasChannelNumber() const
+{
+ return !m_inputBuffer.empty();
+}
+
+std::string CPVRChannelNumberInputHandler::GetChannelNumberLabel() const
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ return m_label;
+}
+
+void CPVRChannelNumberInputHandler::SetLabel(const std::string& label)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ if (label != m_label)
+ {
+ m_label = label;
+
+ // inform subscribers
+ m_events.Publish(PVRChannelNumberInputChangedEvent(m_label));
+ }
+}
diff --git a/xbmc/pvr/PVRChannelNumberInputHandler.h b/xbmc/pvr/PVRChannelNumberInputHandler.h
new file mode 100644
index 0000000..efa28ca
--- /dev/null
+++ b/xbmc/pvr/PVRChannelNumberInputHandler.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/channels/PVRChannelNumber.h"
+#include "threads/CriticalSection.h"
+#include "threads/Timer.h"
+#include "utils/EventStream.h"
+
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+struct PVRChannelNumberInputChangedEvent
+{
+ explicit PVRChannelNumberInputChangedEvent(const std::string& input) : m_input(input) {}
+ virtual ~PVRChannelNumberInputChangedEvent() = default;
+
+ std::string m_input;
+};
+
+class CPVRChannelNumberInputHandler : private ITimerCallback
+{
+public:
+ static const int CHANNEL_NUMBER_INPUT_MAX_DIGITS = 5;
+
+ CPVRChannelNumberInputHandler();
+
+ /*!
+ * @brief ctor.
+ * @param iDelay timer delay in millisecods.
+ * @param iMaxDigits maximum number of display digits to use.
+ */
+ CPVRChannelNumberInputHandler(int iDelay, int iMaxDigits = CHANNEL_NUMBER_INPUT_MAX_DIGITS);
+
+ ~CPVRChannelNumberInputHandler() override = default;
+
+ /*!
+ * @brief Get the events available for CEventStream.
+ * @return The events.
+ */
+ CEventStream<PVRChannelNumberInputChangedEvent>& Events() { return m_events; }
+
+ // implementation of ITimerCallback
+ void OnTimeout() override;
+
+ /*!
+ * @brief Get the currently available channel numbers.
+ * @param channelNumbers The list to fill with the channel numbers.
+ */
+ virtual void GetChannelNumbers(std::vector<std::string>& channelNumbers) = 0;
+
+ /*!
+ * @brief This method gets called after the channel number input timer has expired.
+ */
+ virtual void OnInputDone() = 0;
+
+ /*!
+ * @brief Appends a channel number character.
+ * @param cCharacter The character to append. value must be CPVRChannelNumber::SEPARATOR ('.') or any char in the range from '0' to '9'.
+ */
+ virtual void AppendChannelNumberCharacter(char cCharacter);
+
+ /*!
+ * @brief Check whether a channel number was entered.
+ * @return True if the handler currently holds a channel number, false otherwise.
+ */
+ bool HasChannelNumber() const;
+
+ /*!
+ * @brief Get the currently entered channel number as a formatted string.
+ * @return the channel number string.
+ */
+ std::string GetChannelNumberLabel() const;
+
+ /*!
+ * @brief If a number was entered, execute the associated action.
+ * @return True, if the action was executed, false otherwise.
+ */
+ bool CheckInputAndExecuteAction();
+
+protected:
+ /*!
+ * @brief Get the currently entered channel number.
+ * @return the channel number.
+ */
+ CPVRChannelNumber GetChannelNumber() const;
+
+ /*!
+ * @brief Get the currently entered number of digits.
+ * @return the number of digits.
+ */
+ size_t GetCurrentDigitCount() const { return m_inputBuffer.size(); }
+
+ mutable CCriticalSection m_mutex;
+
+private:
+ void ExecuteAction();
+
+ void SetLabel(const std::string& label);
+
+ std::vector<std::string> m_sortedChannelNumbers;
+ const int m_iDelay;
+ const int m_iMaxDigits;
+ std::string m_inputBuffer;
+ std::string m_label;
+ CTimer m_timer;
+ CEventSource<PVRChannelNumberInputChangedEvent> m_events;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRComponentRegistration.cpp b/xbmc/pvr/PVRComponentRegistration.cpp
new file mode 100644
index 0000000..7532283
--- /dev/null
+++ b/xbmc/pvr/PVRComponentRegistration.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRComponentRegistration.h"
+
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsClients.h"
+#include "pvr/guilib/PVRGUIActionsDatabase.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsPowerManagement.h"
+#include "pvr/guilib/PVRGUIActionsRecordings.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/guilib/PVRGUIActionsUtils.h"
+
+#include <memory>
+
+using namespace PVR;
+
+CPVRComponentRegistration::CPVRComponentRegistration()
+{
+ RegisterComponent(std::make_shared<CPVRGUIActionsChannels>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsClients>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsDatabase>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsEPG>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsParentalControl>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsPlayback>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsPowerManagement>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsRecordings>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsTimers>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsUtils>());
+}
+
+CPVRComponentRegistration::~CPVRComponentRegistration()
+{
+ DeregisterComponent(typeid(CPVRGUIActionsUtils));
+ DeregisterComponent(typeid(CPVRGUIActionsTimers));
+ DeregisterComponent(typeid(CPVRGUIActionsRecordings));
+ DeregisterComponent(typeid(CPVRGUIActionsPowerManagement));
+ DeregisterComponent(typeid(CPVRGUIActionsPlayback));
+ DeregisterComponent(typeid(CPVRGUIActionsParentalControl));
+ DeregisterComponent(typeid(CPVRGUIActionsEPG));
+ DeregisterComponent(typeid(CPVRGUIActionsDatabase));
+ DeregisterComponent(typeid(CPVRGUIActionsClients));
+ DeregisterComponent(typeid(CPVRGUIActionsChannels));
+}
diff --git a/xbmc/pvr/PVRComponentRegistration.h b/xbmc/pvr/PVRComponentRegistration.h
new file mode 100644
index 0000000..d81d449
--- /dev/null
+++ b/xbmc/pvr/PVRComponentRegistration.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/IPVRComponent.h"
+#include "utils/ComponentContainer.h"
+
+namespace PVR
+{
+class CPVRComponentRegistration : public CComponentContainer<IPVRComponent>
+{
+public:
+ CPVRComponentRegistration();
+ virtual ~CPVRComponentRegistration();
+};
+} // namespace PVR
diff --git a/xbmc/pvr/PVRContextMenus.cpp b/xbmc/pvr/PVRContextMenus.cpp
new file mode 100644
index 0000000..724872d
--- /dev/null
+++ b/xbmc/pvr/PVRContextMenus.cpp
@@ -0,0 +1,780 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRContextMenus.h"
+
+#include "ContextMenuItem.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClientMenuHooks.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsRecordings.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "utils/URIUtils.h"
+
+#include <memory>
+#include <string>
+
+namespace PVR
+{
+namespace CONTEXTMENUITEM
+{
+#define DECL_STATICCONTEXTMENUITEM(clazz) \
+ class clazz : public CStaticContextMenuAction \
+ { \
+ public: \
+ explicit clazz(uint32_t label) : CStaticContextMenuAction(label) {} \
+ bool IsVisible(const CFileItem& item) const override; \
+ bool Execute(const CFileItemPtr& item) const override; \
+ };
+
+#define DECL_CONTEXTMENUITEM(clazz) \
+ class clazz : public IContextMenuItem \
+ { \
+ public: \
+ std::string GetLabel(const CFileItem& item) const override; \
+ bool IsVisible(const CFileItem& item) const override; \
+ bool Execute(const CFileItemPtr& item) const override; \
+ };
+
+DECL_STATICCONTEXTMENUITEM(PlayEpgTag);
+DECL_STATICCONTEXTMENUITEM(PlayRecording);
+DECL_CONTEXTMENUITEM(ShowInformation);
+DECL_STATICCONTEXTMENUITEM(ShowChannelGuide);
+DECL_STATICCONTEXTMENUITEM(FindSimilar);
+DECL_STATICCONTEXTMENUITEM(StartRecording);
+DECL_STATICCONTEXTMENUITEM(StopRecording);
+DECL_STATICCONTEXTMENUITEM(AddTimerRule);
+DECL_CONTEXTMENUITEM(EditTimerRule);
+DECL_STATICCONTEXTMENUITEM(DeleteTimerRule);
+DECL_CONTEXTMENUITEM(EditTimer);
+DECL_CONTEXTMENUITEM(DeleteTimer);
+DECL_STATICCONTEXTMENUITEM(EditRecording);
+DECL_CONTEXTMENUITEM(DeleteRecording);
+DECL_STATICCONTEXTMENUITEM(UndeleteRecording);
+DECL_STATICCONTEXTMENUITEM(DeleteWatchedRecordings);
+DECL_CONTEXTMENUITEM(ToggleTimerState);
+DECL_STATICCONTEXTMENUITEM(AddReminder);
+DECL_STATICCONTEXTMENUITEM(ExecuteSearch);
+DECL_STATICCONTEXTMENUITEM(EditSearch);
+DECL_STATICCONTEXTMENUITEM(RenameSearch);
+DECL_STATICCONTEXTMENUITEM(DeleteSearch);
+
+class PVRClientMenuHook : public IContextMenuItem
+{
+public:
+ explicit PVRClientMenuHook(const CPVRClientMenuHook& hook) : m_hook(hook) {}
+
+ std::string GetLabel(const CFileItem& item) const override;
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const CFileItemPtr& item) const override;
+
+ const CPVRClientMenuHook& GetHook() const { return m_hook; }
+
+private:
+ const CPVRClientMenuHook m_hook;
+};
+
+std::shared_ptr<CPVRTimerInfoTag> GetTimerInfoTagFromItem(const CFileItem& item)
+{
+ std::shared_ptr<CPVRTimerInfoTag> timer;
+
+ const std::shared_ptr<CPVREpgInfoTag> epg(item.GetEPGInfoTag());
+ if (epg)
+ timer = CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg);
+
+ if (!timer)
+ timer = item.GetPVRTimerInfoTag();
+
+ return timer;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Play epg tag
+
+bool PlayEpgTag::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epg(item.GetEPGInfoTag());
+ if (epg)
+ return epg->IsPlayable();
+
+ return false;
+}
+
+bool PlayEpgTag::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayEpgTag(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Play recording
+
+bool PlayRecording::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRRecording> recording =
+ CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(item.GetEPGInfoTag());
+ if (recording)
+ return !recording->IsDeleted();
+
+ return false;
+}
+
+bool PlayRecording::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(
+ *item, true /* bCheckResume */);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Show information (epg, recording)
+
+std::string ShowInformation::GetLabel(const CFileItem& item) const
+{
+ if (item.GetPVRRecordingInfoTag())
+ return g_localizeStrings.Get(19053); /* Recording Information */
+
+ return g_localizeStrings.Get(19047); /* Programme information */
+}
+
+bool ShowInformation::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRChannel> channel(item.GetPVRChannelInfoTag());
+ if (channel)
+ return channel->GetEPGNow().get() != nullptr;
+
+ if (item.HasEPGInfoTag())
+ return !item.GetEPGInfoTag()->IsGapTag();
+
+ const std::shared_ptr<CPVRTimerInfoTag> timer(item.GetPVRTimerInfoTag());
+ if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ return timer->GetEpgInfoTag().get() != nullptr;
+
+ if (item.GetPVRRecordingInfoTag())
+ return true;
+
+ return false;
+}
+
+bool ShowInformation::Execute(const CFileItemPtr& item) const
+{
+ if (item->GetPVRRecordingInfoTag())
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo(*item);
+
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Show channel guide
+
+bool ShowChannelGuide::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRChannel> channel(item.GetPVRChannelInfoTag());
+ if (channel)
+ return channel->GetEPGNow().get() != nullptr;
+
+ return false;
+}
+
+bool ShowChannelGuide::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowChannelEPG(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Find similar
+
+bool FindSimilar::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRChannel> channel(item.GetPVRChannelInfoTag());
+ if (channel)
+ return channel->GetEPGNow().get() != nullptr;
+
+ if (item.HasEPGInfoTag())
+ return !item.GetEPGInfoTag()->IsGapTag();
+
+ const std::shared_ptr<CPVRTimerInfoTag> timer(item.GetPVRTimerInfoTag());
+ if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ return timer->GetEpgInfoTag().get() != nullptr;
+
+ const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag());
+ if (recording)
+ return !recording->IsDeleted();
+
+ return false;
+}
+
+bool FindSimilar::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().FindSimilar(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Start recording
+
+bool StartRecording::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(item);
+
+ std::shared_ptr<CPVRChannel> channel = item.GetPVRChannelInfoTag();
+ if (channel)
+ return client && client->GetClientCapabilities().SupportsTimers() &&
+ !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel);
+
+ const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag();
+ if (epg && epg->IsRecordable())
+ {
+ if (epg->IsGapTag())
+ {
+ channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(epg);
+ if (channel)
+ {
+ return client && client->GetClientCapabilities().SupportsTimers() &&
+ !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel);
+ }
+ }
+ else
+ {
+ return client && client->GetClientCapabilities().SupportsTimers() &&
+ !CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg);
+ }
+ }
+ return false;
+}
+
+bool StartRecording::Execute(const CFileItemPtr& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = item->GetEPGInfoTag();
+ if (!epgTag || epgTag->IsActive())
+ {
+ // instant recording
+ std::shared_ptr<CPVRChannel> channel;
+ if (epgTag)
+ channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(epgTag);
+
+ if (!channel)
+ channel = item->GetPVRChannelInfoTag();
+
+ if (channel)
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().SetRecordingOnChannel(channel,
+ true);
+ }
+
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimer(*item, false);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Stop recording
+
+bool StopRecording::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag());
+ if (recording && recording->IsInProgress())
+ return true;
+
+ std::shared_ptr<CPVRChannel> channel = item.GetPVRChannelInfoTag();
+ if (channel)
+ return CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel);
+
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ return timer->IsRecording();
+
+ const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag();
+ if (epg && epg->IsGapTag())
+ {
+ channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(epg);
+ if (channel)
+ return CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel);
+ }
+
+ return false;
+}
+
+bool StopRecording::Execute(const CFileItemPtr& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = item->GetEPGInfoTag();
+ if (epgTag && epgTag->IsGapTag())
+ {
+ // instance recording
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(epgTag);
+ if (channel)
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().SetRecordingOnChannel(channel,
+ false);
+ }
+
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().StopRecording(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Edit recording
+
+bool EditRecording::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag());
+ if (recording && !recording->IsDeleted() && !recording->IsInProgress())
+ {
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().CanEditRecording(item);
+ }
+ return false;
+}
+
+bool EditRecording::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().EditRecording(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Delete recording
+
+std::string DeleteRecording::GetLabel(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag());
+ if (recording && recording->IsDeleted())
+ return g_localizeStrings.Get(19291); /* Delete permanently */
+
+ return g_localizeStrings.Get(117); /* Delete */
+}
+
+bool DeleteRecording::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(item);
+ if (client && !client->GetClientCapabilities().SupportsRecordingsDelete())
+ return false;
+
+ const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag());
+ if (recording && !recording->IsInProgress())
+ return true;
+
+ // recordings folder?
+ if (item.m_bIsFolder &&
+ CServiceBroker::GetPVRManager().Clients()->AnyClientSupportingRecordingsDelete())
+ {
+ const CPVRRecordingsPath path(item.GetPath());
+ return path.IsValid() && !path.IsRecordingsRoot();
+ }
+
+ return false;
+}
+
+bool DeleteRecording::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().DeleteRecording(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Undelete recording
+
+bool UndeleteRecording::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag());
+ if (recording && recording->IsDeleted())
+ return true;
+
+ return false;
+}
+
+bool UndeleteRecording::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().UndeleteRecording(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Delete watched recordings
+
+bool DeleteWatchedRecordings::IsVisible(const CFileItem& item) const
+{
+ // recordings folder?
+ if (item.m_bIsFolder && !item.IsParentFolder())
+ return CPVRRecordingsPath(item.GetPath()).IsValid();
+
+ return false;
+}
+
+bool DeleteWatchedRecordings::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().DeleteWatchedRecordings(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Add reminder
+
+bool AddReminder::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag();
+ if (epg && !CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg) &&
+ epg->StartAsLocalTime() > CDateTime::GetCurrentDateTime())
+ return true;
+
+ return false;
+}
+
+bool AddReminder::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddReminder(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Activate / deactivate timer or timer rule
+
+std::string ToggleTimerState::GetLabel(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(item.GetPVRTimerInfoTag());
+ if (timer && !timer->IsDisabled())
+ return g_localizeStrings.Get(844); /* Deactivate */
+
+ return g_localizeStrings.Get(843); /* Activate */
+}
+
+bool ToggleTimerState::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(item.GetPVRTimerInfoTag());
+ if (!timer || URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER) ||
+ timer->IsBroken())
+ return false;
+
+ return timer->GetTimerType()->SupportsEnableDisable();
+}
+
+bool ToggleTimerState::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleTimerState(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Add timer rule
+
+bool AddTimerRule::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag();
+ return (epg && !epg->IsGapTag() &&
+ !CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg));
+}
+
+bool AddTimerRule::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimerRule(*item, true, true);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Edit timer rule
+
+std::string EditTimerRule::GetLabel(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> parentTimer(
+ CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer));
+ if (parentTimer)
+ {
+ if (!parentTimer->GetTimerType()->IsReadOnly())
+ return g_localizeStrings.Get(19243); /* Edit timer rule */
+ }
+ }
+
+ return g_localizeStrings.Get(19304); /* View timer rule */
+}
+
+bool EditTimerRule::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ return timer->HasParent();
+
+ return false;
+}
+
+bool EditTimerRule::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().EditTimerRule(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Delete timer rule
+
+bool DeleteTimerRule::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> parentTimer(
+ CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer));
+ if (parentTimer)
+ return parentTimer->GetTimerType()->AllowsDelete();
+ }
+
+ return false;
+}
+
+bool DeleteTimerRule::Execute(const CFileItemPtr& item) const
+{
+ auto& timers = CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>();
+ const std::shared_ptr<CFileItem> parentTimer = timers.GetTimerRule(*item);
+ if (parentTimer)
+ return timers.DeleteTimerRule(*parentTimer);
+
+ return false;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Edit / View timer
+
+std::string EditTimer::GetLabel(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ if (timer)
+ {
+ const std::shared_ptr<CPVRTimerType> timerType = timer->GetTimerType();
+ if (item.GetEPGInfoTag())
+ {
+ if (timerType->IsReminder())
+ return g_localizeStrings.Get(timerType->IsReadOnly() ? 829 /* View reminder */
+ : 830); /* Edit reminder */
+ else
+ return g_localizeStrings.Get(timerType->IsReadOnly() ? 19241 /* View timer */
+ : 19242); /* Edit timer */
+ }
+ else
+ return g_localizeStrings.Get(timerType->IsReadOnly() ? 21483 : 21450); /* View/Edit */
+ }
+ return g_localizeStrings.Get(19241); /* View timer */
+}
+
+bool EditTimer::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ return timer && (!item.GetEPGInfoTag() ||
+ !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER));
+}
+
+bool EditTimer::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().EditTimer(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Delete timer
+
+std::string DeleteTimer::GetLabel(const CFileItem& item) const
+{
+ if (item.GetPVRTimerInfoTag())
+ return g_localizeStrings.Get(117); /* Delete */
+
+ const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag();
+ if (epg)
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer =
+ CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg);
+ if (timer && timer->IsReminder())
+ return g_localizeStrings.Get(827); /* Delete reminder */
+ }
+ return g_localizeStrings.Get(19060); /* Delete timer */
+}
+
+bool DeleteTimer::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ if (timer &&
+ (!item.GetEPGInfoTag() ||
+ !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER)) &&
+ !timer->IsRecording())
+ return timer->GetTimerType()->AllowsDelete();
+
+ return false;
+}
+
+bool DeleteTimer::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().DeleteTimer(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// PVR Client menu hook
+
+std::string PVRClientMenuHook::GetLabel(const CFileItem& item) const
+{
+ return m_hook.GetLabel();
+}
+
+bool PVRClientMenuHook::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(item);
+ if (!client || m_hook.GetAddonId() != client->ID())
+ return false;
+
+ if (m_hook.IsAllHook())
+ return !item.m_bIsFolder &&
+ !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER);
+ else if (m_hook.IsEpgHook())
+ return item.IsEPG();
+ else if (m_hook.IsChannelHook())
+ return item.IsPVRChannel();
+ else if (m_hook.IsDeletedRecordingHook())
+ return item.IsDeletedPVRRecording();
+ else if (m_hook.IsRecordingHook())
+ return item.IsUsablePVRRecording();
+ else if (m_hook.IsTimerHook())
+ return item.IsPVRTimer();
+ else
+ return false;
+}
+
+bool PVRClientMenuHook::Execute(const CFileItemPtr& item) const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item);
+ if (!client)
+ return false;
+
+ if (item->IsEPG())
+ return client->CallEpgTagMenuHook(m_hook, item->GetEPGInfoTag()) == PVR_ERROR_NO_ERROR;
+ else if (item->IsPVRChannel())
+ return client->CallChannelMenuHook(m_hook, item->GetPVRChannelInfoTag()) == PVR_ERROR_NO_ERROR;
+ else if (item->IsDeletedPVRRecording())
+ return client->CallRecordingMenuHook(m_hook, item->GetPVRRecordingInfoTag(), true) ==
+ PVR_ERROR_NO_ERROR;
+ else if (item->IsUsablePVRRecording())
+ return client->CallRecordingMenuHook(m_hook, item->GetPVRRecordingInfoTag(), false) ==
+ PVR_ERROR_NO_ERROR;
+ else if (item->IsPVRTimer())
+ return client->CallTimerMenuHook(m_hook, item->GetPVRTimerInfoTag()) == PVR_ERROR_NO_ERROR;
+ else
+ return false;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Execute saved search
+
+bool ExecuteSearch::IsVisible(const CFileItem& item) const
+{
+ return item.HasEPGSearchFilter();
+}
+
+bool ExecuteSearch::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ExecuteSavedSearch(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Edit saved search
+
+bool EditSearch::IsVisible(const CFileItem& item) const
+{
+ return item.HasEPGSearchFilter();
+}
+
+bool EditSearch::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().EditSavedSearch(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Rename saved search
+
+bool RenameSearch::IsVisible(const CFileItem& item) const
+{
+ return item.HasEPGSearchFilter();
+}
+
+bool RenameSearch::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().RenameSavedSearch(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Delete saved search
+
+bool DeleteSearch::IsVisible(const CFileItem& item) const
+{
+ return item.HasEPGSearchFilter();
+}
+
+bool DeleteSearch::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().DeleteSavedSearch(*item);
+}
+
+} // namespace CONTEXTMENUITEM
+
+CPVRContextMenuManager& CPVRContextMenuManager::GetInstance()
+{
+ static CPVRContextMenuManager instance;
+ return instance;
+}
+
+CPVRContextMenuManager::CPVRContextMenuManager()
+ : m_items({
+ std::make_shared<CONTEXTMENUITEM::PlayEpgTag>(19190), /* Play programme */
+ std::make_shared<CONTEXTMENUITEM::PlayRecording>(19687), /* Play recording */
+ std::make_shared<CONTEXTMENUITEM::ShowInformation>(),
+ std::make_shared<CONTEXTMENUITEM::ShowChannelGuide>(19686), /* Channel guide */
+ std::make_shared<CONTEXTMENUITEM::FindSimilar>(19003), /* Find similar */
+ std::make_shared<CONTEXTMENUITEM::ToggleTimerState>(),
+ std::make_shared<CONTEXTMENUITEM::AddTimerRule>(19061), /* Add timer */
+ std::make_shared<CONTEXTMENUITEM::EditTimerRule>(),
+ std::make_shared<CONTEXTMENUITEM::DeleteTimerRule>(19295), /* Delete timer rule */
+ std::make_shared<CONTEXTMENUITEM::EditTimer>(),
+ std::make_shared<CONTEXTMENUITEM::DeleteTimer>(),
+ std::make_shared<CONTEXTMENUITEM::StartRecording>(264), /* Record */
+ std::make_shared<CONTEXTMENUITEM::StopRecording>(19059), /* Stop recording */
+ std::make_shared<CONTEXTMENUITEM::EditRecording>(21450), /* Edit */
+ std::make_shared<CONTEXTMENUITEM::DeleteRecording>(),
+ std::make_shared<CONTEXTMENUITEM::UndeleteRecording>(19290), /* Undelete */
+ std::make_shared<CONTEXTMENUITEM::DeleteWatchedRecordings>(19327), /* Delete watched */
+ std::make_shared<CONTEXTMENUITEM::AddReminder>(826), /* Set reminder */
+ std::make_shared<CONTEXTMENUITEM::ExecuteSearch>(137), /* Search */
+ std::make_shared<CONTEXTMENUITEM::EditSearch>(21450), /* Edit */
+ std::make_shared<CONTEXTMENUITEM::RenameSearch>(118), /* Rename */
+ std::make_shared<CONTEXTMENUITEM::DeleteSearch>(117), /* Delete */
+ })
+{
+}
+
+void CPVRContextMenuManager::AddMenuHook(const CPVRClientMenuHook& hook)
+{
+ if (hook.IsSettingsHook())
+ return; // settings hooks are not handled using context menus
+
+ const auto item = std::make_shared<CONTEXTMENUITEM::PVRClientMenuHook>(hook);
+ m_items.emplace_back(item);
+ m_events.Publish(PVRContextMenuEvent(PVRContextMenuEventAction::ADD_ITEM, item));
+}
+
+void CPVRContextMenuManager::RemoveMenuHook(const CPVRClientMenuHook& hook)
+{
+ if (hook.IsSettingsHook())
+ return; // settings hooks are not handled using context menus
+
+ for (auto it = m_items.begin(); it < m_items.end(); ++it)
+ {
+ const CONTEXTMENUITEM::PVRClientMenuHook* cmh =
+ dynamic_cast<const CONTEXTMENUITEM::PVRClientMenuHook*>((*it).get());
+ if (cmh && cmh->GetHook() == hook)
+ {
+ m_events.Publish(PVRContextMenuEvent(PVRContextMenuEventAction::REMOVE_ITEM, *it));
+ m_items.erase(it);
+ return;
+ }
+ }
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRContextMenus.h b/xbmc/pvr/PVRContextMenus.h
new file mode 100644
index 0000000..357db17
--- /dev/null
+++ b/xbmc/pvr/PVRContextMenus.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "utils/EventStream.h"
+
+#include <memory>
+#include <vector>
+
+class IContextMenuItem;
+
+namespace PVR
+{
+enum class PVRContextMenuEventAction
+{
+ ADD_ITEM,
+ REMOVE_ITEM
+};
+
+struct PVRContextMenuEvent
+{
+ PVRContextMenuEvent(const PVRContextMenuEventAction& a,
+ const std::shared_ptr<IContextMenuItem>& i)
+ : action(a), item(i)
+ {
+ }
+
+ PVRContextMenuEventAction action;
+ std::shared_ptr<IContextMenuItem> item;
+};
+
+class CPVRClientMenuHook;
+
+class CPVRContextMenuManager
+{
+public:
+ static CPVRContextMenuManager& GetInstance();
+
+ std::vector<std::shared_ptr<IContextMenuItem>> GetMenuItems() const { return m_items; }
+
+ void AddMenuHook(const CPVRClientMenuHook& hook);
+ void RemoveMenuHook(const CPVRClientMenuHook& hook);
+
+ /*!
+ * @brief Query the events available for CEventStream
+ */
+ CEventStream<PVRContextMenuEvent>& Events() { return m_events; }
+
+private:
+ CPVRContextMenuManager();
+ CPVRContextMenuManager(const CPVRContextMenuManager&) = delete;
+ CPVRContextMenuManager const& operator=(CPVRContextMenuManager const&) = delete;
+ virtual ~CPVRContextMenuManager() = default;
+
+ std::vector<std::shared_ptr<IContextMenuItem>> m_items;
+ CEventSource<PVRContextMenuEvent> m_events;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRDatabase.cpp b/xbmc/pvr/PVRDatabase.cpp
new file mode 100644
index 0000000..9475c5a
--- /dev/null
+++ b/xbmc/pvr/PVRDatabase.cpp
@@ -0,0 +1,1141 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRDatabase.h"
+
+#include "ServiceBroker.h"
+#include "dbwrappers/dataset.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/providers/PVRProvider.h"
+#include "pvr/providers/PVRProviders.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimerType.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <cstdlib>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace dbiplus;
+using namespace PVR;
+
+namespace
+{
+// clang-format off
+
+ static const std::string sqlCreateTimersTable =
+ "CREATE TABLE timers ("
+ "iClientIndex integer primary key, "
+ "iParentClientIndex integer, "
+ "iClientId integer, "
+ "iTimerType integer, "
+ "iState integer, "
+ "sTitle varchar(255), "
+ "iClientChannelUid integer, "
+ "sSeriesLink varchar(255), "
+ "sStartTime varchar(20), "
+ "bStartAnyTime bool, "
+ "sEndTime varchar(20), "
+ "bEndAnyTime bool, "
+ "sFirstDay varchar(20), "
+ "iWeekdays integer, "
+ "iEpgUid integer, "
+ "iMarginStart integer, "
+ "iMarginEnd integer, "
+ "sEpgSearchString varchar(255), "
+ "bFullTextEpgSearch bool, "
+ "iPreventDuplicates integer,"
+ "iPrority integer,"
+ "iLifetime integer,"
+ "iMaxRecordings integer,"
+ "iRecordingGroup integer"
+ ")";
+
+ static const std::string sqlCreateChannelGroupsTable =
+ "CREATE TABLE channelgroups ("
+ "idGroup integer primary key,"
+ "bIsRadio bool, "
+ "iGroupType integer, "
+ "sName varchar(64), "
+ "iLastWatched integer, "
+ "bIsHidden bool, "
+ "iPosition integer, "
+ "iLastOpened bigint unsigned"
+ ")";
+
+ static const std::string sqlCreateProvidersTable =
+ "CREATE TABLE providers ("
+ "idProvider integer primary key, "
+ "iUniqueId integer, "
+ "iClientId integer, "
+ "sName varchar(64), "
+ "iType integer, "
+ "sIconPath varchar(255), "
+ "sCountries varchar(64), "
+ "sLanguages varchar(64) "
+ ")";
+
+ // clang-format on
+
+ std::string GetClientIdsSQL(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+ {
+ if (clients.empty())
+ return {};
+
+ std::string clientIds = "(";
+ for (auto it = clients.cbegin(); it != clients.cend(); ++it)
+ {
+ if (it != clients.cbegin())
+ clientIds += " OR ";
+
+ clientIds += "iClientId = ";
+ clientIds += std::to_string((*it)->GetID());
+ }
+ clientIds += ")";
+ return clientIds;
+ }
+
+} // unnamed namespace
+
+bool CPVRDatabase::Open()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return CDatabase::Open(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseTV);
+}
+
+void CPVRDatabase::Close()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CDatabase::Close();
+}
+
+void CPVRDatabase::Lock()
+{
+ m_critSection.lock();
+}
+
+void CPVRDatabase::Unlock()
+{
+ m_critSection.unlock();
+}
+
+void CPVRDatabase::CreateTables()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CLog::LogF(LOGINFO, "Creating PVR database tables");
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'channels'");
+ m_pDS->exec("CREATE TABLE channels ("
+ "idChannel integer primary key, "
+ "iUniqueId integer, "
+ "bIsRadio bool, "
+ "bIsHidden bool, "
+ "bIsUserSetIcon bool, "
+ "bIsUserSetName bool, "
+ "bIsLocked bool, "
+ "sIconPath varchar(255), "
+ "sChannelName varchar(64), "
+ "bIsVirtual bool, "
+ "bEPGEnabled bool, "
+ "sEPGScraper varchar(32), "
+ "iLastWatched integer, "
+ "iClientId integer, " //! @todo use mapping table
+ "idEpg integer, "
+ "bHasArchive bool, "
+ "iClientProviderUid integer, "
+ "bIsUserSetHidden bool"
+ ")");
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'channelgroups'");
+ m_pDS->exec(sqlCreateChannelGroupsTable);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'map_channelgroups_channels'");
+ m_pDS->exec(
+ "CREATE TABLE map_channelgroups_channels ("
+ "idChannel integer, "
+ "idGroup integer, "
+ "iChannelNumber integer, "
+ "iSubChannelNumber integer, "
+ "iOrder integer, "
+ "iClientChannelNumber integer, "
+ "iClientSubChannelNumber integer"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'clients'");
+ m_pDS->exec(
+ "CREATE TABLE clients ("
+ "idClient integer primary key, "
+ "iPriority integer"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'timers'");
+ m_pDS->exec(sqlCreateTimersTable);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'providers'");
+ m_pDS->exec(sqlCreateProvidersTable);
+}
+
+void CPVRDatabase::CreateAnalytics()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CLog::LogF(LOGINFO, "Creating PVR database indices");
+ m_pDS->exec("CREATE INDEX idx_clients_idClient on clients(idClient);");
+ m_pDS->exec("CREATE UNIQUE INDEX idx_channels_iClientId_iUniqueId on channels(iClientId, iUniqueId);");
+ m_pDS->exec("CREATE INDEX idx_channelgroups_bIsRadio on channelgroups(bIsRadio);");
+ m_pDS->exec("CREATE UNIQUE INDEX idx_idGroup_idChannel on map_channelgroups_channels(idGroup, idChannel);");
+ m_pDS->exec("CREATE INDEX idx_timers_iClientIndex on timers(iClientIndex);");
+}
+
+void CPVRDatabase::UpdateTables(int iVersion)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (iVersion < 13)
+ m_pDS->exec("ALTER TABLE channels ADD idEpg integer;");
+
+ if (iVersion < 20)
+ m_pDS->exec("ALTER TABLE channels ADD bIsUserSetIcon bool");
+
+ if (iVersion < 21)
+ m_pDS->exec("ALTER TABLE channelgroups ADD iGroupType integer");
+
+ if (iVersion < 22)
+ m_pDS->exec("ALTER TABLE channels ADD bIsLocked bool");
+
+ if (iVersion < 23)
+ m_pDS->exec("ALTER TABLE channelgroups ADD iLastWatched integer");
+
+ if (iVersion < 24)
+ m_pDS->exec("ALTER TABLE channels ADD bIsUserSetName bool");
+
+ if (iVersion < 25)
+ m_pDS->exec("DROP TABLE IF EXISTS channelsettings");
+
+ if (iVersion < 26)
+ {
+ m_pDS->exec("ALTER TABLE channels ADD iClientSubChannelNumber integer");
+ m_pDS->exec("UPDATE channels SET iClientSubChannelNumber = 0");
+ m_pDS->exec("ALTER TABLE map_channelgroups_channels ADD iSubChannelNumber integer");
+ m_pDS->exec("UPDATE map_channelgroups_channels SET iSubChannelNumber = 0");
+ }
+
+ if (iVersion < 27)
+ m_pDS->exec("ALTER TABLE channelgroups ADD bIsHidden bool");
+
+ if (iVersion < 28)
+ m_pDS->exec("DROP TABLE clients");
+
+ if (iVersion < 29)
+ m_pDS->exec("ALTER TABLE channelgroups ADD iPosition integer");
+
+ if (iVersion < 32)
+ m_pDS->exec("CREATE TABLE clients (idClient integer primary key, iPriority integer)");
+
+ if (iVersion < 33)
+ m_pDS->exec(sqlCreateTimersTable);
+
+ if (iVersion < 34)
+ m_pDS->exec("ALTER TABLE channels ADD bHasArchive bool");
+
+ if (iVersion < 35)
+ {
+ m_pDS->exec("ALTER TABLE map_channelgroups_channels ADD iOrder integer");
+ m_pDS->exec("UPDATE map_channelgroups_channels SET iOrder = 0");
+ }
+
+ if (iVersion < 36)
+ {
+ m_pDS->exec("ALTER TABLE map_channelgroups_channels ADD iClientChannelNumber integer");
+ m_pDS->exec("UPDATE map_channelgroups_channels SET iClientChannelNumber = 0");
+ m_pDS->exec("ALTER TABLE map_channelgroups_channels ADD iClientSubChannelNumber integer");
+ m_pDS->exec("UPDATE map_channelgroups_channels SET iClientSubChannelNumber = 0");
+ }
+
+ if (iVersion < 37)
+ m_pDS->exec("ALTER TABLE channelgroups ADD iLastOpened integer");
+
+ if (iVersion < 38)
+ {
+ m_pDS->exec("ALTER TABLE channelgroups "
+ "RENAME TO channelgroups_old");
+
+ m_pDS->exec(sqlCreateChannelGroupsTable);
+
+ m_pDS->exec(
+ "INSERT INTO channelgroups (bIsRadio, iGroupType, sName, iLastWatched, bIsHidden, "
+ "iPosition, iLastOpened) "
+ "SELECT bIsRadio, iGroupType, sName, iLastWatched, bIsHidden, iPosition, iLastOpened "
+ "FROM channelgroups_old");
+
+ m_pDS->exec("DROP TABLE channelgroups_old");
+ }
+
+ if (iVersion < 39)
+ {
+ m_pDS->exec(sqlCreateProvidersTable);
+ m_pDS->exec("CREATE UNIQUE INDEX idx_iUniqueId_iClientId on providers(iUniqueId, iClientId);");
+ m_pDS->exec("ALTER TABLE channels ADD iClientProviderUid integer");
+ m_pDS->exec("UPDATE channels SET iClientProviderUid = -1");
+ }
+
+ if (iVersion < 40)
+ {
+ m_pDS->exec("ALTER TABLE channels ADD bIsUserSetHidden bool");
+ m_pDS->exec("UPDATE channels SET bIsUserSetHidden = bIsHidden");
+ }
+}
+
+/********** Client methods **********/
+
+bool CPVRDatabase::DeleteClients()
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all clients from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("clients");
+}
+
+bool CPVRDatabase::Persist(const CPVRClient& client)
+{
+ if (client.GetID() == PVR_INVALID_CLIENT_ID)
+ return false;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Persisting client '{}' to database", client.ID());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::string strQuery = PrepareSQL("REPLACE INTO clients (idClient, iPriority) VALUES (%i, %i);",
+ client.GetID(), client.GetPriority());
+
+ return ExecuteQuery(strQuery);
+}
+
+bool CPVRDatabase::Delete(const CPVRClient& client)
+{
+ if (client.GetID() == PVR_INVALID_CLIENT_ID)
+ return false;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting client '{}' from the database", client.ID());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idClient = '%i'", client.GetID()));
+
+ return DeleteValues("clients", filter);
+}
+
+int CPVRDatabase::GetPriority(const CPVRClient& client)
+{
+ if (client.GetID() == PVR_INVALID_CLIENT_ID)
+ return 0;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Getting priority for client '{}' from the database", client.ID());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::string strWhereClause = PrepareSQL("idClient = '%i'", client.GetID());
+ const std::string strValue = GetSingleValue("clients", "iPriority", strWhereClause);
+
+ if (strValue.empty())
+ return 0;
+
+ return atoi(strValue.c_str());
+}
+
+/********** Channel provider methods **********/
+
+bool CPVRDatabase::DeleteProviders()
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all providers from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("providers");
+}
+
+bool CPVRDatabase::Persist(CPVRProvider& provider, bool updateRecord /* = false */)
+{
+ bool bReturn = false;
+ if (provider.GetName().empty())
+ {
+ CLog::LogF(LOGERROR, "Empty provider name");
+ return bReturn;
+ }
+
+ std::string strQuery;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ {
+ /* insert a new entry when this is a new group, or replace the existing one otherwise */
+ if (!updateRecord)
+ strQuery =
+ PrepareSQL("INSERT INTO providers (idProvider, iUniqueId, iClientId, sName, "
+ "iType, sIconPath, sCountries, sLanguages) "
+ "VALUES (%i, %i, %i, '%s', %i, '%s', '%s', '%s');",
+ provider.GetDatabaseId(), provider.GetUniqueId(), provider.GetClientId(),
+ provider.GetName().c_str(), static_cast<int>(provider.GetType()),
+ provider.GetClientIconPath().c_str(), provider.GetCountriesDBString().c_str(),
+ provider.GetLanguagesDBString().c_str());
+ else
+ strQuery =
+ PrepareSQL("REPLACE INTO providers (idProvider, iUniqueId, iClientId, sName, "
+ "iType, sIconPath, sCountries, sLanguages) "
+ "VALUES (%i, %i, %i, '%s', %i, '%s', '%s', '%s');",
+ provider.GetDatabaseId(), provider.GetUniqueId(), provider.GetClientId(),
+ provider.GetName().c_str(), static_cast<int>(provider.GetType()),
+ provider.GetClientIconPath().c_str(), provider.GetCountriesDBString().c_str(),
+ provider.GetLanguagesDBString().c_str());
+
+ bReturn = ExecuteQuery(strQuery);
+
+ /* set the provider id if it was <= 0 */
+ if (bReturn && provider.GetDatabaseId() <= 0)
+ {
+ provider.SetDatabaseId(static_cast<int>(m_pDS->lastinsertid()));
+ }
+ }
+
+ return bReturn;
+}
+
+bool CPVRDatabase::Delete(const CPVRProvider& provider)
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting provider '{}' from the database",
+ provider.GetName());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idProvider = '%i'", provider.GetDatabaseId()));
+
+ return DeleteValues("providers", filter);
+}
+
+bool CPVRDatabase::Get(CPVRProviders& results,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients) const
+{
+ bool bReturn = false;
+
+ std::string strQuery = "SELECT * from providers ";
+ const std::string clientIds = GetClientIdsSQL(clients);
+ if (!clientIds.empty())
+ strQuery += "WHERE " + clientIds + " OR iType = 1"; // always load addon providers
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ strQuery = PrepareSQL(strQuery);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ std::shared_ptr<CPVRProvider> provider = std::make_shared<CPVRProvider>(
+ m_pDS->fv("iUniqueId").get_asInt(), m_pDS->fv("iClientId").get_asInt());
+
+ provider->SetDatabaseId(m_pDS->fv("idProvider").get_asInt());
+ provider->SetName(m_pDS->fv("sName").get_asString());
+ provider->SetType(
+ static_cast<PVR_PROVIDER_TYPE>(m_pDS->fv("iType").get_asInt()));
+ provider->SetIconPath(m_pDS->fv("sIconPath").get_asString());
+ provider->SetCountriesFromDBString(m_pDS->fv("sCountries").get_asString());
+ provider->SetLanguagesFromDBString(m_pDS->fv("sLanguages").get_asString());
+
+ results.CheckAndAddEntry(provider, ProviderUpdateMode::BY_DATABASE);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Channel Provider '{}' loaded from PVR database",
+ provider->GetName());
+ m_pDS->next();
+ }
+
+ m_pDS->close();
+ bReturn = true;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Couldn't load providers from PVR database");
+ }
+ }
+
+ return bReturn;
+}
+
+int CPVRDatabase::GetMaxProviderId()
+{
+ std::string strQuery = PrepareSQL("SELECT max(idProvider) as maxProviderId from providers");
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ return GetSingleValueInt(strQuery);
+}
+
+/********** Channel methods **********/
+
+int CPVRDatabase::Get(bool bRadio,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& results) const
+{
+ int iReturn = 0;
+
+ std::string strQuery = "SELECT * from channels WHERE bIsRadio = %u ";
+ const std::string clientIds = GetClientIdsSQL(clients);
+ if (!clientIds.empty())
+ strQuery += "AND " + clientIds;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ strQuery = PrepareSQL(strQuery, bRadio);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ const std::shared_ptr<CPVRChannel> channel(new CPVRChannel(
+ m_pDS->fv("bIsRadio").get_asBool(), m_pDS->fv("sIconPath").get_asString()));
+
+ channel->m_iChannelId = m_pDS->fv("idChannel").get_asInt();
+ channel->m_iUniqueId = m_pDS->fv("iUniqueId").get_asInt();
+ channel->m_bIsHidden = m_pDS->fv("bIsHidden").get_asBool();
+ channel->m_bIsUserSetIcon = m_pDS->fv("bIsUserSetIcon").get_asBool();
+ channel->m_bIsUserSetName = m_pDS->fv("bIsUserSetName").get_asBool();
+ channel->m_bIsLocked = m_pDS->fv("bIsLocked").get_asBool();
+ channel->m_strChannelName = m_pDS->fv("sChannelName").get_asString();
+ channel->m_bEPGEnabled = m_pDS->fv("bEPGEnabled").get_asBool();
+ channel->m_strEPGScraper = m_pDS->fv("sEPGScraper").get_asString();
+ channel->m_iLastWatched = static_cast<time_t>(m_pDS->fv("iLastWatched").get_asInt());
+ channel->m_iClientId = m_pDS->fv("iClientId").get_asInt();
+ channel->m_iEpgId = m_pDS->fv("idEpg").get_asInt();
+ channel->m_bHasArchive = m_pDS->fv("bHasArchive").get_asBool();
+ channel->m_iClientProviderUid = m_pDS->fv("iClientProviderUid").get_asInt();
+ channel->m_bIsUserSetHidden = m_pDS->fv("bIsUserSetHidden").get_asBool();
+
+ channel->UpdateEncryptionName();
+
+ results.insert({channel->StorageId(), channel});
+
+ m_pDS->next();
+ ++iReturn;
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Couldn't load channels from PVR database");
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "PVR database query failed");
+ }
+
+ m_pDS->close();
+ return iReturn;
+}
+
+bool CPVRDatabase::DeleteChannels()
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all channels from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("channels");
+}
+
+bool CPVRDatabase::QueueDeleteQuery(const CPVRChannel& channel)
+{
+ /* invalid channel */
+ if (channel.ChannelID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel id: {}", channel.ChannelID());
+ return false;
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Queueing delete for channel '{}' from the database",
+ channel.ChannelName());
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idChannel = %i", channel.ChannelID()));
+
+ std::string strQuery;
+ if (BuildSQL(PrepareSQL("DELETE FROM %s ", "channels"), filter, strQuery))
+ return CDatabase::QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+/********** Channel group member methods **********/
+
+bool CPVRDatabase::QueueDeleteQuery(const CPVRChannelGroupMember& groupMember)
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Queueing delete for channel group member '{}' from the database",
+ groupMember.Channel() ? groupMember.Channel()->ChannelName()
+ : std::to_string(groupMember.ChannelDatabaseID()));
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idGroup = %i", groupMember.GroupID()));
+ filter.AppendWhere(PrepareSQL("idChannel = %i", groupMember.ChannelDatabaseID()));
+
+ std::string strQuery;
+ if (BuildSQL(PrepareSQL("DELETE FROM %s ", "map_channelgroups_channels"), filter, strQuery))
+ return CDatabase::QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+/********** Channel group methods **********/
+
+bool CPVRDatabase::RemoveChannelsFromGroup(const CPVRChannelGroup& group)
+{
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idGroup = %i", group.GroupID()));
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("map_channelgroups_channels", filter);
+}
+
+bool CPVRDatabase::DeleteChannelGroups()
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all channel groups from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("channelgroups") && DeleteValues("map_channelgroups_channels");
+}
+
+bool CPVRDatabase::Delete(const CPVRChannelGroup& group)
+{
+ /* invalid group id */
+ if (group.GroupID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel group id: {}", group.GroupID());
+ return false;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idGroup = %i", group.GroupID()));
+ filter.AppendWhere(PrepareSQL("bIsRadio = %u", group.IsRadio()));
+
+ return RemoveChannelsFromGroup(group) && DeleteValues("channelgroups", filter);
+}
+
+int CPVRDatabase::Get(CPVRChannelGroups& results) const
+{
+ int iLoaded = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::string strQuery = PrepareSQL("SELECT * from channelgroups WHERE bIsRadio = %u", results.IsRadio());
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ results.CreateChannelGroup(m_pDS->fv("iGroupType").get_asInt(),
+ CPVRChannelsPath(m_pDS->fv("bIsRadio").get_asBool(),
+ m_pDS->fv("sName").get_asString()));
+
+ group->m_iGroupId = m_pDS->fv("idGroup").get_asInt();
+ group->m_iGroupType = m_pDS->fv("iGroupType").get_asInt();
+ group->m_iLastWatched = static_cast<time_t>(m_pDS->fv("iLastWatched").get_asInt());
+ group->m_bHidden = m_pDS->fv("bIsHidden").get_asBool();
+ group->m_iPosition = m_pDS->fv("iPosition").get_asInt();
+ group->m_iLastOpened = static_cast<uint64_t>(m_pDS->fv("iLastOpened").get_asInt64());
+ results.Update(group);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Group '{}' loaded from PVR database", group->GroupName());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ iLoaded++;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Couldn't load channel groups from PVR database. Exception.");
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Couldn't load channel groups from PVR database. Query failed.");
+ }
+
+ return iLoaded;
+}
+
+std::vector<std::shared_ptr<CPVRChannelGroupMember>> CPVRDatabase::Get(
+ const CPVRChannelGroup& group, const std::vector<std::shared_ptr<CPVRClient>>& clients) const
+{
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> results;
+
+ /* invalid group id */
+ if (group.GroupID() < 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel group id: {}", group.GroupID());
+ return results;
+ }
+
+ std::string strQuery =
+ "SELECT map_channelgroups_channels.idChannel, "
+ "map_channelgroups_channels.iChannelNumber, "
+ "map_channelgroups_channels.iSubChannelNumber, "
+ "map_channelgroups_channels.iOrder, "
+ "map_channelgroups_channels.iClientChannelNumber, "
+ "map_channelgroups_channels.iClientSubChannelNumber, "
+ "channels.iClientId, channels.iUniqueId, channels.bIsRadio "
+ "FROM map_channelgroups_channels "
+ "LEFT JOIN channels ON channels.idChannel = map_channelgroups_channels.idChannel "
+ "WHERE map_channelgroups_channels.idGroup = %i ";
+ const std::string clientIds = GetClientIdsSQL(clients);
+ if (!clientIds.empty())
+ strQuery += "AND " + clientIds;
+ strQuery += " ORDER BY map_channelgroups_channels.iChannelNumber";
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ strQuery = PrepareSQL(strQuery, group.GroupID());
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ const auto newMember = std::make_shared<CPVRChannelGroupMember>();
+ newMember->m_iChannelDatabaseID = m_pDS->fv("idChannel").get_asInt();
+ newMember->m_iClientID = m_pDS->fv("iClientId").get_asInt();
+ newMember->m_iChannelUID = m_pDS->fv("iUniqueId").get_asInt();
+ newMember->m_iGroupID = group.GroupID();
+ newMember->m_bIsRadio = m_pDS->fv("bIsRadio").get_asBool();
+ newMember->m_channelNumber = {
+ static_cast<unsigned int>(m_pDS->fv("iChannelNumber").get_asInt()),
+ static_cast<unsigned int>(m_pDS->fv("iSubChannelNumber").get_asInt())};
+ newMember->m_clientChannelNumber = {
+ static_cast<unsigned int>(m_pDS->fv("iClientChannelNumber").get_asInt()),
+ static_cast<unsigned int>(m_pDS->fv("iClientSubChannelNumber").get_asInt())};
+ newMember->m_iOrder = static_cast<int>(m_pDS->fv("iOrder").get_asInt());
+ newMember->SetGroupName(group.GroupName());
+
+ results.emplace_back(newMember);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch(...)
+ {
+ CLog::LogF(LOGERROR, "Failed to get channel group members");
+ }
+ }
+
+ return results;
+}
+
+bool CPVRDatabase::PersistChannels(const CPVRChannelGroup& group)
+{
+ /* invalid group id */
+ if (group.GroupID() < 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel group id: {}", group.GroupID());
+ return false;
+ }
+
+ bool bReturn(true);
+
+ std::shared_ptr<CPVRChannel> channel;
+ for (const auto& groupMember : group.m_members)
+ {
+ channel = groupMember.second->Channel();
+ if (channel->IsChanged() || channel->IsNew())
+ {
+ if (Persist(*channel, false))
+ {
+ channel->Persisted();
+ bReturn = true;
+ }
+ }
+ }
+
+ bReturn &= CommitInsertQueries();
+
+ if (bReturn)
+ {
+ std::string strQuery;
+ std::string strValue;
+ for (const auto& groupMember : group.m_members)
+ {
+ channel = groupMember.second->Channel();
+ strQuery =
+ PrepareSQL("iUniqueId = %i AND iClientId = %i", channel->UniqueID(), channel->ClientID());
+ strValue = GetSingleValue("channels", "idChannel", strQuery);
+ if (!strValue.empty() && StringUtils::IsInteger(strValue))
+ {
+ const int iChannelID = std::atoi(strValue.c_str());
+ channel->SetChannelID(iChannelID);
+ groupMember.second->m_iChannelDatabaseID = iChannelID;
+ }
+ }
+ }
+
+ return bReturn;
+}
+
+bool CPVRDatabase::PersistGroupMembers(const CPVRChannelGroup& group)
+{
+ /* invalid group id */
+ if (group.GroupID() < 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel group id: {}", group.GroupID());
+ return false;
+ }
+
+ bool bReturn = true;
+
+ if (group.HasChannels())
+ {
+ for (const auto& groupMember : group.m_sortedMembers)
+ {
+ if (groupMember->NeedsSave())
+ {
+ if (groupMember->ChannelDatabaseID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel id: {}", groupMember->ChannelDatabaseID());
+ continue;
+ }
+
+ const std::string strWhereClause =
+ PrepareSQL("idChannel = %i AND idGroup = %i AND iChannelNumber = %u AND "
+ "iSubChannelNumber = %u AND "
+ "iOrder = %i AND iClientChannelNumber = %u AND iClientSubChannelNumber = %u",
+ groupMember->ChannelDatabaseID(), group.GroupID(),
+ groupMember->ChannelNumber().GetChannelNumber(),
+ groupMember->ChannelNumber().GetSubChannelNumber(), groupMember->Order(),
+ groupMember->ClientChannelNumber().GetChannelNumber(),
+ groupMember->ClientChannelNumber().GetSubChannelNumber());
+
+ const std::string strValue =
+ GetSingleValue("map_channelgroups_channels", "idChannel", strWhereClause);
+ if (strValue.empty())
+ {
+ const std::string strQuery =
+ PrepareSQL("REPLACE INTO map_channelgroups_channels ("
+ "idGroup, idChannel, iChannelNumber, iSubChannelNumber, iOrder, "
+ "iClientChannelNumber, iClientSubChannelNumber) "
+ "VALUES (%i, %i, %i, %i, %i, %i, %i);",
+ group.GroupID(), groupMember->ChannelDatabaseID(),
+ groupMember->ChannelNumber().GetChannelNumber(),
+ groupMember->ChannelNumber().GetSubChannelNumber(), groupMember->Order(),
+ groupMember->ClientChannelNumber().GetChannelNumber(),
+ groupMember->ClientChannelNumber().GetSubChannelNumber());
+ QueueInsertQuery(strQuery);
+ }
+ }
+ }
+
+ bReturn = CommitInsertQueries();
+
+ if (bReturn)
+ {
+ for (const auto& groupMember : group.m_sortedMembers)
+ {
+ groupMember->SetSaved();
+ }
+ }
+ }
+
+ return bReturn;
+}
+
+/********** Client methods **********/
+
+bool CPVRDatabase::ResetEPG()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("UPDATE channels SET idEpg = 0");
+ return ExecuteQuery(strQuery);
+}
+
+bool CPVRDatabase::Persist(CPVRChannelGroup& group)
+{
+ bool bReturn(false);
+ if (group.GroupName().empty())
+ {
+ CLog::LogF(LOGERROR, "Empty group name");
+ return bReturn;
+ }
+
+ std::string strQuery;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (group.HasChanges() || group.IsNew())
+ {
+ /* insert a new entry when this is a new group, or replace the existing one otherwise */
+ if (group.IsNew())
+ strQuery =
+ PrepareSQL("INSERT INTO channelgroups (bIsRadio, iGroupType, sName, iLastWatched, "
+ "bIsHidden, iPosition, iLastOpened) VALUES (%i, %i, '%s', %u, %i, %i, %llu)",
+ (group.IsRadio() ? 1 : 0), group.GroupType(), group.GroupName().c_str(),
+ static_cast<unsigned int>(group.LastWatched()), group.IsHidden(),
+ group.GetPosition(), group.LastOpened());
+ else
+ strQuery = PrepareSQL(
+ "REPLACE INTO channelgroups (idGroup, bIsRadio, iGroupType, sName, iLastWatched, "
+ "bIsHidden, iPosition, iLastOpened) VALUES (%i, %i, %i, '%s', %u, %i, %i, %llu)",
+ group.GroupID(), (group.IsRadio() ? 1 : 0), group.GroupType(), group.GroupName().c_str(),
+ static_cast<unsigned int>(group.LastWatched()), group.IsHidden(), group.GetPosition(),
+ group.LastOpened());
+
+ bReturn = ExecuteQuery(strQuery);
+
+ // set the group ID for new groups
+ if (bReturn && group.IsNew())
+ group.SetGroupID(static_cast<int>(m_pDS->lastinsertid()));
+ }
+ else
+ bReturn = true;
+
+ /* only persist the channel data for the internal groups */
+ if (group.IsInternalGroup())
+ bReturn &= PersistChannels(group);
+
+ /* persist the group member entries */
+ if (bReturn)
+ bReturn = PersistGroupMembers(group);
+
+ return bReturn;
+}
+
+bool CPVRDatabase::Persist(CPVRChannel& channel, bool bCommit)
+{
+ bool bReturn(false);
+
+ /* invalid channel */
+ if (channel.UniqueID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel uid: {}", channel.UniqueID());
+ return bReturn;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // Note: Do not use channel.ChannelID value to check presence of channel in channels table. It might not yet be set correctly.
+ std::string strQuery =
+ PrepareSQL("iUniqueId = %i AND iClientId = %i", channel.UniqueID(), channel.ClientID());
+ const std::string strValue = GetSingleValue("channels", "idChannel", strQuery);
+ if (strValue.empty())
+ {
+ /* new channel */
+ strQuery = PrepareSQL(
+ "INSERT INTO channels ("
+ "iUniqueId, bIsRadio, bIsHidden, bIsUserSetIcon, bIsUserSetName, bIsLocked, "
+ "sIconPath, sChannelName, bIsVirtual, bEPGEnabled, sEPGScraper, iLastWatched, iClientId, "
+ "idEpg, bHasArchive, iClientProviderUid, bIsUserSetHidden) "
+ "VALUES (%i, %i, %i, %i, %i, %i, '%s', '%s', %i, %i, '%s', %u, %i, %i, %i, %i, %i)",
+ channel.UniqueID(), (channel.IsRadio() ? 1 : 0), (channel.IsHidden() ? 1 : 0),
+ (channel.IsUserSetIcon() ? 1 : 0), (channel.IsUserSetName() ? 1 : 0),
+ (channel.IsLocked() ? 1 : 0), channel.IconPath().c_str(), channel.ChannelName().c_str(), 0,
+ (channel.EPGEnabled() ? 1 : 0), channel.EPGScraper().c_str(),
+ static_cast<unsigned int>(channel.LastWatched()), channel.ClientID(), channel.EpgID(),
+ channel.HasArchive(), channel.ClientProviderUid(), channel.IsUserSetHidden() ? 1 : 0);
+ }
+ else
+ {
+ /* update channel */
+ strQuery = PrepareSQL(
+ "REPLACE INTO channels ("
+ "iUniqueId, bIsRadio, bIsHidden, bIsUserSetIcon, bIsUserSetName, bIsLocked, "
+ "sIconPath, sChannelName, bIsVirtual, bEPGEnabled, sEPGScraper, iLastWatched, iClientId, "
+ "idChannel, idEpg, bHasArchive, iClientProviderUid, bIsUserSetHidden) "
+ "VALUES (%i, %i, %i, %i, %i, %i, '%s', '%s', %i, %i, '%s', %u, %i, %s, %i, %i, %i, %i)",
+ channel.UniqueID(), (channel.IsRadio() ? 1 : 0), (channel.IsHidden() ? 1 : 0),
+ (channel.IsUserSetIcon() ? 1 : 0), (channel.IsUserSetName() ? 1 : 0),
+ (channel.IsLocked() ? 1 : 0), channel.ClientIconPath().c_str(),
+ channel.ChannelName().c_str(), 0, (channel.EPGEnabled() ? 1 : 0),
+ channel.EPGScraper().c_str(), static_cast<unsigned int>(channel.LastWatched()),
+ channel.ClientID(), strValue.c_str(), channel.EpgID(), channel.HasArchive(),
+ channel.ClientProviderUid(), channel.IsUserSetHidden() ? 1 : 0);
+ }
+
+ if (QueueInsertQuery(strQuery))
+ {
+ bReturn = true;
+
+ if (bCommit)
+ bReturn = CommitInsertQueries();
+ }
+
+ return bReturn;
+}
+
+bool CPVRDatabase::UpdateLastWatched(const CPVRChannel& channel)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("UPDATE channels SET iLastWatched = %u WHERE idChannel = %i",
+ static_cast<unsigned int>(channel.LastWatched()), channel.ChannelID());
+ return ExecuteQuery(strQuery);
+}
+
+bool CPVRDatabase::UpdateLastWatched(const CPVRChannelGroup& group)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("UPDATE channelgroups SET iLastWatched = %u WHERE idGroup = %i",
+ static_cast<unsigned int>(group.LastWatched()), group.GroupID());
+ return ExecuteQuery(strQuery);
+}
+
+bool CPVRDatabase::UpdateLastOpened(const CPVRChannelGroup& group)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("UPDATE channelgroups SET iLastOpened = %llu WHERE idGroup = %i",
+ group.LastOpened(), group.GroupID());
+ return ExecuteQuery(strQuery);
+}
+
+/********** Timer methods **********/
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRDatabase::GetTimers(
+ CPVRTimers& timers, const std::vector<std::shared_ptr<CPVRClient>>& clients) const
+{
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> result;
+
+ std::string strQuery = "SELECT * FROM timers ";
+ const std::string clientIds = GetClientIdsSQL(clients);
+ if (!clientIds.empty())
+ strQuery += "WHERE " + clientIds;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ strQuery = PrepareSQL(strQuery);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ std::shared_ptr<CPVRTimerInfoTag> newTag(new CPVRTimerInfoTag());
+
+ newTag->m_iClientIndex = -m_pDS->fv("iClientIndex").get_asInt();
+ newTag->m_iParentClientIndex = m_pDS->fv("iParentClientIndex").get_asInt();
+ newTag->m_iClientId = m_pDS->fv("iClientId").get_asInt();
+ newTag->SetTimerType(CPVRTimerType::CreateFromIds(m_pDS->fv("iTimerType").get_asInt(), -1));
+ newTag->m_state = static_cast<PVR_TIMER_STATE>(m_pDS->fv("iState").get_asInt());
+ newTag->m_strTitle = m_pDS->fv("sTitle").get_asString().c_str();
+ newTag->m_iClientChannelUid = m_pDS->fv("iClientChannelUid").get_asInt();
+ newTag->m_strSeriesLink = m_pDS->fv("sSeriesLink").get_asString().c_str();
+ newTag->SetStartFromUTC(CDateTime::FromDBDateTime(m_pDS->fv("sStartTime").get_asString().c_str()));
+ newTag->m_bStartAnyTime = m_pDS->fv("bStartAnyTime").get_asBool();
+ newTag->SetEndFromUTC(CDateTime::FromDBDateTime(m_pDS->fv("sEndTime").get_asString().c_str()));
+ newTag->m_bEndAnyTime = m_pDS->fv("bEndAnyTime").get_asBool();
+ newTag->SetFirstDayFromUTC(CDateTime::FromDBDateTime(m_pDS->fv("sFirstDay").get_asString().c_str()));
+ newTag->m_iWeekdays = m_pDS->fv("iWeekdays").get_asInt();
+ newTag->m_iEpgUid = m_pDS->fv("iEpgUid").get_asInt();
+ newTag->m_iMarginStart = m_pDS->fv("iMarginStart").get_asInt();
+ newTag->m_iMarginEnd = m_pDS->fv("iMarginEnd").get_asInt();
+ newTag->m_strEpgSearchString = m_pDS->fv("sEpgSearchString").get_asString().c_str();
+ newTag->m_bFullTextEpgSearch = m_pDS->fv("bFullTextEpgSearch").get_asBool();
+ newTag->m_iPreventDupEpisodes = m_pDS->fv("iPreventDuplicates").get_asInt();
+ newTag->m_iPriority = m_pDS->fv("iPrority").get_asInt();
+ newTag->m_iLifetime = m_pDS->fv("iLifetime").get_asInt();
+ newTag->m_iMaxRecordings = m_pDS->fv("iMaxRecordings").get_asInt();
+ newTag->m_iRecordingGroup = m_pDS->fv("iRecordingGroup").get_asInt();
+ newTag->UpdateSummary();
+
+ result.emplace_back(newTag);
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load timer data from the database");
+ }
+ }
+ return result;
+}
+
+bool CPVRDatabase::Persist(CPVRTimerInfoTag& timer)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // insert a new entry if this is a new timer, or replace the existing one otherwise
+ std::string strQuery;
+ if (timer.m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX)
+ strQuery = PrepareSQL("INSERT INTO timers "
+ "(iParentClientIndex, iClientId, iTimerType, iState, sTitle, iClientChannelUid, sSeriesLink, sStartTime,"
+ " bStartAnyTime, sEndTime, bEndAnyTime, sFirstDay, iWeekdays, iEpgUid, iMarginStart, iMarginEnd,"
+ " sEpgSearchString, bFullTextEpgSearch, iPreventDuplicates, iPrority, iLifetime, iMaxRecordings, iRecordingGroup) "
+ "VALUES (%i, %i, %u, %i, '%s', %i, '%s', '%s', %i, '%s', %i, '%s', %i, %u, %i, %i, '%s', %i, %i, %i, %i, %i, %i);",
+ timer.m_iParentClientIndex, timer.m_iClientId, timer.GetTimerType()->GetTypeId(), timer.m_state,
+ timer.Title().c_str(), timer.m_iClientChannelUid, timer.SeriesLink().c_str(),
+ timer.StartAsUTC().GetAsDBDateTime().c_str(), timer.m_bStartAnyTime ? 1 : 0,
+ timer.EndAsUTC().GetAsDBDateTime().c_str(), timer.m_bEndAnyTime ? 1 : 0,
+ timer.FirstDayAsUTC().GetAsDBDateTime().c_str(), timer.m_iWeekdays, timer.UniqueBroadcastID(),
+ timer.m_iMarginStart, timer.m_iMarginEnd, timer.m_strEpgSearchString.c_str(), timer.m_bFullTextEpgSearch ? 1 : 0,
+ timer.m_iPreventDupEpisodes, timer.m_iPriority, timer.m_iLifetime, timer.m_iMaxRecordings, timer.m_iRecordingGroup);
+ else
+ strQuery = PrepareSQL("REPLACE INTO timers "
+ "(iClientIndex,"
+ " iParentClientIndex, iClientId, iTimerType, iState, sTitle, iClientChannelUid, sSeriesLink, sStartTime,"
+ " bStartAnyTime, sEndTime, bEndAnyTime, sFirstDay, iWeekdays, iEpgUid, iMarginStart, iMarginEnd,"
+ " sEpgSearchString, bFullTextEpgSearch, iPreventDuplicates, iPrority, iLifetime, iMaxRecordings, iRecordingGroup) "
+ "VALUES (%i, %i, %i, %u, %i, '%s', %i, '%s', '%s', %i, '%s', %i, '%s', %i, %u, %i, %i, '%s', %i, %i, %i, %i, %i, %i);",
+ -timer.m_iClientIndex,
+ timer.m_iParentClientIndex, timer.m_iClientId, timer.GetTimerType()->GetTypeId(), timer.m_state,
+ timer.Title().c_str(), timer.m_iClientChannelUid, timer.SeriesLink().c_str(),
+ timer.StartAsUTC().GetAsDBDateTime().c_str(), timer.m_bStartAnyTime ? 1 : 0,
+ timer.EndAsUTC().GetAsDBDateTime().c_str(), timer.m_bEndAnyTime ? 1 : 0,
+ timer.FirstDayAsUTC().GetAsDBDateTime().c_str(), timer.m_iWeekdays, timer.UniqueBroadcastID(),
+ timer.m_iMarginStart, timer.m_iMarginEnd, timer.m_strEpgSearchString.c_str(), timer.m_bFullTextEpgSearch ? 1 : 0,
+ timer.m_iPreventDupEpisodes, timer.m_iPriority, timer.m_iLifetime, timer.m_iMaxRecordings, timer.m_iRecordingGroup);
+
+ bool bReturn = ExecuteQuery(strQuery);
+
+ // set the client index for just inserted timers
+ if (bReturn && timer.m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX)
+ {
+ // index must be negative for local timers!
+ timer.m_iClientIndex = -static_cast<int>(m_pDS->lastinsertid());
+ }
+
+ return bReturn;
+}
+
+bool CPVRDatabase::Delete(const CPVRTimerInfoTag& timer)
+{
+ if (timer.m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX)
+ return false;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting timer '{}' from the database", timer.m_iClientIndex);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("iClientIndex = '%i'", -timer.m_iClientIndex));
+
+ return DeleteValues("timers", filter);
+}
+
+bool CPVRDatabase::DeleteTimers()
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all timers from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("timers");
+}
diff --git a/xbmc/pvr/PVRDatabase.h b/xbmc/pvr/PVRDatabase.h
new file mode 100644
index 0000000..1e9cbf5
--- /dev/null
+++ b/xbmc/pvr/PVRDatabase.h
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "dbwrappers/Database.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <vector>
+
+namespace PVR
+{
+ class CPVRChannel;
+ class CPVRChannelGroup;
+ class CPVRChannelGroupMember;
+ class CPVRChannelGroups;
+ class CPVRProvider;
+ class CPVRProviders;
+ class CPVRClient;
+ class CPVRTimerInfoTag;
+ class CPVRTimers;
+
+ /** The PVR database */
+
+ static constexpr int CHANNEL_COMMIT_QUERY_COUNT_LIMIT = 10000;
+
+ class CPVRDatabase : public CDatabase
+ {
+ public:
+ /*!
+ * @brief Create a new instance of the PVR database.
+ */
+ CPVRDatabase() = default;
+ ~CPVRDatabase() override = default;
+
+ /*!
+ * @brief Open the database.
+ * @return True if it was opened successfully, false otherwise.
+ */
+ bool Open() override;
+
+ /*!
+ * @brief Close the database.
+ */
+ void Close() override;
+
+ /*!
+ * @brief Lock the database.
+ */
+ void Lock();
+
+ /*!
+ * @brief Unlock the database.
+ */
+ void Unlock();
+
+ /*!
+ * @brief Get the minimal database version that is required to operate correctly.
+ * @return The minimal database version.
+ */
+ int GetSchemaVersion() const override { return 40; }
+
+ /*!
+ * @brief Get the default sqlite database filename.
+ * @return The default filename.
+ */
+ const char* GetBaseDBName() const override { return "TV"; }
+
+ /*! @name Client methods */
+ //@{
+
+ /*!
+ * @brief Remove all client entries from the database.
+ * @return True if all client entries were removed, false otherwise.
+ */
+ bool DeleteClients();
+
+ /*!
+ * @brief Add or update a client entry in the database
+ * @param client The client to persist.
+ * @return True when persisted, false otherwise.
+ */
+ bool Persist(const CPVRClient& client);
+
+ /*!
+ * @brief Remove a client entry from the database
+ * @param client The client to remove.
+ * @return True if the client was removed, false otherwise.
+ */
+ bool Delete(const CPVRClient& client);
+
+ /*!
+ * @brief Get the priority for a given client from the database.
+ * @param client The client.
+ * @return The priority.
+ */
+ int GetPriority(const CPVRClient& client);
+
+ /*! @name Channel methods */
+ //@{
+
+ /*!
+ * @brief Remove all channels from the database.
+ * @return True if all channels were removed, false otherwise.
+ */
+ bool DeleteChannels();
+
+ /*!
+ * @brief Get channels from the database.
+ * @param bRadio Whether to fetch radio or TV channels.
+ * @param clients The PVR clients the channels should be loaded for. Leave empty for all clients.
+ * @param results The container for the channels.
+ * @return The number of channels loaded.
+ */
+ int Get(bool bRadio,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& results) const;
+
+ /*!
+ * @brief Add or update a channel entry in the database
+ * @param channel The channel to persist.
+ * @param bCommit queue only or queue and commit
+ * @return True when persisted or queued, false otherwise.
+ */
+ bool Persist(CPVRChannel& channel, bool bCommit);
+
+ /*!
+ * @brief Remove a channel entry from the database
+ * @param channel The channel to remove.
+ * @return True if the channel was removed, false otherwise.
+ */
+ bool QueueDeleteQuery(const CPVRChannel& channel);
+
+ //@}
+
+ /*! @name Channel group member methods */
+ //@{
+
+ /*!
+ * @brief Remove a channel group member entry from the database
+ * @param groupMember The group member to remove.
+ * @return True if the member was removed, false otherwise.
+ */
+ bool QueueDeleteQuery(const CPVRChannelGroupMember& groupMember);
+
+ //@}
+
+ /*! @name Channel provider methods */
+ //@{
+
+ /*!
+ * @brief Remove all providers from the database.
+ * @return True if all providers were removed, false otherwise.
+ */
+ bool DeleteProviders();
+
+ /*!
+ * @brief Add or update a provider entry in the database
+ * @param provider The provider to persist.
+ * @param updateRecord True if record to be updated, false for insert
+ * @return True when persisted, false otherwise.
+ */
+ bool Persist(CPVRProvider& provider, bool updateRecord = false);
+
+ /*!
+ * @brief Remove a provider entry from the database
+ * @param provider The provider to remove.
+ * @return True if the provider was removed, false otherwise.
+ */
+ bool Delete(const CPVRProvider& provider);
+
+ /*!
+ * @brief Get the list of providers from the database
+ * @param results The providers to store the results in.
+ * @param clients The PVR clients the providers should be loaded for. Leave empty for all clients.
+ * @return The amount of providers that were added.
+ */
+ bool Get(CPVRProviders& results, const std::vector<std::shared_ptr<CPVRClient>>& clients) const;
+
+ /*!
+ * @brief Get the maximum provider id in the database
+ * @return The maximum provider id in the database
+ */
+ int GetMaxProviderId();
+
+ //@}
+
+ /*! @name Channel group methods */
+ //@{
+
+ /*!
+ * @brief Remove all channel groups from the database
+ * @return True if all channel groups were removed.
+ */
+ bool DeleteChannelGroups();
+
+ /*!
+ * @brief Delete a channel group and all its members from the database.
+ * @param group The group to delete.
+ * @return True if the group was deleted successfully, false otherwise.
+ */
+ bool Delete(const CPVRChannelGroup& group);
+
+ /*!
+ * @brief Get the channel groups.
+ * @param results The container to store the results in.
+ * @return The number of groups loaded.
+ */
+ int Get(CPVRChannelGroups& results) const;
+
+ /*!
+ * @brief Get the members of a channel group.
+ * @param group The group to get the members for.
+ * @param clients The PVR clients the group members should be loaded for. Leave empty for all clients.
+ * @return The group members.
+ */
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> Get(
+ const CPVRChannelGroup& group,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients) const;
+
+ /*!
+ * @brief Add or update a channel group entry in the database.
+ * @param group The group to persist.
+ * @return True if the group was persisted successfully, false otherwise.
+ */
+ bool Persist(CPVRChannelGroup& group);
+
+ /*!
+ * @brief Reset all epg ids to 0
+ * @return True when reset, false otherwise.
+ */
+ bool ResetEPG();
+
+ /*! @name Timer methods */
+ //@{
+
+ /*!
+ * @brief Get the timers.
+ * @param timers The container for the timers.
+ * @param clients The PVR clients the timers should be loaded for. Leave empty for all clients.
+ * @return The timers.
+ */
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetTimers(
+ CPVRTimers& timers, const std::vector<std::shared_ptr<CPVRClient>>& clients) const;
+
+ /*!
+ * @brief Add or update a timer entry in the database
+ * @param channel The timer to persist.
+ * @return True if persisted, false otherwise.
+ */
+ bool Persist(CPVRTimerInfoTag& timer);
+
+ /*!
+ * @brief Remove a timer from the database
+ * @param timer The timer to remove.
+ * @return True if the timer was removed, false otherwise.
+ */
+ bool Delete(const CPVRTimerInfoTag& timer);
+
+ /*!
+ * @brief Remove all timer entries from the database.
+ * @return True if all timer entries were removed, false otherwise.
+ */
+ bool DeleteTimers();
+ //@}
+
+ /*! @name Client methods */
+ //@{
+
+ /*!
+ * @brief Updates the last watched timestamp for the channel
+ * @param channel the channel
+ * @return whether the update was successful
+ */
+ bool UpdateLastWatched(const CPVRChannel& channel);
+
+ /*!
+ * @brief Updates the last watched timestamp for the channel group
+ * @param group the group
+ * @return whether the update was successful
+ */
+ bool UpdateLastWatched(const CPVRChannelGroup& group);
+ //@}
+
+ /*!
+ * @brief Updates the last opened timestamp for the channel group
+ * @param group the group
+ * @return whether the update was successful
+ */
+ bool UpdateLastOpened(const CPVRChannelGroup& group);
+ //@}
+
+ private:
+ /*!
+ * @brief Create the PVR database tables.
+ */
+ void CreateTables() override;
+ void CreateAnalytics() override;
+ /*!
+ * @brief Update an old version of the database.
+ * @param version The version to update the database from.
+ */
+ void UpdateTables(int version) override;
+ int GetMinSchemaVersion() const override { return 11; }
+
+ bool PersistGroupMembers(const CPVRChannelGroup& group);
+
+ bool PersistChannels(const CPVRChannelGroup& group);
+
+ bool RemoveChannelsFromGroup(const CPVRChannelGroup& group);
+
+ mutable CCriticalSection m_critSection;
+ };
+}
diff --git a/xbmc/pvr/PVREdl.cpp b/xbmc/pvr/PVREdl.cpp
new file mode 100644
index 0000000..72a3ee4
--- /dev/null
+++ b/xbmc/pvr/PVREdl.cpp
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVREdl.h"
+
+#include "FileItem.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_edl.h"
+#include "cores/EdlEdit.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "utils/log.h"
+
+namespace PVR
+{
+
+std::vector<EDL::Edit> CPVREdl::GetEdits(const CFileItem& item)
+{
+ std::vector<PVR_EDL_ENTRY> edl;
+
+ if (item.HasPVRRecordingInfoTag())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Reading EDL for recording: {}",
+ item.GetPVRRecordingInfoTag()->m_strTitle);
+ edl = item.GetPVRRecordingInfoTag()->GetEdl();
+ }
+ else if (item.HasEPGInfoTag())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Reading EDL for EPG tag: {}", item.GetEPGInfoTag()->Title());
+ edl = item.GetEPGInfoTag()->GetEdl();
+ }
+
+ std::vector<EDL::Edit> editlist;
+ for (const auto& entry : edl)
+ {
+ EDL::Edit edit;
+ edit.start = entry.start;
+ edit.end = entry.end;
+
+ switch (entry.type)
+ {
+ case PVR_EDL_TYPE_CUT:
+ edit.action = EDL::Action::CUT;
+ break;
+ case PVR_EDL_TYPE_MUTE:
+ edit.action = EDL::Action::MUTE;
+ break;
+ case PVR_EDL_TYPE_SCENE:
+ edit.action = EDL::Action::SCENE;
+ break;
+ case PVR_EDL_TYPE_COMBREAK:
+ edit.action = EDL::Action::COMM_BREAK;
+ break;
+ default:
+ CLog::LogF(LOGWARNING, "Ignoring entry of unknown EDL type: {}", entry.type);
+ continue;
+ }
+
+ editlist.emplace_back(edit);
+ }
+ return editlist;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVREdl.h b/xbmc/pvr/PVREdl.h
new file mode 100644
index 0000000..7440b08
--- /dev/null
+++ b/xbmc/pvr/PVREdl.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <vector>
+
+class CFileItem;
+
+namespace EDL
+{
+struct Edit;
+}
+
+namespace PVR
+{
+
+class CPVREdl
+{
+public:
+ /*!
+ * @brief Get the EDL edits for the given item.
+ * @param item The item.
+ * @return The EDL edits or an empty vector if no edits exist.
+ */
+ static std::vector<EDL::Edit> GetEdits(const CFileItem& item);
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVREventLogJob.cpp b/xbmc/pvr/PVREventLogJob.cpp
new file mode 100644
index 0000000..b70ad1c
--- /dev/null
+++ b/xbmc/pvr/PVREventLogJob.cpp
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVREventLogJob.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+
+namespace PVR
+{
+
+CPVREventLogJob::CPVREventLogJob(bool bNotifyUser,
+ EventLevel eLevel,
+ const std::string& label,
+ const std::string& msg,
+ const std::string& icon)
+{
+ AddEvent(bNotifyUser, eLevel, label, msg, icon);
+}
+
+void CPVREventLogJob::AddEvent(bool bNotifyUser,
+ EventLevel eLevel,
+ const std::string& label,
+ const std::string& msg,
+ const std::string& icon)
+{
+ m_events.emplace_back(Event(bNotifyUser, eLevel, label, msg, icon));
+}
+
+bool CPVREventLogJob::DoWork()
+{
+ for (const auto& event : m_events)
+ {
+ if (event.m_bNotifyUser)
+ CGUIDialogKaiToast::QueueNotification(event.m_eLevel == EventLevel::Error
+ ? CGUIDialogKaiToast::Error
+ : CGUIDialogKaiToast::Info,
+ event.m_label, event.m_msg, 5000, true);
+
+ // Write event log entry.
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(std::make_shared<CNotificationEvent>(event.m_label, event.m_msg, event.m_icon,
+ event.m_eLevel));
+ }
+ return true;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVREventLogJob.h b/xbmc/pvr/PVREventLogJob.h
new file mode 100644
index 0000000..a5f0be7
--- /dev/null
+++ b/xbmc/pvr/PVREventLogJob.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "events/EventLog.h"
+#include "utils/Job.h"
+
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+class CPVREventLogJob : public CJob
+{
+public:
+ CPVREventLogJob() = default;
+
+ CPVREventLogJob(bool bNotifyUser,
+ EventLevel eLevel,
+ const std::string& label,
+ const std::string& msg,
+ const std::string& icon);
+
+ ~CPVREventLogJob() override = default;
+ const char* GetType() const override { return "pvr-eventlog-job"; }
+
+ void AddEvent(bool bNotifyUser,
+ EventLevel eLevel,
+ const std::string& label,
+ const std::string& msg,
+ const std::string& icon);
+
+ bool DoWork() override;
+
+private:
+ struct Event
+ {
+ bool m_bNotifyUser;
+ EventLevel m_eLevel{EventLevel::Information};
+ std::string m_label;
+ std::string m_msg;
+ std::string m_icon;
+
+ Event(bool bNotifyUser,
+ EventLevel elevel,
+ const std::string& label,
+ const std::string& msg,
+ const std::string& icon)
+ : m_bNotifyUser(bNotifyUser), m_eLevel(elevel), m_label(label), m_msg(msg), m_icon(icon)
+ {
+ }
+ };
+
+ std::vector<Event> m_events;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/PVRItem.cpp b/xbmc/pvr/PVRItem.cpp
new file mode 100644
index 0000000..5eee911
--- /dev/null
+++ b/xbmc/pvr/PVRItem.cpp
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRItem.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+
+namespace PVR
+{
+std::shared_ptr<CPVREpgInfoTag> CPVRItem::GetEpgInfoTag() const
+{
+ if (m_item->IsEPG())
+ {
+ return m_item->GetEPGInfoTag();
+ }
+ else if (m_item->IsPVRChannel())
+ {
+ return m_item->GetPVRChannelInfoTag()->GetEPGNow();
+ }
+ else if (m_item->IsPVRTimer())
+ {
+ return m_item->GetPVRTimerInfoTag()->GetEpgInfoTag();
+ }
+ else if (URIUtils::IsPVR(m_item->GetDynPath()))
+ {
+ CLog::LogF(LOGERROR, "Unsupported item type!");
+ }
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRItem::GetNextEpgInfoTag() const
+{
+ if (m_item->IsEPG())
+ {
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(
+ m_item->GetEPGInfoTag());
+ if (channel)
+ return channel->GetEPGNext();
+ }
+ else if (m_item->IsPVRChannel())
+ {
+ return m_item->GetPVRChannelInfoTag()->GetEPGNext();
+ }
+ else if (m_item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVRChannel> channel = m_item->GetPVRTimerInfoTag()->Channel();
+ if (channel)
+ return channel->GetEPGNext();
+ }
+ else if (URIUtils::IsPVR(m_item->GetDynPath()))
+ {
+ CLog::LogF(LOGERROR, "Unsupported item type!");
+ }
+ return {};
+}
+
+std::shared_ptr<CPVRChannel> CPVRItem::GetChannel() const
+{
+ if (m_item->IsPVRChannel())
+ {
+ return m_item->GetPVRChannelInfoTag();
+ }
+ else if (m_item->IsEPG())
+ {
+ return CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(
+ m_item->GetEPGInfoTag());
+ }
+ else if (m_item->IsPVRTimer())
+ {
+ return m_item->GetPVRTimerInfoTag()->Channel();
+ }
+ else if (m_item->IsPVRRecording())
+ {
+ return m_item->GetPVRRecordingInfoTag()->Channel();
+ }
+ else if (URIUtils::IsPVR(m_item->GetDynPath()))
+ {
+ CLog::LogF(LOGERROR, "Unsupported item type!");
+ }
+ return {};
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRItem::GetTimerInfoTag() const
+{
+ if (m_item->IsPVRTimer())
+ {
+ return m_item->GetPVRTimerInfoTag();
+ }
+ else if (m_item->IsEPG())
+ {
+ return CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(m_item->GetEPGInfoTag());
+ }
+ else if (m_item->IsPVRChannel())
+ {
+ return CServiceBroker::GetPVRManager().Timers()->GetActiveTimerForChannel(
+ m_item->GetPVRChannelInfoTag());
+ }
+ else if (URIUtils::IsPVR(m_item->GetDynPath()))
+ {
+ CLog::LogF(LOGERROR, "Unsupported item type!");
+ }
+ return {};
+}
+
+std::shared_ptr<CPVRRecording> CPVRItem::GetRecording() const
+{
+ if (m_item->IsPVRRecording())
+ {
+ return m_item->GetPVRRecordingInfoTag();
+ }
+ else if (m_item->IsEPG())
+ {
+ return CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(
+ m_item->GetEPGInfoTag());
+ }
+ else if (URIUtils::IsPVR(m_item->GetDynPath()))
+ {
+ CLog::LogF(LOGERROR, "Unsupported item type!");
+ }
+ return {};
+}
+
+bool CPVRItem::IsRadio() const
+{
+ if (m_item->IsPVRChannel())
+ {
+ return m_item->GetPVRChannelInfoTag()->IsRadio();
+ }
+ else if (m_item->IsEPG())
+ {
+ return m_item->GetEPGInfoTag()->IsRadio();
+ }
+ else if (m_item->IsPVRRecording())
+ {
+ return m_item->GetPVRRecordingInfoTag()->IsRadio();
+ }
+ else if (m_item->IsPVRTimer())
+ {
+ return m_item->GetPVRTimerInfoTag()->IsRadio();
+ }
+ else if (URIUtils::IsPVR(m_item->GetDynPath()))
+ {
+ CLog::LogF(LOGERROR, "Unsupported item type!");
+ }
+ return false;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRItem.h b/xbmc/pvr/PVRItem.h
new file mode 100644
index 0000000..e41ca15
--- /dev/null
+++ b/xbmc/pvr/PVRItem.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <memory>
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVREpgInfoTag;
+class CPVRRecording;
+class CPVRTimerInfoTag;
+
+class CPVRItem
+{
+public:
+ explicit CPVRItem(const std::shared_ptr<CFileItem>& item) : m_item(item.get()) {}
+ explicit CPVRItem(const CFileItem* item) : m_item(item) {}
+ explicit CPVRItem(const CFileItem& item) : m_item(&item) {}
+
+ std::shared_ptr<CPVREpgInfoTag> GetEpgInfoTag() const;
+ std::shared_ptr<CPVREpgInfoTag> GetNextEpgInfoTag() const;
+ std::shared_ptr<CPVRChannel> GetChannel() const;
+ std::shared_ptr<CPVRTimerInfoTag> GetTimerInfoTag() const;
+ std::shared_ptr<CPVRRecording> GetRecording() const;
+
+ bool IsRadio() const;
+
+private:
+ const CFileItem* m_item;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRManager.cpp b/xbmc/pvr/PVRManager.cpp
new file mode 100644
index 0000000..715397e
--- /dev/null
+++ b/xbmc/pvr/PVRManager.cpp
@@ -0,0 +1,1029 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRManager.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRComponentRegistration.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupInternal.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIChannelIconUpdater.h"
+#include "pvr/guilib/PVRGUIProgressHandler.h"
+#include "pvr/guilib/guiinfo/PVRGUIInfo.h"
+#include "pvr/providers/PVRProvider.h"
+#include "pvr/providers/PVRProviders.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/Settings.h"
+#include "utils/JobManager.h"
+#include "utils/Stopwatch.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+using namespace std::chrono_literals;
+
+namespace
+{
+
+class CPVRJob
+{
+public:
+ virtual ~CPVRJob() = default;
+
+ virtual bool DoWork() = 0;
+ virtual const std::string GetType() const = 0;
+
+protected:
+};
+
+template<typename F>
+class CPVRLambdaJob : public CPVRJob
+{
+public:
+ CPVRLambdaJob() = delete;
+ CPVRLambdaJob(const std::string& type, F&& f) : m_type(type), m_f(std::forward<F>(f)) {}
+
+ bool DoWork() override
+ {
+ m_f();
+ return true;
+ }
+
+ const std::string GetType() const override { return m_type; }
+
+private:
+ std::string m_type;
+ F m_f;
+};
+
+} // unnamed namespace
+
+namespace PVR
+{
+
+class CPVRManagerJobQueue
+{
+public:
+ CPVRManagerJobQueue() : m_triggerEvent(false) {}
+
+ void Start();
+ void Stop();
+ void Clear();
+
+ template<typename F>
+ void Append(const std::string& type, F&& f)
+ {
+ AppendJob(new CPVRLambdaJob<F>(type, std::forward<F>(f)));
+ }
+
+ void ExecutePendingJobs();
+
+ bool WaitForJobs(unsigned int milliSeconds)
+ {
+ return m_triggerEvent.Wait(std::chrono::milliseconds(milliSeconds));
+ }
+
+private:
+ void AppendJob(CPVRJob* job);
+
+ CCriticalSection m_critSection;
+ CEvent m_triggerEvent;
+ std::vector<CPVRJob*> m_pendingUpdates;
+ bool m_bStopped = true;
+};
+
+} // namespace PVR
+
+void CPVRManagerJobQueue::Start()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bStopped = false;
+ m_triggerEvent.Set();
+}
+
+void CPVRManagerJobQueue::Stop()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bStopped = true;
+ m_triggerEvent.Reset();
+}
+
+void CPVRManagerJobQueue::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (CPVRJob* updateJob : m_pendingUpdates)
+ delete updateJob;
+
+ m_pendingUpdates.clear();
+ m_triggerEvent.Set();
+}
+
+void CPVRManagerJobQueue::AppendJob(CPVRJob* job)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // check for another pending job of given type...
+ if (std::any_of(m_pendingUpdates.cbegin(), m_pendingUpdates.cend(),
+ [job](CPVRJob* updateJob) { return updateJob->GetType() == job->GetType(); }))
+ {
+ delete job;
+ return;
+ }
+
+ m_pendingUpdates.push_back(job);
+ m_triggerEvent.Set();
+}
+
+void CPVRManagerJobQueue::ExecutePendingJobs()
+{
+ std::vector<CPVRJob*> pendingUpdates;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bStopped)
+ return;
+
+ pendingUpdates = std::move(m_pendingUpdates);
+ m_triggerEvent.Reset();
+ }
+
+ CPVRJob* job = nullptr;
+ while (!pendingUpdates.empty())
+ {
+ job = pendingUpdates.front();
+ pendingUpdates.erase(pendingUpdates.begin());
+
+ job->DoWork();
+ delete job;
+ }
+}
+
+CPVRManager::CPVRManager()
+ : CThread("PVRManager"),
+ m_providers(new CPVRProviders),
+ m_channelGroups(new CPVRChannelGroupsContainer),
+ m_recordings(new CPVRRecordings),
+ m_timers(new CPVRTimers),
+ m_addons(new CPVRClients),
+ m_guiInfo(new CPVRGUIInfo),
+ m_components(new CPVRComponentRegistration),
+ m_epgContainer(m_events),
+ m_pendingUpdates(new CPVRManagerJobQueue),
+ m_database(new CPVRDatabase),
+ m_parentalTimer(new CStopWatch),
+ m_playbackState(new CPVRPlaybackState),
+ m_settings({CSettings::SETTING_PVRPOWERMANAGEMENT_ENABLED,
+ CSettings::SETTING_PVRPOWERMANAGEMENT_SETWAKEUPCMD,
+ CSettings::SETTING_PVRPARENTAL_ENABLED, CSettings::SETTING_PVRPARENTAL_DURATION})
+{
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+ m_actionListener.Init(*this);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "PVR Manager instance created");
+}
+
+CPVRManager::~CPVRManager()
+{
+ m_actionListener.Deinit(*this);
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "PVR Manager instance destroyed");
+}
+
+void CPVRManager::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (!IsStarted())
+ return;
+
+ if ((flag & (ANNOUNCEMENT::GUI)))
+ {
+ if (message == "OnScreensaverActivated")
+ m_addons->OnPowerSavingActivated();
+ else if (message == "OnScreensaverDeactivated")
+ m_addons->OnPowerSavingDeactivated();
+ }
+}
+
+std::shared_ptr<CPVRDatabase> CPVRManager::GetTVDatabase() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_database || !m_database->IsOpen())
+ CLog::LogF(LOGERROR, "Failed to open the PVR database");
+
+ return m_database;
+}
+
+std::shared_ptr<CPVRProviders> CPVRManager::Providers() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_providers;
+}
+
+std::shared_ptr<CPVRChannelGroupsContainer> CPVRManager::ChannelGroups() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelGroups;
+}
+
+std::shared_ptr<CPVRRecordings> CPVRManager::Recordings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_recordings;
+}
+
+std::shared_ptr<CPVRTimers> CPVRManager::Timers() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_timers;
+}
+
+std::shared_ptr<CPVRClients> CPVRManager::Clients() const
+{
+ // note: m_addons is const (only set/reset in ctor/dtor). no need for a lock here.
+ return m_addons;
+}
+
+std::shared_ptr<CPVRClient> CPVRManager::GetClient(const CFileItem& item) const
+{
+ int iClientID = PVR_INVALID_CLIENT_ID;
+
+ if (item.HasPVRChannelInfoTag())
+ iClientID = item.GetPVRChannelInfoTag()->ClientID();
+ else if (item.HasPVRRecordingInfoTag())
+ iClientID = item.GetPVRRecordingInfoTag()->ClientID();
+ else if (item.HasPVRTimerInfoTag())
+ iClientID = item.GetPVRTimerInfoTag()->ClientID();
+ else if (item.HasEPGInfoTag())
+ iClientID = item.GetEPGInfoTag()->ClientID();
+ else if (URIUtils::IsPVRChannel(item.GetPath()))
+ {
+ const std::shared_ptr<CPVRChannel> channel = m_channelGroups->GetByPath(item.GetPath());
+ if (channel)
+ iClientID = channel->ClientID();
+ }
+ else if (URIUtils::IsPVRRecording(item.GetPath()))
+ {
+ const std::shared_ptr<CPVRRecording> recording = m_recordings->GetByPath(item.GetPath());
+ if (recording)
+ iClientID = recording->ClientID();
+ }
+ return GetClient(iClientID);
+}
+
+std::shared_ptr<CPVRClient> CPVRManager::GetClient(int iClientId) const
+{
+ return m_addons->GetCreatedClient(iClientId);
+}
+
+std::shared_ptr<CPVRPlaybackState> CPVRManager::PlaybackState() const
+{
+ // note: m_playbackState is const (only set/reset in ctor/dtor). no need for a lock here.
+ return m_playbackState;
+}
+
+CPVREpgContainer& CPVRManager::EpgContainer()
+{
+ // note: m_epgContainer is const (only set/reset in ctor/dtor). no need for a lock here.
+ return m_epgContainer;
+}
+
+void CPVRManager::Clear()
+{
+ m_playbackState->Clear();
+ m_pendingUpdates->Clear();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_guiInfo.reset();
+ m_timers.reset();
+ m_recordings.reset();
+ m_providers.reset();
+ m_channelGroups.reset();
+ m_parentalTimer.reset();
+ m_database.reset();
+
+ m_bEpgsCreated = false;
+}
+
+void CPVRManager::ResetProperties()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ Clear();
+
+ m_database.reset(new CPVRDatabase);
+ m_providers.reset(new CPVRProviders);
+ m_channelGroups.reset(new CPVRChannelGroupsContainer);
+ m_recordings.reset(new CPVRRecordings);
+ m_timers.reset(new CPVRTimers);
+ m_guiInfo.reset(new CPVRGUIInfo);
+ m_parentalTimer.reset(new CStopWatch);
+ m_knownClients.clear();
+}
+
+void CPVRManager::Init()
+{
+ // initial check for enabled addons
+ // if at least one pvr addon is enabled, PVRManager start up
+ CServiceBroker::GetJobManager()->Submit([this] {
+ Clients()->Start();
+ return true;
+ });
+}
+
+void CPVRManager::Start()
+{
+ std::unique_lock<CCriticalSection> initLock(m_startStopMutex);
+
+ // Prevent concurrent starts
+ if (IsInitialising())
+ return;
+
+ // Note: Stop() must not be called while holding pvr manager's mutex. Stop() calls
+ // StopThread() which can deadlock if the worker thread tries to acquire pvr manager's
+ // lock while StopThread() is waiting for the worker to exit. Thus, we introduce another
+ // lock here (m_startStopMutex), which only gets hold while starting/restarting pvr manager.
+ Stop(true);
+
+ if (!m_addons->HasCreatedClients())
+ return;
+
+ CLog::Log(LOGINFO, "PVR Manager: Starting");
+ SetState(ManagerState::STATE_STARTING);
+
+ /* create the pvrmanager thread, which will ensure that all data will be loaded */
+ Create();
+ SetPriority(ThreadPriority::BELOW_NORMAL);
+}
+
+void CPVRManager::Stop(bool bRestart /* = false */)
+{
+ std::unique_lock<CCriticalSection> initLock(m_startStopMutex);
+
+ // Prevent concurrent stops
+ if (IsStopped())
+ return;
+
+ /* stop playback if needed */
+ if (!bRestart && m_playbackState->IsPlaying())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Stopping PVR playback");
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ }
+
+ CLog::Log(LOGINFO, "PVR Manager: Stopping");
+ SetState(ManagerState::STATE_SSTOPPING);
+
+ StopThread();
+}
+
+void CPVRManager::Unload()
+{
+ // stop pvr manager thread and clear all pvr data
+ Stop();
+ Clear();
+}
+
+void CPVRManager::Deinit()
+{
+ SetWakeupCommand();
+ Unload();
+
+ // release addons
+ m_addons.reset();
+}
+
+CPVRManager::ManagerState CPVRManager::GetState() const
+{
+ std::unique_lock<CCriticalSection> lock(m_managerStateMutex);
+ return m_managerState;
+}
+
+void CPVRManager::SetState(CPVRManager::ManagerState state)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_managerStateMutex);
+ if (m_managerState == state)
+ return;
+
+ m_managerState = state;
+ }
+
+ PVREvent event;
+ switch (state)
+ {
+ case ManagerState::STATE_ERROR:
+ event = PVREvent::ManagerError;
+ break;
+ case ManagerState::STATE_STOPPED:
+ event = PVREvent::ManagerStopped;
+ break;
+ case ManagerState::STATE_STARTING:
+ event = PVREvent::ManagerStarting;
+ break;
+ case ManagerState::STATE_SSTOPPING:
+ event = PVREvent::ManagerStopped;
+ break;
+ case ManagerState::STATE_INTERRUPTED:
+ event = PVREvent::ManagerInterrupted;
+ break;
+ case ManagerState::STATE_STARTED:
+ event = PVREvent::ManagerStarted;
+ break;
+ default:
+ return;
+ }
+
+ PublishEvent(event);
+}
+
+void CPVRManager::PublishEvent(PVREvent event)
+{
+ m_events.Publish(event);
+}
+
+void CPVRManager::Process()
+{
+ m_addons->Continue();
+ m_database->Open();
+
+ if (!IsInitialising())
+ {
+ CLog::Log(LOGINFO, "PVR Manager: Start aborted");
+ return;
+ }
+
+ UnloadComponents();
+
+ if (!IsInitialising())
+ {
+ CLog::Log(LOGINFO, "PVR Manager: Start aborted");
+ return;
+ }
+
+ // Wait for at least one client to come up and load/update data
+ UpdateComponents(ManagerState::STATE_STARTING);
+
+ if (!IsInitialising())
+ {
+ CLog::Log(LOGINFO, "PVR Manager: Start aborted");
+ return;
+ }
+
+ // Load EPGs from database.
+ m_epgContainer.Load();
+
+ // Reinit playbackstate
+ m_playbackState->ReInit();
+
+ m_guiInfo->Start();
+ m_epgContainer.Start();
+ m_timers->Start();
+ m_pendingUpdates->Start();
+
+ SetState(ManagerState::STATE_STARTED);
+ CLog::Log(LOGINFO, "PVR Manager: Started");
+
+ bool bRestart(false);
+ XbmcThreads::EndTime<> cachedImagesCleanupTimeout(30s); // first timeout after 30 secs
+
+ while (IsStarted() && m_addons->HasCreatedClients() && !bRestart)
+ {
+ // In case any new client connected, load from db and fetch data update from new client(s)
+ UpdateComponents(ManagerState::STATE_STARTED);
+
+ if (cachedImagesCleanupTimeout.IsTimePast())
+ {
+ // We don't know for sure what to delete if there are not (yet) connected clients
+ if (m_addons->HasIgnoredClients())
+ {
+ cachedImagesCleanupTimeout.Set(10s); // try again in 10 secs
+ }
+ else
+ {
+ // start a job to erase stale texture db entries and image files
+ TriggerCleanupCachedImages();
+ cachedImagesCleanupTimeout.Set(12h); // following timeouts after 12 hours
+ }
+ }
+
+ /* first startup */
+ if (m_bFirstStart)
+ {
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bFirstStart = false;
+ }
+
+ /* start job to search for missing channel icons */
+ TriggerSearchMissingChannelIcons();
+
+ /* try to play channel on startup */
+ TriggerPlayChannelOnStartup();
+ }
+
+ if (m_addons->AnyClientSupportingRecordingsSize())
+ TriggerRecordingsSizeInProgressUpdate();
+
+ /* execute the next pending jobs if there are any */
+ try
+ {
+ m_pendingUpdates->ExecutePendingJobs();
+ }
+ catch (...)
+ {
+ CLog::LogF(
+ LOGERROR,
+ "An error occurred while trying to execute the last PVR update job, trying to recover");
+ bRestart = true;
+ }
+
+ if (IsStarted() && !bRestart)
+ m_pendingUpdates->WaitForJobs(1000);
+ }
+
+ m_addons->Stop();
+ m_pendingUpdates->Stop();
+ m_timers->Stop();
+ m_epgContainer.Stop();
+ m_guiInfo->Stop();
+
+ SetState(ManagerState::STATE_INTERRUPTED);
+
+ UnloadComponents();
+ m_database->Close();
+
+ ResetProperties();
+
+ CLog::Log(LOGINFO, "PVR Manager: Stopped");
+ SetState(ManagerState::STATE_STOPPED);
+}
+
+bool CPVRManager::SetWakeupCommand()
+{
+#if !defined(TARGET_DARWIN_EMBEDDED) && !defined(TARGET_WINDOWS_STORE)
+ if (!m_settings.GetBoolValue(CSettings::SETTING_PVRPOWERMANAGEMENT_ENABLED))
+ return false;
+
+ const std::string strWakeupCommand(
+ m_settings.GetStringValue(CSettings::SETTING_PVRPOWERMANAGEMENT_SETWAKEUPCMD));
+ if (!strWakeupCommand.empty() && m_timers)
+ {
+ const CDateTime nextEvent = m_timers->GetNextEventTime();
+ if (nextEvent.IsValid())
+ {
+ time_t iWakeupTime;
+ nextEvent.GetAsTime(iWakeupTime);
+
+ std::string strExecCommand = StringUtils::Format("{} {}", strWakeupCommand, iWakeupTime);
+
+ const int iReturn = system(strExecCommand.c_str());
+ if (iReturn != 0)
+ CLog::LogF(LOGERROR, "PVR Manager failed to execute wakeup command '{}': {} ({})",
+ strExecCommand, strerror(iReturn), iReturn);
+
+ return iReturn == 0;
+ }
+ }
+#endif
+ return false;
+}
+
+void CPVRManager::OnSleep()
+{
+ PublishEvent(PVREvent::SystemSleep);
+
+ SetWakeupCommand();
+
+ m_epgContainer.OnSystemSleep();
+
+ m_addons->OnSystemSleep();
+}
+
+void CPVRManager::OnWake()
+{
+ m_addons->OnSystemWake();
+
+ m_epgContainer.OnSystemWake();
+
+ PublishEvent(PVREvent::SystemWake);
+
+ /* start job to search for missing channel icons */
+ TriggerSearchMissingChannelIcons();
+
+ /* try to play channel on startup */
+ TriggerPlayChannelOnStartup();
+
+ /* trigger PVR data updates */
+ TriggerChannelGroupsUpdate();
+ TriggerProvidersUpdate();
+ TriggerChannelsUpdate();
+ TriggerRecordingsUpdate();
+ TriggerEpgsCreate();
+ TriggerTimersUpdate();
+}
+
+void CPVRManager::UpdateComponents(ManagerState stateToCheck)
+{
+ XbmcThreads::EndTime<> progressTimeout(30s);
+ std::unique_ptr<CPVRGUIProgressHandler> progressHandler(
+ new CPVRGUIProgressHandler(g_localizeStrings.Get(19235))); // PVR manager is starting up
+
+ // Wait for at least one client to come up and load/update data
+ while (!UpdateComponents(stateToCheck, progressHandler) && m_addons->HasCreatedClients() &&
+ (stateToCheck == GetState()))
+ {
+ CThread::Sleep(1000ms);
+
+ if (progressTimeout.IsTimePast())
+ progressHandler.reset();
+ }
+}
+
+bool CPVRManager::UpdateComponents(ManagerState stateToCheck,
+ const std::unique_ptr<CPVRGUIProgressHandler>& progressHandler)
+{
+ // find clients which appeared since last check and update them
+ const CPVRClientMap clientMap = m_addons->GetCreatedClients();
+ if (clientMap.empty())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "All created PVR clients gone!");
+ m_knownClients.clear(); // start over
+ PublishEvent(PVREvent::ClientsInvalidated);
+ return false;
+ }
+
+ std::vector<std::shared_ptr<CPVRClient>> newClients;
+ for (const auto& entry : clientMap)
+ {
+ // skip not (yet) connected clients
+ if (entry.second->IgnoreClient())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Skipping not (yet) connected PVR client '{}'",
+ entry.second->ID());
+ continue;
+ }
+
+ if (!IsKnownClient(entry.first))
+ {
+ m_knownClients.emplace_back(entry.second);
+ newClients.emplace_back(entry.second);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Adding new PVR client '{}' to list of known clients",
+ entry.second->ID());
+ }
+ }
+
+ if (newClients.empty())
+ return !m_knownClients.empty();
+
+ // Load all channels and groups
+ if (progressHandler)
+ progressHandler->UpdateProgress(g_localizeStrings.Get(19236), 0); // Loading channels and groups
+
+ if (!m_providers->Update(newClients) || (stateToCheck != GetState()))
+ {
+ CLog::LogF(LOGERROR, "Failed to load PVR providers.");
+ m_knownClients.clear(); // start over
+ PublishEvent(PVREvent::ClientsInvalidated);
+ return false;
+ }
+
+ if (!m_channelGroups->Update(newClients) || (stateToCheck != GetState()))
+ {
+ CLog::LogF(LOGERROR, "Failed to load PVR channels / groups.");
+ m_knownClients.clear(); // start over
+ PublishEvent(PVREvent::ClientsInvalidated);
+ return false;
+ }
+
+ // Load all timers
+ if (progressHandler)
+ progressHandler->UpdateProgress(g_localizeStrings.Get(19237), 50); // Loading timers
+
+ if (!m_timers->Update(newClients) || (stateToCheck != GetState()))
+ {
+ CLog::LogF(LOGERROR, "Failed to load PVR timers.");
+ m_knownClients.clear(); // start over
+ PublishEvent(PVREvent::ClientsInvalidated);
+ return false;
+ }
+
+ // Load all recordings
+ if (progressHandler)
+ progressHandler->UpdateProgress(g_localizeStrings.Get(19238), 75); // Loading recordings
+
+ if (!m_recordings->Update(newClients) || (stateToCheck != GetState()))
+ {
+ CLog::LogF(LOGERROR, "Failed to load PVR recordings.");
+ m_knownClients.clear(); // start over
+ PublishEvent(PVREvent::ClientsInvalidated);
+ return false;
+ }
+
+ // reinit playbackstate as new client may provide new last opened group / last played channel
+ m_playbackState->ReInit();
+
+ PublishEvent(PVREvent::ClientsInvalidated);
+ return true;
+}
+
+void CPVRManager::UnloadComponents()
+{
+ m_recordings->Unload();
+ m_timers->Unload();
+ m_channelGroups->Unload();
+ m_providers->Unload();
+ m_epgContainer.Unload();
+}
+
+bool CPVRManager::IsKnownClient(int clientID) const
+{
+ return std::any_of(m_knownClients.cbegin(), m_knownClients.cend(),
+ [clientID](const auto& client) { return client->GetID() == clientID; });
+}
+
+void CPVRManager::TriggerPlayChannelOnStartup()
+{
+ if (IsStarted())
+ {
+ CServiceBroker::GetJobManager()->Submit(
+ [this] { return Get<PVR::GUI::Playback>().PlayChannelOnStartup(); });
+ }
+}
+
+void CPVRManager::RestartParentalTimer()
+{
+ if (m_parentalTimer)
+ m_parentalTimer->StartZero();
+}
+
+bool CPVRManager::IsParentalLocked(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ return m_channelGroups && epgTag &&
+ IsCurrentlyParentalLocked(
+ m_channelGroups->GetByUniqueID(epgTag->UniqueChannelID(), epgTag->ClientID()),
+ epgTag->IsParentalLocked());
+}
+
+bool CPVRManager::IsParentalLocked(const std::shared_ptr<CPVRChannel>& channel) const
+{
+ return channel && IsCurrentlyParentalLocked(channel, channel->IsLocked());
+}
+
+bool CPVRManager::IsCurrentlyParentalLocked(const std::shared_ptr<CPVRChannel>& channel,
+ bool bGenerallyLocked) const
+{
+ bool bReturn = false;
+
+ if (!channel || !bGenerallyLocked)
+ return bReturn;
+
+ const std::shared_ptr<CPVRChannel> currentChannel = m_playbackState->GetPlayingChannel();
+
+ if ( // if channel in question is currently playing it must be currently unlocked.
+ (!currentChannel || channel != currentChannel) &&
+ // parental control enabled
+ m_settings.GetBoolValue(CSettings::SETTING_PVRPARENTAL_ENABLED))
+ {
+ float parentalDurationMs =
+ m_settings.GetIntValue(CSettings::SETTING_PVRPARENTAL_DURATION) * 1000.0f;
+ bReturn = m_parentalTimer && (!m_parentalTimer->IsRunning() ||
+ m_parentalTimer->GetElapsedMilliseconds() > parentalDurationMs);
+ }
+
+ return bReturn;
+}
+
+void CPVRManager::OnPlaybackStarted(const CFileItem& item)
+{
+ m_playbackState->OnPlaybackStarted(item);
+ Get<PVR::GUI::Channels>().OnPlaybackStarted(item);
+ m_epgContainer.OnPlaybackStarted();
+}
+
+void CPVRManager::OnPlaybackStopped(const CFileItem& item)
+{
+ // Playback ended due to user interaction
+ if (m_playbackState->OnPlaybackStopped(item))
+ PublishEvent(PVREvent::ChannelPlaybackStopped);
+
+ Get<PVR::GUI::Channels>().OnPlaybackStopped(item);
+ m_epgContainer.OnPlaybackStopped();
+}
+
+void CPVRManager::OnPlaybackEnded(const CFileItem& item)
+{
+ // Playback ended, but not due to user interaction
+ OnPlaybackStopped(item);
+}
+
+void CPVRManager::LocalizationChanged()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (IsStarted())
+ {
+ static_cast<CPVRChannelGroupInternal*>(m_channelGroups->GetGroupAllRadio().get())
+ ->CheckGroupName();
+ static_cast<CPVRChannelGroupInternal*>(m_channelGroups->GetGroupAllTV().get())
+ ->CheckGroupName();
+ }
+}
+
+bool CPVRManager::EpgsCreated() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bEpgsCreated;
+}
+
+void CPVRManager::TriggerEpgsCreate()
+{
+ m_pendingUpdates->Append("pvr-create-epgs", [this]() { return CreateChannelEpgs(); });
+}
+
+void CPVRManager::TriggerRecordingsSizeInProgressUpdate()
+{
+ m_pendingUpdates->Append("pvr-update-recordings-size",
+ [this]() { return Recordings()->UpdateInProgressSize(); });
+}
+
+void CPVRManager::TriggerRecordingsUpdate(int clientId)
+{
+ m_pendingUpdates->Append("pvr-update-recordings-" + std::to_string(clientId), [this, clientId]() {
+ if (!IsKnownClient(clientId))
+ return;
+
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client)
+ Recordings()->UpdateFromClients({client});
+ });
+}
+
+void CPVRManager::TriggerRecordingsUpdate()
+{
+ m_pendingUpdates->Append("pvr-update-recordings",
+ [this]() { Recordings()->UpdateFromClients({}); });
+}
+
+void CPVRManager::TriggerTimersUpdate(int clientId)
+{
+ m_pendingUpdates->Append("pvr-update-timers-" + std::to_string(clientId), [this, clientId]() {
+ if (!IsKnownClient(clientId))
+ return;
+
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client)
+ Timers()->UpdateFromClients({client});
+ });
+}
+
+void CPVRManager::TriggerTimersUpdate()
+{
+ m_pendingUpdates->Append("pvr-update-timers", [this]() { Timers()->UpdateFromClients({}); });
+}
+
+void CPVRManager::TriggerProvidersUpdate(int clientId)
+{
+ m_pendingUpdates->Append("pvr-update-channel-providers-" + std::to_string(clientId),
+ [this, clientId]() {
+ if (!IsKnownClient(clientId))
+ return;
+
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client)
+ Providers()->UpdateFromClients({client});
+ });
+}
+
+void CPVRManager::TriggerProvidersUpdate()
+{
+ m_pendingUpdates->Append("pvr-update-channel-providers",
+ [this]() { Providers()->UpdateFromClients({}); });
+}
+
+void CPVRManager::TriggerChannelsUpdate(int clientId)
+{
+ m_pendingUpdates->Append("pvr-update-channels-" + std::to_string(clientId), [this, clientId]() {
+ if (!IsKnownClient(clientId))
+ return;
+
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client)
+ ChannelGroups()->UpdateFromClients({client}, true);
+ });
+}
+
+void CPVRManager::TriggerChannelsUpdate()
+{
+ m_pendingUpdates->Append("pvr-update-channels",
+ [this]() { ChannelGroups()->UpdateFromClients({}, true); });
+}
+
+void CPVRManager::TriggerChannelGroupsUpdate(int clientId)
+{
+ m_pendingUpdates->Append("pvr-update-channelgroups-" + std::to_string(clientId),
+ [this, clientId]() {
+ if (!IsKnownClient(clientId))
+ return;
+
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client)
+ ChannelGroups()->UpdateFromClients({client}, false);
+ });
+}
+
+void CPVRManager::TriggerChannelGroupsUpdate()
+{
+ m_pendingUpdates->Append("pvr-update-channelgroups",
+ [this]() { ChannelGroups()->UpdateFromClients({}, false); });
+}
+
+void CPVRManager::TriggerSearchMissingChannelIcons()
+{
+ m_pendingUpdates->Append("pvr-search-missing-channel-icons", [this]() {
+ CPVRGUIChannelIconUpdater updater(
+ {ChannelGroups()->GetGroupAllTV(), ChannelGroups()->GetGroupAllRadio()}, true);
+ updater.SearchAndUpdateMissingChannelIcons();
+ return true;
+ });
+}
+
+void CPVRManager::TriggerSearchMissingChannelIcons(const std::shared_ptr<CPVRChannelGroup>& group)
+{
+ m_pendingUpdates->Append("pvr-search-missing-channel-icons-" + std::to_string(group->GroupID()),
+ [group]() {
+ CPVRGUIChannelIconUpdater updater({group}, false);
+ updater.SearchAndUpdateMissingChannelIcons();
+ return true;
+ });
+}
+
+void CPVRManager::TriggerCleanupCachedImages()
+{
+ m_pendingUpdates->Append("pvr-cleanup-cached-images", [this]() {
+ int iCleanedImages = 0;
+ CLog::Log(LOGINFO, "PVR Manager: Starting cleanup of cached images.");
+ iCleanedImages += Recordings()->CleanupCachedImages();
+ iCleanedImages += ChannelGroups()->CleanupCachedImages();
+ iCleanedImages += Providers()->CleanupCachedImages();
+ iCleanedImages += EpgContainer().CleanupCachedImages();
+ CLog::Log(LOGINFO, "PVR Manager: Cleaned up {} cached images.", iCleanedImages);
+ return true;
+ });
+}
+
+void CPVRManager::ConnectionStateChange(CPVRClient* client,
+ const std::string& connectString,
+ PVR_CONNECTION_STATE state,
+ const std::string& message)
+{
+ CServiceBroker::GetJobManager()->Submit([this, client, connectString, state, message] {
+ Clients()->ConnectionStateChange(client, connectString, state, message);
+ return true;
+ });
+}
+
+bool CPVRManager::CreateChannelEpgs()
+{
+ if (EpgsCreated())
+ return true;
+
+ bool bEpgsCreated = m_channelGroups->CreateChannelEpgs();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bEpgsCreated = bEpgsCreated;
+ return m_bEpgsCreated;
+}
diff --git a/xbmc/pvr/PVRManager.h b/xbmc/pvr/PVRManager.h
new file mode 100644
index 0000000..06bcb44
--- /dev/null
+++ b/xbmc/pvr/PVRManager.h
@@ -0,0 +1,482 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_general.h"
+#include "interfaces/IAnnouncer.h"
+#include "pvr/PVRComponentRegistration.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/guilib/PVRGUIActionListener.h"
+#include "pvr/settings/PVRSettings.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "utils/EventStream.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CStopWatch;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRChannelGroup;
+class CPVRChannelGroupsContainer;
+class CPVRProviders;
+class CPVRClient;
+class CPVRClients;
+class CPVRDatabase;
+class CPVRGUIInfo;
+class CPVRGUIProgressHandler;
+class CPVRManagerJobQueue;
+class CPVRPlaybackState;
+class CPVRRecording;
+class CPVRRecordings;
+class CPVRTimers;
+
+enum class PVREvent
+{
+ // PVR Manager states
+ ManagerError = 0,
+ ManagerStopped,
+ ManagerStarting,
+ ManagerStopping,
+ ManagerInterrupted,
+ ManagerStarted,
+
+ // Channel events
+ ChannelPlaybackStopped,
+
+ // Channel group events
+ ChannelGroup,
+ ChannelGroupInvalidated,
+ ChannelGroupsInvalidated,
+
+ // Recording events
+ RecordingsInvalidated,
+
+ // Timer events
+ AnnounceReminder,
+ Timers,
+ TimersInvalidated,
+
+ // Client events
+ ClientsInvalidated,
+
+ // EPG events
+ Epg,
+ EpgActiveItem,
+ EpgContainer,
+ EpgItemUpdate,
+ EpgUpdatePending,
+ EpgDeleted,
+
+ // Saved searches events
+ SavedSearchesInvalidated,
+
+ // Item events
+ CurrentItem,
+
+ // System events
+ SystemSleep,
+ SystemWake,
+};
+
+class CPVRManager : private CThread, public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ /*!
+ * @brief Create a new CPVRManager instance, which handles all PVR related operations in XBMC.
+ */
+ CPVRManager();
+
+ /*!
+ * @brief Stop the PVRManager and destroy all objects it created.
+ */
+ ~CPVRManager() override;
+
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ /*!
+ * @brief Get a PVR component.
+ * @return The component.
+ */
+ template<class T>
+ T& Get()
+ {
+ return *m_components->GetComponent<T>();
+ }
+
+ /*!
+ * @brief Get the providers container.
+ * @return The providers container.
+ */
+ std::shared_ptr<CPVRProviders> Providers() const;
+
+ /*!
+ * @brief Get the channel groups container.
+ * @return The groups container.
+ */
+ std::shared_ptr<CPVRChannelGroupsContainer> ChannelGroups() const;
+
+ /*!
+ * @brief Get the recordings container.
+ * @return The recordings container.
+ */
+ std::shared_ptr<CPVRRecordings> Recordings() const;
+
+ /*!
+ * @brief Get the timers container.
+ * @return The timers container.
+ */
+ std::shared_ptr<CPVRTimers> Timers() const;
+
+ /*!
+ * @brief Get the timers container.
+ * @return The timers container.
+ */
+ std::shared_ptr<CPVRClients> Clients() const;
+
+ /*!
+ * @brief Get the instance of a client that matches the given item.
+ * @param item The item containing a PVR recording, a PVR channel, a PVR timer or a PVR EPG event.
+ * @return the requested client on success, nullptr otherwise.
+ */
+ std::shared_ptr<CPVRClient> GetClient(const CFileItem& item) const;
+
+ /*!
+ * @brief Get the instance of a client that matches the given id.
+ * @param iClientId The id of a PVR client.
+ * @return the requested client on success, nullptr otherwise.
+ */
+ std::shared_ptr<CPVRClient> GetClient(int iClientId) const;
+
+ /*!
+ * @brief Get access to the pvr playback state.
+ * @return The playback state.
+ */
+ std::shared_ptr<CPVRPlaybackState> PlaybackState() const;
+
+ /*!
+ * @brief Get access to the epg container.
+ * @return The epg container.
+ */
+ CPVREpgContainer& EpgContainer();
+
+ /*!
+ * @brief Init PVRManager.
+ */
+ void Init();
+
+ /*!
+ * @brief Start the PVRManager, which loads all PVR data and starts some threads to update the PVR data.
+ */
+ void Start();
+
+ /*!
+ * @brief Stop PVRManager.
+ */
+ void Stop(bool bRestart = false);
+
+ /*!
+ * @brief Stop PVRManager, unload data.
+ */
+ void Unload();
+
+ /*!
+ * @brief Deinit PVRManager, unload data, unload addons.
+ */
+ void Deinit();
+
+ /*!
+ * @brief Propagate event on system sleep
+ */
+ void OnSleep();
+
+ /*!
+ * @brief Propagate event on system wake
+ */
+ void OnWake();
+
+ /*!
+ * @brief Get the TV database.
+ * @return The TV database.
+ */
+ std::shared_ptr<CPVRDatabase> GetTVDatabase() const;
+
+ /*!
+ * @brief Check whether the PVRManager has fully started.
+ * @return True if started, false otherwise.
+ */
+ bool IsStarted() const { return GetState() == ManagerState::STATE_STARTED; }
+
+ /*!
+ * @brief Check whether EPG tags for channels have been created.
+ * @return True if EPG tags have been created, false otherwise.
+ */
+ bool EpgsCreated() const;
+
+ /*!
+ * @brief Inform PVR manager that playback of an item just started.
+ * @param item The item that started to play.
+ */
+ void OnPlaybackStarted(const CFileItem& item);
+
+ /*!
+ * @brief Inform PVR manager that playback of an item was stopped due to user interaction.
+ * @param item The item that stopped to play.
+ */
+ void OnPlaybackStopped(const CFileItem& item);
+
+ /*!
+ * @brief Inform PVR manager that playback of an item has stopped without user interaction.
+ * @param item The item that ended to play.
+ */
+ void OnPlaybackEnded(const CFileItem& item);
+
+ /*!
+ * @brief Let the background thread create epg tags for all channels.
+ */
+ void TriggerEpgsCreate();
+
+ /*!
+ * @brief Let the background thread update the recordings list.
+ * @param clientId The id of the PVR client to update.
+ */
+ void TriggerRecordingsUpdate(int clientId);
+ void TriggerRecordingsUpdate();
+
+ /*!
+ * @brief Let the background thread update the size for any in progress recordings.
+ */
+ void TriggerRecordingsSizeInProgressUpdate();
+
+ /*!
+ * @brief Let the background thread update the timer list.
+ * @param clientId The id of the PVR client to update.
+ */
+ void TriggerTimersUpdate(int clientId);
+ void TriggerTimersUpdate();
+
+ /*!
+ * @brief Let the background thread update the channel list.
+ * @param clientId The id of the PVR client to update.
+ */
+ void TriggerChannelsUpdate(int clientId);
+ void TriggerChannelsUpdate();
+
+ /*!
+ * @brief Let the background thread update the provider list.
+ * @param clientId The id of the PVR client to update.
+ */
+ void TriggerProvidersUpdate(int clientId);
+ void TriggerProvidersUpdate();
+
+ /*!
+ * @brief Let the background thread update the channel groups list.
+ * @param clientId The id of the PVR client to update.
+ */
+ void TriggerChannelGroupsUpdate(int clientId);
+ void TriggerChannelGroupsUpdate();
+
+ /*!
+ * @brief Let the background thread search for all missing channel icons.
+ */
+ void TriggerSearchMissingChannelIcons();
+
+ /*!
+ * @brief Let the background thread erase stale texture db entries and image files.
+ */
+ void TriggerCleanupCachedImages();
+
+ /*!
+ * @brief Let the background thread search for missing channel icons for channels contained in the given group.
+ * @param group The channel group.
+ */
+ void TriggerSearchMissingChannelIcons(const std::shared_ptr<CPVRChannelGroup>& group);
+
+ /*!
+ * @brief Check whether names are still correct after the language settings changed.
+ */
+ void LocalizationChanged();
+
+ /*!
+ * @brief Check if parental lock is overridden at the given moment.
+ * @param channel The channel to check.
+ * @return True if parental lock is overridden, false otherwise.
+ */
+ bool IsParentalLocked(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Check if parental lock is overridden at the given moment.
+ * @param epgTag The epg tag to check.
+ * @return True if parental lock is overridden, false otherwise.
+ */
+ bool IsParentalLocked(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ /*!
+ * @brief Restart the parental timer.
+ */
+ void RestartParentalTimer();
+
+ /*!
+ * @brief Create EPG tags for all channels in internal channel groups
+ * @return True if EPG tags where created successfully, false otherwise
+ */
+ bool CreateChannelEpgs();
+
+ /*!
+ * @brief Signal a connection change of a client
+ */
+ void ConnectionStateChange(CPVRClient* client,
+ const std::string& connectString,
+ PVR_CONNECTION_STATE state,
+ const std::string& message);
+
+ /*!
+ * @brief Query the events available for CEventStream
+ */
+ CEventStream<PVREvent>& Events() { return m_events; }
+
+ /*!
+ * @brief Publish an event
+ * @param state the event
+ */
+ void PublishEvent(PVREvent state);
+
+protected:
+ /*!
+ * @brief PVR update and control thread.
+ */
+ void Process() override;
+
+private:
+ /*!
+ * @brief Executes "pvrpowermanagement.setwakeupcmd"
+ */
+ bool SetWakeupCommand();
+
+ enum class ManagerState
+ {
+ STATE_ERROR = 0,
+ STATE_STOPPED,
+ STATE_STARTING,
+ STATE_SSTOPPING,
+ STATE_INTERRUPTED,
+ STATE_STARTED
+ };
+
+ /*!
+ * @return True while the PVRManager is initialising.
+ */
+ bool IsInitialising() const { return GetState() == ManagerState::STATE_STARTING; }
+
+ /*!
+ * @brief Check whether the PVRManager has been stopped.
+ * @return True if stopped, false otherwise.
+ */
+ bool IsStopped() const { return GetState() == ManagerState::STATE_STOPPED; }
+
+ /*!
+ * @brief Get the current state of the PVR manager.
+ * @return the state.
+ */
+ ManagerState GetState() const;
+
+ /*!
+ * @brief Set the current state of the PVR manager.
+ * @param state the new state.
+ */
+ void SetState(ManagerState state);
+
+ /*!
+ * @brief Wait until at least one client is up. Update all data from database and the given PVR clients.
+ * @param stateToCheck Required state of the PVR manager while this method gets called.
+ */
+ void UpdateComponents(ManagerState stateToCheck);
+
+ /*!
+ * @brief Update all data from database and the given PVR clients.
+ * @param stateToCheck Required state of the PVR manager while this method gets called.
+ * @param progressHandler The progress handler to use for showing the different stages.
+ * @return True if at least one client is known and successfully loaded, false otherwise.
+ */
+ bool UpdateComponents(ManagerState stateToCheck,
+ const std::unique_ptr<CPVRGUIProgressHandler>& progressHandler);
+
+ /*!
+ * @brief Unload all PVR data (recordings, timers, channelgroups).
+ */
+ void UnloadComponents();
+
+ /*!
+ * @brief Check whether the given client id belongs to a known client.
+ * @return True if the client is known, false otherwise.
+ */
+ bool IsKnownClient(int clientID) const;
+
+ /*!
+ * @brief Reset all properties.
+ */
+ void ResetProperties();
+
+ /*!
+ * @brief Destroy PVRManager's objects.
+ */
+ void Clear();
+
+ /*!
+ * @brief Continue playback on the last played channel.
+ */
+ void TriggerPlayChannelOnStartup();
+
+ bool IsCurrentlyParentalLocked(const std::shared_ptr<CPVRChannel>& channel,
+ bool bGenerallyLocked) const;
+
+ CEventSource<PVREvent> m_events;
+
+ /** @name containers */
+ //@{
+ std::shared_ptr<CPVRProviders> m_providers; /*!< pointer to the providers container */
+ std::shared_ptr<CPVRChannelGroupsContainer>
+ m_channelGroups; /*!< pointer to the channel groups container */
+ std::shared_ptr<CPVRRecordings> m_recordings; /*!< pointer to the recordings container */
+ std::shared_ptr<CPVRTimers> m_timers; /*!< pointer to the timers container */
+ std::shared_ptr<CPVRClients> m_addons; /*!< pointer to the pvr addon container */
+ std::unique_ptr<CPVRGUIInfo> m_guiInfo; /*!< pointer to the guiinfo data */
+ std::shared_ptr<CPVRComponentRegistration> m_components; /*!< pointer to the PVR components */
+ CPVREpgContainer m_epgContainer; /*!< the epg container */
+ //@}
+
+ std::vector<std::shared_ptr<CPVRClient>> m_knownClients; /*!< vector with all known clients */
+ std::unique_ptr<CPVRManagerJobQueue> m_pendingUpdates; /*!< vector of pending pvr updates */
+ std::shared_ptr<CPVRDatabase> m_database; /*!< the database for all PVR related data */
+ mutable CCriticalSection
+ m_critSection; /*!< critical section for all changes to this class, except for changes to triggers */
+ bool m_bFirstStart = true; /*!< true when the PVR manager was started first, false otherwise */
+ bool m_bEpgsCreated = false; /*!< true if epg data for channels has been created */
+
+ mutable CCriticalSection m_managerStateMutex;
+ ManagerState m_managerState = ManagerState::STATE_STOPPED;
+ std::unique_ptr<CStopWatch> m_parentalTimer;
+
+ CCriticalSection
+ m_startStopMutex; // mutex for protecting pvr manager's start/restart/stop sequence */
+
+ const std::shared_ptr<CPVRPlaybackState> m_playbackState;
+ CPVRGUIActionListener m_actionListener;
+ CPVRSettings m_settings;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/PVRPlaybackState.cpp b/xbmc/pvr/PVRPlaybackState.cpp
new file mode 100644
index 0000000..56bbd5b
--- /dev/null
+++ b/xbmc/pvr/PVRPlaybackState.cpp
@@ -0,0 +1,566 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRPlaybackState.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "cores/DataCacheCore.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/Timer.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <mutex>
+
+using namespace PVR;
+
+class CPVRPlaybackState::CLastWatchedUpdateTimer : public CTimer, private ITimerCallback
+{
+public:
+ CLastWatchedUpdateTimer(CPVRPlaybackState& state,
+ const std::shared_ptr<CPVRChannelGroupMember>& channel,
+ const CDateTime& time)
+ : CTimer(this), m_state(state), m_channel(channel), m_time(time)
+ {
+ }
+
+ // ITimerCallback implementation
+ void OnTimeout() override { m_state.UpdateLastWatched(m_channel, m_time); }
+
+private:
+ CLastWatchedUpdateTimer() = delete;
+
+ CPVRPlaybackState& m_state;
+ const std::shared_ptr<CPVRChannelGroupMember> m_channel;
+ const CDateTime m_time;
+};
+
+CPVRPlaybackState::CPVRPlaybackState() = default;
+
+CPVRPlaybackState::~CPVRPlaybackState() = default;
+
+void CPVRPlaybackState::ReInit()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Clear();
+
+ if (m_playingClientId != -1)
+ {
+ if (m_playingChannelUniqueId != -1)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetByIdFromAll(m_playingGroupId);
+ if (group)
+ m_playingChannel = group->GetByUniqueID({m_playingClientId, m_playingChannelUniqueId});
+ else
+ CLog::LogFC(LOGERROR, LOGPVR, "Failed to obtain group with id '{}'", m_playingGroupId);
+ }
+ else if (!m_strPlayingRecordingUniqueId.empty())
+ {
+ m_playingRecording = CServiceBroker::GetPVRManager().Recordings()->GetById(
+ m_playingClientId, m_strPlayingRecordingUniqueId);
+ }
+ else if (m_playingEpgTagChannelUniqueId != -1 && m_playingEpgTagUniqueId != 0)
+ {
+ const std::shared_ptr<CPVREpg> epg =
+ CServiceBroker::GetPVRManager().EpgContainer().GetByChannelUid(
+ m_playingClientId, m_playingEpgTagChannelUniqueId);
+ if (epg)
+ m_playingEpgTag = epg->GetTagByBroadcastId(m_playingEpgTagUniqueId);
+ }
+ }
+
+ const std::shared_ptr<CPVRChannelGroupsContainer> groups =
+ CServiceBroker::GetPVRManager().ChannelGroups();
+ const CPVRChannelGroups* groupsTV = groups->GetTV();
+ const CPVRChannelGroups* groupsRadio = groups->GetRadio();
+
+ m_activeGroupTV = groupsTV->GetLastOpenedGroup();
+ m_activeGroupRadio = groupsRadio->GetLastOpenedGroup();
+ if (!m_activeGroupTV)
+ m_activeGroupTV = groupsTV->GetGroupAll();
+ if (!m_activeGroupRadio)
+ m_activeGroupRadio = groupsRadio->GetGroupAll();
+
+ GroupMemberPair lastPlayed = groupsTV->GetLastAndPreviousToLastPlayedChannelGroupMember();
+ m_lastPlayedChannelTV = lastPlayed.first;
+ m_previousToLastPlayedChannelTV = lastPlayed.second;
+
+ lastPlayed = groupsRadio->GetLastAndPreviousToLastPlayedChannelGroupMember();
+ m_lastPlayedChannelRadio = lastPlayed.first;
+ m_previousToLastPlayedChannelRadio = lastPlayed.second;
+}
+
+void CPVRPlaybackState::ClearData()
+{
+ m_playingGroupId = -1;
+ m_playingChannelUniqueId = -1;
+ m_strPlayingRecordingUniqueId.clear();
+ m_playingEpgTagChannelUniqueId = -1;
+ m_playingEpgTagUniqueId = 0;
+ m_playingClientId = -1;
+ m_strPlayingClientName.clear();
+}
+
+void CPVRPlaybackState::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_playingChannel.reset();
+ m_playingRecording.reset();
+ m_playingEpgTag.reset();
+ m_lastPlayedChannelTV.reset();
+ m_lastPlayedChannelRadio.reset();
+ m_previousToLastPlayedChannelTV.reset();
+ m_previousToLastPlayedChannelRadio.reset();
+ m_lastWatchedUpdateTimer.reset();
+ m_activeGroupTV.reset();
+ m_activeGroupRadio.reset();
+}
+
+void CPVRPlaybackState::OnPlaybackStarted(const CFileItem& item)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_playingChannel.reset();
+ m_playingRecording.reset();
+ m_playingEpgTag.reset();
+ ClearData();
+
+ if (item.HasPVRChannelGroupMemberInfoTag())
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> channel = item.GetPVRChannelGroupMemberInfoTag();
+
+ m_playingChannel = channel;
+ m_playingGroupId = m_playingChannel->GroupID();
+ m_playingClientId = m_playingChannel->Channel()->ClientID();
+ m_playingChannelUniqueId = m_playingChannel->Channel()->UniqueID();
+
+ SetActiveChannelGroup(channel);
+
+ if (channel->Channel()->IsRadio())
+ {
+ if (m_lastPlayedChannelRadio != channel)
+ {
+ m_previousToLastPlayedChannelRadio = m_lastPlayedChannelRadio;
+ m_lastPlayedChannelRadio = channel;
+ }
+ }
+ else
+ {
+ if (m_lastPlayedChannelTV != channel)
+ {
+ m_previousToLastPlayedChannelTV = m_lastPlayedChannelTV;
+ m_lastPlayedChannelTV = channel;
+ }
+ }
+
+ int iLastWatchedDelay = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_PVRPLAYBACK_DELAYMARKLASTWATCHED) *
+ 1000;
+ if (iLastWatchedDelay > 0)
+ {
+ // Insert new / replace existing last watched update timer
+ if (m_lastWatchedUpdateTimer)
+ m_lastWatchedUpdateTimer->Stop(true);
+
+ m_lastWatchedUpdateTimer.reset(
+ new CLastWatchedUpdateTimer(*this, channel, CDateTime::GetUTCDateTime()));
+ m_lastWatchedUpdateTimer->Start(std::chrono::milliseconds(iLastWatchedDelay));
+ }
+ else
+ {
+ // Store last watched timestamp immediately
+ UpdateLastWatched(channel, CDateTime::GetUTCDateTime());
+ }
+ }
+ else if (item.HasPVRRecordingInfoTag())
+ {
+ m_playingRecording = item.GetPVRRecordingInfoTag();
+ m_playingClientId = m_playingRecording->ClientID();
+ m_strPlayingRecordingUniqueId = m_playingRecording->ClientRecordingID();
+ }
+ else if (item.HasEPGInfoTag())
+ {
+ m_playingEpgTag = item.GetEPGInfoTag();
+ m_playingClientId = m_playingEpgTag->ClientID();
+ m_playingEpgTagChannelUniqueId = m_playingEpgTag->UniqueChannelID();
+ m_playingEpgTagUniqueId = m_playingEpgTag->UniqueBroadcastID();
+ }
+ else if (item.HasPVRChannelInfoTag())
+ {
+ CLog::LogFC(LOGERROR, LOGPVR, "Channel item without channel group member!");
+ }
+
+ if (m_playingClientId != -1)
+ {
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_playingClientId);
+ if (client)
+ m_strPlayingClientName = client->GetFriendlyName();
+ }
+}
+
+bool CPVRPlaybackState::OnPlaybackStopped(const CFileItem& item)
+{
+ // Playback ended due to user interaction
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bool bChanged = false;
+
+ if (item.HasPVRChannelGroupMemberInfoTag() &&
+ item.GetPVRChannelGroupMemberInfoTag()->Channel()->ClientID() == m_playingClientId &&
+ item.GetPVRChannelGroupMemberInfoTag()->Channel()->UniqueID() == m_playingChannelUniqueId)
+ {
+ bool bUpdateLastWatched = true;
+
+ if (m_lastWatchedUpdateTimer)
+ {
+ if (m_lastWatchedUpdateTimer->IsRunning())
+ {
+ // If last watched timer is still running, cancel it. Channel was not watched long enough to store the value.
+ m_lastWatchedUpdateTimer->Stop(true);
+ bUpdateLastWatched = false;
+ }
+ m_lastWatchedUpdateTimer.reset();
+ }
+
+ if (bUpdateLastWatched)
+ {
+ // If last watched timer is not running (any more), channel was watched long enough to store the value.
+ UpdateLastWatched(m_playingChannel, CDateTime::GetUTCDateTime());
+ }
+
+ bChanged = true;
+ m_playingChannel.reset();
+ ClearData();
+ }
+ else if (item.HasPVRRecordingInfoTag() &&
+ item.GetPVRRecordingInfoTag()->ClientID() == m_playingClientId &&
+ item.GetPVRRecordingInfoTag()->ClientRecordingID() == m_strPlayingRecordingUniqueId)
+ {
+ bChanged = true;
+ m_playingRecording.reset();
+ ClearData();
+ }
+ else if (item.HasEPGInfoTag() && item.GetEPGInfoTag()->ClientID() == m_playingClientId &&
+ item.GetEPGInfoTag()->UniqueChannelID() == m_playingEpgTagChannelUniqueId &&
+ item.GetEPGInfoTag()->UniqueBroadcastID() == m_playingEpgTagUniqueId)
+ {
+ bChanged = true;
+ m_playingEpgTag.reset();
+ ClearData();
+ }
+ else if (item.HasPVRChannelInfoTag())
+ {
+ CLog::LogFC(LOGERROR, LOGPVR, "Channel item without channel group member!");
+ }
+
+ return bChanged;
+}
+
+void CPVRPlaybackState::OnPlaybackEnded(const CFileItem& item)
+{
+ // Playback ended, but not due to user interaction
+ OnPlaybackStopped(item);
+}
+
+bool CPVRPlaybackState::IsPlaying() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannel != nullptr || m_playingRecording != nullptr || m_playingEpgTag != nullptr;
+}
+
+bool CPVRPlaybackState::IsPlayingTV() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannel && !m_playingChannel->Channel()->IsRadio();
+}
+
+bool CPVRPlaybackState::IsPlayingRadio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannel && m_playingChannel->Channel()->IsRadio();
+}
+
+bool CPVRPlaybackState::IsPlayingEncryptedChannel() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannel && m_playingChannel->Channel()->IsEncrypted();
+}
+
+bool CPVRPlaybackState::IsPlayingRecording() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingRecording != nullptr;
+}
+
+bool CPVRPlaybackState::IsPlayingEpgTag() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingEpgTag != nullptr;
+}
+
+bool CPVRPlaybackState::IsPlayingChannel(int iClientID, int iUniqueChannelID) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingClientId == iClientID && m_playingChannelUniqueId == iUniqueChannelID;
+}
+
+bool CPVRPlaybackState::IsPlayingChannel(const std::shared_ptr<CPVRChannel>& channel) const
+{
+ if (channel)
+ {
+ const std::shared_ptr<CPVRChannel> current = GetPlayingChannel();
+ if (current && current->ClientID() == channel->ClientID() &&
+ current->UniqueID() == channel->UniqueID())
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRPlaybackState::IsPlayingRecording(const std::shared_ptr<CPVRRecording>& recording) const
+{
+ if (recording)
+ {
+ const std::shared_ptr<CPVRRecording> current = GetPlayingRecording();
+ if (current && current->ClientID() == recording->ClientID() &&
+ current->ClientRecordingID() == recording->ClientRecordingID())
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRPlaybackState::IsPlayingEpgTag(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (epgTag)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> current = GetPlayingEpgTag();
+ if (current && current->ClientID() == epgTag->ClientID() &&
+ current->UniqueChannelID() == epgTag->UniqueChannelID() &&
+ current->UniqueBroadcastID() == epgTag->UniqueBroadcastID())
+ return true;
+ }
+
+ return false;
+}
+
+std::shared_ptr<CPVRChannel> CPVRPlaybackState::GetPlayingChannel() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannel ? m_playingChannel->Channel() : std::shared_ptr<CPVRChannel>();
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRPlaybackState::GetPlayingChannelGroupMember() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannel;
+}
+
+std::shared_ptr<CPVRRecording> CPVRPlaybackState::GetPlayingRecording() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingRecording;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRPlaybackState::GetPlayingEpgTag() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingEpgTag;
+}
+
+int CPVRPlaybackState::GetPlayingChannelUniqueID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannelUniqueId;
+}
+
+std::string CPVRPlaybackState::GetPlayingClientName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strPlayingClientName;
+}
+
+int CPVRPlaybackState::GetPlayingClientID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingClientId;
+}
+
+bool CPVRPlaybackState::IsRecording() const
+{
+ return CServiceBroker::GetPVRManager().Timers()->IsRecording();
+}
+
+bool CPVRPlaybackState::IsRecordingOnPlayingChannel() const
+{
+ const std::shared_ptr<CPVRChannel> currentChannel = GetPlayingChannel();
+ return currentChannel &&
+ CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*currentChannel);
+}
+
+bool CPVRPlaybackState::IsPlayingActiveRecording() const
+{
+ const std::shared_ptr<CPVRRecording> currentRecording = GetPlayingRecording();
+ return currentRecording && currentRecording->IsInProgress();
+}
+
+bool CPVRPlaybackState::CanRecordOnPlayingChannel() const
+{
+ const std::shared_ptr<CPVRChannel> currentChannel = GetPlayingChannel();
+ return currentChannel && currentChannel->CanRecord();
+}
+
+void CPVRPlaybackState::SetActiveChannelGroup(const std::shared_ptr<CPVRChannelGroup>& group)
+{
+ if (group)
+ {
+ if (group->IsRadio())
+ m_activeGroupRadio = group;
+ else
+ m_activeGroupTV = group;
+
+ auto duration = std::chrono::system_clock::now().time_since_epoch();
+ uint64_t tsMillis = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
+ group->SetLastOpened(tsMillis);
+ }
+}
+
+void CPVRPlaybackState::SetActiveChannelGroup(
+ const std::shared_ptr<CPVRChannelGroupMember>& channel)
+{
+ const bool bRadio = channel->Channel()->IsRadio();
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio)->GetById(channel->GroupID());
+
+ SetActiveChannelGroup(group);
+}
+
+namespace
+{
+std::shared_ptr<CPVRChannelGroup> GetFirstNonDeletedAndNonHiddenChannelGroup(bool bRadio)
+{
+ CPVRChannelGroups* groups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio);
+ if (groups)
+ {
+ const std::vector<std::shared_ptr<CPVRChannelGroup>> members =
+ groups->GetMembers(true); // exclude hidden
+
+ const auto it = std::find_if(members.cbegin(), members.cend(),
+ [](const auto& group) { return !group->IsDeleted(); });
+ if (it != members.cend())
+ return (*it);
+ }
+
+ CLog::LogFC(LOGERROR, LOGPVR, "Failed to obtain any non-deleted and non-hidden group");
+ return {};
+}
+} // unnamed namespace
+
+std::shared_ptr<CPVRChannelGroup> CPVRPlaybackState::GetActiveChannelGroup(bool bRadio) const
+{
+ if (bRadio)
+ {
+ if (m_activeGroupRadio && (m_activeGroupRadio->IsDeleted() || m_activeGroupRadio->IsHidden()))
+ {
+ // switch to first non-deleted and non-hidden group
+ const auto group = GetFirstNonDeletedAndNonHiddenChannelGroup(bRadio);
+ if (group)
+ const_cast<CPVRPlaybackState*>(this)->SetActiveChannelGroup(group);
+ }
+ return m_activeGroupRadio;
+ }
+ else
+ {
+ if (m_activeGroupTV && (m_activeGroupTV->IsDeleted() || m_activeGroupTV->IsHidden()))
+ {
+ // switch to first non-deleted and non-hidden group
+ const auto group = GetFirstNonDeletedAndNonHiddenChannelGroup(bRadio);
+ if (group)
+ const_cast<CPVRPlaybackState*>(this)->SetActiveChannelGroup(group);
+ }
+ return m_activeGroupTV;
+ }
+}
+
+CDateTime CPVRPlaybackState::GetPlaybackTime(int iClientID, int iUniqueChannelID) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = GetPlayingEpgTag();
+ if (epgTag && iClientID == epgTag->ClientID() && iUniqueChannelID == epgTag->UniqueChannelID())
+ {
+ // playing an epg tag on requested channel
+ return epgTag->StartAsUTC() +
+ CDateTimeSpan(0, 0, 0, CServiceBroker::GetDataCacheCore().GetPlayTime() / 1000);
+ }
+
+ // not playing / playing live / playing timeshifted
+ return GetChannelPlaybackTime(iClientID, iUniqueChannelID);
+}
+
+CDateTime CPVRPlaybackState::GetChannelPlaybackTime(int iClientID, int iUniqueChannelID) const
+{
+ if (IsPlayingChannel(iClientID, iUniqueChannelID))
+ {
+ // start time valid?
+ time_t startTime = CServiceBroker::GetDataCacheCore().GetStartTime();
+ if (startTime > 0)
+ return CDateTime(startTime + CServiceBroker::GetDataCacheCore().GetPlayTime() / 1000);
+ }
+
+ // not playing / playing live
+ return CDateTime::GetUTCDateTime();
+}
+
+void CPVRPlaybackState::UpdateLastWatched(const std::shared_ptr<CPVRChannelGroupMember>& channel,
+ const CDateTime& time)
+{
+ time_t iTime;
+ time.GetAsTime(iTime);
+
+ channel->Channel()->SetLastWatched(iTime);
+
+ // update last watched timestamp for group
+ const bool bRadio = channel->Channel()->IsRadio();
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio)->GetById(channel->GroupID());
+ if (group)
+ group->SetLastWatched(iTime);
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRPlaybackState::GetLastPlayedChannelGroupMember(
+ bool bRadio) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return bRadio ? m_lastPlayedChannelRadio : m_lastPlayedChannelTV;
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRPlaybackState::
+ GetPreviousToLastPlayedChannelGroupMember(bool bRadio) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return bRadio ? m_previousToLastPlayedChannelRadio : m_previousToLastPlayedChannelTV;
+}
diff --git a/xbmc/pvr/PVRPlaybackState.h b/xbmc/pvr/PVRPlaybackState.h
new file mode 100644
index 0000000..2ef7500
--- /dev/null
+++ b/xbmc/pvr/PVRPlaybackState.h
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2012-2019 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <string>
+
+class CDateTime;
+class CFileItem;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRChannelGroup;
+class CPVRChannelGroupMember;
+class CPVREpgInfoTag;
+class CPVRRecording;
+
+class CPVRPlaybackState
+{
+public:
+ /*!
+ * @brief ctor.
+ */
+ CPVRPlaybackState();
+
+ /*!
+ * @brief dtor.
+ */
+ virtual ~CPVRPlaybackState();
+
+ /*!
+ * @brief clear instances, keep stored UIDs.
+ */
+ void Clear();
+
+ /*!
+ * @brief re-init using stored UIDs.
+ */
+ void ReInit();
+
+ /*!
+ * @brief Inform that playback of an item just started.
+ * @param item The item that started to play.
+ */
+ void OnPlaybackStarted(const CFileItem& item);
+
+ /*!
+ * @brief Inform that playback of an item was stopped due to user interaction.
+ * @param item The item that stopped to play.
+ * @return True, if the state has changed, false otherwise
+ */
+ bool OnPlaybackStopped(const CFileItem& item);
+
+ /*!
+ * @brief Inform that playback of an item has stopped without user interaction.
+ * @param item The item that ended to play.
+ */
+ void OnPlaybackEnded(const CFileItem& item);
+
+ /*!
+ * @brief Check if a TV channel, radio channel or recording is playing.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlaying() const;
+
+ /*!
+ * @brief Check if a TV channel is playing.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingTV() const;
+
+ /*!
+ * @brief Check if a radio channel is playing.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingRadio() const;
+
+ /*!
+ * @brief Check if a an encrypted TV or radio channel is playing.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingEncryptedChannel() const;
+
+ /*!
+ * @brief Check if a recording is playing.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingRecording() const;
+
+ /*!
+ * @brief Check if an epg tag is playing.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingEpgTag() const;
+
+ /*!
+ * @brief Check whether playing channel matches given uids.
+ * @param iClientID The client id.
+ * @param iUniqueChannelID The channel uid.
+ * @return True on match, false if there is no match or no channel is playing.
+ */
+ bool IsPlayingChannel(int iClientID, int iUniqueChannelID) const;
+
+ /*!
+ * @brief Check if the given channel is playing.
+ * @param channel The channel to check.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingChannel(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Check if the given recording is playing.
+ * @param recording The recording to check.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingRecording(const std::shared_ptr<CPVRRecording>& recording) const;
+
+ /*!
+ * @brief Check if the given epg tag is playing.
+ * @param epgTag The tag to check.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingEpgTag(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ /*!
+ * @brief Return the channel that is currently playing.
+ * @return The channel or nullptr if none is playing.
+ */
+ std::shared_ptr<CPVRChannel> GetPlayingChannel() const;
+
+ /*!
+ * @brief Return the channel group member that is currently playing.
+ * @return The channel group member or nullptr if none is playing.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetPlayingChannelGroupMember() const;
+
+ /*!
+ * @brief Return the recording that is currently playing.
+ * @return The recording or nullptr if none is playing.
+ */
+ std::shared_ptr<CPVRRecording> GetPlayingRecording() const;
+
+ /*!
+ * @brief Return the epg tag that is currently playing.
+ * @return The tag or nullptr if none is playing.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetPlayingEpgTag() const;
+
+ /*!
+ * @brief Return playing channel unique identifier
+ * @return The channel id or -1 if not present
+ */
+ int GetPlayingChannelUniqueID() const;
+
+ /*!
+ * @brief Get the name of the playing client, if there is one.
+ * @return The name of the client or an empty string if nothing is playing.
+ */
+ std::string GetPlayingClientName() const;
+
+ /*!
+ * @brief Get the ID of the playing client, if there is one.
+ * @return The ID or -1 if no client is playing.
+ */
+ int GetPlayingClientID() const;
+
+ /*!
+ * @brief Check whether there are active recordings.
+ * @return True if there are active recordings, false otherwise.
+ */
+ bool IsRecording() const;
+
+ /*!
+ * @brief Check whether there is an active recording on the currenlyt playing channel.
+ * @return True if there is a playing channel and there is an active recording on that channel, false otherwise.
+ */
+ bool IsRecordingOnPlayingChannel() const;
+
+ /*!
+ * @brief Check if an active recording is playing.
+ * @return True if an in-progress (active) recording is playing, false otherwise.
+ */
+ bool IsPlayingActiveRecording() const;
+
+ /*!
+ * @brief Check whether the currently playing channel can be recorded.
+ * @return True if there is a playing channel that can be recorded, false otherwise.
+ */
+ bool CanRecordOnPlayingChannel() const;
+
+ /*!
+ * @brief Set the active channel group.
+ * @param group The new group.
+ */
+ void SetActiveChannelGroup(const std::shared_ptr<CPVRChannelGroup>& group);
+
+ /*!
+ * @brief Get the active channel group.
+ * @param bRadio True to get the active radio group, false to get the active TV group.
+ * @return The current group or the group containing all channels if it's not set.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetActiveChannelGroup(bool bRadio) const;
+
+ /*!
+ * @brief Get the last played channel group member.
+ * @param bRadio True to get the radio group member, false to get the TV group member.
+ * @return The last played group member or nullptr if it's not available.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetLastPlayedChannelGroupMember(bool bRadio) const;
+
+ /*!
+ * @brief Get the channel group member that was played before the last played member.
+ * @param bRadio True to get the radio group member, false to get the TV group member.
+ * @return The previous played group member or nullptr if it's not available.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetPreviousToLastPlayedChannelGroupMember(
+ bool bRadio) const;
+
+ /*!
+ * @brief Get current playback time for the given channel, taking timeshifting and playing
+ * epg tags into account.
+ * @param iClientID The client id.
+ * @param iUniqueChannelID The channel uid.
+ * @return The playback time or 'now' if not playing.
+ */
+ CDateTime GetPlaybackTime(int iClientID, int iUniqueChannelID) const;
+
+ /*!
+ * @brief Get current playback time for the given channel, taking timeshifting into account.
+ * @param iClientID The client id.
+ * @param iUniqueChannelID The channel uid.
+ * @return The playback time or 'now' if not playing.
+ */
+ CDateTime GetChannelPlaybackTime(int iClientID, int iUniqueChannelID) const;
+
+private:
+ void ClearData();
+
+ /*!
+ * @brief Set the active group to the group of the supplied channel group member.
+ * @param channel The channel group member
+ */
+ void SetActiveChannelGroup(const std::shared_ptr<CPVRChannelGroupMember>& channel);
+
+ /*!
+ * @brief Updates the last watched timestamps of the channel and group which are currently playing.
+ * @param channel The channel which is updated
+ * @param time The last watched time to set
+ */
+ void UpdateLastWatched(const std::shared_ptr<CPVRChannelGroupMember>& channel,
+ const CDateTime& time);
+
+ mutable CCriticalSection m_critSection;
+
+ std::shared_ptr<CPVRChannelGroupMember> m_playingChannel;
+ std::shared_ptr<CPVRRecording> m_playingRecording;
+ std::shared_ptr<CPVREpgInfoTag> m_playingEpgTag;
+ std::shared_ptr<CPVRChannelGroupMember> m_lastPlayedChannelTV;
+ std::shared_ptr<CPVRChannelGroupMember> m_lastPlayedChannelRadio;
+ std::shared_ptr<CPVRChannelGroupMember> m_previousToLastPlayedChannelTV;
+ std::shared_ptr<CPVRChannelGroupMember> m_previousToLastPlayedChannelRadio;
+ std::string m_strPlayingClientName;
+ int m_playingGroupId = -1;
+ int m_playingClientId = -1;
+ int m_playingChannelUniqueId = -1;
+ std::string m_strPlayingRecordingUniqueId;
+ int m_playingEpgTagChannelUniqueId = -1;
+ unsigned int m_playingEpgTagUniqueId = 0;
+ std::shared_ptr<CPVRChannelGroup> m_activeGroupTV;
+ std::shared_ptr<CPVRChannelGroup> m_activeGroupRadio;
+
+ class CLastWatchedUpdateTimer;
+ std::unique_ptr<CLastWatchedUpdateTimer> m_lastWatchedUpdateTimer;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/PVRStreamProperties.cpp b/xbmc/pvr/PVRStreamProperties.cpp
new file mode 100644
index 0000000..272b33a
--- /dev/null
+++ b/xbmc/pvr/PVRStreamProperties.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012-2019 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRStreamProperties.h"
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_general.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+
+using namespace PVR;
+
+std::string CPVRStreamProperties::GetStreamURL() const
+{
+ const auto it = std::find_if(cbegin(), cend(), [](const auto& prop) {
+ return prop.first == PVR_STREAM_PROPERTY_STREAMURL;
+ });
+ return it != cend() ? (*it).second : std::string();
+}
+
+std::string CPVRStreamProperties::GetStreamMimeType() const
+{
+ const auto it = std::find_if(cbegin(), cend(), [](const auto& prop) {
+ return prop.first == PVR_STREAM_PROPERTY_MIMETYPE;
+ });
+ return it != cend() ? (*it).second : std::string();
+}
+
+bool CPVRStreamProperties::EPGPlaybackAsLive() const
+{
+ const auto it = std::find_if(cbegin(), cend(), [](const auto& prop) {
+ return prop.first == PVR_STREAM_PROPERTY_EPGPLAYBACKASLIVE;
+ });
+ return it != cend() ? StringUtils::EqualsNoCase((*it).second, "true") : false;
+}
diff --git a/xbmc/pvr/PVRStreamProperties.h b/xbmc/pvr/PVRStreamProperties.h
new file mode 100644
index 0000000..6690660
--- /dev/null
+++ b/xbmc/pvr/PVRStreamProperties.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2012-2019 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace PVR
+{
+
+class CPVRStreamProperties : public std::vector<std::pair<std::string, std::string>>
+{
+public:
+ CPVRStreamProperties() = default;
+ virtual ~CPVRStreamProperties() = default;
+
+ /*!
+ * @brief Obtain the URL of the stream.
+ * @return The stream URL or empty string, if not found.
+ */
+ std::string GetStreamURL() const;
+
+ /*!
+ * @brief Obtain the MIME type of the stream.
+ * @return The stream's MIME type or empty string, if not found.
+ */
+ std::string GetStreamMimeType() const;
+
+ /*!
+ * @brief If props are from an EPG tag indicates if playback should be as live playback would be
+ * @return true if it should be played back as live, false otherwise.
+ */
+ bool EPGPlaybackAsLive() const;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRThumbLoader.cpp b/xbmc/pvr/PVRThumbLoader.cpp
new file mode 100644
index 0000000..4b3544c
--- /dev/null
+++ b/xbmc/pvr/PVRThumbLoader.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRThumbLoader.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "TextureCacheJob.h"
+#include "pictures/Picture.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRManager.h"
+#include "pvr/filesystem/PVRGUIDirectory.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <ctime>
+
+using namespace PVR;
+
+bool CPVRThumbLoader::LoadItem(CFileItem* item)
+{
+ bool result = LoadItemCached(item);
+ result |= LoadItemLookup(item);
+ return result;
+}
+
+bool CPVRThumbLoader::LoadItemCached(CFileItem* item)
+{
+ return FillThumb(*item);
+}
+
+bool CPVRThumbLoader::LoadItemLookup(CFileItem* item)
+{
+ return false;
+}
+
+void CPVRThumbLoader::OnLoaderFinish()
+{
+ if (m_bInvalidated)
+ {
+ m_bInvalidated = false;
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated);
+ }
+ CThumbLoader::OnLoaderFinish();
+}
+
+void CPVRThumbLoader::ClearCachedImage(CFileItem& item)
+{
+ const std::string thumb = item.GetArt("thumb");
+ if (!thumb.empty())
+ {
+ CServiceBroker::GetTextureCache()->ClearCachedImage(thumb);
+ if (m_textureDatabase->Open())
+ {
+ m_textureDatabase->ClearTextureForPath(item.GetPath(), "thumb");
+ m_textureDatabase->Close();
+ }
+ item.SetArt("thumb", "");
+ m_bInvalidated = true;
+ }
+}
+
+void CPVRThumbLoader::ClearCachedImages(const CFileItemList& items)
+{
+ for (auto& item : items)
+ ClearCachedImage(*item);
+}
+
+bool CPVRThumbLoader::FillThumb(CFileItem& item)
+{
+ // see whether we have a cached image for this item
+ std::string thumb = GetCachedImage(item, "thumb");
+ if (thumb.empty())
+ {
+ if (item.IsPVRChannelGroup())
+ thumb = CreateChannelGroupThumb(item);
+ else
+ CLog::LogF(LOGERROR, "Unsupported PVR item '{}'", item.GetPath());
+
+ if (!thumb.empty())
+ {
+ SetCachedImage(item, "thumb", thumb);
+ m_bInvalidated = true;
+ }
+ }
+
+ if (thumb.empty())
+ return false;
+
+ item.SetArt("thumb", thumb);
+ return true;
+}
+
+std::string CPVRThumbLoader::CreateChannelGroupThumb(const CFileItem& channelGroupItem)
+{
+ const CPVRGUIDirectory channelGroupDir(channelGroupItem.GetPath());
+ CFileItemList channels;
+ if (channelGroupDir.GetChannelsDirectory(channels))
+ {
+ std::vector<std::string> channelIcons;
+ for (const auto& channel : channels)
+ {
+ const std::string& icon = channel->GetArt("icon");
+ if (!icon.empty())
+ channelIcons.emplace_back(CPVRCachedImages::UnwrapImageURL(icon));
+
+ if (channelIcons.size() == 9) // limit number of tiles
+ break;
+ }
+
+ std::string thumb = StringUtils::Format(
+ "{}?ts={}", // append timestamp to Thumb URL to enforce texture refresh
+ CTextureUtils::GetWrappedImageURL(channelGroupItem.GetPath(), "pvr"), std::time(nullptr));
+ const std::string relativeCacheFile = CTextureCache::GetCacheFile(thumb) + ".png";
+ if (CPicture::CreateTiledThumb(channelIcons, CTextureCache::GetCachedPath(relativeCacheFile)))
+ {
+ CTextureDetails details;
+ details.file = relativeCacheFile;
+ details.width = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes;
+ details.height = details.width;
+ CServiceBroker::GetTextureCache()->AddCachedTexture(thumb, details);
+ return thumb;
+ }
+ }
+
+ return {};
+}
diff --git a/xbmc/pvr/PVRThumbLoader.h b/xbmc/pvr/PVRThumbLoader.h
new file mode 100644
index 0000000..33e1bd4
--- /dev/null
+++ b/xbmc/pvr/PVRThumbLoader.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "ThumbLoader.h"
+
+#include <string>
+
+namespace PVR
+{
+
+class CPVRThumbLoader : public CThumbLoader
+{
+public:
+ CPVRThumbLoader() = default;
+ ~CPVRThumbLoader() override = default;
+
+ bool LoadItem(CFileItem* item) override;
+ bool LoadItemCached(CFileItem* item) override;
+ bool LoadItemLookup(CFileItem* item) override;
+
+ void ClearCachedImage(CFileItem& item);
+ void ClearCachedImages(const CFileItemList& items);
+
+protected:
+ void OnLoaderFinish() override;
+
+private:
+ bool FillThumb(CFileItem& item);
+ std::string CreateChannelGroupThumb(const CFileItem& channelGroupItem);
+
+ bool m_bInvalidated = false;
+};
+
+}
diff --git a/xbmc/pvr/addons/CMakeLists.txt b/xbmc/pvr/addons/CMakeLists.txt
new file mode 100644
index 0000000..e8a0a51
--- /dev/null
+++ b/xbmc/pvr/addons/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES PVRClient.cpp
+ PVRClientCapabilities.cpp
+ PVRClientMenuHooks.cpp
+ PVRClientUID.cpp
+ PVRClients.cpp)
+
+set(HEADERS PVRClient.h
+ PVRClientCapabilities.h
+ PVRClientMenuHooks.h
+ PVRClientUID.h
+ PVRClients.h)
+
+core_add_library(pvr_addons)
diff --git a/xbmc/pvr/addons/PVRClient.cpp b/xbmc/pvr/addons/PVRClient.cpp
new file mode 100644
index 0000000..2a09277
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClient.cpp
@@ -0,0 +1,2023 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRClient.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.h"
+#include "dialogs/GUIDialogKaiToast.h" //! @todo get rid of GUI in core
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRStreamProperties.h"
+#include "pvr/addons/PVRClientMenuHooks.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupInternal.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/providers/PVRProvider.h"
+#include "pvr/providers/PVRProviders.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimerType.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <map>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+
+extern "C"
+{
+#include <libavcodec/avcodec.h>
+}
+
+using namespace ADDON;
+
+namespace PVR
+{
+
+#define DEFAULT_INFO_STRING_VALUE "unknown"
+
+CPVRClient::CPVRClient(const ADDON::AddonInfoPtr& addonInfo,
+ ADDON::AddonInstanceId instanceId,
+ int clientId)
+ : IAddonInstanceHandler(ADDON_INSTANCE_PVR, addonInfo, instanceId), m_iClientId(clientId)
+{
+ // Create all interface parts independent to make API changes easier if
+ // something is added
+ m_ifc.pvr = new AddonInstance_PVR;
+ m_ifc.pvr->props = new AddonProperties_PVR();
+ m_ifc.pvr->toKodi = new AddonToKodiFuncTable_PVR();
+ m_ifc.pvr->toAddon = new KodiToAddonFuncTable_PVR();
+
+ ResetProperties();
+}
+
+CPVRClient::~CPVRClient()
+{
+ Destroy();
+
+ if (m_ifc.pvr)
+ {
+ delete m_ifc.pvr->props;
+ delete m_ifc.pvr->toKodi;
+ delete m_ifc.pvr->toAddon;
+ }
+ delete m_ifc.pvr;
+}
+
+void CPVRClient::StopRunningInstance()
+{
+ // stop the pvr manager and stop and unload the running pvr addon. pvr manager will be restarted on demand.
+ CServiceBroker::GetPVRManager().Stop();
+ CServiceBroker::GetPVRManager().Clients()->StopClient(m_iClientId, false);
+}
+
+void CPVRClient::OnPreInstall()
+{
+ // note: this method is also called on update; thus stop and unload possibly running instance
+ StopRunningInstance();
+}
+
+void CPVRClient::OnPreUnInstall()
+{
+ StopRunningInstance();
+}
+
+void CPVRClient::ResetProperties()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* initialise members */
+ m_strUserPath = CSpecialProtocol::TranslatePath(Profile());
+ m_strClientPath = CSpecialProtocol::TranslatePath(Path());
+ m_bReadyToUse = false;
+ m_bBlockAddonCalls = false;
+ m_iAddonCalls = 0;
+ m_allAddonCallsFinished.Set();
+ m_connectionState = PVR_CONNECTION_STATE_UNKNOWN;
+ m_prevConnectionState = PVR_CONNECTION_STATE_UNKNOWN;
+ m_ignoreClient = false;
+ m_iPriority = 0;
+ m_bPriorityFetched = false;
+ m_strBackendVersion = DEFAULT_INFO_STRING_VALUE;
+ m_strConnectionString = DEFAULT_INFO_STRING_VALUE;
+ m_strBackendName = DEFAULT_INFO_STRING_VALUE;
+ m_strBackendHostname.clear();
+ m_menuhooks.reset();
+ m_timertypes.clear();
+ m_clientCapabilities.clear();
+
+ m_ifc.pvr->props->strUserPath = m_strUserPath.c_str();
+ m_ifc.pvr->props->strClientPath = m_strClientPath.c_str();
+ m_ifc.pvr->props->iEpgMaxPastDays =
+ CServiceBroker::GetPVRManager().EpgContainer().GetPastDaysToDisplay();
+ m_ifc.pvr->props->iEpgMaxFutureDays =
+ CServiceBroker::GetPVRManager().EpgContainer().GetFutureDaysToDisplay();
+
+ m_ifc.pvr->toKodi->kodiInstance = this;
+ m_ifc.pvr->toKodi->TransferEpgEntry = cb_transfer_epg_entry;
+ m_ifc.pvr->toKodi->TransferChannelEntry = cb_transfer_channel_entry;
+ m_ifc.pvr->toKodi->TransferProviderEntry = cb_transfer_provider_entry;
+ m_ifc.pvr->toKodi->TransferTimerEntry = cb_transfer_timer_entry;
+ m_ifc.pvr->toKodi->TransferRecordingEntry = cb_transfer_recording_entry;
+ m_ifc.pvr->toKodi->AddMenuHook = cb_add_menu_hook;
+ m_ifc.pvr->toKodi->RecordingNotification = cb_recording_notification;
+ m_ifc.pvr->toKodi->TriggerChannelUpdate = cb_trigger_channel_update;
+ m_ifc.pvr->toKodi->TriggerProvidersUpdate = cb_trigger_provider_update;
+ m_ifc.pvr->toKodi->TriggerChannelGroupsUpdate = cb_trigger_channel_groups_update;
+ m_ifc.pvr->toKodi->TriggerTimerUpdate = cb_trigger_timer_update;
+ m_ifc.pvr->toKodi->TriggerRecordingUpdate = cb_trigger_recording_update;
+ m_ifc.pvr->toKodi->TriggerEpgUpdate = cb_trigger_epg_update;
+ m_ifc.pvr->toKodi->FreeDemuxPacket = cb_free_demux_packet;
+ m_ifc.pvr->toKodi->AllocateDemuxPacket = cb_allocate_demux_packet;
+ m_ifc.pvr->toKodi->TransferChannelGroup = cb_transfer_channel_group;
+ m_ifc.pvr->toKodi->TransferChannelGroupMember = cb_transfer_channel_group_member;
+ m_ifc.pvr->toKodi->ConnectionStateChange = cb_connection_state_change;
+ m_ifc.pvr->toKodi->EpgEventStateChange = cb_epg_event_state_change;
+ m_ifc.pvr->toKodi->GetCodecByName = cb_get_codec_by_name;
+
+ // Clear function addresses to have NULL if not set by addon
+ memset(m_ifc.pvr->toAddon, 0, sizeof(KodiToAddonFuncTable_PVR));
+}
+
+ADDON_STATUS CPVRClient::Create()
+{
+ ADDON_STATUS status(ADDON_STATUS_UNKNOWN);
+
+ ResetProperties();
+
+ /* initialise the add-on */
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating PVR add-on instance '{}'", ID());
+
+ bool bReadyToUse = false;
+ if ((status = CreateInstance()) == ADDON_STATUS_OK)
+ bReadyToUse = GetAddonProperties();
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Created PVR add-on instance '{}'. readytouse={} ", ID(),
+ bReadyToUse);
+
+ m_bReadyToUse = bReadyToUse;
+ return status;
+}
+
+void CPVRClient::Destroy()
+{
+ if (!m_bReadyToUse)
+ return;
+
+ m_bReadyToUse = false;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Destroying PVR add-on instance '{}'", ID());
+
+ m_bBlockAddonCalls = true;
+ m_allAddonCallsFinished.Wait();
+
+ DestroyInstance();
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Destroyed PVR add-on instance '{}'", ID());
+
+ if (m_menuhooks)
+ m_menuhooks->Clear();
+
+ ResetProperties();
+}
+
+void CPVRClient::Stop()
+{
+ m_bBlockAddonCalls = true;
+ m_bPriorityFetched = false;
+}
+
+void CPVRClient::Continue()
+{
+ m_bBlockAddonCalls = false;
+}
+
+void CPVRClient::ReCreate()
+{
+ Destroy();
+ Create();
+}
+
+bool CPVRClient::ReadyToUse() const
+{
+ return m_bReadyToUse;
+}
+
+PVR_CONNECTION_STATE CPVRClient::GetConnectionState() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_connectionState;
+}
+
+void CPVRClient::SetConnectionState(PVR_CONNECTION_STATE state)
+{
+ if (state == PVR_CONNECTION_STATE_CONNECTED)
+ {
+ // update properties - some will only be available after add-on is connected to backend
+ if (!GetAddonProperties())
+ CLog::LogF(LOGERROR, "Error reading PVR client properties");
+ }
+ else
+ {
+ if (!GetAddonNameStringProperties())
+ CLog::LogF(LOGERROR, "Cannot read PVR client name string properties");
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_prevConnectionState = m_connectionState;
+ m_connectionState = state;
+
+ if (m_connectionState == PVR_CONNECTION_STATE_CONNECTED)
+ m_ignoreClient = false;
+ else if (m_connectionState == PVR_CONNECTION_STATE_CONNECTING &&
+ m_prevConnectionState == PVR_CONNECTION_STATE_UNKNOWN)
+ m_ignoreClient = true; // ignore until connected
+}
+
+PVR_CONNECTION_STATE CPVRClient::GetPreviousConnectionState() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_prevConnectionState;
+}
+
+bool CPVRClient::IgnoreClient() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_ignoreClient;
+}
+
+bool CPVRClient::IsEnabled() const
+{
+ if (InstanceId() == ADDON_SINGLETON_INSTANCE_ID)
+ {
+ return !CServiceBroker::GetAddonMgr().IsAddonDisabled(ID());
+ }
+ else
+ {
+ bool instanceEnabled{false};
+ Addon()->ReloadSettings(InstanceId());
+ Addon()->GetSettingBool(ADDON_SETTING_INSTANCE_ENABLED_VALUE, instanceEnabled, InstanceId());
+ return instanceEnabled;
+ }
+}
+
+int CPVRClient::GetID() const
+{
+ return m_iClientId;
+}
+
+bool CPVRClient::GetAddonProperties()
+{
+ if (!GetAddonNameStringProperties())
+ return false;
+
+ PVR_ADDON_CAPABILITIES addonCapabilities = {};
+ std::vector<std::shared_ptr<CPVRTimerType>> timerTypes;
+
+ /* get the capabilities */
+ PVR_ERROR retVal = DoAddonCall(
+ __func__,
+ [&addonCapabilities](const AddonInstance* addon) {
+ return addon->toAddon->GetCapabilities(addon, &addonCapabilities);
+ },
+ true, false);
+
+ if (retVal != PVR_ERROR_NO_ERROR)
+ return false;
+
+ /* timer types */
+ retVal = DoAddonCall(
+ __func__,
+ [this, &addonCapabilities, &timerTypes](const AddonInstance* addon) {
+ std::unique_ptr<PVR_TIMER_TYPE[]> types_array(
+ new PVR_TIMER_TYPE[PVR_ADDON_TIMERTYPE_ARRAY_SIZE]);
+ int size = PVR_ADDON_TIMERTYPE_ARRAY_SIZE;
+
+ PVR_ERROR retval = addon->toAddon->GetTimerTypes(addon, types_array.get(), &size);
+
+ if (retval == PVR_ERROR_NOT_IMPLEMENTED)
+ {
+ // begin compat section
+ CLog::LogF(LOGWARNING,
+ "Add-on {} does not support timer types. It will work, but not benefit from "
+ "the timer features introduced with PVR Addon API 2.0.0",
+ GetFriendlyName());
+
+ // Create standard timer types (mostly) matching the timer functionality available in Isengard.
+ // This is for migration only and does not make changes to the addons obsolete. Addons should
+ // work and benefit from some UI changes (e.g. some of the timer settings dialog enhancements),
+ // but all old problems/bugs due to static attributes and values will remain the same as in
+ // Isengard. Also, new features (like epg search) are not available to addons automatically.
+ // This code can be removed once all addons actually support the respective PVR Addon API version.
+
+ size = 0;
+ // manual one time
+ memset(&types_array[size], 0, sizeof(types_array[size]));
+ types_array[size].iId = size + 1;
+ types_array[size].iAttributes =
+ PVR_TIMER_TYPE_IS_MANUAL | PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS | PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME | PVR_TIMER_TYPE_SUPPORTS_PRIORITY |
+ PVR_TIMER_TYPE_SUPPORTS_LIFETIME | PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS;
+ ++size;
+
+ // manual timer rule
+ memset(&types_array[size], 0, sizeof(types_array[size]));
+ types_array[size].iId = size + 1;
+ types_array[size].iAttributes =
+ PVR_TIMER_TYPE_IS_MANUAL | PVR_TIMER_TYPE_IS_REPEATING |
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE | PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME | PVR_TIMER_TYPE_SUPPORTS_END_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_PRIORITY | PVR_TIMER_TYPE_SUPPORTS_LIFETIME |
+ PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY | PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS |
+ PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS;
+ ++size;
+
+ if (addonCapabilities.bSupportsEPG)
+ {
+ // One-shot epg-based
+ memset(&types_array[size], 0, sizeof(types_array[size]));
+ types_array[size].iId = size + 1;
+ types_array[size].iAttributes =
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE | PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS | PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME | PVR_TIMER_TYPE_SUPPORTS_PRIORITY |
+ PVR_TIMER_TYPE_SUPPORTS_LIFETIME | PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS;
+ ++size;
+ }
+
+ retval = PVR_ERROR_NO_ERROR;
+ // end compat section
+ }
+
+ if (retval == PVR_ERROR_NO_ERROR)
+ {
+ timerTypes.reserve(size);
+ for (int i = 0; i < size; ++i)
+ {
+ if (types_array[i].iId == PVR_TIMER_TYPE_NONE)
+ {
+ CLog::LogF(LOGERROR, "Invalid timer type supplied by add-on '{}'.", ID());
+ continue;
+ }
+ timerTypes.emplace_back(
+ std::shared_ptr<CPVRTimerType>(new CPVRTimerType(types_array[i], m_iClientId)));
+ }
+ }
+ return retval;
+ },
+ addonCapabilities.bSupportsTimers, false);
+
+ if (retVal == PVR_ERROR_NOT_IMPLEMENTED)
+ retVal = PVR_ERROR_NO_ERROR; // timer support is optional.
+
+ /* update the members */
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_clientCapabilities = addonCapabilities;
+ m_timertypes = timerTypes;
+
+ return retVal == PVR_ERROR_NO_ERROR;
+}
+
+bool CPVRClient::GetAddonNameStringProperties()
+{
+ char strBackendName[PVR_ADDON_NAME_STRING_LENGTH] = {};
+ char strConnectionString[PVR_ADDON_NAME_STRING_LENGTH] = {};
+ char strBackendVersion[PVR_ADDON_NAME_STRING_LENGTH] = {};
+ char strBackendHostname[PVR_ADDON_NAME_STRING_LENGTH] = {};
+
+ /* get the name of the backend */
+ PVR_ERROR retVal = DoAddonCall(
+ __func__,
+ [&strBackendName](const AddonInstance* addon) {
+ return addon->toAddon->GetBackendName(addon, strBackendName, sizeof(strBackendName));
+ },
+ true, false);
+
+ if (retVal != PVR_ERROR_NO_ERROR)
+ return false;
+
+ /* get the connection string */
+ retVal = DoAddonCall(
+ __func__,
+ [&strConnectionString](const AddonInstance* addon) {
+ return addon->toAddon->GetConnectionString(addon, strConnectionString,
+ sizeof(strConnectionString));
+ },
+ true, false);
+
+ if (retVal != PVR_ERROR_NO_ERROR && retVal != PVR_ERROR_NOT_IMPLEMENTED)
+ return false;
+
+ /* backend version number */
+ retVal = DoAddonCall(
+ __func__,
+ [&strBackendVersion](const AddonInstance* addon) {
+ return addon->toAddon->GetBackendVersion(addon, strBackendVersion,
+ sizeof(strBackendVersion));
+ },
+ true, false);
+
+ if (retVal != PVR_ERROR_NO_ERROR)
+ return false;
+
+ /* backend hostname */
+ retVal = DoAddonCall(
+ __func__,
+ [&strBackendHostname](const AddonInstance* addon) {
+ return addon->toAddon->GetBackendHostname(addon, strBackendHostname,
+ sizeof(strBackendHostname));
+ },
+ true, false);
+
+ if (retVal != PVR_ERROR_NO_ERROR && retVal != PVR_ERROR_NOT_IMPLEMENTED)
+ return false;
+
+ /* update the members */
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strBackendName = strBackendName;
+ m_strConnectionString = strConnectionString;
+ m_strBackendVersion = strBackendVersion;
+ m_strBackendHostname = strBackendHostname;
+
+ return true;
+}
+
+const std::string& CPVRClient::GetBackendName() const
+{
+ return m_strBackendName;
+}
+
+const std::string& CPVRClient::GetBackendVersion() const
+{
+ return m_strBackendVersion;
+}
+
+const std::string& CPVRClient::GetBackendHostname() const
+{
+ return m_strBackendHostname;
+}
+
+const std::string& CPVRClient::GetConnectionString() const
+{
+ return m_strConnectionString;
+}
+
+const std::string CPVRClient::GetFriendlyName() const
+{
+
+ if (Addon()->SupportsInstanceSettings())
+ {
+ std::string instanceName;
+ Addon()->GetSettingString(ADDON_SETTING_INSTANCE_NAME_VALUE, instanceName, InstanceId());
+ if (!instanceName.empty())
+ return StringUtils::Format("{} ({})", Name(), instanceName);
+ }
+ return Name();
+}
+
+PVR_ERROR CPVRClient::GetDriveSpace(uint64_t& iTotal, uint64_t& iUsed)
+{
+ /* default to 0 in case of error */
+ iTotal = 0;
+ iUsed = 0;
+
+ return DoAddonCall(__func__, [&iTotal, &iUsed](const AddonInstance* addon) {
+ uint64_t iTotalSpace = 0;
+ uint64_t iUsedSpace = 0;
+ PVR_ERROR error = addon->toAddon->GetDriveSpace(addon, &iTotalSpace, &iUsedSpace);
+ if (error == PVR_ERROR_NO_ERROR)
+ {
+ iTotal = iTotalSpace;
+ iUsed = iUsedSpace;
+ }
+ return error;
+ });
+}
+
+PVR_ERROR CPVRClient::StartChannelScan()
+{
+ return DoAddonCall(
+ __func__,
+ [](const AddonInstance* addon) { return addon->toAddon->OpenDialogChannelScan(addon); },
+ m_clientCapabilities.SupportsChannelScan());
+}
+
+PVR_ERROR CPVRClient::OpenDialogChannelAdd(const std::shared_ptr<CPVRChannel>& channel)
+{
+ return DoAddonCall(
+ __func__,
+ [channel](const AddonInstance* addon) {
+ PVR_CHANNEL addonChannel;
+ channel->FillAddonData(addonChannel);
+ return addon->toAddon->OpenDialogChannelAdd(addon, &addonChannel);
+ },
+ m_clientCapabilities.SupportsChannelSettings());
+}
+
+PVR_ERROR CPVRClient::OpenDialogChannelSettings(const std::shared_ptr<CPVRChannel>& channel)
+{
+ return DoAddonCall(
+ __func__,
+ [channel](const AddonInstance* addon) {
+ PVR_CHANNEL addonChannel;
+ channel->FillAddonData(addonChannel);
+ return addon->toAddon->OpenDialogChannelSettings(addon, &addonChannel);
+ },
+ m_clientCapabilities.SupportsChannelSettings());
+}
+
+PVR_ERROR CPVRClient::DeleteChannel(const std::shared_ptr<CPVRChannel>& channel)
+{
+ return DoAddonCall(
+ __func__,
+ [channel](const AddonInstance* addon) {
+ PVR_CHANNEL addonChannel;
+ channel->FillAddonData(addonChannel);
+ return addon->toAddon->DeleteChannel(addon, &addonChannel);
+ },
+ m_clientCapabilities.SupportsChannelSettings());
+}
+
+PVR_ERROR CPVRClient::RenameChannel(const std::shared_ptr<CPVRChannel>& channel)
+{
+ return DoAddonCall(
+ __func__,
+ [channel](const AddonInstance* addon) {
+ PVR_CHANNEL addonChannel;
+ channel->FillAddonData(addonChannel);
+ strncpy(addonChannel.strChannelName, channel->ChannelName().c_str(),
+ sizeof(addonChannel.strChannelName) - 1);
+ return addon->toAddon->RenameChannel(addon, &addonChannel);
+ },
+ m_clientCapabilities.SupportsChannelSettings());
+}
+
+PVR_ERROR CPVRClient::GetEPGForChannel(int iChannelUid, CPVREpg* epg, time_t start, time_t end)
+{
+ return DoAddonCall(
+ __func__,
+ [this, iChannelUid, epg, start, end](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = epg;
+
+ int iPVRTimeCorrection =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection;
+
+ return addon->toAddon->GetEPGForChannel(addon, &handle, iChannelUid,
+ start ? start - iPVRTimeCorrection : 0,
+ end ? end - iPVRTimeCorrection : 0);
+ },
+ m_clientCapabilities.SupportsEPG());
+}
+
+PVR_ERROR CPVRClient::SetEPGMaxPastDays(int iPastDays)
+{
+ return DoAddonCall(
+ __func__,
+ [iPastDays](const AddonInstance* addon) {
+ return addon->toAddon->SetEPGMaxPastDays(addon, iPastDays);
+ },
+ m_clientCapabilities.SupportsEPG());
+}
+
+PVR_ERROR CPVRClient::SetEPGMaxFutureDays(int iFutureDays)
+{
+ return DoAddonCall(
+ __func__,
+ [iFutureDays](const AddonInstance* addon) {
+ return addon->toAddon->SetEPGMaxFutureDays(addon, iFutureDays);
+ },
+ m_clientCapabilities.SupportsEPG());
+}
+
+// This class wraps an EPG_TAG (PVR Addon API struct) to ensure that the string members of
+// that struct, which are const char pointers, stay valid until the EPG_TAG gets destructed.
+// Please note that this struct is also used to transfer huge amount of EPG_TAGs from
+// addon to Kodi. Thus, changing the struct to contain char arrays is not recommended,
+// because this would lead to huge amount of string copies when transferring epg data
+// from addon to Kodi.
+class CAddonEpgTag : public EPG_TAG
+{
+public:
+ CAddonEpgTag() = delete;
+ explicit CAddonEpgTag(const std::shared_ptr<const CPVREpgInfoTag>& kodiTag)
+ : m_strTitle(kodiTag->Title()),
+ m_strPlotOutline(kodiTag->PlotOutline()),
+ m_strPlot(kodiTag->Plot()),
+ m_strOriginalTitle(kodiTag->OriginalTitle()),
+ m_strCast(kodiTag->DeTokenize(kodiTag->Cast())),
+ m_strDirector(kodiTag->DeTokenize(kodiTag->Directors())),
+ m_strWriter(kodiTag->DeTokenize(kodiTag->Writers())),
+ m_strIMDBNumber(kodiTag->IMDBNumber()),
+ m_strEpisodeName(kodiTag->EpisodeName()),
+ m_strIconPath(kodiTag->ClientIconPath()),
+ m_strSeriesLink(kodiTag->SeriesLink()),
+ m_strGenreDescription(kodiTag->GenreDescription()),
+ m_strParentalRatingCode(kodiTag->ParentalRatingCode())
+ {
+ time_t t;
+ kodiTag->StartAsUTC().GetAsTime(t);
+ startTime = t;
+ kodiTag->EndAsUTC().GetAsTime(t);
+ endTime = t;
+
+ const CDateTime firstAired = kodiTag->FirstAired();
+ if (firstAired.IsValid())
+ m_strFirstAired = firstAired.GetAsW3CDate();
+
+ iUniqueBroadcastId = kodiTag->UniqueBroadcastID();
+ iUniqueChannelId = kodiTag->UniqueChannelID();
+ iParentalRating = kodiTag->ParentalRating();
+ iSeriesNumber = kodiTag->SeriesNumber();
+ iEpisodeNumber = kodiTag->EpisodeNumber();
+ iEpisodePartNumber = kodiTag->EpisodePart();
+ iStarRating = kodiTag->StarRating();
+ iYear = kodiTag->Year();
+ iFlags = kodiTag->Flags();
+ iGenreType = kodiTag->GenreType();
+ iGenreSubType = kodiTag->GenreSubType();
+ strTitle = m_strTitle.c_str();
+ strPlotOutline = m_strPlotOutline.c_str();
+ strPlot = m_strPlot.c_str();
+ strOriginalTitle = m_strOriginalTitle.c_str();
+ strCast = m_strCast.c_str();
+ strDirector = m_strDirector.c_str();
+ strWriter = m_strWriter.c_str();
+ strIMDBNumber = m_strIMDBNumber.c_str();
+ strEpisodeName = m_strEpisodeName.c_str();
+ strIconPath = m_strIconPath.c_str();
+ strSeriesLink = m_strSeriesLink.c_str();
+ strGenreDescription = m_strGenreDescription.c_str();
+ strFirstAired = m_strFirstAired.c_str();
+ strParentalRatingCode = m_strParentalRatingCode.c_str();
+ }
+
+ virtual ~CAddonEpgTag() = default;
+
+private:
+ std::string m_strTitle;
+ std::string m_strPlotOutline;
+ std::string m_strPlot;
+ std::string m_strOriginalTitle;
+ std::string m_strCast;
+ std::string m_strDirector;
+ std::string m_strWriter;
+ std::string m_strIMDBNumber;
+ std::string m_strEpisodeName;
+ std::string m_strIconPath;
+ std::string m_strSeriesLink;
+ std::string m_strGenreDescription;
+ std::string m_strFirstAired;
+ std::string m_strParentalRatingCode;
+};
+
+PVR_ERROR CPVRClient::IsRecordable(const std::shared_ptr<const CPVREpgInfoTag>& tag,
+ bool& bIsRecordable) const
+{
+ return DoAddonCall(
+ __func__,
+ [tag, &bIsRecordable](const AddonInstance* addon) {
+ CAddonEpgTag addonTag(tag);
+ return addon->toAddon->IsEPGTagRecordable(addon, &addonTag, &bIsRecordable);
+ },
+ m_clientCapabilities.SupportsRecordings() && m_clientCapabilities.SupportsEPG());
+}
+
+PVR_ERROR CPVRClient::IsPlayable(const std::shared_ptr<const CPVREpgInfoTag>& tag,
+ bool& bIsPlayable) const
+{
+ return DoAddonCall(
+ __func__,
+ [tag, &bIsPlayable](const AddonInstance* addon) {
+ CAddonEpgTag addonTag(tag);
+ return addon->toAddon->IsEPGTagPlayable(addon, &addonTag, &bIsPlayable);
+ },
+ m_clientCapabilities.SupportsEPG());
+}
+
+void CPVRClient::WriteStreamProperties(const PVR_NAMED_VALUE* properties,
+ unsigned int iPropertyCount,
+ CPVRStreamProperties& props)
+{
+ for (unsigned int i = 0; i < iPropertyCount; ++i)
+ {
+ props.emplace_back(std::make_pair(properties[i].strName, properties[i].strValue));
+ }
+}
+
+PVR_ERROR CPVRClient::GetEpgTagStreamProperties(const std::shared_ptr<CPVREpgInfoTag>& tag,
+ CPVRStreamProperties& props)
+{
+ return DoAddonCall(__func__, [&tag, &props](const AddonInstance* addon) {
+ CAddonEpgTag addonTag(tag);
+
+ unsigned int iPropertyCount = STREAM_MAX_PROPERTY_COUNT;
+ std::unique_ptr<PVR_NAMED_VALUE[]> properties(new PVR_NAMED_VALUE[iPropertyCount]);
+ memset(properties.get(), 0, iPropertyCount * sizeof(PVR_NAMED_VALUE));
+
+ PVR_ERROR error = addon->toAddon->GetEPGTagStreamProperties(addon, &addonTag, properties.get(),
+ &iPropertyCount);
+ if (error == PVR_ERROR_NO_ERROR)
+ WriteStreamProperties(properties.get(), iPropertyCount, props);
+
+ return error;
+ });
+}
+
+PVR_ERROR CPVRClient::GetEpgTagEdl(const std::shared_ptr<const CPVREpgInfoTag>& epgTag,
+ std::vector<PVR_EDL_ENTRY>& edls)
+{
+ edls.clear();
+ return DoAddonCall(
+ __func__,
+ [&epgTag, &edls](const AddonInstance* addon) {
+ CAddonEpgTag addonTag(epgTag);
+
+ PVR_EDL_ENTRY edl_array[PVR_ADDON_EDL_LENGTH];
+ int size = PVR_ADDON_EDL_LENGTH;
+ PVR_ERROR error = addon->toAddon->GetEPGTagEdl(addon, &addonTag, edl_array, &size);
+ if (error == PVR_ERROR_NO_ERROR)
+ {
+ edls.reserve(size);
+ for (int i = 0; i < size; ++i)
+ edls.emplace_back(edl_array[i]);
+ }
+ return error;
+ },
+ m_clientCapabilities.SupportsEpgTagEdl());
+}
+
+PVR_ERROR CPVRClient::GetChannelGroupsAmount(int& iGroups)
+{
+ iGroups = -1;
+ return DoAddonCall(
+ __func__,
+ [&iGroups](const AddonInstance* addon) {
+ return addon->toAddon->GetChannelGroupsAmount(addon, &iGroups);
+ },
+ m_clientCapabilities.SupportsChannelGroups());
+}
+
+PVR_ERROR CPVRClient::GetChannelGroups(CPVRChannelGroups* groups)
+{
+ return DoAddonCall(__func__,
+ [this, groups](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = groups;
+ return addon->toAddon->GetChannelGroups(addon, &handle, groups->IsRadio());
+ },
+ m_clientCapabilities.SupportsChannelGroups());
+}
+
+PVR_ERROR CPVRClient::GetChannelGroupMembers(
+ CPVRChannelGroup* group, std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers)
+{
+ return DoAddonCall(__func__,
+ [this, group, &groupMembers](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = &groupMembers;
+
+ PVR_CHANNEL_GROUP tag;
+ group->FillAddonData(tag);
+ return addon->toAddon->GetChannelGroupMembers(addon, &handle, &tag);
+ },
+ m_clientCapabilities.SupportsChannelGroups());
+}
+
+PVR_ERROR CPVRClient::GetProvidersAmount(int& iProviders)
+{
+ iProviders = -1;
+ return DoAddonCall(__func__, [&iProviders](const AddonInstance* addon) {
+ return addon->toAddon->GetProvidersAmount(addon, &iProviders);
+ });
+}
+
+PVR_ERROR CPVRClient::GetProviders(CPVRProvidersContainer& providers)
+{
+ return DoAddonCall(__func__,
+ [this, &providers](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = &providers;
+ return addon->toAddon->GetProviders(addon, &handle);
+ },
+ m_clientCapabilities.SupportsProviders());
+}
+
+PVR_ERROR CPVRClient::GetChannelsAmount(int& iChannels)
+{
+ iChannels = -1;
+ return DoAddonCall(__func__, [&iChannels](const AddonInstance* addon) {
+ return addon->toAddon->GetChannelsAmount(addon, &iChannels);
+ });
+}
+
+PVR_ERROR CPVRClient::GetChannels(bool radio, std::vector<std::shared_ptr<CPVRChannel>>& channels)
+{
+ return DoAddonCall(__func__,
+ [this, radio, &channels](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = &channels;
+ return addon->toAddon->GetChannels(addon, &handle, radio);
+ },
+ (radio && m_clientCapabilities.SupportsRadio()) ||
+ (!radio && m_clientCapabilities.SupportsTV()));
+}
+
+PVR_ERROR CPVRClient::GetRecordingsAmount(bool deleted, int& iRecordings)
+{
+ iRecordings = -1;
+ return DoAddonCall(
+ __func__,
+ [deleted, &iRecordings](const AddonInstance* addon) {
+ return addon->toAddon->GetRecordingsAmount(addon, deleted, &iRecordings);
+ },
+ m_clientCapabilities.SupportsRecordings() &&
+ (!deleted || m_clientCapabilities.SupportsRecordingsUndelete()));
+}
+
+PVR_ERROR CPVRClient::GetRecordings(CPVRRecordings* results, bool deleted)
+{
+ return DoAddonCall(__func__,
+ [this, results, deleted](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = results;
+ return addon->toAddon->GetRecordings(addon, &handle, deleted);
+ },
+ m_clientCapabilities.SupportsRecordings() &&
+ (!deleted || m_clientCapabilities.SupportsRecordingsUndelete()));
+}
+
+PVR_ERROR CPVRClient::DeleteRecording(const CPVRRecording& recording)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->DeleteRecording(addon, &tag);
+ },
+ m_clientCapabilities.SupportsRecordings() && m_clientCapabilities.SupportsRecordingsDelete());
+}
+
+PVR_ERROR CPVRClient::UndeleteRecording(const CPVRRecording& recording)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->UndeleteRecording(addon, &tag);
+ },
+ m_clientCapabilities.SupportsRecordingsUndelete());
+}
+
+PVR_ERROR CPVRClient::DeleteAllRecordingsFromTrash()
+{
+ return DoAddonCall(
+ __func__,
+ [](const AddonInstance* addon) {
+ return addon->toAddon->DeleteAllRecordingsFromTrash(addon);
+ },
+ m_clientCapabilities.SupportsRecordingsUndelete());
+}
+
+PVR_ERROR CPVRClient::RenameRecording(const CPVRRecording& recording)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->RenameRecording(addon, &tag);
+ },
+ m_clientCapabilities.SupportsRecordings());
+}
+
+PVR_ERROR CPVRClient::SetRecordingLifetime(const CPVRRecording& recording)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->SetRecordingLifetime(addon, &tag);
+ },
+ m_clientCapabilities.SupportsRecordingsLifetimeChange());
+}
+
+PVR_ERROR CPVRClient::SetRecordingPlayCount(const CPVRRecording& recording, int count)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording, count](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->SetRecordingPlayCount(addon, &tag, count);
+ },
+ m_clientCapabilities.SupportsRecordingsPlayCount());
+}
+
+PVR_ERROR CPVRClient::SetRecordingLastPlayedPosition(const CPVRRecording& recording,
+ int lastplayedposition)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording, lastplayedposition](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->SetRecordingLastPlayedPosition(addon, &tag, lastplayedposition);
+ },
+ m_clientCapabilities.SupportsRecordingsLastPlayedPosition());
+}
+
+PVR_ERROR CPVRClient::GetRecordingLastPlayedPosition(const CPVRRecording& recording, int& iPosition)
+{
+ iPosition = -1;
+ return DoAddonCall(
+ __func__,
+ [&recording, &iPosition](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->GetRecordingLastPlayedPosition(addon, &tag, &iPosition);
+ },
+ m_clientCapabilities.SupportsRecordingsLastPlayedPosition());
+}
+
+PVR_ERROR CPVRClient::GetRecordingEdl(const CPVRRecording& recording,
+ std::vector<PVR_EDL_ENTRY>& edls)
+{
+ edls.clear();
+ return DoAddonCall(
+ __func__,
+ [&recording, &edls](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+
+ PVR_EDL_ENTRY edl_array[PVR_ADDON_EDL_LENGTH];
+ int size = PVR_ADDON_EDL_LENGTH;
+ PVR_ERROR error = addon->toAddon->GetRecordingEdl(addon, &tag, edl_array, &size);
+ if (error == PVR_ERROR_NO_ERROR)
+ {
+ edls.reserve(size);
+ for (int i = 0; i < size; ++i)
+ edls.emplace_back(edl_array[i]);
+ }
+ return error;
+ },
+ m_clientCapabilities.SupportsRecordingsEdl());
+}
+
+PVR_ERROR CPVRClient::GetRecordingSize(const CPVRRecording& recording, int64_t& sizeInBytes)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording, &sizeInBytes](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->GetRecordingSize(addon, &tag, &sizeInBytes);
+ },
+ m_clientCapabilities.SupportsRecordingsSize());
+}
+
+PVR_ERROR CPVRClient::GetTimersAmount(int& iTimers)
+{
+ iTimers = -1;
+ return DoAddonCall(
+ __func__,
+ [&iTimers](const AddonInstance* addon) {
+ return addon->toAddon->GetTimersAmount(addon, &iTimers);
+ },
+ m_clientCapabilities.SupportsTimers());
+}
+
+PVR_ERROR CPVRClient::GetTimers(CPVRTimersContainer* results)
+{
+ return DoAddonCall(__func__,
+ [this, results](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = results;
+ return addon->toAddon->GetTimers(addon, &handle);
+ },
+ m_clientCapabilities.SupportsTimers());
+}
+
+PVR_ERROR CPVRClient::AddTimer(const CPVRTimerInfoTag& timer)
+{
+ return DoAddonCall(
+ __func__,
+ [&timer](const AddonInstance* addon) {
+ PVR_TIMER tag;
+ timer.FillAddonData(tag);
+ return addon->toAddon->AddTimer(addon, &tag);
+ },
+ m_clientCapabilities.SupportsTimers());
+}
+
+PVR_ERROR CPVRClient::DeleteTimer(const CPVRTimerInfoTag& timer, bool bForce /* = false */)
+{
+ return DoAddonCall(
+ __func__,
+ [&timer, bForce](const AddonInstance* addon) {
+ PVR_TIMER tag;
+ timer.FillAddonData(tag);
+ return addon->toAddon->DeleteTimer(addon, &tag, bForce);
+ },
+ m_clientCapabilities.SupportsTimers());
+}
+
+PVR_ERROR CPVRClient::UpdateTimer(const CPVRTimerInfoTag& timer)
+{
+ return DoAddonCall(
+ __func__,
+ [&timer](const AddonInstance* addon) {
+ PVR_TIMER tag;
+ timer.FillAddonData(tag);
+ return addon->toAddon->UpdateTimer(addon, &tag);
+ },
+ m_clientCapabilities.SupportsTimers());
+}
+
+PVR_ERROR CPVRClient::GetTimerTypes(std::vector<std::shared_ptr<CPVRTimerType>>& results) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ results = m_timertypes;
+ return PVR_ERROR_NO_ERROR;
+}
+
+PVR_ERROR CPVRClient::GetStreamReadChunkSize(int& iChunkSize)
+{
+ return DoAddonCall(
+ __func__,
+ [&iChunkSize](const AddonInstance* addon) {
+ return addon->toAddon->GetStreamReadChunkSize(addon, &iChunkSize);
+ },
+ m_clientCapabilities.SupportsRecordings() || m_clientCapabilities.HandlesInputStream());
+}
+
+PVR_ERROR CPVRClient::ReadLiveStream(void* lpBuf, int64_t uiBufSize, int& iRead)
+{
+ iRead = -1;
+ return DoAddonCall(__func__, [&lpBuf, uiBufSize, &iRead](const AddonInstance* addon) {
+ iRead = addon->toAddon->ReadLiveStream(addon, static_cast<unsigned char*>(lpBuf),
+ static_cast<int>(uiBufSize));
+ return (iRead == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::ReadRecordedStream(void* lpBuf, int64_t uiBufSize, int& iRead)
+{
+ iRead = -1;
+ return DoAddonCall(__func__, [&lpBuf, uiBufSize, &iRead](const AddonInstance* addon) {
+ iRead = addon->toAddon->ReadRecordedStream(addon, static_cast<unsigned char*>(lpBuf),
+ static_cast<int>(uiBufSize));
+ return (iRead == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::SeekLiveStream(int64_t iFilePosition, int iWhence, int64_t& iPosition)
+{
+ iPosition = -1;
+ return DoAddonCall(__func__, [iFilePosition, iWhence, &iPosition](const AddonInstance* addon) {
+ iPosition = addon->toAddon->SeekLiveStream(addon, iFilePosition, iWhence);
+ return (iPosition == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::SeekRecordedStream(int64_t iFilePosition, int iWhence, int64_t& iPosition)
+{
+ iPosition = -1;
+ return DoAddonCall(__func__, [iFilePosition, iWhence, &iPosition](const AddonInstance* addon) {
+ iPosition = addon->toAddon->SeekRecordedStream(addon, iFilePosition, iWhence);
+ return (iPosition == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::SeekTime(double time, bool backwards, double* startpts)
+{
+ return DoAddonCall(__func__, [time, backwards, &startpts](const AddonInstance* addon) {
+ return addon->toAddon->SeekTime(addon, time, backwards, startpts) ? PVR_ERROR_NO_ERROR
+ : PVR_ERROR_NOT_IMPLEMENTED;
+ });
+}
+
+PVR_ERROR CPVRClient::GetLiveStreamLength(int64_t& iLength)
+{
+ iLength = -1;
+ return DoAddonCall(__func__, [&iLength](const AddonInstance* addon) {
+ iLength = addon->toAddon->LengthLiveStream(addon);
+ return (iLength == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::GetRecordedStreamLength(int64_t& iLength)
+{
+ iLength = -1;
+ return DoAddonCall(__func__, [&iLength](const AddonInstance* addon) {
+ iLength = addon->toAddon->LengthRecordedStream(addon);
+ return (iLength == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::SignalQuality(int channelUid, PVR_SIGNAL_STATUS& qualityinfo)
+{
+ return DoAddonCall(__func__, [channelUid, &qualityinfo](const AddonInstance* addon) {
+ return addon->toAddon->GetSignalStatus(addon, channelUid, &qualityinfo);
+ });
+}
+
+PVR_ERROR CPVRClient::GetDescrambleInfo(int channelUid, PVR_DESCRAMBLE_INFO& descrambleinfo) const
+{
+ return DoAddonCall(
+ __func__,
+ [channelUid, &descrambleinfo](const AddonInstance* addon) {
+ return addon->toAddon->GetDescrambleInfo(addon, channelUid, &descrambleinfo);
+ },
+ m_clientCapabilities.SupportsDescrambleInfo());
+}
+
+PVR_ERROR CPVRClient::GetChannelStreamProperties(const std::shared_ptr<CPVRChannel>& channel,
+ CPVRStreamProperties& props)
+{
+ return DoAddonCall(__func__, [this, &channel, &props](const AddonInstance* addon) {
+ if (!CanPlayChannel(channel))
+ return PVR_ERROR_NO_ERROR; // no error, but no need to obtain the values from the addon
+
+ PVR_CHANNEL tag = {};
+ channel->FillAddonData(tag);
+
+ unsigned int iPropertyCount = STREAM_MAX_PROPERTY_COUNT;
+ std::unique_ptr<PVR_NAMED_VALUE[]> properties(new PVR_NAMED_VALUE[iPropertyCount]);
+ memset(properties.get(), 0, iPropertyCount * sizeof(PVR_NAMED_VALUE));
+
+ PVR_ERROR error =
+ addon->toAddon->GetChannelStreamProperties(addon, &tag, properties.get(), &iPropertyCount);
+ if (error == PVR_ERROR_NO_ERROR)
+ WriteStreamProperties(properties.get(), iPropertyCount, props);
+
+ return error;
+ });
+}
+
+PVR_ERROR CPVRClient::GetRecordingStreamProperties(const std::shared_ptr<CPVRRecording>& recording,
+ CPVRStreamProperties& props)
+{
+ return DoAddonCall(__func__, [this, &recording, &props](const AddonInstance* addon) {
+ if (!m_clientCapabilities.SupportsRecordings())
+ return PVR_ERROR_NO_ERROR; // no error, but no need to obtain the values from the addon
+
+ PVR_RECORDING tag = {};
+ recording->FillAddonData(tag);
+
+ unsigned int iPropertyCount = STREAM_MAX_PROPERTY_COUNT;
+ std::unique_ptr<PVR_NAMED_VALUE[]> properties(new PVR_NAMED_VALUE[iPropertyCount]);
+ memset(properties.get(), 0, iPropertyCount * sizeof(PVR_NAMED_VALUE));
+
+ PVR_ERROR error = addon->toAddon->GetRecordingStreamProperties(addon, &tag, properties.get(),
+ &iPropertyCount);
+ if (error == PVR_ERROR_NO_ERROR)
+ WriteStreamProperties(properties.get(), iPropertyCount, props);
+
+ return error;
+ });
+}
+
+PVR_ERROR CPVRClient::GetStreamProperties(PVR_STREAM_PROPERTIES* props)
+{
+ return DoAddonCall(__func__, [&props](const AddonInstance* addon) {
+ return addon->toAddon->GetStreamProperties(addon, props);
+ });
+}
+
+PVR_ERROR CPVRClient::DemuxReset()
+{
+ return DoAddonCall(
+ __func__,
+ [](const AddonInstance* addon) {
+ addon->toAddon->DemuxReset(addon);
+ return PVR_ERROR_NO_ERROR;
+ },
+ m_clientCapabilities.HandlesDemuxing());
+}
+
+PVR_ERROR CPVRClient::DemuxAbort()
+{
+ return DoAddonCall(
+ __func__,
+ [](const AddonInstance* addon) {
+ addon->toAddon->DemuxAbort(addon);
+ return PVR_ERROR_NO_ERROR;
+ },
+ m_clientCapabilities.HandlesDemuxing());
+}
+
+PVR_ERROR CPVRClient::DemuxFlush()
+{
+ return DoAddonCall(
+ __func__,
+ [](const AddonInstance* addon) {
+ addon->toAddon->DemuxFlush(addon);
+ return PVR_ERROR_NO_ERROR;
+ },
+ m_clientCapabilities.HandlesDemuxing());
+}
+
+PVR_ERROR CPVRClient::DemuxRead(DemuxPacket*& packet)
+{
+ return DoAddonCall(
+ __func__,
+ [&packet](const AddonInstance* addon) {
+ packet = static_cast<DemuxPacket*>(addon->toAddon->DemuxRead(addon));
+ return packet ? PVR_ERROR_NO_ERROR : PVR_ERROR_NOT_IMPLEMENTED;
+ },
+ m_clientCapabilities.HandlesDemuxing());
+}
+
+const char* CPVRClient::ToString(const PVR_ERROR error)
+{
+ switch (error)
+ {
+ case PVR_ERROR_NO_ERROR:
+ return "no error";
+ case PVR_ERROR_NOT_IMPLEMENTED:
+ return "not implemented";
+ case PVR_ERROR_SERVER_ERROR:
+ return "server error";
+ case PVR_ERROR_SERVER_TIMEOUT:
+ return "server timeout";
+ case PVR_ERROR_RECORDING_RUNNING:
+ return "recording already running";
+ case PVR_ERROR_ALREADY_PRESENT:
+ return "already present";
+ case PVR_ERROR_REJECTED:
+ return "rejected by the backend";
+ case PVR_ERROR_INVALID_PARAMETERS:
+ return "invalid parameters for this method";
+ case PVR_ERROR_FAILED:
+ return "the command failed";
+ case PVR_ERROR_UNKNOWN:
+ default:
+ return "unknown error";
+ }
+}
+
+PVR_ERROR CPVRClient::DoAddonCall(const char* strFunctionName,
+ const std::function<PVR_ERROR(const AddonInstance*)>& function,
+ bool bIsImplemented /* = true */,
+ bool bCheckReadyToUse /* = true */) const
+{
+ // Check preconditions.
+ if (!bIsImplemented)
+ return PVR_ERROR_NOT_IMPLEMENTED;
+
+ if (m_bBlockAddonCalls)
+ {
+ CLog::Log(LOGWARNING, "{}: Blocking call to add-on '{}'.", strFunctionName, ID());
+ return PVR_ERROR_SERVER_ERROR;
+ }
+
+ if (bCheckReadyToUse && IgnoreClient())
+ {
+ CLog::Log(LOGWARNING, "{}: Blocking call to add-on '{}'. Add-on not (yet) connected.",
+ strFunctionName, ID());
+ return PVR_ERROR_SERVER_ERROR;
+ }
+
+ if (bCheckReadyToUse && !ReadyToUse())
+ {
+ CLog::Log(LOGWARNING, "{}: Blocking call to add-on '{}'. Add-on not ready to use.",
+ strFunctionName, ID());
+ return PVR_ERROR_SERVER_ERROR;
+ }
+
+ // Call.
+ m_allAddonCallsFinished.Reset();
+ m_iAddonCalls++;
+
+ const PVR_ERROR error = function(m_ifc.pvr);
+
+ m_iAddonCalls--;
+ if (m_iAddonCalls == 0)
+ m_allAddonCallsFinished.Set();
+
+ // Log error, if any.
+ if (error != PVR_ERROR_NO_ERROR && error != PVR_ERROR_NOT_IMPLEMENTED)
+ CLog::Log(LOGERROR, "{}: Add-on '{}' returned an error: {}", strFunctionName, ID(),
+ ToString(error));
+
+ return error;
+}
+
+bool CPVRClient::CanPlayChannel(const std::shared_ptr<CPVRChannel>& channel) const
+{
+ return (m_bReadyToUse && ((m_clientCapabilities.SupportsTV() && !channel->IsRadio()) ||
+ (m_clientCapabilities.SupportsRadio() && channel->IsRadio())));
+}
+
+PVR_ERROR CPVRClient::OpenLiveStream(const std::shared_ptr<CPVRChannel>& channel)
+{
+ if (!channel)
+ return PVR_ERROR_INVALID_PARAMETERS;
+
+ return DoAddonCall(__func__, [this, channel](const AddonInstance* addon) {
+ CloseLiveStream();
+
+ if (!CanPlayChannel(channel))
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Add-on '{}' can not play channel '{}'", ID(),
+ channel->ChannelName());
+ return PVR_ERROR_SERVER_ERROR;
+ }
+ else
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Opening live stream for channel '{}'", channel->ChannelName());
+ PVR_CHANNEL tag;
+ channel->FillAddonData(tag);
+ return addon->toAddon->OpenLiveStream(addon, &tag) ? PVR_ERROR_NO_ERROR
+ : PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ });
+}
+
+PVR_ERROR CPVRClient::OpenRecordedStream(const std::shared_ptr<CPVRRecording>& recording)
+{
+ if (!recording)
+ return PVR_ERROR_INVALID_PARAMETERS;
+
+ return DoAddonCall(
+ __func__,
+ [this, recording](const AddonInstance* addon) {
+ CloseRecordedStream();
+
+ PVR_RECORDING tag;
+ recording->FillAddonData(tag);
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Opening stream for recording '{}'", recording->m_strTitle);
+ return addon->toAddon->OpenRecordedStream(addon, &tag) ? PVR_ERROR_NO_ERROR
+ : PVR_ERROR_NOT_IMPLEMENTED;
+ },
+ m_clientCapabilities.SupportsRecordings());
+}
+
+PVR_ERROR CPVRClient::CloseLiveStream()
+{
+ return DoAddonCall(__func__, [](const AddonInstance* addon) {
+ addon->toAddon->CloseLiveStream(addon);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::CloseRecordedStream()
+{
+ return DoAddonCall(__func__, [](const AddonInstance* addon) {
+ addon->toAddon->CloseRecordedStream(addon);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::PauseStream(bool bPaused)
+{
+ return DoAddonCall(__func__, [bPaused](const AddonInstance* addon) {
+ addon->toAddon->PauseStream(addon, bPaused);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::SetSpeed(int speed)
+{
+ return DoAddonCall(__func__, [speed](const AddonInstance* addon) {
+ addon->toAddon->SetSpeed(addon, speed);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::FillBuffer(bool mode)
+{
+ return DoAddonCall(__func__, [mode](const AddonInstance* addon) {
+ addon->toAddon->FillBuffer(addon, mode);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::CanPauseStream(bool& bCanPause) const
+{
+ bCanPause = false;
+ return DoAddonCall(__func__, [&bCanPause](const AddonInstance* addon) {
+ bCanPause = addon->toAddon->CanPauseStream(addon);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::CanSeekStream(bool& bCanSeek) const
+{
+ bCanSeek = false;
+ return DoAddonCall(__func__, [&bCanSeek](const AddonInstance* addon) {
+ bCanSeek = addon->toAddon->CanSeekStream(addon);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::GetStreamTimes(PVR_STREAM_TIMES* times)
+{
+ return DoAddonCall(__func__, [&times](const AddonInstance* addon) {
+ return addon->toAddon->GetStreamTimes(addon, times);
+ });
+}
+
+PVR_ERROR CPVRClient::IsRealTimeStream(bool& bRealTime) const
+{
+ bRealTime = false;
+ return DoAddonCall(__func__, [&bRealTime](const AddonInstance* addon) {
+ bRealTime = addon->toAddon->IsRealTimeStream(addon);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::OnSystemSleep()
+{
+ return DoAddonCall(
+ __func__, [](const AddonInstance* addon) { return addon->toAddon->OnSystemSleep(addon); });
+}
+
+PVR_ERROR CPVRClient::OnSystemWake()
+{
+ return DoAddonCall(
+ __func__, [](const AddonInstance* addon) { return addon->toAddon->OnSystemWake(addon); });
+}
+
+PVR_ERROR CPVRClient::OnPowerSavingActivated()
+{
+ return DoAddonCall(__func__, [](const AddonInstance* addon) {
+ return addon->toAddon->OnPowerSavingActivated(addon);
+ });
+}
+
+PVR_ERROR CPVRClient::OnPowerSavingDeactivated()
+{
+ return DoAddonCall(__func__, [](const AddonInstance* addon) {
+ return addon->toAddon->OnPowerSavingDeactivated(addon);
+ });
+}
+
+std::shared_ptr<CPVRClientMenuHooks> CPVRClient::GetMenuHooks()
+{
+ if (!m_menuhooks)
+ m_menuhooks.reset(new CPVRClientMenuHooks(ID()));
+
+ return m_menuhooks;
+}
+
+PVR_ERROR CPVRClient::CallEpgTagMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ return DoAddonCall(__func__, [&hook, &tag](const AddonInstance* addon) {
+ CAddonEpgTag addonTag(tag);
+
+ PVR_MENUHOOK menuHook;
+ menuHook.category = PVR_MENUHOOK_EPG;
+ menuHook.iHookId = hook.GetId();
+ menuHook.iLocalizedStringId = hook.GetLabelId();
+
+ return addon->toAddon->CallEPGMenuHook(addon, &menuHook, &addonTag);
+ });
+}
+
+PVR_ERROR CPVRClient::CallChannelMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVRChannel>& channel)
+{
+ return DoAddonCall(__func__, [&hook, &channel](const AddonInstance* addon) {
+ PVR_CHANNEL tag;
+ channel->FillAddonData(tag);
+
+ PVR_MENUHOOK menuHook;
+ menuHook.category = PVR_MENUHOOK_CHANNEL;
+ menuHook.iHookId = hook.GetId();
+ menuHook.iLocalizedStringId = hook.GetLabelId();
+
+ return addon->toAddon->CallChannelMenuHook(addon, &menuHook, &tag);
+ });
+}
+
+PVR_ERROR CPVRClient::CallRecordingMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVRRecording>& recording,
+ bool bDeleted)
+{
+ return DoAddonCall(__func__, [&hook, &recording, &bDeleted](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording->FillAddonData(tag);
+
+ PVR_MENUHOOK menuHook;
+ menuHook.category = bDeleted ? PVR_MENUHOOK_DELETED_RECORDING : PVR_MENUHOOK_RECORDING;
+ menuHook.iHookId = hook.GetId();
+ menuHook.iLocalizedStringId = hook.GetLabelId();
+
+ return addon->toAddon->CallRecordingMenuHook(addon, &menuHook, &tag);
+ });
+}
+
+PVR_ERROR CPVRClient::CallTimerMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVRTimerInfoTag>& timer)
+{
+ return DoAddonCall(__func__, [&hook, &timer](const AddonInstance* addon) {
+ PVR_TIMER tag;
+ timer->FillAddonData(tag);
+
+ PVR_MENUHOOK menuHook;
+ menuHook.category = PVR_MENUHOOK_TIMER;
+ menuHook.iHookId = hook.GetId();
+ menuHook.iLocalizedStringId = hook.GetLabelId();
+
+ return addon->toAddon->CallTimerMenuHook(addon, &menuHook, &tag);
+ });
+}
+
+PVR_ERROR CPVRClient::CallSettingsMenuHook(const CPVRClientMenuHook& hook)
+{
+ return DoAddonCall(__func__, [&hook](const AddonInstance* addon) {
+ PVR_MENUHOOK menuHook;
+ menuHook.category = PVR_MENUHOOK_SETTING;
+ menuHook.iHookId = hook.GetId();
+ menuHook.iLocalizedStringId = hook.GetLabelId();
+
+ return addon->toAddon->CallSettingsMenuHook(addon, &menuHook);
+ });
+}
+
+void CPVRClient::SetPriority(int iPriority)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_iPriority != iPriority)
+ {
+ m_iPriority = iPriority;
+ if (m_iClientId > PVR_INVALID_CLIENT_ID)
+ {
+ CServiceBroker::GetPVRManager().GetTVDatabase()->Persist(*this);
+ m_bPriorityFetched = true;
+ }
+ }
+}
+
+int CPVRClient::GetPriority() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_bPriorityFetched && m_iClientId > PVR_INVALID_CLIENT_ID)
+ {
+ m_iPriority = CServiceBroker::GetPVRManager().GetTVDatabase()->GetPriority(*this);
+ m_bPriorityFetched = true;
+ }
+ return m_iPriority;
+}
+
+void CPVRClient::HandleAddonCallback(const char* strFunctionName,
+ void* kodiInstance,
+ const std::function<void(CPVRClient* client)>& function,
+ bool bForceCall /* = false */)
+{
+ // Check preconditions.
+ CPVRClient* client = static_cast<CPVRClient*>(kodiInstance);
+ if (!client)
+ {
+ CLog::Log(LOGERROR, "{}: No instance pointer given!", strFunctionName);
+ return;
+ }
+
+ if (!bForceCall && client->m_bBlockAddonCalls && client->m_iAddonCalls == 0)
+ {
+ CLog::Log(LOGWARNING, LOGPVR, "{}: Ignoring callback from PVR client '{}'", strFunctionName,
+ client->ID());
+ return;
+ }
+
+ // Call.
+ function(client);
+}
+
+void CPVRClient::cb_transfer_channel_group(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_CHANNEL_GROUP* group)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!handle || !group)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ if (strlen(group->strGroupName) == 0)
+ {
+ CLog::LogF(LOGERROR, "Empty group name");
+ return;
+ }
+
+ // transfer this entry to the groups container
+ CPVRChannelGroups* kodiGroups = static_cast<CPVRChannelGroups*>(handle->dataAddress);
+ const auto transferGroup =
+ std::make_shared<CPVRChannelGroup>(*group, kodiGroups->GetGroupAll());
+ kodiGroups->UpdateFromClient(transferGroup);
+ });
+}
+
+void CPVRClient::cb_transfer_channel_group_member(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_CHANNEL_GROUP_MEMBER* member)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!handle || !member)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetByUniqueID(member->iChannelUniqueId,
+ client->GetID());
+ if (!channel)
+ {
+ CLog::LogF(LOGERROR, "Cannot find group '{}' or channel '{}'", member->strGroupName,
+ member->iChannelUniqueId);
+ }
+ else
+ {
+ auto* groupMembers =
+ static_cast<std::vector<std::shared_ptr<CPVRChannelGroupMember>>*>(handle->dataAddress);
+ groupMembers->emplace_back(
+ std::make_shared<CPVRChannelGroupMember>(member->strGroupName, member->iOrder, channel));
+ }
+ });
+}
+
+void CPVRClient::cb_transfer_epg_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const EPG_TAG* epgentry)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!handle || !epgentry)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ // transfer this entry to the epg
+ CPVREpg* epg = static_cast<CPVREpg*>(handle->dataAddress);
+ epg->UpdateEntry(epgentry, client->GetID());
+ });
+}
+
+void CPVRClient::cb_transfer_provider_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_PROVIDER* provider)
+{
+ if (!handle)
+ {
+ CLog::LogF(LOGERROR, "Invalid handler data");
+ return;
+ }
+
+ CPVRClient* client = static_cast<CPVRClient*>(kodiInstance);
+ CPVRProvidersContainer* kodiProviders = static_cast<CPVRProvidersContainer*>(handle->dataAddress);
+ if (!provider || !client || !kodiProviders)
+ {
+ CLog::LogF(LOGERROR, "Invalid handler data");
+ return;
+ }
+
+ /* transfer this entry to the internal channels group */
+ std::shared_ptr<CPVRProvider> transferProvider(
+ std::make_shared<CPVRProvider>(*provider, client->GetID()));
+ kodiProviders->UpdateFromClient(transferProvider);
+}
+
+void CPVRClient::cb_transfer_channel_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_CHANNEL* channel)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!handle || !channel)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ auto* channels = static_cast<std::vector<std::shared_ptr<CPVRChannel>>*>(handle->dataAddress);
+ channels->emplace_back(std::make_shared<CPVRChannel>(*channel, client->GetID()));
+ });
+}
+
+void CPVRClient::cb_transfer_recording_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_RECORDING* recording)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!handle || !recording)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ // transfer this entry to the recordings container
+ const std::shared_ptr<CPVRRecording> transferRecording =
+ std::make_shared<CPVRRecording>(*recording, client->GetID());
+ CPVRRecordings* recordings = static_cast<CPVRRecordings*>(handle->dataAddress);
+ recordings->UpdateFromClient(transferRecording, *client);
+ });
+}
+
+void CPVRClient::cb_transfer_timer_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_TIMER* timer)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!handle || !timer)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ // Note: channel can be nullptr here, for instance for epg-based timer rules
+ // ("record on any channel" condition)
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetByUniqueID(timer->iClientChannelUid,
+ client->GetID());
+
+ // transfer this entry to the timers container
+ const std::shared_ptr<CPVRTimerInfoTag> transferTimer =
+ std::make_shared<CPVRTimerInfoTag>(*timer, channel, client->GetID());
+ CPVRTimersContainer* timers = static_cast<CPVRTimersContainer*>(handle->dataAddress);
+ timers->UpdateFromClient(transferTimer);
+ });
+}
+
+void CPVRClient::cb_add_menu_hook(void* kodiInstance, const PVR_MENUHOOK* hook)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!hook)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ client->GetMenuHooks()->AddHook(*hook);
+ });
+}
+
+void CPVRClient::cb_recording_notification(void* kodiInstance,
+ const char* strName,
+ const char* strFileName,
+ bool bOnOff)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!strFileName)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ const std::string strLine1 = StringUtils::Format(g_localizeStrings.Get(bOnOff ? 19197 : 19198),
+ client->GetFriendlyName());
+ std::string strLine2;
+ if (strName)
+ strLine2 = strName;
+ else
+ strLine2 = strFileName;
+
+ // display a notification for 5 seconds
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, strLine1, strLine2, 5000,
+ false);
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(EventPtr(
+ new CNotificationEvent(client->GetFriendlyName(), strLine1, client->Icon(), strLine2)));
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Recording {} on client '{}'. name='{}' filename='{}'",
+ bOnOff ? "started" : "finished", client->ID(), strName, strFileName);
+ });
+}
+
+void CPVRClient::cb_trigger_channel_update(void* kodiInstance)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ // update channels in the next iteration of the pvrmanager's main loop
+ CServiceBroker::GetPVRManager().TriggerChannelsUpdate(client->GetID());
+ });
+}
+
+void CPVRClient::cb_trigger_provider_update(void* kodiInstance)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ /* update the providers table in the next iteration of the pvrmanager's main loop */
+ CServiceBroker::GetPVRManager().TriggerProvidersUpdate(client->GetID());
+ });
+}
+
+void CPVRClient::cb_trigger_timer_update(void* kodiInstance)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ // update timers in the next iteration of the pvrmanager's main loop
+ CServiceBroker::GetPVRManager().TriggerTimersUpdate(client->GetID());
+ });
+}
+
+void CPVRClient::cb_trigger_recording_update(void* kodiInstance)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ // update recordings in the next iteration of the pvrmanager's main loop
+ CServiceBroker::GetPVRManager().TriggerRecordingsUpdate(client->GetID());
+ });
+}
+
+void CPVRClient::cb_trigger_channel_groups_update(void* kodiInstance)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ // update all channel groups in the next iteration of the pvrmanager's main loop
+ CServiceBroker::GetPVRManager().TriggerChannelGroupsUpdate(client->GetID());
+ });
+}
+
+void CPVRClient::cb_trigger_epg_update(void* kodiInstance, unsigned int iChannelUid)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ CServiceBroker::GetPVRManager().EpgContainer().UpdateRequest(client->GetID(), iChannelUid);
+ });
+}
+
+void CPVRClient::cb_free_demux_packet(void* kodiInstance, DEMUX_PACKET* pPacket)
+{
+ HandleAddonCallback(__func__, kodiInstance,
+ [&](CPVRClient* client) {
+ CDVDDemuxUtils::FreeDemuxPacket(static_cast<DemuxPacket*>(pPacket));
+ },
+ true);
+}
+
+DEMUX_PACKET* CPVRClient::cb_allocate_demux_packet(void* kodiInstance, int iDataSize)
+{
+ DEMUX_PACKET* result = nullptr;
+
+ HandleAddonCallback(
+ __func__, kodiInstance,
+ [&](CPVRClient* client) { result = CDVDDemuxUtils::AllocateDemuxPacket(iDataSize); }, true);
+
+ return result;
+}
+
+void CPVRClient::cb_connection_state_change(void* kodiInstance,
+ const char* strConnectionString,
+ PVR_CONNECTION_STATE newState,
+ const char* strMessage)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!strConnectionString)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ const PVR_CONNECTION_STATE prevState(client->GetConnectionState());
+ if (prevState == newState)
+ return;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR,
+ "State for connection '{}' on client '{}' changed from '{}' to '{}'",
+ strConnectionString, client->ID(), prevState, newState);
+
+ client->SetConnectionState(newState);
+
+ std::string msg;
+ if (strMessage)
+ msg = strMessage;
+
+ CServiceBroker::GetPVRManager().ConnectionStateChange(client, std::string(strConnectionString),
+ newState, msg);
+ });
+}
+
+void CPVRClient::cb_epg_event_state_change(void* kodiInstance,
+ EPG_TAG* tag,
+ EPG_EVENT_STATE newState)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!tag)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ // Note: channel data and epg id may not yet be available. Tag will be fully initialized later.
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ std::make_shared<CPVREpgInfoTag>(*tag, client->GetID(), nullptr, -1);
+ CServiceBroker::GetPVRManager().EpgContainer().UpdateFromClient(epgTag, newState);
+ });
+}
+
+class CCodecIds
+{
+public:
+ virtual ~CCodecIds() = default;
+
+ static CCodecIds& GetInstance()
+ {
+ static CCodecIds _instance;
+ return _instance;
+ }
+
+ PVR_CODEC GetCodecByName(const char* strCodecName)
+ {
+ PVR_CODEC retVal = PVR_INVALID_CODEC;
+ if (strlen(strCodecName) == 0)
+ return retVal;
+
+ std::string strUpperCodecName = strCodecName;
+ StringUtils::ToUpper(strUpperCodecName);
+
+ std::map<std::string, PVR_CODEC>::const_iterator it = m_lookup.find(strUpperCodecName);
+ if (it != m_lookup.end())
+ retVal = it->second;
+
+ return retVal;
+ }
+
+private:
+ CCodecIds()
+ {
+ // get ids and names
+ const AVCodec* codec = nullptr;
+ void* i = nullptr;
+ PVR_CODEC tmp;
+ while ((codec = av_codec_iterate(&i)))
+ {
+ if (av_codec_is_decoder(codec))
+ {
+ tmp.codec_type = static_cast<PVR_CODEC_TYPE>(codec->type);
+ tmp.codec_id = codec->id;
+
+ std::string strUpperCodecName = codec->name;
+ StringUtils::ToUpper(strUpperCodecName);
+
+ m_lookup.insert(std::make_pair(strUpperCodecName, tmp));
+ }
+ }
+
+ // teletext is not returned by av_codec_next. we got our own decoder
+ tmp.codec_type = PVR_CODEC_TYPE_SUBTITLE;
+ tmp.codec_id = AV_CODEC_ID_DVB_TELETEXT;
+ m_lookup.insert(std::make_pair("TELETEXT", tmp));
+
+ // rds is not returned by av_codec_next. we got our own decoder
+ tmp.codec_type = PVR_CODEC_TYPE_RDS;
+ tmp.codec_id = AV_CODEC_ID_NONE;
+ m_lookup.insert(std::make_pair("RDS", tmp));
+
+ // ID3 is not returned by av_codec_next. we got our own decoder
+ tmp.codec_type = PVR_CODEC_TYPE_ID3;
+ tmp.codec_id = AV_CODEC_ID_NONE;
+ m_lookup.insert({"ID3", tmp});
+ }
+
+ std::map<std::string, PVR_CODEC> m_lookup;
+};
+
+PVR_CODEC CPVRClient::cb_get_codec_by_name(const void* kodiInstance, const char* strCodecName)
+{
+ PVR_CODEC result = PVR_INVALID_CODEC;
+
+ HandleAddonCallback(
+ __func__, const_cast<void*>(kodiInstance),
+ [&](CPVRClient* client) { result = CCodecIds::GetInstance().GetCodecByName(strCodecName); },
+ true);
+
+ return result;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/addons/PVRClient.h b/xbmc/pvr/addons/PVRClient.h
new file mode 100644
index 0000000..10203cf
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClient.h
@@ -0,0 +1,1062 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/binary-addons/AddonInstanceHandler.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr.h"
+#include "pvr/addons/PVRClientCapabilities.h"
+#include "threads/Event.h"
+
+#include <atomic>
+#include <functional>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+struct DemuxPacket;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRChannelGroup;
+class CPVRChannelGroupMember;
+class CPVRChannelGroups;
+class CPVRProvider;
+class CPVRProvidersContainer;
+class CPVRClientMenuHook;
+class CPVRClientMenuHooks;
+class CPVREpg;
+class CPVREpgInfoTag;
+class CPVRRecording;
+class CPVRRecordings;
+class CPVRStreamProperties;
+class CPVRTimerInfoTag;
+class CPVRTimerType;
+class CPVRTimersContainer;
+
+#define PVR_INVALID_CLIENT_ID (-2)
+
+/*!
+ * Interface from Kodi to a PVR add-on.
+ *
+ * Also translates Kodi's C++ structures to the add-on's C structures.
+ */
+class CPVRClient : public ADDON::IAddonInstanceHandler
+{
+public:
+ CPVRClient(const ADDON::AddonInfoPtr& addonInfo, ADDON::AddonInstanceId instanceId, int clientId);
+ ~CPVRClient() override;
+
+ void OnPreInstall() override;
+ void OnPreUnInstall() override;
+
+ /** @name PVR add-on methods */
+ //@{
+
+ /*!
+ * @brief Initialise the instance of this add-on.
+ */
+ ADDON_STATUS Create();
+
+ /*!
+ * @brief Stop this add-on instance. No more client add-on access after this call.
+ */
+ void Stop();
+
+ /*!
+ * @brief Continue this add-on instance. Client add-on access is okay again after this call.
+ */
+ void Continue();
+
+ /*!
+ * @brief Destroy the instance of this add-on.
+ */
+ void Destroy();
+
+ /*!
+ * @brief Destroy and recreate this add-on.
+ */
+ void ReCreate();
+
+ /*!
+ * @return True if this instance is initialised (ADDON_Create returned true), false otherwise.
+ */
+ bool ReadyToUse() const;
+
+ /*!
+ * @brief Gets the backend connection state.
+ * @return the backend connection state.
+ */
+ PVR_CONNECTION_STATE GetConnectionState() const;
+
+ /*!
+ * @brief Sets the backend connection state.
+ * @param state the new backend connection state.
+ */
+ void SetConnectionState(PVR_CONNECTION_STATE state);
+
+ /*!
+ * @brief Gets the backend's previous connection state.
+ * @return the backend's previous connection state.
+ */
+ PVR_CONNECTION_STATE GetPreviousConnectionState() const;
+
+ /*!
+ * @brief Check whether this client should be ignored.
+ * @return True if this client should be ignored, false otherwise.
+ */
+ bool IgnoreClient() const;
+
+ /*!
+ * @brief Check whether this client is enabled, according to its instance/add-on configuration.
+ * @return True if this client is enabled, false otherwise.
+ */
+ bool IsEnabled() const;
+
+ /*!
+ * @return The ID of this instance.
+ */
+ int GetID() const;
+
+ //@}
+ /** @name PVR server methods */
+ //@{
+
+ /*!
+ * @brief Query this add-on's capabilities.
+ * @return The add-on's capabilities.
+ */
+ const CPVRClientCapabilities& GetClientCapabilities() const { return m_clientCapabilities; }
+
+ /*!
+ * @brief Get the stream properties of the stream that's currently being read.
+ * @param pProperties The properties.
+ * @return PVR_ERROR_NO_ERROR if the properties have been fetched successfully.
+ */
+ PVR_ERROR GetStreamProperties(PVR_STREAM_PROPERTIES* pProperties);
+
+ /*!
+ * @return The name reported by the backend.
+ */
+ const std::string& GetBackendName() const;
+
+ /*!
+ * @return The version string reported by the backend.
+ */
+ const std::string& GetBackendVersion() const;
+
+ /*!
+ * @brief the ip address or alias of the pvr backend server
+ */
+ const std::string& GetBackendHostname() const;
+
+ /*!
+ * @return The connection string reported by the backend.
+ */
+ const std::string& GetConnectionString() const;
+
+ /*!
+ * @brief A friendly name used to uniquely identify the addon instance
+ * @return string that can be used in log messages and the GUI.
+ */
+ const std::string GetFriendlyName() const;
+
+ /*!
+ * @brief Get the disk space reported by the server.
+ * @param iTotal The total disk space.
+ * @param iUsed The used disk space.
+ * @return PVR_ERROR_NO_ERROR if the drive space has been fetched successfully.
+ */
+ PVR_ERROR GetDriveSpace(uint64_t& iTotal, uint64_t& iUsed);
+
+ /*!
+ * @brief Start a channel scan on the server.
+ * @return PVR_ERROR_NO_ERROR if the channel scan has been started successfully.
+ */
+ PVR_ERROR StartChannelScan();
+
+ /*!
+ * @brief Request the client to open dialog about given channel to add
+ * @param channel The channel to add
+ * @return PVR_ERROR_NO_ERROR if the add has been fetched successfully.
+ */
+ PVR_ERROR OpenDialogChannelAdd(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Request the client to open dialog about given channel settings
+ * @param channel The channel to edit
+ * @return PVR_ERROR_NO_ERROR if the edit has been fetched successfully.
+ */
+ PVR_ERROR OpenDialogChannelSettings(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Request the client to delete given channel
+ * @param channel The channel to delete
+ * @return PVR_ERROR_NO_ERROR if the delete has been fetched successfully.
+ */
+ PVR_ERROR DeleteChannel(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Request the client to rename given channel
+ * @param channel The channel to rename
+ * @return PVR_ERROR_NO_ERROR if the rename has been fetched successfully.
+ */
+ PVR_ERROR RenameChannel(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*
+ * @brief Check if an epg tag can be recorded
+ * @param tag The epg tag
+ * @param bIsRecordable Set to true if the tag can be recorded
+ * @return PVR_ERROR_NO_ERROR if bIsRecordable has been set successfully.
+ */
+ PVR_ERROR IsRecordable(const std::shared_ptr<const CPVREpgInfoTag>& tag,
+ bool& bIsRecordable) const;
+
+ /*
+ * @brief Check if an epg tag can be played
+ * @param tag The epg tag
+ * @param bIsPlayable Set to true if the tag can be played
+ * @return PVR_ERROR_NO_ERROR if bIsPlayable has been set successfully.
+ */
+ PVR_ERROR IsPlayable(const std::shared_ptr<const CPVREpgInfoTag>& tag, bool& bIsPlayable) const;
+
+ /*!
+ * @brief Fill the given container with the properties required for playback
+ * of the given EPG tag. Values are obtained from the PVR backend.
+ *
+ * @param tag The EPG tag.
+ * @param props The container to be filled with the stream properties.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetEpgTagStreamProperties(const std::shared_ptr<CPVREpgInfoTag>& tag,
+ CPVRStreamProperties& props);
+
+ //@}
+ /** @name PVR EPG methods */
+ //@{
+
+ /*!
+ * @brief Request an EPG table for a channel from the client.
+ * @param iChannelUid The UID of the channel to get the EPG table for.
+ * @param epg The table to write the data to.
+ * @param start The start time to use.
+ * @param end The end time to use.
+ * @return PVR_ERROR_NO_ERROR if the table has been fetched successfully.
+ */
+ PVR_ERROR GetEPGForChannel(int iChannelUid, CPVREpg* epg, time_t start, time_t end);
+
+ /*!
+ * @brief Tell the client the past time frame to use when notifying epg events back
+ * to Kodi.
+ *
+ * The client might push epg events asynchronously to Kodi using the callback
+ * function EpgEventStateChange. To be able to only push events that are
+ * actually of interest for Kodi, client needs to know about the past epg time
+ * frame Kodi uses.
+ *
+ * @param[in] iPastDays number of days before "now".
+ @ref EPG_TIMEFRAME_UNLIMITED means that Kodi is interested in all epg events,
+ regardless of event times.
+ * @return PVR_ERROR_NO_ERROR if new value was successfully set.
+ */
+ PVR_ERROR SetEPGMaxPastDays(int iPastDays);
+
+ /*!
+ * @brief Tell the client the future time frame to use when notifying epg events back
+ * to Kodi.
+ *
+ * The client might push epg events asynchronously to Kodi using the callback
+ * function EpgEventStateChange. To be able to only push events that are
+ * actually of interest for Kodi, client needs to know about the future epg time
+ * frame Kodi uses.
+ *
+ * @param[in] iFutureDays number of days after "now".
+ @ref EPG_TIMEFRAME_UNLIMITED means that Kodi is interested in all epg events,
+ regardless of event times.
+ * @return PVR_ERROR_NO_ERROR if new value was successfully set.
+ */
+ PVR_ERROR SetEPGMaxFutureDays(int iFutureDays);
+
+ //@}
+ /** @name PVR channel group methods */
+ //@{
+
+ /*!
+ * @brief Get the total amount of channel groups from the backend.
+ * @param iGroups The total amount of channel groups on the server or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetChannelGroupsAmount(int& iGroups);
+
+ /*!
+ * @brief Request the list of all channel groups from the backend.
+ * @param groups The groups container to get the groups for.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetChannelGroups(CPVRChannelGroups* groups);
+
+ /*!
+ * @brief Request the list of all group members from the backend.
+ * @param group The group to get the members for.
+ * @param groupMembers The container for the group members.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetChannelGroupMembers(
+ CPVRChannelGroup* group, std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers);
+
+ //@}
+ /** @name PVR channel methods */
+ //@{
+
+ /*!
+ * @brief Get the total amount of channels from the backend.
+ * @param iChannels The total amount of channels on the server or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetChannelsAmount(int& iChannels);
+
+ /*!
+ * @brief Request the list of all channels from the backend.
+ * @param bRadio True to get the radio channels, false to get the TV channels.
+ * @param channels The container for the channels.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetChannels(bool bRadio, std::vector<std::shared_ptr<CPVRChannel>>& channels);
+
+ /*!
+ * @brief Get the total amount of providers from the backend.
+ * @param iChannels The total amount of channels on the server or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetProvidersAmount(int& iProviders);
+
+ /*!
+ * @brief Request the list of all providers from the backend.
+ * @param providers The providers list to add the providers to.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetProviders(CPVRProvidersContainer& providers);
+
+ //@}
+ /** @name PVR recording methods */
+ //@{
+
+ /*!
+ * @brief Get the total amount of recordings from the backend.
+ * @param deleted True to return deleted recordings.
+ * @param iRecordings The total amount of recordings on the server or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetRecordingsAmount(bool deleted, int& iRecordings);
+
+ /*!
+ * @brief Request the list of all recordings from the backend.
+ * @param results The container to add the recordings to.
+ * @param deleted True to return deleted recordings.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetRecordings(CPVRRecordings* results, bool deleted);
+
+ /*!
+ * @brief Delete a recording on the backend.
+ * @param recording The recording to delete.
+ * @return PVR_ERROR_NO_ERROR if the recording has been deleted successfully.
+ */
+ PVR_ERROR DeleteRecording(const CPVRRecording& recording);
+
+ /*!
+ * @brief Undelete a recording on the backend.
+ * @param recording The recording to undelete.
+ * @return PVR_ERROR_NO_ERROR if the recording has been undeleted successfully.
+ */
+ PVR_ERROR UndeleteRecording(const CPVRRecording& recording);
+
+ /*!
+ * @brief Delete all recordings permanent which in the deleted folder on the backend.
+ * @return PVR_ERROR_NO_ERROR if the recordings has been deleted successfully.
+ */
+ PVR_ERROR DeleteAllRecordingsFromTrash();
+
+ /*!
+ * @brief Rename a recording on the backend.
+ * @param recording The recording to rename.
+ * @return PVR_ERROR_NO_ERROR if the recording has been renamed successfully.
+ */
+ PVR_ERROR RenameRecording(const CPVRRecording& recording);
+
+ /*!
+ * @brief Set the lifetime of a recording on the backend.
+ * @param recording The recording to set the lifetime for. recording.m_iLifetime contains the new lifetime value.
+ * @return PVR_ERROR_NO_ERROR if the recording's lifetime has been set successfully.
+ */
+ PVR_ERROR SetRecordingLifetime(const CPVRRecording& recording);
+
+ /*!
+ * @brief Set the play count of a recording on the backend.
+ * @param recording The recording to set the play count.
+ * @param count Play count.
+ * @return PVR_ERROR_NO_ERROR if the recording's play count has been set successfully.
+ */
+ PVR_ERROR SetRecordingPlayCount(const CPVRRecording& recording, int count);
+
+ /*!
+ * @brief Set the last watched position of a recording on the backend.
+ * @param recording The recording.
+ * @param lastplayedposition The last watched position in seconds
+ * @return PVR_ERROR_NO_ERROR if the position has been stored successfully.
+ */
+ PVR_ERROR SetRecordingLastPlayedPosition(const CPVRRecording& recording, int lastplayedposition);
+
+ /*!
+ * @brief Retrieve the last watched position of a recording on the backend.
+ * @param recording The recording.
+ * @param iPosition The last watched position in seconds or -1 on error
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetRecordingLastPlayedPosition(const CPVRRecording& recording, int& iPosition);
+
+ /*!
+ * @brief Retrieve the edit decision list (EDL) from the backend.
+ * @param recording The recording.
+ * @param edls The edit decision list (empty on error).
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetRecordingEdl(const CPVRRecording& recording, std::vector<PVR_EDL_ENTRY>& edls);
+
+ /*!
+ * @brief Retrieve the size of a recording on the backend.
+ * @param recording The recording.
+ * @param sizeInBytes The size in bytes
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetRecordingSize(const CPVRRecording& recording, int64_t& sizeInBytes);
+
+ /*!
+ * @brief Retrieve the edit decision list (EDL) from the backend.
+ * @param epgTag The EPG tag.
+ * @param edls The edit decision list (empty on error).
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetEpgTagEdl(const std::shared_ptr<const CPVREpgInfoTag>& epgTag,
+ std::vector<PVR_EDL_ENTRY>& edls);
+
+ //@}
+ /** @name PVR timer methods */
+ //@{
+
+ /*!
+ * @brief Get the total amount of timers from the backend.
+ * @param iTimers The total amount of timers on the backend or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetTimersAmount(int& iTimers);
+
+ /*!
+ * @brief Request the list of all timers from the backend.
+ * @param results The container to store the result in.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetTimers(CPVRTimersContainer* results);
+
+ /*!
+ * @brief Add a timer on the backend.
+ * @param timer The timer to add.
+ * @return PVR_ERROR_NO_ERROR if the timer has been added successfully.
+ */
+ PVR_ERROR AddTimer(const CPVRTimerInfoTag& timer);
+
+ /*!
+ * @brief Delete a timer on the backend.
+ * @param timer The timer to delete.
+ * @param bForce Set to true to delete a timer that is currently recording a program.
+ * @return PVR_ERROR_NO_ERROR if the timer has been deleted successfully.
+ */
+ PVR_ERROR DeleteTimer(const CPVRTimerInfoTag& timer, bool bForce = false);
+
+ /*!
+ * @brief Update the timer information on the server.
+ * @param timer The timer to update.
+ * @return PVR_ERROR_NO_ERROR if the timer has been updated successfully.
+ */
+ PVR_ERROR UpdateTimer(const CPVRTimerInfoTag& timer);
+
+ /*!
+ * @brief Get all timer types supported by the backend.
+ * @param results The container to store the result in.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetTimerTypes(std::vector<std::shared_ptr<CPVRTimerType>>& results) const;
+
+ //@}
+ /** @name PVR live stream methods */
+ //@{
+
+ /*!
+ * @brief Open a live stream on the server.
+ * @param channel The channel to stream.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR OpenLiveStream(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Close an open live stream.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CloseLiveStream();
+
+ /*!
+ * @brief Read from an open live stream.
+ * @param lpBuf The buffer to store the data in.
+ * @param uiBufSize The amount of bytes to read.
+ * @param iRead The amount of bytes that were actually read from the stream.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR ReadLiveStream(void* lpBuf, int64_t uiBufSize, int& iRead);
+
+ /*!
+ * @brief Seek in a live stream on a backend.
+ * @param iFilePosition The position to seek to.
+ * @param iWhence ?
+ * @param iPosition The new position or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR SeekLiveStream(int64_t iFilePosition, int iWhence, int64_t& iPosition);
+
+ /*!
+ * @brief Get the length of the currently playing live stream, if any.
+ * @param iLength The total length of the stream that's currently being read or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetLiveStreamLength(int64_t& iLength);
+
+ /*!
+ * @brief (Un)Pause a stream.
+ * @param bPaused True to pause the stream, false to unpause.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR PauseStream(bool bPaused);
+
+ /*!
+ * @brief Get the signal quality of the stream that's currently open.
+ * @param channelUid Channel unique identifier
+ * @param qualityinfo The signal quality.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR SignalQuality(int channelUid, PVR_SIGNAL_STATUS& qualityinfo);
+
+ /*!
+ * @brief Get the descramble information of the stream that's currently open.
+ * @param channelUid Channel unique identifier
+ * @param descrambleinfo The descramble information.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetDescrambleInfo(int channelUid, PVR_DESCRAMBLE_INFO& descrambleinfo) const;
+
+ /*!
+ * @brief Fill the given container with the properties required for playback of the given channel. Values are obtained from the PVR backend.
+ * @param channel The channel.
+ * @param props The container to be filled with the stream properties.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetChannelStreamProperties(const std::shared_ptr<CPVRChannel>& channel,
+ CPVRStreamProperties& props);
+
+ /*!
+ * @brief Check whether PVR backend supports pausing the currently playing stream
+ * @param bCanPause True if the stream can be paused, false otherwise.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CanPauseStream(bool& bCanPause) const;
+
+ /*!
+ * @brief Check whether PVR backend supports seeking for the currently playing stream
+ * @param bCanSeek True if the stream can be seeked, false otherwise.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CanSeekStream(bool& bCanSeek) const;
+
+ /*!
+ * @brief Notify the pvr addon/demuxer that Kodi wishes to seek the stream by time
+ * @param time The absolute time since stream start
+ * @param backwards True to seek to keyframe BEFORE time, else AFTER
+ * @param startpts can be updated to point to where display should start
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ * @remarks Optional, and only used if addon has its own demuxer.
+ */
+ PVR_ERROR SeekTime(double time, bool backwards, double* startpts);
+
+ /*!
+ * @brief Notify the pvr addon/demuxer that Kodi wishes to change playback speed
+ * @param speed The requested playback speed
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ * @remarks Optional, and only used if addon has its own demuxer.
+ */
+ PVR_ERROR SetSpeed(int speed);
+
+ /*!
+ * @brief Notify the pvr addon/demuxer that Kodi wishes to fill demux queue
+ * @param mode for setting on/off
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ * @remarks Optional, and only used if addon has its own demuxer.
+ */
+ PVR_ERROR FillBuffer(bool mode);
+
+ //@}
+ /** @name PVR recording stream methods */
+ //@{
+
+ /*!
+ * @brief Open a recording on the server.
+ * @param recording The recording to open.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR OpenRecordedStream(const std::shared_ptr<CPVRRecording>& recording);
+
+ /*!
+ * @brief Close an open recording stream.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CloseRecordedStream();
+
+ /*!
+ * @brief Read from an open recording stream.
+ * @param lpBuf The buffer to store the data in.
+ * @param uiBufSize The amount of bytes to read.
+ * @param iRead The amount of bytes that were actually read from the stream.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR ReadRecordedStream(void* lpBuf, int64_t uiBufSize, int& iRead);
+
+ /*!
+ * @brief Seek in a recording stream on a backend.
+ * @param iFilePosition The position to seek to.
+ * @param iWhence ?
+ * @param iPosition The new position or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR SeekRecordedStream(int64_t iFilePosition, int iWhence, int64_t& iPosition);
+
+ /*!
+ * @brief Get the length of the currently playing recording stream, if any.
+ * @param iLength The total length of the stream that's currently being read or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetRecordedStreamLength(int64_t& iLength);
+
+ /*!
+ * @brief Fill the given container with the properties required for playback of the given recording. Values are obtained from the PVR backend.
+ * @param recording The recording.
+ * @param props The container to be filled with the stream properties.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetRecordingStreamProperties(const std::shared_ptr<CPVRRecording>& recording,
+ CPVRStreamProperties& props);
+
+ //@}
+ /** @name PVR demultiplexer methods */
+ //@{
+
+ /*!
+ * @brief Reset the demultiplexer in the add-on.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR DemuxReset();
+
+ /*!
+ * @brief Abort the demultiplexer thread in the add-on.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR DemuxAbort();
+
+ /*!
+ * @brief Flush all data that's currently in the demultiplexer buffer in the add-on.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR DemuxFlush();
+
+ /*!
+ * @brief Read a packet from the demultiplexer.
+ * @param packet The packet read.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR DemuxRead(DemuxPacket*& packet);
+
+ static const char* ToString(const PVR_ERROR error);
+
+ /*!
+ * @brief Check whether the currently playing stream, if any, is a real-time stream.
+ * @param bRealTime True if real-time, false otherwise.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR IsRealTimeStream(bool& bRealTime) const;
+
+ /*!
+ * @brief Get Stream times for the currently playing stream, if any (will be moved to inputstream).
+ * @param times The stream times.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetStreamTimes(PVR_STREAM_TIMES* times);
+
+ /*!
+ * @brief Get the client's menu hooks.
+ * @return The hooks. Guaranteed never to be nullptr.
+ */
+ std::shared_ptr<CPVRClientMenuHooks> GetMenuHooks();
+
+ /*!
+ * @brief Call one of the EPG tag menu hooks of the client.
+ * @param hook The hook to call.
+ * @param tag The EPG tag associated with the hook to be called.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CallEpgTagMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVREpgInfoTag>& tag);
+
+ /*!
+ * @brief Call one of the channel menu hooks of the client.
+ * @param hook The hook to call.
+ * @param tag The channel associated with the hook to be called.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CallChannelMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Call one of the recording menu hooks of the client.
+ * @param hook The hook to call.
+ * @param tag The recording associated with the hook to be called.
+ * @param bDeleted True, if the recording is deleted (trashed), false otherwise
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CallRecordingMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVRRecording>& recording,
+ bool bDeleted);
+
+ /*!
+ * @brief Call one of the timer menu hooks of the client.
+ * @param hook The hook to call.
+ * @param tag The timer associated with the hook to be called.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CallTimerMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVRTimerInfoTag>& timer);
+
+ /*!
+ * @brief Call one of the settings menu hooks of the client.
+ * @param hook The hook to call.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CallSettingsMenuHook(const CPVRClientMenuHook& hook);
+
+ /*!
+ * @brief Propagate power management events to this add-on
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR OnSystemSleep();
+ PVR_ERROR OnSystemWake();
+ PVR_ERROR OnPowerSavingActivated();
+ PVR_ERROR OnPowerSavingDeactivated();
+
+ /*!
+ * @brief Get the priority of this client. Larger value means higher priority.
+ * @return The priority.
+ */
+ int GetPriority() const;
+
+ /*!
+ * @brief Set a new priority for this client.
+ * @param iPriority The new priority.
+ */
+ void SetPriority(int iPriority);
+
+ /*!
+ * @brief Obtain the chunk size to use when reading streams.
+ * @param iChunkSize the chunk size in bytes.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetStreamReadChunkSize(int& iChunkSize);
+
+ /*!
+ * @brief Get the interface table used between addon and Kodi.
+ * @todo This function will be removed after old callback library system is removed.
+ */
+ AddonInstance_PVR* GetInstanceInterface() { return m_ifc.pvr; }
+
+private:
+ /*!
+ * @brief Resets all class members to their defaults, accept the client id.
+ */
+ void ResetProperties();
+
+ /*!
+ * @brief reads the client's properties.
+ * @return True on success, false otherwise.
+ */
+ bool GetAddonProperties();
+
+ /*!
+ * @brief reads the client's name string properties
+ * @return True on success, false otherwise.
+ */
+ bool GetAddonNameStringProperties();
+
+ /*!
+ * @brief Write the given addon properties to the given properties container.
+ * @param properties Pointer to an array of addon properties.
+ * @param iPropertyCount The number of properties contained in the addon properties array.
+ * @param props The container the addon properties shall be written to.
+ */
+ static void WriteStreamProperties(const PVR_NAMED_VALUE* properties,
+ unsigned int iPropertyCount,
+ CPVRStreamProperties& props);
+
+ /*!
+ * @brief Whether a channel can be played by this add-on
+ * @param channel The channel to check.
+ * @return True when it can be played, false otherwise.
+ */
+ bool CanPlayChannel(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Stop this instance, if it is currently running.
+ */
+ void StopRunningInstance();
+
+ /*!
+ * @brief Wraps an addon function call in order to do common pre and post function invocation actions.
+ * @param strFunctionName The function name, for logging purposes.
+ * @param function The function to wrap. It has to have return type PVR_ERROR and must take one parameter of type const AddonInstance*.
+ * @param bIsImplemented If false, this method will return PVR_ERROR_NOT_IMPLEMENTED.
+ * @param bCheckReadyToUse If true, this method will check whether this instance is ready for use and return PVR_ERROR_SERVER_ERROR if it is not.
+ * @return PVR_ERROR_NO_ERROR on success, any other PVR_ERROR_* value otherwise.
+ */
+ typedef AddonInstance_PVR AddonInstance;
+ PVR_ERROR DoAddonCall(const char* strFunctionName,
+ const std::function<PVR_ERROR(const AddonInstance*)>& function,
+ bool bIsImplemented = true,
+ bool bCheckReadyToUse = true) const;
+
+ /*!
+ * @brief Wraps an addon callback function call in order to do common pre and post function invocation actions.
+ * @param strFunctionName The function name, for logging purposes.
+ * @param kodiInstance The addon instance pointer.
+ * @param function The function to wrap. It must take one parameter of type CPVRClient*.
+ * @param bForceCall If true, make the call, ignoring client's state.
+ */
+ static void HandleAddonCallback(const char* strFunctionName,
+ void* kodiInstance,
+ const std::function<void(CPVRClient* client)>& function,
+ bool bForceCall = false);
+
+ /*!
+ * @brief Callback functions from addon to kodi
+ */
+ //@{
+
+ /*!
+ * @brief Transfer a channel group from the add-on to Kodi. The group will be created if it doesn't exist.
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the channel groups list
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_channel_group(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_CHANNEL_GROUP* entry);
+
+ /*!
+ * @brief Transfer a channel group member entry from the add-on to Kodi. The channel will be added to the group if the group can be found.
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the channel group members list
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_channel_group_member(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_CHANNEL_GROUP_MEMBER* entry);
+
+ /*!
+ * @brief Transfer an EPG tag from the add-on to Kodi
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the EPG data
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_epg_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const EPG_TAG* entry);
+
+ /*!
+ * @brief Transfer a channel entry from the add-on to Kodi
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the channel list
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_channel_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_CHANNEL* entry);
+
+ /*!
+ * @brief Transfer a provider entry from the add-on to Kodi
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the channel list
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_provider_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_PROVIDER* entry);
+
+ /*!
+ * @brief Transfer a timer entry from the add-on to Kodi
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the timers list
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_timer_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_TIMER* entry);
+
+ /*!
+ * @brief Transfer a recording entry from the add-on to Kodi
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the recordings list
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_recording_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_RECORDING* entry);
+
+ /*!
+ * @brief Add or replace a menu hook for the context menu for this add-on
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param hook The hook to add.
+ */
+ static void cb_add_menu_hook(void* kodiInstance, const PVR_MENUHOOK* hook);
+
+ /*!
+ * @brief Display a notification in Kodi that a recording started or stopped on the server
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param strName The name of the recording to display
+ * @param strFileName The filename of the recording
+ * @param bOnOff True when recording started, false when it stopped
+ */
+ static void cb_recording_notification(void* kodiInstance,
+ const char* strName,
+ const char* strFileName,
+ bool bOnOff);
+
+ /*!
+ * @brief Request Kodi to update it's list of channels
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ */
+ static void cb_trigger_channel_update(void* kodiInstance);
+
+ /*!
+ * @brief Request Kodi to update it's list of providers
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ */
+ static void cb_trigger_provider_update(void* kodiInstance);
+
+ /*!
+ * @brief Request Kodi to update it's list of timers
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ */
+ static void cb_trigger_timer_update(void* kodiInstance);
+
+ /*!
+ * @brief Request Kodi to update it's list of recordings
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ */
+ static void cb_trigger_recording_update(void* kodiInstance);
+
+ /*!
+ * @brief Request Kodi to update it's list of channel groups
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ */
+ static void cb_trigger_channel_groups_update(void* kodiInstance);
+
+ /*!
+ * @brief Schedule an EPG update for the given channel channel
+ * @param kodiInstance A pointer to the add-on
+ * @param iChannelUid The unique id of the channel for this add-on
+ */
+ static void cb_trigger_epg_update(void* kodiInstance, unsigned int iChannelUid);
+
+ /*!
+ * @brief Free a packet that was allocated with AllocateDemuxPacket
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param pPacket The packet to free.
+ */
+ static void cb_free_demux_packet(void* kodiInstance, DEMUX_PACKET* pPacket);
+
+ /*!
+ * @brief Allocate a demux packet. Free with FreeDemuxPacket
+ * @param kodiInstance Pointer to Kodi's CPVRClient class.
+ * @param iDataSize The size of the data that will go into the packet
+ * @return The allocated packet.
+ */
+ static DEMUX_PACKET* cb_allocate_demux_packet(void* kodiInstance, int iDataSize = 0);
+
+ /*!
+ * @brief Notify a state change for a PVR backend connection
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param strConnectionString The connection string reported by the backend that can be displayed in the UI.
+ * @param newState The new state.
+ * @param strMessage A localized addon-defined string representing the new state, that can be displayed
+ * in the UI or NULL if the Kodi-defined default string for the new state shall be displayed.
+ */
+ static void cb_connection_state_change(void* kodiInstance,
+ const char* strConnectionString,
+ PVR_CONNECTION_STATE newState,
+ const char* strMessage);
+
+ /*!
+ * @brief Notify a state change for an EPG event
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param tag The EPG event.
+ * @param newState The new state.
+ * @param newState The new state. For EPG_EVENT_CREATED and EPG_EVENT_UPDATED, tag must be filled with all available
+ * event data, not just a delta. For EPG_EVENT_DELETED, it is sufficient to fill EPG_TAG.iUniqueBroadcastId
+ */
+ static void cb_epg_event_state_change(void* kodiInstance, EPG_TAG* tag, EPG_EVENT_STATE newState);
+
+ /*! @todo remove the use complete from them, or add as generl function?!
+ * Returns the ffmpeg codec id from given ffmpeg codec string name
+ */
+ static PVR_CODEC cb_get_codec_by_name(const void* kodiInstance, const char* strCodecName);
+ //@}
+
+ const int m_iClientId; /*!< unique ID of the client */
+ std::atomic<bool>
+ m_bReadyToUse; /*!< true if this add-on is initialised (ADDON_Create returned true), false otherwise */
+ std::atomic<bool> m_bBlockAddonCalls; /*!< true if no add-on API calls are allowed */
+ mutable std::atomic<int> m_iAddonCalls; /*!< number of in-progress addon calls */
+ mutable CEvent m_allAddonCallsFinished; /*!< fires after last in-progress addon call finished */
+ PVR_CONNECTION_STATE m_connectionState; /*!< the backend connection state */
+ PVR_CONNECTION_STATE m_prevConnectionState; /*!< the previous backend connection state */
+ bool
+ m_ignoreClient; /*!< signals to PVRManager to ignore this client until it has been connected */
+ std::vector<std::shared_ptr<CPVRTimerType>>
+ m_timertypes; /*!< timer types supported by this backend */
+ mutable int m_iPriority; /*!< priority of the client */
+ mutable bool m_bPriorityFetched;
+
+ /* cached data */
+ std::string m_strBackendName; /*!< the cached backend version */
+ std::string m_strBackendVersion; /*!< the cached backend version */
+ std::string m_strConnectionString; /*!< the cached connection string */
+ std::string m_strBackendHostname; /*!< the cached backend hostname */
+ CPVRClientCapabilities m_clientCapabilities; /*!< the cached add-on's capabilities */
+ std::shared_ptr<CPVRClientMenuHooks> m_menuhooks; /*!< the menu hooks for this add-on */
+
+ /* stored strings to make sure const char* members in AddonProperties_PVR stay valid */
+ std::string m_strUserPath; /*!< @brief translated path to the user profile */
+ std::string m_strClientPath; /*!< @brief translated path to this add-on */
+
+ mutable CCriticalSection m_critSection;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/addons/PVRClientCapabilities.cpp b/xbmc/pvr/addons/PVRClientCapabilities.cpp
new file mode 100644
index 0000000..8650465
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClientCapabilities.cpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRClientCapabilities.h"
+
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+#include <iterator>
+
+using namespace PVR;
+
+CPVRClientCapabilities::CPVRClientCapabilities(const CPVRClientCapabilities& other)
+{
+ if (other.m_addonCapabilities)
+ m_addonCapabilities.reset(new PVR_ADDON_CAPABILITIES(*other.m_addonCapabilities));
+ InitRecordingsLifetimeValues();
+}
+
+const CPVRClientCapabilities& CPVRClientCapabilities::operator=(const CPVRClientCapabilities& other)
+{
+ if (other.m_addonCapabilities)
+ m_addonCapabilities.reset(new PVR_ADDON_CAPABILITIES(*other.m_addonCapabilities));
+ InitRecordingsLifetimeValues();
+ return *this;
+}
+
+const CPVRClientCapabilities& CPVRClientCapabilities::operator=(
+ const PVR_ADDON_CAPABILITIES& addonCapabilities)
+{
+ m_addonCapabilities.reset(new PVR_ADDON_CAPABILITIES(addonCapabilities));
+ InitRecordingsLifetimeValues();
+ return *this;
+}
+
+void CPVRClientCapabilities::clear()
+{
+ m_recordingsLifetimeValues.clear();
+ m_addonCapabilities.reset();
+}
+
+void CPVRClientCapabilities::InitRecordingsLifetimeValues()
+{
+ m_recordingsLifetimeValues.clear();
+ if (m_addonCapabilities && m_addonCapabilities->iRecordingsLifetimesSize > 0)
+ {
+ for (unsigned int i = 0; i < m_addonCapabilities->iRecordingsLifetimesSize; ++i)
+ {
+ int iValue = m_addonCapabilities->recordingsLifetimeValues[i].iValue;
+ std::string strDescr(m_addonCapabilities->recordingsLifetimeValues[i].strDescription);
+ if (strDescr.empty())
+ {
+ // No description given by addon. Create one from value.
+ strDescr = std::to_string(iValue);
+ }
+ m_recordingsLifetimeValues.emplace_back(strDescr, iValue);
+ }
+ }
+ else if (SupportsRecordingsLifetimeChange())
+ {
+ // No values given by addon, but lifetime supported. Use default values 1..365
+ for (int i = 1; i < 366; ++i)
+ {
+ m_recordingsLifetimeValues.emplace_back(StringUtils::Format(g_localizeStrings.Get(17999), i),
+ i); // "{} days"
+ }
+ }
+ else
+ {
+ // No lifetime supported.
+ }
+}
+
+void CPVRClientCapabilities::GetRecordingsLifetimeValues(
+ std::vector<std::pair<std::string, int>>& list) const
+{
+ std::copy(m_recordingsLifetimeValues.cbegin(), m_recordingsLifetimeValues.cend(),
+ std::back_inserter(list));
+}
diff --git a/xbmc/pvr/addons/PVRClientCapabilities.h b/xbmc/pvr/addons/PVRClientCapabilities.h
new file mode 100644
index 0000000..edf2090
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClientCapabilities.h
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace PVR
+{
+
+class CPVRClientCapabilities
+{
+public:
+ CPVRClientCapabilities() = default;
+ virtual ~CPVRClientCapabilities() = default;
+
+ CPVRClientCapabilities(const CPVRClientCapabilities& other);
+ const CPVRClientCapabilities& operator=(const CPVRClientCapabilities& other);
+
+ const CPVRClientCapabilities& operator=(const PVR_ADDON_CAPABILITIES& addonCapabilities);
+
+ void clear();
+
+ /////////////////////////////////////////////////////////////////////////////////
+ //
+ // Channels
+ //
+ /////////////////////////////////////////////////////////////////////////////////
+
+ /*!
+ * @brief Check whether this add-on supports TV channels.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsTV() const { return m_addonCapabilities && m_addonCapabilities->bSupportsTV; }
+
+ /*!
+ * @brief Check whether this add-on supports radio channels.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRadio() const { return m_addonCapabilities && m_addonCapabilities->bSupportsRadio; }
+
+ /*!
+ * @brief Check whether this add-on supports providers.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsProviders() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsProviders;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports channel groups.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsChannelGroups() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsChannelGroups;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports scanning for new channels on the backend.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsChannelScan() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsChannelScan;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports the following functions:
+ * DeleteChannel, RenameChannel, DialogChannelSettings and DialogAddChannel.
+ *
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsChannelSettings() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsChannelSettings;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports descramble information for playing channels.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsDescrambleInfo() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsDescrambleInfo;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////
+ //
+ // EPG
+ //
+ /////////////////////////////////////////////////////////////////////////////////
+
+ /*!
+ * @brief Check whether this add-on provides EPG information.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsEPG() const { return m_addonCapabilities && m_addonCapabilities->bSupportsEPG; }
+
+ /*!
+ * @brief Check whether this add-on supports asynchronous transfer of epg events.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsAsyncEPGTransfer() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsAsyncEPGTransfer;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////
+ //
+ // Timers
+ //
+ /////////////////////////////////////////////////////////////////////////////////
+
+ /*!
+ * @brief Check whether this add-on supports the creation and editing of timers.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsTimers() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsTimers;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////
+ //
+ // Recordings
+ //
+ /////////////////////////////////////////////////////////////////////////////////
+
+ /*!
+ * @brief Check whether this add-on supports recordings.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordings() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports undelete of deleted recordings.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsUndelete() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingsUndelete;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports play count for recordings.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsPlayCount() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingPlayCount;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports store/retrieve of last played position for recordings..
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsLastPlayedPosition() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsLastPlayedPosition;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports retrieving an edit decision list for recordings.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsEdl() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingEdl;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports retrieving an edit decision list for epg tags.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsEpgTagEdl() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsEPG &&
+ m_addonCapabilities->bSupportsEPGEdl;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports renaming recordings..
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsRename() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingsRename;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports changing lifetime of recording.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsLifetimeChange() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingsLifetimeChange;
+ }
+
+ /*!
+ * @brief Obtain a list with all possible values for recordings lifetime.
+ * @param list out, the list with the values or an empty list, if lifetime is not supported.
+ */
+ void GetRecordingsLifetimeValues(std::vector<std::pair<std::string, int>>& list) const;
+
+ /*!
+ * @brief Check whether this add-on supports retrieving the size recordings..
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsSize() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingSize;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports deleting recordings.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsDelete() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingsDelete;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////
+ //
+ // Streams
+ //
+ /////////////////////////////////////////////////////////////////////////////////
+
+ /*!
+ * @brief Check whether this add-on provides an input stream. false if Kodi handles the stream.
+ * @return True if supported, false otherwise.
+ */
+ bool HandlesInputStream() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bHandlesInputStream;
+ }
+
+ /*!
+ * @brief Check whether this add-on demultiplexes packets.
+ * @return True if supported, false otherwise.
+ */
+ bool HandlesDemuxing() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bHandlesDemuxing;
+ }
+
+private:
+ void InitRecordingsLifetimeValues();
+
+ std::unique_ptr<PVR_ADDON_CAPABILITIES> m_addonCapabilities;
+ std::vector<std::pair<std::string, int>> m_recordingsLifetimeValues;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/addons/PVRClientMenuHooks.cpp b/xbmc/pvr/addons/PVRClientMenuHooks.cpp
new file mode 100644
index 0000000..12f150a
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClientMenuHooks.cpp
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRClientMenuHooks.h"
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_menu_hook.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRContextMenus.h"
+#include "utils/log.h"
+
+namespace PVR
+{
+
+CPVRClientMenuHook::CPVRClientMenuHook(const std::string& addonId, const PVR_MENUHOOK& hook)
+: m_addonId(addonId),
+ m_hook(new PVR_MENUHOOK(hook))
+{
+ if (hook.category != PVR_MENUHOOK_UNKNOWN &&
+ hook.category != PVR_MENUHOOK_ALL &&
+ hook.category != PVR_MENUHOOK_CHANNEL &&
+ hook.category != PVR_MENUHOOK_TIMER &&
+ hook.category != PVR_MENUHOOK_EPG &&
+ hook.category != PVR_MENUHOOK_RECORDING &&
+ hook.category != PVR_MENUHOOK_DELETED_RECORDING &&
+ hook.category != PVR_MENUHOOK_SETTING)
+ CLog::LogF(LOGERROR, "Unknown PVR_MENUHOOK_CAT value: {}", hook.category);
+}
+
+bool CPVRClientMenuHook::operator ==(const CPVRClientMenuHook& right) const
+{
+ if (this == &right)
+ return true;
+
+ return m_addonId == right.m_addonId &&
+ m_hook->iHookId == right.m_hook->iHookId &&
+ m_hook->iLocalizedStringId == right.m_hook->iLocalizedStringId &&
+ m_hook->category == right.m_hook->category;
+}
+
+bool CPVRClientMenuHook::IsAllHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_ALL;
+}
+
+bool CPVRClientMenuHook::IsChannelHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_CHANNEL;
+}
+
+bool CPVRClientMenuHook::IsTimerHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_TIMER;
+}
+
+bool CPVRClientMenuHook::IsEpgHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_EPG;
+}
+
+bool CPVRClientMenuHook::IsRecordingHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_RECORDING;
+}
+
+bool CPVRClientMenuHook::IsDeletedRecordingHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_DELETED_RECORDING;
+}
+
+bool CPVRClientMenuHook::IsSettingsHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_SETTING;
+}
+
+std::string CPVRClientMenuHook::GetAddonId() const
+{
+ return m_addonId;
+}
+
+unsigned int CPVRClientMenuHook::GetId() const
+{
+ return m_hook->iHookId;
+}
+
+unsigned int CPVRClientMenuHook::GetLabelId() const
+{
+ return m_hook->iLocalizedStringId;
+}
+
+std::string CPVRClientMenuHook::GetLabel() const
+{
+ return g_localizeStrings.GetAddonString(m_addonId, m_hook->iLocalizedStringId);
+}
+
+void CPVRClientMenuHooks::AddHook(const PVR_MENUHOOK& addonHook)
+{
+ if (!m_hooks)
+ m_hooks.reset(new std::vector<CPVRClientMenuHook>());
+
+ const CPVRClientMenuHook hook(m_addonId, addonHook);
+ m_hooks->emplace_back(hook);
+ CPVRContextMenuManager::GetInstance().AddMenuHook(hook);
+}
+
+void CPVRClientMenuHooks::Clear()
+{
+ if (!m_hooks)
+ return;
+
+ for (const auto& hook : *m_hooks)
+ CPVRContextMenuManager::GetInstance().RemoveMenuHook(hook);
+
+ m_hooks.reset();
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetHooks(
+ const std::function<bool(const CPVRClientMenuHook& hook)>& function) const
+{
+ std::vector<CPVRClientMenuHook> hooks;
+
+ if (!m_hooks)
+ return hooks;
+
+ for (const CPVRClientMenuHook& hook : *m_hooks)
+ {
+ if (function(hook) || hook.IsAllHook())
+ hooks.emplace_back(hook);
+ }
+ return hooks;
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetChannelHooks() const
+{
+ return GetHooks([](const CPVRClientMenuHook& hook)
+ {
+ return hook.IsChannelHook();
+ });
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetTimerHooks() const
+{
+ return GetHooks([](const CPVRClientMenuHook& hook)
+ {
+ return hook.IsTimerHook();
+ });
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetEpgHooks() const
+{
+ return GetHooks([](const CPVRClientMenuHook& hook)
+ {
+ return hook.IsEpgHook();
+ });
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetRecordingHooks() const
+{
+ return GetHooks([](const CPVRClientMenuHook& hook)
+ {
+ return hook.IsRecordingHook();
+ });
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetDeletedRecordingHooks() const
+{
+ return GetHooks([](const CPVRClientMenuHook& hook)
+ {
+ return hook.IsDeletedRecordingHook();
+ });
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetSettingsHooks() const
+{
+ return GetHooks([](const CPVRClientMenuHook& hook)
+ {
+ return hook.IsSettingsHook();
+ });
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/addons/PVRClientMenuHooks.h b/xbmc/pvr/addons/PVRClientMenuHooks.h
new file mode 100644
index 0000000..69d7f79
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClientMenuHooks.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <vector>
+
+struct PVR_MENUHOOK;
+
+namespace PVR
+{
+ class CPVRClientMenuHook
+ {
+ public:
+ CPVRClientMenuHook() = delete;
+ virtual ~CPVRClientMenuHook() = default;
+
+ CPVRClientMenuHook(const std::string& addonId, const PVR_MENUHOOK& hook);
+
+ bool operator ==(const CPVRClientMenuHook& right) const;
+
+ bool IsAllHook() const;
+ bool IsChannelHook() const;
+ bool IsTimerHook() const;
+ bool IsEpgHook() const;
+ bool IsRecordingHook() const;
+ bool IsDeletedRecordingHook() const;
+ bool IsSettingsHook() const;
+
+ std::string GetAddonId() const;
+ unsigned int GetId() const;
+ unsigned int GetLabelId() const;
+ std::string GetLabel() const;
+
+ private:
+ std::string m_addonId;
+ std::shared_ptr<PVR_MENUHOOK> m_hook;
+ };
+
+ class CPVRClientMenuHooks
+ {
+ public:
+ CPVRClientMenuHooks() = default;
+ virtual ~CPVRClientMenuHooks() = default;
+
+ explicit CPVRClientMenuHooks(const std::string& addonId) : m_addonId(addonId) {}
+
+ void AddHook(const PVR_MENUHOOK& addonHook);
+ void Clear();
+
+ std::vector<CPVRClientMenuHook> GetChannelHooks() const;
+ std::vector<CPVRClientMenuHook> GetTimerHooks() const;
+ std::vector<CPVRClientMenuHook> GetEpgHooks() const;
+ std::vector<CPVRClientMenuHook> GetRecordingHooks() const;
+ std::vector<CPVRClientMenuHook> GetDeletedRecordingHooks() const;
+ std::vector<CPVRClientMenuHook> GetSettingsHooks() const;
+
+ private:
+ std::vector<CPVRClientMenuHook> GetHooks(
+ const std::function<bool(const CPVRClientMenuHook& hook)>& function) const;
+
+ std::string m_addonId;
+ std::unique_ptr<std::vector<CPVRClientMenuHook>> m_hooks;
+ };
+}
diff --git a/xbmc/pvr/addons/PVRClientUID.cpp b/xbmc/pvr/addons/PVRClientUID.cpp
new file mode 100644
index 0000000..8788385
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClientUID.cpp
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2012-2022 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRClientUID.h"
+
+#include <functional>
+
+using namespace PVR;
+
+int CPVRClientUID::GetUID() const
+{
+ if (!m_uidCreated)
+ {
+ std::hash<std::string> hasher;
+
+ // Note: For database backwards compatibility reasons the hash of the first instance
+ // must be calculated just from the addonId, not from addonId and instanceId.
+ m_uid = static_cast<int>(hasher(
+ (m_instanceID > ADDON::ADDON_FIRST_INSTANCE_ID ? std::to_string(m_instanceID) + "@" : "") +
+ m_addonID));
+ if (m_uid < 0)
+ m_uid = -m_uid;
+
+ m_uidCreated = true;
+ }
+
+ return m_uid;
+}
diff --git a/xbmc/pvr/addons/PVRClientUID.h b/xbmc/pvr/addons/PVRClientUID.h
new file mode 100644
index 0000000..5b5d1c0
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClientUID.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012-2022 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/IAddon.h"
+
+#include <string>
+
+namespace PVR
+{
+class CPVRClientUID final
+{
+public:
+ CPVRClientUID(const std::string& addonID, ADDON::AddonInstanceId instanceID)
+ : m_addonID(addonID), m_instanceID(instanceID)
+ {
+ }
+
+ virtual ~CPVRClientUID() = default;
+
+ /*!
+ * @brief Return the numeric UID.
+ * @return The numeric UID.
+ */
+ int GetUID() const;
+
+private:
+ CPVRClientUID() = delete;
+
+ std::string m_addonID;
+ ADDON::AddonInstanceId m_instanceID{ADDON::ADDON_SINGLETON_INSTANCE_ID};
+
+ mutable bool m_uidCreated{false};
+ mutable int m_uid{0};
+};
+} // namespace PVR
diff --git a/xbmc/pvr/addons/PVRClients.cpp b/xbmc/pvr/addons/PVRClients.cpp
new file mode 100644
index 0000000..8cda035
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClients.cpp
@@ -0,0 +1,977 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRClients.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonEvents.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVREventLogJob.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClientUID.h"
+#include "pvr/channels/PVRChannelGroupInternal.h"
+#include "pvr/guilib/PVRGUIProgressHandler.h"
+#include "utils/JobManager.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace ADDON;
+using namespace PVR;
+
+CPVRClients::CPVRClients()
+{
+ CServiceBroker::GetAddonMgr().RegisterAddonMgrCallback(AddonType::PVRDLL, this);
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CPVRClients::OnAddonEvent);
+}
+
+CPVRClients::~CPVRClients()
+{
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+ CServiceBroker::GetAddonMgr().UnregisterAddonMgrCallback(AddonType::PVRDLL);
+
+ for (const auto& client : m_clientMap)
+ {
+ client.second->Destroy();
+ }
+}
+
+void CPVRClients::Start()
+{
+ UpdateClients();
+}
+
+void CPVRClients::Stop()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& client : m_clientMap)
+ {
+ client.second->Stop();
+ }
+}
+
+void CPVRClients::Continue()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& client : m_clientMap)
+ {
+ client.second->Continue();
+ }
+}
+
+void CPVRClients::UpdateClients(
+ const std::string& changedAddonId /* = "" */,
+ ADDON::AddonInstanceId changedInstanceId /* = ADDON::ADDON_SINGLETON_INSTANCE_ID */)
+{
+ std::vector<std::pair<AddonInfoPtr, bool>> addonsWithStatus;
+ if (!GetAddonsWithStatus(changedAddonId, addonsWithStatus))
+ return;
+
+ std::vector<std::shared_ptr<CPVRClient>> clientsToCreate; // client
+ std::vector<std::pair<int, std::string>> clientsToReCreate; // client id, addon name
+ std::vector<int> clientsToDestroy; // client id
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& addonWithStatus : addonsWithStatus)
+ {
+ const AddonInfoPtr addon = addonWithStatus.first;
+ const std::vector<std::pair<ADDON::AddonInstanceId, bool>> instanceIdsWithStatus =
+ GetInstanceIdsWithStatus(addon, addonWithStatus.second);
+
+ for (const auto& instanceIdWithStatus : instanceIdsWithStatus)
+ {
+ const ADDON::AddonInstanceId instanceId = instanceIdWithStatus.first;
+ bool instanceEnabled = instanceIdWithStatus.second;
+ const CPVRClientUID clientUID(addon->ID(), instanceId);
+ const int clientId = clientUID.GetUID();
+
+ if (instanceEnabled && (!IsKnownClient(clientId) || !IsCreatedClient(clientId)))
+ {
+ std::shared_ptr<CPVRClient> client;
+ const bool isKnownClient = IsKnownClient(clientId);
+ if (isKnownClient)
+ {
+ client = GetClient(clientId);
+ }
+ else
+ {
+ client = std::make_shared<CPVRClient>(addon, instanceId, clientId);
+ if (!client)
+ {
+ CLog::LogF(LOGERROR, "Severe error, incorrect add-on type");
+ continue;
+ }
+ }
+
+ // determine actual enabled state of instance
+ if (instanceId != ADDON_SINGLETON_INSTANCE_ID)
+ instanceEnabled = client->IsEnabled();
+
+ if (instanceEnabled)
+ clientsToCreate.emplace_back(client);
+ else if (isKnownClient)
+ clientsToDestroy.emplace_back(clientId);
+ }
+ else if (IsCreatedClient(clientId))
+ {
+ // determine actual enabled state of instance
+ if (instanceEnabled && instanceId != ADDON_SINGLETON_INSTANCE_ID)
+ {
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ instanceEnabled = client ? client->IsEnabled() : false;
+ }
+
+ if (instanceEnabled)
+ clientsToReCreate.emplace_back(clientId, addon->Name());
+ else
+ clientsToDestroy.emplace_back(clientId);
+ }
+ }
+ }
+ }
+
+ if (!clientsToCreate.empty() || !clientsToReCreate.empty() || !clientsToDestroy.empty())
+ {
+ CServiceBroker::GetPVRManager().Stop();
+
+ auto progressHandler = std::make_unique<CPVRGUIProgressHandler>(
+ g_localizeStrings.Get(19239)); // Creating PVR clients
+
+ unsigned int i = 0;
+ for (const auto& client : clientsToCreate)
+ {
+ progressHandler->UpdateProgress(client->Name(), i++,
+ clientsToCreate.size() + clientsToReCreate.size());
+
+ const ADDON_STATUS status = client->Create();
+
+ if (status != ADDON_STATUS_OK)
+ {
+ CLog::LogF(LOGERROR, "Failed to create add-on {}, status = {}", client->ID(), status);
+ if (status == ADDON_STATUS_PERMANENT_FAILURE)
+ {
+ CServiceBroker::GetAddonMgr().DisableAddon(client->ID(),
+ AddonDisabledReason::PERMANENT_FAILURE);
+ CServiceBroker::GetJobManager()->AddJob(
+ new CPVREventLogJob(true, EventLevel::Error, client->Name(),
+ g_localizeStrings.Get(24070), client->Icon()),
+ nullptr);
+ }
+ }
+ }
+
+ for (const auto& clientInfo : clientsToReCreate)
+ {
+ progressHandler->UpdateProgress(clientInfo.second, i++,
+ clientsToCreate.size() + clientsToReCreate.size());
+
+ // stop and recreate client
+ StopClient(clientInfo.first, true /* restart */);
+ }
+
+ progressHandler.reset();
+
+ for (const auto& client : clientsToDestroy)
+ {
+ // destroy client
+ StopClient(client, false /* no restart */);
+ }
+
+ if (!clientsToCreate.empty())
+ {
+ // update created clients map
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& client : clientsToCreate)
+ {
+ if (m_clientMap.find(client->GetID()) == m_clientMap.end())
+ {
+ m_clientMap.insert({client->GetID(), client});
+ }
+ }
+ }
+
+ CServiceBroker::GetPVRManager().Start();
+ }
+}
+
+bool CPVRClients::RequestRestart(const std::string& addonId,
+ ADDON::AddonInstanceId instanceId,
+ bool bDataChanged)
+{
+ CServiceBroker::GetJobManager()->Submit([this, addonId, instanceId] {
+ UpdateClients(addonId, instanceId);
+ return true;
+ });
+ return true;
+}
+
+bool CPVRClients::StopClient(int clientId, bool restart)
+{
+ // stop playback if needed
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying())
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client)
+ {
+ if (restart)
+ {
+ client->ReCreate();
+ }
+ else
+ {
+ const auto it = m_clientMap.find(clientId);
+ if (it != m_clientMap.end())
+ m_clientMap.erase(it);
+
+ client->Destroy();
+ }
+ return true;
+ }
+
+ return false;
+}
+
+void CPVRClients::OnAddonEvent(const AddonEvent& event)
+{
+ if (typeid(event) == typeid(AddonEvents::Enabled) || // also called on install,
+ typeid(event) == typeid(AddonEvents::Disabled) || // not called on uninstall
+ typeid(event) == typeid(AddonEvents::UnInstalled) ||
+ typeid(event) == typeid(AddonEvents::ReInstalled) ||
+ typeid(event) == typeid(AddonEvents::InstanceAdded) ||
+ typeid(event) == typeid(AddonEvents::InstanceRemoved))
+ {
+ // update addons
+ const std::string addonId = event.addonId;
+ const ADDON::AddonInstanceId instanceId = event.instanceId;
+ if (CServiceBroker::GetAddonMgr().HasType(addonId, AddonType::PVRDLL))
+ {
+ CServiceBroker::GetJobManager()->Submit([this, addonId, instanceId] {
+ UpdateClients(addonId, instanceId);
+ return true;
+ });
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// client access
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+std::shared_ptr<CPVRClient> CPVRClients::GetClient(int clientId) const
+{
+ if (clientId <= PVR_INVALID_CLIENT_ID)
+ return {};
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = m_clientMap.find(clientId);
+ if (it != m_clientMap.end())
+ return it->second;
+
+ return {};
+}
+
+int CPVRClients::CreatedClientAmount() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::count_if(m_clientMap.cbegin(), m_clientMap.cend(),
+ [](const auto& client) { return client.second->ReadyToUse(); });
+}
+
+bool CPVRClients::HasCreatedClients() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(),
+ [](const auto& client) { return client.second->ReadyToUse(); });
+}
+
+bool CPVRClients::IsKnownClient(int clientId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // valid client IDs start at 1
+ const auto it = m_clientMap.find(clientId);
+ return (it != m_clientMap.end() && (*it).second->GetID() > 0);
+}
+
+bool CPVRClients::IsCreatedClient(int iClientId) const
+{
+ return GetCreatedClient(iClientId) != nullptr;
+}
+
+std::shared_ptr<CPVRClient> CPVRClients::GetCreatedClient(int clientId) const
+{
+ std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client && client->ReadyToUse())
+ return client;
+
+ return {};
+}
+
+CPVRClientMap CPVRClients::GetCreatedClients() const
+{
+ CPVRClientMap clients;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& client : m_clientMap)
+ {
+ if (client.second->ReadyToUse())
+ {
+ clients.insert(std::make_pair(client.second->GetID(), client.second));
+ }
+ }
+
+ return clients;
+}
+
+std::vector<CVariant> CPVRClients::GetClientProviderInfos() const
+{
+ std::vector<AddonInfoPtr> addonInfos;
+ // Get enabled and disabled PVR client addon infos
+ CServiceBroker::GetAddonMgr().GetAddonInfos(addonInfos, false, AddonType::PVRDLL);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ std::vector<CVariant> clientProviderInfos;
+ for (const auto& addonInfo : addonInfos)
+ {
+ std::vector<ADDON::AddonInstanceId> instanceIds = addonInfo->GetKnownInstanceIds();
+ for (const auto& instanceId : instanceIds)
+ {
+ CVariant clientProviderInfo(CVariant::VariantTypeObject);
+ clientProviderInfo["clientid"] = CPVRClientUID(addonInfo->ID(), instanceId).GetUID();
+ clientProviderInfo["addonid"] = addonInfo->ID();
+ clientProviderInfo["instanceid"] = instanceId;
+ clientProviderInfo["enabled"] =
+ !CServiceBroker::GetAddonMgr().IsAddonDisabled(addonInfo->ID());
+ clientProviderInfo["name"] = addonInfo->Name();
+ clientProviderInfo["icon"] = addonInfo->Icon();
+ auto& artMap = addonInfo->Art();
+ auto thumbEntry = artMap.find("thumb");
+ if (thumbEntry != artMap.end())
+ clientProviderInfo["thumb"] = thumbEntry->second;
+
+ clientProviderInfos.emplace_back(clientProviderInfo);
+ }
+ }
+
+ return clientProviderInfos;
+}
+
+int CPVRClients::GetFirstCreatedClientID()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = std::find_if(m_clientMap.cbegin(), m_clientMap.cend(),
+ [](const auto& client) { return client.second->ReadyToUse(); });
+ return it != m_clientMap.cend() ? (*it).second->GetID() : -1;
+}
+
+PVR_ERROR CPVRClients::GetCallableClients(CPVRClientMap& clientsReady,
+ std::vector<int>& clientsNotReady) const
+{
+ clientsNotReady.clear();
+
+ std::vector<AddonInfoPtr> addons;
+ CServiceBroker::GetAddonMgr().GetAddonInfos(addons, true, AddonType::PVRDLL);
+
+ for (const auto& addon : addons)
+ {
+ std::vector<ADDON::AddonInstanceId> instanceIds = addon->GetKnownInstanceIds();
+ for (const auto& instanceId : instanceIds)
+ {
+ const int clientId = CPVRClientUID(addon->ID(), instanceId).GetUID();
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+
+ if (client && client->ReadyToUse() && !client->IgnoreClient())
+ {
+ clientsReady.insert(std::make_pair(clientId, client));
+ }
+ else
+ {
+ clientsNotReady.emplace_back(clientId);
+ }
+ }
+ }
+
+ return clientsNotReady.empty() ? PVR_ERROR_NO_ERROR : PVR_ERROR_SERVER_ERROR;
+}
+
+int CPVRClients::EnabledClientAmount() const
+{
+ CPVRClientMap clientMap;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ clientMap = m_clientMap;
+ }
+
+ ADDON::CAddonMgr& addonMgr = CServiceBroker::GetAddonMgr();
+ return std::count_if(clientMap.cbegin(), clientMap.cend(), [&addonMgr](const auto& client) {
+ return !addonMgr.IsAddonDisabled(client.second->ID());
+ });
+}
+
+bool CPVRClients::IsEnabledClient(int clientId) const
+{
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ return client && !CServiceBroker::GetAddonMgr().IsAddonDisabled(client->ID());
+}
+
+std::vector<CVariant> CPVRClients::GetEnabledClientInfos() const
+{
+ std::vector<CVariant> clientInfos;
+
+ CPVRClientMap clientMap;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ clientMap = m_clientMap;
+ }
+
+ for (const auto& client : clientMap)
+ {
+ const auto& addonInfo =
+ CServiceBroker::GetAddonMgr().GetAddonInfo(client.second->ID(), AddonType::PVRDLL);
+
+ if (addonInfo)
+ {
+ // This will be the same variant structure used in the json api
+ CVariant clientInfo(CVariant::VariantTypeObject);
+ clientInfo["clientid"] = client.first;
+ clientInfo["addonid"] = client.second->ID();
+ clientInfo["instanceid"] = client.second->InstanceId();
+ clientInfo["label"] = addonInfo->Name(); // Note that this is called label instead of name
+
+ const auto& capabilities = client.second->GetClientCapabilities();
+ clientInfo["supportstv"] = capabilities.SupportsTV();
+ clientInfo["supportsradio"] = capabilities.SupportsRadio();
+ clientInfo["supportsepg"] = capabilities.SupportsEPG();
+ clientInfo["supportsrecordings"] = capabilities.SupportsRecordings();
+ clientInfo["supportstimers"] = capabilities.SupportsTimers();
+ clientInfo["supportschannelgroups"] = capabilities.SupportsChannelGroups();
+ clientInfo["supportschannelscan"] = capabilities.SupportsChannelScan();
+ clientInfo["supportchannelproviders"] = capabilities.SupportsProviders();
+
+ clientInfos.push_back(clientInfo);
+ }
+ }
+
+ return clientInfos;
+}
+
+bool CPVRClients::HasIgnoredClients() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(),
+ [](const auto& client) { return client.second->IgnoreClient(); });
+}
+
+std::vector<ADDON::AddonInstanceId> CPVRClients::GetKnownInstanceIds(
+ const std::string& addonID) const
+{
+ std::vector<ADDON::AddonInstanceId> instanceIds;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& entry : m_clientMap)
+ {
+ if (entry.second->ID() == addonID)
+ instanceIds.emplace_back(entry.second->InstanceId());
+ }
+
+ return instanceIds;
+}
+
+bool CPVRClients::GetAddonsWithStatus(
+ const std::string& changedAddonId,
+ std::vector<std::pair<AddonInfoPtr, bool>>& addonsWithStatus) const
+{
+ std::vector<AddonInfoPtr> addons;
+ CServiceBroker::GetAddonMgr().GetAddonInfos(addons, false, AddonType::PVRDLL);
+
+ if (addons.empty())
+ return false;
+
+ bool foundChangedAddon = changedAddonId.empty();
+ for (const auto& addon : addons)
+ {
+ bool enabled = !CServiceBroker::GetAddonMgr().IsAddonDisabled(addon->ID());
+ addonsWithStatus.emplace_back(std::make_pair(addon, enabled));
+
+ if (!foundChangedAddon && addon->ID() == changedAddonId)
+ foundChangedAddon = true;
+ }
+
+ return foundChangedAddon;
+}
+
+std::vector<std::pair<ADDON::AddonInstanceId, bool>> CPVRClients::GetInstanceIdsWithStatus(
+ const AddonInfoPtr& addon, bool addonIsEnabled) const
+{
+ std::vector<std::pair<ADDON::AddonInstanceId, bool>> instanceIdsWithStatus;
+
+ std::vector<ADDON::AddonInstanceId> instanceIds = addon->GetKnownInstanceIds();
+ std::transform(instanceIds.cbegin(), instanceIds.cend(),
+ std::back_inserter(instanceIdsWithStatus), [addonIsEnabled](const auto& id) {
+ return std::pair<ADDON::AddonInstanceId, bool>(id, addonIsEnabled);
+ });
+
+ // find removed instances
+ const std::vector<ADDON::AddonInstanceId> knownInstanceIds = GetKnownInstanceIds(addon->ID());
+ for (const auto& knownInstanceId : knownInstanceIds)
+ {
+ if (std::find(instanceIds.begin(), instanceIds.end(), knownInstanceId) == instanceIds.end())
+ {
+ // instance was removed
+ instanceIdsWithStatus.emplace_back(
+ std::pair<ADDON::AddonInstanceId, bool>(knownInstanceId, false));
+ }
+ }
+
+ return instanceIdsWithStatus;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// client API calls
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+std::vector<SBackend> CPVRClients::GetBackendProperties() const
+{
+ std::vector<SBackend> backendProperties;
+
+ ForCreatedClients(__FUNCTION__, [&backendProperties](const std::shared_ptr<CPVRClient>& client) {
+ SBackend properties;
+
+ if (client->GetDriveSpace(properties.diskTotal, properties.diskUsed) == PVR_ERROR_NO_ERROR)
+ {
+ properties.diskTotal *= 1024;
+ properties.diskUsed *= 1024;
+ }
+
+ int iAmount = 0;
+ if (client->GetProvidersAmount(iAmount) == PVR_ERROR_NO_ERROR)
+ properties.numProviders = iAmount;
+ if (client->GetChannelGroupsAmount(iAmount) == PVR_ERROR_NO_ERROR)
+ properties.numChannelGroups = iAmount;
+ if (client->GetChannelsAmount(iAmount) == PVR_ERROR_NO_ERROR)
+ properties.numChannels = iAmount;
+ if (client->GetTimersAmount(iAmount) == PVR_ERROR_NO_ERROR)
+ properties.numTimers = iAmount;
+ if (client->GetRecordingsAmount(false, iAmount) == PVR_ERROR_NO_ERROR)
+ properties.numRecordings = iAmount;
+ if (client->GetRecordingsAmount(true, iAmount) == PVR_ERROR_NO_ERROR)
+ properties.numDeletedRecordings = iAmount;
+ properties.name = client->GetBackendName();
+ properties.version = client->GetBackendVersion();
+ properties.host = client->GetConnectionString();
+
+ backendProperties.emplace_back(properties);
+ return PVR_ERROR_NO_ERROR;
+ });
+
+ return backendProperties;
+}
+
+bool CPVRClients::GetTimers(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRTimersContainer* timers,
+ std::vector<int>& failedClients)
+{
+ return ForClients(__FUNCTION__, clients,
+ [timers](const std::shared_ptr<CPVRClient>& client) {
+ return client->GetTimers(timers);
+ },
+ failedClients) == PVR_ERROR_NO_ERROR;
+}
+
+PVR_ERROR CPVRClients::GetTimerTypes(std::vector<std::shared_ptr<CPVRTimerType>>& results) const
+{
+ return ForCreatedClients(__FUNCTION__, [&results](const std::shared_ptr<CPVRClient>& client) {
+ std::vector<std::shared_ptr<CPVRTimerType>> types;
+ PVR_ERROR ret = client->GetTimerTypes(types);
+ if (ret == PVR_ERROR_NO_ERROR)
+ results.insert(results.end(), types.begin(), types.end());
+ return ret;
+ });
+}
+
+PVR_ERROR CPVRClients::GetRecordings(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRRecordings* recordings,
+ bool deleted,
+ std::vector<int>& failedClients)
+{
+ return ForClients(__FUNCTION__, clients,
+ [recordings, deleted](const std::shared_ptr<CPVRClient>& client) {
+ return client->GetRecordings(recordings, deleted);
+ },
+ failedClients);
+}
+
+PVR_ERROR CPVRClients::DeleteAllRecordingsFromTrash()
+{
+ return ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) {
+ return client->DeleteAllRecordingsFromTrash();
+ });
+}
+
+PVR_ERROR CPVRClients::SetEPGMaxPastDays(int iPastDays)
+{
+ return ForCreatedClients(__FUNCTION__, [iPastDays](const std::shared_ptr<CPVRClient>& client) {
+ return client->SetEPGMaxPastDays(iPastDays);
+ });
+}
+
+PVR_ERROR CPVRClients::SetEPGMaxFutureDays(int iFutureDays)
+{
+ return ForCreatedClients(__FUNCTION__, [iFutureDays](const std::shared_ptr<CPVRClient>& client) {
+ return client->SetEPGMaxFutureDays(iFutureDays);
+ });
+}
+
+PVR_ERROR CPVRClients::GetChannels(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ bool bRadio,
+ std::vector<std::shared_ptr<CPVRChannel>>& channels,
+ std::vector<int>& failedClients)
+{
+ return ForClients(__FUNCTION__, clients,
+ [bRadio, &channels](const std::shared_ptr<CPVRClient>& client) {
+ return client->GetChannels(bRadio, channels);
+ },
+ failedClients);
+}
+
+PVR_ERROR CPVRClients::GetProviders(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRProvidersContainer* providers,
+ std::vector<int>& failedClients)
+{
+ return ForClients(__FUNCTION__, clients,
+ [providers](const std::shared_ptr<CPVRClient>& client) {
+ return client->GetProviders(*providers);
+ },
+ failedClients);
+}
+
+PVR_ERROR CPVRClients::GetChannelGroups(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRChannelGroups* groups,
+ std::vector<int>& failedClients)
+{
+ return ForClients(__FUNCTION__, clients,
+ [groups](const std::shared_ptr<CPVRClient>& client) {
+ return client->GetChannelGroups(groups);
+ },
+ failedClients);
+}
+
+PVR_ERROR CPVRClients::GetChannelGroupMembers(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRChannelGroup* group,
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers,
+ std::vector<int>& failedClients)
+{
+ return ForClients(__FUNCTION__, clients,
+ [group, &groupMembers](const std::shared_ptr<CPVRClient>& client) {
+ return client->GetChannelGroupMembers(group, groupMembers);
+ },
+ failedClients);
+}
+
+std::vector<std::shared_ptr<CPVRClient>> CPVRClients::GetClientsSupportingChannelScan() const
+{
+ std::vector<std::shared_ptr<CPVRClient>> possibleScanClients;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& entry : m_clientMap)
+ {
+ const auto& client = entry.second;
+ if (client->ReadyToUse() && !client->IgnoreClient() &&
+ client->GetClientCapabilities().SupportsChannelScan())
+ possibleScanClients.emplace_back(client);
+ }
+
+ return possibleScanClients;
+}
+
+std::vector<std::shared_ptr<CPVRClient>> CPVRClients::GetClientsSupportingChannelSettings(bool bRadio) const
+{
+ std::vector<std::shared_ptr<CPVRClient>> possibleSettingsClients;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& entry : m_clientMap)
+ {
+ const auto& client = entry.second;
+ if (client->ReadyToUse() && !client->IgnoreClient())
+ {
+ const CPVRClientCapabilities& caps = client->GetClientCapabilities();
+ if (caps.SupportsChannelSettings() &&
+ ((bRadio && caps.SupportsRadio()) || (!bRadio && caps.SupportsTV())))
+ possibleSettingsClients.emplace_back(client);
+ }
+ }
+
+ return possibleSettingsClients;
+}
+
+bool CPVRClients::AnyClientSupportingRecordingsSize() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), [](const auto& entry) {
+ const auto& client = entry.second;
+ return client->ReadyToUse() && !client->IgnoreClient() &&
+ client->GetClientCapabilities().SupportsRecordingsSize();
+ });
+}
+
+bool CPVRClients::AnyClientSupportingEPG() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), [](const auto& entry) {
+ const auto& client = entry.second;
+ return client->ReadyToUse() && !client->IgnoreClient() &&
+ client->GetClientCapabilities().SupportsEPG();
+ });
+}
+
+bool CPVRClients::AnyClientSupportingRecordings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), [](const auto& entry) {
+ const auto& client = entry.second;
+ return client->ReadyToUse() && !client->IgnoreClient() &&
+ client->GetClientCapabilities().SupportsRecordings();
+ });
+}
+
+bool CPVRClients::AnyClientSupportingRecordingsDelete() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), [](const auto& entry) {
+ const auto& client = entry.second;
+ return client->ReadyToUse() && !client->IgnoreClient() &&
+ client->GetClientCapabilities().SupportsRecordingsDelete();
+ });
+}
+
+void CPVRClients::OnSystemSleep()
+{
+ ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) {
+ client->OnSystemSleep();
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+void CPVRClients::OnSystemWake()
+{
+ ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) {
+ client->OnSystemWake();
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+void CPVRClients::OnPowerSavingActivated()
+{
+ ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) {
+ client->OnPowerSavingActivated();
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+void CPVRClients::OnPowerSavingDeactivated()
+{
+ ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) {
+ client->OnPowerSavingDeactivated();
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+void CPVRClients::ConnectionStateChange(CPVRClient* client,
+ const std::string& strConnectionString,
+ PVR_CONNECTION_STATE newState,
+ const std::string& strMessage)
+{
+ if (!client)
+ return;
+
+ int iMsg = -1;
+ EventLevel eLevel = EventLevel::Error;
+ bool bNotify = true;
+
+ switch (newState)
+ {
+ case PVR_CONNECTION_STATE_SERVER_UNREACHABLE:
+ iMsg = 35505; // Server is unreachable
+ if (client->GetPreviousConnectionState() == PVR_CONNECTION_STATE_UNKNOWN ||
+ client->GetPreviousConnectionState() == PVR_CONNECTION_STATE_CONNECTING)
+ {
+ // Make our users happy. There were so many complaints about this notification because their TV backend
+ // was not up quick enough after Kodi start. So, ignore the very first 'server not reachable' notification.
+ bNotify = false;
+ }
+ break;
+ case PVR_CONNECTION_STATE_SERVER_MISMATCH:
+ iMsg = 35506; // Server does not respond properly
+ break;
+ case PVR_CONNECTION_STATE_VERSION_MISMATCH:
+ iMsg = 35507; // Server version is not compatible
+ break;
+ case PVR_CONNECTION_STATE_ACCESS_DENIED:
+ iMsg = 35508; // Access denied
+ break;
+ case PVR_CONNECTION_STATE_CONNECTED:
+ eLevel = EventLevel::Basic;
+ iMsg = 36034; // Connection established
+ if (client->GetPreviousConnectionState() == PVR_CONNECTION_STATE_UNKNOWN ||
+ client->GetPreviousConnectionState() == PVR_CONNECTION_STATE_CONNECTING)
+ bNotify = false;
+ break;
+ case PVR_CONNECTION_STATE_DISCONNECTED:
+ iMsg = 36030; // Connection lost
+ break;
+ case PVR_CONNECTION_STATE_CONNECTING:
+ eLevel = EventLevel::Information;
+ iMsg = 35509; // Connecting
+ bNotify = false;
+ break;
+ default:
+ CLog::LogF(LOGERROR, "Unknown connection state");
+ return;
+ }
+
+ // Use addon-supplied message, if present
+ std::string strMsg;
+ if (!strMessage.empty())
+ strMsg = strMessage;
+ else
+ strMsg = g_localizeStrings.Get(iMsg);
+
+ if (!strConnectionString.empty())
+ strMsg = StringUtils::Format("{} ({})", strMsg, strConnectionString);
+
+ // Notify user.
+ CServiceBroker::GetJobManager()->AddJob(
+ new CPVREventLogJob(bNotify, eLevel, client->GetFriendlyName(), strMsg, client->Icon()),
+ nullptr);
+}
+
+namespace
+{
+
+void LogClientWarning(const char* strFunctionName, const std::shared_ptr<CPVRClient>& client)
+{
+ if (client->IgnoreClient())
+ CLog::Log(LOGWARNING, "{}: Not calling add-on '{}'. Add-on not (yet) connected.",
+ strFunctionName, client->ID());
+ else if (!client->ReadyToUse())
+ CLog::Log(LOGWARNING, "{}: Not calling add-on '{}'. Add-on not ready to use.", strFunctionName,
+ client->ID());
+ else
+ CLog::Log(LOGERROR, "{}: Not calling add-on '{}' for unexpected reason.", strFunctionName,
+ client->ID());
+}
+
+} // unnamed namespace
+
+PVR_ERROR CPVRClients::ForCreatedClients(const char* strFunctionName,
+ const PVRClientFunction& function) const
+{
+ std::vector<int> failedClients;
+ return ForCreatedClients(strFunctionName, function, failedClients);
+}
+
+PVR_ERROR CPVRClients::ForCreatedClients(const char* strFunctionName,
+ const PVRClientFunction& function,
+ std::vector<int>& failedClients) const
+{
+ PVR_ERROR lastError = PVR_ERROR_NO_ERROR;
+
+ CPVRClientMap clients;
+ GetCallableClients(clients, failedClients);
+
+ if (!failedClients.empty())
+ {
+ std::shared_ptr<CPVRClient> client;
+ for (int id : failedClients)
+ {
+ client = GetClient(id);
+ if (client)
+ LogClientWarning(strFunctionName, client);
+ }
+ }
+
+ for (const auto& clientEntry : clients)
+ {
+ PVR_ERROR currentError = function(clientEntry.second);
+
+ if (currentError != PVR_ERROR_NO_ERROR && currentError != PVR_ERROR_NOT_IMPLEMENTED)
+ {
+ lastError = currentError;
+ failedClients.emplace_back(clientEntry.first);
+ }
+ }
+ return lastError;
+}
+
+PVR_ERROR CPVRClients::ForClients(const char* strFunctionName,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ const PVRClientFunction& function,
+ std::vector<int>& failedClients) const
+{
+ if (clients.empty())
+ return ForCreatedClients(strFunctionName, function, failedClients);
+
+ PVR_ERROR lastError = PVR_ERROR_NO_ERROR;
+
+ failedClients.clear();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& entry : m_clientMap)
+ {
+ if (entry.second->ReadyToUse() && !entry.second->IgnoreClient() &&
+ std::any_of(clients.cbegin(), clients.cend(),
+ [&entry](const auto& client) { return client->GetID() == entry.first; }))
+ {
+ // Allow ready to use clients that shall be called
+ continue;
+ }
+
+ failedClients.emplace_back(entry.first);
+ }
+ }
+
+ for (const auto& client : clients)
+ {
+ if (std::none_of(failedClients.cbegin(), failedClients.cend(),
+ [&client](int failedClientId) { return failedClientId == client->GetID(); }))
+ {
+ PVR_ERROR currentError = function(client);
+
+ if (currentError != PVR_ERROR_NO_ERROR && currentError != PVR_ERROR_NOT_IMPLEMENTED)
+ {
+ lastError = currentError;
+ failedClients.emplace_back(client->GetID());
+ }
+ }
+ else
+ {
+ LogClientWarning(strFunctionName, client);
+ }
+ }
+ return lastError;
+}
diff --git a/xbmc/pvr/addons/PVRClients.h b/xbmc/pvr/addons/PVRClients.h
new file mode 100644
index 0000000..32248fa
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClients.h
@@ -0,0 +1,485 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/IAddonManagerCallback.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_general.h"
+#include "threads/CriticalSection.h"
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CVariant;
+
+namespace ADDON
+{
+ struct AddonEvent;
+ class CAddonInfo;
+}
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRChannelGroupInternal;
+class CPVRChannelGroup;
+class CPVRChannelGroupMember;
+class CPVRChannelGroups;
+class CPVRProvidersContainer;
+class CPVRClient;
+class CPVREpg;
+class CPVRRecordings;
+class CPVRTimerType;
+class CPVRTimersContainer;
+
+typedef std::map<int, std::shared_ptr<CPVRClient>> CPVRClientMap;
+
+/**
+ * Holds generic data about a backend (number of channels etc.)
+ */
+struct SBackend
+{
+ std::string name;
+ std::string version;
+ std::string host;
+ int numTimers = 0;
+ int numRecordings = 0;
+ int numDeletedRecordings = 0;
+ int numProviders = 0;
+ int numChannelGroups = 0;
+ int numChannels = 0;
+ uint64_t diskUsed = 0;
+ uint64_t diskTotal = 0;
+};
+
+ class CPVRClients : public ADDON::IAddonMgrCallback
+ {
+ public:
+ CPVRClients();
+ ~CPVRClients() override;
+
+ /*!
+ * @brief Start all clients.
+ */
+ void Start();
+
+ /*!
+ * @brief Stop all clients.
+ */
+ void Stop();
+
+ /*!
+ * @brief Continue all clients.
+ */
+ void Continue();
+
+ /*!
+ * @brief Update all clients, sync with Addon Manager state (start, restart, shutdown clients).
+ * @param changedAddonId The id of the changed addon, empty string denotes 'any addon'.
+ * @param changedInstanceId The Identifier of the changed add-on instance
+ */
+ void UpdateClients(
+ const std::string& changedAddonId = "",
+ ADDON::AddonInstanceId changedInstanceId = ADDON::ADDON_SINGLETON_INSTANCE_ID);
+
+ /*!
+ * @brief Restart a single client add-on.
+ * @param addonId The add-on to restart.
+ * @param instanceId Instance identifier to use
+ * @param bDataChanged True if the client's data changed, false otherwise (unused).
+ * @return True if the client was found and restarted, false otherwise.
+ */
+ bool RequestRestart(const std::string& addonId,
+ ADDON::AddonInstanceId instanceId,
+ bool bDataChanged) override;
+
+ /*!
+ * @brief Stop a client.
+ * @param clientId The id of the client to stop.
+ * @param restart If true, restart the client.
+ * @return True if the client was found, false otherwise.
+ */
+ bool StopClient(int clientId, bool restart);
+
+ /*!
+ * @brief Handle addon events (enable, disable, ...).
+ * @param event The addon event.
+ */
+ void OnAddonEvent(const ADDON::AddonEvent& event);
+
+ /*!
+ * @brief Get the number of created clients.
+ * @return The amount of created clients.
+ */
+ int CreatedClientAmount() const;
+
+ /*!
+ * @brief Check whether there are any created clients.
+ * @return True if at least one client is created.
+ */
+ bool HasCreatedClients() const;
+
+ /*!
+ * @brief Check whether a given client ID points to a created client.
+ * @param iClientId The client ID.
+ * @return True if the the client ID represents a created client, false otherwise.
+ */
+ bool IsCreatedClient(int iClientId) const;
+
+ /*!
+ * @brief Get the the client for the given client id, if it is created.
+ * @param clientId The ID of the client to get.
+ * @return The client if found, nullptr otherwise.
+ */
+ std::shared_ptr<CPVRClient> GetCreatedClient(int clientId) const;
+
+ /*!
+ * @brief Get all created clients.
+ * @return All created clients.
+ */
+ CPVRClientMap GetCreatedClients() const;
+
+ /*!
+ * @brief Get the ID of the first created client.
+ * @return the ID or -1 if no clients are created;
+ */
+ int GetFirstCreatedClientID();
+
+ /*!
+ * @brief Check whether there are any created, but not (yet) connected clients.
+ * @return True if at least one client is ignored.
+ */
+ bool HasIgnoredClients() const;
+
+ /*!
+ * @brief Get the number of enabled clients.
+ * @return The amount of enabled clients.
+ */
+ int EnabledClientAmount() const;
+
+ /*!
+ * @brief Check whether a given client ID points to an enabled client.
+ * @param clientId The client ID.
+ * @return True if the the client ID represents an enabled client, false otherwise.
+ */
+ bool IsEnabledClient(int clientId) const;
+
+ /*!
+ * @brief Get a list of the enabled client infos.
+ * @return A list of enabled client infos.
+ */
+ std::vector<CVariant> GetEnabledClientInfos() const;
+
+ /*!
+ * @brief Get info required for providers. Include both enabled and disabled PVR add-ons
+ * @return A list containing the information required to create client providers.
+ */
+ std::vector<CVariant> GetClientProviderInfos() const;
+
+ //@}
+
+ /*! @name general methods */
+ //@{
+
+ /*!
+ * @brief Returns properties about all created clients
+ * @return the properties
+ */
+ std::vector<SBackend> GetBackendProperties() const;
+
+ //@}
+
+ /*! @name Timer methods */
+ //@{
+
+ /*!
+ * @brief Get all timers from the given clients
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param timers Store the timers in this container.
+ * @param failedClients in case of errors will contain the ids of the clients for which the timers could not be obtained.
+ * @return true on success for all clients, false in case of error for at least one client.
+ */
+ bool GetTimers(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRTimersContainer* timers,
+ std::vector<int>& failedClients);
+
+ /*!
+ * @brief Get all supported timer types.
+ * @param results The container to store the result in.
+ * @return PVR_ERROR_NO_ERROR if the operation succeeded, the respective PVR_ERROR value otherwise.
+ */
+ PVR_ERROR GetTimerTypes(std::vector<std::shared_ptr<CPVRTimerType>>& results) const;
+
+ //@}
+
+ /*! @name Recording methods */
+ //@{
+
+ /*!
+ * @brief Get all recordings from the given clients
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param recordings Store the recordings in this container.
+ * @param deleted If true, return deleted recordings, return not deleted recordings otherwise.
+ * @param failedClients in case of errors will contain the ids of the clients for which the recordings could not be obtained.
+ * @return PVR_ERROR_NO_ERROR if the operation succeeded, the respective PVR_ERROR value otherwise.
+ */
+ PVR_ERROR GetRecordings(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRRecordings* recordings,
+ bool deleted,
+ std::vector<int>& failedClients);
+
+ /*!
+ * @brief Delete all "soft" deleted recordings permanently on the backend.
+ * @return PVR_ERROR_NO_ERROR if the operation succeeded, the respective PVR_ERROR value otherwise.
+ */
+ PVR_ERROR DeleteAllRecordingsFromTrash();
+
+ //@}
+
+ /*! @name EPG methods */
+ //@{
+
+ /*!
+ * @brief Tell all clients the past time frame to use when notifying epg events back to Kodi.
+ *
+ * The clients might push epg events asynchronously to Kodi using the callback function
+ * EpgEventStateChange. To be able to only push events that are actually of interest for Kodi,
+ * clients need to know about the future epg time frame Kodi uses.
+ *
+ * @param[in] iPastDays number of days before "now".
+ * @ref EPG_TIMEFRAME_UNLIMITED means that Kodi is interested in all
+ * epg events, regardless of event times.
+ * @return @ref PVR_ERROR_NO_ERROR if the operation succeeded, the respective @ref PVR_ERROR
+ * value otherwise.
+ */
+ PVR_ERROR SetEPGMaxPastDays(int iPastDays);
+
+ /*!
+ * @brief Tell all clients the future time frame to use when notifying epg events back to Kodi.
+ *
+ * The clients might push epg events asynchronously to Kodi using the callback function
+ * EpgEventStateChange. To be able to only push events that are actually of interest for Kodi,
+ * clients need to know about the future epg time frame Kodi uses.
+ *
+ * @param[in] iFutureDays number of days from "now".
+ * @ref EPG_TIMEFRAME_UNLIMITED means that Kodi is interested in all
+ * epg events, regardless of event times.
+ * @return @ref PVR_ERROR_NO_ERROR if the operation succeeded, the respective @ref PVR_ERROR
+ * value otherwise.
+ */
+ PVR_ERROR SetEPGMaxFutureDays(int iFutureDays);
+
+ //@}
+
+ /*! @name Channel methods */
+ //@{
+
+ /*!
+ * @brief Get all channels from the given clients.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param bRadio Whether to fetch radio or TV channels.
+ * @param channels The container to store the channels.
+ * @param failedClients in case of errors will contain the ids of the clients for which the channels could not be obtained.
+ * @return PVR_ERROR_NO_ERROR if the channels were fetched successfully, last error otherwise.
+ */
+ PVR_ERROR GetChannels(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ bool bRadio,
+ std::vector<std::shared_ptr<CPVRChannel>>& channels,
+ std::vector<int>& failedClients);
+
+ /*!
+ * @brief Get all providers from backends.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param group The container to store the providers in.
+ * @param failedClients in case of errors will contain the ids of the clients for which the providers could not be obtained.
+ * @return PVR_ERROR_NO_ERROR if the providers were fetched successfully, last error otherwise.
+ */
+ PVR_ERROR GetProviders(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRProvidersContainer* providers,
+ std::vector<int>& failedClients);
+
+ /*!
+ * @brief Get all channel groups from the given clients.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param groups Store the channel groups in this container.
+ * @param failedClients in case of errors will contain the ids of the clients for which the channel groups could not be obtained.
+ * @return PVR_ERROR_NO_ERROR if the channel groups were fetched successfully, last error otherwise.
+ */
+ PVR_ERROR GetChannelGroups(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRChannelGroups* groups,
+ std::vector<int>& failedClients);
+
+ /*!
+ * @brief Get all group members of a channel group from the given clients.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param group The group to get the member for.
+ * @param groupMembers The container for the group members.
+ * @param failedClients in case of errors will contain the ids of the clients for which the channel group members could not be obtained.
+ * @return PVR_ERROR_NO_ERROR if the channel group members were fetched successfully, last error otherwise.
+ */
+ PVR_ERROR GetChannelGroupMembers(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRChannelGroup* group,
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers,
+ std::vector<int>& failedClients);
+
+ /*!
+ * @brief Get a list of clients providing a channel scan dialog.
+ * @return All clients supporting channel scan.
+ */
+ std::vector<std::shared_ptr<CPVRClient>> GetClientsSupportingChannelScan() const;
+
+ /*!
+ * @brief Get a list of clients providing a channel settings dialog.
+ * @return All clients supporting channel settings.
+ */
+ std::vector<std::shared_ptr<CPVRClient>> GetClientsSupportingChannelSettings(bool bRadio) const;
+
+ /*!
+ * @brief Get whether or not any client supports recording size.
+ * @return True if any client supports recording size.
+ */
+ bool AnyClientSupportingRecordingsSize() const;
+
+ /*!
+ * @brief Get whether or not any client supports EPG.
+ * @return True if any client supports EPG.
+ */
+ bool AnyClientSupportingEPG() const;
+
+ /*!
+ * @brief Get whether or not any client supports recordings.
+ * @return True if any client supports recordings.
+ */
+ bool AnyClientSupportingRecordings() const;
+ //@}
+
+ /*!
+ * @brief Get whether or not any client supports recordings delete.
+ * @return True if any client supports recordings delete.
+ */
+ bool AnyClientSupportingRecordingsDelete() const;
+ //@}
+
+ /*! @name Power management methods */
+ //@{
+
+ /*!
+ * @brief Propagate "system sleep" event to clients
+ */
+ void OnSystemSleep();
+
+ /*!
+ * @brief Propagate "system wakeup" event to clients
+ */
+ void OnSystemWake();
+
+ /*!
+ * @brief Propagate "power saving activated" event to clients
+ */
+ void OnPowerSavingActivated();
+
+ /*!
+ * @brief Propagate "power saving deactivated" event to clients
+ */
+ void OnPowerSavingDeactivated();
+
+ //@}
+
+ /*!
+ * @brief Notify a change of an addon connection state.
+ * @param client The changed client.
+ * @param strConnectionString A human-readable string providing additional information.
+ * @param newState The new connection state.
+ * @param strMessage A human readable string replacing default state message.
+ */
+ void ConnectionStateChange(CPVRClient* client,
+ const std::string& strConnectionString,
+ PVR_CONNECTION_STATE newState,
+ const std::string& strMessage);
+
+ private:
+ /*!
+ * @brief Get the known instance ids for a given addon id.
+ * @param addonID The addon id.
+ * @return The list of known instance ids.
+ */
+ std::vector<ADDON::AddonInstanceId> GetKnownInstanceIds(const std::string& addonID) const;
+
+ bool GetAddonsWithStatus(
+ const std::string& changedAddonId,
+ std::vector<std::pair<std::shared_ptr<ADDON::CAddonInfo>, bool>>& addonsWithStatus) const;
+
+ std::vector<std::pair<ADDON::AddonInstanceId, bool>> GetInstanceIdsWithStatus(
+ const std::shared_ptr<ADDON::CAddonInfo>& addon, bool addonIsEnabled) const;
+
+ /*!
+ * @brief Get the client instance for a given client id.
+ * @param clientId The id of the client to get.
+ * @return The client if found, nullptr otherwise.
+ */
+ std::shared_ptr<CPVRClient> GetClient(int clientId) const;
+
+ /*!
+ * @brief Check whether a client is known.
+ * @param iClientId The id of the client to check.
+ * @return True if this client is known, false otherwise.
+ */
+ bool IsKnownClient(int iClientId) const;
+
+ /*!
+ * @brief Get all created clients and clients not (yet) ready to use.
+ * @param clientsReady Store the created clients in this map.
+ * @param clientsNotReady Store the the ids of the not (yet) ready clients in this list.
+ * @return PVR_ERROR_NO_ERROR in case all clients are ready, PVR_ERROR_SERVER_ERROR otherwise.
+ */
+ PVR_ERROR GetCallableClients(CPVRClientMap& clientsReady,
+ std::vector<int>& clientsNotReady) const;
+
+ typedef std::function<PVR_ERROR(const std::shared_ptr<CPVRClient>&)> PVRClientFunction;
+
+ /*!
+ * @brief Wraps calls to the given clients in order to do common pre and post function invocation actions.
+ * @param strFunctionName The function name, for logging purposes.
+ * @param clients The clients to wrap.
+ * @param function The function to wrap. It has to have return type PVR_ERROR and must take a const reference to a std::shared_ptr<CPVRClient> as parameter.
+ * @param failedClients Contains a list of the ids of clients for that the call failed, if any.
+ * @return PVR_ERROR_NO_ERROR on success, any other PVR_ERROR_* value otherwise.
+ */
+ PVR_ERROR ForClients(const char* strFunctionName,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ const PVRClientFunction& function,
+ std::vector<int>& failedClients) const;
+
+ /*!
+ * @brief Wraps calls to all created clients in order to do common pre and post function invocation actions.
+ * @param strFunctionName The function name, for logging purposes.
+ * @param function The function to wrap. It has to have return type PVR_ERROR and must take a const reference to a std::shared_ptr<CPVRClient> as parameter.
+ * @return PVR_ERROR_NO_ERROR on success, any other PVR_ERROR_* value otherwise.
+ */
+ PVR_ERROR ForCreatedClients(const char* strFunctionName,
+ const PVRClientFunction& function) const;
+
+ /*!
+ * @brief Wraps calls to all created clients in order to do common pre and post function invocation actions.
+ * @param strFunctionName The function name, for logging purposes.
+ * @param function The function to wrap. It has to have return type PVR_ERROR and must take a const reference to a std::shared_ptr<CPVRClient> as parameter.
+ * @param failedClients Contains a list of the ids of clients for that the call failed, if any.
+ * @return PVR_ERROR_NO_ERROR on success, any other PVR_ERROR_* value otherwise.
+ */
+ PVR_ERROR ForCreatedClients(const char* strFunctionName,
+ const PVRClientFunction& function,
+ std::vector<int>& failedClients) const;
+
+ mutable CCriticalSection m_critSection;
+ CPVRClientMap m_clientMap;
+ };
+}
diff --git a/xbmc/pvr/channels/CMakeLists.txt b/xbmc/pvr/channels/CMakeLists.txt
new file mode 100644
index 0000000..714f232
--- /dev/null
+++ b/xbmc/pvr/channels/CMakeLists.txt
@@ -0,0 +1,23 @@
+set(SOURCES PVRChannel.cpp
+ PVRChannelGroup.cpp
+ PVRChannelGroupInternal.cpp
+ PVRChannelGroupMember.cpp
+ PVRChannelGroupSettings.cpp
+ PVRChannelGroups.cpp
+ PVRChannelGroupsContainer.cpp
+ PVRChannelNumber.cpp
+ PVRRadioRDSInfoTag.cpp
+ PVRChannelsPath.cpp)
+
+set(HEADERS PVRChannel.h
+ PVRChannelGroup.h
+ PVRChannelGroupInternal.h
+ PVRChannelGroupMember.h
+ PVRChannelGroupSettings.h
+ PVRChannelGroups.h
+ PVRChannelGroupsContainer.h
+ PVRChannelNumber.h
+ PVRRadioRDSInfoTag.h
+ PVRChannelsPath.h)
+
+core_add_library(pvr_channels)
diff --git a/xbmc/pvr/channels/PVRChannel.cpp b/xbmc/pvr/channels/PVRChannel.cpp
new file mode 100644
index 0000000..135719f
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannel.cpp
@@ -0,0 +1,843 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRChannel.h"
+
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/providers/PVRProviders.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+
+using namespace PVR;
+
+const std::string CPVRChannel::IMAGE_OWNER_PATTERN = "pvrchannel_{}";
+
+bool CPVRChannel::operator==(const CPVRChannel& right) const
+{
+ return (m_bIsRadio == right.m_bIsRadio && m_iUniqueId == right.m_iUniqueId &&
+ m_iClientId == right.m_iClientId);
+}
+
+bool CPVRChannel::operator!=(const CPVRChannel& right) const
+{
+ return !(*this == right);
+}
+
+CPVRChannel::CPVRChannel(bool bRadio)
+ : m_bIsRadio(bRadio),
+ m_iconPath("", StringUtils::Format(IMAGE_OWNER_PATTERN, bRadio ? "radio" : "tv"))
+{
+ UpdateEncryptionName();
+}
+
+CPVRChannel::CPVRChannel(bool bRadio, const std::string& iconPath)
+ : m_bIsRadio(bRadio),
+ m_iconPath(iconPath, StringUtils::Format(IMAGE_OWNER_PATTERN, bRadio ? "radio" : "tv"))
+{
+ UpdateEncryptionName();
+}
+
+CPVRChannel::CPVRChannel(const PVR_CHANNEL& channel, unsigned int iClientId)
+ : m_bIsRadio(channel.bIsRadio),
+ m_bIsHidden(channel.bIsHidden),
+ m_iconPath(channel.strIconPath,
+ StringUtils::Format(IMAGE_OWNER_PATTERN, channel.bIsRadio ? "radio" : "tv")),
+ m_strChannelName(channel.strChannelName),
+ m_bHasArchive(channel.bHasArchive),
+ m_bEPGEnabled(!channel.bIsHidden),
+ m_iUniqueId(channel.iUniqueId),
+ m_iClientId(iClientId),
+ m_clientChannelNumber(channel.iChannelNumber, channel.iSubChannelNumber),
+ m_strClientChannelName(channel.strChannelName),
+ m_strMimeType(channel.strMimeType),
+ m_iClientEncryptionSystem(channel.iEncryptionSystem),
+ m_iClientOrder(channel.iOrder),
+ m_iClientProviderUid(channel.iClientProviderUid)
+{
+ if (m_strChannelName.empty())
+ m_strChannelName = StringUtils::Format("{} {}", g_localizeStrings.Get(19029), m_iUniqueId);
+
+ UpdateEncryptionName();
+}
+
+CPVRChannel::~CPVRChannel()
+{
+ ResetEPG();
+}
+
+void CPVRChannel::FillAddonData(PVR_CHANNEL& channel) const
+{
+ channel = {};
+ channel.iUniqueId = UniqueID();
+ channel.iChannelNumber = ClientChannelNumber().GetChannelNumber();
+ channel.iSubChannelNumber = ClientChannelNumber().GetSubChannelNumber();
+ strncpy(channel.strChannelName, ClientChannelName().c_str(), sizeof(channel.strChannelName) - 1);
+ strncpy(channel.strIconPath, ClientIconPath().c_str(), sizeof(channel.strIconPath) - 1);
+ channel.iEncryptionSystem = EncryptionSystem();
+ channel.bIsRadio = IsRadio();
+ channel.bIsHidden = IsHidden();
+ strncpy(channel.strMimeType, MimeType().c_str(), sizeof(channel.strMimeType) - 1);
+ channel.iClientProviderUid = ClientProviderUid();
+ channel.bHasArchive = HasArchive();
+}
+
+void CPVRChannel::Serialize(CVariant& value) const
+{
+ value["channelid"] = m_iChannelId;
+ value["channeltype"] = m_bIsRadio ? "radio" : "tv";
+ value["hidden"] = m_bIsHidden;
+ value["locked"] = m_bIsLocked;
+ value["icon"] = ClientIconPath();
+ value["channel"] = m_strChannelName;
+ value["uniqueid"] = m_iUniqueId;
+ CDateTime lastPlayed(m_iLastWatched);
+ value["lastplayed"] = lastPlayed.IsValid() ? lastPlayed.GetAsDBDate() : "";
+
+ std::shared_ptr<CPVREpgInfoTag> epg = GetEPGNow();
+ if (epg)
+ {
+ // add the properties of the current EPG item to the main object
+ epg->Serialize(value);
+ // and add an extra sub-object with only the current EPG details
+ epg->Serialize(value["broadcastnow"]);
+ }
+
+ epg = GetEPGNext();
+ if (epg)
+ epg->Serialize(value["broadcastnext"]);
+
+ value["hasarchive"] = m_bHasArchive;
+ value["clientid"] = m_iClientId;
+}
+
+bool CPVRChannel::QueueDelete()
+{
+ bool bReturn = false;
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (!database)
+ return bReturn;
+
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ ResetEPG();
+
+ bReturn = database->QueueDeleteQuery(*this);
+ return bReturn;
+}
+
+std::shared_ptr<CPVREpg> CPVRChannel::GetEPG() const
+{
+ const_cast<CPVRChannel*>(this)->CreateEPG();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_bIsHidden && m_bEPGEnabled)
+ return m_epg;
+
+ return {};
+}
+
+bool CPVRChannel::CreateEPG()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_epg)
+ {
+ m_epg = CServiceBroker::GetPVRManager().EpgContainer().CreateChannelEpg(
+ m_iEpgId, m_strEPGScraper, std::make_shared<CPVREpgChannelData>(*this));
+ if (m_epg)
+ {
+ if (m_epg->EpgID() != m_iEpgId)
+ {
+ m_iEpgId = m_epg->EpgID();
+ m_bChanged = true;
+ }
+
+ // Subscribe for EPG delete event
+ m_epg->Events().Subscribe(this, &CPVRChannel::Notify);
+ return true;
+ }
+ }
+ return false;
+}
+
+void CPVRChannel::Notify(const PVREvent& event)
+{
+ if (event == PVREvent::EpgDeleted)
+ {
+ ResetEPG();
+ }
+}
+
+void CPVRChannel::ResetEPG()
+{
+ std::shared_ptr<CPVREpg> epgToUnsubscribe;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_epg)
+ {
+ epgToUnsubscribe = m_epg;
+ m_epg.reset();
+ }
+ }
+
+ if (epgToUnsubscribe)
+ epgToUnsubscribe->Events().Unsubscribe(this);
+}
+
+bool CPVRChannel::UpdateFromClient(const std::shared_ptr<CPVRChannel>& channel)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ SetClientID(channel->ClientID());
+ SetArchive(channel->HasArchive());
+ SetClientProviderUid(channel->ClientProviderUid());
+
+ m_clientChannelNumber = channel->m_clientChannelNumber;
+ m_strMimeType = channel->MimeType();
+ m_iClientEncryptionSystem = channel->EncryptionSystem();
+ m_strClientChannelName = channel->ClientChannelName();
+
+ UpdateEncryptionName();
+
+ // only update the channel name, icon, and hidden flag if the user hasn't changed them manually
+ if (m_strChannelName.empty() || !IsUserSetName())
+ SetChannelName(channel->ClientChannelName());
+ if (IconPath().empty() || !IsUserSetIcon())
+ SetIconPath(channel->ClientIconPath());
+ if (!IsUserSetHidden())
+ SetHidden(channel->IsHidden());
+
+ return m_bChanged;
+}
+
+bool CPVRChannel::Persist()
+{
+ {
+ // not changed
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_bChanged && m_iChannelId > 0)
+ return true;
+ }
+
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Persisting channel '{}'", m_strChannelName);
+
+ bool bReturn = database->Persist(*this, true);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bChanged = !bReturn;
+ return bReturn;
+ }
+
+ return false;
+}
+
+bool CPVRChannel::SetChannelID(int iChannelId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_iChannelId != iChannelId)
+ {
+ m_iChannelId = iChannelId;
+
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ epg->GetChannelData()->SetChannelId(m_iChannelId);
+
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRChannel::SetHidden(bool bIsHidden, bool bIsUserSetHidden /*= false*/)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bIsHidden != bIsHidden)
+ {
+ m_bIsHidden = bIsHidden;
+ m_bIsUserSetHidden = bIsUserSetHidden;
+
+ if (m_epg)
+ m_epg->GetChannelData()->SetHidden(m_bIsHidden);
+
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRChannel::SetLocked(bool bIsLocked)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bIsLocked != bIsLocked)
+ {
+ m_bIsLocked = bIsLocked;
+
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ epg->GetChannelData()->SetLocked(m_bIsLocked);
+
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+std::shared_ptr<CPVRRadioRDSInfoTag> CPVRChannel::GetRadioRDSInfoTag() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_rdsTag;
+}
+
+void CPVRChannel::SetRadioRDSInfoTag(const std::shared_ptr<CPVRRadioRDSInfoTag>& tag)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_rdsTag = tag;
+}
+
+bool CPVRChannel::HasArchive() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bHasArchive;
+}
+
+bool CPVRChannel::SetArchive(bool bHasArchive)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bHasArchive != bHasArchive)
+ {
+ m_bHasArchive = bHasArchive;
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRChannel::SetIconPath(const std::string& strIconPath, bool bIsUserSetIcon /* = false */)
+{
+ if (StringUtils::StartsWith(strIconPath, "image://"))
+ {
+ CLog::LogF(LOGERROR, "Not allowed to call this method with an image URL");
+ return false;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (ClientIconPath() == strIconPath)
+ return false;
+
+ m_iconPath.SetClientImage(strIconPath);
+
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ epg->GetChannelData()->SetChannelIconPath(strIconPath);
+
+ m_bChanged = true;
+ m_bIsUserSetIcon = bIsUserSetIcon && !IconPath().empty();
+ return true;
+}
+
+bool CPVRChannel::SetChannelName(const std::string& strChannelName, bool bIsUserSetName /*= false*/)
+{
+ std::string strName(strChannelName);
+
+ if (strName.empty())
+ strName = StringUtils::Format(g_localizeStrings.Get(19085),
+ m_clientChannelNumber.FormattedChannelNumber());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_strChannelName != strName)
+ {
+ m_strChannelName = strName;
+ m_bIsUserSetName = bIsUserSetName;
+
+ /* if the user changes the name manually to an empty string we reset the
+ flag and use the name from the client instead */
+ if (bIsUserSetName && strChannelName.empty())
+ {
+ m_bIsUserSetName = false;
+ m_strChannelName = ClientChannelName();
+ }
+
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ epg->GetChannelData()->SetChannelName(m_strChannelName);
+
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRChannel::SetLastWatched(time_t iLastWatched)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iLastWatched = iLastWatched;
+ }
+
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ return database->UpdateLastWatched(*this);
+
+ return false;
+}
+
+/********** Client related channel methods **********/
+
+bool CPVRChannel::SetClientID(int iClientId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iClientId != iClientId)
+ {
+ m_iClientId = iClientId;
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+std::string CPVRChannel::GetEncryptionName(int iCaid)
+{
+ // http://www.dvb.org/index.php?id=174
+ // http://en.wikipedia.org/wiki/Conditional_access_system
+ std::string strName(g_localizeStrings.Get(13205)); /* Unknown */
+
+ if (iCaid == 0x0000)
+ strName = g_localizeStrings.Get(19013); /* Free To Air */
+ else if (iCaid >= 0x0001 && iCaid <= 0x009F)
+ strName = g_localizeStrings.Get(19014); /* Fixed */
+ else if (iCaid >= 0x00A0 && iCaid <= 0x00A1)
+ strName = g_localizeStrings.Get(338); /* Analog */
+ else if (iCaid >= 0x00A2 && iCaid <= 0x00FF)
+ strName = g_localizeStrings.Get(19014); /* Fixed */
+ else if (iCaid >= 0x0100 && iCaid <= 0x01FF)
+ strName = "SECA Mediaguard";
+ else if (iCaid == 0x0464)
+ strName = "EuroDec";
+ else if (iCaid >= 0x0500 && iCaid <= 0x05FF)
+ strName = "Viaccess";
+ else if (iCaid >= 0x0600 && iCaid <= 0x06FF)
+ strName = "Irdeto";
+ else if (iCaid >= 0x0900 && iCaid <= 0x09FF)
+ strName = "NDS Videoguard";
+ else if (iCaid >= 0x0B00 && iCaid <= 0x0BFF)
+ strName = "Conax";
+ else if (iCaid >= 0x0D00 && iCaid <= 0x0DFF)
+ strName = "CryptoWorks";
+ else if (iCaid >= 0x0E00 && iCaid <= 0x0EFF)
+ strName = "PowerVu";
+ else if (iCaid == 0x1000)
+ strName = "RAS";
+ else if (iCaid >= 0x1200 && iCaid <= 0x12FF)
+ strName = "NagraVision";
+ else if (iCaid >= 0x1700 && iCaid <= 0x17FF)
+ strName = "BetaCrypt";
+ else if (iCaid >= 0x1800 && iCaid <= 0x18FF)
+ strName = "NagraVision";
+ else if (iCaid == 0x22F0)
+ strName = "Codicrypt";
+ else if (iCaid == 0x2600)
+ strName = "BISS";
+ else if (iCaid == 0x4347)
+ strName = "CryptOn";
+ else if (iCaid == 0x4800)
+ strName = "Accessgate";
+ else if (iCaid == 0x4900)
+ strName = "China Crypt";
+ else if (iCaid == 0x4A10)
+ strName = "EasyCas";
+ else if (iCaid == 0x4A20)
+ strName = "AlphaCrypt";
+ else if (iCaid == 0x4A70)
+ strName = "DreamCrypt";
+ else if (iCaid == 0x4A60)
+ strName = "SkyCrypt";
+ else if (iCaid == 0x4A61)
+ strName = "Neotioncrypt";
+ else if (iCaid == 0x4A62)
+ strName = "SkyCrypt";
+ else if (iCaid == 0x4A63)
+ strName = "Neotion SHL";
+ else if (iCaid >= 0x4A64 && iCaid <= 0x4A6F)
+ strName = "SkyCrypt";
+ else if (iCaid == 0x4A80)
+ strName = "ThalesCrypt";
+ else if (iCaid == 0x4AA1)
+ strName = "KeyFly";
+ else if (iCaid == 0x4ABF)
+ strName = "DG-Crypt";
+ else if (iCaid >= 0x4AD0 && iCaid <= 0x4AD1)
+ strName = "X-Crypt";
+ else if (iCaid == 0x4AD4)
+ strName = "OmniCrypt";
+ else if (iCaid == 0x4AE0)
+ strName = "RossCrypt";
+ else if (iCaid == 0x5500)
+ strName = "Z-Crypt";
+ else if (iCaid == 0x5501)
+ strName = "Griffin";
+ else if (iCaid == 0x5601)
+ strName = "Verimatrix";
+
+ if (iCaid >= 0)
+ strName += StringUtils::Format(" ({:04X})", iCaid);
+
+ return strName;
+}
+
+void CPVRChannel::UpdateEncryptionName()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strClientEncryptionName = GetEncryptionName(m_iClientEncryptionSystem);
+}
+
+bool CPVRChannel::SetClientProviderUid(int iClientProviderUid)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iClientProviderUid != iClientProviderUid)
+ {
+ m_iClientProviderUid = iClientProviderUid;
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+/********** EPG methods **********/
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVRChannel::GetEpgTags() const
+{
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (!epg)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Cannot get EPG for channel '{}'", m_strChannelName);
+ return {};
+ }
+
+ return epg->GetTags();
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVRChannel::GetEPGTimeline(
+ const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const
+{
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ {
+ return epg->GetTimeline(timelineStart, timelineEnd, minEventEnd, maxEventStart);
+ }
+ else
+ {
+ // return single gap tag spanning whole timeline
+ return std::vector<std::shared_ptr<CPVREpgInfoTag>>{
+ CreateEPGGapTag(timelineStart, timelineEnd)};
+ }
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRChannel::CreateEPGGapTag(const CDateTime& start,
+ const CDateTime& end) const
+{
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ return std::make_shared<CPVREpgInfoTag>(epg->GetChannelData(), epg->EpgID(), start, end, true);
+ else
+ return std::make_shared<CPVREpgInfoTag>(std::make_shared<CPVREpgChannelData>(*this), -1, start,
+ end, true);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRChannel::GetEPGNow() const
+{
+ std::shared_ptr<CPVREpgInfoTag> tag;
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ tag = epg->GetTagNow();
+
+ return tag;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRChannel::GetEPGNext() const
+{
+ std::shared_ptr<CPVREpgInfoTag> tag;
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ tag = epg->GetTagNext();
+
+ return tag;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRChannel::GetEPGPrevious() const
+{
+ std::shared_ptr<CPVREpgInfoTag> tag;
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ tag = epg->GetTagPrevious();
+
+ return tag;
+}
+
+bool CPVRChannel::SetEPGEnabled(bool bEPGEnabled)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bEPGEnabled != bEPGEnabled)
+ {
+ m_bEPGEnabled = bEPGEnabled;
+
+ if (m_epg)
+ {
+ m_epg->GetChannelData()->SetEPGEnabled(m_bEPGEnabled);
+
+ if (m_bEPGEnabled)
+ m_epg->ForceUpdate();
+ else
+ m_epg->Clear();
+ }
+
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRChannel::SetEPGScraper(const std::string& strScraper)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_strEPGScraper != strScraper)
+ {
+ bool bCleanEPG = !m_strEPGScraper.empty() || strScraper.empty();
+
+ m_strEPGScraper = strScraper;
+
+ if (bCleanEPG && m_epg)
+ m_epg->Clear();
+
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+void CPVRChannel::ToSortable(SortItem& sortable, Field field) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (field == FieldChannelName)
+ sortable[FieldChannelName] = m_strChannelName;
+ else if (field == FieldLastPlayed)
+ {
+ const CDateTime lastWatched(m_iLastWatched);
+ sortable[FieldLastPlayed] =
+ lastWatched.IsValid() ? lastWatched.GetAsDBDateTime() : StringUtils::Empty;
+ }
+ else if (field == FieldProvider)
+ sortable[FieldProvider] = StringUtils::Format("{} {}", m_iClientId, m_iClientProviderUid);
+}
+
+int CPVRChannel::ChannelID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iChannelId;
+}
+
+bool CPVRChannel::IsNew() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iChannelId <= 0;
+}
+
+bool CPVRChannel::IsHidden() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsHidden;
+}
+
+bool CPVRChannel::IsLocked() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsLocked;
+}
+
+std::string CPVRChannel::ClientIconPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iconPath.GetClientImage();
+}
+
+std::string CPVRChannel::IconPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iconPath.GetLocalImage();
+}
+
+bool CPVRChannel::IsUserSetIcon() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsUserSetIcon;
+}
+
+bool CPVRChannel::IsUserSetName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsUserSetName;
+}
+
+bool CPVRChannel::IsUserSetHidden() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsUserSetHidden;
+}
+
+std::string CPVRChannel::ChannelName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strChannelName;
+}
+
+time_t CPVRChannel::LastWatched() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iLastWatched;
+}
+
+bool CPVRChannel::IsChanged() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bChanged;
+}
+
+void CPVRChannel::Persisted()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bChanged = false;
+}
+
+int CPVRChannel::UniqueID() const
+{
+ return m_iUniqueId;
+}
+
+int CPVRChannel::ClientID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientId;
+}
+
+const CPVRChannelNumber& CPVRChannel::ClientChannelNumber() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_clientChannelNumber;
+}
+
+std::string CPVRChannel::ClientChannelName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strClientChannelName;
+}
+
+std::string CPVRChannel::MimeType() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strMimeType;
+}
+
+bool CPVRChannel::IsEncrypted() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientEncryptionSystem > 0;
+}
+
+int CPVRChannel::EncryptionSystem() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientEncryptionSystem;
+}
+
+std::string CPVRChannel::EncryptionName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strClientEncryptionName;
+}
+
+int CPVRChannel::EpgID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iEpgId;
+}
+
+bool CPVRChannel::EPGEnabled() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bEPGEnabled;
+}
+
+std::string CPVRChannel::EPGScraper() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strEPGScraper;
+}
+
+bool CPVRChannel::CanRecord() const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ return client && client->GetClientCapabilities().SupportsRecordings() &&
+ client->GetClientCapabilities().SupportsTimers();
+}
+
+std::shared_ptr<CPVRProvider> CPVRChannel::GetDefaultProvider() const
+{
+ return CServiceBroker::GetPVRManager().Providers()->GetByClient(m_iClientId,
+ PVR_PROVIDER_INVALID_UID);
+}
+
+bool CPVRChannel::HasClientProvider() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientProviderUid != PVR_PROVIDER_INVALID_UID;
+}
+
+std::shared_ptr<CPVRProvider> CPVRChannel::GetProvider() const
+{
+ auto provider =
+ CServiceBroker::GetPVRManager().Providers()->GetByClient(m_iClientId, m_iClientProviderUid);
+
+ if (!provider)
+ provider = GetDefaultProvider();
+
+ return provider;
+}
diff --git a/xbmc/pvr/channels/PVRChannel.h b/xbmc/pvr/channels/PVRChannel.h
new file mode 100644
index 0000000..8ff5054
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannel.h
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_providers.h"
+#include "pvr/PVRCachedImage.h"
+#include "pvr/channels/PVRChannelNumber.h"
+#include "threads/CriticalSection.h"
+#include "utils/ISerializable.h"
+#include "utils/ISortable.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CDateTime;
+
+namespace PVR
+{
+enum class PVREvent;
+
+class CPVRProvider;
+class CPVREpg;
+class CPVREpgInfoTag;
+class CPVRRadioRDSInfoTag;
+
+class CPVRChannel : public ISerializable, public ISortable
+{
+ friend class CPVRDatabase;
+
+public:
+ static const std::string IMAGE_OWNER_PATTERN;
+
+ explicit CPVRChannel(bool bRadio);
+ CPVRChannel(bool bRadio, const std::string& iconPath);
+ CPVRChannel(const PVR_CHANNEL& channel, unsigned int iClientId);
+
+ virtual ~CPVRChannel();
+
+ bool operator==(const CPVRChannel& right) const;
+ bool operator!=(const CPVRChannel& right) const;
+
+ /*!
+ * @brief Copy over data to the given PVR_CHANNEL instance.
+ * @param channel The channel instance to fill.
+ */
+ void FillAddonData(PVR_CHANNEL& channel) const;
+
+ void Serialize(CVariant& value) const override;
+
+ /*! @name Kodi related channel methods
+ */
+ //@{
+
+ /*!
+ * @brief Delete this channel from the database.
+ * @return True if it was deleted successfully, false otherwise.
+ */
+ bool QueueDelete();
+
+ /*!
+ * @brief Update this channel tag with the data of the given channel tag.
+ * @param channel The new channel data.
+ * @return True if something changed, false otherwise.
+ */
+ bool UpdateFromClient(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Persists the changes in the database.
+ * @return True if the changes were saved successfully, false otherwise.
+ */
+ bool Persist();
+
+ /*!
+ * @return The identifier given to this channel by the TV database.
+ */
+ int ChannelID() const;
+
+ /*!
+ * @return True when not persisted yet, false otherwise.
+ */
+ bool IsNew() const;
+
+ /*!
+ * @brief Set the identifier for this channel.
+ * @param iDatabaseId The new channel ID
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetChannelID(int iDatabaseId);
+
+ /*!
+ * @return True if this channel is a radio channel, false if not.
+ */
+ bool IsRadio() const { return m_bIsRadio; }
+
+ /*!
+ * @return True if this channel is hidden. False if not.
+ */
+ bool IsHidden() const;
+
+ /*!
+ * @brief Set to true to hide this channel. Set to false to unhide it.
+ *
+ * Set to true to hide this channel. Set to false to unhide it.
+ * The EPG of hidden channels won't be updated.
+ * @param bIsHidden The new setting.
+ * @param bIsUserSetIcon true if user changed the hidden flag via GUI, false otherwise.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetHidden(bool bIsHidden, bool bIsUserSetHidden = false);
+
+ /*!
+ * @return True if this channel is locked. False if not.
+ */
+ bool IsLocked() const;
+
+ /*!
+ * @brief Set to true to lock this channel. Set to false to unlock it.
+ *
+ * Set to true to lock this channel. Set to false to unlock it.
+ * Locked channels need can only be viewed if parental PIN entered.
+ * @param bIsLocked The new setting.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetLocked(bool bIsLocked);
+
+ /*!
+ * @brief Obtain the Radio RDS data for this channel, if available.
+ * @return The Radio RDS data or nullptr.
+ */
+ std::shared_ptr<CPVRRadioRDSInfoTag> GetRadioRDSInfoTag() const;
+
+ /*!
+ * @brief Set the Radio RDS data for the channel.
+ * @param tag The RDS data.
+ */
+ void SetRadioRDSInfoTag(const std::shared_ptr<CPVRRadioRDSInfoTag>& tag);
+
+ /*!
+ * @return True if this channel has archive support, false otherwise
+ */
+ bool HasArchive() const;
+
+ /*!
+ * @brief Set the archive support flag for this channel.
+ * @param bHasArchive True to set the flag, false to reset.
+ * @return True if the flag was changed, false otherwise.
+ */
+ bool SetArchive(bool bHasArchive);
+
+ /*!
+ * @return The path to the icon for this channel as given by the client.
+ */
+ std::string ClientIconPath() const;
+
+ /*!
+ * @return The path to the icon for this channel.
+ */
+ std::string IconPath() const;
+
+ /*!
+ * @return True if this user changed icon via GUI. False if not.
+ */
+ bool IsUserSetIcon() const;
+
+ /*!
+ * @return whether the user has changed the channel name through the GUI
+ */
+ bool IsUserSetName() const;
+
+ /*!
+ * @return True if user changed the hidden flag via GUI, False if not
+ */
+ bool IsUserSetHidden() const;
+
+ /*!
+ * @brief Set the path to the icon for this channel.
+ * @param strIconPath The new path.
+ * @param bIsUserSetIcon true if user changed the icon via GUI, false otherwise.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetIconPath(const std::string& strIconPath, bool bIsUserSetIcon = false);
+
+ /*!
+ * @return The name for this channel used by XBMC.
+ */
+ std::string ChannelName() const;
+
+ /*!
+ * @brief Set the name for this channel used by XBMC.
+ * @param strChannelName The new channel name.
+ * @param bIsUserSetName whether the change was triggered by the user directly
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetChannelName(const std::string& strChannelName, bool bIsUserSetName = false);
+
+ /*!
+ * @return Time channel has been watched last.
+ */
+ time_t LastWatched() const;
+
+ /*!
+ * @brief Last time channel has been watched
+ * @param iLastWatched The new value.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetLastWatched(time_t iLastWatched);
+
+ /*!
+ * @brief Check whether this channel has unpersisted data changes.
+ * @return True if this channel has changes to persist, false otherwise
+ */
+ bool IsChanged() const;
+
+ /*!
+ * @brief reset changed flag after persist
+ */
+ void Persisted();
+ //@}
+
+ /*! @name Client related channel methods
+ */
+ //@{
+
+ /*!
+ * @brief A unique identifier for this channel.
+ *
+ * A unique identifier for this channel.
+ * It can be used to find the same channel on different providers
+ *
+ * @return The Unique ID.
+ */
+ int UniqueID() const;
+
+ /*!
+ * @return The identifier of the client that serves this channel.
+ */
+ int ClientID() const;
+
+ /*!
+ * @brief Set the identifier of the client that serves this channel.
+ * @param iClientId The new ID.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetClientID(int iClientId);
+
+ /*!
+ * Get the channel number on the client.
+ * @return The channel number on the client.
+ */
+ const CPVRChannelNumber& ClientChannelNumber() const;
+
+ /*!
+ * @return The name of this channel on the client.
+ */
+ std::string ClientChannelName() const;
+
+ /*!
+ * @brief The stream input mime type
+ *
+ * The stream input type
+ * If it is empty, ffmpeg will try to scan the stream to find the right input format.
+ * See https://www.iana.org/assignments/media-types/media-types.xhtml for a
+ * list of the input formats.
+ *
+ * @return The stream input type
+ */
+ std::string MimeType() const;
+
+ // ISortable implementation
+ void ToSortable(SortItem& sortable, Field field) const override;
+
+ /*!
+ * @return Storage id for this channel in CPVRChannelGroup
+ */
+ std::pair<int, int> StorageId() const { return std::make_pair(m_iClientId, m_iUniqueId); }
+
+ /*!
+ * @brief Return true if this channel is encrypted.
+ *
+ * Return true if this channel is encrypted. Does not inform whether XBMC can play the file.
+ * Decryption should be done by the client.
+ *
+ * @return Return true if this channel is encrypted.
+ */
+ bool IsEncrypted() const;
+
+ /*!
+ * @brief Return the encryption system ID for this channel. 0 for FTA.
+ *
+ * Return the encryption system ID for this channel. 0 for FTA.
+ * The values are documented on: http://www.dvb.org/index.php?id=174.
+ *
+ * @return Return the encryption system ID for this channel.
+ */
+ int EncryptionSystem() const;
+
+ /*!
+ * @return A friendly name for the used encryption system.
+ */
+ std::string EncryptionName() const;
+ //@}
+
+ /*! @name EPG methods
+ */
+ //@{
+
+ /*!
+ * @return The ID of the EPG table to use for this channel or -1 if it isn't set.
+ */
+ int EpgID() const;
+
+ /*!
+ * @brief Create the EPG for this channel, if it does not yet exist
+ * @return true if a new epg was created, false otherwise.
+ */
+ bool CreateEPG();
+
+ /*!
+ * @brief Get the EPG table for this channel.
+ * @return The EPG for this channel.
+ */
+ std::shared_ptr<CPVREpg> GetEPG() const;
+
+ /*!
+ * @brief Get the EPG tags for this channel.
+ * @return The tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTags() const;
+
+ /*!
+ * @brief Get all EPG tags for the given time frame, including "gap" tags.
+ * @param timelineStart Start of time line.
+ * @param timelineEnd End of time line.
+ * @param minEventEnd The minimum end time of the events to return.
+ * @param maxEventStart The maximum start time of the events to return.
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEPGTimeline(const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const;
+
+ /*!
+ * @brief Create a "gap" EPG tag.
+ * @param start Start of gap.
+ * @param end End of gap.
+ * @return The tag.
+ */
+ std::shared_ptr<CPVREpgInfoTag> CreateEPGGapTag(const CDateTime& start,
+ const CDateTime& end) const;
+
+ /*!
+ * @brief Get the EPG tag that is now active on this channel.
+ *
+ * Get the EPG tag that is now active on this channel.
+ * Will return an empty tag if there is none.
+ *
+ * @return The EPG tag that is now active.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEPGNow() const;
+
+ /*!
+ * @brief Get the EPG tag that was previously active on this channel.
+ *
+ * Get the EPG tag that was previously active on this channel.
+ * Will return an empty tag if there is none.
+ *
+ * @return The EPG tag that was previously active.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEPGPrevious() const;
+
+ /*!
+ * @brief Get the EPG tag that will be next active on this channel.
+ *
+ * Get the EPG tag that will be next active on this channel.
+ * Will return an empty tag if there is none.
+ *
+ * @return The EPG tag that will be next active.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEPGNext() const;
+
+ /*!
+ * @return Don't use an EPG for this channel if set to false.
+ */
+ bool EPGEnabled() const;
+
+ /*!
+ * @brief Set to true if an EPG should be used for this channel. Set to false otherwise.
+ * @param bEPGEnabled The new value.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetEPGEnabled(bool bEPGEnabled);
+
+ /*!
+ * @brief Get the name of the scraper to be used for this channel.
+ *
+ * Get the name of the scraper to be used for this channel.
+ * The default is 'client', which means the EPG should be loaded from the backend.
+ *
+ * @return The name of the scraper to be used for this channel.
+ */
+ std::string EPGScraper() const;
+
+ /*!
+ * @brief Set the name of the scraper to be used for this channel.
+ *
+ * Set the name of the scraper to be used for this channel.
+ * Set to "client" to load the EPG from the backend
+ *
+ * @param strScraper The new scraper name.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetEPGScraper(const std::string& strScraper);
+
+ bool CanRecord() const;
+
+ static std::string GetEncryptionName(int iCaid);
+
+ /*!
+ * @brief Get the client order for this channel
+ * @return iOrder The order for this channel
+ */
+ int ClientOrder() const { return m_iClientOrder; }
+
+ /*!
+ * @brief Get the client provider Uid for this channel
+ * @return m_iClientProviderUid The provider Uid for this channel
+ */
+ int ClientProviderUid() const { return m_iClientProviderUid; }
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+
+ /*!
+ * @brief Lock the instance. No other thread gets access to this channel until Unlock was called.
+ */
+ void Lock() { m_critSection.lock(); }
+
+ /*!
+ * @brief Unlock the instance. Other threads may get access to this channel again.
+ */
+ void Unlock() { m_critSection.unlock(); }
+
+ /*!
+ * @brief Get the default provider of this channel. The default
+ * provider represents the PVR add-on itself.
+ * @return The default provider of this channel
+ */
+ std::shared_ptr<CPVRProvider> GetDefaultProvider() const;
+
+ /*!
+ * @brief Whether or not this channel has a provider set by the client.
+ * @return True if a provider was set by the client, false otherwise.
+ */
+ bool HasClientProvider() const;
+
+ /*!
+ * @brief Get the provider of this channel. This may be the default provider or a
+ * custom provider set by the client. If @ref "HasClientProvider()" returns true
+ * the provider will be custom from the client, otherwise the default provider.
+ * @return The provider of this channel
+ */
+ std::shared_ptr<CPVRProvider> GetProvider() const;
+
+ //@}
+private:
+ CPVRChannel() = delete;
+ CPVRChannel(const CPVRChannel& tag) = delete;
+ CPVRChannel& operator=(const CPVRChannel& channel) = delete;
+
+ /*!
+ * @brief Update the encryption name after SetEncryptionSystem() has been called.
+ */
+ void UpdateEncryptionName();
+
+ /*!
+ * @brief Reset the EPG instance pointer.
+ */
+ void ResetEPG();
+
+ /*!
+ * @brief Set the client provider Uid for this channel
+ * @param iClientProviderUid The provider Uid for this channel
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetClientProviderUid(int iClientProviderUid);
+
+ /*! @name Kodi related channel data
+ */
+ //@{
+ int m_iChannelId = -1; /*!< the identifier given to this channel by the TV database */
+ bool m_bIsRadio = false; /*!< true if this channel is a radio channel, false if not */
+ bool m_bIsHidden = false; /*!< true if this channel is hidden, false if not */
+ bool m_bIsUserSetName = false; /*!< true if user set the channel name via GUI, false if not */
+ bool m_bIsUserSetIcon = false; /*!< true if user set the icon via GUI, false if not */
+ bool m_bIsUserSetHidden = false; /*!< true if user set the hidden flag via GUI, false if not */
+ bool m_bIsLocked = false; /*!< true if channel is locked, false if not */
+ CPVRCachedImage m_iconPath; /*!< the path to the icon for this channel */
+ std::string m_strChannelName; /*!< the name for this channel used by Kodi */
+ time_t m_iLastWatched = 0; /*!< last time channel has been watched */
+ bool m_bChanged =
+ false; /*!< true if anything in this entry was changed that needs to be persisted */
+ std::shared_ptr<CPVRRadioRDSInfoTag>
+ m_rdsTag; /*! < the radio rds data, if available for the channel. */
+ bool m_bHasArchive = false; /*!< true if this channel supports archive */
+ //@}
+
+ /*! @name EPG related channel data
+ */
+ //@{
+ int m_iEpgId = -1; /*!< the id of the EPG for this channel */
+ bool m_bEPGEnabled = false; /*!< don't use an EPG for this channel if set to false */
+ std::string m_strEPGScraper =
+ "client"; /*!< the name of the scraper to be used for this channel */
+ std::shared_ptr<CPVREpg> m_epg;
+ //@}
+
+ /*! @name Client related channel data
+ */
+ //@{
+ int m_iUniqueId = -1; /*!< the unique identifier for this channel */
+ int m_iClientId = -1; /*!< the identifier of the client that serves this channel */
+ CPVRChannelNumber m_clientChannelNumber; /*!< the channel number on the client */
+ std::string m_strClientChannelName; /*!< the name of this channel on the client */
+ std::string
+ m_strMimeType; /*!< the stream input type based mime type, see @ref https://www.iana.org/assignments/media-types/media-types.xhtml#video */
+ int m_iClientEncryptionSystem =
+ -1; /*!< the encryption system used by this channel. 0 for FreeToAir, -1 for unknown */
+ std::string
+ m_strClientEncryptionName; /*!< the name of the encryption system used by this channel */
+ int m_iClientOrder = 0; /*!< the order from this channels group member */
+ int m_iClientProviderUid =
+ PVR_PROVIDER_INVALID_UID; /*!< the unique id for this provider from the client */
+ //@}
+
+ mutable CCriticalSection m_critSection;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/channels/PVRChannelGroup.cpp b/xbmc/pvr/channels/PVRChannelGroup.cpp
new file mode 100644
index 0000000..7f03430
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroup.cpp
@@ -0,0 +1,1176 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+//! @todo use Observable here, so we can use event driven operations later
+
+#include "PVRChannelGroup.h"
+
+#include "ServiceBroker.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channel_groups.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+CPVRChannelGroup::CPVRChannelGroup(const CPVRChannelsPath& path,
+ const std::shared_ptr<CPVRChannelGroup>& allChannelsGroup)
+ : m_allChannelsGroup(allChannelsGroup), m_path(path)
+{
+ GetSettings()->RegisterCallback(this);
+}
+
+CPVRChannelGroup::CPVRChannelGroup(const PVR_CHANNEL_GROUP& group,
+ const std::shared_ptr<CPVRChannelGroup>& allChannelsGroup)
+ : m_iPosition(group.iPosition),
+ m_allChannelsGroup(allChannelsGroup),
+ m_path(group.bIsRadio, group.strGroupName)
+{
+ GetSettings()->RegisterCallback(this);
+}
+
+CPVRChannelGroup::~CPVRChannelGroup()
+{
+ GetSettings()->UnregisterCallback(this);
+}
+
+bool CPVRChannelGroup::operator==(const CPVRChannelGroup& right) const
+{
+ return (m_iGroupType == right.m_iGroupType && m_iGroupId == right.m_iGroupId &&
+ m_iPosition == right.m_iPosition && m_path == right.m_path);
+}
+
+bool CPVRChannelGroup::operator!=(const CPVRChannelGroup& right) const
+{
+ return !(*this == right);
+}
+
+void CPVRChannelGroup::FillAddonData(PVR_CHANNEL_GROUP& group) const
+{
+ group = {};
+ group.bIsRadio = IsRadio();
+ strncpy(group.strGroupName, GroupName().c_str(), sizeof(group.strGroupName) - 1);
+ group.iPosition = GetPosition();
+}
+
+CCriticalSection CPVRChannelGroup::m_settingsSingletonCritSection;
+std::weak_ptr<CPVRChannelGroupSettings> CPVRChannelGroup::m_settingsSingleton;
+
+std::shared_ptr<CPVRChannelGroupSettings> CPVRChannelGroup::GetSettings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_settings)
+ {
+ std::unique_lock<CCriticalSection> singletonLock(m_settingsSingletonCritSection);
+ const std::shared_ptr<CPVRChannelGroupSettings> settings = m_settingsSingleton.lock();
+ if (settings)
+ {
+ m_settings = settings;
+ }
+ else
+ {
+ m_settings = std::make_shared<CPVRChannelGroupSettings>();
+ m_settingsSingleton = m_settings;
+ }
+ }
+ return m_settings;
+}
+
+bool CPVRChannelGroup::LoadFromDatabase(
+ const std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& channels,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ const int iChannelCount = m_iGroupId > 0 ? LoadFromDatabase(clients) : 0;
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Fetched {} {} group members from the database for group '{}'",
+ iChannelCount, IsRadio() ? "radio" : "TV", GroupName());
+
+ for (const auto& groupMember : m_members)
+ {
+ if (groupMember.second->Channel())
+ continue;
+
+ auto channel = channels.find(groupMember.first);
+ if (channel == channels.end())
+ {
+ CLog::Log(LOGERROR, "Cannot find group member '{},{}' in channels!", groupMember.first.first,
+ groupMember.first.second);
+ // No workaround here, please. We need to find and fix the root cause of this case!
+ }
+ groupMember.second->SetChannel((*channel).second);
+ }
+
+ m_bLoaded = true;
+ return true;
+}
+
+void CPVRChannelGroup::Unload()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_sortedMembers.clear();
+ m_members.clear();
+ m_failedClients.clear();
+}
+
+bool CPVRChannelGroup::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ if (GroupType() == PVR_GROUP_TYPE_USER_DEFINED || !GetSettings()->SyncChannelGroups())
+ return true;
+
+ // get the channel group members from the backends.
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers;
+ CServiceBroker::GetPVRManager().Clients()->GetChannelGroupMembers(clients, this, groupMembers,
+ m_failedClients);
+ return UpdateGroupEntries(groupMembers);
+}
+
+const CPVRChannelsPath& CPVRChannelGroup::GetPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_path;
+}
+
+void CPVRChannelGroup::SetPath(const CPVRChannelsPath& path)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_path != path)
+ {
+ m_path = path;
+ if (m_bLoaded)
+ {
+ // note: path contains both the radio flag and the group name, which are stored in the db
+ m_bChanged = true;
+ Persist(); //! @todo why must we persist immediately?
+ }
+ }
+}
+
+bool CPVRChannelGroup::SetChannelNumber(const std::shared_ptr<CPVRChannel>& channel,
+ const CPVRChannelNumber& channelNumber)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it =
+ std::find_if(m_sortedMembers.cbegin(), m_sortedMembers.cend(),
+ [&channel](const auto& member) { return *member->Channel() == *channel; });
+
+ if (it != m_sortedMembers.cend() && (*it)->ChannelNumber() != channelNumber)
+ {
+ (*it)->SetChannelNumber(channelNumber);
+ return true;
+ }
+
+ return false;
+}
+
+/********** sort methods **********/
+
+struct sortByClientChannelNumber
+{
+ bool operator()(const std::shared_ptr<CPVRChannelGroupMember>& channel1,
+ const std::shared_ptr<CPVRChannelGroupMember>& channel2) const
+ {
+ if (channel1->ClientPriority() == channel2->ClientPriority())
+ {
+ if (channel1->ClientChannelNumber() == channel2->ClientChannelNumber())
+ return channel1->Channel()->ChannelName() < channel2->Channel()->ChannelName();
+
+ return channel1->ClientChannelNumber() < channel2->ClientChannelNumber();
+ }
+ return channel1->ClientPriority() > channel2->ClientPriority();
+ }
+};
+
+struct sortByChannelNumber
+{
+ bool operator()(const std::shared_ptr<CPVRChannelGroupMember>& channel1,
+ const std::shared_ptr<CPVRChannelGroupMember>& channel2) const
+ {
+ return channel1->ChannelNumber() < channel2->ChannelNumber();
+ }
+};
+
+void CPVRChannelGroup::Sort()
+{
+ if (GetSettings()->UseBackendChannelOrder())
+ SortByClientChannelNumber();
+ else
+ SortByChannelNumber();
+}
+
+bool CPVRChannelGroup::SortAndRenumber()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ Sort();
+ return Renumber();
+}
+
+void CPVRChannelGroup::SortByClientChannelNumber()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::sort(m_sortedMembers.begin(), m_sortedMembers.end(), sortByClientChannelNumber());
+}
+
+void CPVRChannelGroup::SortByChannelNumber()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::sort(m_sortedMembers.begin(), m_sortedMembers.end(), sortByChannelNumber());
+}
+
+bool CPVRChannelGroup::UpdateClientPriorities()
+{
+ const std::shared_ptr<CPVRClients> clients = CServiceBroker::GetPVRManager().Clients();
+ bool bChanged = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const bool bUseBackendChannelOrder = GetSettings()->UseBackendChannelOrder();
+ for (auto& member : m_sortedMembers)
+ {
+ int iNewPriority = 0;
+
+ if (bUseBackendChannelOrder)
+ {
+ const std::shared_ptr<CPVRClient> client =
+ clients->GetCreatedClient(member->Channel()->ClientID());
+ if (!client)
+ continue;
+
+ iNewPriority = client->GetPriority();
+ }
+ else
+ {
+ iNewPriority = 0;
+ }
+
+ bChanged |= (member->ClientPriority() != iNewPriority);
+ member->SetClientPriority(iNewPriority);
+ }
+
+ return bChanged;
+}
+
+/********** getters **********/
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetByUniqueID(
+ const std::pair<int, int>& id) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = m_members.find(id);
+ return it != m_members.end() ? it->second : std::shared_ptr<CPVRChannelGroupMember>();
+}
+
+std::shared_ptr<CPVRChannel> CPVRChannelGroup::GetByUniqueID(int iUniqueChannelId,
+ int iClientID) const
+{
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ GetByUniqueID(std::make_pair(iClientID, iUniqueChannelId));
+ return groupMember ? groupMember->Channel() : std::shared_ptr<CPVRChannel>();
+}
+
+std::shared_ptr<CPVRChannel> CPVRChannelGroup::GetByChannelID(int iChannelID) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it =
+ std::find_if(m_members.cbegin(), m_members.cend(), [iChannelID](const auto& member) {
+ return member.second->Channel()->ChannelID() == iChannelID;
+ });
+ return it != m_members.cend() ? (*it).second->Channel() : std::shared_ptr<CPVRChannel>();
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetLastPlayedChannelGroupMember(
+ int iCurrentChannel /* = -1 */) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ std::shared_ptr<CPVRChannelGroupMember> groupMember;
+ for (const auto& memberPair : m_members)
+ {
+ const std::shared_ptr<CPVRChannel> channel = memberPair.second->Channel();
+ if (channel->ChannelID() != iCurrentChannel &&
+ CServiceBroker::GetPVRManager().Clients()->IsCreatedClient(channel->ClientID()) &&
+ channel->LastWatched() > 0 &&
+ (!groupMember || channel->LastWatched() > groupMember->Channel()->LastWatched()))
+ {
+ groupMember = memberPair.second;
+ }
+ }
+
+ return groupMember;
+}
+
+GroupMemberPair CPVRChannelGroup::GetLastAndPreviousToLastPlayedChannelGroupMember() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_sortedMembers.empty())
+ return {};
+
+ auto members = m_sortedMembers;
+ lock.unlock();
+
+ std::sort(members.begin(), members.end(), [](const auto& a, const auto& b) {
+ return a->Channel()->LastWatched() > b->Channel()->LastWatched();
+ });
+
+ std::shared_ptr<CPVRChannelGroupMember> last;
+ std::shared_ptr<CPVRChannelGroupMember> previousToLast;
+ if (members[0]->Channel()->LastWatched())
+ {
+ last = members[0];
+ if (members.size() > 1 && members[1]->Channel()->LastWatched())
+ previousToLast = members[1];
+ }
+
+ return {last, previousToLast};
+}
+
+CPVRChannelNumber CPVRChannelGroup::GetChannelNumber(
+ const std::shared_ptr<CPVRChannel>& channel) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRChannelGroupMember> member = GetByUniqueID(channel->StorageId());
+ return member ? member->ChannelNumber() : CPVRChannelNumber();
+}
+
+CPVRChannelNumber CPVRChannelGroup::GetClientChannelNumber(
+ const std::shared_ptr<CPVRChannel>& channel) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRChannelGroupMember> member = GetByUniqueID(channel->StorageId());
+ return member ? member->ClientChannelNumber() : CPVRChannelNumber();
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetByChannelNumber(
+ const CPVRChannelNumber& channelNumber) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const bool bUseBackendChannelNumbers = GetSettings()->UseBackendChannelNumbers();
+ for (const auto& member : m_sortedMembers)
+ {
+ CPVRChannelNumber activeChannelNumber =
+ bUseBackendChannelNumbers ? member->ClientChannelNumber() : member->ChannelNumber();
+ if (activeChannelNumber == channelNumber)
+ return member;
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetNextChannelGroupMember(
+ const std::shared_ptr<CPVRChannelGroupMember>& groupMember) const
+{
+ std::shared_ptr<CPVRChannelGroupMember> nextMember;
+
+ if (groupMember)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (auto it = m_sortedMembers.cbegin(); !nextMember && it != m_sortedMembers.cend(); ++it)
+ {
+ if (*it == groupMember)
+ {
+ do
+ {
+ if ((++it) == m_sortedMembers.cend())
+ it = m_sortedMembers.cbegin();
+ if ((*it)->Channel() && !(*it)->Channel()->IsHidden())
+ nextMember = *it;
+ } while (!nextMember && *it != groupMember);
+
+ break;
+ }
+ }
+ }
+
+ return nextMember;
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetPreviousChannelGroupMember(
+ const std::shared_ptr<CPVRChannelGroupMember>& groupMember) const
+{
+ std::shared_ptr<CPVRChannelGroupMember> previousMember;
+
+ if (groupMember)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (auto it = m_sortedMembers.crbegin(); !previousMember && it != m_sortedMembers.crend();
+ ++it)
+ {
+ if (*it == groupMember)
+ {
+ do
+ {
+ if ((++it) == m_sortedMembers.crend())
+ it = m_sortedMembers.crbegin();
+ if ((*it)->Channel() && !(*it)->Channel()->IsHidden())
+ previousMember = *it;
+ } while (!previousMember && *it != groupMember);
+
+ break;
+ }
+ }
+ }
+ return previousMember;
+}
+
+std::vector<std::shared_ptr<CPVRChannelGroupMember>> CPVRChannelGroup::GetMembers(
+ Include eFilter /* = Include::ALL */) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (eFilter == Include::ALL)
+ return m_sortedMembers;
+
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> members;
+ for (const auto& member : m_sortedMembers)
+ {
+ switch (eFilter)
+ {
+ case Include::ONLY_HIDDEN:
+ if (!member->Channel()->IsHidden())
+ continue;
+ break;
+ case Include::ONLY_VISIBLE:
+ if (member->Channel()->IsHidden())
+ continue;
+ break;
+ default:
+ break;
+ }
+
+ members.emplace_back(member);
+ }
+
+ return members;
+}
+
+void CPVRChannelGroup::GetChannelNumbers(std::vector<std::string>& channelNumbers) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const bool bUseBackendChannelNumbers = GetSettings()->UseBackendChannelNumbers();
+ for (const auto& member : m_sortedMembers)
+ {
+ CPVRChannelNumber activeChannelNumber =
+ bUseBackendChannelNumbers ? member->ClientChannelNumber() : member->ChannelNumber();
+ channelNumbers.emplace_back(activeChannelNumber.FormattedChannelNumber());
+ }
+}
+
+int CPVRChannelGroup::LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase());
+ if (!database)
+ return -1;
+
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> results =
+ database->Get(*this, clients);
+
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> membersToDelete;
+ if (!results.empty())
+ {
+ const std::shared_ptr<CPVRClients> allClients = CServiceBroker::GetPVRManager().Clients();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& member : results)
+ {
+ // Consistency check.
+ if (member->ClientID() > 0 && member->ChannelUID() > 0 && member->IsRadio() == IsRadio())
+ {
+ // Ignore data from unknown/disabled clients
+ if (allClients->IsEnabledClient(member->ClientID()))
+ {
+ m_sortedMembers.emplace_back(member);
+ m_members.emplace(std::make_pair(member->ClientID(), member->ChannelUID()), member);
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGWARNING,
+ "Skipping member with channel database id {} of {} channel group '{}'. "
+ "Channel not found in the database or radio flag changed.",
+ member->ChannelDatabaseID(), IsRadio() ? "radio" : "TV", GroupName());
+ membersToDelete.emplace_back(member);
+ }
+ }
+
+ SortByChannelNumber();
+ }
+
+ DeleteGroupMembersFromDb(membersToDelete);
+
+ return results.size() - membersToDelete.size();
+}
+
+void CPVRChannelGroup::DeleteGroupMembersFromDb(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& membersToDelete)
+{
+ if (!membersToDelete.empty())
+ {
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No TV database");
+ return;
+ }
+
+ // Note: We must lock the db the whole time, otherwise races may occur.
+ database->Lock();
+
+ bool commitPending = false;
+
+ for (const auto& member : membersToDelete)
+ {
+ commitPending |= member->QueueDelete();
+
+ size_t queryCount = database->GetDeleteQueriesCount();
+ if (queryCount > CHANNEL_COMMIT_QUERY_COUNT_LIMIT)
+ database->CommitDeleteQueries();
+ }
+
+ if (commitPending)
+ database->CommitDeleteQueries();
+
+ database->Unlock();
+ }
+}
+
+bool CPVRChannelGroup::UpdateFromClient(const std::shared_ptr<CPVRChannelGroupMember>& groupMember)
+{
+ bool bChanged = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::shared_ptr<CPVRChannel> channel = groupMember->Channel();
+ const std::shared_ptr<CPVRChannelGroupMember> existingMember =
+ GetByUniqueID(channel->StorageId());
+ if (existingMember)
+ {
+ // update existing channel
+ if (IsInternalGroup() && existingMember->Channel()->UpdateFromClient(channel))
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updated {} channel '{}' from PVR client",
+ IsRadio() ? "radio" : "TV", channel->ChannelName());
+ bChanged = true;
+ }
+
+ existingMember->SetClientChannelNumber(channel->ClientChannelNumber());
+ existingMember->SetOrder(groupMember->Order());
+
+ if (existingMember->NeedsSave())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updated {} channel group member '{}' in group '{}'",
+ IsRadio() ? "radio" : "TV", channel->ChannelName(), GroupName());
+ bChanged = true;
+ }
+ }
+ else
+ {
+ if (groupMember->GroupID() == -1)
+ groupMember->SetGroupID(GroupID());
+
+ m_sortedMembers.emplace_back(groupMember);
+ m_members.emplace(channel->StorageId(), groupMember);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Added {} channel group member '{}' to group '{}'",
+ IsRadio() ? "radio" : "TV", channel->ChannelName(), GroupName());
+
+ // create EPG for new channel
+ if (IsInternalGroup() && channel->CreateEPG())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Created EPG for {} channel '{}' from PVR client",
+ IsRadio() ? "radio" : "TV", channel->ChannelName());
+ }
+
+ bChanged = true;
+ }
+
+ return bChanged;
+}
+
+bool CPVRChannelGroup::AddAndUpdateGroupMembers(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers)
+{
+ return std::accumulate(groupMembers.cbegin(), groupMembers.cend(), false,
+ [this](bool changed, const auto& groupMember) {
+ return UpdateFromClient(groupMember) ? true : changed;
+ });
+}
+
+bool CPVRChannelGroup::HasValidDataForClient(int iClientId) const
+{
+ return std::find(m_failedClients.begin(), m_failedClients.end(), iClientId) ==
+ m_failedClients.end();
+}
+
+bool CPVRChannelGroup::HasValidDataForClients(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients) const
+{
+ return m_failedClients.empty() || std::none_of(clients.cbegin(), clients.cend(),
+ [this](const std::shared_ptr<CPVRClient>& client) {
+ return !HasValidDataForClient(client->GetID());
+ });
+}
+
+bool CPVRChannelGroup::UpdateChannelNumbersFromAllChannelsGroup()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bool bChanged = false;
+
+ if (!IsInternalGroup())
+ {
+ // If we don't sync channel groups make sure the channel numbers are set from
+ // the all channels group using the non default renumber call before sorting
+ if (Renumber(IGNORE_NUMBERING_FROM_ONE) || SortAndRenumber())
+ bChanged = true;
+ }
+
+ m_events.Publish(IsInternalGroup() || bChanged ? PVREvent::ChannelGroupInvalidated
+ : PVREvent::ChannelGroup);
+
+ return bChanged;
+}
+
+std::vector<std::shared_ptr<CPVRChannelGroupMember>> CPVRChannelGroup::RemoveDeletedGroupMembers(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers)
+{
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> membersToRemove;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // put group members into map to speedup the following lookups
+ std::map<std::pair<int, int>, std::shared_ptr<CPVRChannelGroupMember>> membersMap;
+ std::transform(groupMembers.begin(), groupMembers.end(),
+ std::inserter(membersMap, membersMap.end()),
+ [](const std::shared_ptr<CPVRChannelGroupMember>& member) {
+ return std::make_pair(member->Channel()->StorageId(), member);
+ });
+
+ // check for deleted channels
+ for (auto it = m_sortedMembers.begin(); it != m_sortedMembers.end();)
+ {
+ const std::shared_ptr<CPVRChannel> channel = (*it)->Channel();
+ auto mapIt = membersMap.find(channel->StorageId());
+ if (mapIt == membersMap.end())
+ {
+ if (HasValidDataForClient(channel->ClientID()))
+ {
+ CLog::Log(LOGINFO, "Removed stale {} channel '{}' from group '{}'",
+ IsRadio() ? "radio" : "TV", channel->ChannelName(), GroupName());
+ membersToRemove.emplace_back(*it);
+
+ m_members.erase(channel->StorageId());
+ it = m_sortedMembers.erase(it);
+ continue;
+ }
+ }
+ else
+ {
+ membersMap.erase(mapIt);
+ }
+ ++it;
+ }
+
+ DeleteGroupMembersFromDb(membersToRemove);
+
+ return membersToRemove;
+}
+
+bool CPVRChannelGroup::UpdateGroupEntries(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers)
+{
+ bool bReturn = false;
+ bool bChanged = false;
+ bool bRemoved = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bRemoved = !RemoveDeletedGroupMembers(groupMembers).empty();
+ bChanged = AddAndUpdateGroupMembers(groupMembers) || bRemoved;
+ bChanged |= UpdateClientPriorities();
+
+ if (bChanged)
+ {
+ // renumber to make sure all group members have a channel number. New members were added at the
+ // back, so they'll get the highest numbers
+ bool bRenumbered = SortAndRenumber();
+ bReturn = Persist();
+ m_events.Publish(HasNewChannels() || bRemoved || bRenumbered ? PVREvent::ChannelGroupInvalidated
+ : PVREvent::ChannelGroup);
+ }
+ else
+ {
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CPVRChannelGroup::RemoveFromGroup(const std::shared_ptr<CPVRChannel>& channel)
+{
+ bool bReturn = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (auto it = m_sortedMembers.begin(); it != m_sortedMembers.end(); ++it)
+ {
+ const auto storageId = (*it)->Channel()->StorageId();
+ if (channel->StorageId() == storageId)
+ {
+ m_members.erase(storageId);
+ m_sortedMembers.erase(it);
+ bReturn = true;
+ break;
+ }
+ }
+
+ // no need to renumber if nothing was removed
+ if (bReturn)
+ Renumber();
+
+ return bReturn;
+}
+
+bool CPVRChannelGroup::AppendToGroup(const std::shared_ptr<CPVRChannel>& channel)
+{
+ bool bReturn = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!CPVRChannelGroup::IsGroupMember(channel))
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> allGroupMember =
+ m_allChannelsGroup->GetByUniqueID(channel->StorageId());
+
+ if (allGroupMember)
+ {
+ unsigned int channelNumberMax =
+ std::accumulate(m_sortedMembers.cbegin(), m_sortedMembers.cend(), 0,
+ [](unsigned int last, const auto& member) {
+ return (member->ChannelNumber().GetChannelNumber() > last)
+ ? member->ChannelNumber().GetChannelNumber()
+ : last;
+ });
+
+ const auto newMember = std::make_shared<CPVRChannelGroupMember>(GroupID(), GroupName(),
+ allGroupMember->Channel());
+ newMember->SetChannelNumber(CPVRChannelNumber(channelNumberMax + 1, 0));
+ newMember->SetClientPriority(allGroupMember->ClientPriority());
+
+ m_sortedMembers.emplace_back(newMember);
+ m_members.emplace(allGroupMember->Channel()->StorageId(), newMember);
+
+ SortAndRenumber();
+ bReturn = true;
+ }
+ }
+ return bReturn;
+}
+
+bool CPVRChannelGroup::IsGroupMember(const std::shared_ptr<CPVRChannel>& channel) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_members.find(channel->StorageId()) != m_members.end();
+}
+
+bool CPVRChannelGroup::Persist()
+{
+ bool bReturn(true);
+ const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // do not persist if the group is not fully loaded and was saved before.
+ if (!m_bLoaded && m_iGroupId != INVALID_GROUP_ID)
+ return bReturn;
+
+ // Mark newly created groups as loaded so future updates will also be persisted...
+ if (m_iGroupId == INVALID_GROUP_ID)
+ m_bLoaded = true;
+
+ if (database)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Persisting channel group '{}' with {} channels", GroupName(),
+ static_cast<int>(m_members.size()));
+
+ bReturn = database->Persist(*this);
+ m_bChanged = false;
+ }
+ else
+ {
+ bReturn = false;
+ }
+
+ return bReturn;
+}
+
+void CPVRChannelGroup::Delete()
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No TV database");
+ return;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iGroupId > 0)
+ {
+ if (database->Delete(*this))
+ m_bDeleted = true;
+ }
+}
+
+bool CPVRChannelGroup::Renumber(RenumberMode mode /* = NORMAL */)
+{
+ bool bReturn(false);
+ unsigned int iChannelNumber(0);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const bool bUseBackendChannelNumbers = GetSettings()->UseBackendChannelNumbers();
+ const bool bStartGroupChannelNumbersFromOne = GetSettings()->StartGroupChannelNumbersFromOne();
+
+ CPVRChannelNumber currentChannelNumber;
+ CPVRChannelNumber currentClientChannelNumber;
+ for (auto& sortedMember : m_sortedMembers)
+ {
+ currentClientChannelNumber = sortedMember->ClientChannelNumber();
+ if (m_allChannelsGroup && !currentClientChannelNumber.IsValid())
+ currentClientChannelNumber =
+ m_allChannelsGroup->GetClientChannelNumber(sortedMember->Channel());
+
+ if (bUseBackendChannelNumbers)
+ {
+ currentChannelNumber = currentClientChannelNumber;
+ }
+ else if (sortedMember->Channel()->IsHidden())
+ {
+ currentChannelNumber = CPVRChannelNumber(0, 0);
+ }
+ else
+ {
+ if (IsInternalGroup())
+ {
+ currentChannelNumber = CPVRChannelNumber(++iChannelNumber, 0);
+ }
+ else
+ {
+ if (bStartGroupChannelNumbersFromOne && mode != IGNORE_NUMBERING_FROM_ONE)
+ currentChannelNumber = CPVRChannelNumber(++iChannelNumber, 0);
+ else
+ currentChannelNumber = m_allChannelsGroup->GetChannelNumber(sortedMember->Channel());
+ }
+ }
+
+ if (sortedMember->ChannelNumber() != currentChannelNumber ||
+ sortedMember->ClientChannelNumber() != currentClientChannelNumber)
+ {
+ bReturn = true;
+ sortedMember->SetChannelNumber(currentChannelNumber);
+ sortedMember->SetClientChannelNumber(currentClientChannelNumber);
+ }
+ }
+
+ if (bReturn)
+ Sort();
+
+ return bReturn;
+}
+
+bool CPVRChannelGroup::HasNewChannels() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_members.cbegin(), m_members.cend(),
+ [](const auto& member) { return member.second->Channel()->ChannelID() <= 0; });
+}
+
+bool CPVRChannelGroup::HasChanges() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bChanged;
+}
+
+bool CPVRChannelGroup::IsNew() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iGroupId <= 0;
+}
+
+void CPVRChannelGroup::UseBackendChannelOrderChanged()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ UpdateClientPriorities();
+ OnSettingChanged();
+}
+
+void CPVRChannelGroup::UseBackendChannelNumbersChanged()
+{
+ OnSettingChanged();
+}
+
+void CPVRChannelGroup::StartGroupChannelNumbersFromOneChanged()
+{
+ OnSettingChanged();
+}
+
+void CPVRChannelGroup::OnSettingChanged()
+{
+ //! @todo while pvr manager is starting up do accept setting changes.
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ {
+ CLog::Log(LOGWARNING, "Channel group setting change ignored while PVR Manager is starting");
+ return;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR,
+ "Renumbering channel group '{}' to use the backend channel order and/or numbers",
+ GroupName());
+
+ // If we don't sync channel groups make sure the channel numbers are set from
+ // the all channels group using the non default renumber call before sorting
+ if (!GetSettings()->SyncChannelGroups())
+ Renumber(IGNORE_NUMBERING_FROM_ONE);
+
+ const bool bRenumbered = SortAndRenumber();
+ Persist();
+
+ m_events.Publish(bRenumbered ? PVREvent::ChannelGroupInvalidated : PVREvent::ChannelGroup);
+}
+
+int CPVRChannelGroup::GroupID() const
+{
+ return m_iGroupId;
+}
+
+void CPVRChannelGroup::SetGroupID(int iGroupId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (iGroupId >= 0 && m_iGroupId != iGroupId)
+ {
+ m_iGroupId = iGroupId;
+
+ // propagate the new id to the group members
+ for (const auto& member : m_members)
+ member.second->SetGroupID(iGroupId);
+ }
+}
+
+void CPVRChannelGroup::SetGroupType(int iGroupType)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_iGroupType != iGroupType)
+ {
+ m_iGroupType = iGroupType;
+ if (m_bLoaded)
+ m_bChanged = true;
+ }
+}
+
+int CPVRChannelGroup::GroupType() const
+{
+ return m_iGroupType;
+}
+
+std::string CPVRChannelGroup::GroupName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_path.GetGroupName();
+}
+
+void CPVRChannelGroup::SetGroupName(const std::string& strGroupName)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_path.GetGroupName() != strGroupName)
+ {
+ m_path = CPVRChannelsPath(m_path.IsRadio(), strGroupName);
+ if (m_bLoaded)
+ {
+ m_bChanged = true;
+ Persist(); //! @todo why must we persist immediately?
+ }
+ }
+}
+
+bool CPVRChannelGroup::IsRadio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_path.IsRadio();
+}
+
+time_t CPVRChannelGroup::LastWatched() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iLastWatched;
+}
+
+void CPVRChannelGroup::SetLastWatched(time_t iLastWatched)
+{
+ const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iLastWatched != iLastWatched)
+ {
+ m_iLastWatched = iLastWatched;
+ if (m_bLoaded && database)
+ database->UpdateLastWatched(*this);
+ }
+}
+
+uint64_t CPVRChannelGroup::LastOpened() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iLastOpened;
+}
+
+void CPVRChannelGroup::SetLastOpened(uint64_t iLastOpened)
+{
+ const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iLastOpened != iLastOpened)
+ {
+ m_iLastOpened = iLastOpened;
+ if (m_bLoaded && database)
+ database->UpdateLastOpened(*this);
+ }
+}
+
+bool CPVRChannelGroup::UpdateChannel(const std::pair<int, int>& storageId,
+ const std::string& strChannelName,
+ const std::string& strIconPath,
+ int iEPGSource,
+ int iChannelNumber,
+ bool bHidden,
+ bool bEPGEnabled,
+ bool bParentalLocked,
+ bool bUserSetIcon,
+ bool bUserSetHidden)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* get the real channel from the group */
+ const std::shared_ptr<CPVRChannel> channel = GetByUniqueID(storageId)->Channel();
+ if (!channel)
+ return false;
+
+ channel->SetChannelName(strChannelName, true);
+ channel->SetHidden(bHidden, bUserSetHidden);
+ channel->SetLocked(bParentalLocked);
+ channel->SetIconPath(strIconPath, bUserSetIcon);
+
+ if (iEPGSource == 0)
+ channel->SetEPGScraper("client");
+
+ //! @todo add other scrapers
+ channel->SetEPGEnabled(bEPGEnabled);
+
+ /* set new values in the channel tag */
+ if (bHidden)
+ {
+ // sort or previous changes will be overwritten
+ Sort();
+
+ RemoveFromGroup(channel);
+ }
+ else if (iChannelNumber > 0)
+ {
+ SetChannelNumber(channel, CPVRChannelNumber(iChannelNumber, 0));
+ }
+
+ return true;
+}
+
+size_t CPVRChannelGroup::Size() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_members.size();
+}
+
+bool CPVRChannelGroup::HasChannels() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return !m_members.empty();
+}
+
+bool CPVRChannelGroup::CreateChannelEpgs(bool bForce /* = false */)
+{
+ /* used only by internal channel groups */
+ return true;
+}
+
+bool CPVRChannelGroup::SetHidden(bool bHidden)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bHidden != bHidden)
+ {
+ m_bHidden = bHidden;
+ if (m_bLoaded)
+ m_bChanged = true;
+ }
+
+ return m_bChanged;
+}
+
+bool CPVRChannelGroup::IsHidden() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bHidden;
+}
+
+int CPVRChannelGroup::GetPosition() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iPosition;
+}
+
+void CPVRChannelGroup::SetPosition(int iPosition)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iPosition != iPosition)
+ {
+ m_iPosition = iPosition;
+ if (m_bLoaded)
+ m_bChanged = true;
+ }
+}
+
+int CPVRChannelGroup::CleanupCachedImages()
+{
+ std::vector<std::string> urlsToCheck;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::transform(
+ m_members.cbegin(), m_members.cend(), std::back_inserter(urlsToCheck),
+ [](const auto& groupMember) { return groupMember.second->Channel()->ClientIconPath(); });
+ }
+
+ const std::string owner =
+ StringUtils::Format(CPVRChannel::IMAGE_OWNER_PATTERN, IsRadio() ? "radio" : "tv");
+ return CPVRCachedImages::Cleanup({{owner, ""}}, urlsToCheck);
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroup.h b/xbmc/pvr/channels/PVRChannelGroup.h
new file mode 100644
index 0000000..66c45d6
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroup.h
@@ -0,0 +1,548 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/channels/PVRChannelGroupSettings.h"
+#include "pvr/channels/PVRChannelNumber.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "utils/EventStream.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+struct PVR_CHANNEL_GROUP;
+
+namespace PVR
+{
+#define PVR_GROUP_TYPE_DEFAULT 0
+#define PVR_GROUP_TYPE_INTERNAL 1
+#define PVR_GROUP_TYPE_USER_DEFINED 2
+
+enum class PVREvent;
+
+class CPVRChannel;
+class CPVRChannelGroupMember;
+class CPVRClient;
+class CPVREpgInfoTag;
+
+enum RenumberMode
+{
+ NORMAL = 0,
+ IGNORE_NUMBERING_FROM_ONE = 1
+};
+
+using GroupMemberPair =
+ std::pair<std::shared_ptr<CPVRChannelGroupMember>, std::shared_ptr<CPVRChannelGroupMember>>;
+
+class CPVRChannelGroup : public IChannelGroupSettingsCallback
+{
+ friend class CPVRDatabase;
+
+public:
+ static const int INVALID_GROUP_ID = -1;
+
+ /*!
+ * @brief Create a new channel group instance.
+ * @param path The channel group path.
+ * @param allChannelsGroup The channel group containing all TV or radio channels.
+ */
+ CPVRChannelGroup(const CPVRChannelsPath& path,
+ const std::shared_ptr<CPVRChannelGroup>& allChannelsGroup);
+
+ /*!
+ * @brief Create a new channel group instance from a channel group provided by an add-on.
+ * @param group The channel group provided by the add-on.
+ * @param allChannelsGroup The channel group containing all TV or radio channels.
+ */
+ CPVRChannelGroup(const PVR_CHANNEL_GROUP& group,
+ const std::shared_ptr<CPVRChannelGroup>& allChannelsGroup);
+
+ ~CPVRChannelGroup() override;
+
+ bool operator==(const CPVRChannelGroup& right) const;
+ bool operator!=(const CPVRChannelGroup& right) const;
+
+ /*!
+ * @brief Copy over data to the given PVR_CHANNEL_GROUP instance.
+ * @param group The group instance to fill.
+ */
+ void FillAddonData(PVR_CHANNEL_GROUP& group) const;
+
+ /*!
+ * @brief Query the events available for CEventStream
+ */
+ CEventStream<PVREvent>& Events() { return m_events; }
+
+ /*!
+ * @brief Load the channels from the database.
+ * @param channels All available channels.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True when loaded successfully, false otherwise.
+ */
+ virtual bool LoadFromDatabase(
+ const std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& channels,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief Clear all data.
+ */
+ virtual void Unload();
+
+ /*!
+ * @return The amount of group members
+ */
+ size_t Size() const;
+
+ /*!
+ * @brief Update data with channel group members from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @return True on success, false otherwise.
+ */
+ virtual bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief Get the path of this group.
+ * @return the path.
+ */
+ const CPVRChannelsPath& GetPath() const;
+
+ /*!
+ * @brief Set the path of this group.
+ * @param the path.
+ */
+ void SetPath(const CPVRChannelsPath& path);
+
+ /*!
+ * @brief Change the channelnumber of a group. Used by CGUIDialogPVRChannelManager.
+ * Call SortByChannelNumber() and Renumber() after all changes are done.
+ * @param channel The channel to change the channel number for.
+ * @param channelNumber The new channel number.
+ */
+ bool SetChannelNumber(const std::shared_ptr<CPVRChannel>& channel,
+ const CPVRChannelNumber& channelNumber);
+
+ /*!
+ * @brief Remove a channel from this container.
+ * @param channel The channel to remove.
+ * @return True if the channel was found and removed, false otherwise.
+ */
+ virtual bool RemoveFromGroup(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Append a channel to this container.
+ * @param channel The channel to append.
+ * @return True if the channel was appended, false otherwise.
+ */
+ virtual bool AppendToGroup(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Change the name of this group.
+ * @param strGroupName The new group name.
+ */
+ void SetGroupName(const std::string& strGroupName);
+
+ /*!
+ * @brief Persist changed or new data.
+ * @return True if the channel was persisted, false otherwise.
+ */
+ bool Persist();
+
+ /*!
+ * @brief Check whether a channel is in this container.
+ * @param channel The channel to find.
+ * @return True if the channel was found, false otherwise.
+ */
+ virtual bool IsGroupMember(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Check if this group is the internal group containing all channels.
+ * @return True if it's the internal group, false otherwise.
+ */
+ bool IsInternalGroup() const { return m_iGroupType == PVR_GROUP_TYPE_INTERNAL; }
+
+ /*!
+ * @brief True if this group holds radio channels, false if it holds TV channels.
+ * @return True if this group holds radio channels, false if it holds TV channels.
+ */
+ bool IsRadio() const;
+
+ /*!
+ * @brief The database ID of this group.
+ * @return The database ID of this group.
+ */
+ int GroupID() const;
+
+ /*!
+ * @brief Set the database ID of this group.
+ * @param iGroupId The new database ID.
+ */
+ void SetGroupID(int iGroupId);
+
+ /*!
+ * @brief Set the type of this group.
+ * @param the new type for this group.
+ */
+ void SetGroupType(int iGroupType);
+
+ /*!
+ * @brief Return the type of this group.
+ */
+ int GroupType() const;
+
+ /*!
+ * @return Time group has been watched last.
+ */
+ time_t LastWatched() const;
+
+ /*!
+ * @brief Last time group has been watched
+ * @param iLastWatched The new value.
+ */
+ void SetLastWatched(time_t iLastWatched);
+
+ /*!
+ * @return Time in milliseconds from epoch this group was last opened.
+ */
+ uint64_t LastOpened() const;
+
+ /*!
+ * @brief Set the time in milliseconds from epoch this group was last opened.
+ * @param iLastOpened The new value.
+ */
+ void SetLastOpened(uint64_t iLastOpened);
+
+ /*!
+ * @brief The name of this group.
+ * @return The name of this group.
+ */
+ std::string GroupName() const;
+
+ /*! @name Sort methods
+ */
+ //@{
+
+ /*!
+ * @brief Sort the group.
+ */
+ void Sort();
+
+ /*!
+ * @brief Sort the group and fix up channel numbers.
+ * @return True when numbering changed, false otherwise
+ */
+ bool SortAndRenumber();
+
+ /*!
+ * @brief Remove invalid channels and updates the channel numbers.
+ * @param mode the numbering mode to use
+ * @return True if something changed, false otherwise.
+ */
+ bool Renumber(RenumberMode mode = NORMAL);
+
+ //@}
+
+ // IChannelGroupSettingsCallback implementation
+ void UseBackendChannelOrderChanged() override;
+ void UseBackendChannelNumbersChanged() override;
+ void StartGroupChannelNumbersFromOneChanged() override;
+
+ /*!
+ * @brief Get the channel group member that was played last.
+ * @param iCurrentChannel The channelid of the current channel that is playing, or -1 if none
+ * @return The requested channel group member or nullptr.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetLastPlayedChannelGroupMember(
+ int iCurrentChannel = -1) const;
+
+ /*!
+ * @brief Get the last and previous to last played channel group members.
+ * @return The members. pair.first contains the last, pair.second the previous to last member.
+ */
+ GroupMemberPair GetLastAndPreviousToLastPlayedChannelGroupMember() const;
+
+ /*!
+ * @brief Get a channel group member given it's active channel number
+ * @param channelNumber The channel number.
+ * @return The channel group member or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetByChannelNumber(
+ const CPVRChannelNumber& channelNumber) const;
+
+ /*!
+ * @brief Get the channel number in this group of the given channel.
+ * @param channel The channel to get the channel number for.
+ * @return The channel number in this group.
+ */
+ CPVRChannelNumber GetChannelNumber(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Get the client channel number in this group of the given channel.
+ * @param channel The channel to get the channel number for.
+ * @return The client channel number in this group.
+ */
+ CPVRChannelNumber GetClientChannelNumber(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Get the next channel group member in this group.
+ * @param groupMember The current channel group member.
+ * @return The channel group member or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetNextChannelGroupMember(
+ const std::shared_ptr<CPVRChannelGroupMember>& groupMember) const;
+
+ /*!
+ * @brief Get the previous channel group member in this group.
+ * @param groupMember The current channel group member.
+ * @return The channel group member or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetPreviousChannelGroupMember(
+ const std::shared_ptr<CPVRChannelGroupMember>& groupMember) const;
+
+ /*!
+ * @brief Get a channel given it's channel ID.
+ * @param iChannelID The channel ID.
+ * @return The channel or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannel> GetByChannelID(int iChannelID) const;
+
+ enum class Include
+ {
+ ALL,
+ ONLY_HIDDEN,
+ ONLY_VISIBLE
+ };
+
+ /*!
+ * @brief Get the current members of this group
+ * @param eFilter A filter to apply.
+ * @return The group members
+ */
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> GetMembers(
+ Include eFilter = Include::ALL) const;
+
+ /*!
+ * @brief Get the list of active channel numbers in a group.
+ * @param channelNumbers The list to store the numbers in.
+ */
+ void GetChannelNumbers(std::vector<std::string>& channelNumbers) const;
+
+ /*!
+ * @brief The amount of hidden channels in this container.
+ * @return The amount of hidden channels in this container.
+ */
+ virtual size_t GetNumHiddenChannels() const { return 0; }
+
+ /*!
+ * @brief Does this container holds channels.
+ * @return True if there is at least one channel in this container, otherwise false.
+ */
+ bool HasChannels() const;
+
+ /*!
+ * @return True if there is at least one new channel in this group that hasn't been persisted, false otherwise.
+ */
+ bool HasNewChannels() const;
+
+ /*!
+ * @return True if anything changed in this group that hasn't been persisted, false otherwise.
+ */
+ bool HasChanges() const;
+
+ /*!
+ * @return True if the group was never persisted, false otherwise.
+ */
+ bool IsNew() const;
+
+ /*!
+ * @brief Create an EPG table for each channel.
+ * @brief bForce Create the tables, even if they already have been created before.
+ * @return True if all tables were created successfully, false otherwise.
+ */
+ virtual bool CreateChannelEpgs(bool bForce = false);
+
+ /*!
+ * @brief Update a channel group member with given data.
+ * @param storageId The storage id of the channel.
+ * @param strChannelName The channel name to set.
+ * @param strIconPath The icon path to set.
+ * @param iEPGSource The EPG id.
+ * @param iChannelNumber The channel number to set.
+ * @param bHidden Set/Remove hidden flag for the channel group member identified by storage id.
+ * @param bEPGEnabled Set/Remove EPG enabled flag for the channel group member identified by storage id.
+ * @param bParentalLocked Set/Remove parental locked flag for the channel group member identified by storage id.
+ * @param bUserSetIcon Set/Remove user set icon flag for the channel group member identified by storage id.
+ * @param bUserSetHidden Set/Remove user set hidden flag for the channel group member identified by storage id.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateChannel(const std::pair<int, int>& storageId,
+ const std::string& strChannelName,
+ const std::string& strIconPath,
+ int iEPGSource,
+ int iChannelNumber,
+ bool bHidden,
+ bool bEPGEnabled,
+ bool bParentalLocked,
+ bool bUserSetIcon,
+ bool bUserSetHidden);
+
+ /*!
+ * @brief Get a channel given the channel number on the client.
+ * @param iUniqueChannelId The unique channel id on the client.
+ * @param iClientID The ID of the client.
+ * @return The channel or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannel> GetByUniqueID(int iUniqueChannelId, int iClientID) const;
+
+ /*!
+ * @brief Get a channel group member given its storage id.
+ * @param id The storage id (a pair of client id and unique channel id).
+ * @return The channel group member or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetByUniqueID(const std::pair<int, int>& id) const;
+
+ bool SetHidden(bool bHidden);
+ bool IsHidden() const;
+
+ int GetPosition() const;
+ void SetPosition(int iPosition);
+
+ /*!
+ * @brief Check, whether data for a given pvr client are currently valid. For instance, data
+ * can be invalid because the client's backend was offline when data was last queried.
+ * @param iClientId The id of the client.
+ * @return True, if data is currently valid, false otherwise.
+ */
+ bool HasValidDataForClient(int iClientId) const;
+
+ /*!
+ * @brief Check, whether data for given pvr clients are currently valid. For instance, data
+ * can be invalid because the client's backend was offline when data was last queried.
+ * @param clients The clients to check. Check all active clients if vector is empty.
+ * @return True, if data is currently valid, false otherwise.
+ */
+ bool HasValidDataForClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) const;
+
+ /*!
+ * @brief Update the channel numbers according to the all channels group and publish event.
+ * @return True, if a channel number was changed, false otherwise.
+ */
+ bool UpdateChannelNumbersFromAllChannelsGroup();
+
+ /*!
+ * @brief Remove this group from database.
+ */
+ void Delete();
+
+ /*!
+ * @brief Whether this group is deleted.
+ * @return True, if deleted, false otherwise.
+ */
+ bool IsDeleted() const { return m_bDeleted; }
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+protected:
+ /*!
+ * @brief Remove deleted group members from this group.
+ * @param groupMembers The group members to use to update this list.
+ * @return The removed members .
+ */
+ virtual std::vector<std::shared_ptr<CPVRChannelGroupMember>> RemoveDeletedGroupMembers(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers);
+
+ /*!
+ * @brief Update the current channel group members with the given list.
+ * @param groupMembers The group members to use to update this list.
+ * @return True if everything went well, false otherwise.
+ */
+ bool UpdateGroupEntries(const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers);
+
+ /*!
+ * @brief Sort the current channel list by client channel number.
+ */
+ void SortByClientChannelNumber();
+
+ /*!
+ * @brief Sort the current channel list by channel number.
+ */
+ void SortByChannelNumber();
+
+ /*!
+ * @brief Update the priority for all members of all channel groups.
+ */
+ bool UpdateClientPriorities();
+
+ std::shared_ptr<CPVRChannelGroupSettings> GetSettings() const;
+
+ int m_iGroupType = PVR_GROUP_TYPE_DEFAULT; /*!< The type of this group */
+ int m_iGroupId = INVALID_GROUP_ID; /*!< The ID of this group in the database */
+ bool m_bLoaded = false; /*!< True if this container is loaded, false otherwise */
+ bool m_bChanged =
+ false; /*!< true if anything changed in this group that hasn't been persisted, false otherwise */
+ time_t m_iLastWatched = 0; /*!< last time group has been watched */
+ uint64_t m_iLastOpened = 0; /*!< time in milliseconds from epoch this group was last opened */
+ bool m_bHidden = false; /*!< true if this group is hidden, false otherwise */
+ int m_iPosition = 0; /*!< the position of this group within the group list */
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>>
+ m_sortedMembers; /*!< members sorted by channel number */
+ std::map<std::pair<int, int>, std::shared_ptr<CPVRChannelGroupMember>>
+ m_members; /*!< members with key clientid+uniqueid */
+ mutable CCriticalSection m_critSection;
+ std::vector<int> m_failedClients;
+ CEventSource<PVREvent> m_events;
+ mutable std::shared_ptr<CPVRChannelGroupSettings> m_settings;
+
+ // the settings singleton shared between all group instances
+ static CCriticalSection m_settingsSingletonCritSection;
+ static std::weak_ptr<CPVRChannelGroupSettings> m_settingsSingleton;
+
+private:
+ /*!
+ * @brief Load the channel group members stored in the database.
+ * @param clients The PVR clients to load data for. Leave empty for all clients.
+ * @return The amount of channel group members that were added.
+ */
+ int LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief Delete channel group members from database.
+ * @param membersToDelete The channel group members.
+ */
+ void DeleteGroupMembersFromDb(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& membersToDelete);
+
+ /*!
+ * @brief Update this group's data with a channel group member provided by a client.
+ * @param groupMember The updated group member.
+ * @return True if group member data were changed, false otherwise.
+ */
+ bool UpdateFromClient(const std::shared_ptr<CPVRChannelGroupMember>& groupMember);
+
+ /*!
+ * @brief Add new channel group members to this group; update data.
+ * @param groupMembers The group members to use to update this list.
+ * @return True if group member data were changed, false otherwise.
+ */
+ bool AddAndUpdateGroupMembers(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers);
+
+ void OnSettingChanged();
+
+ std::shared_ptr<CPVRChannelGroup> m_allChannelsGroup;
+ CPVRChannelsPath m_path;
+ bool m_bDeleted = false;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/channels/PVRChannelGroupInternal.cpp b/xbmc/pvr/channels/PVRChannelGroupInternal.cpp
new file mode 100644
index 0000000..f79e02f
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupInternal.cpp
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRChannelGroupInternal.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/epg/EpgContainer.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+CPVRChannelGroupInternal::CPVRChannelGroupInternal(bool bRadio)
+ : CPVRChannelGroup(CPVRChannelsPath(bRadio, g_localizeStrings.Get(19287)), nullptr),
+ m_iHiddenChannels(0)
+{
+ m_iGroupType = PVR_GROUP_TYPE_INTERNAL;
+}
+
+CPVRChannelGroupInternal::CPVRChannelGroupInternal(const CPVRChannelsPath& path)
+ : CPVRChannelGroup(path, nullptr), m_iHiddenChannels(0)
+{
+ m_iGroupType = PVR_GROUP_TYPE_INTERNAL;
+}
+
+CPVRChannelGroupInternal::~CPVRChannelGroupInternal()
+{
+ CServiceBroker::GetPVRManager().Events().Unsubscribe(this);
+}
+
+bool CPVRChannelGroupInternal::LoadFromDatabase(
+ const std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& channels,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ if (CPVRChannelGroup::LoadFromDatabase(channels, clients))
+ {
+ for (const auto& groupMember : m_members)
+ {
+ const std::shared_ptr<CPVRChannel> channel = groupMember.second->Channel();
+
+ // create the EPG for the channel
+ if (channel->CreateEPG())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Created EPG for {} channel '{}'", IsRadio() ? "radio" : "TV",
+ channel->ChannelName());
+ }
+ }
+
+ UpdateChannelPaths();
+ CServiceBroker::GetPVRManager().Events().Subscribe(this, &CPVRChannelGroupInternal::OnPVRManagerEvent);
+ return true;
+ }
+
+ CLog::LogF(LOGERROR, "Failed to load channels");
+ return false;
+}
+
+void CPVRChannelGroupInternal::Unload()
+{
+ CServiceBroker::GetPVRManager().Events().Unsubscribe(this);
+ CPVRChannelGroup::Unload();
+}
+
+void CPVRChannelGroupInternal::CheckGroupName()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* check whether the group name is still correct, or channels will fail to load after the language setting changed */
+ const std::string& strNewGroupName = g_localizeStrings.Get(19287);
+ if (GroupName() != strNewGroupName)
+ {
+ SetGroupName(strNewGroupName);
+ UpdateChannelPaths();
+ }
+}
+
+void CPVRChannelGroupInternal::UpdateChannelPaths()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iHiddenChannels = 0;
+ for (auto& groupMemberPair : m_members)
+ {
+ if (groupMemberPair.second->Channel()->IsHidden())
+ ++m_iHiddenChannels;
+ else
+ groupMemberPair.second->SetGroupName(GroupName());
+ }
+}
+
+bool CPVRChannelGroupInternal::UpdateFromClients(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ // get the channels from the given clients
+ std::vector<std::shared_ptr<CPVRChannel>> channels;
+ CServiceBroker::GetPVRManager().Clients()->GetChannels(clients, IsRadio(), channels,
+ m_failedClients);
+
+ // create group members for the channels
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers;
+ std::transform(channels.cbegin(), channels.cend(), std::back_inserter(groupMembers),
+ [this](const auto& channel) {
+ return std::make_shared<CPVRChannelGroupMember>(GroupID(), GroupName(), channel);
+ });
+
+ return UpdateGroupEntries(groupMembers);
+}
+
+std::vector<std::shared_ptr<CPVRChannelGroupMember>> CPVRChannelGroupInternal::
+ RemoveDeletedGroupMembers(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers)
+{
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> removedMembers =
+ CPVRChannelGroup::RemoveDeletedGroupMembers(groupMembers);
+ if (!removedMembers.empty())
+ {
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No TV database");
+ }
+ else
+ {
+ std::vector<std::shared_ptr<CPVREpg>> epgsToRemove;
+ for (const auto& member : removedMembers)
+ {
+ const auto channel = member->Channel();
+ const auto epg = channel->GetEPG();
+ if (epg)
+ epgsToRemove.emplace_back(epg);
+
+ // Note: We need to obtain a lock for every channel instance before we can lock
+ // the TV db. This order is important. Otherwise deadlocks may occur.
+ channel->Lock();
+ }
+
+ // Note: We must lock the db the whole time, otherwise races may occur.
+ database->Lock();
+
+ bool commitPending = false;
+
+ for (const auto& member : removedMembers)
+ {
+ // since channel was not found in the internal group, it was deleted from the backend
+
+ const auto channel = member->Channel();
+ commitPending |= channel->QueueDelete();
+ channel->Unlock();
+
+ size_t queryCount = database->GetDeleteQueriesCount();
+ if (queryCount > CHANNEL_COMMIT_QUERY_COUNT_LIMIT)
+ database->CommitDeleteQueries();
+ }
+
+ if (commitPending)
+ database->CommitDeleteQueries();
+
+ database->Unlock();
+
+ // delete the EPG data for the removed channels
+ CServiceBroker::GetPVRManager().EpgContainer().QueueDeleteEpgs(epgsToRemove);
+ }
+ }
+ return removedMembers;
+}
+
+bool CPVRChannelGroupInternal::AppendToGroup(const std::shared_ptr<CPVRChannel>& channel)
+{
+ if (IsGroupMember(channel))
+ return false;
+
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember = GetByUniqueID(channel->StorageId());
+ if (!groupMember)
+ return false;
+
+ channel->SetHidden(false, true);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iHiddenChannels > 0)
+ m_iHiddenChannels--;
+
+ const unsigned int iChannelNumber = m_members.size() - m_iHiddenChannels;
+ groupMember->SetChannelNumber(CPVRChannelNumber(iChannelNumber, 0));
+
+ SortAndRenumber();
+ return true;
+}
+
+bool CPVRChannelGroupInternal::RemoveFromGroup(const std::shared_ptr<CPVRChannel>& channel)
+{
+ if (!IsGroupMember(channel))
+ return false;
+
+ channel->SetHidden(true, true);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ ++m_iHiddenChannels;
+
+ SortAndRenumber();
+ return true;
+}
+
+bool CPVRChannelGroupInternal::IsGroupMember(const std::shared_ptr<CPVRChannel>& channel) const
+{
+ return !channel->IsHidden();
+}
+
+bool CPVRChannelGroupInternal::CreateChannelEpgs(bool bForce /* = false */)
+{
+ if (!CServiceBroker::GetPVRManager().EpgContainer().IsStarted())
+ return false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (auto& groupMemberPair : m_members)
+ groupMemberPair.second->Channel()->CreateEPG();
+ }
+
+ return Persist();
+}
+
+void CPVRChannelGroupInternal::OnPVRManagerEvent(const PVR::PVREvent& event)
+{
+ if (event == PVREvent::ManagerStarted)
+ CServiceBroker::GetPVRManager().TriggerEpgsCreate();
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroupInternal.h b/xbmc/pvr/channels/PVRChannelGroupInternal.h
new file mode 100644
index 0000000..5f8a06d
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupInternal.h
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/channels/PVRChannelGroup.h"
+
+#include <memory>
+#include <vector>
+
+namespace PVR
+{
+ enum class PVREvent;
+
+ class CPVRChannel;
+ class CPVRChannelNumber;
+
+ class CPVRChannelGroupInternal : public CPVRChannelGroup
+ {
+ public:
+ CPVRChannelGroupInternal() = delete;
+
+ /*!
+ * @brief Create a new internal channel group.
+ * @param bRadio True if this group holds radio channels.
+ */
+ explicit CPVRChannelGroupInternal(bool bRadio);
+
+ /*!
+ * @brief Create a new internal channel group.
+ * @param path The path for the new group.
+ */
+ explicit CPVRChannelGroupInternal(const CPVRChannelsPath& path);
+
+ ~CPVRChannelGroupInternal() override;
+
+ /**
+ * @brief The amount of channels in this container.
+ * @return The amount of channels in this container.
+ */
+ size_t GetNumHiddenChannels() const override { return m_iHiddenChannels; }
+
+ /*!
+ * @see CPVRChannelGroup::IsGroupMember
+ */
+ bool IsGroupMember(const std::shared_ptr<CPVRChannel>& channel) const override;
+
+ /*!
+ * @see CPVRChannelGroup::AppendToGroup
+ */
+ bool AppendToGroup(const std::shared_ptr<CPVRChannel>& channel) override;
+
+ /*!
+ * @see CPVRChannelGroup::RemoveFromGroup
+ */
+ bool RemoveFromGroup(const std::shared_ptr<CPVRChannel>& channel) override;
+
+ /*!
+ * @brief Check whether the group name is still correct after the language setting changed.
+ */
+ void CheckGroupName();
+
+ /*!
+ * @brief Create an EPG table for each channel.
+ * @brief bForce Create the tables, even if they already have been created before.
+ * @return True if all tables were created successfully, false otherwise.
+ */
+ bool CreateChannelEpgs(bool bForce = false) override;
+
+ protected:
+ /*!
+ * @brief Remove deleted group members from this group. Delete stale channels.
+ * @param groupMembers The group members to use to update this list.
+ * @return The removed members .
+ */
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> RemoveDeletedGroupMembers(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers) override;
+
+ /*!
+ * @brief Update data with 'all channels' group members from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) override;
+
+ /*!
+ * @brief Load the channels from the database.
+ * @param channels All available channels.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True when loaded successfully, false otherwise.
+ */
+ bool LoadFromDatabase(
+ const std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& channels,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients) override;
+
+ /*!
+ * @brief Clear all data.
+ */
+ void Unload() override;
+
+ /*!
+ * @brief Update the vfs paths of all channels.
+ */
+ void UpdateChannelPaths();
+
+ size_t m_iHiddenChannels; /*!< the amount of hidden channels in this container */
+
+ private:
+ void OnPVRManagerEvent(const PVREvent& event);
+ };
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroupMember.cpp b/xbmc/pvr/channels/PVRChannelGroupMember.cpp
new file mode 100644
index 0000000..27592b5
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupMember.cpp
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012-2021 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRChannelGroupMember.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "utils/DatabaseUtils.h"
+#include "utils/SortUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace PVR;
+
+CPVRChannelGroupMember::CPVRChannelGroupMember(const std::string& groupName,
+ int order,
+ const std::shared_ptr<CPVRChannel>& channel)
+ : m_clientChannelNumber(channel->ClientChannelNumber()), m_iOrder(order)
+{
+ SetChannel(channel);
+ SetGroupName(groupName);
+}
+
+CPVRChannelGroupMember::CPVRChannelGroupMember(int iGroupID,
+ const std::string& groupName,
+ const std::shared_ptr<CPVRChannel>& channel)
+ : m_iGroupID(iGroupID),
+ m_clientChannelNumber(channel->ClientChannelNumber()),
+ m_iOrder(channel->ClientOrder())
+{
+ SetChannel(channel);
+ SetGroupName(groupName);
+}
+
+void CPVRChannelGroupMember::Serialize(CVariant& value) const
+{
+ value["channelnumber"] = m_channelNumber.GetChannelNumber();
+ value["subchannelnumber"] = m_channelNumber.GetSubChannelNumber();
+}
+
+void CPVRChannelGroupMember::SetChannel(const std::shared_ptr<CPVRChannel>& channel)
+{
+ // note: no need to set m_bChanged, as these values are not persisted in the db
+ m_channel = channel;
+ m_iClientID = channel->ClientID();
+ m_iChannelUID = channel->UniqueID();
+ m_iChannelDatabaseID = channel->ChannelID();
+ m_bIsRadio = channel->IsRadio();
+}
+
+void CPVRChannelGroupMember::ToSortable(SortItem& sortable, Field field) const
+{
+ if (field == FieldChannelNumber)
+ {
+ sortable[FieldChannelNumber] = m_channelNumber.SortableChannelNumber();
+ }
+ else if (field == FieldClientChannelOrder)
+ {
+ if (m_iOrder)
+ sortable[FieldClientChannelOrder] = m_iOrder;
+ else
+ sortable[FieldClientChannelOrder] = m_clientChannelNumber.SortableChannelNumber();
+ }
+}
+
+void CPVRChannelGroupMember::SetGroupID(int iGroupID)
+{
+ if (m_iGroupID != iGroupID)
+ {
+ m_iGroupID = iGroupID;
+ m_bNeedsSave = true;
+ }
+}
+
+void CPVRChannelGroupMember::SetGroupName(const std::string& groupName)
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientID);
+ if (client)
+ m_path =
+ CPVRChannelsPath(m_bIsRadio, groupName, client->ID(), client->InstanceId(), m_iChannelUID);
+ else
+ CLog::LogF(LOGERROR, "Unable to obtain instance for client id: {}", m_iClientID);
+}
+
+void CPVRChannelGroupMember::SetChannelNumber(const CPVRChannelNumber& channelNumber)
+{
+ if (m_channelNumber != channelNumber)
+ {
+ m_channelNumber = channelNumber;
+ m_bNeedsSave = true;
+ }
+}
+
+void CPVRChannelGroupMember::SetClientChannelNumber(const CPVRChannelNumber& clientChannelNumber)
+{
+ if (m_clientChannelNumber != clientChannelNumber)
+ {
+ m_clientChannelNumber = clientChannelNumber;
+ m_bNeedsSave = true;
+ }
+}
+
+void CPVRChannelGroupMember::SetClientPriority(int iClientPriority)
+{
+ if (m_iClientPriority != iClientPriority)
+ {
+ m_iClientPriority = iClientPriority;
+ // Note: do not set m_bNeedsSave here as priority is not stored in database
+ }
+}
+
+void CPVRChannelGroupMember::SetOrder(int iOrder)
+{
+ if (m_iOrder != iOrder)
+ {
+ m_iOrder = iOrder;
+ m_bNeedsSave = true;
+ }
+}
+
+bool CPVRChannelGroupMember::QueueDelete()
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (!database)
+ return false;
+
+ return database->QueueDeleteQuery(*this);
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroupMember.h b/xbmc/pvr/channels/PVRChannelGroupMember.h
new file mode 100644
index 0000000..cb3ac83
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupMember.h
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012-2021 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/channels/PVRChannelNumber.h"
+#include "utils/ISerializable.h"
+#include "utils/ISortable.h"
+
+#include <memory>
+#include <string>
+
+namespace PVR
+{
+
+class CPVRChannel;
+
+class CPVRChannelGroupMember : public ISerializable, public ISortable
+{
+ friend class CPVRDatabase;
+
+public:
+ CPVRChannelGroupMember() : m_bNeedsSave(false) {}
+
+ CPVRChannelGroupMember(const std::string& groupName,
+ int order,
+ const std::shared_ptr<CPVRChannel>& channel);
+
+ CPVRChannelGroupMember(int iGroupID,
+ const std::string& groupName,
+ const std::shared_ptr<CPVRChannel>& channel);
+
+ virtual ~CPVRChannelGroupMember() = default;
+
+ // ISerializable implementation
+ void Serialize(CVariant& value) const override;
+
+ // ISortable implementation
+ void ToSortable(SortItem& sortable, Field field) const override;
+
+ std::shared_ptr<CPVRChannel> Channel() const { return m_channel; }
+ void SetChannel(const std::shared_ptr<CPVRChannel>& channel);
+
+ int GroupID() const { return m_iGroupID; }
+ void SetGroupID(int iGroupID);
+
+ const std::string& Path() const { return m_path; }
+ void SetGroupName(const std::string& groupName);
+
+ const CPVRChannelNumber& ChannelNumber() const { return m_channelNumber; }
+ void SetChannelNumber(const CPVRChannelNumber& channelNumber);
+
+ const CPVRChannelNumber& ClientChannelNumber() const { return m_clientChannelNumber; }
+ void SetClientChannelNumber(const CPVRChannelNumber& clientChannelNumber);
+
+ int ClientPriority() const { return m_iClientPriority; }
+ void SetClientPriority(int iClientPriority);
+
+ int Order() const { return m_iOrder; }
+ void SetOrder(int iOrder);
+
+ bool NeedsSave() const { return m_bNeedsSave; }
+ void SetSaved() { m_bNeedsSave = false; }
+
+ int ClientID() const { return m_iClientID; }
+
+ int ChannelUID() const { return m_iChannelUID; }
+
+ int ChannelDatabaseID() const { return m_iChannelDatabaseID; }
+
+ bool IsRadio() const { return m_bIsRadio; }
+
+ /*!
+ * @brief Delete this group member from the database.
+ * @return True if it was deleted successfully, false otherwise.
+ */
+ bool QueueDelete();
+
+private:
+ int m_iGroupID = -1;
+ int m_iClientID = -1;
+ int m_iChannelUID = -1;
+ int m_iChannelDatabaseID = -1;
+ bool m_bIsRadio = false;
+ std::shared_ptr<CPVRChannel> m_channel;
+ std::string m_path;
+ CPVRChannelNumber m_channelNumber; // the channel number this channel has in the group
+ CPVRChannelNumber
+ m_clientChannelNumber; // the client channel number this channel has in the group
+ int m_iClientPriority = 0;
+ int m_iOrder = 0; // The value denoting the order of this member in the group
+
+ bool m_bNeedsSave = true;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/channels/PVRChannelGroupSettings.cpp b/xbmc/pvr/channels/PVRChannelGroupSettings.cpp
new file mode 100644
index 0000000..3cbc0bd
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupSettings.cpp
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012-2021 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRChannelGroupSettings.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "settings/Settings.h"
+#include "settings/lib/Setting.h"
+
+using namespace PVR;
+
+CPVRChannelGroupSettings::CPVRChannelGroupSettings()
+ : m_settings({CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS,
+ CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER,
+ CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS,
+ CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS,
+ CSettings::SETTING_PVRMANAGER_STARTGROUPCHANNELNUMBERSFROMONE})
+{
+ UpdateSyncChannelGroups();
+ UpdateUseBackendChannelOrder();
+ UpdateUseBackendChannelNumbers();
+ UpdateStartGroupChannelNumbersFromOne();
+
+ m_settings.RegisterCallback(this);
+}
+
+CPVRChannelGroupSettings::~CPVRChannelGroupSettings()
+{
+ m_settings.UnregisterCallback(this);
+}
+
+void CPVRChannelGroupSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (!setting)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS)
+ {
+ if (SyncChannelGroups() != UpdateSyncChannelGroups())
+ {
+ for (const auto& callback : m_callbacks)
+ callback->SyncChannelGroupsChanged();
+ }
+ }
+ else if (settingId == CSettings::CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER)
+ {
+ if (UseBackendChannelOrder() != UpdateUseBackendChannelOrder())
+ {
+ for (const auto& callback : m_callbacks)
+ callback->UseBackendChannelOrderChanged();
+ }
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS ||
+ settingId == CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS)
+ {
+ if (UseBackendChannelNumbers() != UpdateUseBackendChannelNumbers())
+ {
+ for (const auto& callback : m_callbacks)
+ callback->UseBackendChannelNumbersChanged();
+ }
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_STARTGROUPCHANNELNUMBERSFROMONE)
+ {
+ if (StartGroupChannelNumbersFromOne() != UpdateStartGroupChannelNumbersFromOne())
+ {
+ for (const auto& callback : m_callbacks)
+ callback->StartGroupChannelNumbersFromOneChanged();
+ }
+ }
+}
+
+void CPVRChannelGroupSettings::RegisterCallback(IChannelGroupSettingsCallback* callback)
+{
+ m_callbacks.insert(callback);
+}
+
+void CPVRChannelGroupSettings::UnregisterCallback(IChannelGroupSettingsCallback* callback)
+{
+ m_callbacks.erase(callback);
+}
+
+bool CPVRChannelGroupSettings::UpdateSyncChannelGroups()
+{
+ m_bSyncChannelGroups = m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS);
+ return m_bSyncChannelGroups;
+}
+
+bool CPVRChannelGroupSettings::UpdateUseBackendChannelOrder()
+{
+ m_bUseBackendChannelOrder =
+ m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER);
+ return m_bUseBackendChannelOrder;
+}
+
+bool CPVRChannelGroupSettings::UpdateUseBackendChannelNumbers()
+{
+ const int enabledClientAmount = CServiceBroker::GetPVRManager().Clients()->EnabledClientAmount();
+ m_bUseBackendChannelNumbers =
+ m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS) &&
+ (enabledClientAmount == 1 ||
+ (m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS) &&
+ enabledClientAmount > 1));
+ return m_bUseBackendChannelNumbers;
+}
+
+bool CPVRChannelGroupSettings::UpdateStartGroupChannelNumbersFromOne()
+{
+ m_bStartGroupChannelNumbersFromOne =
+ m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_STARTGROUPCHANNELNUMBERSFROMONE) &&
+ !UseBackendChannelNumbers();
+ return m_bStartGroupChannelNumbersFromOne;
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroupSettings.h b/xbmc/pvr/channels/PVRChannelGroupSettings.h
new file mode 100644
index 0000000..abf842a
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupSettings.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012-2021 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/settings/PVRSettings.h"
+#include "settings/lib/ISettingCallback.h"
+
+#include <memory>
+#include <set>
+
+namespace PVR
+{
+
+class IChannelGroupSettingsCallback
+{
+public:
+ virtual ~IChannelGroupSettingsCallback() = default;
+
+ virtual void SyncChannelGroupsChanged() {}
+ virtual void UseBackendChannelOrderChanged() {}
+ virtual void UseBackendChannelNumbersChanged() {}
+ virtual void StartGroupChannelNumbersFromOneChanged() {}
+};
+
+class CPVRChannelGroupSettings : public ISettingCallback
+{
+public:
+ CPVRChannelGroupSettings();
+ virtual ~CPVRChannelGroupSettings();
+
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ void RegisterCallback(IChannelGroupSettingsCallback* callback);
+ void UnregisterCallback(IChannelGroupSettingsCallback* callback);
+
+ bool SyncChannelGroups() const { return m_bSyncChannelGroups; }
+ bool UseBackendChannelOrder() const { return m_bUseBackendChannelOrder; }
+ bool UseBackendChannelNumbers() const { return m_bUseBackendChannelNumbers; }
+ bool StartGroupChannelNumbersFromOne() const { return m_bStartGroupChannelNumbersFromOne; }
+
+private:
+ bool UpdateSyncChannelGroups();
+ bool UpdateUseBackendChannelOrder();
+ bool UpdateUseBackendChannelNumbers();
+ bool UpdateStartGroupChannelNumbersFromOne();
+
+ bool m_bSyncChannelGroups = false;
+ bool m_bUseBackendChannelOrder = false;
+ bool m_bUseBackendChannelNumbers = false;
+ bool m_bStartGroupChannelNumbersFromOne = false;
+
+ CPVRSettings m_settings;
+ std::set<IChannelGroupSettingsCallback*> m_callbacks;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/channels/PVRChannelGroups.cpp b/xbmc/pvr/channels/PVRChannelGroups.cpp
new file mode 100644
index 0000000..77cc2b5
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroups.cpp
@@ -0,0 +1,621 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRChannelGroups.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClientUID.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupInternal.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <chrono>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+CPVRChannelGroups::CPVRChannelGroups(bool bRadio) :
+ m_bRadio(bRadio)
+{
+}
+
+CPVRChannelGroups::~CPVRChannelGroups()
+{
+ Unload();
+}
+
+void CPVRChannelGroups::Unload()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& group : m_groups)
+ group->Unload();
+
+ m_groups.clear();
+ m_failedClientsForChannelGroups.clear();
+}
+
+bool CPVRChannelGroups::Update(const std::shared_ptr<CPVRChannelGroup>& group,
+ bool bUpdateFromClient /* = false */)
+{
+ if (group->GroupName().empty() && group->GroupID() <= 0)
+ return true;
+
+ std::shared_ptr<CPVRChannelGroup> updateGroup;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // There can be only one internal group! Make sure we never push a new one!
+ if (group->IsInternalGroup())
+ updateGroup = GetGroupAll();
+
+ // try to find the group by id
+ if (!updateGroup && group->GroupID() > 0)
+ updateGroup = GetById(group->GroupID());
+
+ // try to find the group by name if we didn't find it yet
+ if (!updateGroup)
+ updateGroup = GetByName(group->GroupName());
+
+ if (updateGroup)
+ {
+ updateGroup->SetPath(group->GetPath());
+ updateGroup->SetGroupID(group->GroupID());
+ updateGroup->SetGroupType(group->GroupType());
+ updateGroup->SetPosition(group->GetPosition());
+
+ // don't override properties we only store locally in our PVR database
+ if (!bUpdateFromClient)
+ {
+ updateGroup->SetLastWatched(group->LastWatched());
+ updateGroup->SetHidden(group->IsHidden());
+ updateGroup->SetLastOpened(group->LastOpened());
+ }
+ }
+ else
+ {
+ updateGroup = group;
+ m_groups.emplace_back(updateGroup);
+ }
+ }
+
+ // sort groups
+ SortGroups();
+
+ // persist changes
+ if (bUpdateFromClient)
+ return updateGroup->Persist();
+
+ return true;
+}
+
+void CPVRChannelGroups::SortGroups()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // check if one of the group holds a valid sort position
+ const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(),
+ [](const auto& group) { return (group->GetPosition() > 0); });
+
+ // sort by position if we found a valid sort position
+ if (it != m_groups.cend())
+ {
+ std::sort(m_groups.begin(), m_groups.end(), [](const auto& group1, const auto& group2) {
+ return group1->GetPosition() < group2->GetPosition();
+ });
+ }
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroups::GetChannelGroupMemberByPath(
+ const CPVRChannelsPath& path) const
+{
+ if (path.IsChannel())
+ {
+ const std::shared_ptr<CPVRChannelGroup> group = GetByName(path.GetGroupName());
+ if (group)
+ return group->GetByUniqueID(
+ {CPVRClientUID(path.GetAddonID(), path.GetInstanceID()).GetUID(), path.GetChannelUID()});
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetById(int iGroupId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), [iGroupId](const auto& group) {
+ return group->GroupID() == iGroupId;
+ });
+ return (it != m_groups.cend()) ? (*it) : std::shared_ptr<CPVRChannelGroup>();
+}
+
+std::vector<std::shared_ptr<CPVRChannelGroup>> CPVRChannelGroups::GetGroupsByChannel(const std::shared_ptr<CPVRChannel>& channel, bool bExcludeHidden /* = false */) const
+{
+ std::vector<std::shared_ptr<CPVRChannelGroup>> groups;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::copy_if(m_groups.cbegin(), m_groups.cend(), std::back_inserter(groups),
+ [bExcludeHidden, &channel](const auto& group) {
+ return (!bExcludeHidden || !group->IsHidden()) && group->IsGroupMember(channel);
+ });
+ return groups;
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetGroupByPath(const std::string& strInPath) const
+{
+ const CPVRChannelsPath path(strInPath);
+ if (path.IsChannelGroup())
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(),
+ [&path](const auto& group) { return group->GetPath() == path; });
+ if (it != m_groups.cend())
+ return (*it);
+ }
+ return {};
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetByName(const std::string& strName) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), [&strName](const auto& group) {
+ return group->GroupName() == strName;
+ });
+ return (it != m_groups.cend()) ? (*it) : std::shared_ptr<CPVRChannelGroup>();
+}
+
+bool CPVRChannelGroups::HasValidDataForClients(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients) const
+{
+ return m_failedClientsForChannelGroups.empty() ||
+ std::none_of(clients.cbegin(), clients.cend(),
+ [this](const std::shared_ptr<CPVRClient>& client) {
+ return std::find(m_failedClientsForChannelGroups.cbegin(),
+ m_failedClientsForChannelGroups.cend(),
+ client->GetID()) != m_failedClientsForChannelGroups.cend();
+ });
+}
+
+bool CPVRChannelGroups::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ bool bChannelsOnly /* = false */)
+{
+ bool bSyncWithBackends = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS);
+ bool bUpdateAllGroups = !bChannelsOnly && bSyncWithBackends;
+ bool bReturn = true;
+
+ // sync groups
+ const int iSize = m_groups.size();
+ if (bUpdateAllGroups)
+ {
+ // get channel groups from the clients
+ CServiceBroker::GetPVRManager().Clients()->GetChannelGroups(clients, this,
+ m_failedClientsForChannelGroups);
+ CLog::LogFC(LOGDEBUG, LOGPVR, "{} new user defined {} channel groups fetched from clients",
+ (m_groups.size() - iSize), m_bRadio ? "radio" : "TV");
+ }
+ else if (!bSyncWithBackends)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "'sync channelgroups' is disabled; skipping groups from clients");
+ }
+
+ // sync channels in groups
+ std::vector<std::shared_ptr<CPVRChannelGroup>> groups;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ groups = m_groups;
+ }
+
+ std::vector<std::shared_ptr<CPVRChannelGroup>> emptyGroups;
+
+ for (const auto& group : groups)
+ {
+ if (bUpdateAllGroups || group->IsInternalGroup())
+ {
+ const int iMemberCount = group->Size();
+ if (!group->UpdateFromClients(clients))
+ {
+ CLog::LogFC(LOGERROR, LOGPVR, "Failed to update channel group '{}'", group->GroupName());
+ bReturn = false;
+ }
+
+ const int iChangedMembersCount = static_cast<int>(group->Size()) - iMemberCount;
+ if (iChangedMembersCount > 0)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "{} channel group members added to group '{}'",
+ iChangedMembersCount, group->GroupName());
+ }
+ else if (iChangedMembersCount < 0)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "{} channel group members removed from group '{}'",
+ -iChangedMembersCount, group->GroupName());
+ }
+ else
+ {
+ // could still be changed if same amount of members was removed as was added, but too
+ // complicated to calculate just for debug logging
+ }
+ }
+
+ // remove empty groups if sync with backend is enabled and we have valid data from all clients
+ if (bSyncWithBackends && group->Size() == 0 && !group->IsInternalGroup() &&
+ HasValidDataForClients(clients) && group->HasValidDataForClients(clients))
+ {
+ emptyGroups.emplace_back(group);
+ }
+
+ if (bReturn &&
+ group->IsInternalGroup() &&
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bPVRChannelIconsAutoScan)
+ {
+ CServiceBroker::GetPVRManager().TriggerSearchMissingChannelIcons(group);
+ }
+ }
+
+ for (const auto& group : emptyGroups)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting empty channel group '{}'", group->GroupName());
+ DeleteGroup(group);
+ }
+
+ if (bChannelsOnly)
+ {
+ // changes in the all channels group may require resorting/renumbering of other groups.
+ // if we updated all groups this already has been done while updating the single groups.
+ UpdateChannelNumbersFromAllChannelsGroup();
+ }
+
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated);
+
+ // persist changes
+ return PersistAll() && bReturn;
+}
+
+bool CPVRChannelGroups::UpdateChannelNumbersFromAllChannelsGroup()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::accumulate(
+ m_groups.cbegin(), m_groups.cend(), false, [](bool changed, const auto& group) {
+ return group->UpdateChannelNumbersFromAllChannelsGroup() ? true : changed;
+ });
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::CreateChannelGroup(
+ int iType, const CPVRChannelsPath& path)
+{
+ if (iType == PVR_GROUP_TYPE_INTERNAL)
+ return std::make_shared<CPVRChannelGroupInternal>(path);
+ else
+ return std::make_shared<CPVRChannelGroup>(path, GetGroupAll());
+}
+
+bool CPVRChannelGroups::LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase());
+ if (!database)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // Ensure we have an internal group. It is important that the internal group is created before
+ // loading contents from database and that it gets inserted in front of m_groups. Look at
+ // GetGroupAll() implementation to see why.
+ if (m_groups.empty())
+ {
+ const auto internalGroup = std::make_shared<CPVRChannelGroupInternal>(m_bRadio);
+ m_groups.emplace_back(internalGroup);
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Loading all {} channel groups and members",
+ m_bRadio ? "radio" : "TV");
+
+ // load all channels from the database
+ std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>> channels;
+ database->Get(m_bRadio, clients, channels);
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Fetched {} {} channels from the database", channels.size(),
+ m_bRadio ? "radio" : "TV");
+
+ // load all groups from the database
+ const int iLoaded = database->Get(*this);
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Fetched {} {} groups from the database", iLoaded,
+ m_bRadio ? "radio" : "TV");
+
+ // load all group members from the database
+ for (const auto& group : m_groups)
+ {
+ if (!group->LoadFromDatabase(channels, clients))
+ {
+ CLog::LogFC(LOGERROR, LOGPVR,
+ "Failed to load members of {} channel group '{}' from the database",
+ m_bRadio ? "radio" : "TV", group->GroupName());
+ }
+ }
+
+ // Hide empty groups
+ for (auto it = m_groups.begin(); it != m_groups.end();)
+ {
+ if ((*it)->Size() == 0 && !(*it)->IsInternalGroup())
+ it = m_groups.erase(it);
+ else
+ ++it;
+ }
+
+ return true;
+}
+
+bool CPVRChannelGroups::PersistAll()
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Persisting all channel group changes");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::accumulate(
+ m_groups.cbegin(), m_groups.cend(), true,
+ [](bool success, const auto& group) { return !group->Persist() ? false : success; });
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetGroupAll() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_groups.empty())
+ return m_groups.front();
+
+ return std::shared_ptr<CPVRChannelGroup>();
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetLastGroup() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_groups.empty())
+ return m_groups.back();
+
+ return std::shared_ptr<CPVRChannelGroup>();
+}
+
+GroupMemberPair CPVRChannelGroups::GetLastAndPreviousToLastPlayedChannelGroupMember() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_groups.empty())
+ return {};
+
+ auto groups = m_groups;
+ lock.unlock();
+
+ std::sort(groups.begin(), groups.end(),
+ [](const auto& a, const auto& b) { return a->LastWatched() > b->LastWatched(); });
+
+ // Last is always 'first' of last played group.
+ const GroupMemberPair members = groups[0]->GetLastAndPreviousToLastPlayedChannelGroupMember();
+ std::shared_ptr<CPVRChannelGroupMember> last = members.first;
+
+ // Previous to last is either 'second' of first group or 'first' of second group.
+ std::shared_ptr<CPVRChannelGroupMember> previousToLast = members.second;
+ if (groups.size() > 1 && groups[0]->LastWatched() && groups[1]->LastWatched() && members.second &&
+ members.second->Channel()->LastWatched())
+ {
+ if (groups[1]->LastWatched() >= members.second->Channel()->LastWatched())
+ {
+ const GroupMemberPair membersPreviousToLastPlayedGroup =
+ groups[1]->GetLastAndPreviousToLastPlayedChannelGroupMember();
+ previousToLast = membersPreviousToLastPlayedGroup.first;
+ }
+ }
+
+ return {last, previousToLast};
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetLastOpenedGroup() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::accumulate(
+ m_groups.cbegin(), m_groups.cend(), std::shared_ptr<CPVRChannelGroup>{},
+ [](const std::shared_ptr<CPVRChannelGroup>& last,
+ const std::shared_ptr<CPVRChannelGroup>& group)
+ {
+ return group->LastOpened() > 0 && (!last || group->LastOpened() > last->LastOpened())
+ ? group
+ : last;
+ });
+}
+
+std::vector<std::shared_ptr<CPVRChannelGroup>> CPVRChannelGroups::GetMembers(bool bExcludeHidden /* = false */) const
+{
+ std::vector<std::shared_ptr<CPVRChannelGroup>> groups;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::copy_if(
+ m_groups.cbegin(), m_groups.cend(), std::back_inserter(groups),
+ [bExcludeHidden](const auto& group) { return (!bExcludeHidden || !group->IsHidden()); });
+ return groups;
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetPreviousGroup(const CPVRChannelGroup& group) const
+{
+ {
+ bool bReturnNext = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (std::vector<std::shared_ptr<CPVRChannelGroup>>::const_reverse_iterator it = m_groups.rbegin(); it != m_groups.rend(); ++it)
+ {
+ // return this entry
+ if (bReturnNext && !(*it)->IsHidden())
+ return *it;
+
+ // return the next entry
+ if ((*it)->GroupID() == group.GroupID())
+ bReturnNext = true;
+ }
+
+ // no match return last visible group
+ for (std::vector<std::shared_ptr<CPVRChannelGroup>>::const_reverse_iterator it = m_groups.rbegin(); it != m_groups.rend(); ++it)
+ {
+ if (!(*it)->IsHidden())
+ return *it;
+ }
+ }
+
+ // no match
+ return GetLastGroup();
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetNextGroup(const CPVRChannelGroup& group) const
+{
+ {
+ bool bReturnNext = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (std::vector<std::shared_ptr<CPVRChannelGroup>>::const_iterator it = m_groups.begin(); it != m_groups.end(); ++it)
+ {
+ // return this entry
+ if (bReturnNext && !(*it)->IsHidden())
+ return *it;
+
+ // return the next entry
+ if ((*it)->GroupID() == group.GroupID())
+ bReturnNext = true;
+ }
+
+ // no match return first visible group
+ for (std::vector<std::shared_ptr<CPVRChannelGroup>>::const_iterator it = m_groups.begin(); it != m_groups.end(); ++it)
+ {
+ if (!(*it)->IsHidden())
+ return *it;
+ }
+ }
+
+ // no match
+ return GetFirstGroup();
+}
+
+bool CPVRChannelGroups::AddGroup(const std::string& strName)
+{
+ bool bPersist(false);
+ std::shared_ptr<CPVRChannelGroup> group;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // check if there's no group with the same name yet
+ group = GetByName(strName);
+ if (!group)
+ {
+ // create a new group
+ group.reset(new CPVRChannelGroup(CPVRChannelsPath(m_bRadio, strName), GetGroupAll()));
+
+ m_groups.push_back(group);
+ bPersist = true;
+
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated);
+ }
+ }
+
+ // persist in the db if a new group was added
+ return bPersist ? group->Persist() : true;
+}
+
+bool CPVRChannelGroups::DeleteGroup(const std::shared_ptr<CPVRChannelGroup>& group)
+{
+ // don't delete internal groups
+ if (group->IsInternalGroup())
+ {
+ CLog::LogF(LOGERROR, "Internal channel group cannot be deleted");
+ return false;
+ }
+
+ bool bFound(false);
+
+ // delete the group in this container
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (auto it = m_groups.begin(); it != m_groups.end(); ++it)
+ {
+ if (*it == group || (group->GroupID() > 0 && (*it)->GroupID() == group->GroupID()))
+ {
+ m_groups.erase(it);
+ bFound = true;
+ break;
+ }
+ }
+ }
+
+ if (bFound && group->GroupID() > 0)
+ {
+ // delete the group from the database
+ group->Delete();
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated);
+ }
+ return bFound;
+}
+
+bool CPVRChannelGroups::HideGroup(const std::shared_ptr<CPVRChannelGroup>& group, bool bHide)
+{
+ bool bReturn = false;
+
+ if (group)
+ {
+ if (group->SetHidden(bHide))
+ {
+ // state changed
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated);
+ }
+ bReturn = true;
+ }
+ return bReturn;
+}
+
+bool CPVRChannelGroups::CreateChannelEpgs()
+{
+ bool bReturn(false);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (std::vector<std::shared_ptr<CPVRChannelGroup>>::iterator it = m_groups.begin(); it != m_groups.end(); ++it)
+ {
+ /* Only create EPGs for the internal groups */
+ if ((*it)->IsInternalGroup())
+ bReturn = (*it)->CreateChannelEpgs();
+ }
+ return bReturn;
+}
+
+int CPVRChannelGroups::CleanupCachedImages()
+{
+ int iCleanedImages = 0;
+
+ // cleanup channels
+ iCleanedImages += GetGroupAll()->CleanupCachedImages();
+
+ // cleanup groups
+ std::vector<std::string> urlsToCheck;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::transform(m_groups.cbegin(), m_groups.cend(), std::back_inserter(urlsToCheck),
+ [](const auto& group) { return group->GetPath(); });
+ }
+
+ // kodi-generated thumbnail (see CPVRThumbLoader)
+ const std::string path = StringUtils::Format("pvr://channels/{}/", IsRadio() ? "radio" : "tv");
+ iCleanedImages += CPVRCachedImages::Cleanup({{"pvr", path}}, urlsToCheck, true);
+
+ return iCleanedImages;
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroups.h b/xbmc/pvr/channels/PVRChannelGroups.h
new file mode 100644
index 0000000..709dcfb
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroups.h
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/channels/PVRChannelGroup.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+ class CPVRChannel;
+ class CPVRClient;
+
+ /** A container class for channel groups */
+
+ class CPVRChannelGroups
+ {
+ public:
+ /*!
+ * @brief Create a new group container.
+ * @param bRadio True if this is a container for radio channels, false if it is for tv channels.
+ */
+ explicit CPVRChannelGroups(bool bRadio);
+ virtual ~CPVRChannelGroups();
+
+ /*!
+ * @brief Remove all groups from this container.
+ */
+ void Unload();
+
+ /*!
+ * @brief Load all channel groups and all channels from PVR database.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief Create a channel group matching the given type.
+ * @param iType The type for the group.
+ * @param path The path of the group.
+ * @return The new group.
+ */
+ std::shared_ptr<CPVRChannelGroup> CreateChannelGroup(int iType, const CPVRChannelsPath& path);
+
+ /*!
+ * @return Amount of groups in this container
+ */
+ size_t Size() const
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_groups.size();
+ }
+
+ /*!
+ * @brief Update a group or add it if it's not in here yet.
+ * @param group The group to update.
+ * @param bUpdateFromClient True to save the changes in the db.
+ * @return True if the group was added or update successfully, false otherwise.
+ */
+ bool Update(const std::shared_ptr<CPVRChannelGroup>& group, bool bUpdateFromClient = false);
+
+ /*!
+ * @brief Called by the add-on callback to add a new group
+ * @param group The group to add
+ * @return True when updated, false otherwise
+ */
+ bool UpdateFromClient(const std::shared_ptr<CPVRChannelGroup>& group)
+ {
+ return Update(group, true);
+ }
+
+ /*!
+ * @brief Get a channel group member given its path
+ * @param strPath The path to the channel group member
+ * @return The channel group member, or nullptr if not found
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMemberByPath(
+ const CPVRChannelsPath& path) const;
+
+ /*!
+ * @brief Get a pointer to a channel group given its ID.
+ * @param iGroupId The ID of the group.
+ * @return The group or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetById(int iGroupId) const;
+
+ /*!
+ * @brief Get all groups the given channel is a member.
+ * @param channel The channel.
+ * @param bExcludeHidden Whenever to exclude hidden channel groups.
+ * @return A list of groups the channel is a member.
+ */
+ std::vector<std::shared_ptr<CPVRChannelGroup>> GetGroupsByChannel(const std::shared_ptr<CPVRChannel>& channel, bool bExcludeHidden = false) const;
+
+ /*!
+ * @brief Get a channel group given its path
+ * @param strPath The path to the channel group
+ * @return The channel group, or nullptr if not found
+ */
+ std::shared_ptr<CPVRChannelGroup> GetGroupByPath(const std::string& strPath) const;
+
+ /*!
+ * @brief Get a group given its name.
+ * @param strName The name.
+ * @return The group or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetByName(const std::string& strName) const;
+
+ /*!
+ * @brief Get the group that contains all channels.
+ * @return The group that contains all channels.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetGroupAll() const;
+
+ /*!
+ * @return The first group in this container, which always is the group with all channels.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetFirstGroup() const { return GetGroupAll(); }
+
+ /*!
+ * @return The last group in this container.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetLastGroup() const;
+
+ /*!
+ * @return The last and previous to last played channel group members. pair.first contains the last, pair.second the previous to last member.
+ */
+ GroupMemberPair GetLastAndPreviousToLastPlayedChannelGroupMember() const;
+
+ /*!
+ * @return The last opened group.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetLastOpenedGroup() const;
+
+ /*!
+ * @brief Get the list of groups.
+ * @param groups The list to store the results in.
+ * @param bExcludeHidden Whenever to exclude hidden channel groups.
+ * @return The amount of items that were added.
+ */
+ std::vector<std::shared_ptr<CPVRChannelGroup>> GetMembers(bool bExcludeHidden = false) const;
+
+ /*!
+ * @brief Get the previous group in this container.
+ * @param group The current group.
+ * @return The previous group or the group containing all channels if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetPreviousGroup(const CPVRChannelGroup& group) const;
+
+ /*!
+ * @brief Get the next group in this container.
+ * @param group The current group.
+ * @return The next group or the group containing all channels if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetNextGroup(const CPVRChannelGroup& group) const;
+
+ /*!
+ * @brief Add a group to this container.
+ * @param strName The name of the group.
+ * @return True if the group was added, false otherwise.
+ */
+ bool AddGroup(const std::string& strName);
+
+ /*!
+ * @brief Remove a group from this container and delete it from the database.
+ * @param group The group to delete.
+ * @return True if it was deleted successfully, false if not.
+ */
+ bool DeleteGroup(const std::shared_ptr<CPVRChannelGroup>& group);
+
+ /*!
+ * @brief Hide/unhide a group in this container.
+ * @param group The group to hide/unhide.
+ * @param bHide True to hide the group, false to unhide it.
+ * @return True on success, false otherwise.
+ */
+ bool HideGroup(const std::shared_ptr<CPVRChannelGroup>& group, bool bHide);
+
+ /*!
+ * @brief Create EPG tags for all channels of the internal group.
+ * @return True if EPG tags where created successfully, false if not.
+ */
+ bool CreateChannelEpgs();
+
+ /*!
+ * @brief Persist all changes in channel groups.
+ * @return True if everything was persisted, false otherwise.
+ */
+ bool PersistAll();
+
+ /*!
+ * @return True when this container contains radio groups, false otherwise
+ */
+ bool IsRadio() const { return m_bRadio; }
+
+ /*!
+ * @brief Update data with groups and channels from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param bChannelsOnly Set to true to only update channels, not the groups themselves.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ bool bChannelsOnly = false);
+
+ /*!
+ * @brief Update the channel numbers across the channel groups from the all channels group
+ * @return True if any channel number was changed, false otherwise.
+ */
+ bool UpdateChannelNumbersFromAllChannelsGroup();
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+ private:
+ void SortGroups();
+
+ /*!
+ * @brief Check, whether data for given pvr clients are currently valid. For instance, data
+ * can be invalid because the client's backend was offline when data was last queried.
+ * @param clients The clients to check. Check all active clients if vector is empty.
+ * @return True, if data is currently valid, false otherwise.
+ */
+ bool HasValidDataForClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) const;
+
+ bool m_bRadio; /*!< true if this is a container for radio channels, false if it is for tv channels */
+ std::vector<std::shared_ptr<CPVRChannelGroup>> m_groups; /*!< the groups in this container */
+ mutable CCriticalSection m_critSection;
+ std::vector<int> m_failedClientsForChannelGroups;
+ };
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroupsContainer.cpp b/xbmc/pvr/channels/PVRChannelGroupsContainer.cpp
new file mode 100644
index 0000000..5f65cdd
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupsContainer.cpp
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRChannelGroupsContainer.h"
+
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+
+using namespace PVR;
+
+CPVRChannelGroupsContainer::CPVRChannelGroupsContainer() :
+ m_groupsRadio(new CPVRChannelGroups(true)),
+ m_groupsTV(new CPVRChannelGroups(false))
+{
+}
+
+CPVRChannelGroupsContainer::~CPVRChannelGroupsContainer()
+{
+ Unload();
+ delete m_groupsRadio;
+ delete m_groupsTV;
+}
+
+bool CPVRChannelGroupsContainer::Update(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ return LoadFromDatabase(clients) && UpdateFromClients(clients);
+}
+
+bool CPVRChannelGroupsContainer::LoadFromDatabase(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ return m_groupsTV->LoadFromDatabase(clients) && m_groupsRadio->LoadFromDatabase(clients);
+}
+
+bool CPVRChannelGroupsContainer::UpdateFromClients(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients, bool bChannelsOnly /* = false */)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bIsUpdating)
+ return false;
+ m_bIsUpdating = true;
+ lock.unlock();
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updating {}", bChannelsOnly ? "channels" : "channel groups");
+ bool bReturn = m_groupsTV->UpdateFromClients(clients, bChannelsOnly) &&
+ m_groupsRadio->UpdateFromClients(clients, bChannelsOnly);
+
+ lock.lock();
+ m_bIsUpdating = false;
+ lock.unlock();
+
+ return bReturn;
+}
+
+void CPVRChannelGroupsContainer::Unload()
+{
+ m_groupsRadio->Unload();
+ m_groupsTV->Unload();
+}
+
+CPVRChannelGroups* CPVRChannelGroupsContainer::Get(bool bRadio) const
+{
+ return bRadio ? m_groupsRadio : m_groupsTV;
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroupsContainer::GetGroupAll(bool bRadio) const
+{
+ return Get(bRadio)->GetGroupAll();
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroupsContainer::GetByIdFromAll(int iGroupId) const
+{
+ std::shared_ptr<CPVRChannelGroup> group = m_groupsTV->GetById(iGroupId);
+ if (!group)
+ group = m_groupsRadio->GetById(iGroupId);
+
+ return group;
+}
+
+std::shared_ptr<CPVRChannel> CPVRChannelGroupsContainer::GetChannelById(int iChannelId) const
+{
+ std::shared_ptr<CPVRChannel> channel = m_groupsTV->GetGroupAll()->GetByChannelID(iChannelId);
+ if (!channel)
+ channel = m_groupsRadio->GetGroupAll()->GetByChannelID(iChannelId);
+
+ return channel;
+}
+
+std::shared_ptr<CPVRChannel> CPVRChannelGroupsContainer::GetChannelForEpgTag(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (!epgTag)
+ return {};
+
+ return Get(epgTag->IsRadio())->GetGroupAll()->GetByUniqueID(epgTag->UniqueChannelID(), epgTag->ClientID());
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroupsContainer::GetChannelGroupMemberByPath(
+ const std::string& strPath) const
+{
+ const CPVRChannelsPath path(strPath);
+ if (path.IsValid())
+ return Get(path.IsRadio())->GetChannelGroupMemberByPath(path);
+
+ return {};
+}
+
+std::shared_ptr<CPVRChannel> CPVRChannelGroupsContainer::GetByPath(const std::string& strPath) const
+{
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember = GetChannelGroupMemberByPath(strPath);
+ if (groupMember)
+ return groupMember->Channel();
+
+ return {};
+}
+
+std::shared_ptr<CPVRChannel> CPVRChannelGroupsContainer::GetByUniqueID(int iUniqueChannelId, int iClientID) const
+{
+ std::shared_ptr<CPVRChannel> channel;
+ std::shared_ptr<CPVRChannelGroup> channelgroup = GetGroupAllTV();
+ if (channelgroup)
+ channel = channelgroup->GetByUniqueID(iUniqueChannelId, iClientID);
+
+ if (!channelgroup || !channel)
+ channelgroup = GetGroupAllRadio();
+ if (channelgroup)
+ channel = channelgroup->GetByUniqueID(iUniqueChannelId, iClientID);
+
+ return channel;
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroupsContainer::
+ GetLastPlayedChannelGroupMember() const
+{
+ std::shared_ptr<CPVRChannelGroupMember> channelTV =
+ m_groupsTV->GetGroupAll()->GetLastPlayedChannelGroupMember();
+ std::shared_ptr<CPVRChannelGroupMember> channelRadio =
+ m_groupsRadio->GetGroupAll()->GetLastPlayedChannelGroupMember();
+
+ if (!channelTV || (channelRadio &&
+ channelRadio->Channel()->LastWatched() > channelTV->Channel()->LastWatched()))
+ return channelRadio;
+
+ return channelTV;
+}
+
+bool CPVRChannelGroupsContainer::CreateChannelEpgs()
+{
+ bool bReturn = m_groupsTV->CreateChannelEpgs();
+ bReturn &= m_groupsRadio->CreateChannelEpgs();
+ return bReturn;
+}
+
+int CPVRChannelGroupsContainer::CleanupCachedImages()
+{
+ return m_groupsTV->CleanupCachedImages() + m_groupsRadio->CleanupCachedImages();
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroupsContainer.h b/xbmc/pvr/channels/PVRChannelGroupsContainer.h
new file mode 100644
index 0000000..d7b7f57
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupsContainer.h
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <vector>
+
+namespace PVR
+{
+ class CPVRChannel;
+ class CPVRChannelGroup;
+ class CPVRChannelGroupMember;
+ class CPVRChannelGroups;
+ class CPVRClient;
+ class CPVREpgInfoTag;
+
+ class CPVRChannelGroupsContainer
+ {
+ public:
+ /*!
+ * @brief Create a new container for all channel groups
+ */
+ CPVRChannelGroupsContainer();
+
+ /*!
+ * @brief Destroy this container.
+ */
+ virtual ~CPVRChannelGroupsContainer();
+
+ /*!
+ * @brief Update all channel groups and all channels from PVR database and from given clients.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool Update(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief Update data with groups and channels from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param bChannelsOnly Set to true to only update channels, not the groups themselves.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ bool bChannelsOnly = false);
+
+ /*!
+ * @brief Unload and destruct all channel groups and all channels in them.
+ */
+ void Unload();
+
+ /*!
+ * @brief Get the TV channel groups.
+ * @return The TV channel groups.
+ */
+ CPVRChannelGroups* GetTV() const { return Get(false); }
+
+ /*!
+ * @brief Get the radio channel groups.
+ * @return The radio channel groups.
+ */
+ CPVRChannelGroups* GetRadio() const { return Get(true); }
+
+ /*!
+ * @brief Get the radio or TV channel groups.
+ * @param bRadio If true, get the radio channel groups. Get the TV channel groups otherwise.
+ * @return The requested groups.
+ */
+ CPVRChannelGroups* Get(bool bRadio) const;
+
+ /*!
+ * @brief Get the group containing all TV channels.
+ * @return The group containing all TV channels.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetGroupAllTV() const { return GetGroupAll(false); }
+
+ /*!
+ * @brief Get the group containing all radio channels.
+ * @return The group containing all radio channels.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetGroupAllRadio() const { return GetGroupAll(true); }
+
+ /*!
+ * @brief Get the group containing all TV or radio channels.
+ * @param bRadio If true, get the group containing all radio channels. Get the group containing all TV channels otherwise.
+ * @return The requested group.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetGroupAll(bool bRadio) const;
+
+ /*!
+ * @brief Get a group given it's ID.
+ * @param iGroupId The ID of the group.
+ * @return The requested group or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetByIdFromAll(int iGroupId) const;
+
+ /*!
+ * @brief Get a channel given it's database ID.
+ * @param iChannelId The ID of the channel.
+ * @return The channel or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannel> GetChannelById(int iChannelId) const;
+
+ /*!
+ * @brief Get the channel for the given epg tag.
+ * @param epgTag The epg tag.
+ * @return The channel.
+ */
+ std::shared_ptr<CPVRChannel> GetChannelForEpgTag(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ /*!
+ * @brief Get a channel given it's path.
+ * @param strPath The path.
+ * @return The channel or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannel> GetByPath(const std::string& strPath) const;
+
+ /*!
+ * @brief Get a channel group member given it's path.
+ * @param strPath The path.
+ * @return The channel group member or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMemberByPath(
+ const std::string& strPath) const;
+
+ /*!
+ * @brief Get a channel given it's channel ID from all containers.
+ * @param iUniqueChannelId The unique channel id on the client.
+ * @param iClientID The ID of the client.
+ * @return The channel or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannel> GetByUniqueID(int iUniqueChannelId, int iClientID) const;
+
+ /*!
+ * @brief Get the channel group member that was played last.
+ * @return The requested channel group member or nullptr.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetLastPlayedChannelGroupMember() const;
+
+ /*!
+ * @brief Create EPG tags for channels in all internal channel groups.
+ * @return True if EPG tags were created successfully.
+ */
+ bool CreateChannelEpgs();
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+ private:
+ CPVRChannelGroupsContainer& operator=(const CPVRChannelGroupsContainer&) = delete;
+ CPVRChannelGroupsContainer(const CPVRChannelGroupsContainer&) = delete;
+
+ /*!
+ * @brief Load all channel groups and all channels from PVR database.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ CPVRChannelGroups* m_groupsRadio; /*!< all radio channel groups */
+ CPVRChannelGroups* m_groupsTV; /*!< all TV channel groups */
+ CCriticalSection m_critSection;
+ bool m_bIsUpdating = false;
+ };
+}
diff --git a/xbmc/pvr/channels/PVRChannelNumber.cpp b/xbmc/pvr/channels/PVRChannelNumber.cpp
new file mode 100644
index 0000000..403938e
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelNumber.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRChannelNumber.h"
+
+#include "utils/StringUtils.h"
+
+using namespace PVR;
+
+const char CPVRChannelNumber::SEPARATOR = '.';
+
+std::string CPVRChannelNumber::FormattedChannelNumber() const
+{
+ return ToString(SEPARATOR);
+}
+
+std::string CPVRChannelNumber::SortableChannelNumber() const
+{
+ // Note: The subchannel separator is a character that does not work for a
+ // SortItem (at least not on all platforms). See SortUtils::Sort for
+ // details. Only numbers, letters and the blank are safe to use.
+ return ToString(' ');
+}
+
+std::string CPVRChannelNumber::ToString(char separator) const
+{
+ if (m_iSubChannelNumber == 0)
+ return std::to_string(m_iChannelNumber);
+ else
+ return StringUtils::Format("{}{}{}", m_iChannelNumber, separator, m_iSubChannelNumber);
+}
diff --git a/xbmc/pvr/channels/PVRChannelNumber.h b/xbmc/pvr/channels/PVRChannelNumber.h
new file mode 100644
index 0000000..b6e2efc
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelNumber.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace PVR
+{
+ class CPVRChannelNumber
+ {
+ public:
+ CPVRChannelNumber() = default;
+
+ constexpr CPVRChannelNumber(unsigned int iChannelNumber, unsigned int iSubChannelNumber)
+ : m_iChannelNumber(iChannelNumber), m_iSubChannelNumber(iSubChannelNumber) {}
+
+ constexpr bool operator ==(const CPVRChannelNumber& right) const
+ {
+ return (m_iChannelNumber == right.m_iChannelNumber &&
+ m_iSubChannelNumber == right.m_iSubChannelNumber);
+ }
+
+ constexpr bool operator !=(const CPVRChannelNumber& right) const
+ {
+ return !(*this == right);
+ }
+
+ constexpr bool operator <(const CPVRChannelNumber& right) const
+ {
+ return m_iChannelNumber == right.m_iChannelNumber
+ ? m_iSubChannelNumber < right.m_iSubChannelNumber
+ : m_iChannelNumber < right.m_iChannelNumber;
+ }
+
+ /*!
+ * @brief Check whether this channel number is valid (main channel number > 0).
+ * @return True if valid, false otherwise..
+ */
+ constexpr bool IsValid() const { return m_iChannelNumber > 0; }
+
+ /*!
+ * @brief Set the primary channel number.
+ * @return The channel number.
+ */
+ constexpr unsigned int GetChannelNumber() const
+ {
+ return m_iChannelNumber;
+ }
+
+ /*!
+ * @brief Set the sub channel number.
+ * @return The sub channel number (ATSC).
+ */
+ constexpr unsigned int GetSubChannelNumber() const
+ {
+ return m_iSubChannelNumber;
+ }
+
+ /*!
+ * @brief The character used to separate channel and subchannel number.
+ */
+ static const char SEPARATOR; // '.'
+
+ /*!
+ * @brief Get a string representation for the channel number.
+ * @return The formatted string in the form <channel>SEPARATOR<subchannel>.
+ */
+ std::string FormattedChannelNumber() const;
+
+ /*!
+ * @brief Get a string representation for the channel number that can be used for SortItems.
+ * @return The sortable string in the form <channel> <subchannel>.
+ */
+ std::string SortableChannelNumber() const;
+
+ private:
+ std::string ToString(char separator) const;
+
+ unsigned int m_iChannelNumber = 0;
+ unsigned int m_iSubChannelNumber = 0;
+ };
+}
diff --git a/xbmc/pvr/channels/PVRChannelsPath.cpp b/xbmc/pvr/channels/PVRChannelsPath.cpp
new file mode 100644
index 0000000..4ae2e28
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelsPath.cpp
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRChannelsPath.h"
+
+#include "URL.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+const std::string CPVRChannelsPath::PATH_TV_CHANNELS = "pvr://channels/tv/";
+const std::string CPVRChannelsPath::PATH_RADIO_CHANNELS = "pvr://channels/radio/";
+
+
+CPVRChannelsPath::CPVRChannelsPath(const std::string& strPath)
+{
+ std::string strVarPath = TrimSlashes(strPath);
+ const std::vector<std::string> segments = URIUtils::SplitPath(strVarPath);
+
+ for (const std::string& segment : segments)
+ {
+ switch (m_kind)
+ {
+ case Kind::INVALID:
+ if (segment == "pvr://")
+ m_kind = Kind::PROTO; // pvr:// followed by something => go on
+ else if (segment == "pvr:" && segments.size() == 1) // just pvr:// => invalid
+ strVarPath = "pvr:/";
+ break;
+
+ case Kind::PROTO:
+ if (segment == "channels")
+ m_kind = Kind::EMPTY; // pvr://channels
+ else
+ m_kind = Kind::INVALID;
+ break;
+
+ case Kind::EMPTY:
+ if (segment == "tv" || segment == "radio")
+ {
+ m_kind = Kind::ROOT; // pvr://channels/(tv|radio)
+ m_bRadio = (segment == "radio");
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Invalid channels path '{}' - channel root segment syntax error.",
+ strPath);
+ m_kind = Kind::INVALID;
+ }
+ break;
+
+ case Kind::ROOT:
+ m_kind = Kind::GROUP; // pvr://channels/(tv|radio)/<groupname>
+ m_group = CURL::Decode(segment);
+ break;
+
+ case Kind::GROUP:
+ {
+ std::vector<std::string> tokens = StringUtils::Split(segment, "_");
+ if (tokens.size() == 2)
+ {
+ std::vector<std::string> instance = StringUtils::Split(tokens[0], "@");
+ if (instance.size() == 2)
+ {
+ m_instanceID = std::atoi(instance[0].c_str());
+ m_addonID = instance[1];
+ }
+ else
+ {
+ m_instanceID = ADDON::ADDON_SINGLETON_INSTANCE_ID;
+ m_addonID = tokens[0];
+ }
+
+ tokens = StringUtils::Split(tokens[1], ".");
+ if (tokens.size() == 2 && tokens[1] == "pvr")
+ {
+ std::string channelUID = tokens[0];
+ if (!channelUID.empty() && channelUID.find_first_not_of("0123456789") == std::string::npos)
+ m_iChannelUID = std::atoi(channelUID.c_str());
+ }
+ }
+
+ if (!m_addonID.empty() && m_iChannelUID >= 0)
+ {
+ m_kind = Kind::
+ CHANNEL; // pvr://channels/(tv|radio)/<groupname>/<instanceid>@<addonid>_<channeluid>.pvr
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Invalid channels path '{}' - channel segment syntax error.",
+ strPath);
+ m_kind = Kind::INVALID;
+ }
+ break;
+ }
+
+ case Kind::CHANNEL:
+ CLog::LogF(LOGERROR, "Invalid channels path '{}' - too many path segments.", strPath);
+ m_kind = Kind::INVALID; // too many segments
+ break;
+ }
+
+ if (m_kind == Kind::INVALID)
+ break;
+ }
+
+ // append slash to all folders
+ if (m_kind < Kind::CHANNEL)
+ strVarPath.append("/");
+
+ m_path = strVarPath;
+}
+
+CPVRChannelsPath::CPVRChannelsPath(bool bRadio, bool bHidden, const std::string& strGroupName)
+ : m_bRadio(bRadio)
+{
+ if (!bHidden && strGroupName.empty())
+ m_kind = Kind::EMPTY;
+ else
+ m_kind = Kind::GROUP;
+
+ m_group = bHidden ? ".hidden" : strGroupName;
+ m_path =
+ StringUtils::Format("pvr://channels/{}/{}", bRadio ? "radio" : "tv", CURL::Encode(m_group));
+
+ if (!m_group.empty())
+ m_path.append("/");
+}
+
+CPVRChannelsPath::CPVRChannelsPath(bool bRadio, const std::string& strGroupName)
+ : m_bRadio(bRadio)
+{
+ if (strGroupName.empty())
+ m_kind = Kind::EMPTY;
+ else
+ m_kind = Kind::GROUP;
+
+ m_group = strGroupName;
+ m_path =
+ StringUtils::Format("pvr://channels/{}/{}", bRadio ? "radio" : "tv", CURL::Encode(m_group));
+
+ if (!m_group.empty())
+ m_path.append("/");
+}
+
+CPVRChannelsPath::CPVRChannelsPath(bool bRadio,
+ const std::string& strGroupName,
+ const std::string& strAddonID,
+ ADDON::AddonInstanceId instanceID,
+ int iChannelUID)
+ : m_bRadio(bRadio)
+{
+ if (!strGroupName.empty() && !strAddonID.empty() && iChannelUID >= 0)
+ {
+ m_kind = Kind::CHANNEL;
+ m_group = strGroupName;
+ m_addonID = strAddonID;
+ m_instanceID = instanceID;
+ m_iChannelUID = iChannelUID;
+ m_path = StringUtils::Format("pvr://channels/{}/{}/{}@{}_{}.pvr", bRadio ? "radio" : "tv",
+ CURL::Encode(m_group), m_instanceID, m_addonID, m_iChannelUID);
+ }
+}
+
+bool CPVRChannelsPath::IsHiddenChannelGroup() const
+{
+ return m_kind == Kind::GROUP && m_group == ".hidden";
+}
+
+std::string CPVRChannelsPath::TrimSlashes(const std::string& strString)
+{
+ std::string strTrimmed = strString;
+ while (!strTrimmed.empty() && strTrimmed.front() == '/')
+ strTrimmed.erase(0, 1);
+
+ while (!strTrimmed.empty() && strTrimmed.back() == '/')
+ strTrimmed.pop_back();
+
+ return strTrimmed;
+}
diff --git a/xbmc/pvr/channels/PVRChannelsPath.h b/xbmc/pvr/channels/PVRChannelsPath.h
new file mode 100644
index 0000000..7b825c1
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelsPath.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/IAddon.h"
+
+class CDateTime;
+
+namespace PVR
+{
+ class CPVRChannelsPath
+ {
+ public:
+ static const std::string PATH_TV_CHANNELS;
+ static const std::string PATH_RADIO_CHANNELS;
+
+ explicit CPVRChannelsPath(const std::string& strPath);
+ CPVRChannelsPath(bool bRadio, const std::string& strGroupName);
+ CPVRChannelsPath(bool bRadio, bool bHidden, const std::string& strGroupName);
+ CPVRChannelsPath(bool bRadio,
+ const std::string& strGroupName,
+ const std::string& strAddonID,
+ ADDON::AddonInstanceId instanceID,
+ int iChannelUID);
+
+ operator std::string() const { return m_path; }
+ bool operator ==(const CPVRChannelsPath& right) const { return m_path == right.m_path; }
+ bool operator !=(const CPVRChannelsPath& right) const { return !(*this == right); }
+
+ bool IsValid() const { return m_kind > Kind::PROTO; }
+
+ bool IsEmpty() const { return m_kind == Kind::EMPTY; }
+ bool IsChannelsRoot() const { return m_kind == Kind::ROOT; }
+ bool IsChannelGroup() const { return m_kind == Kind::GROUP; }
+ bool IsChannel() const { return m_kind == Kind::CHANNEL; }
+
+ bool IsHiddenChannelGroup() const;
+
+ bool IsRadio() const { return m_bRadio; }
+
+ const std::string& GetGroupName() const { return m_group; }
+ const std::string& GetAddonID() const { return m_addonID; }
+ ADDON::AddonInstanceId GetInstanceID() const { return m_instanceID; }
+ int GetChannelUID() const { return m_iChannelUID; }
+
+ private:
+ static std::string TrimSlashes(const std::string& strString);
+
+ enum class Kind
+ {
+ INVALID,
+ PROTO,
+ EMPTY,
+ ROOT,
+ GROUP,
+ CHANNEL,
+ };
+
+ Kind m_kind = Kind::INVALID;
+ bool m_bRadio = false;;
+ std::string m_path;
+ std::string m_group;
+ std::string m_addonID;
+ ADDON::AddonInstanceId m_instanceID{ADDON::ADDON_SINGLETON_INSTANCE_ID};
+ int m_iChannelUID = -1;
+ };
+}
diff --git a/xbmc/pvr/channels/PVRRadioRDSInfoTag.cpp b/xbmc/pvr/channels/PVRRadioRDSInfoTag.cpp
new file mode 100644
index 0000000..e1ee649
--- /dev/null
+++ b/xbmc/pvr/channels/PVRRadioRDSInfoTag.cpp
@@ -0,0 +1,736 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRRadioRDSInfoTag.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/Archive.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+#include <mutex>
+#include <string>
+#include <utility>
+
+using namespace PVR;
+
+CPVRRadioRDSInfoTag::CPVRRadioRDSInfoTag()
+{
+ Clear();
+}
+
+void CPVRRadioRDSInfoTag::Serialize(CVariant& value) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ value["strLanguage"] = m_strLanguage;
+ value["strCountry"] = m_strCountry;
+ value["strTitle"] = m_strTitle;
+ value["strBand"] = m_strBand;
+ value["strArtist"] = m_strArtist;
+ value["strComposer"] = m_strComposer;
+ value["strConductor"] = m_strConductor;
+ value["strAlbum"] = m_strAlbum;
+ value["iAlbumTracknumber"] = m_iAlbumTracknumber;
+ value["strProgStation"] = m_strProgStation;
+ value["strProgStyle"] = m_strProgStyle;
+ value["strProgHost"] = m_strProgHost;
+ value["strProgWebsite"] = m_strProgWebsite;
+ value["strProgNow"] = m_strProgNow;
+ value["strProgNext"] = m_strProgNext;
+ value["strPhoneHotline"] = m_strPhoneHotline;
+ value["strEMailHotline"] = m_strEMailHotline;
+ value["strPhoneStudio"] = m_strPhoneStudio;
+ value["strEMailStudio"] = m_strEMailStudio;
+ value["strSMSStudio"] = m_strSMSStudio;
+ value["strRadioStyle"] = m_strRadioStyle;
+}
+
+void CPVRRadioRDSInfoTag::Archive(CArchive& ar)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (ar.IsStoring())
+ {
+ ar << m_strLanguage;
+ ar << m_strCountry;
+ ar << m_strTitle;
+ ar << m_strBand;
+ ar << m_strArtist;
+ ar << m_strComposer;
+ ar << m_strConductor;
+ ar << m_strAlbum;
+ ar << m_iAlbumTracknumber;
+ ar << m_strProgStation;
+ ar << m_strProgStyle;
+ ar << m_strProgHost;
+ ar << m_strProgWebsite;
+ ar << m_strProgNow;
+ ar << m_strProgNext;
+ ar << m_strPhoneHotline;
+ ar << m_strEMailHotline;
+ ar << m_strPhoneStudio;
+ ar << m_strEMailStudio;
+ ar << m_strSMSStudio;
+ ar << m_strRadioStyle;
+ }
+ else
+ {
+ ar >> m_strLanguage;
+ ar >> m_strCountry;
+ ar >> m_strTitle;
+ ar >> m_strBand;
+ ar >> m_strArtist;
+ ar >> m_strComposer;
+ ar >> m_strConductor;
+ ar >> m_strAlbum;
+ ar >> m_iAlbumTracknumber;
+ ar >> m_strProgStation;
+ ar >> m_strProgStyle;
+ ar >> m_strProgHost;
+ ar >> m_strProgWebsite;
+ ar >> m_strProgNow;
+ ar >> m_strProgNext;
+ ar >> m_strPhoneHotline;
+ ar >> m_strEMailHotline;
+ ar >> m_strPhoneStudio;
+ ar >> m_strEMailStudio;
+ ar >> m_strSMSStudio;
+ ar >> m_strRadioStyle;
+ }
+}
+
+bool CPVRRadioRDSInfoTag::operator==(const CPVRRadioRDSInfoTag& right) const
+{
+ if (this == &right)
+ return true;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return (
+ m_strLanguage == right.m_strLanguage && m_strCountry == right.m_strCountry &&
+ m_strTitle == right.m_strTitle && m_strBand == right.m_strBand &&
+ m_strArtist == right.m_strArtist && m_strComposer == right.m_strComposer &&
+ m_strConductor == right.m_strConductor && m_strAlbum == right.m_strAlbum &&
+ m_iAlbumTracknumber == right.m_iAlbumTracknumber && m_strInfoNews == right.m_strInfoNews &&
+ m_strInfoNewsLocal == right.m_strInfoNewsLocal && m_strInfoSport == right.m_strInfoSport &&
+ m_strInfoStock == right.m_strInfoStock && m_strInfoWeather == right.m_strInfoWeather &&
+ m_strInfoLottery == right.m_strInfoLottery && m_strInfoOther == right.m_strInfoOther &&
+ m_strProgStyle == right.m_strProgStyle && m_strProgHost == right.m_strProgHost &&
+ m_strProgStation == right.m_strProgStation && m_strProgWebsite == right.m_strProgWebsite &&
+ m_strProgNow == right.m_strProgNow && m_strProgNext == right.m_strProgNext &&
+ m_strPhoneHotline == right.m_strPhoneHotline &&
+ m_strEMailHotline == right.m_strEMailHotline && m_strPhoneStudio == right.m_strPhoneStudio &&
+ m_strEMailStudio == right.m_strEMailStudio && m_strSMSStudio == right.m_strSMSStudio &&
+ m_strRadioStyle == right.m_strRadioStyle && m_strInfoHoroscope == right.m_strInfoHoroscope &&
+ m_strInfoCinema == right.m_strInfoCinema && m_strComment == right.m_strComment &&
+ m_strEditorialStaff == right.m_strEditorialStaff && m_strRadioText == right.m_strRadioText &&
+ m_strProgramServiceText == right.m_strProgramServiceText &&
+ m_strProgramServiceLine0 == right.m_strProgramServiceLine0 &&
+ m_strProgramServiceLine1 == right.m_strProgramServiceLine1 &&
+ m_bHaveRadioText == right.m_bHaveRadioText &&
+ m_bHaveRadioTextPlus == right.m_bHaveRadioTextPlus);
+}
+
+bool CPVRRadioRDSInfoTag::operator !=(const CPVRRadioRDSInfoTag& right) const
+{
+ if (this == &right)
+ return false;
+
+ return !(*this == right);
+}
+
+void CPVRRadioRDSInfoTag::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_RDS_SpeechActive = false;
+
+ ResetSongInformation();
+
+ m_strLanguage.erase();
+ m_strCountry.erase();
+ m_strComment.erase();
+ m_strInfoNews.Clear();
+ m_strInfoNewsLocal.Clear();
+ m_strInfoSport.Clear();
+ m_strInfoStock.Clear();
+ m_strInfoWeather.Clear();
+ m_strInfoLottery.Clear();
+ m_strInfoOther.Clear();
+ m_strInfoHoroscope.Clear();
+ m_strInfoCinema.Clear();
+ m_strProgStyle.erase();
+ m_strProgHost.erase();
+ m_strProgStation.erase();
+ m_strProgWebsite.erase();
+ m_strPhoneHotline.erase();
+ m_strEMailHotline.erase();
+ m_strPhoneStudio.erase();
+ m_strEMailStudio.erase();
+ m_strSMSStudio.erase();
+ m_strRadioStyle = "unknown";
+ m_strEditorialStaff.Clear();
+ m_strRadioText.Clear();
+ m_strProgramServiceText.Clear();
+ m_strProgramServiceLine0.erase();
+ m_strProgramServiceLine1.erase();
+
+ m_bHaveRadioText = false;
+ m_bHaveRadioTextPlus = false;
+}
+
+void CPVRRadioRDSInfoTag::ResetSongInformation()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_strTitle.erase();
+ m_strBand.erase();
+ m_strArtist.erase();
+ m_strComposer.erase();
+ m_strConductor.erase();
+ m_strAlbum.erase();
+ m_iAlbumTracknumber = 0;
+}
+
+void CPVRRadioRDSInfoTag::SetSpeechActive(bool active)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_RDS_SpeechActive = active;
+}
+
+void CPVRRadioRDSInfoTag::SetLanguage(const std::string& strLanguage)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strLanguage = Trim(strLanguage);
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetLanguage() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strLanguage;
+}
+
+void CPVRRadioRDSInfoTag::SetCountry(const std::string& strCountry)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strCountry = Trim(strCountry);
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetCountry() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strCountry;
+}
+
+void CPVRRadioRDSInfoTag::SetTitle(const std::string& strTitle)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strTitle = Trim(strTitle);
+}
+
+void CPVRRadioRDSInfoTag::SetArtist(const std::string& strArtist)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strArtist = Trim(strArtist);
+}
+
+void CPVRRadioRDSInfoTag::SetBand(const std::string& strBand)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strBand = Trim(strBand);
+ g_charsetConverter.unknownToUTF8(m_strBand);
+}
+
+void CPVRRadioRDSInfoTag::SetComposer(const std::string& strComposer)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strComposer = Trim(strComposer);
+ g_charsetConverter.unknownToUTF8(m_strComposer);
+}
+
+void CPVRRadioRDSInfoTag::SetConductor(const std::string& strConductor)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strConductor = Trim(strConductor);
+ g_charsetConverter.unknownToUTF8(m_strConductor);
+}
+
+void CPVRRadioRDSInfoTag::SetAlbum(const std::string& strAlbum)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strAlbum = Trim(strAlbum);
+ g_charsetConverter.unknownToUTF8(m_strAlbum);
+}
+
+void CPVRRadioRDSInfoTag::SetAlbumTrackNumber(int track)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iAlbumTracknumber = track;
+}
+
+void CPVRRadioRDSInfoTag::SetComment(const std::string& strComment)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strComment = Trim(strComment);
+ g_charsetConverter.unknownToUTF8(m_strComment);
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetTitle() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strTitle;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetArtist() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strArtist;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetBand() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strBand;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetComposer() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strComposer;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetConductor() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strConductor;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetAlbum() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strAlbum;
+}
+
+int CPVRRadioRDSInfoTag::GetAlbumTrackNumber() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iAlbumTracknumber;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetComment() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strComment;
+}
+
+void CPVRRadioRDSInfoTag::SetInfoNews(const std::string& strNews)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoNews.Add(strNews);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoNews() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoNews.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoNewsLocal(const std::string& strNews)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoNewsLocal.Add(strNews);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoNewsLocal() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoNewsLocal.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoSport(const std::string& strSport)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoSport.Add(strSport);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoSport() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoSport.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoStock(const std::string& strStock)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoStock.Add(strStock);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoStock() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoStock.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoWeather(const std::string& strWeather)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoWeather.Add(strWeather);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoWeather() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoWeather.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoLottery(const std::string& strLottery)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoLottery.Add(strLottery);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoLottery() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoLottery.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetEditorialStaff(const std::string& strEditorialStaff)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strEditorialStaff.Add(strEditorialStaff);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetEditorialStaff() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strEditorialStaff.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoHoroscope(const std::string& strHoroscope)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoHoroscope.Add(strHoroscope);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoHoroscope() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoHoroscope.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoCinema(const std::string& strCinema)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoCinema.Add(strCinema);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoCinema() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoCinema.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoOther(const std::string& strOther)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoOther.Add(strOther);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoOther() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoOther.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetProgStation(const std::string& strProgStation)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgStation = Trim(strProgStation);
+ g_charsetConverter.unknownToUTF8(m_strProgStation);
+}
+
+void CPVRRadioRDSInfoTag::SetProgHost(const std::string& strProgHost)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgHost = Trim(strProgHost);
+ g_charsetConverter.unknownToUTF8(m_strProgHost);
+}
+
+void CPVRRadioRDSInfoTag::SetProgStyle(const std::string& strProgStyle)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgStyle = Trim(strProgStyle);
+ g_charsetConverter.unknownToUTF8(m_strProgStyle);
+}
+
+void CPVRRadioRDSInfoTag::SetProgWebsite(const std::string& strWebsite)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgWebsite = Trim(strWebsite);
+ g_charsetConverter.unknownToUTF8(m_strProgWebsite);
+}
+
+void CPVRRadioRDSInfoTag::SetProgNow(const std::string& strNow)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgNow = Trim(strNow);
+ g_charsetConverter.unknownToUTF8(m_strProgNow);
+}
+
+void CPVRRadioRDSInfoTag::SetProgNext(const std::string& strNext)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgNext = Trim(strNext);
+ g_charsetConverter.unknownToUTF8(m_strProgNext);
+}
+
+void CPVRRadioRDSInfoTag::SetPhoneHotline(const std::string& strHotline)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strPhoneHotline = Trim(strHotline);
+ g_charsetConverter.unknownToUTF8(m_strPhoneHotline);
+}
+
+void CPVRRadioRDSInfoTag::SetEMailHotline(const std::string& strHotline)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strEMailHotline = Trim(strHotline);
+ g_charsetConverter.unknownToUTF8(m_strEMailHotline);
+}
+
+void CPVRRadioRDSInfoTag::SetPhoneStudio(const std::string& strPhone)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strPhoneStudio = Trim(strPhone);
+ g_charsetConverter.unknownToUTF8(m_strPhoneStudio);
+}
+
+void CPVRRadioRDSInfoTag::SetEMailStudio(const std::string& strEMail)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strEMailStudio = Trim(strEMail);
+ g_charsetConverter.unknownToUTF8(m_strEMailStudio);
+}
+
+void CPVRRadioRDSInfoTag::SetSMSStudio(const std::string& strSMS)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strSMSStudio = Trim(strSMS);
+ g_charsetConverter.unknownToUTF8(m_strSMSStudio);
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetProgStyle() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProgStyle;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetProgHost() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProgHost;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetProgStation() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProgStation;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetProgWebsite() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProgWebsite;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetProgNow() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProgNow;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetProgNext() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProgNext;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetPhoneHotline() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strPhoneHotline;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetEMailHotline() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strEMailHotline;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetPhoneStudio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strPhoneStudio;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetEMailStudio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strEMailStudio;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetSMSStudio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strSMSStudio;
+}
+
+void CPVRRadioRDSInfoTag::SetRadioStyle(const std::string& style)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strRadioStyle = style;
+}
+
+const std::string CPVRRadioRDSInfoTag::GetRadioStyle() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strRadioStyle;
+}
+
+void CPVRRadioRDSInfoTag::SetRadioText(const std::string& strRadioText)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strRadioText.Add(strRadioText);
+}
+
+std::string CPVRRadioRDSInfoTag::GetRadioText(unsigned int line) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_strRadioText.Size() > 0)
+ {
+ return m_strRadioText.GetLine(line);
+ }
+ else if (line == 0)
+ {
+ return m_strProgramServiceLine0;
+ }
+ else if (line == 1)
+ {
+ return m_strProgramServiceLine1;
+ }
+ return {};
+}
+
+void CPVRRadioRDSInfoTag::SetProgramServiceText(const std::string& strPSText)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgramServiceText.Add(strPSText);
+
+ m_strProgramServiceLine0.erase();
+ for (size_t i = m_strProgramServiceText.MaxSize() / 2 + 1; i < m_strProgramServiceText.MaxSize();
+ ++i)
+ {
+ m_strProgramServiceLine0 += m_strProgramServiceText.GetLine(i);
+ m_strProgramServiceLine0 += ' ';
+ }
+
+ m_strProgramServiceLine1.erase();
+ for (size_t i = 0; i < m_strProgramServiceText.MaxSize() / 2; ++i)
+ {
+ m_strProgramServiceLine1 += m_strProgramServiceText.GetLine(i);
+ m_strProgramServiceLine1 += ' ';
+ }
+}
+
+void CPVRRadioRDSInfoTag::SetPlayingRadioText(bool yesNo)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bHaveRadioText = yesNo;
+}
+
+bool CPVRRadioRDSInfoTag::IsPlayingRadioText() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bHaveRadioText;
+}
+
+void CPVRRadioRDSInfoTag::SetPlayingRadioTextPlus(bool yesNo)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bHaveRadioTextPlus = yesNo;
+}
+
+bool CPVRRadioRDSInfoTag::IsPlayingRadioTextPlus() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bHaveRadioTextPlus;
+}
+
+std::string CPVRRadioRDSInfoTag::Trim(const std::string& value)
+{
+ std::string trimmedValue(value);
+ StringUtils::TrimLeft(trimmedValue);
+ StringUtils::TrimRight(trimmedValue, " \n\r");
+ return trimmedValue;
+}
+
+bool CPVRRadioRDSInfoTag::Info::operator==(const CPVRRadioRDSInfoTag::Info& right) const
+{
+ if (this == &right)
+ return true;
+
+ return (m_infoText == right.m_infoText && m_data == right.m_data &&
+ m_maxSize == right.m_maxSize && m_prependData == right.m_prependData);
+}
+
+void CPVRRadioRDSInfoTag::Info::Clear()
+{
+ m_data.clear();
+ m_infoText.clear();
+}
+
+void CPVRRadioRDSInfoTag::Info::Add(const std::string& text)
+{
+ std::string tmp = Trim(text);
+ g_charsetConverter.unknownToUTF8(tmp);
+
+ if (std::find(m_data.begin(), m_data.end(), tmp) != m_data.end())
+ return;
+
+ if (m_data.size() >= m_maxSize)
+ {
+ if (m_prependData)
+ m_data.pop_back();
+ else
+ m_data.pop_front();
+ }
+
+ if (m_prependData)
+ m_data.emplace_front(std::move(tmp));
+ else
+ m_data.emplace_back(std::move(tmp));
+
+ m_infoText.clear();
+ for (const std::string& data : m_data)
+ {
+ m_infoText += data;
+ m_infoText += '\n';
+ }
+
+ // send a message to all windows to tell them to update the radiotext
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_RADIOTEXT);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
diff --git a/xbmc/pvr/channels/PVRRadioRDSInfoTag.h b/xbmc/pvr/channels/PVRRadioRDSInfoTag.h
new file mode 100644
index 0000000..bc66989
--- /dev/null
+++ b/xbmc/pvr/channels/PVRRadioRDSInfoTag.h
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+#include "utils/IArchivable.h"
+#include "utils/ISerializable.h"
+
+#include <deque>
+#include <string>
+
+namespace PVR
+{
+
+class CPVRRadioRDSInfoTag final : public IArchivable, public ISerializable
+{
+public:
+ CPVRRadioRDSInfoTag();
+
+ bool operator ==(const CPVRRadioRDSInfoTag& right) const;
+ bool operator !=(const CPVRRadioRDSInfoTag& right) const;
+
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& value) const override;
+
+ void Clear();
+ void ResetSongInformation();
+
+ /**! Basic RDS related information */
+ void SetSpeechActive(bool active);
+ void SetLanguage(const std::string& strLanguage);
+ const std::string& GetLanguage() const;
+ void SetCountry(const std::string& strCountry);
+ const std::string& GetCountry() const;
+ void SetRadioText(const std::string& strRadioText);
+ std::string GetRadioText(unsigned int line) const;
+ void SetProgramServiceText(const std::string& strPSText);
+
+ /**! RDS RadioText related information */
+ void SetTitle(const std::string& strTitle);
+ void SetBand(const std::string& strBand);
+ void SetArtist(const std::string& strArtist);
+ void SetComposer(const std::string& strComposer);
+ void SetConductor(const std::string& strConductor);
+ void SetAlbum(const std::string& strAlbum);
+ void SetComment(const std::string& strComment);
+ void SetAlbumTrackNumber(int track);
+
+ const std::string& GetTitle() const;
+ const std::string& GetBand() const;
+ const std::string& GetArtist() const;
+ const std::string& GetComposer() const;
+ const std::string& GetConductor() const;
+ const std::string& GetAlbum() const;
+ const std::string& GetComment() const;
+ int GetAlbumTrackNumber() const;
+
+ void SetProgStation(const std::string& strProgStation);
+ void SetProgStyle(const std::string& strProgStyle);
+ void SetProgHost(const std::string& strProgHost);
+ void SetProgWebsite(const std::string& strWebsite);
+ void SetProgNow(const std::string& strNow);
+ void SetProgNext(const std::string& strNext);
+ void SetPhoneHotline(const std::string& strHotline);
+ void SetEMailHotline(const std::string& strHotline);
+ void SetPhoneStudio(const std::string& strPhone);
+ void SetEMailStudio(const std::string& strEMail);
+ void SetSMSStudio(const std::string& strSMS);
+
+ const std::string& GetProgStation() const;
+ const std::string& GetProgStyle() const;
+ const std::string& GetProgHost() const;
+ const std::string& GetProgWebsite() const;
+ const std::string& GetProgNow() const;
+ const std::string& GetProgNext() const;
+ const std::string& GetPhoneHotline() const;
+ const std::string& GetEMailHotline() const;
+ const std::string& GetPhoneStudio() const;
+ const std::string& GetEMailStudio() const;
+ const std::string& GetSMSStudio() const;
+
+ void SetInfoNews(const std::string& strNews);
+ const std::string GetInfoNews() const;
+
+ void SetInfoNewsLocal(const std::string& strNews);
+ const std::string GetInfoNewsLocal() const;
+
+ void SetInfoSport(const std::string& strSport);
+ const std::string GetInfoSport() const;
+
+ void SetInfoStock(const std::string& strSport);
+ const std::string GetInfoStock() const;
+
+ void SetInfoWeather(const std::string& strWeather);
+ const std::string GetInfoWeather() const;
+
+ void SetInfoHoroscope(const std::string& strHoroscope);
+ const std::string GetInfoHoroscope() const;
+
+ void SetInfoCinema(const std::string& strCinema);
+ const std::string GetInfoCinema() const;
+
+ void SetInfoLottery(const std::string& strLottery);
+ const std::string GetInfoLottery() const;
+
+ void SetInfoOther(const std::string& strOther);
+ const std::string GetInfoOther() const;
+
+ void SetEditorialStaff(const std::string& strEditorialStaff);
+ const std::string GetEditorialStaff() const;
+
+ void SetRadioStyle(const std::string& style);
+ const std::string GetRadioStyle() const;
+
+ void SetPlayingRadioText(bool yesNo);
+ bool IsPlayingRadioText() const;
+
+ void SetPlayingRadioTextPlus(bool yesNo);
+ bool IsPlayingRadioTextPlus() const;
+
+private:
+ CPVRRadioRDSInfoTag(const CPVRRadioRDSInfoTag& tag) = delete;
+ const CPVRRadioRDSInfoTag& operator =(const CPVRRadioRDSInfoTag& tag) = delete;
+
+ static std::string Trim(const std::string& value);
+
+ mutable CCriticalSection m_critSection;
+
+ bool m_RDS_SpeechActive;
+
+ std::string m_strLanguage;
+ std::string m_strCountry;
+ std::string m_strTitle;
+ std::string m_strBand;
+ std::string m_strArtist;
+ std::string m_strComposer;
+ std::string m_strConductor;
+ std::string m_strAlbum;
+ std::string m_strComment;
+ int m_iAlbumTracknumber;
+ std::string m_strRadioStyle;
+
+ class Info
+ {
+ public:
+ Info() = delete;
+ Info(size_t maxSize, bool prependData) : m_maxSize(maxSize), m_prependData(prependData) {}
+
+ bool operator==(const Info& right) const;
+
+ size_t Size() const { return m_data.size(); }
+ size_t MaxSize() const { return m_maxSize; }
+
+ void Clear();
+ void Add(const std::string& text);
+
+ const std::string& GetText() const { return m_infoText; }
+ std::string GetLine(unsigned int line) const
+ {
+ return line < m_data.size() ? m_data.at(line) : "";
+ }
+
+ private:
+ const size_t m_maxSize = 10;
+ const bool m_prependData = false;
+ std::deque<std::string> m_data;
+ std::string m_infoText;
+ };
+
+ Info m_strInfoNews{10, false};
+ Info m_strInfoNewsLocal{10, false};
+ Info m_strInfoSport{10, false};
+ Info m_strInfoStock{10, false};
+ Info m_strInfoWeather{10, false};
+ Info m_strInfoLottery{10, false};
+ Info m_strInfoOther{10, false};
+ Info m_strInfoHoroscope{10, false};
+ Info m_strInfoCinema{10, false};
+ Info m_strEditorialStaff{10, false};
+
+ Info m_strRadioText{6, true};
+
+ Info m_strProgramServiceText{12, false};
+ std::string m_strProgramServiceLine0;
+ std::string m_strProgramServiceLine1;
+
+ std::string m_strProgStyle;
+ std::string m_strProgHost;
+ std::string m_strProgStation;
+ std::string m_strProgWebsite;
+ std::string m_strProgNow;
+ std::string m_strProgNext;
+ std::string m_strPhoneHotline;
+ std::string m_strEMailHotline;
+ std::string m_strPhoneStudio;
+ std::string m_strEMailStudio;
+ std::string m_strSMSStudio;
+
+ bool m_bHaveRadioText;
+ bool m_bHaveRadioTextPlus;
+};
+}
diff --git a/xbmc/pvr/channels/test/CMakeLists.txt b/xbmc/pvr/channels/test/CMakeLists.txt
new file mode 100644
index 0000000..d88bab8
--- /dev/null
+++ b/xbmc/pvr/channels/test/CMakeLists.txt
@@ -0,0 +1,4 @@
+set(SOURCES TestPVRChannelsPath.cpp)
+set(HEADERS)
+
+core_add_test_library(pvrchannels_test)
diff --git a/xbmc/pvr/channels/test/TestPVRChannelsPath.cpp b/xbmc/pvr/channels/test/TestPVRChannelsPath.cpp
new file mode 100644
index 0000000..c7232db
--- /dev/null
+++ b/xbmc/pvr/channels/test/TestPVRChannelsPath.cpp
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "pvr/channels/PVRChannelsPath.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestPVRChannelsPath, Parse_Protocol)
+{
+ // pvr protocol is generally fine, but not sufficient for channels pvr paths - component is missing for that.
+ PVR::CPVRChannelsPath path("pvr://");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Component_1)
+{
+ PVR::CPVRChannelsPath path("pvr://channels");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_TRUE(path.IsEmpty());
+}
+
+TEST(TestPVRChannelsPath, Parse_Component_2)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_TRUE(path.IsEmpty());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_Component)
+{
+ PVR::CPVRChannelsPath path("pvr://foo/");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_TV_Root_1)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_TRUE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_TV_Root_2)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv/");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_TRUE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_Radio_Root_1)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/radio");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_TRUE(path.IsRadio());
+ EXPECT_TRUE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_Radio_Root_2)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/radio/");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_TRUE(path.IsRadio());
+ EXPECT_TRUE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_Root)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/foo");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_TV_Group_1)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv/Group1");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_TV_Group_2)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv/Group1/");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_Hidden_TV_Group)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv/.hidden");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/.hidden/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_TRUE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), ".hidden");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_Special_TV_Group)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv/foo%2Fbar%20baz");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/foo%2Fbar%20baz/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "foo/bar baz");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_Special_TV_Group)
+{
+ // special chars in group name not escaped
+ PVR::CPVRChannelsPath path("pvr://channels/tv/foo/bar baz");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Radio_Group)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/Group1/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_TRUE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_TV_Channel)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv/Group1/5@pvr.demo_4711.pvr");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/5@pvr.demo_4711.pvr");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_TRUE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "pvr.demo");
+ EXPECT_EQ(path.GetInstanceID(), 5);
+ EXPECT_EQ(path.GetChannelUID(), 4711);
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_1)
+{
+ // trailing ".pvr" missing
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo_4711");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_2)
+{
+ // '-' instead of '_' as clientid / channeluid delimiter
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo-4711.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_3)
+{
+ // channeluid not numerical
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo_abc4711.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_4)
+{
+ // channeluid not positive or zero
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo_-4711.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_5)
+{
+ // empty clientid
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/_4711.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_6)
+{
+ // empty channeluid
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo_.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_7)
+{
+ // empty clientid and empty channeluid
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/_.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_8)
+{
+ // empty clientid and empty channeluid, only extension ".pvr" given
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, TV_Channelgroup)
+{
+ PVR::CPVRChannelsPath path(false, "Group1");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Radio_Channelgroup)
+{
+ PVR::CPVRChannelsPath path(true, "Group1");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/Group1/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_TRUE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Hidden_TV_Channelgroup)
+{
+ PVR::CPVRChannelsPath path(false, true, "Group1");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/.hidden/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_TRUE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), ".hidden");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Hidden_Radio_Channelgroup)
+{
+ PVR::CPVRChannelsPath path(true, true, "Group1");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/.hidden/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_TRUE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_TRUE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), ".hidden");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, TV_Channel)
+{
+ PVR::CPVRChannelsPath path(false, "Group1", "pvr.demo", ADDON::ADDON_SINGLETON_INSTANCE_ID, 4711);
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/0@pvr.demo_4711.pvr");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_TRUE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "pvr.demo");
+ EXPECT_EQ(path.GetChannelUID(), 4711);
+}
+
+TEST(TestPVRChannelsPath, Radio_Channel)
+{
+ PVR::CPVRChannelsPath path(true, "Group1", "pvr.demo", ADDON::ADDON_SINGLETON_INSTANCE_ID, 4711);
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/Group1/0@pvr.demo_4711.pvr");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_TRUE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_TRUE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "pvr.demo");
+ EXPECT_EQ(path.GetChannelUID(), 4711);
+}
+
+TEST(TestPVRChannelsPath, Operator_Equals)
+{
+ PVR::CPVRChannelsPath path2(true, "Group1");
+ PVR::CPVRChannelsPath path(static_cast<std::string>(path2));
+
+ EXPECT_EQ(path, path2);
+}
diff --git a/xbmc/pvr/dialogs/CMakeLists.txt b/xbmc/pvr/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..39b88ed
--- /dev/null
+++ b/xbmc/pvr/dialogs/CMakeLists.txt
@@ -0,0 +1,29 @@
+set(SOURCES GUIDialogPVRChannelManager.cpp
+ GUIDialogPVRChannelsOSD.cpp
+ GUIDialogPVRGroupManager.cpp
+ GUIDialogPVRGuideInfo.cpp
+ GUIDialogPVRChannelGuide.cpp
+ GUIDialogPVRGuideControls.cpp
+ GUIDialogPVRGuideSearch.cpp
+ GUIDialogPVRRadioRDSInfo.cpp
+ GUIDialogPVRRecordingInfo.cpp
+ GUIDialogPVRRecordingSettings.cpp
+ GUIDialogPVRTimerSettings.cpp
+ GUIDialogPVRClientPriorities.cpp
+ GUIDialogPVRItemsViewBase.cpp)
+
+set(HEADERS GUIDialogPVRChannelManager.h
+ GUIDialogPVRChannelsOSD.h
+ GUIDialogPVRGroupManager.h
+ GUIDialogPVRGuideInfo.h
+ GUIDialogPVRChannelGuide.h
+ GUIDialogPVRGuideControls.h
+ GUIDialogPVRGuideSearch.h
+ GUIDialogPVRRadioRDSInfo.h
+ GUIDialogPVRRecordingInfo.h
+ GUIDialogPVRRecordingSettings.h
+ GUIDialogPVRTimerSettings.h
+ GUIDialogPVRClientPriorities.h
+ GUIDialogPVRItemsViewBase.h)
+
+core_add_library(pvr_dialogs)
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.cpp b/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.cpp
new file mode 100644
index 0000000..10b2623
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.cpp
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRChannelGuide.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/epg/EpgInfoTag.h"
+
+#include <memory>
+#include <vector>
+
+using namespace PVR;
+
+CGUIDialogPVRChannelGuide::CGUIDialogPVRChannelGuide()
+ : CGUIDialogPVRItemsViewBase(WINDOW_DIALOG_PVR_CHANNEL_GUIDE, "DialogPVRChannelGuide.xml")
+{
+}
+
+void CGUIDialogPVRChannelGuide::Open(const std::shared_ptr<CPVRChannel>& channel)
+{
+ m_channel = channel;
+ CGUIDialogPVRItemsViewBase::Open();
+}
+
+void CGUIDialogPVRChannelGuide::OnInitWindow()
+{
+ // no user-specific channel is set; use current playing channel
+ if (!m_channel)
+ m_channel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+
+ if (!m_channel)
+ {
+ Close();
+ return;
+ }
+
+ Init();
+
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags = m_channel->GetEpgTags();
+ for (const auto& tag : tags)
+ {
+ m_vecItems->Add(std::make_shared<CFileItem>(tag));
+ }
+
+ m_viewControl.SetItems(*m_vecItems);
+
+ CGUIDialogPVRItemsViewBase::OnInitWindow();
+
+ // select the active entry
+ unsigned int iSelectedItem = 0;
+ for (int iEpgPtr = 0; iEpgPtr < m_vecItems->Size(); ++iEpgPtr)
+ {
+ const CFileItemPtr entry = m_vecItems->Get(iEpgPtr);
+ if (entry->HasEPGInfoTag() && entry->GetEPGInfoTag()->IsActive())
+ {
+ iSelectedItem = iEpgPtr;
+ break;
+ }
+ }
+ m_viewControl.SetSelectedItem(iSelectedItem);
+}
+
+void CGUIDialogPVRChannelGuide::OnDeinitWindow(int nextWindowID)
+{
+ CGUIDialogPVRItemsViewBase::OnDeinitWindow(nextWindowID);
+ m_channel.reset();
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.h b/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.h
new file mode 100644
index 0000000..b348a9e
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/dialogs/GUIDialogPVRItemsViewBase.h"
+
+#include <memory>
+
+namespace PVR
+{
+ class CPVRChannel;
+
+ class CGUIDialogPVRChannelGuide : public CGUIDialogPVRItemsViewBase
+ {
+ public:
+ CGUIDialogPVRChannelGuide();
+ ~CGUIDialogPVRChannelGuide() override = default;
+
+ void Open(const std::shared_ptr<CPVRChannel>& channel);
+
+ protected:
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ private:
+ std::shared_ptr<CPVRChannel> m_channel;
+ };
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.cpp b/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.cpp
new file mode 100644
index 0000000..267b3c6
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.cpp
@@ -0,0 +1,1076 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRChannelManager.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUISpinControlEx.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "profiles/ProfileManager.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/dialogs/GUIDialogPVRGroupManager.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#define BUTTON_OK 4
+#define BUTTON_APPLY 5
+#define BUTTON_CANCEL 6
+#define RADIOBUTTON_ACTIVE 7
+#define EDIT_NAME 8
+#define BUTTON_CHANNEL_LOGO 9
+#define IMAGE_CHANNEL_LOGO 10
+#define RADIOBUTTON_USEEPG 12
+#define SPIN_EPGSOURCE_SELECTION 13
+#define RADIOBUTTON_PARENTAL_LOCK 14
+#define CONTROL_LIST_CHANNELS 20
+#define BUTTON_GROUP_MANAGER 30
+#define BUTTON_NEW_CHANNEL 31
+#define BUTTON_RADIO_TV 34
+#define BUTTON_REFRESH_LOGOS 35
+
+namespace
+{
+constexpr const char* LABEL_CHANNEL_DISABLED = "0";
+
+// Note: strings must not be changed; they are part of the public skinning API for this dialog.
+constexpr const char* PROPERTY_CHANNEL_NUMBER = "Number";
+constexpr const char* PROPERTY_CHANNEL_ENABLED = "ActiveChannel";
+constexpr const char* PROPERTY_CHANNEL_USER_SET_HIDDEN = "UserSetHidden";
+constexpr const char* PROPERTY_CHANNEL_LOCKED = "ParentalLocked";
+constexpr const char* PROPERTY_CHANNEL_ICON = "Icon";
+constexpr const char* PROPERTY_CHANNEL_CUSTOM_ICON = "UserSetIcon";
+constexpr const char* PROPERTY_CHANNEL_NAME = "Name";
+constexpr const char* PROPERTY_CHANNEL_EPG_ENABLED = "UseEPG";
+constexpr const char* PROPERTY_CHANNEL_EPG_SOURCE = "EPGSource";
+constexpr const char* PROPERTY_CLIENT_SUPPORTS_SETTINGS = "SupportsSettings";
+constexpr const char* PROPERTY_CLIENT_NAME = "ClientName";
+constexpr const char* PROPERTY_ITEM_CHANGED = "Changed";
+
+} // namespace
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+CGUIDialogPVRChannelManager::CGUIDialogPVRChannelManager() :
+ CGUIDialog(WINDOW_DIALOG_PVR_CHANNEL_MANAGER, "DialogPVRChannelManager.xml"),
+ m_channelItems(new CFileItemList)
+{
+ SetRadio(false);
+}
+
+CGUIDialogPVRChannelManager::~CGUIDialogPVRChannelManager()
+{
+ delete m_channelItems;
+}
+
+bool CGUIDialogPVRChannelManager::OnActionMove(const CAction& action)
+{
+ bool bReturn(false);
+ int iActionId = action.GetID();
+
+ if (GetFocusedControlID() == CONTROL_LIST_CHANNELS)
+ {
+ if (iActionId == ACTION_MOUSE_MOVE)
+ {
+ int iSelected = m_viewControl.GetSelectedItem();
+ if (m_iSelected < iSelected)
+ {
+ iActionId = ACTION_MOVE_DOWN;
+ }
+ else if (m_iSelected > iSelected)
+ {
+ iActionId = ACTION_MOVE_UP;
+ }
+ else
+ {
+ return bReturn;
+ }
+ }
+
+ if (iActionId == ACTION_MOVE_DOWN || iActionId == ACTION_MOVE_UP ||
+ iActionId == ACTION_PAGE_DOWN || iActionId == ACTION_PAGE_UP ||
+ iActionId == ACTION_FIRST_PAGE || iActionId == ACTION_LAST_PAGE)
+ {
+ CGUIDialog::OnAction(action);
+ int iSelected = m_viewControl.GetSelectedItem();
+
+ bReturn = true;
+ if (!m_bMovingMode)
+ {
+ if (iSelected != m_iSelected)
+ {
+ m_iSelected = iSelected;
+ SetData(m_iSelected);
+ }
+ }
+ else
+ {
+ bool bMoveUp = iActionId == ACTION_PAGE_UP || iActionId == ACTION_MOVE_UP || iActionId == ACTION_FIRST_PAGE;
+ unsigned int iLines = bMoveUp ? abs(m_iSelected - iSelected) : 1;
+ bool bOutOfBounds = bMoveUp ? m_iSelected <= 0 : m_iSelected >= m_channelItems->Size() - 1;
+ if (bOutOfBounds)
+ {
+ bMoveUp = !bMoveUp;
+ iLines = m_channelItems->Size() - 1;
+ }
+ for (unsigned int iLine = 0; iLine < iLines; ++iLine)
+ {
+ unsigned int iNewSelect = bMoveUp ? m_iSelected - 1 : m_iSelected + 1;
+
+ const CFileItemPtr newItem = m_channelItems->Get(iNewSelect);
+ const std::string number = newItem->GetProperty(PROPERTY_CHANNEL_NUMBER).asString();
+ if (number != LABEL_CHANNEL_DISABLED)
+ {
+ // Swap channel numbers
+ const CFileItemPtr item = m_channelItems->Get(m_iSelected);
+ newItem->SetProperty(PROPERTY_CHANNEL_NUMBER,
+ item->GetProperty(PROPERTY_CHANNEL_NUMBER));
+ SetItemChanged(newItem);
+ item->SetProperty(PROPERTY_CHANNEL_NUMBER, number);
+ SetItemChanged(item);
+ }
+
+ // swap items
+ m_channelItems->Swap(iNewSelect, m_iSelected);
+ m_iSelected = iNewSelect;
+ }
+
+ m_viewControl.SetItems(*m_channelItems);
+ m_viewControl.SetSelectedItem(m_iSelected);
+ }
+ }
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRChannelManager::OnAction(const CAction& action)
+{
+ return OnActionMove(action) ||
+ CGUIDialog::OnAction(action);
+}
+
+void CGUIDialogPVRChannelManager::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ m_iSelected = 0;
+ m_bMovingMode = false;
+ m_bAllowNewChannel = false;
+
+ EnableChannelOptions(false);
+ CONTROL_DISABLE(BUTTON_APPLY);
+
+ // prevent resorting channels if backend channel numbers or backend channel order shall be used
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ m_bAllowRenumber = !settings->GetBool(CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS);
+ m_bAllowReorder =
+ m_bAllowRenumber && !settings->GetBool(CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER);
+
+ Update();
+
+ if (m_initialSelection)
+ {
+ // set initial selection
+ const std::shared_ptr<CPVRChannel> channel = m_initialSelection->GetPVRChannelInfoTag();
+ for (int i = 0; i < m_channelItems->Size(); ++i)
+ {
+ if (m_channelItems->Get(i)->GetPVRChannelInfoTag() == channel)
+ {
+ m_iSelected = i;
+ m_viewControl.SetSelectedItem(m_iSelected);
+ break;
+ }
+ }
+ m_initialSelection.reset();
+ }
+ SetData(m_iSelected);
+}
+
+void CGUIDialogPVRChannelManager::OnDeinitWindow(int nextWindowID)
+{
+ Clear();
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIDialogPVRChannelManager::SetRadio(bool bIsRadio)
+{
+ m_bIsRadio = bIsRadio;
+ SetProperty("IsRadio", m_bIsRadio ? "true" : "");
+}
+
+void CGUIDialogPVRChannelManager::Open(const std::shared_ptr<CFileItem>& initialSelection)
+{
+ m_initialSelection = initialSelection;
+ CGUIDialog::Open();
+}
+
+bool CGUIDialogPVRChannelManager::OnClickListChannels(const CGUIMessage& message)
+{
+ if (!m_bMovingMode)
+ {
+ int iAction = message.GetParam1();
+ int iItem = m_viewControl.GetSelectedItem();
+
+ /* Check file item is in list range and get his pointer */
+ if (iItem < 0 || iItem >= m_channelItems->Size()) return true;
+
+ /* Process actions */
+ if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_CONTEXT_MENU || iAction == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ /* Show Contextmenu */
+ OnPopupMenu(iItem);
+
+ return true;
+ }
+ }
+ else
+ {
+ CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ if (pItem)
+ {
+ pItem->Select(false);
+ m_bMovingMode = false;
+ SetItemChanged(pItem);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonOK()
+{
+ SaveList();
+ Close();
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonApply()
+{
+ SaveList();
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonCancel()
+{
+ Close();
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonRadioTV()
+{
+ PromptAndSaveList();
+
+ m_iSelected = 0;
+ m_bMovingMode = false;
+ m_bAllowNewChannel = false;
+ m_bIsRadio = !m_bIsRadio;
+ SetProperty("IsRadio", m_bIsRadio ? "true" : "");
+ Update();
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonRadioActive()
+{
+ CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), RADIOBUTTON_ACTIVE);
+ if (OnMessage(msg))
+ {
+ CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ if (pItem)
+ {
+ const bool selected = (msg.GetParam1() == 1);
+ if (pItem->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean() != selected)
+ {
+ pItem->SetProperty(PROPERTY_CHANNEL_ENABLED, selected);
+ pItem->SetProperty(PROPERTY_CHANNEL_USER_SET_HIDDEN, true);
+ SetItemChanged(pItem);
+ Renumber();
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonRadioParentalLocked()
+{
+ CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), RADIOBUTTON_PARENTAL_LOCK);
+ if (!OnMessage(msg))
+ return false;
+
+ bool selected(msg.GetParam1() == 1);
+
+ // ask for PIN first
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalPIN() !=
+ ParentalCheckResult::SUCCESS)
+ { // failed - reset to previous
+ SET_CONTROL_SELECTED(GetID(), RADIOBUTTON_PARENTAL_LOCK, !selected);
+ return false;
+ }
+
+ CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ if (pItem)
+ {
+ if (pItem->GetProperty(PROPERTY_CHANNEL_LOCKED).asBoolean() != selected)
+ {
+ pItem->SetProperty(PROPERTY_CHANNEL_LOCKED, selected);
+ SetItemChanged(pItem);
+ Renumber();
+ }
+ return true;
+ }
+
+ return false;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonEditName()
+{
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), EDIT_NAME);
+ if (OnMessage(msg))
+ {
+ CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ if (pItem)
+ {
+ if (pItem->GetProperty(PROPERTY_CHANNEL_NAME).asString() != msg.GetLabel())
+ {
+ pItem->SetProperty(PROPERTY_CHANNEL_NAME, msg.GetLabel());
+ SetItemChanged(pItem);
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonChannelLogo()
+{
+ CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ if (!pItem)
+ return false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (profileManager->GetCurrentProfile().canWriteSources() && !g_passwordManager.IsProfileLockUnlocked())
+ return false;
+
+ // setup our thumb list
+ CFileItemList items;
+
+ // add the current thumb, if available
+ if (!pItem->GetProperty(PROPERTY_CHANNEL_ICON).asString().empty())
+ {
+ CFileItemPtr current(new CFileItem("thumb://Current", false));
+ current->SetArt("thumb", pItem->GetPVRChannelInfoTag()->IconPath());
+ current->SetLabel(g_localizeStrings.Get(19282));
+ items.Add(current);
+ }
+ else if (pItem->HasArt("thumb"))
+ { // already have a thumb that the share doesn't know about - must be a local one, so we mayaswell reuse it.
+ CFileItemPtr current(new CFileItem("thumb://Current", false));
+ current->SetArt("thumb", pItem->GetArt("thumb"));
+ current->SetLabel(g_localizeStrings.Get(19282));
+ items.Add(current);
+ }
+
+ // and add a "no thumb" entry as well
+ CFileItemPtr nothumb(new CFileItem("thumb://None", false));
+ nothumb->SetArt("icon", pItem->GetArt("icon"));
+ nothumb->SetLabel(g_localizeStrings.Get(19283));
+ items.Add(nothumb);
+
+ std::string strThumb;
+ VECSOURCES shares;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetString(CSettings::SETTING_PVRMENU_ICONPATH) != "")
+ {
+ CMediaSource share1;
+ share1.strPath = settings->GetString(CSettings::SETTING_PVRMENU_ICONPATH);
+ share1.strName = g_localizeStrings.Get(19066);
+ shares.push_back(share1);
+ }
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ if (!CGUIDialogFileBrowser::ShowAndGetImage(items, shares, g_localizeStrings.Get(19285), strThumb, NULL, 19285))
+ return false;
+
+ if (strThumb == "thumb://Current")
+ return true;
+
+ if (strThumb == "thumb://None")
+ strThumb = "";
+
+ if (pItem->GetProperty(PROPERTY_CHANNEL_ICON).asString() != strThumb)
+ {
+ pItem->SetProperty(PROPERTY_CHANNEL_ICON, strThumb);
+ pItem->SetProperty(PROPERTY_CHANNEL_CUSTOM_ICON, true);
+ SetItemChanged(pItem);
+ }
+
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonUseEPG()
+{
+ CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), RADIOBUTTON_USEEPG);
+ if (OnMessage(msg))
+ {
+ CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ if (pItem)
+ {
+ const bool selected = (msg.GetParam1() == 1);
+ if (pItem->GetProperty(PROPERTY_CHANNEL_EPG_ENABLED).asBoolean() != selected)
+ {
+ pItem->SetProperty(PROPERTY_CHANNEL_EPG_ENABLED, selected);
+ SetItemChanged(pItem);
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickEPGSourceSpin()
+{
+ //! @todo Add EPG scraper support
+ return true;
+ // CGUISpinControlEx* pSpin = static_cast<CGUISpinControlEx*>(GetControl(SPIN_EPGSOURCE_SELECTION));
+ // if (pSpin)
+ // {
+ // CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ // if (pItem)
+ // {
+ // if (pItem->GetProperty(PROPERTY_CHANNEL_EPG_SOURCE).asInteger() != 0)
+ // {
+ // pItem->SetProperty(PROPERTY_CHANNEL_EPG_SOURCE, static_cast<int>(0));
+ // SetItemChanged(pItem);
+ // }
+ // return true;
+ // }
+ // }
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonGroupManager()
+{
+ PromptAndSaveList();
+
+ /* Load group manager dialog */
+ CGUIDialogPVRGroupManager* pDlgInfo = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGroupManager>(WINDOW_DIALOG_PVR_GROUP_MANAGER);
+ if (!pDlgInfo)
+ return false;
+
+ pDlgInfo->SetRadio(m_bIsRadio);
+
+ /* Open dialog window */
+ pDlgInfo->Open();
+
+ Update();
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonNewChannel()
+{
+ PromptAndSaveList();
+
+ int iSelection = 0;
+ if (m_clientsWithSettingsList.size() > 1)
+ {
+ CGUIDialogSelect* pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (!pDlgSelect)
+ return false;
+
+ pDlgSelect->SetHeading(CVariant{19213}); // Select Client
+
+ for (const auto& client : m_clientsWithSettingsList)
+ pDlgSelect->Add(client->Name());
+ pDlgSelect->Open();
+
+ iSelection = pDlgSelect->GetSelectedItem();
+ }
+
+ if (iSelection >= 0 && iSelection < static_cast<int>(m_clientsWithSettingsList.size()))
+ {
+ int iClientID = m_clientsWithSettingsList[iSelection]->GetID();
+
+ std::shared_ptr<CPVRChannel> channel(new CPVRChannel(m_bIsRadio));
+ channel->SetChannelName(g_localizeStrings.Get(19204)); // New channel
+ channel->SetClientID(iClientID);
+
+ PVR_ERROR ret = PVR_ERROR_UNKNOWN;
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(iClientID);
+ if (client)
+ {
+ channel->SetEPGEnabled(client->GetClientCapabilities().SupportsEPG());
+ ret = client->OpenDialogChannelAdd(channel);
+ }
+
+ if (ret == PVR_ERROR_NO_ERROR)
+ {
+ CFileItemList prevChannelItems;
+ prevChannelItems.Assign(*m_channelItems);
+
+ Update();
+
+ for (int index = 0; index < m_channelItems->Size(); ++index)
+ {
+ if (!prevChannelItems.Contains(m_channelItems->Get(index)->GetPath()))
+ {
+ m_iSelected = index;
+ m_viewControl.SetSelectedItem(m_iSelected);
+ SetData(m_iSelected);
+ break;
+ }
+ }
+ }
+ else if (ret == PVR_ERROR_NOT_IMPLEMENTED)
+ HELPERS::ShowOKDialogText(CVariant{19033}, CVariant{19038}); // "Information", "Not supported by the PVR backend."
+ else
+ HELPERS::ShowOKDialogText(CVariant{2103}, CVariant{16029}); // "Add-on error", "Check the log for more information about this message."
+ }
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonRefreshChannelLogos()
+{
+ for (const auto& item : *m_channelItems)
+ {
+ const std::string thumb = item->GetArt("thumb");
+ if (!thumb.empty())
+ {
+ // clear current cached image
+ CServiceBroker::GetTextureCache()->ClearCachedImage(thumb);
+ item->SetArt("thumb", "");
+ }
+ }
+
+ m_iSelected = 0;
+ Update();
+
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_REFRESH_THUMBS);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnMessageClick(const CGUIMessage& message)
+{
+ int iControl = message.GetSenderId();
+ switch(iControl)
+ {
+ case CONTROL_LIST_CHANNELS:
+ return OnClickListChannels(message);
+ case BUTTON_OK:
+ return OnClickButtonOK();
+ case BUTTON_APPLY:
+ return OnClickButtonApply();
+ case BUTTON_CANCEL:
+ return OnClickButtonCancel();
+ case BUTTON_RADIO_TV:
+ return OnClickButtonRadioTV();
+ case RADIOBUTTON_ACTIVE:
+ return OnClickButtonRadioActive();
+ case RADIOBUTTON_PARENTAL_LOCK:
+ return OnClickButtonRadioParentalLocked();
+ case EDIT_NAME:
+ return OnClickButtonEditName();
+ case BUTTON_CHANNEL_LOGO:
+ return OnClickButtonChannelLogo();
+ case RADIOBUTTON_USEEPG:
+ return OnClickButtonUseEPG();
+ case SPIN_EPGSOURCE_SELECTION:
+ return OnClickEPGSourceSpin();
+ case BUTTON_GROUP_MANAGER:
+ return OnClickButtonGroupManager();
+ case BUTTON_NEW_CHANNEL:
+ return OnClickButtonNewChannel();
+ case BUTTON_REFRESH_LOGOS:
+ return OnClickButtonRefreshChannelLogos();
+ default:
+ return false;
+ }
+}
+
+bool CGUIDialogPVRChannelManager::OnMessage(CGUIMessage& message)
+{
+ unsigned int iMessage = message.GetMessage();
+
+ switch (iMessage)
+ {
+ case GUI_MSG_CLICKED:
+ return OnMessageClick(message);
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogPVRChannelManager::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+
+ m_viewControl.Reset();
+ m_viewControl.SetParentWindow(GetID());
+ m_viewControl.AddView(GetControl(CONTROL_LIST_CHANNELS));
+}
+
+void CGUIDialogPVRChannelManager::OnWindowUnload()
+{
+ CGUIDialog::OnWindowUnload();
+ m_viewControl.Reset();
+}
+
+CFileItemPtr CGUIDialogPVRChannelManager::GetCurrentListItem(int offset)
+{
+ return m_channelItems->Get(m_iSelected);
+}
+
+bool CGUIDialogPVRChannelManager::OnPopupMenu(int iItem)
+{
+ // popup the context menu
+ // grab our context menu
+ CContextButtons buttons;
+
+ // mark the item
+ if (iItem >= 0 && iItem < m_channelItems->Size())
+ m_channelItems->Get(iItem)->Select(true);
+ else
+ return false;
+
+ CFileItemPtr pItem = m_channelItems->Get(iItem);
+ if (!pItem)
+ return false;
+
+ if (m_bAllowReorder &&
+ pItem->GetProperty(PROPERTY_CHANNEL_NUMBER).asString() != LABEL_CHANNEL_DISABLED)
+ buttons.Add(CONTEXT_BUTTON_MOVE, 116); /* Move channel up or down */
+
+ if (pItem->GetProperty(PROPERTY_CLIENT_SUPPORTS_SETTINGS).asBoolean())
+ {
+ buttons.Add(CONTEXT_BUTTON_SETTINGS, 10004); /* Open add-on channel settings dialog */
+ buttons.Add(CONTEXT_BUTTON_DELETE, 117); /* Delete add-on channel */
+ }
+
+ int choice = CGUIDialogContextMenu::ShowAndGetChoice(buttons);
+
+ // deselect our item
+ if (iItem >= 0 && iItem < m_channelItems->Size())
+ m_channelItems->Get(iItem)->Select(false);
+
+ if (choice < 0)
+ return false;
+
+ return OnContextButton(iItem, (CONTEXT_BUTTON)choice);
+}
+
+bool CGUIDialogPVRChannelManager::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ /* Check file item is in list range and get his pointer */
+ if (itemNumber < 0 || itemNumber >= m_channelItems->Size()) return false;
+
+ CFileItemPtr pItem = m_channelItems->Get(itemNumber);
+ if (!pItem)
+ return false;
+
+ if (button == CONTEXT_BUTTON_MOVE)
+ {
+ m_bMovingMode = true;
+ pItem->Select(true);
+ }
+ else if (button == CONTEXT_BUTTON_SETTINGS)
+ {
+ PromptAndSaveList();
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*pItem);
+ PVR_ERROR ret = PVR_ERROR_UNKNOWN;
+ if (client)
+ ret = client->OpenDialogChannelSettings(pItem->GetPVRChannelInfoTag());
+
+ if (ret == PVR_ERROR_NO_ERROR)
+ {
+ Update();
+ SetData(m_iSelected);
+ }
+ else if (ret == PVR_ERROR_NOT_IMPLEMENTED)
+ HELPERS::ShowOKDialogText(CVariant{19033}, CVariant{19038}); // "Information", "Not supported by the PVR backend."
+ else
+ HELPERS::ShowOKDialogText(CVariant{2103}, CVariant{16029}); // "Add-on error", "Check the log for more information about this message."
+ }
+ else if (button == CONTEXT_BUTTON_DELETE)
+ {
+ CGUIDialogYesNo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (!pDialog)
+ return true;
+
+ pDialog->SetHeading(CVariant{19211}); // Delete channel
+ pDialog->SetText(CVariant{750}); // Are you sure?
+ pDialog->Open();
+
+ if (pDialog->IsConfirmed())
+ {
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*pItem);
+ if (client)
+ {
+ const std::shared_ptr<CPVRChannel> channel = pItem->GetPVRChannelInfoTag();
+ PVR_ERROR ret = client->DeleteChannel(channel);
+ if (ret == PVR_ERROR_NO_ERROR)
+ {
+ CPVRChannelGroups* groups =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio);
+ if (groups)
+ {
+ groups->UpdateFromClients({});
+ Update();
+ }
+ }
+ else if (ret == PVR_ERROR_NOT_IMPLEMENTED)
+ HELPERS::ShowOKDialogText(CVariant{19033}, CVariant{19038}); // "Information", "Not supported by the PVR backend."
+ else
+ HELPERS::ShowOKDialogText(CVariant{2103}, CVariant{16029}); // "Add-on error", "Check the log for more information about this message."
+ }
+ }
+ }
+ return true;
+}
+
+void CGUIDialogPVRChannelManager::SetData(int iItem)
+{
+ if (iItem < 0 || iItem >= m_channelItems->Size())
+ {
+ ClearChannelOptions();
+ EnableChannelOptions(false);
+ return;
+ }
+
+ CFileItemPtr pItem = m_channelItems->Get(iItem);
+ if (!pItem)
+ return;
+
+ SET_CONTROL_LABEL2(EDIT_NAME, pItem->GetProperty(PROPERTY_CHANNEL_NAME).asString());
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), EDIT_NAME, CGUIEditControl::INPUT_TYPE_TEXT, 19208);
+ OnMessage(msg);
+
+ SET_CONTROL_SELECTED(GetID(), RADIOBUTTON_ACTIVE,
+ pItem->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean());
+ SET_CONTROL_SELECTED(GetID(), RADIOBUTTON_USEEPG,
+ pItem->GetProperty(PROPERTY_CHANNEL_EPG_ENABLED).asBoolean());
+ SET_CONTROL_SELECTED(GetID(), RADIOBUTTON_PARENTAL_LOCK,
+ pItem->GetProperty(PROPERTY_CHANNEL_LOCKED).asBoolean());
+
+ EnableChannelOptions(true);
+}
+
+void CGUIDialogPVRChannelManager::Update()
+{
+ m_viewControl.SetCurrentView(CONTROL_LIST_CHANNELS);
+
+ // empty the lists ready for population
+ Clear();
+
+ std::shared_ptr<CPVRChannelGroup> channels = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_bIsRadio);
+
+ // No channels available, nothing to do.
+ if (!channels)
+ return;
+
+ channels->UpdateFromClients({});
+
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers = channels->GetMembers();
+ std::shared_ptr<CFileItem> channelFile;
+ for (const auto& member : groupMembers)
+ {
+ channelFile = std::make_shared<CFileItem>(member);
+ if (!channelFile)
+ continue;
+ const std::shared_ptr<CPVRChannel> channel(channelFile->GetPVRChannelInfoTag());
+
+ channelFile->SetProperty(PROPERTY_CHANNEL_ENABLED, !channel->IsHidden());
+ channelFile->SetProperty(PROPERTY_CHANNEL_USER_SET_HIDDEN, channel->IsUserSetHidden());
+ channelFile->SetProperty(PROPERTY_CHANNEL_NAME, channel->ChannelName());
+ channelFile->SetProperty(PROPERTY_CHANNEL_EPG_ENABLED, channel->EPGEnabled());
+ channelFile->SetProperty(PROPERTY_CHANNEL_ICON, channel->ClientIconPath());
+ channelFile->SetProperty(PROPERTY_CHANNEL_CUSTOM_ICON, channel->IsUserSetIcon());
+ channelFile->SetProperty(PROPERTY_CHANNEL_EPG_SOURCE, 0);
+ channelFile->SetProperty(PROPERTY_CHANNEL_LOCKED, channel->IsLocked());
+ channelFile->SetProperty(PROPERTY_CHANNEL_NUMBER,
+ member->ChannelNumber().FormattedChannelNumber());
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*channelFile);
+ if (client)
+ {
+ channelFile->SetProperty(PROPERTY_CLIENT_NAME, client->GetFriendlyName());
+ channelFile->SetProperty(PROPERTY_CLIENT_SUPPORTS_SETTINGS,
+ client->GetClientCapabilities().SupportsChannelSettings());
+ }
+
+ m_channelItems->Add(channelFile);
+ }
+
+ {
+ std::vector< std::pair<std::string, int> > labels;
+ labels.emplace_back(g_localizeStrings.Get(19210), 0);
+ //! @todo Add Labels for EPG scrapers here
+ SET_CONTROL_LABELS(SPIN_EPGSOURCE_SELECTION, 0, &labels);
+ }
+
+ m_clientsWithSettingsList = CServiceBroker::GetPVRManager().Clients()->GetClientsSupportingChannelSettings(m_bIsRadio);
+ if (!m_clientsWithSettingsList.empty())
+ m_bAllowNewChannel = true;
+
+ if (m_bAllowNewChannel)
+ CONTROL_ENABLE(BUTTON_NEW_CHANNEL);
+ else
+ CONTROL_DISABLE(BUTTON_NEW_CHANNEL);
+
+ Renumber();
+ m_viewControl.SetItems(*m_channelItems);
+ if (m_iSelected >= m_channelItems->Size())
+ m_iSelected = m_channelItems->Size() - 1;
+ m_viewControl.SetSelectedItem(m_iSelected);
+ SetData(m_iSelected);
+}
+
+void CGUIDialogPVRChannelManager::Clear()
+{
+ m_viewControl.Clear();
+ m_channelItems->Clear();
+
+ ClearChannelOptions();
+ EnableChannelOptions(false);
+
+ CONTROL_DISABLE(BUTTON_APPLY);
+}
+
+void CGUIDialogPVRChannelManager::ClearChannelOptions()
+{
+ CONTROL_DESELECT(RADIOBUTTON_ACTIVE);
+ SET_CONTROL_LABEL2(EDIT_NAME, "");
+ SET_CONTROL_FILENAME(BUTTON_CHANNEL_LOGO, "");
+ CONTROL_DESELECT(RADIOBUTTON_USEEPG);
+
+ std::vector<std::pair<std::string, int>> labels = {{g_localizeStrings.Get(19210), 0}};
+ SET_CONTROL_LABELS(SPIN_EPGSOURCE_SELECTION, 0, &labels);
+
+ CONTROL_DESELECT(RADIOBUTTON_PARENTAL_LOCK);
+}
+
+void CGUIDialogPVRChannelManager::EnableChannelOptions(bool bEnable)
+{
+ if (bEnable)
+ {
+ CONTROL_ENABLE(RADIOBUTTON_ACTIVE);
+ CONTROL_ENABLE(EDIT_NAME);
+ CONTROL_ENABLE(BUTTON_CHANNEL_LOGO);
+ CONTROL_ENABLE(IMAGE_CHANNEL_LOGO);
+ CONTROL_ENABLE(RADIOBUTTON_USEEPG);
+ CONTROL_ENABLE(SPIN_EPGSOURCE_SELECTION);
+ CONTROL_ENABLE(RADIOBUTTON_PARENTAL_LOCK);
+ }
+ else
+ {
+ CONTROL_DISABLE(RADIOBUTTON_ACTIVE);
+ CONTROL_DISABLE(EDIT_NAME);
+ CONTROL_DISABLE(BUTTON_CHANNEL_LOGO);
+ CONTROL_DISABLE(IMAGE_CHANNEL_LOGO);
+ CONTROL_DISABLE(RADIOBUTTON_USEEPG);
+ CONTROL_DISABLE(SPIN_EPGSOURCE_SELECTION);
+ CONTROL_DISABLE(RADIOBUTTON_PARENTAL_LOCK);
+ }
+}
+
+void CGUIDialogPVRChannelManager::RenameChannel(const CFileItemPtr& pItem)
+{
+ std::string strChannelName = pItem->GetProperty(PROPERTY_CHANNEL_NAME).asString();
+ if (strChannelName != pItem->GetPVRChannelInfoTag()->ChannelName())
+ {
+ std::shared_ptr<CPVRChannel> channel = pItem->GetPVRChannelInfoTag();
+ channel->SetChannelName(strChannelName);
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*pItem);
+ if (!client || (client->RenameChannel(channel) != PVR_ERROR_NO_ERROR))
+ HELPERS::ShowOKDialogText(CVariant{2103}, CVariant{16029}); // Add-on error;Check the log file for details.
+ }
+}
+
+bool CGUIDialogPVRChannelManager::PersistChannel(const CFileItemPtr& pItem,
+ const std::shared_ptr<CPVRChannelGroup>& group)
+{
+ if (!pItem || !group)
+ return false;
+
+ return group->UpdateChannel(
+ pItem->GetPVRChannelInfoTag()->StorageId(),
+ pItem->GetProperty(PROPERTY_CHANNEL_NAME).asString(),
+ pItem->GetProperty(PROPERTY_CHANNEL_ICON).asString(),
+ static_cast<int>(pItem->GetProperty(PROPERTY_CHANNEL_EPG_SOURCE).asInteger()),
+ m_bAllowRenumber ? pItem->GetProperty(PROPERTY_CHANNEL_NUMBER).asInteger() : 0,
+ !pItem->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean(), // hidden
+ pItem->GetProperty(PROPERTY_CHANNEL_EPG_ENABLED).asBoolean(),
+ pItem->GetProperty(PROPERTY_CHANNEL_LOCKED).asBoolean(),
+ pItem->GetProperty(PROPERTY_CHANNEL_CUSTOM_ICON).asBoolean(),
+ pItem->GetProperty(PROPERTY_CHANNEL_USER_SET_HIDDEN).asBoolean());
+}
+
+void CGUIDialogPVRChannelManager::PromptAndSaveList()
+{
+ if (!HasChangedItems())
+ return;
+
+ CGUIDialogYesNo* pDialogYesNo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (pDialogYesNo)
+ {
+ pDialogYesNo->SetHeading(CVariant{20052});
+ pDialogYesNo->SetLine(0, CVariant{""});
+ pDialogYesNo->SetLine(1, CVariant{19212});
+ pDialogYesNo->SetLine(2, CVariant{20103});
+ pDialogYesNo->Open();
+
+ if (pDialogYesNo->IsConfirmed())
+ SaveList();
+ else
+ Update();
+ }
+}
+
+void CGUIDialogPVRChannelManager::SaveList()
+{
+ if (!HasChangedItems())
+ return;
+
+ /* display the progress dialog */
+ CGUIDialogProgress* pDlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ pDlgProgress->SetHeading(CVariant{190});
+ pDlgProgress->SetLine(0, CVariant{""});
+ pDlgProgress->SetLine(1, CVariant{328});
+ pDlgProgress->SetLine(2, CVariant{""});
+ pDlgProgress->Open();
+ pDlgProgress->Progress();
+ pDlgProgress->SetPercentage(0);
+
+ /* persist all channels */
+ std::shared_ptr<CPVRChannelGroup> group = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_bIsRadio);
+ if (!group)
+ return;
+
+ for (int iListPtr = 0; iListPtr < m_channelItems->Size(); ++iListPtr)
+ {
+ CFileItemPtr pItem = m_channelItems->Get(iListPtr);
+ if (pItem && pItem->GetProperty(PROPERTY_ITEM_CHANGED).asBoolean())
+ {
+ if (pItem->GetProperty(PROPERTY_CLIENT_SUPPORTS_SETTINGS).asBoolean())
+ RenameChannel(pItem);
+
+ if (PersistChannel(pItem, group))
+ pItem->SetProperty(PROPERTY_ITEM_CHANGED, false);
+ }
+
+ pDlgProgress->SetPercentage(iListPtr * 100 / m_channelItems->Size());
+ }
+
+ group->SortAndRenumber();
+
+ auto channelGroups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio);
+ channelGroups->UpdateChannelNumbersFromAllChannelsGroup();
+ channelGroups->PersistAll();
+ pDlgProgress->Close();
+
+ CONTROL_DISABLE(BUTTON_APPLY);
+}
+
+bool CGUIDialogPVRChannelManager::HasChangedItems() const
+{
+ return std::any_of(m_channelItems->cbegin(), m_channelItems->cend(), [](const auto& item) {
+ return item && item->GetProperty(PROPERTY_ITEM_CHANGED).asBoolean();
+ });
+}
+
+namespace
+{
+
+bool IsItemChanged(const std::shared_ptr<CFileItem>& item)
+{
+ const std::shared_ptr<CPVRChannelGroupMember> member = item->GetPVRChannelGroupMemberInfoTag();
+ const std::shared_ptr<CPVRChannel> channel = member->Channel();
+
+ return item->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean() == channel->IsHidden() ||
+ item->GetProperty(PROPERTY_CHANNEL_USER_SET_HIDDEN).asBoolean() !=
+ channel->IsUserSetHidden() ||
+ item->GetProperty(PROPERTY_CHANNEL_NAME).asString() != channel->ChannelName() ||
+ item->GetProperty(PROPERTY_CHANNEL_EPG_ENABLED).asBoolean() != channel->EPGEnabled() ||
+ item->GetProperty(PROPERTY_CHANNEL_ICON).asString() != channel->ClientIconPath() ||
+ item->GetProperty(PROPERTY_CHANNEL_CUSTOM_ICON).asBoolean() != channel->IsUserSetIcon() ||
+ item->GetProperty(PROPERTY_CHANNEL_EPG_SOURCE).asInteger() != 0 ||
+ item->GetProperty(PROPERTY_CHANNEL_LOCKED).asBoolean() != channel->IsLocked() ||
+ item->GetProperty(PROPERTY_CHANNEL_NUMBER).asString() !=
+ member->ChannelNumber().FormattedChannelNumber();
+}
+
+} // namespace
+
+void CGUIDialogPVRChannelManager::SetItemChanged(const CFileItemPtr& pItem)
+{
+ const bool changed = IsItemChanged(pItem);
+ pItem->SetProperty(PROPERTY_ITEM_CHANGED, changed);
+
+ if (changed || HasChangedItems())
+ CONTROL_ENABLE(BUTTON_APPLY);
+ else
+ CONTROL_DISABLE(BUTTON_APPLY);
+}
+
+void CGUIDialogPVRChannelManager::Renumber()
+{
+ if (!m_bAllowRenumber)
+ return;
+
+ int iNextChannelNumber = 0;
+ for (const auto& item : *m_channelItems)
+ {
+ const std::string number = item->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean()
+ ? std::to_string(++iNextChannelNumber)
+ : LABEL_CHANNEL_DISABLED;
+
+ if (item->GetProperty(PROPERTY_CHANNEL_NUMBER).asString() != number)
+ {
+ item->SetProperty(PROPERTY_CHANNEL_NUMBER, number);
+ SetItemChanged(item);
+ }
+ }
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.h b/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.h
new file mode 100644
index 0000000..0716436
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "dialogs/GUIDialogContextMenu.h"
+#include "guilib/GUIDialog.h"
+#include "view/GUIViewControl.h"
+
+#include <memory>
+#include <vector>
+
+class CAction;
+class CFileItemList;
+class CGUIMessage;
+
+namespace PVR
+{
+ class CPVRChannelGroup;
+ class CPVRClient;
+
+ class CGUIDialogPVRChannelManager : public CGUIDialog
+ {
+ public:
+ CGUIDialogPVRChannelManager();
+ ~CGUIDialogPVRChannelManager() override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+ bool HasListItems() const override{ return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+
+ void Open(const std::shared_ptr<CFileItem>& initialSelection);
+ void SetRadio(bool bIsRadio);
+
+ protected:
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ private:
+ void Clear();
+ void Update();
+ void PromptAndSaveList();
+ void SaveList();
+ void Renumber();
+ void SetData(int iItem);
+ void RenameChannel(const CFileItemPtr& pItem);
+
+ void ClearChannelOptions();
+ void EnableChannelOptions(bool bEnable);
+
+ bool OnPopupMenu(int iItem);
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button);
+ bool OnActionMove(const CAction& action);
+ bool OnMessageClick(const CGUIMessage& message);
+ bool OnClickListChannels(const CGUIMessage& message);
+ bool OnClickButtonOK();
+ bool OnClickButtonApply();
+ bool OnClickButtonCancel();
+ bool OnClickButtonRadioTV();
+ bool OnClickButtonRadioActive();
+ bool OnClickButtonRadioParentalLocked();
+ bool OnClickButtonEditName();
+ bool OnClickButtonChannelLogo();
+ bool OnClickButtonUseEPG();
+ bool OnClickEPGSourceSpin();
+ bool OnClickButtonGroupManager();
+ bool OnClickButtonNewChannel();
+ bool OnClickButtonRefreshChannelLogos();
+
+ bool PersistChannel(const CFileItemPtr& pItem, const std::shared_ptr<CPVRChannelGroup>& group);
+
+ bool HasChangedItems() const;
+ void SetItemChanged(const CFileItemPtr& pItem);
+
+ bool m_bIsRadio = false;
+ bool m_bMovingMode = false;
+ bool m_bAllowNewChannel = false;
+ bool m_bAllowRenumber = false;
+ bool m_bAllowReorder = false;
+
+ std::shared_ptr<CFileItem> m_initialSelection;
+ int m_iSelected = 0;
+ CFileItemList* m_channelItems;
+ CGUIViewControl m_viewControl;
+
+ std::vector<std::shared_ptr<CPVRClient>> m_clientsWithSettingsList;
+ };
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.cpp b/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.cpp
new file mode 100644
index 0000000..b5830d4
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.cpp
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRChannelsOSD.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+using namespace std::chrono_literals;
+
+#define MAX_INVALIDATION_FREQUENCY 2000ms // limit to one invalidation per X milliseconds
+
+CGUIDialogPVRChannelsOSD::CGUIDialogPVRChannelsOSD()
+ : CGUIDialogPVRItemsViewBase(WINDOW_DIALOG_PVR_OSD_CHANNELS, "DialogPVRChannelsOSD.xml")
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().RegisterChannelNumberInputHandler(this);
+}
+
+CGUIDialogPVRChannelsOSD::~CGUIDialogPVRChannelsOSD()
+{
+ auto& mgr = CServiceBroker::GetPVRManager();
+ mgr.Events().Unsubscribe(this);
+ mgr.Get<PVR::GUI::Channels>().DeregisterChannelNumberInputHandler(this);
+}
+
+bool CGUIDialogPVRChannelsOSD::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_REFRESH_LIST)
+ {
+ switch (static_cast<PVREvent>(message.GetParam1()))
+ {
+ case PVREvent::CurrentItem:
+ m_viewControl.SetItems(*m_vecItems);
+ return true;
+
+ case PVREvent::Epg:
+ case PVREvent::EpgContainer:
+ case PVREvent::EpgActiveItem:
+ if (IsActive())
+ SetInvalid();
+ return true;
+
+ default:
+ break;
+ }
+ }
+ return CGUIDialogPVRItemsViewBase::OnMessage(message);
+}
+
+void CGUIDialogPVRChannelsOSD::OnInitWindow()
+{
+ if (!CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV() &&
+ !CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio())
+ {
+ Close();
+ return;
+ }
+
+ Init();
+ Update();
+ CGUIDialogPVRItemsViewBase::OnInitWindow();
+}
+
+void CGUIDialogPVRChannelsOSD::OnDeinitWindow(int nextWindowID)
+{
+ if (m_group)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().SetSelectedChannelPath(
+ m_group->IsRadio(), m_viewControl.GetSelectedItemPath());
+
+ // next OnInitWindow will set the group which is then selected
+ m_group.reset();
+ }
+
+ CGUIDialogPVRItemsViewBase::OnDeinitWindow(nextWindowID);
+}
+
+bool CGUIDialogPVRChannelsOSD::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ {
+ // If direct channel number input is active, select the entered channel.
+ if (CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNumberInputHandler()
+ .CheckInputAndExecuteAction())
+ return true;
+
+ if (m_viewControl.HasControl(GetFocusedControlID()))
+ {
+ // Switch to channel
+ GotoChannel(m_viewControl.GetSelectedItem());
+ return true;
+ }
+ break;
+ }
+ case ACTION_PREVIOUS_CHANNELGROUP:
+ case ACTION_NEXT_CHANNELGROUP:
+ {
+ // save control states and currently selected item of group
+ SaveControlStates();
+
+ // switch to next or previous group
+ const CPVRChannelGroups* groups =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_group->IsRadio());
+ const std::shared_ptr<CPVRChannelGroup> nextGroup = action.GetID() == ACTION_NEXT_CHANNELGROUP
+ ? groups->GetNextGroup(*m_group)
+ : groups->GetPreviousGroup(*m_group);
+ CServiceBroker::GetPVRManager().PlaybackState()->SetActiveChannelGroup(nextGroup);
+ m_group = nextGroup;
+ Init();
+ Update();
+
+ // restore control states and previously selected item of group
+ RestoreControlStates();
+ return true;
+ }
+ case REMOTE_0:
+ case REMOTE_1:
+ case REMOTE_2:
+ case REMOTE_3:
+ case REMOTE_4:
+ case REMOTE_5:
+ case REMOTE_6:
+ case REMOTE_7:
+ case REMOTE_8:
+ case REMOTE_9:
+ {
+ AppendChannelNumberCharacter(static_cast<char>(action.GetID() - REMOTE_0) + '0');
+ return true;
+ }
+ case ACTION_CHANNEL_NUMBER_SEP:
+ {
+ AppendChannelNumberCharacter(CPVRChannelNumber::SEPARATOR);
+ return true;
+ }
+ default:
+ break;
+ }
+
+ return CGUIDialogPVRItemsViewBase::OnAction(action);
+}
+
+void CGUIDialogPVRChannelsOSD::Update()
+{
+ CPVRManager& pvrMgr = CServiceBroker::GetPVRManager();
+ pvrMgr.Events().Subscribe(this, &CGUIDialogPVRChannelsOSD::Notify);
+
+ const std::shared_ptr<CPVRChannel> channel = pvrMgr.PlaybackState()->GetPlayingChannel();
+ if (channel)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ pvrMgr.PlaybackState()->GetActiveChannelGroup(channel->IsRadio());
+ if (group)
+ {
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ group->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+ for (const auto& groupMember : groupMembers)
+ {
+ m_vecItems->Add(std::make_shared<CFileItem>(groupMember));
+ }
+
+ m_viewControl.SetItems(*m_vecItems);
+
+ if (!m_group)
+ {
+ m_group = group;
+ m_viewControl.SetSelectedItem(
+ pvrMgr.Get<PVR::GUI::Channels>().GetSelectedChannelPath(channel->IsRadio()));
+ SaveSelectedItemPath(group->GroupID());
+ }
+ }
+ }
+}
+
+void CGUIDialogPVRChannelsOSD::SetInvalid()
+{
+ if (m_refreshTimeout.IsTimePast())
+ {
+ for (const auto& item : *m_vecItems)
+ item->SetInvalid();
+
+ CGUIDialogPVRItemsViewBase::SetInvalid();
+ m_refreshTimeout.Set(MAX_INVALIDATION_FREQUENCY);
+ }
+}
+
+void CGUIDialogPVRChannelsOSD::SaveControlStates()
+{
+ CGUIDialogPVRItemsViewBase::SaveControlStates();
+
+ if (m_group)
+ SaveSelectedItemPath(m_group->GroupID());
+}
+
+void CGUIDialogPVRChannelsOSD::RestoreControlStates()
+{
+ CGUIDialogPVRItemsViewBase::RestoreControlStates();
+
+ if (m_group)
+ {
+ const std::string path = GetLastSelectedItemPath(m_group->GroupID());
+ if (path.empty())
+ m_viewControl.SetSelectedItem(0);
+ else
+ m_viewControl.SetSelectedItem(path);
+ }
+}
+
+void CGUIDialogPVRChannelsOSD::GotoChannel(int iItem)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ return;
+
+ // Preserve the item before closing self, because this will clear m_vecItems
+ const std::shared_ptr<CFileItem> item = m_vecItems->Get(iItem);
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRMENU_CLOSECHANNELOSDONSWITCH))
+ Close();
+
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ *item, true /* bCheckResume */);
+}
+
+void CGUIDialogPVRChannelsOSD::Notify(const PVREvent& event)
+{
+ const CGUIMessage m(GUI_MSG_REFRESH_LIST, GetID(), 0, static_cast<int>(event));
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(m);
+}
+
+void CGUIDialogPVRChannelsOSD::SaveSelectedItemPath(int iGroupID)
+{
+ m_groupSelectedItemPaths[iGroupID] = m_viewControl.GetSelectedItemPath();
+}
+
+std::string CGUIDialogPVRChannelsOSD::GetLastSelectedItemPath(int iGroupID) const
+{
+ const auto it = m_groupSelectedItemPaths.find(iGroupID);
+ if (it != m_groupSelectedItemPaths.end())
+ return it->second;
+
+ return std::string();
+}
+
+void CGUIDialogPVRChannelsOSD::GetChannelNumbers(std::vector<std::string>& channelNumbers)
+{
+ if (m_group)
+ m_group->GetChannelNumbers(channelNumbers);
+}
+
+void CGUIDialogPVRChannelsOSD::OnInputDone()
+{
+ const CPVRChannelNumber channelNumber = GetChannelNumber();
+ if (channelNumber.IsValid())
+ {
+ int itemIndex = 0;
+ for (const CFileItemPtr& channel : *m_vecItems)
+ {
+ if (channel->GetPVRChannelGroupMemberInfoTag()->ChannelNumber() == channelNumber)
+ {
+ m_viewControl.SetSelectedItem(itemIndex);
+ return;
+ }
+ ++itemIndex;
+ }
+ }
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.h b/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.h
new file mode 100644
index 0000000..c23fb92
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/PVRChannelNumberInputHandler.h"
+#include "pvr/dialogs/GUIDialogPVRItemsViewBase.h"
+#include "threads/SystemClock.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+namespace PVR
+{
+enum class PVREvent;
+
+class CPVRChannelGroup;
+
+class CGUIDialogPVRChannelsOSD : public CGUIDialogPVRItemsViewBase,
+ public CPVRChannelNumberInputHandler
+{
+public:
+ CGUIDialogPVRChannelsOSD();
+ ~CGUIDialogPVRChannelsOSD() override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+
+ // CPVRChannelNumberInputHandler implementation
+ void GetChannelNumbers(std::vector<std::string>& channelNumbers) override;
+ void OnInputDone() override;
+
+protected:
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ void RestoreControlStates() override;
+ void SaveControlStates() override;
+ void SetInvalid() override;
+
+private:
+ void GotoChannel(int iItem);
+ void Update();
+ void SaveSelectedItemPath(int iGroupID);
+ std::string GetLastSelectedItemPath(int iGroupID) const;
+
+ std::shared_ptr<CPVRChannelGroup> m_group;
+ std::map<int, std::string> m_groupSelectedItemPaths;
+ XbmcThreads::EndTime<> m_refreshTimeout;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.cpp b/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.cpp
new file mode 100644
index 0000000..ca83ba1
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.cpp
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRClientPriorities.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIMessage.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <cstdlib>
+#include <memory>
+
+using namespace PVR;
+
+CGUIDialogPVRClientPriorities::CGUIDialogPVRClientPriorities() :
+ CGUIDialogSettingsManualBase(WINDOW_DIALOG_PVR_CLIENT_PRIORITIES, "DialogSettings.xml")
+{
+ m_loadType = LOAD_EVERY_TIME;
+}
+
+void CGUIDialogPVRClientPriorities::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+
+ SetHeading(19240); // Client priorities
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186); // OK
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222); // Cancel
+}
+
+std::string CGUIDialogPVRClientPriorities::GetSettingsLabel(
+ const std::shared_ptr<ISetting>& pSetting)
+{
+ int iClientId = std::atoi(pSetting->GetId().c_str());
+ auto clientEntry = m_clients.find(iClientId);
+ if (clientEntry != m_clients.end())
+ return clientEntry->second->GetFriendlyName();
+
+ CLog::LogF(LOGERROR, "Unable to obtain pvr client with id '{}'", iClientId);
+ return CGUIDialogSettingsManualBase::GetLocalizedString(13205); // Unknown
+}
+
+void CGUIDialogPVRClientPriorities::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("pvrclientpriorities", -1);
+ if (category == nullptr)
+ {
+ CLog::LogF(LOGERROR, "Unable to add settings category");
+ return;
+ }
+
+ const std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == nullptr)
+ {
+ CLog::LogF(LOGERROR, "Unable to add settings group");
+ return;
+ }
+
+ m_clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients();
+ for (const auto& client : m_clients)
+ {
+ AddEdit(group, std::to_string(client.second->GetID()), 13205 /* Unknown */, SettingLevel::Basic,
+ client.second->GetPriority());
+ }
+}
+
+void CGUIDialogPVRClientPriorities::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ {
+ CLog::LogF(LOGERROR, "No setting");
+ return;
+ }
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ m_changedValues[setting->GetId()] = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+}
+
+bool CGUIDialogPVRClientPriorities::Save()
+{
+ for (const auto& changedClient : m_changedValues)
+ {
+ int iClientId = std::atoi(changedClient.first.c_str());
+ auto clientEntry = m_clients.find(iClientId);
+ if (clientEntry != m_clients.end())
+ clientEntry->second->SetPriority(changedClient.second);
+ }
+
+ return true;
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.h b/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.h
new file mode 100644
index 0000000..a6ca62d
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/addons/PVRClients.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+#include <map>
+#include <string>
+
+namespace PVR
+{
+ class CGUIDialogPVRClientPriorities : public CGUIDialogSettingsManualBase
+ {
+ public:
+ CGUIDialogPVRClientPriorities();
+
+ protected:
+ // implementation of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ std::string GetSettingsLabel(const std::shared_ptr<ISetting>& pSetting) override;
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+ private:
+ CPVRClientMap m_clients;
+ std::map<std::string, int> m_changedValues;
+ };
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.cpp b/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.cpp
new file mode 100644
index 0000000..dbc37f3
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.cpp
@@ -0,0 +1,546 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRGroupManager.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIRadioButtonControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/helpers//DialogOKHelper.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/filesystem/PVRGUIDirectory.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+using namespace KODI::MESSAGING;
+using namespace PVR;
+
+#define CONTROL_LIST_CHANNELS_LEFT 11
+#define CONTROL_LIST_CHANNELS_RIGHT 12
+#define CONTROL_LIST_CHANNEL_GROUPS 13
+#define CONTROL_CURRENT_GROUP_LABEL 20
+#define CONTROL_UNGROUPED_LABEL 21
+#define CONTROL_IN_GROUP_LABEL 22
+#define BUTTON_HIDE_GROUP 25
+#define BUTTON_NEWGROUP 26
+#define BUTTON_RENAMEGROUP 27
+#define BUTTON_DELGROUP 28
+#define BUTTON_OK 29
+#define BUTTON_TOGGLE_RADIO_TV 34
+#define BUTTON_RECREATE_GROUP_THUMB 35
+
+CGUIDialogPVRGroupManager::CGUIDialogPVRGroupManager() :
+ CGUIDialog(WINDOW_DIALOG_PVR_GROUP_MANAGER, "DialogPVRGroupManager.xml")
+{
+ m_ungroupedChannels = new CFileItemList;
+ m_groupMembers = new CFileItemList;
+ m_channelGroups = new CFileItemList;
+
+ SetRadio(false);
+}
+
+CGUIDialogPVRGroupManager::~CGUIDialogPVRGroupManager()
+{
+ delete m_ungroupedChannels;
+ delete m_groupMembers;
+ delete m_channelGroups;
+}
+
+void CGUIDialogPVRGroupManager::SetRadio(bool bIsRadio)
+{
+ m_bIsRadio = bIsRadio;
+ SetProperty("IsRadio", m_bIsRadio ? "true" : "");
+}
+
+bool CGUIDialogPVRGroupManager::PersistChanges()
+{
+ return CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio)->PersistAll();
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonOk(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (iControl == BUTTON_OK)
+ {
+ PersistChanges();
+ Close();
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonNewGroup(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (iControl == BUTTON_NEWGROUP)
+ {
+ std::string strGroupName = "";
+ /* prompt for a group name */
+ if (CGUIKeyboardFactory::ShowAndGetInput(strGroupName, CVariant{g_localizeStrings.Get(19139)}, false))
+ {
+ if (strGroupName != "")
+ {
+ /* add the group if it doesn't already exist */
+ CPVRChannelGroups* groups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio);
+ if (groups->AddGroup(strGroupName))
+ {
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio)->GetByName(strGroupName)->SetGroupType(PVR_GROUP_TYPE_USER_DEFINED);
+ m_iSelectedChannelGroup = groups->Size() - 1;
+ Update();
+ }
+ }
+ }
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonDeleteGroup(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (iControl == BUTTON_DELGROUP)
+ {
+ if (!m_selectedGroup)
+ return bReturn;
+
+ CGUIDialogYesNo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (!pDialog)
+ return bReturn;
+
+ pDialog->SetHeading(CVariant{117});
+ pDialog->SetLine(0, CVariant{""});
+ pDialog->SetLine(1, CVariant{m_selectedGroup->GroupName()});
+ pDialog->SetLine(2, CVariant{""});
+ pDialog->Open();
+
+ if (pDialog->IsConfirmed())
+ {
+ ClearSelectedGroupsThumbnail();
+ if (CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(m_bIsRadio)
+ ->DeleteGroup(m_selectedGroup))
+ Update();
+ }
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonRenameGroup(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (iControl == BUTTON_RENAMEGROUP)
+ {
+ if (!m_selectedGroup)
+ return bReturn;
+
+ std::string strGroupName(m_selectedGroup->GroupName());
+ if (CGUIKeyboardFactory::ShowAndGetInput(strGroupName, CVariant{g_localizeStrings.Get(19139)}, false))
+ {
+ if (!strGroupName.empty())
+ {
+ ClearSelectedGroupsThumbnail();
+ m_selectedGroup->SetGroupName(strGroupName);
+ Update();
+ }
+ }
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonUngroupedChannels(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (m_viewUngroupedChannels.HasControl(iControl)) // list/thumb control
+ {
+ m_iSelectedUngroupedChannel = m_viewUngroupedChannels.GetSelectedItem();
+ int iAction = message.GetParam1();
+
+ if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK)
+ {
+ if (m_channelGroups->GetFolderCount() == 0)
+ {
+ HELPERS::ShowOKDialogText(CVariant{19033}, CVariant{19137});
+ }
+ else if (m_ungroupedChannels->GetFileCount() > 0)
+ {
+ CFileItemPtr pItemChannel = m_ungroupedChannels->Get(m_iSelectedUngroupedChannel);
+
+ if (m_selectedGroup->AppendToGroup(pItemChannel->GetPVRChannelInfoTag()))
+ {
+ ClearSelectedGroupsThumbnail();
+ Update();
+ }
+ }
+ }
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonGroupMembers(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (m_viewGroupMembers.HasControl(iControl)) // list/thumb control
+ {
+ m_iSelectedGroupMember = m_viewGroupMembers.GetSelectedItem();
+ int iAction = message.GetParam1();
+
+ if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK)
+ {
+ if (m_selectedGroup && m_groupMembers->GetFileCount() > 0)
+ {
+ CFileItemPtr pItemChannel = m_groupMembers->Get(m_iSelectedGroupMember);
+ m_selectedGroup->RemoveFromGroup(pItemChannel->GetPVRChannelInfoTag());
+ ClearSelectedGroupsThumbnail();
+ Update();
+ }
+ }
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonChannelGroups(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (m_viewChannelGroups.HasControl(iControl)) // list/thumb control
+ {
+ int iAction = message.GetParam1();
+
+ if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK)
+ {
+ m_iSelectedChannelGroup = m_viewChannelGroups.GetSelectedItem();
+ Update();
+ }
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonHideGroup(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == BUTTON_HIDE_GROUP && m_selectedGroup)
+ {
+ CGUIRadioButtonControl* button = static_cast<CGUIRadioButtonControl*>(GetControl(message.GetSenderId()));
+ if (button)
+ {
+ CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(m_bIsRadio)
+ ->HideGroup(m_selectedGroup, button->IsSelected());
+ Update();
+ }
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonToggleRadioTV(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == BUTTON_TOGGLE_RADIO_TV)
+ {
+ PersistChanges();
+ SetRadio(!m_bIsRadio);
+ Update();
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonRecreateThumbnail(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == BUTTON_RECREATE_GROUP_THUMB)
+ {
+ m_thumbLoader.ClearCachedImages(*m_channelGroups);
+ Update();
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::OnMessageClick(const CGUIMessage& message)
+{
+ return ActionButtonOk(message) ||
+ ActionButtonNewGroup(message) ||
+ ActionButtonDeleteGroup(message) ||
+ ActionButtonRenameGroup(message) ||
+ ActionButtonUngroupedChannels(message) ||
+ ActionButtonGroupMembers(message) ||
+ ActionButtonChannelGroups(message) ||
+ ActionButtonHideGroup(message) ||
+ ActionButtonToggleRadioTV(message) ||
+ ActionButtonRecreateThumbnail(message);
+}
+
+bool CGUIDialogPVRGroupManager::OnMessage(CGUIMessage& message)
+{
+ unsigned int iMessage = message.GetMessage();
+
+ switch (iMessage)
+ {
+ case GUI_MSG_CLICKED:
+ {
+ OnMessageClick(message);
+ }
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogPVRGroupManager::OnActionMove(const CAction& action)
+{
+ bool bReturn = false;
+ int iActionId = action.GetID();
+
+ if (GetFocusedControlID() == CONTROL_LIST_CHANNEL_GROUPS)
+ {
+ if (iActionId == ACTION_MOUSE_MOVE)
+ {
+ int iSelected = m_viewChannelGroups.GetSelectedItem();
+ if (m_iSelectedChannelGroup < iSelected)
+ {
+ iActionId = ACTION_MOVE_DOWN;
+ }
+ else if (m_iSelectedChannelGroup > iSelected)
+ {
+ iActionId = ACTION_MOVE_UP;
+ }
+ else
+ {
+ return bReturn;
+ }
+ }
+
+ if (iActionId == ACTION_MOVE_DOWN || iActionId == ACTION_MOVE_UP ||
+ iActionId == ACTION_PAGE_DOWN || iActionId == ACTION_PAGE_UP ||
+ iActionId == ACTION_FIRST_PAGE || iActionId == ACTION_LAST_PAGE)
+ {
+ CGUIDialog::OnAction(action);
+ int iSelected = m_viewChannelGroups.GetSelectedItem();
+
+ bReturn = true;
+ if (iSelected != m_iSelectedChannelGroup)
+ {
+ m_iSelectedChannelGroup = iSelected;
+ Update();
+ }
+ }
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::OnAction(const CAction& action)
+{
+ return OnActionMove(action) ||
+ CGUIDialog::OnAction(action);
+}
+
+void CGUIDialogPVRGroupManager::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+ m_iSelectedUngroupedChannel = 0;
+ m_iSelectedGroupMember = 0;
+ m_iSelectedChannelGroup = 0;
+ Update();
+}
+
+void CGUIDialogPVRGroupManager::OnDeinitWindow(int nextWindowID)
+{
+ Clear();
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIDialogPVRGroupManager::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+
+ m_viewUngroupedChannels.Reset();
+ m_viewUngroupedChannels.SetParentWindow(GetID());
+ m_viewUngroupedChannels.AddView(GetControl(CONTROL_LIST_CHANNELS_LEFT));
+
+ m_viewGroupMembers.Reset();
+ m_viewGroupMembers.SetParentWindow(GetID());
+ m_viewGroupMembers.AddView(GetControl(CONTROL_LIST_CHANNELS_RIGHT));
+
+ m_viewChannelGroups.Reset();
+ m_viewChannelGroups.SetParentWindow(GetID());
+ m_viewChannelGroups.AddView(GetControl(CONTROL_LIST_CHANNEL_GROUPS));
+}
+
+void CGUIDialogPVRGroupManager::OnWindowUnload()
+{
+ CGUIDialog::OnWindowUnload();
+ m_viewUngroupedChannels.Reset();
+ m_viewGroupMembers.Reset();
+ m_viewChannelGroups.Reset();
+}
+
+void CGUIDialogPVRGroupManager::Update()
+{
+ m_viewUngroupedChannels.SetCurrentView(CONTROL_LIST_CHANNELS_LEFT);
+ m_viewGroupMembers.SetCurrentView(CONTROL_LIST_CHANNELS_RIGHT);
+ m_viewChannelGroups.SetCurrentView(CONTROL_LIST_CHANNEL_GROUPS);
+
+ Clear();
+
+ // get the groups list
+ CPVRGUIDirectory::GetChannelGroupsDirectory(m_bIsRadio, false, *m_channelGroups);
+
+ // Load group thumbnails
+ m_thumbLoader.Load(*m_channelGroups);
+
+ m_viewChannelGroups.SetItems(*m_channelGroups);
+ m_viewChannelGroups.SetSelectedItem(m_iSelectedChannelGroup);
+
+ /* select a group or select the default group if no group was selected */
+ CFileItemPtr pItem = m_channelGroups->Get(m_viewChannelGroups.GetSelectedItem());
+ m_selectedGroup = CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio)->GetByName(pItem->m_strTitle);
+ if (m_selectedGroup)
+ {
+ /* set this group in the pvrmanager, so it becomes the selected group in other dialogs too */
+ CServiceBroker::GetPVRManager().PlaybackState()->SetActiveChannelGroup(m_selectedGroup);
+ SET_CONTROL_LABEL(CONTROL_CURRENT_GROUP_LABEL, m_selectedGroup->GroupName());
+ SET_CONTROL_SELECTED(GetID(), BUTTON_HIDE_GROUP, m_selectedGroup->IsHidden());
+
+ CONTROL_ENABLE_ON_CONDITION(BUTTON_DELGROUP, !m_selectedGroup->IsInternalGroup());
+
+ if (m_selectedGroup->IsInternalGroup())
+ {
+ std::string strNewLabel = StringUtils::Format("{} {}", g_localizeStrings.Get(19022),
+ m_bIsRadio ? g_localizeStrings.Get(19024)
+ : g_localizeStrings.Get(19023));
+ SET_CONTROL_LABEL(CONTROL_UNGROUPED_LABEL, strNewLabel);
+
+ strNewLabel = StringUtils::Format("{} {}", g_localizeStrings.Get(19218),
+ m_bIsRadio ? g_localizeStrings.Get(19024)
+ : g_localizeStrings.Get(19023));
+ SET_CONTROL_LABEL(CONTROL_IN_GROUP_LABEL, strNewLabel);
+ }
+ else
+ {
+ std::string strNewLabel = g_localizeStrings.Get(19219);
+ SET_CONTROL_LABEL(CONTROL_UNGROUPED_LABEL, strNewLabel);
+
+ strNewLabel =
+ StringUtils::Format("{} {}", g_localizeStrings.Get(19220), m_selectedGroup->GroupName());
+ SET_CONTROL_LABEL(CONTROL_IN_GROUP_LABEL, strNewLabel);
+ }
+
+ // Slightly different handling for "all" group...
+ if (m_selectedGroup->IsInternalGroup())
+ {
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ m_selectedGroup->GetMembers(CPVRChannelGroup::Include::ALL);
+ for (const auto& groupMember : groupMembers)
+ {
+ if (groupMember->Channel()->IsHidden())
+ m_ungroupedChannels->Add(std::make_shared<CFileItem>(groupMember));
+ else
+ m_groupMembers->Add(std::make_shared<CFileItem>(groupMember));
+ }
+ }
+ else
+ {
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ m_selectedGroup->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+ for (const auto& groupMember : groupMembers)
+ {
+ m_groupMembers->Add(std::make_shared<CFileItem>(groupMember));
+ }
+
+ /* for the center part, get all channels of the "all" channels group that are not in this group */
+ const std::shared_ptr<CPVRChannelGroup> allGroup = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_bIsRadio);
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> allGroupMembers =
+ allGroup->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+ for (const auto& groupMember : allGroupMembers)
+ {
+ if (!m_selectedGroup->IsGroupMember(groupMember->Channel()))
+ m_ungroupedChannels->Add(std::make_shared<CFileItem>(groupMember));
+ }
+ }
+ m_viewGroupMembers.SetItems(*m_groupMembers);
+ m_viewGroupMembers.SetSelectedItem(m_iSelectedGroupMember);
+
+ m_viewUngroupedChannels.SetItems(*m_ungroupedChannels);
+ m_viewUngroupedChannels.SetSelectedItem(m_iSelectedUngroupedChannel);
+ }
+}
+
+void CGUIDialogPVRGroupManager::Clear()
+{
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ m_viewUngroupedChannels.Clear();
+ m_viewGroupMembers.Clear();
+ m_viewChannelGroups.Clear();
+
+ m_ungroupedChannels->Clear();
+ m_groupMembers->Clear();
+ m_channelGroups->Clear();
+}
+
+void CGUIDialogPVRGroupManager::ClearSelectedGroupsThumbnail()
+{
+ m_thumbLoader.ClearCachedImage(*m_channelGroups->Get(m_iSelectedChannelGroup));
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.h b/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.h
new file mode 100644
index 0000000..524205a
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "guilib/GUIDialog.h"
+#include "pvr/PVRThumbLoader.h"
+#include "view/GUIViewControl.h"
+
+#include <memory>
+
+class CFileItemList;
+class CGUIMessage;
+
+namespace PVR
+{
+ class CPVRChannelGroup;
+
+ class CGUIDialogPVRGroupManager : public CGUIDialog
+ {
+ public:
+ CGUIDialogPVRGroupManager();
+ ~CGUIDialogPVRGroupManager() override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+
+ void SetRadio(bool bIsRadio);
+
+ protected:
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ private:
+ void Clear();
+ void ClearSelectedGroupsThumbnail();
+ void Update();
+ bool PersistChanges();
+ bool ActionButtonOk(const CGUIMessage& message);
+ bool ActionButtonNewGroup(const CGUIMessage& message);
+ bool ActionButtonDeleteGroup(const CGUIMessage& message);
+ bool ActionButtonRenameGroup(const CGUIMessage& message);
+ bool ActionButtonUngroupedChannels(const CGUIMessage& message);
+ bool ActionButtonGroupMembers(const CGUIMessage& message);
+ bool ActionButtonChannelGroups(const CGUIMessage& message);
+ bool ActionButtonHideGroup(const CGUIMessage& message);
+ bool ActionButtonToggleRadioTV(const CGUIMessage& message);
+ bool ActionButtonRecreateThumbnail(const CGUIMessage& message);
+ bool OnMessageClick(const CGUIMessage& message);
+ bool OnActionMove(const CAction& action);
+
+ std::shared_ptr<CPVRChannelGroup> m_selectedGroup;
+ bool m_bIsRadio;
+
+ int m_iSelectedUngroupedChannel = 0;
+ int m_iSelectedGroupMember = 0;
+ int m_iSelectedChannelGroup = 0;
+
+ CFileItemList * m_ungroupedChannels;
+ CFileItemList * m_groupMembers;
+ CFileItemList * m_channelGroups;
+
+ CGUIViewControl m_viewUngroupedChannels;
+ CGUIViewControl m_viewGroupMembers;
+ CGUIViewControl m_viewChannelGroups;
+
+ CPVRThumbLoader m_thumbLoader;
+ };
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.cpp b/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.cpp
new file mode 100644
index 0000000..2f41af5
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.cpp
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRGuideControls.h"
+
+using namespace PVR;
+
+CGUIDialogPVRGuideControls::CGUIDialogPVRGuideControls()
+ : CGUIDialog(WINDOW_DIALOG_PVR_GUIDE_CONTROLS, "DialogPVRGuideControls.xml")
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogPVRGuideControls::~CGUIDialogPVRGuideControls() = default;
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.h b/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.h
new file mode 100644
index 0000000..9b81f2b
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "guilib/GUIDialog.h"
+
+namespace PVR
+{
+class CGUIDialogPVRGuideControls : public CGUIDialog
+{
+public:
+ CGUIDialogPVRGuideControls();
+ ~CGUIDialogPVRGuideControls() override;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.cpp b/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.cpp
new file mode 100644
index 0000000..9bce21e
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.cpp
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRGuideInfo.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIMessage.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+
+#include <memory>
+
+using namespace PVR;
+
+#define CONTROL_BTN_FIND 4
+#define CONTROL_BTN_SWITCH 5
+#define CONTROL_BTN_RECORD 6
+#define CONTROL_BTN_OK 7
+#define CONTROL_BTN_PLAY_RECORDING 8
+#define CONTROL_BTN_ADD_TIMER 9
+#define CONTROL_BTN_PLAY_EPGTAG 10
+#define CONTROL_BTN_SET_REMINDER 11
+
+CGUIDialogPVRGuideInfo::CGUIDialogPVRGuideInfo()
+ : CGUIDialog(WINDOW_DIALOG_PVR_GUIDE_INFO, "DialogPVRInfo.xml")
+{
+}
+
+CGUIDialogPVRGuideInfo::~CGUIDialogPVRGuideInfo() = default;
+
+bool CGUIDialogPVRGuideInfo::OnClickButtonOK(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_OK)
+ {
+ Close();
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGuideInfo::OnClickButtonRecord(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_RECORD)
+ {
+ auto& mgr = CServiceBroker::GetPVRManager();
+
+ const std::shared_ptr<CPVRTimerInfoTag> timerTag =
+ mgr.Timers()->GetTimerForEpgTag(m_progItem->GetEPGInfoTag());
+ if (timerTag)
+ {
+ if (timerTag->IsRecording())
+ bReturn = mgr.Get<PVR::GUI::Timers>().StopRecording(CFileItem(timerTag));
+ else
+ bReturn = mgr.Get<PVR::GUI::Timers>().DeleteTimer(CFileItem(timerTag));
+ }
+ else
+ {
+ bReturn = mgr.Get<PVR::GUI::Timers>().AddTimer(*m_progItem, false);
+ }
+ }
+
+ if (bReturn)
+ Close();
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGuideInfo::OnClickButtonAddTimer(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_ADD_TIMER)
+ {
+ auto& mgr = CServiceBroker::GetPVRManager();
+ if (m_progItem && !mgr.Timers()->GetTimerForEpgTag(m_progItem->GetEPGInfoTag()))
+ {
+ bReturn = mgr.Get<PVR::GUI::Timers>().AddTimerRule(*m_progItem, true, true);
+ }
+ }
+
+ if (bReturn)
+ Close();
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGuideInfo::OnClickButtonSetReminder(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_SET_REMINDER)
+ {
+ auto& mgr = CServiceBroker::GetPVRManager();
+ if (m_progItem && !mgr.Timers()->GetTimerForEpgTag(m_progItem->GetEPGInfoTag()))
+ {
+ bReturn = mgr.Get<PVR::GUI::Timers>().AddReminder(*m_progItem);
+ }
+ }
+
+ if (bReturn)
+ Close();
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGuideInfo::OnClickButtonPlay(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_SWITCH ||
+ message.GetSenderId() == CONTROL_BTN_PLAY_RECORDING ||
+ message.GetSenderId() == CONTROL_BTN_PLAY_EPGTAG)
+ {
+ Close();
+
+ if (m_progItem)
+ {
+ if (message.GetSenderId() == CONTROL_BTN_PLAY_RECORDING)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(
+ *m_progItem, true /* bCheckResume */);
+ else if (message.GetSenderId() == CONTROL_BTN_PLAY_EPGTAG &&
+ m_progItem->GetEPGInfoTag()->IsPlayable())
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayEpgTag(*m_progItem);
+ else
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ *m_progItem, true /* bCheckResume */);
+
+ bReturn = true;
+ }
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGuideInfo::OnClickButtonFind(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_FIND)
+ {
+ Close();
+ if (m_progItem)
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().FindSimilar(*m_progItem);
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGuideInfo::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ return OnClickButtonOK(message) || OnClickButtonRecord(message) ||
+ OnClickButtonPlay(message) || OnClickButtonFind(message) ||
+ OnClickButtonAddTimer(message) || OnClickButtonSetReminder(message);
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogPVRGuideInfo::OnInfo(int actionID)
+{
+ Close();
+ return true;
+}
+
+void CGUIDialogPVRGuideInfo::SetProgInfo(const std::shared_ptr<CFileItem>& item)
+{
+ m_progItem = item;
+}
+
+CFileItemPtr CGUIDialogPVRGuideInfo::GetCurrentListItem(int offset)
+{
+ return m_progItem;
+}
+
+void CGUIDialogPVRGuideInfo::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ if (!m_progItem)
+ {
+ /* no epg event selected */
+ return;
+ }
+
+ auto& mgr = CServiceBroker::GetPVRManager();
+ const auto epgTag = m_progItem->GetEPGInfoTag();
+
+ if (!mgr.Recordings()->GetRecordingForEpgTag(epgTag))
+ {
+ /* not recording. hide the play recording button */
+ SET_CONTROL_HIDDEN(CONTROL_BTN_PLAY_RECORDING);
+ }
+
+ bool bHideRecord = true;
+ bool bHideAddTimer = true;
+ const std::shared_ptr<CPVRTimerInfoTag> timer = mgr.Timers()->GetTimerForEpgTag(epgTag);
+ bool bHideSetReminder = timer || (epgTag->StartAsLocalTime() <= CDateTime::GetCurrentDateTime());
+
+ if (timer)
+ {
+ if (timer->IsRecording())
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_RECORD, 19059); /* Stop recording */
+ bHideRecord = false;
+ }
+ else if (!timer->GetTimerType()->IsReadOnly())
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_RECORD, 19060); /* Delete timer */
+ bHideRecord = false;
+ }
+ }
+ else if (epgTag->IsRecordable())
+ {
+ const std::shared_ptr<CPVRClient> client = mgr.GetClient(epgTag->ClientID());
+ if (client && client->GetClientCapabilities().SupportsTimers())
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_RECORD, 264); /* Record */
+ bHideRecord = false;
+ bHideAddTimer = false;
+ }
+ }
+
+ if (!epgTag->IsPlayable())
+ SET_CONTROL_HIDDEN(CONTROL_BTN_PLAY_EPGTAG);
+
+ if (bHideRecord)
+ SET_CONTROL_HIDDEN(CONTROL_BTN_RECORD);
+
+ if (bHideAddTimer)
+ SET_CONTROL_HIDDEN(CONTROL_BTN_ADD_TIMER);
+
+ if (bHideSetReminder)
+ SET_CONTROL_HIDDEN(CONTROL_BTN_SET_REMINDER);
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.h b/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.h
new file mode 100644
index 0000000..8d9bda5
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "guilib/GUIDialog.h"
+
+#include <memory>
+
+class CGUIMessage;
+
+namespace PVR
+{
+class CGUIDialogPVRGuideInfo : public CGUIDialog
+{
+public:
+ CGUIDialogPVRGuideInfo();
+ ~CGUIDialogPVRGuideInfo() override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnInfo(int actionID) override;
+ bool HasListItems() const override { return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+
+ void SetProgInfo(const std::shared_ptr<CFileItem>& item);
+
+protected:
+ void OnInitWindow() override;
+
+private:
+ bool OnClickButtonOK(const CGUIMessage& message);
+ bool OnClickButtonRecord(const CGUIMessage& message);
+ bool OnClickButtonPlay(const CGUIMessage& message);
+ bool OnClickButtonFind(const CGUIMessage& message);
+ bool OnClickButtonAddTimer(const CGUIMessage& message);
+ bool OnClickButtonSetReminder(const CGUIMessage& message);
+
+ std::shared_ptr<CFileItem> m_progItem;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.cpp b/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.cpp
new file mode 100644
index 0000000..4cf7f2f
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.cpp
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRGuideSearch.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "utils/StringUtils.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+#define CONTROL_EDIT_SEARCH 9
+#define CONTROL_BTN_INC_DESC 10
+#define CONTROL_BTN_CASE_SENS 11
+#define CONTROL_SPIN_MIN_DURATION 12
+#define CONTROL_SPIN_MAX_DURATION 13
+#define CONTROL_EDIT_START_DATE 14
+#define CONTROL_EDIT_STOP_DATE 15
+#define CONTROL_EDIT_START_TIME 16
+#define CONTROL_EDIT_STOP_TIME 17
+#define CONTROL_SPIN_GENRE 18
+#define CONTROL_SPIN_NO_REPEATS 19
+#define CONTROL_BTN_UNK_GENRE 20
+#define CONTROL_SPIN_GROUPS 21
+#define CONTROL_BTN_FTA_ONLY 22
+#define CONTROL_SPIN_CHANNELS 23
+#define CONTROL_BTN_IGNORE_TMR 24
+#define CONTROL_BTN_CANCEL 25
+#define CONTROL_BTN_SEARCH 26
+#define CONTROL_BTN_IGNORE_REC 27
+#define CONTROL_BTN_DEFAULTS 28
+static constexpr int CONTROL_BTN_SAVE = 29;
+static constexpr int CONTROL_BTN_IGNORE_FINISHED = 30;
+static constexpr int CONTROL_BTN_IGNORE_FUTURE = 31;
+
+CGUIDialogPVRGuideSearch::CGUIDialogPVRGuideSearch()
+ : CGUIDialog(WINDOW_DIALOG_PVR_GUIDE_SEARCH, "DialogPVRGuideSearch.xml")
+{
+}
+
+void CGUIDialogPVRGuideSearch::SetFilterData(
+ const std::shared_ptr<CPVREpgSearchFilter>& searchFilter)
+{
+ m_searchFilter = searchFilter;
+}
+
+void CGUIDialogPVRGuideSearch::UpdateChannelSpin()
+{
+ int iChannelGroup = GetSpinValue(CONTROL_SPIN_GROUPS);
+
+ std::vector< std::pair<std::string, int> > labels;
+ if (m_searchFilter->IsRadio())
+ labels.emplace_back(g_localizeStrings.Get(19216), EPG_SEARCH_UNSET); // All radio channels
+ else
+ labels.emplace_back(g_localizeStrings.Get(19217), EPG_SEARCH_UNSET); // All TV channels
+
+ std::shared_ptr<CPVRChannelGroup> group;
+ if (iChannelGroup != EPG_SEARCH_UNSET)
+ group = CServiceBroker::GetPVRManager().ChannelGroups()->GetByIdFromAll(iChannelGroup);
+
+ if (!group)
+ group = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_searchFilter->IsRadio());
+
+ m_channelsMap.clear();
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ group->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+ int iIndex = 0;
+ int iSelectedChannel = EPG_SEARCH_UNSET;
+ for (const auto& groupMember : groupMembers)
+ {
+ labels.emplace_back(std::make_pair(groupMember->Channel()->ChannelName(), iIndex));
+ m_channelsMap.insert(std::make_pair(iIndex, groupMember));
+
+ if (iSelectedChannel == EPG_SEARCH_UNSET &&
+ groupMember->ChannelUID() == m_searchFilter->GetChannelUID() &&
+ groupMember->ClientID() == m_searchFilter->GetClientID())
+ iSelectedChannel = iIndex;
+
+ ++iIndex;
+ }
+
+ SET_CONTROL_LABELS(CONTROL_SPIN_CHANNELS, iSelectedChannel, &labels);
+}
+
+void CGUIDialogPVRGuideSearch::UpdateGroupsSpin()
+{
+ std::vector<std::pair<std::string, int>> labels;
+ const std::vector<std::shared_ptr<CPVRChannelGroup>> groups =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_searchFilter->IsRadio())->GetMembers();
+ int selectedGroup = EPG_SEARCH_UNSET;
+ for (const auto& group : groups)
+ {
+ labels.emplace_back(group->GroupName(), group->GroupID());
+
+ if (selectedGroup == EPG_SEARCH_UNSET &&
+ group->GroupID() == m_searchFilter->GetChannelGroupID())
+ selectedGroup = group->GroupID();
+ }
+
+ SET_CONTROL_LABELS(CONTROL_SPIN_GROUPS, selectedGroup, &labels);
+}
+
+void CGUIDialogPVRGuideSearch::UpdateGenreSpin()
+{
+ std::vector< std::pair<std::string, int> > labels;
+ labels.emplace_back(g_localizeStrings.Get(593), EPG_SEARCH_UNSET);
+ labels.emplace_back(g_localizeStrings.Get(19500), EPG_EVENT_CONTENTMASK_MOVIEDRAMA);
+ labels.emplace_back(g_localizeStrings.Get(19516), EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS);
+ labels.emplace_back(g_localizeStrings.Get(19532), EPG_EVENT_CONTENTMASK_SHOW);
+ labels.emplace_back(g_localizeStrings.Get(19548), EPG_EVENT_CONTENTMASK_SPORTS);
+ labels.emplace_back(g_localizeStrings.Get(19564), EPG_EVENT_CONTENTMASK_CHILDRENYOUTH);
+ labels.emplace_back(g_localizeStrings.Get(19580), EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE);
+ labels.emplace_back(g_localizeStrings.Get(19596), EPG_EVENT_CONTENTMASK_ARTSCULTURE);
+ labels.emplace_back(g_localizeStrings.Get(19612), EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS);
+ labels.emplace_back(g_localizeStrings.Get(19628), EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE);
+ labels.emplace_back(g_localizeStrings.Get(19644), EPG_EVENT_CONTENTMASK_LEISUREHOBBIES);
+ labels.emplace_back(g_localizeStrings.Get(19660), EPG_EVENT_CONTENTMASK_SPECIAL);
+ labels.emplace_back(g_localizeStrings.Get(19499), EPG_EVENT_CONTENTMASK_USERDEFINED);
+
+ SET_CONTROL_LABELS(CONTROL_SPIN_GENRE, m_searchFilter->GetGenreType(), &labels);
+}
+
+void CGUIDialogPVRGuideSearch::UpdateDurationSpin()
+{
+ /* minimum duration */
+ std::vector< std::pair<std::string, int> > labels;
+
+ labels.emplace_back("-", EPG_SEARCH_UNSET);
+ for (int i = 1; i < 12*60/5; ++i)
+ labels.emplace_back(StringUtils::Format(g_localizeStrings.Get(14044), i * 5), i * 5);
+
+ SET_CONTROL_LABELS(CONTROL_SPIN_MIN_DURATION, m_searchFilter->GetMinimumDuration(), &labels);
+
+ /* maximum duration */
+ labels.clear();
+
+ labels.emplace_back("-", EPG_SEARCH_UNSET);
+ for (int i = 1; i < 12*60/5; ++i)
+ labels.emplace_back(StringUtils::Format(g_localizeStrings.Get(14044), i * 5), i * 5);
+
+ SET_CONTROL_LABELS(CONTROL_SPIN_MAX_DURATION, m_searchFilter->GetMaximumDuration(), &labels);
+}
+
+bool CGUIDialogPVRGuideSearch::OnMessage(CGUIMessage& message)
+{
+ CGUIDialog::OnMessage(message);
+
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTN_SEARCH)
+ {
+ // Read data from controls, update m_searchfilter accordingly
+ UpdateSearchFilter();
+
+ m_result = Result::SEARCH;
+ Close();
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_CANCEL)
+ {
+ m_result = Result::CANCEL;
+ Close();
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_DEFAULTS)
+ {
+ if (m_searchFilter)
+ {
+ m_searchFilter->Reset();
+ Update();
+ }
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_SAVE)
+ {
+ // Read data from controls, update m_searchfilter accordingly
+ UpdateSearchFilter();
+
+ std::string title = m_searchFilter->GetTitle();
+ if (title.empty())
+ {
+ title = m_searchFilter->GetSearchTerm();
+ if (title.empty())
+ title = g_localizeStrings.Get(137); // "Search"
+ else
+ StringUtils::Trim(title, "\"");
+
+ if (!CGUIKeyboardFactory::ShowAndGetInput(
+ title, CVariant{g_localizeStrings.Get(528)}, // "Enter title"
+ false))
+ {
+ return false;
+ }
+ m_searchFilter->SetTitle(title);
+ }
+
+ m_result = Result::SAVE;
+ Close();
+ return true;
+ }
+ else if (iControl == CONTROL_SPIN_GROUPS)
+ {
+ UpdateChannelSpin();
+ return true;
+ }
+ }
+ break;
+ }
+
+ return false;
+}
+
+void CGUIDialogPVRGuideSearch::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ m_result = Result::CANCEL;
+}
+
+void CGUIDialogPVRGuideSearch::OnWindowLoaded()
+{
+ Update();
+ return CGUIDialog::OnWindowLoaded();
+}
+
+CDateTime CGUIDialogPVRGuideSearch::ReadDateTime(const std::string& strDate, const std::string& strTime) const
+{
+ CDateTime dateTime;
+ int iHours, iMinutes;
+ sscanf(strTime.c_str(), "%d:%d", &iHours, &iMinutes);
+ dateTime.SetFromDBDate(strDate);
+ dateTime.SetDateTime(dateTime.GetYear(), dateTime.GetMonth(), dateTime.GetDay(), iHours, iMinutes, 0);
+ return dateTime.GetAsUTCDateTime();
+}
+
+bool CGUIDialogPVRGuideSearch::IsRadioSelected(int controlID)
+{
+ CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), controlID);
+ OnMessage(msg);
+ return (msg.GetParam1() == 1);
+}
+
+int CGUIDialogPVRGuideSearch::GetSpinValue(int controlID)
+{
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), controlID);
+ OnMessage(msg);
+ return msg.GetParam1();
+}
+
+std::string CGUIDialogPVRGuideSearch::GetEditValue(int controlID)
+{
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), controlID);
+ OnMessage(msg);
+ return msg.GetLabel();
+}
+
+void CGUIDialogPVRGuideSearch::UpdateSearchFilter()
+{
+ if (!m_searchFilter)
+ return;
+
+ m_searchFilter->SetSearchTerm(GetEditValue(CONTROL_EDIT_SEARCH));
+
+ m_searchFilter->SetSearchInDescription(IsRadioSelected(CONTROL_BTN_INC_DESC));
+ m_searchFilter->SetCaseSensitive(IsRadioSelected(CONTROL_BTN_CASE_SENS));
+ m_searchFilter->SetFreeToAirOnly(IsRadioSelected(CONTROL_BTN_FTA_ONLY));
+ m_searchFilter->SetIncludeUnknownGenres(IsRadioSelected(CONTROL_BTN_UNK_GENRE));
+ m_searchFilter->SetIgnorePresentRecordings(IsRadioSelected(CONTROL_BTN_IGNORE_REC));
+ m_searchFilter->SetIgnorePresentTimers(IsRadioSelected(CONTROL_BTN_IGNORE_TMR));
+ m_searchFilter->SetRemoveDuplicates(IsRadioSelected(CONTROL_SPIN_NO_REPEATS));
+ m_searchFilter->SetIgnoreFinishedBroadcasts(IsRadioSelected(CONTROL_BTN_IGNORE_FINISHED));
+ m_searchFilter->SetIgnoreFutureBroadcasts(IsRadioSelected(CONTROL_BTN_IGNORE_FUTURE));
+ m_searchFilter->SetGenreType(GetSpinValue(CONTROL_SPIN_GENRE));
+ m_searchFilter->SetMinimumDuration(GetSpinValue(CONTROL_SPIN_MIN_DURATION));
+ m_searchFilter->SetMaximumDuration(GetSpinValue(CONTROL_SPIN_MAX_DURATION));
+
+ auto it = m_channelsMap.find(GetSpinValue(CONTROL_SPIN_CHANNELS));
+ m_searchFilter->SetClientID(it == m_channelsMap.end() ? -1 : (*it).second->ClientID());
+ m_searchFilter->SetChannelUID(it == m_channelsMap.end() ? -1 : (*it).second->ChannelUID());
+ m_searchFilter->SetChannelGroupID(GetSpinValue(CONTROL_SPIN_GROUPS));
+
+ const CDateTime start =
+ ReadDateTime(GetEditValue(CONTROL_EDIT_START_DATE), GetEditValue(CONTROL_EDIT_START_TIME));
+ if (start != m_startDateTime)
+ {
+ m_searchFilter->SetStartDateTime(start);
+ m_startDateTime = start;
+ }
+ const CDateTime end =
+ ReadDateTime(GetEditValue(CONTROL_EDIT_STOP_DATE), GetEditValue(CONTROL_EDIT_STOP_TIME));
+ if (end != m_endDateTime)
+ {
+ m_searchFilter->SetEndDateTime(end);
+ m_endDateTime = end;
+ }
+}
+
+void CGUIDialogPVRGuideSearch::Update()
+{
+ if (!m_searchFilter)
+ return;
+
+ SET_CONTROL_LABEL2(CONTROL_EDIT_SEARCH, m_searchFilter->GetSearchTerm());
+ {
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_SEARCH, CGUIEditControl::INPUT_TYPE_TEXT, 16017);
+ OnMessage(msg);
+ }
+
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_CASE_SENS, m_searchFilter->IsCaseSensitive());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_INC_DESC, m_searchFilter->ShouldSearchInDescription());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_FTA_ONLY, m_searchFilter->IsFreeToAirOnly());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_UNK_GENRE, m_searchFilter->ShouldIncludeUnknownGenres());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_IGNORE_REC, m_searchFilter->ShouldIgnorePresentRecordings());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_IGNORE_TMR, m_searchFilter->ShouldIgnorePresentTimers());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_SPIN_NO_REPEATS, m_searchFilter->ShouldRemoveDuplicates());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_IGNORE_FINISHED,
+ m_searchFilter->ShouldIgnoreFinishedBroadcasts());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_IGNORE_FUTURE,
+ m_searchFilter->ShouldIgnoreFutureBroadcasts());
+
+ // Set start/end datetime fields
+ m_startDateTime = m_searchFilter->GetStartDateTime();
+ m_endDateTime = m_searchFilter->GetEndDateTime();
+ if (!m_startDateTime.IsValid() || !m_endDateTime.IsValid())
+ {
+ const auto dates = CServiceBroker::GetPVRManager().EpgContainer().GetFirstAndLastEPGDate();
+ if (!m_startDateTime.IsValid())
+ m_startDateTime = dates.first;
+ if (!m_endDateTime.IsValid())
+ m_endDateTime = dates.second;
+ }
+
+ if (!m_startDateTime.IsValid())
+ m_startDateTime = CDateTime::GetUTCDateTime();
+
+ if (!m_endDateTime.IsValid())
+ m_endDateTime = m_startDateTime + CDateTimeSpan(10, 0, 0, 0); // default to start + 10 days
+
+ CDateTime startLocal;
+ startLocal.SetFromUTCDateTime(m_startDateTime);
+ CDateTime endLocal;
+ endLocal.SetFromUTCDateTime(m_endDateTime);
+
+ SET_CONTROL_LABEL2(CONTROL_EDIT_START_TIME, startLocal.GetAsLocalizedTime("", false));
+ {
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_START_TIME, CGUIEditControl::INPUT_TYPE_TIME, 14066);
+ OnMessage(msg);
+ }
+ SET_CONTROL_LABEL2(CONTROL_EDIT_STOP_TIME, endLocal.GetAsLocalizedTime("", false));
+ {
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_STOP_TIME, CGUIEditControl::INPUT_TYPE_TIME, 14066);
+ OnMessage(msg);
+ }
+ SET_CONTROL_LABEL2(CONTROL_EDIT_START_DATE, startLocal.GetAsDBDate());
+ {
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_START_DATE, CGUIEditControl::INPUT_TYPE_DATE, 14067);
+ OnMessage(msg);
+ }
+ SET_CONTROL_LABEL2(CONTROL_EDIT_STOP_DATE, endLocal.GetAsDBDate());
+ {
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_STOP_DATE, CGUIEditControl::INPUT_TYPE_DATE, 14067);
+ OnMessage(msg);
+ }
+
+ UpdateDurationSpin();
+ UpdateGroupsSpin();
+ UpdateChannelSpin();
+ UpdateGenreSpin();
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.h b/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.h
new file mode 100644
index 0000000..4d02167
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "guilib/GUIDialog.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+namespace PVR
+{
+ class CPVREpgSearchFilter;
+ class CPVRChannelGroupMember;
+
+ class CGUIDialogPVRGuideSearch : public CGUIDialog
+ {
+ public:
+ CGUIDialogPVRGuideSearch();
+ ~CGUIDialogPVRGuideSearch() override = default;
+ bool OnMessage(CGUIMessage& message) override;
+ void OnWindowLoaded() override;
+
+ void SetFilterData(const std::shared_ptr<CPVREpgSearchFilter>& searchFilter);
+
+ enum class Result
+ {
+ SEARCH,
+ SAVE,
+ CANCEL
+ };
+ Result GetResult() const { return m_result; }
+
+ protected:
+ void OnInitWindow() override;
+
+ private:
+ void UpdateSearchFilter();
+ void UpdateChannelSpin();
+ void UpdateGroupsSpin();
+ void UpdateGenreSpin();
+ void UpdateDurationSpin();
+ CDateTime ReadDateTime(const std::string& strDate, const std::string& strTime) const;
+ void Update();
+
+ bool IsRadioSelected(int controlID);
+ int GetSpinValue(int controlID);
+ std::string GetEditValue(int controlID);
+
+ Result m_result = Result::CANCEL;
+ std::shared_ptr<CPVREpgSearchFilter> m_searchFilter;
+ std::map<int, std::shared_ptr<CPVRChannelGroupMember>> m_channelsMap;
+
+ CDateTime m_startDateTime;
+ CDateTime m_endDateTime;
+ };
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.cpp b/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.cpp
new file mode 100644
index 0000000..2add783
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.cpp
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRItemsViewBase.h"
+
+#include "ContextMenuManager.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "view/ViewState.h"
+
+#include <utility>
+
+#define CONTROL_LIST 11
+
+using namespace PVR;
+
+CGUIDialogPVRItemsViewBase::CGUIDialogPVRItemsViewBase(int id, const std::string& xmlFile)
+ : CGUIDialog(id, xmlFile), m_vecItems(new CFileItemList)
+{
+}
+
+void CGUIDialogPVRItemsViewBase::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+ m_viewControl.Reset();
+ m_viewControl.SetParentWindow(GetID());
+ m_viewControl.AddView(GetControl(CONTROL_LIST));
+}
+
+void CGUIDialogPVRItemsViewBase::OnWindowUnload()
+{
+ CGUIDialog::OnWindowUnload();
+ m_viewControl.Reset();
+}
+
+void CGUIDialogPVRItemsViewBase::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+}
+
+void CGUIDialogPVRItemsViewBase::OnDeinitWindow(int nextWindowID)
+{
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+ Clear();
+}
+
+bool CGUIDialogPVRItemsViewBase::OnAction(const CAction& action)
+{
+ if (m_viewControl.HasControl(GetFocusedControlID()))
+ {
+ switch (action.GetID())
+ {
+ case ACTION_SHOW_INFO:
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ ShowInfo(m_viewControl.GetSelectedItem());
+ return true;
+
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ return ContextMenu(m_viewControl.GetSelectedItem());
+
+ default:
+ break;
+ }
+ }
+ return CGUIDialog::OnAction(action);
+}
+
+CGUIControl* CGUIDialogPVRItemsViewBase::GetFirstFocusableControl(int id)
+{
+ if (m_viewControl.HasControl(id))
+ id = m_viewControl.GetCurrentControl();
+
+ return CGUIDialog::GetFirstFocusableControl(id);
+}
+
+void CGUIDialogPVRItemsViewBase::ShowInfo(int itemIdx)
+{
+ if (itemIdx < 0 || itemIdx >= m_vecItems->Size())
+ return;
+
+ const std::shared_ptr<CFileItem> item = m_vecItems->Get(itemIdx);
+ if (!item)
+ return;
+
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*item);
+}
+
+bool CGUIDialogPVRItemsViewBase::ContextMenu(int itemIdx)
+{
+ auto InRange = [](size_t i, std::pair<size_t, size_t> range) {
+ return i >= range.first && i < range.second;
+ };
+
+ if (itemIdx < 0 || itemIdx >= m_vecItems->Size())
+ return false;
+
+ const CFileItemPtr item = m_vecItems->Get(itemIdx);
+ if (!item)
+ return false;
+
+ CContextButtons buttons;
+
+ // Add the global menu
+ const ContextMenuView globalMenu =
+ CServiceBroker::GetContextMenuManager().GetItems(*item, CContextMenuManager::MAIN);
+ auto globalMenuRange = std::make_pair(buttons.size(), buttons.size() + globalMenu.size());
+ for (const auto& menu : globalMenu)
+ buttons.emplace_back(~buttons.size(), menu->GetLabel(*item));
+
+ // Add addon menus
+ const ContextMenuView addonMenu =
+ CServiceBroker::GetContextMenuManager().GetAddonItems(*item, CContextMenuManager::MAIN);
+ auto addonMenuRange = std::make_pair(buttons.size(), buttons.size() + addonMenu.size());
+ for (const auto& menu : addonMenu)
+ buttons.emplace_back(~buttons.size(), menu->GetLabel(*item));
+
+ if (buttons.empty())
+ return true;
+
+ int idx = CGUIDialogContextMenu::Show(buttons);
+ if (idx < 0 || idx >= static_cast<int>(buttons.size()))
+ return false;
+
+ Close();
+
+ if (InRange(idx, globalMenuRange))
+ return CONTEXTMENU::LoopFrom(*globalMenu[idx - globalMenuRange.first], item);
+
+ return CONTEXTMENU::LoopFrom(*addonMenu[idx - addonMenuRange.first], item);
+}
+
+void CGUIDialogPVRItemsViewBase::Init()
+{
+ m_viewControl.SetCurrentView(DEFAULT_VIEW_LIST);
+ Clear();
+}
+
+void CGUIDialogPVRItemsViewBase::Clear()
+{
+ m_viewControl.Clear();
+ m_vecItems->Clear();
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.h b/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.h
new file mode 100644
index 0000000..66530e4
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "guilib/GUIDialog.h"
+#include "view/GUIViewControl.h"
+
+#include <memory>
+#include <string>
+
+class CFileItemList;
+
+namespace PVR
+{
+class CGUIDialogPVRItemsViewBase : public CGUIDialog
+{
+public:
+ CGUIDialogPVRItemsViewBase() = delete;
+ CGUIDialogPVRItemsViewBase(int id, const std::string& xmlFile);
+ ~CGUIDialogPVRItemsViewBase() override = default;
+
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+ bool OnAction(const CAction& action) override;
+
+protected:
+ void Init();
+
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ CGUIControl* GetFirstFocusableControl(int id) override;
+
+ std::unique_ptr<CFileItemList> m_vecItems;
+ CGUIViewControl m_viewControl;
+
+private:
+ void Clear();
+ void ShowInfo(int itemIdx);
+ bool ContextMenu(int iItemIdx);
+};
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.cpp b/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.cpp
new file mode 100644
index 0000000..ead92d2
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.cpp
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRRadioRDSInfo.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUISpinControl.h"
+#include "guilib/GUITextBox.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRRadioRDSInfoTag.h"
+
+using namespace PVR;
+
+#define CONTROL_BTN_OK 10
+#define SPIN_CONTROL_INFO 21
+#define TEXT_INFO 22
+#define CONTROL_NEXT_PAGE 60
+#define CONTROL_INFO_LIST 70
+
+#define INFO_NEWS 1
+#define INFO_NEWS_LOCAL 2
+#define INFO_SPORT 3
+#define INFO_WEATHER 4
+#define INFO_LOTTERY 5
+#define INFO_STOCK 6
+#define INFO_OTHER 7
+#define INFO_CINEMA 8
+#define INFO_HOROSCOPE 9
+
+CGUIDialogPVRRadioRDSInfo::CGUIDialogPVRRadioRDSInfo()
+ : CGUIDialog(WINDOW_DIALOG_PVR_RADIO_RDS_INFO, "DialogPVRRadioRDSInfo.xml")
+ , m_InfoNews(29916, INFO_NEWS)
+ , m_InfoNewsLocal(29917, INFO_NEWS_LOCAL)
+ , m_InfoSport(29918, INFO_SPORT)
+ , m_InfoWeather(400, INFO_WEATHER)
+ , m_InfoLottery(29919, INFO_LOTTERY)
+ , m_InfoStock(29920, INFO_STOCK)
+ , m_InfoOther(29921, INFO_OTHER)
+ , m_InfoCinema(19602, INFO_CINEMA)
+ , m_InfoHoroscope(29922, INFO_HOROSCOPE)
+{
+}
+
+bool CGUIDialogPVRRadioRDSInfo::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_CLICKED)
+ {
+ int iControl = message.GetSenderId();
+
+ if (iControl == CONTROL_BTN_OK)
+ {
+ Close();
+ return true;
+ }
+ else if (iControl == SPIN_CONTROL_INFO)
+ {
+ const std::shared_ptr<CPVRChannel> channel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (!channel)
+ return false;
+
+ const std::shared_ptr<CPVRRadioRDSInfoTag> currentRDS = channel->GetRadioRDSInfoTag();
+ if (!currentRDS)
+ return false;
+
+ const CGUISpinControl* spin = static_cast<CGUISpinControl*>(GetControl(SPIN_CONTROL_INFO));
+ if (!spin)
+ return false;
+
+ CGUITextBox* textbox = static_cast<CGUITextBox*>(GetControl(TEXT_INFO));
+ if (!textbox)
+ return false;
+
+ switch (spin->GetValue())
+ {
+ case INFO_NEWS:
+ textbox->SetInfo(currentRDS->GetInfoNews());
+ break;
+ case INFO_NEWS_LOCAL:
+ textbox->SetInfo(currentRDS->GetInfoNewsLocal());
+ break;
+ case INFO_SPORT:
+ textbox->SetInfo(currentRDS->GetInfoSport());
+ break;
+ case INFO_WEATHER:
+ textbox->SetInfo(currentRDS->GetInfoWeather());
+ break;
+ case INFO_LOTTERY:
+ textbox->SetInfo(currentRDS->GetInfoLottery());
+ break;
+ case INFO_STOCK:
+ textbox->SetInfo(currentRDS->GetInfoStock());
+ break;
+ case INFO_OTHER:
+ textbox->SetInfo(currentRDS->GetInfoOther());
+ break;
+ case INFO_CINEMA:
+ textbox->SetInfo(currentRDS->GetInfoCinema());
+ break;
+ case INFO_HOROSCOPE:
+ textbox->SetInfo(currentRDS->GetInfoHoroscope());
+ break;
+ }
+
+ SET_CONTROL_VISIBLE(CONTROL_INFO_LIST);
+ }
+ }
+ else if (message.GetMessage() == GUI_MSG_NOTIFY_ALL)
+ {
+ if (message.GetParam1() == GUI_MSG_UPDATE_RADIOTEXT && IsActive())
+ {
+ UpdateInfoControls();
+ }
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogPVRRadioRDSInfo::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ InitInfoControls();
+}
+
+void CGUIDialogPVRRadioRDSInfo::InitInfoControls()
+{
+ SET_CONTROL_HIDDEN(CONTROL_INFO_LIST);
+
+ CGUISpinControl* spin = static_cast<CGUISpinControl*>(GetControl(SPIN_CONTROL_INFO));
+ if (spin)
+ spin->Clear();
+
+ CGUITextBox* textbox = static_cast<CGUITextBox*>(GetControl(TEXT_INFO));
+
+ m_InfoNews.Init(spin, textbox);
+ m_InfoNewsLocal.Init(spin, textbox);
+ m_InfoSport.Init(spin, textbox);
+ m_InfoWeather.Init(spin, textbox);
+ m_InfoLottery.Init(spin, textbox);
+ m_InfoStock.Init(spin, textbox);
+ m_InfoOther.Init(spin, textbox);
+ m_InfoCinema.Init(spin, textbox);
+ m_InfoHoroscope.Init(spin, textbox);
+
+ if (spin && textbox)
+ UpdateInfoControls();
+}
+
+void CGUIDialogPVRRadioRDSInfo::UpdateInfoControls()
+{
+ const std::shared_ptr<CPVRChannel> channel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (!channel)
+ return;
+
+ const std::shared_ptr<CPVRRadioRDSInfoTag> currentRDS = channel->GetRadioRDSInfoTag();
+ if (!currentRDS)
+ return;
+
+ bool bInfoPresent = m_InfoNews.Update(currentRDS->GetInfoNews());
+ bInfoPresent |= m_InfoNewsLocal.Update(currentRDS->GetInfoNewsLocal());
+ bInfoPresent |= m_InfoSport.Update(currentRDS->GetInfoSport());
+ bInfoPresent |= m_InfoWeather.Update(currentRDS->GetInfoWeather());
+ bInfoPresent |= m_InfoLottery.Update(currentRDS->GetInfoLottery());
+ bInfoPresent |= m_InfoStock.Update(currentRDS->GetInfoStock());
+ bInfoPresent |= m_InfoOther.Update(currentRDS->GetInfoOther());
+ bInfoPresent |= m_InfoCinema.Update(currentRDS->GetInfoCinema());
+ bInfoPresent |= m_InfoHoroscope.Update(currentRDS->GetInfoHoroscope());
+
+ if (bInfoPresent)
+ SET_CONTROL_VISIBLE(CONTROL_INFO_LIST);
+}
+
+CGUIDialogPVRRadioRDSInfo::InfoControl::InfoControl(uint32_t iSpinLabelId, uint32_t iSpinControlId)
+: m_iSpinLabelId(iSpinLabelId),
+ m_iSpinControlId(iSpinControlId)
+{
+}
+
+void CGUIDialogPVRRadioRDSInfo::InfoControl::Init(CGUISpinControl* spin, CGUITextBox* textbox)
+{
+ m_spinControl = spin;
+ m_textbox = textbox;
+ m_bSpinLabelPresent = false;
+ m_textboxValue.clear();
+}
+
+bool CGUIDialogPVRRadioRDSInfo::InfoControl::Update(const std::string& textboxValue)
+{
+ if (m_spinControl && m_textbox && !textboxValue.empty())
+ {
+ if (!m_bSpinLabelPresent)
+ {
+ m_spinControl->AddLabel(g_localizeStrings.Get(m_iSpinLabelId), m_iSpinControlId);
+ m_bSpinLabelPresent = true;
+ }
+
+ if (m_textboxValue != textboxValue)
+ {
+ m_spinControl->SetValue(m_iSpinControlId);
+ m_textboxValue = textboxValue;
+ m_textbox->SetInfo(textboxValue);
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.h b/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.h
new file mode 100644
index 0000000..4ae1b0b
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "guilib/GUIDialog.h"
+
+#include <string>
+
+class CGUISpinControl;
+class CGUITextBox;
+
+namespace PVR
+{
+ class CGUIDialogPVRRadioRDSInfo : public CGUIDialog
+ {
+ public:
+ CGUIDialogPVRRadioRDSInfo();
+ ~CGUIDialogPVRRadioRDSInfo() override = default;
+ bool OnMessage(CGUIMessage& message) override;
+
+ protected:
+ void OnInitWindow() override;
+
+ private:
+ class InfoControl
+ {
+ public:
+ InfoControl(uint32_t iSpinLabelId, uint32_t iSpinControlId);
+ void Init(CGUISpinControl* spin, CGUITextBox* textbox);
+ bool Update(const std::string& textboxValue);
+
+ private:
+ CGUISpinControl* m_spinControl = nullptr;
+ uint32_t m_iSpinLabelId = 0;
+ uint32_t m_iSpinControlId = 0;
+ CGUITextBox* m_textbox = nullptr;
+ bool m_bSpinLabelPresent = false;
+ std::string m_textboxValue;
+ };
+
+ void InitInfoControls();
+ void UpdateInfoControls();
+
+ InfoControl m_InfoNews;
+ InfoControl m_InfoNewsLocal;
+ InfoControl m_InfoSport;
+ InfoControl m_InfoWeather;
+ InfoControl m_InfoLottery;
+ InfoControl m_InfoStock;
+ InfoControl m_InfoOther;
+ InfoControl m_InfoCinema;
+ InfoControl m_InfoHoroscope;
+ };
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.cpp b/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.cpp
new file mode 100644
index 0000000..9396083
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.cpp
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRRecordingInfo.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIMessage.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+
+using namespace PVR;
+
+#define CONTROL_BTN_FIND 4
+#define CONTROL_BTN_OK 7
+#define CONTROL_BTN_PLAY_RECORDING 8
+
+CGUIDialogPVRRecordingInfo::CGUIDialogPVRRecordingInfo()
+ : CGUIDialog(WINDOW_DIALOG_PVR_RECORDING_INFO, "DialogPVRInfo.xml"), m_recordItem(new CFileItem)
+{
+}
+
+bool CGUIDialogPVRRecordingInfo::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ return OnClickButtonOK(message) || OnClickButtonPlay(message) || OnClickButtonFind(message);
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogPVRRecordingInfo::OnClickButtonOK(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_OK)
+ {
+ Close();
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRRecordingInfo::OnClickButtonPlay(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_PLAY_RECORDING)
+ {
+ Close();
+
+ if (m_recordItem)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(
+ *m_recordItem, true /* check resume */);
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRRecordingInfo::OnClickButtonFind(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_FIND)
+ {
+ Close();
+
+ if (m_recordItem)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().FindSimilar(*m_recordItem);
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRRecordingInfo::OnInfo(int actionID)
+{
+ Close();
+ return true;
+}
+
+void CGUIDialogPVRRecordingInfo::SetRecording(const CFileItem& item)
+{
+ m_recordItem = std::make_shared<CFileItem>(item);
+}
+
+CFileItemPtr CGUIDialogPVRRecordingInfo::GetCurrentListItem(int offset)
+{
+ return m_recordItem;
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.h b/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.h
new file mode 100644
index 0000000..b22e0d7
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "guilib/GUIDialog.h"
+
+class CFileItem;
+class CGUIMessage;
+
+namespace PVR
+{
+class CGUIDialogPVRRecordingInfo : public CGUIDialog
+{
+public:
+ CGUIDialogPVRRecordingInfo();
+ ~CGUIDialogPVRRecordingInfo() override = default;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnInfo(int actionID) override;
+ bool HasListItems() const override { return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+
+ void SetRecording(const CFileItem& item);
+
+private:
+ bool OnClickButtonFind(const CGUIMessage& message);
+ bool OnClickButtonOK(const CGUIMessage& message);
+ bool OnClickButtonPlay(const CGUIMessage& message);
+
+ CFileItemPtr m_recordItem;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.cpp b/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.cpp
new file mode 100644
index 0000000..ed8cd7b
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.cpp
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRRecordingSettings.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "settings/dialogs/GUIDialogSettingsBase.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+#define SETTING_RECORDING_NAME "recording.name"
+#define SETTING_RECORDING_PLAYCOUNT "recording.playcount"
+#define SETTING_RECORDING_LIFETIME "recording.lifetime"
+
+CGUIDialogPVRRecordingSettings::CGUIDialogPVRRecordingSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_PVR_RECORDING_SETTING, "DialogSettings.xml")
+{
+ m_loadType = LOAD_EVERY_TIME;
+}
+
+void CGUIDialogPVRRecordingSettings::SetRecording(const std::shared_ptr<CPVRRecording>& recording)
+{
+ if (!recording)
+ {
+ CLog::LogF(LOGERROR, "No recording given");
+ return;
+ }
+
+ m_recording = recording;
+
+ // Copy data we need from tag. Do not modify the tag itself until Save()!
+ m_strTitle = m_recording->m_strTitle;
+ m_iPlayCount = m_recording->GetLocalPlayCount();
+ m_iLifetime = m_recording->LifeTime();
+}
+
+void CGUIDialogPVRRecordingSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+ SetHeading(19068); // Recording settings
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186); // OK
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222); // Cancel
+}
+
+void CGUIDialogPVRRecordingSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("pvrrecordingsettings", -1);
+ if (category == nullptr)
+ {
+ CLog::LogF(LOGERROR, "Unable to add settings category");
+ return;
+ }
+
+ const std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == nullptr)
+ {
+ CLog::LogF(LOGERROR, "Unable to add settings group");
+ return;
+ }
+
+ std::shared_ptr<CSetting> setting = nullptr;
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_recording->ClientID());
+
+ // Name
+ setting = AddEdit(group, SETTING_RECORDING_NAME, 19075, SettingLevel::Basic, m_strTitle);
+ setting->SetEnabled(client && client->GetClientCapabilities().SupportsRecordingsRename());
+
+ // Play count
+ if (client && client->GetClientCapabilities().SupportsRecordingsPlayCount())
+ setting = AddEdit(group, SETTING_RECORDING_PLAYCOUNT, 567, SettingLevel::Basic,
+ m_recording->GetLocalPlayCount());
+
+ // Lifetime
+ if (client && client->GetClientCapabilities().SupportsRecordingsLifetimeChange())
+ setting = AddList(group, SETTING_RECORDING_LIFETIME, 19083, SettingLevel::Basic, m_iLifetime,
+ LifetimesFiller, 19083);
+}
+
+bool CGUIDialogPVRRecordingSettings::CanEditRecording(const CFileItem& item)
+{
+ if (!item.HasPVRRecordingInfoTag())
+ return false;
+
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(item.GetPVRRecordingInfoTag()->ClientID());
+
+ if (!client)
+ return false;
+
+ const CPVRClientCapabilities& capabilities = client->GetClientCapabilities();
+
+ return capabilities.SupportsRecordingsRename() || capabilities.SupportsRecordingsPlayCount() ||
+ capabilities.SupportsRecordingsLifetimeChange();
+}
+
+bool CGUIDialogPVRRecordingSettings::OnSettingChanging(
+ const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ {
+ CLog::LogF(LOGERROR, "No setting");
+ return false;
+ }
+
+ const std::string& settingId = setting->GetId();
+
+ if (settingId == SETTING_RECORDING_LIFETIME)
+ {
+ int iNewLifetime = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ if (m_recording->WillBeExpiredWithNewLifetime(iNewLifetime))
+ {
+ if (HELPERS::ShowYesNoDialogText(
+ CVariant{19068}, // "Recording settings"
+ StringUtils::Format(g_localizeStrings.Get(19147),
+ iNewLifetime)) // "Setting the lifetime..."
+ != HELPERS::DialogResponse::CHOICE_YES)
+ return false;
+ }
+ }
+
+ return CGUIDialogSettingsManualBase::OnSettingChanging(setting);
+}
+
+void CGUIDialogPVRRecordingSettings::OnSettingChanged(
+ const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ {
+ CLog::LogF(LOGERROR, "No setting");
+ return;
+ }
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string& settingId = setting->GetId();
+
+ if (settingId == SETTING_RECORDING_NAME)
+ {
+ m_strTitle = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_RECORDING_PLAYCOUNT)
+ {
+ m_iPlayCount = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_RECORDING_LIFETIME)
+ {
+ m_iLifetime = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+}
+
+bool CGUIDialogPVRRecordingSettings::Save()
+{
+ // Name
+ m_recording->m_strTitle = m_strTitle;
+
+ // Play count
+ m_recording->SetLocalPlayCount(m_iPlayCount);
+
+ // Lifetime
+ m_recording->SetLifeTime(m_iLifetime);
+
+ return true;
+}
+
+void CGUIDialogPVRRecordingSettings::LifetimesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRRecordingSettings* pThis = static_cast<CGUIDialogPVRRecordingSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(pThis->m_recording->ClientID());
+ if (client)
+ {
+ std::vector<std::pair<std::string, int>> values;
+ client->GetClientCapabilities().GetRecordingsLifetimeValues(values);
+ std::transform(
+ values.cbegin(), values.cend(), std::back_inserter(list),
+ [](const auto& value) { return IntegerSettingOption(value.first, value.second); });
+ }
+
+ current = pThis->m_iLifetime;
+
+ auto it = list.begin();
+ while (it != list.end())
+ {
+ if (it->value == current)
+ break; // value already in list
+
+ ++it;
+ }
+
+ if (it == list.end())
+ {
+ // PVR backend supplied value is not in the list of predefined values. Insert it.
+ list.insert(it, IntegerSettingOption(
+ StringUtils::Format(g_localizeStrings.Get(17999), current) /* {} days */,
+ current));
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.h b/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.h
new file mode 100644
index 0000000..f0de299
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CSetting;
+
+struct IntegerSettingOption;
+
+namespace PVR
+{
+class CPVRRecording;
+
+class CGUIDialogPVRRecordingSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogPVRRecordingSettings();
+
+ void SetRecording(const std::shared_ptr<CPVRRecording>& recording);
+ static bool CanEditRecording(const CFileItem& item);
+
+protected:
+ // implementation of ISettingCallback
+ bool OnSettingChanging(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+private:
+ static void LifetimesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ std::shared_ptr<CPVRRecording> m_recording;
+ std::string m_strTitle;
+ int m_iPlayCount = 0;
+ int m_iLifetime = 0;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.cpp b/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.cpp
new file mode 100644
index 0000000..f786627
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.cpp
@@ -0,0 +1,1513 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogPVRTimerSettings.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimerType.h"
+#include "settings/SettingUtils.h"
+#include "settings/dialogs/GUIDialogSettingsBase.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingsManager.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+#define SETTING_TMR_TYPE "timer.type"
+#define SETTING_TMR_ACTIVE "timer.active"
+#define SETTING_TMR_NAME "timer.name"
+#define SETTING_TMR_EPGSEARCH "timer.epgsearch"
+#define SETTING_TMR_FULLTEXT "timer.fulltext"
+#define SETTING_TMR_CHANNEL "timer.channel"
+#define SETTING_TMR_START_ANYTIME "timer.startanytime"
+#define SETTING_TMR_END_ANYTIME "timer.endanytime"
+#define SETTING_TMR_START_DAY "timer.startday"
+#define SETTING_TMR_END_DAY "timer.endday"
+#define SETTING_TMR_BEGIN "timer.begin"
+#define SETTING_TMR_END "timer.end"
+#define SETTING_TMR_WEEKDAYS "timer.weekdays"
+#define SETTING_TMR_FIRST_DAY "timer.firstday"
+#define SETTING_TMR_NEW_EPISODES "timer.newepisodes"
+#define SETTING_TMR_BEGIN_PRE "timer.startmargin"
+#define SETTING_TMR_END_POST "timer.endmargin"
+#define SETTING_TMR_PRIORITY "timer.priority"
+#define SETTING_TMR_LIFETIME "timer.lifetime"
+#define SETTING_TMR_MAX_REC "timer.maxrecordings"
+#define SETTING_TMR_DIR "timer.directory"
+#define SETTING_TMR_REC_GROUP "timer.recgroup"
+
+#define TYPE_DEP_VISIBI_COND_ID_POSTFIX "visibi.typedep"
+#define TYPE_DEP_ENABLE_COND_ID_POSTFIX "enable.typedep"
+#define CHANNEL_DEP_VISIBI_COND_ID_POSTFIX "visibi.channeldep"
+#define START_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX "visibi.startanytimedep"
+#define END_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX "visibi.endanytimedep"
+
+CGUIDialogPVRTimerSettings::CGUIDialogPVRTimerSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_PVR_TIMER_SETTING, "DialogSettings.xml"),
+ m_iWeekdays(PVR_WEEKDAY_NONE)
+{
+ m_loadType = LOAD_EVERY_TIME;
+}
+
+CGUIDialogPVRTimerSettings::~CGUIDialogPVRTimerSettings() = default;
+
+bool CGUIDialogPVRTimerSettings::CanBeActivated() const
+{
+ if (!m_timerInfoTag)
+ {
+ CLog::LogF(LOGERROR, "No timer info tag");
+ return false;
+ }
+ return true;
+}
+
+void CGUIDialogPVRTimerSettings::SetTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer)
+{
+ if (!timer)
+ {
+ CLog::LogF(LOGERROR, "No timer given");
+ return;
+ }
+
+ m_timerInfoTag = timer;
+
+ // Copy data we need from tag. Do not modify the tag itself until Save()!
+ m_timerType = m_timerInfoTag->GetTimerType();
+ m_bIsRadio = m_timerInfoTag->m_bIsRadio;
+ m_bIsNewTimer = m_timerInfoTag->m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX;
+ m_bTimerActive = m_bIsNewTimer || !m_timerType->SupportsEnableDisable() ||
+ !(m_timerInfoTag->m_state == PVR_TIMER_STATE_DISABLED);
+ m_bStartAnyTime =
+ m_bIsNewTimer || !m_timerType->SupportsStartAnyTime() || m_timerInfoTag->m_bStartAnyTime;
+ m_bEndAnyTime =
+ m_bIsNewTimer || !m_timerType->SupportsEndAnyTime() || m_timerInfoTag->m_bEndAnyTime;
+ m_strTitle = m_timerInfoTag->m_strTitle;
+
+ m_startLocalTime = m_timerInfoTag->StartAsLocalTime();
+ m_endLocalTime = m_timerInfoTag->EndAsLocalTime();
+
+ m_timerStartTimeStr = m_startLocalTime.GetAsLocalizedTime("", false);
+ m_timerEndTimeStr = m_endLocalTime.GetAsLocalizedTime("", false);
+ m_firstDayLocalTime = m_timerInfoTag->FirstDayAsLocalTime();
+
+ m_strEpgSearchString = m_timerInfoTag->m_strEpgSearchString;
+ if ((m_bIsNewTimer || !m_timerType->SupportsEpgTitleMatch()) && m_strEpgSearchString.empty())
+ m_strEpgSearchString = m_strTitle;
+
+ m_bFullTextEpgSearch = m_timerInfoTag->m_bFullTextEpgSearch;
+
+ m_iWeekdays = m_timerInfoTag->m_iWeekdays;
+ if ((m_bIsNewTimer || !m_timerType->SupportsWeekdays()) && m_iWeekdays == PVR_WEEKDAY_NONE)
+ m_iWeekdays = PVR_WEEKDAY_ALLDAYS;
+
+ m_iPreventDupEpisodes = m_timerInfoTag->m_iPreventDupEpisodes;
+ m_iMarginStart = m_timerInfoTag->m_iMarginStart;
+ m_iMarginEnd = m_timerInfoTag->m_iMarginEnd;
+ m_iPriority = m_timerInfoTag->m_iPriority;
+ m_iLifetime = m_timerInfoTag->m_iLifetime;
+ m_iMaxRecordings = m_timerInfoTag->m_iMaxRecordings;
+
+ if (m_bIsNewTimer && m_timerInfoTag->m_strDirectory.empty() &&
+ m_timerType->SupportsRecordingFolders())
+ m_strDirectory = m_strTitle;
+ else
+ m_strDirectory = m_timerInfoTag->m_strDirectory;
+
+ m_iRecordingGroup = m_timerInfoTag->m_iRecordingGroup;
+
+ InitializeChannelsList();
+ InitializeTypesList();
+
+ // Channel
+ m_channel = ChannelDescriptor();
+
+ if (m_timerInfoTag->m_iClientChannelUid == PVR_CHANNEL_INVALID_UID)
+ {
+ if (m_timerType->SupportsAnyChannel())
+ {
+ // Select first matching "Any channel" entry.
+ const auto it = std::find_if(m_channelEntries.cbegin(), m_channelEntries.cend(),
+ [this](const auto& channel) {
+ return channel.second.channelUid == PVR_CHANNEL_INVALID_UID &&
+ channel.second.clientId == m_timerInfoTag->m_iClientId;
+ });
+
+ if (it != m_channelEntries.cend())
+ {
+ m_channel = (*it).second;
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to map PVR_CHANNEL_INVALID_UID to channel entry!");
+ }
+ }
+ else if (m_bIsNewTimer)
+ {
+ // Select first matching regular (not "Any channel") entry.
+ const auto it = std::find_if(m_channelEntries.cbegin(), m_channelEntries.cend(),
+ [this](const auto& channel) {
+ return channel.second.channelUid != PVR_CHANNEL_INVALID_UID &&
+ channel.second.clientId == m_timerInfoTag->m_iClientId;
+ });
+
+ if (it != m_channelEntries.cend())
+ {
+ m_channel = (*it).second;
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to map PVR_CHANNEL_INVALID_UID to channel entry!");
+ }
+ }
+ }
+ else
+ {
+ // Find matching channel entry
+ const auto it = std::find_if(
+ m_channelEntries.cbegin(), m_channelEntries.cend(), [this](const auto& channel) {
+ return channel.second.channelUid == m_timerInfoTag->m_iClientChannelUid &&
+ channel.second.clientId == m_timerInfoTag->m_iClientId;
+ });
+
+ if (it != m_channelEntries.cend())
+ {
+ m_channel = (*it).second;
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to map channel uid to channel entry!");
+ }
+ }
+}
+
+void CGUIDialogPVRTimerSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+ SetHeading(19065);
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222);
+ SetButtonLabels();
+}
+
+void CGUIDialogPVRTimerSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("pvrtimersettings", -1);
+ if (category == NULL)
+ {
+ CLog::LogF(LOGERROR, "Unable to add settings category");
+ return;
+ }
+
+ const std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == NULL)
+ {
+ CLog::LogF(LOGERROR, "Unable to add settings group");
+ return;
+ }
+
+ std::shared_ptr<CSetting> setting = NULL;
+
+ // Timer type
+ bool useDetails = false;
+ bool foundClientSupportingTimers = false;
+
+ const CPVRClientMap clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients();
+ for (const auto& client : clients)
+ {
+ if (client.second->GetClientCapabilities().SupportsTimers())
+ {
+ if (foundClientSupportingTimers)
+ {
+ // found second client supporting timers, use detailed timer type list layout
+ useDetails = true;
+ break;
+ }
+ foundClientSupportingTimers = true;
+ }
+ }
+
+ setting = AddList(group, SETTING_TMR_TYPE, 803, SettingLevel::Basic, 0, TypesFiller, 803, true,
+ -1, useDetails);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_TYPE);
+
+ // Timer enabled/disabled
+ setting = AddToggle(group, SETTING_TMR_ACTIVE, 305, SettingLevel::Basic, m_bTimerActive);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_ACTIVE);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_ACTIVE);
+
+ // Name
+ setting =
+ AddEdit(group, SETTING_TMR_NAME, 19075, SettingLevel::Basic, m_strTitle, true, false, 19097);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_NAME);
+
+ // epg search string (only for epg-based timer rules)
+ setting = AddEdit(group, SETTING_TMR_EPGSEARCH, 804, SettingLevel::Basic, m_strEpgSearchString,
+ true, false, 805);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_EPGSEARCH);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_EPGSEARCH);
+
+ // epg fulltext search (only for epg-based timer rules)
+ setting = AddToggle(group, SETTING_TMR_FULLTEXT, 806, SettingLevel::Basic, m_bFullTextEpgSearch);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_FULLTEXT);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_FULLTEXT);
+
+ // Channel
+ setting =
+ AddList(group, SETTING_TMR_CHANNEL, 19078, SettingLevel::Basic, 0, ChannelsFiller, 19078);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_CHANNEL);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_CHANNEL);
+
+ // Days of week (only for timer rules)
+ std::vector<int> weekdaysPreselect;
+ if (m_iWeekdays & PVR_WEEKDAY_MONDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_MONDAY);
+ if (m_iWeekdays & PVR_WEEKDAY_TUESDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_TUESDAY);
+ if (m_iWeekdays & PVR_WEEKDAY_WEDNESDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_WEDNESDAY);
+ if (m_iWeekdays & PVR_WEEKDAY_THURSDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_THURSDAY);
+ if (m_iWeekdays & PVR_WEEKDAY_FRIDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_FRIDAY);
+ if (m_iWeekdays & PVR_WEEKDAY_SATURDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_SATURDAY);
+ if (m_iWeekdays & PVR_WEEKDAY_SUNDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_SUNDAY);
+
+ setting = AddList(group, SETTING_TMR_WEEKDAYS, 19079, SettingLevel::Basic, weekdaysPreselect,
+ WeekdaysFiller, 19079, 1, -1, true, -1, WeekdaysValueFormatter);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_WEEKDAYS);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_WEEKDAYS);
+
+ // "Start any time" (only for timer rules)
+ setting = AddToggle(group, SETTING_TMR_START_ANYTIME, 810, SettingLevel::Basic, m_bStartAnyTime);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_START_ANYTIME);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_START_ANYTIME);
+
+ // Start day (day + month + year only, no hours, minutes)
+ setting = AddSpinner(group, SETTING_TMR_START_DAY, 19128, SettingLevel::Basic,
+ GetDateAsIndex(m_startLocalTime), DaysFiller);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_START_DAY);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_START_DAY);
+ AddStartAnytimeDependentVisibilityCondition(setting, SETTING_TMR_START_DAY);
+
+ // Start time (hours + minutes only, no day, month, year)
+ setting = AddButton(group, SETTING_TMR_BEGIN, 19126, SettingLevel::Basic);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_BEGIN);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_BEGIN);
+ AddStartAnytimeDependentVisibilityCondition(setting, SETTING_TMR_BEGIN);
+
+ // "End any time" (only for timer rules)
+ setting = AddToggle(group, SETTING_TMR_END_ANYTIME, 817, SettingLevel::Basic, m_bEndAnyTime);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_END_ANYTIME);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_END_ANYTIME);
+
+ // End day (day + month + year only, no hours, minutes)
+ setting = AddSpinner(group, SETTING_TMR_END_DAY, 19129, SettingLevel::Basic,
+ GetDateAsIndex(m_endLocalTime), DaysFiller);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_END_DAY);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_END_DAY);
+ AddEndAnytimeDependentVisibilityCondition(setting, SETTING_TMR_END_DAY);
+
+ // End time (hours + minutes only, no day, month, year)
+ setting = AddButton(group, SETTING_TMR_END, 19127, SettingLevel::Basic);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_END);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_END);
+ AddEndAnytimeDependentVisibilityCondition(setting, SETTING_TMR_END);
+
+ // First day (only for timer rules)
+ setting = AddSpinner(group, SETTING_TMR_FIRST_DAY, 19084, SettingLevel::Basic,
+ GetDateAsIndex(m_firstDayLocalTime), DaysFiller);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_FIRST_DAY);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_FIRST_DAY);
+
+ // "Prevent duplicate episodes" (only for timer rules)
+ setting = AddList(group, SETTING_TMR_NEW_EPISODES, 812, SettingLevel::Basic,
+ m_iPreventDupEpisodes, DupEpisodesFiller, 812);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_NEW_EPISODES);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_NEW_EPISODES);
+
+ // Pre and post record time
+ setting = AddList(group, SETTING_TMR_BEGIN_PRE, 813, SettingLevel::Basic, m_iMarginStart,
+ MarginTimeFiller, 813);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_BEGIN_PRE);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_BEGIN_PRE);
+
+ setting = AddList(group, SETTING_TMR_END_POST, 814, SettingLevel::Basic, m_iMarginEnd,
+ MarginTimeFiller, 814);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_END_POST);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_END_POST);
+
+ // Priority
+ setting = AddList(group, SETTING_TMR_PRIORITY, 19082, SettingLevel::Basic, m_iPriority,
+ PrioritiesFiller, 19082);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_PRIORITY);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_PRIORITY);
+
+ // Lifetime
+ setting = AddList(group, SETTING_TMR_LIFETIME, 19083, SettingLevel::Basic, m_iLifetime,
+ LifetimesFiller, 19083);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_LIFETIME);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_LIFETIME);
+
+ // MaxRecordings
+ setting = AddList(group, SETTING_TMR_MAX_REC, 818, SettingLevel::Basic, m_iMaxRecordings,
+ MaxRecordingsFiller, 818);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_MAX_REC);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_MAX_REC);
+
+ // Recording folder
+ setting = AddEdit(group, SETTING_TMR_DIR, 19076, SettingLevel::Basic, m_strDirectory, true, false,
+ 19104);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_DIR);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_DIR);
+
+ // Recording group
+ setting = AddList(group, SETTING_TMR_REC_GROUP, 811, SettingLevel::Basic, m_iRecordingGroup,
+ RecordingGroupFiller, 811);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_REC_GROUP);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_REC_GROUP);
+}
+
+int CGUIDialogPVRTimerSettings::GetWeekdaysFromSetting(const SettingConstPtr& setting)
+{
+ std::shared_ptr<const CSettingList> settingList =
+ std::static_pointer_cast<const CSettingList>(setting);
+ if (settingList->GetElementType() != SettingType::Integer)
+ {
+ CLog::LogF(LOGERROR, "Wrong weekdays element type");
+ return 0;
+ }
+ int weekdays = 0;
+ std::vector<CVariant> list = CSettingUtils::GetList(settingList);
+ for (const auto& value : list)
+ {
+ if (!value.isInteger())
+ {
+ CLog::LogF(LOGERROR, "Wrong weekdays value type");
+ return 0;
+ }
+ weekdays += static_cast<int>(value.asInteger());
+ }
+
+ return weekdays;
+}
+
+void CGUIDialogPVRTimerSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ {
+ CLog::LogF(LOGERROR, "No setting");
+ return;
+ }
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string& settingId = setting->GetId();
+
+ if (settingId == SETTING_TMR_TYPE)
+ {
+ int idx = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ const auto it = m_typeEntries.find(idx);
+ if (it != m_typeEntries.end())
+ {
+ m_timerType = it->second;
+
+ // reset certain settings to the defaults of the new timer type
+
+ if (m_timerType->SupportsPriority())
+ m_iPriority = m_timerType->GetPriorityDefault();
+
+ if (m_timerType->SupportsLifetime())
+ m_iLifetime = m_timerType->GetLifetimeDefault();
+
+ if (m_timerType->SupportsMaxRecordings())
+ m_iMaxRecordings = m_timerType->GetMaxRecordingsDefault();
+
+ if (m_timerType->SupportsRecordingGroup())
+ m_iRecordingGroup = m_timerType->GetRecordingGroupDefault();
+
+ if (m_timerType->SupportsRecordOnlyNewEpisodes())
+ m_iPreventDupEpisodes = m_timerType->GetPreventDuplicateEpisodesDefault();
+
+ if (m_timerType->IsTimerRule() && (m_iWeekdays == PVR_WEEKDAY_ALLDAYS))
+ SetButtonLabels(); // update "Any day" vs. "Every day"
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to get 'type' value");
+ }
+ }
+ else if (settingId == SETTING_TMR_ACTIVE)
+ {
+ m_bTimerActive = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_NAME)
+ {
+ m_strTitle = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_EPGSEARCH)
+ {
+ m_strEpgSearchString = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_FULLTEXT)
+ {
+ m_bFullTextEpgSearch = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_CHANNEL)
+ {
+ int idx = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ const auto it = m_channelEntries.find(idx);
+ if (it != m_channelEntries.end())
+ {
+ m_channel = it->second;
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to get 'type' value");
+ }
+ }
+ else if (settingId == SETTING_TMR_WEEKDAYS)
+ {
+ m_iWeekdays = GetWeekdaysFromSetting(setting);
+ }
+ else if (settingId == SETTING_TMR_START_ANYTIME)
+ {
+ m_bStartAnyTime = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_END_ANYTIME)
+ {
+ m_bEndAnyTime = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_START_DAY)
+ {
+ SetDateFromIndex(m_startLocalTime,
+ std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ }
+ else if (settingId == SETTING_TMR_END_DAY)
+ {
+ SetDateFromIndex(m_endLocalTime,
+ std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ }
+ else if (settingId == SETTING_TMR_FIRST_DAY)
+ {
+ SetDateFromIndex(m_firstDayLocalTime,
+ std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ }
+ else if (settingId == SETTING_TMR_NEW_EPISODES)
+ {
+ m_iPreventDupEpisodes = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_BEGIN_PRE)
+ {
+ m_iMarginStart = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_END_POST)
+ {
+ m_iMarginEnd = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_PRIORITY)
+ {
+ m_iPriority = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_LIFETIME)
+ {
+ m_iLifetime = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_MAX_REC)
+ {
+ m_iMaxRecordings = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_DIR)
+ {
+ m_strDirectory = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_REC_GROUP)
+ {
+ m_iRecordingGroup = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+}
+
+void CGUIDialogPVRTimerSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ {
+ CLog::LogF(LOGERROR, "No setting");
+ return;
+ }
+
+ CGUIDialogSettingsManualBase::OnSettingAction(setting);
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == SETTING_TMR_BEGIN)
+ {
+ KODI::TIME::SystemTime timerStartTime;
+ m_startLocalTime.GetAsSystemTime(timerStartTime);
+ if (CGUIDialogNumeric::ShowAndGetTime(timerStartTime, g_localizeStrings.Get(14066)))
+ {
+ SetTimeFromSystemTime(m_startLocalTime, timerStartTime);
+ m_timerStartTimeStr = m_startLocalTime.GetAsLocalizedTime("", false);
+ SetButtonLabels();
+ }
+ }
+ else if (settingId == SETTING_TMR_END)
+ {
+ KODI::TIME::SystemTime timerEndTime;
+ m_endLocalTime.GetAsSystemTime(timerEndTime);
+ if (CGUIDialogNumeric::ShowAndGetTime(timerEndTime, g_localizeStrings.Get(14066)))
+ {
+ SetTimeFromSystemTime(m_endLocalTime, timerEndTime);
+ m_timerEndTimeStr = m_endLocalTime.GetAsLocalizedTime("", false);
+ SetButtonLabels();
+ }
+ }
+}
+
+bool CGUIDialogPVRTimerSettings::Validate()
+{
+ // @todo: Timer rules may have no date (time-only), so we can't check those for now.
+ // We need to extend the api with additional attributes to properly fix this
+ if (m_timerType->IsTimerRule())
+ return true;
+
+ bool bStartAnyTime = m_bStartAnyTime;
+ bool bEndAnyTime = m_bEndAnyTime;
+
+ if (!m_timerType->SupportsStartAnyTime() ||
+ !m_timerType->IsEpgBased()) // Start anytime toggle is not displayed
+ bStartAnyTime = false; // Assume start time change needs checking for
+
+ if (!m_timerType->SupportsEndAnyTime() ||
+ !m_timerType->IsEpgBased()) // End anytime toggle is not displayed
+ bEndAnyTime = false; // Assume end time change needs checking for
+
+ // Begin and end time
+ if (!bStartAnyTime && !bEndAnyTime)
+ {
+ if (m_timerType->SupportsStartTime() && m_timerType->SupportsEndTime() &&
+ m_endLocalTime < m_startLocalTime)
+ {
+ HELPERS::ShowOKDialogText(CVariant{19065}, // "Timer settings"
+ CVariant{19072}); // In order to add/update a timer
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool CGUIDialogPVRTimerSettings::Save()
+{
+ if (!Validate())
+ return false;
+
+ // Timer type
+ m_timerInfoTag->SetTimerType(m_timerType);
+
+ // Timer active/inactive
+ m_timerInfoTag->m_state = m_bTimerActive ? PVR_TIMER_STATE_SCHEDULED : PVR_TIMER_STATE_DISABLED;
+
+ // Name
+ m_timerInfoTag->m_strTitle = m_strTitle;
+
+ // epg search string (only for epg-based timer rules)
+ m_timerInfoTag->m_strEpgSearchString = m_strEpgSearchString;
+
+ // epg fulltext search, instead of just title match. (only for epg-based timer rules)
+ m_timerInfoTag->m_bFullTextEpgSearch = m_bFullTextEpgSearch;
+
+ // Channel
+ m_timerInfoTag->m_iClientChannelUid = m_channel.channelUid;
+ m_timerInfoTag->m_iClientId = m_channel.clientId;
+ m_timerInfoTag->m_bIsRadio = m_bIsRadio;
+ m_timerInfoTag->UpdateChannel();
+
+ if (!m_timerType->SupportsStartAnyTime() ||
+ !m_timerType->IsEpgBased()) // Start anytime toggle is not displayed
+ m_bStartAnyTime = false; // Assume start time change needs checking for
+ m_timerInfoTag->m_bStartAnyTime = m_bStartAnyTime;
+
+ if (!m_timerType->SupportsEndAnyTime() ||
+ !m_timerType->IsEpgBased()) // End anytime toggle is not displayed
+ m_bEndAnyTime = false; // Assume end time change needs checking for
+ m_timerInfoTag->m_bEndAnyTime = m_bEndAnyTime;
+
+ // Begin and end time
+ if (!m_bStartAnyTime && !m_bEndAnyTime)
+ {
+ if (m_timerType->SupportsStartTime() && // has start clock entry
+ m_timerType->SupportsEndTime() && // and end clock entry
+ m_timerType->IsTimerRule()) // but no associated start/end day spinners
+ {
+ if (m_endLocalTime < m_startLocalTime) // And the end clock is earlier than the start clock
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "End before start, adding a day.");
+ m_endLocalTime += CDateTimeSpan(1, 0, 0, 0);
+ if (m_endLocalTime < m_startLocalTime)
+ {
+ CLog::Log(LOGWARNING,
+ "Timer settings dialog: End before start. Setting end time to start time.");
+ m_endLocalTime = m_startLocalTime;
+ }
+ }
+ else if (m_endLocalTime >
+ (m_startLocalTime + CDateTimeSpan(1, 0, 0, 0))) // Or the duration is more than a day
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "End > 1 day after start, removing a day.");
+ m_endLocalTime -= CDateTimeSpan(1, 0, 0, 0);
+ if (m_endLocalTime > (m_startLocalTime + CDateTimeSpan(1, 0, 0, 0)))
+ {
+ CLog::Log(
+ LOGWARNING,
+ "Timer settings dialog: End > 1 day after start. Setting end time to start time.");
+ m_endLocalTime = m_startLocalTime;
+ }
+ }
+ }
+ else if (m_endLocalTime < m_startLocalTime)
+ {
+ // this case will fail validation so this can't be reached.
+ }
+ m_timerInfoTag->SetStartFromLocalTime(m_startLocalTime);
+ m_timerInfoTag->SetEndFromLocalTime(m_endLocalTime);
+ }
+ else if (!m_bStartAnyTime)
+ m_timerInfoTag->SetStartFromLocalTime(m_startLocalTime);
+ else if (!m_bEndAnyTime)
+ m_timerInfoTag->SetEndFromLocalTime(m_endLocalTime);
+
+ // Days of week (only for timer rules)
+ if (m_timerType->IsTimerRule())
+ m_timerInfoTag->m_iWeekdays = m_iWeekdays;
+ else
+ m_timerInfoTag->m_iWeekdays = PVR_WEEKDAY_NONE;
+
+ // First day (only for timer rules)
+ m_timerInfoTag->SetFirstDayFromLocalTime(m_firstDayLocalTime);
+
+ // "New episodes only" (only for timer rules)
+ m_timerInfoTag->m_iPreventDupEpisodes = m_iPreventDupEpisodes;
+
+ // Pre and post record time
+ m_timerInfoTag->m_iMarginStart = m_iMarginStart;
+ m_timerInfoTag->m_iMarginEnd = m_iMarginEnd;
+
+ // Priority
+ m_timerInfoTag->m_iPriority = m_iPriority;
+
+ // Lifetime
+ m_timerInfoTag->m_iLifetime = m_iLifetime;
+
+ // MaxRecordings
+ m_timerInfoTag->m_iMaxRecordings = m_iMaxRecordings;
+
+ // Recording folder
+ m_timerInfoTag->m_strDirectory = m_strDirectory;
+
+ // Recording group
+ m_timerInfoTag->m_iRecordingGroup = m_iRecordingGroup;
+
+ // Set the timer's title to the channel name if it's empty or 'New Timer'
+ if (m_strTitle.empty() || m_strTitle == g_localizeStrings.Get(19056))
+ {
+ const std::string channelName = m_timerInfoTag->ChannelName();
+ if (!channelName.empty())
+ m_timerInfoTag->m_strTitle = channelName;
+ }
+
+ // Update summary
+ m_timerInfoTag->UpdateSummary();
+
+ return true;
+}
+
+void CGUIDialogPVRTimerSettings::SetButtonLabels()
+{
+ // timer start time
+ BaseSettingControlPtr settingControl = GetSettingControl(SETTING_TMR_BEGIN);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ {
+ SET_CONTROL_LABEL2(settingControl->GetID(), m_timerStartTimeStr);
+ }
+
+ // timer end time
+ settingControl = GetSettingControl(SETTING_TMR_END);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ {
+ SET_CONTROL_LABEL2(settingControl->GetID(), m_timerEndTimeStr);
+ }
+}
+
+void CGUIDialogPVRTimerSettings::AddCondition(const std::shared_ptr<CSetting>& setting,
+ const std::string& identifier,
+ SettingConditionCheck condition,
+ SettingDependencyType depType,
+ const std::string& settingId)
+{
+ GetSettingsManager()->AddDynamicCondition(identifier, condition, this);
+ CSettingDependency dep(depType, GetSettingsManager());
+ dep.And()->Add(CSettingDependencyConditionPtr(
+ new CSettingDependencyCondition(identifier, "true", settingId, false, GetSettingsManager())));
+ SettingDependencies deps(setting->GetDependencies());
+ deps.push_back(dep);
+ setting->SetDependencies(deps);
+}
+
+int CGUIDialogPVRTimerSettings::GetDateAsIndex(const CDateTime& datetime)
+{
+ const CDateTime date(datetime.GetYear(), datetime.GetMonth(), datetime.GetDay(), 0, 0, 0);
+ time_t t(0);
+ date.GetAsTime(t);
+ return static_cast<int>(t);
+}
+
+void CGUIDialogPVRTimerSettings::SetDateFromIndex(CDateTime& datetime, int date)
+{
+ const CDateTime newDate(static_cast<time_t>(date));
+ datetime.SetDateTime(newDate.GetYear(), newDate.GetMonth(), newDate.GetDay(), datetime.GetHour(),
+ datetime.GetMinute(), datetime.GetSecond());
+}
+
+void CGUIDialogPVRTimerSettings::SetTimeFromSystemTime(CDateTime& datetime,
+ const KODI::TIME::SystemTime& time)
+{
+ const CDateTime newTime(time);
+ datetime.SetDateTime(datetime.GetYear(), datetime.GetMonth(), datetime.GetDay(),
+ newTime.GetHour(), newTime.GetMinute(), newTime.GetSecond());
+}
+
+void CGUIDialogPVRTimerSettings::InitializeTypesList()
+{
+ m_typeEntries.clear();
+
+ // If timer is read-only or was created by a timer rule, only add current type, for information. Type can't be changed.
+ if (m_timerType->IsReadOnly() || m_timerInfoTag->HasParent())
+ {
+ m_typeEntries.insert(std::make_pair(0, m_timerType));
+ return;
+ }
+
+ bool bFoundThisType(false);
+ int idx(0);
+ const std::vector<std::shared_ptr<CPVRTimerType>> types(CPVRTimerType::GetAllTypes());
+ for (const auto& type : types)
+ {
+ // Type definition prohibits created of new instances.
+ // But the dialog can act as a viewer for these types.
+ if (type->ForbidsNewInstances())
+ continue;
+
+ // Read-only timers cannot be created using this dialog.
+ // But the dialog can act as a viewer for read-only types.
+ if (type->IsReadOnly())
+ continue;
+
+ // Drop TimerTypes that require EPGInfo, if none is populated
+ if (type->RequiresEpgTagOnCreate() && !m_timerInfoTag->GetEpgInfoTag())
+ continue;
+
+ // Drop TimerTypes without 'Series' EPG attributes if none are set
+ if (type->RequiresEpgSeriesOnCreate())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag(m_timerInfoTag->GetEpgInfoTag());
+ if (epgTag && !epgTag->IsSeries())
+ continue;
+ }
+
+ // Drop TimerTypes which need series link if none is set
+ if (type->RequiresEpgSeriesLinkOnCreate())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag(m_timerInfoTag->GetEpgInfoTag());
+ if (!epgTag || epgTag->SeriesLink().empty())
+ continue;
+ }
+
+ // Drop TimerTypes that forbid EPGInfo, if it is populated
+ if (type->ForbidsEpgTagOnCreate() && m_timerInfoTag->GetEpgInfoTag())
+ continue;
+
+ // Drop TimerTypes that aren't rules and cannot be recorded
+ if (!type->IsTimerRule())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag(m_timerInfoTag->GetEpgInfoTag());
+ bool bCanRecord = epgTag ? epgTag->IsRecordable()
+ : m_timerInfoTag->EndAsLocalTime() > CDateTime::GetCurrentDateTime();
+ if (!bCanRecord)
+ continue;
+ }
+
+ if (!bFoundThisType && *type == *m_timerType)
+ bFoundThisType = true;
+
+ m_typeEntries.insert(std::make_pair(idx++, type));
+ }
+
+ if (!bFoundThisType)
+ m_typeEntries.insert(std::make_pair(idx++, m_timerType));
+}
+
+void CGUIDialogPVRTimerSettings::InitializeChannelsList()
+{
+ m_channelEntries.clear();
+
+ int index = 0;
+
+ // Add special "any channel" entries - one for every client (used for epg-based timer rules).
+ const CPVRClientMap clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients();
+ for (const auto& client : clients)
+ {
+ m_channelEntries.insert(
+ {index, ChannelDescriptor(PVR_CHANNEL_INVALID_UID, client.second->GetID(),
+ g_localizeStrings.Get(809))}); // "Any channel"
+ ++index;
+ }
+
+ // Add regular channels
+ const std::shared_ptr<CPVRChannelGroup> allGroup =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_bIsRadio);
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ allGroup->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+ for (const auto& groupMember : groupMembers)
+ {
+ const std::shared_ptr<CPVRChannel> channel = groupMember->Channel();
+ const std::string channelDescription = StringUtils::Format(
+ "{} {}", groupMember->ChannelNumber().FormattedChannelNumber(), channel->ChannelName());
+ m_channelEntries.insert(
+ {index, ChannelDescriptor(channel->UniqueID(), channel->ClientID(), channelDescription)});
+ ++index;
+ }
+}
+
+void CGUIDialogPVRTimerSettings::TypesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+ current = 0;
+
+ static const std::vector<std::pair<std::string, CVariant>> reminderTimerProps{
+ std::make_pair("PVR.IsRemindingTimer", CVariant{true})};
+ static const std::vector<std::pair<std::string, CVariant>> recordingTimerProps{
+ std::make_pair("PVR.IsRecordingTimer", CVariant{true})};
+
+ const auto clients = CServiceBroker::GetPVRManager().Clients();
+
+ bool foundCurrent(false);
+ for (const auto& typeEntry : pThis->m_typeEntries)
+ {
+ std::string clientName;
+
+ const auto client = clients->GetCreatedClient(typeEntry.second->GetClientId());
+ if (client)
+ clientName = client->GetFriendlyName();
+
+ list.emplace_back(typeEntry.second->GetDescription(), clientName, typeEntry.first,
+ typeEntry.second->IsReminder() ? reminderTimerProps : recordingTimerProps);
+
+ if (!foundCurrent && (*(pThis->m_timerType) == *(typeEntry.second)))
+ {
+ current = typeEntry.first;
+ foundCurrent = true;
+ }
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::ChannelsFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+ current = 0;
+
+ bool foundCurrent(false);
+ for (const auto& channelEntry : pThis->m_channelEntries)
+ {
+ // Only include channels for the currently selected timer type or all channels if type is client-independent.
+ if (pThis->m_timerType->GetClientId() == -1 || // client-independent
+ pThis->m_timerType->GetClientId() == channelEntry.second.clientId)
+ {
+ // Do not add "any channel" entry if not supported by selected timer type.
+ if (channelEntry.second.channelUid == PVR_CHANNEL_INVALID_UID &&
+ !pThis->m_timerType->SupportsAnyChannel())
+ continue;
+
+ list.emplace_back(
+ IntegerSettingOption(channelEntry.second.description, channelEntry.first));
+ }
+
+ if (!foundCurrent && (pThis->m_channel == channelEntry.second))
+ {
+ current = channelEntry.first;
+ foundCurrent = true;
+ }
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::DaysFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+ current = 0;
+
+ // Data range: "today" until "yesterday next year"
+ const CDateTime now(CDateTime::GetCurrentDateTime());
+ CDateTime time(now.GetYear(), now.GetMonth(), now.GetDay(), 0, 0, 0);
+ const CDateTime yesterdayPlusOneYear(CDateTime(time.GetYear() + 1, time.GetMonth(),
+ time.GetDay(), time.GetHour(), time.GetMinute(),
+ time.GetSecond()) -
+ CDateTimeSpan(1, 0, 0, 0));
+
+ CDateTime oldCDateTime;
+ if (setting->GetId() == SETTING_TMR_FIRST_DAY)
+ oldCDateTime = pThis->m_timerInfoTag->FirstDayAsLocalTime();
+ else if (setting->GetId() == SETTING_TMR_START_DAY)
+ oldCDateTime = pThis->m_timerInfoTag->StartAsLocalTime();
+ else
+ oldCDateTime = pThis->m_timerInfoTag->EndAsLocalTime();
+ const CDateTime oldCDate(oldCDateTime.GetYear(), oldCDateTime.GetMonth(), oldCDateTime.GetDay(),
+ 0, 0, 0);
+
+ if ((oldCDate < time) || (oldCDate > yesterdayPlusOneYear))
+ list.emplace_back(oldCDate.GetAsLocalizedDate(true /*long date*/), GetDateAsIndex(oldCDate));
+
+ while (time <= yesterdayPlusOneYear)
+ {
+ list.emplace_back(time.GetAsLocalizedDate(true /*long date*/), GetDateAsIndex(time));
+ time += CDateTimeSpan(1, 0, 0, 0);
+ }
+
+ if (setting->GetId() == SETTING_TMR_FIRST_DAY)
+ current = GetDateAsIndex(pThis->m_firstDayLocalTime);
+ else if (setting->GetId() == SETTING_TMR_START_DAY)
+ current = GetDateAsIndex(pThis->m_startLocalTime);
+ else
+ current = GetDateAsIndex(pThis->m_endLocalTime);
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::DupEpisodesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ std::vector<std::pair<std::string, int>> values;
+ pThis->m_timerType->GetPreventDuplicateEpisodesValues(values);
+ std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) {
+ return IntegerSettingOption(value.first, value.second);
+ });
+
+ current = pThis->m_iPreventDupEpisodes;
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::WeekdaysFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+ list.emplace_back(g_localizeStrings.Get(831), PVR_WEEKDAY_MONDAY); // "Mondays"
+ list.emplace_back(g_localizeStrings.Get(832), PVR_WEEKDAY_TUESDAY); // "Tuesdays"
+ list.emplace_back(g_localizeStrings.Get(833), PVR_WEEKDAY_WEDNESDAY); // "Wednesdays"
+ list.emplace_back(g_localizeStrings.Get(834), PVR_WEEKDAY_THURSDAY); // "Thursdays"
+ list.emplace_back(g_localizeStrings.Get(835), PVR_WEEKDAY_FRIDAY); // "Fridays"
+ list.emplace_back(g_localizeStrings.Get(836), PVR_WEEKDAY_SATURDAY); // "Saturdays"
+ list.emplace_back(g_localizeStrings.Get(837), PVR_WEEKDAY_SUNDAY); // "Sundays"
+
+ current = pThis->m_iWeekdays;
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::PrioritiesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ std::vector<std::pair<std::string, int>> values;
+ pThis->m_timerType->GetPriorityValues(values);
+ std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) {
+ return IntegerSettingOption(value.first, value.second);
+ });
+
+ current = pThis->m_iPriority;
+
+ auto it = list.begin();
+ while (it != list.end())
+ {
+ if (it->value == current)
+ break; // value already in list
+
+ ++it;
+ }
+
+ if (it == list.end())
+ {
+ // PVR backend supplied value is not in the list of predefined values. Insert it.
+ list.insert(it, IntegerSettingOption(std::to_string(current), current));
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::LifetimesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ std::vector<std::pair<std::string, int>> values;
+ pThis->m_timerType->GetLifetimeValues(values);
+ std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) {
+ return IntegerSettingOption(value.first, value.second);
+ });
+
+ current = pThis->m_iLifetime;
+
+ auto it = list.begin();
+ while (it != list.end())
+ {
+ if (it->value == current)
+ break; // value already in list
+
+ ++it;
+ }
+
+ if (it == list.end())
+ {
+ // PVR backend supplied value is not in the list of predefined values. Insert it.
+ list.insert(it, IntegerSettingOption(
+ StringUtils::Format(g_localizeStrings.Get(17999), current) /* {} days */,
+ current));
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::MaxRecordingsFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ std::vector<std::pair<std::string, int>> values;
+ pThis->m_timerType->GetMaxRecordingsValues(values);
+ std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) {
+ return IntegerSettingOption(value.first, value.second);
+ });
+
+ current = pThis->m_iMaxRecordings;
+
+ auto it = list.begin();
+ while (it != list.end())
+ {
+ if (it->value == current)
+ break; // value already in list
+
+ ++it;
+ }
+
+ if (it == list.end())
+ {
+ // PVR backend supplied value is not in the list of predefined values. Insert it.
+ list.insert(it, IntegerSettingOption(std::to_string(current), current));
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::RecordingGroupFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ std::vector<std::pair<std::string, int>> values;
+ pThis->m_timerType->GetRecordingGroupValues(values);
+ std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) {
+ return IntegerSettingOption(value.first, value.second);
+ });
+
+ current = pThis->m_iRecordingGroup;
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::MarginTimeFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ // Get global settings values
+ CPVRSettings::MarginTimeFiller(setting, list, current, data);
+
+ if (setting->GetId() == SETTING_TMR_BEGIN_PRE)
+ current = pThis->m_iMarginStart;
+ else
+ current = pThis->m_iMarginEnd;
+
+ bool bInsertValue = true;
+ auto it = list.begin();
+ while (it != list.end())
+ {
+ if (it->value == current)
+ {
+ bInsertValue = false;
+ break; // value already in list
+ }
+
+ if (it->value > current)
+ break;
+
+ ++it;
+ }
+
+ if (bInsertValue)
+ {
+ // PVR backend supplied value is not in the list of predefined values. Insert it.
+ list.insert(it, IntegerSettingOption(
+ StringUtils::Format(g_localizeStrings.Get(14044), current) /* {} min */,
+ current));
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+std::string CGUIDialogPVRTimerSettings::WeekdaysValueFormatter(const SettingConstPtr& setting)
+{
+ return CPVRTimerInfoTag::GetWeekdaysString(GetWeekdaysFromSetting(setting), true, true);
+}
+
+void CGUIDialogPVRTimerSettings::AddTypeDependentEnableCondition(
+ const std::shared_ptr<CSetting>& setting, const std::string& identifier)
+{
+ // Enable setting depending on read-only attribute of the selected timer type
+ std::string id(identifier);
+ id.append(TYPE_DEP_ENABLE_COND_ID_POSTFIX);
+ AddCondition(setting, id, TypeReadOnlyCondition, SettingDependencyType::Enable, SETTING_TMR_TYPE);
+}
+
+bool CGUIDialogPVRTimerSettings::TypeReadOnlyCondition(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis == NULL)
+ {
+ CLog::LogF(LOGERROR, "No dialog");
+ return false;
+ }
+
+ if (!StringUtils::EqualsNoCase(value, "true"))
+ return false;
+
+ std::string cond(condition);
+ cond.erase(cond.find(TYPE_DEP_ENABLE_COND_ID_POSTFIX));
+
+ // If only one type is available, disable type selector.
+ if (pThis->m_typeEntries.size() == 1)
+ {
+ if (cond == SETTING_TMR_TYPE)
+ return false;
+ }
+
+ // For existing one time epg-based timers, disable editing of epg-filled data.
+ if (!pThis->m_bIsNewTimer && pThis->m_timerType->IsEpgBasedOnetime())
+ {
+ if ((cond == SETTING_TMR_NAME) || (cond == SETTING_TMR_CHANNEL) ||
+ (cond == SETTING_TMR_START_DAY) || (cond == SETTING_TMR_END_DAY) ||
+ (cond == SETTING_TMR_BEGIN) || (cond == SETTING_TMR_END))
+ return false;
+ }
+
+ /* Always enable enable/disable, if supported by the timer type. */
+ if (pThis->m_timerType->SupportsEnableDisable() && !pThis->m_timerInfoTag->IsBroken())
+ {
+ if (cond == SETTING_TMR_ACTIVE)
+ return true;
+ }
+
+ // Let the PVR client decide...
+ int idx = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ const auto entry = pThis->m_typeEntries.find(idx);
+ if (entry != pThis->m_typeEntries.end())
+ return !entry->second->IsReadOnly();
+ else
+ CLog::LogF(LOGERROR, "No type entry");
+
+ return false;
+}
+
+void CGUIDialogPVRTimerSettings::AddTypeDependentVisibilityCondition(
+ const std::shared_ptr<CSetting>& setting, const std::string& identifier)
+{
+ // Show or hide setting depending on attributes of the selected timer type
+ std::string id(identifier);
+ id.append(TYPE_DEP_VISIBI_COND_ID_POSTFIX);
+ AddCondition(setting, id, TypeSupportsCondition, SettingDependencyType::Visible,
+ SETTING_TMR_TYPE);
+}
+
+bool CGUIDialogPVRTimerSettings::TypeSupportsCondition(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis == NULL)
+ {
+ CLog::LogF(LOGERROR, "No dialog");
+ return false;
+ }
+
+ if (!StringUtils::EqualsNoCase(value, "true"))
+ return false;
+
+ int idx = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ const auto entry = pThis->m_typeEntries.find(idx);
+ if (entry != pThis->m_typeEntries.end())
+ {
+ std::string cond(condition);
+ cond.erase(cond.find(TYPE_DEP_VISIBI_COND_ID_POSTFIX));
+
+ if (cond == SETTING_TMR_EPGSEARCH)
+ return entry->second->SupportsEpgTitleMatch() || entry->second->SupportsEpgFulltextMatch();
+ else if (cond == SETTING_TMR_FULLTEXT)
+ return entry->second->SupportsEpgFulltextMatch();
+ else if (cond == SETTING_TMR_ACTIVE)
+ return entry->second->SupportsEnableDisable();
+ else if (cond == SETTING_TMR_CHANNEL)
+ return entry->second->SupportsChannels();
+ else if (cond == SETTING_TMR_START_ANYTIME)
+ return entry->second->SupportsStartAnyTime() && entry->second->IsEpgBased();
+ else if (cond == SETTING_TMR_END_ANYTIME)
+ return entry->second->SupportsEndAnyTime() && entry->second->IsEpgBased();
+ else if (cond == SETTING_TMR_START_DAY)
+ return entry->second->SupportsStartTime() && entry->second->IsOnetime();
+ else if (cond == SETTING_TMR_END_DAY)
+ return entry->second->SupportsEndTime() && entry->second->IsOnetime();
+ else if (cond == SETTING_TMR_BEGIN)
+ return entry->second->SupportsStartTime();
+ else if (cond == SETTING_TMR_END)
+ return entry->second->SupportsEndTime();
+ else if (cond == SETTING_TMR_WEEKDAYS)
+ return entry->second->SupportsWeekdays();
+ else if (cond == SETTING_TMR_FIRST_DAY)
+ return entry->second->SupportsFirstDay();
+ else if (cond == SETTING_TMR_NEW_EPISODES)
+ return entry->second->SupportsRecordOnlyNewEpisodes();
+ else if (cond == SETTING_TMR_BEGIN_PRE)
+ return entry->second->SupportsStartMargin();
+ else if (cond == SETTING_TMR_END_POST)
+ return entry->second->SupportsEndMargin();
+ else if (cond == SETTING_TMR_PRIORITY)
+ return entry->second->SupportsPriority();
+ else if (cond == SETTING_TMR_LIFETIME)
+ return entry->second->SupportsLifetime();
+ else if (cond == SETTING_TMR_MAX_REC)
+ return entry->second->SupportsMaxRecordings();
+ else if (cond == SETTING_TMR_DIR)
+ return entry->second->SupportsRecordingFolders();
+ else if (cond == SETTING_TMR_REC_GROUP)
+ return entry->second->SupportsRecordingGroup();
+ else
+ CLog::LogF(LOGERROR, "Unknown condition");
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "No type entry");
+ }
+ return false;
+}
+
+void CGUIDialogPVRTimerSettings::AddStartAnytimeDependentVisibilityCondition(
+ const std::shared_ptr<CSetting>& setting, const std::string& identifier)
+{
+ // Show or hide setting depending on value of setting "any time"
+ std::string id(identifier);
+ id.append(START_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX);
+ AddCondition(setting, id, StartAnytimeSetCondition, SettingDependencyType::Visible,
+ SETTING_TMR_START_ANYTIME);
+}
+
+bool CGUIDialogPVRTimerSettings::StartAnytimeSetCondition(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis == NULL)
+ {
+ CLog::LogF(LOGERROR, "No dialog");
+ return false;
+ }
+
+ if (!StringUtils::EqualsNoCase(value, "true"))
+ return false;
+
+ // "any time" setting is only relevant for epg-based timers.
+ if (!pThis->m_timerType->IsEpgBased())
+ return true;
+
+ // If 'Start anytime' option isn't supported, don't hide start time
+ if (!pThis->m_timerType->SupportsStartAnyTime())
+ return true;
+
+ std::string cond(condition);
+ cond.erase(cond.find(START_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX));
+
+ if ((cond == SETTING_TMR_START_DAY) || (cond == SETTING_TMR_BEGIN))
+ {
+ bool bAnytime = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ return !bAnytime;
+ }
+ return false;
+}
+
+void CGUIDialogPVRTimerSettings::AddEndAnytimeDependentVisibilityCondition(
+ const std::shared_ptr<CSetting>& setting, const std::string& identifier)
+{
+ // Show or hide setting depending on value of setting "any time"
+ std::string id(identifier);
+ id.append(END_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX);
+ AddCondition(setting, id, EndAnytimeSetCondition, SettingDependencyType::Visible,
+ SETTING_TMR_END_ANYTIME);
+}
+
+bool CGUIDialogPVRTimerSettings::EndAnytimeSetCondition(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis == NULL)
+ {
+ CLog::LogF(LOGERROR, "No dialog");
+ return false;
+ }
+
+ if (!StringUtils::EqualsNoCase(value, "true"))
+ return false;
+
+ // "any time" setting is only relevant for epg-based timers.
+ if (!pThis->m_timerType->IsEpgBased())
+ return true;
+
+ // If 'End anytime' option isn't supported, don't hide end time
+ if (!pThis->m_timerType->SupportsEndAnyTime())
+ return true;
+
+ std::string cond(condition);
+ cond.erase(cond.find(END_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX));
+
+ if ((cond == SETTING_TMR_END_DAY) || (cond == SETTING_TMR_END))
+ {
+ bool bAnytime = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ return !bAnytime;
+ }
+ return false;
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.h b/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.h
new file mode 100644
index 0000000..d3e9e3e
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.h
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h" // PVR_CHANNEL_INVALID_UID
+#include "settings/SettingConditions.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+#include "settings/lib/SettingDependency.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CSetting;
+
+struct IntegerSettingOption;
+
+namespace PVR
+{
+class CPVRTimerInfoTag;
+class CPVRTimerType;
+
+class CGUIDialogPVRTimerSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogPVRTimerSettings();
+ ~CGUIDialogPVRTimerSettings() override;
+
+ bool CanBeActivated() const override;
+
+ void SetTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer);
+
+protected:
+ // implementation of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+private:
+ bool Validate();
+ void InitializeTypesList();
+ void InitializeChannelsList();
+ void SetButtonLabels();
+
+ static int GetDateAsIndex(const CDateTime& datetime);
+ static void SetDateFromIndex(CDateTime& datetime, int date);
+ static void SetTimeFromSystemTime(CDateTime& datetime, const KODI::TIME::SystemTime& time);
+
+ static int GetWeekdaysFromSetting(const std::shared_ptr<const CSetting>& setting);
+
+ static void TypesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void ChannelsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void DaysFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void DupEpisodesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void WeekdaysFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void PrioritiesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void LifetimesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void MaxRecordingsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void RecordingGroupFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void MarginTimeFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ static std::string WeekdaysValueFormatter(const std::shared_ptr<const CSetting>& setting);
+
+ void AddCondition(const std::shared_ptr<CSetting>& setting,
+ const std::string& identifier,
+ SettingConditionCheck condition,
+ SettingDependencyType depType,
+ const std::string& settingId);
+
+ void AddTypeDependentEnableCondition(const std::shared_ptr<CSetting>& setting,
+ const std::string& identifier);
+ static bool TypeReadOnlyCondition(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+ void AddTypeDependentVisibilityCondition(const std::shared_ptr<CSetting>& setting,
+ const std::string& identifier);
+ static bool TypeSupportsCondition(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+ void AddStartAnytimeDependentVisibilityCondition(const std::shared_ptr<CSetting>& setting,
+ const std::string& identifier);
+ static bool StartAnytimeSetCondition(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+ void AddEndAnytimeDependentVisibilityCondition(const std::shared_ptr<CSetting>& setting,
+ const std::string& identifier);
+ static bool EndAnytimeSetCondition(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+ typedef std::map<int, std::shared_ptr<CPVRTimerType>> TypeEntriesMap;
+
+ typedef struct ChannelDescriptor
+ {
+ int channelUid;
+ int clientId;
+ std::string description;
+
+ ChannelDescriptor(int _channelUid = PVR_CHANNEL_INVALID_UID,
+ int _clientId = -1,
+ const std::string& _description = "")
+ : channelUid(_channelUid), clientId(_clientId), description(_description)
+ {
+ }
+
+ inline bool operator==(const ChannelDescriptor& right) const
+ {
+ return (channelUid == right.channelUid && clientId == right.clientId &&
+ description == right.description);
+ }
+
+ } ChannelDescriptor;
+
+ typedef std::map<int, ChannelDescriptor> ChannelEntriesMap;
+
+ std::shared_ptr<CPVRTimerInfoTag> m_timerInfoTag;
+ TypeEntriesMap m_typeEntries;
+ ChannelEntriesMap m_channelEntries;
+ std::string m_timerStartTimeStr;
+ std::string m_timerEndTimeStr;
+
+ std::shared_ptr<CPVRTimerType> m_timerType;
+ bool m_bIsRadio = false;
+ bool m_bIsNewTimer = true;
+ bool m_bTimerActive = false;
+ std::string m_strTitle;
+ std::string m_strEpgSearchString;
+ bool m_bFullTextEpgSearch = true;
+ ChannelDescriptor m_channel;
+ CDateTime m_startLocalTime;
+ CDateTime m_endLocalTime;
+ bool m_bStartAnyTime = false;
+ bool m_bEndAnyTime = false;
+ unsigned int m_iWeekdays;
+ CDateTime m_firstDayLocalTime;
+ unsigned int m_iPreventDupEpisodes = 0;
+ unsigned int m_iMarginStart = 0;
+ unsigned int m_iMarginEnd = 0;
+ int m_iPriority = 0;
+ int m_iLifetime = 0;
+ int m_iMaxRecordings = 0;
+ std::string m_strDirectory;
+ unsigned int m_iRecordingGroup = 0;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/epg/CMakeLists.txt b/xbmc/pvr/epg/CMakeLists.txt
new file mode 100644
index 0000000..0ea75b7
--- /dev/null
+++ b/xbmc/pvr/epg/CMakeLists.txt
@@ -0,0 +1,22 @@
+set(SOURCES EpgContainer.cpp
+ Epg.cpp
+ EpgDatabase.cpp
+ EpgInfoTag.cpp
+ EpgSearchFilter.cpp
+ EpgSearchPath.cpp
+ EpgChannelData.cpp
+ EpgTagsCache.cpp
+ EpgTagsContainer.cpp)
+
+set(HEADERS Epg.h
+ EpgContainer.h
+ EpgDatabase.h
+ EpgInfoTag.h
+ EpgSearchData.h
+ EpgSearchFilter.h
+ EpgSearchPath.h
+ EpgChannelData.h
+ EpgTagsCache.h
+ EpgTagsContainer.h)
+
+core_add_library(pvr_epg)
diff --git a/xbmc/pvr/epg/Epg.cpp b/xbmc/pvr/epg/Epg.cpp
new file mode 100644
index 0000000..1112b2f
--- /dev/null
+++ b/xbmc/pvr/epg/Epg.cpp
@@ -0,0 +1,554 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "Epg.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+CPVREpg::CPVREpg(int iEpgID,
+ const std::string& strName,
+ const std::string& strScraperName,
+ const std::shared_ptr<CPVREpgDatabase>& database)
+ : m_iEpgID(iEpgID),
+ m_strName(strName),
+ m_strScraperName(strScraperName),
+ m_channelData(new CPVREpgChannelData),
+ m_tags(m_iEpgID, m_channelData, database)
+{
+}
+
+CPVREpg::CPVREpg(int iEpgID,
+ const std::string& strName,
+ const std::string& strScraperName,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database)
+ : m_bChanged(true),
+ m_iEpgID(iEpgID),
+ m_strName(strName),
+ m_strScraperName(strScraperName),
+ m_channelData(channelData),
+ m_tags(m_iEpgID, m_channelData, database)
+{
+}
+
+CPVREpg::~CPVREpg()
+{
+ Clear();
+}
+
+void CPVREpg::ForceUpdate()
+{
+ m_bUpdatePending = true;
+ m_events.Publish(PVREvent::EpgUpdatePending);
+}
+
+void CPVREpg::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_tags.Clear();
+}
+
+void CPVREpg::Cleanup(int iPastDays)
+{
+ const CDateTime cleanupTime = CDateTime::GetUTCDateTime() - CDateTimeSpan(iPastDays, 0, 0, 0);
+ Cleanup(cleanupTime);
+}
+
+void CPVREpg::Cleanup(const CDateTime& time)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_tags.Cleanup(time);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagNow() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetActiveTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagNext() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetNextStartingTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagPrevious() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetLastEndedTag();
+}
+
+bool CPVREpg::CheckPlayingEvent()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_tags.UpdateActiveTag())
+ {
+ m_events.Publish(PVREvent::EpgActiveItem);
+ return true;
+ }
+ return false;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagByBroadcastId(unsigned int iUniqueBroadcastId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetTag(iUniqueBroadcastId);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagByDatabaseId(int iDatabaseId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetTagByDatabaseID(iDatabaseId);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagBetween(const CDateTime& beginTime, const CDateTime& endTime, bool bUpdateFromClient /* = false */)
+{
+ std::shared_ptr<CPVREpgInfoTag> tag;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ tag = m_tags.GetTagBetween(beginTime, endTime);
+
+ if (!tag && bUpdateFromClient)
+ {
+ // not found locally; try to fetch from client
+ time_t b;
+ beginTime.GetAsTime(b);
+ time_t e;
+ endTime.GetAsTime(e);
+
+ const std::shared_ptr<CPVREpg> tmpEpg = std::make_shared<CPVREpg>(
+ m_iEpgID, m_strName, m_strScraperName, m_channelData, std::shared_ptr<CPVREpgDatabase>());
+ if (tmpEpg->UpdateFromScraper(b, e, true))
+ tag = tmpEpg->GetTagBetween(beginTime, endTime, false);
+
+ if (tag)
+ m_tags.UpdateEntry(tag);
+ }
+
+ return tag;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpg::GetTimeline(
+ const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetTimeline(timelineStart, timelineEnd, minEventEnd, maxEventStart);
+}
+
+bool CPVREpg::UpdateEntries(const CPVREpg& epg)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* copy over tags */
+ m_tags.UpdateEntries(epg.m_tags);
+
+ /* update the last scan time of this table */
+ m_lastScanTime = CDateTime::GetUTCDateTime();
+ m_bUpdateLastScanTime = true;
+
+ m_events.Publish(PVREvent::Epg);
+ return true;
+}
+
+namespace
+{
+
+bool IsTagExpired(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ // Respect epg linger time.
+ const int iPastDays = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_EPG_PAST_DAYSTODISPLAY);
+ const CDateTime cleanupTime(CDateTime::GetUTCDateTime() - CDateTimeSpan(iPastDays, 0, 0, 0));
+
+ return tag->EndAsUTC() < cleanupTime;
+}
+
+} // unnamed namespace
+
+bool CPVREpg::UpdateEntry(const EPG_TAG* data, int iClientId)
+{
+ if (!data)
+ return false;
+
+ const std::shared_ptr<CPVREpgInfoTag> tag =
+ std::make_shared<CPVREpgInfoTag>(*data, iClientId, m_channelData, m_iEpgID);
+
+ return !IsTagExpired(tag) && m_tags.UpdateEntry(tag);
+}
+
+bool CPVREpg::UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE newState)
+{
+ bool bRet = true;
+ bool bNotify = true;
+
+ if (newState == EPG_EVENT_CREATED || newState == EPG_EVENT_UPDATED)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bRet = !IsTagExpired(tag) && m_tags.UpdateEntry(tag);
+ }
+ else if (newState == EPG_EVENT_DELETED)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVREpgInfoTag> existingTag = m_tags.GetTag(tag->UniqueBroadcastID());
+ if (!existingTag)
+ {
+ bRet = false;
+ }
+ else
+ {
+ if (IsTagExpired(existingTag))
+ {
+ m_tags.DeleteEntry(existingTag);
+ }
+ else
+ {
+ bNotify = false;
+ }
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unknown epg event state value: {}", newState);
+ bRet = false;
+ }
+
+ if (bRet && bNotify)
+ m_events.Publish(PVREvent::EpgItemUpdate);
+
+ return bRet;
+}
+
+bool CPVREpg::Update(time_t start,
+ time_t end,
+ int iUpdateTime,
+ int iPastDays,
+ const std::shared_ptr<CPVREpgDatabase>& database,
+ bool bForceUpdate /* = false */)
+{
+ bool bUpdate = false;
+ std::shared_ptr<CPVREpg> tmpEpg;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_lastScanTime.IsValid())
+ {
+ database->GetLastEpgScanTime(m_iEpgID, &m_lastScanTime);
+
+ if (!m_lastScanTime.IsValid())
+ {
+ m_lastScanTime.SetFromUTCDateTime(time_t(0));
+ m_bUpdateLastScanTime = true;
+ }
+ }
+
+ // enforce advanced settings update interval override for channels with no EPG data
+ if (m_tags.IsEmpty() && m_channelData->ChannelId() > 0) //! @todo why the channelid check?
+ iUpdateTime = CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iEpgUpdateEmptyTagsInterval;
+
+ if (bForceUpdate)
+ {
+ bUpdate = true;
+ }
+ else
+ {
+ // check if we have to update
+ time_t iNow = 0;
+ CDateTime::GetUTCDateTime().GetAsTime(iNow);
+
+ time_t iLastUpdate = 0;
+ m_lastScanTime.GetAsTime(iLastUpdate);
+
+ bUpdate = (iNow > iLastUpdate + iUpdateTime);
+ }
+
+ if (bUpdate)
+ {
+ tmpEpg = std::make_shared<CPVREpg>(m_iEpgID, m_strName, m_strScraperName, m_channelData,
+ std::shared_ptr<CPVREpgDatabase>());
+ }
+ }
+
+ // remove obsolete tags
+ Cleanup(iPastDays);
+
+ bool bGrabSuccess = true;
+
+ if (bUpdate)
+ {
+ bGrabSuccess = tmpEpg->UpdateFromScraper(start, end, bForceUpdate) && UpdateEntries(*tmpEpg);
+
+ if (!bGrabSuccess)
+ CLog::LogF(LOGERROR, "Failed to update table '{}'", Name());
+ }
+
+ m_bUpdatePending = false;
+ return bGrabSuccess;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpg::GetTags() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetAllTags();
+}
+
+bool CPVREpg::QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ // Note: It is guaranteed that both this EPG instance and database instance are already
+ // locked when this method gets called! No additional locking is needed here!
+
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return false;
+ }
+
+ if (m_iEpgID <= 0 || m_bChanged)
+ {
+ const int iId = database->Persist(*this, m_iEpgID > 0);
+ if (iId > 0 && m_iEpgID != iId)
+ {
+ m_iEpgID = iId;
+ m_tags.SetEpgID(iId);
+ }
+ }
+
+ if (m_tags.NeedsSave())
+ m_tags.QueuePersistQuery();
+
+ if (m_bUpdateLastScanTime)
+ database->QueuePersistLastEpgScanTimeQuery(m_iEpgID, m_lastScanTime);
+
+ m_bChanged = false;
+ m_bUpdateLastScanTime = false;
+
+ return true;
+}
+
+bool CPVREpg::QueueDeleteQueries(const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return false;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // delete own epg db entry
+ database->QueueDeleteEpgQuery(*this);
+
+ // delete last scan time db entry for this epg
+ database->QueueDeleteLastEpgScanTimeQuery(*this);
+
+ // delete all tags for this epg from db
+ m_tags.QueueDelete();
+
+ Clear();
+
+ return true;
+}
+
+std::pair<CDateTime, CDateTime> CPVREpg::GetFirstAndLastUncommitedEPGDate() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetFirstAndLastUncommitedEPGDate();
+}
+
+bool CPVREpg::UpdateFromScraper(time_t start, time_t end, bool bForceUpdate)
+{
+ if (m_strScraperName.empty())
+ {
+ CLog::LogF(LOGERROR, "No EPG scraper defined for table '{}'", m_strName);
+ }
+ else if (m_strScraperName == "client")
+ {
+ if (!CServiceBroker::GetPVRManager().EpgsCreated())
+ return false;
+
+ if (!m_channelData->IsEPGEnabled() || m_channelData->IsHidden())
+ {
+ // ignore. not interested in any updates.
+ return true;
+ }
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
+ if (client)
+ {
+ if (!client->GetClientCapabilities().SupportsEPG())
+ {
+ CLog::LogF(LOGERROR, "The backend for channel '{}' on client '{}' does not support EPGs",
+ m_channelData->ChannelName(), m_channelData->ClientId());
+ }
+ else if (!bForceUpdate && client->GetClientCapabilities().SupportsAsyncEPGTransfer())
+ {
+ // nothing to do. client will provide epg updates asynchronously
+ return true;
+ }
+ else
+ {
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Updating EPG for channel '{}' from client '{}'",
+ m_channelData->ChannelName(), m_channelData->ClientId());
+ return (client->GetEPGForChannel(m_channelData->UniqueClientChannelId(), this, start, end) == PVR_ERROR_NO_ERROR);
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Client '{}' not found, can't update", m_channelData->ClientId());
+ }
+ }
+ else // other non-empty scraper name...
+ {
+ CLog::LogF(LOGERROR, "Loading the EPG via scraper is not yet implemented!");
+ //! @todo Add Support for Web EPG Scrapers here
+ }
+
+ return false;
+}
+
+const std::string& CPVREpg::ConvertGenreIdToString(int iID, int iSubID)
+{
+ unsigned int iLabelId = 19499;
+ switch (iID)
+ {
+ case EPG_EVENT_CONTENTMASK_MOVIEDRAMA:
+ iLabelId = (iSubID <= 8) ? 19500 + iSubID : 19500;
+ break;
+ case EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS:
+ iLabelId = (iSubID <= 4) ? 19516 + iSubID : 19516;
+ break;
+ case EPG_EVENT_CONTENTMASK_SHOW:
+ iLabelId = (iSubID <= 3) ? 19532 + iSubID : 19532;
+ break;
+ case EPG_EVENT_CONTENTMASK_SPORTS:
+ iLabelId = (iSubID <= 11) ? 19548 + iSubID : 19548;
+ break;
+ case EPG_EVENT_CONTENTMASK_CHILDRENYOUTH:
+ iLabelId = (iSubID <= 5) ? 19564 + iSubID : 19564;
+ break;
+ case EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE:
+ iLabelId = (iSubID <= 6) ? 19580 + iSubID : 19580;
+ break;
+ case EPG_EVENT_CONTENTMASK_ARTSCULTURE:
+ iLabelId = (iSubID <= 11) ? 19596 + iSubID : 19596;
+ break;
+ case EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS:
+ iLabelId = (iSubID <= 3) ? 19612 + iSubID : 19612;
+ break;
+ case EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE:
+ iLabelId = (iSubID <= 7) ? 19628 + iSubID : 19628;
+ break;
+ case EPG_EVENT_CONTENTMASK_LEISUREHOBBIES:
+ iLabelId = (iSubID <= 7) ? 19644 + iSubID : 19644;
+ break;
+ case EPG_EVENT_CONTENTMASK_SPECIAL:
+ iLabelId = (iSubID <= 3) ? 19660 + iSubID : 19660;
+ break;
+ case EPG_EVENT_CONTENTMASK_USERDEFINED:
+ iLabelId = (iSubID <= 8) ? 19676 + iSubID : 19676;
+ break;
+ default:
+ break;
+ }
+
+ return g_localizeStrings.Get(iLabelId);
+}
+
+std::shared_ptr<CPVREpgChannelData> CPVREpg::GetChannelData() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData;
+}
+
+void CPVREpg::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channelData = data;
+ m_tags.SetChannelData(data);
+}
+
+int CPVREpg::ChannelID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->ChannelId();
+}
+
+const std::string& CPVREpg::ScraperName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strScraperName;
+}
+
+const std::string& CPVREpg::Name() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strName;
+}
+
+int CPVREpg::EpgID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iEpgID;
+}
+
+bool CPVREpg::UpdatePending() const
+{
+ return m_bUpdatePending;
+}
+
+bool CPVREpg::NeedsSave() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bChanged || m_bUpdateLastScanTime || m_tags.NeedsSave();
+}
+
+bool CPVREpg::IsValid() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (ScraperName() == "client")
+ return m_channelData->ClientId() != -1 && m_channelData->UniqueClientChannelId() != PVR_CHANNEL_INVALID_UID;
+
+ return true;
+}
+
+void CPVREpg::RemovedFromContainer()
+{
+ m_events.Publish(PVREvent::EpgDeleted);
+}
+
+int CPVREpg::CleanupCachedImages(const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ const std::vector<std::string> urlsToCheck = database->GetAllIconPaths(EpgID());
+ const std::string owner = StringUtils::Format(CPVREpgInfoTag::IMAGE_OWNER_PATTERN, EpgID());
+
+ return CPVRCachedImages::Cleanup({{owner, ""}}, urlsToCheck);
+}
diff --git a/xbmc/pvr/epg/Epg.h b/xbmc/pvr/epg/Epg.h
new file mode 100644
index 0000000..31bc8d0
--- /dev/null
+++ b/xbmc/pvr/epg/Epg.h
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h"
+#include "pvr/epg/EpgTagsContainer.h"
+#include "threads/CriticalSection.h"
+#include "utils/EventStream.h"
+
+#include <atomic>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+ enum class PVREvent;
+
+ class CPVREpgChannelData;
+ class CPVREpgDatabase;
+ class CPVREpgInfoTag;
+
+ class CPVREpg
+ {
+ friend class CPVREpgDatabase;
+
+ public:
+ /*!
+ * @brief Create a new EPG instance.
+ * @param iEpgID The ID of this table or <= 0 to create a new ID.
+ * @param strName The name of this table.
+ * @param strScraperName The name of the scraper to use.
+ * @param database The EPG database
+ */
+ CPVREpg(int iEpgID,
+ const std::string& strName,
+ const std::string& strScraperName,
+ const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Create a new EPG instance.
+ * @param iEpgID The ID of this table or <= 0 to create a new ID.
+ * @param strName The name of this table.
+ * @param strScraperName The name of the scraper to use.
+ * @param channelData The channel data.
+ * @param database The EPG database
+ */
+ CPVREpg(int iEpgID,
+ const std::string& strName,
+ const std::string& strScraperName,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Destroy this EPG instance.
+ */
+ virtual ~CPVREpg();
+
+ /*!
+ * @brief Get data for the channel associated with this EPG.
+ * @return The data.
+ */
+ std::shared_ptr<CPVREpgChannelData> GetChannelData() const;
+
+ /*!
+ * @brief Set data for the channel associated with this EPG.
+ * @param data The data.
+ */
+ void SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data);
+
+ /*!
+ * @brief The id of the channel associated with this EPG.
+ * @return The channel id or -1 if no channel is associated
+ */
+ int ChannelID() const;
+
+ /*!
+ * @brief Get the name of the scraper to use for this table.
+ * @return The name of the scraper to use for this table.
+ */
+ const std::string& ScraperName() const;
+
+ /*!
+ * @brief Returns if there is a manual update pending for this EPG
+ * @return True if there is a manual update pending, false otherwise
+ */
+ bool UpdatePending() const;
+
+ /*!
+ * @brief Clear the current tags and schedule manual update
+ */
+ void ForceUpdate();
+
+ /*!
+ * @brief Get the name of this table.
+ * @return The name of this table.
+ */
+ const std::string& Name() const;
+
+ /*!
+ * @brief Get the database ID of this table.
+ * @return The database ID of this table.
+ */
+ int EpgID() const;
+
+ /*!
+ * @brief Remove all entries from this EPG that finished before the given time.
+ * @param time Delete entries with an end time before this time in UTC.
+ */
+ void Cleanup(const CDateTime& time);
+
+ /*!
+ * @brief Remove all entries from this EPG.
+ */
+ void Clear();
+
+ /*!
+ * @brief Get the event that is occurring now
+ * @return The current event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagNow() const;
+
+ /*!
+ * @brief Get the event that will occur next
+ * @return The next event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagNext() const;
+
+ /*!
+ * @brief Get the event that occurred previously
+ * @return The previous event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagPrevious() const;
+
+ /*!
+ * @brief Get the event that occurs between the given begin and end time.
+ * @param beginTime Minimum start time in UTC of the event.
+ * @param endTime Maximum end time in UTC of the event.
+ * @param bUpdateFromClient if true, try to fetch the event from the client if not found locally.
+ * @return The found tag or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagBetween(const CDateTime& beginTime, const CDateTime& endTime, bool bUpdateFromClient = false);
+
+ /*!
+ * @brief Get the event matching the given unique broadcast id
+ * @param iUniqueBroadcastId The uid to look up
+ * @return The matching event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagByBroadcastId(unsigned int iUniqueBroadcastId) const;
+
+ /*!
+ * @brief Get the event matching the given database id
+ * @param iDatabaseId The id to look up
+ * @return The matching event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagByDatabaseId(int iDatabaseId) const;
+
+ /*!
+ * @brief Update an entry in this EPG.
+ * @param data The tag to update.
+ * @param iClientId The id of the pvr client this event belongs to.
+ * @return True if it was updated successfully, false otherwise.
+ */
+ bool UpdateEntry(const EPG_TAG* data, int iClientId);
+
+ /*!
+ * @brief Update an entry in this EPG.
+ * @param tag The tag to update.
+ * @param newState the new state of the event.
+ * @return True if it was updated successfully, false otherwise.
+ */
+ bool UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE newState);
+
+ /*!
+ * @brief Update the EPG from 'start' till 'end'.
+ * @param start The start time.
+ * @param end The end time.
+ * @param iUpdateTime Update the table after the given amount of time has passed.
+ * @param iPastDays Amount of past days from now on, for which past entries are to be kept.
+ * @param database If given, the database to store the data.
+ * @param bForceUpdate Force update from client even if it's not the time to
+ * @return True if the update was successful, false otherwise.
+ */
+ bool Update(time_t start, time_t end, int iUpdateTime, int iPastDays, const std::shared_ptr<CPVREpgDatabase>& database, bool bForceUpdate = false);
+
+ /*!
+ * @brief Get all EPG tags.
+ * @return The tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetTags() const;
+
+ /*!
+ * @brief Get all EPG tags for the given time frame, including "gap" tags.
+ * @param timelineStart Start of time line
+ * @param timelineEnd End of time line
+ * @param minEventEnd The minimum end time of the events to return
+ * @param maxEventStart The maximum start time of the events to return
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetTimeline(const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const;
+
+ /*!
+ * @brief Write the query to persist data into given database's queue
+ * @param database The database.
+ * @return True on success, false otherwise.
+ */
+ bool QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Write the delete queries into the given database's queue
+ * @param database The database.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteQueries(const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Get the start and end time of the last not yet commited entry in this table.
+ * @return The times; first: start time, second: end time.
+ */
+ std::pair<CDateTime, CDateTime> GetFirstAndLastUncommitedEPGDate() const;
+
+ /*!
+ * @brief Notify observers when the currently active tag changed.
+ * @return True if the playing tag has changed, false otherwise.
+ */
+ bool CheckPlayingEvent();
+
+ /*!
+ * @brief Convert a genre id and subid to a human readable name.
+ * @param iID The genre ID.
+ * @param iSubID The genre sub ID.
+ * @return A human readable name.
+ */
+ static const std::string& ConvertGenreIdToString(int iID, int iSubID);
+
+ /*!
+ * @brief Check whether this EPG has unsaved data.
+ * @return True if this EPG contains unsaved data, false otherwise.
+ */
+ bool NeedsSave() const;
+
+ /*!
+ * @brief Check whether this EPG is valid.
+ * @return True if this EPG is valid and can be updated, false otherwise.
+ */
+ bool IsValid() const;
+
+ /*!
+ * @brief Query the events available for CEventStream
+ */
+ CEventStream<PVREvent>& Events() { return m_events; }
+
+ /*!
+ * @brief Lock the instance. No other thread gets access to this EPG until Unlock was called.
+ */
+ void Lock() { m_critSection.lock(); }
+
+ /*!
+ * @brief Unlock the instance. Other threads may get access to this EPG again.
+ */
+ void Unlock() { m_critSection.unlock(); }
+
+ /*!
+ * @brief Called to inform the EPG that it has been removed from the EPG container.
+ */
+ void RemovedFromContainer();
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @param database The EPG database
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages(const std::shared_ptr<CPVREpgDatabase>& database);
+
+ private:
+ CPVREpg() = delete;
+ CPVREpg(const CPVREpg&) = delete;
+ CPVREpg& operator =(const CPVREpg&) = delete;
+
+ /*!
+ * @brief Update the EPG from a scraper set in the channel tag.
+ * @todo not implemented yet for non-pvr EPGs
+ * @param start Get entries with a start date after this time.
+ * @param end Get entries with an end date before this time.
+ * @param bForceUpdate Force update from client even if it's not the time to
+ * @return True if the update was successful, false otherwise.
+ */
+ bool UpdateFromScraper(time_t start, time_t end, bool bForceUpdate);
+
+ /*!
+ * @brief Update the contents of this table with the contents provided in "epg"
+ * @param epg The updated contents.
+ * @return True if the update was successful, false otherwise.
+ */
+ bool UpdateEntries(const CPVREpg& epg);
+
+ /*!
+ * @brief Remove all entries from this EPG that finished before the given amount of days.
+ * @param iPastDays Delete entries with an end time before the given amount of days from now on.
+ */
+ void Cleanup(int iPastDays);
+
+ bool m_bChanged = false; /*!< true if anything changed that needs to be persisted, false otherwise */
+ std::atomic<bool> m_bUpdatePending = {false}; /*!< true if manual update is pending */
+ int m_iEpgID = 0; /*!< the database ID of this table */
+ std::string m_strName; /*!< the name of this table */
+ std::string m_strScraperName; /*!< the name of the scraper to use */
+ CDateTime m_lastScanTime; /*!< the last time the EPG has been updated */
+ mutable CCriticalSection m_critSection; /*!< critical section for changes in this table */
+ bool m_bUpdateLastScanTime = false;
+ std::shared_ptr<CPVREpgChannelData> m_channelData;
+ CPVREpgTagsContainer m_tags;
+
+ CEventSource<PVREvent> m_events;
+ };
+}
diff --git a/xbmc/pvr/epg/EpgChannelData.cpp b/xbmc/pvr/epg/EpgChannelData.cpp
new file mode 100644
index 0000000..259b296
--- /dev/null
+++ b/xbmc/pvr/epg/EpgChannelData.cpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgChannelData.h"
+
+#include "XBDateTime.h"
+#include "pvr/channels/PVRChannel.h"
+
+using namespace PVR;
+
+CPVREpgChannelData::CPVREpgChannelData(int iClientId, int iUniqueClientChannelId)
+ : m_iClientId(iClientId), m_iUniqueClientChannelId(iUniqueClientChannelId)
+{
+}
+
+CPVREpgChannelData::CPVREpgChannelData(const CPVRChannel& channel)
+ : m_bIsRadio(channel.IsRadio()),
+ m_iClientId(channel.ClientID()),
+ m_iUniqueClientChannelId(channel.UniqueID()),
+ m_bIsHidden(channel.IsHidden()),
+ m_bIsLocked(channel.IsLocked()),
+ m_bIsEPGEnabled(channel.EPGEnabled()),
+ m_iChannelId(channel.ChannelID()),
+ m_strChannelName(channel.ChannelName()),
+ m_strChannelIconPath(channel.IconPath())
+{
+}
+
+bool CPVREpgChannelData::IsRadio() const
+{
+ return m_bIsRadio;
+}
+
+int CPVREpgChannelData::ClientId() const
+{
+ return m_iClientId;
+}
+
+int CPVREpgChannelData::UniqueClientChannelId() const
+{
+ return m_iUniqueClientChannelId;
+}
+
+bool CPVREpgChannelData::IsHidden() const
+{
+ return m_bIsHidden;
+}
+
+void CPVREpgChannelData::SetHidden(bool bIsHidden)
+{
+ m_bIsHidden = bIsHidden;
+}
+
+bool CPVREpgChannelData::IsLocked() const
+{
+ return m_bIsLocked;
+}
+
+void CPVREpgChannelData::SetLocked(bool bIsLocked)
+{
+ m_bIsLocked = bIsLocked;
+}
+
+bool CPVREpgChannelData::IsEPGEnabled() const
+{
+ return m_bIsEPGEnabled;
+}
+
+void CPVREpgChannelData::SetEPGEnabled(bool bIsEPGEnabled)
+{
+ m_bIsEPGEnabled = bIsEPGEnabled;
+}
+
+int CPVREpgChannelData::ChannelId() const
+{
+ return m_iChannelId;
+}
+
+void CPVREpgChannelData::SetChannelId(int iChannelId)
+{
+ m_iChannelId = iChannelId;
+}
+
+const std::string& CPVREpgChannelData::ChannelName() const
+{
+ return m_strChannelName;
+}
+
+void CPVREpgChannelData::SetChannelName(const std::string& strChannelName)
+{
+ m_strChannelName = strChannelName;
+}
+
+const std::string& CPVREpgChannelData::ChannelIconPath() const
+{
+ return m_strChannelIconPath;
+}
+
+void CPVREpgChannelData::SetChannelIconPath(const std::string& strChannelIconPath)
+{
+ m_strChannelIconPath = strChannelIconPath;
+}
diff --git a/xbmc/pvr/epg/EpgChannelData.h b/xbmc/pvr/epg/EpgChannelData.h
new file mode 100644
index 0000000..a2a63ec
--- /dev/null
+++ b/xbmc/pvr/epg/EpgChannelData.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <ctime>
+#include <string>
+
+namespace PVR
+{
+class CPVRChannel;
+
+class CPVREpgChannelData
+{
+public:
+ CPVREpgChannelData() = default;
+ CPVREpgChannelData(int iClientId, int iUniqueClientChannelId);
+ explicit CPVREpgChannelData(const CPVRChannel& channel);
+
+ int ClientId() const;
+ int UniqueClientChannelId() const;
+ bool IsRadio() const;
+
+ bool IsHidden() const;
+ void SetHidden(bool bIsHidden);
+
+ bool IsLocked() const;
+ void SetLocked(bool bIsLocked);
+
+ bool IsEPGEnabled() const;
+ void SetEPGEnabled(bool bIsEPGEnabled);
+
+ int ChannelId() const;
+ void SetChannelId(int iChannelId);
+
+ const std::string& ChannelName() const;
+ void SetChannelName(const std::string& strChannelName);
+
+ const std::string& ChannelIconPath() const;
+ void SetChannelIconPath(const std::string& strChannelIconPath);
+
+private:
+ const bool m_bIsRadio = false;
+ const int m_iClientId = -1;
+ const int m_iUniqueClientChannelId = -1;
+
+ bool m_bIsHidden = false;
+ bool m_bIsLocked = false;
+ bool m_bIsEPGEnabled = true;
+ int m_iChannelId = -1;
+ std::string m_strChannelName;
+ std::string m_strChannelIconPath;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgContainer.cpp b/xbmc/pvr/epg/EpgContainer.cpp
new file mode 100644
index 0000000..d6f2015
--- /dev/null
+++ b/xbmc/pvr/epg/EpgContainer.cpp
@@ -0,0 +1,1028 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgContainer.h"
+
+#include "ServiceBroker.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h" // PVR_CHANNEL_INVALID_UID
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIProgressHandler.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SystemClock.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <utility>
+#include <vector>
+
+using namespace std::chrono_literals;
+
+namespace PVR
+{
+
+class CEpgUpdateRequest
+{
+public:
+ CEpgUpdateRequest() : CEpgUpdateRequest(-1, PVR_CHANNEL_INVALID_UID) {}
+ CEpgUpdateRequest(int iClientID, int iUniqueChannelID) : m_iClientID(iClientID), m_iUniqueChannelID(iUniqueChannelID) {}
+
+ void Deliver();
+
+private:
+ int m_iClientID;
+ int m_iUniqueChannelID;
+};
+
+void CEpgUpdateRequest::Deliver()
+{
+ const std::shared_ptr<CPVREpg> epg = CServiceBroker::GetPVRManager().EpgContainer().GetByChannelUid(m_iClientID, m_iUniqueChannelID);
+ if (!epg)
+ {
+ CLog::LogF(LOGERROR,
+ "Unable to obtain EPG for client {} and channel {}! Unable to deliver the epg "
+ "update request!",
+ m_iClientID, m_iUniqueChannelID);
+ return;
+ }
+
+ epg->ForceUpdate();
+}
+
+class CEpgTagStateChange
+{
+public:
+ CEpgTagStateChange() = default;
+ CEpgTagStateChange(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE eNewState) : m_epgtag(tag), m_state(eNewState) {}
+
+ void Deliver();
+
+private:
+ std::shared_ptr<CPVREpgInfoTag> m_epgtag;
+ EPG_EVENT_STATE m_state = EPG_EVENT_CREATED;
+};
+
+void CEpgTagStateChange::Deliver()
+{
+ const CPVREpgContainer& epgContainer = CServiceBroker::GetPVRManager().EpgContainer();
+
+ const std::shared_ptr<CPVREpg> epg = epgContainer.GetByChannelUid(m_epgtag->ClientID(), m_epgtag->UniqueChannelID());
+ if (!epg)
+ {
+ CLog::LogF(LOGERROR,
+ "Unable to obtain EPG for client {} and channel {}! Unable to deliver state change "
+ "for tag '{}'!",
+ m_epgtag->ClientID(), m_epgtag->UniqueChannelID(), m_epgtag->UniqueBroadcastID());
+ return;
+ }
+
+ if (m_epgtag->EpgID() < 0)
+ {
+ // now that we have the epg instance, fully initialize the tag
+ m_epgtag->SetEpgID(epg->EpgID());
+ m_epgtag->SetChannelData(epg->GetChannelData());
+ }
+
+ epg->UpdateEntry(m_epgtag, m_state);
+}
+
+CPVREpgContainer::CPVREpgContainer(CEventSource<PVREvent>& eventSource)
+ : CThread("EPGUpdater"),
+ m_database(new CPVREpgDatabase),
+ m_settings({CSettings::SETTING_EPG_EPGUPDATE, CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY,
+ CSettings::SETTING_EPG_PAST_DAYSTODISPLAY,
+ CSettings::SETTING_EPG_PREVENTUPDATESWHILEPLAYINGTV}),
+ m_events(eventSource)
+{
+ m_bStop = true; // base class member
+ m_updateEvent.Reset();
+}
+
+CPVREpgContainer::~CPVREpgContainer()
+{
+ Stop();
+ Unload();
+}
+
+std::shared_ptr<CPVREpgDatabase> CPVREpgContainer::GetEpgDatabase() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_database->IsOpen())
+ m_database->Open();
+
+ return m_database;
+}
+
+bool CPVREpgContainer::IsStarted() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bStarted;
+}
+
+int CPVREpgContainer::NextEpgId()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return ++m_iNextEpgId;
+}
+
+void CPVREpgContainer::Start()
+{
+ Stop();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bIsInitialising = true;
+
+ Create();
+ SetPriority(ThreadPriority::BELOW_NORMAL);
+
+ m_bStarted = true;
+ }
+}
+
+void CPVREpgContainer::Stop()
+{
+ StopThread();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bStarted = false;
+ }
+}
+
+bool CPVREpgContainer::Load()
+{
+ // EPGs must be loaded via PVR Manager -> channel groups -> EPG container to associate the
+ // channels with the right EPG.
+ CServiceBroker::GetPVRManager().TriggerEpgsCreate();
+ return true;
+}
+
+void CPVREpgContainer::Unload()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_updateRequestsLock);
+ m_updateRequests.clear();
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_epgTagChangesLock);
+ m_epgTagChanges.clear();
+ }
+
+ std::vector<std::shared_ptr<CPVREpg>> epgs;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* clear all epg tables and remove pointers to epg tables on channels */
+ std::transform(m_epgIdToEpgMap.cbegin(), m_epgIdToEpgMap.cend(), std::back_inserter(epgs),
+ [](const auto& epgEntry) { return epgEntry.second; });
+
+ m_epgIdToEpgMap.clear();
+ m_channelUidToEpgMap.clear();
+
+ m_iNextEpgUpdate = 0;
+ m_iNextEpgId = 0;
+ m_iNextEpgActiveTagCheck = 0;
+ m_bUpdateNotificationPending = false;
+ m_bLoaded = false;
+
+ m_database->Close();
+ }
+
+ for (const auto& epg : epgs)
+ {
+ epg->Events().Unsubscribe(this);
+ epg->RemovedFromContainer();
+ }
+}
+
+void CPVREpgContainer::Notify(const PVREvent& event)
+{
+ if (event == PVREvent::EpgItemUpdate)
+ {
+ // there can be many of these notifications during short time period. Thus, announce async and not every event.
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bUpdateNotificationPending = true;
+ return;
+ }
+ else if (event == PVREvent::EpgUpdatePending)
+ {
+ SetHasPendingUpdates(true);
+ return;
+ }
+ else if (event == PVREvent::EpgActiveItem)
+ {
+ // No need to propagate the change. See CPVREpgContainer::CheckPlayingEvents
+ return;
+ }
+
+ m_events.Publish(event);
+}
+
+void CPVREpgContainer::LoadFromDatabase()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bLoaded)
+ return;
+
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ database->Lock();
+ m_iNextEpgId = database->GetLastEPGId();
+ const std::vector<std::shared_ptr<CPVREpg>> result = database->GetAll();
+ database->Unlock();
+
+ for (const auto& entry : result)
+ InsertFromDB(entry);
+
+ m_bLoaded = true;
+}
+
+bool CPVREpgContainer::PersistAll(unsigned int iMaxTimeslice) const
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return false;
+ }
+
+ std::vector<std::shared_ptr<CPVREpg>> changedEpgs;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& epg : m_epgIdToEpgMap)
+ {
+ if (epg.second && epg.second->NeedsSave())
+ {
+ // Note: We need to obtain a lock for every epg instance before we can lock
+ // the epg db. This order is important. Otherwise deadlocks may occur.
+ epg.second->Lock();
+ changedEpgs.emplace_back(epg.second);
+ }
+ }
+ }
+
+ bool bReturn = true;
+
+ if (!changedEpgs.empty())
+ {
+ // Note: We must lock the db the whole time, otherwise races may occur.
+ database->Lock();
+
+ XbmcThreads::EndTime<> processTimeslice{std::chrono::milliseconds(iMaxTimeslice)};
+ for (const auto& epg : changedEpgs)
+ {
+ if (!processTimeslice.IsTimePast())
+ {
+ CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Container: Persisting events for channel '{}'...",
+ epg->GetChannelData()->ChannelName());
+
+ bReturn &= epg->QueuePersistQuery(database);
+
+ size_t queryCount = database->GetInsertQueriesCount() + database->GetDeleteQueriesCount();
+ if (queryCount > EPG_COMMIT_QUERY_COUNT_LIMIT)
+ {
+ CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Container: committing {} queries in loop.",
+ queryCount);
+ database->CommitDeleteQueries();
+ database->CommitInsertQueries();
+ CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Container: committed {} queries in loop.", queryCount);
+ }
+ }
+
+ epg->Unlock();
+ }
+
+ if (bReturn)
+ {
+ database->CommitDeleteQueries();
+ database->CommitInsertQueries();
+ }
+
+ database->Unlock();
+ }
+
+ return bReturn;
+}
+
+void CPVREpgContainer::Process()
+{
+ time_t iNow = 0;
+ time_t iLastSave = 0;
+
+ SetPriority(ThreadPriority::LOWEST);
+
+ while (!m_bStop)
+ {
+ time_t iLastEpgCleanup = 0;
+ bool bUpdateEpg = true;
+
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(iNow);
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bUpdateEpg = (iNow >= m_iNextEpgUpdate) && !m_bSuspended;
+ iLastEpgCleanup = m_iLastEpgCleanup;
+ }
+
+ /* update the EPG */
+ if (!InterruptUpdate() && bUpdateEpg && CServiceBroker::GetPVRManager().EpgsCreated() && UpdateEPG())
+ m_bIsInitialising = false;
+
+ /* clean up old entries */
+ if (!m_bStop && !m_bSuspended &&
+ iNow >= iLastEpgCleanup + CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iEpgCleanupInterval)
+ RemoveOldEntries();
+
+ /* check for pending manual EPG updates */
+
+ while (!m_bStop && !m_bSuspended)
+ {
+ CEpgUpdateRequest request;
+ {
+ std::unique_lock<CCriticalSection> lock(m_updateRequestsLock);
+ if (m_updateRequests.empty())
+ break;
+
+ request = m_updateRequests.front();
+ m_updateRequests.pop_front();
+ }
+
+ // do the update
+ request.Deliver();
+ }
+
+ /* check for pending EPG tag changes */
+
+ // during Kodi startup, addons may push updates very early, even before EPGs are ready to use.
+ if (!m_bStop && !m_bSuspended && CServiceBroker::GetPVRManager().EpgsCreated())
+ {
+ unsigned int iProcessed = 0;
+ XbmcThreads::EndTime<> processTimeslice(
+ 1000ms); // max 1 sec per cycle, regardless of how many events are in the queue
+
+ while (!InterruptUpdate())
+ {
+ CEpgTagStateChange change;
+ {
+ std::unique_lock<CCriticalSection> lock(m_epgTagChangesLock);
+ if (processTimeslice.IsTimePast() || m_epgTagChanges.empty())
+ {
+ if (iProcessed > 0)
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Processed {} queued epg event changes.", iProcessed);
+
+ break;
+ }
+
+ change = m_epgTagChanges.front();
+ m_epgTagChanges.pop_front();
+ }
+
+ iProcessed++;
+
+ // deliver the updated tag to the respective epg
+ change.Deliver();
+ }
+ }
+
+ if (!m_bStop && !m_bSuspended)
+ {
+ bool bHasPendingUpdates = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bHasPendingUpdates = (m_pendingUpdates > 0);
+ }
+
+ if (bHasPendingUpdates)
+ UpdateEPG(true);
+ }
+
+ /* check for updated active tag */
+ if (!m_bStop)
+ CheckPlayingEvents();
+
+ /* check for pending update notifications */
+ if (!m_bStop)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bUpdateNotificationPending)
+ {
+ m_bUpdateNotificationPending = false;
+ m_events.Publish(PVREvent::Epg);
+ }
+ }
+
+ /* check for changes that need to be saved every 60 seconds */
+ if ((iNow - iLastSave > 60) && !InterruptUpdate())
+ {
+ PersistAll(1000);
+ iLastSave = iNow;
+ }
+
+ CThread::Sleep(1000ms);
+ }
+
+ // store data on exit
+ CLog::Log(LOGINFO, "EPG Container: Persisting unsaved events...");
+ PersistAll(std::numeric_limits<unsigned int>::max());
+ CLog::Log(LOGINFO, "EPG Container: Persisting events done");
+}
+
+std::vector<std::shared_ptr<CPVREpg>> CPVREpgContainer::GetAllEpgs() const
+{
+ std::vector<std::shared_ptr<CPVREpg>> epgs;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::transform(m_epgIdToEpgMap.cbegin(), m_epgIdToEpgMap.cend(), std::back_inserter(epgs),
+ [](const auto& epgEntry) { return epgEntry.second; });
+
+ return epgs;
+}
+
+std::shared_ptr<CPVREpg> CPVREpgContainer::GetById(int iEpgId) const
+{
+ std::shared_ptr<CPVREpg> retval;
+
+ if (iEpgId < 0)
+ return retval;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto& epgEntry = m_epgIdToEpgMap.find(iEpgId);
+ if (epgEntry != m_epgIdToEpgMap.end())
+ retval = epgEntry->second;
+
+ return retval;
+}
+
+std::shared_ptr<CPVREpg> CPVREpgContainer::GetByChannelUid(int iClientId, int iChannelUid) const
+{
+ std::shared_ptr<CPVREpg> epg;
+
+ if (iClientId < 0 || iChannelUid < 0)
+ return epg;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto& epgEntry = m_channelUidToEpgMap.find(std::pair<int, int>(iClientId, iChannelUid));
+ if (epgEntry != m_channelUidToEpgMap.end())
+ epg = epgEntry->second;
+
+ return epg;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgContainer::GetTagById(const std::shared_ptr<CPVREpg>& epg, unsigned int iBroadcastId) const
+{
+ std::shared_ptr<CPVREpgInfoTag> retval;
+
+ if (iBroadcastId == EPG_TAG_INVALID_UID)
+ return retval;
+
+ if (epg)
+ {
+ retval = epg->GetTagByBroadcastId(iBroadcastId);
+ }
+
+ return retval;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgContainer::GetTagByDatabaseId(int iDatabaseId) const
+{
+ std::shared_ptr<CPVREpgInfoTag> retval;
+
+ if (iDatabaseId <= 0)
+ return retval;
+
+ m_critSection.lock();
+ const auto epgs = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ for (const auto& epgEntry : epgs)
+ {
+ retval = epgEntry.second->GetTagByDatabaseId(iDatabaseId);
+ if (retval)
+ break;
+ }
+
+ return retval;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgContainer::GetTags(
+ const PVREpgSearchData& searchData) const
+{
+ // make sure we have up-to-date data in the database.
+ PersistAll(std::numeric_limits<unsigned int>::max());
+
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> results = database->GetEpgTags(searchData);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tag : results)
+ {
+ const auto& it = m_epgIdToEpgMap.find(tag->EpgID());
+ if (it != m_epgIdToEpgMap.cend())
+ tag->SetChannelData((*it).second->GetChannelData());
+ }
+
+ return results;
+}
+
+void CPVREpgContainer::InsertFromDB(const std::shared_ptr<CPVREpg>& newEpg)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // table might already have been created when pvr channels were loaded
+ std::shared_ptr<CPVREpg> epg = GetById(newEpg->EpgID());
+ if (!epg)
+ {
+ // create a new epg table
+ epg = newEpg;
+ m_epgIdToEpgMap.insert({epg->EpgID(), epg});
+ epg->Events().Subscribe(this, &CPVREpgContainer::Notify);
+ }
+}
+
+std::shared_ptr<CPVREpg> CPVREpgContainer::CreateChannelEpg(int iEpgId, const std::string& strScraperName, const std::shared_ptr<CPVREpgChannelData>& channelData)
+{
+ std::shared_ptr<CPVREpg> epg;
+
+ WaitForUpdateFinish();
+ LoadFromDatabase();
+
+ if (iEpgId > 0)
+ epg = GetById(iEpgId);
+
+ if (!epg)
+ {
+ if (iEpgId <= 0)
+ iEpgId = NextEpgId();
+
+ epg.reset(new CPVREpg(iEpgId, channelData->ChannelName(), strScraperName, channelData,
+ GetEpgDatabase()));
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_epgIdToEpgMap.insert({iEpgId, epg});
+ m_channelUidToEpgMap.insert({{channelData->ClientId(), channelData->UniqueClientChannelId()}, epg});
+ epg->Events().Subscribe(this, &CPVREpgContainer::Notify);
+ }
+ else if (epg->ChannelID() == -1)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channelUidToEpgMap.insert({{channelData->ClientId(), channelData->UniqueClientChannelId()}, epg});
+ epg->SetChannelData(channelData);
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bPreventUpdates = false;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(m_iNextEpgUpdate);
+ }
+
+ m_events.Publish(PVREvent::EpgContainer);
+
+ return epg;
+}
+
+bool CPVREpgContainer::RemoveOldEntries()
+{
+ const CDateTime cleanupTime(CDateTime::GetUTCDateTime() - CDateTimeSpan(GetPastDaysToDisplay(), 0, 0, 0));
+
+ m_critSection.lock();
+ const auto epgs = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ for (const auto& epgEntry : epgs)
+ epgEntry.second->Cleanup(cleanupTime);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(m_iLastEpgCleanup);
+
+ return true;
+}
+
+bool CPVREpgContainer::QueueDeleteEpgs(const std::vector<std::shared_ptr<CPVREpg>>& epgs)
+{
+ if (epgs.empty())
+ return true;
+
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return false;
+ }
+
+ for (const auto& epg : epgs)
+ {
+ // Note: We need to obtain a lock for every epg instance before we can lock
+ // the epg db. This order is important. Otherwise deadlocks may occur.
+ epg->Lock();
+ }
+
+ database->Lock();
+ for (const auto& epg : epgs)
+ {
+ QueueDeleteEpg(epg, database);
+ epg->Unlock();
+
+ size_t queryCount = database->GetDeleteQueriesCount();
+ if (queryCount > EPG_COMMIT_QUERY_COUNT_LIMIT)
+ database->CommitDeleteQueries();
+ }
+ database->CommitDeleteQueries();
+ database->Unlock();
+
+ return true;
+}
+
+bool CPVREpgContainer::QueueDeleteEpg(const std::shared_ptr<CPVREpg>& epg,
+ const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ if (!epg || epg->EpgID() < 0)
+ return false;
+
+ std::shared_ptr<CPVREpg> epgToDelete;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const auto& epgEntry = m_epgIdToEpgMap.find(epg->EpgID());
+ if (epgEntry == m_epgIdToEpgMap.end())
+ return false;
+
+ const auto& epgEntry1 = m_channelUidToEpgMap.find(std::make_pair(
+ epg->GetChannelData()->ClientId(), epg->GetChannelData()->UniqueClientChannelId()));
+ if (epgEntry1 != m_channelUidToEpgMap.end())
+ m_channelUidToEpgMap.erase(epgEntry1);
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Deleting EPG table {} ({})", epg->Name(), epg->EpgID());
+ epgEntry->second->QueueDeleteQueries(database);
+
+ epgToDelete = epgEntry->second;
+ m_epgIdToEpgMap.erase(epgEntry);
+ }
+
+ epgToDelete->Events().Unsubscribe(this);
+ epgToDelete->RemovedFromContainer();
+ return true;
+}
+
+bool CPVREpgContainer::InterruptUpdate() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bStop ||
+ m_bPreventUpdates ||
+ (m_bPlaying && m_settings.GetBoolValue(CSettings::SETTING_EPG_PREVENTUPDATESWHILEPLAYINGTV));
+}
+
+void CPVREpgContainer::WaitForUpdateFinish()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bPreventUpdates = true;
+
+ if (!m_bIsUpdating)
+ return;
+
+ m_updateEvent.Reset();
+ }
+
+ m_updateEvent.Wait();
+}
+
+bool CPVREpgContainer::UpdateEPG(bool bOnlyPending /* = false */)
+{
+ bool bInterrupted = false;
+ unsigned int iUpdatedTables = 0;
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ /* set start and end time */
+ time_t start;
+ time_t end;
+ CDateTime::GetUTCDateTime().GetAsTime(start);
+ end = start + GetFutureDaysToDisplay() * 24 * 60 * 60;
+ start -= GetPastDaysToDisplay() * 24 * 60 * 60;
+
+ bool bShowProgress = (m_bIsInitialising || advancedSettings->m_bEpgDisplayIncrementalUpdatePopup) &&
+ advancedSettings->m_bEpgDisplayUpdatePopup;
+ int pendingUpdates = 0;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bIsUpdating || InterruptUpdate())
+ return false;
+
+ m_bIsUpdating = true;
+ pendingUpdates = m_pendingUpdates;
+ }
+
+ std::vector<std::shared_ptr<CPVREpg>> invalidTables;
+
+ unsigned int iCounter = 0;
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+
+ m_critSection.lock();
+ const auto epgsToUpdate = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ std::unique_ptr<CPVRGUIProgressHandler> progressHandler;
+ if (bShowProgress && !bOnlyPending && !epgsToUpdate.empty())
+ progressHandler.reset(
+ new CPVRGUIProgressHandler(g_localizeStrings.Get(19004))); // Loading programme guide
+
+ for (const auto& epgEntry : epgsToUpdate)
+ {
+ if (InterruptUpdate())
+ {
+ bInterrupted = true;
+ break;
+ }
+
+ const std::shared_ptr<CPVREpg> epg = epgEntry.second;
+ if (!epg)
+ continue;
+
+ if (progressHandler)
+ progressHandler->UpdateProgress(epg->GetChannelData()->ChannelName(), ++iCounter,
+ epgsToUpdate.size());
+
+ if ((!bOnlyPending || epg->UpdatePending()) &&
+ epg->Update(start,
+ end,
+ m_settings.GetIntValue(CSettings::SETTING_EPG_EPGUPDATE) * 60,
+ m_settings.GetIntValue(CSettings::SETTING_EPG_PAST_DAYSTODISPLAY),
+ database,
+ bOnlyPending))
+ {
+ iUpdatedTables++;
+ }
+ else if (!epg->IsValid())
+ {
+ invalidTables.push_back(epg);
+ }
+ }
+
+ progressHandler.reset();
+
+ QueueDeleteEpgs(invalidTables);
+
+ if (bInterrupted)
+ {
+ /* the update has been interrupted. try again later */
+ time_t iNow;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(iNow);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iNextEpgUpdate = iNow + advancedSettings->m_iEpgRetryInterruptedUpdateInterval;
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(m_iNextEpgUpdate);
+ m_iNextEpgUpdate += advancedSettings->m_iEpgUpdateCheckInterval;
+ if (m_pendingUpdates == pendingUpdates)
+ m_pendingUpdates = 0;
+ }
+
+ if (iUpdatedTables > 0)
+ m_events.Publish(PVREvent::EpgContainer);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bIsUpdating = false;
+ m_updateEvent.Set();
+
+ return !bInterrupted;
+}
+
+std::pair<CDateTime, CDateTime> CPVREpgContainer::GetFirstAndLastEPGDate() const
+{
+ // Get values from db
+ std::pair<CDateTime, CDateTime> dbDates;
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (database)
+ dbDates = database->GetFirstAndLastEPGDate();
+
+ // Merge not yet commited changes
+ m_critSection.lock();
+ const auto epgs = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ CDateTime first(dbDates.first);
+ CDateTime last(dbDates.second);
+
+ for (const auto& epgEntry : epgs)
+ {
+ const auto dates = epgEntry.second->GetFirstAndLastUncommitedEPGDate();
+
+ if (dates.first.IsValid() && (!first.IsValid() || dates.first < first))
+ first = dates.first;
+
+ if (dates.second.IsValid() && (!last.IsValid() || dates.second > last))
+ last = dates.second;
+ }
+
+ return {first, last};
+}
+
+bool CPVREpgContainer::CheckPlayingEvents()
+{
+ bool bReturn = false;
+ bool bFoundChanges = false;
+
+ m_critSection.lock();
+ const auto epgs = m_epgIdToEpgMap;
+ time_t iNextEpgActiveTagCheck = m_iNextEpgActiveTagCheck;
+ m_critSection.unlock();
+
+ time_t iNow;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(iNow);
+ if (iNow >= iNextEpgActiveTagCheck)
+ {
+ bFoundChanges = std::accumulate(epgs.cbegin(), epgs.cend(), bFoundChanges,
+ [](bool found, const auto& epgEntry) {
+ return epgEntry.second->CheckPlayingEvent() ? true : found;
+ });
+
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(iNextEpgActiveTagCheck);
+ iNextEpgActiveTagCheck += CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iEpgActiveTagCheckInterval;
+
+ /* pvr tags always start on the full minute */
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ iNextEpgActiveTagCheck -= iNextEpgActiveTagCheck % 60;
+
+ bReturn = true;
+ }
+
+ if (bReturn)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iNextEpgActiveTagCheck = iNextEpgActiveTagCheck;
+ }
+
+ if (bFoundChanges)
+ m_events.Publish(PVREvent::EpgActiveItem);
+
+ return bReturn;
+}
+
+void CPVREpgContainer::SetHasPendingUpdates(bool bHasPendingUpdates /* = true */)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (bHasPendingUpdates)
+ m_pendingUpdates++;
+ else
+ m_pendingUpdates = 0;
+}
+
+void CPVREpgContainer::UpdateRequest(int iClientID, int iUniqueChannelID)
+{
+ std::unique_lock<CCriticalSection> lock(m_updateRequestsLock);
+ m_updateRequests.emplace_back(CEpgUpdateRequest(iClientID, iUniqueChannelID));
+}
+
+void CPVREpgContainer::UpdateFromClient(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE eNewState)
+{
+ std::unique_lock<CCriticalSection> lock(m_epgTagChangesLock);
+ m_epgTagChanges.emplace_back(CEpgTagStateChange(tag, eNewState));
+}
+
+int CPVREpgContainer::GetPastDaysToDisplay() const
+{
+ return m_settings.GetIntValue(CSettings::SETTING_EPG_PAST_DAYSTODISPLAY);
+}
+
+int CPVREpgContainer::GetFutureDaysToDisplay() const
+{
+ return m_settings.GetIntValue(CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY);
+}
+
+void CPVREpgContainer::OnPlaybackStarted()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bPlaying = true;
+}
+
+void CPVREpgContainer::OnPlaybackStopped()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bPlaying = false;
+}
+
+void CPVREpgContainer::OnSystemSleep()
+{
+ m_bSuspended = true;
+}
+
+void CPVREpgContainer::OnSystemWake()
+{
+ m_bSuspended = false;
+}
+
+int CPVREpgContainer::CleanupCachedImages()
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return 0;
+ }
+
+ // Processing can take some time. Do not block.
+ m_critSection.lock();
+ const std::map<int, std::shared_ptr<CPVREpg>> epgIdToEpgMap = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ return std::accumulate(epgIdToEpgMap.cbegin(), epgIdToEpgMap.cend(), 0,
+ [&database](int cleanedImages, const auto& epg) {
+ return cleanedImages + epg.second->CleanupCachedImages(database);
+ });
+}
+
+std::vector<std::shared_ptr<CPVREpgSearchFilter>> CPVREpgContainer::GetSavedSearches(bool bRadio)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ return database->GetSavedSearches(bRadio);
+}
+
+std::shared_ptr<CPVREpgSearchFilter> CPVREpgContainer::GetSavedSearchById(bool bRadio, int iId)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ return database->GetSavedSearchById(bRadio, iId);
+}
+
+bool CPVREpgContainer::PersistSavedSearch(CPVREpgSearchFilter& search)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ if (database->Persist(search))
+ {
+ m_events.Publish(PVREvent::SavedSearchesInvalidated);
+ return true;
+ }
+ return false;
+}
+
+bool CPVREpgContainer::UpdateSavedSearchLastExecuted(const CPVREpgSearchFilter& epgSearch)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ return database->UpdateSavedSearchLastExecuted(epgSearch);
+}
+
+bool CPVREpgContainer::DeleteSavedSearch(const CPVREpgSearchFilter& search)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ if (database->Delete(search))
+ {
+ m_events.Publish(PVREvent::SavedSearchesInvalidated);
+ return true;
+ }
+ return false;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgContainer.h b/xbmc/pvr/epg/EpgContainer.h
new file mode 100644
index 0000000..68cf50d
--- /dev/null
+++ b/xbmc/pvr/epg/EpgContainer.h
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h"
+#include "pvr/settings/PVRSettings.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "utils/EventStream.h"
+
+#include <atomic>
+#include <list>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CDateTime;
+
+namespace PVR
+{
+ class CEpgUpdateRequest;
+ class CEpgTagStateChange;
+ class CPVREpg;
+ class CPVREpgChannelData;
+ class CPVREpgDatabase;
+ class CPVREpgInfoTag;
+ class CPVREpgSearchFilter;
+
+ enum class PVREvent;
+
+ struct PVREpgSearchData;
+
+ class CPVREpgContainer : private CThread
+ {
+ friend class CPVREpgDatabase;
+
+ public:
+ CPVREpgContainer() = delete;
+
+ /*!
+ * @brief Create a new EPG table container.
+ */
+ explicit CPVREpgContainer(CEventSource<PVREvent>& eventSource);
+
+ /*!
+ * @brief Destroy this instance.
+ */
+ ~CPVREpgContainer() override;
+
+ /*!
+ * @brief Get a pointer to the database instance.
+ * @return A pointer to the database instance.
+ */
+ std::shared_ptr<CPVREpgDatabase> GetEpgDatabase() const;
+
+ /*!
+ * @brief Start the EPG update thread.
+ */
+ void Start();
+
+ /*!
+ * @brief Stop the EPG update thread.
+ */
+ void Stop();
+
+ /**
+ * @brief (re)load EPG data.
+ * @return True if loaded successfully, false otherwise.
+ */
+ bool Load();
+
+ /**
+ * @brief unload all EPG data.
+ */
+ void Unload();
+
+ /*!
+ * @brief Check whether the EpgContainer has fully started.
+ * @return True if started, false otherwise.
+ */
+ bool IsStarted() const;
+
+ /*!
+ * @brief Queue the deletion of the given EPG tables from this container.
+ * @param epg The tables to delete.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteEpgs(const std::vector<std::shared_ptr<CPVREpg>>& epgs);
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+
+ /*!
+ * @brief Create the EPg for a given channel.
+ * @param iEpgId The EPG id.
+ * @param strScraperName The scraper name.
+ * @param channelData The channel data.
+ * @return the created EPG
+ */
+ std::shared_ptr<CPVREpg> CreateChannelEpg(int iEpgId, const std::string& strScraperName, const std::shared_ptr<CPVREpgChannelData>& channelData);
+
+ /*!
+ * @brief Get the start and end time across all EPGs.
+ * @return The times; first: start time, second: end time.
+ */
+ std::pair<CDateTime, CDateTime> GetFirstAndLastEPGDate() const;
+
+ /*!
+ * @brief Get all EPGs.
+ * @return The EPGs.
+ */
+ std::vector<std::shared_ptr<CPVREpg>> GetAllEpgs() const;
+
+ /*!
+ * @brief Get an EPG given its ID.
+ * @param iEpgId The database ID of the table.
+ * @return The EPG or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVREpg> GetById(int iEpgId) const;
+
+ /*!
+ * @brief Get an EPG given its client id and channel uid.
+ * @param iClientId the id of the pvr client providing the EPG
+ * @param iChannelUid the uid of the channel for the EPG
+ * @return The EPG or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVREpg> GetByChannelUid(int iClientId, int iChannelUid) const;
+
+ /*!
+ * @brief Get the EPG event with the given event id
+ * @param epg The epg to lookup the event.
+ * @param iBroadcastId The event id to lookup.
+ * @return The requested event, or an empty tag when not found
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagById(const std::shared_ptr<CPVREpg>& epg, unsigned int iBroadcastId) const;
+
+ /*!
+ * @brief Get the EPG event with the given database id
+ * @param iDatabaseId The id to lookup.
+ * @return The requested event, or an empty tag when not found
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagByDatabaseId(int iDatabaseId) const;
+
+ /*!
+ * @brief Get all EPG tags matching the given search criteria.
+ * @param searchData The search criteria.
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetTags(const PVREpgSearchData& searchData) const;
+
+ /*!
+ * @brief Notify EPG container that there are pending manual EPG updates
+ * @param bHasPendingUpdates The new value
+ */
+ void SetHasPendingUpdates(bool bHasPendingUpdates = true);
+
+ /*!
+ * @brief A client triggered an epg update request for a channel
+ * @param iClientID The id of the client which triggered the update request
+ * @param iUniqueChannelID The uid of the channel for which the epg shall be updated
+ */
+ void UpdateRequest(int iClientID, int iUniqueChannelID);
+
+ /*!
+ * @brief A client announced an updated epg tag for a channel
+ * @param tag The epg tag containing the updated data
+ * @param eNewState The kind of change (CREATED, UPDATED, DELETED)
+ */
+ void UpdateFromClient(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE eNewState);
+
+ /*!
+ * @brief Get the number of past days to show in the guide and to import from backends.
+ * @return the number of past epg days.
+ */
+ int GetPastDaysToDisplay() const;
+
+ /*!
+ * @brief Get the number of future days to show in the guide and to import from backends.
+ * @return the number of future epg days.
+ */
+ int GetFutureDaysToDisplay() const;
+
+ /*!
+ * @brief Inform the epg container that playback of an item just started.
+ */
+ void OnPlaybackStarted();
+
+ /*!
+ * @brief Inform the epg container that playback of an item was stopped due to user interaction.
+ */
+ void OnPlaybackStopped();
+
+ /*!
+ * @brief Inform the epg container that the system is going to sleep
+ */
+ void OnSystemSleep();
+
+ /*!
+ * @brief Inform the epg container that the system gets awake from sleep
+ */
+ void OnSystemWake();
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+ /*!
+ * @brief Get all saved searches from the database.
+ * @param bRadio Whether to fetch saved searches for radio or TV.
+ * @return The searches.
+ */
+ std::vector<std::shared_ptr<CPVREpgSearchFilter>> GetSavedSearches(bool bRadio);
+
+ /*!
+ * @brief Get the saved search matching the given id.
+ * @param bRadio Whether to fetch a TV or radio saved search.
+ * @param iId The id.
+ * @return The saved search or nullptr if not found.
+ */
+ std::shared_ptr<CPVREpgSearchFilter> GetSavedSearchById(bool bRadio, int iId);
+
+ /*!
+ * @brief Persist a saved search in the database.
+ * @param search The saved search.
+ * @return True on success, false otherwise.
+ */
+ bool PersistSavedSearch(CPVREpgSearchFilter& search);
+
+ /*!
+ * @brief Update time last executed for the given search.
+ * @param epgSearch The search.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateSavedSearchLastExecuted(const CPVREpgSearchFilter& epgSearch);
+
+ /*!
+ * @brief Delete a saved search from the database.
+ * @param search The saved search.
+ * @return True on success, false otherwise.
+ */
+ bool DeleteSavedSearch(const CPVREpgSearchFilter& search);
+
+ private:
+ /*!
+ * @brief Notify EPG table observers when the currently active tag changed.
+ * @return True if the check was done, false if it was not the right time to check
+ */
+ bool CheckPlayingEvents();
+
+ /*!
+ * @brief The next EPG ID to be given to a table when the db isn't being used.
+ * @return The next ID.
+ */
+ int NextEpgId();
+
+ /*!
+ * @brief Wait for an EPG update to finish.
+ */
+ void WaitForUpdateFinish();
+
+ /*!
+ * @brief Call Persist() on each table
+ * @param iMaxTimeslice time in milliseconds for max processing. Return after this time
+ * even if not all data was persisted, unless value is -1
+ * @return True when they all were persisted, false otherwise.
+ */
+ bool PersistAll(unsigned int iMaxTimeslice) const;
+
+ /*!
+ * @brief Remove old EPG entries.
+ * @return True if the old entries were removed successfully, false otherwise.
+ */
+ bool RemoveOldEntries();
+
+ /*!
+ * @brief Load and update the EPG data.
+ * @param bOnlyPending Only check and update EPG tables with pending manual updates
+ * @return True if the update has not been interrupted, false otherwise.
+ */
+ bool UpdateEPG(bool bOnlyPending = false);
+
+ /*!
+ * @brief Check whether a running update should be interrupted.
+ * @return True if a running update should be interrupted, false otherwise.
+ */
+ bool InterruptUpdate() const;
+
+ /*!
+ * @brief EPG update thread
+ */
+ void Process() override;
+
+ /*!
+ * @brief Load all tables from the database
+ */
+ void LoadFromDatabase();
+
+ /*!
+ * @brief Insert data from database
+ * @param newEpg the EPG containing the updated data.
+ */
+ void InsertFromDB(const std::shared_ptr<CPVREpg>& newEpg);
+
+ /*!
+ * @brief Queue the deletion of an EPG table from this container.
+ * @param epg The table to delete.
+ * @param database The database containing the epg data.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteEpg(const std::shared_ptr<CPVREpg>& epg,
+ const std::shared_ptr<CPVREpgDatabase>& database);
+
+ std::shared_ptr<CPVREpgDatabase> m_database; /*!< the EPG database */
+
+ bool m_bIsUpdating = false; /*!< true while an update is running */
+ std::atomic<bool> m_bIsInitialising = {
+ true}; /*!< true while the epg manager hasn't loaded all tables */
+ bool m_bStarted = false; /*!< true if EpgContainer has fully started */
+ bool m_bLoaded = false; /*!< true after epg data is initially loaded from the database */
+ bool m_bPreventUpdates = false; /*!< true to prevent EPG updates */
+ bool m_bPlaying = false; /*!< true if Kodi is currently playing something */
+ int m_pendingUpdates = 0; /*!< count of pending manual updates */
+ time_t m_iLastEpgCleanup = 0; /*!< the time the EPG was cleaned up */
+ time_t m_iNextEpgUpdate = 0; /*!< the time the EPG will be updated */
+ time_t m_iNextEpgActiveTagCheck = 0; /*!< the time the EPG will be checked for active tag updates */
+ int m_iNextEpgId = 0; /*!< the next epg ID that will be given to a new table when the db isn't being used */
+
+ std::map<int, std::shared_ptr<CPVREpg>> m_epgIdToEpgMap; /*!< the EPGs in this container. maps epg ids to epgs */
+ std::map<std::pair<int, int>, std::shared_ptr<CPVREpg>> m_channelUidToEpgMap; /*!< the EPGs in this container. maps channel uids to epgs */
+
+ mutable CCriticalSection m_critSection; /*!< a critical section for changes to this container */
+ CEvent m_updateEvent; /*!< trigger when an update finishes */
+
+ std::list<CEpgUpdateRequest> m_updateRequests; /*!< list of update requests triggered by addon */
+ CCriticalSection m_updateRequestsLock; /*!< protect update requests */
+
+ std::list<CEpgTagStateChange> m_epgTagChanges; /*!< list of updated epg tags announced by addon */
+ CCriticalSection m_epgTagChangesLock; /*!< protect changed epg tags list */
+
+ bool m_bUpdateNotificationPending = false; /*!< true while an epg updated notification to observers is pending. */
+ CPVRSettings m_settings;
+ CEventSource<PVREvent>& m_events;
+
+ std::atomic<bool> m_bSuspended = {false};
+ };
+}
diff --git a/xbmc/pvr/epg/EpgDatabase.cpp b/xbmc/pvr/epg/EpgDatabase.cpp
new file mode 100644
index 0000000..191f595
--- /dev/null
+++ b/xbmc/pvr/epg/EpgDatabase.cpp
@@ -0,0 +1,1494 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgDatabase.h"
+
+#include "ServiceBroker.h"
+#include "dbwrappers/dataset.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgSearchData.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace dbiplus;
+using namespace PVR;
+
+bool CPVREpgDatabase::Open()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return CDatabase::Open(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseEpg);
+}
+
+void CPVREpgDatabase::Close()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CDatabase::Close();
+}
+
+void CPVREpgDatabase::Lock()
+{
+ m_critSection.lock();
+}
+
+void CPVREpgDatabase::Unlock()
+{
+ m_critSection.unlock();
+}
+
+void CPVREpgDatabase::CreateTables()
+{
+ CLog::Log(LOGINFO, "Creating EPG database tables");
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating table 'epg'");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_pDS->exec(
+ "CREATE TABLE epg ("
+ "idEpg integer primary key, "
+ "sName varchar(64),"
+ "sScraperName varchar(32)"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating table 'epgtags'");
+ m_pDS->exec(
+ "CREATE TABLE epgtags ("
+ "idBroadcast integer primary key, "
+ "iBroadcastUid integer, "
+ "idEpg integer, "
+ "sTitle varchar(128), "
+ "sPlotOutline text, "
+ "sPlot text, "
+ "sOriginalTitle varchar(128), "
+ "sCast varchar(255), "
+ "sDirector varchar(255), "
+ "sWriter varchar(255), "
+ "iYear integer, "
+ "sIMDBNumber varchar(50), "
+ "sIconPath varchar(255), "
+ "iStartTime integer, "
+ "iEndTime integer, "
+ "iGenreType integer, "
+ "iGenreSubType integer, "
+ "sGenre varchar(128), "
+ "sFirstAired varchar(32), "
+ "iParentalRating integer, "
+ "iStarRating integer, "
+ "iSeriesId integer, "
+ "iEpisodeId integer, "
+ "iEpisodePart integer, "
+ "sEpisodeName varchar(128), "
+ "iFlags integer, "
+ "sSeriesLink varchar(255), "
+ "sParentalRatingCode varchar(64)"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating table 'lastepgscan'");
+ m_pDS->exec("CREATE TABLE lastepgscan ("
+ "idEpg integer primary key, "
+ "sLastScan varchar(20)"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating table 'savedsearches'");
+ m_pDS->exec("CREATE TABLE savedsearches ("
+ "idSearch integer primary key,"
+ "sTitle varchar(255), "
+ "sLastExecutedDateTime varchar(20), "
+ "sSearchTerm varchar(255), "
+ "bSearchInDescription bool, "
+ "iGenreType integer, "
+ "sStartDateTime varchar(20), "
+ "sEndDateTime varchar(20), "
+ "bIsCaseSensitive bool, "
+ "iMinimumDuration integer, "
+ "iMaximumDuration integer, "
+ "bIsRadio bool, "
+ "iClientId integer, "
+ "iChannelUid integer, "
+ "bIncludeUnknownGenres bool, "
+ "bRemoveDuplicates bool, "
+ "bIgnoreFinishedBroadcasts bool, "
+ "bIgnoreFutureBroadcasts bool, "
+ "bFreeToAirOnly bool, "
+ "bIgnorePresentTimers bool, "
+ "bIgnorePresentRecordings bool,"
+ "iChannelGroup integer"
+ ")");
+}
+
+void CPVREpgDatabase::CreateAnalytics()
+{
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating EPG database indices");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_pDS->exec("CREATE UNIQUE INDEX idx_epg_idEpg_iStartTime on epgtags(idEpg, iStartTime desc);");
+ m_pDS->exec("CREATE INDEX idx_epg_iEndTime on epgtags(iEndTime);");
+}
+
+void CPVREpgDatabase::UpdateTables(int iVersion)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (iVersion < 5)
+ m_pDS->exec("ALTER TABLE epgtags ADD sGenre varchar(128);");
+
+ if (iVersion < 9)
+ m_pDS->exec("ALTER TABLE epgtags ADD sIconPath varchar(255);");
+
+ if (iVersion < 10)
+ {
+ m_pDS->exec("ALTER TABLE epgtags ADD sOriginalTitle varchar(128);");
+ m_pDS->exec("ALTER TABLE epgtags ADD sCast varchar(255);");
+ m_pDS->exec("ALTER TABLE epgtags ADD sDirector varchar(255);");
+ m_pDS->exec("ALTER TABLE epgtags ADD sWriter varchar(255);");
+ m_pDS->exec("ALTER TABLE epgtags ADD iYear integer;");
+ m_pDS->exec("ALTER TABLE epgtags ADD sIMDBNumber varchar(50);");
+ }
+
+ if (iVersion < 11)
+ {
+ m_pDS->exec("ALTER TABLE epgtags ADD iFlags integer;");
+ }
+
+ if (iVersion < 12)
+ {
+ m_pDS->exec("ALTER TABLE epgtags ADD sSeriesLink varchar(255);");
+ }
+
+ if (iVersion < 13)
+ {
+ const bool isMySQL = StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseEpg.type, "mysql");
+
+ m_pDS->exec(
+ "CREATE TABLE epgtags_new ("
+ "idBroadcast integer primary key, "
+ "iBroadcastUid integer, "
+ "idEpg integer, "
+ "sTitle varchar(128), "
+ "sPlotOutline text, "
+ "sPlot text, "
+ "sOriginalTitle varchar(128), "
+ "sCast varchar(255), "
+ "sDirector varchar(255), "
+ "sWriter varchar(255), "
+ "iYear integer, "
+ "sIMDBNumber varchar(50), "
+ "sIconPath varchar(255), "
+ "iStartTime integer, "
+ "iEndTime integer, "
+ "iGenreType integer, "
+ "iGenreSubType integer, "
+ "sGenre varchar(128), "
+ "sFirstAired varchar(32), "
+ "iParentalRating integer, "
+ "iStarRating integer, "
+ "iSeriesId integer, "
+ "iEpisodeId integer, "
+ "iEpisodePart integer, "
+ "sEpisodeName varchar(128), "
+ "iFlags integer, "
+ "sSeriesLink varchar(255)"
+ ")"
+ );
+
+ m_pDS->exec(
+ "INSERT INTO epgtags_new ("
+ "idBroadcast, "
+ "iBroadcastUid, "
+ "idEpg, "
+ "sTitle, "
+ "sPlotOutline, "
+ "sPlot, "
+ "sOriginalTitle, "
+ "sCast, "
+ "sDirector, "
+ "sWriter, "
+ "iYear, "
+ "sIMDBNumber, "
+ "sIconPath, "
+ "iStartTime, "
+ "iEndTime, "
+ "iGenreType, "
+ "iGenreSubType, "
+ "sGenre, "
+ "sFirstAired, "
+ "iParentalRating, "
+ "iStarRating, "
+ "iSeriesId, "
+ "iEpisodeId, "
+ "iEpisodePart, "
+ "sEpisodeName, "
+ "iFlags, "
+ "sSeriesLink"
+ ") "
+ "SELECT "
+ "idBroadcast, "
+ "iBroadcastUid, "
+ "idEpg, "
+ "sTitle, "
+ "sPlotOutline, "
+ "sPlot, "
+ "sOriginalTitle, "
+ "sCast, "
+ "sDirector, "
+ "sWriter, "
+ "iYear, "
+ "sIMDBNumber, "
+ "sIconPath, "
+ "iStartTime, "
+ "iEndTime, "
+ "iGenreType, "
+ "iGenreSubType, "
+ "sGenre, "
+ "'' AS sFirstAired, "
+ "iParentalRating, "
+ "iStarRating, "
+ "iSeriesId, "
+ "iEpisodeId, "
+ "iEpisodePart, "
+ "sEpisodeName, "
+ "iFlags, "
+ "sSeriesLink "
+ "FROM epgtags"
+ );
+
+ if (isMySQL)
+ m_pDS->exec(
+ "UPDATE epgtags_new INNER JOIN epgtags ON epgtags_new.idBroadcast = epgtags.idBroadcast "
+ "SET epgtags_new.sFirstAired = DATE(FROM_UNIXTIME(epgtags.iFirstAired)) "
+ "WHERE epgtags.iFirstAired > 0"
+ );
+ else
+ m_pDS->exec(
+ "UPDATE epgtags_new SET sFirstAired = "
+ "COALESCE((SELECT STRFTIME('%Y-%m-%d', iFirstAired, 'UNIXEPOCH') "
+ "FROM epgtags WHERE epgtags.idBroadcast = epgtags_new.idBroadcast "
+ "AND epgtags.iFirstAired > 0), '')"
+ );
+
+ m_pDS->exec("DROP TABLE epgtags");
+ m_pDS->exec("ALTER TABLE epgtags_new RENAME TO epgtags");
+ }
+
+ if (iVersion < 14)
+ {
+ m_pDS->exec("ALTER TABLE epgtags ADD sParentalRatingCode varchar(64);");
+ }
+
+ if (iVersion < 15)
+ {
+ m_pDS->exec("CREATE TABLE savedsearches ("
+ "idSearch integer primary key,"
+ "sTitle varchar(255), "
+ "sLastExecutedDateTime varchar(20), "
+ "sSearchTerm varchar(255), "
+ "bSearchInDescription bool, "
+ "iGenreType integer, "
+ "sStartDateTime varchar(20), "
+ "sEndDateTime varchar(20), "
+ "bIsCaseSensitive bool, "
+ "iMinimumDuration integer, "
+ "iMaximumDuration integer, "
+ "bIsRadio bool, "
+ "iClientId integer, "
+ "iChannelUid integer, "
+ "bIncludeUnknownGenres bool, "
+ "bRemoveDuplicates bool, "
+ "bIgnoreFinishedBroadcasts bool, "
+ "bIgnoreFutureBroadcasts bool, "
+ "bFreeToAirOnly bool, "
+ "bIgnorePresentTimers bool, "
+ "bIgnorePresentRecordings bool"
+ ")");
+ }
+
+ if (iVersion < 16)
+ {
+ m_pDS->exec("ALTER TABLE savedsearches ADD iChannelGroup integer;");
+ m_pDS->exec("UPDATE savedsearches SET iChannelGroup = -1");
+ }
+}
+
+bool CPVREpgDatabase::DeleteEpg()
+{
+ bool bReturn(false);
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Deleting all EPG data from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bReturn = DeleteValues("epg") || bReturn;
+ bReturn = DeleteValues("epgtags") || bReturn;
+ bReturn = DeleteValues("lastepgscan") || bReturn;
+
+ return bReturn;
+}
+
+bool CPVREpgDatabase::QueueDeleteEpgQuery(const CPVREpg& table)
+{
+ /* invalid channel */
+ if (table.EpgID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel id: {}", table.EpgID());
+ return false;
+ }
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u", table.EpgID()));
+
+ std::string strQuery;
+ if (BuildSQL(PrepareSQL("DELETE FROM %s ", "epg"), filter, strQuery))
+ return QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+bool CPVREpgDatabase::QueueDeleteTagQuery(const CPVREpgInfoTag& tag)
+{
+ /* tag without a database ID was not persisted */
+ if (tag.DatabaseID() <= 0)
+ return false;
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idBroadcast = %u", tag.DatabaseID()));
+
+ std::string strQuery;
+ BuildSQL(PrepareSQL("DELETE FROM %s ", "epgtags"), filter, strQuery);
+ return QueueDeleteQuery(strQuery);
+}
+
+std::vector<std::shared_ptr<CPVREpg>> CPVREpgDatabase::GetAll()
+{
+ std::vector<std::shared_ptr<CPVREpg>> result;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string strQuery = PrepareSQL("SELECT idEpg, sName, sScraperName FROM epg;");
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ int iEpgID = m_pDS->fv("idEpg").get_asInt();
+ std::string strName = m_pDS->fv("sName").get_asString().c_str();
+ std::string strScraperName = m_pDS->fv("sScraperName").get_asString().c_str();
+
+ result.emplace_back(new CPVREpg(iEpgID, strName, strScraperName, shared_from_this()));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG data from the database");
+ }
+ }
+
+ return result;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::CreateEpgTag(
+ const std::unique_ptr<dbiplus::Dataset>& pDS)
+{
+ if (!pDS->eof())
+ {
+ std::shared_ptr<CPVREpgInfoTag> newTag(
+ new CPVREpgInfoTag(m_pDS->fv("idEpg").get_asInt(), m_pDS->fv("sIconPath").get_asString()));
+
+ time_t iStartTime;
+ iStartTime = static_cast<time_t>(m_pDS->fv("iStartTime").get_asInt());
+ const CDateTime startTime(iStartTime);
+ newTag->m_startTime = startTime;
+
+ time_t iEndTime = static_cast<time_t>(m_pDS->fv("iEndTime").get_asInt());
+ const CDateTime endTime(iEndTime);
+ newTag->m_endTime = endTime;
+
+ const std::string sFirstAired = m_pDS->fv("sFirstAired").get_asString();
+ if (sFirstAired.length() > 0)
+ newTag->m_firstAired.SetFromW3CDate(sFirstAired);
+
+ int iBroadcastUID = m_pDS->fv("iBroadcastUid").get_asInt();
+ // Compat: null value for broadcast uid changed from numerical -1 to 0 with PVR Addon API v4.0.0
+ newTag->m_iUniqueBroadcastID = iBroadcastUID == -1 ? EPG_TAG_INVALID_UID : iBroadcastUID;
+
+ newTag->m_iDatabaseID = m_pDS->fv("idBroadcast").get_asInt();
+ newTag->m_strTitle = m_pDS->fv("sTitle").get_asString();
+ newTag->m_strPlotOutline = m_pDS->fv("sPlotOutline").get_asString();
+ newTag->m_strPlot = m_pDS->fv("sPlot").get_asString();
+ newTag->m_strOriginalTitle = m_pDS->fv("sOriginalTitle").get_asString();
+ newTag->m_cast = newTag->Tokenize(m_pDS->fv("sCast").get_asString());
+ newTag->m_directors = newTag->Tokenize(m_pDS->fv("sDirector").get_asString());
+ newTag->m_writers = newTag->Tokenize(m_pDS->fv("sWriter").get_asString());
+ newTag->m_iYear = m_pDS->fv("iYear").get_asInt();
+ newTag->m_strIMDBNumber = m_pDS->fv("sIMDBNumber").get_asString();
+ newTag->m_iParentalRating = m_pDS->fv("iParentalRating").get_asInt();
+ newTag->m_iStarRating = m_pDS->fv("iStarRating").get_asInt();
+ newTag->m_iEpisodeNumber = m_pDS->fv("iEpisodeId").get_asInt();
+ newTag->m_iEpisodePart = m_pDS->fv("iEpisodePart").get_asInt();
+ newTag->m_strEpisodeName = m_pDS->fv("sEpisodeName").get_asString();
+ newTag->m_iSeriesNumber = m_pDS->fv("iSeriesId").get_asInt();
+ newTag->m_iFlags = m_pDS->fv("iFlags").get_asInt();
+ newTag->m_strSeriesLink = m_pDS->fv("sSeriesLink").get_asString();
+ newTag->m_strParentalRatingCode = m_pDS->fv("sParentalRatingCode").get_asString();
+ newTag->m_iGenreType = m_pDS->fv("iGenreType").get_asInt();
+ newTag->m_iGenreSubType = m_pDS->fv("iGenreSubType").get_asInt();
+ newTag->m_strGenreDescription = m_pDS->fv("sGenre").get_asString();
+
+ return newTag;
+ }
+ return {};
+}
+
+bool CPVREpgDatabase::HasTags(int iEpgID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT iStartTime FROM epgtags WHERE idEpg = %u LIMIT 1;", iEpgID);
+ std::string strValue = GetSingleValue(strQuery);
+ return !strValue.empty();
+}
+
+CDateTime CPVREpgDatabase::GetLastEndTime(int iEpgID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT MAX(iEndTime) FROM epgtags WHERE idEpg = %u;", iEpgID);
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ return CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ return {};
+}
+
+std::pair<CDateTime, CDateTime> CPVREpgDatabase::GetFirstAndLastEPGDate()
+{
+ CDateTime first;
+ CDateTime last;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // 1st query: get min start time
+ std::string strQuery = PrepareSQL("SELECT MIN(iStartTime) FROM epgtags;");
+
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ first = CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ // 2nd query: get max end time
+ strQuery = PrepareSQL("SELECT MAX(iEndTime) FROM epgtags;");
+
+ strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ last = CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ return {first, last};
+}
+
+CDateTime CPVREpgDatabase::GetMinStartTime(int iEpgID, const CDateTime& minStart)
+{
+ time_t t;
+ minStart.GetAsTime(t);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT MIN(iStartTime) "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iStartTime > %u;",
+ iEpgID, static_cast<unsigned int>(t));
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ return CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ return {};
+}
+
+CDateTime CPVREpgDatabase::GetMaxEndTime(int iEpgID, const CDateTime& maxEnd)
+{
+ time_t t;
+ maxEnd.GetAsTime(t);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT MAX(iEndTime) "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iEndTime <= %u;",
+ iEpgID, static_cast<unsigned int>(t));
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ return CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ return {};
+}
+
+namespace
+{
+
+class CSearchTermConverter
+{
+public:
+ explicit CSearchTermConverter(const std::string& strSearchTerm) { Parse(strSearchTerm); }
+
+ std::string ToSQL(const std::string& strFieldName) const
+ {
+ std::string result = "(";
+
+ for (auto it = m_fragments.cbegin(); it != m_fragments.cend();)
+ {
+ result += (*it);
+
+ ++it;
+ if (it != m_fragments.cend())
+ result += strFieldName;
+ }
+
+ StringUtils::TrimRight(result);
+ result += ")";
+ return result;
+ }
+
+private:
+ void Parse(const std::string& strSearchTerm)
+ {
+ std::string strParsedSearchTerm(strSearchTerm);
+ StringUtils::Trim(strParsedSearchTerm);
+
+ std::string strFragment;
+
+ bool bNextOR = false;
+ while (!strParsedSearchTerm.empty())
+ {
+ StringUtils::TrimLeft(strParsedSearchTerm);
+
+ if (StringUtils::StartsWith(strParsedSearchTerm, "!") ||
+ StringUtils::StartsWithNoCase(strParsedSearchTerm, "not"))
+ {
+ std::string strDummy;
+ GetAndCutNextTerm(strParsedSearchTerm, strDummy);
+ strFragment += " NOT ";
+ bNextOR = false;
+ }
+ else if (StringUtils::StartsWith(strParsedSearchTerm, "+") ||
+ StringUtils::StartsWithNoCase(strParsedSearchTerm, "and"))
+ {
+ std::string strDummy;
+ GetAndCutNextTerm(strParsedSearchTerm, strDummy);
+ strFragment += " AND ";
+ bNextOR = false;
+ }
+ else if (StringUtils::StartsWith(strParsedSearchTerm, "|") ||
+ StringUtils::StartsWithNoCase(strParsedSearchTerm, "or"))
+ {
+ std::string strDummy;
+ GetAndCutNextTerm(strParsedSearchTerm, strDummy);
+ strFragment += " OR ";
+ bNextOR = false;
+ }
+ else
+ {
+ std::string strTerm;
+ GetAndCutNextTerm(strParsedSearchTerm, strTerm);
+ if (!strTerm.empty())
+ {
+ if (bNextOR && !m_fragments.empty())
+ strFragment += " OR "; // default operator
+
+ strFragment += "(UPPER(";
+
+ m_fragments.emplace_back(strFragment);
+ strFragment.clear();
+
+ strFragment += ") LIKE UPPER('%";
+ StringUtils::Replace(strTerm, "'", "''"); // escape '
+ strFragment += strTerm;
+ strFragment += "%')) ";
+
+ bNextOR = true;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ StringUtils::TrimLeft(strParsedSearchTerm);
+ }
+
+ if (!strFragment.empty())
+ m_fragments.emplace_back(strFragment);
+ }
+
+ static void GetAndCutNextTerm(std::string& strSearchTerm, std::string& strNextTerm)
+ {
+ std::string strFindNext(" ");
+
+ if (StringUtils::EndsWith(strSearchTerm, "\""))
+ {
+ strSearchTerm.erase(0, 1);
+ strFindNext = "\"";
+ }
+
+ const size_t iNextPos = strSearchTerm.find(strFindNext);
+ if (iNextPos != std::string::npos)
+ {
+ strNextTerm = strSearchTerm.substr(0, iNextPos);
+ strSearchTerm.erase(0, iNextPos + 1);
+ }
+ else
+ {
+ strNextTerm = strSearchTerm;
+ strSearchTerm.clear();
+ }
+ }
+
+ std::vector<std::string> m_fragments;
+};
+
+} // unnamed namespace
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgDatabase::GetEpgTags(
+ const PVREpgSearchData& searchData)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ std::string strQuery = PrepareSQL("SELECT * FROM epgtags");
+
+ Filter filter;
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // min start datetime
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_startDateTime.IsValid())
+ {
+ time_t minStart;
+ searchData.m_startDateTime.GetAsTime(minStart);
+ filter.AppendWhere(PrepareSQL("iStartTime >= %u", static_cast<unsigned int>(minStart)));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // max end datetime
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_endDateTime.IsValid())
+ {
+ time_t maxEnd;
+ searchData.m_endDateTime.GetAsTime(maxEnd);
+ filter.AppendWhere(PrepareSQL("iEndTime <= %u", static_cast<unsigned int>(maxEnd)));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // ignore finished broadcasts
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_bIgnoreFinishedBroadcasts)
+ {
+ const time_t minEnd = std::time(nullptr); // now
+ filter.AppendWhere(PrepareSQL("iEndTime > %u", static_cast<unsigned int>(minEnd)));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // ignore future broadcasts
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_bIgnoreFutureBroadcasts)
+ {
+ const time_t maxStart = std::time(nullptr); // now
+ filter.AppendWhere(PrepareSQL("iStartTime < %u", static_cast<unsigned int>(maxStart)));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // genre type
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_iGenreType != EPG_SEARCH_UNSET)
+ {
+ if (searchData.m_bIncludeUnknownGenres)
+ {
+ // match the exact genre and everything with unknown genre
+ filter.AppendWhere(PrepareSQL("(iGenreType == %u) OR (iGenreType < %u) OR (iGenreType > %u)",
+ searchData.m_iGenreType, EPG_EVENT_CONTENTMASK_MOVIEDRAMA,
+ EPG_EVENT_CONTENTMASK_USERDEFINED));
+ }
+ else
+ {
+ // match only the exact genre
+ filter.AppendWhere(PrepareSQL("iGenreType == %u", searchData.m_iGenreType));
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // search term
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (!searchData.m_strSearchTerm.empty())
+ {
+ const CSearchTermConverter conv(searchData.m_strSearchTerm);
+
+ // title
+ std::string strWhere = conv.ToSQL("sTitle");
+
+ // plot outline
+ strWhere += " OR ";
+ strWhere += conv.ToSQL("sPlotOutline");
+
+ if (searchData.m_bSearchInDescription)
+ {
+ // plot
+ strWhere += " OR ";
+ strWhere += conv.ToSQL("sPlot");
+ }
+
+ filter.AppendWhere(strWhere);
+ }
+
+ if (BuildSQL(strQuery, filter, strQuery))
+ {
+ try
+ {
+ if (m_pDS->query(strQuery))
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ while (!m_pDS->eof())
+ {
+ tags.emplace_back(CreateEpgTag(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return tags;
+ }
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags for given search criteria");
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByUniqueBroadcastID(
+ int iEpgID, unsigned int iUniqueBroadcastId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iBroadcastUid = %u;",
+ iEpgID, iUniqueBroadcastId);
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG tag with unique broadcast ID ({}) from the database",
+ iUniqueBroadcastId);
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByDatabaseID(int iEpgID, int iDatabaseId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND idBroadcast = %u;",
+ iEpgID, iDatabaseId);
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG tag with database ID ({}) from the database",
+ iDatabaseId);
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByStartTime(int iEpgID,
+ const CDateTime& startTime)
+{
+ time_t start;
+ startTime.GetAsTime(start);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iStartTime = %u;",
+ iEpgID, static_cast<unsigned int>(start));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG tag with start time ({}) from the database",
+ startTime.GetAsDBDateTime());
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByMinStartTime(
+ int iEpgID, const CDateTime& minStartTime)
+{
+ time_t minStart;
+ minStartTime.GetAsTime(minStart);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iStartTime >= %u ORDER BY iStartTime ASC LIMIT 1;",
+ iEpgID, static_cast<unsigned int>(minStart));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags with min start time ({}) for EPG ({})",
+ minStartTime.GetAsDBDateTime(), iEpgID);
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByMaxEndTime(int iEpgID,
+ const CDateTime& maxEndTime)
+{
+ time_t maxEnd;
+ maxEndTime.GetAsTime(maxEnd);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iEndTime <= %u ORDER BY iStartTime DESC LIMIT 1;",
+ iEpgID, static_cast<unsigned int>(maxEnd));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags with max end time ({}) for EPG ({})",
+ maxEndTime.GetAsDBDateTime(), iEpgID);
+ }
+ }
+
+ return {};
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgDatabase::GetEpgTagsByMinStartMaxEndTime(
+ int iEpgID, const CDateTime& minStartTime, const CDateTime& maxEndTime)
+{
+ time_t minStart;
+ minStartTime.GetAsTime(minStart);
+
+ time_t maxEnd;
+ maxEndTime.GetAsTime(maxEnd);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iStartTime >= %u AND iEndTime <= %u ORDER BY iStartTime;",
+ iEpgID, static_cast<unsigned int>(minStart), static_cast<unsigned int>(maxEnd));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ while (!m_pDS->eof())
+ {
+ tags.emplace_back(CreateEpgTag(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return tags;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR,
+ "Could not load tags with min start time ({}) and max end time ({}) for EPG ({})",
+ minStartTime.GetAsDBDateTime(), maxEndTime.GetAsDBDateTime(), iEpgID);
+ }
+ }
+
+ return {};
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgDatabase::GetEpgTagsByMinEndMaxStartTime(
+ int iEpgID, const CDateTime& minEndTime, const CDateTime& maxStartTime)
+{
+ time_t minEnd;
+ minEndTime.GetAsTime(minEnd);
+
+ time_t maxStart;
+ maxStartTime.GetAsTime(maxStart);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iEndTime >= %u AND iStartTime <= %u ORDER BY iStartTime;",
+ iEpgID, static_cast<unsigned int>(minEnd), static_cast<unsigned int>(maxStart));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ while (!m_pDS->eof())
+ {
+ tags.emplace_back(CreateEpgTag(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return tags;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR,
+ "Could not load tags with min end time ({}) and max start time ({}) for EPG ({})",
+ minEndTime.GetAsDBDateTime(), maxStartTime.GetAsDBDateTime(), iEpgID);
+ }
+ }
+
+ return {};
+}
+
+bool CPVREpgDatabase::QueueDeleteEpgTagsByMinEndMaxStartTimeQuery(int iEpgID,
+ const CDateTime& minEndTime,
+ const CDateTime& maxStartTime)
+{
+ time_t minEnd;
+ minEndTime.GetAsTime(minEnd);
+
+ time_t maxStart;
+ maxStartTime.GetAsTime(maxStart);
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u AND iEndTime >= %u AND iStartTime <= %u", iEpgID,
+ static_cast<unsigned int>(minEnd),
+ static_cast<unsigned int>(maxStart)));
+
+ std::string strQuery;
+ if (BuildSQL("DELETE FROM epgtags", filter, strQuery))
+ return QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgDatabase::GetAllEpgTags(int iEpgID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * FROM epgtags WHERE idEpg = %u ORDER BY iStartTime;", iEpgID);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ while (!m_pDS->eof())
+ {
+ tags.emplace_back(CreateEpgTag(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return tags;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags for EPG ({})", iEpgID);
+ }
+ }
+ return {};
+}
+
+std::vector<std::string> CPVREpgDatabase::GetAllIconPaths(int iEpgID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT sIconPath FROM epgtags WHERE idEpg = %u;", iEpgID);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::vector<std::string> paths;
+ while (!m_pDS->eof())
+ {
+ paths.emplace_back(m_pDS->fv("sIconPath").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return paths;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags for EPG ({})", iEpgID);
+ }
+ }
+ return {};
+}
+
+bool CPVREpgDatabase::GetLastEpgScanTime(int iEpgId, CDateTime* lastScan)
+{
+ bool bReturn = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string strWhereClause = PrepareSQL("idEpg = %u", iEpgId);
+ std::string strValue = GetSingleValue("lastepgscan", "sLastScan", strWhereClause);
+
+ if (!strValue.empty())
+ {
+ lastScan->SetFromDBDateTime(strValue);
+ bReturn = true;
+ }
+ else
+ {
+ lastScan->SetValid(false);
+ }
+
+ return bReturn;
+}
+
+bool CPVREpgDatabase::QueuePersistLastEpgScanTimeQuery(int iEpgId, const CDateTime& lastScanTime)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string strQuery = PrepareSQL("REPLACE INTO lastepgscan(idEpg, sLastScan) VALUES (%u, '%s');",
+ iEpgId, lastScanTime.GetAsDBDateTime().c_str());
+
+ return QueueInsertQuery(strQuery);
+}
+
+bool CPVREpgDatabase::QueueDeleteLastEpgScanTimeQuery(const CPVREpg& table)
+{
+ if (table.EpgID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid EPG id: {}", table.EpgID());
+ return false;
+ }
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u", table.EpgID()));
+
+ std::string strQuery;
+ if (BuildSQL(PrepareSQL("DELETE FROM %s ", "lastepgscan"), filter, strQuery))
+ return QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+int CPVREpgDatabase::Persist(const CPVREpg& epg, bool bQueueWrite)
+{
+ int iReturn = -1;
+ std::string strQuery;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (epg.EpgID() > 0)
+ strQuery = PrepareSQL("REPLACE INTO epg (idEpg, sName, sScraperName) "
+ "VALUES (%u, '%s', '%s');",
+ epg.EpgID(), epg.Name().c_str(), epg.ScraperName().c_str());
+ else
+ strQuery = PrepareSQL("INSERT INTO epg (sName, sScraperName) "
+ "VALUES ('%s', '%s');",
+ epg.Name().c_str(), epg.ScraperName().c_str());
+
+ if (bQueueWrite)
+ {
+ if (QueueInsertQuery(strQuery))
+ iReturn = epg.EpgID() <= 0 ? 0 : epg.EpgID();
+ }
+ else
+ {
+ if (ExecuteQuery(strQuery))
+ iReturn = epg.EpgID() <= 0 ? static_cast<int>(m_pDS->lastinsertid()) : epg.EpgID();
+ }
+
+ return iReturn;
+}
+
+bool CPVREpgDatabase::DeleteEpgTags(int iEpgId, const CDateTime& maxEndTime)
+{
+ time_t iMaxEndTime;
+ maxEndTime.GetAsTime(iMaxEndTime);
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(
+ PrepareSQL("idEpg = %u AND iEndTime < %u", iEpgId, static_cast<unsigned int>(iMaxEndTime)));
+ return DeleteValues("epgtags", filter);
+}
+
+bool CPVREpgDatabase::DeleteEpgTags(int iEpgId)
+{
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u", iEpgId));
+ return DeleteValues("epgtags", filter);
+}
+
+bool CPVREpgDatabase::QueueDeleteEpgTags(int iEpgId)
+{
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u", iEpgId));
+
+ std::string strQuery;
+ BuildSQL(PrepareSQL("DELETE FROM %s ", "epgtags"), filter, strQuery);
+ return QueueDeleteQuery(strQuery);
+}
+
+bool CPVREpgDatabase::QueuePersistQuery(const CPVREpgInfoTag& tag)
+{
+ if (tag.EpgID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Tag '{}' does not have a valid table", tag.Title());
+ return false;
+ }
+
+ time_t iStartTime, iEndTime;
+ tag.StartAsUTC().GetAsTime(iStartTime);
+ tag.EndAsUTC().GetAsTime(iEndTime);
+
+ std::string sFirstAired;
+ if (tag.FirstAired().IsValid())
+ sFirstAired = tag.FirstAired().GetAsW3CDate();
+
+ int iBroadcastId = tag.DatabaseID();
+ std::string strQuery;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (iBroadcastId < 0)
+ {
+ strQuery = PrepareSQL(
+ "REPLACE INTO epgtags (idEpg, iStartTime, "
+ "iEndTime, sTitle, sPlotOutline, sPlot, sOriginalTitle, sCast, sDirector, sWriter, iYear, "
+ "sIMDBNumber, "
+ "sIconPath, iGenreType, iGenreSubType, sGenre, sFirstAired, iParentalRating, iStarRating, "
+ "iSeriesId, "
+ "iEpisodeId, iEpisodePart, sEpisodeName, iFlags, sSeriesLink, sParentalRatingCode, "
+ "iBroadcastUid) "
+ "VALUES (%u, %u, %u, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', %i, %i, "
+ "'%s', '%s', %i, %i, %i, %i, %i, '%s', %i, '%s', '%s', %i);",
+ tag.EpgID(), static_cast<unsigned int>(iStartTime), static_cast<unsigned int>(iEndTime),
+ tag.Title().c_str(), tag.PlotOutline().c_str(), tag.Plot().c_str(),
+ tag.OriginalTitle().c_str(), tag.DeTokenize(tag.Cast()).c_str(),
+ tag.DeTokenize(tag.Directors()).c_str(), tag.DeTokenize(tag.Writers()).c_str(), tag.Year(),
+ tag.IMDBNumber().c_str(), tag.ClientIconPath().c_str(), tag.GenreType(), tag.GenreSubType(),
+ tag.GenreDescription().c_str(), sFirstAired.c_str(), tag.ParentalRating(), tag.StarRating(),
+ tag.SeriesNumber(), tag.EpisodeNumber(), tag.EpisodePart(), tag.EpisodeName().c_str(),
+ tag.Flags(), tag.SeriesLink().c_str(), tag.ParentalRatingCode().c_str(),
+ tag.UniqueBroadcastID());
+ }
+ else
+ {
+ strQuery = PrepareSQL(
+ "REPLACE INTO epgtags (idEpg, iStartTime, "
+ "iEndTime, sTitle, sPlotOutline, sPlot, sOriginalTitle, sCast, sDirector, sWriter, iYear, "
+ "sIMDBNumber, "
+ "sIconPath, iGenreType, iGenreSubType, sGenre, sFirstAired, iParentalRating, iStarRating, "
+ "iSeriesId, "
+ "iEpisodeId, iEpisodePart, sEpisodeName, iFlags, sSeriesLink, sParentalRatingCode, "
+ "iBroadcastUid, idBroadcast) "
+ "VALUES (%u, %u, %u, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', %i, %i, "
+ "'%s', '%s', %i, %i, %i, %i, %i, '%s', %i, '%s', '%s', %i, %i);",
+ tag.EpgID(), static_cast<unsigned int>(iStartTime), static_cast<unsigned int>(iEndTime),
+ tag.Title().c_str(), tag.PlotOutline().c_str(), tag.Plot().c_str(),
+ tag.OriginalTitle().c_str(), tag.DeTokenize(tag.Cast()).c_str(),
+ tag.DeTokenize(tag.Directors()).c_str(), tag.DeTokenize(tag.Writers()).c_str(), tag.Year(),
+ tag.IMDBNumber().c_str(), tag.ClientIconPath().c_str(), tag.GenreType(), tag.GenreSubType(),
+ tag.GenreDescription().c_str(), sFirstAired.c_str(), tag.ParentalRating(), tag.StarRating(),
+ tag.SeriesNumber(), tag.EpisodeNumber(), tag.EpisodePart(), tag.EpisodeName().c_str(),
+ tag.Flags(), tag.SeriesLink().c_str(), tag.ParentalRatingCode().c_str(),
+ tag.UniqueBroadcastID(), iBroadcastId);
+ }
+
+ QueueInsertQuery(strQuery);
+ return true;
+}
+
+int CPVREpgDatabase::GetLastEPGId()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string strQuery = PrepareSQL("SELECT MAX(idEpg) FROM epg");
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ return std::atoi(strValue.c_str());
+ return 0;
+}
+
+/********** Saved searches methods **********/
+
+std::shared_ptr<CPVREpgSearchFilter> CPVREpgDatabase::CreateEpgSearchFilter(
+ bool bRadio, const std::unique_ptr<dbiplus::Dataset>& pDS)
+{
+ if (!pDS->eof())
+ {
+ auto newSearch = std::make_shared<CPVREpgSearchFilter>(bRadio);
+
+ newSearch->SetDatabaseId(m_pDS->fv("idSearch").get_asInt());
+ newSearch->SetTitle(m_pDS->fv("sTitle").get_asString());
+
+ const std::string lastExec = m_pDS->fv("sLastExecutedDateTime").get_asString();
+ if (!lastExec.empty())
+ newSearch->SetLastExecutedDateTime(CDateTime::FromDBDateTime(lastExec));
+
+ newSearch->SetSearchTerm(m_pDS->fv("sSearchTerm").get_asString());
+ newSearch->SetSearchInDescription(m_pDS->fv("bSearchInDescription").get_asBool());
+ newSearch->SetGenreType(m_pDS->fv("iGenreType").get_asInt());
+
+ const std::string start = m_pDS->fv("sStartDateTime").get_asString();
+ if (!start.empty())
+ newSearch->SetStartDateTime(CDateTime::FromDBDateTime(start));
+
+ const std::string end = m_pDS->fv("sEndDateTime").get_asString();
+ if (!end.empty())
+ newSearch->SetEndDateTime(CDateTime::FromDBDateTime(end));
+
+ newSearch->SetCaseSensitive(m_pDS->fv("bIsCaseSensitive").get_asBool());
+ newSearch->SetMinimumDuration(m_pDS->fv("iMinimumDuration").get_asInt());
+ newSearch->SetMaximumDuration(m_pDS->fv("iMaximumDuration").get_asInt());
+ newSearch->SetClientID(m_pDS->fv("iClientId").get_asInt());
+ newSearch->SetChannelUID(m_pDS->fv("iChannelUid").get_asInt());
+ newSearch->SetIncludeUnknownGenres(m_pDS->fv("bIncludeUnknownGenres").get_asBool());
+ newSearch->SetRemoveDuplicates(m_pDS->fv("bRemoveDuplicates").get_asBool());
+ newSearch->SetIgnoreFinishedBroadcasts(m_pDS->fv("bIgnoreFinishedBroadcasts").get_asBool());
+ newSearch->SetIgnoreFutureBroadcasts(m_pDS->fv("bIgnoreFutureBroadcasts").get_asBool());
+ newSearch->SetFreeToAirOnly(m_pDS->fv("bFreeToAirOnly").get_asBool());
+ newSearch->SetIgnorePresentTimers(m_pDS->fv("bIgnorePresentTimers").get_asBool());
+ newSearch->SetIgnorePresentRecordings(m_pDS->fv("bIgnorePresentRecordings").get_asBool());
+ newSearch->SetChannelGroupID(m_pDS->fv("iChannelGroup").get_asInt());
+
+ newSearch->SetChanged(false);
+
+ return newSearch;
+ }
+ return {};
+}
+
+std::vector<std::shared_ptr<CPVREpgSearchFilter>> CPVREpgDatabase::GetSavedSearches(bool bRadio)
+{
+ std::vector<std::shared_ptr<CPVREpgSearchFilter>> result;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * FROM savedsearches WHERE bIsRadio = %u", bRadio);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ result.emplace_back(CreateEpgSearchFilter(bRadio, m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG search data from the database");
+ }
+ }
+ return result;
+}
+
+std::shared_ptr<CPVREpgSearchFilter> CPVREpgDatabase::GetSavedSearchById(bool bRadio, int iId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * FROM savedsearches WHERE bIsRadio = %u AND idSearch = %u;", bRadio, iId);
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgSearchFilter> filter = CreateEpgSearchFilter(bRadio, m_pDS);
+ m_pDS->close();
+ return filter;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG search filter with id ({}) from the database", iId);
+ }
+ }
+
+ return {};
+}
+
+bool CPVREpgDatabase::Persist(CPVREpgSearchFilter& epgSearch)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // Insert a new entry if this is a new search, replace the existing otherwise
+ std::string strQuery;
+ if (epgSearch.GetDatabaseId() == -1)
+ strQuery = PrepareSQL(
+ "INSERT INTO savedsearches "
+ "(sTitle, sLastExecutedDateTime, sSearchTerm, bSearchInDescription, bIsCaseSensitive, "
+ "iGenreType, bIncludeUnknownGenres, sStartDateTime, sEndDateTime, iMinimumDuration, "
+ "iMaximumDuration, bIsRadio, iClientId, iChannelUid, bRemoveDuplicates, "
+ "bIgnoreFinishedBroadcasts, bIgnoreFutureBroadcasts, bFreeToAirOnly, bIgnorePresentTimers, "
+ "bIgnorePresentRecordings, iChannelGroup) "
+ "VALUES ('%s', '%s', '%s', %i, %i, %i, %i, '%s', '%s', %i, %i, %i, %i, %i, %i, %i, %i, "
+ "%i, %i, %i, %i);",
+ epgSearch.GetTitle().c_str(),
+ epgSearch.GetLastExecutedDateTime().IsValid()
+ ? epgSearch.GetLastExecutedDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetSearchTerm().c_str(), epgSearch.ShouldSearchInDescription() ? 1 : 0,
+ epgSearch.IsCaseSensitive() ? 1 : 0, epgSearch.GetGenreType(),
+ epgSearch.ShouldIncludeUnknownGenres() ? 1 : 0,
+ epgSearch.GetStartDateTime().IsValid()
+ ? epgSearch.GetStartDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetEndDateTime().IsValid() ? epgSearch.GetEndDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetMinimumDuration(), epgSearch.GetMaximumDuration(), epgSearch.IsRadio() ? 1 : 0,
+ epgSearch.GetClientID(), epgSearch.GetChannelUID(),
+ epgSearch.ShouldRemoveDuplicates() ? 1 : 0,
+ epgSearch.ShouldIgnoreFinishedBroadcasts() ? 1 : 0,
+ epgSearch.ShouldIgnoreFutureBroadcasts() ? 1 : 0, epgSearch.IsFreeToAirOnly() ? 1 : 0,
+ epgSearch.ShouldIgnorePresentTimers() ? 1 : 0,
+ epgSearch.ShouldIgnorePresentRecordings() ? 1 : 0, epgSearch.GetChannelGroupID());
+ else
+ strQuery = PrepareSQL(
+ "REPLACE INTO savedsearches "
+ "(idSearch, sTitle, sLastExecutedDateTime, sSearchTerm, bSearchInDescription, "
+ "bIsCaseSensitive, iGenreType, bIncludeUnknownGenres, sStartDateTime, sEndDateTime, "
+ "iMinimumDuration, iMaximumDuration, bIsRadio, iClientId, iChannelUid, bRemoveDuplicates, "
+ "bIgnoreFinishedBroadcasts, bIgnoreFutureBroadcasts, bFreeToAirOnly, bIgnorePresentTimers, "
+ "bIgnorePresentRecordings, iChannelGroup) "
+ "VALUES (%i, '%s', '%s', '%s', %i, %i, %i, %i, '%s', '%s', %i, %i, %i, %i, %i, %i, %i, %i, "
+ "%i, %i, %i, %i);",
+ epgSearch.GetDatabaseId(), epgSearch.GetTitle().c_str(),
+ epgSearch.GetLastExecutedDateTime().IsValid()
+ ? epgSearch.GetLastExecutedDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetSearchTerm().c_str(), epgSearch.ShouldSearchInDescription() ? 1 : 0,
+ epgSearch.IsCaseSensitive() ? 1 : 0, epgSearch.GetGenreType(),
+ epgSearch.ShouldIncludeUnknownGenres() ? 1 : 0,
+ epgSearch.GetStartDateTime().IsValid()
+ ? epgSearch.GetStartDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetEndDateTime().IsValid() ? epgSearch.GetEndDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetMinimumDuration(), epgSearch.GetMaximumDuration(), epgSearch.IsRadio() ? 1 : 0,
+ epgSearch.GetClientID(), epgSearch.GetChannelUID(),
+ epgSearch.ShouldRemoveDuplicates() ? 1 : 0,
+ epgSearch.ShouldIgnoreFinishedBroadcasts() ? 1 : 0,
+ epgSearch.ShouldIgnoreFutureBroadcasts() ? 1 : 0, epgSearch.IsFreeToAirOnly() ? 1 : 0,
+ epgSearch.ShouldIgnorePresentTimers() ? 1 : 0,
+ epgSearch.ShouldIgnorePresentRecordings() ? 1 : 0, epgSearch.GetChannelGroupID());
+
+ bool bReturn = ExecuteQuery(strQuery);
+
+ if (bReturn)
+ {
+ // Set the database id for searches persisted for the first time
+ if (epgSearch.GetDatabaseId() == -1)
+ epgSearch.SetDatabaseId(static_cast<int>(m_pDS->lastinsertid()));
+
+ epgSearch.SetChanged(false);
+ }
+
+ return bReturn;
+}
+
+bool CPVREpgDatabase::UpdateSavedSearchLastExecuted(const CPVREpgSearchFilter& epgSearch)
+{
+ if (epgSearch.GetDatabaseId() == -1)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::string strQuery = PrepareSQL(
+ "UPDATE savedsearches SET sLastExecutedDateTime = '%s' WHERE idSearch = %i",
+ epgSearch.GetLastExecutedDateTime().GetAsDBDateTime().c_str(), epgSearch.GetDatabaseId());
+ return ExecuteQuery(strQuery);
+}
+
+bool CPVREpgDatabase::Delete(const CPVREpgSearchFilter& epgSearch)
+{
+ if (epgSearch.GetDatabaseId() == -1)
+ return false;
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Deleting saved search '{}' from the database",
+ epgSearch.GetTitle());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idSearch = '%i'", epgSearch.GetDatabaseId()));
+
+ return DeleteValues("savedsearches", filter);
+}
+
+bool CPVREpgDatabase::DeleteSavedSearches()
+{
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Deleting all saved searches from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("savedsearches");
+}
diff --git a/xbmc/pvr/epg/EpgDatabase.h b/xbmc/pvr/epg/EpgDatabase.h
new file mode 100644
index 0000000..580568d
--- /dev/null
+++ b/xbmc/pvr/epg/EpgDatabase.h
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "dbwrappers/Database.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <vector>
+
+class CDateTime;
+
+namespace PVR
+{
+ class CPVREpg;
+ class CPVREpgInfoTag;
+ class CPVREpgSearchFilter;
+
+ struct PVREpgSearchData;
+
+ /** The EPG database */
+
+ static constexpr int EPG_COMMIT_QUERY_COUNT_LIMIT = 10000;
+
+ class CPVREpgDatabase : public CDatabase, public std::enable_shared_from_this<CPVREpgDatabase>
+ {
+ public:
+ /*!
+ * @brief Create a new instance of the EPG database.
+ */
+ CPVREpgDatabase() = default;
+
+ /*!
+ * @brief Destroy this instance.
+ */
+ ~CPVREpgDatabase() override = default;
+
+ /*!
+ * @brief Open the database.
+ * @return True if it was opened successfully, false otherwise.
+ */
+ bool Open() override;
+
+ /*!
+ * @brief Close the database.
+ */
+ void Close() override;
+
+ /*!
+ * @brief Lock the database.
+ */
+ void Lock();
+
+ /*!
+ * @brief Unlock the database.
+ */
+ void Unlock();
+
+ /*!
+ * @brief Get the minimal database version that is required to operate correctly.
+ * @return The minimal database version.
+ */
+ int GetSchemaVersion() const override { return 16; }
+
+ /*!
+ * @brief Get the default sqlite database filename.
+ * @return The default filename.
+ */
+ const char* GetBaseDBName() const override { return "Epg"; }
+
+ /*! @name EPG methods */
+ //@{
+
+ /*!
+ * @brief Remove all EPG information from the database
+ * @return True if the EPG information was erased, false otherwise.
+ */
+ bool DeleteEpg();
+
+ /*!
+ * @brief Queue deletionof an EPG table.
+ * @param tag The table to queue for deletion.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteEpgQuery(const CPVREpg& table);
+
+ /*!
+ * @brief Write the query to delete the given EPG tag to db query queue.
+ * @param tag The EPG tag to remove.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteTagQuery(const CPVREpgInfoTag& tag);
+
+ /*!
+ * @brief Get all EPG tables from the database. Does not get the EPG tables' entries.
+ * @return The entries.
+ */
+ std::vector<std::shared_ptr<CPVREpg>> GetAll();
+
+ /*!
+ * @brief Get all tags for a given EPG id.
+ * @param iEpgID The ID of the EPG.
+ * @return The entries.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetAllEpgTags(int iEpgID);
+
+ /*!
+ * @brief Get all icon paths for a given EPG id.
+ * @param iEpgID The ID of the EPG.
+ * @return The entries.
+ */
+ std::vector<std::string> GetAllIconPaths(int iEpgID);
+
+ /*!
+ * @brief Check whether this EPG has any tags.
+ * @param iEpgID The ID of the EPG.
+ * @return True in case there are tags, false otherwise.
+ */
+ bool HasTags(int iEpgID);
+
+ /*!
+ * @brief Get the end time of the last tag in this EPG.
+ * @param iEpgID The ID of the EPG.
+ * @return The time.
+ */
+ CDateTime GetLastEndTime(int iEpgID);
+
+ /*!
+ * @brief Get the start and end time across all EPGs.
+ * @return The times; first: start time, second: end time.
+ */
+ std::pair<CDateTime, CDateTime> GetFirstAndLastEPGDate();
+
+ /*!
+ * @brief Get the start time of the first tag with a start time greater than the given min time.
+ * @param iEpgID The ID of the EPG.
+ * @param minStart The min start time.
+ * @return The time.
+ */
+ CDateTime GetMinStartTime(int iEpgID, const CDateTime& minStart);
+
+ /*!
+ * @brief Get the end time of the first tag with an end time less than the given max time.
+ * @param iEpgID The ID of the EPG.
+ * @param maxEnd The mx end time.
+ * @return The time.
+ */
+ CDateTime GetMaxEndTime(int iEpgID, const CDateTime& maxEnd);
+
+ /*!
+ * @brief Get all EPG tags matching the given search criteria.
+ * @param searchData The search criteria.
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTags(const PVREpgSearchData& searchData);
+
+ /*!
+ * @brief Get an EPG tag given its EPG id and unique broadcast ID.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param iUniqueBroadcastId The unique broadcast ID for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByUniqueBroadcastID(int iEpgID,
+ unsigned int iUniqueBroadcastId);
+
+ /*!
+ * @brief Get an EPG tag given its EPG id and database ID.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param iDatabaseId The database ID for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByDatabaseID(int iEpgID, int iDatabaseId);
+
+ /*!
+ * @brief Get an EPG tag given its EPG ID and start time.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param startTime The start time for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByStartTime(int iEpgID, const CDateTime& startTime);
+
+ /*!
+ * @brief Get the next EPG tag matching the given EPG id and min start time.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param minStartTime The min start time for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByMinStartTime(int iEpgID,
+ const CDateTime& minStartTime);
+
+ /*!
+ * @brief Get the next EPG tag matching the given EPG id and max end time.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param maxEndTime The max end time for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByMaxEndTime(int iEpgID, const CDateTime& maxEndTime);
+
+ /*!
+ * @brief Get all EPG tags matching the given EPG id, min start time and max end time.
+ * @param iEpgID The ID of the EPG for the tags to get.
+ * @param minStartTime The min start time for the tags to get.
+ * @param maxEndTime The max end time for the tags to get.
+ * @return The tags or empty vector, if no tags were found.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTagsByMinStartMaxEndTime(
+ int iEpgID, const CDateTime& minStartTime, const CDateTime& maxEndTime);
+
+ /*!
+ * @brief Get all EPG tags matching the given EPG id, min end time and max start time.
+ * @param iEpgID The ID of the EPG for the tags to get.
+ * @param minEndTime The min end time for the tags to get.
+ * @param maxStartTime The max start time for the tags to get.
+ * @return The tags or empty vector, if no tags were found.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTagsByMinEndMaxStartTime(
+ int iEpgID, const CDateTime& minEndTime, const CDateTime& maxStartTime);
+
+ /*!
+ * @brief Write the query to delete all EPG tags in range of given EPG id, min end time and max
+ * start time to db query queue. .
+ * @param iEpgID The ID of the EPG for the tags to delete.
+ * @param minEndTime The min end time for the tags to delete.
+ * @param maxStartTime The max start time for the tags to delete.
+ * @return True if it was removed or queued successfully, false otherwise.
+ */
+ bool QueueDeleteEpgTagsByMinEndMaxStartTimeQuery(int iEpgID,
+ const CDateTime& minEndTime,
+ const CDateTime& maxStartTime);
+
+ /*!
+ * @brief Get the last stored EPG scan time.
+ * @param iEpgId The table to update the time for. Use 0 for a global value.
+ * @param lastScan The last scan time or -1 if it wasn't found.
+ * @return True if the time was fetched successfully, false otherwise.
+ */
+ bool GetLastEpgScanTime(int iEpgId, CDateTime* lastScan);
+
+ /*!
+ * @brief Write the query to update the last scan time for the given EPG to db query queue.
+ * @param iEpgId The table to update the time for.
+ * @param lastScanTime The time to write to the database.
+ * @return True on success, false otherwise.
+ */
+ bool QueuePersistLastEpgScanTimeQuery(int iEpgId, const CDateTime& lastScanTime);
+
+ /*!
+ * @brief Write the query to delete the last scan time for the given EPG to db query queue.
+ * @param iEpgId The table to delete the time for.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteLastEpgScanTimeQuery(const CPVREpg& table);
+
+ /*!
+ * @brief Persist an EPG table. It's entries are not persisted.
+ * @param epg The table to persist.
+ * @param bQueueWrite If true, don't execute the query immediately but queue it.
+ * @return The database ID of this entry or 0 if bQueueWrite is false and the query was queued.
+ */
+ int Persist(const CPVREpg& epg, bool bQueueWrite);
+
+ /*!
+ * @brief Erase all EPG tags with the given epg ID and an end time less than the given time.
+ * @param iEpgId The ID of the EPG.
+ * @param maxEndTime The maximum allowed end time.
+ * @return True if the entries were removed successfully, false otherwise.
+ */
+ bool DeleteEpgTags(int iEpgId, const CDateTime& maxEndTime);
+
+ /*!
+ * @brief Erase all EPG tags with the given epg ID.
+ * @param iEpgId The ID of the EPG.
+ * @return True if the entries were removed successfully, false otherwise.
+ */
+ bool DeleteEpgTags(int iEpgId);
+
+ /*!
+ * @brief Queue the erase all EPG tags with the given epg ID.
+ * @param iEpgId The ID of the EPG.
+ * @return True if the entries were queued successfully, false otherwise.
+ */
+ bool QueueDeleteEpgTags(int iEpgId);
+
+ /*!
+ * @brief Write the query to persist the given EPG tag to db query queue.
+ * @param tag The tag to persist.
+ * @return True on success, false otherwise.
+ */
+ bool QueuePersistQuery(const CPVREpgInfoTag& tag);
+
+ /*!
+ * @return Last EPG id in the database
+ */
+ int GetLastEPGId();
+
+ //@}
+
+ /*! @name EPG searches methods */
+ //@{
+
+ /*!
+ * @brief Get all saved searches from the database.
+ * @param bRadio Whether to fetch saved searches for radio or TV.
+ * @return The searches.
+ */
+ std::vector<std::shared_ptr<CPVREpgSearchFilter>> GetSavedSearches(bool bRadio);
+
+ /*!
+ * @brief Get the saved search matching the given id.
+ * @param bRadio Whether to fetch a TV or radio saved search.
+ * @param iId The id.
+ * @return The saved search or nullptr if not found.
+ */
+ std::shared_ptr<CPVREpgSearchFilter> GetSavedSearchById(bool bRadio, int iId);
+
+ /*!
+ * @brief Persist a search.
+ * @param epgSearch The search.
+ * @return True on success, false otherwise.
+ */
+ bool Persist(CPVREpgSearchFilter& epgSearch);
+
+ /*!
+ * @brief Update time last executed for the given search.
+ * @param epgSearch The search.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateSavedSearchLastExecuted(const CPVREpgSearchFilter& epgSearch);
+
+ /*!
+ * @brief Delete a saved search.
+ * @param epgSearch The search.
+ * @return True on success, false otherwise.
+ */
+ bool Delete(const CPVREpgSearchFilter& epgSearch);
+
+ /*!
+ * @brief Delete all saved searches.
+ * @return True on success, false otherwise.
+ */
+ bool DeleteSavedSearches();
+
+ //@}
+
+ private:
+ /*!
+ * @brief Create the EPG database tables.
+ */
+ void CreateTables() override;
+
+ /*!
+ * @brief Create the EPG database analytics.
+ */
+ void CreateAnalytics() override;
+
+ /*!
+ * @brief Update an old version of the database.
+ * @param version The version to update the database from.
+ */
+ void UpdateTables(int version) override;
+
+ int GetMinSchemaVersion() const override { return 4; }
+
+ std::shared_ptr<CPVREpgInfoTag> CreateEpgTag(const std::unique_ptr<dbiplus::Dataset>& pDS);
+
+ std::shared_ptr<CPVREpgSearchFilter> CreateEpgSearchFilter(
+ bool bRadio, const std::unique_ptr<dbiplus::Dataset>& pDS);
+
+ CCriticalSection m_critSection;
+ };
+}
diff --git a/xbmc/pvr/epg/EpgInfoTag.cpp b/xbmc/pvr/epg/EpgInfoTag.cpp
new file mode 100644
index 0000000..8b0930e
--- /dev/null
+++ b/xbmc/pvr/epg/EpgInfoTag.cpp
@@ -0,0 +1,681 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgInfoTag.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+const std::string CPVREpgInfoTag::IMAGE_OWNER_PATTERN = "epgtag_{}";
+
+CPVREpgInfoTag::CPVREpgInfoTag(int iEpgID, const std::string& iconPath)
+ : m_iUniqueBroadcastID(EPG_TAG_INVALID_UID),
+ m_iconPath(iconPath, StringUtils::Format(IMAGE_OWNER_PATTERN, iEpgID)),
+ m_iFlags(EPG_TAG_FLAG_UNDEFINED),
+ m_channelData(new CPVREpgChannelData),
+ m_iEpgID(iEpgID)
+{
+}
+
+CPVREpgInfoTag::CPVREpgInfoTag(const std::shared_ptr<CPVREpgChannelData>& channelData,
+ int iEpgID,
+ const CDateTime& start,
+ const CDateTime& end,
+ bool bIsGapTag)
+ : m_iUniqueBroadcastID(EPG_TAG_INVALID_UID),
+ m_iconPath(StringUtils::Format(IMAGE_OWNER_PATTERN, iEpgID)),
+ m_iFlags(EPG_TAG_FLAG_UNDEFINED),
+ m_bIsGapTag(bIsGapTag),
+ m_iEpgID(iEpgID)
+{
+ if (channelData)
+ m_channelData = channelData;
+ else
+ m_channelData = std::make_shared<CPVREpgChannelData>();
+
+ const CDateTimeSpan correction(
+ 0, 0, 0, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection);
+ m_startTime = start + correction;
+ m_endTime = end + correction;
+}
+
+CPVREpgInfoTag::CPVREpgInfoTag(const EPG_TAG& data,
+ int iClientId,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ int iEpgID)
+ : m_iGenreType(data.iGenreType),
+ m_iGenreSubType(data.iGenreSubType),
+ m_iParentalRating(data.iParentalRating),
+ m_iStarRating(data.iStarRating),
+ m_iSeriesNumber(data.iSeriesNumber),
+ m_iEpisodeNumber(data.iEpisodeNumber),
+ m_iEpisodePart(data.iEpisodePartNumber),
+ m_iUniqueBroadcastID(data.iUniqueBroadcastId),
+ m_iYear(data.iYear),
+ m_iconPath(data.strIconPath ? data.strIconPath : "",
+ StringUtils::Format(IMAGE_OWNER_PATTERN, iEpgID)),
+ m_startTime(
+ data.startTime +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection),
+ m_endTime(data.endTime +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection),
+ m_iFlags(data.iFlags),
+ m_iEpgID(iEpgID)
+{
+ // strFirstAired is optional, so check if supported before assigning it
+ if (data.strFirstAired && strlen(data.strFirstAired) > 0)
+ m_firstAired.SetFromW3CDate(data.strFirstAired);
+
+ if (channelData)
+ {
+ m_channelData = channelData;
+
+ if (m_channelData->ClientId() != iClientId)
+ CLog::LogF(LOGERROR, "Client id mismatch (channel: {}, epg: {})!", m_channelData->ClientId(),
+ iClientId);
+ if (m_channelData->UniqueClientChannelId() != static_cast<int>(data.iUniqueChannelId))
+ CLog::LogF(LOGERROR, "Channel uid mismatch (channel: {}, epg: {})!",
+ m_channelData->UniqueClientChannelId(), data.iUniqueChannelId);
+ }
+ else
+ {
+ // provide minimalistic channel data until we get fully initialized later
+ m_channelData = std::make_shared<CPVREpgChannelData>(iClientId, data.iUniqueChannelId);
+ }
+
+ // explicit NULL check, because there is no implicit NULL constructor for std::string
+ if (data.strTitle)
+ m_strTitle = data.strTitle;
+ if (data.strGenreDescription)
+ m_strGenreDescription = data.strGenreDescription;
+ if (data.strPlotOutline)
+ m_strPlotOutline = data.strPlotOutline;
+ if (data.strPlot)
+ m_strPlot = data.strPlot;
+ if (data.strOriginalTitle)
+ m_strOriginalTitle = data.strOriginalTitle;
+ if (data.strCast)
+ m_cast = Tokenize(data.strCast);
+ if (data.strDirector)
+ m_directors = Tokenize(data.strDirector);
+ if (data.strWriter)
+ m_writers = Tokenize(data.strWriter);
+ if (data.strIMDBNumber)
+ m_strIMDBNumber = data.strIMDBNumber;
+ if (data.strEpisodeName)
+ m_strEpisodeName = data.strEpisodeName;
+ if (data.strSeriesLink)
+ m_strSeriesLink = data.strSeriesLink;
+ if (data.strParentalRatingCode)
+ m_strParentalRatingCode = data.strParentalRatingCode;
+}
+
+void CPVREpgInfoTag::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (data)
+ m_channelData = data;
+ else
+ m_channelData.reset(new CPVREpgChannelData);
+}
+
+bool CPVREpgInfoTag::operator==(const CPVREpgInfoTag& right) const
+{
+ if (this == &right)
+ return true;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return (m_iUniqueBroadcastID == right.m_iUniqueBroadcastID && m_channelData &&
+ right.m_channelData &&
+ m_channelData->UniqueClientChannelId() == right.m_channelData->UniqueClientChannelId() &&
+ m_channelData->ClientId() == right.m_channelData->ClientId());
+}
+
+bool CPVREpgInfoTag::operator!=(const CPVREpgInfoTag& right) const
+{
+ if (this == &right)
+ return false;
+
+ return !(*this == right);
+}
+
+void CPVREpgInfoTag::Serialize(CVariant& value) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ value["broadcastid"] = m_iDatabaseID; // Use DB id here as it is unique across PVR clients
+ value["channeluid"] = m_channelData->UniqueClientChannelId();
+ value["parentalrating"] = m_iParentalRating;
+ value["parentalratingcode"] = m_strParentalRatingCode;
+ value["rating"] = m_iStarRating;
+ value["title"] = m_strTitle;
+ value["plotoutline"] = m_strPlotOutline;
+ value["plot"] = m_strPlot;
+ value["originaltitle"] = m_strOriginalTitle;
+ value["thumbnail"] = ClientIconPath();
+ value["cast"] = DeTokenize(m_cast);
+ value["director"] = DeTokenize(m_directors);
+ value["writer"] = DeTokenize(m_writers);
+ value["year"] = m_iYear;
+ value["imdbnumber"] = m_strIMDBNumber;
+ value["genre"] = Genre();
+ value["filenameandpath"] = Path();
+ value["starttime"] = m_startTime.IsValid() ? m_startTime.GetAsDBDateTime() : StringUtils::Empty;
+ value["endtime"] = m_endTime.IsValid() ? m_endTime.GetAsDBDateTime() : StringUtils::Empty;
+ value["runtime"] = GetDuration() / 60;
+ value["firstaired"] = m_firstAired.IsValid() ? m_firstAired.GetAsDBDate() : StringUtils::Empty;
+ value["progress"] = Progress();
+ value["progresspercentage"] = ProgressPercentage();
+ value["episodename"] = m_strEpisodeName;
+ value["episodenum"] = m_iEpisodeNumber;
+ value["episodepart"] = m_iEpisodePart;
+ value["seasonnum"] = m_iSeriesNumber;
+ value["isactive"] = IsActive();
+ value["wasactive"] = WasActive();
+ value["isseries"] = IsSeries();
+ value["serieslink"] = m_strSeriesLink;
+ value["clientid"] = m_channelData->ClientId();
+}
+
+int CPVREpgInfoTag::ClientID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->ClientId();
+}
+
+CDateTime CPVREpgInfoTag::GetCurrentPlayingTime() const
+{
+ return CServiceBroker::GetPVRManager().PlaybackState()->GetChannelPlaybackTime(ClientID(),
+ UniqueChannelID());
+}
+
+bool CPVREpgInfoTag::IsActive() const
+{
+ CDateTime now = GetCurrentPlayingTime();
+ return (m_startTime <= now && m_endTime > now);
+}
+
+bool CPVREpgInfoTag::WasActive() const
+{
+ CDateTime now = GetCurrentPlayingTime();
+ return (m_endTime < now);
+}
+
+bool CPVREpgInfoTag::IsUpcoming() const
+{
+ CDateTime now = GetCurrentPlayingTime();
+ return (m_startTime > now);
+}
+
+float CPVREpgInfoTag::ProgressPercentage() const
+{
+ float fReturn = 0.0f;
+
+ time_t currentTime, startTime, endTime;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(currentTime);
+ m_startTime.GetAsTime(startTime);
+ m_endTime.GetAsTime(endTime);
+ int iDuration = endTime - startTime > 0 ? endTime - startTime : 3600;
+
+ if (currentTime >= startTime && currentTime <= endTime)
+ fReturn = static_cast<float>(currentTime - startTime) * 100.0f / iDuration;
+ else if (currentTime > endTime)
+ fReturn = 100.0f;
+
+ return fReturn;
+}
+
+int CPVREpgInfoTag::Progress() const
+{
+ time_t currentTime, startTime;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(currentTime);
+ m_startTime.GetAsTime(startTime);
+ int iDuration = currentTime - startTime;
+
+ if (iDuration <= 0)
+ return 0;
+
+ return iDuration;
+}
+
+void CPVREpgInfoTag::SetUniqueBroadcastID(unsigned int iUniqueBroadcastID)
+{
+ m_iUniqueBroadcastID = iUniqueBroadcastID;
+}
+
+unsigned int CPVREpgInfoTag::UniqueBroadcastID() const
+{
+ return m_iUniqueBroadcastID;
+}
+
+int CPVREpgInfoTag::DatabaseID() const
+{
+ return m_iDatabaseID;
+}
+
+int CPVREpgInfoTag::UniqueChannelID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->UniqueClientChannelId();
+}
+
+std::string CPVREpgInfoTag::ChannelIconPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->ChannelIconPath();
+}
+
+CDateTime CPVREpgInfoTag::StartAsUTC() const
+{
+ return m_startTime;
+}
+
+CDateTime CPVREpgInfoTag::StartAsLocalTime() const
+{
+ CDateTime retVal;
+ retVal.SetFromUTCDateTime(m_startTime);
+ return retVal;
+}
+
+CDateTime CPVREpgInfoTag::EndAsUTC() const
+{
+ return m_endTime;
+}
+
+CDateTime CPVREpgInfoTag::EndAsLocalTime() const
+{
+ CDateTime retVal;
+ retVal.SetFromUTCDateTime(m_endTime);
+ return retVal;
+}
+
+void CPVREpgInfoTag::SetEndFromUTC(const CDateTime& end)
+{
+ m_endTime = end;
+}
+
+int CPVREpgInfoTag::GetDuration() const
+{
+ time_t start, end;
+ m_startTime.GetAsTime(start);
+ m_endTime.GetAsTime(end);
+ return end - start > 0 ? end - start : 3600;
+}
+
+std::string CPVREpgInfoTag::Title() const
+{
+ return m_strTitle;
+}
+
+std::string CPVREpgInfoTag::PlotOutline() const
+{
+ return m_strPlotOutline;
+}
+
+std::string CPVREpgInfoTag::Plot() const
+{
+ return m_strPlot;
+}
+
+std::string CPVREpgInfoTag::OriginalTitle() const
+{
+ return m_strOriginalTitle;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Cast() const
+{
+ return m_cast;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Directors() const
+{
+ return m_directors;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Writers() const
+{
+ return m_writers;
+}
+
+const std::string CPVREpgInfoTag::GetCastLabel() const
+{
+ // Note: see CVideoInfoTag::GetCast for reference implementation.
+ std::string strLabel;
+ for (const auto& castEntry : m_cast)
+ strLabel += StringUtils::Format("{}\n", castEntry);
+
+ return StringUtils::TrimRight(strLabel, "\n");
+}
+
+const std::string CPVREpgInfoTag::GetDirectorsLabel() const
+{
+ return StringUtils::Join(
+ m_directors,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+}
+
+const std::string CPVREpgInfoTag::GetWritersLabel() const
+{
+ return StringUtils::Join(
+ m_writers,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+}
+
+const std::string CPVREpgInfoTag::GetGenresLabel() const
+{
+ return StringUtils::Join(
+ Genre(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+}
+
+int CPVREpgInfoTag::Year() const
+{
+ return m_iYear;
+}
+
+std::string CPVREpgInfoTag::IMDBNumber() const
+{
+ return m_strIMDBNumber;
+}
+
+int CPVREpgInfoTag::GenreType() const
+{
+ return m_iGenreType;
+}
+
+int CPVREpgInfoTag::GenreSubType() const
+{
+ return m_iGenreSubType;
+}
+
+std::string CPVREpgInfoTag::GenreDescription() const
+{
+ return m_strGenreDescription;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Genre() const
+{
+ if (m_genre.empty())
+ {
+ if ((m_iGenreType == EPG_GENRE_USE_STRING || m_iGenreSubType == EPG_GENRE_USE_STRING) &&
+ !m_strGenreDescription.empty())
+ {
+ // Type and sub type are both not given. No EPG color coding possible unless sub type is
+ // used to specify EPG_GENRE_USE_STRING leaving type available for genre category, use the
+ // provided genre description for the text.
+ m_genre = Tokenize(m_strGenreDescription);
+ }
+
+ if (m_genre.empty())
+ {
+ // Determine the genre from the type and subtype IDs.
+ m_genre = Tokenize(CPVREpg::ConvertGenreIdToString(m_iGenreType, m_iGenreSubType));
+ }
+ }
+ return m_genre;
+}
+
+CDateTime CPVREpgInfoTag::FirstAired() const
+{
+ return m_firstAired;
+}
+
+int CPVREpgInfoTag::ParentalRating() const
+{
+ return m_iParentalRating;
+}
+
+std::string CPVREpgInfoTag::ParentalRatingCode() const
+{
+ return m_strParentalRatingCode;
+}
+
+int CPVREpgInfoTag::StarRating() const
+{
+ return m_iStarRating;
+}
+
+int CPVREpgInfoTag::SeriesNumber() const
+{
+ return m_iSeriesNumber;
+}
+
+std::string CPVREpgInfoTag::SeriesLink() const
+{
+ return m_strSeriesLink;
+}
+
+int CPVREpgInfoTag::EpisodeNumber() const
+{
+ return m_iEpisodeNumber;
+}
+
+int CPVREpgInfoTag::EpisodePart() const
+{
+ return m_iEpisodePart;
+}
+
+std::string CPVREpgInfoTag::EpisodeName() const
+{
+ return m_strEpisodeName;
+}
+
+std::string CPVREpgInfoTag::IconPath() const
+{
+ return m_iconPath.GetLocalImage();
+}
+
+std::string CPVREpgInfoTag::ClientIconPath() const
+{
+ return m_iconPath.GetClientImage();
+}
+
+std::string CPVREpgInfoTag::Path() const
+{
+ return StringUtils::Format("pvr://guide/{:04}/{}.epg", EpgID(), m_startTime.GetAsDBDateTime());
+}
+
+bool CPVREpgInfoTag::Update(const CPVREpgInfoTag& tag, bool bUpdateBroadcastId /* = true */)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bool bChanged =
+ (m_strTitle != tag.m_strTitle || m_strPlotOutline != tag.m_strPlotOutline ||
+ m_strPlot != tag.m_strPlot || m_strOriginalTitle != tag.m_strOriginalTitle ||
+ m_cast != tag.m_cast || m_directors != tag.m_directors || m_writers != tag.m_writers ||
+ m_iYear != tag.m_iYear || m_strIMDBNumber != tag.m_strIMDBNumber ||
+ m_startTime != tag.m_startTime || m_endTime != tag.m_endTime ||
+ m_iGenreType != tag.m_iGenreType || m_iGenreSubType != tag.m_iGenreSubType ||
+ m_strGenreDescription != tag.m_strGenreDescription || m_firstAired != tag.m_firstAired ||
+ m_iParentalRating != tag.m_iParentalRating ||
+ m_strParentalRatingCode != tag.m_strParentalRatingCode ||
+ m_iStarRating != tag.m_iStarRating || m_iEpisodeNumber != tag.m_iEpisodeNumber ||
+ m_iEpisodePart != tag.m_iEpisodePart || m_iSeriesNumber != tag.m_iSeriesNumber ||
+ m_strEpisodeName != tag.m_strEpisodeName ||
+ m_iUniqueBroadcastID != tag.m_iUniqueBroadcastID || m_iEpgID != tag.m_iEpgID ||
+ m_genre != tag.m_genre || m_iconPath != tag.m_iconPath || m_iFlags != tag.m_iFlags ||
+ m_strSeriesLink != tag.m_strSeriesLink || m_channelData != tag.m_channelData);
+
+ if (bUpdateBroadcastId)
+ bChanged |= (m_iDatabaseID != tag.m_iDatabaseID);
+
+ if (bChanged)
+ {
+ if (bUpdateBroadcastId)
+ m_iDatabaseID = tag.m_iDatabaseID;
+
+ m_strTitle = tag.m_strTitle;
+ m_strPlotOutline = tag.m_strPlotOutline;
+ m_strPlot = tag.m_strPlot;
+ m_strOriginalTitle = tag.m_strOriginalTitle;
+ m_cast = tag.m_cast;
+ m_directors = tag.m_directors;
+ m_writers = tag.m_writers;
+ m_iYear = tag.m_iYear;
+ m_strIMDBNumber = tag.m_strIMDBNumber;
+ m_startTime = tag.m_startTime;
+ m_endTime = tag.m_endTime;
+ m_iGenreType = tag.m_iGenreType;
+ m_iGenreSubType = tag.m_iGenreSubType;
+ m_strGenreDescription = tag.m_strGenreDescription;
+ m_genre = tag.m_genre;
+ m_iEpgID = tag.m_iEpgID;
+ m_iFlags = tag.m_iFlags;
+ m_strSeriesLink = tag.m_strSeriesLink;
+ m_firstAired = tag.m_firstAired;
+ m_iParentalRating = tag.m_iParentalRating;
+ m_strParentalRatingCode = tag.m_strParentalRatingCode;
+ m_iStarRating = tag.m_iStarRating;
+ m_iEpisodeNumber = tag.m_iEpisodeNumber;
+ m_iEpisodePart = tag.m_iEpisodePart;
+ m_iSeriesNumber = tag.m_iSeriesNumber;
+ m_strEpisodeName = tag.m_strEpisodeName;
+ m_iUniqueBroadcastID = tag.m_iUniqueBroadcastID;
+ m_iconPath = tag.m_iconPath;
+ m_channelData = tag.m_channelData;
+ }
+
+ return bChanged;
+}
+
+bool CPVREpgInfoTag::QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "Could not open the EPG database");
+ return false;
+ }
+
+ return database->QueuePersistQuery(*this);
+}
+
+std::vector<PVR_EDL_ENTRY> CPVREpgInfoTag::GetEdl() const
+{
+ std::vector<PVR_EDL_ENTRY> edls;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
+
+ if (client && client->GetClientCapabilities().SupportsEpgTagEdl())
+ client->GetEpgTagEdl(shared_from_this(), edls);
+
+ return edls;
+}
+
+int CPVREpgInfoTag::EpgID() const
+{
+ return m_iEpgID;
+}
+
+void CPVREpgInfoTag::SetEpgID(int iEpgID)
+{
+ m_iEpgID = iEpgID;
+ m_iconPath.SetOwner(StringUtils::Format(IMAGE_OWNER_PATTERN, m_iEpgID));
+}
+
+bool CPVREpgInfoTag::IsRecordable() const
+{
+ bool bIsRecordable = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
+ if (!client || (client->IsRecordable(shared_from_this(), bIsRecordable) != PVR_ERROR_NO_ERROR))
+ {
+ // event end time based fallback
+ bIsRecordable = EndAsLocalTime() > CDateTime::GetCurrentDateTime();
+ }
+ return bIsRecordable;
+}
+
+bool CPVREpgInfoTag::IsPlayable() const
+{
+ bool bIsPlayable = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
+ if (!client || (client->IsPlayable(shared_from_this(), bIsPlayable) != PVR_ERROR_NO_ERROR))
+ {
+ // fallback
+ bIsPlayable = false;
+ }
+ return bIsPlayable;
+}
+
+bool CPVREpgInfoTag::IsSeries() const
+{
+ if ((m_iFlags & EPG_TAG_FLAG_IS_SERIES) > 0 || SeriesNumber() >= 0 || EpisodeNumber() >= 0 ||
+ EpisodePart() >= 0)
+ return true;
+ else
+ return false;
+}
+
+bool CPVREpgInfoTag::IsRadio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->IsRadio();
+}
+
+bool CPVREpgInfoTag::IsParentalLocked() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->IsLocked();
+}
+
+bool CPVREpgInfoTag::IsGapTag() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsGapTag;
+}
+
+bool CPVREpgInfoTag::IsNew() const
+{
+ return (m_iFlags & EPG_TAG_FLAG_IS_NEW) > 0;
+}
+
+bool CPVREpgInfoTag::IsPremiere() const
+{
+ return (m_iFlags & EPG_TAG_FLAG_IS_PREMIERE) > 0;
+}
+
+bool CPVREpgInfoTag::IsFinale() const
+{
+ return (m_iFlags & EPG_TAG_FLAG_IS_FINALE) > 0;
+}
+
+bool CPVREpgInfoTag::IsLive() const
+{
+ return (m_iFlags & EPG_TAG_FLAG_IS_LIVE) > 0;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Tokenize(const std::string& str)
+{
+ return StringUtils::Split(str, EPG_STRING_TOKEN_SEPARATOR);
+}
+
+const std::string CPVREpgInfoTag::DeTokenize(const std::vector<std::string>& tokens)
+{
+ return StringUtils::Join(tokens, EPG_STRING_TOKEN_SEPARATOR);
+}
diff --git a/xbmc/pvr/epg/EpgInfoTag.h b/xbmc/pvr/epg/EpgInfoTag.h
new file mode 100644
index 0000000..89f2e0c
--- /dev/null
+++ b/xbmc/pvr/epg/EpgInfoTag.h
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "pvr/PVRCachedImage.h"
+#include "threads/CriticalSection.h"
+#include "utils/ISerializable.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+struct EPG_TAG;
+struct PVR_EDL_ENTRY;
+
+namespace PVR
+{
+class CPVREpgChannelData;
+class CPVREpgDatabase;
+
+class CPVREpgInfoTag final : public ISerializable,
+ public std::enable_shared_from_this<CPVREpgInfoTag>
+{
+ friend class CPVREpgDatabase;
+
+public:
+ static const std::string IMAGE_OWNER_PATTERN;
+
+ /*!
+ * @brief Create a new EPG infotag.
+ * @param data The tag's data.
+ * @param iClientId The client id.
+ * @param channelData The channel data.
+ * @param iEpgId The id of the EPG this tag belongs to.
+ */
+ CPVREpgInfoTag(const EPG_TAG& data,
+ int iClientId,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ int iEpgID);
+
+ /*!
+ * @brief Create a new EPG infotag.
+ * @param channelData The channel data.
+ * @param iEpgId The id of the EPG this tag belongs to.
+ * @param start The start time of the event
+ * @param end The end time of the event
+ * @param bIsGapTagTrue if this is a "gap" tag, false if this is a real EPG event
+ */
+ CPVREpgInfoTag(const std::shared_ptr<CPVREpgChannelData>& channelData,
+ int iEpgID,
+ const CDateTime& start,
+ const CDateTime& end,
+ bool bIsGapTag);
+
+ /*!
+ * @brief Set data for the channel linked to this EPG infotag.
+ * @param data The channel data.
+ */
+ void SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data);
+
+ bool operator==(const CPVREpgInfoTag& right) const;
+ bool operator!=(const CPVREpgInfoTag& right) const;
+
+ // ISerializable implementation
+ void Serialize(CVariant& value) const override;
+
+ /*!
+ * @brief Get the identifier of the client that serves this event.
+ * @return The identifier.
+ */
+ int ClientID() const;
+
+ /*!
+ * @brief Check if this event is currently active.
+ * @return True if it's active, false otherwise.
+ */
+ bool IsActive() const;
+
+ /*!
+ * @brief Check if this event is in the past.
+ * @return True when this event has already passed, false otherwise.
+ */
+ bool WasActive() const;
+
+ /*!
+ * @brief Check if this event is in the future.
+ * @return True when this event is an upcoming event, false otherwise.
+ */
+ bool IsUpcoming() const;
+
+ /*!
+ * @brief Get the progress of this tag in percent.
+ * @return The current progress of this tag.
+ */
+ float ProgressPercentage() const;
+
+ /*!
+ * @brief Get the progress of this tag in seconds.
+ * @return The current progress of this tag in seconds.
+ */
+ int Progress() const;
+
+ /*!
+ * @brief Get EPG ID of this tag.
+ * @return The epg ID.
+ */
+ int EpgID() const;
+
+ /*!
+ * @brief Sets the EPG id for this event.
+ * @param iEpgID The EPG id.
+ */
+ void SetEpgID(int iEpgID);
+
+ /*!
+ * @brief Change the unique broadcast ID of this event.
+ * @param iUniqueBroadcastId The new unique broadcast ID.
+ */
+ void SetUniqueBroadcastID(unsigned int iUniqueBroadcastID);
+
+ /*!
+ * @brief Get the unique broadcast ID.
+ * @return The unique broadcast ID.
+ */
+ unsigned int UniqueBroadcastID() const;
+
+ /*!
+ * @brief Get the event's database ID.
+ * @return The database ID.
+ */
+ int DatabaseID() const;
+
+ /*!
+ * @brief Get the unique ID of the channel associated with this event.
+ * @return The unique channel ID.
+ */
+ int UniqueChannelID() const;
+
+ /*!
+ * @brief Get the path for the icon of the channel associated with this event.
+ * @return The channel icon path.
+ */
+ std::string ChannelIconPath() const;
+
+ /*!
+ * @brief Get the event's start time.
+ * @return The start time in UTC.
+ */
+ CDateTime StartAsUTC() const;
+
+ /*!
+ * @brief Get the event's start time.
+ * @return The start time as local time.
+ */
+ CDateTime StartAsLocalTime() const;
+
+ /*!
+ * @brief Get the event's end time.
+ * @return The end time in UTC.
+ */
+ CDateTime EndAsUTC() const;
+
+ /*!
+ * @brief Get the event's end time.
+ * @return The end time as local time.
+ */
+ CDateTime EndAsLocalTime() const;
+
+ /*!
+ * @brief Change the event's end time.
+ * @param end The new end time.
+ */
+ void SetEndFromUTC(const CDateTime& end);
+
+ /*!
+ * @brief Get the duration of this event in seconds.
+ * @return The duration.
+ */
+ int GetDuration() const;
+
+ /*!
+ * @brief Get the title of this event.
+ * @return The title.
+ */
+ std::string Title() const;
+
+ /*!
+ * @brief Get the plot outline of this event.
+ * @return The plot outline.
+ */
+ std::string PlotOutline() const;
+
+ /*!
+ * @brief Get the plot of this event.
+ * @return The plot.
+ */
+ std::string Plot() const;
+
+ /*!
+ * @brief Get the original title of this event.
+ * @return The original title.
+ */
+ std::string OriginalTitle() const;
+
+ /*!
+ * @brief Get the cast of this event.
+ * @return The cast.
+ */
+ const std::vector<std::string> Cast() const;
+
+ /*!
+ * @brief Get the director(s) of this event.
+ * @return The director(s).
+ */
+ const std::vector<std::string> Directors() const;
+
+ /*!
+ * @brief Get the writer(s) of this event.
+ * @return The writer(s).
+ */
+ const std::vector<std::string> Writers() const;
+
+ /*!
+ * @brief Get the cast of this event as formatted string.
+ * @return The cast label.
+ */
+ const std::string GetCastLabel() const;
+
+ /*!
+ * @brief Get the director(s) of this event as formatted string.
+ * @return The directors label.
+ */
+ const std::string GetDirectorsLabel() const;
+
+ /*!
+ * @brief Get the writer(s) of this event as formatted string.
+ * @return The writers label.
+ */
+ const std::string GetWritersLabel() const;
+
+ /*!
+ * @brief Get the genre(s) of this event as formatted string.
+ * @return The genres label.
+ */
+ const std::string GetGenresLabel() const;
+
+ /*!
+ * @brief Get the year of this event.
+ * @return The year.
+ */
+ int Year() const;
+
+ /*!
+ * @brief Get the imdbnumber of this event.
+ * @return The imdbnumber.
+ */
+ std::string IMDBNumber() const;
+
+ /*!
+ * @brief Get the genre type ID of this event.
+ * @return The genre type ID.
+ */
+ int GenreType() const;
+
+ /*!
+ * @brief Get the genre subtype ID of this event.
+ * @return The genre subtype ID.
+ */
+ int GenreSubType() const;
+
+ /*!
+ * @brief Get the genre description of this event.
+ * @return The genre.
+ */
+ std::string GenreDescription() const;
+
+ /*!
+ * @brief Get the genre as human readable string.
+ * @return The genre.
+ */
+ const std::vector<std::string> Genre() const;
+
+ /*!
+ * @brief Get the first air date of this event.
+ * @return The first air date.
+ */
+ CDateTime FirstAired() const;
+
+ /*!
+ * @brief Get the parental rating of this event.
+ * @return The parental rating.
+ */
+ int ParentalRating() const;
+
+ /*!
+ * @brief Get the parental rating code of this event.
+ * @return The parental rating code.
+ */
+ std::string ParentalRatingCode() const;
+
+ /*!
+ * @brief Get the star rating of this event.
+ * @return The star rating.
+ */
+ int StarRating() const;
+
+ /*!
+ * @brief The series number of this event.
+ * @return The series number.
+ */
+ int SeriesNumber() const;
+
+ /*!
+ * @brief The series link for this event.
+ * @return The series link or empty string, if not available.
+ */
+ std::string SeriesLink() const;
+
+ /*!
+ * @brief The episode number of this event.
+ * @return The episode number.
+ */
+ int EpisodeNumber() const;
+
+ /*!
+ * @brief The episode part number of this event.
+ * @return The episode part number.
+ */
+ int EpisodePart() const;
+
+ /*!
+ * @brief The episode name of this event.
+ * @return The episode name.
+ */
+ std::string EpisodeName() const;
+
+ /*!
+ * @brief Get the path to the icon for this event used by Kodi.
+ * @return The path to the icon
+ */
+ std::string IconPath() const;
+
+ /*!
+ * @brief Get the path to the icon for this event as given by the client.
+ * @return The path to the icon
+ */
+ std::string ClientIconPath() const;
+
+ /*!
+ * @brief The path to this event.
+ * @return The path.
+ */
+ std::string Path() const;
+
+ /*!
+ * @brief Check if this event can be recorded.
+ * @return True if it can be recorded, false otherwise.
+ */
+ bool IsRecordable() const;
+
+ /*!
+ * @brief Check if this event can be played.
+ * @return True if it can be played, false otherwise.
+ */
+ bool IsPlayable() const;
+
+ /*!
+ * @brief Write query to persist this tag in the query queue of the given database.
+ * @param database The database.
+ * @return True on success, false otherwise.
+ */
+ bool QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Update the information in this tag with the info in the given tag.
+ * @param tag The new info.
+ * @param bUpdateBroadcastId If set to false, the tag BroadcastId (locally unique) will not be
+ * checked/updated
+ * @return True if something changed, false otherwise.
+ */
+ bool Update(const CPVREpgInfoTag& tag, bool bUpdateBroadcastId = true);
+
+ /*!
+ * @brief Retrieve the edit decision list (EDL) of an EPG tag.
+ * @return The edit decision list (empty on error)
+ */
+ std::vector<PVR_EDL_ENTRY> GetEdl() const;
+
+ /*!
+ * @brief Check whether this tag has any series attributes.
+ * @return True if this tag has any series attributes, false otherwise
+ */
+ bool IsSeries() const;
+
+ /*!
+ * @brief Check whether this tag is associated with a radion or TV channel.
+ * @return True if this tag is associated with a radio channel, false otherwise.
+ */
+ bool IsRadio() const;
+
+ /*!
+ * @brief Check whether this event is parental locked.
+ * @return True if whether this event is parental locked, false otherwise.
+ */
+ bool IsParentalLocked() const;
+
+ /*!
+ * @brief Check whether this event is a real event or a gap in the EPG timeline.
+ * @return True if this event is a gap, false otherwise.
+ */
+ bool IsGapTag() const;
+
+ /*!
+ * @brief Check whether this tag will be flagged as new.
+ * @return True if this tag will be flagged as new, false otherwise
+ */
+ bool IsNew() const;
+
+ /*!
+ * @brief Check whether this tag will be flagged as a premiere.
+ * @return True if this tag will be flagged as a premiere, false otherwise
+ */
+ bool IsPremiere() const;
+
+ /*!
+ * @brief Check whether this tag will be flagged as a finale.
+ * @return True if this tag will be flagged as a finale, false otherwise
+ */
+ bool IsFinale() const;
+
+ /*!
+ * @brief Check whether this tag will be flagged as live.
+ * @return True if this tag will be flagged as live, false otherwise
+ */
+ bool IsLive() const;
+
+ /*!
+ * @brief Return the flags (EPG_TAG_FLAG_*) of this event as a bitfield.
+ * @return the flags.
+ */
+ unsigned int Flags() const { return m_iFlags; }
+
+ /*!
+ * @brief Split the given string into tokens. Interprets occurrences of EPG_STRING_TOKEN_SEPARATOR
+ * in the string as separator.
+ * @param str The string to tokenize.
+ * @return the tokens.
+ */
+ static const std::vector<std::string> Tokenize(const std::string& str);
+
+ /*!
+ * @brief Combine the given strings to a single string. Inserts EPG_STRING_TOKEN_SEPARATOR as
+ * separator.
+ * @param tokens The tokens.
+ * @return the combined string.
+ */
+ static const std::string DeTokenize(const std::vector<std::string>& tokens);
+
+private:
+ CPVREpgInfoTag(int iEpgID, const std::string& iconPath);
+
+ CPVREpgInfoTag() = delete;
+ CPVREpgInfoTag(const CPVREpgInfoTag& tag) = delete;
+ CPVREpgInfoTag& operator=(const CPVREpgInfoTag& other) = delete;
+
+ /*!
+ * @brief Get current time, taking timeshifting into account.
+ * @return The playing time.
+ */
+ CDateTime GetCurrentPlayingTime() const;
+
+ int m_iDatabaseID = -1; /*!< database ID */
+ int m_iGenreType = 0; /*!< genre type */
+ int m_iGenreSubType = 0; /*!< genre subtype */
+ std::string m_strGenreDescription; /*!< genre description */
+ int m_iParentalRating = 0; /*!< parental rating */
+ std::string m_strParentalRatingCode; /*!< parental rating code */
+ int m_iStarRating = 0; /*!< star rating */
+ int m_iSeriesNumber = -1; /*!< series number */
+ int m_iEpisodeNumber = -1; /*!< episode number */
+ int m_iEpisodePart = -1; /*!< episode part number */
+ unsigned int m_iUniqueBroadcastID = 0; /*!< unique broadcast ID */
+ std::string m_strTitle; /*!< title */
+ std::string m_strPlotOutline; /*!< plot outline */
+ std::string m_strPlot; /*!< plot */
+ std::string m_strOriginalTitle; /*!< original title */
+ std::vector<std::string> m_cast; /*!< cast */
+ std::vector<std::string> m_directors; /*!< director(s) */
+ std::vector<std::string> m_writers; /*!< writer(s) */
+ int m_iYear = 0; /*!< year */
+ std::string m_strIMDBNumber; /*!< imdb number */
+ mutable std::vector<std::string> m_genre; /*!< genre */
+ std::string m_strEpisodeName; /*!< episode name */
+ CPVRCachedImage m_iconPath; /*!< the path to the icon */
+ CDateTime m_startTime; /*!< event start time */
+ CDateTime m_endTime; /*!< event end time */
+ CDateTime m_firstAired; /*!< first airdate */
+ unsigned int m_iFlags = 0; /*!< the flags applicable to this EPG entry */
+ std::string m_strSeriesLink; /*!< series link */
+ bool m_bIsGapTag = false;
+
+ mutable CCriticalSection m_critSection;
+ std::shared_ptr<CPVREpgChannelData> m_channelData;
+ int m_iEpgID = -1;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgSearchData.h b/xbmc/pvr/epg/EpgSearchData.h
new file mode 100644
index 0000000..e5e6ef7
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchData.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+
+#include <string>
+
+namespace PVR
+{
+
+static constexpr int EPG_SEARCH_UNSET = -1;
+
+struct PVREpgSearchData
+{
+ std::string m_strSearchTerm; /*!< The term to search for */
+ bool m_bSearchInDescription = false; /*!< Search for strSearchTerm in the description too */
+ bool m_bIncludeUnknownGenres = false; /*!< Whether to include unknown genres */
+ int m_iGenreType = EPG_SEARCH_UNSET; /*!< The genre type for an entry */
+ bool m_bIgnoreFinishedBroadcasts; /*!< True to ignore finished broadcasts, false if not */
+ bool m_bIgnoreFutureBroadcasts; /*!< True to ignore future broadcasts, false if not */
+ CDateTime m_startDateTime; /*!< The minimum start time for an entry */
+ CDateTime m_endDateTime; /*!< The maximum end time for an entry */
+
+ void Reset()
+ {
+ m_strSearchTerm.clear();
+ m_bSearchInDescription = false;
+ m_bIncludeUnknownGenres = false;
+ m_iGenreType = EPG_SEARCH_UNSET;
+ m_bIgnoreFinishedBroadcasts = true;
+ m_bIgnoreFutureBroadcasts = false;
+ m_startDateTime.SetValid(false);
+ m_endDateTime.SetValid(false);
+ }
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgSearchFilter.cpp b/xbmc/pvr/epg/EpgSearchFilter.cpp
new file mode 100644
index 0000000..e1cc35f
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchFilter.cpp
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgSearchFilter.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgSearchPath.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimers.h"
+#include "utils/TextSearch.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <memory>
+
+using namespace PVR;
+
+CPVREpgSearchFilter::CPVREpgSearchFilter(bool bRadio)
+: m_bIsRadio(bRadio)
+{
+ Reset();
+}
+
+void CPVREpgSearchFilter::Reset()
+{
+ m_searchData.Reset();
+ m_bEpgSearchDataFiltered = false;
+
+ m_bIsCaseSensitive = false;
+ m_iMinimumDuration = EPG_SEARCH_UNSET;
+ m_iMaximumDuration = EPG_SEARCH_UNSET;
+ m_bRemoveDuplicates = false;
+
+ /* pvr specific filters */
+ m_iClientID = -1;
+ m_iChannelGroupID = -1;
+ m_iChannelUID = -1;
+ m_bFreeToAirOnly = false;
+ m_bIgnorePresentTimers = true;
+ m_bIgnorePresentRecordings = true;
+
+ m_groupIdMatches.reset();
+
+ m_iDatabaseId = -1;
+ m_title.clear();
+ m_lastExecutedDateTime.SetValid(false);
+}
+
+std::string CPVREpgSearchFilter::GetPath() const
+{
+ return CPVREpgSearchPath(*this).GetPath();
+}
+
+void CPVREpgSearchFilter::SetSearchTerm(const std::string& strSearchTerm)
+{
+ if (m_searchData.m_strSearchTerm != strSearchTerm)
+ {
+ m_searchData.m_strSearchTerm = strSearchTerm;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetSearchPhrase(const std::string& strSearchPhrase)
+{
+ // match the exact phrase
+ SetSearchTerm("\"" + strSearchPhrase + "\"");
+}
+
+void CPVREpgSearchFilter::SetCaseSensitive(bool bIsCaseSensitive)
+{
+ if (m_bIsCaseSensitive != bIsCaseSensitive)
+ {
+ m_bIsCaseSensitive = bIsCaseSensitive;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetSearchInDescription(bool bSearchInDescription)
+{
+ if (m_searchData.m_bSearchInDescription != bSearchInDescription)
+ {
+ m_searchData.m_bSearchInDescription = bSearchInDescription;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetGenreType(int iGenreType)
+{
+ if (m_searchData.m_iGenreType != iGenreType)
+ {
+ m_searchData.m_iGenreType = iGenreType;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetMinimumDuration(int iMinimumDuration)
+{
+ if (m_iMinimumDuration != iMinimumDuration)
+ {
+ m_iMinimumDuration = iMinimumDuration;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetMaximumDuration(int iMaximumDuration)
+{
+ if (m_iMaximumDuration != iMaximumDuration)
+ {
+ m_iMaximumDuration = iMaximumDuration;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIgnoreFinishedBroadcasts(bool bIgnoreFinishedBroadcasts)
+{
+ if (m_searchData.m_bIgnoreFinishedBroadcasts != bIgnoreFinishedBroadcasts)
+ {
+ m_searchData.m_bIgnoreFinishedBroadcasts = bIgnoreFinishedBroadcasts;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIgnoreFutureBroadcasts(bool bIgnoreFutureBroadcasts)
+{
+ if (m_searchData.m_bIgnoreFutureBroadcasts != bIgnoreFutureBroadcasts)
+ {
+ m_searchData.m_bIgnoreFutureBroadcasts = bIgnoreFutureBroadcasts;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetStartDateTime(const CDateTime& startDateTime)
+{
+ if (m_searchData.m_startDateTime != startDateTime)
+ {
+ m_searchData.m_startDateTime = startDateTime;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetEndDateTime(const CDateTime& endDateTime)
+{
+ if (m_searchData.m_endDateTime != endDateTime)
+ {
+ m_searchData.m_endDateTime = endDateTime;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIncludeUnknownGenres(bool bIncludeUnknownGenres)
+{
+ if (m_searchData.m_bIncludeUnknownGenres != bIncludeUnknownGenres)
+ {
+ m_searchData.m_bIncludeUnknownGenres = bIncludeUnknownGenres;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetRemoveDuplicates(bool bRemoveDuplicates)
+{
+ if (m_bRemoveDuplicates != bRemoveDuplicates)
+ {
+ m_bRemoveDuplicates = bRemoveDuplicates;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetClientID(int iClientID)
+{
+ if (m_iClientID != iClientID)
+ {
+ m_iClientID = iClientID;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetChannelGroupID(int iChannelGroupID)
+{
+ if (m_iChannelGroupID != iChannelGroupID)
+ {
+ m_iChannelGroupID = iChannelGroupID;
+ m_groupIdMatches.reset();
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetChannelUID(int iChannelUID)
+{
+ if (m_iChannelUID != iChannelUID)
+ {
+ m_iChannelUID = iChannelUID;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetFreeToAirOnly(bool bFreeToAirOnly)
+{
+ if (m_bFreeToAirOnly != bFreeToAirOnly)
+ {
+ m_bFreeToAirOnly = bFreeToAirOnly;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIgnorePresentTimers(bool bIgnorePresentTimers)
+{
+ if (m_bIgnorePresentTimers != bIgnorePresentTimers)
+ {
+ m_bIgnorePresentTimers = bIgnorePresentTimers;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIgnorePresentRecordings(bool bIgnorePresentRecordings)
+{
+ if (m_bIgnorePresentRecordings != bIgnorePresentRecordings)
+ {
+ m_bIgnorePresentRecordings = bIgnorePresentRecordings;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetDatabaseId(int iDatabaseId)
+{
+ if (m_iDatabaseId != iDatabaseId)
+ {
+ m_iDatabaseId = iDatabaseId;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetTitle(const std::string& title)
+{
+ if (m_title != title)
+ {
+ m_title = title;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetLastExecutedDateTime(const CDateTime& lastExecutedDateTime)
+{
+ // Note: No need to set m_bChanged here
+ m_lastExecutedDateTime = lastExecutedDateTime;
+}
+
+bool CPVREpgSearchFilter::MatchGenre(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (m_bEpgSearchDataFiltered)
+ return true;
+
+ if (m_searchData.m_iGenreType != EPG_SEARCH_UNSET)
+ {
+ if (m_searchData.m_bIncludeUnknownGenres)
+ {
+ // match the exact genre and everything with unknown genre
+ return (tag->GenreType() == m_searchData.m_iGenreType ||
+ tag->GenreType() < EPG_EVENT_CONTENTMASK_MOVIEDRAMA ||
+ tag->GenreType() > EPG_EVENT_CONTENTMASK_USERDEFINED);
+ }
+ else
+ {
+ // match only the exact genre
+ return (tag->GenreType() == m_searchData.m_iGenreType);
+ }
+ }
+
+ // match any genre
+ return true;
+}
+
+bool CPVREpgSearchFilter::MatchDuration(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ bool bReturn(true);
+
+ if (m_iMinimumDuration != EPG_SEARCH_UNSET)
+ bReturn = (tag->GetDuration() > m_iMinimumDuration * 60);
+
+ if (bReturn && m_iMaximumDuration != EPG_SEARCH_UNSET)
+ bReturn = (tag->GetDuration() < m_iMaximumDuration * 60);
+
+ return bReturn;
+}
+
+bool CPVREpgSearchFilter::MatchStartAndEndTimes(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (m_bEpgSearchDataFiltered)
+ return true;
+
+ return ((!m_searchData.m_bIgnoreFinishedBroadcasts ||
+ tag->EndAsUTC() > CDateTime::GetUTCDateTime()) &&
+ (!m_searchData.m_bIgnoreFutureBroadcasts ||
+ tag->StartAsUTC() < CDateTime::GetUTCDateTime()) &&
+ (!m_searchData.m_startDateTime.IsValid() || // invalid => match any datetime
+ tag->StartAsUTC() >= m_searchData.m_startDateTime) &&
+ (!m_searchData.m_endDateTime.IsValid() || // invalid => match any datetime
+ tag->EndAsUTC() <= m_searchData.m_endDateTime));
+}
+
+bool CPVREpgSearchFilter::MatchSearchTerm(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ bool bReturn(true);
+
+ if (!m_searchData.m_strSearchTerm.empty())
+ {
+ bReturn = !CServiceBroker::GetPVRManager().IsParentalLocked(tag);
+ if (bReturn && (m_bIsCaseSensitive || !m_bEpgSearchDataFiltered))
+ {
+ CTextSearch search(m_searchData.m_strSearchTerm, m_bIsCaseSensitive, SEARCH_DEFAULT_OR);
+
+ bReturn = search.Search(tag->Title()) || search.Search(tag->PlotOutline()) ||
+ (m_searchData.m_bSearchInDescription && search.Search(tag->Plot()));
+ }
+ }
+
+ return bReturn;
+}
+
+bool CPVREpgSearchFilter::FilterEntry(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ return MatchGenre(tag) && MatchDuration(tag) && MatchStartAndEndTimes(tag) &&
+ MatchSearchTerm(tag) && MatchChannel(tag) && MatchChannelGroup(tag) && MatchTimers(tag) &&
+ MatchRecordings(tag) && MatchFreeToAir(tag);
+}
+
+void CPVREpgSearchFilter::RemoveDuplicates(std::vector<std::shared_ptr<CPVREpgInfoTag>>& results)
+{
+ for (auto it = results.begin(); it != results.end();)
+ {
+ it = results.erase(std::remove_if(results.begin(),
+ results.end(),
+ [&it](const std::shared_ptr<CPVREpgInfoTag>& entry)
+ {
+ return *it != entry &&
+ (*it)->Title() == entry->Title() &&
+ (*it)->Plot() == entry->Plot() &&
+ (*it)->PlotOutline() == entry->PlotOutline();
+ }),
+ results.end());
+ }
+}
+
+bool CPVREpgSearchFilter::MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ return tag && (tag->IsRadio() == m_bIsRadio) &&
+ (m_iClientID == -1 || tag->ClientID() == m_iClientID) &&
+ (m_iChannelUID == -1 || tag->UniqueChannelID() == m_iChannelUID) &&
+ CServiceBroker::GetPVRManager().Clients()->IsCreatedClient(tag->ClientID());
+}
+
+bool CPVREpgSearchFilter::MatchChannelGroup(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (m_iChannelGroupID != -1)
+ {
+ if (!m_groupIdMatches.has_value())
+ {
+ const std::shared_ptr<CPVRChannelGroup> group = CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(m_bIsRadio)
+ ->GetById(m_iChannelGroupID);
+ m_groupIdMatches =
+ group && (group->GetByUniqueID({tag->ClientID(), tag->UniqueChannelID()}) != nullptr);
+ }
+
+ return *m_groupIdMatches;
+ }
+
+ return true;
+}
+
+bool CPVREpgSearchFilter::MatchFreeToAir(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (m_bFreeToAirOnly)
+ {
+ const std::shared_ptr<CPVRChannel> channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(tag);
+ return channel && !channel->IsEncrypted();
+ }
+
+ return true;
+}
+
+bool CPVREpgSearchFilter::MatchTimers(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ return (!m_bIgnorePresentTimers || !CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(tag));
+}
+
+bool CPVREpgSearchFilter::MatchRecordings(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ return (!m_bIgnorePresentRecordings || !CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(tag));
+}
diff --git a/xbmc/pvr/epg/EpgSearchFilter.h b/xbmc/pvr/epg/EpgSearchFilter.h
new file mode 100644
index 0000000..aa6a2e5
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchFilter.h
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "pvr/epg/EpgSearchData.h"
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+ class CPVREpgInfoTag;
+
+ class CPVREpgSearchFilter
+ {
+ public:
+ CPVREpgSearchFilter() = delete;
+
+ /*!
+ * @brief ctor.
+ * @param bRadio the type of channels to search - if true, 'radio'. 'tv', otherwise.
+ */
+ explicit CPVREpgSearchFilter(bool bRadio);
+
+ /*!
+ * @brief Clear this filter.
+ */
+ void Reset();
+
+ /*!
+ * @brief Return the path for this filter.
+ * @return the path.
+ */
+ std::string GetPath() const;
+
+ /*!
+ * @brief Check if a tag will be filtered or not.
+ * @param tag The tag to check.
+ * @return True if this tag matches the filter, false if not.
+ */
+ bool FilterEntry(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+
+ /*!
+ * @brief remove duplicates from a list of epg tags.
+ * @param results The list of epg tags.
+ */
+ static void RemoveDuplicates(std::vector<std::shared_ptr<CPVREpgInfoTag>>& results);
+
+ /*!
+ * @brief Get the type of channels to search.
+ * @return true, if 'radio'. false, otherwise.
+ */
+ bool IsRadio() const { return m_bIsRadio; }
+
+ const std::string& GetSearchTerm() const { return m_searchData.m_strSearchTerm; }
+ void SetSearchTerm(const std::string& strSearchTerm);
+
+ void SetSearchPhrase(const std::string& strSearchPhrase);
+
+ bool IsCaseSensitive() const { return m_bIsCaseSensitive; }
+ void SetCaseSensitive(bool bIsCaseSensitive);
+
+ bool ShouldSearchInDescription() const { return m_searchData.m_bSearchInDescription; }
+ void SetSearchInDescription(bool bSearchInDescription);
+
+ int GetGenreType() const { return m_searchData.m_iGenreType; }
+ void SetGenreType(int iGenreType);
+
+ int GetMinimumDuration() const { return m_iMinimumDuration; }
+ void SetMinimumDuration(int iMinimumDuration);
+
+ int GetMaximumDuration() const { return m_iMaximumDuration; }
+ void SetMaximumDuration(int iMaximumDuration);
+
+ bool ShouldIgnoreFinishedBroadcasts() const { return m_searchData.m_bIgnoreFinishedBroadcasts; }
+ void SetIgnoreFinishedBroadcasts(bool bIgnoreFinishedBroadcasts);
+
+ bool ShouldIgnoreFutureBroadcasts() const { return m_searchData.m_bIgnoreFutureBroadcasts; }
+ void SetIgnoreFutureBroadcasts(bool bIgnoreFutureBroadcasts);
+
+ const CDateTime& GetStartDateTime() const { return m_searchData.m_startDateTime; }
+ void SetStartDateTime(const CDateTime& startDateTime);
+
+ const CDateTime& GetEndDateTime() const { return m_searchData.m_endDateTime; }
+ void SetEndDateTime(const CDateTime& endDateTime);
+
+ bool ShouldIncludeUnknownGenres() const { return m_searchData.m_bIncludeUnknownGenres; }
+ void SetIncludeUnknownGenres(bool bIncludeUnknownGenres);
+
+ bool ShouldRemoveDuplicates() const { return m_bRemoveDuplicates; }
+ void SetRemoveDuplicates(bool bRemoveDuplicates);
+
+ int GetClientID() const { return m_iClientID; }
+ void SetClientID(int iClientID);
+
+ int GetChannelGroupID() const { return m_iChannelGroupID; }
+ void SetChannelGroupID(int iChannelGroupID);
+
+ int GetChannelUID() const { return m_iChannelUID; }
+ void SetChannelUID(int iChannelUID);
+
+ bool IsFreeToAirOnly() const { return m_bFreeToAirOnly; }
+ void SetFreeToAirOnly(bool bFreeToAirOnly);
+
+ bool ShouldIgnorePresentTimers() const { return m_bIgnorePresentTimers; }
+ void SetIgnorePresentTimers(bool bIgnorePresentTimers);
+
+ bool ShouldIgnorePresentRecordings() const { return m_bIgnorePresentRecordings; }
+ void SetIgnorePresentRecordings(bool bIgnorePresentRecordings);
+
+ int GetDatabaseId() const { return m_iDatabaseId; }
+ void SetDatabaseId(int iDatabaseId);
+
+ const std::string& GetTitle() const { return m_title; }
+ void SetTitle(const std::string& title);
+
+ const CDateTime& GetLastExecutedDateTime() const { return m_lastExecutedDateTime; }
+ void SetLastExecutedDateTime(const CDateTime& lastExecutedDateTime);
+
+ const PVREpgSearchData& GetEpgSearchData() const { return m_searchData; }
+ void SetEpgSearchDataFiltered() { m_bEpgSearchDataFiltered = true; }
+
+ bool IsChanged() const { return m_bChanged; }
+ void SetChanged(bool bChanged) { m_bChanged = bChanged; }
+
+ private:
+ bool MatchGenre(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchDuration(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchStartAndEndTimes(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchSearchTerm(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchChannelGroup(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchFreeToAir(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchTimers(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchRecordings(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+
+ bool m_bChanged = false;
+
+ PVREpgSearchData m_searchData;
+ bool m_bEpgSearchDataFiltered = false;
+
+ bool m_bIsCaseSensitive; /*!< Do a case sensitive search */
+ int m_iMinimumDuration; /*!< The minimum duration for an entry */
+ int m_iMaximumDuration; /*!< The maximum duration for an entry */
+ bool m_bRemoveDuplicates; /*!< True to remove duplicate events, false if not */
+
+ // PVR specific filters
+ bool m_bIsRadio; /*!< True to filter radio channels only, false to tv only */
+ int m_iClientID = -1; /*!< The client id */
+ int m_iChannelGroupID{-1}; /*! The channel group id */
+ int m_iChannelUID = -1; /*!< The channel uid */
+ bool m_bFreeToAirOnly; /*!< Include free to air channels only */
+ bool m_bIgnorePresentTimers; /*!< True to ignore currently present timers (future recordings), false if not */
+ bool m_bIgnorePresentRecordings; /*!< True to ignore currently active recordings, false if not */
+
+ mutable std::optional<bool> m_groupIdMatches;
+
+ int m_iDatabaseId = -1;
+ std::string m_title;
+ CDateTime m_lastExecutedDateTime;
+ };
+}
diff --git a/xbmc/pvr/epg/EpgSearchPath.cpp b/xbmc/pvr/epg/EpgSearchPath.cpp
new file mode 100644
index 0000000..9b24e10
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchPath.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012-2021 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgSearchPath.h"
+
+#include "pvr/epg/EpgSearchFilter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <cstdlib>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+const std::string CPVREpgSearchPath::PATH_SEARCH_DIALOG = "pvr://search/search_dialog";
+const std::string CPVREpgSearchPath::PATH_TV_SEARCH = "pvr://search/tv/";
+const std::string CPVREpgSearchPath::PATH_TV_SAVEDSEARCHES = "pvr://search/tv/savedsearches/";
+const std::string CPVREpgSearchPath::PATH_RADIO_SEARCH = "pvr://search/radio/";
+const std::string CPVREpgSearchPath::PATH_RADIO_SAVEDSEARCHES = "pvr://search/radio/savedsearches/";
+
+CPVREpgSearchPath::CPVREpgSearchPath(const std::string& strPath)
+{
+ Init(strPath);
+}
+
+CPVREpgSearchPath::CPVREpgSearchPath(const CPVREpgSearchFilter& search)
+ : m_path(StringUtils::Format("pvr://search/{}/savedsearches/{}",
+ search.IsRadio() ? "radio" : "tv",
+ search.GetDatabaseId())),
+ m_bValid(true),
+ m_bRoot(false),
+ m_bRadio(search.IsRadio()),
+ m_bSavedSearchesRoot(false)
+{
+}
+
+bool CPVREpgSearchPath::Init(const std::string& strPath)
+{
+ std::string strVarPath(strPath);
+ URIUtils::RemoveSlashAtEnd(strVarPath);
+
+ m_path = strVarPath;
+ const std::vector<std::string> segments = URIUtils::SplitPath(m_path);
+
+ m_bValid =
+ ((segments.size() >= 3) && (segments.size() <= 5) && (segments.at(1) == "search") &&
+ ((segments.at(2) == "radio") || (segments.at(2) == "tv") || (segments.at(2) == "search")) &&
+ ((segments.size() == 3) || (segments.at(3) == "savedsearches")));
+ m_bRoot = (m_bValid && (segments.size() == 3) && (segments.at(2) != "search"));
+ m_bRadio = (m_bValid && (segments.at(2) == "radio"));
+ m_bSavedSearchesRoot =
+ (m_bValid && (segments.size() == 4) && (segments.at(3) == "savedsearches"));
+ m_bSavedSearch = (m_bValid && (segments.size() == 5));
+ if (m_bSavedSearch)
+ m_iId = std::stoi(segments.at(4));
+
+ return m_bValid;
+}
diff --git a/xbmc/pvr/epg/EpgSearchPath.h b/xbmc/pvr/epg/EpgSearchPath.h
new file mode 100644
index 0000000..559a3c6
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchPath.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012-2021 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace PVR
+{
+class CPVREpgSearchFilter;
+
+class CPVREpgSearchPath
+{
+public:
+ static const std::string PATH_SEARCH_DIALOG;
+ static const std::string PATH_TV_SEARCH;
+ static const std::string PATH_TV_SAVEDSEARCHES;
+ static const std::string PATH_RADIO_SEARCH;
+ static const std::string PATH_RADIO_SAVEDSEARCHES;
+
+ explicit CPVREpgSearchPath(const std::string& strPath);
+ explicit CPVREpgSearchPath(const CPVREpgSearchFilter& search);
+
+ bool IsValid() const { return m_bValid; }
+
+ const std::string& GetPath() const { return m_path; }
+ bool IsSearchRoot() const { return m_bRoot; }
+ bool IsRadio() const { return m_bRadio; }
+ bool IsSavedSearchesRoot() const { return m_bSavedSearchesRoot; }
+ bool IsSavedSearch() const { return m_bSavedSearch; }
+ int GetId() const { return m_iId; }
+
+private:
+ bool Init(const std::string& strPath);
+
+ std::string m_path;
+ bool m_bValid = false;
+ bool m_bRoot = false;
+ bool m_bRadio = false;
+ bool m_bSavedSearchesRoot = false;
+ bool m_bSavedSearch = false;
+ int m_iId = -1;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgTagsCache.cpp b/xbmc/pvr/epg/EpgTagsCache.cpp
new file mode 100644
index 0000000..2c4d111
--- /dev/null
+++ b/xbmc/pvr/epg/EpgTagsCache.cpp
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgTagsCache.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace PVR;
+
+namespace
+{
+const CDateTimeSpan ONE_SECOND(0, 0, 0, 1);
+}
+
+void CPVREpgTagsCache::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
+{
+ m_channelData = data;
+
+ if (m_lastEndedTag)
+ m_lastEndedTag->SetChannelData(data);
+ if (m_nowActiveTag)
+ m_nowActiveTag->SetChannelData(data);
+ if (m_nextStartingTag)
+ m_nextStartingTag->SetChannelData(data);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsCache::GetLastEndedTag()
+{
+ Refresh();
+ return m_lastEndedTag;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsCache::GetNowActiveTag()
+{
+ Refresh();
+ return m_nowActiveTag;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsCache::GetNextStartingTag()
+{
+ Refresh();
+ return m_nextStartingTag;
+}
+
+void CPVREpgTagsCache::Reset()
+{
+ m_lastEndedTag.reset();
+
+ m_nowActiveTag.reset();
+ m_nowActiveStart.Reset();
+ m_nowActiveEnd.Reset();
+
+ m_nextStartingTag.reset();
+}
+
+bool CPVREpgTagsCache::Refresh()
+{
+ const CDateTime activeTime =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetChannelPlaybackTime(
+ m_channelData->ClientId(), m_channelData->UniqueClientChannelId());
+
+ if (m_nowActiveStart.IsValid() && m_nowActiveEnd.IsValid() && m_nowActiveStart <= activeTime &&
+ m_nowActiveEnd > activeTime)
+ return false;
+
+ const std::shared_ptr<CPVREpgInfoTag> prevNowActiveTag = m_nowActiveTag;
+
+ m_lastEndedTag.reset();
+ m_nowActiveTag.reset();
+ m_nextStartingTag.reset();
+
+ const auto it =
+ std::find_if(m_changedTags.cbegin(), m_changedTags.cend(), [&activeTime](const auto& tag) {
+ return tag.second->StartAsUTC() <= activeTime && tag.second->EndAsUTC() > activeTime;
+ });
+
+ if (it != m_changedTags.cend())
+ {
+ m_nowActiveTag = (*it).second;
+ m_nowActiveStart = m_nowActiveTag->StartAsUTC();
+ m_nowActiveEnd = m_nowActiveTag->EndAsUTC();
+ }
+
+ if (!m_nowActiveTag && m_database)
+ {
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags =
+ m_database->GetEpgTagsByMinEndMaxStartTime(m_iEpgID, activeTime + ONE_SECOND, activeTime);
+ if (!tags.empty())
+ {
+ if (tags.size() > 1)
+ CLog::LogF(LOGWARNING, "Got multiple results. Picking up the first.");
+
+ m_nowActiveTag = tags.front();
+ m_nowActiveTag->SetChannelData(m_channelData);
+ m_nowActiveStart = m_nowActiveTag->StartAsUTC();
+ m_nowActiveEnd = m_nowActiveTag->EndAsUTC();
+ }
+ }
+
+ RefreshLastEndedTag(activeTime);
+ RefreshNextStartingTag(activeTime);
+
+ if (!m_nowActiveTag)
+ {
+ // we're in a gap. remember start and end time of that gap to avoid unneeded db load.
+ if (m_lastEndedTag)
+ m_nowActiveStart = m_lastEndedTag->EndAsUTC();
+ else
+ m_nowActiveStart = activeTime - CDateTimeSpan(1000, 0, 0, 0); // fake start far in the past
+
+ if (m_nextStartingTag)
+ m_nowActiveEnd = m_nextStartingTag->StartAsUTC();
+ else
+ m_nowActiveEnd = activeTime + CDateTimeSpan(1000, 0, 0, 0); // fake end far in the future
+ }
+
+ const bool tagChanged =
+ m_nowActiveTag && (!prevNowActiveTag || *prevNowActiveTag != *m_nowActiveTag);
+ const bool tagRemoved = !m_nowActiveTag && prevNowActiveTag;
+
+ return (tagChanged || tagRemoved);
+}
+
+void CPVREpgTagsCache::RefreshLastEndedTag(const CDateTime& activeTime)
+{
+ if (m_database)
+ {
+ m_lastEndedTag = m_database->GetEpgTagByMaxEndTime(m_iEpgID, activeTime);
+ if (m_lastEndedTag)
+ m_lastEndedTag->SetChannelData(m_channelData);
+ }
+
+ for (auto it = m_changedTags.rbegin(); it != m_changedTags.rend(); ++it)
+ {
+ if (it->second->WasActive())
+ {
+ if (!m_lastEndedTag || m_lastEndedTag->EndAsUTC() < it->second->EndAsUTC())
+ {
+ m_lastEndedTag = it->second;
+ break;
+ }
+ }
+ }
+}
+
+void CPVREpgTagsCache::RefreshNextStartingTag(const CDateTime& activeTime)
+{
+ if (m_database)
+ {
+ m_nextStartingTag = m_database->GetEpgTagByMinStartTime(m_iEpgID, activeTime + ONE_SECOND);
+ if (m_nextStartingTag)
+ m_nextStartingTag->SetChannelData(m_channelData);
+ }
+
+ for (const auto& tag : m_changedTags)
+ {
+ if (tag.second->IsUpcoming())
+ {
+ if (!m_nextStartingTag || m_nextStartingTag->StartAsUTC() > tag.second->StartAsUTC())
+ {
+ m_nextStartingTag = tag.second;
+ break;
+ }
+ }
+ }
+}
diff --git a/xbmc/pvr/epg/EpgTagsCache.h b/xbmc/pvr/epg/EpgTagsCache.h
new file mode 100644
index 0000000..f3505bc
--- /dev/null
+++ b/xbmc/pvr/epg/EpgTagsCache.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+
+#include <map>
+#include <memory>
+
+namespace PVR
+{
+class CPVREpgChannelData;
+class CPVREpgDatabase;
+class CPVREpgInfoTag;
+
+class CPVREpgTagsCache
+{
+public:
+ CPVREpgTagsCache() = delete;
+ CPVREpgTagsCache(int iEpgID,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database,
+ const std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& changedTags)
+ : m_iEpgID(iEpgID), m_channelData(channelData), m_database(database), m_changedTags(changedTags)
+ {
+ }
+
+ void SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data);
+
+ void Reset();
+
+ bool Refresh();
+
+ std::shared_ptr<CPVREpgInfoTag> GetLastEndedTag();
+ std::shared_ptr<CPVREpgInfoTag> GetNowActiveTag();
+ std::shared_ptr<CPVREpgInfoTag> GetNextStartingTag();
+
+private:
+ void RefreshLastEndedTag(const CDateTime& activeTime);
+ void RefreshNextStartingTag(const CDateTime& activeTime);
+
+ int m_iEpgID;
+ std::shared_ptr<CPVREpgChannelData> m_channelData;
+ std::shared_ptr<CPVREpgDatabase> m_database;
+ const std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& m_changedTags;
+
+ std::shared_ptr<CPVREpgInfoTag> m_lastEndedTag;
+ std::shared_ptr<CPVREpgInfoTag> m_nowActiveTag;
+ std::shared_ptr<CPVREpgInfoTag> m_nextStartingTag;
+
+ CDateTime m_nowActiveStart;
+ CDateTime m_nowActiveEnd;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgTagsContainer.cpp b/xbmc/pvr/epg/EpgTagsContainer.cpp
new file mode 100644
index 0000000..b4a778c
--- /dev/null
+++ b/xbmc/pvr/epg/EpgTagsContainer.cpp
@@ -0,0 +1,668 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgTagsContainer.h"
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgTagsCache.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+
+using namespace PVR;
+
+namespace
+{
+const CDateTimeSpan ONE_SECOND(0, 0, 0, 1);
+}
+
+CPVREpgTagsContainer::CPVREpgTagsContainer(int iEpgID,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database)
+ : m_iEpgID(iEpgID),
+ m_channelData(channelData),
+ m_database(database),
+ m_tagsCache(new CPVREpgTagsCache(iEpgID, channelData, database, m_changedTags))
+{
+}
+
+CPVREpgTagsContainer::~CPVREpgTagsContainer() = default;
+
+void CPVREpgTagsContainer::SetEpgID(int iEpgID)
+{
+ m_iEpgID = iEpgID;
+ for (const auto& tag : m_changedTags)
+ tag.second->SetEpgID(iEpgID);
+}
+
+void CPVREpgTagsContainer::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
+{
+ m_channelData = data;
+ m_tagsCache->SetChannelData(data);
+ for (const auto& tag : m_changedTags)
+ tag.second->SetChannelData(data);
+}
+
+namespace
+{
+
+void ResolveConflictingTags(const std::shared_ptr<CPVREpgInfoTag>& changedTag,
+ std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags)
+{
+ const CDateTime changedTagStart = changedTag->StartAsUTC();
+ const CDateTime changedTagEnd = changedTag->EndAsUTC();
+
+ for (auto it = tags.begin(); it != tags.end();)
+ {
+ bool bInsert = false;
+
+ if (changedTagEnd > (*it)->StartAsUTC() && changedTagStart < (*it)->EndAsUTC())
+ {
+ it = tags.erase(it);
+
+ if (it == tags.end())
+ {
+ bInsert = true;
+ }
+ }
+ else if ((*it)->StartAsUTC() >= changedTagEnd)
+ {
+ bInsert = true;
+ }
+ else
+ {
+ ++it;
+ }
+
+ if (bInsert)
+ {
+ tags.emplace(it, changedTag);
+ break;
+ }
+ }
+}
+
+bool FixOverlap(const std::shared_ptr<CPVREpgInfoTag>& previousTag,
+ const std::shared_ptr<CPVREpgInfoTag>& currentTag)
+{
+ if (!previousTag)
+ return true;
+
+ if (previousTag->EndAsUTC() >= currentTag->EndAsUTC())
+ {
+ // delete the current tag. it's completely overlapped
+ CLog::LogF(LOGDEBUG,
+ "Erasing completely overlapped event from EPG timeline "
+ "({} - {} - {} - {}) "
+ "({} - {} - {} - {}).",
+ previousTag->UniqueBroadcastID(), previousTag->Title(),
+ previousTag->StartAsUTC().GetAsDBDateTime(),
+ previousTag->EndAsUTC().GetAsDBDateTime(), currentTag->UniqueBroadcastID(),
+ currentTag->Title(), currentTag->StartAsUTC().GetAsDBDateTime(),
+ currentTag->EndAsUTC().GetAsDBDateTime());
+
+ return false;
+ }
+ else if (previousTag->EndAsUTC() > currentTag->StartAsUTC())
+ {
+ // fix the end time of the predecessor of the event
+ CLog::LogF(LOGDEBUG,
+ "Fixing partly overlapped event in EPG timeline "
+ "({} - {} - {} - {}) "
+ "({} - {} - {} - {}).",
+ previousTag->UniqueBroadcastID(), previousTag->Title(),
+ previousTag->StartAsUTC().GetAsDBDateTime(),
+ previousTag->EndAsUTC().GetAsDBDateTime(), currentTag->UniqueBroadcastID(),
+ currentTag->Title(), currentTag->StartAsUTC().GetAsDBDateTime(),
+ currentTag->EndAsUTC().GetAsDBDateTime());
+
+ previousTag->SetEndFromUTC(currentTag->StartAsUTC());
+ }
+ return true;
+}
+
+} // unnamed namespace
+
+bool CPVREpgTagsContainer::UpdateEntries(const CPVREpgTagsContainer& tags)
+{
+ if (tags.m_changedTags.empty())
+ return false;
+
+ if (m_database)
+ {
+ const CDateTime minEventEnd = (*tags.m_changedTags.cbegin()).second->StartAsUTC() + ONE_SECOND;
+ const CDateTime maxEventStart = (*tags.m_changedTags.crbegin()).second->EndAsUTC();
+
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> existingTags =
+ m_database->GetEpgTagsByMinEndMaxStartTime(m_iEpgID, minEventEnd, maxEventStart);
+
+ if (!m_changedTags.empty())
+ {
+ // Fix data inconsistencies
+ for (const auto& changedTagsEntry : m_changedTags)
+ {
+ const auto& changedTag = changedTagsEntry.second;
+
+ if (changedTag->EndAsUTC() > minEventEnd && changedTag->StartAsUTC() < maxEventStart)
+ {
+ // tag is in queried range, thus it could cause inconsistencies...
+ ResolveConflictingTags(changedTag, existingTags);
+ }
+ }
+ }
+
+ bool bResetCache = false;
+ for (const auto& tagsEntry : tags.m_changedTags)
+ {
+ const auto& tag = tagsEntry.second;
+
+ tag->SetChannelData(m_channelData);
+ tag->SetEpgID(m_iEpgID);
+
+ const auto it =
+ std::find_if(existingTags.cbegin(), existingTags.cend(),
+ [&tag](const auto& t) { return t->StartAsUTC() == tag->StartAsUTC(); });
+
+ if (it != existingTags.cend())
+ {
+ const std::shared_ptr<CPVREpgInfoTag>& existingTag = *it;
+
+ existingTag->SetChannelData(m_channelData);
+ existingTag->SetEpgID(m_iEpgID);
+
+ if (existingTag->Update(*tag, false))
+ {
+ // tag differs from existing tag and must be persisted
+ m_changedTags.insert({existingTag->StartAsUTC(), existingTag});
+ bResetCache = true;
+ }
+ }
+ else
+ {
+ // new tags must always be persisted
+ m_changedTags.insert({tag->StartAsUTC(), tag});
+ bResetCache = true;
+ }
+ }
+
+ if (bResetCache)
+ m_tagsCache->Reset();
+ }
+ else
+ {
+ for (const auto& tag : tags.m_changedTags)
+ UpdateEntry(tag.second);
+ }
+
+ return true;
+}
+
+void CPVREpgTagsContainer::FixOverlappingEvents(
+ std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const
+{
+ bool bResetCache = false;
+
+ std::shared_ptr<CPVREpgInfoTag> previousTag;
+ for (auto it = tags.begin(); it != tags.end();)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> currentTag = *it;
+ if (FixOverlap(previousTag, currentTag))
+ {
+ previousTag = currentTag;
+ ++it;
+ }
+ else
+ {
+ it = tags.erase(it);
+ bResetCache = true;
+ }
+ }
+
+ if (bResetCache)
+ m_tagsCache->Reset();
+}
+
+void CPVREpgTagsContainer::FixOverlappingEvents(
+ std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& tags) const
+{
+ bool bResetCache = false;
+
+ std::shared_ptr<CPVREpgInfoTag> previousTag;
+ for (auto it = tags.begin(); it != tags.end();)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> currentTag = (*it).second;
+ if (FixOverlap(previousTag, currentTag))
+ {
+ previousTag = currentTag;
+ ++it;
+ }
+ else
+ {
+ it = tags.erase(it);
+ bResetCache = true;
+ }
+ }
+
+ if (bResetCache)
+ m_tagsCache->Reset();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::CreateEntry(
+ const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (tag)
+ {
+ tag->SetChannelData(m_channelData);
+ }
+ return tag;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgTagsContainer::CreateEntries(
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const
+{
+ for (auto& tag : tags)
+ {
+ tag->SetChannelData(m_channelData);
+ }
+ return tags;
+}
+
+bool CPVREpgTagsContainer::UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ tag->SetChannelData(m_channelData);
+ tag->SetEpgID(m_iEpgID);
+
+ std::shared_ptr<CPVREpgInfoTag> existingTag = GetTag(tag->StartAsUTC());
+ if (existingTag)
+ {
+ if (existingTag->Update(*tag, false))
+ {
+ // tag differs from existing tag and must be persisted
+ m_changedTags.insert({existingTag->StartAsUTC(), existingTag});
+ m_tagsCache->Reset();
+ }
+ }
+ else
+ {
+ // new tags must always be persisted
+ m_changedTags.insert({tag->StartAsUTC(), tag});
+ m_tagsCache->Reset();
+ }
+
+ return true;
+}
+
+bool CPVREpgTagsContainer::DeleteEntry(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ m_changedTags.erase(tag->StartAsUTC());
+ m_deletedTags.insert({tag->StartAsUTC(), tag});
+ m_tagsCache->Reset();
+ return true;
+}
+
+void CPVREpgTagsContainer::Cleanup(const CDateTime& time)
+{
+ bool bResetCache = false;
+ for (auto it = m_changedTags.begin(); it != m_changedTags.end();)
+ {
+ if (it->second->EndAsUTC() < time)
+ {
+ const auto it1 = m_deletedTags.find(it->first);
+ if (it1 != m_deletedTags.end())
+ m_deletedTags.erase(it1);
+
+ it = m_changedTags.erase(it);
+ bResetCache = true;
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ if (bResetCache)
+ m_tagsCache->Reset();
+
+ if (m_database)
+ m_database->DeleteEpgTags(m_iEpgID, time);
+}
+
+void CPVREpgTagsContainer::Clear()
+{
+ m_changedTags.clear();
+ m_tagsCache->Reset();
+}
+
+bool CPVREpgTagsContainer::IsEmpty() const
+{
+ if (!m_changedTags.empty())
+ return false;
+
+ if (m_database)
+ return !m_database->HasTags(m_iEpgID);
+
+ return true;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTag(const CDateTime& startTime) const
+{
+ const auto it = m_changedTags.find(startTime);
+ if (it != m_changedTags.cend())
+ return (*it).second;
+
+ if (m_database)
+ return CreateEntry(m_database->GetEpgTagByStartTime(m_iEpgID, startTime));
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTag(unsigned int iUniqueBroadcastID) const
+{
+ if (iUniqueBroadcastID == EPG_TAG_INVALID_UID)
+ return {};
+
+ const auto it = std::find_if(m_changedTags.cbegin(), m_changedTags.cend(),
+ [iUniqueBroadcastID](const auto& tag) {
+ return tag.second->UniqueBroadcastID() == iUniqueBroadcastID;
+ });
+
+ if (it != m_changedTags.cend())
+ return (*it).second;
+
+ if (m_database)
+ return CreateEntry(m_database->GetEpgTagByUniqueBroadcastID(m_iEpgID, iUniqueBroadcastID));
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTagByDatabaseID(int iDatabaseID) const
+{
+ if (iDatabaseID <= 0)
+ return {};
+
+ const auto it =
+ std::find_if(m_changedTags.cbegin(), m_changedTags.cend(), [iDatabaseID](const auto& tag) {
+ return tag.second->DatabaseID() == iDatabaseID;
+ });
+
+ if (it != m_changedTags.cend())
+ return (*it).second;
+
+ if (m_database)
+ return CreateEntry(m_database->GetEpgTagByDatabaseID(m_iEpgID, iDatabaseID));
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTagBetween(const CDateTime& start,
+ const CDateTime& end) const
+{
+ for (const auto& tag : m_changedTags)
+ {
+ if (tag.second->StartAsUTC() >= start)
+ {
+ if (tag.second->EndAsUTC() <= end)
+ return tag.second;
+ else
+ break;
+ }
+ }
+
+ if (m_database)
+ {
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags =
+ CreateEntries(m_database->GetEpgTagsByMinStartMaxEndTime(m_iEpgID, start, end));
+ if (!tags.empty())
+ {
+ if (tags.size() > 1)
+ CLog::LogF(LOGWARNING, "Got multiple tags. Picking up the first.");
+
+ return tags.front();
+ }
+ }
+
+ return {};
+}
+
+bool CPVREpgTagsContainer::UpdateActiveTag()
+{
+ return m_tagsCache->Refresh();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetActiveTag() const
+{
+ return m_tagsCache->GetNowActiveTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetLastEndedTag() const
+{
+ return m_tagsCache->GetLastEndedTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetNextStartingTag() const
+{
+ return m_tagsCache->GetNextStartingTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::CreateGapTag(const CDateTime& start,
+ const CDateTime& end) const
+{
+ return std::make_shared<CPVREpgInfoTag>(m_channelData, m_iEpgID, start, end, true);
+}
+
+void CPVREpgTagsContainer::MergeTags(const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart,
+ std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const
+{
+ for (const auto& changedTagsEntry : m_changedTags)
+ {
+ const auto& changedTag = changedTagsEntry.second;
+
+ if (changedTag->EndAsUTC() > minEventEnd && changedTag->StartAsUTC() < maxEventStart)
+ tags.emplace_back(changedTag);
+ }
+
+ if (!tags.empty())
+ FixOverlappingEvents(tags);
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgTagsContainer::GetTimeline(
+ const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const
+{
+ if (m_database)
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+
+ bool loadFromDb = true;
+ if (!m_changedTags.empty())
+ {
+ const CDateTime lastEnd = m_database->GetLastEndTime(m_iEpgID);
+ if (!lastEnd.IsValid() || lastEnd < minEventEnd)
+ {
+ // nothing in the db yet. take what we have in memory.
+ loadFromDb = false;
+ MergeTags(minEventEnd, maxEventStart, tags);
+ }
+ }
+
+ if (loadFromDb)
+ {
+ tags = m_database->GetEpgTagsByMinEndMaxStartTime(m_iEpgID, minEventEnd, maxEventStart);
+
+ if (!m_changedTags.empty())
+ {
+ // Fix data inconsistencies
+ for (const auto& changedTagsEntry : m_changedTags)
+ {
+ const auto& changedTag = changedTagsEntry.second;
+
+ if (changedTag->EndAsUTC() > minEventEnd && changedTag->StartAsUTC() < maxEventStart)
+ {
+ // tag is in queried range, thus it could cause inconsistencies...
+ ResolveConflictingTags(changedTag, tags);
+ }
+ }
+
+ // Append missing tags
+ MergeTags(tags.empty() ? minEventEnd : tags.back()->EndAsUTC(), maxEventStart, tags);
+ }
+ }
+
+ tags = CreateEntries(tags);
+
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> result;
+
+ for (const auto& epgTag : tags)
+ {
+ if (!result.empty())
+ {
+ const CDateTime currStart = epgTag->StartAsUTC();
+ const CDateTime prevEnd = result.back()->EndAsUTC();
+ if ((currStart - prevEnd) >= ONE_SECOND)
+ {
+ // insert gap tag before current tag
+ result.emplace_back(CreateGapTag(prevEnd, currStart));
+ }
+ }
+
+ result.emplace_back(epgTag);
+ }
+
+ if (result.empty())
+ {
+ // create single gap tag
+ CDateTime maxEnd = m_database->GetMaxEndTime(m_iEpgID, minEventEnd);
+ if (!maxEnd.IsValid() || maxEnd < timelineStart)
+ maxEnd = timelineStart;
+
+ CDateTime minStart = m_database->GetMinStartTime(m_iEpgID, maxEventStart);
+ if (!minStart.IsValid() || minStart > timelineEnd)
+ minStart = timelineEnd;
+
+ result.emplace_back(CreateGapTag(maxEnd, minStart));
+ }
+ else
+ {
+ if (result.front()->StartAsUTC() > minEventEnd)
+ {
+ // prepend gap tag
+ CDateTime maxEnd = m_database->GetMaxEndTime(m_iEpgID, minEventEnd);
+ if (!maxEnd.IsValid() || maxEnd < timelineStart)
+ maxEnd = timelineStart;
+
+ result.insert(result.begin(), CreateGapTag(maxEnd, result.front()->StartAsUTC()));
+ }
+
+ if (result.back()->EndAsUTC() < maxEventStart)
+ {
+ // append gap tag
+ CDateTime minStart = m_database->GetMinStartTime(m_iEpgID, maxEventStart);
+ if (!minStart.IsValid() || minStart > timelineEnd)
+ minStart = timelineEnd;
+
+ result.emplace_back(CreateGapTag(result.back()->EndAsUTC(), minStart));
+ }
+ }
+
+ return result;
+ }
+
+ return {};
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgTagsContainer::GetAllTags() const
+{
+ if (m_database)
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ if (!m_changedTags.empty() && !m_database->HasTags(m_iEpgID))
+ {
+ // nothing in the db yet. take what we have in memory.
+ std::transform(m_changedTags.cbegin(), m_changedTags.cend(), std::back_inserter(tags),
+ [](const auto& tag) { return tag.second; });
+
+ FixOverlappingEvents(tags);
+ }
+ else
+ {
+ tags = m_database->GetAllEpgTags(m_iEpgID);
+
+ if (!m_changedTags.empty())
+ {
+ // Fix data inconsistencies
+ for (const auto& changedTagsEntry : m_changedTags)
+ {
+ ResolveConflictingTags(changedTagsEntry.second, tags);
+ }
+ }
+ }
+
+ return CreateEntries(tags);
+ }
+
+ return {};
+}
+
+std::pair<CDateTime, CDateTime> CPVREpgTagsContainer::GetFirstAndLastUncommitedEPGDate() const
+{
+ if (m_changedTags.empty())
+ return {};
+
+ return {(*m_changedTags.cbegin()).second->StartAsUTC(),
+ (*m_changedTags.crbegin()).second->EndAsUTC()};
+}
+
+bool CPVREpgTagsContainer::NeedsSave() const
+{
+ return !m_changedTags.empty() || !m_deletedTags.empty();
+}
+
+void CPVREpgTagsContainer::QueuePersistQuery()
+{
+ if (m_database)
+ {
+ m_database->Lock();
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Tags Container: Updating {}, deleting {} events...",
+ m_changedTags.size(), m_deletedTags.size());
+
+ for (const auto& tag : m_deletedTags)
+ m_database->QueueDeleteTagQuery(*tag.second);
+
+ m_deletedTags.clear();
+
+ FixOverlappingEvents(m_changedTags);
+
+ for (const auto& tag : m_changedTags)
+ {
+ // remove any conflicting events from database before persisting the new event
+ m_database->QueueDeleteEpgTagsByMinEndMaxStartTimeQuery(
+ m_iEpgID, tag.second->StartAsUTC() + ONE_SECOND, tag.second->EndAsUTC() - ONE_SECOND);
+
+ tag.second->QueuePersistQuery(m_database);
+ }
+
+ Clear();
+
+ m_database->Unlock();
+ }
+}
+
+void CPVREpgTagsContainer::QueueDelete()
+{
+ if (m_database)
+ m_database->QueueDeleteEpgTags(m_iEpgID);
+
+ Clear();
+}
diff --git a/xbmc/pvr/epg/EpgTagsContainer.h b/xbmc/pvr/epg/EpgTagsContainer.h
new file mode 100644
index 0000000..fcbe0d1
--- /dev/null
+++ b/xbmc/pvr/epg/EpgTagsContainer.h
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+
+#include <map>
+#include <memory>
+#include <vector>
+
+namespace PVR
+{
+class CPVREpgTagsCache;
+class CPVREpgChannelData;
+class CPVREpgDatabase;
+class CPVREpgInfoTag;
+
+class CPVREpgTagsContainer
+{
+public:
+ CPVREpgTagsContainer() = delete;
+ CPVREpgTagsContainer(int iEpgID,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database);
+ virtual ~CPVREpgTagsContainer();
+
+ /*!
+ * @brief Set the EPG id for this EPG.
+ * @param iEpgID The ID.
+ */
+ void SetEpgID(int iEpgID);
+
+ /*!
+ * @brief Set the channel data for this EPG.
+ * @param data The channel data.
+ */
+ void SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data);
+
+ /*!
+ * @brief Update an entry.
+ * @param tag The tag to update.
+ * @return True if it was updated successfully, false otherwise.
+ */
+ bool UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag);
+
+ /*!
+ * @brief Delete an entry.
+ * @param tag The tag to delete.
+ * @return True if it was deleted successfully, false otherwise.
+ */
+ bool DeleteEntry(const std::shared_ptr<CPVREpgInfoTag>& tag);
+
+ /*!
+ * @brief Update all entries with the provided tags.
+ * @param tags The updated tags.
+ * @return True if the update was successful, false otherwise.
+ */
+ bool UpdateEntries(const CPVREpgTagsContainer& tags);
+
+ /*!
+ * @brief Release all entries.
+ */
+ void Clear();
+
+ /*!
+ * @brief Remove all entries which were finished before the given time.
+ * @param time Delete entries with an end time before this time.
+ */
+ void Cleanup(const CDateTime& time);
+
+ /*!
+ * @brief Check whether this container is empty.
+ * @return True if the container does not contain any entries, false otherwise.
+ */
+ bool IsEmpty() const;
+
+ /*!
+ * @brief Get an EPG tag given its start time.
+ * @param startTime The start time
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTag(const CDateTime& startTime) const;
+
+ /*!
+ * @brief Get an EPG tag given its unique broadcast ID.
+ * @param iUniqueBroadcastID The ID.
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTag(unsigned int iUniqueBroadcastID) const;
+
+ /*!
+ * @brief Get an EPG tag given its database ID.
+ * @param iDatabaseID The ID.
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagByDatabaseID(int iDatabaseID) const;
+
+ /*!
+ * @brief Get the event that occurs between the given begin and end time.
+ * @param start The start of the time interval.
+ * @param end The end of the time interval.
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagBetween(const CDateTime& start, const CDateTime& end) const;
+
+ /*!
+ * @brief Update the currently active event.
+ * @return True if the active event was updated, false otherwise.
+ */
+ bool UpdateActiveTag();
+
+ /*!
+ * @brief Get the event that is occurring now
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetActiveTag() const;
+
+ /*!
+ * @brief Get the event that will occur next
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetNextStartingTag() const;
+
+ /*!
+ * @brief Get the event that occurred previously
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetLastEndedTag() const;
+
+ /*!
+ * @brief Get all EPG tags for the given time frame, including "gap" tags.
+ * @param timelineStart Start of time line
+ * @param timelineEnd End of time line
+ * @param minEventEnd The minimum end time of the events to return
+ * @param maxEventStart The maximum start time of the events to return
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetTimeline(const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const;
+
+ /*!
+ * @brief Get all EPG tags.
+ * @return The tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetAllTags() const;
+
+ /*!
+ * @brief Get the start and end time of the last not yet commited entry in this EPG.
+ * @return The times; first: start time, second: end time.
+ */
+ std::pair<CDateTime, CDateTime> GetFirstAndLastUncommitedEPGDate() const;
+
+ /*!
+ * @brief Check whether this container has unsaved data.
+ * @return True if this container contains unsaved data, false otherwise.
+ */
+ bool NeedsSave() const;
+
+ /*!
+ * @brief Write the query to persist data into database's queue
+ */
+ void QueuePersistQuery();
+
+ /*!
+ * @brief Queue the deletion of this container from its database.
+ */
+ void QueueDelete();
+
+private:
+ /*!
+ * @brief Complete the instance data for the given tags.
+ * @param tags The tags to complete.
+ * @return The completed tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> CreateEntries(
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const;
+
+ /*!
+ * @brief Complete the instance data for the given tag.
+ * @param tags The tag to complete.
+ * @return The completed tag.
+ */
+ std::shared_ptr<CPVREpgInfoTag> CreateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+
+ /*!
+ * @brief Create a "gap" tag
+ * @param start The start time of the gap.
+ * @param end The end time of the gap.
+ * @return The tag.
+ */
+ std::shared_ptr<CPVREpgInfoTag> CreateGapTag(const CDateTime& start, const CDateTime& end) const;
+
+ /*!
+ * @brief Merge m_changedTags tags into given tags, resolving conflicts.
+ * @param minEventEnd The minimum end time of the events to return
+ * @param maxEventStart The maximum start time of the events to return
+ * @param tags The merged tags.
+ */
+ void MergeTags(const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart,
+ std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const;
+
+ /*!
+ * @brief Fix overlapping events.
+ * @param tags The events to check/fix.
+ */
+ void FixOverlappingEvents(std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const;
+ void FixOverlappingEvents(std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& tags) const;
+
+ int m_iEpgID = 0;
+ std::shared_ptr<CPVREpgChannelData> m_channelData;
+ const std::shared_ptr<CPVREpgDatabase> m_database;
+ const std::unique_ptr<CPVREpgTagsCache> m_tagsCache;
+
+ std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>> m_changedTags;
+ std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>> m_deletedTags;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/filesystem/CMakeLists.txt b/xbmc/pvr/filesystem/CMakeLists.txt
new file mode 100644
index 0000000..823d783
--- /dev/null
+++ b/xbmc/pvr/filesystem/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES PVRGUIDirectory.cpp)
+
+set(HEADERS PVRGUIDirectory.h)
+
+core_add_library(pvr_filesystem)
diff --git a/xbmc/pvr/filesystem/PVRGUIDirectory.cpp b/xbmc/pvr/filesystem/PVRGUIDirectory.cpp
new file mode 100644
index 0000000..1ef7810
--- /dev/null
+++ b/xbmc/pvr/filesystem/PVRGUIDirectory.cpp
@@ -0,0 +1,625 @@
+/*
+ * Copyright (C) 2012-2019 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIDirectory.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "input/WindowTranslator.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "pvr/epg/EpgSearchPath.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+bool CPVRGUIDirectory::Exists() const
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return false;
+
+ return m_url.IsProtocol("pvr") && StringUtils::StartsWith(m_url.GetFileName(), "recordings");
+}
+
+bool CPVRGUIDirectory::SupportsWriteFileOperations() const
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return false;
+
+ const std::string filename = m_url.GetFileName();
+ return URIUtils::IsPVRRecording(filename);
+}
+
+namespace
+{
+
+bool GetRootDirectory(bool bRadio, CFileItemList& results)
+{
+ std::shared_ptr<CFileItem> item;
+
+ const std::shared_ptr<CPVRClients> clients = CServiceBroker::GetPVRManager().Clients();
+
+ // EPG
+ const bool bAnyClientSupportingEPG = clients->AnyClientSupportingEPG();
+ if (bAnyClientSupportingEPG)
+ {
+ item.reset(
+ new CFileItem(StringUtils::Format("pvr://guide/{}/", bRadio ? "radio" : "tv"), true));
+ item->SetLabel(g_localizeStrings.Get(19069)); // Guide
+ item->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio ? WINDOW_RADIO_GUIDE
+ : WINDOW_TV_GUIDE));
+ item->SetArt("icon", "DefaultPVRGuide.png");
+ results.Add(item);
+ }
+
+ // Channels
+ item.reset(new CFileItem(
+ bRadio ? CPVRChannelsPath::PATH_RADIO_CHANNELS : CPVRChannelsPath::PATH_TV_CHANNELS, true));
+ item->SetLabel(g_localizeStrings.Get(19019)); // Channels
+ item->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio ? WINDOW_RADIO_CHANNELS
+ : WINDOW_TV_CHANNELS));
+ item->SetArt("icon", "DefaultPVRChannels.png");
+ results.Add(item);
+
+ // Recordings
+ if (clients->AnyClientSupportingRecordings())
+ {
+ item.reset(new CFileItem(bRadio ? CPVRRecordingsPath::PATH_ACTIVE_RADIO_RECORDINGS
+ : CPVRRecordingsPath::PATH_ACTIVE_TV_RECORDINGS,
+ true));
+ item->SetLabel(g_localizeStrings.Get(19017)); // Recordings
+ item->SetProperty("node.target", CWindowTranslator::TranslateWindow(
+ bRadio ? WINDOW_RADIO_RECORDINGS : WINDOW_TV_RECORDINGS));
+ item->SetArt("icon", "DefaultPVRRecordings.png");
+ results.Add(item);
+ }
+
+ // Timers/Timer rules
+ // - always present, because Reminders are always available, no client support needed for this
+ item.reset(new CFileItem(
+ bRadio ? CPVRTimersPath::PATH_RADIO_TIMERS : CPVRTimersPath::PATH_TV_TIMERS, true));
+ item->SetLabel(g_localizeStrings.Get(19040)); // Timers
+ item->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio ? WINDOW_RADIO_TIMERS
+ : WINDOW_TV_TIMERS));
+ item->SetArt("icon", "DefaultPVRTimers.png");
+ results.Add(item);
+
+ item.reset(new CFileItem(
+ bRadio ? CPVRTimersPath::PATH_RADIO_TIMER_RULES : CPVRTimersPath::PATH_TV_TIMER_RULES, true));
+ item->SetLabel(g_localizeStrings.Get(19138)); // Timer rules
+ item->SetProperty("node.target", CWindowTranslator::TranslateWindow(
+ bRadio ? WINDOW_RADIO_TIMER_RULES : WINDOW_TV_TIMER_RULES));
+ item->SetArt("icon", "DefaultPVRTimerRules.png");
+ results.Add(item);
+
+ // Search
+ if (bAnyClientSupportingEPG)
+ {
+ item.reset(new CFileItem(
+ bRadio ? CPVREpgSearchPath::PATH_RADIO_SEARCH : CPVREpgSearchPath::PATH_TV_SEARCH, true));
+ item->SetLabel(g_localizeStrings.Get(137)); // Search
+ item->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio ? WINDOW_RADIO_SEARCH
+ : WINDOW_TV_SEARCH));
+ item->SetArt("icon", "DefaultPVRSearch.png");
+ results.Add(item);
+ }
+
+ return true;
+}
+
+} // unnamed namespace
+
+bool CPVRGUIDirectory::GetDirectory(CFileItemList& results) const
+{
+ std::string base = m_url.Get();
+ URIUtils::RemoveSlashAtEnd(base);
+
+ std::string fileName = m_url.GetFileName();
+ URIUtils::RemoveSlashAtEnd(fileName);
+
+ results.SetCacheToDisc(CFileItemList::CACHE_NEVER);
+
+ if (fileName.empty())
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ std::shared_ptr<CFileItem> item;
+
+ item.reset(new CFileItem(base + "channels/", true));
+ item->SetLabel(g_localizeStrings.Get(19019)); // Channels
+ item->SetLabelPreformatted(true);
+ results.Add(item);
+
+ item.reset(new CFileItem(base + "recordings/active/", true));
+ item->SetLabel(g_localizeStrings.Get(19017)); // Recordings
+ item->SetLabelPreformatted(true);
+ results.Add(item);
+
+ item.reset(new CFileItem(base + "recordings/deleted/", true));
+ item->SetLabel(g_localizeStrings.Get(19184)); // Deleted recordings
+ item->SetLabelPreformatted(true);
+ results.Add(item);
+
+ // Sort by name only. Labels are preformatted.
+ results.AddSortMethod(SortByLabel, 551 /* Name */, LABEL_MASKS("%L", "", "%L", ""));
+ }
+ return true;
+ }
+ else if (StringUtils::StartsWith(fileName, "tv"))
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ return GetRootDirectory(false, results);
+ }
+ return true;
+ }
+ else if (StringUtils::StartsWith(fileName, "radio"))
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ return GetRootDirectory(true, results);
+ }
+ return true;
+ }
+ else if (StringUtils::StartsWith(fileName, "recordings"))
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ return GetRecordingsDirectory(results);
+ }
+ return true;
+ }
+ else if (StringUtils::StartsWith(fileName, "channels"))
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ return GetChannelsDirectory(results);
+ }
+ return true;
+ }
+ else if (StringUtils::StartsWith(fileName, "timers"))
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ return GetTimersDirectory(results);
+ }
+ return true;
+ }
+
+ const CPVREpgSearchPath path(m_url.Get());
+ if (path.IsValid())
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ if (path.IsSavedSearchesRoot())
+ return GetSavedSearchesDirectory(path.IsRadio(), results);
+ }
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRGUIDirectory::HasTVRecordings()
+{
+ return CServiceBroker::GetPVRManager().IsStarted() &&
+ CServiceBroker::GetPVRManager().Recordings()->GetNumTVRecordings() > 0;
+}
+
+bool CPVRGUIDirectory::HasDeletedTVRecordings()
+{
+ return CServiceBroker::GetPVRManager().IsStarted() &&
+ CServiceBroker::GetPVRManager().Recordings()->HasDeletedTVRecordings();
+}
+
+bool CPVRGUIDirectory::HasRadioRecordings()
+{
+ return CServiceBroker::GetPVRManager().IsStarted() &&
+ CServiceBroker::GetPVRManager().Recordings()->GetNumRadioRecordings() > 0;
+}
+
+bool CPVRGUIDirectory::HasDeletedRadioRecordings()
+{
+ return CServiceBroker::GetPVRManager().IsStarted() &&
+ CServiceBroker::GetPVRManager().Recordings()->HasDeletedRadioRecordings();
+}
+
+namespace
+{
+
+std::string TrimSlashes(const std::string& strOrig)
+{
+ std::string strReturn = strOrig;
+ while (strReturn[0] == '/')
+ strReturn.erase(0, 1);
+
+ URIUtils::RemoveSlashAtEnd(strReturn);
+ return strReturn;
+}
+
+bool IsDirectoryMember(const std::string& strDirectory,
+ const std::string& strEntryDirectory,
+ bool bGrouped)
+{
+ const std::string strUseDirectory = TrimSlashes(strDirectory);
+ const std::string strUseEntryDirectory = TrimSlashes(strEntryDirectory);
+
+ // Case-insensitive comparison since sub folders are created with case-insensitive matching (GetSubDirectories)
+ if (bGrouped)
+ return StringUtils::EqualsNoCase(strUseDirectory, strUseEntryDirectory);
+ else
+ return StringUtils::StartsWithNoCase(strUseEntryDirectory, strUseDirectory);
+}
+
+void GetSubDirectories(const CPVRRecordingsPath& recParentPath,
+ const std::vector<std::shared_ptr<CPVRRecording>>& recordings,
+ CFileItemList& results)
+{
+ // Only active recordings are fetched to provide sub directories.
+ // Not applicable for deleted view which is supposed to be flattened.
+ std::set<std::shared_ptr<CFileItem>> unwatchedFolders;
+ bool bRadio = recParentPath.IsRadio();
+
+ for (const auto& recording : recordings)
+ {
+ if (recording->IsDeleted())
+ continue;
+
+ if (recording->IsRadio() != bRadio)
+ continue;
+
+ const std::string strCurrent =
+ recParentPath.GetUnescapedSubDirectoryPath(recording->Directory());
+ if (strCurrent.empty())
+ continue;
+
+ CPVRRecordingsPath recChildPath(recParentPath);
+ recChildPath.AppendSegment(strCurrent);
+ const std::string strFilePath = recChildPath;
+
+ std::shared_ptr<CFileItem> item;
+ if (!results.Contains(strFilePath))
+ {
+ item.reset(new CFileItem(strCurrent, true));
+ item->SetPath(strFilePath);
+ item->SetLabel(strCurrent);
+ item->SetLabelPreformatted(true);
+ item->m_dateTime = recording->RecordingTimeAsLocalTime();
+ item->SetProperty("totalepisodes", 0);
+ item->SetProperty("watchedepisodes", 0);
+ item->SetProperty("unwatchedepisodes", 0);
+ item->SetProperty("sizeinbytes", UINT64_C(0));
+
+ // Assume all folders are watched, we'll change the overlay later
+ item->SetOverlayImage(CGUIListItem::ICON_OVERLAY_WATCHED, false);
+ results.Add(item);
+ }
+ else
+ {
+ item = results.Get(strFilePath);
+ if (item->m_dateTime < recording->RecordingTimeAsLocalTime())
+ item->m_dateTime = recording->RecordingTimeAsLocalTime();
+ }
+
+ item->IncrementProperty("totalepisodes", 1);
+ if (recording->GetPlayCount() == 0)
+ {
+ unwatchedFolders.insert(item);
+ item->IncrementProperty("unwatchedepisodes", 1);
+ }
+ else
+ {
+ item->IncrementProperty("watchedepisodes", 1);
+ }
+ item->SetLabel2(StringUtils::Format("{} / {}", item->GetProperty("watchedepisodes").asString(),
+ item->GetProperty("totalepisodes").asString()));
+
+ item->IncrementProperty("sizeinbytes", recording->GetSizeInBytes());
+ }
+
+ // Replace the incremental size of the recordings with a string equivalent
+ for (auto& item : results.GetList())
+ {
+ int64_t size = item->GetProperty("sizeinbytes").asInteger();
+ item->ClearProperty("sizeinbytes");
+ item->m_dwSize = size; // We'll also sort recording folders by size
+ if (size > 0)
+ item->SetProperty("recordingsize", StringUtils::SizeToString(size));
+ }
+
+ // Change the watched overlay to unwatched for folders containing unwatched entries
+ for (auto& item : unwatchedFolders)
+ item->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, false);
+}
+
+} // unnamed namespace
+
+bool CPVRGUIDirectory::GetRecordingsDirectory(CFileItemList& results) const
+{
+ results.SetContent("recordings");
+
+ bool bGrouped = false;
+ const std::vector<std::shared_ptr<CPVRRecording>> recordings =
+ CServiceBroker::GetPVRManager().Recordings()->GetAll();
+
+ if (m_url.HasOption("view"))
+ {
+ const std::string view = m_url.GetOption("view");
+ if (view == "grouped")
+ bGrouped = true;
+ else if (view == "flat")
+ bGrouped = false;
+ else
+ {
+ CLog::LogF(LOGERROR, "Unsupported value '{}' for url parameter 'view'", view);
+ return false;
+ }
+ }
+ else
+ {
+ bGrouped = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRRECORD_GROUPRECORDINGS);
+ }
+
+ const CPVRRecordingsPath recPath(m_url.GetWithoutOptions());
+ if (recPath.IsValid())
+ {
+ // Get the directory structure if in non-flatten mode
+ // Deleted view is always flatten. So only for an active view
+ const std::string strDirectory = recPath.GetUnescapedDirectoryPath();
+ if (!recPath.IsDeleted() && bGrouped)
+ GetSubDirectories(recPath, recordings, results);
+
+ // get all files of the current directory or recursively all files starting at the current directory if in flatten mode
+ std::shared_ptr<CFileItem> item;
+ for (const auto& recording : recordings)
+ {
+ // Omit recordings not matching criteria
+ if (recording->IsDeleted() != recPath.IsDeleted() ||
+ recording->IsRadio() != recPath.IsRadio() ||
+ !IsDirectoryMember(strDirectory, recording->Directory(), bGrouped))
+ continue;
+
+ item = std::make_shared<CFileItem>(recording);
+ item->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, recording->GetPlayCount() > 0);
+ results.Add(item);
+ }
+ }
+
+ return recPath.IsValid();
+}
+
+bool CPVRGUIDirectory::GetSavedSearchesDirectory(bool bRadio, CFileItemList& results) const
+{
+ const std::vector<std::shared_ptr<CPVREpgSearchFilter>> searches =
+ CServiceBroker::GetPVRManager().EpgContainer().GetSavedSearches(bRadio);
+
+ for (const auto& search : searches)
+ {
+ results.Add(std::make_shared<CFileItem>(search));
+ }
+ return true;
+}
+
+bool CPVRGUIDirectory::GetChannelGroupsDirectory(bool bRadio,
+ bool bExcludeHidden,
+ CFileItemList& results)
+{
+ const CPVRChannelGroups* channelGroups =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio);
+ if (channelGroups)
+ {
+ std::shared_ptr<CFileItem> item;
+ const std::vector<std::shared_ptr<CPVRChannelGroup>> groups =
+ channelGroups->GetMembers(bExcludeHidden);
+ for (const auto& group : groups)
+ {
+ item = std::make_shared<CFileItem>(group->GetPath(), true);
+ item->m_strTitle = group->GroupName();
+ item->SetLabel(group->GroupName());
+ results.Add(item);
+ }
+ return true;
+ }
+ return false;
+}
+
+bool CPVRGUIDirectory::GetChannelsDirectory(CFileItemList& results) const
+{
+ const CPVRChannelsPath path(m_url.GetWithoutOptions());
+ if (path.IsValid())
+ {
+ if (path.IsEmpty())
+ {
+ std::shared_ptr<CFileItem> item;
+
+ // all tv channels
+ item.reset(new CFileItem(CPVRChannelsPath::PATH_TV_CHANNELS, true));
+ item->SetLabel(g_localizeStrings.Get(19020)); // TV
+ item->SetLabelPreformatted(true);
+ results.Add(item);
+
+ // all radio channels
+ item.reset(new CFileItem(CPVRChannelsPath::PATH_RADIO_CHANNELS, true));
+ item->SetLabel(g_localizeStrings.Get(19021)); // Radio
+ item->SetLabelPreformatted(true);
+ results.Add(item);
+
+ return true;
+ }
+ else if (path.IsChannelsRoot())
+ {
+ return GetChannelGroupsDirectory(path.IsRadio(), true, results);
+ }
+ else if (path.IsChannelGroup())
+ {
+ const std::string& strGroupName = path.GetGroupName();
+ bool bShowHiddenChannels = path.IsHiddenChannelGroup();
+
+ std::shared_ptr<CPVRChannelGroup> group;
+ if (bShowHiddenChannels || strGroupName == "*") // all channels
+ {
+ group = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(path.IsRadio());
+ }
+ else
+ {
+ group = CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(path.IsRadio())
+ ->GetByName(strGroupName);
+ }
+
+ if (group)
+ {
+ const bool playedOnly =
+ (m_url.HasOption("view") && (m_url.GetOption("view") == "lastplayed"));
+
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ group->GetMembers();
+ for (const auto& groupMember : groupMembers)
+ {
+ if (bShowHiddenChannels != groupMember->Channel()->IsHidden())
+ continue;
+
+ if (playedOnly && !groupMember->Channel()->LastWatched())
+ continue;
+
+ results.Add(std::make_shared<CFileItem>(groupMember));
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to obtain members of channel group '{}'", strGroupName);
+ return false;
+ }
+
+ return true;
+ }
+ }
+ return false;
+}
+
+namespace
+{
+
+bool GetTimersRootDirectory(const CPVRTimersPath& path,
+ bool bHideDisabled,
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>>& timers,
+ CFileItemList& results)
+{
+ bool bRadio = path.IsRadio();
+ bool bRules = path.IsRules();
+
+ for (const auto& timer : timers)
+ {
+ if ((bRadio == timer->IsRadio() ||
+ (bRules && timer->ClientChannelUID() == PVR_TIMER_ANY_CHANNEL)) &&
+ (bRules == timer->IsTimerRule()) && (!bHideDisabled || !timer->IsDisabled()))
+ {
+ const auto item = std::make_shared<CFileItem>(timer);
+ const CPVRTimersPath timersPath(path.GetPath(), timer->ClientID(), timer->ClientIndex());
+ item->SetPath(timersPath.GetPath());
+ results.Add(item);
+ }
+ }
+ return true;
+}
+
+bool GetTimersSubDirectory(const CPVRTimersPath& path,
+ bool bHideDisabled,
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>>& timers,
+ CFileItemList& results)
+{
+ bool bRadio = path.IsRadio();
+ int iParentId = path.GetParentId();
+ int iClientId = path.GetClientId();
+
+ std::shared_ptr<CFileItem> item;
+
+ for (const auto& timer : timers)
+ {
+ if ((timer->IsRadio() == bRadio) && timer->HasParent() && (timer->ClientID() == iClientId) &&
+ (timer->ParentClientIndex() == iParentId) && (!bHideDisabled || !timer->IsDisabled()))
+ {
+ item.reset(new CFileItem(timer));
+ const CPVRTimersPath timersPath(path.GetPath(), timer->ClientID(), timer->ClientIndex());
+ item->SetPath(timersPath.GetPath());
+ results.Add(item);
+ }
+ }
+ return true;
+}
+
+} // unnamed namespace
+
+bool CPVRGUIDirectory::GetTimersDirectory(CFileItemList& results) const
+{
+ const CPVRTimersPath path(m_url.GetWithoutOptions());
+ if (path.IsValid() && (path.IsTimersRoot() || path.IsTimerRule()))
+ {
+ bool bHideDisabled = false;
+ if (m_url.HasOption("view"))
+ {
+ const std::string view = m_url.GetOption("view");
+ if (view == "hidedisabled")
+ {
+ bHideDisabled = true;
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unsupported value '{}' for url parameter 'view'", view);
+ return false;
+ }
+ }
+ else
+ {
+ bHideDisabled = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRTIMERS_HIDEDISABLEDTIMERS);
+ }
+
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>> timers =
+ CServiceBroker::GetPVRManager().Timers()->GetAll();
+
+ if (path.IsTimersRoot())
+ {
+ /* Root folder containing either timer rules or timers. */
+ return GetTimersRootDirectory(path, bHideDisabled, timers, results);
+ }
+ else if (path.IsTimerRule())
+ {
+ /* Sub folder containing the timers scheduled by the given timer rule. */
+ return GetTimersSubDirectory(path, bHideDisabled, timers, results);
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/pvr/filesystem/PVRGUIDirectory.h b/xbmc/pvr/filesystem/PVRGUIDirectory.h
new file mode 100644
index 0000000..462c553
--- /dev/null
+++ b/xbmc/pvr/filesystem/PVRGUIDirectory.h
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2012-2019 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "URL.h"
+
+#include <string>
+
+class CFileItemList;
+
+namespace PVR
+{
+
+class CPVRGUIDirectory
+{
+public:
+ /*!
+ * @brief PVR GUI directory ctor.
+ * @param url The directory's URL.
+ */
+ explicit CPVRGUIDirectory(const CURL& url) : m_url(url) {}
+
+ /*!
+ * @brief PVR GUI directory ctor.
+ * @param path The directory's path.
+ */
+ explicit CPVRGUIDirectory(const std::string& path) : m_url(path) {}
+
+ /*!
+ * @brief PVR GUI directory dtor.
+ */
+ virtual ~CPVRGUIDirectory() = default;
+
+ /*!
+ * @brief Check existence of this directory.
+ * @return True if the directory exists, false otherwise.
+ */
+ bool Exists() const;
+
+ /*!
+ * @brief Obtain the directory listing.
+ * @param results The list to fill with the results.
+ * @return True on success, false otherwise.
+ */
+ bool GetDirectory(CFileItemList& results) const;
+
+ /*!
+ * @brief Check if this directory supports file write operations.
+ * @return True if the directory supports file write operations, false otherwise.
+ */
+ bool SupportsWriteFileOperations() const;
+
+ /*!
+ * @brief Check if any TV recordings are existing.
+ * @return True if TV recordings exists, false otherwise.
+ */
+ static bool HasTVRecordings();
+
+ /*!
+ * @brief Check if any deleted TV recordings are existing.
+ * @return True if deleted TV recordings exists, false otherwise.
+ */
+ static bool HasDeletedTVRecordings();
+
+ /*!
+ * @brief Check if any radio recordings are existing.
+ * @return True if radio recordings exists, false otherwise.
+ */
+ static bool HasRadioRecordings();
+
+ /*!
+ * @brief Check if any deleted radio recordings are existing.
+ * @return True if deleted radio recordings exists, false otherwise.
+ */
+ static bool HasDeletedRadioRecordings();
+
+ /*!
+ * @brief Get the list of channel groups.
+ * @param bRadio If true, obtain radio groups, tv groups otherwise.
+ * @param bExcludeHidden If true exclude hidden groups, include hidden groups otherwise.
+ * @param results The file list to store the results in.
+ * @return True on success, false otherwise..
+ */
+ static bool GetChannelGroupsDirectory(bool bRadio, bool bExcludeHidden, CFileItemList& results);
+
+ /*!
+ * @brief Get the list of channels.
+ * @param results The file list to store the results in.
+ * @return True on success, false otherwise..
+ */
+ bool GetChannelsDirectory(CFileItemList& results) const;
+
+private:
+ bool GetTimersDirectory(CFileItemList& results) const;
+ bool GetRecordingsDirectory(CFileItemList& results) const;
+ bool GetSavedSearchesDirectory(bool bRadio, CFileItemList& results) const;
+
+ const CURL m_url;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/CMakeLists.txt b/xbmc/pvr/guilib/CMakeLists.txt
new file mode 100644
index 0000000..3a73a68
--- /dev/null
+++ b/xbmc/pvr/guilib/CMakeLists.txt
@@ -0,0 +1,35 @@
+set(SOURCES GUIEPGGridContainer.cpp
+ GUIEPGGridContainerModel.cpp
+ PVRGUIActionListener.cpp
+ PVRGUIActionsChannels.cpp
+ PVRGUIActionsClients.cpp
+ PVRGUIActionsDatabase.cpp
+ PVRGUIActionsEPG.cpp
+ PVRGUIActionsUtils.cpp
+ PVRGUIActionsParentalControl.cpp
+ PVRGUIActionsPlayback.cpp
+ PVRGUIActionsPowerManagement.cpp
+ PVRGUIActionsRecordings.cpp
+ PVRGUIActionsTimers.cpp
+ PVRGUIChannelIconUpdater.cpp
+ PVRGUIChannelNavigator.cpp
+ PVRGUIProgressHandler.cpp)
+
+set(HEADERS GUIEPGGridContainer.h
+ GUIEPGGridContainerModel.h
+ PVRGUIActionListener.h
+ PVRGUIActionsChannels.h
+ PVRGUIActionsClients.h
+ PVRGUIActionsDatabase.h
+ PVRGUIActionsEPG.h
+ PVRGUIActionsUtils.h
+ PVRGUIActionsParentalControl.h
+ PVRGUIActionsPlayback.h
+ PVRGUIActionsPowerManagement.h
+ PVRGUIActionsRecordings.h
+ PVRGUIActionsTimers.h
+ PVRGUIChannelIconUpdater.h
+ PVRGUIChannelNavigator.h
+ PVRGUIProgressHandler.h)
+
+core_add_library(pvr_guilib)
diff --git a/xbmc/pvr/guilib/GUIEPGGridContainer.cpp b/xbmc/pvr/guilib/GUIEPGGridContainer.cpp
new file mode 100644
index 0000000..d88dc86
--- /dev/null
+++ b/xbmc/pvr/guilib/GUIEPGGridContainer.cpp
@@ -0,0 +1,2549 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIEPGGridContainer.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/DirtyRegion.h"
+#include "guilib/GUIAction.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "input/Key.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/GUIEPGGridContainerModel.h"
+#include "utils/MathUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+
+#include <tinyxml.h>
+
+using namespace PVR;
+
+#define BLOCKJUMP 4 // how many blocks are jumped with each analogue scroll action
+static const int BLOCK_SCROLL_OFFSET = 60 / CGUIEPGGridContainerModel::MINSPERBLOCK; // how many blocks are jumped if we are at left/right edge of grid
+
+CGUIEPGGridContainer::CGUIEPGGridContainer(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ ORIENTATION orientation,
+ int scrollTime,
+ int preloadItems,
+ int timeBlocks,
+ int rulerUnit,
+ const CTextureInfo& progressIndicatorTexture)
+ : IGUIContainer(parentID, controlID, posX, posY, width, height),
+ m_orientation(orientation),
+ m_channelLayout(nullptr),
+ m_focusedChannelLayout(nullptr),
+ m_programmeLayout(nullptr),
+ m_focusedProgrammeLayout(nullptr),
+ m_rulerLayout(nullptr),
+ m_rulerDateLayout(nullptr),
+ m_pageControl(0),
+ m_rulerUnit(rulerUnit),
+ m_channelsPerPage(0),
+ m_programmesPerPage(0),
+ m_channelCursor(0),
+ m_channelOffset(0),
+ m_blocksPerPage(timeBlocks),
+ m_blockCursor(0),
+ m_blockOffset(0),
+ m_blockTravelAxis(0),
+ m_cacheChannelItems(preloadItems),
+ m_cacheProgrammeItems(preloadItems),
+ m_cacheRulerItems(preloadItems),
+ m_rulerDateHeight(0),
+ m_rulerDateWidth(0),
+ m_rulerPosX(0),
+ m_rulerPosY(0),
+ m_rulerHeight(0),
+ m_rulerWidth(0),
+ m_channelPosX(0),
+ m_channelPosY(0),
+ m_channelHeight(0),
+ m_channelWidth(0),
+ m_gridPosX(0),
+ m_gridPosY(0),
+ m_gridWidth(0),
+ m_gridHeight(0),
+ m_blockSize(0),
+ m_analogScrollCount(0),
+ m_guiProgressIndicatorTexture(
+ CGUITexture::CreateTexture(posX, posY, width, height, progressIndicatorTexture)),
+ m_scrollTime(scrollTime ? scrollTime : 1),
+ m_programmeScrollLastTime(0),
+ m_programmeScrollSpeed(0),
+ m_programmeScrollOffset(0),
+ m_channelScrollLastTime(0),
+ m_channelScrollSpeed(0),
+ m_channelScrollOffset(0),
+ m_gridModel(new CGUIEPGGridContainerModel)
+{
+ ControlType = GUICONTAINER_EPGGRID;
+}
+
+CGUIEPGGridContainer::CGUIEPGGridContainer(const CGUIEPGGridContainer& other)
+ : IGUIContainer(other),
+ m_renderOffset(other.m_renderOffset),
+ m_orientation(other.m_orientation),
+ m_channelLayouts(other.m_channelLayouts),
+ m_focusedChannelLayouts(other.m_focusedChannelLayouts),
+ m_focusedProgrammeLayouts(other.m_focusedProgrammeLayouts),
+ m_programmeLayouts(other.m_programmeLayouts),
+ m_rulerLayouts(other.m_rulerLayouts),
+ m_rulerDateLayouts(other.m_rulerDateLayouts),
+ m_channelLayout(other.m_channelLayout),
+ m_focusedChannelLayout(other.m_focusedChannelLayout),
+ m_programmeLayout(other.m_programmeLayout),
+ m_focusedProgrammeLayout(other.m_focusedProgrammeLayout),
+ m_rulerLayout(other.m_rulerLayout),
+ m_rulerDateLayout(other.m_rulerDateLayout),
+ m_pageControl(other.m_pageControl),
+ m_rulerUnit(other.m_rulerUnit),
+ m_channelsPerPage(other.m_channelsPerPage),
+ m_programmesPerPage(other.m_programmesPerPage),
+ m_channelCursor(other.m_channelCursor),
+ m_channelOffset(other.m_channelOffset),
+ m_blocksPerPage(other.m_blocksPerPage),
+ m_blockCursor(other.m_blockCursor),
+ m_blockOffset(other.m_blockOffset),
+ m_blockTravelAxis(other.m_blockTravelAxis),
+ m_cacheChannelItems(other.m_cacheChannelItems),
+ m_cacheProgrammeItems(other.m_cacheProgrammeItems),
+ m_cacheRulerItems(other.m_cacheRulerItems),
+ m_rulerDateHeight(other.m_rulerDateHeight),
+ m_rulerDateWidth(other.m_rulerDateWidth),
+ m_rulerPosX(other.m_rulerPosX),
+ m_rulerPosY(other.m_rulerPosY),
+ m_rulerHeight(other.m_rulerHeight),
+ m_rulerWidth(other.m_rulerWidth),
+ m_channelPosX(other.m_channelPosX),
+ m_channelPosY(other.m_channelPosY),
+ m_channelHeight(other.m_channelHeight),
+ m_channelWidth(other.m_channelWidth),
+ m_gridPosX(other.m_gridPosX),
+ m_gridPosY(other.m_gridPosY),
+ m_gridWidth(other.m_gridWidth),
+ m_gridHeight(other.m_gridHeight),
+ m_blockSize(other.m_blockSize),
+ m_analogScrollCount(other.m_analogScrollCount),
+ m_guiProgressIndicatorTexture(other.m_guiProgressIndicatorTexture->Clone()),
+ m_lastItem(other.m_lastItem),
+ m_lastChannel(other.m_lastChannel),
+ m_scrollTime(other.m_scrollTime),
+ m_programmeScrollLastTime(other.m_programmeScrollLastTime),
+ m_programmeScrollSpeed(other.m_programmeScrollSpeed),
+ m_programmeScrollOffset(other.m_programmeScrollOffset),
+ m_channelScrollLastTime(other.m_channelScrollLastTime),
+ m_channelScrollSpeed(other.m_channelScrollSpeed),
+ m_channelScrollOffset(other.m_channelScrollOffset),
+ m_gridModel(new CGUIEPGGridContainerModel(*other.m_gridModel)),
+ m_updatedGridModel(other.m_updatedGridModel
+ ? new CGUIEPGGridContainerModel(*other.m_updatedGridModel)
+ : nullptr),
+ m_itemStartBlock(other.m_itemStartBlock)
+{
+}
+
+bool CGUIEPGGridContainer::HasData() const
+{
+ return m_gridModel && m_gridModel->HasChannelItems();
+}
+
+void CGUIEPGGridContainer::AllocResources()
+{
+ IGUIContainer::AllocResources();
+ m_guiProgressIndicatorTexture->AllocResources();
+}
+
+void CGUIEPGGridContainer::FreeResources(bool immediately)
+{
+ m_guiProgressIndicatorTexture->FreeResources(immediately);
+ IGUIContainer::FreeResources(immediately);
+}
+
+void CGUIEPGGridContainer::SetPageControl(int id)
+{
+ m_pageControl = id;
+}
+
+void CGUIEPGGridContainer::Process(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ ValidateOffset();
+
+ if (m_bInvalidated)
+ {
+ UpdateLayout();
+
+ if (m_pageControl)
+ {
+ int iItemsPerPage;
+ int iTotalItems;
+
+ if (m_orientation == VERTICAL)
+ {
+ iItemsPerPage = m_channelsPerPage;
+ iTotalItems = m_gridModel->ChannelItemsSize();
+ }
+ else
+ {
+ iItemsPerPage = m_blocksPerPage;
+ iTotalItems = m_gridModel->GridItemsSize();
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), m_pageControl, iItemsPerPage, iTotalItems);
+ SendWindowMessage(msg);
+ }
+ }
+
+ UpdateScrollOffset(currentTime);
+ ProcessChannels(currentTime, dirtyregions);
+ ProcessRulerDate(currentTime, dirtyregions);
+ ProcessRuler(currentTime, dirtyregions);
+ ProcessProgrammeGrid(currentTime, dirtyregions);
+ ProcessProgressIndicator(currentTime, dirtyregions);
+
+ if (m_pageControl)
+ {
+ int iItem =
+ (m_orientation == VERTICAL)
+ ? MathUtils::round_int(static_cast<double>(m_channelScrollOffset / m_channelHeight))
+ : MathUtils::round_int(
+ static_cast<double>(m_programmeScrollOffset / (m_gridHeight / m_blocksPerPage)));
+
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), m_pageControl, iItem);
+ SendWindowMessage(msg);
+ }
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIEPGGridContainer::Render()
+{
+ RenderChannels();
+ RenderRulerDate();
+ RenderRuler();
+ RenderProgrammeGrid();
+ RenderProgressIndicator();
+
+ CGUIControl::Render();
+}
+
+void CGUIEPGGridContainer::ProcessChannels(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ HandleChannels(false, currentTime, dirtyregions);
+}
+
+void CGUIEPGGridContainer::RenderChannels()
+{
+ // params not needed for render.
+ unsigned int dummyTime = 0;
+ CDirtyRegionList dummyRegions;
+ HandleChannels(true, dummyTime, dummyRegions);
+}
+
+void CGUIEPGGridContainer::ProcessRulerDate(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ HandleRulerDate(false, currentTime, dirtyregions);
+}
+
+void CGUIEPGGridContainer::RenderRulerDate()
+{
+ // params not needed for render.
+ unsigned int dummyTime = 0;
+ CDirtyRegionList dummyRegions;
+ HandleRulerDate(true, dummyTime, dummyRegions);
+}
+
+void CGUIEPGGridContainer::ProcessRuler(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ HandleRuler(false, currentTime, dirtyregions);
+}
+
+void CGUIEPGGridContainer::RenderRuler()
+{
+ // params not needed for render.
+ unsigned int dummyTime = 0;
+ CDirtyRegionList dummyRegions;
+ HandleRuler(true, dummyTime, dummyRegions);
+}
+
+void CGUIEPGGridContainer::ProcessProgrammeGrid(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ HandleProgrammeGrid(false, currentTime, dirtyregions);
+}
+
+void CGUIEPGGridContainer::RenderProgrammeGrid()
+{
+ // params not needed for render.
+ unsigned int dummyTime = 0;
+ CDirtyRegionList dummyRegions;
+ HandleProgrammeGrid(true, dummyTime, dummyRegions);
+}
+
+float CGUIEPGGridContainer::GetCurrentTimePositionOnPage() const
+{
+ if (!m_gridModel->GetGridStart().IsValid())
+ return -1.0f;
+
+ const CDateTimeSpan startDelta(CDateTime::GetUTCDateTime() - m_gridModel->GetGridStart());
+ const float fPos = (startDelta.GetSecondsTotal() * m_blockSize) /
+ (CGUIEPGGridContainerModel::MINSPERBLOCK * 60) -
+ GetProgrammeScrollOffsetPos();
+ return std::min(fPos, m_orientation == VERTICAL ? m_gridWidth : m_gridHeight);
+}
+
+float CGUIEPGGridContainer::GetProgressIndicatorWidth() const
+{
+ return (m_orientation == VERTICAL) ? GetCurrentTimePositionOnPage() : m_rulerWidth + m_gridWidth;
+}
+
+float CGUIEPGGridContainer::GetProgressIndicatorHeight() const
+{
+ return (m_orientation == VERTICAL) ? m_rulerHeight + m_gridHeight : GetCurrentTimePositionOnPage();
+}
+
+void CGUIEPGGridContainer::ProcessProgressIndicator(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ float width = GetProgressIndicatorWidth();
+ float height = GetProgressIndicatorHeight();
+
+ if (width > 0 && height > 0)
+ {
+ m_guiProgressIndicatorTexture->SetVisible(true);
+ m_guiProgressIndicatorTexture->SetPosition(m_rulerPosX + m_renderOffset.x,
+ m_rulerPosY + m_renderOffset.y);
+ m_guiProgressIndicatorTexture->SetWidth(width);
+ m_guiProgressIndicatorTexture->SetHeight(height);
+ }
+ else
+ {
+ m_guiProgressIndicatorTexture->SetVisible(false);
+ }
+
+ m_guiProgressIndicatorTexture->Process(currentTime);
+}
+
+void CGUIEPGGridContainer::RenderProgressIndicator()
+{
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_rulerPosX, m_rulerPosY, GetProgressIndicatorWidth(), GetProgressIndicatorHeight()))
+ {
+ m_guiProgressIndicatorTexture->SetDiffuseColor(m_diffuseColor);
+ m_guiProgressIndicatorTexture->Render();
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+}
+
+void CGUIEPGGridContainer::ProcessItem(float posX, float posY, const CFileItemPtr& item, CFileItemPtr& lastitem,
+ bool focused, CGUIListItemLayout* normallayout, CGUIListItemLayout* focusedlayout,
+ unsigned int currentTime, CDirtyRegionList& dirtyregions, float resize /* = -1.0f */)
+{
+ if (!normallayout || !focusedlayout)
+ return;
+
+ // set the origin
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(posX, posY);
+
+ if (m_bInvalidated)
+ item->SetInvalid();
+
+ if (focused)
+ {
+ if (!item->GetFocusedLayout())
+ {
+ item->SetFocusedLayout(std::make_unique<CGUIListItemLayout>(*focusedlayout, this));
+ }
+
+ if (resize != -1.0f)
+ {
+ if (m_orientation == VERTICAL)
+ item->GetFocusedLayout()->SetWidth(resize);
+ else
+ item->GetFocusedLayout()->SetHeight(resize);
+ }
+
+ if (item != lastitem || !HasFocus())
+ item->GetFocusedLayout()->SetFocusedItem(0);
+
+ if (item != lastitem && HasFocus())
+ {
+ item->GetFocusedLayout()->ResetAnimation(ANIM_TYPE_UNFOCUS);
+
+ unsigned int subItem = 1;
+ if (lastitem && lastitem->GetFocusedLayout())
+ subItem = lastitem->GetFocusedLayout()->GetFocusedItem();
+
+ item->GetFocusedLayout()->SetFocusedItem(subItem ? subItem : 1);
+ }
+
+ item->GetFocusedLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions);
+ lastitem = item;
+ }
+ else
+ {
+ if (!item->GetLayout())
+ {
+ item->SetLayout(std::make_unique<CGUIListItemLayout>(*normallayout, this));
+ }
+
+ if (resize != -1.0f)
+ {
+ if (m_orientation == VERTICAL)
+ item->GetLayout()->SetWidth(resize);
+ else
+ item->GetLayout()->SetHeight(resize);
+ }
+
+ if (item->GetFocusedLayout())
+ item->GetFocusedLayout()->SetFocusedItem(0);
+
+ if (item->GetFocusedLayout() && item->GetFocusedLayout()->IsAnimating(ANIM_TYPE_UNFOCUS))
+ item->GetFocusedLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions);
+ else
+ item->GetLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions);
+ }
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
+}
+
+void CGUIEPGGridContainer::RenderItem(float posX, float posY, CGUIListItem* item, bool focused)
+{
+ // set the origin
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(posX, posY);
+
+ if (focused)
+ {
+ if (item->GetFocusedLayout())
+ item->GetFocusedLayout()->Render(item, m_parentID);
+ }
+ else
+ {
+ if (item->GetFocusedLayout() && item->GetFocusedLayout()->IsAnimating(ANIM_TYPE_UNFOCUS))
+ item->GetFocusedLayout()->Render(item, m_parentID);
+ else if (item->GetLayout())
+ item->GetLayout()->Render(item, m_parentID);
+ }
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
+}
+
+bool CGUIEPGGridContainer::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_MOVE_LEFT:
+ case ACTION_MOVE_RIGHT:
+ case ACTION_MOVE_DOWN:
+ case ACTION_MOVE_UP:
+ case ACTION_NAV_BACK:
+ // use base class implementation
+ return CGUIControl::OnAction(action);
+
+ case ACTION_NEXT_ITEM:
+ // skip +12h
+ ScrollToBlockOffset(m_blockOffset + (12 * 60 / CGUIEPGGridContainerModel::MINSPERBLOCK));
+ SetBlock(m_blockCursor);
+ return true;
+
+ case ACTION_PREV_ITEM:
+ // skip -12h
+ ScrollToBlockOffset(m_blockOffset - (12 * 60 / CGUIEPGGridContainerModel::MINSPERBLOCK));
+ SetBlock(m_blockCursor);
+ return true;
+
+ case REMOTE_0:
+ GoToNow();
+ return true;
+
+ case ACTION_PAGE_UP:
+ if (m_orientation == VERTICAL)
+ {
+ if (m_channelOffset == 0)
+ {
+ // already on the first page, so move to the first item
+ SetChannel(0);
+ }
+ else
+ {
+ // scroll up to the previous page
+ ChannelScroll(-m_channelsPerPage);
+ }
+ }
+ else
+ {
+ if (m_blockOffset == 0)
+ {
+ // already on the first page, so move to the first item
+ SetBlock(0);
+ }
+ else
+ {
+ // scroll up to the previous page
+ ProgrammesScroll(-m_blocksPerPage);
+ }
+ }
+ return true;
+
+ case ACTION_PAGE_DOWN:
+ if (m_orientation == VERTICAL)
+ {
+ if (m_channelOffset == m_gridModel->ChannelItemsSize() - m_channelsPerPage ||
+ m_gridModel->ChannelItemsSize() < m_channelsPerPage)
+ {
+ // already at the last page, so move to the last item.
+ SetChannel(m_gridModel->GetLastChannel() - m_channelOffset);
+ }
+ else
+ {
+ // scroll down to the next page
+ ChannelScroll(m_channelsPerPage);
+ }
+ }
+ else
+ {
+ if (m_blockOffset == m_gridModel->GridItemsSize() - m_blocksPerPage ||
+ m_gridModel->GridItemsSize() < m_blocksPerPage)
+ {
+ // already at the last page, so move to the last item.
+ SetBlock(m_gridModel->GetLastBlock() - m_blockOffset);
+ }
+ else
+ {
+ // scroll down to the next page
+ ProgrammesScroll(m_blocksPerPage);
+ }
+ }
+
+ return true;
+
+ // smooth scrolling (for analog controls)
+ case ACTION_TELETEXT_RED:
+ case ACTION_TELETEXT_GREEN:
+ case ACTION_SCROLL_UP: // left horizontal scrolling
+ if (m_orientation == VERTICAL)
+ {
+ int blocksToJump = action.GetID() == ACTION_TELETEXT_RED ? m_blocksPerPage / 2 : m_blocksPerPage / 4;
+
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+
+ if (m_blockOffset > 0 && m_blockCursor <= m_blocksPerPage / 2)
+ ProgrammesScroll(-blocksToJump);
+ else if (m_blockCursor > blocksToJump)
+ SetBlock(m_blockCursor - blocksToJump);
+ }
+ return handled;
+ }
+ else
+ {
+ int channelsToJump = action.GetID() == ACTION_TELETEXT_RED ? m_channelsPerPage / 2 : m_channelsPerPage / 4;
+
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+
+ if (m_channelOffset > 0 && m_channelCursor <= m_channelsPerPage / 2)
+ ChannelScroll(-channelsToJump);
+ else if (m_channelCursor > channelsToJump)
+ SetChannel(m_channelCursor - channelsToJump);
+ }
+ return handled;
+ }
+ break;
+
+ case ACTION_TELETEXT_BLUE:
+ case ACTION_TELETEXT_YELLOW:
+ case ACTION_SCROLL_DOWN: // right horizontal scrolling
+ if (m_orientation == VERTICAL)
+ {
+ int blocksToJump = action.GetID() == ACTION_TELETEXT_BLUE ? m_blocksPerPage / 2 : m_blocksPerPage / 4;
+
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+
+ if (m_blockOffset + m_blocksPerPage < m_gridModel->GridItemsSize() &&
+ m_blockCursor >= m_blocksPerPage / 2)
+ ProgrammesScroll(blocksToJump);
+ else if (m_blockCursor < m_blocksPerPage - blocksToJump &&
+ m_blockOffset + m_blockCursor < m_gridModel->GridItemsSize() - blocksToJump)
+ SetBlock(m_blockCursor + blocksToJump);
+ }
+ return handled;
+ }
+ else
+ {
+ int channelsToJump = action.GetID() == ACTION_TELETEXT_BLUE ? m_channelsPerPage / 2 : m_channelsPerPage / 4;
+
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+
+ if (m_channelOffset + m_channelsPerPage < m_gridModel->ChannelItemsSize() && m_channelCursor >= m_channelsPerPage / 2)
+ ChannelScroll(channelsToJump);
+ else if (m_channelCursor < m_channelsPerPage - channelsToJump && m_channelOffset + m_channelCursor < m_gridModel->ChannelItemsSize() - channelsToJump)
+ SetChannel(m_channelCursor + channelsToJump);
+ }
+ return handled;
+ }
+ break;
+
+ default:
+ if (action.GetID())
+ return OnClick(action.GetID());
+
+ break;
+ }
+
+ return false;
+}
+
+bool CGUIEPGGridContainer::OnMessage(CGUIMessage& message)
+{
+ if (message.GetControlId() == GetID())
+ {
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_PAGE_CHANGE:
+ if (message.GetSenderId() == m_pageControl && IsVisible())
+ {
+ if (m_orientation == VERTICAL)
+ {
+ ScrollToChannelOffset(message.GetParam1());
+ SetChannel(m_channelCursor);
+ }
+ else
+ {
+ ScrollToBlockOffset(message.GetParam1());
+ SetBlock(m_blockCursor);
+ }
+ return true;
+ }
+ break;
+
+ case GUI_MSG_LABEL_BIND:
+ UpdateItems();
+ return true;
+
+ case GUI_MSG_REFRESH_LIST:
+ // update our list contents
+ m_gridModel->SetInvalid();
+ break;
+ }
+ }
+
+ return CGUIControl::OnMessage(message);
+}
+
+void CGUIEPGGridContainer::UpdateItems()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_updatedGridModel)
+ return;
+
+ // Save currently selected epg tag and grid coordinates. Selection shall be restored after update.
+ std::shared_ptr<CPVREpgInfoTag> prevSelectedEpgTag;
+ if (HasData())
+ prevSelectedEpgTag =
+ m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset)
+ ->GetEPGInfoTag();
+
+ const int oldChannelIndex = m_channelOffset + m_channelCursor;
+ const int oldBlockIndex = m_blockOffset + m_blockCursor;
+ const CDateTime oldGridStart(m_gridModel->GetGridStart());
+ int eventOffset = oldBlockIndex;
+ int newChannelIndex = oldChannelIndex;
+ int newBlockIndex = oldBlockIndex;
+ int channelUid = -1;
+ unsigned int broadcastUid = 0;
+
+ if (prevSelectedEpgTag)
+ {
+ // get the block offset relative to the first block of the selected event
+ eventOffset =
+ oldBlockIndex - m_gridModel->GetGridItemStartBlock(oldChannelIndex, oldBlockIndex);
+
+ if (!prevSelectedEpgTag->IsGapTag()) // "normal" tag selected
+ {
+ if (oldGridStart >= prevSelectedEpgTag->StartAsUTC())
+ {
+ // start of previously selected event is before grid start
+ newBlockIndex = eventOffset;
+ }
+ else
+ {
+ newBlockIndex = m_gridModel->GetFirstEventBlock(prevSelectedEpgTag) + eventOffset;
+ }
+
+ channelUid = prevSelectedEpgTag->UniqueChannelID();
+ broadcastUid = prevSelectedEpgTag->UniqueBroadcastID();
+ }
+ else // "gap" tag selected
+ {
+ channelUid = prevSelectedEpgTag->UniqueChannelID();
+
+ // As gap tags do not have a unique broadcast id, we will look for the real tag preceding
+ // the gap tag and add the respective offset to restore the gap tag selection, assuming that
+ // the real tag is still the predecessor of the gap tag after the grid model update.
+
+ const std::shared_ptr<CFileItem> prevItem = GetPrevItem().first;
+ if (prevItem)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> tag = prevItem->GetEPGInfoTag();
+ if (tag && !tag->IsGapTag())
+ {
+ if (oldGridStart >= tag->StartAsUTC())
+ {
+ // start of previously selected event is before grid start
+ newBlockIndex = eventOffset;
+ }
+ else
+ {
+ newBlockIndex = m_gridModel->GetFirstEventBlock(tag);
+ eventOffset += m_gridModel->GetFirstEventBlock(prevSelectedEpgTag) - newBlockIndex;
+ }
+
+ broadcastUid = tag->UniqueBroadcastID();
+ }
+ }
+ }
+ }
+
+ m_lastItem = nullptr;
+ m_lastChannel = nullptr;
+
+ // always use asynchronously precalculated grid data.
+ m_gridModel = std::move(m_updatedGridModel);
+
+ if (prevSelectedEpgTag)
+ {
+ if (oldGridStart != m_gridModel->GetGridStart())
+ {
+ // grid start changed. block offset for selected event might have changed.
+ newBlockIndex += m_gridModel->GetBlock(oldGridStart);
+ if (newBlockIndex < 0 || newBlockIndex > m_gridModel->GetLastBlock())
+ {
+ // previous selection is no longer in grid.
+ SetInvalid();
+ m_bEnableChannelScrolling = false;
+ GoToChannel(newChannelIndex);
+ m_bEnableProgrammeScrolling = false;
+ GoToNow();
+ return;
+ }
+ }
+
+ if (newChannelIndex >= m_gridModel->ChannelItemsSize() ||
+ newBlockIndex >= m_gridModel->GridItemsSize() ||
+ m_gridModel->GetGridItem(newChannelIndex, newBlockIndex)->GetEPGInfoTag() !=
+ prevSelectedEpgTag)
+ {
+ int iChannelIndex = CGUIEPGGridContainerModel::INVALID_INDEX;
+ int iBlockIndex = CGUIEPGGridContainerModel::INVALID_INDEX;
+ m_gridModel->FindChannelAndBlockIndex(channelUid, broadcastUid, eventOffset, iChannelIndex, iBlockIndex);
+
+ if (iBlockIndex != CGUIEPGGridContainerModel::INVALID_INDEX)
+ {
+ newBlockIndex = iBlockIndex;
+ }
+ else if (newBlockIndex > m_gridModel->GetLastBlock())
+ {
+ // default to now
+ newBlockIndex = m_gridModel->GetNowBlock();
+
+ if (newBlockIndex > m_gridModel->GetLastBlock())
+ {
+ // last block is in the past. default to last block
+ newBlockIndex = m_gridModel->GetLastBlock();
+ }
+ }
+
+ if (iChannelIndex != CGUIEPGGridContainerModel::INVALID_INDEX)
+ {
+ newChannelIndex = iChannelIndex;
+ }
+ else if (newChannelIndex >= m_gridModel->ChannelItemsSize() ||
+ (m_gridModel->GetGridItem(newChannelIndex, newBlockIndex)->GetEPGInfoTag()->UniqueChannelID() != prevSelectedEpgTag->UniqueChannelID() &&
+ m_gridModel->GetGridItem(newChannelIndex, newBlockIndex)->GetEPGInfoTag()->ClientID() != prevSelectedEpgTag->ClientID()))
+ {
+ // default to first channel
+ newChannelIndex = 0;
+ }
+ }
+
+ // restore previous selection.
+ if (newChannelIndex == oldChannelIndex && newBlockIndex == oldBlockIndex)
+ {
+ // same coordinates, keep current grid view port
+ UpdateItem();
+ }
+ else
+ {
+ // new coordinates, move grid view port accordingly
+ SetInvalid();
+
+ if (newBlockIndex != oldBlockIndex)
+ {
+ m_bEnableProgrammeScrolling = false;
+ GoToBlock(newBlockIndex);
+ }
+
+ if (newChannelIndex != oldChannelIndex)
+ {
+ m_bEnableChannelScrolling = false;
+ GoToChannel(newChannelIndex);
+ }
+ }
+ }
+ else
+ {
+ // no previous selection, goto now
+ SetInvalid();
+ m_bEnableProgrammeScrolling = false;
+ GoToNow();
+ }
+}
+
+float CGUIEPGGridContainer::GetChannelScrollOffsetPos() const
+{
+ if (m_bEnableChannelScrolling)
+ return m_channelScrollOffset;
+ else
+ return m_channelOffset * m_channelLayout->Size(m_orientation);
+}
+
+float CGUIEPGGridContainer::GetProgrammeScrollOffsetPos() const
+{
+ if (m_bEnableProgrammeScrolling)
+ return m_programmeScrollOffset;
+ else
+ return m_blockOffset * m_blockSize;
+}
+
+int CGUIEPGGridContainer::GetChannelScrollOffset(CGUIListItemLayout* layout) const
+{
+ if (m_bEnableChannelScrolling)
+ return MathUtils::round_int(
+ static_cast<double>(m_channelScrollOffset / layout->Size(m_orientation)));
+ else
+ return m_channelOffset;
+}
+
+int CGUIEPGGridContainer::GetProgrammeScrollOffset() const
+{
+ if (m_bEnableProgrammeScrolling)
+ return MathUtils::round_int(static_cast<double>(m_programmeScrollOffset / m_blockSize));
+ else
+ return m_blockOffset;
+}
+
+void CGUIEPGGridContainer::ChannelScroll(int amount)
+{
+ // increase or decrease the vertical offset
+ int offset = m_channelOffset + amount;
+
+ if (offset > m_gridModel->ChannelItemsSize() - m_channelsPerPage)
+ offset = m_gridModel->ChannelItemsSize() - m_channelsPerPage;
+
+ if (offset < 0)
+ offset = 0;
+
+ ScrollToChannelOffset(offset);
+ SetChannel(m_channelCursor);
+}
+
+void CGUIEPGGridContainer::ProgrammesScroll(int amount)
+{
+ // increase or decrease the horizontal offset
+ ScrollToBlockOffset(m_blockOffset + amount);
+ SetBlock(m_blockCursor);
+}
+
+void CGUIEPGGridContainer::OnUp()
+{
+ if (!HasData())
+ return CGUIControl::OnUp();
+
+ if (m_orientation == VERTICAL)
+ {
+ CGUIAction action = GetAction(ACTION_MOVE_UP);
+ if (m_channelCursor > 0)
+ {
+ SetChannel(m_channelCursor - 1);
+ }
+ else if (m_channelCursor == 0 && m_channelOffset)
+ {
+ ScrollToChannelOffset(m_channelOffset - 1);
+ SetChannel(0);
+ }
+ else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around
+ {
+ int offset = m_gridModel->ChannelItemsSize() - m_channelsPerPage;
+
+ if (offset < 0)
+ offset = 0;
+
+ SetChannel(m_gridModel->GetLastChannel() - offset);
+ ScrollToChannelOffset(offset);
+ }
+ else
+ CGUIControl::OnUp();
+ }
+ else
+ {
+ if (m_gridModel->GetGridItemStartBlock(m_channelCursor + m_channelOffset,
+ m_blockCursor + m_blockOffset) > m_blockOffset)
+ {
+ // this is not first item on page
+ SetItem(GetPrevItem());
+ UpdateBlock();
+ return;
+ }
+ else if (m_blockCursor <= 0 && m_blockOffset && m_blockOffset - BLOCK_SCROLL_OFFSET >= 0)
+ {
+ // this is the first item on page
+ ScrollToBlockOffset(m_blockOffset - BLOCK_SCROLL_OFFSET);
+ UpdateBlock();
+ return;
+ }
+
+ CGUIControl::OnUp();
+ }
+}
+
+void CGUIEPGGridContainer::OnDown()
+{
+ if (!HasData())
+ return CGUIControl::OnDown();
+
+ if (m_orientation == VERTICAL)
+ {
+ CGUIAction action = GetAction(ACTION_MOVE_DOWN);
+ if (m_channelOffset + m_channelCursor < m_gridModel->GetLastChannel())
+ {
+ if (m_channelCursor + 1 < m_channelsPerPage)
+ {
+ SetChannel(m_channelCursor + 1);
+ }
+ else
+ {
+ ScrollToChannelOffset(m_channelOffset + 1);
+ SetChannel(m_channelsPerPage - 1);
+ }
+ }
+ else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around
+ {
+ ScrollToChannelOffset(0);
+ SetChannel(0);
+ }
+ else
+ CGUIControl::OnDown();
+ }
+ else
+ {
+ if (m_gridModel->GetGridItemEndBlock(m_channelCursor + m_channelOffset,
+ m_blockCursor + m_blockOffset) <
+ (m_blockOffset + m_blocksPerPage - 1))
+ {
+ // this is not last item on page
+ SetItem(GetNextItem());
+ UpdateBlock();
+ return;
+ }
+ else if ((m_blockOffset != m_gridModel->GridItemsSize() - m_blocksPerPage) &&
+ m_gridModel->GridItemsSize() > m_blocksPerPage &&
+ m_blockOffset + BLOCK_SCROLL_OFFSET < m_gridModel->GetLastBlock())
+ {
+ // this is the last item on page
+ ScrollToBlockOffset(m_blockOffset + BLOCK_SCROLL_OFFSET);
+ UpdateBlock();
+ return;
+ }
+
+ CGUIControl::OnDown();
+ }
+}
+
+void CGUIEPGGridContainer::OnLeft()
+{
+ if (!HasData())
+ return CGUIControl::OnLeft();
+
+ if (m_orientation == VERTICAL)
+ {
+ if (m_gridModel->GetGridItemStartBlock(m_channelCursor + m_channelOffset,
+ m_blockCursor + m_blockOffset) > m_blockOffset)
+ {
+ // this is not first item on page
+ SetItem(GetPrevItem());
+ UpdateBlock();
+ return;
+ }
+ else if (m_blockCursor <= 0 && m_blockOffset && m_blockOffset - BLOCK_SCROLL_OFFSET >= 0)
+ {
+ // this is the first item on page
+ ScrollToBlockOffset(m_blockOffset - BLOCK_SCROLL_OFFSET);
+ UpdateBlock();
+ return;
+ }
+
+ CGUIControl::OnLeft();
+ }
+ else
+ {
+ CGUIAction action = GetAction(ACTION_MOVE_LEFT);
+ if (m_channelCursor > 0)
+ {
+ SetChannel(m_channelCursor - 1);
+ }
+ else if (m_channelCursor == 0 && m_channelOffset)
+ {
+ ScrollToChannelOffset(m_channelOffset - 1);
+ SetChannel(0);
+ }
+ else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around
+ {
+ int offset = m_gridModel->ChannelItemsSize() - m_channelsPerPage;
+
+ if (offset < 0)
+ offset = 0;
+
+ SetChannel(m_gridModel->GetLastChannel() - offset);
+ ScrollToChannelOffset(offset);
+ }
+ else
+ CGUIControl::OnLeft();
+ }
+}
+
+void CGUIEPGGridContainer::OnRight()
+{
+ if (!HasData())
+ return CGUIControl::OnRight();
+
+ if (m_orientation == VERTICAL)
+ {
+ if (m_gridModel->GetGridItemEndBlock(m_channelCursor + m_channelOffset,
+ m_blockCursor + m_blockOffset) <
+ (m_blockOffset + m_blocksPerPage - 1))
+ {
+ // this is not last item on page
+ SetItem(GetNextItem());
+ UpdateBlock();
+ return;
+ }
+ else if ((m_blockOffset != m_gridModel->GridItemsSize() - m_blocksPerPage) &&
+ m_gridModel->GridItemsSize() > m_blocksPerPage &&
+ m_blockOffset + BLOCK_SCROLL_OFFSET < m_gridModel->GetLastBlock())
+ {
+ // this is the last item on page
+ ScrollToBlockOffset(m_blockOffset + BLOCK_SCROLL_OFFSET);
+ UpdateBlock();
+ return;
+ }
+
+ CGUIControl::OnRight();
+ }
+ else
+ {
+ CGUIAction action = GetAction(ACTION_MOVE_RIGHT);
+ if (m_channelOffset + m_channelCursor < m_gridModel->GetLastChannel())
+ {
+ if (m_channelCursor + 1 < m_channelsPerPage)
+ {
+ SetChannel(m_channelCursor + 1);
+ }
+ else
+ {
+ ScrollToChannelOffset(m_channelOffset + 1);
+ SetChannel(m_channelsPerPage - 1);
+ }
+ }
+ else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around
+ {
+ SetChannel(0);
+ ScrollToChannelOffset(0);
+ }
+ else
+ CGUIControl::OnRight();
+ }
+}
+
+bool CGUIEPGGridContainer::SetChannel(const std::string& channel)
+{
+ for (int iIndex = 0; iIndex < m_gridModel->ChannelItemsSize(); iIndex++)
+ {
+ std::string strPath = m_gridModel->GetChannelItem(iIndex)->GetProperty("path").asString();
+ if (strPath == channel)
+ {
+ GoToChannel(iIndex);
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIEPGGridContainer::SetChannel(const std::shared_ptr<CPVRChannel>& channel)
+{
+ for (int iIndex = 0; iIndex < m_gridModel->ChannelItemsSize(); iIndex++)
+ {
+ int iChannelId = static_cast<int>(m_gridModel->GetChannelItem(iIndex)->GetProperty("channelid").asInteger(-1));
+ if (iChannelId == channel->ChannelID())
+ {
+ GoToChannel(iIndex);
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIEPGGridContainer::SetChannel(const CPVRChannelNumber& channelNumber)
+{
+ for (int iIndex = 0; iIndex < m_gridModel->ChannelItemsSize(); iIndex++)
+ {
+ const CPVRChannelNumber& number =
+ m_gridModel->GetChannelItem(iIndex)->GetPVRChannelGroupMemberInfoTag()->ChannelNumber();
+ if (number == channelNumber)
+ {
+ GoToChannel(iIndex);
+ return true;
+ }
+ }
+ return false;
+}
+
+void CGUIEPGGridContainer::SetChannel(int channel)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ int channelIndex = channel + m_channelOffset;
+ int blockIndex = m_blockCursor + m_blockOffset;
+ if (channelIndex < m_gridModel->ChannelItemsSize() && blockIndex < m_gridModel->GridItemsSize())
+ {
+ if (SetItem(m_gridModel->GetGridItem(channelIndex, m_blockTravelAxis), channelIndex,
+ m_blockTravelAxis))
+ {
+ m_channelCursor = channel;
+ MarkDirtyRegion();
+ UpdateBlock(false);
+ }
+ }
+}
+
+void CGUIEPGGridContainer::SetBlock(int block, bool bUpdateBlockTravelAxis /* = true */)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (block < 0)
+ m_blockCursor = 0;
+ else if (block > m_blocksPerPage - 1)
+ m_blockCursor = m_blocksPerPage - 1;
+ else
+ m_blockCursor = block;
+
+ if (bUpdateBlockTravelAxis)
+ m_blockTravelAxis = m_blockOffset + m_blockCursor;
+
+ UpdateItem();
+ MarkDirtyRegion();
+}
+
+void CGUIEPGGridContainer::UpdateBlock(bool bUpdateBlockTravelAxis /* = true */)
+{
+ SetBlock(m_itemStartBlock > 0 ? m_itemStartBlock - m_blockOffset : 0, bUpdateBlockTravelAxis);
+}
+
+CGUIListItemLayout* CGUIEPGGridContainer::GetFocusedLayout() const
+{
+ CGUIListItemPtr item = GetListItem(0);
+
+ if (item)
+ return item->GetFocusedLayout();
+
+ return nullptr;
+}
+
+bool CGUIEPGGridContainer::SelectItemFromPoint(const CPoint& point, bool justGrid /* = false */)
+{
+ /* point has already had origin set to m_posX, m_posY */
+ if (!m_focusedProgrammeLayout || !m_programmeLayout || (justGrid && point.x < 0))
+ return false;
+
+ int channel;
+ int block;
+
+ if (m_orientation == VERTICAL)
+ {
+ channel = point.y / m_channelHeight;
+ block = point.x / m_blockSize;
+ }
+ else
+ {
+ channel = point.x / m_channelWidth;
+ block = point.y / m_blockSize;
+ }
+
+ if (channel > m_channelsPerPage)
+ channel = m_channelsPerPage - 1;
+
+ if (channel >= m_gridModel->ChannelItemsSize())
+ channel = m_gridModel->GetLastChannel();
+
+ if (channel < 0)
+ channel = 0;
+
+ if (block > m_blocksPerPage)
+ block = m_blocksPerPage - 1;
+
+ if (block < 0)
+ block = 0;
+
+ int channelIndex = channel + m_channelOffset;
+ int blockIndex = block + m_blockOffset;
+
+ // bail if out of range
+ if (channelIndex >= m_gridModel->ChannelItemsSize() || blockIndex >= m_gridModel->GridItemsSize())
+ return false;
+
+ // bail if block isn't occupied
+ if (!m_gridModel->GetGridItem(channelIndex, blockIndex))
+ return false;
+
+ SetChannel(channel);
+ SetBlock(block);
+ return true;
+}
+
+EVENT_RESULT CGUIEPGGridContainer::OnMouseEvent(const CPoint& point, const CMouseEvent& event)
+{
+ switch (event.m_id)
+ {
+ case ACTION_MOUSE_LEFT_CLICK:
+ OnMouseClick(0, point);
+ return EVENT_RESULT_HANDLED;
+ case ACTION_MOUSE_RIGHT_CLICK:
+ OnMouseClick(1, point);
+ return EVENT_RESULT_HANDLED;
+ case ACTION_MOUSE_DOUBLE_CLICK:
+ OnMouseDoubleClick(0, point);
+ return EVENT_RESULT_HANDLED;
+ case ACTION_MOUSE_WHEEL_UP:
+ OnMouseWheel(-1, point);
+ return EVENT_RESULT_HANDLED;
+ case ACTION_MOUSE_WHEEL_DOWN:
+ OnMouseWheel(1, point);
+ return EVENT_RESULT_HANDLED;
+ case ACTION_GESTURE_BEGIN:
+ {
+ // we want exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return EVENT_RESULT_HANDLED;
+ }
+ case ACTION_GESTURE_END:
+ case ACTION_GESTURE_ABORT:
+ {
+ // we're done with exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID());
+ SendWindowMessage(msg);
+ ScrollToChannelOffset(MathUtils::round_int(
+ static_cast<double>(m_channelScrollOffset / m_channelLayout->Size(m_orientation))));
+ SetChannel(m_channelCursor);
+ ScrollToBlockOffset(
+ MathUtils::round_int(static_cast<double>(m_programmeScrollOffset / m_blockSize)));
+ SetBlock(m_blockCursor);
+ return EVENT_RESULT_HANDLED;
+ }
+ case ACTION_GESTURE_PAN:
+ {
+ m_programmeScrollOffset -= event.m_offsetX;
+ m_channelScrollOffset -= event.m_offsetY;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_channelOffset = MathUtils::round_int(
+ static_cast<double>(m_channelScrollOffset / m_channelLayout->Size(m_orientation)));
+ m_blockOffset =
+ MathUtils::round_int(static_cast<double>(m_programmeScrollOffset / m_blockSize));
+ ValidateOffset();
+ }
+ return EVENT_RESULT_HANDLED;
+ }
+ default:
+ return EVENT_RESULT_UNHANDLED;
+ }
+}
+
+bool CGUIEPGGridContainer::OnMouseOver(const CPoint& point)
+{
+ // select the item under the pointer
+ SelectItemFromPoint(point - CPoint(m_gridPosX, m_gridPosY), false);
+ return CGUIControl::OnMouseOver(point);
+}
+
+bool CGUIEPGGridContainer::OnMouseClick(int dwButton, const CPoint& point)
+{
+ if (SelectItemFromPoint(point - CPoint(m_gridPosX, m_gridPosY)))
+ {
+ // send click message to window
+ OnClick(ACTION_MOUSE_LEFT_CLICK + dwButton);
+ return true;
+ }
+ return false;
+}
+
+bool CGUIEPGGridContainer::OnMouseDoubleClick(int dwButton, const CPoint& point)
+{
+ if (SelectItemFromPoint(point - CPoint(m_gridPosX, m_gridPosY)))
+ {
+ // send double click message to window
+ OnClick(ACTION_MOUSE_DOUBLE_CLICK + dwButton);
+ return true;
+ }
+ return false;
+}
+
+bool CGUIEPGGridContainer::OnClick(int actionID)
+{
+ int subItem = 0;
+
+ if (actionID == ACTION_SELECT_ITEM || actionID == ACTION_MOUSE_LEFT_CLICK)
+ {
+ // grab the currently focused subitem (if applicable)
+ CGUIListItemLayout* focusedLayout = GetFocusedLayout();
+
+ if (focusedLayout)
+ subItem = focusedLayout->GetFocusedItem();
+ }
+
+ // Don't know what to do, so send to our parent window.
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID(), actionID, subItem);
+ return SendWindowMessage(msg);
+}
+
+bool CGUIEPGGridContainer::OnMouseWheel(char wheel, const CPoint& point)
+{
+ // doesn't work while an item is selected?
+ ProgrammesScroll(-wheel);
+ return true;
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CGUIEPGGridContainer::GetSelectedChannelGroupMember() const
+{
+ CFileItemPtr fileItem;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_channelCursor + m_channelOffset < m_gridModel->ChannelItemsSize())
+ fileItem = m_gridModel->GetChannelItem(m_channelCursor + m_channelOffset);
+ }
+
+ if (fileItem)
+ return fileItem->GetPVRChannelGroupMemberInfoTag();
+
+ return {};
+}
+
+CDateTime CGUIEPGGridContainer::GetSelectedDate() const
+{
+ return m_gridModel->GetStartTimeForBlock(m_blockOffset + m_blockCursor);
+}
+
+CFileItemPtr CGUIEPGGridContainer::GetSelectedGridItem(int offset /*= 0*/) const
+{
+ CFileItemPtr item;
+
+ if (m_channelCursor + m_channelOffset + offset < m_gridModel->ChannelItemsSize() &&
+ m_blockCursor + m_blockOffset < m_gridModel->GridItemsSize())
+ item = m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset);
+
+ return item;
+}
+
+
+CGUIListItemPtr CGUIEPGGridContainer::GetListItem(int offset, unsigned int flag) const
+{
+ if (!m_gridModel->HasChannelItems())
+ return CGUIListItemPtr();
+
+ int item = m_channelCursor + m_channelOffset + offset;
+ if (flag & INFOFLAG_LISTITEM_POSITION)
+ item = GetChannelScrollOffset(m_channelLayout);
+
+ if (flag & INFOFLAG_LISTITEM_WRAP)
+ {
+ item %= m_gridModel->ChannelItemsSize();
+ if (item < 0)
+ item += m_gridModel->ChannelItemsSize();
+
+ return m_gridModel->GetChannelItem(item);
+ }
+ else
+ {
+ if (item >= 0 && item < m_gridModel->ChannelItemsSize())
+ return m_gridModel->GetChannelItem(item);
+ }
+ return CGUIListItemPtr();
+}
+
+std::string CGUIEPGGridContainer::GetLabel(int info) const
+{
+ std::string label;
+ switch (info)
+ {
+ case CONTAINER_NUM_PAGES:
+ if (m_channelsPerPage > 0)
+ label = std::to_string((m_gridModel->ChannelItemsSize() + m_channelsPerPage - 1) /
+ m_channelsPerPage);
+ else
+ label = std::to_string(0);
+ break;
+ case CONTAINER_CURRENT_PAGE:
+ if (m_channelsPerPage > 0)
+ label = std::to_string(1 + (m_channelCursor + m_channelOffset) / m_channelsPerPage);
+ else
+ label = std::to_string(1);
+ break;
+ case CONTAINER_POSITION:
+ label = std::to_string(1 + m_channelCursor + m_channelOffset);
+ break;
+ case CONTAINER_NUM_ITEMS:
+ label = std::to_string(m_gridModel->ChannelItemsSize());
+ break;
+ default:
+ break;
+ }
+ return label;
+}
+
+void CGUIEPGGridContainer::SetItem(const std::pair<std::shared_ptr<CFileItem>, int>& itemInfo)
+{
+ SetItem(itemInfo.first, m_channelCursor + m_channelOffset, itemInfo.second);
+}
+
+bool CGUIEPGGridContainer::SetItem(const std::shared_ptr<CFileItem>& item,
+ int channelIndex,
+ int blockIndex)
+{
+ if (item && channelIndex < m_gridModel->ChannelItemsSize() &&
+ blockIndex < m_gridModel->GridItemsSize())
+ {
+ m_itemStartBlock = m_gridModel->GetGridItemStartBlock(channelIndex, blockIndex);
+ return true;
+ }
+ else
+ {
+ m_itemStartBlock = 0;
+ return false;
+ }
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainer::GetItem() const
+{
+ const int channelIndex = m_channelCursor + m_channelOffset;
+ const int blockIndex = m_blockCursor + m_blockOffset;
+
+ if (channelIndex >= m_gridModel->ChannelItemsSize() || blockIndex >= m_gridModel->GridItemsSize())
+ return {};
+
+ return m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset);
+}
+
+std::pair<std::shared_ptr<CFileItem>, int> CGUIEPGGridContainer::GetNextItem() const
+{
+ int block = m_gridModel->GetGridItemEndBlock(m_channelCursor + m_channelOffset,
+ m_blockCursor + m_blockOffset);
+ if (block < m_gridModel->GridItemsSize())
+ {
+ // first block of next event is one block after end block of selected event
+ block += 1;
+ }
+
+ return {m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, block), block};
+}
+
+std::pair<std::shared_ptr<CFileItem>, int> CGUIEPGGridContainer::GetPrevItem() const
+{
+ int block = m_gridModel->GetGridItemStartBlock(m_channelCursor + m_channelOffset,
+ m_blockCursor + m_blockOffset);
+ if (block > 0)
+ {
+ // last block of previous event is one block before start block of selected event
+ block -= 1;
+ }
+
+ return {m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, block), block};
+}
+
+void CGUIEPGGridContainer::UpdateItem()
+{
+ SetItem(GetItem(), m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset);
+}
+
+void CGUIEPGGridContainer::SetFocus(bool focus)
+{
+ if (focus != HasFocus())
+ SetInvalid();
+
+ CGUIControl::SetFocus(focus);
+}
+
+void CGUIEPGGridContainer::ScrollToChannelOffset(int offset)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ float size = m_programmeLayout->Size(m_orientation);
+ int range = m_channelsPerPage / 4;
+
+ if (range <= 0)
+ range = 1;
+
+ if (offset * size < m_channelScrollOffset && m_channelScrollOffset - offset * size > size * range)
+ {
+ // scrolling up, and we're jumping more than 0.5 of a screen
+ m_channelScrollOffset = (offset + range) * size;
+ }
+
+ if (offset * size > m_channelScrollOffset && offset * size - m_channelScrollOffset > size * range)
+ {
+ // scrolling down, and we're jumping more than 0.5 of a screen
+ m_channelScrollOffset = (offset - range) * size;
+ }
+
+ m_channelScrollSpeed = (offset * size - m_channelScrollOffset) / m_scrollTime;
+ m_channelOffset = offset;
+ MarkDirtyRegion();
+}
+
+void CGUIEPGGridContainer::ScrollToBlockOffset(int offset)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // make sure offset is in valid range
+ offset = std::max(0, std::min(offset, m_gridModel->GridItemsSize() - m_blocksPerPage));
+
+ float size = m_blockSize;
+ int range = m_blocksPerPage / 1;
+
+ if (range <= 0)
+ range = 1;
+
+ if (offset * size < m_programmeScrollOffset && m_programmeScrollOffset - offset * size > size * range)
+ {
+ // scrolling left, and we're jumping more than 0.5 of a screen
+ m_programmeScrollOffset = (offset + range) * size;
+ }
+
+ if (offset * size > m_programmeScrollOffset && offset * size - m_programmeScrollOffset > size * range)
+ {
+ // scrolling right, and we're jumping more than 0.5 of a screen
+ m_programmeScrollOffset = (offset - range) * size;
+ }
+
+ m_programmeScrollSpeed = (offset * size - m_programmeScrollOffset) / m_scrollTime;
+ m_blockOffset = offset;
+ MarkDirtyRegion();
+}
+
+void CGUIEPGGridContainer::ValidateOffset()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_programmeLayout)
+ return;
+
+ float pos = (m_orientation == VERTICAL) ? m_channelHeight : m_channelWidth;
+
+ if (m_gridModel->ChannelItemsSize() &&
+ (m_channelOffset > m_gridModel->ChannelItemsSize() - m_channelsPerPage ||
+ m_channelScrollOffset > (m_gridModel->ChannelItemsSize() - m_channelsPerPage) * pos))
+ {
+ m_channelOffset = m_gridModel->ChannelItemsSize() - m_channelsPerPage;
+ m_channelScrollOffset = m_channelOffset * pos;
+ }
+
+ if (m_channelOffset < 0 || m_channelScrollOffset < 0)
+ {
+ m_channelOffset = 0;
+ m_channelScrollOffset = 0;
+ }
+
+ if (m_gridModel->GridItemsSize() &&
+ (m_blockOffset > m_gridModel->GridItemsSize() - m_blocksPerPage ||
+ m_programmeScrollOffset > (m_gridModel->GridItemsSize() - m_blocksPerPage) * m_blockSize))
+ {
+ m_blockOffset = m_gridModel->GridItemsSize() - m_blocksPerPage;
+ m_programmeScrollOffset = m_blockOffset * m_blockSize;
+ }
+
+ if (m_blockOffset < 0 || m_programmeScrollOffset < 0)
+ {
+ m_blockOffset = 0;
+ m_programmeScrollOffset = 0;
+ }
+}
+
+void CGUIEPGGridContainer::LoadLayout(TiXmlElement* layout)
+{
+ /* layouts for the channel column */
+ TiXmlElement* itemElement = layout->FirstChildElement("channellayout");
+ while (itemElement)
+ {
+ m_channelLayouts.emplace_back();
+ m_channelLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("channellayout");
+ }
+ itemElement = layout->FirstChildElement("focusedchannellayout");
+ while (itemElement)
+ {
+ m_focusedChannelLayouts.emplace_back();
+ m_focusedChannelLayouts.back().LoadLayout(itemElement, GetParentID(), true, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("focusedchannellayout");
+ }
+
+ /* layouts for the grid items */
+ itemElement = layout->FirstChildElement("focusedlayout");
+ while (itemElement)
+ {
+ m_focusedProgrammeLayouts.emplace_back();
+ m_focusedProgrammeLayouts.back().LoadLayout(itemElement, GetParentID(), true, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("focusedlayout");
+ }
+ itemElement = layout->FirstChildElement("itemlayout");
+ while (itemElement)
+ {
+ m_programmeLayouts.emplace_back();
+ m_programmeLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("itemlayout");
+ }
+
+ /* layout for the date label for the grid */
+ itemElement = layout->FirstChildElement("rulerdatelayout");
+ while (itemElement)
+ {
+ m_rulerDateLayouts.emplace_back();
+ m_rulerDateLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("rulerdatelayout");
+ }
+
+ /* layout for the timeline for the grid */
+ itemElement = layout->FirstChildElement("rulerlayout");
+ while (itemElement)
+ {
+ m_rulerLayouts.emplace_back();
+ m_rulerLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("rulerlayout");
+ }
+
+ UpdateLayout();
+}
+
+std::string CGUIEPGGridContainer::GetDescription() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const int channelIndex = m_channelCursor + m_channelOffset;
+ const int blockIndex = m_blockCursor + m_blockOffset;
+
+ if (channelIndex < m_gridModel->ChannelItemsSize() && blockIndex < m_gridModel->GridItemsSize())
+ {
+ const std::shared_ptr<CFileItem> item = m_gridModel->GetGridItem(channelIndex, blockIndex);
+ if (item)
+ return item->GetLabel();
+ }
+
+ return {};
+}
+
+void CGUIEPGGridContainer::JumpToNow()
+{
+ m_bEnableProgrammeScrolling = false;
+ GoToNow();
+}
+
+void CGUIEPGGridContainer::JumpToDate(const CDateTime& date)
+{
+ m_bEnableProgrammeScrolling = false;
+ GoToDate(date);
+}
+
+void CGUIEPGGridContainer::GoToBegin()
+{
+ ScrollToBlockOffset(0);
+ SetBlock(0);
+}
+
+void CGUIEPGGridContainer::GoToEnd()
+{
+ ScrollToBlockOffset(m_gridModel->GetLastBlock() - m_blocksPerPage + 1);
+ SetBlock(m_blocksPerPage - 1);
+}
+
+void CGUIEPGGridContainer::GoToNow()
+{
+ GoToDate(CDateTime::GetUTCDateTime());
+}
+
+void CGUIEPGGridContainer::GoToDate(const CDateTime& date)
+{
+ unsigned int offset = m_gridModel->GetPageNowOffset();
+ ScrollToBlockOffset(m_gridModel->GetBlock(date) - offset);
+
+ // ensure we're selecting the active event, not its predecessor.
+ const int iChannel = m_channelOffset + m_channelCursor;
+ const int iBlock = m_blockOffset + offset;
+ if (iChannel >= m_gridModel->ChannelItemsSize() || iBlock >= m_gridModel->GridItemsSize() ||
+ m_gridModel->GetGridItemEndTime(iChannel, iBlock) > date)
+ {
+ SetBlock(offset);
+ }
+ else
+ {
+ SetBlock(offset + 1);
+ }
+}
+
+void CGUIEPGGridContainer::GoToFirstChannel()
+{
+ GoToChannel(0);
+}
+
+void CGUIEPGGridContainer::GoToLastChannel()
+{
+ if (m_gridModel->ChannelItemsSize())
+ GoToChannel(m_gridModel->GetLastChannel());
+ else
+ GoToChannel(0);
+}
+
+void CGUIEPGGridContainer::GoToTop()
+{
+ if (m_orientation == VERTICAL)
+ {
+ GoToChannel(0);
+ }
+ else
+ {
+ GoToBlock(0);
+ }
+}
+
+void CGUIEPGGridContainer::GoToBottom()
+{
+ if (m_orientation == VERTICAL)
+ {
+ if (m_gridModel->HasChannelItems())
+ GoToChannel(m_gridModel->GetLastChannel());
+ else
+ GoToChannel(0);
+ }
+ else
+ {
+ if (m_gridModel->GridItemsSize())
+ GoToBlock(m_gridModel->GetLastBlock());
+ else
+ GoToBlock(0);
+ }
+}
+
+void CGUIEPGGridContainer::GoToMostLeft()
+{
+ if (m_orientation == VERTICAL)
+ {
+ GoToBlock(0);
+ }
+ else
+ {
+ GoToChannel(0);
+ }
+}
+
+void CGUIEPGGridContainer::GoToMostRight()
+{
+ if (m_orientation == VERTICAL)
+ {
+ if (m_gridModel->GridItemsSize())
+ GoToBlock(m_gridModel->GetLastBlock());
+ else
+ GoToBlock(0);
+ }
+ else
+ {
+ if (m_gridModel->HasChannelItems())
+ GoToChannel(m_gridModel->GetLastChannel());
+ else
+ GoToChannel(0);
+ }
+}
+
+void CGUIEPGGridContainer::SetTimelineItems(const std::unique_ptr<CFileItemList>& items,
+ const CDateTime& gridStart,
+ const CDateTime& gridEnd)
+{
+ int iRulerUnit;
+ int iFirstChannel;
+ int iChannelsPerPage;
+ int iBlocksPerPage;
+ int iFirstBlock;
+ float fBlockSize;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ iRulerUnit = m_rulerUnit;
+ iFirstChannel = m_channelOffset;
+ iChannelsPerPage = m_channelsPerPage;
+ iFirstBlock = m_blockOffset;
+ iBlocksPerPage = m_blocksPerPage;
+ fBlockSize = m_blockSize;
+ }
+
+ std::unique_ptr<CGUIEPGGridContainerModel> oldUpdatedGridModel;
+ std::unique_ptr<CGUIEPGGridContainerModel> newUpdatedGridModel(new CGUIEPGGridContainerModel);
+
+ newUpdatedGridModel->Initialize(items, gridStart, gridEnd, iFirstChannel, iChannelsPerPage,
+ iFirstBlock, iBlocksPerPage, iRulerUnit, fBlockSize);
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // grid contains CFileItem instances. CFileItem dtor locks global graphics mutex.
+ // by increasing its refcount make sure, old data are not deleted while we're holding own mutex.
+ oldUpdatedGridModel = std::move(m_updatedGridModel);
+
+ m_updatedGridModel = std::move(newUpdatedGridModel);
+ }
+}
+
+std::unique_ptr<CFileItemList> CGUIEPGGridContainer::GetCurrentTimeLineItems() const
+{
+ return m_gridModel->GetCurrentTimeLineItems(m_channelOffset, m_channelsPerPage);
+}
+
+void CGUIEPGGridContainer::GoToChannel(int channelIndex)
+{
+ if (channelIndex < m_channelsPerPage)
+ {
+ // first page
+ ScrollToChannelOffset(0);
+ SetChannel(channelIndex);
+ }
+ else if (channelIndex > m_gridModel->ChannelItemsSize() - m_channelsPerPage)
+ {
+ // last page
+ ScrollToChannelOffset(m_gridModel->ChannelItemsSize() - m_channelsPerPage);
+ SetChannel(channelIndex - (m_gridModel->ChannelItemsSize() - m_channelsPerPage));
+ }
+ else
+ {
+ ScrollToChannelOffset(channelIndex - m_channelCursor);
+ SetChannel(m_channelCursor);
+ }
+}
+
+void CGUIEPGGridContainer::GoToBlock(int blockIndex)
+{
+ int lastPage = m_gridModel->GridItemsSize() - m_blocksPerPage;
+ if (blockIndex > lastPage)
+ {
+ // last page
+ ScrollToBlockOffset(lastPage);
+ SetBlock(blockIndex - lastPage);
+ }
+ else
+ {
+ ScrollToBlockOffset(blockIndex - m_blockCursor);
+ SetBlock(m_blockCursor);
+ }
+}
+
+void CGUIEPGGridContainer::UpdateLayout()
+{
+ CGUIListItemLayout* oldFocusedChannelLayout = m_focusedChannelLayout;
+ CGUIListItemLayout* oldChannelLayout = m_channelLayout;
+ CGUIListItemLayout* oldFocusedProgrammeLayout = m_focusedProgrammeLayout;
+ CGUIListItemLayout* oldProgrammeLayout = m_programmeLayout;
+ CGUIListItemLayout* oldRulerLayout = m_rulerLayout;
+ CGUIListItemLayout* oldRulerDateLayout = m_rulerDateLayout;
+
+ GetCurrentLayouts();
+
+ // Note: m_rulerDateLayout is optional
+ if (!m_focusedProgrammeLayout || !m_programmeLayout || !m_focusedChannelLayout || !m_channelLayout || !m_rulerLayout)
+ return;
+
+ if (oldChannelLayout == m_channelLayout && oldFocusedChannelLayout == m_focusedChannelLayout &&
+ oldProgrammeLayout == m_programmeLayout && oldFocusedProgrammeLayout == m_focusedProgrammeLayout &&
+ oldRulerLayout == m_rulerLayout && oldRulerDateLayout == m_rulerDateLayout)
+ return; // nothing has changed, so don't update stuff
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_channelHeight = m_channelLayout->Size(VERTICAL);
+ m_channelWidth = m_channelLayout->Size(HORIZONTAL);
+
+ m_rulerDateHeight = m_rulerDateLayout ? m_rulerDateLayout->Size(VERTICAL) : 0;
+ m_rulerDateWidth = m_rulerDateLayout ? m_rulerDateLayout->Size(HORIZONTAL) : 0;
+
+ if (m_orientation == VERTICAL)
+ {
+ m_rulerHeight = m_rulerLayout->Size(VERTICAL);
+ m_gridPosX = m_posX + m_channelWidth;
+ m_gridPosY = m_posY + m_rulerHeight + m_rulerDateHeight;
+ m_gridWidth = m_width - m_channelWidth;
+ m_gridHeight = m_height - m_rulerHeight - m_rulerDateHeight;
+ m_blockSize = m_gridWidth / m_blocksPerPage;
+ m_rulerWidth = m_rulerUnit * m_blockSize;
+ m_channelPosX = m_posX;
+ m_channelPosY = m_posY + m_rulerHeight + m_rulerDateHeight;
+ m_rulerPosX = m_posX + m_channelWidth;
+ m_rulerPosY = m_posY + m_rulerDateHeight;
+ m_channelsPerPage = m_gridHeight / m_channelHeight;
+ m_programmesPerPage = (m_gridWidth / m_blockSize) + 1;
+
+ m_programmeLayout->SetHeight(m_channelHeight);
+ m_focusedProgrammeLayout->SetHeight(m_channelHeight);
+ }
+ else
+ {
+ m_rulerWidth = m_rulerLayout->Size(HORIZONTAL);
+ m_gridPosX = m_posX + m_rulerWidth;
+ m_gridPosY = m_posY + m_channelHeight + m_rulerDateHeight;
+ m_gridWidth = m_width - m_rulerWidth;
+ m_gridHeight = m_height - m_channelHeight - m_rulerDateHeight;
+ m_blockSize = m_gridHeight / m_blocksPerPage;
+ m_rulerHeight = m_rulerUnit * m_blockSize;
+ m_channelPosX = m_posX + m_rulerWidth;
+ m_channelPosY = m_posY + m_rulerDateHeight;
+ m_rulerPosX = m_posX;
+ m_rulerPosY = m_posY + m_channelHeight + m_rulerDateHeight;
+ m_channelsPerPage = m_gridWidth / m_channelWidth;
+ m_programmesPerPage = (m_gridHeight / m_blockSize) + 1;
+
+ m_programmeLayout->SetWidth(m_channelWidth);
+ m_focusedProgrammeLayout->SetWidth(m_channelWidth);
+ }
+
+ // ensure that the scroll offsets are a multiple of our sizes
+ m_channelScrollOffset = m_channelOffset * m_programmeLayout->Size(m_orientation);
+ m_programmeScrollOffset = m_blockOffset * m_blockSize;
+}
+
+void CGUIEPGGridContainer::UpdateScrollOffset(unsigned int currentTime)
+{
+ if (!m_programmeLayout)
+ return;
+
+ m_channelScrollOffset += m_channelScrollSpeed * (currentTime - m_channelScrollLastTime);
+ if ((m_channelScrollSpeed < 0 && m_channelScrollOffset < m_channelOffset * m_programmeLayout->Size(m_orientation)) ||
+ (m_channelScrollSpeed > 0 && m_channelScrollOffset > m_channelOffset * m_programmeLayout->Size(m_orientation)))
+ {
+ m_channelScrollOffset = m_channelOffset * m_programmeLayout->Size(m_orientation);
+ m_channelScrollSpeed = 0;
+ m_bEnableChannelScrolling = true;
+ }
+
+ m_channelScrollLastTime = currentTime;
+ m_programmeScrollOffset += m_programmeScrollSpeed * (currentTime - m_programmeScrollLastTime);
+
+ if ((m_programmeScrollSpeed < 0 && m_programmeScrollOffset < m_blockOffset * m_blockSize) ||
+ (m_programmeScrollSpeed > 0 && m_programmeScrollOffset > m_blockOffset * m_blockSize))
+ {
+ m_programmeScrollOffset = m_blockOffset * m_blockSize;
+ m_programmeScrollSpeed = 0;
+ m_bEnableProgrammeScrolling = true;
+ }
+
+ m_programmeScrollLastTime = currentTime;
+
+ if (m_channelScrollSpeed || m_programmeScrollSpeed)
+ MarkDirtyRegion();
+}
+
+void CGUIEPGGridContainer::GetCurrentLayouts()
+{
+ m_channelLayout = nullptr;
+
+ for (unsigned int i = 0; i < m_channelLayouts.size(); i++)
+ {
+ if (m_channelLayouts[i].CheckCondition())
+ {
+ m_channelLayout = &m_channelLayouts[i];
+ break;
+ }
+ }
+
+ if (!m_channelLayout && !m_channelLayouts.empty())
+ m_channelLayout = &m_channelLayouts[0]; // failsafe
+
+ m_focusedChannelLayout = nullptr;
+
+ for (unsigned int i = 0; i < m_focusedChannelLayouts.size(); i++)
+ {
+ if (m_focusedChannelLayouts[i].CheckCondition())
+ {
+ m_focusedChannelLayout = &m_focusedChannelLayouts[i];
+ break;
+ }
+ }
+
+ if (!m_focusedChannelLayout && !m_focusedChannelLayouts.empty())
+ m_focusedChannelLayout = &m_focusedChannelLayouts[0]; // failsafe
+
+ m_programmeLayout = nullptr;
+
+ for (unsigned int i = 0; i < m_programmeLayouts.size(); i++)
+ {
+ if (m_programmeLayouts[i].CheckCondition())
+ {
+ m_programmeLayout = &m_programmeLayouts[i];
+ break;
+ }
+ }
+
+ if (!m_programmeLayout && !m_programmeLayouts.empty())
+ m_programmeLayout = &m_programmeLayouts[0]; // failsafe
+
+ m_focusedProgrammeLayout = nullptr;
+
+ for (unsigned int i = 0; i < m_focusedProgrammeLayouts.size(); i++)
+ {
+ if (m_focusedProgrammeLayouts[i].CheckCondition())
+ {
+ m_focusedProgrammeLayout = &m_focusedProgrammeLayouts[i];
+ break;
+ }
+ }
+
+ if (!m_focusedProgrammeLayout && !m_focusedProgrammeLayouts.empty())
+ m_focusedProgrammeLayout = &m_focusedProgrammeLayouts[0]; // failsafe
+
+ m_rulerLayout = nullptr;
+
+ for (unsigned int i = 0; i < m_rulerLayouts.size(); i++)
+ {
+ if (m_rulerLayouts[i].CheckCondition())
+ {
+ m_rulerLayout = &m_rulerLayouts[i];
+ break;
+ }
+ }
+
+ if (!m_rulerLayout && !m_rulerLayouts.empty())
+ m_rulerLayout = &m_rulerLayouts[0]; // failsafe
+
+ m_rulerDateLayout = nullptr;
+
+ for (unsigned int i = 0; i < m_rulerDateLayouts.size(); i++)
+ {
+ if (m_rulerDateLayouts[i].CheckCondition())
+ {
+ m_rulerDateLayout = &m_rulerDateLayouts[i];
+ break;
+ }
+ }
+
+ // Note: m_rulerDateLayout is optional; so no "failsafe" logic here (see above)
+}
+
+void CGUIEPGGridContainer::SetRenderOffset(const CPoint& offset)
+{
+ m_renderOffset = offset;
+}
+
+void CGUIEPGGridContainer::GetChannelCacheOffsets(int& cacheBefore, int& cacheAfter)
+{
+ if (m_channelScrollSpeed > 0)
+ {
+ cacheBefore = 0;
+ cacheAfter = m_cacheChannelItems;
+ }
+ else if (m_channelScrollSpeed < 0)
+ {
+ cacheBefore = m_cacheChannelItems;
+ cacheAfter = 0;
+ }
+ else
+ {
+ cacheBefore = m_cacheChannelItems / 2;
+ cacheAfter = m_cacheChannelItems / 2;
+ }
+}
+
+void CGUIEPGGridContainer::GetProgrammeCacheOffsets(int& cacheBefore, int& cacheAfter)
+{
+ if (m_programmeScrollSpeed > 0)
+ {
+ cacheBefore = 0;
+ cacheAfter = m_cacheProgrammeItems;
+ }
+ else if (m_programmeScrollSpeed < 0)
+ {
+ cacheBefore = m_cacheProgrammeItems;
+ cacheAfter = 0;
+ }
+ else
+ {
+ cacheBefore = m_cacheProgrammeItems / 2;
+ cacheAfter = m_cacheProgrammeItems / 2;
+ }
+}
+
+void CGUIEPGGridContainer::HandleChannels(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ if (!m_focusedChannelLayout || !m_channelLayout)
+ return;
+
+ const int chanOffset = GetChannelScrollOffset(m_programmeLayout);
+
+ int cacheBeforeChannel, cacheAfterChannel;
+ GetChannelCacheOffsets(cacheBeforeChannel, cacheAfterChannel);
+
+ if (bRender)
+ {
+ if (m_orientation == VERTICAL)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_channelPosX, m_channelPosY, m_channelWidth, m_gridHeight);
+ else
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_channelPosX, m_channelPosY, m_gridWidth, m_channelHeight);
+ }
+ else
+ {
+ // Free memory not used on screen
+ if (m_gridModel->ChannelItemsSize() > m_channelsPerPage + cacheBeforeChannel + cacheAfterChannel)
+ m_gridModel->FreeChannelMemory(chanOffset - cacheBeforeChannel,
+ chanOffset + m_channelsPerPage - 1 + cacheAfterChannel);
+ }
+
+ CPoint originChannel = CPoint(m_channelPosX, m_channelPosY) + m_renderOffset;
+ float pos;
+ float end;
+
+ if (m_orientation == VERTICAL)
+ {
+ pos = originChannel.y;
+ end = m_posY + m_height;
+ }
+ else
+ {
+ pos = originChannel.x;
+ end = m_posX + m_width;
+ }
+
+ // we offset our draw position to take into account scrolling and whether or not our focused
+ // item is offscreen "above" the list.
+ float drawOffset = (chanOffset - cacheBeforeChannel) * m_channelLayout->Size(m_orientation) -
+ GetChannelScrollOffsetPos();
+ if (m_channelOffset + m_channelCursor < chanOffset)
+ drawOffset += m_focusedChannelLayout->Size(m_orientation) - m_channelLayout->Size(m_orientation);
+
+ pos += drawOffset;
+ end += cacheAfterChannel * m_channelLayout->Size(m_orientation);
+
+ float focusedPos = 0;
+ CGUIListItemPtr focusedItem;
+
+ CFileItemPtr item;
+ int current = chanOffset - cacheBeforeChannel;
+ while (pos < end && m_gridModel->HasChannelItems())
+ {
+ int itemNo = current;
+ if (itemNo >= m_gridModel->ChannelItemsSize())
+ break;
+
+ bool focused = (current == m_channelOffset + m_channelCursor);
+ if (itemNo >= 0)
+ {
+ item = m_gridModel->GetChannelItem(itemNo);
+ if (bRender)
+ {
+ // render our item
+ if (focused)
+ {
+ focusedPos = pos;
+ focusedItem = item;
+ }
+ else
+ {
+ if (m_orientation == VERTICAL)
+ RenderItem(originChannel.x, pos, item.get(), false);
+ else
+ RenderItem(pos, originChannel.y, item.get(), false);
+ }
+ }
+ else
+ {
+ // process our item
+ if (m_orientation == VERTICAL)
+ ProcessItem(originChannel.x, pos, item, m_lastItem, focused, m_channelLayout, m_focusedChannelLayout, currentTime, dirtyregions);
+ else
+ ProcessItem(pos, originChannel.y, item, m_lastItem, focused, m_channelLayout, m_focusedChannelLayout, currentTime, dirtyregions);
+ }
+ }
+ // increment our position
+ pos += focused ? m_focusedChannelLayout->Size(m_orientation) : m_channelLayout->Size(m_orientation);
+ current++;
+ }
+
+ if (bRender)
+ {
+ // render focused item last so it can overlap other items
+ if (focusedItem)
+ {
+ if (m_orientation == VERTICAL)
+ RenderItem(originChannel.x, focusedPos, focusedItem.get(), true);
+ else
+ RenderItem(focusedPos, originChannel.y, focusedItem.get(), true);
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+}
+
+void CGUIEPGGridContainer::HandleRulerDate(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ if (!m_rulerDateLayout || m_gridModel->RulerItemsSize() <= 1 || m_gridModel->IsZeroGridDuration())
+ return;
+
+ CFileItemPtr item(m_gridModel->GetRulerItem(0));
+
+ if (bRender)
+ {
+ // Render single ruler item with date of selected programme
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_rulerDateWidth, m_rulerDateHeight);
+ RenderItem(m_posX, m_posY, item.get(), false);
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+ else
+ {
+ const int rulerOffset = GetProgrammeScrollOffset();
+ item->SetLabel(m_gridModel->GetRulerItem(rulerOffset / m_rulerUnit + 1)->GetLabel2());
+
+ CFileItemPtr lastitem;
+ ProcessItem(m_posX, m_posY, item, lastitem, false, m_rulerDateLayout, m_rulerDateLayout, currentTime, dirtyregions);
+ }
+}
+
+void CGUIEPGGridContainer::HandleRuler(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ if (!m_rulerLayout || m_gridModel->RulerItemsSize() <= 1 || m_gridModel->IsZeroGridDuration())
+ return;
+
+ int rulerOffset = GetProgrammeScrollOffset();
+
+ CFileItemPtr item(m_gridModel->GetRulerItem(0));
+ CFileItemPtr lastitem;
+ int cacheBeforeRuler, cacheAfterRuler;
+
+ if (bRender)
+ {
+ if (!m_rulerDateLayout)
+ {
+ // Render single ruler item with date of selected programme
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height);
+ RenderItem(m_posX, m_posY, item.get(), false);
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+
+ // render ruler items
+ GetProgrammeCacheOffsets(cacheBeforeRuler, cacheAfterRuler);
+
+ if (m_orientation == VERTICAL)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_rulerPosX, m_rulerPosY, m_gridWidth, m_rulerHeight);
+ else
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_rulerPosX, m_rulerPosY, m_rulerWidth, m_gridHeight);
+ }
+ else
+ {
+ if (!m_rulerDateLayout)
+ {
+ item->SetLabel(m_gridModel->GetRulerItem(rulerOffset / m_rulerUnit + 1)->GetLabel2());
+ ProcessItem(m_posX, m_posY, item, lastitem, false, m_rulerLayout, m_rulerLayout, currentTime, dirtyregions, m_channelWidth);
+ }
+
+ GetProgrammeCacheOffsets(cacheBeforeRuler, cacheAfterRuler);
+
+ // Free memory not used on screen
+ if (m_gridModel->RulerItemsSize() > m_blocksPerPage + cacheBeforeRuler + cacheAfterRuler)
+ m_gridModel->FreeRulerMemory(rulerOffset / m_rulerUnit + 1 - cacheBeforeRuler,
+ rulerOffset / m_rulerUnit + 1 + m_blocksPerPage - 1 +
+ cacheAfterRuler);
+ }
+
+ CPoint originRuler = CPoint(m_rulerPosX, m_rulerPosY) + m_renderOffset;
+ float pos;
+ float end;
+
+ if (m_orientation == VERTICAL)
+ {
+ pos = originRuler.x;
+ end = m_posX + m_width;
+ }
+ else
+ {
+ pos = originRuler.y;
+ end = m_posY + m_height;
+ }
+
+ const float drawOffset =
+ (rulerOffset - cacheBeforeRuler) * m_blockSize - GetProgrammeScrollOffsetPos();
+ pos += drawOffset;
+ end += cacheAfterRuler * m_rulerLayout->Size(m_orientation == VERTICAL ? HORIZONTAL : VERTICAL);
+
+ if (rulerOffset % m_rulerUnit != 0)
+ {
+ /* first ruler marker starts before current view */
+ int startBlock = rulerOffset - 1;
+
+ while (startBlock % m_rulerUnit != 0)
+ startBlock--;
+
+ int missingSection = rulerOffset - startBlock;
+
+ pos -= missingSection * m_blockSize;
+ }
+
+ while (pos < end && (rulerOffset / m_rulerUnit + 1) < m_gridModel->RulerItemsSize())
+ {
+ item = m_gridModel->GetRulerItem(rulerOffset / m_rulerUnit + 1);
+
+ if (m_orientation == VERTICAL)
+ {
+ if (bRender)
+ RenderItem(pos, originRuler.y, item.get(), false);
+ else
+ ProcessItem(pos, originRuler.y, item, lastitem, false, m_rulerLayout, m_rulerLayout, currentTime, dirtyregions, m_rulerWidth);
+
+ pos += m_rulerWidth;
+ }
+ else
+ {
+ if (bRender)
+ RenderItem(originRuler.x, pos, item.get(), false);
+ else
+ ProcessItem(originRuler.x, pos, item, lastitem, false, m_rulerLayout, m_rulerLayout, currentTime, dirtyregions, m_rulerHeight);
+
+ pos += m_rulerHeight;
+ }
+
+ rulerOffset += m_rulerUnit;
+ }
+
+ if (bRender)
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+}
+
+void CGUIEPGGridContainer::HandleProgrammeGrid(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ if (!m_focusedProgrammeLayout || !m_programmeLayout || m_gridModel->RulerItemsSize() <= 1 || m_gridModel->IsZeroGridDuration())
+ return;
+
+ const int blockOffset = GetProgrammeScrollOffset();
+ const int chanOffset = GetChannelScrollOffset(m_programmeLayout);
+
+ int cacheBeforeProgramme, cacheAfterProgramme;
+ GetProgrammeCacheOffsets(cacheBeforeProgramme, cacheAfterProgramme);
+
+ if (bRender)
+ {
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_gridPosX, m_gridPosY, m_gridWidth, m_gridHeight);
+ }
+ else
+ {
+ int cacheBeforeChannel, cacheAfterChannel;
+ GetChannelCacheOffsets(cacheBeforeChannel, cacheAfterChannel);
+
+ // Free memory not used on screen
+ int firstChannel = chanOffset - cacheBeforeChannel;
+ if (firstChannel < 0)
+ firstChannel = 0;
+ int lastChannel = chanOffset + m_channelsPerPage - 1 + cacheAfterChannel;
+ if (lastChannel > m_gridModel->GetLastChannel())
+ lastChannel = m_gridModel->GetLastChannel();
+ int firstBlock = blockOffset - cacheBeforeProgramme;
+ if (firstBlock < 0)
+ firstBlock = 0;
+ int lastBlock = blockOffset + m_programmesPerPage - 1 + cacheAfterProgramme;
+ if (lastBlock > m_gridModel->GetLastBlock())
+ lastBlock = m_gridModel->GetLastBlock();
+
+ if (m_gridModel->FreeProgrammeMemory(firstChannel, lastChannel, firstBlock, lastBlock))
+ {
+ // announce changed viewport
+ const CGUIMessage msg(
+ GUI_MSG_REFRESH_LIST, GetParentID(), GetID(), static_cast<int>(PVREvent::Epg));
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg);
+ }
+ }
+
+ CPoint originProgramme = CPoint(m_gridPosX, m_gridPosY) + m_renderOffset;
+ float posA;
+ float endA;
+ float posB;
+ float endB;
+
+ if (m_orientation == VERTICAL)
+ {
+ posA = originProgramme.x;
+ endA = m_posX + m_width;
+ posB = originProgramme.y;
+ endB = m_gridPosY + m_gridHeight;
+ }
+ else
+ {
+ posA = originProgramme.y;
+ endA = m_posY + m_height;
+ posB = originProgramme.x;
+ endB = m_gridPosX + m_gridWidth;
+ }
+
+ endA += cacheAfterProgramme * m_blockSize;
+
+ const float drawOffsetA = blockOffset * m_blockSize - GetProgrammeScrollOffsetPos();
+ posA += drawOffsetA;
+ const float drawOffsetB =
+ (chanOffset - cacheBeforeProgramme) * m_channelLayout->Size(m_orientation) -
+ GetChannelScrollOffsetPos();
+ posB += drawOffsetB;
+
+ int channel = chanOffset - cacheBeforeProgramme;
+
+ float focusedPosX = 0;
+ float focusedPosY = 0;
+ CFileItemPtr focusedItem;
+ CFileItemPtr item;
+
+ const int lastChannel = m_gridModel->GetLastChannel();
+ while (posB < endB && HasData() && channel <= lastChannel)
+ {
+ if (channel >= 0)
+ {
+ int block = blockOffset;
+ float posA2 = posA;
+
+ const int startBlock = blockOffset == 0 ? 0 : blockOffset - 1;
+ if (startBlock == 0 || m_gridModel->IsSameGridItem(channel, block, startBlock))
+ {
+ // First program starts before current view
+ block = m_gridModel->GetGridItemStartBlock(channel, startBlock);
+ const int missingSection = blockOffset - block;
+ posA2 -= missingSection * m_blockSize;
+ }
+
+ const int lastBlock = m_gridModel->GetLastBlock();
+ while (posA2 < endA && HasData() && block <= lastBlock)
+ {
+ item = m_gridModel->GetGridItem(channel, block);
+
+ bool focused = (channel == m_channelOffset + m_channelCursor) &&
+ m_gridModel->IsSameGridItem(m_channelOffset + m_channelCursor,
+ m_blockOffset + m_blockCursor, block);
+
+ if (bRender)
+ {
+ // reset to grid start position if first item is out of grid view
+ if (posA2 < posA)
+ posA2 = posA;
+
+ // render our item
+ if (focused)
+ {
+ focusedPosX = posA2;
+ focusedPosY = posB;
+ focusedItem = item;
+ }
+ else
+ {
+ if (m_orientation == VERTICAL)
+ RenderItem(posA2, posB, item.get(), focused);
+ else
+ RenderItem(posB, posA2, item.get(), focused);
+ }
+ }
+ else
+ {
+ // calculate the size to truncate if item is out of grid view
+ float truncateSize = 0;
+ if (posA2 < posA)
+ {
+ truncateSize = posA - posA2;
+ posA2 = posA; // reset to grid start position
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ // truncate item's width
+ m_gridModel->DecreaseGridItemWidth(channel, block, truncateSize);
+ }
+
+ if (m_orientation == VERTICAL)
+ ProcessItem(posA2, posB, item, m_lastChannel, focused, m_programmeLayout,
+ m_focusedProgrammeLayout, currentTime, dirtyregions,
+ m_gridModel->GetGridItemWidth(channel, block));
+ else
+ ProcessItem(posB, posA2, item, m_lastChannel, focused, m_programmeLayout,
+ m_focusedProgrammeLayout, currentTime, dirtyregions,
+ m_gridModel->GetGridItemWidth(channel, block));
+ }
+
+ // increment our X position
+ posA2 += m_gridModel->GetGridItemWidth(
+ channel, block); // assumes focused & unfocused layouts have equal length
+ block += MathUtils::round_int(
+ static_cast<double>(m_gridModel->GetGridItemOriginWidth(channel, block) / m_blockSize));
+ }
+ }
+
+ // increment our Y position
+ channel++;
+ posB += (m_orientation == VERTICAL) ? m_channelHeight : m_channelWidth;
+ }
+
+ if (bRender)
+ {
+ // and render the focused item last (for overlapping purposes)
+ if (focusedItem)
+ {
+ if (m_orientation == VERTICAL)
+ RenderItem(focusedPosX, focusedPosY, focusedItem.get(), true);
+ else
+ RenderItem(focusedPosY, focusedPosX, focusedItem.get(), true);
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+}
diff --git a/xbmc/pvr/guilib/GUIEPGGridContainer.dox b/xbmc/pvr/guilib/GUIEPGGridContainer.dox
new file mode 100644
index 0000000..1916cde
--- /dev/null
+++ b/xbmc/pvr/guilib/GUIEPGGridContainer.dox
@@ -0,0 +1,245 @@
+/*!
+
+\page EPGGrid_control EPGGrid control
+\brief **Used to display the EPG guide in the PVR section.**
+
+\tableofcontents
+
+The epggrid control is used for creating an epg timeline in Kodi. You can choose
+the position, size, and look of the grid and it's contents.
+
+
+--------------------------------------------------------------------------------
+\section EPGGrid_control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="epggrid" id="10">
+ <description>EPG Grid</description>
+ <posx>80</posx>
+ <posy>81</posy>
+ <width>1120</width>
+ <height>555</height>
+ <pagecontrol>10</pagecontrol>
+ <scrolltime>350</scrolltime>
+ <timeblocks>40</timeblocks>
+ <rulerunit>6</rulerunit>
+ <progresstexture border="5">PVR-EpgProgressIndicator.png</progresstexture>
+ <orienttation>vertical</orientation>
+ <onleft>31</onleft>
+ <onright>31</onright>
+ <onup>10</onup>
+ <ondown>10</ondown>
+ <rulerlayout height="35" width="40">
+ <control type="image" id="1">
+ <width>40</width>
+ <height>29</height>
+ <posx>0</posx>
+ <posy>0</posy>
+ <texture border="5">button-nofocus.png</texture>
+ </control>
+ <control type="label" id="2">
+ <posx>10</posx>
+ <posy>0</posy>
+ <width>34</width>
+ <height>29</height>
+ <font>font12</font>
+ <aligny>center</aligny>
+ <selectedcolor>selected</selectedcolor>
+ <align>left</align>
+ <label>$INFO[ListItem.Label]</label>
+ </control>
+ </rulerlayout>
+ <channellayout height="52" width="280">
+ <animation effect="fade" start="110" time="200">UnFocus</animation>
+ <control type="image" id="1">
+ <posx>0</posx>
+ <posy>0</posy>
+ <width>270</width>
+ <height>52</height>
+ <texture border="5">button-nofocus.png</texture>
+ </control>
+ <control type="label">
+ <posx>5</posx>
+ <posy>5</posy>
+ <width>40</width>
+ <height>35</height>
+ <font>font12</font>
+ <align>left</align>
+ <aligny>center</aligny>
+ <textcolor>grey</textcolor>
+ <selectedcolor>grey</selectedcolor>
+ <info>ListItem.ChannelNumber</info>
+ </control>
+ <control type="image">
+ <posx>45</posx>
+ <posy>4</posy>
+ <width>45</width>
+ <height>44</height>
+ <texture>$INFO[ListItem.Icon]</texture>
+ </control>
+ <control type="label" id="1">
+ <posx>94</posx>
+ <posy>0</posy>
+ <width>160</width>
+ <height>52</height>
+ <font>special12</font>
+ <aligny>center</aligny>
+ <selectedcolor>selected</selectedcolor>
+ <align>left</align>
+ <label>$INFO[ListItem.ChannelName]</label>
+ </control>
+ </channellayout>
+ <focusedchannellayout height="52" width="280">
+ <animation effect="fade" start="110" time="200">OnFocus</animation>
+ <control type="image" id="1">
+ <posx>0</posx>
+ <posy>0</posy>
+ <width>270</width>
+ <height>52</height>
+ <texture border="5">button-focus.png</texture>
+ </control>
+ <control type="label">
+ <posx>5</posx>
+ <posy>5</posy>
+ <width>40</width>
+ <height>35</height>
+ <font>font12</font>
+ <align>left</align>
+ <aligny>center</aligny>
+ <textcolor>grey</textcolor>
+ <selectedcolor>grey</selectedcolor>
+ <info>ListItem.ChannelNumber</info>
+ </control>
+ <control type="image">
+ <posx>45</posx>
+ <posy>4</posy>
+ <width>45</width>
+ <height>44</height>
+ <texture>$INFO[ListItem.Icon]</texture>
+ </control>
+ <control type="label" id="1">
+ <posx>94</posx>
+ <posy>0</posy>
+ <width>160</width>
+ <height>52</height>
+ <font>special12</font>
+ <aligny>center</aligny>
+ <selectedcolor>selected</selectedcolor>
+ <align>left</align>
+ <label>$INFO[ListItem.ChannelName]</label>
+ </control>
+ </focusedchannellayout>
+ <itemlayout height="52" width="40">
+ <control type="image" id="2">
+ <width>40</width>
+ <height>52</height>
+ <posx>0</posx>
+ <posy>0</posy>
+ <aspectratio>stretch</aspectratio>
+ <texture border="3">epg-genres/$INFO[ListItem.Property(GenreType)].png</texture>
+ </control>
+ <control type="label" id="1">
+ <posx>6</posx>
+ <posy>3</posy>
+ <width>30</width>
+ <height>25</height>
+ <font>font12</font>
+ <aligny>center</aligny>
+ <selectedcolor>selected</selectedcolor>
+ <align>left</align>
+ <info>ListItem.Label</info>
+ </control>
+ <control type="image">
+ <posx>5</posx>
+ <posy>28</posy>
+ <width>30</width>
+ <height>20</height>
+ <texture>PVR-IsRecording.png</texture>
+ <visible>ListItem.IsRecording</visible>
+ </control>
+ <control type="image">
+ <posx>5</posx>
+ <posy>28</posy>
+ <width>20</width>
+ <height>20</height>
+ <texture>PVR-HasTimer.png</texture>
+ <visible>ListItem.HasTimer + !ListItem.IsRecording</visible>
+ </control>
+ </itemlayout>
+ <focusedlayout height="52" width="40">
+ <control type="image" id="14">
+ <width>40</width>
+ <height>52</height>
+ <posx>0</posx>
+ <posy>0</posy>
+ <texture border="5">folder-focus.png</texture>
+ </control>
+ <control type="image" id="2">
+ <width>40</width>
+ <height>52</height>
+ <posx>0</posx>
+ <posy>0</posy>
+ <aspectratio>stretch</aspectratio>
+ <texture border="3">epg-genres/$INFO[ListItem.Property(GenreType)].png</texture>
+ </control>
+ <control type="label" id="1">
+ <posx>6</posx>
+ <posy>3</posy>
+ <width>30</width>
+ <height>25</height>
+ <font>font12</font>
+ <aligny>center</aligny>
+ <selectedcolor>selected</selectedcolor>
+ <align>left</align>
+ <info>ListItem.Label</info>
+ </control>
+ <control type="image">
+ <posx>5</posx>
+ <posy>28</posy>
+ <width>30</width>
+ <height>20</height>
+ <texture>PVR-IsRecording.png</texture>
+ <visible>ListItem.IsRecording</visible>
+ </control>
+ <control type="image">
+ <posx>5</posx>
+ <posy>28</posy>
+ <width>20</width>
+ <height>20</height>
+ <texture>PVR-HasTimer.png</texture>
+ <visible>ListItem.HasTimer + !ListItem.IsRecording</visible>
+ </control>
+ </focusedlayout>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section EPGGrid_control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|----------------------:|:--------------------------------------------------------------|
+| timeblocks | The number of timeframes on the ruler.
+| rulerunit | Timeframe of each unit on ruler. 1 unit equals 5 minutes.
+| rulerdatelayout | The layout of a ruler date item (usually used to display the start date of current epg page).
+| rulerlayout | The layout of a ruler item.
+| progresstexture | A texture which indicates the current progress time
+| channellayout | The layout of a channel item.
+| focusedchannellayout | The focused layout of a channel item.
+| itemlayout | The layout of the grid
+| focusedlayout | The focused layout of the grid
+
+
+--------------------------------------------------------------------------------
+\section EPGGrid_control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/pvr/guilib/GUIEPGGridContainer.h b/xbmc/pvr/guilib/GUIEPGGridContainer.h
new file mode 100644
index 0000000..360ade9
--- /dev/null
+++ b/xbmc/pvr/guilib/GUIEPGGridContainer.h
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "guilib/DirtyRegion.h"
+#include "guilib/GUIControl.h"
+#include "guilib/GUIListItemLayout.h"
+#include "guilib/GUITexture.h"
+#include "guilib/IGUIContainer.h"
+#include "threads/CriticalSection.h"
+#include "utils/Geometry.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CDateTime;
+class CFileItem;
+class CFileItemList;
+class CGUIListItem;
+class CGUIListItemLayout;
+
+namespace PVR
+{
+ class CPVRChannel;
+ class CPVRChannelGroupMember;
+ class CPVRChannelNumber;
+
+ class CGUIEPGGridContainerModel;
+
+ class CGUIEPGGridContainer : public IGUIContainer
+ {
+ public:
+ CGUIEPGGridContainer(int parentID, int controlID, float posX, float posY, float width, float height,
+ ORIENTATION orientation, int scrollTime, int preloadItems, int minutesPerPage,
+ int rulerUnit, const CTextureInfo& progressIndicatorTexture);
+ CGUIEPGGridContainer(const CGUIEPGGridContainer& other);
+
+ CGUIEPGGridContainer* Clone() const override { return new CGUIEPGGridContainer(*this); }
+
+ /*!
+ * @brief Check whether the control currently holds data.
+ * @return true if the control has data, false otherwise.
+ */
+ bool HasData() const;
+
+ void AllocResources() override;
+ void FreeResources(bool immediately) override;
+
+ bool OnAction(const CAction& action) override;
+ void OnDown() override;
+ void OnUp() override;
+ void OnLeft() override;
+ void OnRight() override;
+ bool OnMouseOver(const CPoint& point) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void SetFocus(bool focus) override;
+ std::string GetDescription() const override;
+ EVENT_RESULT OnMouseEvent(const CPoint& point, const CMouseEvent& event) override;
+
+ void Process(unsigned int currentTime, CDirtyRegionList& dirtyregions) override;
+ void Render() override;
+
+ CGUIListItemPtr GetListItem(int offset, unsigned int flag = 0) const override;
+ std::string GetLabel(int info) const override;
+
+ std::shared_ptr<CFileItem> GetSelectedGridItem(int offset = 0) const;
+ std::shared_ptr<CPVRChannelGroupMember> GetSelectedChannelGroupMember() const;
+ CDateTime GetSelectedDate() const;
+
+ void LoadLayout(TiXmlElement* layout);
+ void SetPageControl(int id);
+
+ /*! \brief Set the offset of the first item in the container from the container's position
+ Useful for lists/panels where the focused item may be larger than the non-focused items and thus
+ normally cut off from the clipping window defined by the container's position + size.
+ \param offset CPoint holding the offset in skin coordinates.
+ */
+ void SetRenderOffset(const CPoint& offset);
+
+ void JumpToNow();
+ void JumpToDate(const CDateTime& date);
+
+ void GoToBegin();
+ void GoToEnd();
+ void GoToNow();
+ void GoToDate(const CDateTime& date);
+
+ void GoToFirstChannel();
+ void GoToLastChannel();
+
+ void GoToTop();
+ void GoToBottom();
+ void GoToMostLeft();
+ void GoToMostRight();
+
+ void SetTimelineItems(const std::unique_ptr<CFileItemList>& items,
+ const CDateTime& gridStart,
+ const CDateTime& gridEnd);
+
+ std::unique_ptr<CFileItemList> GetCurrentTimeLineItems() const;
+
+ /*!
+ * @brief Set the control's selection to the given channel and set the control's view port to show the channel.
+ * @param channel the channel.
+ * @return true if the selection was set to the given channel, false otherwise.
+ */
+ bool SetChannel(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Set the control's selection to the given channel and set the control's view port to show the channel.
+ * @param channel the channel's path.
+ * @return true if the selection was set to the given channel, false otherwise.
+ */
+ bool SetChannel(const std::string& channel);
+
+ /*!
+ * @brief Set the control's selection to the given channel and set the control's view port to show the channel.
+ * @param channelNumber the channel's number.
+ * @return true if the selection was set to the given channel, false otherwise.
+ */
+ bool SetChannel(const CPVRChannelNumber& channelNumber);
+
+ private:
+ bool OnClick(int actionID);
+ bool SelectItemFromPoint(const CPoint& point, bool justGrid = true);
+
+ void SetChannel(int channel);
+
+ void SetBlock(int block, bool bUpdateBlockTravelAxis = true);
+ void UpdateBlock(bool bUpdateBlockTravelAxis = true);
+
+ void ChannelScroll(int amount);
+ void ProgrammesScroll(int amount);
+ void ValidateOffset();
+ void UpdateLayout();
+
+ void SetItem(const std::pair<std::shared_ptr<CFileItem>, int>& itemInfo);
+ bool SetItem(const std::shared_ptr<CFileItem>& item, int channelIndex, int blockIndex);
+ std::shared_ptr<CFileItem> GetItem() const;
+ std::pair<std::shared_ptr<CFileItem>, int> GetNextItem() const;
+ std::pair<std::shared_ptr<CFileItem>, int> GetPrevItem() const;
+ void UpdateItem();
+
+ void MoveToRow(int row);
+
+ CGUIListItemLayout* GetFocusedLayout() const;
+
+ void ScrollToBlockOffset(int offset);
+ void ScrollToChannelOffset(int offset);
+ void GoToBlock(int blockIndex);
+ void GoToChannel(int channelIndex);
+ void UpdateScrollOffset(unsigned int currentTime);
+ void ProcessItem(float posX, float posY, const std::shared_ptr<CFileItem>& item, std::shared_ptr<CFileItem>& lastitem, bool focused, CGUIListItemLayout* normallayout, CGUIListItemLayout* focusedlayout, unsigned int currentTime, CDirtyRegionList& dirtyregions, float resize = -1.0f);
+ void RenderItem(float posX, float posY, CGUIListItem* item, bool focused);
+ void GetCurrentLayouts();
+
+ void ProcessChannels(unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void ProcessRuler(unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void ProcessRulerDate(unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void ProcessProgrammeGrid(unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void ProcessProgressIndicator(unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void RenderChannels();
+ void RenderRulerDate();
+ void RenderRuler();
+ void RenderProgrammeGrid();
+ void RenderProgressIndicator();
+
+ CPoint m_renderOffset; ///< \brief render offset of the first item in the list \sa SetRenderOffset
+
+ ORIENTATION m_orientation;
+
+ std::vector<CGUIListItemLayout> m_channelLayouts;
+ std::vector<CGUIListItemLayout> m_focusedChannelLayouts;
+ std::vector<CGUIListItemLayout> m_focusedProgrammeLayouts;
+ std::vector<CGUIListItemLayout> m_programmeLayouts;
+ std::vector<CGUIListItemLayout> m_rulerLayouts;
+ std::vector<CGUIListItemLayout> m_rulerDateLayouts;
+
+ CGUIListItemLayout* m_channelLayout;
+ CGUIListItemLayout* m_focusedChannelLayout;
+ CGUIListItemLayout* m_programmeLayout;
+ CGUIListItemLayout* m_focusedProgrammeLayout;
+ CGUIListItemLayout* m_rulerLayout;
+ CGUIListItemLayout* m_rulerDateLayout;
+
+ int m_pageControl;
+
+ void GetChannelCacheOffsets(int& cacheBefore, int& cacheAfter);
+ void GetProgrammeCacheOffsets(int& cacheBefore, int& cacheAfter);
+
+ private:
+ bool OnMouseClick(int dwButton, const CPoint& point);
+ bool OnMouseDoubleClick(int dwButton, const CPoint& point);
+ bool OnMouseWheel(char wheel, const CPoint& point);
+
+ void HandleChannels(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void HandleRuler(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void HandleRulerDate(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void HandleProgrammeGrid(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions);
+
+ float GetCurrentTimePositionOnPage() const;
+ float GetProgressIndicatorWidth() const;
+ float GetProgressIndicatorHeight() const;
+
+ void UpdateItems();
+
+ float GetChannelScrollOffsetPos() const;
+ float GetProgrammeScrollOffsetPos() const;
+ int GetChannelScrollOffset(CGUIListItemLayout* layout) const;
+ int GetProgrammeScrollOffset() const;
+
+ int m_rulerUnit; //! number of blocks that makes up one element of the ruler
+ int m_channelsPerPage;
+ int m_programmesPerPage;
+ int m_channelCursor;
+ int m_channelOffset;
+ int m_blocksPerPage;
+ int m_blockCursor;
+ int m_blockOffset;
+ int m_blockTravelAxis;
+ int m_cacheChannelItems;
+ int m_cacheProgrammeItems;
+ int m_cacheRulerItems;
+
+ float m_rulerDateHeight; //! height of ruler date item
+ float m_rulerDateWidth; //! width of ruler date item
+ float m_rulerPosX; //! X position of first ruler item
+ float m_rulerPosY; //! Y position of first ruler item
+ float m_rulerHeight; //! height of the scrolling timeline above the ruler items
+ float m_rulerWidth; //! width of each element of the ruler
+ float m_channelPosX; //! X position of first channel row
+ float m_channelPosY; //! Y position of first channel row
+ float m_channelHeight; //! height of the channel item
+ float m_channelWidth; //! width of the channel item
+ float m_gridPosX; //! X position of first grid item
+ float m_gridPosY; //! Y position of first grid item
+ float m_gridWidth; //! width of the epg grid control
+ float m_gridHeight; //! height of the epg grid control
+ float m_blockSize; //! a block's width in pixels
+ float m_analogScrollCount;
+
+ std::unique_ptr<CGUITexture> m_guiProgressIndicatorTexture;
+
+ std::shared_ptr<CFileItem> m_lastItem;
+ std::shared_ptr<CFileItem> m_lastChannel;
+
+ bool m_bEnableProgrammeScrolling = true;
+ bool m_bEnableChannelScrolling = true;
+
+ int m_scrollTime;
+
+ int m_programmeScrollLastTime;
+ float m_programmeScrollSpeed;
+ float m_programmeScrollOffset;
+
+ int m_channelScrollLastTime;
+ float m_channelScrollSpeed;
+ float m_channelScrollOffset;
+
+ mutable CCriticalSection m_critSection;
+ std::unique_ptr<CGUIEPGGridContainerModel> m_gridModel;
+ std::unique_ptr<CGUIEPGGridContainerModel> m_updatedGridModel;
+
+ int m_itemStartBlock = 0;
+ };
+}
diff --git a/xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp b/xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp
new file mode 100644
index 0000000..e8fac01
--- /dev/null
+++ b/xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp
@@ -0,0 +1,723 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIEPGGridContainerModel.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cmath>
+#include <iterator>
+#include <memory>
+#include <vector>
+
+using namespace PVR;
+
+static const unsigned int GRID_START_PADDING = 30; // minutes
+
+void CGUIEPGGridContainerModel::SetInvalid()
+{
+ for (const auto& gridItem : m_gridIndex)
+ gridItem.second.item->SetInvalid();
+ for (const auto& channel : m_channelItems)
+ channel->SetInvalid();
+ for (const auto& ruler : m_rulerItems)
+ ruler->SetInvalid();
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::CreateGapItem(int iChannel) const
+{
+ const std::shared_ptr<CPVRChannel> channel = m_channelItems[iChannel]->GetPVRChannelInfoTag();
+ const std::shared_ptr<CPVREpgInfoTag> gapTag = channel->CreateEPGGapTag(m_gridStart, m_gridEnd);
+ return std::make_shared<CFileItem>(gapTag);
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CGUIEPGGridContainerModel::GetEPGTimeline(
+ int iChannel, const CDateTime& minEventEnd, const CDateTime& maxEventStart) const
+{
+ CDateTime min = minEventEnd - CDateTimeSpan(0, 0, MINSPERBLOCK, 0) + CDateTimeSpan(0, 0, 0, 1);
+ CDateTime max = maxEventStart + CDateTimeSpan(0, 0, MINSPERBLOCK, 0);
+
+ if (min < m_gridStart)
+ min = m_gridStart;
+
+ if (max > m_gridEnd)
+ max = m_gridEnd;
+
+ return m_channelItems[iChannel]->GetPVRChannelInfoTag()->GetEPGTimeline(m_gridStart, m_gridEnd,
+ min, max);
+}
+
+void CGUIEPGGridContainerModel::Initialize(const std::unique_ptr<CFileItemList>& items,
+ const CDateTime& gridStart,
+ const CDateTime& gridEnd,
+ int iFirstChannel,
+ int iChannelsPerPage,
+ int iFirstBlock,
+ int iBlocksPerPage,
+ int iRulerUnit,
+ float fBlockSize)
+{
+ if (!m_channelItems.empty())
+ {
+ CLog::LogF(LOGERROR, "Already initialized!");
+ return;
+ }
+
+ m_fBlockSize = fBlockSize;
+
+ ////////////////////////////////////////////////////////////////////////
+ // Create channel items
+ std::copy(items->cbegin(), items->cend(), std::back_inserter(m_channelItems));
+
+ /* check for invalid start and end time */
+ if (gridStart >= gridEnd)
+ {
+ // default to start "now minus GRID_START_PADDING minutes" and end "start plus one page".
+ m_gridStart = CDateTime::GetUTCDateTime() - CDateTimeSpan(0, 0, GetGridStartPadding(), 0);
+ m_gridEnd = m_gridStart + CDateTimeSpan(0, 0, iBlocksPerPage * MINSPERBLOCK, 0);
+ }
+ else if (gridStart >
+ (CDateTime::GetUTCDateTime() - CDateTimeSpan(0, 0, GetGridStartPadding(), 0)))
+ {
+ // adjust to start "now minus GRID_START_PADDING minutes".
+ m_gridStart = CDateTime::GetUTCDateTime() - CDateTimeSpan(0, 0, GetGridStartPadding(), 0);
+ m_gridEnd = gridEnd;
+ }
+ else
+ {
+ m_gridStart = gridStart;
+ m_gridEnd = gridEnd;
+ }
+
+ // roundup
+ m_gridStart = CDateTime(m_gridStart.GetYear(), m_gridStart.GetMonth(), m_gridStart.GetDay(),
+ m_gridStart.GetHour(), m_gridStart.GetMinute() >= 30 ? 30 : 0, 0);
+ m_gridEnd = CDateTime(m_gridEnd.GetYear(), m_gridEnd.GetMonth(), m_gridEnd.GetDay(),
+ m_gridEnd.GetHour(), m_gridEnd.GetMinute() >= 30 ? 30 : 0, 0);
+
+ m_blocks = GetBlock(m_gridEnd) + 1;
+
+ const int iBlocksLastPage = m_blocks % iBlocksPerPage;
+ if (iBlocksLastPage > 0)
+ {
+ m_gridEnd += CDateTimeSpan(0, 0, (iBlocksPerPage - iBlocksLastPage) * MINSPERBLOCK, 0);
+ m_blocks += (iBlocksPerPage - iBlocksLastPage);
+ }
+
+ ////////////////////////////////////////////////////////////////////////
+ // Create ruler items
+ CDateTime ruler;
+ ruler.SetFromUTCDateTime(m_gridStart);
+ CDateTime rulerEnd;
+ rulerEnd.SetFromUTCDateTime(m_gridEnd);
+ CFileItemPtr rulerItem(new CFileItem(ruler.GetAsLocalizedDate(true)));
+ rulerItem->SetProperty("DateLabel", true);
+ m_rulerItems.emplace_back(rulerItem);
+
+ const CDateTimeSpan unit(0, 0, iRulerUnit * MINSPERBLOCK, 0);
+ for (; ruler < rulerEnd; ruler += unit)
+ {
+ rulerItem.reset(new CFileItem(ruler.GetAsLocalizedTime("", false)));
+ rulerItem->SetLabel2(ruler.GetAsLocalizedDate(true));
+ m_rulerItems.emplace_back(rulerItem);
+ }
+
+ m_firstActiveChannel = iFirstChannel;
+ m_lastActiveChannel = iFirstChannel + iChannelsPerPage - 1;
+ m_firstActiveBlock = iFirstBlock;
+ m_lastActiveBlock = iFirstBlock + iBlocksPerPage - 1;
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::CreateEpgTags(int iChannel, int iBlock) const
+{
+ std::shared_ptr<CFileItem> result;
+
+ const int firstBlock = iBlock < m_firstActiveBlock ? iBlock : m_firstActiveBlock;
+ const int lastBlock = iBlock > m_lastActiveBlock ? iBlock : m_lastActiveBlock;
+
+ const auto tags =
+ GetEPGTimeline(iChannel, GetStartTimeForBlock(firstBlock), GetStartTimeForBlock(lastBlock));
+
+ const int firstResultBlock = GetFirstEventBlock(tags.front());
+ const int lastResultBlock = GetLastEventBlock(tags.back());
+ if (firstResultBlock > lastResultBlock)
+ return result;
+
+ auto it = m_epgItems.insert({iChannel, EpgTags()}).first;
+ EpgTags& epgTags = (*it).second;
+
+ epgTags.firstBlock = firstResultBlock;
+ epgTags.lastBlock = lastResultBlock;
+
+ for (const auto& tag : tags)
+ {
+ if (GetFirstEventBlock(tag) > GetLastEventBlock(tag))
+ continue;
+
+ const std::shared_ptr<CFileItem> item = std::make_shared<CFileItem>(tag);
+ if (!result && IsEventMemberOfBlock(tag, iBlock))
+ result = item;
+
+ epgTags.tags.emplace_back(item);
+ }
+
+ return result;
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetEpgTags(EpgTagsMap::iterator& itEpg,
+ int iChannel,
+ int iBlock) const
+{
+ std::shared_ptr<CFileItem> result;
+
+ EpgTags& epgTags = (*itEpg).second;
+
+ if (iBlock < epgTags.firstBlock)
+ {
+ result = GetEpgTagsBefore(epgTags, iChannel, iBlock);
+ }
+ else if (iBlock > epgTags.lastBlock)
+ {
+ result = GetEpgTagsAfter(epgTags, iChannel, iBlock);
+ }
+ else
+ {
+ const auto it =
+ std::find_if(epgTags.tags.cbegin(), epgTags.tags.cend(), [this, iBlock](const auto& item) {
+ return IsEventMemberOfBlock(item->GetEPGInfoTag(), iBlock);
+ });
+ if (it != epgTags.tags.cend())
+ result = (*it);
+ }
+
+ return result;
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetEpgTagsBefore(EpgTags& epgTags,
+ int iChannel,
+ int iBlock) const
+{
+ std::shared_ptr<CFileItem> result;
+
+ int lastBlock = epgTags.firstBlock - 1;
+ if (lastBlock < 0)
+ lastBlock = 0;
+
+ const auto tags =
+ GetEPGTimeline(iChannel, GetStartTimeForBlock(iBlock), GetStartTimeForBlock(lastBlock));
+
+ if (epgTags.lastBlock == -1)
+ epgTags.lastBlock = lastBlock;
+
+ if (tags.empty())
+ {
+ epgTags.firstBlock = iBlock;
+ }
+ else
+ {
+ const int firstResultBlock = GetFirstEventBlock(tags.front());
+ const int lastResultBlock = GetLastEventBlock(tags.back());
+ if (firstResultBlock > lastResultBlock)
+ return result;
+
+ // insert before the existing tags
+ epgTags.firstBlock = firstResultBlock;
+
+ auto it = tags.crbegin();
+ if (!epgTags.tags.empty())
+ {
+ // ptr comp does not work for gap tags!
+ // if ((*it) == epgTags.tags.front()->GetEPGInfoTag())
+
+ const std::shared_ptr<CPVREpgInfoTag> t = epgTags.tags.front()->GetEPGInfoTag();
+ if ((*it)->StartAsUTC() == t->StartAsUTC() && (*it)->EndAsUTC() == t->EndAsUTC())
+ {
+ if (!result && IsEventMemberOfBlock(*it, iBlock))
+ result = epgTags.tags.front();
+
+ ++it; // skip, because we already have that epg tag
+ }
+ }
+
+ for (; it != tags.crend(); ++it)
+ {
+ if (GetFirstEventBlock(*it) > GetLastEventBlock(*it))
+ continue;
+
+ const std::shared_ptr<CFileItem> item = std::make_shared<CFileItem>(*it);
+ if (!result && IsEventMemberOfBlock(*it, iBlock))
+ result = item;
+
+ epgTags.tags.insert(epgTags.tags.begin(), item);
+ }
+ }
+
+ return result;
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetEpgTagsAfter(EpgTags& epgTags,
+ int iChannel,
+ int iBlock) const
+{
+ std::shared_ptr<CFileItem> result;
+
+ int firstBlock = epgTags.lastBlock + 1;
+ if (firstBlock >= GetLastBlock())
+ firstBlock = GetLastBlock();
+
+ const auto tags =
+ GetEPGTimeline(iChannel, GetStartTimeForBlock(firstBlock), GetStartTimeForBlock(iBlock));
+
+ if (epgTags.firstBlock == -1)
+ epgTags.firstBlock = firstBlock;
+
+ if (tags.empty())
+ {
+ epgTags.lastBlock = iBlock;
+ }
+ else
+ {
+ const int firstResultBlock = GetFirstEventBlock(tags.front());
+ const int lastResultBlock = GetLastEventBlock(tags.back());
+ if (firstResultBlock > lastResultBlock)
+ return result;
+
+ // append to the existing tags
+ epgTags.lastBlock = lastResultBlock;
+
+ auto it = tags.cbegin();
+ if (!epgTags.tags.empty())
+ {
+ // ptr comp does not work for gap tags!
+ // if ((*it) == epgTags.tags.back()->GetEPGInfoTag())
+
+ const std::shared_ptr<CPVREpgInfoTag> t = epgTags.tags.back()->GetEPGInfoTag();
+ if ((*it)->StartAsUTC() == t->StartAsUTC() && (*it)->EndAsUTC() == t->EndAsUTC())
+ {
+ if (!result && IsEventMemberOfBlock(*it, iBlock))
+ result = epgTags.tags.back();
+
+ ++it; // skip, because we already have that epg tag
+ }
+ }
+
+ for (; it != tags.cend(); ++it)
+ {
+ if (GetFirstEventBlock(*it) > GetLastEventBlock(*it))
+ continue;
+
+ const std::shared_ptr<CFileItem> item = std::make_shared<CFileItem>(*it);
+ if (!result && IsEventMemberOfBlock(*it, iBlock))
+ result = item;
+
+ epgTags.tags.emplace_back(item);
+ }
+ }
+
+ return result;
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetItem(int iChannel, int iBlock) const
+{
+ std::shared_ptr<CFileItem> result;
+
+ auto itEpg = m_epgItems.find(iChannel);
+ if (itEpg == m_epgItems.end())
+ {
+ result = CreateEpgTags(iChannel, iBlock);
+ }
+ else
+ {
+ result = GetEpgTags(itEpg, iChannel, iBlock);
+ }
+
+ if (!result)
+ {
+ // Must never happen. if it does, fix the root cause, don't tolerate nullptr!
+ CLog::LogF(LOGERROR, "EPG tag ({}, {}) not found!", iChannel, iBlock);
+ }
+
+ return result;
+}
+
+void CGUIEPGGridContainerModel::FindChannelAndBlockIndex(int channelUid,
+ unsigned int broadcastUid,
+ int eventOffset,
+ int& newChannelIndex,
+ int& newBlockIndex) const
+{
+ newChannelIndex = INVALID_INDEX;
+ newBlockIndex = INVALID_INDEX;
+
+ // find the new channel index
+ int iCurrentChannel = 0;
+ for (const auto& channel : m_channelItems)
+ {
+ if (channel->GetPVRChannelInfoTag()->UniqueID() == channelUid)
+ {
+ newChannelIndex = iCurrentChannel;
+
+ // find the new block index
+ const std::shared_ptr<CPVREpg> epg = channel->GetPVRChannelInfoTag()->GetEPG();
+ if (epg)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> tag = epg->GetTagByBroadcastId(broadcastUid);
+ if (tag)
+ newBlockIndex = GetFirstEventBlock(tag) + eventOffset;
+ }
+ break; // done
+ }
+ iCurrentChannel++;
+ }
+}
+
+GridItem* CGUIEPGGridContainerModel::GetGridItemPtr(int iChannel, int iBlock) const
+{
+ auto it = m_gridIndex.find({iChannel, iBlock});
+ if (it == m_gridIndex.end())
+ {
+ const CDateTime startTime = GetStartTimeForBlock(iBlock);
+ if (startTime < m_gridStart || m_gridEnd < startTime)
+ {
+ CLog::LogF(LOGERROR, "Requested EPG tag ({}, {}) outside grid boundaries!", iChannel, iBlock);
+ return nullptr;
+ }
+
+ const std::shared_ptr<CFileItem> item = GetItem(iChannel, iBlock);
+ if (!item)
+ {
+ CLog::LogF(LOGERROR, "Got no EPG tag ({}, {})!", iChannel, iBlock);
+ return nullptr;
+ }
+
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = item->GetEPGInfoTag();
+
+ const int startBlock = GetFirstEventBlock(epgTag);
+ const int endBlock = GetLastEventBlock(epgTag);
+
+ //! @todo it seems that this should be done somewhere else. CFileItem ctor maybe.
+ item->SetProperty("GenreType", epgTag->GenreType());
+
+ const float fItemWidth = (endBlock - startBlock + 1) * m_fBlockSize;
+ it = m_gridIndex.insert({{iChannel, iBlock}, {item, fItemWidth, startBlock, endBlock}}).first;
+ }
+
+ return &(*it).second;
+}
+
+bool CGUIEPGGridContainerModel::IsSameGridItem(int iChannel, int iBlock1, int iBlock2) const
+{
+ if (iBlock1 == iBlock2)
+ return true;
+
+ const GridItem* item1 = GetGridItemPtr(iChannel, iBlock1);
+ const GridItem* item2 = GetGridItemPtr(iChannel, iBlock2);
+
+ // compare the instances, not instance pointers, pointers are not unique.
+ return *item1 == *item2;
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetGridItem(int iChannel, int iBlock) const
+{
+ return GetGridItemPtr(iChannel, iBlock)->item;
+}
+
+int CGUIEPGGridContainerModel::GetGridItemStartBlock(int iChannel, int iBlock) const
+{
+ return GetGridItemPtr(iChannel, iBlock)->startBlock;
+}
+
+int CGUIEPGGridContainerModel::GetGridItemEndBlock(int iChannel, int iBlock) const
+{
+ return GetGridItemPtr(iChannel, iBlock)->endBlock;
+}
+
+CDateTime CGUIEPGGridContainerModel::GetGridItemEndTime(int iChannel, int iBlock) const
+{
+ return GetGridItemPtr(iChannel, iBlock)->item->GetEPGInfoTag()->EndAsUTC();
+}
+
+float CGUIEPGGridContainerModel::GetGridItemWidth(int iChannel, int iBlock) const
+{
+ return GetGridItemPtr(iChannel, iBlock)->width;
+}
+
+float CGUIEPGGridContainerModel::GetGridItemOriginWidth(int iChannel, int iBlock) const
+{
+ return GetGridItemPtr(iChannel, iBlock)->originWidth;
+}
+
+void CGUIEPGGridContainerModel::DecreaseGridItemWidth(int iChannel, int iBlock, float fSize)
+{
+ auto it = m_gridIndex.find({iChannel, iBlock});
+ if (it != m_gridIndex.end() && (*it).second.width != ((*it).second.originWidth - fSize))
+ (*it).second.width = (*it).second.originWidth - fSize;
+}
+
+unsigned int CGUIEPGGridContainerModel::GetGridStartPadding() const
+{
+ unsigned int iPastMinutes =
+ CServiceBroker::GetPVRManager().EpgContainer().GetPastDaysToDisplay() * 24 * 60;
+
+ if (iPastMinutes < GRID_START_PADDING)
+ return iPastMinutes;
+
+ return GRID_START_PADDING; // minutes
+}
+
+void CGUIEPGGridContainerModel::FreeChannelMemory(int keepStart, int keepEnd)
+{
+ if (keepStart < keepEnd)
+ {
+ // remove before keepStart and after keepEnd
+ for (int i = 0; i < keepStart && i < ChannelItemsSize(); ++i)
+ m_channelItems[i]->FreeMemory();
+ for (int i = keepEnd + 1; i < ChannelItemsSize(); ++i)
+ m_channelItems[i]->FreeMemory();
+ }
+ else
+ {
+ // wrapping
+ for (int i = keepEnd + 1; i < keepStart && i < ChannelItemsSize(); ++i)
+ m_channelItems[i]->FreeMemory();
+ }
+}
+
+bool CGUIEPGGridContainerModel::FreeProgrammeMemory(int firstChannel,
+ int lastChannel,
+ int firstBlock,
+ int lastBlock)
+{
+ const bool channelsChanged =
+ (firstChannel != m_firstActiveChannel || lastChannel != m_lastActiveChannel);
+ const bool blocksChanged = (firstBlock != m_firstActiveBlock || lastBlock != m_lastActiveBlock);
+ if (!channelsChanged && !blocksChanged)
+ return false;
+
+ // clear the grid. it will be recreated on-demand.
+ m_gridIndex.clear();
+
+ bool newChannels = false;
+
+ if (channelsChanged)
+ {
+ // purge epg tags for inactive channels
+ for (auto it = m_epgItems.begin(); it != m_epgItems.end();)
+ {
+ if ((*it).first < firstChannel || (*it).first > lastChannel)
+ {
+ it = m_epgItems.erase(it);
+ continue; // next channel
+ }
+ ++it;
+ }
+
+ newChannels = (firstChannel < m_firstActiveChannel) || (lastChannel > m_lastActiveChannel);
+ }
+
+ if (blocksChanged || newChannels)
+ {
+ // clear and refetch epg tags for active channels
+ const CDateTime maxEnd = GetStartTimeForBlock(firstBlock);
+ const CDateTime minStart = GetStartTimeForBlock(lastBlock);
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ for (int i = firstChannel; i <= lastChannel; ++i)
+ {
+ auto it = m_epgItems.find(i);
+ if (it == m_epgItems.end())
+ it = m_epgItems.insert({i, EpgTags()}).first;
+
+ if (blocksChanged || i < m_firstActiveChannel || i > m_lastActiveChannel)
+ {
+ EpgTags& epgTags = (*it).second;
+
+ (*it).second.tags.clear();
+
+ tags = GetEPGTimeline(i, maxEnd, minStart);
+ const int firstResultBlock = GetFirstEventBlock(tags.front());
+ const int lastResultBlock = GetLastEventBlock(tags.back());
+ if (firstResultBlock > lastResultBlock)
+ continue;
+
+ epgTags.firstBlock = firstResultBlock;
+ epgTags.lastBlock = lastResultBlock;
+
+ for (const auto& tag : tags)
+ {
+ if (GetFirstEventBlock(tag) > GetLastEventBlock(tag))
+ continue;
+
+ epgTags.tags.emplace_back(std::make_shared<CFileItem>(tag));
+ }
+ }
+ }
+ }
+
+ m_firstActiveChannel = firstChannel;
+ m_lastActiveChannel = lastChannel;
+ m_firstActiveBlock = firstBlock;
+ m_lastActiveBlock = lastBlock;
+
+ return true;
+}
+
+void CGUIEPGGridContainerModel::FreeRulerMemory(int keepStart, int keepEnd)
+{
+ if (keepStart < keepEnd)
+ {
+ // remove before keepStart and after keepEnd
+ for (int i = 1; i < keepStart && i < RulerItemsSize(); ++i)
+ m_rulerItems[i]->FreeMemory();
+ for (int i = keepEnd + 1; i < RulerItemsSize(); ++i)
+ m_rulerItems[i]->FreeMemory();
+ }
+ else
+ {
+ // wrapping
+ for (int i = keepEnd + 1; i < keepStart && i < RulerItemsSize(); ++i)
+ {
+ if (i == 0)
+ continue;
+
+ m_rulerItems[i]->FreeMemory();
+ }
+ }
+}
+
+unsigned int CGUIEPGGridContainerModel::GetPageNowOffset() const
+{
+ return GetGridStartPadding() / MINSPERBLOCK; // this is the 'now' block relative to page start
+}
+
+CDateTime CGUIEPGGridContainerModel::GetStartTimeForBlock(int block) const
+{
+ if (block < 0)
+ block = 0;
+ else if (block >= GridItemsSize())
+ block = GetLastBlock();
+
+ return m_gridStart + CDateTimeSpan(0, 0, block * MINSPERBLOCK, 0);
+}
+
+int CGUIEPGGridContainerModel::GetBlock(const CDateTime& datetime) const
+{
+ int diff;
+
+ if (m_gridStart == datetime)
+ return 0; // block is at grid start
+ else if (m_gridStart > datetime)
+ diff = -1 * (m_gridStart - datetime).GetSecondsTotal(); // block is before grid start
+ else
+ diff = (datetime - m_gridStart).GetSecondsTotal(); // block is after grid start
+
+ // Note: Subtract 1 second from diff to ensure that events ending exactly at block boundary
+ // are unambiguous. Example: An event ending at 5:00:00 shall be mapped to block 9 and
+ // an event starting at 5:00:00 shall be mapped to block 10, not both at block 10.
+ // Only exception is grid end, because there is no successor.
+ if (datetime >= m_gridEnd)
+ return diff / 60 / MINSPERBLOCK; // block is equal or after grid end
+ else
+ return (diff - 1) / 60 / MINSPERBLOCK;
+}
+
+int CGUIEPGGridContainerModel::GetNowBlock() const
+{
+ return GetBlock(CDateTime::GetUTCDateTime()) - GetPageNowOffset();
+}
+
+int CGUIEPGGridContainerModel::GetFirstEventBlock(
+ const std::shared_ptr<CPVREpgInfoTag>& event) const
+{
+ const CDateTime eventStart = event->StartAsUTC();
+ int diff;
+
+ if (m_gridStart == eventStart)
+ return 0; // block is at grid start
+ else if (m_gridStart > eventStart)
+ diff = -1 * (m_gridStart - eventStart).GetSecondsTotal();
+ else
+ diff = (eventStart - m_gridStart).GetSecondsTotal();
+
+ // First block of a tag is always the block calculated using event's start time, rounded up.
+ float fBlockIndex = diff / 60.0f / MINSPERBLOCK;
+ return static_cast<int>(std::ceil(fBlockIndex));
+}
+
+int CGUIEPGGridContainerModel::GetLastEventBlock(const std::shared_ptr<CPVREpgInfoTag>& event) const
+{
+ // Last block of a tag is always the block calculated using event's end time, not rounded up.
+ return GetBlock(event->EndAsUTC());
+}
+
+bool CGUIEPGGridContainerModel::IsEventMemberOfBlock(const std::shared_ptr<CPVREpgInfoTag>& event,
+ int iBlock) const
+{
+ const int iFirstBlock = GetFirstEventBlock(event);
+ const int iLastBlock = GetLastEventBlock(event);
+
+ if (iFirstBlock > iLastBlock)
+ {
+ return false;
+ }
+ else if (iFirstBlock == iBlock)
+ {
+ return true;
+ }
+ else if (iFirstBlock < iBlock)
+ {
+ return (iBlock <= iLastBlock);
+ }
+ return false;
+}
+
+std::unique_ptr<CFileItemList> CGUIEPGGridContainerModel::GetCurrentTimeLineItems(
+ int firstChannel, int numChannels) const
+{
+ // Note: No need to keep this in a member. Gets generally not called multiple times for the
+ // same timeline, but content must be synced with m_epgItems, which changes quite often.
+
+ std::unique_ptr<CFileItemList> items(new CFileItemList);
+
+ if (numChannels > ChannelItemsSize())
+ numChannels = ChannelItemsSize();
+
+ int i = 0;
+ for (int channel = firstChannel; channel < (firstChannel + numChannels); ++channel)
+ {
+ // m_epgItems is not sorted, fileitemlist must be sorted, so we have to 'find' the channel
+ const auto itEpg = m_epgItems.find(channel);
+ if (itEpg != m_epgItems.end())
+ {
+ // tags are sorted, so we can iterate and append
+ for (const auto& tag : (*itEpg).second.tags)
+ {
+ tag->SetProperty("TimelineIndex", i);
+ items->Add(tag);
+ ++i;
+ }
+ }
+ else
+ {
+ // fake empty EPG
+ const std::shared_ptr<CFileItem> tag = CreateGapItem(channel);
+ tag->SetProperty("TimelineIndex", i);
+ items->Add(tag);
+ ++i;
+ }
+ }
+ return items;
+}
diff --git a/xbmc/pvr/guilib/GUIEPGGridContainerModel.h b/xbmc/pvr/guilib/GUIEPGGridContainerModel.h
new file mode 100644
index 0000000..2e0a6d6
--- /dev/null
+++ b/xbmc/pvr/guilib/GUIEPGGridContainerModel.h
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+class CFileItem;
+class CFileItemList;
+
+namespace PVR
+{
+struct GridItem
+{
+ GridItem(const std::shared_ptr<CFileItem>& _item, float _width, int _startBlock, int _endBlock)
+ : item(_item), originWidth(_width), width(_width), startBlock(_startBlock), endBlock(_endBlock)
+ {
+ }
+
+ bool operator==(const GridItem& other) const
+ {
+ return (startBlock == other.startBlock && endBlock == other.endBlock);
+ }
+
+ std::shared_ptr<CFileItem> item;
+ float originWidth = 0.0f;
+ float width = 0.0f;
+ int startBlock = 0;
+ int endBlock = 0;
+};
+
+class CPVREpgInfoTag;
+
+class CGUIEPGGridContainerModel
+{
+public:
+ static constexpr int MINSPERBLOCK = 5; // minutes
+
+ CGUIEPGGridContainerModel() = default;
+ virtual ~CGUIEPGGridContainerModel() = default;
+
+ void Initialize(const std::unique_ptr<CFileItemList>& items,
+ const CDateTime& gridStart,
+ const CDateTime& gridEnd,
+ int iFirstChannel,
+ int iChannelsPerPage,
+ int iFirstBlock,
+ int iBlocksPerPage,
+ int iRulerUnit,
+ float fBlockSize);
+ void SetInvalid();
+
+ static const int INVALID_INDEX = -1;
+ void FindChannelAndBlockIndex(int channelUid,
+ unsigned int broadcastUid,
+ int eventOffset,
+ int& newChannelIndex,
+ int& newBlockIndex) const;
+
+ void FreeChannelMemory(int keepStart, int keepEnd);
+ bool FreeProgrammeMemory(int firstChannel, int lastChannel, int firstBlock, int lastBlock);
+ void FreeRulerMemory(int keepStart, int keepEnd);
+
+ std::shared_ptr<CFileItem> GetChannelItem(int iIndex) const { return m_channelItems[iIndex]; }
+ bool HasChannelItems() const { return !m_channelItems.empty(); }
+ int ChannelItemsSize() const { return static_cast<int>(m_channelItems.size()); }
+ int GetLastChannel() const
+ {
+ return m_channelItems.empty() ? -1 : static_cast<int>(m_channelItems.size()) - 1;
+ }
+
+ std::shared_ptr<CFileItem> GetRulerItem(int iIndex) const { return m_rulerItems[iIndex]; }
+ int RulerItemsSize() const { return static_cast<int>(m_rulerItems.size()); }
+
+ int GridItemsSize() const { return m_blocks; }
+ bool IsSameGridItem(int iChannel, int iBlock1, int iBlock2) const;
+ std::shared_ptr<CFileItem> GetGridItem(int iChannel, int iBlock) const;
+ int GetGridItemStartBlock(int iChannel, int iBlock) const;
+ int GetGridItemEndBlock(int iChannel, int iBlock) const;
+ CDateTime GetGridItemEndTime(int iChannel, int iBlock) const;
+ float GetGridItemWidth(int iChannel, int iBlock) const;
+ float GetGridItemOriginWidth(int iChannel, int iBlock) const;
+ void DecreaseGridItemWidth(int iChannel, int iBlock, float fSize);
+
+ bool IsZeroGridDuration() const { return (m_gridEnd - m_gridStart) == CDateTimeSpan(0, 0, 0, 0); }
+ const CDateTime& GetGridStart() const { return m_gridStart; }
+ const CDateTime& GetGridEnd() const { return m_gridEnd; }
+ unsigned int GetGridStartPadding() const;
+
+ unsigned int GetPageNowOffset() const;
+ int GetNowBlock() const;
+ int GetLastBlock() const { return m_blocks - 1; }
+
+ CDateTime GetStartTimeForBlock(int block) const;
+ int GetBlock(const CDateTime& datetime) const;
+ int GetFirstEventBlock(const std::shared_ptr<CPVREpgInfoTag>& event) const;
+ int GetLastEventBlock(const std::shared_ptr<CPVREpgInfoTag>& event) const;
+ bool IsEventMemberOfBlock(const std::shared_ptr<CPVREpgInfoTag>& event, int iBlock) const;
+
+ std::unique_ptr<CFileItemList> GetCurrentTimeLineItems(int firstChannel, int numChannels) const;
+
+private:
+ GridItem* GetGridItemPtr(int iChannel, int iBlock) const;
+ std::shared_ptr<CFileItem> CreateGapItem(int iChannel) const;
+ std::shared_ptr<CFileItem> GetItem(int iChannel, int iBlock) const;
+
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEPGTimeline(int iChannel,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const;
+
+ struct EpgTags
+ {
+ std::vector<std::shared_ptr<CFileItem>> tags;
+ int firstBlock = -1;
+ int lastBlock = -1;
+ };
+
+ using EpgTagsMap = std::unordered_map<int, EpgTags>;
+
+ std::shared_ptr<CFileItem> CreateEpgTags(int iChannel, int iBlock) const;
+ std::shared_ptr<CFileItem> GetEpgTags(EpgTagsMap::iterator& itEpg,
+ int iChannel,
+ int iBlock) const;
+ std::shared_ptr<CFileItem> GetEpgTagsBefore(EpgTags& epgTags, int iChannel, int iBlock) const;
+ std::shared_ptr<CFileItem> GetEpgTagsAfter(EpgTags& epgTags, int iChannel, int iBlock) const;
+
+ mutable EpgTagsMap m_epgItems;
+
+ CDateTime m_gridStart;
+ CDateTime m_gridEnd;
+
+ std::vector<std::shared_ptr<CFileItem>> m_channelItems;
+ std::vector<std::shared_ptr<CFileItem>> m_rulerItems;
+
+ struct GridCoordinates
+ {
+ GridCoordinates(int _channel, int _block) : channel(_channel), block(_block) {}
+
+ bool operator==(const GridCoordinates& other) const
+ {
+ return (channel == other.channel && block == other.block);
+ }
+
+ int channel = 0;
+ int block = 0;
+ };
+
+ struct GridCoordinatesHash
+ {
+ std::size_t operator()(const GridCoordinates& coordinates) const
+ {
+ return std::hash<int>()(coordinates.channel) ^ std::hash<int>()(coordinates.block);
+ }
+ };
+
+ mutable std::unordered_map<GridCoordinates, GridItem, GridCoordinatesHash> m_gridIndex;
+
+ int m_blocks = 0;
+ float m_fBlockSize = 0.0f;
+
+ int m_firstActiveChannel = 0;
+ int m_lastActiveChannel = 0;
+ int m_firstActiveBlock = 0;
+ int m_lastActiveBlock = 0;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionListener.cpp b/xbmc/pvr/guilib/PVRGUIActionListener.cpp
new file mode 100644
index 0000000..3e68386
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionListener.cpp
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIActionListener.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationActionListeners.h"
+#include "application/ApplicationComponents.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsClients.h"
+#include "pvr/guilib/PVRGUIActionsDatabase.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+
+#include <memory>
+#include <string>
+
+namespace PVR
+{
+
+CPVRGUIActionListener::CPVRGUIActionListener()
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appListener = components.GetComponent<CApplicationActionListeners>();
+ appListener->RegisterActionListener(this);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->RegisterCallback(
+ this,
+ {CSettings::SETTING_PVRPARENTAL_ENABLED, CSettings::SETTING_PVRMANAGER_RESETDB,
+ CSettings::SETTING_EPG_RESETEPG, CSettings::SETTING_PVRMANAGER_ADDONS,
+ CSettings::SETTING_PVRMANAGER_CLIENTPRIORITIES, CSettings::SETTING_PVRMANAGER_CHANNELMANAGER,
+ CSettings::SETTING_PVRMANAGER_GROUPMANAGER, CSettings::SETTING_PVRMANAGER_CHANNELSCAN,
+ CSettings::SETTING_PVRMENU_SEARCHICONS, CSettings::SETTING_PVRCLIENT_MENUHOOK,
+ CSettings::SETTING_EPG_PAST_DAYSTODISPLAY, CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY});
+}
+
+CPVRGUIActionListener::~CPVRGUIActionListener()
+{
+ CServiceBroker::GetSettingsComponent()->GetSettings()->UnregisterCallback(this);
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appListener = components.GetComponent<CApplicationActionListeners>();
+ appListener->UnregisterActionListener(this);
+}
+
+void CPVRGUIActionListener::Init(CPVRManager& mgr)
+{
+ mgr.Events().Subscribe(this, &CPVRGUIActionListener::OnPVRManagerEvent);
+}
+
+void CPVRGUIActionListener::Deinit(CPVRManager& mgr)
+{
+ mgr.Events().Unsubscribe(this);
+}
+
+void CPVRGUIActionListener::OnPVRManagerEvent(const PVREvent& event)
+{
+ if (event == PVREvent::AnnounceReminder)
+ {
+ if (g_application.IsInitialized())
+ {
+ // if GUI is ready, dispatch to GUI thread and handle the action there
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(ACTION_PVR_ANNOUNCE_REMINDERS)));
+ }
+ }
+}
+
+ChannelSwitchMode CPVRGUIActionListener::GetChannelSwitchMode(int iAction)
+{
+ if ((iAction == ACTION_MOVE_UP || iAction == ACTION_MOVE_DOWN) &&
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRPLAYBACK_CONFIRMCHANNELSWITCH))
+ return ChannelSwitchMode::NO_SWITCH;
+
+ return ChannelSwitchMode::INSTANT_OR_DELAYED_SWITCH;
+}
+
+bool CPVRGUIActionListener::OnAction(const CAction& action)
+{
+ bool bIsJumpSMS = false;
+ bool bIsPlayingPVR = CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying() &&
+ g_application.CurrentFileItem().HasPVRChannelInfoTag();
+
+ switch (action.GetID())
+ {
+ case ACTION_PVR_PLAY:
+ case ACTION_PVR_PLAY_TV:
+ case ACTION_PVR_PLAY_RADIO:
+ {
+ // see if we're already playing a PVR stream and if not or the stream type
+ // doesn't match the demanded type, start playback of according type
+ switch (action.GetID())
+ {
+ case ACTION_PVR_PLAY:
+ if (!bIsPlayingPVR)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ PlaybackTypeAny);
+ break;
+ case ACTION_PVR_PLAY_TV:
+ if (!bIsPlayingPVR || g_application.CurrentFileItem().GetPVRChannelInfoTag()->IsRadio())
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ PlaybackTypeTV);
+ break;
+ case ACTION_PVR_PLAY_RADIO:
+ if (!bIsPlayingPVR || !g_application.CurrentFileItem().GetPVRChannelInfoTag()->IsRadio())
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ PlaybackTypeRadio);
+ break;
+ }
+ return true;
+ }
+
+ case ACTION_JUMP_SMS2:
+ case ACTION_JUMP_SMS3:
+ case ACTION_JUMP_SMS4:
+ case ACTION_JUMP_SMS5:
+ case ACTION_JUMP_SMS6:
+ case ACTION_JUMP_SMS7:
+ case ACTION_JUMP_SMS8:
+ case ACTION_JUMP_SMS9:
+ bIsJumpSMS = true;
+ // fallthru is intended
+ [[fallthrough]];
+ case REMOTE_0:
+ case REMOTE_1:
+ case REMOTE_2:
+ case REMOTE_3:
+ case REMOTE_4:
+ case REMOTE_5:
+ case REMOTE_6:
+ case REMOTE_7:
+ case REMOTE_8:
+ case REMOTE_9:
+ case ACTION_CHANNEL_NUMBER_SEP:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ if (CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_FULLSCREEN_VIDEO) ||
+ CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_VISUALISATION))
+ {
+ // do not consume action if a python modal is the top most dialog
+ // as a python modal can't return that it consumed the action.
+ if (CServiceBroker::GetGUI()->GetWindowManager().IsPythonWindow(
+ CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog()))
+ return false;
+
+ char cCharacter;
+ if (action.GetID() == ACTION_CHANNEL_NUMBER_SEP)
+ {
+ cCharacter = CPVRChannelNumber::SEPARATOR;
+ }
+ else
+ {
+ int iRemote =
+ bIsJumpSMS ? action.GetID() - (ACTION_JUMP_SMS2 - REMOTE_2) : action.GetID();
+ cCharacter = static_cast<char>(iRemote - REMOTE_0) + '0';
+ }
+ CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNumberInputHandler()
+ .AppendChannelNumberCharacter(cCharacter);
+ return true;
+ }
+ return false;
+ }
+
+ case ACTION_SHOW_INFO:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelNavigator().ToggleInfo();
+ return true;
+ }
+
+ case ACTION_SELECT_ITEM:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ // If the button that caused this action matches action "Select" ...
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRPLAYBACK_CONFIRMCHANNELSWITCH) &&
+ CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNavigator()
+ .IsPreview())
+ {
+ // ... and if "confirm channel switch" setting is active and a channel
+ // preview is currently shown, switch to the currently previewed channel.
+ CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNavigator()
+ .SwitchToCurrentChannel();
+ return true;
+ }
+ else if (CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNumberInputHandler()
+ .CheckInputAndExecuteAction())
+ {
+ // ... or if the action was processed by direct channel number input, we're done.
+ return true;
+ }
+ return false;
+ }
+
+ case ACTION_NEXT_ITEM:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SeekForward();
+ return true;
+ }
+
+ case ACTION_PREV_ITEM:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SeekBackward(
+ CApplication::ACTION_PREV_ITEM_THRESHOLD);
+ return true;
+ }
+
+ case ACTION_MOVE_UP:
+ case ACTION_CHANNEL_UP:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNavigator()
+ .SelectNextChannel(GetChannelSwitchMode(action.GetID()));
+ return true;
+ }
+
+ case ACTION_MOVE_DOWN:
+ case ACTION_CHANNEL_DOWN:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNavigator()
+ .SelectPreviousChannel(GetChannelSwitchMode(action.GetID()));
+ return true;
+ }
+
+ case ACTION_CHANNEL_SWITCH:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ int iChannelNumber = static_cast<int>(action.GetAmount(0));
+ int iSubChannelNumber = static_cast<int>(action.GetAmount(1));
+
+ const std::shared_ptr<CPVRPlaybackState> playbackState =
+ CServiceBroker::GetPVRManager().PlaybackState();
+ const std::shared_ptr<CPVRChannelGroup> activeGroup =
+ playbackState->GetActiveChannelGroup(playbackState->IsPlayingRadio());
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ activeGroup->GetByChannelNumber(CPVRChannelNumber(iChannelNumber, iSubChannelNumber));
+
+ if (!groupMember)
+ return false;
+
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ CFileItem(groupMember), false);
+ return true;
+ }
+
+ case ACTION_RECORD:
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleRecordingOnPlayingChannel();
+ return true;
+ }
+
+ case ACTION_PVR_ANNOUNCE_REMINDERS:
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AnnounceReminders();
+ return true;
+ }
+ }
+ return false;
+}
+
+void CPVRGUIActionListener::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_PVRPARENTAL_ENABLED)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue() &&
+ CServiceBroker::GetSettingsComponent()
+ ->GetSettings()
+ ->GetString(CSettings::SETTING_PVRPARENTAL_PIN)
+ .empty())
+ {
+ std::string newPassword = "";
+ // password set... save it
+ if (CGUIDialogNumeric::ShowAndVerifyNewPassword(newPassword))
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(
+ CSettings::SETTING_PVRPARENTAL_PIN, newPassword);
+ // password not set... disable parental
+ else
+ std::static_pointer_cast<CSettingBool>(std::const_pointer_cast<CSetting>(setting))
+ ->SetValue(false);
+ }
+ }
+ else if (settingId == CSettings::SETTING_EPG_PAST_DAYSTODISPLAY)
+ {
+ CServiceBroker::GetPVRManager().Clients()->SetEPGMaxPastDays(
+ std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ }
+ else if (settingId == CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY)
+ {
+ CServiceBroker::GetPVRManager().Clients()->SetEPGMaxFutureDays(
+ std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ }
+}
+
+void CPVRGUIActionListener::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_PVRMANAGER_RESETDB)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Database>().ResetDatabase(false);
+ }
+ else if (settingId == CSettings::SETTING_EPG_RESETEPG)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Database>().ResetDatabase(true);
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_CLIENTPRIORITIES)
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ CGUIDialog* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetDialog(
+ WINDOW_DIALOG_PVR_CLIENT_PRIORITIES);
+ if (dialog)
+ {
+ dialog->Open();
+ CServiceBroker::GetPVRManager().ChannelGroups()->UpdateFromClients({});
+ }
+ }
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_CHANNELMANAGER)
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ CGUIDialog* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_CHANNEL_MANAGER);
+ if (dialog)
+ dialog->Open();
+ }
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_GROUPMANAGER)
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ CGUIDialog* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_GROUP_MANAGER);
+ if (dialog)
+ dialog->Open();
+ }
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_CHANNELSCAN)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().StartChannelScan();
+ }
+ else if (settingId == CSettings::SETTING_PVRMENU_SEARCHICONS)
+ {
+ CServiceBroker::GetPVRManager().TriggerSearchMissingChannelIcons();
+ }
+ else if (settingId == CSettings::SETTING_PVRCLIENT_MENUHOOK)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Clients>().ProcessSettingsMenuHooks();
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_ADDONS)
+ {
+ const std::vector<std::string> params{"addons://default_binary_addons_source/kodi.pvrclient",
+ "return"};
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_ADDON_BROWSER, params);
+ }
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionListener.h b/xbmc/pvr/guilib/PVRGUIActionListener.h
new file mode 100644
index 0000000..d24818b
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionListener.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/IActionListener.h"
+#include "settings/lib/ISettingCallback.h"
+
+namespace PVR
+{
+
+class CPVRManager;
+enum class ChannelSwitchMode;
+enum class PVREvent;
+
+class CPVRGUIActionListener : public IActionListener, public ISettingCallback
+{
+public:
+ CPVRGUIActionListener();
+ ~CPVRGUIActionListener() override;
+
+ void Init(CPVRManager& mgr);
+ void Deinit(CPVRManager& mgr);
+
+ // IActionListener implementation
+ bool OnAction(const CAction& action) override;
+
+ // ISettingCallback implementation
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ void OnPVRManagerEvent(const PVREvent& event);
+
+private:
+ CPVRGUIActionListener(const CPVRGUIActionListener&) = delete;
+ CPVRGUIActionListener& operator=(const CPVRGUIActionListener&) = delete;
+
+ static ChannelSwitchMode GetChannelSwitchMode(int iAction);
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsChannels.cpp b/xbmc/pvr/guilib/PVRGUIActionsChannels.cpp
new file mode 100644
index 0000000..80fb90e
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsChannels.cpp
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIActionsChannels.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/windows/GUIWindowPVRBase.h"
+#include "settings/Settings.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <chrono>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+void CPVRChannelSwitchingInputHandler::AppendChannelNumberCharacter(char cCharacter)
+{
+ // special case. if only a single zero was typed in, switch to previously played channel.
+ if (GetCurrentDigitCount() == 0 && cCharacter == '0')
+ {
+ SwitchToPreviousChannel();
+ return;
+ }
+
+ CPVRChannelNumberInputHandler::AppendChannelNumberCharacter(cCharacter);
+}
+
+void CPVRChannelSwitchingInputHandler::GetChannelNumbers(std::vector<std::string>& channelNumbers)
+{
+ const CPVRManager& pvrMgr = CServiceBroker::GetPVRManager();
+ const std::shared_ptr<CPVRChannel> playingChannel = pvrMgr.PlaybackState()->GetPlayingChannel();
+ if (playingChannel)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ pvrMgr.ChannelGroups()->GetGroupAll(playingChannel->IsRadio());
+ if (group)
+ group->GetChannelNumbers(channelNumbers);
+ }
+}
+
+void CPVRChannelSwitchingInputHandler::OnInputDone()
+{
+ CPVRChannelNumber channelNumber = GetChannelNumber();
+ if (channelNumber.GetChannelNumber())
+ SwitchToChannel(channelNumber);
+}
+
+void CPVRChannelSwitchingInputHandler::SwitchToChannel(const CPVRChannelNumber& channelNumber)
+{
+ if (channelNumber.IsValid() && CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying())
+ {
+ const std::shared_ptr<CPVRChannel> playingChannel =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (playingChannel)
+ {
+ bool bRadio = playingChannel->IsRadio();
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(bRadio);
+
+ if (channelNumber != group->GetChannelNumber(playingChannel))
+ {
+ // channel number present in active group?
+ std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ group->GetByChannelNumber(channelNumber);
+
+ if (!groupMember)
+ {
+ // channel number present in any group?
+ const CPVRChannelGroups* groupAccess =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio);
+ const std::vector<std::shared_ptr<CPVRChannelGroup>> groups =
+ groupAccess->GetMembers(true);
+ for (const auto& currentGroup : groups)
+ {
+ if (currentGroup == group) // we have already checked this group
+ continue;
+
+ groupMember = currentGroup->GetByChannelNumber(channelNumber);
+ if (groupMember)
+ break;
+ }
+ }
+
+ if (groupMember)
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(
+ ACTION_CHANNEL_SWITCH, static_cast<float>(channelNumber.GetChannelNumber()),
+ static_cast<float>(channelNumber.GetSubChannelNumber()))));
+ }
+ }
+ }
+ }
+}
+
+void CPVRChannelSwitchingInputHandler::SwitchToPreviousChannel()
+{
+ const std::shared_ptr<CPVRPlaybackState> playbackState =
+ CServiceBroker::GetPVRManager().PlaybackState();
+ if (playbackState->IsPlaying())
+ {
+ const std::shared_ptr<CPVRChannel> playingChannel = playbackState->GetPlayingChannel();
+ if (playingChannel)
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ playbackState->GetPreviousToLastPlayedChannelGroupMember(playingChannel->IsRadio());
+ if (groupMember)
+ {
+ const CPVRChannelNumber channelNumber = groupMember->ChannelNumber();
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(
+ ACTION_CHANNEL_SWITCH, static_cast<float>(channelNumber.GetChannelNumber()),
+ static_cast<float>(channelNumber.GetSubChannelNumber()))));
+ }
+ }
+ }
+}
+
+CPVRGUIActionsChannels::CPVRGUIActionsChannels()
+ : m_settings({CSettings::SETTING_PVRMANAGER_PRESELECTPLAYINGCHANNEL})
+{
+ RegisterChannelNumberInputHandler(&m_channelNumberInputHandler);
+}
+
+CPVRGUIActionsChannels::~CPVRGUIActionsChannels()
+{
+ DeregisterChannelNumberInputHandler(&m_channelNumberInputHandler);
+}
+
+void CPVRGUIActionsChannels::RegisterChannelNumberInputHandler(
+ CPVRChannelNumberInputHandler* handler)
+{
+ if (handler)
+ handler->Events().Subscribe(this, &CPVRGUIActionsChannels::Notify);
+}
+
+void CPVRGUIActionsChannels::DeregisterChannelNumberInputHandler(
+ CPVRChannelNumberInputHandler* handler)
+{
+ if (handler)
+ handler->Events().Unsubscribe(this);
+}
+
+void CPVRGUIActionsChannels::Notify(const PVRChannelNumberInputChangedEvent& event)
+{
+ m_events.Publish(event);
+}
+
+bool CPVRGUIActionsChannels::HideChannel(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRChannel> channel = item.GetPVRChannelInfoTag();
+
+ if (!channel)
+ return false;
+
+ if (!CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{19054}, // "Hide channel"
+ CVariant{19039}, // "Are you sure you want to hide this channel?"
+ CVariant{""}, CVariant{channel->ChannelName()}))
+ return false;
+
+ if (!CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->GetGroupAll(channel->IsRadio())
+ ->RemoveFromGroup(channel))
+ return false;
+
+ CGUIWindowPVRBase* pvrWindow =
+ dynamic_cast<CGUIWindowPVRBase*>(CServiceBroker::GetGUI()->GetWindowManager().GetWindow(
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow()));
+ if (pvrWindow)
+ pvrWindow->DoRefresh();
+ else
+ CLog::LogF(LOGERROR, "Called on non-pvr window. No refresh possible.");
+
+ return true;
+}
+
+bool CPVRGUIActionsChannels::StartChannelScan()
+{
+ return StartChannelScan(PVR_INVALID_CLIENT_ID);
+}
+
+bool CPVRGUIActionsChannels::StartChannelScan(int clientId)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted() || IsRunningChannelScan())
+ return false;
+
+ std::shared_ptr<CPVRClient> scanClient;
+ std::vector<std::shared_ptr<CPVRClient>> possibleScanClients =
+ CServiceBroker::GetPVRManager().Clients()->GetClientsSupportingChannelScan();
+ m_bChannelScanRunning = true;
+
+ if (clientId != PVR_INVALID_CLIENT_ID)
+ {
+ const auto it =
+ std::find_if(possibleScanClients.cbegin(), possibleScanClients.cend(),
+ [clientId](const auto& client) { return client->GetID() == clientId; });
+
+ if (it != possibleScanClients.cend())
+ scanClient = (*it);
+
+ if (!scanClient)
+ {
+ CLog::LogF(LOGERROR,
+ "Provided client id '{}' could not be found in list of possible scan clients!",
+ clientId);
+ m_bChannelScanRunning = false;
+ return false;
+ }
+ }
+ /* multiple clients found */
+ else if (possibleScanClients.size() > 1)
+ {
+ CGUIDialogSelect* pDialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (!pDialog)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT!");
+ m_bChannelScanRunning = false;
+ return false;
+ }
+
+ pDialog->Reset();
+ pDialog->SetHeading(CVariant{19119}); // "On which backend do you want to search?"
+
+ for (const auto& client : possibleScanClients)
+ pDialog->Add(client->GetFriendlyName());
+
+ pDialog->Open();
+
+ int selection = pDialog->GetSelectedItem();
+ if (selection >= 0)
+ scanClient = possibleScanClients[selection];
+ }
+ /* one client found */
+ else if (possibleScanClients.size() == 1)
+ {
+ scanClient = possibleScanClients[0];
+ }
+ /* no clients found */
+ else if (!scanClient)
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{19033}, // "Information"
+ CVariant{19192}); // "None of the connected PVR backends supports scanning for channels."
+ m_bChannelScanRunning = false;
+ return false;
+ }
+
+ /* start the channel scan */
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Starting to scan for channels on client {}",
+ scanClient->GetFriendlyName());
+ auto start = std::chrono::steady_clock::now();
+
+ /* do the scan */
+ if (scanClient->StartChannelScan() != PVR_ERROR_NO_ERROR)
+ HELPERS::ShowOKDialogText(
+ CVariant{257}, // "Error"
+ CVariant{
+ 19193}); // "The channel scan can't be started. Check the log for more information about this message."
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Channel scan finished after {} ms", duration.count());
+
+ m_bChannelScanRunning = false;
+ return true;
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRGUIActionsChannels::GetChannelGroupMember(
+ const std::shared_ptr<CPVRChannel>& channel) const
+{
+ if (!channel)
+ return {};
+
+ std::shared_ptr<CPVRChannelGroupMember> groupMember;
+
+ // first, try whether the channel is contained in the active channel group, except
+ // if a window is active which never uses the active channel group, e.g. Timers window
+ const int activeWindowID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+
+ static std::vector<int> windowIDs = {
+ WINDOW_TV_RECORDINGS, WINDOW_TV_TIMERS, WINDOW_TV_TIMER_RULES, WINDOW_TV_SEARCH,
+ WINDOW_RADIO_RECORDINGS, WINDOW_RADIO_TIMERS, WINDOW_RADIO_TIMER_RULES, WINDOW_RADIO_SEARCH,
+ };
+
+ if (std::find(windowIDs.cbegin(), windowIDs.cend(), activeWindowID) == windowIDs.cend())
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(channel->IsRadio());
+ if (group)
+ groupMember = group->GetByUniqueID(channel->StorageId());
+ }
+
+ // as fallback, obtain the member from the 'all channels' group
+ if (!groupMember)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(channel->IsRadio());
+ if (group)
+ groupMember = group->GetByUniqueID(channel->StorageId());
+ }
+
+ return groupMember;
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRGUIActionsChannels::GetChannelGroupMember(
+ const CFileItem& item) const
+{
+ std::shared_ptr<CPVRChannelGroupMember> groupMember = item.GetPVRChannelGroupMemberInfoTag();
+
+ if (!groupMember)
+ groupMember = GetChannelGroupMember(CPVRItem(std::make_shared<CFileItem>(item)).GetChannel());
+
+ return groupMember;
+}
+
+CPVRChannelNumberInputHandler& CPVRGUIActionsChannels::GetChannelNumberInputHandler()
+{
+ // window/dialog specific input handler
+ CPVRChannelNumberInputHandler* windowInputHandler = dynamic_cast<CPVRChannelNumberInputHandler*>(
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow(
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog()));
+ if (windowInputHandler)
+ return *windowInputHandler;
+
+ // default
+ return m_channelNumberInputHandler;
+}
+
+CPVRGUIChannelNavigator& CPVRGUIActionsChannels::GetChannelNavigator()
+{
+ return m_channelNavigator;
+}
+
+void CPVRGUIActionsChannels::OnPlaybackStarted(const CFileItem& item)
+{
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember = GetChannelGroupMember(item);
+ if (groupMember)
+ {
+ m_channelNavigator.SetPlayingChannel(groupMember);
+ SetSelectedChannelPath(groupMember->Channel()->IsRadio(), groupMember->Path());
+ }
+}
+
+void CPVRGUIActionsChannels::OnPlaybackStopped(const CFileItem& item)
+{
+ if (item.HasPVRChannelInfoTag() || item.HasEPGInfoTag())
+ {
+ m_channelNavigator.ClearPlayingChannel();
+ }
+}
+
+void CPVRGUIActionsChannels::SetSelectedChannelPath(bool bRadio, const std::string& path)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (bRadio)
+ m_selectedChannelPathRadio = path;
+ else
+ m_selectedChannelPathTV = path;
+}
+
+std::string CPVRGUIActionsChannels::GetSelectedChannelPath(bool bRadio) const
+{
+ if (m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_PRESELECTPLAYINGCHANNEL))
+ {
+ CPVRManager& mgr = CServiceBroker::GetPVRManager();
+
+ // if preselect playing channel is activated, return the path of the playing channel, if any.
+ const std::shared_ptr<CPVRChannelGroupMember> playingChannel =
+ mgr.PlaybackState()->GetPlayingChannelGroupMember();
+ if (playingChannel && playingChannel->IsRadio() == bRadio)
+ return playingChannel->Path();
+
+ const std::shared_ptr<CPVREpgInfoTag> playingTag = mgr.PlaybackState()->GetPlayingEpgTag();
+ if (playingTag && playingTag->IsRadio() == bRadio)
+ {
+ const std::shared_ptr<CPVRChannel> channel =
+ mgr.ChannelGroups()->GetChannelForEpgTag(playingTag);
+ if (channel)
+ return GetChannelGroupMember(channel)->Path();
+ }
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return bRadio ? m_selectedChannelPathRadio : m_selectedChannelPathTV;
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsChannels.h b/xbmc/pvr/guilib/PVRGUIActionsChannels.h
new file mode 100644
index 0000000..a7656fe
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsChannels.h
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/IPVRComponent.h"
+#include "pvr/PVRChannelNumberInputHandler.h"
+#include "pvr/guilib/PVRGUIChannelNavigator.h"
+#include "pvr/settings/PVRSettings.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRChannelGroupMember;
+
+class CPVRChannelSwitchingInputHandler : public CPVRChannelNumberInputHandler
+{
+public:
+ // CPVRChannelNumberInputHandler implementation
+ void GetChannelNumbers(std::vector<std::string>& channelNumbers) override;
+ void AppendChannelNumberCharacter(char cCharacter) override;
+ void OnInputDone() override;
+
+private:
+ /*!
+ * @brief Switch to the channel with the given number.
+ * @param channelNumber the channel number
+ */
+ void SwitchToChannel(const CPVRChannelNumber& channelNumber);
+
+ /*!
+ * @brief Switch to the previously played channel.
+ */
+ void SwitchToPreviousChannel();
+};
+
+class CPVRGUIActionsChannels : public IPVRComponent
+{
+public:
+ CPVRGUIActionsChannels();
+ ~CPVRGUIActionsChannels() override;
+
+ /*!
+ * @brief Get the events available for CEventStream.
+ * @return The events.
+ */
+ CEventStream<PVRChannelNumberInputChangedEvent>& Events() { return m_events; }
+
+ /*!
+ * @brief Register a handler for channel number input.
+ * @param handler The handler to register.
+ */
+ void RegisterChannelNumberInputHandler(CPVRChannelNumberInputHandler* handler);
+
+ /*!
+ * @brief Deregister a handler for channel number input.
+ * @param handler The handler to deregister.
+ */
+ void DeregisterChannelNumberInputHandler(CPVRChannelNumberInputHandler* handler);
+
+ /*!
+ * @brief CEventStream callback for channel number input changes.
+ * @param event The event.
+ */
+ void Notify(const PVRChannelNumberInputChangedEvent& event);
+
+ /*!
+ * @brief Hide a channel, always showing a confirmation dialog.
+ * @param item containing a channel or an epg tag.
+ * @return true on success, false otherwise.
+ */
+ bool HideChannel(const CFileItem& item) const;
+
+ /*!
+ * @brief Open a selection dialog and start a channel scan on the selected client.
+ * @return true on success, false otherwise.
+ */
+ bool StartChannelScan();
+
+ /*!
+ * @brief Start a channel scan on the specified client or open a dialog to select a client
+ * @param clientId the id of client to scan or PVR_INVALID_CLIENT_ID if a dialog will be opened
+ * @return true on success, false otherwise.
+ */
+ bool StartChannelScan(int clientId);
+
+ /*!
+ * @return True when a channel scan is currently running, false otherwise.
+ */
+ bool IsRunningChannelScan() const { return m_bChannelScanRunning; }
+
+ /*!
+ * @brief Get a channel group member for the given channel, either from the currently active
+ * group or if not found there, from the 'all channels' group.
+ * @param channel the channel.
+ * @return the group member or nullptr if not found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMember(
+ const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Get a channel group member for the given item, either from the currently active group
+ * or if not found there, from the 'all channels' group.
+ * @param item the item containing a channel, channel group, recording, timer or epg tag.
+ * @return the group member or nullptr if not found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMember(const CFileItem& item) const;
+
+ /*!
+ * @brief Get the currently active channel number input handler.
+ * @return the handler.
+ */
+ CPVRChannelNumberInputHandler& GetChannelNumberInputHandler();
+
+ /*!
+ * @brief Get the channel navigator.
+ * @return the navigator.
+ */
+ CPVRGUIChannelNavigator& GetChannelNavigator();
+
+ /*!
+ * @brief Inform GUI actions that playback of an item just started.
+ * @param item The item that started to play.
+ */
+ void OnPlaybackStarted(const CFileItem& item);
+
+ /*!
+ * @brief Inform GUI actions that playback of an item was stopped due to user interaction.
+ * @param item The item that stopped to play.
+ */
+ void OnPlaybackStopped(const CFileItem& item);
+
+ /*!
+ * @brief Get the currently selected channel item path; used across several windows/dialogs to
+ * share item selection.
+ * @param bRadio True to query the selected path for PVR radio, false for Live TV.
+ * @return the path.
+ */
+ std::string GetSelectedChannelPath(bool bRadio) const;
+
+ /*!
+ * @brief Set the currently selected channel item path; used across several windows/dialogs to
+ * share item selection.
+ * @param bRadio True to set the selected path for PVR radio, false for Live TV.
+ * @param path The new path to set.
+ */
+ void SetSelectedChannelPath(bool bRadio, const std::string& path);
+
+private:
+ CPVRGUIActionsChannels(const CPVRGUIActionsChannels&) = delete;
+ CPVRGUIActionsChannels const& operator=(CPVRGUIActionsChannels const&) = delete;
+
+ CPVRChannelSwitchingInputHandler m_channelNumberInputHandler;
+ bool m_bChannelScanRunning{false};
+ CPVRGUIChannelNavigator m_channelNavigator;
+ CEventSource<PVRChannelNumberInputChangedEvent> m_events;
+
+ mutable CCriticalSection m_critSection;
+ CPVRSettings m_settings;
+ std::string m_selectedChannelPathTV;
+ std::string m_selectedChannelPathRadio;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Channels = CPVRGUIActionsChannels;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsClients.cpp b/xbmc/pvr/guilib/PVRGUIActionsClients.cpp
new file mode 100644
index 0000000..b9c598c
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsClients.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIActionsClients.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClientMenuHooks.h"
+#include "pvr/addons/PVRClients.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <utility>
+#include <vector>
+
+using namespace KODI::MESSAGING;
+
+using namespace PVR;
+
+bool CPVRGUIActionsClients::ProcessSettingsMenuHooks()
+{
+ const CPVRClientMap clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients();
+
+ std::vector<std::pair<std::shared_ptr<CPVRClient>, CPVRClientMenuHook>> settingsHooks;
+ for (const auto& client : clients)
+ {
+ const auto hooks = client.second->GetMenuHooks()->GetSettingsHooks();
+ std::transform(hooks.cbegin(), hooks.cend(), std::back_inserter(settingsHooks),
+ [&client](const auto& hook) { return std::make_pair(client.second, hook); });
+ }
+
+ if (settingsHooks.empty())
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{19033}, // "Information"
+ CVariant{19347}); // "None of the active PVR clients does provide client-specific settings."
+ return true; // no settings hooks, no error
+ }
+
+ auto selectedHook = settingsHooks.begin();
+
+ // if there is only one settings hook, execute it directly, otherwise let the user select
+ if (settingsHooks.size() > 1)
+ {
+ CGUIDialogSelect* pDialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (!pDialog)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT!");
+ return false;
+ }
+
+ pDialog->Reset();
+ pDialog->SetHeading(CVariant{19196}); // "PVR client specific actions"
+
+ for (const auto& hook : settingsHooks)
+ {
+ if (clients.size() == 1)
+ pDialog->Add(hook.second.GetLabel());
+ else
+ pDialog->Add(hook.first->GetFriendlyName() + ": " + hook.second.GetLabel());
+ }
+
+ pDialog->Open();
+
+ int selection = pDialog->GetSelectedItem();
+ if (selection < 0)
+ return true; // cancelled
+
+ std::advance(selectedHook, selection);
+ }
+ return selectedHook->first->CallSettingsMenuHook(selectedHook->second) == PVR_ERROR_NO_ERROR;
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsClients.h b/xbmc/pvr/guilib/PVRGUIActionsClients.h
new file mode 100644
index 0000000..7c48274
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsClients.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/IPVRComponent.h"
+
+namespace PVR
+{
+class CPVRGUIActionsClients : public IPVRComponent
+{
+public:
+ CPVRGUIActionsClients() = default;
+ ~CPVRGUIActionsClients() override = default;
+
+ /*!
+ * @brief Select and invoke client-specific settings actions
+ * @return true on success, false otherwise.
+ */
+ bool ProcessSettingsMenuHooks();
+
+private:
+ CPVRGUIActionsClients(const CPVRGUIActionsClients&) = delete;
+ CPVRGUIActionsClients const& operator=(CPVRGUIActionsClients const&) = delete;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Clients = CPVRGUIActionsClients;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp b/xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp
new file mode 100644
index 0000000..53bbf81
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIActionsDatabase.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <memory>
+#include <string>
+
+using namespace PVR;
+
+namespace
+{
+class CPVRGUIDatabaseResetComponentsSelector
+{
+public:
+ CPVRGUIDatabaseResetComponentsSelector() = default;
+ virtual ~CPVRGUIDatabaseResetComponentsSelector() = default;
+
+ bool Select()
+ {
+ CGUIDialogSelect* pDlgSelect =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (!pDlgSelect)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT!");
+ return false;
+ }
+
+ CFileItemList options;
+
+ const std::shared_ptr<CFileItem> itemAll =
+ std::make_shared<CFileItem>(StringUtils::Format(g_localizeStrings.Get(593))); // All
+ itemAll->SetPath("all");
+ options.Add(itemAll);
+
+ // if channels are cleared, groups, EPG data and providers must also be cleared
+ const std::shared_ptr<CFileItem> itemChannels =
+ std::make_shared<CFileItem>(StringUtils::Format("{}, {}, {}, {}",
+ g_localizeStrings.Get(19019), // Channels
+ g_localizeStrings.Get(19146), // Groups
+ g_localizeStrings.Get(19069), // Guide
+ g_localizeStrings.Get(19334))); // Providers
+ itemChannels->SetPath("channels");
+ itemChannels->Select(true); // preselect this item in dialog
+ options.Add(itemChannels);
+
+ const std::shared_ptr<CFileItem> itemGroups =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(19146)); // Groups
+ itemGroups->SetPath("groups");
+ options.Add(itemGroups);
+
+ const std::shared_ptr<CFileItem> itemGuide =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(19069)); // Guide
+ itemGuide->SetPath("guide");
+ options.Add(itemGuide);
+
+ const std::shared_ptr<CFileItem> itemProviders =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(19334)); // Providers
+ itemProviders->SetPath("providers");
+ options.Add(itemProviders);
+
+ const std::shared_ptr<CFileItem> itemReminders =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(19215)); // Reminders
+ itemReminders->SetPath("reminders");
+ options.Add(itemReminders);
+
+ const std::shared_ptr<CFileItem> itemRecordings =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(19017)); // Recordings
+ itemRecordings->SetPath("recordings");
+ options.Add(itemRecordings);
+
+ const std::shared_ptr<CFileItem> itemClients =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(24019)); // PVR clients
+ itemClients->SetPath("clients");
+ options.Add(itemClients);
+
+ pDlgSelect->Reset();
+ pDlgSelect->SetHeading(CVariant{g_localizeStrings.Get(19185)}); // "Clear data"
+ pDlgSelect->SetItems(options);
+ pDlgSelect->SetMultiSelection(true);
+ pDlgSelect->Open();
+
+ if (!pDlgSelect->IsConfirmed())
+ return false;
+
+ for (int i : pDlgSelect->GetSelectedItems())
+ {
+ const std::string path = options.Get(i)->GetPath();
+
+ m_bResetChannels |= (path == "channels" || path == "all");
+ m_bResetGroups |= (path == "groups" || path == "all");
+ m_bResetGuide |= (path == "guide" || path == "all");
+ m_bResetProviders |= (path == "providers" || path == "all");
+ m_bResetReminders |= (path == "reminders" || path == "all");
+ m_bResetRecordings |= (path == "recordings" || path == "all");
+ m_bResetClients |= (path == "clients" || path == "all");
+ }
+
+ m_bResetGroups |= m_bResetChannels;
+ m_bResetGuide |= m_bResetChannels;
+ m_bResetProviders |= m_bResetChannels;
+
+ return (m_bResetChannels || m_bResetGroups || m_bResetGuide || m_bResetProviders ||
+ m_bResetReminders || m_bResetRecordings || m_bResetClients);
+ }
+
+ bool IsResetChannelsSelected() const { return m_bResetChannels; }
+ bool IsResetGroupsSelected() const { return m_bResetGroups; }
+ bool IsResetGuideSelected() const { return m_bResetGuide; }
+ bool IsResetProvidersSelected() const { return m_bResetProviders; }
+ bool IsResetRemindersSelected() const { return m_bResetReminders; }
+ bool IsResetRecordingsSelected() const { return m_bResetRecordings; }
+ bool IsResetClientsSelected() const { return m_bResetClients; }
+
+private:
+ bool m_bResetChannels = false;
+ bool m_bResetGroups = false;
+ bool m_bResetGuide = false;
+ bool m_bResetProviders = false;
+ bool m_bResetReminders = false;
+ bool m_bResetRecordings = false;
+ bool m_bResetClients = false;
+};
+
+} // unnamed namespace
+
+bool CPVRGUIActionsDatabase::ResetDatabase(bool bResetEPGOnly)
+{
+ CGUIDialogProgress* pDlgProgress =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(
+ WINDOW_DIALOG_PROGRESS);
+ if (!pDlgProgress)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PROGRESS!");
+ return false;
+ }
+
+ bool bResetChannels = false;
+ bool bResetGroups = false;
+ bool bResetGuide = false;
+ bool bResetProviders = false;
+ bool bResetReminders = false;
+ bool bResetRecordings = false;
+ bool bResetClients = false;
+
+ if (bResetEPGOnly)
+ {
+ if (!CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{19098}, // "Warning!"
+ CVariant{19188})) // "All guide data will be cleared. Are you sure?"
+ return false;
+
+ bResetGuide = true;
+ }
+ else
+ {
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalPIN() !=
+ ParentalCheckResult::SUCCESS)
+ return false;
+
+ CPVRGUIDatabaseResetComponentsSelector selector;
+ if (!selector.Select())
+ return false;
+
+ if (!CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{19098}, // "Warning!"
+ CVariant{19186})) // "All selected data will be cleared. ... Are you sure?"
+ return false;
+
+ bResetChannels = selector.IsResetChannelsSelected();
+ bResetGroups = selector.IsResetGroupsSelected();
+ bResetGuide = selector.IsResetGuideSelected();
+ bResetProviders = selector.IsResetProvidersSelected();
+ bResetReminders = selector.IsResetRemindersSelected();
+ bResetRecordings = selector.IsResetRecordingsSelected();
+ bResetClients = selector.IsResetClientsSelected();
+ }
+
+ CDateTime::ResetTimezoneBias();
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "PVR clearing {} database", bResetEPGOnly ? "EPG" : "PVR and EPG");
+
+ pDlgProgress->SetHeading(CVariant{313}); // "Cleaning database"
+ pDlgProgress->SetLine(0, CVariant{g_localizeStrings.Get(19187)}); // "Clearing all related data."
+ pDlgProgress->SetLine(1, CVariant{""});
+ pDlgProgress->SetLine(2, CVariant{""});
+
+ pDlgProgress->Open();
+ pDlgProgress->Progress();
+
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying())
+ {
+ CLog::Log(LOGINFO, "PVR is stopping playback for {} database reset",
+ bResetEPGOnly ? "EPG" : "PVR and EPG");
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ }
+
+ const std::shared_ptr<CPVRDatabase> pvrDatabase(CServiceBroker::GetPVRManager().GetTVDatabase());
+ const std::shared_ptr<CPVREpgDatabase> epgDatabase(
+ CServiceBroker::GetPVRManager().EpgContainer().GetEpgDatabase());
+
+ // increase db open refcounts, so they don't get closed during following pvr manager shutdown
+ pvrDatabase->Open();
+ epgDatabase->Open();
+
+ // stop pvr manager; close both pvr and epg databases
+ CServiceBroker::GetPVRManager().Stop();
+
+ const int iProgressStepPercentage =
+ 100 / ((2 * bResetChannels) + bResetGroups + bResetGuide + bResetProviders + bResetReminders +
+ bResetRecordings + bResetClients + 1);
+ int iProgressStepsDone = 0;
+
+ if (bResetProviders)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all providers
+ pvrDatabase->DeleteProviders();
+ }
+
+ if (bResetGuide)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // reset channel's EPG pointers
+ pvrDatabase->ResetEPG();
+
+ // delete all entries from the EPG database
+ epgDatabase->DeleteEpg();
+ }
+
+ if (bResetGroups)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all channel groups (including data only available locally, like user defined groups)
+ pvrDatabase->DeleteChannelGroups();
+ }
+
+ if (bResetChannels)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all channels (including data only available locally, like user set icons)
+ pvrDatabase->DeleteChannels();
+ }
+
+ if (bResetReminders)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all timers data (e.g. all reminders, which are only stored locally)
+ pvrDatabase->DeleteTimers();
+ }
+
+ if (bResetClients)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all clients data (e.g priorities, which are only stored locally)
+ pvrDatabase->DeleteClients();
+ }
+
+ if (bResetChannels || bResetRecordings)
+ {
+ CVideoDatabase videoDatabase;
+
+ if (videoDatabase.Open())
+ {
+ if (bResetChannels)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all channel's entries (e.g. settings, bookmarks, stream details)
+ videoDatabase.EraseAllForPath("pvr://channels/");
+ }
+
+ if (bResetRecordings)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all recording's entries (e.g. settings, bookmarks, stream details)
+ videoDatabase.EraseAllForPath(CPVRRecordingsPath::PATH_RECORDINGS);
+ }
+
+ videoDatabase.Close();
+ }
+ }
+
+ // decrease db open refcounts; this actually closes dbs because refcounts drops to zero
+ pvrDatabase->Close();
+ epgDatabase->Close();
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "{} database cleared", bResetEPGOnly ? "EPG" : "PVR and EPG");
+
+ CLog::Log(LOGINFO, "Restarting the PVR Manager after {} database reset",
+ bResetEPGOnly ? "EPG" : "PVR and EPG");
+ CServiceBroker::GetPVRManager().Start();
+
+ pDlgProgress->SetPercentage(100);
+ pDlgProgress->Close();
+ return true;
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsDatabase.h b/xbmc/pvr/guilib/PVRGUIActionsDatabase.h
new file mode 100644
index 0000000..62eb484
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsDatabase.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/IPVRComponent.h"
+
+namespace PVR
+{
+class CPVRGUIActionsDatabase : public IPVRComponent
+{
+public:
+ CPVRGUIActionsDatabase() = default;
+ ~CPVRGUIActionsDatabase() override = default;
+
+ /*!
+ * @brief Reset the TV database to it's initial state and delete all the data.
+ * @param bResetEPGOnly True to only reset the EPG database, false to reset both PVR and EPG
+ * database.
+ * @return true on success, false otherwise.
+ */
+ bool ResetDatabase(bool bResetEPGOnly);
+
+private:
+ CPVRGUIActionsDatabase(const CPVRGUIActionsDatabase&) = delete;
+ CPVRGUIActionsDatabase const& operator=(CPVRGUIActionsDatabase const&) = delete;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Database = CPVRGUIActionsDatabase;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsEPG.cpp b/xbmc/pvr/guilib/PVRGUIActionsEPG.cpp
new file mode 100644
index 0000000..8b5cc9b
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsEPG.cpp
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIActionsEPG.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/dialogs/GUIDialogPVRChannelGuide.h"
+#include "pvr/dialogs/GUIDialogPVRGuideInfo.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "pvr/windows/GUIWindowPVRSearch.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <string>
+
+using namespace PVR;
+
+namespace
+{
+PVR::CGUIWindowPVRSearchBase* GetSearchWindow(bool bRadio)
+{
+ const int windowSearchId = bRadio ? WINDOW_RADIO_SEARCH : WINDOW_TV_SEARCH;
+
+ PVR::CGUIWindowPVRSearchBase* windowSearch;
+
+ CGUIWindowManager& windowMgr = CServiceBroker::GetGUI()->GetWindowManager();
+ if (bRadio)
+ windowSearch = windowMgr.GetWindow<PVR::CGUIWindowPVRRadioSearch>(windowSearchId);
+ else
+ windowSearch = windowMgr.GetWindow<PVR::CGUIWindowPVRTVSearch>(windowSearchId);
+
+ if (!windowSearch)
+ CLog::LogF(LOGERROR, "Unable to get {}!", bRadio ? "WINDOW_RADIO_SEARCH" : "WINDOW_TV_SEARCH");
+
+ return windowSearch;
+}
+} // unnamed namespace
+
+bool CPVRGUIActionsEPG::ShowEPGInfo(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel());
+ if (channel && CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(
+ channel) != ParentalCheckResult::SUCCESS)
+ return false;
+
+ const std::shared_ptr<CPVREpgInfoTag> epgTag(CPVRItem(item).GetEpgInfoTag());
+ if (!epgTag)
+ {
+ CLog::LogF(LOGERROR, "No epg tag!");
+ return false;
+ }
+
+ CGUIDialogPVRGuideInfo* pDlgInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGuideInfo>(
+ WINDOW_DIALOG_PVR_GUIDE_INFO);
+ if (!pDlgInfo)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_GUIDE_INFO!");
+ return false;
+ }
+
+ pDlgInfo->SetProgInfo(std::make_shared<CFileItem>(epgTag));
+ pDlgInfo->Open();
+ return true;
+}
+
+bool CPVRGUIActionsEPG::ShowChannelEPG(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel());
+ if (channel && CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(
+ channel) != ParentalCheckResult::SUCCESS)
+ return false;
+
+ CGUIDialogPVRChannelGuide* pDlgInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRChannelGuide>(
+ WINDOW_DIALOG_PVR_CHANNEL_GUIDE);
+ if (!pDlgInfo)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_CHANNEL_GUIDE!");
+ return false;
+ }
+
+ pDlgInfo->Open(channel);
+ return true;
+}
+
+bool CPVRGUIActionsEPG::FindSimilar(const CFileItem& item) const
+{
+ CGUIWindowPVRSearchBase* windowSearch = GetSearchWindow(CPVRItem(item).IsRadio());
+ if (!windowSearch)
+ return false;
+
+ //! @todo If we want dialogs to spawn program search in a clean way - without having to force-close any
+ // other dialogs - we must introduce a search dialog with functionality similar to the search window.
+
+ for (int iId = CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog(
+ true /* ignoreClosing */);
+ iId != WINDOW_INVALID;
+ iId = CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog(
+ true /* ignoreClosing */))
+ {
+ CLog::LogF(LOGWARNING,
+ "Have to close modal dialog with id {} before search window can be opened.", iId);
+
+ CGUIWindow* window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(iId);
+ if (window)
+ {
+ window->Close();
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to get window instance {}! Cannot open search window.", iId);
+ return false; // return, otherwise we run into an endless loop
+ }
+ }
+
+ windowSearch->SetItemToSearch(item);
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowSearch->GetID());
+ return true;
+};
+
+bool CPVRGUIActionsEPG::ExecuteSavedSearch(const CFileItem& item)
+{
+ const auto searchFilter = item.GetEPGSearchFilter();
+
+ if (!searchFilter)
+ {
+ CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present.");
+ return false;
+ }
+
+ CGUIWindowPVRSearchBase* windowSearch = GetSearchWindow(searchFilter->IsRadio());
+ if (!windowSearch)
+ return false;
+
+ windowSearch->SetItemToSearch(item);
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowSearch->GetID());
+ return true;
+}
+
+bool CPVRGUIActionsEPG::EditSavedSearch(const CFileItem& item)
+{
+ const auto searchFilter = item.GetEPGSearchFilter();
+
+ if (!searchFilter)
+ {
+ CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present.");
+ return false;
+ }
+
+ CGUIWindowPVRSearchBase* windowSearch = GetSearchWindow(searchFilter->IsRadio());
+ if (!windowSearch)
+ return false;
+
+ if (windowSearch->OpenDialogSearch(item) == CGUIDialogPVRGuideSearch::Result::SEARCH)
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowSearch->GetID());
+
+ return true;
+}
+
+bool CPVRGUIActionsEPG::RenameSavedSearch(const CFileItem& item)
+{
+ const auto searchFilter = item.GetEPGSearchFilter();
+
+ if (!searchFilter)
+ {
+ CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present.");
+ return false;
+ }
+
+ std::string title = searchFilter->GetTitle();
+ if (CGUIKeyboardFactory::ShowAndGetInput(title,
+ CVariant{g_localizeStrings.Get(528)}, // "Enter title"
+ false))
+ {
+ searchFilter->SetTitle(title);
+ CServiceBroker::GetPVRManager().EpgContainer().PersistSavedSearch(*searchFilter);
+ return true;
+ }
+ return false;
+}
+
+bool CPVRGUIActionsEPG::DeleteSavedSearch(const CFileItem& item)
+{
+ const auto searchFilter = item.GetEPGSearchFilter();
+
+ if (!searchFilter)
+ {
+ CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present.");
+ return false;
+ }
+
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{122}, // "Confirm delete"
+ CVariant{19338}, // "Delete this saved search?"
+ CVariant{""}, CVariant{item.GetLabel()}))
+ {
+ return CServiceBroker::GetPVRManager().EpgContainer().DeleteSavedSearch(*searchFilter);
+ }
+ return false;
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsEPG.h b/xbmc/pvr/guilib/PVRGUIActionsEPG.h
new file mode 100644
index 0000000..e0a3edc
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsEPG.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/IPVRComponent.h"
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRGUIActionsEPG : public IPVRComponent
+{
+public:
+ CPVRGUIActionsEPG() = default;
+ ~CPVRGUIActionsEPG() override = default;
+
+ /*!
+ * @brief Open a dialog with epg information for a given item.
+ * @param item containing epg data to show. item must be an epg tag, a channel or a timer.
+ * @return true on success, false otherwise.
+ */
+ bool ShowEPGInfo(const CFileItem& item) const;
+
+ /*!
+ * @brief Open a dialog with the epg list for a given item.
+ * @param item containing channel info. item must be an epg tag, a channel or a timer.
+ * @return true on success, false otherwise.
+ */
+ bool ShowChannelEPG(const CFileItem& item) const;
+
+ /*!
+ * @brief Open a window containing a list of epg tags 'similar' to a given item.
+ * @param item containing epg data for matching. item must be an epg tag, a channel or a
+ * recording.
+ * @return true on success, false otherwise.
+ */
+ bool FindSimilar(const CFileItem& item) const;
+
+ /*!
+ * @brief Execute a saved search. Displays result in search window if it is open.
+ * @param item The item containing a search filter.
+ * @return True on success, false otherwise.
+ */
+ bool ExecuteSavedSearch(const CFileItem& item);
+
+ /*!
+ * @brief Edit a saved search. Opens the search dialog.
+ * @param item The item containing a search filter.
+ * @return True on success, false otherwise.
+ */
+ bool EditSavedSearch(const CFileItem& item);
+
+ /*!
+ * @brief Rename a saved search. Opens a title input dialog.
+ * @param item The item containing a search filter.
+ * @return True on success, false otherwise.
+ */
+ bool RenameSavedSearch(const CFileItem& item);
+
+ /*!
+ * @brief Delete a saved search. Opens confirmation dialog before deleting.
+ * @param item The item containing a search filter.
+ * @return True on success, false otherwise.
+ */
+ bool DeleteSavedSearch(const CFileItem& item);
+
+private:
+ CPVRGUIActionsEPG(const CPVRGUIActionsEPG&) = delete;
+ CPVRGUIActionsEPG const& operator=(CPVRGUIActionsEPG const&) = delete;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using EPG = CPVRGUIActionsEPG;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp
new file mode 100644
index 0000000..96947c4
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIActionsParentalControl.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "settings/Settings.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <string>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+CPVRGUIActionsParentalControl::CPVRGUIActionsParentalControl()
+ : m_settings({CSettings::SETTING_PVRPARENTAL_PIN, CSettings::SETTING_PVRPARENTAL_ENABLED})
+{
+}
+
+ParentalCheckResult CPVRGUIActionsParentalControl::CheckParentalLock(
+ const std::shared_ptr<CPVRChannel>& channel) const
+{
+ if (!CServiceBroker::GetPVRManager().IsParentalLocked(channel))
+ return ParentalCheckResult::SUCCESS;
+
+ ParentalCheckResult ret = CheckParentalPIN();
+
+ if (ret == ParentalCheckResult::FAILED)
+ CLog::LogF(LOGERROR, "Parental lock verification failed for channel '{}': wrong PIN entered.",
+ channel->ChannelName());
+
+ return ret;
+}
+
+ParentalCheckResult CPVRGUIActionsParentalControl::CheckParentalPIN() const
+{
+ if (!m_settings.GetBoolValue(CSettings::SETTING_PVRPARENTAL_ENABLED))
+ return ParentalCheckResult::SUCCESS;
+
+ std::string pinCode = m_settings.GetStringValue(CSettings::SETTING_PVRPARENTAL_PIN);
+ if (pinCode.empty())
+ return ParentalCheckResult::SUCCESS;
+
+ InputVerificationResult ret = CGUIDialogNumeric::ShowAndVerifyInput(
+ pinCode, g_localizeStrings.Get(19262), true); // "Parental control. Enter PIN:"
+
+ if (ret == InputVerificationResult::SUCCESS)
+ {
+ CServiceBroker::GetPVRManager().RestartParentalTimer();
+ return ParentalCheckResult::SUCCESS;
+ }
+ else if (ret == InputVerificationResult::FAILED)
+ {
+ HELPERS::ShowOKDialogText(CVariant{19264},
+ CVariant{19265}); // "Incorrect PIN", "The entered PIN was incorrect."
+ return ParentalCheckResult::FAILED;
+ }
+ else
+ {
+ return ParentalCheckResult::CANCELED;
+ }
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsParentalControl.h b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.h
new file mode 100644
index 0000000..921f597
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/IPVRComponent.h"
+#include "pvr/settings/PVRSettings.h"
+
+#include <memory>
+
+namespace PVR
+{
+enum class ParentalCheckResult
+{
+ CANCELED,
+ FAILED,
+ SUCCESS
+};
+
+class CPVRChannel;
+
+class CPVRGUIActionsParentalControl : public IPVRComponent
+{
+public:
+ CPVRGUIActionsParentalControl();
+ ~CPVRGUIActionsParentalControl() override = default;
+
+ /*!
+ * @brief Check if channel is parental locked. Ask for PIN if necessary.
+ * @param channel The channel to do the check for.
+ * @return the result of the check (success, failed, or canceled by user).
+ */
+ ParentalCheckResult CheckParentalLock(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Open Numeric dialog to check for parental PIN.
+ * @return the result of the check (success, failed, or canceled by user).
+ */
+ ParentalCheckResult CheckParentalPIN() const;
+
+private:
+ CPVRGUIActionsParentalControl(const CPVRGUIActionsParentalControl&) = delete;
+ CPVRGUIActionsParentalControl const& operator=(CPVRGUIActionsParentalControl const&) = delete;
+
+ CPVRSettings m_settings;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Parental = CPVRGUIActionsParentalControl;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp b/xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp
new file mode 100644
index 0000000..66fdb80
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp
@@ -0,0 +1,564 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIActionsPlayback.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationEnums.h"
+#include "cores/DataCacheCore.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/PVRStreamProperties.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+CPVRGUIActionsPlayback::CPVRGUIActionsPlayback()
+ : m_settings({CSettings::SETTING_LOOKANDFEEL_STARTUPACTION,
+ CSettings::SETTING_PVRPLAYBACK_SWITCHTOFULLSCREENCHANNELTYPES})
+{
+}
+
+std::string CPVRGUIActionsPlayback::GetResumeLabel(const CFileItem& item) const
+{
+ std::string resumeString;
+
+ const std::shared_ptr<CPVRRecording> recording(
+ CPVRItem(CFileItemPtr(new CFileItem(item))).GetRecording());
+ if (recording && !recording->IsDeleted())
+ {
+ int positionInSeconds = lrint(recording->GetResumePoint().timeInSeconds);
+ if (positionInSeconds > 0)
+ resumeString = StringUtils::Format(
+ g_localizeStrings.Get(12022),
+ StringUtils::SecondsToTimeString(positionInSeconds, TIME_FORMAT_HH_MM_SS));
+ }
+ return resumeString;
+}
+
+bool CPVRGUIActionsPlayback::CheckResumeRecording(const CFileItem& item) const
+{
+ bool bPlayIt(true);
+ std::string resumeString(GetResumeLabel(item));
+ if (!resumeString.empty())
+ {
+ CContextButtons choices;
+ choices.Add(CONTEXT_BUTTON_RESUME_ITEM, resumeString);
+ choices.Add(CONTEXT_BUTTON_PLAY_ITEM, 12021); // Play from beginning
+ int choice = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+ if (choice > 0)
+ const_cast<CFileItem*>(&item)->SetStartOffset(
+ choice == CONTEXT_BUTTON_RESUME_ITEM ? STARTOFFSET_RESUME : 0);
+ else
+ bPlayIt = false; // context menu cancelled
+ }
+ return bPlayIt;
+}
+
+bool CPVRGUIActionsPlayback::ResumePlayRecording(const CFileItem& item, bool bFallbackToPlay) const
+{
+ bool bCanResume = !GetResumeLabel(item).empty();
+ if (bCanResume)
+ {
+ const_cast<CFileItem*>(&item)->SetStartOffset(STARTOFFSET_RESUME);
+ }
+ else
+ {
+ if (bFallbackToPlay)
+ const_cast<CFileItem*>(&item)->SetStartOffset(0);
+ else
+ return false;
+ }
+
+ return PlayRecording(item, false);
+}
+
+void CPVRGUIActionsPlayback::CheckAndSwitchToFullscreen(bool bFullscreen) const
+{
+ CMediaSettings::GetInstance().SetMediaStartWindowed(!bFullscreen);
+
+ if (bFullscreen)
+ {
+ CGUIMessage msg(GUI_MSG_FULLSCREEN, 0,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+}
+
+void CPVRGUIActionsPlayback::StartPlayback(CFileItem* item,
+ bool bFullscreen,
+ const CPVRStreamProperties* epgProps) const
+{
+ // Obtain dynamic playback url and properties from the respective pvr client
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item);
+ if (client)
+ {
+ CPVRStreamProperties props;
+
+ if (item->IsPVRChannel())
+ {
+ // If this was an EPG Tag to be played as live then PlayEpgTag() will create a channel
+ // fileitem instead and pass the epg tags props so we use those and skip the client call
+ if (epgProps)
+ props = *epgProps;
+ else
+ client->GetChannelStreamProperties(item->GetPVRChannelInfoTag(), props);
+ }
+ else if (item->IsPVRRecording())
+ {
+ client->GetRecordingStreamProperties(item->GetPVRRecordingInfoTag(), props);
+ }
+ else if (item->IsEPG())
+ {
+ if (epgProps) // we already have props from PlayEpgTag()
+ props = *epgProps;
+ else
+ client->GetEpgTagStreamProperties(item->GetEPGInfoTag(), props);
+ }
+
+ if (props.size())
+ {
+ const std::string url = props.GetStreamURL();
+ if (!url.empty())
+ item->SetDynPath(url);
+
+ const std::string mime = props.GetStreamMimeType();
+ if (!mime.empty())
+ {
+ item->SetMimeType(mime);
+ item->SetContentLookup(false);
+ }
+
+ for (const auto& prop : props)
+ item->SetProperty(prop.first, prop.second);
+ }
+ }
+
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 0, 0, static_cast<void*>(item));
+ CheckAndSwitchToFullscreen(bFullscreen);
+}
+
+bool CPVRGUIActionsPlayback::PlayRecording(const CFileItem& item, bool bCheckResume) const
+{
+ const std::shared_ptr<CPVRRecording> recording(CPVRItem(item).GetRecording());
+ if (!recording)
+ return false;
+
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRecording(recording))
+ {
+ CGUIMessage msg(GUI_MSG_FULLSCREEN, 0,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ return true;
+ }
+
+ if (!bCheckResume || CheckResumeRecording(item))
+ {
+ CFileItem* itemToPlay = new CFileItem(recording);
+ itemToPlay->SetStartOffset(item.GetStartOffset());
+ StartPlayback(itemToPlay, true);
+ }
+ return true;
+}
+
+bool CPVRGUIActionsPlayback::PlayEpgTag(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epgTag(CPVRItem(item).GetEpgInfoTag());
+ if (!epgTag)
+ return false;
+
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingEpgTag(epgTag))
+ {
+ CGUIMessage msg(GUI_MSG_FULLSCREEN, 0,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ return true;
+ }
+
+ // Obtain dynamic playback url and properties from the respective pvr client
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(epgTag->ClientID());
+ if (!client)
+ return false;
+
+ CPVRStreamProperties props;
+ client->GetEpgTagStreamProperties(epgTag, props);
+
+ CFileItem* itemToPlay = nullptr;
+ if (props.EPGPlaybackAsLive())
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(item);
+ if (!groupMember)
+ return false;
+
+ itemToPlay = new CFileItem(groupMember);
+ }
+ else
+ {
+ itemToPlay = new CFileItem(epgTag);
+ }
+
+ StartPlayback(itemToPlay, true, &props);
+ return true;
+}
+
+bool CPVRGUIActionsPlayback::SwitchToChannel(const CFileItem& item, bool bCheckResume) const
+{
+ if (item.m_bIsFolder)
+ return false;
+
+ std::shared_ptr<CPVRRecording> recording;
+ const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel());
+ if (channel)
+ {
+ bool bSwitchToFullscreen =
+ CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingChannel(channel);
+
+ if (!bSwitchToFullscreen)
+ {
+ recording =
+ CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(channel->GetEPGNow());
+ bSwitchToFullscreen =
+ recording &&
+ CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRecording(recording);
+ }
+
+ if (bSwitchToFullscreen)
+ {
+ CGUIMessage msg(GUI_MSG_FULLSCREEN, 0,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ return true;
+ }
+ }
+
+ ParentalCheckResult result =
+ channel ? CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(channel)
+ : ParentalCheckResult::FAILED;
+ if (result == ParentalCheckResult::SUCCESS)
+ {
+ // switch to channel or if recording present, ask whether to switch or play recording...
+ if (!recording)
+ recording =
+ CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(channel->GetEPGNow());
+
+ if (recording)
+ {
+ bool bCancel(false);
+ bool bPlayRecording = CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{19687}, // "Play recording"
+ CVariant{""}, CVariant{12021}, // "Play from beginning"
+ CVariant{recording->m_strTitle}, bCancel, CVariant{19000}, // "Switch to channel"
+ CVariant{19687}, // "Play recording"
+ 0); // no autoclose
+ if (bCancel)
+ return false;
+
+ if (bPlayRecording)
+ return PlayRecording(CFileItem(recording), bCheckResume);
+ }
+
+ bool bFullscreen;
+ switch (m_settings.GetIntValue(CSettings::SETTING_PVRPLAYBACK_SWITCHTOFULLSCREENCHANNELTYPES))
+ {
+ case 0: // never
+ bFullscreen = false;
+ break;
+ case 1: // TV channels
+ bFullscreen = !channel->IsRadio();
+ break;
+ case 2: // Radio channels
+ bFullscreen = channel->IsRadio();
+ break;
+ case 3: // TV and radio channels
+ default:
+ bFullscreen = true;
+ break;
+ }
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(item);
+ if (!groupMember)
+ return false;
+
+ StartPlayback(new CFileItem(groupMember), bFullscreen);
+ return true;
+ }
+ else if (result == ParentalCheckResult::FAILED)
+ {
+ const std::string channelName =
+ channel ? channel->ChannelName() : g_localizeStrings.Get(19029); // Channel
+ const std::string msg = StringUtils::Format(
+ g_localizeStrings.Get(19035),
+ channelName); // CHANNELNAME could not be played. Check the log for details.
+
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(19166),
+ msg); // PVR information
+ }
+
+ return false;
+}
+
+bool CPVRGUIActionsPlayback::SwitchToChannel(PlaybackType type) const
+{
+ std::shared_ptr<CPVRChannelGroupMember> groupMember;
+ bool bIsRadio(false);
+
+ // check if the desired PlaybackType is already playing,
+ // and if not, try to grab the last played channel of this type
+ switch (type)
+ {
+ case PlaybackTypeRadio:
+ {
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio())
+ return true;
+
+ const std::shared_ptr<CPVRChannelGroup> allGroup =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAllRadio();
+ if (allGroup)
+ groupMember = allGroup->GetLastPlayedChannelGroupMember();
+
+ bIsRadio = true;
+ break;
+ }
+ case PlaybackTypeTV:
+ {
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV())
+ return true;
+
+ const std::shared_ptr<CPVRChannelGroup> allGroup =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAllTV();
+ if (allGroup)
+ groupMember = allGroup->GetLastPlayedChannelGroupMember();
+
+ break;
+ }
+ default:
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying())
+ return true;
+
+ groupMember =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetLastPlayedChannelGroupMember();
+ break;
+ }
+
+ // if we have a last played channel, start playback
+ if (groupMember)
+ {
+ return SwitchToChannel(CFileItem(groupMember), true);
+ }
+ else
+ {
+ // if we don't, find the active channel group of the demanded type and play it's first channel
+ const std::shared_ptr<CPVRChannelGroup> channelGroup =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(bIsRadio);
+ if (channelGroup)
+ {
+ // try to start playback of first channel in this group
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ channelGroup->GetMembers();
+ if (!groupMembers.empty())
+ {
+ return SwitchToChannel(CFileItem(*groupMembers.begin()), true);
+ }
+ }
+ }
+
+ CLog::LogF(LOGERROR,
+ "Could not determine {} channel to playback. No last played channel found, and "
+ "first channel of active group could also not be determined.",
+ bIsRadio ? "Radio" : "TV");
+
+ CGUIDialogKaiToast::QueueNotification(
+ CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(19166), // PVR information
+ StringUtils::Format(
+ g_localizeStrings.Get(19035),
+ g_localizeStrings.Get(
+ bIsRadio ? 19021
+ : 19020))); // Radio/TV could not be played. Check the log for details.
+ return false;
+}
+
+bool CPVRGUIActionsPlayback::PlayChannelOnStartup() const
+{
+ int iAction = m_settings.GetIntValue(CSettings::SETTING_LOOKANDFEEL_STARTUPACTION);
+ if (iAction != STARTUP_ACTION_PLAY_TV && iAction != STARTUP_ACTION_PLAY_RADIO)
+ return false;
+
+ bool playRadio = (iAction == STARTUP_ACTION_PLAY_RADIO);
+
+ // get the last played channel or fallback to first channel of all channels group
+ std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetLastPlayedChannelGroupMember(playRadio);
+
+ if (!groupMember)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(playRadio)->GetGroupAll();
+ auto channels = group->GetMembers();
+ if (channels.empty())
+ return false;
+
+ groupMember = channels.front();
+ if (!groupMember)
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "PVR is starting playback of channel '{}'",
+ groupMember->Channel()->ChannelName());
+ return SwitchToChannel(CFileItem(groupMember), true);
+}
+
+bool CPVRGUIActionsPlayback::PlayMedia(const CFileItem& item) const
+{
+ std::unique_ptr<CFileItem> pvrItem = std::make_unique<CFileItem>(item);
+ if (URIUtils::IsPVRChannel(item.GetPath()) && !item.HasPVRChannelInfoTag())
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelGroupMemberByPath(
+ item.GetPath());
+ if (groupMember)
+ pvrItem = std::make_unique<CFileItem>(groupMember);
+ }
+ else if (URIUtils::IsPVRRecording(item.GetPath()) && !item.HasPVRRecordingInfoTag())
+ {
+ const std::shared_ptr<CPVRRecording> recording =
+ CServiceBroker::GetPVRManager().Recordings()->GetByPath(item.GetPath());
+ if (recording)
+ {
+ pvrItem = std::make_unique<CFileItem>(recording);
+ pvrItem->SetStartOffset(item.GetStartOffset());
+ }
+ }
+ bool bCheckResume = true;
+ if (item.HasProperty("check_resume"))
+ bCheckResume = item.GetProperty("check_resume").asBoolean();
+
+ if (pvrItem && pvrItem->HasPVRChannelInfoTag())
+ {
+ return SwitchToChannel(*pvrItem, bCheckResume);
+ }
+ else if (pvrItem && pvrItem->HasPVRRecordingInfoTag())
+ {
+ return PlayRecording(*pvrItem, bCheckResume);
+ }
+
+ return false;
+}
+
+void CPVRGUIActionsPlayback::SeekForward()
+{
+ time_t playbackStartTime = CServiceBroker::GetDataCacheCore().GetStartTime();
+ if (playbackStartTime > 0)
+ {
+ const std::shared_ptr<CPVRChannel> playingChannel =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (playingChannel)
+ {
+ time_t nextTime = 0;
+ std::shared_ptr<CPVREpgInfoTag> next = playingChannel->GetEPGNext();
+ if (next)
+ {
+ next->StartAsUTC().GetAsTime(nextTime);
+ }
+ else
+ {
+ // if there is no next event, jump to end of currently playing event
+ next = playingChannel->GetEPGNow();
+ if (next)
+ next->EndAsUTC().GetAsTime(nextTime);
+ }
+
+ int64_t seekTime = 0;
+ if (nextTime != 0)
+ {
+ seekTime = (nextTime - playbackStartTime) * 1000;
+ }
+ else
+ {
+ // no epg; jump to end of buffer
+ seekTime = CServiceBroker::GetDataCacheCore().GetMaxTime();
+ }
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_SEEK_TIME, seekTime);
+ }
+ }
+}
+
+void CPVRGUIActionsPlayback::SeekBackward(unsigned int iThreshold)
+{
+ time_t playbackStartTime = CServiceBroker::GetDataCacheCore().GetStartTime();
+ if (playbackStartTime > 0)
+ {
+ const std::shared_ptr<CPVRChannel> playingChannel =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (playingChannel)
+ {
+ time_t prevTime = 0;
+ std::shared_ptr<CPVREpgInfoTag> prev = playingChannel->GetEPGNow();
+ if (prev)
+ {
+ prev->StartAsUTC().GetAsTime(prevTime);
+
+ // if playback time of current event is above threshold jump to start of current event
+ int64_t playTime = CServiceBroker::GetDataCacheCore().GetPlayTime() / 1000;
+ if ((playbackStartTime + playTime - prevTime) <= iThreshold)
+ {
+ // jump to start of previous event
+ prevTime = 0;
+ prev = playingChannel->GetEPGPrevious();
+ if (prev)
+ prev->StartAsUTC().GetAsTime(prevTime);
+ }
+ }
+
+ int64_t seekTime = 0;
+ if (prevTime != 0)
+ {
+ seekTime = (prevTime - playbackStartTime) * 1000;
+ }
+ else
+ {
+ // no epg; jump to begin of buffer
+ seekTime = CServiceBroker::GetDataCacheCore().GetMinTime();
+ }
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_SEEK_TIME, seekTime);
+ }
+ }
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsPlayback.h b/xbmc/pvr/guilib/PVRGUIActionsPlayback.h
new file mode 100644
index 0000000..8b0ab55
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsPlayback.h
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/IPVRComponent.h"
+#include "pvr/settings/PVRSettings.h"
+
+#include <string>
+
+class CFileItem;
+
+namespace PVR
+{
+enum PlaybackType
+{
+ PlaybackTypeAny = 0,
+ PlaybackTypeTV,
+ PlaybackTypeRadio
+};
+
+class CPVRStreamProperties;
+
+class CPVRGUIActionsPlayback : public IPVRComponent
+{
+public:
+ CPVRGUIActionsPlayback();
+ ~CPVRGUIActionsPlayback() override = default;
+
+ /*!
+ * @brief Get a localized resume play label, if the given item can be resumed.
+ * @param item containing a recording or an epg tag.
+ * @return the localized resume play label that can be used for instance as context menu item
+ * label or an empty string if resume is not possible.
+ */
+ std::string GetResumeLabel(const CFileItem& item) const;
+
+ /*!
+ * @brief Resume a previously not completely played recording.
+ * @param item containing a recording or an epg tag.
+ * @param bFallbackToPlay controls whether playback of the recording should be started at the
+ * beginning ig no resume data are available.
+ * @return true on success, false otherwise.
+ */
+ bool ResumePlayRecording(const CFileItem& item, bool bFallbackToPlay) const;
+
+ /*!
+ * @brief Play recording.
+ * @param item containing a recording or an epg tag.
+ * @param bCheckResume controls resume check.
+ * @return true on success, false otherwise.
+ */
+ bool PlayRecording(const CFileItem& item, bool bCheckResume) const;
+
+ /*!
+ * @brief Play EPG tag.
+ * @param item containing an epg tag.
+ * @return true on success, false otherwise.
+ */
+ bool PlayEpgTag(const CFileItem& item) const;
+
+ /*!
+ * @brief Switch channel.
+ * @param item containing a channel or an epg tag.
+ * @param bCheckResume controls resume check in case a recording for the current epg event is
+ * present.
+ * @return true on success, false otherwise.
+ */
+ bool SwitchToChannel(const CFileItem& item, bool bCheckResume) const;
+
+ /*!
+ * @brief Playback the given file item.
+ * @param item containing a channel or a recording.
+ * @return True if the item could be played, false otherwise.
+ */
+ bool PlayMedia(const CFileItem& item) const;
+
+ /*!
+ * @brief Start playback of the last played channel, and if there is none, play first channel in
+ * the current channelgroup.
+ * @param type The type of playback to be started (any, radio, tv). See PlaybackType enum
+ * @return True if playback was started, false otherwise.
+ */
+ bool SwitchToChannel(PlaybackType type) const;
+
+ /*!
+ * @brief Plays the last played channel or the first channel of TV or Radio on startup.
+ * @return True if playback was started, false otherwise.
+ */
+ bool PlayChannelOnStartup() const;
+
+ /*!
+ * @brief Seek to the start of the next epg event in timeshift buffer, relative to the currently
+ * playing event. If there is no next event, seek to the end of the currently playing event (to
+ * the 'live' position).
+ */
+ void SeekForward();
+
+ /*!
+ * @brief Seek to the start of the previous epg event in timeshift buffer, relative to the
+ * currently playing event or if there is no previous event or if playback time is greater than
+ * given threshold, seek to the start of the playing event.
+ * @param iThreshold the value in seconds to trigger seek to start of current event instead of
+ * start of previous event.
+ */
+ void SeekBackward(unsigned int iThreshold);
+
+private:
+ CPVRGUIActionsPlayback(const CPVRGUIActionsPlayback&) = delete;
+ CPVRGUIActionsPlayback const& operator=(CPVRGUIActionsPlayback const&) = delete;
+
+ /*!
+ * @brief Check whether resume play is possible for a given item, display "resume from ..."/"play
+ * from start" context menu in case.
+ * @param item containing a recording or an epg tag.
+ * @return true, to play/resume the item, false otherwise.
+ */
+ bool CheckResumeRecording(const CFileItem& item) const;
+
+ /*!
+ * @brief Check "play minimized" settings value and switch to fullscreen if not set.
+ * @param bFullscreen switch to fullscreen or set windowed playback.
+ */
+ void CheckAndSwitchToFullscreen(bool bFullscreen) const;
+
+ /*!
+ * @brief Start playback of the given item.
+ * @param bFullscreen start playback fullscreen or not.
+ * @param epgProps properties to be used instead of calling to the client if supplied.
+ * @param item containing a channel or a recording.
+ */
+ void StartPlayback(CFileItem* item,
+ bool bFullscreen,
+ const CPVRStreamProperties* epgProps = nullptr) const;
+
+ CPVRSettings m_settings;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Playback = CPVRGUIActionsPlayback;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp
new file mode 100644
index 0000000..1a0a99a
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIActionsPowerManagement.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "network/Network.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/Settings.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+CPVRGUIActionsPowerManagement::CPVRGUIActionsPowerManagement()
+ : m_settings({CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME,
+ CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME})
+{
+}
+
+bool CPVRGUIActionsPowerManagement::CanSystemPowerdown(bool bAskUser /*= true*/) const
+{
+ bool bReturn(true);
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ std::shared_ptr<CPVRTimerInfoTag> cause;
+ if (!AllLocalBackendsIdle(cause))
+ {
+ if (bAskUser)
+ {
+ std::string text;
+
+ if (cause)
+ {
+ if (cause->IsRecording())
+ {
+ text = StringUtils::Format(
+ g_localizeStrings.Get(19691), // "PVR is currently recording...."
+ cause->Title(), cause->ChannelName());
+ }
+ else
+ {
+ // Next event is due to a local recording or reminder.
+ const CDateTime now(CDateTime::GetUTCDateTime());
+ const CDateTime start(cause->StartAsUTC());
+ const CDateTimeSpan prestart(0, 0, cause->MarginStart(), 0);
+
+ CDateTimeSpan diff(start - now);
+ diff -= prestart;
+ int mins = diff.GetSecondsTotal() / 60;
+
+ std::string dueStr;
+ if (mins > 1)
+ {
+ // "%d minutes"
+ dueStr = StringUtils::Format(g_localizeStrings.Get(19694), mins);
+ }
+ else
+ {
+ // "about a minute"
+ dueStr = g_localizeStrings.Get(19695);
+ }
+
+ text = StringUtils::Format(
+ cause->IsReminder()
+ ? g_localizeStrings.Get(19690) // "PVR has scheduled a reminder...."
+ : g_localizeStrings.Get(19692), // "PVR will start recording...."
+ cause->Title(), cause->ChannelName(), dueStr);
+ }
+ }
+ else
+ {
+ // Next event is due to automatic daily wakeup of PVR.
+ const CDateTime now(CDateTime::GetUTCDateTime());
+
+ CDateTime dailywakeuptime;
+ dailywakeuptime.SetFromDBTime(
+ m_settings.GetStringValue(CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME));
+ dailywakeuptime = dailywakeuptime.GetAsUTCDateTime();
+
+ const CDateTimeSpan diff(dailywakeuptime - now);
+ int mins = diff.GetSecondsTotal() / 60;
+
+ std::string dueStr;
+ if (mins > 1)
+ {
+ // "%d minutes"
+ dueStr = StringUtils::Format(g_localizeStrings.Get(19694), mins);
+ }
+ else
+ {
+ // "about a minute"
+ dueStr = g_localizeStrings.Get(19695);
+ }
+
+ text = StringUtils::Format(g_localizeStrings.Get(19693), // "Daily wakeup is due in...."
+ dueStr);
+ }
+
+ // Inform user about PVR being busy. Ask if user wants to powerdown anyway.
+ bReturn = HELPERS::ShowYesNoDialogText(CVariant{19685}, // "Confirm shutdown"
+ CVariant{text}, CVariant{222}, // "Shutdown anyway",
+ CVariant{19696}, // "Cancel"
+ 10000) // timeout value before closing
+ == HELPERS::DialogResponse::CHOICE_YES;
+ }
+ else
+ bReturn = false; // do not powerdown (busy, but no user interaction requested).
+ }
+ }
+ return bReturn;
+}
+
+bool CPVRGUIActionsPowerManagement::AllLocalBackendsIdle(
+ std::shared_ptr<CPVRTimerInfoTag>& causingEvent) const
+{
+ // active recording on local backend?
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>> activeRecordings =
+ CServiceBroker::GetPVRManager().Timers()->GetActiveRecordings();
+ for (const auto& timer : activeRecordings)
+ {
+ if (EventOccursOnLocalBackend(timer))
+ {
+ causingEvent = timer;
+ return false;
+ }
+ }
+
+ // soon recording on local backend?
+ if (IsNextEventWithinBackendIdleTime())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer =
+ CServiceBroker::GetPVRManager().Timers()->GetNextActiveTimer(false);
+ if (!timer)
+ {
+ // Next event is due to automatic daily wakeup of PVR!
+ causingEvent.reset();
+ return false;
+ }
+
+ if (EventOccursOnLocalBackend(timer))
+ {
+ causingEvent = timer;
+ return false;
+ }
+ }
+ return true;
+}
+
+bool CPVRGUIActionsPowerManagement::EventOccursOnLocalBackend(
+ const std::shared_ptr<CPVRTimerInfoTag>& event) const
+{
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(CFileItem(event));
+ if (client)
+ {
+ const std::string hostname = client->GetBackendHostname();
+ if (!hostname.empty() && CServiceBroker::GetNetwork().IsLocalHost(hostname))
+ return true;
+ }
+ return false;
+}
+
+bool CPVRGUIActionsPowerManagement::IsNextEventWithinBackendIdleTime() const
+{
+ // timers going off soon?
+ const CDateTime now(CDateTime::GetUTCDateTime());
+ const CDateTimeSpan idle(
+ 0, 0, m_settings.GetIntValue(CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME), 0);
+ const CDateTime next(CServiceBroker::GetPVRManager().Timers()->GetNextEventTime());
+ const CDateTimeSpan delta(next - now);
+
+ return (delta <= idle);
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h
new file mode 100644
index 0000000..bf99819
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/IPVRComponent.h"
+#include "pvr/settings/PVRSettings.h"
+
+#include <memory>
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRTimerInfoTag;
+
+class CPVRGUIActionsPowerManagement : public IPVRComponent
+{
+public:
+ CPVRGUIActionsPowerManagement();
+ ~CPVRGUIActionsPowerManagement() override = default;
+
+ /*!
+ * @brief Check whether the system Kodi is running on can be powered down
+ * (shutdown/reboot/suspend/hibernate) without stopping any active recordings and/or without
+ * preventing the start of recordings scheduled for now + pvrpowermanagement.backendidletime.
+ * @param bAskUser True to informs user in case of potential data loss. User can decide to allow
+ * powerdown anyway. False to not to ask user and to not confirm power down.
+ * @return True if system can be safely powered down, false otherwise.
+ */
+ bool CanSystemPowerdown(bool bAskUser = true) const;
+
+private:
+ CPVRGUIActionsPowerManagement(const CPVRGUIActionsPowerManagement&) = delete;
+ CPVRGUIActionsPowerManagement const& operator=(CPVRGUIActionsPowerManagement const&) = delete;
+
+ bool AllLocalBackendsIdle(std::shared_ptr<CPVRTimerInfoTag>& causingEvent) const;
+ bool EventOccursOnLocalBackend(const std::shared_ptr<CPVRTimerInfoTag>& event) const;
+ bool IsNextEventWithinBackendIdleTime() const;
+
+ CPVRSettings m_settings;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using PowerManagement = CPVRGUIActionsPowerManagement;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp b/xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp
new file mode 100644
index 0000000..df23f38
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIActionsRecordings.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/IDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/dialogs/GUIDialogPVRRecordingInfo.h"
+#include "pvr/dialogs/GUIDialogPVRRecordingSettings.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "settings/Settings.h"
+#include "threads/IRunnable.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <numeric>
+#include <string>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+namespace
+{
+class AsyncRecordingAction : private IRunnable
+{
+public:
+ bool Execute(const CFileItem& item);
+
+protected:
+ AsyncRecordingAction() = default;
+
+private:
+ // IRunnable implementation
+ void Run() override;
+
+ // the worker function
+ virtual bool DoRun(const std::shared_ptr<CFileItem>& item) = 0;
+
+ std::shared_ptr<CFileItem> m_item;
+ bool m_bSuccess = false;
+};
+
+bool AsyncRecordingAction::Execute(const CFileItem& item)
+{
+ m_item = std::make_shared<CFileItem>(item);
+ CGUIDialogBusy::Wait(this, 100, false);
+ return m_bSuccess;
+}
+
+void AsyncRecordingAction::Run()
+{
+ m_bSuccess = DoRun(m_item);
+
+ if (m_bSuccess)
+ CServiceBroker::GetPVRManager().TriggerRecordingsUpdate();
+}
+
+class AsyncRenameRecording : public AsyncRecordingAction
+{
+public:
+ explicit AsyncRenameRecording(const std::string& strNewName) : m_strNewName(strNewName) {}
+
+private:
+ bool DoRun(const std::shared_ptr<CFileItem>& item) override
+ {
+ if (item->IsUsablePVRRecording())
+ {
+ return item->GetPVRRecordingInfoTag()->Rename(m_strNewName);
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Cannot rename item '{}': no valid recording tag", item->GetPath());
+ return false;
+ }
+ }
+ std::string m_strNewName;
+};
+
+class AsyncDeleteRecording : public AsyncRecordingAction
+{
+public:
+ explicit AsyncDeleteRecording(bool bWatchedOnly = false) : m_bWatchedOnly(bWatchedOnly) {}
+
+private:
+ bool DoRun(const std::shared_ptr<CFileItem>& item) override
+ {
+ CFileItemList items;
+ if (item->m_bIsFolder)
+ {
+ CUtil::GetRecursiveListing(item->GetPath(), items, "", XFILE::DIR_FLAG_NO_FILE_INFO);
+ }
+ else
+ {
+ items.Add(item);
+ }
+
+ return std::accumulate(
+ items.cbegin(), items.cend(), true, [this](bool success, const auto& itemToDelete) {
+ return (itemToDelete->IsPVRRecording() &&
+ (!m_bWatchedOnly || itemToDelete->GetPVRRecordingInfoTag()->GetPlayCount() > 0) &&
+ !itemToDelete->GetPVRRecordingInfoTag()->Delete())
+ ? false
+ : success;
+ });
+ }
+ bool m_bWatchedOnly = false;
+};
+
+class AsyncEmptyRecordingsTrash : public AsyncRecordingAction
+{
+private:
+ bool DoRun(const std::shared_ptr<CFileItem>& item) override
+ {
+ return CServiceBroker::GetPVRManager().Clients()->DeleteAllRecordingsFromTrash() ==
+ PVR_ERROR_NO_ERROR;
+ }
+};
+
+class AsyncUndeleteRecording : public AsyncRecordingAction
+{
+private:
+ bool DoRun(const std::shared_ptr<CFileItem>& item) override
+ {
+ if (item->IsDeletedPVRRecording())
+ {
+ return item->GetPVRRecordingInfoTag()->Undelete();
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Cannot undelete item '{}': no valid recording tag", item->GetPath());
+ return false;
+ }
+ }
+};
+
+class AsyncSetRecordingPlayCount : public AsyncRecordingAction
+{
+private:
+ bool DoRun(const std::shared_ptr<CFileItem>& item) override
+ {
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item);
+ if (client)
+ {
+ const std::shared_ptr<CPVRRecording> recording = item->GetPVRRecordingInfoTag();
+ return client->SetRecordingPlayCount(*recording, recording->GetLocalPlayCount()) ==
+ PVR_ERROR_NO_ERROR;
+ }
+ return false;
+ }
+};
+
+class AsyncSetRecordingLifetime : public AsyncRecordingAction
+{
+private:
+ bool DoRun(const std::shared_ptr<CFileItem>& item) override
+ {
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item);
+ if (client)
+ return client->SetRecordingLifetime(*item->GetPVRRecordingInfoTag()) == PVR_ERROR_NO_ERROR;
+ return false;
+ }
+};
+
+} // unnamed namespace
+
+bool CPVRGUIActionsRecordings::ShowRecordingInfo(const CFileItem& item) const
+{
+ if (!item.IsPVRRecording())
+ {
+ CLog::LogF(LOGERROR, "No recording!");
+ return false;
+ }
+
+ CGUIDialogPVRRecordingInfo* pDlgInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRRecordingInfo>(
+ WINDOW_DIALOG_PVR_RECORDING_INFO);
+ if (!pDlgInfo)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_RECORDING_INFO!");
+ return false;
+ }
+
+ pDlgInfo->SetRecording(item);
+ pDlgInfo->Open();
+ return true;
+}
+
+bool CPVRGUIActionsRecordings::EditRecording(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRRecording> recording = CPVRItem(item).GetRecording();
+ if (!recording)
+ {
+ CLog::LogF(LOGERROR, "No recording!");
+ return false;
+ }
+
+ std::shared_ptr<CPVRRecording> origRecording(new CPVRRecording);
+ origRecording->Update(*recording,
+ *CServiceBroker::GetPVRManager().GetClient(recording->ClientID()));
+
+ if (!ShowRecordingSettings(recording))
+ return false;
+
+ if (origRecording->m_strTitle != recording->m_strTitle)
+ {
+ if (!AsyncRenameRecording(recording->m_strTitle).Execute(item))
+ CLog::LogF(LOGERROR, "Renaming recording failed!");
+ }
+
+ if (origRecording->GetLocalPlayCount() != recording->GetLocalPlayCount())
+ {
+ if (!AsyncSetRecordingPlayCount().Execute(item))
+ CLog::LogF(LOGERROR, "Setting recording playcount failed!");
+ }
+
+ if (origRecording->LifeTime() != recording->LifeTime())
+ {
+ if (!AsyncSetRecordingLifetime().Execute(item))
+ CLog::LogF(LOGERROR, "Setting recording lifetime failed!");
+ }
+
+ return true;
+}
+
+bool CPVRGUIActionsRecordings::CanEditRecording(const CFileItem& item) const
+{
+ return CGUIDialogPVRRecordingSettings::CanEditRecording(item);
+}
+
+bool CPVRGUIActionsRecordings::DeleteRecording(const CFileItem& item) const
+{
+ if ((!item.IsPVRRecording() && !item.m_bIsFolder) || item.IsParentFolder())
+ return false;
+
+ if (!ConfirmDeleteRecording(item))
+ return false;
+
+ if (!AsyncDeleteRecording().Execute(item))
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19111}); // "Error", "PVR backend error. Check the log for more information about this message."
+ return false;
+ }
+
+ return true;
+}
+
+bool CPVRGUIActionsRecordings::ConfirmDeleteRecording(const CFileItem& item) const
+{
+ return CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{122}, // "Confirm delete"
+ item.m_bIsFolder
+ ? CVariant{19113} // "Delete all recordings in this folder?"
+ : item.GetPVRRecordingInfoTag()->IsDeleted()
+ ? CVariant{19294}
+ // "Remove this deleted recording from trash? This operation cannot be reverted."
+ : CVariant{19112}, // "Delete this recording?"
+ CVariant{""}, CVariant{item.GetLabel()});
+}
+
+bool CPVRGUIActionsRecordings::DeleteWatchedRecordings(const CFileItem& item) const
+{
+ if (!item.m_bIsFolder || item.IsParentFolder())
+ return false;
+
+ if (!ConfirmDeleteWatchedRecordings(item))
+ return false;
+
+ if (!AsyncDeleteRecording(true).Execute(item))
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19111}); // "Error", "PVR backend error. Check the log for more information about this message."
+ return false;
+ }
+
+ return true;
+}
+
+bool CPVRGUIActionsRecordings::ConfirmDeleteWatchedRecordings(const CFileItem& item) const
+{
+ return CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{122}, // "Confirm delete"
+ CVariant{19328}, // "Delete all watched recordings in this folder?"
+ CVariant{""}, CVariant{item.GetLabel()});
+}
+
+bool CPVRGUIActionsRecordings::DeleteAllRecordingsFromTrash() const
+{
+ if (!ConfirmDeleteAllRecordingsFromTrash())
+ return false;
+
+ if (!AsyncEmptyRecordingsTrash().Execute({}))
+ return false;
+
+ return true;
+}
+
+bool CPVRGUIActionsRecordings::ConfirmDeleteAllRecordingsFromTrash() const
+{
+ return CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{19292}, // "Delete all permanently"
+ CVariant{
+ 19293}); // "Remove all deleted recordings from trash? This operation cannot be reverted."
+}
+
+bool CPVRGUIActionsRecordings::UndeleteRecording(const CFileItem& item) const
+{
+ if (!item.IsDeletedPVRRecording())
+ return false;
+
+ if (!AsyncUndeleteRecording().Execute(item))
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19111}); // "Error", "PVR backend error. Check the log for more information about this message."
+ return false;
+ }
+
+ return true;
+}
+
+bool CPVRGUIActionsRecordings::ShowRecordingSettings(
+ const std::shared_ptr<CPVRRecording>& recording) const
+{
+ CGUIDialogPVRRecordingSettings* pDlgInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRRecordingSettings>(
+ WINDOW_DIALOG_PVR_RECORDING_SETTING);
+ if (!pDlgInfo)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_RECORDING_SETTING!");
+ return false;
+ }
+
+ pDlgInfo->SetRecording(recording);
+ pDlgInfo->Open();
+
+ return pDlgInfo->IsConfirmed();
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsRecordings.h b/xbmc/pvr/guilib/PVRGUIActionsRecordings.h
new file mode 100644
index 0000000..795a2c7
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsRecordings.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/IPVRComponent.h"
+
+#include <memory>
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRRecording;
+
+class CPVRGUIActionsRecordings : public IPVRComponent
+{
+public:
+ CPVRGUIActionsRecordings() = default;
+ ~CPVRGUIActionsRecordings() override = default;
+
+ /*!
+ * @brief Open a dialog with information for a given recording.
+ * @param item containing a recording.
+ * @return true on success, false otherwise.
+ */
+ bool ShowRecordingInfo(const CFileItem& item) const;
+
+ /*!
+ * @brief Open the recording settings dialog to edit a recording.
+ * @param item containing the recording to edit.
+ * @return true on success, false otherwise.
+ */
+ bool EditRecording(const CFileItem& item) const;
+
+ /*!
+ * @brief Check if any recording settings can be edited.
+ * @param item containing the recording to edit.
+ * @return true on success, false otherwise.
+ */
+ bool CanEditRecording(const CFileItem& item) const;
+
+ /*!
+ * @brief Delete a recording, always showing a confirmation dialog.
+ * @param item containing a recording to delete.
+ * @return true, if the recording was deleted successfully, false otherwise.
+ */
+ bool DeleteRecording(const CFileItem& item) const;
+
+ /*!
+ * @brief Delete all watched recordings contained in the given folder, always showing a
+ * confirmation dialog.
+ * @param item containing a recording folder containing the items to delete.
+ * @return true, if the recordings were deleted successfully, false otherwise.
+ */
+ bool DeleteWatchedRecordings(const CFileItem& item) const;
+
+ /*!
+ * @brief Delete all recordings from trash, always showing a confirmation dialog.
+ * @return true, if the recordings were permanently deleted successfully, false otherwise.
+ */
+ bool DeleteAllRecordingsFromTrash() const;
+
+ /*!
+ * @brief Undelete a recording.
+ * @param item containing a recording to undelete.
+ * @return true, if the recording was undeleted successfully, false otherwise.
+ */
+ bool UndeleteRecording(const CFileItem& item) const;
+
+private:
+ CPVRGUIActionsRecordings(const CPVRGUIActionsRecordings&) = delete;
+ CPVRGUIActionsRecordings const& operator=(CPVRGUIActionsRecordings const&) = delete;
+
+ /*!
+ * @brief Open a dialog to confirm to delete a recording.
+ * @param item the recording to delete.
+ * @return true, to proceed with delete, false otherwise.
+ */
+ bool ConfirmDeleteRecording(const CFileItem& item) const;
+
+ /*!
+ * @brief Open a dialog to confirm delete all watched recordings contained in the given folder.
+ * @param item containing a recording folder containing the items to delete.
+ * @return true, to proceed with delete, false otherwise.
+ */
+ bool ConfirmDeleteWatchedRecordings(const CFileItem& item) const;
+
+ /*!
+ * @brief Open a dialog to confirm to permanently remove all deleted recordings.
+ * @return true, to proceed with delete, false otherwise.
+ */
+ bool ConfirmDeleteAllRecordingsFromTrash() const;
+
+ /*!
+ * @brief Open the recording settings dialog.
+ * @param recording containing the recording the settings shall be displayed for.
+ * @return true, if the dialog was ended successfully, false otherwise.
+ */
+ bool ShowRecordingSettings(const std::shared_ptr<CPVRRecording>& recording) const;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Recordings = CPVRGUIActionsRecordings;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsTimers.cpp b/xbmc/pvr/guilib/PVRGUIActionsTimers.cpp
new file mode 100644
index 0000000..be8e313
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsTimers.cpp
@@ -0,0 +1,1009 @@
+/*
+ * Copyright (C) 2016-2022 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIActionsTimers.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVREventLogJob.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/dialogs/GUIDialogPVRTimerSettings.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/Settings.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <map>
+#include <memory>
+#include <string>
+#include <thread>
+#include <utility>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+CPVRGUIActionsTimers::CPVRGUIActionsTimers()
+ : m_settings({CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME,
+ CSettings::SETTING_PVRRECORD_INSTANTRECORDACTION,
+ CSettings::SETTING_PVRREMINDERS_AUTOCLOSEDELAY,
+ CSettings::SETTING_PVRREMINDERS_AUTORECORD,
+ CSettings::SETTING_PVRREMINDERS_AUTOSWITCH})
+{
+}
+
+bool CPVRGUIActionsTimers::ShowTimerSettings(const std::shared_ptr<CPVRTimerInfoTag>& timer) const
+{
+ CGUIDialogPVRTimerSettings* pDlgInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRTimerSettings>(
+ WINDOW_DIALOG_PVR_TIMER_SETTING);
+ if (!pDlgInfo)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_TIMER_SETTING!");
+ return false;
+ }
+
+ pDlgInfo->SetTimer(timer);
+ pDlgInfo->Open();
+
+ return pDlgInfo->IsConfirmed();
+}
+
+bool CPVRGUIActionsTimers::AddReminder(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag();
+ if (!epgTag)
+ {
+ CLog::LogF(LOGERROR, "No epg tag!");
+ return false;
+ }
+
+ if (CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epgTag))
+ {
+ HELPERS::ShowOKDialogText(CVariant{19033}, // "Information"
+ CVariant{19034}); // "There is already a timer set for this event"
+ return false;
+ }
+
+ const std::shared_ptr<CPVRTimerInfoTag> newTimer =
+ CPVRTimerInfoTag::CreateReminderFromEpg(epgTag);
+ if (!newTimer)
+ {
+ HELPERS::ShowOKDialogText(CVariant{19033}, // "Information"
+ CVariant{19094}); // Timer creation failed. Unsupported timer type.
+ return false;
+ }
+
+ return AddTimer(newTimer);
+}
+
+bool CPVRGUIActionsTimers::AddTimer(bool bRadio) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> newTimer(new CPVRTimerInfoTag(bRadio));
+ if (ShowTimerSettings(newTimer))
+ {
+ return AddTimer(newTimer);
+ }
+ return false;
+}
+
+bool CPVRGUIActionsTimers::AddTimer(const CFileItem& item, bool bShowTimerSettings) const
+{
+ return AddTimer(item, false, bShowTimerSettings, false);
+}
+
+bool CPVRGUIActionsTimers::AddTimerRule(const CFileItem& item,
+ bool bShowTimerSettings,
+ bool bFallbackToOneShotTimer) const
+{
+ return AddTimer(item, true, bShowTimerSettings, bFallbackToOneShotTimer);
+}
+
+bool CPVRGUIActionsTimers::AddTimer(const CFileItem& item,
+ bool bCreateRule,
+ bool bShowTimerSettings,
+ bool bFallbackToOneShotTimer) const
+{
+ const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel());
+ if (!channel)
+ {
+ CLog::LogF(LOGERROR, "No channel!");
+ return false;
+ }
+
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(channel) !=
+ ParentalCheckResult::SUCCESS)
+ return false;
+
+ std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag();
+ if (epgTag)
+ {
+ if (epgTag->IsGapTag())
+ epgTag.reset(); // for gap tags, we can only create instant timers
+ }
+ else if (bCreateRule)
+ {
+ CLog::LogF(LOGERROR, "No epg tag!");
+ return false;
+ }
+
+ std::shared_ptr<CPVRTimerInfoTag> timer(
+ bCreateRule || !epgTag ? nullptr
+ : CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epgTag));
+ std::shared_ptr<CPVRTimerInfoTag> rule(
+ bCreateRule ? CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer) : nullptr);
+ if (timer || rule)
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{19033},
+ CVariant{19034}); // "Information", "There is already a timer set for this event"
+ return false;
+ }
+
+ std::shared_ptr<CPVRTimerInfoTag> newTimer(
+ epgTag ? CPVRTimerInfoTag::CreateFromEpg(epgTag, bCreateRule)
+ : CPVRTimerInfoTag::CreateInstantTimerTag(channel));
+ if (!newTimer)
+ {
+ if (bCreateRule && bFallbackToOneShotTimer)
+ newTimer = CPVRTimerInfoTag::CreateFromEpg(epgTag, false);
+
+ if (!newTimer)
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{19033}, // "Information"
+ bCreateRule ? CVariant{19095} // Timer rule creation failed. Unsupported timer type.
+ : CVariant{19094}); // Timer creation failed. Unsupported timer type.
+ return false;
+ }
+ }
+
+ if (bShowTimerSettings)
+ {
+ if (!ShowTimerSettings(newTimer))
+ return false;
+ }
+
+ return AddTimer(newTimer);
+}
+
+bool CPVRGUIActionsTimers::AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& item) const
+{
+ if (!item->Channel() && !item->GetTimerType()->IsEpgBasedTimerRule())
+ {
+ CLog::LogF(LOGERROR, "No channel given");
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19109}); // "Error", "Could not save the timer. Check the log for more information about this message."
+ return false;
+ }
+
+ if (!item->IsTimerRule() && item->GetEpgInfoTag() && !item->GetEpgInfoTag()->IsRecordable())
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{19033},
+ CVariant{19189}); // "Information", "The PVR backend does not allow to record this event."
+ return false;
+ }
+
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(
+ item->Channel()) != ParentalCheckResult::SUCCESS)
+ return false;
+
+ if (!CServiceBroker::GetPVRManager().Timers()->AddTimer(item))
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19109}); // "Error", "Could not save the timer. Check the log for more information about this message."
+ return false;
+ }
+
+ return true;
+}
+
+namespace
+{
+enum PVRRECORD_INSTANTRECORDACTION
+{
+ NONE = -1,
+ RECORD_CURRENT_SHOW = 0,
+ RECORD_INSTANTRECORDTIME = 1,
+ ASK = 2,
+ RECORD_30_MINUTES = 3,
+ RECORD_60_MINUTES = 4,
+ RECORD_120_MINUTES = 5,
+ RECORD_NEXT_SHOW = 6
+};
+
+class InstantRecordingActionSelector
+{
+public:
+ explicit InstantRecordingActionSelector(int iInstantRecordTime);
+ virtual ~InstantRecordingActionSelector() = default;
+
+ void AddAction(PVRRECORD_INSTANTRECORDACTION eAction, const std::string& title);
+ void PreSelectAction(PVRRECORD_INSTANTRECORDACTION eAction);
+ PVRRECORD_INSTANTRECORDACTION Select();
+
+private:
+ int m_iInstantRecordTime;
+ CGUIDialogSelect* m_pDlgSelect; // not owner!
+ std::map<PVRRECORD_INSTANTRECORDACTION, int> m_actions;
+};
+
+InstantRecordingActionSelector::InstantRecordingActionSelector(int iInstantRecordTime)
+ : m_iInstantRecordTime(iInstantRecordTime),
+ m_pDlgSelect(CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT))
+{
+ if (m_pDlgSelect)
+ {
+ m_pDlgSelect->Reset();
+ m_pDlgSelect->SetMultiSelection(false);
+ m_pDlgSelect->SetHeading(CVariant{19086}); // Instant recording action
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to obtain WINDOW_DIALOG_SELECT instance");
+ }
+}
+
+void InstantRecordingActionSelector::AddAction(PVRRECORD_INSTANTRECORDACTION eAction,
+ const std::string& title)
+{
+ if (m_actions.find(eAction) == m_actions.end())
+ {
+ switch (eAction)
+ {
+ case RECORD_INSTANTRECORDTIME:
+ m_pDlgSelect->Add(
+ StringUtils::Format(g_localizeStrings.Get(19090),
+ m_iInstantRecordTime)); // Record next <default duration> minutes
+ break;
+ case RECORD_30_MINUTES:
+ m_pDlgSelect->Add(
+ StringUtils::Format(g_localizeStrings.Get(19090), 30)); // Record next 30 minutes
+ break;
+ case RECORD_60_MINUTES:
+ m_pDlgSelect->Add(
+ StringUtils::Format(g_localizeStrings.Get(19090), 60)); // Record next 60 minutes
+ break;
+ case RECORD_120_MINUTES:
+ m_pDlgSelect->Add(
+ StringUtils::Format(g_localizeStrings.Get(19090), 120)); // Record next 120 minutes
+ break;
+ case RECORD_CURRENT_SHOW:
+ m_pDlgSelect->Add(StringUtils::Format(g_localizeStrings.Get(19091),
+ title)); // Record current show (<title>)
+ break;
+ case RECORD_NEXT_SHOW:
+ m_pDlgSelect->Add(StringUtils::Format(g_localizeStrings.Get(19092),
+ title)); // Record next show (<title>)
+ break;
+ case NONE:
+ case ASK:
+ default:
+ return;
+ }
+
+ m_actions.insert(std::make_pair(eAction, static_cast<int>(m_actions.size())));
+ }
+}
+
+void InstantRecordingActionSelector::PreSelectAction(PVRRECORD_INSTANTRECORDACTION eAction)
+{
+ const auto& it = m_actions.find(eAction);
+ if (it != m_actions.end())
+ m_pDlgSelect->SetSelected(it->second);
+}
+
+PVRRECORD_INSTANTRECORDACTION InstantRecordingActionSelector::Select()
+{
+ PVRRECORD_INSTANTRECORDACTION eAction = NONE;
+
+ m_pDlgSelect->Open();
+
+ if (m_pDlgSelect->IsConfirmed())
+ {
+ int iSelection = m_pDlgSelect->GetSelectedItem();
+ const auto it =
+ std::find_if(m_actions.cbegin(), m_actions.cend(),
+ [iSelection](const auto& action) { return action.second == iSelection; });
+
+ if (it != m_actions.cend())
+ eAction = (*it).first;
+ }
+
+ return eAction;
+}
+
+} // unnamed namespace
+
+bool CPVRGUIActionsTimers::ToggleRecordingOnPlayingChannel()
+{
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (channel && channel->CanRecord())
+ return SetRecordingOnChannel(
+ channel, !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel));
+
+ return false;
+}
+
+bool CPVRGUIActionsTimers::SetRecordingOnChannel(const std::shared_ptr<CPVRChannel>& channel,
+ bool bOnOff)
+{
+ bool bReturn = false;
+
+ if (!channel)
+ return bReturn;
+
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(channel) !=
+ ParentalCheckResult::SUCCESS)
+ return bReturn;
+
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(channel->ClientID());
+ if (client && client->GetClientCapabilities().SupportsTimers())
+ {
+ /* timers are supported on this channel */
+ if (bOnOff && !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel))
+ {
+ std::shared_ptr<CPVREpgInfoTag> epgTag;
+ int iDuration = m_settings.GetIntValue(CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME);
+
+ int iAction = m_settings.GetIntValue(CSettings::SETTING_PVRRECORD_INSTANTRECORDACTION);
+ switch (iAction)
+ {
+ case RECORD_CURRENT_SHOW:
+ epgTag = channel->GetEPGNow();
+ break;
+
+ case RECORD_INSTANTRECORDTIME:
+ epgTag.reset();
+ break;
+
+ case ASK:
+ {
+ PVRRECORD_INSTANTRECORDACTION ePreselect = RECORD_INSTANTRECORDTIME;
+ const int iDurationDefault =
+ m_settings.GetIntValue(CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME);
+ InstantRecordingActionSelector selector(iDurationDefault);
+ std::shared_ptr<CPVREpgInfoTag> epgTagNext;
+
+ // fixed length recordings
+ selector.AddAction(RECORD_30_MINUTES, "");
+ selector.AddAction(RECORD_60_MINUTES, "");
+ selector.AddAction(RECORD_120_MINUTES, "");
+
+ if (iDurationDefault != 30 && iDurationDefault != 60 && iDurationDefault != 120)
+ selector.AddAction(RECORD_INSTANTRECORDTIME, "");
+
+ // epg-based recordings
+ epgTag = channel->GetEPGNow();
+ if (epgTag)
+ {
+ bool bLocked = CServiceBroker::GetPVRManager().IsParentalLocked(epgTag);
+
+ // "now"
+ const std::string currentTitle =
+ bLocked ? g_localizeStrings.Get(19266) /* Parental locked */ : epgTag->Title();
+ selector.AddAction(RECORD_CURRENT_SHOW, currentTitle);
+ ePreselect = RECORD_CURRENT_SHOW;
+
+ // "next"
+ epgTagNext = channel->GetEPGNext();
+ if (epgTagNext)
+ {
+ const std::string nextTitle = bLocked
+ ? g_localizeStrings.Get(19266) /* Parental locked */
+ : epgTagNext->Title();
+ selector.AddAction(RECORD_NEXT_SHOW, nextTitle);
+
+ // be smart. if current show is almost over, preselect next show.
+ if (epgTag->ProgressPercentage() > 90.0f)
+ ePreselect = RECORD_NEXT_SHOW;
+ }
+ }
+
+ if (ePreselect == RECORD_INSTANTRECORDTIME)
+ {
+ if (iDurationDefault == 30)
+ ePreselect = RECORD_30_MINUTES;
+ else if (iDurationDefault == 60)
+ ePreselect = RECORD_60_MINUTES;
+ else if (iDurationDefault == 120)
+ ePreselect = RECORD_120_MINUTES;
+ }
+
+ selector.PreSelectAction(ePreselect);
+
+ PVRRECORD_INSTANTRECORDACTION eSelected = selector.Select();
+ switch (eSelected)
+ {
+ case NONE:
+ return false; // dialog canceled
+
+ case RECORD_30_MINUTES:
+ iDuration = 30;
+ epgTag.reset();
+ break;
+
+ case RECORD_60_MINUTES:
+ iDuration = 60;
+ epgTag.reset();
+ break;
+
+ case RECORD_120_MINUTES:
+ iDuration = 120;
+ epgTag.reset();
+ break;
+
+ case RECORD_INSTANTRECORDTIME:
+ iDuration = iDurationDefault;
+ epgTag.reset();
+ break;
+
+ case RECORD_CURRENT_SHOW:
+ break;
+
+ case RECORD_NEXT_SHOW:
+ epgTag = epgTagNext;
+ break;
+
+ default:
+ CLog::LogF(LOGERROR,
+ "Unknown instant record action selection ({}), defaulting to fixed "
+ "length recording.",
+ static_cast<int>(eSelected));
+ epgTag.reset();
+ break;
+ }
+ break;
+ }
+
+ default:
+ CLog::LogF(LOGERROR,
+ "Unknown instant record action setting value ({}), defaulting to fixed "
+ "length recording.",
+ iAction);
+ break;
+ }
+
+ const std::shared_ptr<CPVRTimerInfoTag> newTimer(
+ epgTag ? CPVRTimerInfoTag::CreateFromEpg(epgTag, false)
+ : CPVRTimerInfoTag::CreateInstantTimerTag(channel, iDuration));
+
+ if (newTimer)
+ bReturn = CServiceBroker::GetPVRManager().Timers()->AddTimer(newTimer);
+
+ if (!bReturn)
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19164}); // "Error", "Could not start recording. Check the log for more information about this message."
+ }
+ else if (!bOnOff && CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel))
+ {
+ /* delete active timers */
+ bReturn =
+ CServiceBroker::GetPVRManager().Timers()->DeleteTimersOnChannel(channel, true, true);
+
+ if (!bReturn)
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19170}); // "Error", "Could not stop recording. Check the log for more information about this message."
+ }
+ }
+
+ return bReturn;
+}
+
+bool CPVRGUIActionsTimers::ToggleTimer(const CFileItem& item) const
+{
+ if (!item.HasEPGInfoTag())
+ return false;
+
+ const std::shared_ptr<CPVRTimerInfoTag> timer(CPVRItem(item).GetTimerInfoTag());
+ if (timer)
+ {
+ if (timer->IsRecording())
+ return StopRecording(item);
+ else
+ return DeleteTimer(item);
+ }
+ else
+ return AddTimer(item, false);
+}
+
+bool CPVRGUIActionsTimers::ToggleTimerState(const CFileItem& item) const
+{
+ if (!item.HasPVRTimerInfoTag())
+ return false;
+
+ const std::shared_ptr<CPVRTimerInfoTag> timer = item.GetPVRTimerInfoTag();
+ if (timer->IsDisabled())
+ timer->SetState(PVR_TIMER_STATE_SCHEDULED);
+ else
+ timer->SetState(PVR_TIMER_STATE_DISABLED);
+
+ if (CServiceBroker::GetPVRManager().Timers()->UpdateTimer(timer))
+ return true;
+
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19263}); // "Error", "Could not update the timer. Check the log for more information about this message."
+ return false;
+}
+
+bool CPVRGUIActionsTimers::EditTimer(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(CPVRItem(item).GetTimerInfoTag());
+ if (!timer)
+ {
+ CLog::LogF(LOGERROR, "No timer!");
+ return false;
+ }
+
+ // clone the timer.
+ const std::shared_ptr<CPVRTimerInfoTag> newTimer(new CPVRTimerInfoTag);
+ newTimer->UpdateEntry(timer);
+
+ if (ShowTimerSettings(newTimer) &&
+ (!timer->GetTimerType()->IsReadOnly() || timer->GetTimerType()->SupportsEnableDisable()))
+ {
+ if (newTimer->GetTimerType() == timer->GetTimerType())
+ {
+ if (CServiceBroker::GetPVRManager().Timers()->UpdateTimer(newTimer))
+ return true;
+
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19263}); // "Error", "Could not update the timer. Check the log for more information about this message."
+ return false;
+ }
+ else
+ {
+ // timer type changed. delete the original timer, then create the new timer. this order is
+ // important. for instance, the new timer might be a rule which schedules the original timer.
+ // deleting the original timer after creating the rule would do literally this and we would
+ // end up with one timer missing wrt to the rule defined by the new timer.
+ if (DeleteTimer(timer, timer->IsRecording(), false))
+ {
+ if (AddTimer(newTimer))
+ return true;
+
+ // rollback.
+ return AddTimer(timer);
+ }
+ }
+ }
+ return false;
+}
+
+bool CPVRGUIActionsTimers::EditTimerRule(const CFileItem& item) const
+{
+ const std::shared_ptr<CFileItem> parentTimer = GetTimerRule(item);
+ if (parentTimer)
+ return EditTimer(*parentTimer);
+
+ return false;
+}
+
+std::shared_ptr<CFileItem> CPVRGUIActionsTimers::GetTimerRule(const CFileItem& item) const
+{
+ std::shared_ptr<CPVRTimerInfoTag> timer;
+ if (item.HasEPGInfoTag())
+ timer = CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(item.GetEPGInfoTag());
+ else if (item.HasPVRTimerInfoTag())
+ timer = item.GetPVRTimerInfoTag();
+
+ if (timer)
+ {
+ timer = CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer);
+ if (timer)
+ return std::make_shared<CFileItem>(timer);
+ }
+ return {};
+}
+
+bool CPVRGUIActionsTimers::DeleteTimer(const CFileItem& item) const
+{
+ return DeleteTimer(item, false, false);
+}
+
+bool CPVRGUIActionsTimers::DeleteTimerRule(const CFileItem& item) const
+{
+ return DeleteTimer(item, false, true);
+}
+
+bool CPVRGUIActionsTimers::DeleteTimer(const CFileItem& item,
+ bool bIsRecording,
+ bool bDeleteRule) const
+{
+ std::shared_ptr<CPVRTimerInfoTag> timer;
+ const std::shared_ptr<CPVRRecording> recording(CPVRItem(item).GetRecording());
+ if (recording)
+ timer = recording->GetRecordingTimer();
+
+ if (!timer)
+ timer = CPVRItem(item).GetTimerInfoTag();
+
+ if (!timer)
+ {
+ CLog::LogF(LOGERROR, "No timer!");
+ return false;
+ }
+
+ if (bDeleteRule && !timer->IsTimerRule())
+ timer = CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer);
+
+ if (!timer)
+ {
+ CLog::LogF(LOGERROR, "No timer rule!");
+ return false;
+ }
+
+ if (bIsRecording)
+ {
+ if (ConfirmStopRecording(timer))
+ {
+ if (CServiceBroker::GetPVRManager().Timers()->DeleteTimer(timer, true, false) ==
+ TimerOperationResult::OK)
+ return true;
+
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19170}); // "Error", "Could not stop recording. Check the log for more information about this message."
+ return false;
+ }
+ }
+ else if (!timer->GetTimerType()->AllowsDelete())
+ {
+ return false;
+ }
+ else
+ {
+ bool bAlsoDeleteRule(false);
+ if (ConfirmDeleteTimer(timer, bAlsoDeleteRule))
+ return DeleteTimer(timer, false, bAlsoDeleteRule);
+ }
+ return false;
+}
+
+bool CPVRGUIActionsTimers::DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer,
+ bool bIsRecording,
+ bool bDeleteRule) const
+{
+ TimerOperationResult result =
+ CServiceBroker::GetPVRManager().Timers()->DeleteTimer(timer, bIsRecording, bDeleteRule);
+ switch (result)
+ {
+ case TimerOperationResult::RECORDING:
+ {
+ // recording running. ask the user if it should be deleted anyway
+ if (HELPERS::ShowYesNoDialogText(
+ CVariant{122}, // "Confirm delete"
+ CVariant{
+ 19122}) // "This timer is still recording. Are you sure you want to delete this timer?"
+ != HELPERS::DialogResponse::CHOICE_YES)
+ return false;
+
+ return DeleteTimer(timer, true, bDeleteRule);
+ }
+ case TimerOperationResult::OK:
+ {
+ return true;
+ }
+ case TimerOperationResult::FAILED:
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19110}); // "Error", "Could not delete the timer. Check the log for more information about this message."
+ return false;
+ }
+ default:
+ {
+ CLog::LogF(LOGERROR, "Unhandled TimerOperationResult ({})!", static_cast<int>(result));
+ break;
+ }
+ }
+ return false;
+}
+
+bool CPVRGUIActionsTimers::ConfirmDeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer,
+ bool& bDeleteRule) const
+{
+ bool bConfirmed(false);
+ const std::shared_ptr<CPVRTimerInfoTag> parentTimer(
+ CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer));
+
+ if (parentTimer && parentTimer->GetTimerType()->AllowsDelete())
+ {
+ // timer was scheduled by a deletable timer rule. prompt user for confirmation for deleting the timer rule, including scheduled timers.
+ bool bCancel(false);
+ bDeleteRule = CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{122}, // "Confirm delete"
+ CVariant{
+ 840}, // "Do you want to delete only this timer or also the timer rule that has scheduled it?"
+ CVariant{""}, CVariant{timer->Title()}, bCancel, CVariant{841}, // "Only this"
+ CVariant{593}, // "All"
+ 0); // no autoclose
+ bConfirmed = !bCancel;
+ }
+ else
+ {
+ bDeleteRule = false;
+
+ // prompt user for confirmation for deleting the timer
+ bConfirmed = CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{122}, // "Confirm delete"
+ timer->IsTimerRule()
+ ? CVariant{845}
+ // "Are you sure you want to delete this timer rule and all timers it has scheduled?"
+ : CVariant{846}, // "Are you sure you want to delete this timer?"
+ CVariant{""}, CVariant{timer->Title()});
+ }
+
+ return bConfirmed;
+}
+
+bool CPVRGUIActionsTimers::StopRecording(const CFileItem& item) const
+{
+ if (!DeleteTimer(item, true, false))
+ return false;
+
+ CServiceBroker::GetPVRManager().TriggerRecordingsUpdate();
+ return true;
+}
+
+bool CPVRGUIActionsTimers::ConfirmStopRecording(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer) const
+{
+ return CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{847}, // "Confirm stop recording"
+ CVariant{848}, // "Are you sure you want to stop this recording?"
+ CVariant{""}, CVariant{timer->Title()});
+}
+
+namespace
+{
+std::string GetAnnouncerText(const std::shared_ptr<CPVRTimerInfoTag>& timer, int idEpg, int idNoEpg)
+{
+ std::string text;
+ if (timer->IsEpgBased())
+ {
+ text = StringUtils::Format(g_localizeStrings.Get(idEpg),
+ timer->Title(), // tv show title
+ timer->ChannelName(),
+ timer->StartAsLocalTime().GetAsLocalizedDateTime(false, false));
+ }
+ else
+ {
+ text = StringUtils::Format(g_localizeStrings.Get(idNoEpg), timer->ChannelName(),
+ timer->StartAsLocalTime().GetAsLocalizedDateTime(false, false));
+ }
+ return text;
+}
+
+void AddEventLogEntry(const std::shared_ptr<CPVRTimerInfoTag>& timer, int idEpg, int idNoEpg)
+{
+ std::string name;
+ std::string icon;
+
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(timer->GetTimerType()->GetClientId());
+ if (client)
+ {
+ name = client->GetFriendlyName();
+ icon = client->Icon();
+ }
+ else
+ {
+ name = g_sysinfo.GetAppName();
+ icon = "special://xbmc/media/icon256x256.png";
+ }
+
+ CPVREventLogJob* job = new CPVREventLogJob;
+ job->AddEvent(false, // do not display a toast, only log event
+ EventLevel::Information, // info, no error
+ name, GetAnnouncerText(timer, idEpg, idNoEpg), icon);
+ CServiceBroker::GetJobManager()->AddJob(job, nullptr);
+}
+} // unnamed namespace
+
+void CPVRGUIActionsTimers::AnnounceReminder(const std::shared_ptr<CPVRTimerInfoTag>& timer) const
+{
+ if (!timer->IsReminder())
+ {
+ CLog::LogF(LOGERROR, "No reminder timer!");
+ return;
+ }
+
+ if (timer->EndAsUTC() < CDateTime::GetUTCDateTime())
+ {
+ // expired. timer end is in the past. write event log entry.
+ AddEventLogEntry(timer, 19305, 19306); // Deleted missed PVR reminder ...
+ return;
+ }
+
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingChannel(timer->Channel()))
+ {
+ // no need for an announcement. channel in question is already playing.
+ return;
+ }
+
+ // show the reminder dialog
+ CGUIDialogProgress* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(
+ WINDOW_DIALOG_PROGRESS);
+ if (!dialog)
+ return;
+
+ dialog->Reset();
+
+ dialog->SetHeading(CVariant{19312}); // "PVR reminder"
+ dialog->ShowChoice(0, CVariant{19165}); // "Switch"
+
+ std::string text = GetAnnouncerText(timer, 19307, 19308); // Reminder for ...
+
+ bool bCanRecord = false;
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(timer->ClientID());
+ if (client && client->GetClientCapabilities().SupportsTimers())
+ {
+ bCanRecord = true;
+ dialog->ShowChoice(1, CVariant{264}); // "Record"
+ dialog->ShowChoice(2, CVariant{222}); // "Cancel"
+
+ if (m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTORECORD))
+ text += "\n\n" + g_localizeStrings.Get(
+ 19309); // (Auto-close of this reminder will schedule a recording...)
+ else if (m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTOSWITCH))
+ text += "\n\n" + g_localizeStrings.Get(
+ 19331); // (Auto-close of this reminder will switch to channel...)
+ }
+ else
+ {
+ dialog->ShowChoice(1, CVariant{222}); // "Cancel"
+ }
+
+ dialog->SetText(text);
+ dialog->SetPercentage(100);
+
+ dialog->Open();
+
+ int result = CGUIDialogProgress::CHOICE_NONE;
+
+ static constexpr int PROGRESS_TIMESLICE_MILLISECS = 50;
+
+ const int iWait = m_settings.GetIntValue(CSettings::SETTING_PVRREMINDERS_AUTOCLOSEDELAY) * 1000;
+ int iRemaining = iWait;
+ while (iRemaining > 0)
+ {
+ result = dialog->GetChoice();
+ if (result != CGUIDialogProgress::CHOICE_NONE)
+ break;
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(PROGRESS_TIMESLICE_MILLISECS));
+
+ iRemaining -= PROGRESS_TIMESLICE_MILLISECS;
+ dialog->SetPercentage(iRemaining * 100 / iWait);
+ dialog->Progress();
+ }
+
+ dialog->Close();
+
+ bool bAutoClosed = (iRemaining <= 0);
+ bool bSwitch = (result == 0);
+ bool bRecord = (result == 1);
+
+ if (bAutoClosed)
+ {
+ bRecord = (bCanRecord && m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTORECORD));
+ bSwitch = m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTOSWITCH);
+ }
+
+ if (bRecord)
+ {
+ std::shared_ptr<CPVRTimerInfoTag> newTimer;
+
+ std::shared_ptr<CPVREpgInfoTag> epgTag = timer->GetEpgInfoTag();
+ if (epgTag)
+ {
+ newTimer = CPVRTimerInfoTag::CreateFromEpg(epgTag, false);
+ if (newTimer)
+ {
+ // an epgtag can only have max one timer - we need to clear the reminder to be able to
+ // attach the recording timer
+ DeleteTimer(timer, false, false);
+ }
+ }
+ else
+ {
+ int iDuration = (timer->EndAsUTC() - timer->StartAsUTC()).GetSecondsTotal() / 60;
+ newTimer = CPVRTimerInfoTag::CreateTimerTag(timer->Channel(), timer->StartAsUTC(), iDuration);
+ }
+
+ if (newTimer)
+ {
+ // schedule recording
+ AddTimer(CFileItem(newTimer), false);
+ }
+
+ if (bAutoClosed)
+ {
+ AddEventLogEntry(timer, 19310,
+ 19311); // Scheduled recording for auto-closed PVR reminder ...
+ }
+ }
+
+ if (bSwitch)
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(
+ timer->Channel());
+ if (groupMember)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ CFileItem(groupMember), false);
+
+ if (bAutoClosed)
+ {
+ AddEventLogEntry(timer, 19332,
+ 19333); // Switched channel for auto-closed PVR reminder ...
+ }
+ }
+ }
+}
+
+void CPVRGUIActionsTimers::AnnounceReminders() const
+{
+ // Prevent multiple yesno dialogs, all on same call stack, due to gui message processing while dialog is open.
+ if (m_bReminderAnnouncementRunning)
+ return;
+
+ m_bReminderAnnouncementRunning = true;
+ std::shared_ptr<CPVRTimerInfoTag> timer =
+ CServiceBroker::GetPVRManager().Timers()->GetNextReminderToAnnnounce();
+ while (timer)
+ {
+ AnnounceReminder(timer);
+ timer = CServiceBroker::GetPVRManager().Timers()->GetNextReminderToAnnnounce();
+ }
+ m_bReminderAnnouncementRunning = false;
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsTimers.h b/xbmc/pvr/guilib/PVRGUIActionsTimers.h
new file mode 100644
index 0000000..f6ae8fe
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsTimers.h
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2016-2022 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/IPVRComponent.h"
+#include "pvr/settings/PVRSettings.h"
+
+#include <memory>
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRTimerInfoTag;
+
+class CPVRGUIActionsTimers : public IPVRComponent
+{
+public:
+ CPVRGUIActionsTimers();
+ ~CPVRGUIActionsTimers() override = default;
+
+ /*!
+ * @brief Open the timer settings dialog to create a new tv or radio timer.
+ * @param bRadio indicates whether a radio or tv timer shall be created.
+ * @return true on success, false otherwise.
+ */
+ bool AddTimer(bool bRadio) const;
+
+ /*!
+ * @brief Create a new timer, either interactive or non-interactive.
+ * @param item containing epg data to create a timer for. item must be an epg tag or a channel.
+ * @param bShowTimerSettings is used to control whether a settings dialog will be opened prior
+ * creating the timer.
+ * @return true, if the timer was created successfully, false otherwise.
+ */
+ bool AddTimer(const CFileItem& item, bool bShowTimerSettings) const;
+
+ /*!
+ * @brief Add a timer to the client. Doesn't add the timer to the container. The backend will
+ * do this.
+ * @return True if it was sent correctly, false if not.
+ */
+ bool AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& item) const;
+
+ /*!
+ * @brief Create a new timer rule, either interactive or non-interactive.
+ * @param item containing epg data to create a timer rule for. item must be an epg tag or a
+ * channel.
+ * @param bShowTimerSettings is used to control whether a settings dialog will be opened prior
+ * creating the timer rule.
+ * @param bFallbackToOneShotTimer if no timer rule can be created, try to create a one-shot
+ * timer instead.
+ * @return true, if the timer rule was created successfully, false otherwise.
+ */
+ bool AddTimerRule(const CFileItem& item,
+ bool bShowTimerSettings,
+ bool bFallbackToOneShotTimer) const;
+
+ /*!
+ * @brief Creates or deletes a timer for the given epg tag.
+ * @param item containing an epg tag.
+ * @return true on success, false otherwise.
+ */
+ bool ToggleTimer(const CFileItem& item) const;
+
+ /*!
+ * @brief Toggles a given timer's enabled/disabled state.
+ * @param item containing a timer.
+ * @return true on success, false otherwise.
+ */
+ bool ToggleTimerState(const CFileItem& item) const;
+
+ /*!
+ * @brief Open the timer settings dialog to edit an existing timer.
+ * @param item containing an epg tag or a timer.
+ * @return true on success, false otherwise.
+ */
+ bool EditTimer(const CFileItem& item) const;
+
+ /*!
+ * @brief Open the timer settings dialog to edit an existing timer rule.
+ * @param item containing an epg tag or a timer.
+ * @return true on success, false otherwise.
+ */
+ bool EditTimerRule(const CFileItem& item) const;
+
+ /*!
+ * @brief Get the timer rule for a given timer
+ * @param item containing an item to query the timer rule for. item must be a timer or an epg tag.
+ * @return The timer rule item, or nullptr if none was found.
+ */
+ std::shared_ptr<CFileItem> GetTimerRule(const CFileItem& item) const;
+
+ /*!
+ * @brief Delete a timer, always showing a confirmation dialog.
+ * @param item containing a timer to delete. item must be a timer, an epg tag or a channel.
+ * @return true, if the timer was deleted successfully, false otherwise.
+ */
+ bool DeleteTimer(const CFileItem& item) const;
+
+ /*!
+ * @brief Delete a timer rule, always showing a confirmation dialog.
+ * @param item containing a timer rule to delete. item must be a timer, an epg tag or a channel.
+ * @return true, if the timer rule was deleted successfully, false otherwise.
+ */
+ bool DeleteTimerRule(const CFileItem& item) const;
+
+ /*!
+ * @brief Toggle recording on the currently playing channel, if any.
+ * @return True if the recording was started or stopped successfully, false otherwise.
+ */
+ bool ToggleRecordingOnPlayingChannel();
+
+ /*!
+ * @brief Start or stop recording on a given channel.
+ * @param channel the channel to start/stop recording.
+ * @param bOnOff True to start recording, false to stop.
+ * @return True if the recording was started or stopped successfully, false otherwise.
+ */
+ bool SetRecordingOnChannel(const std::shared_ptr<CPVRChannel>& channel, bool bOnOff);
+
+ /*!
+ * @brief Stop a currently active recording, always showing a confirmation dialog.
+ * @param item containing a recording to stop. item must be a timer, an epg tag or a channel.
+ * @return true, if the recording was stopped successfully, false otherwise.
+ */
+ bool StopRecording(const CFileItem& item) const;
+
+ /*!
+ * @brief Create a new reminder timer, non-interactive.
+ * @param item containing epg data to create a reminder timer for. item must be an epg tag.
+ * @return true, if the timer was created successfully, false otherwise.
+ */
+ bool AddReminder(const CFileItem& item) const;
+
+ /*!
+ * @brief Announce due reminders, if any.
+ */
+ void AnnounceReminders() const;
+
+private:
+ CPVRGUIActionsTimers(const CPVRGUIActionsTimers&) = delete;
+ CPVRGUIActionsTimers const& operator=(CPVRGUIActionsTimers const&) = delete;
+
+ /*!
+ * @brief Open the timer settings dialog.
+ * @param timer containing the timer the settings shall be displayed for.
+ * @return true, if the dialog was ended successfully, false otherwise.
+ */
+ bool ShowTimerSettings(const std::shared_ptr<CPVRTimerInfoTag>& timer) const;
+
+ /*!
+ * @brief Add a timer or timer rule, either interactive or non-interactive.
+ * @param item containing epg data to create a timer or timer rule for. item must be an epg tag
+ * or a channel.
+ * @param bCreateteRule denotes whether to create a one-shot timer or a timer rule.
+ * @param bShowTimerSettings is used to control whether a settings dialog will be opened prior
+ * creating the timer or timer rule.
+ * @param bFallbackToOneShotTimer if bCreateteRule is true and no timer rule can be created, try
+ * to create a one-shot timer instead.
+ * @return true, if the timer or timer rule was created successfully, false otherwise.
+ */
+ bool AddTimer(const CFileItem& item,
+ bool bCreateRule,
+ bool bShowTimerSettings,
+ bool bFallbackToOneShotTimer) const;
+
+ /*!
+ * @brief Delete a timer or timer rule, always showing a confirmation dialog.
+ * @param item containing a timer or timer rule to delete. item must be a timer, an epg tag or
+ * a channel.
+ * @param bIsRecording denotes whether the timer is currently recording (controls correct
+ * confirmation dialog).
+ * @param bDeleteRule denotes to delete a timer rule. For convenience, one can pass a timer
+ * created by a rule.
+ * @return true, if the timer or timer rule was deleted successfully, false otherwise.
+ */
+ bool DeleteTimer(const CFileItem& item, bool bIsRecording, bool bDeleteRule) const;
+
+ /*!
+ * @brief Delete a timer or timer rule, showing a confirmation dialog in case a timer currently
+ * recording shall be deleted.
+ * @param timer containing a timer or timer rule to delete.
+ * @param bIsRecording denotes whether the timer is currently recording (controls correct
+ * confirmation dialog).
+ * @param bDeleteRule denotes to delete a timer rule. For convenience, one can pass a timer
+ * created by a rule.
+ * @return true, if the timer or timer rule was deleted successfully, false otherwise.
+ */
+ bool DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer,
+ bool bIsRecording,
+ bool bDeleteRule) const;
+
+ /*!
+ * @brief Open a dialog to confirm timer delete.
+ * @param timer the timer to delete.
+ * @param bDeleteRule in: ignored. out, for one shot timer scheduled by a timer rule: true to
+ * also delete the timer rule that has scheduled this timer, false to only delete the one shot
+ * timer. out, for one shot timer not scheduled by a timer rule: ignored
+ * @return true, to proceed with delete, false otherwise.
+ */
+ bool ConfirmDeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer, bool& bDeleteRule) const;
+
+ /*!
+ * @brief Open a dialog to confirm stop recording.
+ * @param timer the recording to stop (actually the timer to delete).
+ * @return true, to proceed with delete, false otherwise.
+ */
+ bool ConfirmStopRecording(const std::shared_ptr<CPVRTimerInfoTag>& timer) const;
+
+ /*!
+ * @brief Announce and process a reminder timer.
+ * @param timer The reminder timer.
+ */
+ void AnnounceReminder(const std::shared_ptr<CPVRTimerInfoTag>& timer) const;
+
+ CPVRSettings m_settings;
+ mutable bool m_bReminderAnnouncementRunning{false};
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Timers = CPVRGUIActionsTimers;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsUtils.cpp b/xbmc/pvr/guilib/PVRGUIActionsUtils.cpp
new file mode 100644
index 0000000..28e5582
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsUtils.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIActionsUtils.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsRecordings.h"
+
+namespace PVR
+{
+bool CPVRGUIActionsUtils::OnInfo(const CFileItem& item)
+{
+ if (item.HasPVRRecordingInfoTag())
+ {
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo(item);
+ }
+ else if (item.HasPVRChannelInfoTag() || item.HasPVRTimerInfoTag())
+ {
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(item);
+ }
+ else if (item.HasEPGSearchFilter())
+ {
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().EditSavedSearch(item);
+ }
+ return false;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsUtils.h b/xbmc/pvr/guilib/PVRGUIActionsUtils.h
new file mode 100644
index 0000000..a880548
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsUtils.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/IPVRComponent.h"
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRGUIActionsUtils : public IPVRComponent
+{
+public:
+ CPVRGUIActionsUtils() = default;
+ ~CPVRGUIActionsUtils() override = default;
+
+ /*!
+ * @brief Process info action for the given item.
+ * @param item The item.
+ */
+ bool OnInfo(const CFileItem& item);
+
+private:
+ CPVRGUIActionsUtils(const CPVRGUIActionsUtils&) = delete;
+ CPVRGUIActionsUtils const& operator=(CPVRGUIActionsUtils const&) = delete;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Utils = CPVRGUIActionsUtils;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp
new file mode 100644
index 0000000..a239fe2
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012-2019 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIChannelIconUpdater.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "filesystem/Directory.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/guilib/PVRGUIProgressHandler.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+void CPVRGUIChannelIconUpdater::SearchAndUpdateMissingChannelIcons() const
+{
+ const std::string iconPath = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_PVRMENU_ICONPATH);
+ if (iconPath.empty())
+ return;
+
+ // fetch files in icon path for fast lookup
+ CFileItemList fileItemList;
+ XFILE::CDirectory::GetDirectory(iconPath, fileItemList, ".jpg|.png|.tbn", XFILE::DIR_FLAG_DEFAULTS);
+
+ if (fileItemList.IsEmpty())
+ return;
+
+ CLog::Log(LOGINFO, "Starting PVR channel icon search");
+
+ // create a map for fast lookup of normalized file base name
+ std::map<std::string, std::string> fileItemMap;
+ for (const auto& item : fileItemList)
+ {
+ std::string baseName = URIUtils::GetFileName(item->GetPath());
+ URIUtils::RemoveExtension(baseName);
+ StringUtils::ToLower(baseName);
+ fileItemMap.insert({baseName, item->GetPath()});
+ }
+
+ std::unique_ptr<CPVRGUIProgressHandler> progressHandler;
+ if (!m_groups.empty())
+ progressHandler.reset(
+ new CPVRGUIProgressHandler(g_localizeStrings.Get(19286))); // Searching for channel icons
+
+ for (const auto& group : m_groups)
+ {
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> members = group->GetMembers();
+ int channelIndex = 0;
+ for (const auto& member : members)
+ {
+ const std::shared_ptr<CPVRChannel> channel = member->Channel();
+
+ progressHandler->UpdateProgress(channel->ChannelName(), channelIndex++, members.size());
+
+ // skip if an icon is already set and exists
+ if (CFileUtils::Exists(channel->IconPath()))
+ continue;
+
+ // reset icon before searching for a new one
+ channel->SetIconPath("");
+
+ const std::string strChannelUid = StringUtils::Format("{:08}", channel->UniqueID());
+ std::string strLegalClientChannelName =
+ CUtil::MakeLegalFileName(channel->ClientChannelName());
+ StringUtils::ToLower(strLegalClientChannelName);
+ std::string strLegalChannelName = CUtil::MakeLegalFileName(channel->ChannelName());
+ StringUtils::ToLower(strLegalChannelName);
+
+ std::map<std::string, std::string>::iterator itItem;
+ if ((itItem = fileItemMap.find(strLegalClientChannelName)) != fileItemMap.end() ||
+ (itItem = fileItemMap.find(strLegalChannelName)) != fileItemMap.end() ||
+ (itItem = fileItemMap.find(strChannelUid)) != fileItemMap.end())
+ {
+ channel->SetIconPath(itItem->second, CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_bPVRAutoScanIconsUserSet);
+ }
+
+ if (m_bUpdateDb)
+ channel->Persist();
+ }
+ }
+}
diff --git a/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h
new file mode 100644
index 0000000..d64a41f
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012-2019 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <memory>
+#include <vector>
+
+namespace PVR
+{
+
+class CPVRChannelGroup;
+
+class CPVRGUIChannelIconUpdater
+{
+public:
+ /*!
+ * @brief ctor.
+ * @param groups The channel groups for which the channel icons shall be updated.
+ * @param bUpdateDb If true, persist the changed values in the PVR database.
+ */
+ CPVRGUIChannelIconUpdater(const std::vector<std::shared_ptr<CPVRChannelGroup>>& groups, bool bUpdateDb)
+ : m_groups(groups), m_bUpdateDb(bUpdateDb) {}
+
+ CPVRGUIChannelIconUpdater() = delete;
+ CPVRGUIChannelIconUpdater(const CPVRGUIChannelIconUpdater&) = delete;
+ CPVRGUIChannelIconUpdater& operator=(const CPVRGUIChannelIconUpdater&) = delete;
+
+ virtual ~CPVRGUIChannelIconUpdater() = default;
+
+ /*!
+ * @brief Search and update missing channel icons.
+ */
+ void SearchAndUpdateMissingChannelIcons() const;
+
+private:
+ const std::vector<std::shared_ptr<CPVRChannelGroup>> m_groups;
+ const bool m_bUpdateDb = false;
+};
+
+}
diff --git a/xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp b/xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp
new file mode 100644
index 0000000..0ccfdf8
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIChannelNavigator.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SystemClock.h"
+#include "utils/Job.h"
+#include "utils/JobManager.h"
+#include "utils/XTimeUtils.h"
+
+#include <mutex>
+
+using namespace KODI::GUILIB::GUIINFO;
+using namespace PVR;
+using namespace std::chrono_literals;
+
+namespace
+{
+class CPVRChannelTimeoutJobBase : public CJob, public IJobCallback
+{
+public:
+ CPVRChannelTimeoutJobBase() = delete;
+ CPVRChannelTimeoutJobBase(PVR::CPVRGUIChannelNavigator& channelNavigator,
+ std::chrono::milliseconds timeout)
+ : m_channelNavigator(channelNavigator)
+ {
+ m_delayTimer.Set(timeout);
+ }
+
+ ~CPVRChannelTimeoutJobBase() override = default;
+
+ virtual void OnTimeout() = 0;
+
+ void OnJobComplete(unsigned int iJobID, bool bSuccess, CJob* job) override {}
+
+ bool DoWork() override
+ {
+ while (!ShouldCancel(0, 0))
+ {
+ if (m_delayTimer.IsTimePast())
+ {
+ OnTimeout();
+ return true;
+ }
+ KODI::TIME::Sleep(10ms);
+ }
+ return false;
+ }
+
+protected:
+ PVR::CPVRGUIChannelNavigator& m_channelNavigator;
+
+private:
+ XbmcThreads::EndTime<> m_delayTimer;
+};
+
+class CPVRChannelEntryTimeoutJob : public CPVRChannelTimeoutJobBase
+{
+public:
+ CPVRChannelEntryTimeoutJob(PVR::CPVRGUIChannelNavigator& channelNavigator,
+ std::chrono::milliseconds timeout)
+ : CPVRChannelTimeoutJobBase(channelNavigator, timeout)
+ {
+ }
+ ~CPVRChannelEntryTimeoutJob() override = default;
+ const char* GetType() const override { return "pvr-channel-entry-timeout-job"; }
+ void OnTimeout() override { m_channelNavigator.SwitchToCurrentChannel(); }
+};
+
+class CPVRChannelInfoTimeoutJob : public CPVRChannelTimeoutJobBase
+{
+public:
+ CPVRChannelInfoTimeoutJob(PVR::CPVRGUIChannelNavigator& channelNavigator,
+ std::chrono::milliseconds timeout)
+ : CPVRChannelTimeoutJobBase(channelNavigator, timeout)
+ {
+ }
+ ~CPVRChannelInfoTimeoutJob() override = default;
+ const char* GetType() const override { return "pvr-channel-info-timeout-job"; }
+ void OnTimeout() override { m_channelNavigator.HideInfo(); }
+};
+} // unnamed namespace
+
+CPVRGUIChannelNavigator::CPVRGUIChannelNavigator()
+{
+ // Note: we cannot subscribe to PlayerInfoProvider here, as we're getting constructed
+ // before the info providers. We will subscribe once our first subscriber appears.
+}
+
+CPVRGUIChannelNavigator::~CPVRGUIChannelNavigator()
+{
+ const auto gui = CServiceBroker::GetGUI();
+ if (!gui)
+ return;
+
+ gui->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().Events().Unsubscribe(this);
+}
+
+void CPVRGUIChannelNavigator::SubscribeToShowInfoEventStream()
+{
+ CServiceBroker::GetGUI()
+ ->GetInfoManager()
+ .GetInfoProviders()
+ .GetPlayerInfoProvider()
+ .Events()
+ .Subscribe(this, &CPVRGUIChannelNavigator::Notify);
+}
+
+void CPVRGUIChannelNavigator::CheckAndPublishPreviewAndPlayerShowInfoChangedEvent()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const bool currentValue = IsPreview() && m_playerShowInfo;
+ if (m_previewAndPlayerShowInfo != currentValue)
+ {
+ m_previewAndPlayerShowInfo = currentValue;
+
+ // inform subscribers
+ m_events.Publish(PVRPreviewAndPlayerShowInfoChangedEvent(currentValue));
+ }
+}
+
+void CPVRGUIChannelNavigator::Notify(const PlayerShowInfoChangedEvent& event)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_playerShowInfo = event.m_showInfo;
+ CheckAndPublishPreviewAndPlayerShowInfoChangedEvent();
+}
+
+void CPVRGUIChannelNavigator::SelectNextChannel(ChannelSwitchMode eSwitchMode)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_playerShowInfo && eSwitchMode == ChannelSwitchMode::NO_SWITCH)
+ {
+ // show info for current channel on first next channel selection.
+ ShowInfo(false);
+ return;
+ }
+
+ const std::shared_ptr<CPVRChannelGroupMember> nextMember = GetNextOrPrevChannel(true);
+ if (nextMember)
+ SelectChannel(nextMember, eSwitchMode);
+}
+
+void CPVRGUIChannelNavigator::SelectPreviousChannel(ChannelSwitchMode eSwitchMode)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_playerShowInfo && eSwitchMode == ChannelSwitchMode::NO_SWITCH)
+ {
+ // show info for current channel on first previous channel selection.
+ ShowInfo(false);
+ return;
+ }
+
+ const std::shared_ptr<CPVRChannelGroupMember> prevMember = GetNextOrPrevChannel(false);
+ if (prevMember)
+ SelectChannel(prevMember, eSwitchMode);
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRGUIChannelNavigator::GetNextOrPrevChannel(bool bNext)
+{
+ const bool bPlayingRadio = CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio();
+ const bool bPlayingTV = CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV();
+
+ if (bPlayingTV || bPlayingRadio)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(bPlayingRadio);
+ if (group)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return bNext ? group->GetNextChannelGroupMember(m_currentChannel)
+ : group->GetPreviousChannelGroupMember(m_currentChannel);
+ }
+ }
+ return {};
+}
+
+void CPVRGUIChannelNavigator::SelectChannel(
+ const std::shared_ptr<CPVRChannelGroupMember>& groupMember, ChannelSwitchMode eSwitchMode)
+{
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(CFileItem(groupMember));
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_currentChannel = groupMember;
+ ShowInfo(false);
+
+ CheckAndPublishPreviewAndPlayerShowInfoChangedEvent();
+
+ if (IsPreview() && eSwitchMode == ChannelSwitchMode::INSTANT_OR_DELAYED_SWITCH)
+ {
+ auto timeout =
+ std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_PVRPLAYBACK_CHANNELENTRYTIMEOUT));
+ if (timeout > 0ms)
+ {
+ // delayed switch
+ if (m_iChannelEntryJobId >= 0)
+ CServiceBroker::GetJobManager()->CancelJob(m_iChannelEntryJobId);
+
+ CPVRChannelEntryTimeoutJob* job = new CPVRChannelEntryTimeoutJob(*this, timeout);
+ m_iChannelEntryJobId =
+ CServiceBroker::GetJobManager()->AddJob(job, dynamic_cast<IJobCallback*>(job));
+ }
+ else
+ {
+ // instant switch
+ SwitchToCurrentChannel();
+ }
+ }
+}
+
+void CPVRGUIChannelNavigator::SwitchToCurrentChannel()
+{
+ std::unique_ptr<CFileItem> item;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iChannelEntryJobId >= 0)
+ {
+ CServiceBroker::GetJobManager()->CancelJob(m_iChannelEntryJobId);
+ m_iChannelEntryJobId = -1;
+ }
+
+ item = std::make_unique<CFileItem>(m_currentChannel);
+ }
+
+ if (item)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(*item, false);
+}
+
+bool CPVRGUIChannelNavigator::IsPreview() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_currentChannel != m_playingChannel;
+}
+
+bool CPVRGUIChannelNavigator::IsPreviewAndShowInfo() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_previewAndPlayerShowInfo;
+}
+
+void CPVRGUIChannelNavigator::ShowInfo()
+{
+ ShowInfo(true);
+}
+
+void CPVRGUIChannelNavigator::ShowInfo(bool bForce)
+{
+ auto timeout = std::chrono::seconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_PVRMENU_DISPLAYCHANNELINFO));
+
+ if (bForce || timeout > 0s)
+ {
+ CServiceBroker::GetGUI()
+ ->GetInfoManager()
+ .GetInfoProviders()
+ .GetPlayerInfoProvider()
+ .SetShowInfo(true);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iChannelInfoJobId >= 0)
+ {
+ CServiceBroker::GetJobManager()->CancelJob(m_iChannelInfoJobId);
+ m_iChannelInfoJobId = -1;
+ }
+
+ if (!bForce && timeout > 0s)
+ {
+ CPVRChannelInfoTimeoutJob* job = new CPVRChannelInfoTimeoutJob(*this, timeout);
+ m_iChannelInfoJobId =
+ CServiceBroker::GetJobManager()->AddJob(job, dynamic_cast<IJobCallback*>(job));
+ }
+ }
+}
+
+void CPVRGUIChannelNavigator::HideInfo()
+{
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(
+ false);
+
+ CFileItemPtr item;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iChannelInfoJobId >= 0)
+ {
+ CServiceBroker::GetJobManager()->CancelJob(m_iChannelInfoJobId);
+ m_iChannelInfoJobId = -1;
+ }
+
+ if (m_currentChannel != m_playingChannel)
+ {
+ m_currentChannel = m_playingChannel;
+ if (m_playingChannel)
+ item.reset(new CFileItem(m_playingChannel));
+ }
+
+ CheckAndPublishPreviewAndPlayerShowInfoChangedEvent();
+ }
+
+ if (item)
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*item);
+}
+
+void CPVRGUIChannelNavigator::ToggleInfo()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_playerShowInfo)
+ HideInfo();
+ else
+ ShowInfo();
+}
+
+void CPVRGUIChannelNavigator::SetPlayingChannel(
+ const std::shared_ptr<CPVRChannelGroupMember>& groupMember)
+{
+ CFileItemPtr item;
+
+ if (groupMember)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_playingChannel = groupMember;
+ if (m_currentChannel != m_playingChannel)
+ {
+ m_currentChannel = m_playingChannel;
+ if (m_playingChannel)
+ item.reset(new CFileItem(m_playingChannel));
+ }
+
+ CheckAndPublishPreviewAndPlayerShowInfoChangedEvent();
+ }
+
+ if (item)
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*item);
+
+ ShowInfo(false);
+}
+
+void CPVRGUIChannelNavigator::ClearPlayingChannel()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_playingChannel.reset();
+ HideInfo();
+
+ CheckAndPublishPreviewAndPlayerShowInfoChangedEvent();
+}
diff --git a/xbmc/pvr/guilib/PVRGUIChannelNavigator.h b/xbmc/pvr/guilib/PVRGUIChannelNavigator.h
new file mode 100644
index 0000000..5c27074
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIChannelNavigator.h
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+#include "utils/EventStream.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+struct PlayerShowInfoChangedEvent;
+}
+} // namespace GUILIB
+} // namespace KODI
+
+namespace PVR
+{
+enum class ChannelSwitchMode
+{
+ NO_SWITCH, // no channel switch
+ INSTANT_OR_DELAYED_SWITCH // switch according to SETTING_PVRPLAYBACK_CHANNELENTRYTIMEOUT
+};
+
+struct PVRPreviewAndPlayerShowInfoChangedEvent
+{
+ explicit PVRPreviewAndPlayerShowInfoChangedEvent(bool previewAndPlayerShowInfo)
+ : m_previewAndPlayerShowInfo(previewAndPlayerShowInfo)
+ {
+ }
+ virtual ~PVRPreviewAndPlayerShowInfoChangedEvent() = default;
+
+ bool m_previewAndPlayerShowInfo{false};
+};
+
+class CPVRChannelGroupMember;
+
+class CPVRGUIChannelNavigator
+{
+public:
+ CPVRGUIChannelNavigator();
+ virtual ~CPVRGUIChannelNavigator();
+
+ /*!
+ * @brief Subscribe to the event stream for changes of channel preview and player show info.
+ * @param owner The subscriber.
+ * @param fn The callback function of the subscriber for the events.
+ */
+ template<typename A>
+ void Subscribe(A* owner, void (A::*fn)(const PVRPreviewAndPlayerShowInfoChangedEvent&))
+ {
+ SubscribeToShowInfoEventStream();
+ m_events.Subscribe(owner, fn);
+ }
+
+ /*!
+ * @brief Unsubscribe from the event stream for changes of channel preview and player show info.
+ * @param obj The subscriber.
+ */
+ template<typename A>
+ void Unsubscribe(A* obj)
+ {
+ m_events.Unsubscribe(obj);
+ }
+
+ /*!
+ * @brief CEventStream callback for player show info flag changes.
+ * @param event The event.
+ */
+ void Notify(const KODI::GUILIB::GUIINFO::PlayerShowInfoChangedEvent& event);
+
+ /*!
+ * @brief Select the next channel in currently playing channel group, relative to the currently
+ * selected channel.
+ * @param eSwitchMode controls whether only the channel info OSD is triggered or whether
+ * additionally a (delayed) channel switch will be done.
+ */
+ void SelectNextChannel(ChannelSwitchMode eSwitchMode);
+
+ /*!
+ * @brief Select the previous channel in currently playing channel group, relative to the
+ * currently selected channel.
+ * @param eSwitchMode controls whether only the channel info OSD is triggered or whether
+ * additionally a (delayed) channel switch will be done.
+ */
+ void SelectPreviousChannel(ChannelSwitchMode eSwitchMode);
+
+ /*!
+ * @brief Switch to the currently selected channel.
+ */
+ void SwitchToCurrentChannel();
+
+ /*!
+ * @brief Query the state of channel preview.
+ * @return True, if the currently selected channel is different from the currently playing
+ * channel, False otherwise.
+ */
+ bool IsPreview() const;
+
+ /*!
+ * @brief Query the state of channel preview and channel info OSD.
+ * @return True, if the currently selected channel is different from the currently playing channel
+ * and channel info OSD is active, False otherwise.
+ */
+ bool IsPreviewAndShowInfo() const;
+
+ /*!
+ * @brief Show the channel info OSD.
+ */
+ void ShowInfo();
+
+ /*!
+ * @brief Hide the channel info OSD.
+ */
+ void HideInfo();
+
+ /*!
+ * @brief Toggle the channel info OSD visibility.
+ */
+ void ToggleInfo();
+
+ /*!
+ * @brief Set a new playing channel group member and show the channel info OSD for the new
+ * channel.
+ * @param groupMember The new playing channel group member
+ */
+ void SetPlayingChannel(const std::shared_ptr<CPVRChannelGroupMember>& groupMember);
+
+ /*!
+ * @brief Clear the currently playing channel and hide the channel info OSD.
+ */
+ void ClearPlayingChannel();
+
+private:
+ /*!
+ * @brief Get next or previous channel group member of the playing channel group, relative to the
+ * currently selected channel group member.
+ * @param bNext True to get the next channel group member, false to get the previous channel group
+ * member.
+ * @param return The channel or nullptr if not found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetNextOrPrevChannel(bool bNext);
+
+ /*!
+ * @brief Select a given channel group member, display channel info OSD, switch according to given
+ * switch mode.
+ * @param groupMember The channel group member to select.
+ * @param eSwitchMode The channel switch mode.
+ */
+ void SelectChannel(const std::shared_ptr<CPVRChannelGroupMember>& groupMember,
+ ChannelSwitchMode eSwitchMode);
+
+ /*!
+ * @brief Show the channel info OSD.
+ * @param bForce True ignores value of SETTING_PVRMENU_DISPLAYCHANNELINFO and always activates the
+ * info, False acts aaccording settings value.
+ */
+ void ShowInfo(bool bForce);
+
+ /*!
+ * @brief Subscribe to the event stream for changes of player show info.
+ */
+ void SubscribeToShowInfoEventStream();
+
+ /*!
+ * @brief Check if property preview and show info value changed, inform subscribers in case.
+ */
+ void CheckAndPublishPreviewAndPlayerShowInfoChangedEvent();
+
+ mutable CCriticalSection m_critSection;
+ std::shared_ptr<CPVRChannelGroupMember> m_playingChannel;
+ std::shared_ptr<CPVRChannelGroupMember> m_currentChannel;
+ int m_iChannelEntryJobId = -1;
+ int m_iChannelInfoJobId = -1;
+ CEventSource<PVRPreviewAndPlayerShowInfoChangedEvent> m_events;
+ bool m_playerShowInfo{false};
+ bool m_previewAndPlayerShowInfo{false};
+};
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIProgressHandler.cpp b/xbmc/pvr/guilib/PVRGUIProgressHandler.cpp
new file mode 100644
index 0000000..f876522
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIProgressHandler.cpp
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIProgressHandler.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+
+#include <algorithm>
+#include <cmath>
+#include <mutex>
+#include <string>
+
+using namespace std::chrono_literals;
+
+namespace PVR
+{
+
+CPVRGUIProgressHandler::CPVRGUIProgressHandler(const std::string& strTitle)
+ : CThread("PVRGUIProgressHandler"), m_strTitle(strTitle)
+{
+}
+
+void CPVRGUIProgressHandler::UpdateProgress(const std::string& strText, float fProgress)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bChanged = true;
+ m_strText = strText;
+ m_fProgress = fProgress;
+
+ if (!m_bCreated)
+ {
+ m_bCreated = true;
+ Create();
+ }
+}
+
+void CPVRGUIProgressHandler::UpdateProgress(const std::string& strText, int iCurrent, int iMax)
+{
+ float fPercentage = (iCurrent * 100.0f) / iMax;
+ if (!std::isnan(fPercentage))
+ fPercentage = std::min(100.0f, fPercentage);
+
+ UpdateProgress(strText, fPercentage);
+}
+
+void CPVRGUIProgressHandler::Process()
+{
+ CGUIDialogExtendedProgressBar* progressBar =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(
+ WINDOW_DIALOG_EXT_PROGRESS);
+ if (m_bStop || !progressBar)
+ return;
+
+ CGUIDialogProgressBarHandle* progressHandle = progressBar->GetHandle(m_strTitle);
+ if (!progressHandle)
+ return;
+
+ while (!m_bStop)
+ {
+ float fProgress = 0.0;
+ std::string strText;
+ bool bUpdate = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bChanged)
+ {
+ m_bChanged = false;
+ fProgress = m_fProgress;
+ strText = m_strText;
+ bUpdate = true;
+ }
+ }
+
+ if (bUpdate)
+ {
+ progressHandle->SetPercentage(fProgress);
+ progressHandle->SetText(strText);
+ }
+
+ // Intentionally ignore some changes that come in too fast. Humans cannot read as fast as Mr. Data ;-)
+ CThread::Sleep(100ms);
+ }
+
+ progressHandle->MarkFinished();
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIProgressHandler.h b/xbmc/pvr/guilib/PVRGUIProgressHandler.h
new file mode 100644
index 0000000..6e7b72f
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIProgressHandler.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <string>
+
+namespace PVR
+{
+ class CPVRGUIProgressHandler : private CThread
+ {
+ public:
+ CPVRGUIProgressHandler() = delete;
+
+ /*!
+ * @brief Creates and asynchronously shows a progress dialog with the given title.
+ * @param strTitle The title for the progress dialog.
+ */
+ explicit CPVRGUIProgressHandler(const std::string& strTitle);
+
+ ~CPVRGUIProgressHandler() override = default;
+
+ /*!
+ * @brief Update the progress dialogs's content.
+ * @param strText The new progress text.
+ * @param fProgress The new progress value, in a range from 0.0 to 100.0.
+ */
+ void UpdateProgress(const std::string& strText, float fProgress);
+
+ /*!
+ * @brief Update the progress dialogs's content.
+ * @param strText The new progress text.
+ * @param iCurrent The new current progress value, must be less or equal iMax.
+ * @param iMax The new maximum progress value, must be greater or equal iCurrent.
+ */
+ void UpdateProgress(const std::string& strText, int iCurrent, int iMax);
+
+ protected:
+ // CThread implementation
+ void Process() override;
+
+ private:
+ CCriticalSection m_critSection;
+ const std::string m_strTitle;
+ std::string m_strText;
+ float m_fProgress{0.0f};
+ bool m_bChanged{false};
+ bool m_bCreated{false};
+ };
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/guiinfo/CMakeLists.txt b/xbmc/pvr/guilib/guiinfo/CMakeLists.txt
new file mode 100644
index 0000000..18491ee
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES PVRGUIInfo.cpp
+ PVRGUITimerInfo.cpp
+ PVRGUITimesInfo.cpp)
+
+set(HEADERS PVRGUIInfo.h
+ PVRGUITimerInfo.h
+ PVRGUITimesInfo.h)
+
+core_add_library(pvr_guilib_guiinfo)
diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp
new file mode 100644
index 0000000..9cffb9a
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp
@@ -0,0 +1,2017 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUIInfo.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/channels/PVRRadioRDSInfoTag.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/providers/PVRProvider.h"
+#include "pvr/providers/PVRProviders.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SingleLock.h"
+#include "threads/SystemClock.h"
+#include "utils/StringUtils.h"
+
+#include <cmath>
+#include <ctime>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::GUILIB::GUIINFO;
+using namespace std::chrono_literals;
+
+CPVRGUIInfo::CPVRGUIInfo() : CThread("PVRGUIInfo")
+{
+ ResetProperties();
+}
+
+void CPVRGUIInfo::ResetProperties()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_anyTimersInfo.ResetProperties();
+ m_tvTimersInfo.ResetProperties();
+ m_radioTimersInfo.ResetProperties();
+ m_timesInfo.Reset();
+ m_bHasTVRecordings = false;
+ m_bHasRadioRecordings = false;
+ m_iCurrentActiveClient = 0;
+ m_strPlayingClientName.clear();
+ m_strBackendName.clear();
+ m_strBackendVersion.clear();
+ m_strBackendHost.clear();
+ m_strBackendTimers.clear();
+ m_strBackendRecordings.clear();
+ m_strBackendDeletedRecordings.clear();
+ m_strBackendProviders.clear();
+ m_strBackendChannelGroups.clear();
+ m_strBackendChannels.clear();
+ m_iBackendDiskTotal = 0;
+ m_iBackendDiskUsed = 0;
+ m_bIsPlayingTV = false;
+ m_bIsPlayingRadio = false;
+ m_bIsPlayingRecording = false;
+ m_bIsPlayingEpgTag = false;
+ m_bIsPlayingEncryptedStream = false;
+ m_bIsRecordingPlayingChannel = false;
+ m_bCanRecordPlayingChannel = false;
+ m_bIsPlayingActiveRecording = false;
+ m_bHasTVChannels = false;
+ m_bHasRadioChannels = false;
+
+ ClearQualityInfo(m_qualityInfo);
+ ClearDescrambleInfo(m_descrambleInfo);
+
+ m_updateBackendCacheRequested = false;
+ m_bRegistered = false;
+}
+
+void CPVRGUIInfo::ClearQualityInfo(PVR_SIGNAL_STATUS& qualityInfo)
+{
+ memset(&qualityInfo, 0, sizeof(qualityInfo));
+ strncpy(qualityInfo.strAdapterName, g_localizeStrings.Get(13106).c_str(),
+ PVR_ADDON_NAME_STRING_LENGTH - 1);
+ strncpy(qualityInfo.strAdapterStatus, g_localizeStrings.Get(13106).c_str(),
+ PVR_ADDON_NAME_STRING_LENGTH - 1);
+}
+
+void CPVRGUIInfo::ClearDescrambleInfo(PVR_DESCRAMBLE_INFO& descrambleInfo)
+{
+ descrambleInfo = {};
+}
+
+void CPVRGUIInfo::Start()
+{
+ ResetProperties();
+ Create();
+ SetPriority(ThreadPriority::BELOW_NORMAL);
+}
+
+void CPVRGUIInfo::Stop()
+{
+ StopThread();
+
+ auto& mgr = CServiceBroker::GetPVRManager();
+ auto& channels = mgr.Get<PVR::GUI::Channels>();
+ channels.GetChannelNavigator().Unsubscribe(this);
+ channels.Events().Unsubscribe(this);
+ mgr.Events().Unsubscribe(this);
+
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ {
+ gui->GetInfoManager().UnregisterInfoProvider(this);
+ m_bRegistered = false;
+ }
+}
+
+void CPVRGUIInfo::Notify(const PVREvent& event)
+{
+ if (event == PVREvent::Timers || event == PVREvent::TimersInvalidated)
+ UpdateTimersCache();
+}
+
+void CPVRGUIInfo::Notify(const PVRChannelNumberInputChangedEvent& event)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channelNumberInput = event.m_input;
+}
+
+void CPVRGUIInfo::Notify(const PVRPreviewAndPlayerShowInfoChangedEvent& event)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_previewAndPlayerShowInfo = event.m_previewAndPlayerShowInfo;
+}
+
+void CPVRGUIInfo::Process()
+{
+ auto toggleIntervalMs = std::chrono::milliseconds(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRInfoToggleInterval);
+ XbmcThreads::EndTime<> cacheTimer(toggleIntervalMs);
+
+ auto& mgr = CServiceBroker::GetPVRManager();
+ mgr.Events().Subscribe(this, &CPVRGUIInfo::Notify);
+
+ auto& channels = mgr.Get<PVR::GUI::Channels>();
+ channels.Events().Subscribe(this, &CPVRGUIInfo::Notify);
+ channels.GetChannelNavigator().Subscribe(this, &CPVRGUIInfo::Notify);
+
+ /* updated on request */
+ UpdateTimersCache();
+
+ /* update the backend cache once initially */
+ m_updateBackendCacheRequested = true;
+
+ while (!g_application.m_bStop && !m_bStop)
+ {
+ if (!m_bRegistered)
+ {
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ {
+ gui->GetInfoManager().RegisterInfoProvider(this);
+ m_bRegistered = true;
+ }
+ }
+
+ if (!m_bStop)
+ UpdateQualityData();
+ std::this_thread::yield();
+
+ if (!m_bStop)
+ UpdateDescrambleData();
+ std::this_thread::yield();
+
+ if (!m_bStop)
+ UpdateMisc();
+ std::this_thread::yield();
+
+ if (!m_bStop)
+ UpdateTimeshiftData();
+ std::this_thread::yield();
+
+ if (!m_bStop)
+ UpdateTimersToggle();
+ std::this_thread::yield();
+
+ if (!m_bStop)
+ UpdateNextTimer();
+ std::this_thread::yield();
+
+ // Update the backend cache every toggleInterval seconds
+ if (!m_bStop && cacheTimer.IsTimePast())
+ {
+ UpdateBackendCache();
+ cacheTimer.Set(toggleIntervalMs);
+ }
+
+ if (!m_bStop)
+ CThread::Sleep(500ms);
+ }
+}
+
+void CPVRGUIInfo::UpdateQualityData()
+{
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRPLAYBACK_SIGNALQUALITY))
+ return;
+
+ const std::shared_ptr<CPVRPlaybackState> playbackState =
+ CServiceBroker::GetPVRManager().PlaybackState();
+ if (!playbackState)
+ return;
+
+ PVR_SIGNAL_STATUS qualityInfo;
+ ClearQualityInfo(qualityInfo);
+
+ const int channelUid = playbackState->GetPlayingChannelUniqueID();
+ if (channelUid > 0)
+ {
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().Clients()->GetCreatedClient(
+ playbackState->GetPlayingClientID());
+ if (client)
+ client->SignalQuality(channelUid, qualityInfo);
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_qualityInfo = qualityInfo;
+}
+
+void CPVRGUIInfo::UpdateDescrambleData()
+{
+ const std::shared_ptr<CPVRPlaybackState> playbackState =
+ CServiceBroker::GetPVRManager().PlaybackState();
+ if (!playbackState)
+ return;
+
+ PVR_DESCRAMBLE_INFO descrambleInfo;
+ ClearDescrambleInfo(descrambleInfo);
+
+ const int channelUid = playbackState->GetPlayingChannelUniqueID();
+ if (channelUid > 0)
+ {
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().Clients()->GetCreatedClient(
+ playbackState->GetPlayingClientID());
+ if (client)
+ client->GetDescrambleInfo(channelUid, descrambleInfo);
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_descrambleInfo = descrambleInfo;
+}
+
+void CPVRGUIInfo::UpdateMisc()
+{
+ const CPVRManager& mgr = CServiceBroker::GetPVRManager();
+ bool bStarted = mgr.IsStarted();
+ const std::shared_ptr<CPVRPlaybackState> state = mgr.PlaybackState();
+
+ /* safe to fetch these unlocked, since they're updated from the same thread as this one */
+ const std::string strPlayingClientName = bStarted ? state->GetPlayingClientName() : "";
+ const bool bHasTVRecordings = bStarted && mgr.Recordings()->GetNumTVRecordings() > 0;
+ const bool bHasRadioRecordings = bStarted && mgr.Recordings()->GetNumRadioRecordings() > 0;
+ const bool bIsPlayingTV = bStarted && state->IsPlayingTV();
+ const bool bIsPlayingRadio = bStarted && state->IsPlayingRadio();
+ const bool bIsPlayingRecording = bStarted && state->IsPlayingRecording();
+ const bool bIsPlayingEpgTag = bStarted && state->IsPlayingEpgTag();
+ const bool bIsPlayingEncryptedStream = bStarted && state->IsPlayingEncryptedChannel();
+ const bool bHasTVChannels = bStarted && mgr.ChannelGroups()->GetGroupAllTV()->HasChannels();
+ const bool bHasRadioChannels = bStarted && mgr.ChannelGroups()->GetGroupAllRadio()->HasChannels();
+ const bool bCanRecordPlayingChannel = bStarted && state->CanRecordOnPlayingChannel();
+ const bool bIsRecordingPlayingChannel = bStarted && state->IsRecordingOnPlayingChannel();
+ const bool bIsPlayingActiveRecording = bStarted && state->IsPlayingActiveRecording();
+ const std::string strPlayingTVGroup =
+ (bStarted && bIsPlayingTV) ? state->GetActiveChannelGroup(false)->GroupName() : "";
+ const std::string strPlayingRadioGroup =
+ (bStarted && bIsPlayingRadio) ? state->GetActiveChannelGroup(true)->GroupName() : "";
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strPlayingClientName = strPlayingClientName;
+ m_bHasTVRecordings = bHasTVRecordings;
+ m_bHasRadioRecordings = bHasRadioRecordings;
+ m_bIsPlayingTV = bIsPlayingTV;
+ m_bIsPlayingRadio = bIsPlayingRadio;
+ m_bIsPlayingRecording = bIsPlayingRecording;
+ m_bIsPlayingEpgTag = bIsPlayingEpgTag;
+ m_bIsPlayingEncryptedStream = bIsPlayingEncryptedStream;
+ m_bHasTVChannels = bHasTVChannels;
+ m_bHasRadioChannels = bHasRadioChannels;
+ m_strPlayingTVGroup = strPlayingTVGroup;
+ m_strPlayingRadioGroup = strPlayingRadioGroup;
+ m_bCanRecordPlayingChannel = bCanRecordPlayingChannel;
+ m_bIsRecordingPlayingChannel = bIsRecordingPlayingChannel;
+ m_bIsPlayingActiveRecording = bIsPlayingActiveRecording;
+}
+
+void CPVRGUIInfo::UpdateTimeshiftData()
+{
+ m_timesInfo.Update();
+}
+
+bool CPVRGUIInfo::InitCurrentItem(CFileItem* item)
+{
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::CurrentItem);
+ return false;
+}
+
+bool CPVRGUIInfo::GetLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const CGUIInfo& info,
+ std::string* fallback) const
+{
+ return GetListItemAndPlayerLabel(item, info, value) || GetPVRLabel(item, info, value) ||
+ GetRadioRDSLabel(item, info, value);
+}
+
+namespace
+{
+std::string GetAsLocalizedDateString(const CDateTime& datetime, bool bLongDate)
+{
+ return datetime.IsValid() ? datetime.GetAsLocalizedDate(bLongDate) : "";
+}
+
+std::string GetAsLocalizedTimeString(const CDateTime& datetime)
+{
+ return datetime.IsValid() ? datetime.GetAsLocalizedTime("", false) : "";
+}
+
+std::string GetAsLocalizedDateTimeString(const CDateTime& datetime)
+{
+ return datetime.IsValid() ? datetime.GetAsLocalizedDateTime(false, false) : "";
+}
+
+std::string GetEpgTagTitle(const std::shared_ptr<CPVREpgInfoTag>& epgTag)
+{
+ if (epgTag)
+ {
+ if (CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ return g_localizeStrings.Get(19266); // Parental locked
+ else if (!epgTag->Title().empty())
+ return epgTag->Title();
+ }
+
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_EPG_HIDENOINFOAVAILABLE))
+ return g_localizeStrings.Get(19055); // no information available
+
+ return {};
+}
+
+} // unnamed namespace
+
+bool CPVRGUIInfo::GetListItemAndPlayerLabel(const CFileItem* item,
+ const CGUIInfo& info,
+ std::string& strValue) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer = item->GetPVRTimerInfoTag();
+ if (timer)
+ {
+ switch (info.m_info)
+ {
+ case LISTITEM_DATE:
+ strValue = timer->Summary();
+ return true;
+ case LISTITEM_STARTDATE:
+ strValue = GetAsLocalizedDateString(timer->StartAsLocalTime(), true);
+ return true;
+ case LISTITEM_STARTTIME:
+ strValue = GetAsLocalizedTimeString(timer->StartAsLocalTime());
+ return true;
+ case LISTITEM_ENDDATE:
+ strValue = GetAsLocalizedDateString(timer->EndAsLocalTime(), true);
+ return true;
+ case LISTITEM_ENDTIME:
+ strValue = GetAsLocalizedTimeString(timer->EndAsLocalTime());
+ return true;
+ case LISTITEM_DURATION:
+ if (timer->GetDuration() > 0)
+ {
+ strValue = StringUtils::SecondsToTimeString(timer->GetDuration(),
+ static_cast<TIME_FORMAT>(info.GetData4()));
+ return true;
+ }
+ return false;
+ case LISTITEM_TITLE:
+ strValue = timer->Title();
+ return true;
+ case LISTITEM_COMMENT:
+ strValue =
+ timer->GetStatus(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() ==
+ WINDOW_RADIO_TIMER_RULES);
+ return true;
+ case LISTITEM_TIMERTYPE:
+ strValue = timer->GetTypeAsString();
+ return true;
+ case LISTITEM_CHANNEL_NAME:
+ strValue = timer->ChannelName();
+ return true;
+ case LISTITEM_EPG_EVENT_TITLE:
+ case LISTITEM_EPG_EVENT_ICON:
+ case LISTITEM_GENRE:
+ case LISTITEM_PLOT:
+ case LISTITEM_PLOT_OUTLINE:
+ case LISTITEM_ORIGINALTITLE:
+ case LISTITEM_YEAR:
+ case LISTITEM_SEASON:
+ case LISTITEM_EPISODE:
+ case LISTITEM_EPISODENAME:
+ case LISTITEM_DIRECTOR:
+ case LISTITEM_CHANNEL_NUMBER:
+ case LISTITEM_PREMIERED:
+ break; // obtain value from channel/epg
+ default:
+ return false;
+ }
+ }
+
+ const std::shared_ptr<CPVRRecording> recording(item->GetPVRRecordingInfoTag());
+ if (recording)
+ {
+ // Note: CPVRRecoding is derived from CVideoInfoTag. All base class properties will be handled
+ // by CVideoGUIInfoProvider. Only properties introduced by CPVRRecording need to be handled here.
+ switch (info.m_info)
+ {
+ case LISTITEM_DATE:
+ strValue = GetAsLocalizedDateTimeString(recording->RecordingTimeAsLocalTime());
+ return true;
+ case LISTITEM_STARTDATE:
+ strValue = GetAsLocalizedDateString(recording->RecordingTimeAsLocalTime(), true);
+ return true;
+ case VIDEOPLAYER_STARTTIME:
+ case LISTITEM_STARTTIME:
+ strValue = GetAsLocalizedTimeString(recording->RecordingTimeAsLocalTime());
+ return true;
+ case LISTITEM_ENDDATE:
+ strValue = GetAsLocalizedDateString(recording->EndTimeAsLocalTime(), true);
+ return true;
+ case VIDEOPLAYER_ENDTIME:
+ case LISTITEM_ENDTIME:
+ strValue = GetAsLocalizedTimeString(recording->EndTimeAsLocalTime());
+ return true;
+ case LISTITEM_EXPIRATION_DATE:
+ if (recording->HasExpirationTime())
+ {
+ strValue = GetAsLocalizedDateString(recording->ExpirationTimeAsLocalTime(), false);
+ return true;
+ }
+ break;
+ case LISTITEM_EXPIRATION_TIME:
+ if (recording->HasExpirationTime())
+ {
+ strValue = GetAsLocalizedTimeString(recording->ExpirationTimeAsLocalTime());
+ ;
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_EPISODENAME:
+ case LISTITEM_EPISODENAME:
+ strValue = recording->EpisodeName();
+ // fixup multiline episode name strings (which do not fit in any way in our GUI)
+ StringUtils::Replace(strValue, "\n", ", ");
+ return true;
+ case VIDEOPLAYER_CHANNEL_NAME:
+ case LISTITEM_CHANNEL_NAME:
+ strValue = recording->ChannelName();
+ if (strValue.empty())
+ {
+ if (recording->ProviderName().empty())
+ {
+ const auto& provider = recording->GetProvider();
+ strValue = provider->GetName();
+ }
+ else
+ {
+ strValue = recording->ProviderName();
+ }
+ }
+ return true;
+ case VIDEOPLAYER_CHANNEL_NUMBER:
+ case LISTITEM_CHANNEL_NUMBER:
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(*item);
+ if (groupMember)
+ {
+ strValue = groupMember->ChannelNumber().FormattedChannelNumber();
+ return true;
+ }
+ break;
+ }
+ case LISTITEM_ICON:
+ if (recording->ClientIconPath().empty() && recording->ClientThumbnailPath().empty() &&
+ // Only use a fallback if there is more than a single provider available
+ // Note: an add-on itself is a provider, hence we don't use > 0
+ CServiceBroker::GetPVRManager().Providers()->GetNumProviders() > 1 &&
+ !recording->Channel())
+ {
+ auto provider = recording->GetProvider();
+ if (!provider->GetIconPath().empty())
+ {
+ strValue = provider->GetIconPath();
+ return true;
+ }
+ }
+ return false;
+ case VIDEOPLAYER_CHANNEL_GROUP:
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ strValue = recording->IsRadio() ? m_strPlayingRadioGroup : m_strPlayingTVGroup;
+ return true;
+ }
+ case VIDEOPLAYER_PREMIERED:
+ case LISTITEM_PREMIERED:
+ if (recording->FirstAired().IsValid())
+ {
+ strValue = recording->FirstAired().GetAsLocalizedDate();
+ return true;
+ }
+ else if (recording->HasYear())
+ {
+ strValue = std::to_string(recording->GetYear());
+ return true;
+ }
+ return false;
+ case LISTITEM_SIZE:
+ if (recording->GetSizeInBytes() > 0)
+ {
+ strValue = StringUtils::SizeToString(recording->GetSizeInBytes());
+ return true;
+ }
+ return false;
+ }
+ return false;
+ }
+
+ const std::shared_ptr<CPVREpgSearchFilter> filter = item->GetEPGSearchFilter();
+ if (filter)
+ {
+ switch (info.m_info)
+ {
+ case LISTITEM_DATE:
+ {
+ CDateTime lastExecLocal;
+ lastExecLocal.SetFromUTCDateTime(filter->GetLastExecutedDateTime());
+ strValue = GetAsLocalizedDateTimeString(lastExecLocal);
+ if (strValue.empty())
+ strValue = g_localizeStrings.Get(10006); // "N/A"
+ return true;
+ }
+ }
+ return false;
+ }
+
+ std::shared_ptr<CPVREpgInfoTag> epgTag;
+ std::shared_ptr<CPVRChannel> channel;
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ CPVRItem pvrItem(item);
+ channel = pvrItem.GetChannel();
+
+ switch (info.m_info)
+ {
+ case VIDEOPLAYER_NEXT_TITLE:
+ case VIDEOPLAYER_NEXT_GENRE:
+ case VIDEOPLAYER_NEXT_PLOT:
+ case VIDEOPLAYER_NEXT_PLOT_OUTLINE:
+ case VIDEOPLAYER_NEXT_STARTTIME:
+ case VIDEOPLAYER_NEXT_ENDTIME:
+ case VIDEOPLAYER_NEXT_DURATION:
+ case LISTITEM_NEXT_TITLE:
+ case LISTITEM_NEXT_GENRE:
+ case LISTITEM_NEXT_PLOT:
+ case LISTITEM_NEXT_PLOT_OUTLINE:
+ case LISTITEM_NEXT_STARTDATE:
+ case LISTITEM_NEXT_STARTTIME:
+ case LISTITEM_NEXT_ENDDATE:
+ case LISTITEM_NEXT_ENDTIME:
+ case LISTITEM_NEXT_DURATION:
+ // next playing event
+ epgTag = pvrItem.GetNextEpgInfoTag();
+ break;
+ default:
+ // now playing event
+ epgTag = pvrItem.GetEpgInfoTag();
+ break;
+ }
+
+ switch (info.m_info)
+ {
+ // special handling for channels without epg or with radio rds data
+ case PLAYER_TITLE:
+ case LISTITEM_TITLE:
+ case VIDEOPLAYER_NEXT_TITLE:
+ case LISTITEM_NEXT_TITLE:
+ case LISTITEM_EPG_EVENT_TITLE:
+ // Note: in difference to LISTITEM_TITLE, LISTITEM_EPG_EVENT_TITLE returns the title
+ // associated with the epg event of a timer, if any, and not the title of the timer.
+ strValue = GetEpgTagTitle(epgTag);
+ return true;
+ }
+ }
+
+ if (epgTag)
+ {
+ switch (info.m_info)
+ {
+ case VIDEOPLAYER_GENRE:
+ case LISTITEM_GENRE:
+ case VIDEOPLAYER_NEXT_GENRE:
+ case LISTITEM_NEXT_GENRE:
+ strValue = epgTag->GetGenresLabel();
+ return true;
+ case VIDEOPLAYER_PLOT:
+ case LISTITEM_PLOT:
+ case VIDEOPLAYER_NEXT_PLOT:
+ case LISTITEM_NEXT_PLOT:
+ if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ strValue = epgTag->Plot();
+ return true;
+ case VIDEOPLAYER_PLOT_OUTLINE:
+ case LISTITEM_PLOT_OUTLINE:
+ case VIDEOPLAYER_NEXT_PLOT_OUTLINE:
+ case LISTITEM_NEXT_PLOT_OUTLINE:
+ if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ strValue = epgTag->PlotOutline();
+ return true;
+ case LISTITEM_DATE:
+ strValue = GetAsLocalizedDateTimeString(epgTag->StartAsLocalTime());
+ return true;
+ case LISTITEM_STARTDATE:
+ case LISTITEM_NEXT_STARTDATE:
+ strValue = GetAsLocalizedDateString(epgTag->StartAsLocalTime(), true);
+ return true;
+ case VIDEOPLAYER_STARTTIME:
+ case VIDEOPLAYER_NEXT_STARTTIME:
+ case LISTITEM_STARTTIME:
+ case LISTITEM_NEXT_STARTTIME:
+ strValue = GetAsLocalizedTimeString(epgTag->StartAsLocalTime());
+ return true;
+ case LISTITEM_ENDDATE:
+ case LISTITEM_NEXT_ENDDATE:
+ strValue = GetAsLocalizedDateString(epgTag->EndAsLocalTime(), true);
+ return true;
+ case VIDEOPLAYER_ENDTIME:
+ case VIDEOPLAYER_NEXT_ENDTIME:
+ case LISTITEM_ENDTIME:
+ case LISTITEM_NEXT_ENDTIME:
+ strValue = GetAsLocalizedTimeString(epgTag->EndAsLocalTime());
+ return true;
+ // note: for some reason, there is no VIDEOPLAYER_DURATION
+ case LISTITEM_DURATION:
+ case VIDEOPLAYER_NEXT_DURATION:
+ case LISTITEM_NEXT_DURATION:
+ if (epgTag->GetDuration() > 0)
+ {
+ strValue = StringUtils::SecondsToTimeString(epgTag->GetDuration(),
+ static_cast<TIME_FORMAT>(info.GetData4()));
+ return true;
+ }
+ return false;
+ case VIDEOPLAYER_IMDBNUMBER:
+ case LISTITEM_IMDBNUMBER:
+ strValue = epgTag->IMDBNumber();
+ return true;
+ case VIDEOPLAYER_ORIGINALTITLE:
+ case LISTITEM_ORIGINALTITLE:
+ if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ strValue = epgTag->OriginalTitle();
+ return true;
+ case VIDEOPLAYER_YEAR:
+ case LISTITEM_YEAR:
+ if (epgTag->Year() > 0)
+ {
+ strValue = std::to_string(epgTag->Year());
+ return true;
+ }
+ return false;
+ case VIDEOPLAYER_SEASON:
+ case LISTITEM_SEASON:
+ if (epgTag->SeriesNumber() >= 0)
+ {
+ strValue = std::to_string(epgTag->SeriesNumber());
+ return true;
+ }
+ return false;
+ case VIDEOPLAYER_EPISODE:
+ case LISTITEM_EPISODE:
+ if (epgTag->EpisodeNumber() >= 0)
+ {
+ strValue = std::to_string(epgTag->EpisodeNumber());
+ return true;
+ }
+ return false;
+ case VIDEOPLAYER_EPISODENAME:
+ case LISTITEM_EPISODENAME:
+ if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ {
+ strValue = epgTag->EpisodeName();
+ // fixup multiline episode name strings (which do not fit in any way in our GUI)
+ StringUtils::Replace(strValue, "\n", ", ");
+ }
+ return true;
+ case VIDEOPLAYER_CAST:
+ case LISTITEM_CAST:
+ strValue = epgTag->GetCastLabel();
+ return true;
+ case VIDEOPLAYER_DIRECTOR:
+ case LISTITEM_DIRECTOR:
+ strValue = epgTag->GetDirectorsLabel();
+ return true;
+ case VIDEOPLAYER_WRITER:
+ case LISTITEM_WRITER:
+ strValue = epgTag->GetWritersLabel();
+ return true;
+ case LISTITEM_EPG_EVENT_ICON:
+ strValue = epgTag->IconPath();
+ return true;
+ case VIDEOPLAYER_PARENTAL_RATING:
+ case LISTITEM_PARENTAL_RATING:
+ if (epgTag->ParentalRating() > 0)
+ {
+ strValue = std::to_string(epgTag->ParentalRating());
+ return true;
+ }
+ return false;
+ case VIDEOPLAYER_PREMIERED:
+ case LISTITEM_PREMIERED:
+ if (epgTag->FirstAired().IsValid())
+ {
+ strValue = epgTag->FirstAired().GetAsLocalizedDate();
+ return true;
+ }
+ else if (epgTag->Year() > 0)
+ {
+ strValue = std::to_string(epgTag->Year());
+ return true;
+ }
+ return false;
+ case VIDEOPLAYER_RATING:
+ case LISTITEM_RATING:
+ {
+ int iStarRating = epgTag->StarRating();
+ if (iStarRating > 0)
+ {
+ strValue = StringUtils::FormatNumber(iStarRating);
+ return true;
+ }
+ return false;
+ }
+ }
+ }
+
+ if (channel)
+ {
+ switch (info.m_info)
+ {
+ case MUSICPLAYER_CHANNEL_NAME:
+ {
+ const std::shared_ptr<CPVRRadioRDSInfoTag> rdsTag = channel->GetRadioRDSInfoTag();
+ if (rdsTag)
+ {
+ strValue = rdsTag->GetProgStation();
+ if (!strValue.empty())
+ return true;
+ }
+ // fall-thru is intended
+ [[fallthrough]];
+ }
+ case VIDEOPLAYER_CHANNEL_NAME:
+ case LISTITEM_CHANNEL_NAME:
+ strValue = channel->ChannelName();
+ return true;
+ case MUSICPLAYER_CHANNEL_NUMBER:
+ case VIDEOPLAYER_CHANNEL_NUMBER:
+ case LISTITEM_CHANNEL_NUMBER:
+ {
+ auto groupMember = item->GetPVRChannelGroupMemberInfoTag();
+ if (!groupMember)
+ groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(
+ *item);
+ if (groupMember)
+ {
+ strValue = groupMember->ChannelNumber().FormattedChannelNumber();
+ return true;
+ }
+ break;
+ }
+ case MUSICPLAYER_CHANNEL_GROUP:
+ case VIDEOPLAYER_CHANNEL_GROUP:
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ strValue = channel->IsRadio() ? m_strPlayingRadioGroup : m_strPlayingTVGroup;
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool CPVRGUIInfo::GetPVRLabel(const CFileItem* item,
+ const CGUIInfo& info,
+ std::string& strValue) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ switch (info.m_info)
+ {
+ case PVR_EPG_EVENT_ICON:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ if (epgTag)
+ {
+ strValue = epgTag->IconPath();
+ }
+ return true;
+ }
+ case PVR_EPG_EVENT_DURATION:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ strValue = m_timesInfo.GetEpgEventDuration(epgTag, static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ }
+ case PVR_EPG_EVENT_ELAPSED_TIME:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ strValue =
+ m_timesInfo.GetEpgEventElapsedTime(epgTag, static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ }
+ case PVR_EPG_EVENT_REMAINING_TIME:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ strValue =
+ m_timesInfo.GetEpgEventRemainingTime(epgTag, static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ }
+ case PVR_EPG_EVENT_FINISH_TIME:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ strValue =
+ m_timesInfo.GetEpgEventFinishTime(epgTag, static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ }
+ case PVR_TIMESHIFT_START_TIME:
+ strValue = m_timesInfo.GetTimeshiftStartTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_TIMESHIFT_END_TIME:
+ strValue = m_timesInfo.GetTimeshiftEndTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_TIMESHIFT_PLAY_TIME:
+ strValue = m_timesInfo.GetTimeshiftPlayTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_TIMESHIFT_OFFSET:
+ strValue = m_timesInfo.GetTimeshiftOffset(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_DURATION:
+ strValue =
+ m_timesInfo.GetTimeshiftProgressDuration(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_START_TIME:
+ strValue =
+ m_timesInfo.GetTimeshiftProgressStartTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_END_TIME:
+ strValue = m_timesInfo.GetTimeshiftProgressEndTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_EPG_EVENT_SEEK_TIME:
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ strValue = m_timesInfo.GetEpgEventSeekTime(appPlayer->GetSeekHandler().GetSeekSize(),
+ static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ }
+ case PVR_NOW_RECORDING_TITLE:
+ strValue = m_anyTimersInfo.GetActiveTimerTitle();
+ return true;
+ case PVR_NOW_RECORDING_CHANNEL:
+ strValue = m_anyTimersInfo.GetActiveTimerChannelName();
+ return true;
+ case PVR_NOW_RECORDING_CHAN_ICO:
+ strValue = m_anyTimersInfo.GetActiveTimerChannelIcon();
+ return true;
+ case PVR_NOW_RECORDING_DATETIME:
+ strValue = m_anyTimersInfo.GetActiveTimerDateTime();
+ return true;
+ case PVR_NEXT_RECORDING_TITLE:
+ strValue = m_anyTimersInfo.GetNextTimerTitle();
+ return true;
+ case PVR_NEXT_RECORDING_CHANNEL:
+ strValue = m_anyTimersInfo.GetNextTimerChannelName();
+ return true;
+ case PVR_NEXT_RECORDING_CHAN_ICO:
+ strValue = m_anyTimersInfo.GetNextTimerChannelIcon();
+ return true;
+ case PVR_NEXT_RECORDING_DATETIME:
+ strValue = m_anyTimersInfo.GetNextTimerDateTime();
+ return true;
+ case PVR_TV_NOW_RECORDING_TITLE:
+ strValue = m_tvTimersInfo.GetActiveTimerTitle();
+ return true;
+ case PVR_TV_NOW_RECORDING_CHANNEL:
+ strValue = m_tvTimersInfo.GetActiveTimerChannelName();
+ return true;
+ case PVR_TV_NOW_RECORDING_CHAN_ICO:
+ strValue = m_tvTimersInfo.GetActiveTimerChannelIcon();
+ return true;
+ case PVR_TV_NOW_RECORDING_DATETIME:
+ strValue = m_tvTimersInfo.GetActiveTimerDateTime();
+ return true;
+ case PVR_TV_NEXT_RECORDING_TITLE:
+ strValue = m_tvTimersInfo.GetNextTimerTitle();
+ return true;
+ case PVR_TV_NEXT_RECORDING_CHANNEL:
+ strValue = m_tvTimersInfo.GetNextTimerChannelName();
+ return true;
+ case PVR_TV_NEXT_RECORDING_CHAN_ICO:
+ strValue = m_tvTimersInfo.GetNextTimerChannelIcon();
+ return true;
+ case PVR_TV_NEXT_RECORDING_DATETIME:
+ strValue = m_tvTimersInfo.GetNextTimerDateTime();
+ return true;
+ case PVR_RADIO_NOW_RECORDING_TITLE:
+ strValue = m_radioTimersInfo.GetActiveTimerTitle();
+ return true;
+ case PVR_RADIO_NOW_RECORDING_CHANNEL:
+ strValue = m_radioTimersInfo.GetActiveTimerChannelName();
+ return true;
+ case PVR_RADIO_NOW_RECORDING_CHAN_ICO:
+ strValue = m_radioTimersInfo.GetActiveTimerChannelIcon();
+ return true;
+ case PVR_RADIO_NOW_RECORDING_DATETIME:
+ strValue = m_radioTimersInfo.GetActiveTimerDateTime();
+ return true;
+ case PVR_RADIO_NEXT_RECORDING_TITLE:
+ strValue = m_radioTimersInfo.GetNextTimerTitle();
+ return true;
+ case PVR_RADIO_NEXT_RECORDING_CHANNEL:
+ strValue = m_radioTimersInfo.GetNextTimerChannelName();
+ return true;
+ case PVR_RADIO_NEXT_RECORDING_CHAN_ICO:
+ strValue = m_radioTimersInfo.GetNextTimerChannelIcon();
+ return true;
+ case PVR_RADIO_NEXT_RECORDING_DATETIME:
+ strValue = m_radioTimersInfo.GetNextTimerDateTime();
+ return true;
+ case PVR_NEXT_TIMER:
+ strValue = m_anyTimersInfo.GetNextTimer();
+ return true;
+ case PVR_ACTUAL_STREAM_SIG:
+ CharInfoSignal(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_SNR:
+ CharInfoSNR(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_BER:
+ CharInfoBER(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_UNC:
+ CharInfoUNC(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_CLIENT:
+ CharInfoPlayingClientName(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_DEVICE:
+ CharInfoFrontendName(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_STATUS:
+ CharInfoFrontendStatus(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_CRYPTION:
+ CharInfoEncryption(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_SERVICE:
+ CharInfoService(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_MUX:
+ CharInfoMux(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_PROVIDER:
+ CharInfoProvider(strValue);
+ return true;
+ case PVR_BACKEND_NAME:
+ CharInfoBackendName(strValue);
+ return true;
+ case PVR_BACKEND_VERSION:
+ CharInfoBackendVersion(strValue);
+ return true;
+ case PVR_BACKEND_HOST:
+ CharInfoBackendHost(strValue);
+ return true;
+ case PVR_BACKEND_DISKSPACE:
+ CharInfoBackendDiskspace(strValue);
+ return true;
+ case PVR_BACKEND_PROVIDERS:
+ CharInfoBackendProviders(strValue);
+ return true;
+ case PVR_BACKEND_CHANNEL_GROUPS:
+ CharInfoBackendChannelGroups(strValue);
+ return true;
+ case PVR_BACKEND_CHANNELS:
+ CharInfoBackendChannels(strValue);
+ return true;
+ case PVR_BACKEND_TIMERS:
+ CharInfoBackendTimers(strValue);
+ return true;
+ case PVR_BACKEND_RECORDINGS:
+ CharInfoBackendRecordings(strValue);
+ return true;
+ case PVR_BACKEND_DELETED_RECORDINGS:
+ CharInfoBackendDeletedRecordings(strValue);
+ return true;
+ case PVR_BACKEND_NUMBER:
+ CharInfoBackendNumber(strValue);
+ return true;
+ case PVR_TOTAL_DISKSPACE:
+ CharInfoTotalDiskSpace(strValue);
+ return true;
+ case PVR_CHANNEL_NUMBER_INPUT:
+ strValue = m_channelNumberInput;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRGUIInfo::GetRadioRDSLabel(const CFileItem* item,
+ const CGUIInfo& info,
+ std::string& strValue) const
+{
+ if (!item->HasPVRChannelInfoTag())
+ return false;
+
+ const std::shared_ptr<CPVRRadioRDSInfoTag> tag =
+ item->GetPVRChannelInfoTag()->GetRadioRDSInfoTag();
+ if (tag)
+ {
+ switch (info.m_info)
+ {
+ case RDS_CHANNEL_COUNTRY:
+ strValue = tag->GetCountry();
+ return true;
+ case RDS_TITLE:
+ strValue = tag->GetTitle();
+ return true;
+ case RDS_ARTIST:
+ strValue = tag->GetArtist();
+ return true;
+ case RDS_BAND:
+ strValue = tag->GetBand();
+ return true;
+ case RDS_COMPOSER:
+ strValue = tag->GetComposer();
+ return true;
+ case RDS_CONDUCTOR:
+ strValue = tag->GetConductor();
+ return true;
+ case RDS_ALBUM:
+ strValue = tag->GetAlbum();
+ return true;
+ case RDS_ALBUM_TRACKNUMBER:
+ if (tag->GetAlbumTrackNumber() > 0)
+ {
+ strValue = std::to_string(tag->GetAlbumTrackNumber());
+ return true;
+ }
+ break;
+ case RDS_GET_RADIO_STYLE:
+ strValue = tag->GetRadioStyle();
+ return true;
+ case RDS_COMMENT:
+ strValue = tag->GetComment();
+ return true;
+ case RDS_INFO_NEWS:
+ strValue = tag->GetInfoNews();
+ return true;
+ case RDS_INFO_NEWS_LOCAL:
+ strValue = tag->GetInfoNewsLocal();
+ return true;
+ case RDS_INFO_STOCK:
+ strValue = tag->GetInfoStock();
+ return true;
+ case RDS_INFO_STOCK_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoStock().size()));
+ return true;
+ case RDS_INFO_SPORT:
+ strValue = tag->GetInfoSport();
+ return true;
+ case RDS_INFO_SPORT_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoSport().size()));
+ return true;
+ case RDS_INFO_LOTTERY:
+ strValue = tag->GetInfoLottery();
+ return true;
+ case RDS_INFO_LOTTERY_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoLottery().size()));
+ return true;
+ case RDS_INFO_WEATHER:
+ strValue = tag->GetInfoWeather();
+ return true;
+ case RDS_INFO_WEATHER_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoWeather().size()));
+ return true;
+ case RDS_INFO_HOROSCOPE:
+ strValue = tag->GetInfoHoroscope();
+ return true;
+ case RDS_INFO_HOROSCOPE_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoHoroscope().size()));
+ return true;
+ case RDS_INFO_CINEMA:
+ strValue = tag->GetInfoCinema();
+ return true;
+ case RDS_INFO_CINEMA_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoCinema().size()));
+ return true;
+ case RDS_INFO_OTHER:
+ strValue = tag->GetInfoOther();
+ return true;
+ case RDS_INFO_OTHER_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoOther().size()));
+ return true;
+ case RDS_PROG_HOST:
+ strValue = tag->GetProgHost();
+ return true;
+ case RDS_PROG_EDIT_STAFF:
+ strValue = tag->GetEditorialStaff();
+ return true;
+ case RDS_PROG_HOMEPAGE:
+ strValue = tag->GetProgWebsite();
+ return true;
+ case RDS_PROG_STYLE:
+ strValue = tag->GetProgStyle();
+ return true;
+ case RDS_PHONE_HOTLINE:
+ strValue = tag->GetPhoneHotline();
+ return true;
+ case RDS_PHONE_STUDIO:
+ strValue = tag->GetPhoneStudio();
+ return true;
+ case RDS_SMS_STUDIO:
+ strValue = tag->GetSMSStudio();
+ return true;
+ case RDS_EMAIL_HOTLINE:
+ strValue = tag->GetEMailHotline();
+ return true;
+ case RDS_EMAIL_STUDIO:
+ strValue = tag->GetEMailStudio();
+ return true;
+ case RDS_PROG_STATION:
+ strValue = tag->GetProgStation();
+ return true;
+ case RDS_PROG_NOW:
+ strValue = tag->GetProgNow();
+ return true;
+ case RDS_PROG_NEXT:
+ strValue = tag->GetProgNext();
+ return true;
+ case RDS_AUDIO_LANG:
+ strValue = tag->GetLanguage();
+ return true;
+ case RDS_GET_RADIOTEXT_LINE:
+ strValue = tag->GetRadioText(info.GetData1());
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CPVRGUIInfo::GetFallbackLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const CGUIInfo& info,
+ std::string* fallback)
+{
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ switch (info.m_info)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // VIDEOPLAYER_*, MUSICPLAYER_*
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ case VIDEOPLAYER_TITLE:
+ case MUSICPLAYER_TITLE:
+ value = GetEpgTagTitle(CPVRItem(item).GetEpgInfoTag());
+ return !value.empty();
+ default:
+ break;
+ }
+ }
+ return false;
+}
+
+bool CPVRGUIInfo::GetInt(int& value,
+ const CGUIListItem* item,
+ int contextWindow,
+ const CGUIInfo& info) const
+{
+ if (!item->IsFileItem())
+ return false;
+
+ const CFileItem* fitem = static_cast<const CFileItem*>(item);
+ return GetListItemAndPlayerInt(fitem, info, value) || GetPVRInt(fitem, info, value);
+}
+
+bool CPVRGUIInfo::GetListItemAndPlayerInt(const CFileItem* item,
+ const CGUIInfo& info,
+ int& iValue) const
+{
+ switch (info.m_info)
+ {
+ case LISTITEM_PROGRESS:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag();
+ if (epgTag)
+ iValue = static_cast<int>(epgTag->ProgressPercentage());
+ }
+ return true;
+ }
+ return false;
+}
+
+bool CPVRGUIInfo::GetPVRInt(const CFileItem* item, const CGUIInfo& info, int& iValue) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ switch (info.m_info)
+ {
+ case PVR_EPG_EVENT_DURATION:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ iValue = m_timesInfo.GetEpgEventDuration(epgTag);
+ return true;
+ }
+ case PVR_EPG_EVENT_PROGRESS:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ iValue = m_timesInfo.GetEpgEventProgress(epgTag);
+ return true;
+ }
+ case PVR_TIMESHIFT_PROGRESS:
+ iValue = m_timesInfo.GetTimeshiftProgress();
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_DURATION:
+ iValue = m_timesInfo.GetTimeshiftProgressDuration();
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_PLAY_POS:
+ iValue = m_timesInfo.GetTimeshiftProgressPlayPosition();
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_EPG_START:
+ iValue = m_timesInfo.GetTimeshiftProgressEpgStart();
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_EPG_END:
+ iValue = m_timesInfo.GetTimeshiftProgressEpgEnd();
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_BUFFER_START:
+ iValue = m_timesInfo.GetTimeshiftProgressBufferStart();
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_BUFFER_END:
+ iValue = m_timesInfo.GetTimeshiftProgressBufferEnd();
+ return true;
+ case PVR_TIMESHIFT_SEEKBAR:
+ iValue = GetTimeShiftSeekPercent();
+ return true;
+ case PVR_ACTUAL_STREAM_SIG_PROGR:
+ iValue = std::lrintf(static_cast<float>(m_qualityInfo.iSignal) / 0xFFFF * 100);
+ return true;
+ case PVR_ACTUAL_STREAM_SNR_PROGR:
+ iValue = std::lrintf(static_cast<float>(m_qualityInfo.iSNR) / 0xFFFF * 100);
+ return true;
+ case PVR_BACKEND_DISKSPACE_PROGR:
+ if (m_iBackendDiskTotal > 0)
+ iValue = std::lrintf(static_cast<float>(m_iBackendDiskUsed) / m_iBackendDiskTotal * 100);
+ else
+ iValue = 0xFF;
+ return true;
+ }
+ return false;
+}
+
+bool CPVRGUIInfo::GetBool(bool& value,
+ const CGUIListItem* item,
+ int contextWindow,
+ const CGUIInfo& info) const
+{
+ if (!item->IsFileItem())
+ return false;
+
+ const CFileItem* fitem = static_cast<const CFileItem*>(item);
+ return GetListItemAndPlayerBool(fitem, info, value) || GetPVRBool(fitem, info, value) ||
+ GetRadioRDSBool(fitem, info, value);
+}
+
+bool CPVRGUIInfo::GetListItemAndPlayerBool(const CFileItem* item,
+ const CGUIInfo& info,
+ bool& bValue) const
+{
+ switch (info.m_info)
+ {
+ case LISTITEM_HASARCHIVE:
+ if (item->IsPVRChannel())
+ {
+ bValue = item->GetPVRChannelInfoTag()->HasArchive();
+ return true;
+ }
+ break;
+ case LISTITEM_ISPLAYABLE:
+ if (item->IsEPG())
+ {
+ bValue = item->GetEPGInfoTag()->IsPlayable();
+ return true;
+ }
+ break;
+ case LISTITEM_ISRECORDING:
+ if (item->IsPVRChannel())
+ {
+ bValue = CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(
+ *item->GetPVRChannelInfoTag());
+ return true;
+ }
+ else if (item->IsEPG() || item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = timer->IsRecording();
+ return true;
+ }
+ else if (item->IsPVRRecording())
+ {
+ bValue = item->GetPVRRecordingInfoTag()->IsInProgress();
+ return true;
+ }
+ break;
+ case LISTITEM_INPROGRESS:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag();
+ if (epgTag)
+ bValue = epgTag->IsActive();
+ return true;
+ }
+ break;
+ case LISTITEM_HASTIMER:
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = true;
+ return true;
+ }
+ break;
+ case LISTITEM_HASTIMERSCHEDULE:
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = timer->HasParent();
+ return true;
+ }
+ break;
+ case LISTITEM_HASREMINDER:
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = timer->IsReminder();
+ return true;
+ }
+ break;
+ case LISTITEM_HASREMINDERRULE:
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = timer->IsReminder() && timer->HasParent();
+ return true;
+ }
+ break;
+ case LISTITEM_TIMERISACTIVE:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = timer->IsActive();
+ break;
+ }
+ break;
+ case LISTITEM_TIMERHASCONFLICT:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = timer->HasConflict();
+ return true;
+ }
+ break;
+ case LISTITEM_TIMERHASERROR:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = (timer->IsBroken() && !timer->HasConflict());
+ return true;
+ }
+ break;
+ case LISTITEM_HASRECORDING:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag();
+ if (epgTag)
+ bValue = !!CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(epgTag);
+ return true;
+ }
+ break;
+ case LISTITEM_HAS_EPG:
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag();
+ bValue = (epgTag != nullptr);
+ return true;
+ }
+ break;
+ case LISTITEM_ISENCRYPTED:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVRChannel> channel = CPVRItem(item).GetChannel();
+ if (channel)
+ bValue = channel->IsEncrypted();
+ return true;
+ }
+ break;
+ case LISTITEM_IS_NEW:
+ if (item->IsEPG())
+ {
+ if (item->GetEPGInfoTag())
+ {
+ bValue = item->GetEPGInfoTag()->IsNew();
+ return true;
+ }
+ }
+ else if (item->IsPVRRecording())
+ {
+ bValue = item->GetPVRRecordingInfoTag()->IsNew();
+ return true;
+ }
+ else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag())
+ {
+ bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsNew();
+ return true;
+ }
+ else if (item->IsPVRChannel())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow();
+ bValue = epgNow ? epgNow->IsNew() : false;
+ return true;
+ }
+ break;
+ case LISTITEM_IS_PREMIERE:
+ if (item->IsEPG())
+ {
+ bValue = item->GetEPGInfoTag()->IsPremiere();
+ return true;
+ }
+ else if (item->IsPVRRecording())
+ {
+ bValue = item->GetPVRRecordingInfoTag()->IsPremiere();
+ return true;
+ }
+ else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag())
+ {
+ bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsPremiere();
+ return true;
+ }
+ else if (item->IsPVRChannel())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow();
+ bValue = epgNow ? epgNow->IsPremiere() : false;
+ return true;
+ }
+ break;
+ case LISTITEM_IS_FINALE:
+ if (item->IsEPG())
+ {
+ bValue = item->GetEPGInfoTag()->IsFinale();
+ return true;
+ }
+ else if (item->IsPVRRecording())
+ {
+ bValue = item->GetPVRRecordingInfoTag()->IsFinale();
+ return true;
+ }
+ else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag())
+ {
+ bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsFinale();
+ return true;
+ }
+ else if (item->IsPVRChannel())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow();
+ bValue = epgNow ? epgNow->IsFinale() : false;
+ return true;
+ }
+ break;
+ case LISTITEM_IS_LIVE:
+ if (item->IsEPG())
+ {
+ bValue = item->GetEPGInfoTag()->IsLive();
+ return true;
+ }
+ else if (item->IsPVRRecording())
+ {
+ bValue = item->GetPVRRecordingInfoTag()->IsLive();
+ return true;
+ }
+ else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag())
+ {
+ bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsLive();
+ return true;
+ }
+ else if (item->IsPVRChannel())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow();
+ bValue = epgNow ? epgNow->IsLive() : false;
+ return true;
+ }
+ break;
+ case MUSICPLAYER_CONTENT:
+ case VIDEOPLAYER_CONTENT:
+ if (item->IsPVRChannel())
+ {
+ bValue = StringUtils::EqualsNoCase(info.GetData3(), "livetv");
+ return bValue; // if no match for this provider, other providers shall be asked.
+ }
+ break;
+ case VIDEOPLAYER_HAS_INFO:
+ if (item->IsPVRChannel())
+ {
+ bValue = !item->GetPVRChannelInfoTag()->ChannelName().empty();
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_HAS_EPG:
+ if (item->IsPVRChannel())
+ {
+ bValue = (item->GetPVRChannelInfoTag()->GetEPGNow() != nullptr);
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_CAN_RESUME_LIVE_TV:
+ if (item->IsPVRRecording())
+ {
+ const std::shared_ptr<CPVRRecording> recording = item->GetPVRRecordingInfoTag();
+ const std::shared_ptr<CPVREpg> epg =
+ recording->Channel() ? recording->Channel()->GetEPG() : nullptr;
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ CServiceBroker::GetPVRManager().EpgContainer().GetTagById(epg,
+ recording->BroadcastUid());
+ bValue = (epgTag && epgTag->IsActive());
+ return true;
+ }
+ break;
+ case PLAYER_IS_CHANNEL_PREVIEW_ACTIVE:
+ if (item->IsPVRChannel())
+ {
+ if (m_previewAndPlayerShowInfo)
+ {
+ bValue = true;
+ }
+ else
+ {
+ bValue = !m_videoInfo.valid;
+ if (bValue && item->GetPVRChannelInfoTag()->IsRadio())
+ bValue = !m_audioInfo.valid;
+ }
+ return true;
+ }
+ break;
+ }
+ return false;
+}
+
+bool CPVRGUIInfo::GetPVRBool(const CFileItem* item, const CGUIInfo& info, bool& bValue) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ switch (info.m_info)
+ {
+ case PVR_IS_RECORDING:
+ bValue = m_anyTimersInfo.HasRecordingTimers();
+ return true;
+ case PVR_IS_RECORDING_TV:
+ bValue = m_tvTimersInfo.HasRecordingTimers();
+ return true;
+ case PVR_IS_RECORDING_RADIO:
+ bValue = m_radioTimersInfo.HasRecordingTimers();
+ return true;
+ case PVR_HAS_TIMER:
+ bValue = m_anyTimersInfo.HasTimers();
+ return true;
+ case PVR_HAS_TV_TIMER:
+ bValue = m_tvTimersInfo.HasTimers();
+ return true;
+ case PVR_HAS_RADIO_TIMER:
+ bValue = m_radioTimersInfo.HasTimers();
+ return true;
+ case PVR_HAS_TV_CHANNELS:
+ bValue = m_bHasTVChannels;
+ return true;
+ case PVR_HAS_RADIO_CHANNELS:
+ bValue = m_bHasRadioChannels;
+ return true;
+ case PVR_HAS_NONRECORDING_TIMER:
+ bValue = m_anyTimersInfo.HasNonRecordingTimers();
+ return true;
+ case PVR_HAS_NONRECORDING_TV_TIMER:
+ bValue = m_tvTimersInfo.HasNonRecordingTimers();
+ return true;
+ case PVR_HAS_NONRECORDING_RADIO_TIMER:
+ bValue = m_radioTimersInfo.HasNonRecordingTimers();
+ return true;
+ case PVR_IS_PLAYING_TV:
+ bValue = m_bIsPlayingTV;
+ return true;
+ case PVR_IS_PLAYING_RADIO:
+ bValue = m_bIsPlayingRadio;
+ return true;
+ case PVR_IS_PLAYING_RECORDING:
+ bValue = m_bIsPlayingRecording;
+ return true;
+ case PVR_IS_PLAYING_EPGTAG:
+ bValue = m_bIsPlayingEpgTag;
+ return true;
+ case PVR_ACTUAL_STREAM_ENCRYPTED:
+ bValue = m_bIsPlayingEncryptedStream;
+ return true;
+ case PVR_IS_TIMESHIFTING:
+ bValue = m_timesInfo.IsTimeshifting();
+ return true;
+ case PVR_CAN_RECORD_PLAYING_CHANNEL:
+ bValue = m_bCanRecordPlayingChannel;
+ return true;
+ case PVR_IS_RECORDING_PLAYING_CHANNEL:
+ bValue = m_bIsRecordingPlayingChannel;
+ return true;
+ case PVR_IS_PLAYING_ACTIVE_RECORDING:
+ bValue = m_bIsPlayingActiveRecording;
+ return true;
+ }
+ return false;
+}
+
+bool CPVRGUIInfo::GetRadioRDSBool(const CFileItem* item, const CGUIInfo& info, bool& bValue) const
+{
+ if (!item->HasPVRChannelInfoTag())
+ return false;
+
+ const std::shared_ptr<CPVRRadioRDSInfoTag> tag =
+ item->GetPVRChannelInfoTag()->GetRadioRDSInfoTag();
+ if (tag)
+ {
+ switch (info.m_info)
+ {
+ case RDS_HAS_RADIOTEXT:
+ bValue = tag->IsPlayingRadioText();
+ return true;
+ case RDS_HAS_RADIOTEXT_PLUS:
+ bValue = tag->IsPlayingRadioTextPlus();
+ return true;
+ case RDS_HAS_HOTLINE_DATA:
+ bValue = (!tag->GetEMailHotline().empty() || !tag->GetPhoneHotline().empty());
+ return true;
+ case RDS_HAS_STUDIO_DATA:
+ bValue = (!tag->GetEMailStudio().empty() || !tag->GetSMSStudio().empty() ||
+ !tag->GetPhoneStudio().empty());
+ return true;
+ }
+ }
+
+ switch (info.m_info)
+ {
+ case RDS_HAS_RDS:
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ bValue = appPlayer->IsPlayingRDS();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CPVRGUIInfo::CharInfoBackendNumber(std::string& strValue) const
+{
+ size_t numBackends = m_backendProperties.size();
+
+ if (numBackends > 0)
+ strValue = StringUtils::Format("{0} {1} {2}", m_iCurrentActiveClient + 1,
+ g_localizeStrings.Get(20163), numBackends);
+ else
+ strValue = g_localizeStrings.Get(14023);
+}
+
+void CPVRGUIInfo::CharInfoTotalDiskSpace(std::string& strValue) const
+{
+ strValue = StringUtils::SizeToString(m_iBackendDiskTotal).c_str();
+}
+
+void CPVRGUIInfo::CharInfoSignal(std::string& strValue) const
+{
+ strValue = StringUtils::Format("{} %", m_qualityInfo.iSignal / 655);
+}
+
+void CPVRGUIInfo::CharInfoSNR(std::string& strValue) const
+{
+ strValue = StringUtils::Format("{} %", m_qualityInfo.iSNR / 655);
+}
+
+void CPVRGUIInfo::CharInfoBER(std::string& strValue) const
+{
+ strValue = StringUtils::Format("{:08X}", m_qualityInfo.iBER);
+}
+
+void CPVRGUIInfo::CharInfoUNC(std::string& strValue) const
+{
+ strValue = StringUtils::Format("{:08X}", m_qualityInfo.iUNC);
+}
+
+void CPVRGUIInfo::CharInfoFrontendName(std::string& strValue) const
+{
+ if (!strlen(m_qualityInfo.strAdapterName))
+ strValue = g_localizeStrings.Get(13205);
+ else
+ strValue = m_qualityInfo.strAdapterName;
+}
+
+void CPVRGUIInfo::CharInfoFrontendStatus(std::string& strValue) const
+{
+ if (!strlen(m_qualityInfo.strAdapterStatus))
+ strValue = g_localizeStrings.Get(13205);
+ else
+ strValue = m_qualityInfo.strAdapterStatus;
+}
+
+void CPVRGUIInfo::CharInfoBackendName(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendName;
+}
+
+void CPVRGUIInfo::CharInfoBackendVersion(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendVersion;
+}
+
+void CPVRGUIInfo::CharInfoBackendHost(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendHost;
+}
+
+void CPVRGUIInfo::CharInfoBackendDiskspace(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+
+ auto diskTotal = m_iBackendDiskTotal;
+ auto diskUsed = m_iBackendDiskUsed;
+
+ if (diskTotal > 0)
+ {
+ strValue = StringUtils::Format(g_localizeStrings.Get(802),
+ StringUtils::SizeToString(diskTotal - diskUsed),
+ StringUtils::SizeToString(diskTotal));
+ }
+ else
+ strValue = g_localizeStrings.Get(13205);
+}
+
+void CPVRGUIInfo::CharInfoBackendProviders(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendProviders;
+}
+
+void CPVRGUIInfo::CharInfoBackendChannelGroups(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendChannelGroups;
+}
+
+void CPVRGUIInfo::CharInfoBackendChannels(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendChannels;
+}
+
+void CPVRGUIInfo::CharInfoBackendTimers(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendTimers;
+}
+
+void CPVRGUIInfo::CharInfoBackendRecordings(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendRecordings;
+}
+
+void CPVRGUIInfo::CharInfoBackendDeletedRecordings(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendDeletedRecordings;
+}
+
+void CPVRGUIInfo::CharInfoPlayingClientName(std::string& strValue) const
+{
+ if (m_strPlayingClientName.empty())
+ strValue = g_localizeStrings.Get(13205);
+ else
+ strValue = m_strPlayingClientName;
+}
+
+void CPVRGUIInfo::CharInfoEncryption(std::string& strValue) const
+{
+ if (m_descrambleInfo.iCaid != PVR_DESCRAMBLE_INFO_NOT_AVAILABLE)
+ {
+ // prefer dynamically updated info, if available
+ strValue = CPVRChannel::GetEncryptionName(m_descrambleInfo.iCaid);
+ return;
+ }
+ else
+ {
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (channel)
+ {
+ strValue = channel->EncryptionName();
+ return;
+ }
+ }
+
+ strValue.clear();
+}
+
+void CPVRGUIInfo::CharInfoService(std::string& strValue) const
+{
+ if (!strlen(m_qualityInfo.strServiceName))
+ strValue = g_localizeStrings.Get(13205);
+ else
+ strValue = m_qualityInfo.strServiceName;
+}
+
+void CPVRGUIInfo::CharInfoMux(std::string& strValue) const
+{
+ if (!strlen(m_qualityInfo.strMuxName))
+ strValue = g_localizeStrings.Get(13205);
+ else
+ strValue = m_qualityInfo.strMuxName;
+}
+
+void CPVRGUIInfo::CharInfoProvider(std::string& strValue) const
+{
+ if (!strlen(m_qualityInfo.strProviderName))
+ strValue = g_localizeStrings.Get(13205);
+ else
+ strValue = m_qualityInfo.strProviderName;
+}
+
+void CPVRGUIInfo::UpdateBackendCache()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // Update the backend information for all backends if
+ // an update has been requested
+ if (m_iCurrentActiveClient == 0 && m_updateBackendCacheRequested)
+ {
+ std::vector<SBackend> backendProperties;
+ {
+ CSingleExit exit(m_critSection);
+ backendProperties = CServiceBroker::GetPVRManager().Clients()->GetBackendProperties();
+ }
+
+ m_backendProperties = backendProperties;
+ m_updateBackendCacheRequested = false;
+ }
+
+ // Store some defaults
+ m_strBackendName = g_localizeStrings.Get(13205);
+ m_strBackendVersion = g_localizeStrings.Get(13205);
+ m_strBackendHost = g_localizeStrings.Get(13205);
+ m_strBackendProviders = g_localizeStrings.Get(13205);
+ m_strBackendChannelGroups = g_localizeStrings.Get(13205);
+ m_strBackendChannels = g_localizeStrings.Get(13205);
+ m_strBackendTimers = g_localizeStrings.Get(13205);
+ m_strBackendRecordings = g_localizeStrings.Get(13205);
+ m_strBackendDeletedRecordings = g_localizeStrings.Get(13205);
+ m_iBackendDiskTotal = 0;
+ m_iBackendDiskUsed = 0;
+
+ // Update with values from the current client when we have at least one
+ if (!m_backendProperties.empty())
+ {
+ const auto& backend = m_backendProperties[m_iCurrentActiveClient];
+
+ m_strBackendName = backend.name;
+ m_strBackendVersion = backend.version;
+ m_strBackendHost = backend.host;
+
+ // We always display one extra as the add-on itself counts as a provider
+ if (backend.numProviders >= 0)
+ m_strBackendProviders = std::to_string(backend.numProviders + 1);
+
+ if (backend.numChannelGroups >= 0)
+ m_strBackendChannelGroups = std::to_string(backend.numChannelGroups);
+
+ if (backend.numChannels >= 0)
+ m_strBackendChannels = std::to_string(backend.numChannels);
+
+ if (backend.numTimers >= 0)
+ m_strBackendTimers = std::to_string(backend.numTimers);
+
+ if (backend.numRecordings >= 0)
+ m_strBackendRecordings = std::to_string(backend.numRecordings);
+
+ if (backend.numDeletedRecordings >= 0)
+ m_strBackendDeletedRecordings = std::to_string(backend.numDeletedRecordings);
+
+ m_iBackendDiskTotal = backend.diskTotal;
+ m_iBackendDiskUsed = backend.diskUsed;
+ }
+
+ // Update the current active client, eventually wrapping around
+ if (++m_iCurrentActiveClient >= m_backendProperties.size())
+ m_iCurrentActiveClient = 0;
+}
+
+void CPVRGUIInfo::UpdateTimersCache()
+{
+ m_anyTimersInfo.UpdateTimersCache();
+ m_tvTimersInfo.UpdateTimersCache();
+ m_radioTimersInfo.UpdateTimersCache();
+}
+
+void CPVRGUIInfo::UpdateTimersToggle()
+{
+ m_anyTimersInfo.UpdateTimersToggle();
+ m_tvTimersInfo.UpdateTimersToggle();
+ m_radioTimersInfo.UpdateTimersToggle();
+}
+
+void CPVRGUIInfo::UpdateNextTimer()
+{
+ m_anyTimersInfo.UpdateNextTimer();
+ m_tvTimersInfo.UpdateNextTimer();
+ m_radioTimersInfo.UpdateNextTimer();
+}
+
+int CPVRGUIInfo::GetTimeShiftSeekPercent() const
+{
+ int progress = m_timesInfo.GetTimeshiftProgressPlayPosition();
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ int seekSize = appPlayer->GetSeekHandler().GetSeekSize();
+ if (seekSize != 0)
+ {
+ int total = m_timesInfo.GetTimeshiftProgressDuration();
+
+ float totalTime = static_cast<float>(total);
+ if (totalTime == 0.0f)
+ return 0;
+
+ float percentPerSecond = 100.0f / totalTime;
+ float percent = progress + percentPerSecond * seekSize;
+ percent = std::max(0.0f, std::min(percent, 100.0f));
+ return std::lrintf(percent);
+ }
+ return progress;
+}
diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h
new file mode 100644
index 0000000..5591353
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h"
+#include "guilib/guiinfo/GUIInfoProvider.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/guilib/guiinfo/PVRGUITimerInfo.h"
+#include "pvr/guilib/guiinfo/PVRGUITimesInfo.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <atomic>
+#include <string>
+#include <vector>
+
+class CFileItem;
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+class CGUIInfo;
+}
+} // namespace GUILIB
+} // namespace KODI
+
+namespace PVR
+{
+enum class PVREvent;
+struct PVRChannelNumberInputChangedEvent;
+struct PVRPreviewAndPlayerShowInfoChangedEvent;
+
+class CPVRGUIInfo : public KODI::GUILIB::GUIINFO::CGUIInfoProvider, private CThread
+{
+public:
+ CPVRGUIInfo();
+ ~CPVRGUIInfo() override = default;
+
+ void Start();
+ void Stop();
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+
+ /*!
+ * @brief CEventStream callback for channel number input changes.
+ * @param event The event.
+ */
+ void Notify(const PVRChannelNumberInputChangedEvent& event);
+
+ /*!
+ * @brief CEventStream callback for channel preview and player show info changes.
+ * @param event The event.
+ */
+ void Notify(const PVRPreviewAndPlayerShowInfoChangedEvent& event);
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem* item) override;
+ bool GetLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ std::string* fallback) const override;
+ bool GetFallbackLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ std::string* fallback) override;
+ bool GetInt(int& value,
+ const CGUIListItem* item,
+ int contextWindow,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info) const override;
+ bool GetBool(bool& value,
+ const CGUIListItem* item,
+ int contextWindow,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info) const override;
+
+private:
+ void ResetProperties();
+ void ClearQualityInfo(PVR_SIGNAL_STATUS& qualityInfo);
+ void ClearDescrambleInfo(PVR_DESCRAMBLE_INFO& descrambleInfo);
+
+ void Process() override;
+
+ void UpdateTimersCache();
+ void UpdateBackendCache();
+ void UpdateQualityData();
+ void UpdateDescrambleData();
+ void UpdateMisc();
+ void UpdateNextTimer();
+ void UpdateTimeshiftData();
+ void UpdateTimeshiftProgressData();
+
+ void UpdateTimersToggle();
+
+ bool GetListItemAndPlayerLabel(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ std::string& strValue) const;
+ bool GetPVRLabel(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ std::string& strValue) const;
+ bool GetRadioRDSLabel(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ std::string& strValue) const;
+
+ bool GetListItemAndPlayerInt(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ int& iValue) const;
+ bool GetPVRInt(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ int& iValue) const;
+ int GetTimeShiftSeekPercent() const;
+
+ bool GetListItemAndPlayerBool(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ bool& bValue) const;
+ bool GetPVRBool(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ bool& bValue) const;
+ bool GetRadioRDSBool(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ bool& bValue) const;
+
+ void CharInfoBackendNumber(std::string& strValue) const;
+ void CharInfoTotalDiskSpace(std::string& strValue) const;
+ void CharInfoSignal(std::string& strValue) const;
+ void CharInfoSNR(std::string& strValue) const;
+ void CharInfoBER(std::string& strValue) const;
+ void CharInfoUNC(std::string& strValue) const;
+ void CharInfoFrontendName(std::string& strValue) const;
+ void CharInfoFrontendStatus(std::string& strValue) const;
+ void CharInfoBackendName(std::string& strValue) const;
+ void CharInfoBackendVersion(std::string& strValue) const;
+ void CharInfoBackendHost(std::string& strValue) const;
+ void CharInfoBackendDiskspace(std::string& strValue) const;
+ void CharInfoBackendProviders(std::string& strValue) const;
+ void CharInfoBackendChannelGroups(std::string& strValue) const;
+ void CharInfoBackendChannels(std::string& strValue) const;
+ void CharInfoBackendTimers(std::string& strValue) const;
+ void CharInfoBackendRecordings(std::string& strValue) const;
+ void CharInfoBackendDeletedRecordings(std::string& strValue) const;
+ void CharInfoPlayingClientName(std::string& strValue) const;
+ void CharInfoEncryption(std::string& strValue) const;
+ void CharInfoService(std::string& strValue) const;
+ void CharInfoMux(std::string& strValue) const;
+ void CharInfoProvider(std::string& strValue) const;
+
+ /** @name PVRGUIInfo data */
+ //@{
+ CPVRGUIAnyTimerInfo m_anyTimersInfo; // tv + radio
+ CPVRGUITVTimerInfo m_tvTimersInfo;
+ CPVRGUIRadioTimerInfo m_radioTimersInfo;
+
+ CPVRGUITimesInfo m_timesInfo;
+
+ bool m_bHasTVRecordings;
+ bool m_bHasRadioRecordings;
+ unsigned int m_iCurrentActiveClient;
+ std::string m_strPlayingClientName;
+ std::string m_strBackendName;
+ std::string m_strBackendVersion;
+ std::string m_strBackendHost;
+ std::string m_strBackendTimers;
+ std::string m_strBackendRecordings;
+ std::string m_strBackendDeletedRecordings;
+ std::string m_strBackendProviders;
+ std::string m_strBackendChannelGroups;
+ std::string m_strBackendChannels;
+ long long m_iBackendDiskTotal;
+ long long m_iBackendDiskUsed;
+ bool m_bIsPlayingTV;
+ bool m_bIsPlayingRadio;
+ bool m_bIsPlayingRecording;
+ bool m_bIsPlayingEpgTag;
+ bool m_bIsPlayingEncryptedStream;
+ bool m_bHasTVChannels;
+ bool m_bHasRadioChannels;
+ bool m_bCanRecordPlayingChannel;
+ bool m_bIsRecordingPlayingChannel;
+ bool m_bIsPlayingActiveRecording;
+ std::string m_strPlayingTVGroup;
+ std::string m_strPlayingRadioGroup;
+
+ //@}
+
+ PVR_SIGNAL_STATUS m_qualityInfo; /*!< stream quality information */
+ PVR_DESCRAMBLE_INFO m_descrambleInfo; /*!< stream descramble information */
+ std::vector<SBackend> m_backendProperties;
+
+ std::string m_channelNumberInput;
+ bool m_previewAndPlayerShowInfo{false};
+
+ mutable CCriticalSection m_critSection;
+
+ /**
+ * The various backend-related fields will only be updated when this
+ * flag is set. This is done to limit the amount of unnecessary
+ * backend querying when we're not displaying any of the queried
+ * information.
+ */
+ mutable std::atomic<bool> m_updateBackendCacheRequested;
+
+ bool m_bRegistered;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp
new file mode 100644
index 0000000..24a468a
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUITimerInfo.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+CPVRGUITimerInfo::CPVRGUITimerInfo()
+{
+ ResetProperties();
+}
+
+void CPVRGUITimerInfo::ResetProperties()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strActiveTimerTitle.clear();
+ m_strActiveTimerChannelName.clear();
+ m_strActiveTimerChannelIcon.clear();
+ m_strActiveTimerTime.clear();
+ m_strNextTimerInfo.clear();
+ m_strNextRecordingTitle.clear();
+ m_strNextRecordingChannelName.clear();
+ m_strNextRecordingChannelIcon.clear();
+ m_strNextRecordingTime.clear();
+ m_iTimerAmount = 0;
+ m_iRecordingTimerAmount = 0;
+ m_iTimerInfoToggleStart = {};
+ m_iTimerInfoToggleCurrent = 0;
+}
+
+bool CPVRGUITimerInfo::TimerInfoToggle()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_iTimerInfoToggleStart.time_since_epoch().count() == 0)
+ {
+ m_iTimerInfoToggleStart = std::chrono::steady_clock::now();
+ m_iTimerInfoToggleCurrent = 0;
+ return true;
+ }
+
+ auto now = std::chrono::steady_clock::now();
+ auto duration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - m_iTimerInfoToggleStart);
+
+ if (duration.count() >
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRInfoToggleInterval)
+ {
+ unsigned int iPrevious = m_iTimerInfoToggleCurrent;
+ unsigned int iBoundary = m_iRecordingTimerAmount > 0 ? m_iRecordingTimerAmount : m_iTimerAmount;
+ if (++m_iTimerInfoToggleCurrent > iBoundary - 1)
+ m_iTimerInfoToggleCurrent = 0;
+
+ if (m_iTimerInfoToggleCurrent != iPrevious)
+ {
+ m_iTimerInfoToggleStart = std::chrono::steady_clock::now();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CPVRGUITimerInfo::UpdateTimersToggle()
+{
+ if (!TimerInfoToggle())
+ return;
+
+ std::string strActiveTimerTitle;
+ std::string strActiveTimerChannelName;
+ std::string strActiveTimerChannelIcon;
+ std::string strActiveTimerTime;
+
+ /* safe to fetch these unlocked, since they're updated from the same thread as this one */
+ if (m_iRecordingTimerAmount > 0)
+ {
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> activeTags = GetActiveRecordings();
+ if (m_iTimerInfoToggleCurrent < activeTags.size())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> tag = activeTags.at(m_iTimerInfoToggleCurrent);
+ strActiveTimerTitle = tag->Title();
+ strActiveTimerChannelName = tag->ChannelName();
+ strActiveTimerChannelIcon = tag->ChannelIcon();
+ strActiveTimerTime = tag->StartAsLocalTime().GetAsLocalizedDateTime(false, false);
+ }
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strActiveTimerTitle = strActiveTimerTitle;
+ m_strActiveTimerChannelName = strActiveTimerChannelName;
+ m_strActiveTimerChannelIcon = strActiveTimerChannelIcon;
+ m_strActiveTimerTime = strActiveTimerTime;
+}
+
+void CPVRGUITimerInfo::UpdateTimersCache()
+{
+ int iTimerAmount = AmountActiveTimers();
+ int iRecordingTimerAmount = AmountActiveRecordings();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iTimerAmount = iTimerAmount;
+ m_iRecordingTimerAmount = iRecordingTimerAmount;
+ m_iTimerInfoToggleStart = {};
+ }
+
+ UpdateTimersToggle();
+}
+
+void CPVRGUITimerInfo::UpdateNextTimer()
+{
+ std::string strNextRecordingTitle;
+ std::string strNextRecordingChannelName;
+ std::string strNextRecordingChannelIcon;
+ std::string strNextRecordingTime;
+ std::string strNextTimerInfo;
+
+ const std::shared_ptr<CPVRTimerInfoTag> timer = GetNextActiveTimer();
+ if (timer)
+ {
+ strNextRecordingTitle = timer->Title();
+ strNextRecordingChannelName = timer->ChannelName();
+ strNextRecordingChannelIcon = timer->ChannelIcon();
+ strNextRecordingTime = timer->StartAsLocalTime().GetAsLocalizedDateTime(false, false);
+
+ strNextTimerInfo = StringUtils::Format("{} {} {} {}", g_localizeStrings.Get(19106),
+ timer->StartAsLocalTime().GetAsLocalizedDate(true),
+ g_localizeStrings.Get(19107),
+ timer->StartAsLocalTime().GetAsLocalizedTime("", false));
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strNextRecordingTitle = strNextRecordingTitle;
+ m_strNextRecordingChannelName = strNextRecordingChannelName;
+ m_strNextRecordingChannelIcon = strNextRecordingChannelIcon;
+ m_strNextRecordingTime = strNextRecordingTime;
+ m_strNextTimerInfo = strNextTimerInfo;
+}
+
+const std::string& CPVRGUITimerInfo::GetActiveTimerTitle() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strActiveTimerTitle;
+}
+
+const std::string& CPVRGUITimerInfo::GetActiveTimerChannelName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strActiveTimerChannelName;
+}
+
+const std::string& CPVRGUITimerInfo::GetActiveTimerChannelIcon() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strActiveTimerChannelIcon;
+}
+
+const std::string& CPVRGUITimerInfo::GetActiveTimerDateTime() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strActiveTimerTime;
+}
+
+const std::string& CPVRGUITimerInfo::GetNextTimerTitle() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strNextRecordingTitle;
+}
+
+const std::string& CPVRGUITimerInfo::GetNextTimerChannelName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strNextRecordingChannelName;
+}
+
+const std::string& CPVRGUITimerInfo::GetNextTimerChannelIcon() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strNextRecordingChannelIcon;
+}
+
+const std::string& CPVRGUITimerInfo::GetNextTimerDateTime() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strNextRecordingTime;
+}
+
+const std::string& CPVRGUITimerInfo::GetNextTimer() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strNextTimerInfo;
+}
+
+int CPVRGUIAnyTimerInfo::AmountActiveTimers()
+{
+ return CServiceBroker::GetPVRManager().Timers()->AmountActiveTimers();
+}
+
+int CPVRGUIAnyTimerInfo::AmountActiveRecordings()
+{
+ return CServiceBroker::GetPVRManager().Timers()->AmountActiveRecordings();
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRGUIAnyTimerInfo::GetActiveRecordings()
+{
+ return CServiceBroker::GetPVRManager().Timers()->GetActiveRecordings();
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRGUIAnyTimerInfo::GetNextActiveTimer()
+{
+ return CServiceBroker::GetPVRManager().Timers()->GetNextActiveTimer();
+}
+
+int CPVRGUITVTimerInfo::AmountActiveTimers()
+{
+ return CServiceBroker::GetPVRManager().Timers()->AmountActiveTVTimers();
+}
+
+int CPVRGUITVTimerInfo::AmountActiveRecordings()
+{
+ return CServiceBroker::GetPVRManager().Timers()->AmountActiveTVRecordings();
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRGUITVTimerInfo::GetActiveRecordings()
+{
+ return CServiceBroker::GetPVRManager().Timers()->GetActiveTVRecordings();
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRGUITVTimerInfo::GetNextActiveTimer()
+{
+ return CServiceBroker::GetPVRManager().Timers()->GetNextActiveTVTimer();
+}
+
+int CPVRGUIRadioTimerInfo::AmountActiveTimers()
+{
+ return CServiceBroker::GetPVRManager().Timers()->AmountActiveRadioTimers();
+}
+
+int CPVRGUIRadioTimerInfo::AmountActiveRecordings()
+{
+ return CServiceBroker::GetPVRManager().Timers()->AmountActiveRadioRecordings();
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRGUIRadioTimerInfo::GetActiveRecordings()
+{
+ return CServiceBroker::GetPVRManager().Timers()->GetActiveRadioRecordings();
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRGUIRadioTimerInfo::GetNextActiveTimer()
+{
+ return CServiceBroker::GetPVRManager().Timers()->GetNextActiveRadioTimer();
+}
diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h
new file mode 100644
index 0000000..260db6c
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+
+#include <chrono>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+ class CPVRTimerInfoTag;
+
+ class CPVRGUITimerInfo
+ {
+ public:
+ CPVRGUITimerInfo();
+ virtual ~CPVRGUITimerInfo() = default;
+
+ void ResetProperties();
+
+ void UpdateTimersCache();
+ void UpdateTimersToggle();
+ void UpdateNextTimer();
+
+ const std::string& GetActiveTimerTitle() const;
+ const std::string& GetActiveTimerChannelName() const;
+ const std::string& GetActiveTimerChannelIcon() const;
+ const std::string& GetActiveTimerDateTime() const;
+ const std::string& GetNextTimerTitle() const;
+ const std::string& GetNextTimerChannelName() const;
+ const std::string& GetNextTimerChannelIcon() const;
+ const std::string& GetNextTimerDateTime() const;
+ const std::string& GetNextTimer() const;
+
+ bool HasTimers() const { return m_iTimerAmount > 0; }
+ bool HasRecordingTimers() const { return m_iRecordingTimerAmount > 0; }
+ bool HasNonRecordingTimers() const { return m_iTimerAmount - m_iRecordingTimerAmount > 0; }
+
+ private:
+ bool TimerInfoToggle();
+
+ virtual int AmountActiveTimers() = 0;
+ virtual int AmountActiveRecordings() = 0;
+ virtual std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() = 0;
+ virtual std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() = 0;
+
+ unsigned int m_iTimerAmount;
+ unsigned int m_iRecordingTimerAmount;
+
+ std::string m_strActiveTimerTitle;
+ std::string m_strActiveTimerChannelName;
+ std::string m_strActiveTimerChannelIcon;
+ std::string m_strActiveTimerTime;
+ std::string m_strNextRecordingTitle;
+ std::string m_strNextRecordingChannelName;
+ std::string m_strNextRecordingChannelIcon;
+ std::string m_strNextRecordingTime;
+ std::string m_strNextTimerInfo;
+
+ std::chrono::time_point<std::chrono::steady_clock> m_iTimerInfoToggleStart;
+ unsigned int m_iTimerInfoToggleCurrent;
+
+ mutable CCriticalSection m_critSection;
+ };
+
+ class CPVRGUIAnyTimerInfo : public CPVRGUITimerInfo
+ {
+ public:
+ CPVRGUIAnyTimerInfo() = default;
+
+ private:
+ int AmountActiveTimers() override;
+ int AmountActiveRecordings() override;
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() override;
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() override;
+ };
+
+ class CPVRGUITVTimerInfo : public CPVRGUITimerInfo
+ {
+ public:
+ CPVRGUITVTimerInfo() = default;
+
+ private:
+ int AmountActiveTimers() override;
+ int AmountActiveRecordings() override;
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() override;
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() override;
+ };
+
+ class CPVRGUIRadioTimerInfo : public CPVRGUITimerInfo
+ {
+ public:
+ CPVRGUIRadioTimerInfo() = default;
+
+ private:
+ int AmountActiveTimers() override;
+ int AmountActiveRecordings() override;
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() override;
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() override;
+ };
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp
new file mode 100644
index 0000000..e5dfdb9
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRGUITimesInfo.h"
+
+#include "ServiceBroker.h"
+#include "cores/DataCacheCore.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+
+#include <cmath>
+#include <ctime>
+#include <memory>
+#include <mutex>
+
+using namespace PVR;
+
+CPVRGUITimesInfo::CPVRGUITimesInfo()
+{
+ Reset();
+}
+
+void CPVRGUITimesInfo::Reset()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_iStartTime = 0;
+ m_iDuration = 0;
+ m_iTimeshiftStartTime = 0;
+ m_iTimeshiftEndTime = 0;
+ m_iTimeshiftPlayTime = 0;
+ m_iTimeshiftOffset = 0;
+
+ m_iTimeshiftProgressStartTime = 0;
+ m_iTimeshiftProgressEndTime = 0;
+ m_iTimeshiftProgressDuration = 0;
+
+ m_playingEpgTag.reset();
+ m_playingChannel.reset();
+}
+
+void CPVRGUITimesInfo::UpdatePlayingTag()
+{
+ const std::shared_ptr<CPVRChannel> currentChannel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ std::shared_ptr<CPVREpgInfoTag> currentTag = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingEpgTag();
+
+ if (currentChannel || currentTag)
+ {
+ if (currentChannel && !currentTag)
+ currentTag = currentChannel->GetEPGNow();
+
+ const std::shared_ptr<CPVRChannelGroupsContainer> groups = CServiceBroker::GetPVRManager().ChannelGroups();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::shared_ptr<CPVRChannel> playingChannel =
+ m_playingEpgTag ? groups->GetChannelForEpgTag(m_playingEpgTag) : nullptr;
+
+ if (!m_playingEpgTag || !currentTag || !playingChannel || !currentChannel ||
+ m_playingEpgTag->StartAsUTC() != currentTag->StartAsUTC() ||
+ m_playingEpgTag->EndAsUTC() != currentTag->EndAsUTC() || *playingChannel != *currentChannel)
+ {
+ if (currentTag)
+ {
+ m_playingEpgTag = currentTag;
+ m_iDuration = m_playingEpgTag->GetDuration();
+ }
+ else if (m_iTimeshiftEndTime > m_iTimeshiftStartTime)
+ {
+ m_playingEpgTag.reset();
+ m_iDuration = m_iTimeshiftEndTime - m_iTimeshiftStartTime;
+ }
+ else
+ {
+ m_playingEpgTag.reset();
+ m_iDuration = 0;
+ }
+ }
+ }
+ else
+ {
+ const std::shared_ptr<CPVRRecording> recording = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingRecording();
+ if (recording)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_playingEpgTag.reset();
+ m_iDuration = recording->GetDuration();
+ }
+ }
+}
+
+void CPVRGUITimesInfo::UpdateTimeshiftData()
+{
+ if (!CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV() && !CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio())
+ {
+ // If nothing is playing (anymore), there is no need to update data.
+ Reset();
+ return;
+ }
+
+ time_t now = std::time(nullptr);
+ time_t iStartTime;
+ int64_t iPlayTime, iMinTime, iMaxTime;
+ CServiceBroker::GetDataCacheCore().GetPlayTimes(iStartTime, iPlayTime, iMinTime, iMaxTime);
+ bool bPlaying = CServiceBroker::GetDataCacheCore().GetSpeed() == 1.0f;
+ const std::shared_ptr<CPVRChannel> playingChannel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (playingChannel != m_playingChannel)
+ {
+ // playing channel changed. we need to reset offset and playtime.
+ m_iTimeshiftOffset = 0;
+ m_iTimeshiftPlayTime = 0;
+ m_playingChannel = playingChannel;
+ }
+
+ if (!iStartTime)
+ {
+ if (m_iStartTime == 0)
+ iStartTime = now;
+ else
+ iStartTime = m_iStartTime;
+
+ iMinTime = iPlayTime;
+ iMaxTime = iPlayTime;
+ }
+
+ m_iStartTime = iStartTime;
+ m_iTimeshiftStartTime = iStartTime + iMinTime / 1000;
+ m_iTimeshiftEndTime = iStartTime + iMaxTime / 1000;
+
+ if (m_iTimeshiftEndTime > m_iTimeshiftStartTime)
+ {
+ // timeshifting supported
+ m_iTimeshiftPlayTime = iStartTime + iPlayTime / 1000;
+ if (iMaxTime > iPlayTime)
+ m_iTimeshiftOffset = (iMaxTime - iPlayTime) / 1000;
+ else
+ m_iTimeshiftOffset = 0;
+ }
+ else
+ {
+ // timeshifting not supported
+ if (bPlaying)
+ m_iTimeshiftPlayTime = now - m_iTimeshiftOffset;
+
+ m_iTimeshiftOffset = now - m_iTimeshiftPlayTime;
+ }
+
+ UpdateTimeshiftProgressData();
+}
+
+void CPVRGUITimesInfo::UpdateTimeshiftProgressData()
+{
+ // Note: General idea of the ts progress is always to be able to visualise both the complete
+ // ts buffer and the complete playing epg event (if any) side by side with the same time
+ // scale. TS progress start and end times will be calculated accordingly.
+ // + Start is usually ts buffer start, except if start time of playing epg event is
+ // before ts buffer start, then progress start is epg event start.
+ // + End is usually ts buffer end, except if end time of playing epg event is
+ // after ts buffer end, then progress end is epg event end.
+ // In simple timeshift mode (settings value), progress start is always the start time of
+ // playing epg event and progress end is always the end time of playing epg event.
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // start time
+ //////////////////////////////////////////////////////////////////////////////////////
+ bool bUpdatedStartTime = false;
+ if (m_playingEpgTag)
+ {
+ time_t start = 0;
+ m_playingEpgTag->StartAsUTC().GetAsTime(start);
+ if (start < m_iTimeshiftStartTime ||
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bPVRTimeshiftSimpleOSD)
+ {
+ // playing event started before start of ts buffer or simple ts osd to be used
+ m_iTimeshiftProgressStartTime = start;
+ bUpdatedStartTime = true;
+ }
+ }
+
+ if (!bUpdatedStartTime)
+ {
+ // default to ts buffer start
+ m_iTimeshiftProgressStartTime = m_iTimeshiftStartTime;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // end time
+ //////////////////////////////////////////////////////////////////////////////////////
+ bool bUpdatedEndTime = false;
+ if (m_playingEpgTag)
+ {
+ time_t end = 0;
+ m_playingEpgTag->EndAsUTC().GetAsTime(end);
+ if (end > m_iTimeshiftEndTime ||
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bPVRTimeshiftSimpleOSD)
+ {
+ // playing event will end after end of ts buffer or simple ts osd to be used
+ m_iTimeshiftProgressEndTime = end;
+ bUpdatedEndTime = true;
+ }
+ }
+
+ if (!bUpdatedEndTime)
+ {
+ // default to ts buffer end
+ m_iTimeshiftProgressEndTime = m_iTimeshiftEndTime;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // duration
+ //////////////////////////////////////////////////////////////////////////////////////
+ m_iTimeshiftProgressDuration = m_iTimeshiftProgressEndTime - m_iTimeshiftProgressStartTime;
+}
+
+void CPVRGUITimesInfo::Update()
+{
+ UpdatePlayingTag();
+ UpdateTimeshiftData();
+}
+
+std::string CPVRGUITimesInfo::TimeToTimeString(time_t datetime, TIME_FORMAT format, bool withSeconds)
+{
+ CDateTime time;
+ time.SetFromUTCDateTime(datetime);
+ return time.GetAsLocalizedTime(format, withSeconds);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftStartTime(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return TimeToTimeString(m_iTimeshiftStartTime, format, false);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftEndTime(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return TimeToTimeString(m_iTimeshiftEndTime, format, false);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftPlayTime(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return TimeToTimeString(m_iTimeshiftPlayTime, format, true);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftOffset(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return StringUtils::SecondsToTimeString(m_iTimeshiftOffset, format);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftProgressDuration(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return StringUtils::SecondsToTimeString(m_iTimeshiftProgressDuration, format);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftProgressStartTime(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return TimeToTimeString(m_iTimeshiftProgressStartTime, format, false);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftProgressEndTime(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return TimeToTimeString(m_iTimeshiftProgressEndTime, format, false);
+}
+
+std::string CPVRGUITimesInfo::GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return StringUtils::SecondsToTimeString(GetEpgEventDuration(epgTag), format);
+}
+
+std::string CPVRGUITimesInfo::GetEpgEventElapsedTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const
+{
+ int iElapsed = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag)
+ iElapsed = epgTag->Progress();
+ else
+ iElapsed = GetElapsedTime();
+
+ return StringUtils::SecondsToTimeString(iElapsed, format);
+}
+
+std::string CPVRGUITimesInfo::GetEpgEventRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return StringUtils::SecondsToTimeString(GetRemainingTime(epgTag), format);
+}
+
+std::string CPVRGUITimesInfo::GetEpgEventFinishTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const
+{
+ CDateTime finish = CDateTime::GetCurrentDateTime();
+ finish += CDateTimeSpan(0, 0, 0, GetRemainingTime(epgTag));
+ return finish.GetAsLocalizedTime(format);
+}
+
+std::string CPVRGUITimesInfo::GetEpgEventSeekTime(int iSeekSize, TIME_FORMAT format) const
+{
+ return StringUtils::SecondsToTimeString(GetElapsedTime() + iSeekSize, format);
+}
+
+int CPVRGUITimesInfo::GetElapsedTime() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_playingEpgTag || m_iTimeshiftStartTime)
+ {
+ CDateTime current(m_iTimeshiftPlayTime);
+ CDateTime start = m_playingEpgTag ? m_playingEpgTag->StartAsUTC() : CDateTime(m_iTimeshiftStartTime);
+ CDateTimeSpan time = current > start ? current - start : CDateTimeSpan(0, 0, 0, 0);
+ return time.GetSecondsTotal();
+ }
+ else
+ {
+ return 0;
+ }
+}
+
+int CPVRGUITimesInfo::GetRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag)
+ return epgTag->GetDuration() - epgTag->Progress();
+ else
+ return m_iDuration - GetElapsedTime();
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgress() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::lrintf(static_cast<float>(m_iTimeshiftPlayTime - m_iTimeshiftStartTime) / (m_iTimeshiftEndTime - m_iTimeshiftStartTime) * 100);
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgressDuration() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iTimeshiftProgressDuration;
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgressPlayPosition() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::lrintf(static_cast<float>(m_iTimeshiftPlayTime - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100);
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgressEpgStart() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_playingEpgTag)
+ {
+ time_t epgStart = 0;
+ m_playingEpgTag->StartAsUTC().GetAsTime(epgStart);
+ return std::lrintf(static_cast<float>(epgStart - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100);
+ }
+ return 0;
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgressEpgEnd() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_playingEpgTag)
+ {
+ time_t epgEnd = 0;
+ m_playingEpgTag->EndAsUTC().GetAsTime(epgEnd);
+ return std::lrintf(static_cast<float>(epgEnd - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100);
+ }
+ return 0;
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgressBufferStart() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::lrintf(static_cast<float>(m_iTimeshiftStartTime - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100);
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgressBufferEnd() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::lrintf(static_cast<float>(m_iTimeshiftEndTime - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100);
+}
+
+int CPVRGUITimesInfo::GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag)
+ return epgTag->GetDuration();
+ else
+ return m_iDuration;
+}
+
+int CPVRGUITimesInfo::GetEpgEventProgress(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag)
+ return std::lrintf(epgTag->ProgressPercentage());
+ else
+ return std::lrintf(static_cast<float>(GetElapsedTime()) / m_iDuration * 100);
+}
+
+bool CPVRGUITimesInfo::IsTimeshifting() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return (m_iTimeshiftOffset > static_cast<unsigned int>(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeshiftThreshold));
+}
diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h
new file mode 100644
index 0000000..47dd9f5
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+#include "utils/TimeFormat.h"
+
+#include <memory>
+
+namespace PVR
+{
+ class CPVRChannel;
+ class CPVREpgInfoTag;
+
+ class CPVRGUITimesInfo
+ {
+ public:
+ CPVRGUITimesInfo();
+ virtual ~CPVRGUITimesInfo() = default;
+
+ void Reset();
+ void Update();
+
+ // GUI info labels
+ std::string GetTimeshiftStartTime(TIME_FORMAT format) const;
+ std::string GetTimeshiftEndTime(TIME_FORMAT format) const;
+ std::string GetTimeshiftPlayTime(TIME_FORMAT format) const;
+ std::string GetTimeshiftOffset(TIME_FORMAT format) const;
+ std::string GetTimeshiftProgressDuration(TIME_FORMAT format) const;
+ std::string GetTimeshiftProgressStartTime(TIME_FORMAT format) const;
+ std::string GetTimeshiftProgressEndTime(TIME_FORMAT format) const;
+
+ std::string GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const;
+ std::string GetEpgEventElapsedTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const;
+ std::string GetEpgEventRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const;
+ std::string GetEpgEventFinishTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const;
+ std::string GetEpgEventSeekTime(int iSeekSize, TIME_FORMAT format) const;
+
+ // GUI info ints
+ int GetTimeshiftProgress() const;
+ int GetTimeshiftProgressDuration() const;
+ int GetTimeshiftProgressPlayPosition() const;
+ int GetTimeshiftProgressEpgStart() const;
+ int GetTimeshiftProgressEpgEnd() const;
+ int GetTimeshiftProgressBufferStart() const;
+ int GetTimeshiftProgressBufferEnd() const;
+
+ int GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+ int GetEpgEventProgress(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ // GUI info bools
+ bool IsTimeshifting() const;
+
+ private:
+ void UpdatePlayingTag();
+ void UpdateTimeshiftData();
+ void UpdateTimeshiftProgressData();
+
+ static std::string TimeToTimeString(time_t datetime, TIME_FORMAT format, bool withSeconds);
+
+ int GetElapsedTime() const;
+ int GetRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ mutable CCriticalSection m_critSection;
+
+ std::shared_ptr<CPVREpgInfoTag> m_playingEpgTag;
+ std::shared_ptr<CPVRChannel> m_playingChannel;
+
+ time_t m_iStartTime;
+ unsigned int m_iDuration;
+ time_t m_iTimeshiftStartTime;
+ time_t m_iTimeshiftEndTime;
+ time_t m_iTimeshiftPlayTime;
+ unsigned int m_iTimeshiftOffset;
+
+ time_t m_iTimeshiftProgressStartTime;
+ time_t m_iTimeshiftProgressEndTime;
+ unsigned int m_iTimeshiftProgressDuration;
+ };
+
+} // namespace PVR
diff --git a/xbmc/pvr/providers/CMakeLists.txt b/xbmc/pvr/providers/CMakeLists.txt
new file mode 100644
index 0000000..f01a35a
--- /dev/null
+++ b/xbmc/pvr/providers/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES PVRProvider.cpp
+ PVRProviders.cpp)
+
+set(HEADERS PVRProvider.h
+ PVRProviders.h)
+
+core_add_library(pvr_providers)
diff --git a/xbmc/pvr/providers/PVRProvider.cpp b/xbmc/pvr/providers/PVRProvider.cpp
new file mode 100644
index 0000000..ab2e7a3
--- /dev/null
+++ b/xbmc/pvr/providers/PVRProvider.cpp
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRProvider.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+
+using namespace PVR;
+
+
+const std::string CPVRProvider::IMAGE_OWNER_PATTERN = "pvrprovider";
+
+CPVRProvider::CPVRProvider(int iUniqueId, int iClientId)
+ : m_iUniqueId(iUniqueId),
+ m_iClientId(iClientId),
+ m_iconPath(IMAGE_OWNER_PATTERN),
+ m_thumbPath(IMAGE_OWNER_PATTERN)
+{
+}
+
+CPVRProvider::CPVRProvider(const PVR_PROVIDER& provider, int iClientId)
+ : m_iUniqueId(provider.iUniqueId),
+ m_iClientId(iClientId),
+ m_strName(provider.strName),
+ m_type(provider.type),
+ m_iconPath(provider.strIconPath, IMAGE_OWNER_PATTERN),
+ m_strCountries(provider.strCountries),
+ m_strLanguages(provider.strLanguages),
+ m_thumbPath(IMAGE_OWNER_PATTERN)
+{
+}
+
+CPVRProvider::CPVRProvider(int iClientId,
+ const std::string& addonProviderName,
+ const std::string& addonIconPath,
+ const std::string& addonThumbPath)
+ : m_iClientId(iClientId),
+ m_strName(addonProviderName),
+ m_type(PVR_PROVIDER_TYPE_ADDON),
+ m_iconPath(addonIconPath, IMAGE_OWNER_PATTERN),
+ m_bIsClientProvider(true),
+ m_thumbPath(addonThumbPath, IMAGE_OWNER_PATTERN)
+{
+}
+
+bool CPVRProvider::operator==(const CPVRProvider& right) const
+{
+ return (m_iUniqueId == right.m_iUniqueId && m_iClientId == right.m_iClientId);
+}
+
+bool CPVRProvider::operator!=(const CPVRProvider& right) const
+{
+ return !(*this == right);
+}
+
+void CPVRProvider::Serialize(CVariant& value) const
+{
+ value["providerid"] = m_iDatabaseId;
+ value["clientid"] = m_iClientId;
+ value["providername"] = m_strName;
+ switch (m_type)
+ {
+ case PVR_PROVIDER_TYPE_ADDON:
+ value["providertype"] = "addon";
+ break;
+ case PVR_PROVIDER_TYPE_SATELLITE:
+ value["providertype"] = "satellite";
+ break;
+ case PVR_PROVIDER_TYPE_CABLE:
+ value["providertype"] = "cable";
+ break;
+ case PVR_PROVIDER_TYPE_AERIAL:
+ value["providertype"] = "aerial";
+ break;
+ case PVR_PROVIDER_TYPE_IPTV:
+ value["providertype"] = "iptv";
+ break;
+ case PVR_PROVIDER_TYPE_OTHER:
+ value["providertype"] = "other";
+ break;
+ default:
+ value["state"] = "unknown";
+ break;
+ }
+ value["iconpath"] = GetClientIconPath();
+ value["countries"] = m_strCountries;
+ value["languages"] = m_strLanguages;
+}
+
+int CPVRProvider::GetDatabaseId() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iDatabaseId;
+}
+
+bool CPVRProvider::SetDatabaseId(int iDatabaseId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iDatabaseId != iDatabaseId)
+ {
+ m_iDatabaseId = iDatabaseId;
+ return true;
+ }
+
+ return false;
+}
+
+int CPVRProvider::GetUniqueId() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iUniqueId;
+}
+
+int CPVRProvider::GetClientId() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientId;
+}
+
+std::string CPVRProvider::GetName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strName;
+}
+
+bool CPVRProvider::SetName(const std::string& strName)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_strName != strName)
+ {
+ m_strName = strName;
+ return true;
+ }
+
+ return false;
+}
+
+PVR_PROVIDER_TYPE CPVRProvider::GetType() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_type;
+}
+
+bool CPVRProvider::SetType(PVR_PROVIDER_TYPE type)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_type != type)
+ {
+ m_type = type;
+ return true;
+ }
+
+ return false;
+}
+
+std::string CPVRProvider::GetClientIconPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iconPath.GetClientImage();
+}
+
+std::string CPVRProvider::GetIconPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iconPath.GetLocalImage();
+}
+
+bool CPVRProvider::SetIconPath(const std::string& strIconPath)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (GetClientIconPath() != strIconPath)
+ {
+ m_iconPath.SetClientImage(strIconPath);
+ return true;
+ }
+
+ return false;
+}
+
+namespace
+{
+
+const std::vector<std::string> Tokenize(const std::string& str)
+{
+ return StringUtils::Split(str, PROVIDER_STRING_TOKEN_SEPARATOR);
+}
+
+const std::string DeTokenize(const std::vector<std::string>& tokens)
+{
+ return StringUtils::Join(tokens, PROVIDER_STRING_TOKEN_SEPARATOR);
+}
+
+} // unnamed namespace
+
+std::vector<std::string> CPVRProvider::GetCountries() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ return Tokenize(m_strCountries);
+}
+
+bool CPVRProvider::SetCountries(const std::vector<std::string>& countries)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strCountries = DeTokenize(countries);
+ if (m_strCountries != strCountries)
+ {
+ m_strCountries = strCountries;
+ return true;
+ }
+
+ return false;
+}
+
+std::string CPVRProvider::GetCountriesDBString() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strCountries;
+}
+
+bool CPVRProvider::SetCountriesFromDBString(const std::string& strCountries)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_strCountries != strCountries)
+ {
+ m_strCountries = strCountries;
+ return true;
+ }
+
+ return false;
+}
+
+std::vector<std::string> CPVRProvider::GetLanguages() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return Tokenize(m_strLanguages);
+}
+
+bool CPVRProvider::SetLanguages(const std::vector<std::string>& languages)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strLanguages = DeTokenize(languages);
+ if (m_strLanguages != strLanguages)
+ {
+ m_strLanguages = strLanguages;
+ return true;
+ }
+
+ return false;
+}
+
+std::string CPVRProvider::GetLanguagesDBString() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strLanguages;
+}
+
+bool CPVRProvider::SetLanguagesFromDBString(const std::string& strLanguages)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_strLanguages != strLanguages)
+ {
+ m_strLanguages = strLanguages;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRProvider::Persist(bool updateRecord /* = false */)
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return database->Persist(*this, updateRecord);
+ }
+
+ return false;
+}
+
+bool CPVRProvider::DeleteFromDatabase()
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return database->Delete(*this);
+ }
+
+ return false;
+}
+
+bool CPVRProvider::UpdateEntry(const std::shared_ptr<CPVRProvider>& fromProvider,
+ ProviderUpdateMode updateMode)
+{
+ bool bChanged = false;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (updateMode == ProviderUpdateMode::BY_DATABASE)
+ {
+ m_iDatabaseId = fromProvider->m_iDatabaseId;
+
+ m_strName = fromProvider->m_strName;
+ m_type = fromProvider->m_type;
+ m_iconPath = fromProvider->m_iconPath;
+
+ if (fromProvider->m_bIsClientProvider)
+ {
+ m_thumbPath = fromProvider->m_thumbPath;
+ m_bIsClientProvider = fromProvider->m_bIsClientProvider;
+ }
+
+ m_strCountries = fromProvider->m_strCountries;
+ m_strLanguages = fromProvider->m_strLanguages;
+ }
+ else if (updateMode == ProviderUpdateMode::BY_CLIENT)
+ {
+ if (m_strName != fromProvider->m_strName)
+ {
+ m_strName = fromProvider->m_strName;
+ bChanged = true;
+ }
+
+ if (m_type != fromProvider->m_type)
+ {
+ m_type = fromProvider->m_type;
+ bChanged = true;
+ }
+
+ if (m_iconPath != fromProvider->m_iconPath)
+ {
+ m_iconPath = fromProvider->m_iconPath;
+ bChanged = true;
+ }
+
+ if (fromProvider->m_bIsClientProvider)
+ {
+ m_thumbPath = fromProvider->m_thumbPath;
+ m_bIsClientProvider = fromProvider->m_bIsClientProvider;
+ }
+
+ if (m_strCountries != fromProvider->m_strCountries)
+ {
+ m_strCountries = fromProvider->m_strCountries;
+ bChanged = true;
+ }
+
+ if (m_strLanguages != fromProvider->m_strLanguages)
+ {
+ m_strLanguages = fromProvider->m_strLanguages;
+ bChanged = true;
+ }
+ }
+
+ return bChanged;
+}
+
+bool CPVRProvider::HasThumbPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return (m_type == PVR_PROVIDER_TYPE_ADDON && !m_thumbPath.GetLocalImage().empty());
+}
+
+std::string CPVRProvider::GetThumbPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_thumbPath.GetLocalImage();
+}
+
+std::string CPVRProvider::GetClientThumbPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_thumbPath.GetClientImage();
+}
diff --git a/xbmc/pvr/providers/PVRProvider.h b/xbmc/pvr/providers/PVRProvider.h
new file mode 100644
index 0000000..1e8d835
--- /dev/null
+++ b/xbmc/pvr/providers/PVRProvider.h
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_providers.h"
+#include "pvr/PVRCachedImage.h"
+#include "threads/CriticalSection.h"
+#include "utils/ISerializable.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+
+enum class ProviderUpdateMode
+{
+ BY_CLIENT,
+ BY_DATABASE
+};
+
+static constexpr int PVR_PROVIDER_ADDON_UID = -1;
+static constexpr int PVR_PROVIDER_INVALID_DB_ID = -1;
+
+class CPVRProvider final : public ISerializable
+{
+public:
+ static const std::string IMAGE_OWNER_PATTERN;
+
+ CPVRProvider(int iUniqueId, int iClientId);
+ CPVRProvider(const PVR_PROVIDER& provider, int iClientId);
+ CPVRProvider(int iClientId,
+ const std::string& addonProviderName,
+ const std::string& addonIconPath,
+ const std::string& addonThumbPath);
+
+ bool operator==(const CPVRProvider& right) const;
+ bool operator!=(const CPVRProvider& right) const;
+
+ void Serialize(CVariant& value) const override;
+
+ /*!
+ * @brief The database id of this provider
+ *
+ * A unique identifier for this provider.
+ * It can be used to find the same provider on this clients backend
+ *
+ * @return The database id of this provider
+ */
+ int GetDatabaseId() const;
+
+ /*!
+ * @brief Set the database id of this provider
+ * @param iDatabaseId The new ID.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetDatabaseId(int iDatabaseId);
+
+ /*!
+ * @brief A unique identifier for this provider.
+ *
+ * A unique identifier for this provider.
+ * It can be used to find the same provider on this clients backend
+ *
+ * @return The Unique ID.
+ */
+ int GetUniqueId() const;
+
+ /*!
+ * @return The identifier of the client that supplies this provider.
+ */
+ int GetClientId() const;
+
+ /*!
+ * @return The name of the provider. Can be user provided or the backend name
+ */
+ std::string GetName() const;
+
+ /*!
+ * @brief Set the name of the provider.
+ * @param name The new name of the provider.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetName(const std::string& iName);
+
+ /*!
+ * @brief Checks whether this provider has a known type
+ * @return True if this provider has a type other than unknown, false otherwise
+ */
+ bool HasType() const { return m_type != PVR_PROVIDER_TYPE_UNKNOWN; }
+
+ /*!
+ * @brief Gets the type of this provider.
+ * @return the type of this provider.
+ */
+ PVR_PROVIDER_TYPE GetType() const;
+
+ /*!
+ * @brief Sets the type of this provider.
+ * @param type the new provider type.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetType(PVR_PROVIDER_TYPE type);
+
+ /*!
+ * @brief Get the path for this provider's icon
+ * @return iconpath for this provider's icon
+ */
+ std::string GetIconPath() const;
+
+ /*!
+ * @brief Set the path for this icon
+ * @param strIconPath The new path of the icon.
+ * @return true if the icon path was updated successfully
+ */
+ bool SetIconPath(const std::string& strIconPath);
+
+ /*!
+ * @return Get the path to the icon for this provider as given by the client.
+ */
+ std::string GetClientIconPath() const;
+
+ /*!
+ * @brief Get this provider's country codes (ISO 3166).
+ * @return This provider's country codes.
+ */
+ std::vector<std::string> GetCountries() const;
+
+ /*!
+ * @brief Set the country codes for this provider
+ * @param countries The new ISO 3166 country codes for this provider.
+ * @return true if the country codes were updated successfully
+ */
+ bool SetCountries(const std::vector<std::string>& countries);
+
+ /*!
+ * @brief Get this provider's country codes (ISO 3166) as a string.
+ * @return This provider's country codes.
+ */
+ std::string GetCountriesDBString() const;
+
+ /*!
+ * @brief Set the country codes for this provider from a string
+ * @param strCountries The new ISO 3166 country codes for this provider.
+ * @return true if the country codes were updated successfully
+ */
+ bool SetCountriesFromDBString(const std::string& strCountries);
+
+ /*!
+ * @brief Get this provider's language codes (RFC 5646).
+ * @return This provider's language codes
+ */
+ std::vector<std::string> GetLanguages() const;
+
+ /*!
+ * @brief Set the language codes for this provider
+ * @param languages The new RFC 5646 language codes for this provider.
+ * @return true if the language codes were updated successfully
+ */
+ bool SetLanguages(const std::vector<std::string>& languages);
+
+ /*!
+ * @brief Get this provider's language codes (RFC 5646) as a string.
+ * @return This provider's language codes.
+ */
+ std::string GetLanguagesDBString() const;
+
+ /*!
+ * @brief Set the language codes for this provider from a string
+ * @param strLanguages The new RFC 5646 language codes for this provider.
+ * @return true if the language codes were updated successfully
+ */
+ bool SetLanguagesFromDBString(const std::string& strLanguages);
+
+ /*!
+ * @brief Get if this provider has a thumb image path.
+ * @return True if this add-on provider has a thumb image path, false otherwise.
+ */
+ bool HasThumbPath() const;
+
+ /*!
+ * @brief Get this provider's thumb image path. Note only PVR add-on providers will set this value.
+ * @return This add-on provider's thumb image path.
+ */
+ std::string GetThumbPath() const;
+
+ /*!
+ * @return Get the path to the thumb for this provider as given by the client.
+ */
+ std::string GetClientThumbPath() const;
+
+ /*!
+ * @brief Whether a provider is a default provider of a PVR Client add-on or not
+ * @return True if this provider is of a PVR Client add-on, false otherwise.
+ */
+ bool IsClientProvider() const { return m_bIsClientProvider; }
+
+ /*!
+ * @brief updates this provider from the provided entry
+ * @param fromProvider A provider containing the data that shall be merged into this provider's data.
+ * @param updateMode update as User, Client or DB
+ * @return true if the provider was updated successfully
+ */
+ bool UpdateEntry(const std::shared_ptr<CPVRProvider>& fromProvider,
+ ProviderUpdateMode updateMode);
+
+ /*!
+ * @brief Persist this provider in the local database.
+ * @param updateRecord True if an existing record should be updated, false for an insert
+ * @return True on success, false otherwise.
+ */
+ bool Persist(bool updateRecord = false);
+
+ /*!
+ * @brief Delete this provider from the local database.
+ * @return True on success, false otherwise.
+ */
+ bool DeleteFromDatabase();
+
+private:
+ CPVRProvider(const CPVRProvider& provider) = delete;
+ CPVRProvider& operator=(const CPVRProvider& orig) = delete;
+
+ int m_iDatabaseId = PVR_PROVIDER_INVALID_DB_ID; /*!< the identifier given to this provider by the TV database */
+
+ int m_iUniqueId = PVR_PROVIDER_ADDON_UID; /*!< @brief unique ID of the provider on the backend */
+ int m_iClientId; /*!< @brief ID of the backend */
+ std::string m_strName; /*!< @brief name of this provider */
+ PVR_PROVIDER_TYPE m_type = PVR_PROVIDER_TYPE_UNKNOWN; /*!< @brief service type for this provider */
+ CPVRCachedImage m_iconPath; /*!< @brief the path to the icon for this provider */
+ std::string m_strCountries; /*!< @brief the country codes for this provider (empty if undefined) */
+ std::string m_strLanguages; /*!< @brief the language codes for this provider (empty if undefined) */
+ bool m_bIsClientProvider = false; /*!< the provider is a default provider of a PVR Client add-on */
+ CPVRCachedImage m_thumbPath; /*!< a thumb image path for providers that are PVR add-ons */
+
+ mutable CCriticalSection m_critSection;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/providers/PVRProviders.cpp b/xbmc/pvr/providers/PVRProviders.cpp
new file mode 100644
index 0000000..6d751a2
--- /dev/null
+++ b/xbmc/pvr/providers/PVRProviders.cpp
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRProviders.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/providers/PVRProvider.h"
+#include "settings/Settings.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+bool CPVRProvidersContainer::UpdateFromClient(const std::shared_ptr<CPVRProvider>& provider)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRProvider> providerToUpdate =
+ GetByClient(provider->GetClientId(), provider->GetUniqueId());
+ if (providerToUpdate)
+ {
+ return providerToUpdate->UpdateEntry(provider, ProviderUpdateMode::BY_CLIENT);
+ }
+ else
+ {
+ provider->SetDatabaseId(++m_iLastId);
+ InsertEntry(provider, ProviderUpdateMode::BY_CLIENT);
+ }
+
+ return true;
+}
+
+std::shared_ptr<CPVRProvider> CPVRProvidersContainer::GetByClient(int iClientId,
+ int iUniqueId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = std::find_if(
+ m_providers.cbegin(), m_providers.cend(), [iClientId, iUniqueId](const auto& provider) {
+ return provider->GetClientId() == iClientId && provider->GetUniqueId() == iUniqueId;
+ });
+ return it != m_providers.cend() ? (*it) : std::shared_ptr<CPVRProvider>();
+}
+
+void CPVRProvidersContainer::InsertEntry(const std::shared_ptr<CPVRProvider>& newProvider,
+ ProviderUpdateMode updateMode)
+{
+ bool found = false;
+ for (auto& provider : m_providers)
+ {
+ if (provider->GetClientId() == newProvider->GetClientId() &&
+ provider->GetUniqueId() == newProvider->GetUniqueId())
+ {
+ found = true;
+ provider->UpdateEntry(newProvider, updateMode);
+ }
+ }
+
+ if (!found)
+ {
+ m_providers.emplace_back(newProvider);
+ }
+}
+
+std::vector<std::shared_ptr<CPVRProvider>> CPVRProvidersContainer::GetProvidersList() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_providers;
+}
+
+std::size_t CPVRProvidersContainer::GetNumProviders() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_providers.size();
+}
+
+bool CPVRProviders::Update(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ return LoadFromDatabase(clients) && UpdateFromClients(clients);
+}
+
+void CPVRProviders::Unload()
+{
+ // remove all tags
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_providers.clear();
+}
+
+bool CPVRProviders::LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ {
+ m_iLastId = database->GetMaxProviderId();
+
+ CPVRProviders providers;
+ database->Get(providers, clients);
+
+ for (auto& provider : providers.GetProvidersList())
+ CheckAndAddEntry(provider, ProviderUpdateMode::BY_DATABASE);
+ }
+ return true;
+}
+
+bool CPVRProviders::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bIsUpdating)
+ return false;
+ m_bIsUpdating = true;
+ }
+
+ // Default client providers are the add-ons themselves, we retrieve both enabled
+ // and disabled add-ons as we don't want them removed from the DB
+ CPVRProviders newAddonProviderList;
+ std::vector<int> disabledClients;
+ std::vector<CVariant> clientProviderInfos =
+ CServiceBroker::GetPVRManager().Clients()->GetClientProviderInfos();
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Adding default providers, found {} PVR add-ons",
+ clientProviderInfos.size());
+ for (const auto& clientInfo : clientProviderInfos)
+ {
+ auto addonProvider = std::make_shared<CPVRProvider>(
+ clientInfo["clientid"].asInteger32(), clientInfo["name"].asString(),
+ clientInfo["icon"].asString(), clientInfo["thumb"].asString());
+
+ newAddonProviderList.CheckAndAddEntry(addonProvider, ProviderUpdateMode::BY_CLIENT);
+
+ if (!clientInfo["enabled"].asBoolean())
+ disabledClients.emplace_back(clientInfo["clientid"].asInteger32());
+ }
+ UpdateDefaultEntries(newAddonProviderList);
+
+ // Client providers are retrieved from the clients
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updating providers");
+ CPVRProvidersContainer newProviderList;
+ std::vector<int> failedClients;
+ CServiceBroker::GetPVRManager().Clients()->GetProviders(clients, &newProviderList, failedClients);
+ return UpdateClientEntries(newProviderList, failedClients, disabledClients);
+}
+
+bool CPVRProviders::UpdateDefaultEntries(const CPVRProvidersContainer& newProviders)
+{
+ bool bChanged = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // go through the provider list and check for updated or new providers
+ const auto newProviderList = newProviders.GetProvidersList();
+ bChanged = std::accumulate(
+ newProviderList.cbegin(), newProviderList.cend(), false,
+ [this](bool changed, const auto& newProvider) {
+ return (CheckAndPersistEntry(newProvider, ProviderUpdateMode::BY_CLIENT) != nullptr)
+ ? true
+ : changed;
+ });
+
+ // check for deleted providers
+ for (std::vector<std::shared_ptr<CPVRProvider>>::iterator it = m_providers.begin();
+ it != m_providers.end();)
+ {
+ const std::shared_ptr<CPVRProvider> provider = *it;
+ if (!newProviders.GetByClient(provider->GetClientId(), provider->GetUniqueId()))
+ {
+ // provider was not found
+ bool bIgnoreProvider = false;
+
+ // ignore add-on any providers that are no PVR Client addon providers
+ if (!provider->IsClientProvider())
+ bIgnoreProvider = true;
+
+ if (bIgnoreProvider)
+ {
+ ++it;
+ continue;
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleted provider {} on client {}", provider->GetUniqueId(),
+ provider->GetClientId());
+
+ (*it)->DeleteFromDatabase();
+ it = m_providers.erase(it);
+
+ bChanged |= true;
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ return bChanged;
+}
+
+bool CPVRProviders::UpdateClientEntries(const CPVRProvidersContainer& newProviders,
+ const std::vector<int>& failedClients,
+ const std::vector<int>& disabledClients)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // go through the provider list and check for updated or new providers
+ for (const auto& newProvider : newProviders.GetProvidersList())
+ {
+ CheckAndPersistEntry(newProvider, ProviderUpdateMode::BY_CLIENT);
+ }
+
+ // check for deleted providers
+ for (auto it = m_providers.begin(); it != m_providers.end();)
+ {
+ const std::shared_ptr<CPVRProvider> provider = *it;
+ if (!newProviders.GetByClient(provider->GetClientId(), provider->GetUniqueId()))
+ {
+ const bool bIgnoreProvider =
+ (provider->IsClientProvider() || // ignore add-on providers as they are a special case
+ std::any_of(failedClients.cbegin(), failedClients.cend(),
+ [&provider](const auto& failedClient) {
+ return failedClient == provider->GetClientId();
+ }) ||
+ std::any_of(disabledClients.cbegin(), disabledClients.cend(),
+ [&provider](const auto& disabledClient) {
+ return disabledClient == provider->GetClientId();
+ }));
+ if (bIgnoreProvider)
+ {
+ ++it;
+ continue;
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleted provider {} on client {}", provider->GetUniqueId(),
+ provider->GetClientId());
+
+ (*it)->DeleteFromDatabase();
+ it = m_providers.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ m_bIsUpdating = false;
+
+ return true;
+}
+
+std::shared_ptr<CPVRProvider> CPVRProviders::CheckAndAddEntry(
+ const std::shared_ptr<CPVRProvider>& newProvider, ProviderUpdateMode updateMode)
+{
+ bool bChanged = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::shared_ptr<CPVRProvider> provider =
+ GetByClient(newProvider->GetClientId(), newProvider->GetUniqueId());
+ if (provider)
+ {
+ bChanged = provider->UpdateEntry(newProvider, updateMode);
+ }
+ else
+ {
+ // We don't set an id as this came from the DB so it has one already
+ InsertEntry(newProvider, updateMode);
+
+ if (newProvider->GetDatabaseId() > m_iLastId)
+ m_iLastId = newProvider->GetDatabaseId();
+
+ provider = newProvider;
+ bChanged = true;
+ }
+
+ if (bChanged)
+ return provider;
+
+ return {};
+}
+
+std::shared_ptr<CPVRProvider> CPVRProviders::CheckAndPersistEntry(
+ const std::shared_ptr<CPVRProvider>& newProvider, ProviderUpdateMode updateMode)
+{
+ bool bChanged = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::shared_ptr<CPVRProvider> provider =
+ GetByClient(newProvider->GetClientId(), newProvider->GetUniqueId());
+ if (provider)
+ {
+ bChanged = provider->UpdateEntry(newProvider, updateMode);
+
+ if (bChanged)
+ provider->Persist(true);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updated provider {} on client {}", newProvider->GetUniqueId(),
+ newProvider->GetClientId());
+ }
+ else
+ {
+ newProvider->SetDatabaseId(++m_iLastId);
+ InsertEntry(newProvider, updateMode);
+
+ newProvider->Persist();
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Added provider {} on client {}", newProvider->GetUniqueId(),
+ newProvider->GetClientId());
+
+ provider = newProvider;
+ bChanged = true;
+ }
+
+ if (bChanged)
+ return provider;
+
+ return {};
+}
+
+bool CPVRProviders::PersistUserChanges(const std::vector<std::shared_ptr<CPVRProvider>>& providers)
+{
+ for (const auto& provider : providers)
+ {
+ provider->Persist(true);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updated provider {} on client {}", provider->GetUniqueId(),
+ provider->GetClientId());
+ }
+
+ return true;
+}
+
+std::shared_ptr<CPVRProvider> CPVRProviders::GetById(int iProviderId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it =
+ std::find_if(m_providers.cbegin(), m_providers.cend(), [iProviderId](const auto& provider) {
+ return provider->GetDatabaseId() == iProviderId;
+ });
+ return it != m_providers.cend() ? (*it) : std::shared_ptr<CPVRProvider>();
+}
+
+void CPVRProviders::RemoveEntry(const std::shared_ptr<CPVRProvider>& provider)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_providers.erase(
+ std::remove_if(m_providers.begin(), m_providers.end(),
+ [&provider](const std::shared_ptr<CPVRProvider>& providerToRemove) {
+ return provider->GetClientId() == providerToRemove->GetClientId() &&
+ provider->GetUniqueId() == providerToRemove->GetUniqueId();
+ }),
+ m_providers.end());
+}
+
+int CPVRProviders::CleanupCachedImages()
+{
+ std::vector<std::string> urlsToCheck;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& provider : m_providers)
+ {
+ urlsToCheck.emplace_back(provider->GetClientIconPath());
+ urlsToCheck.emplace_back(provider->GetClientThumbPath());
+ }
+ }
+
+ static const std::vector<PVRImagePattern> urlPatterns = {{CPVRProvider::IMAGE_OWNER_PATTERN, ""}};
+ return CPVRCachedImages::Cleanup(urlPatterns, urlsToCheck);
+}
diff --git a/xbmc/pvr/providers/PVRProviders.h b/xbmc/pvr/providers/PVRProviders.h
new file mode 100644
index 0000000..8196d3b
--- /dev/null
+++ b/xbmc/pvr/providers/PVRProviders.h
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <vector>
+
+namespace PVR
+{
+class CPVRClient;
+class CPVRProvider;
+
+enum class ProviderUpdateMode;
+
+class CPVRProvidersContainer
+{
+public:
+ /*!
+ * @brief Add a provider to this container or update the provider if already present in this container.
+ * @param The provider
+ * @return True, if the update was successful. False, otherwise.
+ */
+ bool UpdateFromClient(const std::shared_ptr<CPVRProvider>& provider);
+
+ /*!
+ * @brief Get the provider denoted by given client id and unique client provider id.
+ * @param iClientId The client id.
+ * @param iUniqueId The client provider id.
+ * @return the provider if found, null otherwise.
+ */
+ std::shared_ptr<CPVRProvider> GetByClient(int iClientId, int iUniqueId) const;
+
+ /*!
+ * Get all providers in this container
+ * @return The list of all providers
+ */
+ std::vector<std::shared_ptr<CPVRProvider>> GetProvidersList() const;
+
+ /*!
+ * Get the number of providers in this container
+ * @return The total number of providers
+ */
+ std::size_t GetNumProviders() const;
+
+protected:
+ void InsertEntry(const std::shared_ptr<CPVRProvider>& newProvider, ProviderUpdateMode updateMode);
+
+ mutable CCriticalSection m_critSection;
+ int m_iLastId = 0;
+ std::vector<std::shared_ptr<CPVRProvider>> m_providers;
+};
+
+class CPVRProviders : public CPVRProvidersContainer
+{
+public:
+ CPVRProviders() = default;
+ ~CPVRProviders() = default;
+
+ /**
+ * @brief Update all providers from PVR database and from given clients.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool Update(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /**
+ * @brief unload all providers.
+ */
+ void Unload();
+
+ /**
+ * @brief Update data with providers from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /**
+ * @brief Load all local providers from PVR database.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief Check if the entry exists in the container, if it does update it otherwise add it
+ * @param newProvider The provider entry to update/add in/to the container
+ * @param updateMode update as Client (respect User set values) or DB (update all values)
+ * @return The provider if updated or added, otherwise an empty object (nullptr)
+ */
+ std::shared_ptr<CPVRProvider> CheckAndAddEntry(const std::shared_ptr<CPVRProvider>& newProvider,
+ ProviderUpdateMode updateMode);
+
+ /*!
+ * @brief Check if the entry exists in the container, if it does update it and persist
+ * it in the DB otherwise add it and persist it in the DB.
+ * @param newProvider The provider entry to update/add in/to the container and DB
+ * @param updateMode update as Client (respect User set values) or DB (update all values)
+ * @return The provider if updated or added, otherwise an empty object (nullptr)
+ */
+ std::shared_ptr<CPVRProvider> CheckAndPersistEntry(
+ const std::shared_ptr<CPVRProvider>& newProvider, ProviderUpdateMode updateMode);
+
+ /**
+ * @brief Persist user changes to the current state of the providers in the DB.
+ */
+ bool PersistUserChanges(const std::vector<std::shared_ptr<CPVRProvider>>& providers);
+
+ /*!
+ * @brief Get a provider given it's database ID
+ * @param iProviderId The ID to find
+ * @return The provider, or an empty one when not found
+ */
+ std::shared_ptr<CPVRProvider> GetById(int iProviderId) const;
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+private:
+ void RemoveEntry(const std::shared_ptr<CPVRProvider>& provider);
+ bool UpdateDefaultEntries(const CPVRProvidersContainer& newProviders);
+ bool UpdateClientEntries(const CPVRProvidersContainer& newProviders,
+ const std::vector<int>& failedClients,
+ const std::vector<int>& disabledClients);
+
+ bool m_bIsUpdating = false;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/recordings/CMakeLists.txt b/xbmc/pvr/recordings/CMakeLists.txt
new file mode 100644
index 0000000..55f7028
--- /dev/null
+++ b/xbmc/pvr/recordings/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES PVRRecording.cpp
+ PVRRecordings.cpp
+ PVRRecordingsPath.cpp)
+
+set(HEADERS PVRRecording.h
+ PVRRecordings.h
+ PVRRecordingsPath.h)
+
+core_add_library(pvr_recordings)
diff --git a/xbmc/pvr/recordings/PVRRecording.cpp b/xbmc/pvr/recordings/PVRRecording.cpp
new file mode 100644
index 0000000..a2e1cbd
--- /dev/null
+++ b/xbmc/pvr/recordings/PVRRecording.cpp
@@ -0,0 +1,730 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRRecording.h"
+
+#include "ServiceBroker.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_recordings.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/providers/PVRProvider.h"
+#include "pvr/providers/PVRProviders.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+using namespace std::chrono_literals;
+
+CPVRRecordingUid::CPVRRecordingUid(int iClientId, const std::string& strRecordingId)
+ : m_iClientId(iClientId), m_strRecordingId(strRecordingId)
+{
+}
+
+bool CPVRRecordingUid::operator>(const CPVRRecordingUid& right) const
+{
+ return (m_iClientId == right.m_iClientId) ? m_strRecordingId > right.m_strRecordingId
+ : m_iClientId > right.m_iClientId;
+}
+
+bool CPVRRecordingUid::operator<(const CPVRRecordingUid& right) const
+{
+ return (m_iClientId == right.m_iClientId) ? m_strRecordingId < right.m_strRecordingId
+ : m_iClientId < right.m_iClientId;
+}
+
+bool CPVRRecordingUid::operator==(const CPVRRecordingUid& right) const
+{
+ return m_iClientId == right.m_iClientId && m_strRecordingId == right.m_strRecordingId;
+}
+
+bool CPVRRecordingUid::operator!=(const CPVRRecordingUid& right) const
+{
+ return m_iClientId != right.m_iClientId || m_strRecordingId != right.m_strRecordingId;
+}
+
+const std::string CPVRRecording::IMAGE_OWNER_PATTERN = "pvrrecording";
+
+CPVRRecording::CPVRRecording()
+ : m_iconPath(IMAGE_OWNER_PATTERN),
+ m_thumbnailPath(IMAGE_OWNER_PATTERN),
+ m_fanartPath(IMAGE_OWNER_PATTERN)
+{
+ Reset();
+}
+
+CPVRRecording::CPVRRecording(const PVR_RECORDING& recording, unsigned int iClientId)
+ : m_iconPath(recording.strIconPath, IMAGE_OWNER_PATTERN),
+ m_thumbnailPath(recording.strThumbnailPath, IMAGE_OWNER_PATTERN),
+ m_fanartPath(recording.strFanartPath, IMAGE_OWNER_PATTERN)
+{
+ Reset();
+
+ m_strRecordingId = recording.strRecordingId;
+ m_strTitle = recording.strTitle;
+ m_strShowTitle = recording.strEpisodeName;
+ m_iSeason = recording.iSeriesNumber;
+ m_iEpisode = recording.iEpisodeNumber;
+ if (recording.iYear > 0)
+ SetYear(recording.iYear);
+ m_iClientId = iClientId;
+ m_recordingTime =
+ recording.recordingTime +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection;
+ m_iPriority = recording.iPriority;
+ m_iLifetime = recording.iLifetime;
+ // Deleted recording is placed at the root of the deleted view
+ m_strDirectory = recording.bIsDeleted ? "" : recording.strDirectory;
+ m_strPlot = recording.strPlot;
+ m_strPlotOutline = recording.strPlotOutline;
+ m_strChannelName = recording.strChannelName;
+ m_bIsDeleted = recording.bIsDeleted;
+ m_iEpgEventId = recording.iEpgEventId;
+ m_iChannelUid = recording.iChannelUid;
+ if (strlen(recording.strFirstAired) > 0)
+ m_firstAired.SetFromW3CDateTime(recording.strFirstAired);
+ m_iFlags = recording.iFlags;
+ if (recording.sizeInBytes >= 0)
+ m_sizeInBytes = recording.sizeInBytes;
+ m_strProviderName = recording.strProviderName;
+ m_iClientProviderUniqueId = recording.iClientProviderUid;
+
+ SetGenre(recording.iGenreType, recording.iGenreSubType, recording.strGenreDescription);
+ CVideoInfoTag::SetPlayCount(recording.iPlayCount);
+ if (recording.iLastPlayedPosition > 0 && recording.iDuration > recording.iLastPlayedPosition)
+ CVideoInfoTag::SetResumePoint(recording.iLastPlayedPosition, recording.iDuration, "");
+ SetDuration(recording.iDuration);
+
+ // As the channel a recording was done on (probably long time ago) might no longer be
+ // available today prefer addon-supplied channel type (tv/radio) over channel attribute.
+ if (recording.channelType != PVR_RECORDING_CHANNEL_TYPE_UNKNOWN)
+ {
+ m_bRadio = recording.channelType == PVR_RECORDING_CHANNEL_TYPE_RADIO;
+ }
+ else
+ {
+ const std::shared_ptr<CPVRChannel> channel(Channel());
+ if (channel)
+ {
+ m_bRadio = channel->IsRadio();
+ }
+ else
+ {
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ bool bSupportsRadio = client && client->GetClientCapabilities().SupportsRadio();
+ if (bSupportsRadio && client && client->GetClientCapabilities().SupportsTV())
+ {
+ CLog::Log(LOGWARNING, "Unable to determine channel type. Defaulting to TV.");
+ m_bRadio = false; // Assume TV.
+ }
+ else
+ {
+ m_bRadio = bSupportsRadio;
+ }
+ }
+ }
+
+ UpdatePath();
+}
+
+bool CPVRRecording::operator==(const CPVRRecording& right) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return (this == &right) ||
+ (m_strRecordingId == right.m_strRecordingId && m_iClientId == right.m_iClientId &&
+ m_strChannelName == right.m_strChannelName && m_recordingTime == right.m_recordingTime &&
+ GetDuration() == right.GetDuration() && m_strPlotOutline == right.m_strPlotOutline &&
+ m_strPlot == right.m_strPlot && m_iPriority == right.m_iPriority &&
+ m_iLifetime == right.m_iLifetime && m_strDirectory == right.m_strDirectory &&
+ m_strFileNameAndPath == right.m_strFileNameAndPath && m_strTitle == right.m_strTitle &&
+ m_strShowTitle == right.m_strShowTitle && m_iSeason == right.m_iSeason &&
+ m_iEpisode == right.m_iEpisode && GetPremiered() == right.GetPremiered() &&
+ m_iconPath == right.m_iconPath && m_thumbnailPath == right.m_thumbnailPath &&
+ m_fanartPath == right.m_fanartPath && m_iRecordingId == right.m_iRecordingId &&
+ m_bIsDeleted == right.m_bIsDeleted && m_iEpgEventId == right.m_iEpgEventId &&
+ m_iChannelUid == right.m_iChannelUid && m_bRadio == right.m_bRadio &&
+ m_genre == right.m_genre && m_iGenreType == right.m_iGenreType &&
+ m_iGenreSubType == right.m_iGenreSubType && m_firstAired == right.m_firstAired &&
+ m_iFlags == right.m_iFlags && m_sizeInBytes == right.m_sizeInBytes &&
+ m_strProviderName == right.m_strProviderName &&
+ m_iClientProviderUniqueId == right.m_iClientProviderUniqueId);
+}
+
+bool CPVRRecording::operator!=(const CPVRRecording& right) const
+{
+ return !(*this == right);
+}
+
+void CPVRRecording::FillAddonData(PVR_RECORDING& recording) const
+{
+ time_t recTime;
+ RecordingTimeAsUTC().GetAsTime(recTime);
+
+ recording = {};
+ strncpy(recording.strRecordingId, ClientRecordingID().c_str(),
+ sizeof(recording.strRecordingId) - 1);
+ strncpy(recording.strTitle, m_strTitle.c_str(), sizeof(recording.strTitle) - 1);
+ strncpy(recording.strEpisodeName, m_strShowTitle.c_str(), sizeof(recording.strEpisodeName) - 1);
+ recording.iSeriesNumber = m_iSeason;
+ recording.iEpisodeNumber = m_iEpisode;
+ recording.iYear = GetYear();
+ strncpy(recording.strDirectory, Directory().c_str(), sizeof(recording.strDirectory) - 1);
+ strncpy(recording.strPlotOutline, m_strPlotOutline.c_str(), sizeof(recording.strPlotOutline) - 1);
+ strncpy(recording.strPlot, m_strPlot.c_str(), sizeof(recording.strPlot) - 1);
+ strncpy(recording.strGenreDescription, GetGenresLabel().c_str(),
+ sizeof(recording.strGenreDescription) - 1);
+ strncpy(recording.strChannelName, ChannelName().c_str(), sizeof(recording.strChannelName) - 1);
+ strncpy(recording.strIconPath, ClientIconPath().c_str(), sizeof(recording.strIconPath) - 1);
+ strncpy(recording.strThumbnailPath, ClientThumbnailPath().c_str(),
+ sizeof(recording.strThumbnailPath) - 1);
+ strncpy(recording.strFanartPath, ClientFanartPath().c_str(), sizeof(recording.strFanartPath) - 1);
+ recording.recordingTime =
+ recTime - CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection;
+ recording.iDuration = GetDuration();
+ recording.iPriority = Priority();
+ recording.iLifetime = LifeTime();
+ recording.iGenreType = GenreType();
+ recording.iGenreSubType = GenreSubType();
+ recording.iPlayCount = GetLocalPlayCount();
+ recording.iLastPlayedPosition = std::lrint(GetLocalResumePoint().timeInSeconds);
+ recording.bIsDeleted = IsDeleted();
+ recording.iEpgEventId = m_iEpgEventId;
+ recording.iChannelUid = ChannelUid();
+ recording.channelType =
+ IsRadio() ? PVR_RECORDING_CHANNEL_TYPE_RADIO : PVR_RECORDING_CHANNEL_TYPE_TV;
+ if (FirstAired().IsValid())
+ strncpy(recording.strFirstAired, FirstAired().GetAsW3CDate().c_str(),
+ sizeof(recording.strFirstAired) - 1);
+ recording.iFlags = Flags();
+ recording.sizeInBytes = GetSizeInBytes();
+ strncpy(recording.strProviderName, ProviderName().c_str(), sizeof(recording.strProviderName) - 1);
+ recording.iClientProviderUid = ClientProviderUniqueId();
+}
+
+void CPVRRecording::Serialize(CVariant& value) const
+{
+ CVideoInfoTag::Serialize(value);
+
+ value["channel"] = m_strChannelName;
+ value["lifetime"] = m_iLifetime;
+ value["directory"] = m_strDirectory;
+ value["icon"] = ClientIconPath();
+ value["starttime"] = m_recordingTime.IsValid() ? m_recordingTime.GetAsDBDateTime() : "";
+ value["endtime"] = m_recordingTime.IsValid() ? EndTimeAsUTC().GetAsDBDateTime() : "";
+ value["recordingid"] = m_iRecordingId;
+ value["isdeleted"] = m_bIsDeleted;
+ value["epgeventid"] = m_iEpgEventId;
+ value["channeluid"] = m_iChannelUid;
+ value["radio"] = m_bRadio;
+ value["genre"] = m_genre;
+
+ if (!value.isMember("art"))
+ value["art"] = CVariant(CVariant::VariantTypeObject);
+ if (!ClientThumbnailPath().empty())
+ value["art"]["thumb"] = ClientThumbnailPath();
+ if (!ClientFanartPath().empty())
+ value["art"]["fanart"] = ClientFanartPath();
+
+ value["clientid"] = m_iClientId;
+}
+
+void CPVRRecording::ToSortable(SortItem& sortable, Field field) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (field == FieldSize)
+ sortable[FieldSize] = m_sizeInBytes;
+ else if (field == FieldProvider)
+ sortable[FieldProvider] = StringUtils::Format("{} {}", m_iClientId, m_iClientProviderUniqueId);
+ else
+ CVideoInfoTag::ToSortable(sortable, field);
+}
+
+void CPVRRecording::Reset()
+{
+ m_strRecordingId.clear();
+ m_iClientId = -1;
+ m_strChannelName.clear();
+ m_strDirectory.clear();
+ m_iPriority = -1;
+ m_iLifetime = -1;
+ m_strFileNameAndPath.clear();
+ m_bGotMetaData = false;
+ m_iRecordingId = 0;
+ m_bIsDeleted = false;
+ m_bInProgress = true;
+ m_iEpgEventId = EPG_TAG_INVALID_UID;
+ m_iSeason = -1;
+ m_iEpisode = -1;
+ m_iChannelUid = PVR_CHANNEL_INVALID_UID;
+ m_bRadio = false;
+ m_iFlags = PVR_RECORDING_FLAG_UNDEFINED;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_sizeInBytes = 0;
+ }
+ m_strProviderName.clear();
+ m_iClientProviderUniqueId = PVR_PROVIDER_INVALID_UID;
+
+ m_recordingTime.Reset();
+ CVideoInfoTag::Reset();
+}
+
+bool CPVRRecording::Delete()
+{
+ std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ return client && (client->DeleteRecording(*this) == PVR_ERROR_NO_ERROR);
+}
+
+bool CPVRRecording::Undelete()
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ return client && (client->UndeleteRecording(*this) == PVR_ERROR_NO_ERROR);
+}
+
+bool CPVRRecording::Rename(const std::string& strNewName)
+{
+ m_strTitle = strNewName;
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ return client && (client->RenameRecording(*this) == PVR_ERROR_NO_ERROR);
+}
+
+bool CPVRRecording::SetPlayCount(int count)
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsPlayCount())
+ {
+ if (client->SetRecordingPlayCount(*this, count) != PVR_ERROR_NO_ERROR)
+ return false;
+ }
+
+ return CVideoInfoTag::SetPlayCount(count);
+}
+
+bool CPVRRecording::IncrementPlayCount()
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsPlayCount())
+ {
+ if (client->SetRecordingPlayCount(*this, CVideoInfoTag::GetPlayCount() + 1) !=
+ PVR_ERROR_NO_ERROR)
+ return false;
+ }
+
+ return CVideoInfoTag::IncrementPlayCount();
+}
+
+bool CPVRRecording::SetResumePoint(const CBookmark& resumePoint)
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsLastPlayedPosition())
+ {
+ if (client->SetRecordingLastPlayedPosition(*this, lrint(resumePoint.timeInSeconds)) !=
+ PVR_ERROR_NO_ERROR)
+ return false;
+ }
+
+ return CVideoInfoTag::SetResumePoint(resumePoint);
+}
+
+bool CPVRRecording::SetResumePoint(double timeInSeconds,
+ double totalTimeInSeconds,
+ const std::string& playerState /* = "" */)
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsLastPlayedPosition())
+ {
+ if (client->SetRecordingLastPlayedPosition(*this, lrint(timeInSeconds)) != PVR_ERROR_NO_ERROR)
+ return false;
+ }
+
+ return CVideoInfoTag::SetResumePoint(timeInSeconds, totalTimeInSeconds, playerState);
+}
+
+CBookmark CPVRRecording::GetResumePoint() const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsLastPlayedPosition() &&
+ m_resumePointRefetchTimeout.IsTimePast())
+ {
+ // @todo: root cause should be fixed. details: https://github.com/xbmc/xbmc/pull/14961
+ m_resumePointRefetchTimeout.Set(10s); // update resume point from backend at most every 10 secs
+
+ int pos = -1;
+ client->GetRecordingLastPlayedPosition(*this, pos);
+
+ if (pos >= 0)
+ {
+ CBookmark resumePoint(CVideoInfoTag::GetResumePoint());
+ resumePoint.timeInSeconds = pos;
+ resumePoint.totalTimeInSeconds = (pos == 0) ? 0 : m_duration;
+ CPVRRecording* pThis = const_cast<CPVRRecording*>(this);
+ pThis->CVideoInfoTag::SetResumePoint(resumePoint);
+ }
+ }
+ return CVideoInfoTag::GetResumePoint();
+}
+
+bool CPVRRecording::UpdateRecordingSize()
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsSize() &&
+ m_recordingSizeRefetchTimeout.IsTimePast())
+ {
+ // @todo: root cause should be fixed. details: https://github.com/xbmc/xbmc/pull/14961
+ m_recordingSizeRefetchTimeout.Set(10s); // update size from backend at most every 10 secs
+
+ int64_t sizeInBytes = -1;
+ client->GetRecordingSize(*this, sizeInBytes);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (sizeInBytes >= 0 && sizeInBytes != m_sizeInBytes)
+ {
+ m_sizeInBytes = sizeInBytes;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CPVRRecording::UpdateMetadata(CVideoDatabase& db, const CPVRClient& client)
+{
+ if (m_bGotMetaData || !db.IsOpen())
+ return;
+
+ if (!client.GetClientCapabilities().SupportsRecordingsPlayCount())
+ CVideoInfoTag::SetPlayCount(db.GetPlayCount(m_strFileNameAndPath));
+
+ if (!client.GetClientCapabilities().SupportsRecordingsLastPlayedPosition())
+ {
+ CBookmark resumePoint;
+ if (db.GetResumeBookMark(m_strFileNameAndPath, resumePoint))
+ CVideoInfoTag::SetResumePoint(resumePoint);
+ }
+
+ m_lastPlayed = db.GetLastPlayed(m_strFileNameAndPath);
+
+ m_bGotMetaData = true;
+}
+
+std::vector<PVR_EDL_ENTRY> CPVRRecording::GetEdl() const
+{
+ std::vector<PVR_EDL_ENTRY> edls;
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsEdl())
+ client->GetRecordingEdl(*this, edls);
+
+ return edls;
+}
+
+void CPVRRecording::Update(const CPVRRecording& tag, const CPVRClient& client)
+{
+ m_strRecordingId = tag.m_strRecordingId;
+ m_iClientId = tag.m_iClientId;
+ m_strTitle = tag.m_strTitle;
+ m_strShowTitle = tag.m_strShowTitle;
+ m_iSeason = tag.m_iSeason;
+ m_iEpisode = tag.m_iEpisode;
+ SetPremiered(tag.GetPremiered());
+ m_recordingTime = tag.m_recordingTime;
+ m_iPriority = tag.m_iPriority;
+ m_iLifetime = tag.m_iLifetime;
+ m_strDirectory = tag.m_strDirectory;
+ m_strPlot = tag.m_strPlot;
+ m_strPlotOutline = tag.m_strPlotOutline;
+ m_strChannelName = tag.m_strChannelName;
+ m_genre = tag.m_genre;
+ m_iconPath = tag.m_iconPath;
+ m_thumbnailPath = tag.m_thumbnailPath;
+ m_fanartPath = tag.m_fanartPath;
+ m_bIsDeleted = tag.m_bIsDeleted;
+ m_iEpgEventId = tag.m_iEpgEventId;
+ m_iChannelUid = tag.m_iChannelUid;
+ m_bRadio = tag.m_bRadio;
+ m_firstAired = tag.m_firstAired;
+ m_iFlags = tag.m_iFlags;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_sizeInBytes = tag.m_sizeInBytes;
+ m_strProviderName = tag.m_strProviderName;
+ m_iClientProviderUniqueId = tag.m_iClientProviderUniqueId;
+ }
+
+ if (client.GetClientCapabilities().SupportsRecordingsPlayCount())
+ CVideoInfoTag::SetPlayCount(tag.GetLocalPlayCount());
+
+ if (client.GetClientCapabilities().SupportsRecordingsLastPlayedPosition())
+ CVideoInfoTag::SetResumePoint(tag.GetLocalResumePoint());
+
+ SetDuration(tag.GetDuration());
+
+ if (m_iGenreType == EPG_GENRE_USE_STRING || m_iGenreSubType == EPG_GENRE_USE_STRING)
+ {
+ /* No type/subtype. Use the provided description */
+ m_genre = tag.m_genre;
+ }
+ else
+ {
+ /* Determine genre description by type/subtype */
+ m_genre = StringUtils::Split(
+ CPVREpg::ConvertGenreIdToString(tag.m_iGenreType, tag.m_iGenreSubType),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ }
+
+ //Old Method of identifying TV show title and subtitle using m_strDirectory and strPlotOutline (deprecated)
+ std::string strShow = StringUtils::Format("{} - ", g_localizeStrings.Get(20364));
+ if (StringUtils::StartsWithNoCase(m_strPlotOutline, strShow))
+ {
+ CLog::Log(LOGWARNING, "PVR addon provides episode name in strPlotOutline which is deprecated");
+ std::string strEpisode = m_strPlotOutline;
+ std::string strTitle = m_strDirectory;
+
+ size_t pos = strTitle.rfind('/');
+ strTitle.erase(0, pos + 1);
+ strEpisode.erase(0, strShow.size());
+ m_strTitle = strTitle;
+ pos = strEpisode.find('-');
+ strEpisode.erase(0, pos + 2);
+ m_strShowTitle = strEpisode;
+ }
+
+ UpdatePath();
+}
+
+void CPVRRecording::UpdatePath()
+{
+ m_strFileNameAndPath = CPVRRecordingsPath(m_bIsDeleted, m_bRadio, m_strDirectory, m_strTitle,
+ m_iSeason, m_iEpisode, GetYear(), m_strShowTitle,
+ m_strChannelName, m_recordingTime, m_strRecordingId);
+}
+
+const CDateTime& CPVRRecording::RecordingTimeAsLocalTime() const
+{
+ static CDateTime tmp;
+ tmp.SetFromUTCDateTime(m_recordingTime);
+
+ return tmp;
+}
+
+CDateTime CPVRRecording::EndTimeAsUTC() const
+{
+ unsigned int duration = GetDuration();
+ return m_recordingTime + CDateTimeSpan(0, 0, duration / 60, duration % 60);
+}
+
+CDateTime CPVRRecording::EndTimeAsLocalTime() const
+{
+ CDateTime ret;
+ ret.SetFromUTCDateTime(EndTimeAsUTC());
+ return ret;
+}
+
+bool CPVRRecording::WillBeExpiredWithNewLifetime(int iLifetime) const
+{
+ if (iLifetime > 0)
+ return (EndTimeAsUTC() + CDateTimeSpan(iLifetime, 0, 0, 0)) <= CDateTime::GetUTCDateTime();
+
+ return false;
+}
+
+CDateTime CPVRRecording::ExpirationTimeAsLocalTime() const
+{
+ CDateTime ret;
+ if (m_iLifetime > 0)
+ ret = EndTimeAsLocalTime() + CDateTimeSpan(m_iLifetime, 0, 0, 0);
+
+ return ret;
+}
+
+std::string CPVRRecording::GetTitleFromURL(const std::string& url)
+{
+ return CPVRRecordingsPath(url).GetTitle();
+}
+
+std::shared_ptr<CPVRChannel> CPVRRecording::Channel() const
+{
+ if (m_iChannelUid != PVR_CHANNEL_INVALID_UID)
+ return CServiceBroker::GetPVRManager().ChannelGroups()->GetByUniqueID(m_iChannelUid,
+ m_iClientId);
+
+ return std::shared_ptr<CPVRChannel>();
+}
+
+int CPVRRecording::ChannelUid() const
+{
+ return m_iChannelUid;
+}
+
+int CPVRRecording::ClientID() const
+{
+ return m_iClientId;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRRecording::GetRecordingTimer() const
+{
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>> recordingTimers =
+ CServiceBroker::GetPVRManager().Timers()->GetActiveRecordings();
+
+ for (const auto& timer : recordingTimers)
+ {
+ if (timer->ClientID() == ClientID() && timer->ClientChannelUID() == ChannelUid())
+ {
+ // first, match epg event uids, if available
+ if (timer->UniqueBroadcastID() == BroadcastUid() &&
+ timer->UniqueBroadcastID() != EPG_TAG_INVALID_UID)
+ return timer;
+
+ // alternatively, match start and end times
+ const CDateTime timerStart =
+ timer->StartAsUTC() - CDateTimeSpan(0, 0, timer->MarginStart(), 0);
+ const CDateTime timerEnd = timer->EndAsUTC() + CDateTimeSpan(0, 0, timer->MarginEnd(), 0);
+ if (timerStart <= RecordingTimeAsUTC() && timerEnd >= EndTimeAsUTC())
+ return timer;
+ }
+ }
+ return {};
+}
+
+bool CPVRRecording::IsInProgress() const
+{
+ // Note: It is not enough to only check recording time and duration against 'now'.
+ // Only the state of the related timer is a safe indicator that the backend
+ // actually is recording this.
+ // Once the recording is known to not be in progress that will never change.
+ if (m_bInProgress)
+ m_bInProgress = GetRecordingTimer() != nullptr;
+ return m_bInProgress;
+}
+
+void CPVRRecording::SetGenre(int iGenreType, int iGenreSubType, const std::string& strGenre)
+{
+ m_iGenreType = iGenreType;
+ m_iGenreSubType = iGenreSubType;
+
+ if ((iGenreType == EPG_GENRE_USE_STRING || iGenreSubType == EPG_GENRE_USE_STRING) &&
+ !strGenre.empty())
+ {
+ /* Type and sub type are not given. Use the provided genre description if available. */
+ m_genre = StringUtils::Split(
+ strGenre,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ }
+ else
+ {
+ /* Determine the genre description from the type and subtype IDs */
+ m_genre = StringUtils::Split(
+ CPVREpg::ConvertGenreIdToString(iGenreType, iGenreSubType),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ }
+}
+
+const std::string CPVRRecording::GetGenresLabel() const
+{
+ return StringUtils::Join(
+ m_genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+}
+
+CDateTime CPVRRecording::FirstAired() const
+{
+ return m_firstAired;
+}
+
+void CPVRRecording::SetYear(int year)
+{
+ if (year > 0)
+ m_premiered = CDateTime(year, 1, 1, 0, 0, 0);
+}
+
+int CPVRRecording::GetYear() const
+{
+ return m_premiered.IsValid() ? m_premiered.GetYear() : 0;
+}
+
+bool CPVRRecording::HasYear() const
+{
+ return m_premiered.IsValid();
+}
+
+bool CPVRRecording::IsNew() const
+{
+ return (m_iFlags & PVR_RECORDING_FLAG_IS_NEW) > 0;
+}
+
+bool CPVRRecording::IsPremiere() const
+{
+ return (m_iFlags & PVR_RECORDING_FLAG_IS_PREMIERE) > 0;
+}
+
+bool CPVRRecording::IsLive() const
+{
+ return (m_iFlags & PVR_RECORDING_FLAG_IS_LIVE) > 0;
+}
+
+bool CPVRRecording::IsFinale() const
+{
+ return (m_iFlags & PVR_RECORDING_FLAG_IS_FINALE) > 0;
+}
+
+int64_t CPVRRecording::GetSizeInBytes() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_sizeInBytes;
+}
+
+int CPVRRecording::ClientProviderUniqueId() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientProviderUniqueId;
+}
+
+std::string CPVRRecording::ProviderName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProviderName;
+}
+
+std::shared_ptr<CPVRProvider> CPVRRecording::GetDefaultProvider() const
+{
+ return CServiceBroker::GetPVRManager().Providers()->GetByClient(m_iClientId,
+ PVR_PROVIDER_INVALID_UID);
+}
+
+bool CPVRRecording::HasClientProvider() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientProviderUniqueId != PVR_PROVIDER_INVALID_UID;
+}
+
+std::shared_ptr<CPVRProvider> CPVRRecording::GetProvider() const
+{
+ auto provider = CServiceBroker::GetPVRManager().Providers()->GetByClient(
+ m_iClientId, m_iClientProviderUniqueId);
+
+ if (!provider)
+ provider = GetDefaultProvider();
+
+ return provider;
+}
diff --git a/xbmc/pvr/recordings/PVRRecording.h b/xbmc/pvr/recordings/PVRRecording.h
new file mode 100644
index 0000000..bdd8378
--- /dev/null
+++ b/xbmc/pvr/recordings/PVRRecording.h
@@ -0,0 +1,541 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_providers.h"
+#include "pvr/PVRCachedImage.h"
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "video/Bookmark.h"
+#include "video/VideoInfoTag.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CVideoDatabase;
+
+struct PVR_EDL_ENTRY;
+struct PVR_RECORDING;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRClient;
+class CPVRProvider;
+class CPVRTimerInfoTag;
+
+/*!
+ * @brief Representation of a CPVRRecording unique ID.
+ */
+class CPVRRecordingUid final
+{
+public:
+ int m_iClientId; /*!< ID of the backend */
+ std::string m_strRecordingId; /*!< unique ID of the recording on the client */
+
+ CPVRRecordingUid(int iClientId, const std::string& strRecordingId);
+
+ bool operator>(const CPVRRecordingUid& right) const;
+ bool operator<(const CPVRRecordingUid& right) const;
+ bool operator==(const CPVRRecordingUid& right) const;
+ bool operator!=(const CPVRRecordingUid& right) const;
+};
+
+class CPVRRecording final : public CVideoInfoTag
+{
+public:
+ static const std::string IMAGE_OWNER_PATTERN;
+
+ CPVRRecording();
+ CPVRRecording(const PVR_RECORDING& recording, unsigned int iClientId);
+
+ bool operator==(const CPVRRecording& right) const;
+ bool operator!=(const CPVRRecording& right) const;
+
+ /*!
+ * @brief Copy over data to the given PVR_RECORDING instance.
+ * @param recording The recording instance to fill.
+ */
+ void FillAddonData(PVR_RECORDING& recording) const;
+
+ void Serialize(CVariant& value) const override;
+
+ // ISortable implementation
+ void ToSortable(SortItem& sortable, Field field) const override;
+
+ /*!
+ * @brief Reset this tag to it's initial state.
+ */
+ void Reset();
+
+ /*!
+ * @brief Delete this recording on the client (if supported).
+ * @return True if it was deleted successfully, false otherwise.
+ */
+ bool Delete();
+
+ /*!
+ * @brief Undelete this recording on the client (if supported).
+ * @return True if it was undeleted successfully, false otherwise.
+ */
+ bool Undelete();
+
+ /*!
+ * @brief Rename this recording on the client (if supported).
+ * @param strNewName The new name.
+ * @return True if it was renamed successfully, false otherwise.
+ */
+ bool Rename(const std::string& strNewName);
+
+ /*!
+ * @brief Set this recording's play count. The value will be transferred to the backend if it supports server-side play counts.
+ * @param count play count.
+ * @return True if play count was set successfully, false otherwise.
+ */
+ bool SetPlayCount(int count) override;
+
+ /*!
+ * @brief Increment this recording's play count. The value will be transferred to the backend if it supports server-side play counts.
+ * @return True if play count was increased successfully, false otherwise.
+ */
+ bool IncrementPlayCount() override;
+
+ /*!
+ * @brief Set this recording's play count without transferring the value to the backend, even if it supports server-side play counts.
+ * @param count play count.
+ * @return True if play count was set successfully, false otherwise.
+ */
+ bool SetLocalPlayCount(int count) { return CVideoInfoTag::SetPlayCount(count); }
+
+ /*!
+ * @brief Get this recording's local play count. The value will not be obtained from the backend, even if it supports server-side play counts.
+ * @return the play count.
+ */
+ int GetLocalPlayCount() const { return CVideoInfoTag::GetPlayCount(); }
+
+ /*!
+ * @brief Set this recording's resume point. The value will be transferred to the backend if it supports server-side resume points.
+ * @param resumePoint resume point.
+ * @return True if resume point was set successfully, false otherwise.
+ */
+ bool SetResumePoint(const CBookmark& resumePoint) override;
+
+ /*!
+ * @brief Set this recording's resume point. The value will be transferred to the backend if it supports server-side resume points.
+ * @param timeInSeconds the time of the resume point
+ * @param totalTimeInSeconds the total time of the video
+ * @param playerState the player state
+ * @return True if resume point was set successfully, false otherwise.
+ */
+ bool SetResumePoint(double timeInSeconds,
+ double totalTimeInSeconds,
+ const std::string& playerState = "") override;
+
+ /*!
+ * @brief Get this recording's resume point. The value will be obtained from the backend if it supports server-side resume points.
+ * @return the resume point.
+ */
+ CBookmark GetResumePoint() const override;
+
+ /*!
+ * @brief Update this recording's size. The value will be obtained from the backend if it supports server-side size retrieval.
+ * @return true if the the updated value is different, false otherwise.
+ */
+ bool UpdateRecordingSize();
+
+ /*!
+ * @brief Get this recording's local resume point. The value will not be obtained from the backend even if it supports server-side resume points.
+ * @return the resume point.
+ */
+ CBookmark GetLocalResumePoint() const { return CVideoInfoTag::GetResumePoint(); }
+
+ /*!
+ * @brief Retrieve the edit decision list (EDL) of a recording on the backend.
+ * @return The edit decision list (empty on error)
+ */
+ std::vector<PVR_EDL_ENTRY> GetEdl() const;
+
+ /*!
+ * @brief Get the resume point and play count from the database if the
+ * client doesn't handle it itself.
+ * @param db The database to read the data from.
+ * @param client The client this recording belongs to.
+ */
+ void UpdateMetadata(CVideoDatabase& db, const CPVRClient& client);
+
+ /*!
+ * @brief Update this tag with the contents of the given tag.
+ * @param tag The new tag info.
+ * @param client The client this recording belongs to.
+ */
+ void Update(const CPVRRecording& tag, const CPVRClient& client);
+
+ /*!
+ * @brief Retrieve the recording start as UTC time
+ * @return the recording start time
+ */
+ const CDateTime& RecordingTimeAsUTC() const { return m_recordingTime; }
+
+ /*!
+ * @brief Retrieve the recording start as local time
+ * @return the recording start time
+ */
+ const CDateTime& RecordingTimeAsLocalTime() const;
+
+ /*!
+ * @brief Retrieve the recording end as UTC time
+ * @return the recording end time
+ */
+ CDateTime EndTimeAsUTC() const;
+
+ /*!
+ * @brief Retrieve the recording end as local time
+ * @return the recording end time
+ */
+ CDateTime EndTimeAsLocalTime() const;
+
+ /*!
+ * @brief Check whether this recording has an expiration time
+ * @return True if the recording has an expiration time, false otherwise
+ */
+ bool HasExpirationTime() const { return m_iLifetime > 0; }
+
+ /*!
+ * @brief Retrieve the recording expiration time as local time
+ * @return the recording expiration time
+ */
+ CDateTime ExpirationTimeAsLocalTime() const;
+
+ /*!
+ * @brief Check whether this recording will immediately expire if the given lifetime value would be set
+ * @param iLifetime The lifetime value to check
+ * @return True if the recording would immediately expire, false otherwiese
+ */
+ bool WillBeExpiredWithNewLifetime(int iLifetime) const;
+
+ /*!
+ * @brief Retrieve the recording title from the URL path
+ * @param url the URL for the recording
+ * @return Title of the recording
+ */
+ static std::string GetTitleFromURL(const std::string& url);
+
+ /*!
+ * @brief If deleted but can be undeleted it is true
+ */
+ bool IsDeleted() const { return m_bIsDeleted; }
+
+ /*!
+ * @brief Check whether this is a tv or radio recording
+ * @return true if this is a radio recording, false if this is a tv recording
+ */
+ bool IsRadio() const { return m_bRadio; }
+
+ /*!
+ * @return Broadcast id of the EPG event associated with this recording or EPG_TAG_INVALID_UID
+ */
+ unsigned int BroadcastUid() const { return m_iEpgEventId; }
+
+ /*!
+ * @return Get the channel on which this recording is/was running
+ * @note Only works if the recording has a channel uid provided by the add-on
+ */
+ std::shared_ptr<CPVRChannel> Channel() const;
+
+ /*!
+ * @brief Get the uid of the channel on which this recording is/was running
+ * @return the uid of the channel or PVR_CHANNEL_INVALID_UID
+ */
+ int ChannelUid() const;
+
+ /*!
+ * @brief the identifier of the client that serves this recording
+ * @return the client identifier
+ */
+ int ClientID() const;
+
+ /*!
+ * @brief Get the recording ID as upplied by the client
+ * @return the recording identifier
+ */
+ std::string ClientRecordingID() const { return m_strRecordingId; }
+
+ /*!
+ * @brief Get the recording ID as upplied by the client
+ * @return the recording identifier
+ */
+ unsigned int RecordingID() const { return m_iRecordingId; }
+
+ /*!
+ * @brief Set the recording ID
+ * @param recordingId The new identifier
+ */
+ void SetRecordingID(unsigned int recordingId) { m_iRecordingId = recordingId; }
+
+ /*!
+ * @brief Get the directory for this recording
+ * @return the directory
+ */
+ std::string Directory() const { return m_strDirectory; }
+
+ /*!
+ * @brief Get the priority for this recording
+ * @return the priority
+ */
+ int Priority() const { return m_iPriority; }
+
+ /*!
+ * @brief Get the lifetime for this recording
+ * @return the lifetime
+ */
+ int LifeTime() const { return m_iLifetime; }
+
+ /*!
+ * @brief Set the lifetime for this recording
+ * @param lifeTime The lifetime
+ */
+ void SetLifeTime(int lifeTime) { m_iLifetime = lifeTime; }
+
+ /*!
+ * @brief Get the channel name for this recording
+ * @return the channel name
+ */
+ std::string ChannelName() const { return m_strChannelName; }
+
+ /*!
+ * @brief Return the icon path as given by the client.
+ * @return The path.
+ */
+ const std::string& ClientIconPath() const { return m_iconPath.GetClientImage(); }
+
+ /*!
+ * @brief Return the thumbnail path as given by the client.
+ * @return The path.
+ */
+ const std::string& ClientThumbnailPath() const { return m_thumbnailPath.GetClientImage(); }
+
+ /*!
+ * @brief Return the fanart path as given by the client.
+ * @return The path.
+ */
+ const std::string& ClientFanartPath() const { return m_fanartPath.GetClientImage(); }
+
+ /*!
+ * @brief Return the icon path used by Kodi.
+ * @return The path.
+ */
+ const std::string& IconPath() const { return m_iconPath.GetLocalImage(); }
+
+ /*!
+ * @brief Return the thumnail path used by Kodi.
+ * @return The path.
+ */
+ const std::string& ThumbnailPath() const { return m_thumbnailPath.GetLocalImage(); }
+
+ /*!
+ * @brief Return the fanart path used by Kodi.
+ * @return The path.
+ */
+ const std::string& FanartPath() const { return m_fanartPath.GetLocalImage(); }
+
+ /*!
+ * @brief Retrieve the recording Episode Name
+ * @note Returns an empty string if no Episode Name was provided by the PVR client
+ */
+ std::string EpisodeName() const { return m_strShowTitle; }
+
+ /*!
+ * @brief check whether this recording is currently in progress
+ * @return true if the recording is in progress, false otherwise
+ */
+ bool IsInProgress() const;
+
+ /*!
+ * @brief return the timer for an in-progress recording, if any
+ * @return the timer if the recording is in progress, nullptr otherwise
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetRecordingTimer() const;
+
+ /*!
+ * @brief set the genre for this recording.
+ * @param iGenreType The genre type ID. If set to EPG_GENRE_USE_STRING, set genre to the value provided with strGenre. Otherwise, compile the genre string from the values given by iGenreType and iGenreSubType
+ * @param iGenreSubType The genre subtype ID
+ * @param strGenre The genre
+ */
+ void SetGenre(int iGenreType, int iGenreSubType, const std::string& strGenre);
+
+ /*!
+ * @brief Get the genre type ID of this recording.
+ * @return The genre type ID.
+ */
+ int GenreType() const { return m_iGenreType; }
+
+ /*!
+ * @brief Get the genre subtype ID of this recording.
+ * @return The genre subtype ID.
+ */
+ int GenreSubType() const { return m_iGenreSubType; }
+
+ /*!
+ * @brief Get the genre as human readable string.
+ * @return The genre.
+ */
+ const std::vector<std::string> Genre() const { return m_genre; }
+
+ /*!
+ * @brief Get the genre(s) of this recording as formatted string.
+ * @return The genres label.
+ */
+ const std::string GetGenresLabel() const;
+
+ /*!
+ * @brief Get the first air date of this recording.
+ * @return The first air date.
+ */
+ CDateTime FirstAired() const;
+
+ /*!
+ * @brief Get the premiere year of this recording.
+ * @return The premiere year
+ */
+ int GetYear() const override;
+
+ /*!
+ * @brief Set the premiere year of this recording.
+ * @param year The premiere year
+ */
+ void SetYear(int year) override;
+
+ /*!
+ * @brief Check if the premiere year of this recording is valid
+ * @return True if the recording has as valid premiere date, false otherwise
+ */
+ bool HasYear() const override;
+
+ /*!
+ * @brief Check whether this recording will be flagged as new.
+ * @return True if this recording will be flagged as new, false otherwise
+ */
+ bool IsNew() const;
+
+ /*!
+ * @brief Check whether this recording will be flagged as a premiere.
+ * @return True if this recording will be flagged as a premiere, false otherwise
+ */
+ bool IsPremiere() const;
+
+ /*!
+ * @brief Check whether this recording will be flagged as a finale.
+ * @return True if this recording will be flagged as a finale, false otherwise
+ */
+ bool IsFinale() const;
+
+ /*!
+ * @brief Check whether this recording will be flagged as live.
+ * @return True if this recording will be flagged as live, false otherwise
+ */
+ bool IsLive() const;
+
+ /*!
+ * @brief Return the flags (PVR_RECORDING_FLAG_*) of this recording as a bitfield.
+ * @return the flags.
+ */
+ unsigned int Flags() const { return m_iFlags; }
+
+ /*!
+ * @brief Return the size of this recording in bytes.
+ * @return the size in bytes.
+ */
+ int64_t GetSizeInBytes() const;
+
+ /*!
+ * @brief Mark a recording as dirty/clean.
+ * @param bDirty true to mark as dirty, false to mark as clean.
+ */
+ void SetDirty(bool bDirty) { m_bDirty = bDirty; }
+
+ /*!
+ * @brief Return whether the recording is marked dirty.
+ * @return true if dirty, false otherwise.
+ */
+ bool IsDirty() const { return m_bDirty; }
+
+ /*!
+ * @brief Get the uid of the provider on the client which this recording is from
+ * @return the client uid of the provider or PVR_PROVIDER_INVALID_UID
+ */
+ int ClientProviderUniqueId() const;
+
+ /*!
+ * @brief Get the client provider name for this recording
+ * @return m_strProviderName The name for this recording provider
+ */
+ std::string ProviderName() const;
+
+ /*!
+ * @brief Get the default provider of this recording. The default
+ * provider represents the PVR add-on itself.
+ * @return The default provider of this recording
+ */
+ std::shared_ptr<CPVRProvider> GetDefaultProvider() const;
+
+ /*!
+ * @brief Whether or not this recording has a provider set by the client.
+ * @return True if a provider was set by the client, false otherwise.
+ */
+ bool HasClientProvider() const;
+
+ /*!
+ * @brief Get the provider of this recording. This may be the default provider or a
+ * custom provider set by the client. If @ref "HasClientProvider()" returns true
+ * the provider will be custom from the client, otherwise the default provider.
+ * @return The provider of this recording
+ */
+ std::shared_ptr<CPVRProvider> GetProvider() const;
+
+private:
+ CPVRRecording(const CPVRRecording& tag) = delete;
+ CPVRRecording& operator=(const CPVRRecording& other) = delete;
+
+ void UpdatePath();
+
+ int m_iClientId; /*!< ID of the backend */
+ std::string m_strRecordingId; /*!< unique ID of the recording on the client */
+ std::string m_strChannelName; /*!< name of the channel this was recorded from */
+ int m_iPriority; /*!< priority of this recording */
+ int m_iLifetime; /*!< lifetime of this recording */
+ std::string m_strDirectory; /*!< directory of this recording on the client */
+ unsigned int m_iRecordingId; /*!< id that won't change while xbmc is running */
+
+ CPVRCachedImage m_iconPath; /*!< icon path */
+ CPVRCachedImage m_thumbnailPath; /*!< thumbnail path */
+ CPVRCachedImage m_fanartPath; /*!< fanart path */
+ CDateTime m_recordingTime; /*!< start time of the recording */
+ bool m_bGotMetaData;
+ bool m_bIsDeleted; /*!< set if entry is a deleted recording which can be undelete */
+ mutable bool m_bInProgress; /*!< set if recording might be in progress */
+ unsigned int m_iEpgEventId; /*!< epg broadcast id associated with this recording */
+ int m_iChannelUid; /*!< channel uid associated with this recording */
+ bool m_bRadio; /*!< radio or tv recording */
+ int m_iGenreType = 0; /*!< genre type */
+ int m_iGenreSubType = 0; /*!< genre subtype */
+ mutable XbmcThreads::EndTime<> m_resumePointRefetchTimeout;
+ unsigned int m_iFlags = 0; /*!< the flags applicable to this recording */
+ mutable XbmcThreads::EndTime<> m_recordingSizeRefetchTimeout;
+ int64_t m_sizeInBytes = 0; /*!< the size of the recording in bytes */
+ bool m_bDirty = false;
+ std::string m_strProviderName; /*!< name of the provider this recording is from */
+ int m_iClientProviderUniqueId =
+ PVR_PROVIDER_INVALID_UID; /*!< provider uid associated with this recording on the client */
+
+ mutable CCriticalSection m_critSection;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/recordings/PVRRecordings.cpp b/xbmc/pvr/recordings/PVRRecordings.cpp
new file mode 100644
index 0000000..d6fd480
--- /dev/null
+++ b/xbmc/pvr/recordings/PVRRecordings.cpp
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRRecordings.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+CPVRRecordings::CPVRRecordings() = default;
+
+CPVRRecordings::~CPVRRecordings()
+{
+ if (m_database && m_database->IsOpen())
+ m_database->Close();
+}
+
+bool CPVRRecordings::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bIsUpdating)
+ return false;
+
+ m_bIsUpdating = true;
+
+ for (const auto& recording : m_recordings)
+ recording.second->SetDirty(true);
+
+ std::vector<int> failedClients;
+ CServiceBroker::GetPVRManager().Clients()->GetRecordings(clients, this, false, failedClients);
+ CServiceBroker::GetPVRManager().Clients()->GetRecordings(clients, this, true, failedClients);
+
+ // remove recordings that were deleted at the backend
+ for (auto it = m_recordings.begin(); it != m_recordings.end();)
+ {
+ if ((*it).second->IsDirty() && std::find(failedClients.begin(), failedClients.end(),
+ (*it).second->ClientID()) == failedClients.end())
+ it = m_recordings.erase(it);
+ else
+ ++it;
+ }
+
+ m_bIsUpdating = false;
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::RecordingsInvalidated);
+ return true;
+}
+
+bool CPVRRecordings::Update(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ return UpdateFromClients(clients);
+}
+
+void CPVRRecordings::Unload()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bDeletedTVRecordings = false;
+ m_bDeletedRadioRecordings = false;
+ m_iTVRecordings = 0;
+ m_iRadioRecordings = 0;
+ m_recordings.clear();
+}
+
+void CPVRRecordings::UpdateInProgressSize()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bIsUpdating)
+ return;
+ m_bIsUpdating = true;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updating recordings size");
+ bool bHaveUpdatedInProgessRecording = false;
+ for (auto& recording : m_recordings)
+ {
+ if (recording.second->IsInProgress())
+ {
+ if (recording.second->UpdateRecordingSize())
+ bHaveUpdatedInProgessRecording = true;
+ }
+ }
+
+ m_bIsUpdating = false;
+
+ if (bHaveUpdatedInProgessRecording)
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::RecordingsInvalidated);
+}
+
+int CPVRRecordings::GetNumTVRecordings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iTVRecordings;
+}
+
+bool CPVRRecordings::HasDeletedTVRecordings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bDeletedTVRecordings;
+}
+
+int CPVRRecordings::GetNumRadioRecordings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iRadioRecordings;
+}
+
+bool CPVRRecordings::HasDeletedRadioRecordings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bDeletedRadioRecordings;
+}
+
+std::vector<std::shared_ptr<CPVRRecording>> CPVRRecordings::GetAll() const
+{
+ std::vector<std::shared_ptr<CPVRRecording>> recordings;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::transform(m_recordings.cbegin(), m_recordings.cend(), std::back_inserter(recordings),
+ [](const auto& recordingEntry) { return recordingEntry.second; });
+
+ return recordings;
+}
+
+std::shared_ptr<CPVRRecording> CPVRRecordings::GetById(unsigned int iId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it =
+ std::find_if(m_recordings.cbegin(), m_recordings.cend(),
+ [iId](const auto& recording) { return recording.second->RecordingID() == iId; });
+ return it != m_recordings.cend() ? (*it).second : std::shared_ptr<CPVRRecording>();
+}
+
+std::shared_ptr<CPVRRecording> CPVRRecordings::GetByPath(const std::string& path) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CPVRRecordingsPath recPath(path);
+ if (recPath.IsValid())
+ {
+ bool bDeleted = recPath.IsDeleted();
+ bool bRadio = recPath.IsRadio();
+
+ for (const auto& recording : m_recordings)
+ {
+ std::shared_ptr<CPVRRecording> current = recording.second;
+ // Omit recordings not matching criteria
+ if (!URIUtils::PathEquals(path, current->m_strFileNameAndPath) ||
+ bDeleted != current->IsDeleted() || bRadio != current->IsRadio())
+ continue;
+
+ return current;
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVRRecording> CPVRRecordings::GetById(int iClientId,
+ const std::string& strRecordingId) const
+{
+ std::shared_ptr<CPVRRecording> retVal;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = m_recordings.find(CPVRRecordingUid(iClientId, strRecordingId));
+ if (it != m_recordings.end())
+ retVal = it->second;
+
+ return retVal;
+}
+
+void CPVRRecordings::UpdateFromClient(const std::shared_ptr<CPVRRecording>& tag,
+ const CPVRClient& client)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (tag->IsDeleted())
+ {
+ if (tag->IsRadio())
+ m_bDeletedRadioRecordings = true;
+ else
+ m_bDeletedTVRecordings = true;
+ }
+
+ std::shared_ptr<CPVRRecording> existingTag = GetById(tag->ClientID(), tag->ClientRecordingID());
+ if (existingTag)
+ {
+ existingTag->Update(*tag, client);
+ existingTag->SetDirty(false);
+ }
+ else
+ {
+ tag->UpdateMetadata(GetVideoDatabase(), client);
+ tag->SetRecordingID(++m_iLastId);
+ m_recordings.insert({CPVRRecordingUid(tag->ClientID(), tag->ClientRecordingID()), tag});
+ if (tag->IsRadio())
+ ++m_iRadioRecordings;
+ else
+ ++m_iTVRecordings;
+ }
+}
+
+std::shared_ptr<CPVRRecording> CPVRRecordings::GetRecordingForEpgTag(
+ const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (!epgTag)
+ return {};
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& recording : m_recordings)
+ {
+ if (recording.second->IsDeleted())
+ continue;
+
+ if (recording.second->ClientID() != epgTag->ClientID())
+ continue;
+
+ if (recording.second->ChannelUid() != epgTag->UniqueChannelID())
+ continue;
+
+ unsigned int iEpgEvent = recording.second->BroadcastUid();
+ if (iEpgEvent != EPG_TAG_INVALID_UID)
+ {
+ if (iEpgEvent == epgTag->UniqueBroadcastID())
+ return recording.second;
+ }
+ else
+ {
+ if (recording.second->RecordingTimeAsUTC() <= epgTag->StartAsUTC() &&
+ recording.second->EndTimeAsUTC() >= epgTag->EndAsUTC())
+ return recording.second;
+ }
+ }
+
+ return std::shared_ptr<CPVRRecording>();
+}
+
+bool CPVRRecordings::SetRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording,
+ int count)
+{
+ return ChangeRecordingsPlayCount(recording, count);
+}
+
+bool CPVRRecordings::IncrementRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording)
+{
+ return ChangeRecordingsPlayCount(recording, INCREMENT_PLAY_COUNT);
+}
+
+bool CPVRRecordings::ChangeRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording,
+ int count)
+{
+ if (recording)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CVideoDatabase& db = GetVideoDatabase();
+ if (db.IsOpen())
+ {
+ if (count == INCREMENT_PLAY_COUNT)
+ recording->IncrementPlayCount();
+ else
+ recording->SetPlayCount(count);
+
+ // Clear resume bookmark
+ if (recording->GetPlayCount() > 0)
+ {
+ db.ClearBookMarksOfFile(recording->m_strFileNameAndPath, CBookmark::RESUME);
+ recording->SetResumePoint(CBookmark());
+ }
+
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::RecordingsInvalidated);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CPVRRecordings::MarkWatched(const std::shared_ptr<CPVRRecording>& recording, bool bWatched)
+{
+ if (bWatched)
+ return IncrementRecordingsPlayCount(recording);
+ else
+ return SetRecordingsPlayCount(recording, 0);
+}
+
+bool CPVRRecordings::ResetResumePoint(const std::shared_ptr<CPVRRecording>& recording)
+{
+ bool bResult = false;
+
+ if (recording)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CVideoDatabase& db = GetVideoDatabase();
+ if (db.IsOpen())
+ {
+ bResult = true;
+
+ db.ClearBookMarksOfFile(recording->m_strFileNameAndPath, CBookmark::RESUME);
+ recording->SetResumePoint(CBookmark());
+
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::RecordingsInvalidated);
+ }
+ }
+ return bResult;
+}
+
+CVideoDatabase& CPVRRecordings::GetVideoDatabase()
+{
+ if (!m_database)
+ {
+ m_database.reset(new CVideoDatabase());
+ m_database->Open();
+
+ if (!m_database->IsOpen())
+ CLog::LogF(LOGERROR, "Failed to open the video database");
+ }
+
+ return *m_database;
+}
+
+int CPVRRecordings::CleanupCachedImages()
+{
+ std::vector<std::string> urlsToCheck;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& recording : m_recordings)
+ {
+ urlsToCheck.emplace_back(recording.second->ClientIconPath());
+ urlsToCheck.emplace_back(recording.second->ClientThumbnailPath());
+ urlsToCheck.emplace_back(recording.second->ClientFanartPath());
+ urlsToCheck.emplace_back(recording.second->m_strFileNameAndPath);
+ }
+ }
+
+ static const std::vector<PVRImagePattern> urlPatterns = {
+ {CPVRRecording::IMAGE_OWNER_PATTERN, ""}, // client-supplied icon, thumbnail, fanart
+ {"video", "pvr://recordings/"}, // kodi-generated video thumbnail
+ };
+ return CPVRCachedImages::Cleanup(urlPatterns, urlsToCheck);
+}
diff --git a/xbmc/pvr/recordings/PVRRecordings.h b/xbmc/pvr/recordings/PVRRecordings.h
new file mode 100644
index 0000000..1a7753f
--- /dev/null
+++ b/xbmc/pvr/recordings/PVRRecordings.h
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CVideoDatabase;
+
+namespace PVR
+{
+class CPVRClient;
+class CPVREpgInfoTag;
+class CPVRRecording;
+class CPVRRecordingUid;
+class CPVRRecordingsPath;
+
+class CPVRRecordings
+{
+public:
+ CPVRRecordings();
+ virtual ~CPVRRecordings();
+
+ /*!
+ * @brief Update all recordings from the given PVR clients.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool Update(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief unload all recordings.
+ */
+ void Unload();
+
+ /*!
+ * @brief Update data with recordings from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief client has delivered a new/updated recording.
+ * @param tag The recording
+ * @param client The client the recording belongs to.
+ */
+ void UpdateFromClient(const std::shared_ptr<CPVRRecording>& tag, const CPVRClient& client);
+
+ /*!
+ * @brief refresh the size of any in progress recordings from the clients.
+ */
+ void UpdateInProgressSize();
+
+ int GetNumTVRecordings() const;
+ bool HasDeletedTVRecordings() const;
+ int GetNumRadioRecordings() const;
+ bool HasDeletedRadioRecordings() const;
+
+ /*!
+ * @brief Set a recording's watched state
+ * @param recording The recording
+ * @param bWatched True to set watched, false to set unwatched state
+ * @return True on success, false otherwise
+ */
+ bool MarkWatched(const std::shared_ptr<CPVRRecording>& recording, bool bWatched);
+
+ /*!
+ * @brief Reset a recording's resume point, if any
+ * @param recording The recording
+ * @return True on success, false otherwise
+ */
+ bool ResetResumePoint(const std::shared_ptr<CPVRRecording>& recording);
+
+ /*!
+ * @brief Get a list of all recordings
+ * @return the list of all recordings
+ */
+ std::vector<std::shared_ptr<CPVRRecording>> GetAll() const;
+
+ std::shared_ptr<CPVRRecording> GetByPath(const std::string& path) const;
+ std::shared_ptr<CPVRRecording> GetById(int iClientId, const std::string& strRecordingId) const;
+ std::shared_ptr<CPVRRecording> GetById(unsigned int iId) const;
+
+ /*!
+ * @brief Get the recording for the given epg tag, if any.
+ * @param epgTag The epg tag.
+ * @return The requested recording, or an empty recordingptr if none was found.
+ */
+ std::shared_ptr<CPVRRecording> GetRecordingForEpgTag(
+ const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+private:
+ /*!
+ * @brief Get/Open the video database.
+ * @return A reference to the video database.
+ */
+ CVideoDatabase& GetVideoDatabase();
+
+ /*!
+ * @brief Set a recording's play count
+ * @param recording The recording
+ * @param count The new play count
+ * @return True on success, false otherwise
+ */
+ bool SetRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording, int count);
+
+ /*!
+ * @brief Increment a recording's play count
+ * @param recording The recording
+ * @return True on success, false otherwise
+ */
+ bool IncrementRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording);
+
+ /*!
+ * @brief special value for parameter count of method ChangeRecordingsPlayCount
+ */
+ static const int INCREMENT_PLAY_COUNT = -1;
+
+ /*!
+ * @brief change the play count of the given recording
+ * @param recording The recording
+ * @param count The new play count or INCREMENT_PLAY_COUNT to denote that the current play count is to be incremented by one
+ * @return true if the play count was changed successfully
+ */
+ bool ChangeRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording, int count);
+
+ mutable CCriticalSection m_critSection;
+ bool m_bIsUpdating = false;
+ std::map<CPVRRecordingUid, std::shared_ptr<CPVRRecording>> m_recordings;
+ unsigned int m_iLastId = 0;
+ std::unique_ptr<CVideoDatabase> m_database;
+ bool m_bDeletedTVRecordings = false;
+ bool m_bDeletedRadioRecordings = false;
+ unsigned int m_iTVRecordings = 0;
+ unsigned int m_iRadioRecordings = 0;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/recordings/PVRRecordingsPath.cpp b/xbmc/pvr/recordings/PVRRecordingsPath.cpp
new file mode 100644
index 0000000..470d674
--- /dev/null
+++ b/xbmc/pvr/recordings/PVRRecordingsPath.cpp
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRRecordingsPath.h"
+
+#include "URL.h"
+#include "XBDateTime.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+const std::string CPVRRecordingsPath::PATH_RECORDINGS = "pvr://recordings/";
+const std::string CPVRRecordingsPath::PATH_ACTIVE_TV_RECORDINGS = "pvr://recordings/tv/active/";
+const std::string CPVRRecordingsPath::PATH_ACTIVE_RADIO_RECORDINGS =
+ "pvr://recordings/radio/active/";
+const std::string CPVRRecordingsPath::PATH_DELETED_TV_RECORDINGS = "pvr://recordings/tv/deleted/";
+const std::string CPVRRecordingsPath::PATH_DELETED_RADIO_RECORDINGS =
+ "pvr://recordings/radio/deleted/";
+
+CPVRRecordingsPath::CPVRRecordingsPath(const std::string& strPath)
+{
+ std::string strVarPath(TrimSlashes(strPath));
+ const std::vector<std::string> segments = URIUtils::SplitPath(strVarPath);
+
+ m_bValid = ((segments.size() >= 4) && // at least pvr://recordings/[tv|radio]/[active|deleted]
+ StringUtils::StartsWith(strVarPath, "pvr://") && (segments.at(1) == "recordings") &&
+ ((segments.at(2) == "tv") || (segments.at(2) == "radio")) &&
+ ((segments.at(3) == "active") || (segments.at(3) == "deleted")));
+ if (m_bValid)
+ {
+ m_bRoot = (segments.size() == 4);
+ m_bRadio = (segments.at(2) == "radio");
+ m_bActive = (segments.at(3) == "active");
+
+ if (m_bRoot)
+ strVarPath.append("/");
+ else
+ {
+ size_t paramStart = m_path.find(", TV");
+ if (paramStart == std::string::npos)
+ m_directoryPath = strVarPath.substr(GetDirectoryPathPosition());
+ else
+ {
+ size_t dirStart = GetDirectoryPathPosition();
+ m_directoryPath = strVarPath.substr(dirStart, paramStart - dirStart);
+ m_params = strVarPath.substr(paramStart);
+ }
+ }
+
+ m_path = strVarPath;
+ }
+ else
+ {
+ m_bRoot = false;
+ m_bActive = false;
+ m_bRadio = false;
+ }
+}
+
+CPVRRecordingsPath::CPVRRecordingsPath(bool bDeleted, bool bRadio)
+ : m_bValid(true),
+ m_bRoot(true),
+ m_bActive(!bDeleted),
+ m_bRadio(bRadio),
+ m_path(StringUtils::Format(
+ "pvr://recordings/{}/{}/", bRadio ? "radio" : "tv", bDeleted ? "deleted" : "active"))
+{
+}
+
+CPVRRecordingsPath::CPVRRecordingsPath(bool bDeleted,
+ bool bRadio,
+ const std::string& strDirectory,
+ const std::string& strTitle,
+ int iSeason,
+ int iEpisode,
+ int iYear,
+ const std::string& strSubtitle,
+ const std::string& strChannelName,
+ const CDateTime& recordingTime,
+ const std::string& strId)
+ : m_bValid(true), m_bRoot(false), m_bActive(!bDeleted), m_bRadio(bRadio)
+{
+ std::string strDirectoryN(TrimSlashes(strDirectory));
+ if (!strDirectoryN.empty())
+ strDirectoryN = StringUtils::Format("{}/", strDirectoryN);
+
+ std::string strTitleN(strTitle);
+ strTitleN = CURL::Encode(strTitleN);
+
+ std::string strSeasonEpisodeN;
+ if ((iSeason > -1 && iEpisode > -1 && (iSeason > 0 || iEpisode > 0)))
+ strSeasonEpisodeN = StringUtils::Format("s{:02}e{:02}", iSeason, iEpisode);
+ if (!strSeasonEpisodeN.empty())
+ strSeasonEpisodeN = StringUtils::Format(" {}", strSeasonEpisodeN);
+
+ std::string strYearN(iYear > 0 ? StringUtils::Format(" ({})", iYear) : "");
+
+ std::string strSubtitleN;
+ if (!strSubtitle.empty())
+ {
+ strSubtitleN = StringUtils::Format(" {}", strSubtitle);
+ strSubtitleN = CURL::Encode(strSubtitleN);
+ }
+
+ std::string strChannelNameN;
+ if (!strChannelName.empty())
+ {
+ strChannelNameN = StringUtils::Format(" ({})", strChannelName);
+ strChannelNameN = CURL::Encode(strChannelNameN);
+ }
+
+ m_directoryPath = StringUtils::Format("{}{}{}{}{}", strDirectoryN, strTitleN, strSeasonEpisodeN,
+ strYearN, strSubtitleN);
+ m_params = StringUtils::Format(", TV{}, {}, {}.pvr", strChannelNameN,
+ recordingTime.GetAsSaveString(), strId);
+ m_path = StringUtils::Format("pvr://recordings/{}/{}/{}{}", bRadio ? "radio" : "tv",
+ bDeleted ? "deleted" : "active", m_directoryPath, m_params);
+}
+
+std::string CPVRRecordingsPath::GetUnescapedDirectoryPath() const
+{
+ return CURL::Decode(m_directoryPath);
+}
+
+namespace
+{
+bool PathHasParent(const std::string& path, const std::string& parent)
+{
+ if (path == parent)
+ return true;
+
+ if (!parent.empty() && parent.back() != '/')
+ return StringUtils::StartsWith(path, parent + '/');
+
+ return StringUtils::StartsWith(path, parent);
+}
+} // unnamed namespace
+
+std::string CPVRRecordingsPath::GetUnescapedSubDirectoryPath(const std::string& strPath) const
+{
+ // note: strPath must be unescaped.
+
+ std::string strReturn;
+ std::string strUsePath(TrimSlashes(strPath));
+
+ const std::string strUnescapedDirectoryPath(GetUnescapedDirectoryPath());
+
+ /* adding "/" to make sure that base matches the complete folder name and not only parts of it */
+ if (!strUnescapedDirectoryPath.empty() &&
+ (strUsePath.size() <= strUnescapedDirectoryPath.size() ||
+ !PathHasParent(strUsePath, strUnescapedDirectoryPath)))
+ return strReturn;
+
+ strUsePath.erase(0, strUnescapedDirectoryPath.size());
+ strUsePath = TrimSlashes(strUsePath);
+
+ /* check for more occurrences */
+ size_t iDelimiter = strUsePath.find('/');
+ if (iDelimiter == std::string::npos)
+ strReturn = strUsePath;
+ else
+ strReturn = strUsePath.substr(0, iDelimiter);
+
+ return strReturn;
+}
+
+const std::string CPVRRecordingsPath::GetTitle() const
+{
+ if (m_bValid)
+ {
+ CRegExp reg(true);
+ if (reg.RegComp("pvr://recordings/(.*/)*(.*), TV( \\(.*\\))?, "
+ "(19[0-9][0-9]|20[0-9][0-9])[0-9][0-9][0-9][0-9]_[0-9][0-9][0-9][0-9][0-9][0-9]"
+ ", (.*).pvr"))
+ {
+ if (reg.RegFind(m_path.c_str()) >= 0)
+ return reg.GetMatch(2);
+ }
+ }
+ return "";
+}
+
+void CPVRRecordingsPath::AppendSegment(const std::string& strSegment)
+{
+ if (!m_bValid || strSegment.empty() || strSegment == "/")
+ return;
+
+ std::string strVarSegment(TrimSlashes(strSegment));
+ strVarSegment = CURL::Encode(strVarSegment);
+
+ if (!m_directoryPath.empty() && m_directoryPath.back() != '/')
+ m_directoryPath.push_back('/');
+
+ m_directoryPath += strVarSegment;
+ m_directoryPath.push_back('/');
+
+ size_t paramStart = m_path.find(", TV");
+ if (paramStart == std::string::npos)
+ {
+ if (!m_path.empty() && m_path.back() != '/')
+ m_path.push_back('/');
+
+ // append the segment
+ m_path += strVarSegment;
+ m_path.push_back('/');
+ }
+ else
+ {
+ if (m_path.back() != '/')
+ m_path.insert(paramStart, "/");
+
+ // insert the segment between end of current directory path and parameters
+ m_path.insert(paramStart, strVarSegment);
+ }
+
+ m_bRoot = false;
+}
+
+std::string CPVRRecordingsPath::TrimSlashes(const std::string& strString)
+{
+ std::string strTrimmed(strString);
+ while (!strTrimmed.empty() && strTrimmed.front() == '/')
+ strTrimmed.erase(0, 1);
+
+ while (!strTrimmed.empty() && strTrimmed.back() == '/')
+ strTrimmed.pop_back();
+
+ return strTrimmed;
+}
+
+size_t CPVRRecordingsPath::GetDirectoryPathPosition() const
+{
+ if (m_bActive)
+ {
+ if (m_bRadio)
+ return PATH_ACTIVE_RADIO_RECORDINGS.size();
+ else
+ return PATH_ACTIVE_TV_RECORDINGS.size();
+ }
+ else
+ {
+ if (m_bRadio)
+ return PATH_DELETED_RADIO_RECORDINGS.size();
+ else
+ return PATH_DELETED_TV_RECORDINGS.size();
+ }
+ // unreachable
+}
diff --git a/xbmc/pvr/recordings/PVRRecordingsPath.h b/xbmc/pvr/recordings/PVRRecordingsPath.h
new file mode 100644
index 0000000..e95dc92
--- /dev/null
+++ b/xbmc/pvr/recordings/PVRRecordingsPath.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+
+class CDateTime;
+
+namespace PVR
+{
+class CPVRRecordingsPath
+{
+public:
+ static const std::string PATH_RECORDINGS;
+ static const std::string PATH_ACTIVE_TV_RECORDINGS;
+ static const std::string PATH_ACTIVE_RADIO_RECORDINGS;
+ static const std::string PATH_DELETED_TV_RECORDINGS;
+ static const std::string PATH_DELETED_RADIO_RECORDINGS;
+
+ explicit CPVRRecordingsPath(const std::string& strPath);
+ CPVRRecordingsPath(bool bDeleted, bool bRadio);
+ CPVRRecordingsPath(bool bDeleted,
+ bool bRadio,
+ const std::string& strDirectory,
+ const std::string& strTitle,
+ int iSeason,
+ int iEpisode,
+ int iYear,
+ const std::string& strSubtitle,
+ const std::string& strChannelName,
+ const CDateTime& recordingTime,
+ const std::string& strId);
+
+ operator std::string() const { return m_path; }
+
+ bool IsValid() const { return m_bValid; }
+
+ const std::string& GetPath() const { return m_path; }
+ bool IsRecordingsRoot() const { return m_bRoot; }
+ bool IsActive() const { return m_bActive; }
+ bool IsDeleted() const { return !IsActive(); }
+ bool IsRadio() const { return m_bRadio; }
+ bool IsTV() const { return !IsRadio(); }
+ std::string GetUnescapedDirectoryPath() const;
+ std::string GetUnescapedSubDirectoryPath(const std::string& strPath) const;
+
+ const std::string GetTitle() const;
+ void AppendSegment(const std::string& strSegment);
+
+private:
+ static std::string TrimSlashes(const std::string& strString);
+ size_t GetDirectoryPathPosition() const;
+
+ bool m_bValid;
+ bool m_bRoot;
+ bool m_bActive;
+ bool m_bRadio;
+ std::string m_directoryPath;
+ std::string m_params;
+ std::string m_path;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/settings/CMakeLists.txt b/xbmc/pvr/settings/CMakeLists.txt
new file mode 100644
index 0000000..6b32503
--- /dev/null
+++ b/xbmc/pvr/settings/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES PVRSettings.cpp)
+
+set(HEADERS PVRSettings.h)
+
+core_add_library(pvr_settings)
diff --git a/xbmc/pvr/settings/PVRSettings.cpp b/xbmc/pvr/settings/PVRSettings.cpp
new file mode 100644
index 0000000..f28651a
--- /dev/null
+++ b/xbmc/pvr/settings/PVRSettings.cpp
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRSettings.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <set>
+#include <string>
+#include <utility>
+
+using namespace PVR;
+
+unsigned int CPVRSettings::m_iInstances = 0;
+
+CPVRSettings::CPVRSettings(const std::set<std::string>& settingNames)
+{
+ Init(settingNames);
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->GetSettingsManager()->RegisterSettingsHandler(this);
+ settings->RegisterCallback(this, settingNames);
+
+ if (m_iInstances == 0)
+ {
+ // statics must only be registered once, not per instance
+ settings->GetSettingsManager()->RegisterSettingOptionsFiller("pvrrecordmargins", MarginTimeFiller);
+ settings->GetSettingsManager()->AddDynamicCondition("pvrsettingvisible", IsSettingVisible);
+ settings->GetSettingsManager()->AddDynamicCondition("checkpvrparentalpin", CheckParentalPin);
+ }
+ m_iInstances++;
+}
+
+CPVRSettings::~CPVRSettings()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ m_iInstances--;
+ if (m_iInstances == 0)
+ {
+ settings->GetSettingsManager()->RemoveDynamicCondition("checkpvrparentalpin");
+ settings->GetSettingsManager()->RemoveDynamicCondition("pvrsettingvisible");
+ settings->GetSettingsManager()->UnregisterSettingOptionsFiller("pvrrecordmargins");
+ }
+
+ settings->UnregisterCallback(this);
+ settings->GetSettingsManager()->UnregisterSettingsHandler(this);
+}
+
+void CPVRSettings::RegisterCallback(ISettingCallback* callback)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_callbacks.insert(callback);
+}
+
+void CPVRSettings::UnregisterCallback(ISettingCallback* callback)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_callbacks.erase(callback);
+}
+
+void CPVRSettings::Init(const std::set<std::string>& settingNames)
+{
+ for (const auto& settingName : settingNames)
+ {
+ SettingPtr setting = CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(settingName);
+ if (!setting)
+ {
+ CLog::LogF(LOGERROR, "Unknown PVR setting '{}'", settingName);
+ continue;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_settings.insert(std::make_pair(settingName, setting->Clone(settingName)));
+ }
+}
+
+void CPVRSettings::OnSettingsLoaded()
+{
+ std::set<std::string> settingNames;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& settingName : m_settings)
+ settingNames.insert(settingName.first);
+
+ m_settings.clear();
+ }
+
+ Init(settingNames);
+}
+
+void CPVRSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_settings[setting->GetId()] = setting->Clone(setting->GetId());
+ const auto callbacks(m_callbacks);
+ lock.unlock();
+
+ for (const auto& callback : callbacks)
+ callback->OnSettingChanged(setting);
+}
+
+bool CPVRSettings::GetBoolValue(const std::string& settingName) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto settingIt = m_settings.find(settingName);
+ if (settingIt != m_settings.end() && (*settingIt).second->GetType() == SettingType::Boolean)
+ {
+ std::shared_ptr<const CSettingBool> setting = std::dynamic_pointer_cast<const CSettingBool>((*settingIt).second);
+ if (setting)
+ return setting->GetValue();
+ }
+
+ CLog::LogF(LOGERROR, "PVR setting '{}' not found or wrong type given", settingName);
+ return false;
+}
+
+int CPVRSettings::GetIntValue(const std::string& settingName) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto settingIt = m_settings.find(settingName);
+ if (settingIt != m_settings.end() && (*settingIt).second->GetType() == SettingType::Integer)
+ {
+ std::shared_ptr<const CSettingInt> setting = std::dynamic_pointer_cast<const CSettingInt>((*settingIt).second);
+ if (setting)
+ return setting->GetValue();
+ }
+
+ CLog::LogF(LOGERROR, "PVR setting '{}' not found or wrong type given", settingName);
+ return -1;
+}
+
+std::string CPVRSettings::GetStringValue(const std::string& settingName) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto settingIt = m_settings.find(settingName);
+ if (settingIt != m_settings.end() && (*settingIt).second->GetType() == SettingType::String)
+ {
+ std::shared_ptr<const CSettingString> setting = std::dynamic_pointer_cast<const CSettingString>((*settingIt).second);
+ if (setting)
+ return setting->GetValue();
+ }
+
+ CLog::LogF(LOGERROR, "PVR setting '{}' not found or wrong type given", settingName);
+ return "";
+}
+
+void CPVRSettings::MarginTimeFiller(const SettingConstPtr& /*setting*/,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* /*data*/)
+{
+ list.clear();
+
+ static const int marginTimeValues[] =
+ {
+ 0, 1, 3, 5, 10, 15, 20, 30, 60, 90, 120, 180 // minutes
+ };
+
+ for (int iValue : marginTimeValues)
+ {
+ list.emplace_back(StringUtils::Format(g_localizeStrings.Get(14044), iValue) /* {} min */,
+ iValue);
+ }
+}
+
+bool CPVRSettings::IsSettingVisible(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data)
+{
+ if (setting == nullptr)
+ return false;
+
+ const std::string& settingId = setting->GetId();
+
+ if (settingId == CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS)
+ {
+ // Setting is only visible if exactly one PVR client is enabled or
+ // the expert setting to always use backend numbers is enabled
+ const auto& settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ int enabledClientAmount = CServiceBroker::GetPVRManager().Clients()->EnabledClientAmount();
+
+ return enabledClientAmount == 1 ||
+ (settings->GetBool(CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS) &&
+ enabledClientAmount > 1);
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS)
+ {
+ // Setting is only visible if more than one PVR client is enabled.
+ return CServiceBroker::GetPVRManager().Clients()->EnabledClientAmount() > 1;
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_CLIENTPRIORITIES)
+ {
+ // Setting is only visible if more than one PVR client is enabeld.
+ return CServiceBroker::GetPVRManager().Clients()->EnabledClientAmount() > 1;
+ }
+ else
+ {
+ // Show all other settings unconditionally.
+ return true;
+ }
+}
+
+bool CPVRSettings::CheckParentalPin(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data)
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalPIN() ==
+ ParentalCheckResult::SUCCESS;
+}
diff --git a/xbmc/pvr/settings/PVRSettings.h b/xbmc/pvr/settings/PVRSettings.h
new file mode 100644
index 0000000..7cab9d6
--- /dev/null
+++ b/xbmc/pvr/settings/PVRSettings.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+class CSetting;
+
+struct IntegerSettingOption;
+
+namespace PVR
+{
+ class CPVRSettings : private ISettingsHandler, private ISettingCallback
+ {
+ public:
+ explicit CPVRSettings(const std::set<std::string>& settingNames);
+ ~CPVRSettings() override;
+
+ void RegisterCallback(ISettingCallback* callback);
+ void UnregisterCallback(ISettingCallback* callback);
+
+ // ISettingsHandler implementation
+ void OnSettingsLoaded() override;
+
+ // ISettingCallback implementation
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ bool GetBoolValue(const std::string& settingName) const;
+ int GetIntValue(const std::string& settingName) const;
+ std::string GetStringValue(const std::string& settingName) const;
+
+ // settings value filler for start/end recording margin time for PVR timers.
+ static void MarginTimeFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ // Dynamically hide or show settings.
+ static bool IsSettingVisible(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+ // Do parental PIN check.
+ static bool CheckParentalPin(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+ private:
+ CPVRSettings(const CPVRSettings&) = delete;
+ CPVRSettings& operator=(CPVRSettings const&) = delete;
+
+ void Init(const std::set<std::string>& settingNames);
+
+ mutable CCriticalSection m_critSection;
+ std::map<std::string, std::shared_ptr<CSetting>> m_settings;
+ std::set<ISettingCallback*> m_callbacks;
+
+ static unsigned int m_iInstances;
+ };
+}
diff --git a/xbmc/pvr/timers/CMakeLists.txt b/xbmc/pvr/timers/CMakeLists.txt
new file mode 100644
index 0000000..e1031b4
--- /dev/null
+++ b/xbmc/pvr/timers/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES PVRTimerInfoTag.cpp
+ PVRTimerRuleMatcher.cpp
+ PVRTimers.cpp
+ PVRTimersPath.cpp
+ PVRTimerType.cpp)
+
+set(HEADERS PVRTimerInfoTag.h
+ PVRTimerRuleMatcher.h
+ PVRTimers.h
+ PVRTimersPath.h
+ PVRTimerType.h)
+
+core_add_library(pvr_timers)
diff --git a/xbmc/pvr/timers/PVRTimerInfoTag.cpp b/xbmc/pvr/timers/PVRTimerInfoTag.cpp
new file mode 100644
index 0000000..24df4cc
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimerInfoTag.cpp
@@ -0,0 +1,1389 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRTimerInfoTag.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <ctime>
+#include <memory>
+#include <mutex>
+#include <stdexcept>
+#include <string>
+
+using namespace PVR;
+
+CPVRTimerInfoTag::CPVRTimerInfoTag(bool bRadio /* = false */)
+ : m_strTitle(g_localizeStrings.Get(19056)), // New Timer
+ m_iClientId(CServiceBroker::GetPVRManager().Clients()->GetFirstCreatedClientID()),
+ m_iClientIndex(PVR_TIMER_NO_CLIENT_INDEX),
+ m_iParentClientIndex(PVR_TIMER_NO_PARENT),
+ m_iClientChannelUid(PVR_CHANNEL_INVALID_UID),
+ m_iPriority(DEFAULT_RECORDING_PRIORITY),
+ m_iLifetime(DEFAULT_RECORDING_LIFETIME),
+ m_iPreventDupEpisodes(DEFAULT_RECORDING_DUPLICATEHANDLING),
+ m_bIsRadio(bRadio),
+ m_iMarginStart(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_PVRRECORD_MARGINSTART)),
+ m_iMarginEnd(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_PVRRECORD_MARGINEND)),
+ m_iEpgUid(EPG_TAG_INVALID_UID),
+ m_StartTime(CDateTime::GetUTCDateTime()),
+ m_StopTime(m_StartTime + CDateTimeSpan(0, 2, 0, 0)),
+ m_FirstDay(m_StartTime)
+{
+ static const uint64_t iMustHaveAttr = PVR_TIMER_TYPE_IS_MANUAL;
+ static const uint64_t iMustNotHaveAttr =
+ PVR_TIMER_TYPE_IS_REPEATING | PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES;
+
+ std::shared_ptr<CPVRTimerType> type;
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsTimers())
+ {
+ // default to first available type for given client
+ type = CPVRTimerType::GetFirstAvailableType(client);
+ }
+
+ // fallback to manual one-shot reminder, which is always available
+ if (!type)
+ type = CPVRTimerType::CreateFromAttributes(iMustHaveAttr | PVR_TIMER_TYPE_IS_REMINDER,
+ iMustNotHaveAttr, m_iClientId);
+
+ if (type)
+ SetTimerType(type);
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to obtain timer type!");
+ throw std::logic_error("CPVRTimerInfoTag::CPVRTimerInfoTag - Unable to obtain timer type!");
+ }
+
+ m_iWeekdays = m_timerType->IsTimerRule() ? PVR_WEEKDAY_ALLDAYS : PVR_WEEKDAY_NONE;
+
+ UpdateSummary();
+}
+
+CPVRTimerInfoTag::CPVRTimerInfoTag(const PVR_TIMER& timer,
+ const std::shared_ptr<CPVRChannel>& channel,
+ unsigned int iClientId)
+ : m_strTitle(timer.strTitle),
+ m_strEpgSearchString(timer.strEpgSearchString),
+ m_bFullTextEpgSearch(timer.bFullTextEpgSearch),
+ m_strDirectory(timer.strDirectory),
+ m_state(timer.state),
+ m_iClientId(iClientId),
+ m_iClientIndex(timer.iClientIndex),
+ m_iParentClientIndex(timer.iParentClientIndex),
+ m_iClientChannelUid(channel ? channel->UniqueID()
+ : (timer.iClientChannelUid > 0) ? timer.iClientChannelUid
+ : PVR_CHANNEL_INVALID_UID),
+ m_bStartAnyTime(timer.bStartAnyTime),
+ m_bEndAnyTime(timer.bEndAnyTime),
+ m_iPriority(timer.iPriority),
+ m_iLifetime(timer.iLifetime),
+ m_iMaxRecordings(timer.iMaxRecordings),
+ m_iWeekdays(timer.iWeekdays),
+ m_iPreventDupEpisodes(timer.iPreventDuplicateEpisodes),
+ m_iRecordingGroup(timer.iRecordingGroup),
+ m_strFileNameAndPath(
+ StringUtils::Format("pvr://client{}/timers/{}", m_iClientId, m_iClientIndex)),
+ m_bIsRadio(channel && channel->IsRadio()),
+ m_iMarginStart(timer.iMarginStart),
+ m_iMarginEnd(timer.iMarginEnd),
+ m_iEpgUid(timer.iEpgUid),
+ m_strSeriesLink(timer.strSeriesLink),
+ m_StartTime(
+ timer.startTime +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection),
+ m_StopTime(timer.endTime +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection),
+ m_channel(channel)
+{
+ if (timer.firstDay)
+ m_FirstDay = CDateTime(
+ timer.firstDay +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection);
+ else
+ m_FirstDay = CDateTime::GetUTCDateTime();
+
+ if (m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX)
+ CLog::LogF(LOGERROR, "Invalid client index supplied by client {} (must be > 0)!", m_iClientId);
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsTimers())
+ {
+ // begin compat section
+
+ // Create timer type according to certain timer values already available in Isengard.
+ // This is for migration only and does not make changes to the addons obsolete. Addons should
+ // work and benefit from some UI changes (e.g. some of the timer settings dialog enhancements),
+ // but all old problems/bugs due to static attributes and values will remain the same as in
+ // Isengard. Also, new features (like epg search) are not available to addons automatically.
+ // This code can be removed once all addons actually support the respective PVR Addon API version.
+ if (timer.iTimerType == PVR_TIMER_TYPE_NONE)
+ {
+ // Create type according to certain timer values.
+ uint64_t iMustHave = PVR_TIMER_TYPE_ATTRIBUTE_NONE;
+ uint64_t iMustNotHave = PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES;
+
+ if (timer.iEpgUid == PVR_TIMER_NO_EPG_UID && timer.iWeekdays != PVR_WEEKDAY_NONE)
+ iMustHave |= PVR_TIMER_TYPE_IS_REPEATING;
+ else
+ iMustNotHave |= PVR_TIMER_TYPE_IS_REPEATING;
+
+ if (timer.iEpgUid == PVR_TIMER_NO_EPG_UID)
+ iMustHave |= PVR_TIMER_TYPE_IS_MANUAL;
+ else
+ iMustNotHave |= PVR_TIMER_TYPE_IS_MANUAL;
+
+ const std::shared_ptr<CPVRTimerType> type =
+ CPVRTimerType::CreateFromAttributes(iMustHave, iMustNotHave, m_iClientId);
+
+ if (type)
+ SetTimerType(type);
+ }
+ // end compat section
+ else
+ {
+ SetTimerType(CPVRTimerType::CreateFromIds(timer.iTimerType, m_iClientId));
+ }
+
+ if (!m_timerType)
+ {
+ CLog::LogF(LOGERROR, "No timer type, although timers are supported by client {}!",
+ m_iClientId);
+ throw std::logic_error("CPVRTimerInfoTag::CPVRTimerInfoTag - Unable to obtain timer type!");
+ }
+ else if (m_iEpgUid == EPG_TAG_INVALID_UID && m_timerType->IsEpgBasedOnetime())
+ {
+ CLog::LogF(LOGERROR, "No epg tag given for epg based timer type ({})!",
+ m_timerType->GetTypeId());
+ }
+ }
+
+ UpdateSummary();
+ UpdateEpgInfoTag();
+}
+
+bool CPVRTimerInfoTag::operator==(const CPVRTimerInfoTag& right) const
+{
+ bool bChannelsMatch = true;
+ if (m_channel && right.m_channel)
+ bChannelsMatch = *m_channel == *right.m_channel;
+ else if (m_channel != right.m_channel)
+ bChannelsMatch = false;
+
+ return (bChannelsMatch && m_iClientIndex == right.m_iClientIndex &&
+ m_iParentClientIndex == right.m_iParentClientIndex &&
+ m_strSummary == right.m_strSummary && m_iClientChannelUid == right.m_iClientChannelUid &&
+ m_bIsRadio == right.m_bIsRadio && m_iPreventDupEpisodes == right.m_iPreventDupEpisodes &&
+ m_iRecordingGroup == right.m_iRecordingGroup && m_StartTime == right.m_StartTime &&
+ m_StopTime == right.m_StopTime && m_bStartAnyTime == right.m_bStartAnyTime &&
+ m_bEndAnyTime == right.m_bEndAnyTime && m_FirstDay == right.m_FirstDay &&
+ m_iWeekdays == right.m_iWeekdays && m_iPriority == right.m_iPriority &&
+ m_iLifetime == right.m_iLifetime && m_iMaxRecordings == right.m_iMaxRecordings &&
+ m_strFileNameAndPath == right.m_strFileNameAndPath && m_strTitle == right.m_strTitle &&
+ m_strEpgSearchString == right.m_strEpgSearchString &&
+ m_bFullTextEpgSearch == right.m_bFullTextEpgSearch &&
+ m_strDirectory == right.m_strDirectory && m_iClientId == right.m_iClientId &&
+ m_iMarginStart == right.m_iMarginStart && m_iMarginEnd == right.m_iMarginEnd &&
+ m_state == right.m_state && m_timerType == right.m_timerType &&
+ m_iTimerId == right.m_iTimerId && m_strSeriesLink == right.m_strSeriesLink &&
+ m_iEpgUid == right.m_iEpgUid && m_iTVChildTimersActive == right.m_iTVChildTimersActive &&
+ m_iTVChildTimersConflictNOK == right.m_iTVChildTimersConflictNOK &&
+ m_iTVChildTimersRecording == right.m_iTVChildTimersRecording &&
+ m_iTVChildTimersErrors == right.m_iTVChildTimersErrors &&
+ m_iRadioChildTimersActive == right.m_iRadioChildTimersActive &&
+ m_iRadioChildTimersConflictNOK == right.m_iRadioChildTimersConflictNOK &&
+ m_iRadioChildTimersRecording == right.m_iRadioChildTimersRecording &&
+ m_iRadioChildTimersErrors == right.m_iRadioChildTimersErrors);
+}
+
+bool CPVRTimerInfoTag::operator!=(const CPVRTimerInfoTag& right) const
+{
+ return !(*this == right);
+}
+
+void CPVRTimerInfoTag::FillAddonData(PVR_TIMER& timer) const
+{
+ time_t start, end, firstDay;
+ StartAsUTC().GetAsTime(start);
+ EndAsUTC().GetAsTime(end);
+ FirstDayAsUTC().GetAsTime(firstDay);
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = GetEpgInfoTag();
+ const int iPVRTimeCorrection =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection;
+
+ timer = {};
+ timer.iClientIndex = m_iClientIndex;
+ timer.iParentClientIndex = m_iParentClientIndex;
+ timer.state = m_state;
+ timer.iTimerType = GetTimerType()->GetTypeId();
+ timer.iClientChannelUid = m_iClientChannelUid;
+ strncpy(timer.strTitle, m_strTitle.c_str(), sizeof(timer.strTitle) - 1);
+ strncpy(timer.strEpgSearchString, m_strEpgSearchString.c_str(),
+ sizeof(timer.strEpgSearchString) - 1);
+ timer.bFullTextEpgSearch = m_bFullTextEpgSearch;
+ strncpy(timer.strDirectory, m_strDirectory.c_str(), sizeof(timer.strDirectory) - 1);
+ timer.iPriority = m_iPriority;
+ timer.iLifetime = m_iLifetime;
+ timer.iMaxRecordings = m_iMaxRecordings;
+ timer.iPreventDuplicateEpisodes = m_iPreventDupEpisodes;
+ timer.iRecordingGroup = m_iRecordingGroup;
+ timer.iWeekdays = m_iWeekdays;
+ timer.startTime = start - iPVRTimeCorrection;
+ timer.endTime = end - iPVRTimeCorrection;
+ timer.bStartAnyTime = m_bStartAnyTime;
+ timer.bEndAnyTime = m_bEndAnyTime;
+ timer.firstDay = firstDay - iPVRTimeCorrection;
+ timer.iEpgUid = epgTag ? epgTag->UniqueBroadcastID() : PVR_TIMER_NO_EPG_UID;
+ strncpy(timer.strSummary, m_strSummary.c_str(), sizeof(timer.strSummary) - 1);
+ timer.iMarginStart = m_iMarginStart;
+ timer.iMarginEnd = m_iMarginEnd;
+ timer.iGenreType = epgTag ? epgTag->GenreType() : 0;
+ timer.iGenreSubType = epgTag ? epgTag->GenreSubType() : 0;
+ strncpy(timer.strSeriesLink, SeriesLink().c_str(), sizeof(timer.strSeriesLink) - 1);
+}
+
+void CPVRTimerInfoTag::Serialize(CVariant& value) const
+{
+ value["channelid"] = m_channel != NULL ? m_channel->ChannelID() : -1;
+ value["summary"] = m_strSummary;
+ value["isradio"] = m_bIsRadio;
+ value["preventduplicateepisodes"] = m_iPreventDupEpisodes;
+ value["starttime"] = m_StartTime.IsValid() ? m_StartTime.GetAsDBDateTime() : "";
+ value["endtime"] = m_StopTime.IsValid() ? m_StopTime.GetAsDBDateTime() : "";
+ value["startanytime"] = m_bStartAnyTime;
+ value["endanytime"] = m_bEndAnyTime;
+ value["runtime"] = m_StartTime.IsValid() && m_StopTime.IsValid()
+ ? (m_StopTime - m_StartTime).GetSecondsTotal()
+ : 0;
+ value["firstday"] = m_FirstDay.IsValid() ? m_FirstDay.GetAsDBDate() : "";
+
+ CVariant weekdays(CVariant::VariantTypeArray);
+ if (m_iWeekdays & PVR_WEEKDAY_MONDAY)
+ weekdays.push_back("monday");
+ if (m_iWeekdays & PVR_WEEKDAY_TUESDAY)
+ weekdays.push_back("tuesday");
+ if (m_iWeekdays & PVR_WEEKDAY_WEDNESDAY)
+ weekdays.push_back("wednesday");
+ if (m_iWeekdays & PVR_WEEKDAY_THURSDAY)
+ weekdays.push_back("thursday");
+ if (m_iWeekdays & PVR_WEEKDAY_FRIDAY)
+ weekdays.push_back("friday");
+ if (m_iWeekdays & PVR_WEEKDAY_SATURDAY)
+ weekdays.push_back("saturday");
+ if (m_iWeekdays & PVR_WEEKDAY_SUNDAY)
+ weekdays.push_back("sunday");
+ value["weekdays"] = weekdays;
+
+ value["priority"] = m_iPriority;
+ value["lifetime"] = m_iLifetime;
+ value["title"] = m_strTitle;
+ value["directory"] = m_strDirectory;
+ value["startmargin"] = m_iMarginStart;
+ value["endmargin"] = m_iMarginEnd;
+
+ value["timerid"] = m_iTimerId;
+
+ switch (m_state)
+ {
+ case PVR_TIMER_STATE_NEW:
+ value["state"] = "new";
+ break;
+ case PVR_TIMER_STATE_SCHEDULED:
+ value["state"] = "scheduled";
+ break;
+ case PVR_TIMER_STATE_RECORDING:
+ value["state"] = "recording";
+ break;
+ case PVR_TIMER_STATE_COMPLETED:
+ value["state"] = "completed";
+ break;
+ case PVR_TIMER_STATE_ABORTED:
+ value["state"] = "aborted";
+ break;
+ case PVR_TIMER_STATE_CANCELLED:
+ value["state"] = "cancelled";
+ break;
+ case PVR_TIMER_STATE_CONFLICT_OK:
+ value["state"] = "conflict_ok";
+ break;
+ case PVR_TIMER_STATE_CONFLICT_NOK:
+ value["state"] = "conflict_notok";
+ break;
+ case PVR_TIMER_STATE_ERROR:
+ value["state"] = "error";
+ break;
+ case PVR_TIMER_STATE_DISABLED:
+ value["state"] = "disabled";
+ break;
+ default:
+ value["state"] = "unknown";
+ break;
+ }
+
+ value["istimerrule"] = m_timerType->IsTimerRule();
+ value["ismanual"] = m_timerType->IsManual();
+ value["isreadonly"] = m_timerType->IsReadOnly();
+ value["isreminder"] = m_timerType->IsReminder();
+
+ value["epgsearchstring"] = m_strEpgSearchString;
+ value["fulltextepgsearch"] = m_bFullTextEpgSearch;
+ value["recordinggroup"] = m_iRecordingGroup;
+ value["maxrecordings"] = m_iMaxRecordings;
+ value["epguid"] = m_iEpgUid;
+ value["broadcastid"] = m_epgTag ? m_epgTag->DatabaseID() : -1;
+ value["serieslink"] = m_strSeriesLink;
+
+ value["clientid"] = m_iClientId;
+}
+
+void CPVRTimerInfoTag::UpdateSummary()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strSummary.clear();
+
+ const std::string startDate(StartAsLocalTime().GetAsLocalizedDate());
+ const std::string endDate(EndAsLocalTime().GetAsLocalizedDate());
+
+ if (m_bEndAnyTime)
+ {
+ m_strSummary = StringUtils::Format(
+ "{} {} {}",
+ m_iWeekdays != PVR_WEEKDAY_NONE ? GetWeekdaysString()
+ : startDate, //for "Any day" set PVR_WEEKDAY_ALLDAYS
+ g_localizeStrings.Get(19107), // "at"
+ m_bStartAnyTime ? g_localizeStrings.Get(19161) /* "any time" */
+ : StartAsLocalTime().GetAsLocalizedTime("", false));
+ }
+ else if ((m_iWeekdays != PVR_WEEKDAY_NONE) || (startDate == endDate))
+ {
+ m_strSummary = StringUtils::Format(
+ "{} {} {} {} {}",
+ m_iWeekdays != PVR_WEEKDAY_NONE ? GetWeekdaysString()
+ : startDate, //for "Any day" set PVR_WEEKDAY_ALLDAYS
+ g_localizeStrings.Get(19159), // "from"
+ m_bStartAnyTime ? g_localizeStrings.Get(19161) /* "any time" */
+ : StartAsLocalTime().GetAsLocalizedTime("", false),
+ g_localizeStrings.Get(19160), // "to"
+ m_bEndAnyTime ? g_localizeStrings.Get(19161) /* "any time" */
+ : EndAsLocalTime().GetAsLocalizedTime("", false));
+ }
+ else
+ {
+ m_strSummary =
+ StringUtils::Format("{} {} {} {} {} {}", startDate,
+ g_localizeStrings.Get(19159), // "from"
+ m_bStartAnyTime ? g_localizeStrings.Get(19161) /* "any time" */
+ : StartAsLocalTime().GetAsLocalizedTime("", false),
+ g_localizeStrings.Get(19160), // "to"
+ endDate,
+ m_bEndAnyTime ? g_localizeStrings.Get(19161) /* "any time" */
+ : EndAsLocalTime().GetAsLocalizedTime("", false));
+ }
+}
+
+void CPVRTimerInfoTag::SetTimerType(const std::shared_ptr<CPVRTimerType>& type)
+{
+ if (!type)
+ throw std::logic_error("CPVRTimerInfoTag::SetTimerType - Attempt to set 'null' timer type!");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_timerType = type;
+
+ if (m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX)
+ {
+ m_iPriority = m_timerType->GetPriorityDefault();
+ m_iLifetime = m_timerType->GetLifetimeDefault();
+ m_iMaxRecordings = m_timerType->GetMaxRecordingsDefault();
+ m_iPreventDupEpisodes = m_timerType->GetPreventDuplicateEpisodesDefault();
+ m_iRecordingGroup = m_timerType->GetRecordingGroupDefault();
+ }
+
+ if (!m_timerType->IsTimerRule())
+ m_iWeekdays = PVR_WEEKDAY_NONE;
+}
+
+std::string CPVRTimerInfoTag::GetStatus(bool bRadio) const
+{
+ std::string strReturn = g_localizeStrings.Get(305);
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (URIUtils::PathEquals(m_strFileNameAndPath, CPVRTimersPath::PATH_ADDTIMER))
+ strReturn = g_localizeStrings.Get(19026);
+ else if (m_state == PVR_TIMER_STATE_CANCELLED || m_state == PVR_TIMER_STATE_ABORTED)
+ strReturn = g_localizeStrings.Get(13106);
+ else if (m_state == PVR_TIMER_STATE_RECORDING)
+ strReturn = g_localizeStrings.Get(19162);
+ else if (m_state == PVR_TIMER_STATE_CONFLICT_OK)
+ strReturn = g_localizeStrings.Get(19275);
+ else if (m_state == PVR_TIMER_STATE_CONFLICT_NOK)
+ strReturn = g_localizeStrings.Get(19276);
+ else if (m_state == PVR_TIMER_STATE_ERROR)
+ strReturn = g_localizeStrings.Get(257);
+ else if (m_state == PVR_TIMER_STATE_DISABLED)
+ strReturn = g_localizeStrings.Get(13106);
+ else if (m_state == PVR_TIMER_STATE_COMPLETED)
+ {
+ if ((m_iTVChildTimersRecording > 0 && !bRadio) || (m_iRadioChildTimersRecording > 0 && bRadio))
+ strReturn = g_localizeStrings.Get(19162); // "Recording active"
+ else
+ strReturn = g_localizeStrings.Get(19256); // "Completed"
+ }
+ else if (m_state == PVR_TIMER_STATE_SCHEDULED || m_state == PVR_TIMER_STATE_NEW)
+ {
+ if ((m_iTVChildTimersRecording > 0 && !bRadio) || (m_iRadioChildTimersRecording > 0 && bRadio))
+ strReturn = g_localizeStrings.Get(19162); // "Recording active"
+ else if ((m_iTVChildTimersErrors > 0 && !bRadio) || (m_iRadioChildTimersErrors > 0 && bRadio))
+ strReturn = g_localizeStrings.Get(257); // "Error"
+ else if ((m_iTVChildTimersConflictNOK > 0 && !bRadio) ||
+ (m_iRadioChildTimersConflictNOK > 0 && bRadio))
+ strReturn = g_localizeStrings.Get(19276); // "Conflict error"
+ else if ((m_iTVChildTimersActive > 0 && !bRadio) || (m_iRadioChildTimersActive > 0 && bRadio))
+ strReturn = StringUtils::Format(g_localizeStrings.Get(19255),
+ bRadio ? m_iRadioChildTimersActive
+ : m_iTVChildTimersActive); // "{} scheduled"
+ }
+
+ return strReturn;
+}
+
+/**
+ * Get the type string of this timer
+ */
+std::string CPVRTimerInfoTag::GetTypeAsString() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_timerType->GetDescription();
+}
+
+namespace
+{
+void AppendDay(std::string& strReturn, unsigned int iId)
+{
+ if (!strReturn.empty())
+ strReturn += "-";
+
+ if (iId > 0)
+ strReturn += g_localizeStrings.Get(iId).c_str();
+ else
+ strReturn += "__";
+}
+} // unnamed namespace
+
+std::string CPVRTimerInfoTag::GetWeekdaysString(unsigned int iWeekdays,
+ bool bEpgBased,
+ bool bLongMultiDaysFormat)
+{
+ std::string strReturn;
+
+ if (iWeekdays == PVR_WEEKDAY_NONE)
+ return strReturn;
+ else if (iWeekdays == PVR_WEEKDAY_ALLDAYS)
+ strReturn = bEpgBased ? g_localizeStrings.Get(807) // "Any day"
+ : g_localizeStrings.Get(808); // "Every day"
+ else if (iWeekdays == PVR_WEEKDAY_MONDAY)
+ strReturn = g_localizeStrings.Get(831); // "Mondays"
+ else if (iWeekdays == PVR_WEEKDAY_TUESDAY)
+ strReturn = g_localizeStrings.Get(832); // "Tuesdays"
+ else if (iWeekdays == PVR_WEEKDAY_WEDNESDAY)
+ strReturn = g_localizeStrings.Get(833); // "Wednesdays"
+ else if (iWeekdays == PVR_WEEKDAY_THURSDAY)
+ strReturn = g_localizeStrings.Get(834); // "Thursdays"
+ else if (iWeekdays == PVR_WEEKDAY_FRIDAY)
+ strReturn = g_localizeStrings.Get(835); // "Fridays"
+ else if (iWeekdays == PVR_WEEKDAY_SATURDAY)
+ strReturn = g_localizeStrings.Get(836); // "Saturdays"
+ else if (iWeekdays == PVR_WEEKDAY_SUNDAY)
+ strReturn = g_localizeStrings.Get(837); // "Sundays"
+ else
+ {
+ // Any other combination. Assemble custom string.
+ if (iWeekdays & PVR_WEEKDAY_MONDAY)
+ AppendDay(strReturn, 19149); // Mo
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+
+ if (iWeekdays & PVR_WEEKDAY_TUESDAY)
+ AppendDay(strReturn, 19150); // Tu
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+
+ if (iWeekdays & PVR_WEEKDAY_WEDNESDAY)
+ AppendDay(strReturn, 19151); // We
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+
+ if (iWeekdays & PVR_WEEKDAY_THURSDAY)
+ AppendDay(strReturn, 19152); // Th
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+
+ if (iWeekdays & PVR_WEEKDAY_FRIDAY)
+ AppendDay(strReturn, 19153); // Fr
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+
+ if (iWeekdays & PVR_WEEKDAY_SATURDAY)
+ AppendDay(strReturn, 19154); // Sa
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+
+ if (iWeekdays & PVR_WEEKDAY_SUNDAY)
+ AppendDay(strReturn, 19155); // So
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+ }
+ return strReturn;
+}
+
+std::string CPVRTimerInfoTag::GetWeekdaysString() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return GetWeekdaysString(m_iWeekdays, m_timerType->IsEpgBased(), false);
+}
+
+bool CPVRTimerInfoTag::IsOwnedByClient() const
+{
+ return m_timerType->GetClientId() > -1;
+}
+
+bool CPVRTimerInfoTag::AddToClient() const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client)
+ return client->AddTimer(*this) == PVR_ERROR_NO_ERROR;
+ return false;
+}
+
+TimerOperationResult CPVRTimerInfoTag::DeleteFromClient(bool bForce /* = false */) const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ PVR_ERROR error = PVR_ERROR_UNKNOWN;
+
+ if (client)
+ error = client->DeleteTimer(*this, bForce);
+
+ if (error == PVR_ERROR_RECORDING_RUNNING)
+ return TimerOperationResult::RECORDING;
+
+ return (error == PVR_ERROR_NO_ERROR) ? TimerOperationResult::OK : TimerOperationResult::FAILED;
+}
+
+bool CPVRTimerInfoTag::Persist()
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ return database->Persist(*this);
+
+ return false;
+}
+
+bool CPVRTimerInfoTag::DeleteFromDatabase()
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ return database->Delete(*this);
+
+ return false;
+}
+
+bool CPVRTimerInfoTag::UpdateEntry(const std::shared_ptr<CPVRTimerInfoTag>& tag)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_iClientId = tag->m_iClientId;
+ m_iClientIndex = tag->m_iClientIndex;
+ m_iParentClientIndex = tag->m_iParentClientIndex;
+ m_strTitle = tag->m_strTitle;
+ m_strEpgSearchString = tag->m_strEpgSearchString;
+ m_bFullTextEpgSearch = tag->m_bFullTextEpgSearch;
+ m_strDirectory = tag->m_strDirectory;
+ m_iClientChannelUid = tag->m_iClientChannelUid;
+ m_StartTime = tag->m_StartTime;
+ m_StopTime = tag->m_StopTime;
+ m_bStartAnyTime = tag->m_bStartAnyTime;
+ m_bEndAnyTime = tag->m_bEndAnyTime;
+ m_FirstDay = tag->m_FirstDay;
+ m_iPriority = tag->m_iPriority;
+ m_iLifetime = tag->m_iLifetime;
+ m_iMaxRecordings = tag->m_iMaxRecordings;
+ m_state = tag->m_state;
+ m_iPreventDupEpisodes = tag->m_iPreventDupEpisodes;
+ m_iRecordingGroup = tag->m_iRecordingGroup;
+ m_iWeekdays = tag->m_iWeekdays;
+ m_bIsRadio = tag->m_bIsRadio;
+ m_iMarginStart = tag->m_iMarginStart;
+ m_iMarginEnd = tag->m_iMarginEnd;
+ m_strSeriesLink = tag->m_strSeriesLink;
+ m_iEpgUid = tag->m_iEpgUid;
+ m_epgTag = tag->m_epgTag;
+ m_strSummary = tag->m_strSummary;
+ m_channel = tag->m_channel;
+ m_bProbedEpgTag = tag->m_bProbedEpgTag;
+
+ m_iTVChildTimersActive = tag->m_iTVChildTimersActive;
+ m_iTVChildTimersConflictNOK = tag->m_iTVChildTimersConflictNOK;
+ m_iTVChildTimersRecording = tag->m_iTVChildTimersRecording;
+ m_iTVChildTimersErrors = tag->m_iTVChildTimersErrors;
+ m_iRadioChildTimersActive = tag->m_iRadioChildTimersActive;
+ m_iRadioChildTimersConflictNOK = tag->m_iRadioChildTimersConflictNOK;
+ m_iRadioChildTimersRecording = tag->m_iRadioChildTimersRecording;
+ m_iRadioChildTimersErrors = tag->m_iRadioChildTimersErrors;
+
+ SetTimerType(tag->m_timerType);
+
+ if (m_strSummary.empty())
+ UpdateSummary();
+
+ UpdateEpgInfoTag();
+
+ return true;
+}
+
+bool CPVRTimerInfoTag::UpdateChildState(const std::shared_ptr<CPVRTimerInfoTag>& childTimer,
+ bool bAdd)
+{
+ if (!childTimer || childTimer->m_iParentClientIndex != m_iClientIndex)
+ return false;
+
+ int iDelta = bAdd ? +1 : -1;
+ switch (childTimer->m_state)
+ {
+ case PVR_TIMER_STATE_NEW:
+ case PVR_TIMER_STATE_SCHEDULED:
+ case PVR_TIMER_STATE_CONFLICT_OK:
+ if (childTimer->m_bIsRadio)
+ m_iRadioChildTimersActive += iDelta;
+ else
+ m_iTVChildTimersActive += iDelta;
+ break;
+ case PVR_TIMER_STATE_RECORDING:
+ if (childTimer->m_bIsRadio)
+ {
+ m_iRadioChildTimersActive += iDelta;
+ m_iRadioChildTimersRecording += iDelta;
+ }
+ else
+ {
+ m_iTVChildTimersActive += iDelta;
+ m_iTVChildTimersRecording += iDelta;
+ }
+ break;
+ case PVR_TIMER_STATE_CONFLICT_NOK:
+ if (childTimer->m_bIsRadio)
+ m_iRadioChildTimersConflictNOK += iDelta;
+ else
+ m_iTVChildTimersConflictNOK += iDelta;
+ break;
+ case PVR_TIMER_STATE_ERROR:
+ if (childTimer->m_bIsRadio)
+ m_iRadioChildTimersErrors += iDelta;
+ else
+ m_iTVChildTimersErrors += iDelta;
+ break;
+ case PVR_TIMER_STATE_COMPLETED:
+ case PVR_TIMER_STATE_ABORTED:
+ case PVR_TIMER_STATE_CANCELLED:
+ case PVR_TIMER_STATE_DISABLED:
+ //these are not the child timers we are looking for
+ break;
+ }
+ return true;
+}
+
+void CPVRTimerInfoTag::ResetChildState()
+{
+ m_iTVChildTimersActive = 0;
+ m_iTVChildTimersRecording = 0;
+ m_iTVChildTimersConflictNOK = 0;
+ m_iTVChildTimersErrors = 0;
+ m_iRadioChildTimersActive = 0;
+ m_iRadioChildTimersRecording = 0;
+ m_iRadioChildTimersConflictNOK = 0;
+ m_iRadioChildTimersErrors = 0;
+}
+
+bool CPVRTimerInfoTag::UpdateOnClient()
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ return client && (client->UpdateTimer(*this) == PVR_ERROR_NO_ERROR);
+}
+
+std::string CPVRTimerInfoTag::ChannelName() const
+{
+ std::string strReturn;
+ std::shared_ptr<CPVRChannel> channeltag = Channel();
+ if (channeltag)
+ strReturn = channeltag->ChannelName();
+ else if (m_timerType->IsEpgBasedTimerRule())
+ strReturn = StringUtils::Format("({})", g_localizeStrings.Get(809)); // "Any channel"
+
+ return strReturn;
+}
+
+std::string CPVRTimerInfoTag::ChannelIcon() const
+{
+ std::string strReturn;
+ std::shared_ptr<CPVRChannel> channeltag = Channel();
+ if (channeltag)
+ strReturn = channeltag->IconPath();
+ return strReturn;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateReminderFromDate(
+ const CDateTime& start,
+ int iDuration,
+ const std::shared_ptr<CPVRTimerInfoTag>& parent /* = std::shared_ptr<CPVRTimerInfoTag>() */)
+{
+ bool bReadOnly = !!parent; // children of reminder rules are always read-only
+ std::shared_ptr<CPVRTimerInfoTag> newTimer =
+ CreateFromDate(parent->Channel(), start, iDuration, true, bReadOnly);
+ if (newTimer && parent)
+ {
+ // set parent
+ newTimer->m_iParentClientIndex = parent->m_iClientIndex;
+
+ // set relevant props for the new one-time reminder timer
+ newTimer->m_strTitle = parent->Title();
+ }
+ return newTimer;
+}
+
+static const time_t INSTANT_TIMER_START =
+ 0; // PVR addon API: special start time value to denote an instant timer
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateInstantTimerTag(
+ const std::shared_ptr<CPVRChannel>& channel,
+ int iDuration /* = DEFAULT_PVRRECORD_INSTANTRECORDTIME */)
+{
+ return CreateFromDate(channel, CDateTime(INSTANT_TIMER_START), iDuration, false, false);
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateTimerTag(
+ const std::shared_ptr<CPVRChannel>& channel, const CDateTime& start, int iDuration)
+{
+ return CreateFromDate(channel, start, iDuration, false, false);
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateFromDate(
+ const std::shared_ptr<CPVRChannel>& channel,
+ const CDateTime& start,
+ int iDuration,
+ bool bCreateReminder,
+ bool bReadOnly)
+{
+ if (!channel)
+ {
+ CLog::LogF(LOGERROR, "No channel");
+ return std::shared_ptr<CPVRTimerInfoTag>();
+ }
+
+ bool bInstantStart = (start == CDateTime(INSTANT_TIMER_START));
+
+ std::shared_ptr<CPVREpgInfoTag> epgTag;
+ if (!bCreateReminder) // time-based reminders never have epg tags
+ {
+ if (bInstantStart)
+ epgTag = channel->GetEPGNow();
+ else if (channel->GetEPG())
+ epgTag = channel->GetEPG()->GetTagBetween(start, start + CDateTimeSpan(0, 0, iDuration, 0));
+ }
+
+ std::shared_ptr<CPVRTimerInfoTag> newTimer;
+ if (epgTag)
+ {
+ if (epgTag->IsRecordable())
+ {
+ newTimer = CreateFromEpg(epgTag, false, bCreateReminder, bReadOnly);
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "EPG tag is not recordable");
+ return std::shared_ptr<CPVRTimerInfoTag>();
+ }
+ }
+
+ if (!newTimer)
+ {
+ newTimer.reset(new CPVRTimerInfoTag);
+
+ newTimer->m_iClientIndex = PVR_TIMER_NO_CLIENT_INDEX;
+ newTimer->m_iParentClientIndex = PVR_TIMER_NO_PARENT;
+ newTimer->m_channel = channel;
+ newTimer->m_strTitle = channel->ChannelName();
+ newTimer->m_iClientChannelUid = channel->UniqueID();
+ newTimer->m_iClientId = channel->ClientID();
+ newTimer->m_bIsRadio = channel->IsRadio();
+
+ uint64_t iMustHaveAttribs = PVR_TIMER_TYPE_IS_MANUAL;
+ if (bCreateReminder)
+ iMustHaveAttribs |= PVR_TIMER_TYPE_IS_REMINDER;
+ if (bReadOnly)
+ iMustHaveAttribs |= PVR_TIMER_TYPE_IS_READONLY;
+
+ // timertype: manual one-shot timer for given client
+ const std::shared_ptr<CPVRTimerType> timerType = CPVRTimerType::CreateFromAttributes(
+ iMustHaveAttribs, PVR_TIMER_TYPE_IS_REPEATING | PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES,
+ channel->ClientID());
+ if (!timerType)
+ {
+ CLog::LogF(LOGERROR, "Unable to create one shot manual timer type");
+ return std::shared_ptr<CPVRTimerInfoTag>();
+ }
+
+ newTimer->SetTimerType(timerType);
+
+ if (epgTag)
+ newTimer->SetEpgInfoTag(epgTag);
+ else
+ newTimer->UpdateEpgInfoTag();
+ }
+
+ /* no matter the timer was created from an epg tag, overwrite timer start and end times. */
+ CDateTime now(CDateTime::GetUTCDateTime());
+ if (bInstantStart)
+ newTimer->SetStartFromUTC(now);
+ else
+ newTimer->SetStartFromUTC(start);
+
+ if (iDuration == DEFAULT_PVRRECORD_INSTANTRECORDTIME)
+ iDuration = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME);
+
+ if (bInstantStart)
+ {
+ CDateTime endTime = now + CDateTimeSpan(0, 0, iDuration ? iDuration : 120, 0);
+ newTimer->SetEndFromUTC(endTime);
+ }
+ else
+ {
+ CDateTime endTime = start + CDateTimeSpan(0, 0, iDuration ? iDuration : 120, 0);
+ newTimer->SetEndFromUTC(endTime);
+ }
+
+ /* update summary string according to instant recording start/end time */
+ newTimer->UpdateSummary();
+
+ if (bInstantStart)
+ {
+ // "Instant recording: <summary>
+ newTimer->m_strSummary = StringUtils::Format(g_localizeStrings.Get(19093), newTimer->Summary());
+
+ // now that we have a nice summary, we can set the "special" start time value that indicates an instant recording
+ newTimer->SetStartFromUTC(start);
+ }
+
+ newTimer->m_iMarginStart = 0;
+ newTimer->m_iMarginEnd = 0;
+
+ /* unused only for reference */
+ newTimer->m_strFileNameAndPath = CPVRTimersPath::PATH_NEW;
+
+ return newTimer;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateReminderFromEpg(
+ const std::shared_ptr<CPVREpgInfoTag>& tag,
+ const std::shared_ptr<CPVRTimerInfoTag>& parent /* = std::shared_ptr<CPVRTimerInfoTag>() */)
+{
+ bool bReadOnly = !!parent; // children of reminder rules are always read-only
+ std::shared_ptr<CPVRTimerInfoTag> newTimer = CreateFromEpg(tag, false, true, bReadOnly);
+ if (newTimer && parent)
+ {
+ // set parent
+ newTimer->m_iParentClientIndex = parent->m_iClientIndex;
+
+ // set relevant props for the new one-time reminder timer (everything not epg-related)
+ newTimer->m_iMarginStart = parent->m_iMarginStart;
+ newTimer->m_iMarginEnd = parent->m_iMarginEnd;
+ }
+ return newTimer;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateFromEpg(
+ const std::shared_ptr<CPVREpgInfoTag>& tag, bool bCreateRule /* = false */)
+{
+ return CreateFromEpg(tag, bCreateRule, false, false);
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateFromEpg(
+ const std::shared_ptr<CPVREpgInfoTag>& tag,
+ bool bCreateRule,
+ bool bCreateReminder,
+ bool bReadOnly /* = false */)
+{
+ std::shared_ptr<CPVRTimerInfoTag> newTag(new CPVRTimerInfoTag());
+
+ /* check if a valid channel is set */
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(tag);
+ if (!channel)
+ {
+ CLog::LogF(LOGERROR, "EPG tag has no channel");
+ return std::shared_ptr<CPVRTimerInfoTag>();
+ }
+
+ newTag->m_iClientIndex = PVR_TIMER_NO_CLIENT_INDEX;
+ newTag->m_iParentClientIndex = PVR_TIMER_NO_PARENT;
+ if (!CServiceBroker::GetPVRManager().IsParentalLocked(tag))
+ newTag->m_strTitle = tag->Title();
+ if (newTag->m_strTitle.empty())
+ newTag->m_strTitle = channel->ChannelName();
+ newTag->m_iClientChannelUid = channel->UniqueID();
+ newTag->m_iClientId = channel->ClientID();
+ newTag->m_bIsRadio = channel->IsRadio();
+ newTag->m_channel = channel;
+ newTag->m_strSeriesLink = tag->SeriesLink();
+ newTag->m_iEpgUid = tag->UniqueBroadcastID();
+ newTag->SetStartFromUTC(tag->StartAsUTC());
+ newTag->SetEndFromUTC(tag->EndAsUTC());
+
+ const uint64_t iMustNotHaveAttribs = PVR_TIMER_TYPE_IS_MANUAL |
+ PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES |
+ PVR_TIMER_TYPE_FORBIDS_EPG_TAG_ON_CREATE;
+ std::shared_ptr<CPVRTimerType> timerType;
+ if (bCreateRule)
+ {
+ // create epg-based timer rule, prefer rule using series link if available.
+
+ uint64_t iMustHaveAttribs = PVR_TIMER_TYPE_IS_REPEATING;
+ if (bCreateReminder)
+ iMustHaveAttribs |= PVR_TIMER_TYPE_IS_REMINDER;
+ if (bReadOnly)
+ iMustHaveAttribs |= PVR_TIMER_TYPE_IS_READONLY;
+
+ if (!tag->SeriesLink().empty())
+ timerType = CPVRTimerType::CreateFromAttributes(
+ iMustHaveAttribs | PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE, iMustNotHaveAttribs,
+ channel->ClientID());
+ if (!timerType)
+ timerType = CPVRTimerType::CreateFromAttributes(
+ iMustHaveAttribs, iMustNotHaveAttribs | PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE,
+ channel->ClientID());
+ if (timerType)
+ {
+ if (timerType->SupportsEpgTitleMatch())
+ newTag->m_strEpgSearchString = newTag->m_strTitle;
+
+ if (timerType->SupportsWeekdays())
+ newTag->m_iWeekdays = PVR_WEEKDAY_ALLDAYS;
+
+ if (timerType->SupportsStartAnyTime())
+ newTag->m_bStartAnyTime = true;
+
+ if (timerType->SupportsEndAnyTime())
+ newTag->m_bEndAnyTime = true;
+ }
+ }
+ else
+ {
+ // create one-shot epg-based timer
+
+ uint64_t iMustHaveAttribs = PVR_TIMER_TYPE_ATTRIBUTE_NONE;
+ if (bCreateReminder)
+ iMustHaveAttribs |= PVR_TIMER_TYPE_IS_REMINDER;
+ if (bReadOnly)
+ iMustHaveAttribs |= PVR_TIMER_TYPE_IS_READONLY;
+
+ timerType = CPVRTimerType::CreateFromAttributes(
+ iMustHaveAttribs, PVR_TIMER_TYPE_IS_REPEATING | iMustNotHaveAttribs, channel->ClientID());
+ }
+
+ if (!timerType)
+ {
+ CLog::LogF(LOGERROR, "Unable to create any epg-based timer type");
+ return std::shared_ptr<CPVRTimerInfoTag>();
+ }
+
+ newTag->SetTimerType(timerType);
+ newTag->UpdateSummary();
+ newTag->SetEpgInfoTag(tag);
+
+ /* unused only for reference */
+ newTag->m_strFileNameAndPath = CPVRTimersPath::PATH_NEW;
+
+ return newTag;
+}
+
+//! @todo CDateTime class does not handle daylight saving timezone bias correctly (and cannot easily
+// be changed to do so due to performance and platform specific issues). In most cases this only
+// causes GUI presentation glitches, but reminder timer rules rely on correct local time values.
+
+namespace
+{
+#define IsLeapYear(y) ((y % 4 == 0) && (y % 100 != 0 || y % 400 == 0))
+
+int days_from_0(int year)
+{
+ year--;
+ return 365 * year + (year / 400) - (year / 100) + (year / 4);
+}
+
+int days_from_1970(int32_t year)
+{
+ static const int days_from_0_to_1970 = days_from_0(1970);
+ return days_from_0(year) - days_from_0_to_1970;
+}
+
+int days_from_1jan(int year, int month, int day)
+{
+ static const int days[2][12] = {{0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334},
+ {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}};
+ return days[IsLeapYear(year)][month - 1] + day - 1;
+}
+
+time_t mytimegm(struct tm* time)
+{
+ int year = time->tm_year + 1900;
+ int month = time->tm_mon;
+
+ if (month > 11)
+ {
+ year += month / 12;
+ month %= 12;
+ }
+ else if (month < 0)
+ {
+ int years_diff = (-month + 11) / 12;
+ year -= years_diff;
+ month += 12 * years_diff;
+ }
+
+ month++;
+
+ int day_of_year = days_from_1jan(year, month, time->tm_mday);
+ int days_since_epoch = days_from_1970(year) + day_of_year;
+
+ return 3600 * 24 * days_since_epoch + 3600 * time->tm_hour + 60 * time->tm_min + time->tm_sec;
+}
+} // namespace
+
+CDateTime CPVRTimerInfoTag::ConvertUTCToLocalTime(const CDateTime& utc)
+{
+ time_t time = 0;
+ utc.GetAsTime(time);
+
+ struct tm* tms;
+#ifdef HAVE_LOCALTIME_R
+ struct tm gbuf;
+ tms = localtime_r(&time, &gbuf);
+#else
+ tms = localtime(&time);
+#endif
+
+ if (!tms)
+ {
+ CLog::LogF(LOGWARNING, "localtime() returned NULL!");
+ return {};
+ }
+
+ return CDateTime(mytimegm(tms));
+}
+
+CDateTime CPVRTimerInfoTag::ConvertLocalTimeToUTC(const CDateTime& local)
+{
+ time_t time = 0;
+ local.GetAsTime(time);
+
+ struct tm* tms;
+
+ // obtain dst flag for given datetime
+#ifdef HAVE_LOCALTIME_R
+ struct tm loc_buf;
+ tms = localtime_r(&time, &loc_buf);
+#else
+ tms = localtime(&time);
+#endif
+
+ if (!tms)
+ {
+ CLog::LogF(LOGWARNING, "localtime() returned NULL!");
+ return {};
+ }
+
+ int isdst = tms->tm_isdst;
+
+#ifdef HAVE_GMTIME_R
+ struct tm gm_buf;
+ tms = gmtime_r(&time, &gm_buf);
+#else
+ tms = gmtime(&time);
+#endif
+
+ if (!tms)
+ {
+ CLog::LogF(LOGWARNING, "gmtime() returned NULL!");
+ return {};
+ }
+
+ tms->tm_isdst = isdst;
+ return CDateTime(mktime(tms));
+}
+
+CDateTime CPVRTimerInfoTag::StartAsUTC() const
+{
+ return m_StartTime;
+}
+
+CDateTime CPVRTimerInfoTag::StartAsLocalTime() const
+{
+ return ConvertUTCToLocalTime(m_StartTime);
+}
+
+void CPVRTimerInfoTag::SetStartFromUTC(const CDateTime& start)
+{
+ m_StartTime = start;
+}
+
+void CPVRTimerInfoTag::SetStartFromLocalTime(const CDateTime& start)
+{
+ m_StartTime = ConvertLocalTimeToUTC(start);
+}
+
+CDateTime CPVRTimerInfoTag::EndAsUTC() const
+{
+ return m_StopTime;
+}
+
+CDateTime CPVRTimerInfoTag::EndAsLocalTime() const
+{
+ return ConvertUTCToLocalTime(m_StopTime);
+}
+
+void CPVRTimerInfoTag::SetEndFromUTC(const CDateTime& end)
+{
+ m_StopTime = end;
+}
+
+void CPVRTimerInfoTag::SetEndFromLocalTime(const CDateTime& end)
+{
+ m_StopTime = ConvertLocalTimeToUTC(end);
+}
+
+int CPVRTimerInfoTag::GetDuration() const
+{
+ time_t start, end;
+ m_StartTime.GetAsTime(start);
+ m_StopTime.GetAsTime(end);
+ return end - start > 0 ? end - start : 3600;
+}
+
+CDateTime CPVRTimerInfoTag::FirstDayAsUTC() const
+{
+ return m_FirstDay;
+}
+
+CDateTime CPVRTimerInfoTag::FirstDayAsLocalTime() const
+{
+ return ConvertUTCToLocalTime(m_FirstDay);
+}
+
+void CPVRTimerInfoTag::SetFirstDayFromUTC(const CDateTime& firstDay)
+{
+ m_FirstDay = firstDay;
+}
+
+void CPVRTimerInfoTag::SetFirstDayFromLocalTime(const CDateTime& firstDay)
+{
+ m_FirstDay = ConvertLocalTimeToUTC(firstDay);
+}
+
+std::string CPVRTimerInfoTag::GetNotificationText() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ int stringID = 0;
+
+ switch (m_state)
+ {
+ case PVR_TIMER_STATE_ABORTED:
+ case PVR_TIMER_STATE_CANCELLED:
+ stringID = 19224; // Recording aborted
+ break;
+ case PVR_TIMER_STATE_SCHEDULED:
+ if (IsTimerRule())
+ stringID = 19058; // Timer enabled
+ else
+ stringID = 19225; // Recording scheduled
+ break;
+ case PVR_TIMER_STATE_RECORDING:
+ stringID = 19226; // Recording started
+ break;
+ case PVR_TIMER_STATE_COMPLETED:
+ stringID = 19227; // Recording completed
+ break;
+ case PVR_TIMER_STATE_CONFLICT_OK:
+ case PVR_TIMER_STATE_CONFLICT_NOK:
+ stringID = 19277; // Recording conflict
+ break;
+ case PVR_TIMER_STATE_ERROR:
+ stringID = 19278; // Recording error
+ break;
+ case PVR_TIMER_STATE_DISABLED:
+ stringID = 19057; // Timer disabled
+ break;
+ default:
+ break;
+ }
+
+ if (stringID != 0)
+ return StringUtils::Format("{}: '{}'", g_localizeStrings.Get(stringID), m_strTitle);
+
+ return {};
+}
+
+std::string CPVRTimerInfoTag::GetDeletedNotificationText() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ int stringID = 0;
+ // The state in this case is the state the timer had when it was last seen
+ switch (m_state)
+ {
+ case PVR_TIMER_STATE_RECORDING:
+ stringID = 19227; // Recording completed
+ break;
+ case PVR_TIMER_STATE_SCHEDULED:
+ default:
+ if (IsTimerRule())
+ stringID = 828; // Timer rule deleted
+ else
+ stringID = 19228; // Timer deleted
+ }
+
+ return StringUtils::Format("{}: '{}'", g_localizeStrings.Get(stringID), m_strTitle);
+}
+
+void CPVRTimerInfoTag::SetEpgInfoTag(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_epgTag = tag;
+ m_bProbedEpgTag = true;
+}
+
+void CPVRTimerInfoTag::UpdateEpgInfoTag()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_epgTag.reset();
+ m_bProbedEpgTag = false;
+ GetEpgInfoTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRTimerInfoTag::GetEpgInfoTag(bool bCreate /* = true */) const
+{
+ if (!m_epgTag && !m_bProbedEpgTag && bCreate && CServiceBroker::GetPVRManager().EpgsCreated())
+ {
+ std::shared_ptr<CPVRChannel> channel(m_channel);
+ if (!channel)
+ {
+ channel = CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(m_bIsRadio)
+ ->GetGroupAll()
+ ->GetByUniqueID(m_iClientChannelUid, m_iClientId);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channel = channel;
+ }
+
+ if (channel)
+ {
+ const std::shared_ptr<CPVREpg> epg(channel->GetEPG());
+ if (epg)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_epgTag && m_iEpgUid != EPG_TAG_INVALID_UID)
+ {
+ m_epgTag = epg->GetTagByBroadcastId(m_iEpgUid);
+ }
+
+ if (!m_epgTag && !IsTimerRule() && IsOwnedByClient())
+ {
+ time_t startTime = 0;
+ time_t endTime = 0;
+
+ StartAsUTC().GetAsTime(startTime);
+ if (startTime > 0)
+ EndAsUTC().GetAsTime(endTime);
+
+ if (startTime > 0 && endTime > 0)
+ {
+ // try to fetch missing epg tag from backend
+ m_epgTag = epg->GetTagBetween(StartAsUTC() - CDateTimeSpan(0, 0, 2, 0),
+ EndAsUTC() + CDateTimeSpan(0, 0, 2, 0), true);
+ if (m_epgTag)
+ m_iEpgUid = m_epgTag->UniqueBroadcastID();
+ }
+ }
+ }
+ }
+ m_bProbedEpgTag = true;
+ }
+ return m_epgTag;
+}
+
+bool CPVRTimerInfoTag::HasChannel() const
+{
+ return m_channel.get() != nullptr;
+}
+
+std::shared_ptr<CPVRChannel> CPVRTimerInfoTag::Channel() const
+{
+ return m_channel;
+}
+
+void CPVRTimerInfoTag::UpdateChannel()
+{
+ const std::shared_ptr<CPVRChannel> channel(CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(m_bIsRadio)
+ ->GetGroupAll()
+ ->GetByUniqueID(m_iClientChannelUid, m_iClientId));
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channel = channel;
+}
+
+const std::string& CPVRTimerInfoTag::Title() const
+{
+ return m_strTitle;
+}
+
+const std::string& CPVRTimerInfoTag::Summary() const
+{
+ return m_strSummary;
+}
+
+const std::string& CPVRTimerInfoTag::Path() const
+{
+ return m_strFileNameAndPath;
+}
+
+const std::string& CPVRTimerInfoTag::SeriesLink() const
+{
+ return m_strSeriesLink;
+}
diff --git a/xbmc/pvr/timers/PVRTimerInfoTag.h b/xbmc/pvr/timers/PVRTimerInfoTag.h
new file mode 100644
index 0000000..ad2d3d8
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimerInfoTag.h
@@ -0,0 +1,644 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "pvr/timers/PVRTimerType.h"
+#include "threads/CriticalSection.h"
+#include "utils/ISerializable.h"
+
+#include <memory>
+#include <string>
+
+struct PVR_TIMER;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVREpgInfoTag;
+
+enum class TimerOperationResult
+{
+ OK = 0,
+ FAILED,
+ RECORDING // The timer was not deleted because it is currently recording (see DeleteTimer).
+};
+
+class CPVRTimerInfoTag final : public ISerializable
+{
+ // allow these classes direct access to members as they act as timer tag instance factories.
+ friend class CGUIDialogPVRTimerSettings;
+ friend class CPVRDatabase;
+
+public:
+ explicit CPVRTimerInfoTag(bool bRadio = false);
+ CPVRTimerInfoTag(const PVR_TIMER& timer,
+ const std::shared_ptr<CPVRChannel>& channel,
+ unsigned int iClientId);
+
+ bool operator==(const CPVRTimerInfoTag& right) const;
+ bool operator!=(const CPVRTimerInfoTag& right) const;
+
+ /*!
+ * @brief Copy over data to the given PVR_TIMER instance.
+ * @param timer The timer instance to fill.
+ */
+ void FillAddonData(PVR_TIMER& timer) const;
+
+ // ISerializable implementation
+ void Serialize(CVariant& value) const override;
+
+ static constexpr int DEFAULT_PVRRECORD_INSTANTRECORDTIME = -1;
+
+ /*!
+ * @brief create a tag for an instant timer for a given channel
+ * @param channel is the channel the instant timer is to be created for
+ * @param iDuration is the duration for the instant timer, DEFAULT_PVRRECORD_INSTANTRECORDTIME
+ * denotes system default (setting value)
+ * @return the timer or null if timer could not be created
+ */
+ static std::shared_ptr<CPVRTimerInfoTag> CreateInstantTimerTag(
+ const std::shared_ptr<CPVRChannel>& channel,
+ int iDuration = DEFAULT_PVRRECORD_INSTANTRECORDTIME);
+
+ /*!
+ * @brief create a tag for a timer for a given channel at given time for given duration
+ * @param channel is the channel the timer is to be created for
+ * @param start is the start time for the recording
+ * @param iDuration is the duration of the recording
+ * @return the timer or null if timer could not be created
+ */
+ static std::shared_ptr<CPVRTimerInfoTag> CreateTimerTag(
+ const std::shared_ptr<CPVRChannel>& channel, const CDateTime& start, int iDuration);
+
+ /*!
+ * @brief create a recording or reminder timer or timer rule for the given epg info tag.
+ * @param tag the epg info tag
+ * @param bCreateRule if true, create a timer rule, create a one shot timer otherwise
+ * @param bCreateReminder if true, create a reminder timer or rule, create a recording timer
+ * or rule otherwise
+ * @param bReadOnly whether the timer/rule is read only
+ * @return the timer or null if timer could not be created
+ */
+ static std::shared_ptr<CPVRTimerInfoTag> CreateFromEpg(const std::shared_ptr<CPVREpgInfoTag>& tag,
+ bool bCreateRule,
+ bool bCreateReminder,
+ bool bReadOnly = false);
+
+ /*!
+ * @brief create a timer or timer rule for the given epg info tag.
+ * @param tag the epg info tag
+ * @param bCreateRule if true, create a timer rule, create a one shot timer otherwise
+ * @return the timer or null if timer could not be created
+ */
+ static std::shared_ptr<CPVRTimerInfoTag> CreateFromEpg(const std::shared_ptr<CPVREpgInfoTag>& tag,
+ bool bCreateRule = false);
+
+ /*!
+ * @brief create a reminder timer for the given start date.
+ * @param start the start date
+ * @param iDuration the duration the reminder is valid
+ * @param parent If non-zero, the new timer will be made a child of the given timer rule
+ * @return the timer or null if timer could not be created
+ */
+ static std::shared_ptr<CPVRTimerInfoTag> CreateReminderFromDate(
+ const CDateTime& start,
+ int iDuration,
+ const std::shared_ptr<CPVRTimerInfoTag>& parent = std::shared_ptr<CPVRTimerInfoTag>());
+
+ /*!
+ * @brief create a reminder timer for the given epg info tag.
+ * @param tag the epg info tag
+ * @param parent If non-zero, the new timer will be made a child of the given timer rule
+ * @return the timer or null if timer could not be created
+ */
+ static std::shared_ptr<CPVRTimerInfoTag> CreateReminderFromEpg(
+ const std::shared_ptr<CPVREpgInfoTag>& tag,
+ const std::shared_ptr<CPVRTimerInfoTag>& parent = std::shared_ptr<CPVRTimerInfoTag>());
+
+ /*!
+ * @brief Associate the given epg tag with this timer.
+ * @param tag The epg tag to assign.
+ */
+ void SetEpgInfoTag(const std::shared_ptr<CPVREpgInfoTag>& tag);
+
+ /*!
+ * @brief get the epg info tag associated with this timer, if any
+ * @param bCreate if true, try to find the epg tag if not yet set (lazy evaluation)
+ * @return the epg info tag associated with this timer or null if there is no tag
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgInfoTag(bool bCreate = true) const;
+
+ /*!
+ * @brief updates this timer excluding the state of any children.
+ * @param tag A timer containing the data that shall be merged into this timer's data.
+ * @return true if the timer was updated successfully
+ */
+ bool UpdateEntry(const std::shared_ptr<CPVRTimerInfoTag>& tag);
+
+ /*!
+ * @brief merge in the state of this child timer.
+ * @param childTimer The child timer
+ * @param bAdd If true, add child's data to parent's state, otherwise subtract.
+ * @return true if the child timer's state was merged successfully
+ */
+ bool UpdateChildState(const std::shared_ptr<CPVRTimerInfoTag>& childTimer, bool bAdd);
+
+ /*!
+ * @brief reset the state of children related to this timer.
+ */
+ void ResetChildState();
+
+ /*!
+ * @brief Whether this timer is active.
+ * @return True if this timer is active, false otherwise.
+ */
+ bool IsActive() const
+ {
+ return m_state == PVR_TIMER_STATE_SCHEDULED || m_state == PVR_TIMER_STATE_RECORDING ||
+ m_state == PVR_TIMER_STATE_CONFLICT_OK || m_state == PVR_TIMER_STATE_CONFLICT_NOK ||
+ m_state == PVR_TIMER_STATE_ERROR;
+ }
+
+ /*!
+ * @brief Whether this timer is broken.
+ * @return True if this timer won't result in a recording because it is broken for some reason,
+ * false otherwise
+ */
+ bool IsBroken() const
+ {
+ return m_state == PVR_TIMER_STATE_CONFLICT_NOK || m_state == PVR_TIMER_STATE_ERROR;
+ }
+
+ /*!
+ * @brief Whether this timer has a conflict.
+ * @return True if this timer won't result in a recording because it is in conflict with another
+ * timer or live stream, false otherwise.
+ */
+ bool HasConflict() const { return m_state == PVR_TIMER_STATE_CONFLICT_NOK; }
+
+ /*!
+ * @brief Whether this timer is currently recording.
+ * @return True if recording, false otherwise.
+ */
+ bool IsRecording() const { return m_state == PVR_TIMER_STATE_RECORDING; }
+
+ /*!
+ * @brief Whether this timer is disabled, for example by the user.
+ * @return True if disabled, false otherwise.
+ */
+ bool IsDisabled() const { return m_state == PVR_TIMER_STATE_DISABLED; }
+
+ /*!
+ * @brief Gets the type of this timer.
+ * @return the timer type or NULL if this tag has no timer type.
+ */
+ const std::shared_ptr<CPVRTimerType> GetTimerType() const { return m_timerType; }
+
+ /*!
+ * @brief Sets the type of this timer.
+ * @param the new timer type.
+ */
+ void SetTimerType(const std::shared_ptr<CPVRTimerType>& type);
+
+ /*!
+ * @brief Checks whether this is a timer rule (vs. one time timer).
+ * @return True if this is a timer rule, false otherwise.
+ */
+ bool IsTimerRule() const { return m_timerType && m_timerType->IsTimerRule(); }
+
+ /*!
+ * @brief Checks whether this is a reminder timer (vs. recording timer).
+ * @return True if this is a reminder timer, false otherwise.
+ */
+ bool IsReminder() const { return m_timerType && m_timerType->IsReminder(); }
+
+ /*!
+ * @brief Checks whether this is a manual (vs. epg-based) timer.
+ * @return True if this is a manual timer, false otherwise.
+ */
+ bool IsManual() const { return m_timerType && m_timerType->IsManual(); }
+
+ /*!
+ * @brief Checks whether this is an epg-based (vs. manual) timer.
+ * @return True if this is an epg-Based timer, false otherwise.
+ */
+ bool IsEpgBased() const { return !IsManual(); }
+
+ /*!
+ * @brief The ID of the client for this timer.
+ * @return The client ID or -1 if this is a local timer.
+ */
+ int ClientID() const { return m_iClientId; }
+
+ /*!
+ * @brief Check, whether this timer is owned by a pvr client or by Kodi.
+ * @return True, if owned by a pvr client, false otherwise.
+ */
+ bool IsOwnedByClient() const;
+
+ /*!
+ * @brief Whether this timer is for Radio or TV.
+ * @return True if Radio, false otherwise.
+ */
+ bool IsRadio() const { return m_bIsRadio; }
+
+ /*!
+ * @brief The path that identifies this timer.
+ * @return The path.
+ */
+ const std::string& Path() const;
+
+ /*!
+ * @brief The index for this timer, as given by the client.
+ * @return The client index or PVR_TIMER_NO_CLIENT_INDEX if the timer was just created locally
+ * by Kodi and was not yet added by the client.
+ */
+ int ClientIndex() const { return m_iClientIndex; }
+
+ /*!
+ * @brief The index for the parent of this timer, as given by the client. Timers scheduled by a
+ * timer rule will have a parant index != PVR_TIMER_NO_PARENT.
+ * @return The client index or PVR_TIMER_NO_PARENT if the timer has no parent.
+ */
+ int ParentClientIndex() const { return m_iParentClientIndex; }
+
+ /*!
+ * @brief Whether this timer has a parent.
+ * @return True if timer has a parent, false otherwise.
+ */
+ bool HasParent() const { return m_iParentClientIndex != PVR_TIMER_NO_PARENT; }
+
+ /*!
+ * @brief The local ID for this timer, as given by Kodi.
+ * @return The ID or 0 if not yet set.
+ */
+ unsigned int TimerID() const { return m_iTimerId; }
+
+ /*!
+ * @brief Set the local ID for this timer.
+ * @param id The ID to set.
+ */
+ void SetTimerID(unsigned int id) { m_iTimerId = id; }
+
+ /*!
+ * @brief The UID of the channel for this timer.
+ * @return The channel UID or PVR_CHANNEL_INVALID_UID if not available.
+ */
+ int ClientChannelUID() const { return m_iClientChannelUid; }
+
+ /*!
+ * @brief The state for this timer.
+ * @return The state.
+ */
+ PVR_TIMER_STATE State() const { return m_state; }
+
+ /*!
+ * @brief Set the state for this timer.
+ * @param state The state to set.
+ */
+ void SetState(PVR_TIMER_STATE state) { m_state = state; }
+
+ /*!
+ * @brief The title for this timer.
+ * @return The title.
+ */
+ const std::string& Title() const;
+
+ /*!
+ * @brief Check whether this timer has an associated channel.
+ * @return True if this timer has a channel set, false otherwise.
+ */
+ bool HasChannel() const;
+
+ /*!
+ * @brief Get the channel associated with this timer, if any.
+ * @return the channel or null if non is associated with this timer.
+ */
+ std::shared_ptr<CPVRChannel> Channel() const;
+
+ /*!
+ * @brief Update the channel associated with this timer, based on current client ID and
+ * channel UID.
+ */
+ void UpdateChannel();
+
+ /*!
+ * @brief The name of the channel associated with this timer, if any.
+ * @return The channel name.
+ */
+ std::string ChannelName() const;
+
+ /*!
+ * @brief The path for the channel icon associated with this timer, if any.
+ * @return The channel icon path.
+ */
+ std::string ChannelIcon() const;
+
+ /*!
+ * @brief The start date and time for this timer, as UTC.
+ * @return The start date and time.
+ */
+ CDateTime StartAsUTC() const;
+
+ /*!
+ * @brief The start date and time for this timer, as local time.
+ * @return The start date and time.
+ */
+ CDateTime StartAsLocalTime() const;
+
+ /*!
+ * @brief Set the start date and time from a CDateTime instance carrying the data as UTC.
+ * @param start The start date and time as UTC.
+ */
+ void SetStartFromUTC(const CDateTime& start);
+
+ /*!
+ * @brief Set the start date and time from a CDateTime instance carrying the data as local time.
+ * @param start The start date and time as local time.
+ */
+ void SetStartFromLocalTime(const CDateTime& start);
+
+ /*!
+ * @brief The end date and time for this timer, as UTC.
+ * @return The start date and time.
+ */
+ CDateTime EndAsUTC() const;
+
+ /*!
+ * @brief The end date and time for this timer, as local time.
+ * @return The start date and time.
+ */
+ CDateTime EndAsLocalTime() const;
+
+ /*!
+ * @brief Set the end date and time from a CDateTime instance carrying the data as UTC.
+ * @param start The end date and time as UTC.
+ */
+ void SetEndFromUTC(const CDateTime& end);
+
+ /*!
+ * @brief Set the end date and time from a CDateTime instance carrying the data as local time.
+ * @param start The end date and time as local time.
+ */
+ void SetEndFromLocalTime(const CDateTime& end);
+
+ /*!
+ * @brief The first day for this timer, as UTC.
+ * @return The first day.
+ */
+ CDateTime FirstDayAsUTC() const;
+
+ /*!
+ * @brief The first day for this timer, as local time.
+ * @return The first day.
+ */
+ CDateTime FirstDayAsLocalTime() const;
+
+ /*!
+ * @brief Set the first dday from a CDateTime instance carrying the data as UTC.
+ * @param start The first day as UTC.
+ */
+ void SetFirstDayFromUTC(const CDateTime& firstDay);
+
+ /*!
+ * @brief Set the first dday from a CDateTime instance carrying the data as local time.
+ * @param start The first day as local time.
+ */
+ void SetFirstDayFromLocalTime(const CDateTime& firstDay);
+
+ /*!
+ * @brief Helper function to convert a given CDateTime containing data as UTC to local time.
+ * @param utc A CDateTime instance carrying data as UTC.
+ * @return A CDateTime instance carrying data as local time.
+ */
+ static CDateTime ConvertUTCToLocalTime(const CDateTime& utc);
+
+ /*!
+ * @brief Helper function to convert a given CDateTime containing data as local time to UTC.
+ * @param local A CDateTime instance carrying data as local time.
+ * @return A CDateTime instance carrying data as UTC.
+ */
+ static CDateTime ConvertLocalTimeToUTC(const CDateTime& local);
+
+ /*!
+ * @brief Get the duration of this timer in seconds, excluding padding times.
+ * @return The duration.
+ */
+ int GetDuration() const;
+
+ /*!
+ * @brief Get time in minutes to start the recording before the start time of the programme.
+ * @return The start padding time.
+ */
+ unsigned int MarginStart() const { return m_iMarginStart; }
+
+ /*!
+ * @brief Get time in minutes to end the recording after the end time of the programme.
+ * @return The end padding time.
+ */
+ unsigned int MarginEnd() const { return m_iMarginEnd; }
+
+ /*!
+ * @brief For timer rules, the days of week this timer rule is scheduled for.
+ * @return The days of week.
+ */
+ unsigned int WeekDays() const { return m_iWeekdays; }
+
+ /*!
+ * @brief For timer rules, whether start time is "any time", not a particular time.
+ * @return True, if timer start is "any time", false otherwise.
+ */
+ bool IsStartAnyTime() const { return m_bStartAnyTime; }
+
+ /*!
+ * @brief For timer rules, whether end time is "any time", not a particular time.
+ * @return True, if timer end is "any time", false otherwise.
+ */
+ bool IsEndAnyTime() const { return m_bEndAnyTime; }
+
+ /*!
+ * @brief For timer rules, whether only the EPG programme title shall be searched or also other
+ * data like the programme's plot, if available.
+ * @return True, if not only the programme's title shall be included in EPG search,
+ * false otherwise.
+ */
+ bool IsFullTextEpgSearch() const { return m_bFullTextEpgSearch; }
+
+ /*!
+ * @brief For timer rules, the epg data match string for searches. Format is backend-dependent,
+ * for example regexp.
+ * @return The search string
+ */
+ const std::string& EpgSearchString() const { return m_strEpgSearchString; }
+
+ /*!
+ * @brief The series link for this timer.
+ * @return The series link or empty string, if not available.
+ */
+ const std::string& SeriesLink() const;
+
+ /*!
+ * @brief Get the UID of the epg event associated with this timer tag, if any.
+ * @return The UID or EPG_TAG_INVALID_UID.
+ */
+ unsigned int UniqueBroadcastID() const { return m_iEpgUid; }
+
+ /*!
+ * @brief Add this timer to the backend, transferring all local data of this timer to the backend.
+ * @return True on success, false otherwise.
+ */
+ bool AddToClient() const;
+
+ /*!
+ * @brief Delete this timer on the backend.
+ * @param bForce Control what to do in case the timer is currently recording.
+ * True to force to delete the timer, false to return TimerDeleteResult::RECORDING.
+ * @return The result.
+ */
+ TimerOperationResult DeleteFromClient(bool bForce = false) const;
+
+ /*!
+ * @brief Update this timer on the backend, transferring all local data of this timer to
+ * the backend.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateOnClient();
+
+ /*!
+ * @brief Persist this timer in the local database.
+ * @return True on success, false otherwise.
+ */
+ bool Persist();
+
+ /*!
+ * @brief Delete this timer from the local database.
+ * @return True on success, false otherwise.
+ */
+ bool DeleteFromDatabase();
+
+ /*!
+ * @brief GUI support: Get the text for the timer GUI notification.
+ * @return The notification text.
+ */
+ std::string GetNotificationText() const;
+
+ /*!
+ * @brief GUI support: Get the text for the timer GUI notification when a timer has been deleted.
+ * @return The notification text.
+ */
+ std::string GetDeletedNotificationText() const;
+
+ /*!
+ * @brief GUI support: Get the summary text for this timer, reflecting the timer schedule in a
+ * human readable form.
+ * @return The summary string.
+ */
+ const std::string& Summary() const;
+
+ /*!
+ * @brief GUI support: Update the summary text for this timer.
+ */
+ void UpdateSummary();
+
+ /*!
+ * @brief GUI support: Get the status text for this timer, reflecting its current state in a
+ * human readable form.
+ * @return The status string.
+ */
+ std::string GetStatus(bool bRadio) const;
+
+ /*!
+ * @brief GUI support: Get the timer string in a human readable form.
+ * @return The type string.
+ */
+ std::string GetTypeAsString() const;
+
+ /*!
+ * @brief GUI support: Return string representation for any possible combination of weekdays.
+ * @param iWeekdays weekdays as bit mask (0x01 = Mo, 0x02 = Tu, ...)
+ * @param bEpgBased context is an epg-based timer
+ * @param bLongMultiDaysFormat use long format. ("Mo-__-We-__-Fr-Sa-__" vs. "Mo-We-Fr-Sa")
+ * @return The weekdays string representation.
+ */
+ static std::string GetWeekdaysString(unsigned int iWeekdays,
+ bool bEpgBased,
+ bool bLongMultiDaysFormat);
+
+private:
+ CPVRTimerInfoTag(const CPVRTimerInfoTag& tag) = delete;
+ CPVRTimerInfoTag& operator=(const CPVRTimerInfoTag& orig) = delete;
+
+ std::string GetWeekdaysString() const;
+ void UpdateEpgInfoTag();
+
+ static std::shared_ptr<CPVRTimerInfoTag> CreateFromDate(
+ const std::shared_ptr<CPVRChannel>& channel,
+ const CDateTime& start,
+ int iDuration,
+ bool bCreateReminder,
+ bool bReadOnly);
+
+ mutable CCriticalSection m_critSection;
+
+ std::string m_strTitle; /*!< @brief name of this timer */
+ std::string
+ m_strEpgSearchString; /*!< @brief a epg data match string for epg-based timer rules. Format is backend-dependent, for example regexp */
+ bool m_bFullTextEpgSearch =
+ false; /*!< @brief indicates whether only epg episode title can be matched by the pvr backend or "more" (backend-dependent") data. */
+ std::string m_strDirectory; /*!< @brief directory where the recording must be stored */
+ std::string m_strSummary; /*!< @brief summary string with the time to show inside a GUI list */
+ PVR_TIMER_STATE m_state = PVR_TIMER_STATE_SCHEDULED; /*!< @brief the state of this timer */
+ int m_iClientId; /*!< @brief ID of the backend */
+ int m_iClientIndex; /*!< @brief index number of the tag, given by the backend, PVR_TIMER_NO_CLIENT_INDEX for new */
+ int m_iParentClientIndex; /*!< @brief for timers scheduled by a timer rule, the index number of the parent, given by the backend, PVR_TIMER_NO_PARENT for no parent */
+ int m_iClientChannelUid; /*!< @brief channel uid */
+ bool m_bStartAnyTime =
+ false; /*!< @brief Ignore start date and time clock. Record at 'Any Time' */
+ bool m_bEndAnyTime = false; /*!< @brief Ignore end date and time clock. Record at 'Any Time' */
+ int m_iPriority; /*!< @brief priority of the timer */
+ int m_iLifetime; /*!< @brief lifetime of the timer in days */
+ int m_iMaxRecordings =
+ 0; /*!< @brief (optional) backend setting for maximum number of recordings to keep*/
+ unsigned int m_iWeekdays; /*!< @brief bit based store of weekdays for timer rules */
+ unsigned int
+ m_iPreventDupEpisodes; /*!< @brief only record new episodes for epg-based timer rules */
+ unsigned int m_iRecordingGroup =
+ 0; /*!< @brief (optional) if set, the addon/backend stores the recording to a group (sub-folder) */
+ std::string m_strFileNameAndPath; /*!< @brief file name is only for reference */
+ bool m_bIsRadio; /*!< @brief is radio channel if set */
+ unsigned int m_iTimerId = 0; /*!< @brief id that won't change as long as Kodi is running */
+ unsigned int
+ m_iMarginStart; /*!< @brief (optional) if set, the backend starts the recording iMarginStart minutes before startTime. */
+ unsigned int
+ m_iMarginEnd; /*!< @brief (optional) if set, the backend ends the recording iMarginEnd minutes after endTime. */
+ mutable unsigned int
+ m_iEpgUid; /*!< id of epg event associated with this timer, EPG_TAG_INVALID_UID if none. */
+ std::string m_strSeriesLink; /*!< series link */
+
+ CDateTime m_StartTime; /*!< start time */
+ CDateTime m_StopTime; /*!< stop time */
+ CDateTime m_FirstDay; /*!< if it is a manual timer rule the first date it starts */
+ std::shared_ptr<CPVRTimerType> m_timerType; /*!< the type of this timer */
+
+ unsigned int m_iTVChildTimersActive = 0;
+ unsigned int m_iTVChildTimersConflictNOK = 0;
+ unsigned int m_iTVChildTimersRecording = 0;
+ unsigned int m_iTVChildTimersErrors = 0;
+ unsigned int m_iRadioChildTimersActive = 0;
+ unsigned int m_iRadioChildTimersConflictNOK = 0;
+ unsigned int m_iRadioChildTimersRecording = 0;
+ unsigned int m_iRadioChildTimersErrors = 0;
+
+ mutable std::shared_ptr<CPVREpgInfoTag> m_epgTag; /*!< epg info tag matching m_iEpgUid. */
+ mutable std::shared_ptr<CPVRChannel> m_channel;
+
+ mutable bool m_bProbedEpgTag = false;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/timers/PVRTimerRuleMatcher.cpp b/xbmc/pvr/timers/PVRTimerRuleMatcher.cpp
new file mode 100644
index 0000000..9178e85
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimerRuleMatcher.cpp
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRTimerRuleMatcher.h"
+
+#include "XBDateTime.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h" // PVR_CHANNEL_INVALID_UID
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "utils/RegExp.h"
+
+using namespace PVR;
+
+CPVRTimerRuleMatcher::CPVRTimerRuleMatcher(const std::shared_ptr<CPVRTimerInfoTag>& timerRule,
+ const CDateTime& start)
+ : m_timerRule(timerRule), m_start(CPVRTimerInfoTag::ConvertUTCToLocalTime(start))
+{
+}
+
+CPVRTimerRuleMatcher::~CPVRTimerRuleMatcher() = default;
+
+std::shared_ptr<CPVRChannel> CPVRTimerRuleMatcher::GetChannel() const
+{
+ if (m_timerRule->GetTimerType()->SupportsChannels())
+ return m_timerRule->Channel();
+
+ return {};
+}
+
+CDateTime CPVRTimerRuleMatcher::GetNextTimerStart() const
+{
+ if (!m_timerRule->GetTimerType()->SupportsStartTime())
+ return CDateTime(); // invalid datetime
+
+ const CDateTime startDateLocal = m_timerRule->GetTimerType()->SupportsFirstDay()
+ ? m_timerRule->FirstDayAsLocalTime()
+ : m_start;
+ const CDateTime startTimeLocal = m_timerRule->StartAsLocalTime();
+ CDateTime nextStart(startDateLocal.GetYear(), startDateLocal.GetMonth(), startDateLocal.GetDay(),
+ startTimeLocal.GetHour(), startTimeLocal.GetMinute(), 0);
+
+ const CDateTimeSpan oneDay(1, 0, 0, 0);
+ while (nextStart < m_start)
+ {
+ nextStart += oneDay;
+ }
+
+ if (m_timerRule->GetTimerType()->SupportsWeekdays() &&
+ m_timerRule->WeekDays() != PVR_WEEKDAY_ALLDAYS)
+ {
+ bool bMatch = false;
+ while (!bMatch)
+ {
+ int startWeekday = nextStart.GetDayOfWeek();
+ if (startWeekday == 0)
+ startWeekday = 7;
+
+ bMatch = ((1 << (startWeekday - 1)) & m_timerRule->WeekDays());
+ if (!bMatch)
+ nextStart += oneDay;
+ }
+ }
+
+ return nextStart.GetAsUTCDateTime();
+}
+
+bool CPVRTimerRuleMatcher::Matches(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ return epgTag && CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->EndAsUTC()) > m_start &&
+ MatchSeriesLink(epgTag) && MatchChannel(epgTag) && MatchStart(epgTag) &&
+ MatchEnd(epgTag) && MatchDayOfWeek(epgTag) && MatchSearchText(epgTag);
+}
+
+bool CPVRTimerRuleMatcher::MatchSeriesLink(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (m_timerRule->GetTimerType()->RequiresEpgSeriesLinkOnCreate())
+ return epgTag->SeriesLink() == m_timerRule->SeriesLink();
+ else
+ return true;
+}
+
+bool CPVRTimerRuleMatcher::MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (m_timerRule->GetTimerType()->SupportsAnyChannel() &&
+ m_timerRule->ClientChannelUID() == PVR_CHANNEL_INVALID_UID)
+ return true; // matches any channel
+
+ if (m_timerRule->GetTimerType()->SupportsChannels())
+ return m_timerRule->ClientChannelUID() != PVR_CHANNEL_INVALID_UID &&
+ epgTag->ClientID() == m_timerRule->ClientID() &&
+ epgTag->UniqueChannelID() == m_timerRule->ClientChannelUID();
+ else
+ return true;
+}
+
+bool CPVRTimerRuleMatcher::MatchStart(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (m_timerRule->GetTimerType()->SupportsFirstDay())
+ {
+ // only year, month and day do matter here...
+ const CDateTime startEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->StartAsUTC());
+ const CDateTime startEpg(startEpgLocal.GetYear(), startEpgLocal.GetMonth(),
+ startEpgLocal.GetDay(), 0, 0, 0);
+ const CDateTime firstDayLocal = m_timerRule->FirstDayAsLocalTime();
+ const CDateTime startTimer(firstDayLocal.GetYear(), firstDayLocal.GetMonth(),
+ firstDayLocal.GetDay(), 0, 0, 0);
+ if (startEpg < startTimer)
+ return false;
+ }
+
+ if (m_timerRule->GetTimerType()->SupportsStartAnyTime() && m_timerRule->IsStartAnyTime())
+ return true; // matches any start time
+
+ if (m_timerRule->GetTimerType()->SupportsStartTime())
+ {
+ // only hours and minutes do matter here...
+ const CDateTime startEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->StartAsUTC());
+ const CDateTime startEpg(2000, 1, 1, startEpgLocal.GetHour(), startEpgLocal.GetMinute(), 0);
+ const CDateTime startTimerLocal = m_timerRule->StartAsLocalTime();
+ const CDateTime startTimer(2000, 1, 1, startTimerLocal.GetHour(), startTimerLocal.GetMinute(),
+ 0);
+ return startEpg >= startTimer;
+ }
+ else
+ return true;
+}
+
+bool CPVRTimerRuleMatcher::MatchEnd(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (m_timerRule->GetTimerType()->SupportsEndAnyTime() && m_timerRule->IsEndAnyTime())
+ return true; // matches any end time
+
+ if (m_timerRule->GetTimerType()->SupportsEndTime())
+ {
+ // only hours and minutes do matter here...
+ const CDateTime endEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->EndAsUTC());
+ const CDateTime endEpg(2000, 1, 1, endEpgLocal.GetHour(), endEpgLocal.GetMinute(), 0);
+ const CDateTime endTimerLocal = m_timerRule->EndAsLocalTime();
+ const CDateTime endTimer(2000, 1, 1, endTimerLocal.GetHour(), endTimerLocal.GetMinute(), 0);
+ return endEpg <= endTimer;
+ }
+ else
+ return true;
+}
+
+bool CPVRTimerRuleMatcher::MatchDayOfWeek(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (m_timerRule->GetTimerType()->SupportsWeekdays())
+ {
+ if (m_timerRule->WeekDays() != PVR_WEEKDAY_ALLDAYS)
+ {
+ const CDateTime startEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->StartAsUTC());
+ int startWeekday = startEpgLocal.GetDayOfWeek();
+ if (startWeekday == 0)
+ startWeekday = 7;
+
+ return ((1 << (startWeekday - 1)) & m_timerRule->WeekDays());
+ }
+ }
+ return true;
+}
+
+bool CPVRTimerRuleMatcher::MatchSearchText(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (m_timerRule->GetTimerType()->SupportsEpgFulltextMatch() && m_timerRule->IsFullTextEpgSearch())
+ {
+ if (!m_textSearch)
+ {
+ m_textSearch.reset(new CRegExp(true /* case insensitive */));
+ m_textSearch->RegComp(m_timerRule->EpgSearchString());
+ }
+ return m_textSearch->RegFind(epgTag->Title()) >= 0 ||
+ m_textSearch->RegFind(epgTag->EpisodeName()) >= 0 ||
+ m_textSearch->RegFind(epgTag->PlotOutline()) >= 0 ||
+ m_textSearch->RegFind(epgTag->Plot()) >= 0;
+ }
+ else if (m_timerRule->GetTimerType()->SupportsEpgTitleMatch())
+ {
+ if (!m_textSearch)
+ {
+ m_textSearch.reset(new CRegExp(true /* case insensitive */));
+ m_textSearch->RegComp(m_timerRule->EpgSearchString());
+ }
+ return m_textSearch->RegFind(epgTag->Title()) >= 0;
+ }
+ else
+ return true;
+}
diff --git a/xbmc/pvr/timers/PVRTimerRuleMatcher.h b/xbmc/pvr/timers/PVRTimerRuleMatcher.h
new file mode 100644
index 0000000..9d502af
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimerRuleMatcher.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+
+#include <memory>
+
+class CRegExp;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRTimerInfoTag;
+class CPVREpgInfoTag;
+
+class CPVRTimerRuleMatcher
+{
+public:
+ CPVRTimerRuleMatcher(const std::shared_ptr<CPVRTimerInfoTag>& timerRule, const CDateTime& start);
+ virtual ~CPVRTimerRuleMatcher();
+
+ std::shared_ptr<CPVRTimerInfoTag> GetTimerRule() const { return m_timerRule; }
+
+ std::shared_ptr<CPVRChannel> GetChannel() const;
+ CDateTime GetNextTimerStart() const;
+ bool Matches(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+private:
+ bool MatchSeriesLink(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+ bool MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+ bool MatchStart(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+ bool MatchEnd(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+ bool MatchDayOfWeek(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+ bool MatchSearchText(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ const std::shared_ptr<CPVRTimerInfoTag> m_timerRule;
+ CDateTime m_start;
+ mutable std::unique_ptr<CRegExp> m_textSearch;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/timers/PVRTimerType.cpp b/xbmc/pvr/timers/PVRTimerType.cpp
new file mode 100644
index 0000000..db20fa1
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimerType.cpp
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRTimerType.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+const std::vector<std::shared_ptr<CPVRTimerType>> CPVRTimerType::GetAllTypes()
+{
+ std::vector<std::shared_ptr<CPVRTimerType>> allTypes;
+ CServiceBroker::GetPVRManager().Clients()->GetTimerTypes(allTypes);
+
+ // Add local reminder timer types. Local reminders are always available.
+ int iTypeId = PVR_TIMER_TYPE_NONE;
+
+ // one time time-based reminder
+ allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId,
+ PVR_TIMER_TYPE_IS_MANUAL |
+ PVR_TIMER_TYPE_IS_REMINDER |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME));
+
+ // one time epg-based reminder
+ allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId,
+ PVR_TIMER_TYPE_IS_REMINDER |
+ PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_START_MARGIN));
+
+ // time-based reminder rule
+ allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId,
+ PVR_TIMER_TYPE_IS_REPEATING |
+ PVR_TIMER_TYPE_IS_MANUAL |
+ PVR_TIMER_TYPE_IS_REMINDER |
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY |
+ PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS));
+
+ // one time read-only time-based reminder (created by timer rule)
+ allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId,
+ PVR_TIMER_TYPE_IS_MANUAL |
+ PVR_TIMER_TYPE_IS_REMINDER |
+ PVR_TIMER_TYPE_IS_READONLY |
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME,
+ g_localizeStrings.Get(819))); // One time (Scheduled by timer rule)
+
+ // epg-based reminder rule
+ allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId,
+ PVR_TIMER_TYPE_IS_REPEATING |
+ PVR_TIMER_TYPE_IS_REMINDER |
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE |
+ PVR_TIMER_TYPE_SUPPORTS_TITLE_EPG_MATCH |
+ PVR_TIMER_TYPE_SUPPORTS_FULLTEXT_EPG_MATCH |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_ANY_CHANNEL |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_START_ANYTIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_ANYTIME |
+ PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY |
+ PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS |
+ PVR_TIMER_TYPE_SUPPORTS_START_END_MARGIN));
+
+ // one time read-only epg-based reminder (created by timer rule)
+ allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId,
+ PVR_TIMER_TYPE_IS_REMINDER |
+ PVR_TIMER_TYPE_IS_READONLY |
+ PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE |
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_START_MARGIN,
+ g_localizeStrings.Get(819))); // One time (Scheduled by timer rule)
+
+ return allTypes;
+}
+
+const std::shared_ptr<CPVRTimerType> CPVRTimerType::GetFirstAvailableType(const std::shared_ptr<CPVRClient>& client)
+{
+ if (client)
+ {
+ std::vector<std::shared_ptr<CPVRTimerType>> types;
+ if (client->GetTimerTypes(types) == PVR_ERROR_NO_ERROR && !types.empty())
+ {
+ return *(types.begin());
+ }
+ }
+ return {};
+}
+
+std::shared_ptr<CPVRTimerType> CPVRTimerType::CreateFromIds(unsigned int iTypeId, int iClientId)
+{
+ const std::vector<std::shared_ptr<CPVRTimerType>> types = GetAllTypes();
+ const auto it =
+ std::find_if(types.cbegin(), types.cend(), [iClientId, iTypeId](const auto& type) {
+ return type->GetClientId() == iClientId && type->GetTypeId() == iTypeId;
+ });
+ if (it != types.cend())
+ return (*it);
+
+ if (iClientId != -1)
+ {
+ // fallback. try to obtain local timer type.
+ std::shared_ptr<CPVRTimerType> type = CreateFromIds(iTypeId, -1);
+ if (type)
+ return type;
+ }
+
+ CLog::LogF(LOGERROR, "Unable to resolve numeric timer type ({}, {})", iTypeId, iClientId);
+ return {};
+}
+
+std::shared_ptr<CPVRTimerType> CPVRTimerType::CreateFromAttributes(uint64_t iMustHaveAttr,
+ uint64_t iMustNotHaveAttr,
+ int iClientId)
+{
+ const std::vector<std::shared_ptr<CPVRTimerType>> types = GetAllTypes();
+ const auto it = std::find_if(types.cbegin(), types.cend(),
+ [iClientId, iMustHaveAttr, iMustNotHaveAttr](const auto& type) {
+ return type->GetClientId() == iClientId &&
+ (type->GetAttributes() & iMustHaveAttr) == iMustHaveAttr &&
+ (type->GetAttributes() & iMustNotHaveAttr) == 0;
+ });
+ if (it != types.cend())
+ return (*it);
+
+ if (iClientId != -1)
+ {
+ // fallback. try to obtain local timer type.
+ std::shared_ptr<CPVRTimerType> type = CreateFromAttributes(iMustHaveAttr, iMustNotHaveAttr, -1);
+ if (type)
+ return type;
+ }
+
+ CLog::LogF(LOGERROR, "Unable to resolve timer type (0x{:x}, 0x{:x}, {})", iMustHaveAttr,
+ iMustNotHaveAttr, iClientId);
+ return {};
+}
+
+CPVRTimerType::CPVRTimerType() :
+ m_iTypeId(PVR_TIMER_TYPE_NONE),
+ m_iAttributes(PVR_TIMER_TYPE_ATTRIBUTE_NONE)
+{
+}
+
+CPVRTimerType::CPVRTimerType(const PVR_TIMER_TYPE& type, int iClientId) :
+ m_iClientId(iClientId),
+ m_iTypeId(type.iId),
+ m_iAttributes(type.iAttributes),
+ m_strDescription(type.strDescription)
+{
+ InitDescription();
+ InitAttributeValues(type);
+}
+
+CPVRTimerType::CPVRTimerType(unsigned int iTypeId,
+ uint64_t iAttributes,
+ const std::string& strDescription)
+ : m_iTypeId(iTypeId), m_iAttributes(iAttributes), m_strDescription(strDescription)
+{
+ InitDescription();
+}
+
+CPVRTimerType::~CPVRTimerType() = default;
+
+bool CPVRTimerType::operator ==(const CPVRTimerType& right) const
+{
+ return (m_iClientId == right.m_iClientId &&
+ m_iTypeId == right.m_iTypeId &&
+ m_iAttributes == right.m_iAttributes &&
+ m_strDescription == right.m_strDescription &&
+ m_priorityValues == right.m_priorityValues &&
+ m_iPriorityDefault == right.m_iPriorityDefault &&
+ m_lifetimeValues == right.m_lifetimeValues &&
+ m_iLifetimeDefault == right.m_iLifetimeDefault &&
+ m_maxRecordingsValues == right.m_maxRecordingsValues &&
+ m_iMaxRecordingsDefault == right.m_iMaxRecordingsDefault &&
+ m_preventDupEpisodesValues == right.m_preventDupEpisodesValues &&
+ m_iPreventDupEpisodesDefault == right.m_iPreventDupEpisodesDefault &&
+ m_recordingGroupValues == right.m_recordingGroupValues &&
+ m_iRecordingGroupDefault == right.m_iRecordingGroupDefault);
+}
+
+bool CPVRTimerType::operator !=(const CPVRTimerType& right) const
+{
+ return !(*this == right);
+}
+
+void CPVRTimerType::InitDescription()
+{
+ // if no description was given, compile it
+ if (m_strDescription.empty())
+ {
+ int id;
+ if (m_iAttributes & PVR_TIMER_TYPE_IS_REPEATING)
+ {
+ id = (m_iAttributes & PVR_TIMER_TYPE_IS_MANUAL)
+ ? 822 // "Timer rule"
+ : 823; // "Timer rule (guide-based)"
+ }
+ else
+ {
+ id = (m_iAttributes & PVR_TIMER_TYPE_IS_MANUAL)
+ ? 820 // "One time"
+ : 821; // "One time (guide-based)
+ }
+ m_strDescription = g_localizeStrings.Get(id);
+ }
+
+ // add reminder/recording prefix
+ int prefixId = (m_iAttributes & PVR_TIMER_TYPE_IS_REMINDER)
+ ? 824 // Reminder: ...
+ : 825; // Recording: ...
+
+ m_strDescription = StringUtils::Format(g_localizeStrings.Get(prefixId), m_strDescription);
+}
+
+void CPVRTimerType::InitAttributeValues(const PVR_TIMER_TYPE& type)
+{
+ InitPriorityValues(type);
+ InitLifetimeValues(type);
+ InitMaxRecordingsValues(type);
+ InitPreventDuplicateEpisodesValues(type);
+ InitRecordingGroupValues(type);
+}
+
+void CPVRTimerType::InitPriorityValues(const PVR_TIMER_TYPE& type)
+{
+ if (type.iPrioritiesSize > 0)
+ {
+ for (unsigned int i = 0; i < type.iPrioritiesSize; ++i)
+ {
+ std::string strDescr(type.priorities[i].strDescription);
+ if (strDescr.empty())
+ {
+ // No description given by addon. Create one from value.
+ strDescr = std::to_string(type.priorities[i].iValue);
+ }
+ m_priorityValues.emplace_back(strDescr, type.priorities[i].iValue);
+ }
+
+ m_iPriorityDefault = type.iPrioritiesDefault;
+ }
+ else if (SupportsPriority())
+ {
+ // No values given by addon, but priority supported. Use default values 1..100
+ for (int i = 1; i < 101; ++i)
+ m_priorityValues.emplace_back(std::to_string(i), i);
+
+ m_iPriorityDefault = DEFAULT_RECORDING_PRIORITY;
+ }
+ else
+ {
+ // No priority supported.
+ m_iPriorityDefault = DEFAULT_RECORDING_PRIORITY;
+ }
+}
+
+void CPVRTimerType::GetPriorityValues(std::vector<std::pair<std::string, int>>& list) const
+{
+ std::copy(m_priorityValues.cbegin(), m_priorityValues.cend(), std::back_inserter(list));
+}
+
+void CPVRTimerType::InitLifetimeValues(const PVR_TIMER_TYPE& type)
+{
+ if (type.iLifetimesSize > 0)
+ {
+ for (unsigned int i = 0; i < type.iLifetimesSize; ++i)
+ {
+ int iValue = type.lifetimes[i].iValue;
+ std::string strDescr(type.lifetimes[i].strDescription);
+ if (strDescr.empty())
+ {
+ // No description given by addon. Create one from value.
+ strDescr = std::to_string(iValue);
+ }
+ m_lifetimeValues.emplace_back(strDescr, iValue);
+ }
+
+ m_iLifetimeDefault = type.iLifetimesDefault;
+ }
+ else if (SupportsLifetime())
+ {
+ // No values given by addon, but lifetime supported. Use default values 1..365
+ for (int i = 1; i < 366; ++i)
+ {
+ m_lifetimeValues.emplace_back(StringUtils::Format(g_localizeStrings.Get(17999), i),
+ i); // "{} days"
+ }
+ m_iLifetimeDefault = DEFAULT_RECORDING_LIFETIME;
+ }
+ else
+ {
+ // No lifetime supported.
+ m_iLifetimeDefault = DEFAULT_RECORDING_LIFETIME;
+ }
+}
+
+void CPVRTimerType::GetLifetimeValues(std::vector<std::pair<std::string, int>>& list) const
+{
+ std::copy(m_lifetimeValues.cbegin(), m_lifetimeValues.cend(), std::back_inserter(list));
+}
+
+void CPVRTimerType::InitMaxRecordingsValues(const PVR_TIMER_TYPE& type)
+{
+ if (type.iMaxRecordingsSize > 0)
+ {
+ for (unsigned int i = 0; i < type.iMaxRecordingsSize; ++i)
+ {
+ std::string strDescr(type.maxRecordings[i].strDescription);
+ if (strDescr.empty())
+ {
+ // No description given by addon. Create one from value.
+ strDescr = std::to_string(type.maxRecordings[i].iValue);
+ }
+ m_maxRecordingsValues.emplace_back(strDescr, type.maxRecordings[i].iValue);
+ }
+
+ m_iMaxRecordingsDefault = type.iMaxRecordingsDefault;
+ }
+}
+
+void CPVRTimerType::GetMaxRecordingsValues(std::vector<std::pair<std::string, int>>& list) const
+{
+ std::copy(m_maxRecordingsValues.cbegin(), m_maxRecordingsValues.cend(), std::back_inserter(list));
+}
+
+void CPVRTimerType::InitPreventDuplicateEpisodesValues(const PVR_TIMER_TYPE& type)
+{
+ if (type.iPreventDuplicateEpisodesSize > 0)
+ {
+ for (unsigned int i = 0; i < type.iPreventDuplicateEpisodesSize; ++i)
+ {
+ std::string strDescr(type.preventDuplicateEpisodes[i].strDescription);
+ if (strDescr.empty())
+ {
+ // No description given by addon. Create one from value.
+ strDescr = std::to_string(type.preventDuplicateEpisodes[i].iValue);
+ }
+ m_preventDupEpisodesValues.emplace_back(strDescr, type.preventDuplicateEpisodes[i].iValue);
+ }
+
+ m_iPreventDupEpisodesDefault = type.iPreventDuplicateEpisodesDefault;
+ }
+ else if (SupportsRecordOnlyNewEpisodes())
+ {
+ // No values given by addon, but prevent duplicate episodes supported. Use default values 0..1
+ m_preventDupEpisodesValues.emplace_back(g_localizeStrings.Get(815), 0); // "Record all episodes"
+ m_preventDupEpisodesValues.emplace_back(g_localizeStrings.Get(816), 1); // "Record only new episodes"
+ m_iPreventDupEpisodesDefault = DEFAULT_RECORDING_DUPLICATEHANDLING;
+ }
+ else
+ {
+ // No prevent duplicate episodes supported.
+ m_iPreventDupEpisodesDefault = DEFAULT_RECORDING_DUPLICATEHANDLING;
+ }
+}
+
+void CPVRTimerType::GetPreventDuplicateEpisodesValues(std::vector<std::pair<std::string, int>>& list) const
+{
+ std::copy(m_preventDupEpisodesValues.cbegin(), m_preventDupEpisodesValues.cend(),
+ std::back_inserter(list));
+}
+
+void CPVRTimerType::InitRecordingGroupValues(const PVR_TIMER_TYPE& type)
+{
+ if (type.iRecordingGroupSize > 0)
+ {
+ for (unsigned int i = 0; i < type.iRecordingGroupSize; ++i)
+ {
+ std::string strDescr(type.recordingGroup[i].strDescription);
+ if (strDescr.empty())
+ {
+ // No description given by addon. Create one from value.
+ strDescr = StringUtils::Format("{} {}",
+ g_localizeStrings.Get(811), // Recording group
+ type.recordingGroup[i].iValue);
+ }
+ m_recordingGroupValues.emplace_back(strDescr, type.recordingGroup[i].iValue);
+ }
+
+ m_iRecordingGroupDefault = type.iRecordingGroupDefault;
+ }
+}
+
+void CPVRTimerType::GetRecordingGroupValues(std::vector< std::pair<std::string, int>>& list) const
+{
+ std::copy(m_recordingGroupValues.cbegin(), m_recordingGroupValues.cend(),
+ std::back_inserter(list));
+}
diff --git a/xbmc/pvr/timers/PVRTimerType.h b/xbmc/pvr/timers/PVRTimerType.h
new file mode 100644
index 0000000..8ee9e22
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimerType.h
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_timers.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+struct PVR_TIMER_TYPE;
+
+namespace PVR
+{
+ class CPVRClient;
+
+ static const int DEFAULT_RECORDING_PRIORITY = 50;
+ static const int DEFAULT_RECORDING_LIFETIME = 99; // days
+ static const unsigned int DEFAULT_RECORDING_DUPLICATEHANDLING = 0;
+
+ class CPVRTimerType
+ {
+ public:
+ /*!
+ * @brief Return a list with all known timer types.
+ * @return A list of timer types or an empty list if no types available.
+ */
+ static const std::vector<std::shared_ptr<CPVRTimerType>> GetAllTypes();
+
+ /*!
+ * @brief Return the first available timer type from given client.
+ * @param client the PVR client.
+ * @return A timer type or NULL if none available.
+ */
+ static const std::shared_ptr<CPVRTimerType> GetFirstAvailableType(const std::shared_ptr<CPVRClient>& client);
+
+ /*!
+ * @brief Create a timer type from given timer type id and client id.
+ * @param iTimerType the timer type id.
+ * @param iClientId the PVR client id.
+ * @return A timer type instance.
+ */
+ static std::shared_ptr<CPVRTimerType> CreateFromIds(unsigned int iTypeId, int iClientId);
+
+ /*!
+ * @brief Create a timer type from given timer type attributes and client id.
+ * @param iMustHaveAttr a combination of PVR_TIMER_TYPE_* attributes the type to create must have.
+ * @param iMustNotHaveAttr a combination of PVR_TIMER_TYPE_* attributes the type to create must not have.
+ * @param iClientId the PVR client id.
+ * @return A timer type instance.
+ */
+ static std::shared_ptr<CPVRTimerType> CreateFromAttributes(uint64_t iMustHaveAttr,
+ uint64_t iMustNotHaveAttr,
+ int iClientId);
+
+ CPVRTimerType();
+ CPVRTimerType(const PVR_TIMER_TYPE& type, int iClientId);
+ CPVRTimerType(unsigned int iTypeId,
+ uint64_t iAttributes,
+ const std::string& strDescription = "");
+
+ virtual ~CPVRTimerType();
+
+ CPVRTimerType(const CPVRTimerType& type) = delete;
+ CPVRTimerType& operator=(const CPVRTimerType& orig) = delete;
+
+ bool operator ==(const CPVRTimerType& right) const;
+ bool operator !=(const CPVRTimerType& right) const;
+
+ /*!
+ * @brief Get the PVR client id for this type.
+ * @return The PVR client id.
+ */
+ int GetClientId() const { return m_iClientId; }
+
+ /*!
+ * @brief Get the numeric type id of this type.
+ * @return The type id.
+ */
+ unsigned int GetTypeId() const { return m_iTypeId; }
+
+ /*!
+ * @brief Get the plain text (UI) description of this type.
+ * @return The description.
+ */
+ const std::string& GetDescription() const { return m_strDescription; }
+
+ /*!
+ * @brief Get the attributes of this type.
+ * @return The attributes.
+ */
+ uint64_t GetAttributes() const { return m_iAttributes; }
+
+ /*!
+ * @brief Check whether this type is for timer rules or one time timers.
+ * @return True if type represents a timer rule, false otherwise.
+ */
+ bool IsTimerRule() const { return (m_iAttributes & PVR_TIMER_TYPE_IS_REPEATING) > 0; }
+
+ /*!
+ * @brief Check whether this type is for reminder timers or recording timers.
+ * @return True if type represents a reminder timer, false otherwise.
+ */
+ bool IsReminder() const { return (m_iAttributes & PVR_TIMER_TYPE_IS_REMINDER) > 0; }
+
+ /*!
+ * @brief Check whether this type is for timer rules or one time timers.
+ * @return True if type represents a one time timer, false otherwise.
+ */
+ bool IsOnetime() const { return !IsTimerRule(); }
+
+ /*!
+ * @brief Check whether this type is for epg-based or manual timers.
+ * @return True if manual, false otherwise.
+ */
+ bool IsManual() const { return (m_iAttributes & PVR_TIMER_TYPE_IS_MANUAL) > 0; }
+
+ /*!
+ * @brief Check whether this type is for epg-based or manual timers.
+ * @return True if epg-based, false otherwise.
+ */
+ bool IsEpgBased() const { return !IsManual(); }
+
+ /*!
+ * @brief Check whether this type is for epg-based timer rules.
+ * @return True if epg-based timer rule, false otherwise.
+ */
+ bool IsEpgBasedTimerRule() const { return IsEpgBased() && IsTimerRule(); }
+
+ /*!
+ * @brief Check whether this type is for one time epg-based timers.
+ * @return True if one time epg-based, false otherwise.
+ */
+ bool IsEpgBasedOnetime() const { return IsEpgBased() && IsOnetime(); }
+
+ /*!
+ * @brief Check whether this type is for manual timer rules.
+ * @return True if manual timer rule, false otherwise.
+ */
+ bool IsManualTimerRule() const { return IsManual() && IsTimerRule(); }
+
+ /*!
+ * @brief Check whether this type is for one time manual timers.
+ * @return True if one time manual, false otherwise.
+ */
+ bool IsManualOnetime() const { return IsManual() && IsOnetime(); }
+
+ /*!
+ * @brief Check whether this type is readonly (must not be modified after initial creation).
+ * @return True if readonly, false otherwise.
+ */
+ bool IsReadOnly() const { return (m_iAttributes & PVR_TIMER_TYPE_IS_READONLY) > 0; }
+
+ /*!
+ * @brief Check whether this type allows deletion.
+ * @return True if type allows deletion, false otherwise.
+ */
+ bool AllowsDelete() const { return !IsReadOnly() || SupportsReadOnlyDelete(); }
+
+ /*!
+ * @brief Check whether this type forbids creation of new timers of this type.
+ * @return True if new instances are forbidden, false otherwise.
+ */
+ bool ForbidsNewInstances() const { return (m_iAttributes & PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES) > 0; }
+
+ /*!
+ * @brief Check whether this timer type is forbidden when epg tag info is present.
+ * @return True if new instances are forbidden when epg info is present, false otherwise.
+ */
+ bool ForbidsEpgTagOnCreate() const { return (m_iAttributes & PVR_TIMER_TYPE_FORBIDS_EPG_TAG_ON_CREATE) > 0; }
+
+ /*!
+ * @brief Check whether this timer type requires epg tag info to be present.
+ * @return True if new instances require EPG info, false otherwise.
+ */
+ bool RequiresEpgTagOnCreate() const { return (m_iAttributes & (PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE |
+ PVR_TIMER_TYPE_REQUIRES_EPG_SERIES_ON_CREATE |
+ PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE)) > 0; }
+
+ /*!
+ * @brief Check whether this timer type requires epg tag info including series attributes to be present.
+ * @return True if new instances require an EPG tag with series attributes, false otherwise.
+ */
+ bool RequiresEpgSeriesOnCreate() const { return (m_iAttributes & PVR_TIMER_TYPE_REQUIRES_EPG_SERIES_ON_CREATE) > 0; }
+
+ /*!
+ * @brief Check whether this timer type requires epg tag info including a series link to be present.
+ * @return True if new instances require an EPG tag with a series link, false otherwise.
+ */
+ bool RequiresEpgSeriesLinkOnCreate() const { return (m_iAttributes & PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE) > 0; }
+
+ /*!
+ * @brief Check whether this type supports the "enabling/disabling" of timers of its type.
+ * @return True if "enabling/disabling" feature is supported, false otherwise.
+ */
+ bool SupportsEnableDisable() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE) > 0; }
+
+ /*!
+ * @brief Check whether this type supports channels.
+ * @return True if channels are supported, false otherwise.
+ */
+ bool SupportsChannels() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_CHANNELS) > 0; }
+
+ /*!
+ * @brief Check whether this type supports start time.
+ * @return True if start time values are supported, false otherwise.
+ */
+ bool SupportsStartTime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_TIME) > 0; }
+
+ /*!
+ * @brief Check whether this type supports end time.
+ * @return True if end time values are supported, false otherwise.
+ */
+ bool SupportsEndTime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_END_TIME) > 0; }
+ /*!
+ * @brief Check whether this type supports start any time.
+ * @return True if start any time is supported, false otherwise.
+ */
+ bool SupportsStartAnyTime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_ANYTIME) > 0; }
+
+ /*!
+ * @brief Check whether this type supports end any time.
+ * @return True if end any time is supported, false otherwise.
+ */
+ bool SupportsEndAnyTime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_END_ANYTIME) > 0; }
+
+ /*!
+ * @brief Check whether this type supports matching a search string against epg episode title.
+ * @return True if title matching is supported, false otherwise.
+ */
+ bool SupportsEpgTitleMatch() const { return (m_iAttributes & (PVR_TIMER_TYPE_SUPPORTS_TITLE_EPG_MATCH | PVR_TIMER_TYPE_SUPPORTS_FULLTEXT_EPG_MATCH)) > 0; }
+
+ /*!
+ * @brief Check whether this type supports matching a search string against extended (fulltext) epg data. This
+ includes title matching.
+ * @return True if fulltext matching is supported, false otherwise.
+ */
+ bool SupportsEpgFulltextMatch() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_FULLTEXT_EPG_MATCH) > 0; }
+
+ /*!
+ * @brief Check whether this type supports a first day the timer is active.
+ * @return True if first day is supported, false otherwise.
+ */
+ bool SupportsFirstDay() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY) > 0; }
+
+ /*!
+ * @brief Check whether this type supports weekdays for timer schedules.
+ * @return True if weekdays are supported, false otherwise.
+ */
+ bool SupportsWeekdays() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS) > 0; }
+
+ /*!
+ * @brief Check whether this type supports the "record only new episodes" feature.
+ * @return True if the "record only new episodes" feature is supported, false otherwise.
+ */
+ bool SupportsRecordOnlyNewEpisodes() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_RECORD_ONLY_NEW_EPISODES) > 0; }
+
+ /*!
+ * @brief Check whether this type supports pre record time.
+ * @return True if pre record time is supported, false otherwise.
+ */
+ bool SupportsStartMargin() const
+ {
+ return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_MARGIN) > 0 ||
+ (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_END_MARGIN) > 0;
+ }
+
+ /*!
+ * @brief Check whether this type supports post record time.
+ * @return True if post record time is supported, false otherwise.
+ */
+ bool SupportsEndMargin() const
+ {
+ return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_END_MARGIN) > 0 ||
+ (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_END_MARGIN) > 0;
+ }
+
+ /*!
+ * @brief Check whether this type supports recording priorities.
+ * @return True if recording priority is supported, false otherwise.
+ */
+ bool SupportsPriority() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_PRIORITY) > 0; }
+
+ /*!
+ * @brief Check whether this type supports lifetime for recordings.
+ * @return True if recording lifetime is supported, false otherwise.
+ */
+ bool SupportsLifetime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_LIFETIME) > 0; }
+
+ /*!
+ * @brief Check whether this type supports MaxRecordings for recordings.
+ * @return True if MaxRecordings is supported, false otherwise.
+ */
+ bool SupportsMaxRecordings() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_MAX_RECORDINGS) > 0; }
+
+ /*!
+ * @brief Check whether this type supports user specified recording folders.
+ * @return True if recording folders are supported, false otherwise.
+ */
+ bool SupportsRecordingFolders() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS) > 0; }
+
+ /*!
+ * @brief Check whether this type supports recording groups.
+ * @return True if recording groups are supported, false otherwise.
+ */
+ bool SupportsRecordingGroup() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_RECORDING_GROUP) > 0; }
+
+ /*!
+ * @brief Check whether this type supports 'any channel', for example for defining a timer rule that should match any channel instead of a particular channel.
+ * @return True if any channel is supported, false otherwise.
+ */
+ bool SupportsAnyChannel() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_ANY_CHANNEL) > 0; }
+
+ /*!
+ * @brief Check whether this type supports deletion of an otherwise read-only timer.
+ * @return True if read-only deletion is supported, false otherwise.
+ */
+ bool SupportsReadOnlyDelete() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_READONLY_DELETE) > 0; }
+
+ /*!
+ * @brief Obtain a list with all possible values for the priority attribute.
+ * @param list out, the list with the values or an empty list, if priority is not supported by this type.
+ */
+ void GetPriorityValues(std::vector<std::pair<std::string, int>>& list) const;
+
+ /*!
+ * @brief Obtain the default value for the priority attribute.
+ * @return the default value.
+ */
+ int GetPriorityDefault() const { return m_iPriorityDefault; }
+
+ /*!
+ * @brief Obtain a list with all possible values for the lifetime attribute.
+ * @param list out, the list with the values or an empty list, if lifetime is not supported by this type.
+ */
+ void GetLifetimeValues(std::vector<std::pair<std::string, int>>& list) const;
+
+ /*!
+ * @brief Obtain the default value for the lifetime attribute.
+ * @return the default value.
+ */
+ int GetLifetimeDefault() const { return m_iLifetimeDefault; }
+
+ /*!
+ * @brief Obtain a list with all possible values for the MaxRecordings attribute.
+ * @param list out, the list with the values or an empty list, if MaxRecordings is not supported by this type.
+ */
+ void GetMaxRecordingsValues(std::vector<std::pair<std::string, int>>& list) const;
+
+ /*!
+ * @brief Obtain the default value for the MaxRecordings attribute.
+ * @return the default value.
+ */
+ int GetMaxRecordingsDefault() const { return m_iMaxRecordingsDefault; }
+
+ /*!
+ * @brief Obtain a list with all possible values for the duplicate episode prevention attribute.
+ * @param list out, the list with the values or an empty list, if duplicate episode prevention is not supported by this type.
+ */
+ void GetPreventDuplicateEpisodesValues(std::vector<std::pair<std::string, int>>& list) const;
+
+ /*!
+ * @brief Obtain the default value for the duplicate episode prevention attribute.
+ * @return the default value.
+ */
+ int GetPreventDuplicateEpisodesDefault() const { return m_iPreventDupEpisodesDefault; }
+
+ /*!
+ * @brief Obtain a list with all possible values for the recording group attribute.
+ * @param list out, the list with the values or an empty list, if recording group is not supported by this type.
+ */
+ void GetRecordingGroupValues(std::vector<std::pair<std::string, int>>& list) const;
+
+ /*!
+ * @brief Obtain the default value for the Recording Group attribute.
+ * @return the default value.
+ */
+ int GetRecordingGroupDefault() const { return m_iRecordingGroupDefault; }
+
+ private:
+ void InitDescription();
+ void InitAttributeValues(const PVR_TIMER_TYPE& type);
+ void InitPriorityValues(const PVR_TIMER_TYPE& type);
+ void InitLifetimeValues(const PVR_TIMER_TYPE& type);
+ void InitMaxRecordingsValues(const PVR_TIMER_TYPE& type);
+ void InitPreventDuplicateEpisodesValues(const PVR_TIMER_TYPE& type);
+ void InitRecordingGroupValues(const PVR_TIMER_TYPE& type);
+
+ int m_iClientId = -1;
+ unsigned int m_iTypeId;
+ uint64_t m_iAttributes;
+ std::string m_strDescription;
+ std::vector< std::pair<std::string, int> > m_priorityValues;
+ int m_iPriorityDefault = DEFAULT_RECORDING_PRIORITY;
+ std::vector< std::pair<std::string, int> > m_lifetimeValues;
+ int m_iLifetimeDefault = DEFAULT_RECORDING_LIFETIME;
+ std::vector< std::pair<std::string, int> > m_maxRecordingsValues;
+ int m_iMaxRecordingsDefault = 0;
+ std::vector< std::pair<std::string, int> > m_preventDupEpisodesValues;
+ unsigned int m_iPreventDupEpisodesDefault = DEFAULT_RECORDING_DUPLICATEHANDLING;
+ std::vector< std::pair<std::string, int> > m_recordingGroupValues;
+ unsigned int m_iRecordingGroupDefault = 0;
+ };
+}
diff --git a/xbmc/pvr/timers/PVRTimers.cpp b/xbmc/pvr/timers/PVRTimers.cpp
new file mode 100644
index 0000000..760bb19
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimers.cpp
@@ -0,0 +1,1338 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRTimers.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVREventLogJob.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimerRuleMatcher.h"
+#include "settings/Settings.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+using namespace std::chrono_literals;
+
+namespace
+{
+constexpr auto MAX_NOTIFICATION_DELAY = 10s;
+}
+
+bool CPVRTimersContainer::UpdateFromClient(const std::shared_ptr<CPVRTimerInfoTag>& timer)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::shared_ptr<CPVRTimerInfoTag> tag = GetByClient(timer->ClientID(), timer->ClientIndex());
+ if (tag)
+ {
+ return tag->UpdateEntry(timer);
+ }
+ else
+ {
+ timer->SetTimerID(++m_iLastId);
+ InsertEntry(timer);
+ }
+
+ return true;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimersContainer::GetByClient(int iClientId,
+ int iClientIndex) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& startDates : m_tags)
+ {
+ const auto it = std::find_if(startDates.second.cbegin(), startDates.second.cend(),
+ [iClientId, iClientIndex](const auto& timer) {
+ return timer->ClientID() == iClientId &&
+ timer->ClientIndex() == iClientIndex;
+ });
+ if (it != startDates.second.cend())
+ return (*it);
+ }
+
+ return {};
+}
+
+void CPVRTimersContainer::InsertEntry(const std::shared_ptr<CPVRTimerInfoTag>& newTimer)
+{
+ auto it = m_tags.find(newTimer->IsStartAnyTime() ? CDateTime() : newTimer->StartAsUTC());
+ if (it == m_tags.end())
+ {
+ VecTimerInfoTag addEntry({newTimer});
+ m_tags.insert(std::make_pair(newTimer->IsStartAnyTime() ? CDateTime() : newTimer->StartAsUTC(),
+ addEntry));
+ }
+ else
+ {
+ it->second.emplace_back(newTimer);
+ }
+}
+
+CPVRTimers::CPVRTimers()
+ : CThread("PVRTimers"),
+ m_settings({CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUP,
+ CSettings::SETTING_PVRPOWERMANAGEMENT_PREWAKEUP,
+ CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME,
+ CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME,
+ CSettings::SETTING_PVRRECORD_TIMERNOTIFICATIONS})
+{
+}
+
+bool CPVRTimers::Update(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ return LoadFromDatabase(clients) && UpdateFromClients(clients);
+}
+
+bool CPVRTimers::LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ // load local timers from database
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ {
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>> timers =
+ database->GetTimers(*this, clients);
+
+ if (std::accumulate(timers.cbegin(), timers.cend(), false,
+ [this](bool changed, const auto& timer) {
+ return (UpdateEntry(timer) != nullptr) ? true : changed;
+ }))
+ NotifyTimersEvent();
+ }
+
+ // ensure that every timer has its channel set
+ UpdateChannels();
+ return true;
+}
+
+void CPVRTimers::Unload()
+{
+ // remove all tags
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_tags.clear();
+}
+
+void CPVRTimers::Start()
+{
+ Stop();
+
+ CServiceBroker::GetPVRManager().Events().Subscribe(this, &CPVRTimers::Notify);
+ Create();
+}
+
+void CPVRTimers::Stop()
+{
+ StopThread();
+ CServiceBroker::GetPVRManager().Events().Unsubscribe(this);
+}
+
+bool CPVRTimers::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bIsUpdating)
+ return false;
+ m_bIsUpdating = true;
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updating timers");
+ CPVRTimersContainer newTimerList;
+ std::vector<int> failedClients;
+ CServiceBroker::GetPVRManager().Clients()->GetTimers(clients, &newTimerList, failedClients);
+ return UpdateEntries(newTimerList, failedClients);
+}
+
+void CPVRTimers::Process()
+{
+ while (!m_bStop)
+ {
+ // update all timers not owned by a client (e.g. reminders)
+ UpdateEntries(MAX_NOTIFICATION_DELAY.count());
+
+ CThread::Sleep(MAX_NOTIFICATION_DELAY);
+ }
+}
+
+bool CPVRTimers::IsRecording() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ if (std::any_of(tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [](const auto& timersEntry) { return timersEntry->IsRecording(); }))
+ return true;
+ }
+
+ return false;
+}
+
+void CPVRTimers::RemoveEntry(const std::shared_ptr<CPVRTimerInfoTag>& tag)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ auto it = m_tags.find(tag->IsStartAnyTime() ? CDateTime() : tag->StartAsUTC());
+ if (it != m_tags.end())
+ {
+ it->second.erase(std::remove_if(it->second.begin(), it->second.end(),
+ [&tag](const std::shared_ptr<CPVRTimerInfoTag>& timer) {
+ return tag->ClientID() == timer->ClientID() &&
+ tag->ClientIndex() == timer->ClientIndex();
+ }),
+ it->second.end());
+
+ if (it->second.empty())
+ m_tags.erase(it);
+ }
+}
+
+bool CPVRTimers::CheckAndAppendTimerNotification(
+ std::vector<std::pair<int, std::string>>& timerNotifications,
+ const std::shared_ptr<CPVRTimerInfoTag>& tag,
+ bool bDeleted) const
+{
+ // no notification on first update or if previous update failed for tag's client.
+ if (!m_bFirstUpdate && std::find(m_failedClients.cbegin(), m_failedClients.cend(),
+ tag->ClientID()) == m_failedClients.cend())
+ {
+ const std::string strMessage =
+ bDeleted ? tag->GetDeletedNotificationText() : tag->GetNotificationText();
+ timerNotifications.emplace_back(std::make_pair(tag->ClientID(), strMessage));
+ return true;
+ }
+ return false;
+}
+
+bool CPVRTimers::UpdateEntries(const CPVRTimersContainer& timers,
+ const std::vector<int>& failedClients)
+{
+ bool bChanged(false);
+ bool bAddedOrDeleted(false);
+ std::vector<std::pair<int, std::string>> timerNotifications;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* go through the timer list and check for updated or new timers */
+ for (const auto& tagsEntry : timers.GetTags())
+ {
+ for (const auto& timersEntry : tagsEntry.second)
+ {
+ /* check if this timer is present in this container */
+ const std::shared_ptr<CPVRTimerInfoTag> existingTimer =
+ GetByClient(timersEntry->ClientID(), timersEntry->ClientIndex());
+ if (existingTimer)
+ {
+ /* if it's present, update the current tag */
+ bool bStateChanged(existingTimer->State() != timersEntry->State());
+ if (existingTimer->UpdateEntry(timersEntry))
+ {
+ bChanged = true;
+ existingTimer->ResetChildState();
+
+ if (bStateChanged)
+ CheckAndAppendTimerNotification(timerNotifications, existingTimer, false);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updated timer {} on client {}", timersEntry->ClientIndex(),
+ timersEntry->ClientID());
+ }
+ }
+ else
+ {
+ /* new timer */
+ std::shared_ptr<CPVRTimerInfoTag> newTimer =
+ std::shared_ptr<CPVRTimerInfoTag>(new CPVRTimerInfoTag);
+ newTimer->UpdateEntry(timersEntry);
+ newTimer->SetTimerID(++m_iLastId);
+ InsertEntry(newTimer);
+
+ bChanged = true;
+ bAddedOrDeleted = true;
+
+ CheckAndAppendTimerNotification(timerNotifications, newTimer, false);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Added timer {} on client {}", timersEntry->ClientIndex(),
+ timersEntry->ClientID());
+ }
+ }
+ }
+
+ /* to collect timer with changed starting time */
+ VecTimerInfoTag timersToMove;
+
+ /* check for deleted timers */
+ for (auto it = m_tags.begin(); it != m_tags.end();)
+ {
+ for (auto it2 = it->second.begin(); it2 != it->second.end();)
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = *it2;
+ if (!timers.GetByClient(timer->ClientID(), timer->ClientIndex()))
+ {
+ /* timer was not found */
+ bool bIgnoreTimer = !timer->IsOwnedByClient();
+ if (!bIgnoreTimer)
+ {
+ bIgnoreTimer = std::any_of(
+ failedClients.cbegin(), failedClients.cend(),
+ [&timer](const auto& failedClient) { return failedClient == timer->ClientID(); });
+ }
+
+ if (bIgnoreTimer)
+ {
+ ++it2;
+ continue;
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleted timer {} on client {}", timer->ClientIndex(),
+ timer->ClientID());
+
+ CheckAndAppendTimerNotification(timerNotifications, timer, true);
+
+ it2 = it->second.erase(it2);
+
+ bChanged = true;
+ bAddedOrDeleted = true;
+ }
+ else if ((timer->IsStartAnyTime() && it->first != CDateTime()) ||
+ (!timer->IsStartAnyTime() && timer->StartAsUTC() != it->first))
+ {
+ /* timer start has changed */
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Changed start time timer {} on client {}",
+ timer->ClientIndex(), timer->ClientID());
+
+ /* remember timer */
+ timersToMove.push_back(timer);
+
+ /* remove timer for now, reinsert later */
+ it2 = it->second.erase(it2);
+
+ bChanged = true;
+ bAddedOrDeleted = true;
+ }
+ else
+ {
+ ++it2;
+ }
+ }
+ if (it->second.empty())
+ it = m_tags.erase(it);
+ else
+ ++it;
+ }
+
+ /* reinsert timers with changed timer start */
+ for (const auto& timer : timersToMove)
+ {
+ InsertEntry(timer);
+ }
+
+ /* update child information for all parent timers */
+ for (const auto& tagsEntry : m_tags)
+ {
+ for (const auto& timersEntry : tagsEntry.second)
+ {
+ if (timersEntry->IsTimerRule())
+ timersEntry->ResetChildState();
+ }
+
+ for (const auto& timersEntry : tagsEntry.second)
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> parentTimer = GetTimerRule(timersEntry);
+ if (parentTimer)
+ parentTimer->UpdateChildState(timersEntry, true);
+ }
+ }
+
+ m_failedClients = failedClients;
+ m_bFirstUpdate = false;
+ m_bIsUpdating = false;
+
+ if (bChanged)
+ {
+ UpdateChannels();
+ lock.unlock();
+
+ NotifyTimersEvent(bAddedOrDeleted);
+
+ if (!timerNotifications.empty())
+ {
+ CPVREventLogJob* job = new CPVREventLogJob;
+
+ /* queue notifications / fill eventlog */
+ for (const auto& entry : timerNotifications)
+ {
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(entry.first);
+ if (client)
+ {
+ job->AddEvent(m_settings.GetBoolValue(CSettings::SETTING_PVRRECORD_TIMERNOTIFICATIONS),
+ EventLevel::Information, // info, no error
+ client->GetFriendlyName(), entry.second, client->Icon());
+ }
+ }
+
+ CServiceBroker::GetJobManager()->AddJob(job, nullptr);
+ }
+ }
+
+ return true;
+}
+
+namespace
+{
+std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTagsForTimerRule(
+ const CPVRTimerRuleMatcher& matcher)
+{
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> matches;
+
+ const std::shared_ptr<CPVRChannel> channel = matcher.GetChannel();
+ if (channel)
+ {
+ // match single channel
+ const std::shared_ptr<CPVREpg> epg = channel->GetEPG();
+ if (epg)
+ {
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags = epg->GetTags();
+ std::copy_if(tags.cbegin(), tags.cend(), std::back_inserter(matches),
+ [&matcher](const auto& tag) { return matcher.Matches(tag); });
+ }
+ }
+ else
+ {
+ // match any channel
+ const std::vector<std::shared_ptr<CPVREpg>> epgs =
+ CServiceBroker::GetPVRManager().EpgContainer().GetAllEpgs();
+
+ for (const auto& epg : epgs)
+ {
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags = epg->GetTags();
+ std::copy_if(tags.cbegin(), tags.cend(), std::back_inserter(matches),
+ [&matcher](const auto& tag) { return matcher.Matches(tag); });
+ }
+ }
+
+ return matches;
+}
+
+void AddTimerRuleToEpgMap(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer,
+ const CDateTime& now,
+ std::map<std::shared_ptr<CPVREpg>, std::vector<std::shared_ptr<CPVRTimerRuleMatcher>>>& epgMap,
+ bool& bFetchedAllEpgs)
+{
+ const std::shared_ptr<CPVRChannel> channel = timer->Channel();
+ if (channel)
+ {
+ const std::shared_ptr<CPVREpg> epg = channel->GetEPG();
+ if (epg)
+ {
+ const std::shared_ptr<CPVRTimerRuleMatcher> matcher =
+ std::make_shared<CPVRTimerRuleMatcher>(timer, now);
+ auto it = epgMap.find(epg);
+ if (it == epgMap.end())
+ epgMap.insert({epg, {matcher}});
+ else
+ it->second.emplace_back(matcher);
+ }
+ }
+ else
+ {
+ // rule matches "any channel" => we need to check all channels
+ if (!bFetchedAllEpgs)
+ {
+ const std::vector<std::shared_ptr<CPVREpg>> epgs =
+ CServiceBroker::GetPVRManager().EpgContainer().GetAllEpgs();
+ for (const auto& epg : epgs)
+ {
+ const std::shared_ptr<CPVRTimerRuleMatcher> matcher =
+ std::make_shared<CPVRTimerRuleMatcher>(timer, now);
+ auto it = epgMap.find(epg);
+ if (it == epgMap.end())
+ epgMap.insert({epg, {matcher}});
+ else
+ it->second.emplace_back(matcher);
+ }
+ bFetchedAllEpgs = true;
+ }
+ else
+ {
+ for (auto& epgMapEntry : epgMap)
+ {
+ const std::shared_ptr<CPVRTimerRuleMatcher> matcher =
+ std::make_shared<CPVRTimerRuleMatcher>(timer, now);
+ epgMapEntry.second.emplace_back(matcher);
+ }
+ }
+ }
+}
+} // unnamed namespace
+
+bool CPVRTimers::UpdateEntries(int iMaxNotificationDelay)
+{
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> timersToReinsert;
+ std::vector<std::pair<std::shared_ptr<CPVRTimerInfoTag>, std::shared_ptr<CPVRTimerInfoTag>>>
+ childTimersToInsert;
+ bool bChanged = false;
+ const CDateTime now = CDateTime::GetUTCDateTime();
+ bool bFetchedAllEpgs = false;
+ std::map<std::shared_ptr<CPVREpg>, std::vector<std::shared_ptr<CPVRTimerRuleMatcher>>> epgMap;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (auto it = m_tags.begin(); it != m_tags.end();)
+ {
+ for (auto it2 = it->second.begin(); it2 != it->second.end();)
+ {
+ std::shared_ptr<CPVRTimerInfoTag> timer = *it2;
+ bool bDeleteTimer = false;
+ if (!timer->IsOwnedByClient())
+ {
+ if (timer->IsEpgBased())
+ {
+ // update epg tag
+ const std::shared_ptr<CPVREpg> epg =
+ CServiceBroker::GetPVRManager().EpgContainer().GetByChannelUid(
+ timer->Channel()->ClientID(), timer->Channel()->UniqueID());
+ if (epg)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ epg->GetTagByBroadcastId(timer->UniqueBroadcastID());
+ if (epgTag)
+ {
+ timer->SetEpgInfoTag(epgTag);
+
+ bool bStartChanged =
+ !timer->IsStartAnyTime() && epgTag->StartAsUTC() != timer->StartAsUTC();
+ bool bEndChanged = !timer->IsEndAnyTime() && epgTag->EndAsUTC() != timer->EndAsUTC();
+ if (bStartChanged || bEndChanged)
+ {
+ if (bStartChanged)
+ timer->SetStartFromUTC(epgTag->StartAsUTC());
+ if (bEndChanged)
+ timer->SetEndFromUTC(epgTag->EndAsUTC());
+
+ timer->UpdateSummary();
+ bChanged = true;
+
+ if (bStartChanged)
+ {
+ // start time changed. timer must be reinserted in timer map
+ bDeleteTimer = true;
+ timersToReinsert.emplace_back(timer); // remember and reinsert/save later
+ }
+ else
+ {
+ // save changes to database
+ timer->Persist();
+ }
+ }
+ }
+ }
+ }
+
+ // check for due timers and announce/delete them
+ int iMarginStart = timer->GetTimerType()->SupportsStartMargin() ? timer->MarginStart() : 0;
+ if (!timer->IsTimerRule() &&
+ (timer->StartAsUTC() - CDateTimeSpan(0, 0, iMarginStart, iMaxNotificationDelay)) < now)
+ {
+ if (timer->IsReminder() && !timer->IsDisabled())
+ {
+ // reminder is due / over due. announce it.
+ m_remindersToAnnounce.push(timer);
+ }
+
+ if (timer->EndAsUTC() >= now)
+ {
+ // disable timer until timer's end time is due
+ if (!timer->IsDisabled())
+ {
+ timer->SetState(PVR_TIMER_STATE_DISABLED);
+ bChanged = true;
+ }
+ }
+ else
+ {
+ // end time due. delete completed timer
+ bChanged = true;
+ bDeleteTimer = true;
+ timer->DeleteFromDatabase();
+ }
+ }
+
+ if (timer->IsTimerRule() && timer->IsReminder() && timer->IsActive())
+ {
+ if (timer->IsEpgBased())
+ {
+ if (m_bReminderRulesUpdatePending)
+ AddTimerRuleToEpgMap(timer, now, epgMap, bFetchedAllEpgs);
+ }
+ else
+ {
+ // create new children of time-based reminder timer rules
+ const CPVRTimerRuleMatcher matcher(timer, now);
+ const CDateTime nextStart = matcher.GetNextTimerStart();
+ if (nextStart.IsValid())
+ {
+ bool bCreate = false;
+ const auto it1 = m_tags.find(nextStart);
+ if (it1 == m_tags.end())
+ bCreate = true;
+ else
+ bCreate = std::none_of(it1->second.cbegin(), it1->second.cend(),
+ [&timer](const std::shared_ptr<CPVRTimerInfoTag>& tmr) {
+ return tmr->ParentClientIndex() == timer->ClientIndex();
+ });
+ if (bCreate)
+ {
+ const CDateTimeSpan duration = timer->EndAsUTC() - timer->StartAsUTC();
+ const std::shared_ptr<CPVRTimerInfoTag> childTimer =
+ CPVRTimerInfoTag::CreateReminderFromDate(
+ nextStart, duration.GetSecondsTotal() / 60, timer);
+ if (childTimer)
+ {
+ bChanged = true;
+ childTimersToInsert.emplace_back(
+ std::make_pair(timer, childTimer)); // remember and insert/save later
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (bDeleteTimer)
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> parent = GetTimerRule(timer);
+ if (parent)
+ parent->UpdateChildState(timer, false);
+
+ it2 = it->second.erase(it2);
+ }
+ else
+ {
+ ++it2;
+ }
+ }
+
+ if (it->second.empty())
+ it = m_tags.erase(it);
+ else
+ ++it;
+ }
+
+ // create new children of local epg-based reminder timer rules
+ for (const auto& epgMapEntry : epgMap)
+ {
+ const auto epgTags = epgMapEntry.first->GetTags();
+ for (const auto& epgTag : epgTags)
+ {
+ if (GetTimerForEpgTag(epgTag))
+ continue;
+
+ for (const auto& matcher : epgMapEntry.second)
+ {
+ if (!matcher->Matches(epgTag))
+ continue;
+
+ const std::shared_ptr<CPVRTimerInfoTag> childTimer =
+ CPVRTimerInfoTag::CreateReminderFromEpg(epgTag, matcher->GetTimerRule());
+ if (childTimer)
+ {
+ bChanged = true;
+ childTimersToInsert.emplace_back(std::make_pair(
+ matcher->GetTimerRule(), childTimer)); // remember and insert/save later
+ }
+ }
+ }
+ }
+
+ // reinsert timers with changed timer start
+ for (const auto& timer : timersToReinsert)
+ {
+ InsertEntry(timer);
+ timer->Persist();
+ }
+
+ // insert new children of time-based local timer rules
+ for (const auto& timerPair : childTimersToInsert)
+ {
+ PersistAndUpdateLocalTimer(timerPair.second, timerPair.first);
+ }
+
+ m_bReminderRulesUpdatePending = false;
+
+ // announce changes
+ if (bChanged)
+ NotifyTimersEvent();
+
+ if (!m_remindersToAnnounce.empty())
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::AnnounceReminder);
+
+ return bChanged;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextReminderToAnnnounce()
+{
+ std::shared_ptr<CPVRTimerInfoTag> ret;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_remindersToAnnounce.empty())
+ {
+ ret = m_remindersToAnnounce.front();
+ m_remindersToAnnounce.pop();
+ }
+ return ret;
+}
+
+bool CPVRTimers::KindMatchesTag(const TimerKind& eKind,
+ const std::shared_ptr<CPVRTimerInfoTag>& tag) const
+{
+ return (eKind == TimerKindAny) || (eKind == TimerKindTV && !tag->IsRadio()) ||
+ (eKind == TimerKindRadio && tag->IsRadio());
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextActiveTimer(const TimerKind& eKind,
+ bool bIgnoreReminders) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ for (const auto& timersEntry : tagsEntry.second)
+ {
+ if (bIgnoreReminders && timersEntry->IsReminder())
+ continue;
+
+ if (KindMatchesTag(eKind, timersEntry) && timersEntry->IsActive() &&
+ !timersEntry->IsRecording() && !timersEntry->IsTimerRule() && !timersEntry->IsBroken())
+ return timersEntry;
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextActiveTimer(
+ bool bIgnoreReminders /* = true */) const
+{
+ return GetNextActiveTimer(TimerKindAny, bIgnoreReminders);
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextActiveTVTimer() const
+{
+ return GetNextActiveTimer(TimerKindTV, true);
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextActiveRadioTimer() const
+{
+ return GetNextActiveTimer(TimerKindRadio, true);
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveTimers() const
+{
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> tags;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ std::copy_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(), std::back_inserter(tags),
+ [](const auto& timersEntry) {
+ return timersEntry->IsActive() && !timersEntry->IsBroken() &&
+ !timersEntry->IsReminder() && !timersEntry->IsTimerRule();
+ });
+ }
+
+ return tags;
+}
+
+int CPVRTimers::AmountActiveTimers(const TimerKind& eKind) const
+{
+ int iReturn = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ iReturn += std::count_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [this, &eKind](const auto& timersEntry) {
+ return KindMatchesTag(eKind, timersEntry) &&
+ timersEntry->IsActive() && !timersEntry->IsBroken() &&
+ !timersEntry->IsReminder() && !timersEntry->IsTimerRule();
+ });
+ }
+
+ return iReturn;
+}
+
+int CPVRTimers::AmountActiveTimers() const
+{
+ return AmountActiveTimers(TimerKindAny);
+}
+
+int CPVRTimers::AmountActiveTVTimers() const
+{
+ return AmountActiveTimers(TimerKindTV);
+}
+
+int CPVRTimers::AmountActiveRadioTimers() const
+{
+ return AmountActiveTimers(TimerKindRadio);
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveRecordings(
+ const TimerKind& eKind) const
+{
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> tags;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ std::copy_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(), std::back_inserter(tags),
+ [this, &eKind](const auto& timersEntry) {
+ return KindMatchesTag(eKind, timersEntry) && timersEntry->IsRecording() &&
+ !timersEntry->IsTimerRule() && !timersEntry->IsBroken() &&
+ !timersEntry->IsReminder();
+ });
+ }
+
+ return tags;
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveRecordings() const
+{
+ return GetActiveRecordings(TimerKindAny);
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveTVRecordings() const
+{
+ return GetActiveRecordings(TimerKindTV);
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveRadioRecordings() const
+{
+ return GetActiveRecordings(TimerKindRadio);
+}
+
+int CPVRTimers::AmountActiveRecordings(const TimerKind& eKind) const
+{
+ int iReturn = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ iReturn += std::count_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [this, &eKind](const auto& timersEntry) {
+ return KindMatchesTag(eKind, timersEntry) &&
+ timersEntry->IsRecording() && !timersEntry->IsTimerRule() &&
+ !timersEntry->IsBroken() && !timersEntry->IsReminder();
+ });
+ }
+
+ return iReturn;
+}
+
+int CPVRTimers::AmountActiveRecordings() const
+{
+ return AmountActiveRecordings(TimerKindAny);
+}
+
+int CPVRTimers::AmountActiveTVRecordings() const
+{
+ return AmountActiveRecordings(TimerKindTV);
+}
+
+int CPVRTimers::AmountActiveRadioRecordings() const
+{
+ return AmountActiveRecordings(TimerKindRadio);
+}
+
+/********** channel methods **********/
+
+bool CPVRTimers::DeleteTimersOnChannel(const std::shared_ptr<CPVRChannel>& channel,
+ bool bDeleteTimerRules /* = true */,
+ bool bCurrentlyActiveOnly /* = false */)
+{
+ bool bReturn = false;
+ bool bChanged = false;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (MapTags::reverse_iterator it = m_tags.rbegin(); it != m_tags.rend(); ++it)
+ {
+ for (const auto& timersEntry : (*it).second)
+ {
+ bool bDeleteActiveItem = !bCurrentlyActiveOnly || timersEntry->IsRecording();
+ bool bDeleteTimerRuleItem = bDeleteTimerRules || !timersEntry->IsTimerRule();
+ bool bChannelsMatch = timersEntry->HasChannel() && timersEntry->Channel() == channel;
+
+ if (bDeleteActiveItem && bDeleteTimerRuleItem && bChannelsMatch)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleted timer {} on client {}", timersEntry->ClientIndex(),
+ timersEntry->ClientID());
+ bReturn = (timersEntry->DeleteFromClient(true) == TimerOperationResult::OK) || bReturn;
+ bChanged = true;
+ }
+ }
+ }
+ }
+
+ if (bChanged)
+ NotifyTimersEvent();
+
+ return bReturn;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::UpdateEntry(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer)
+{
+ bool bChanged = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::shared_ptr<CPVRTimerInfoTag> tag = GetByClient(timer->ClientID(), timer->ClientIndex());
+ if (tag)
+ {
+ bool bReinsert = tag->StartAsUTC() != timer->StartAsUTC();
+ if (bReinsert)
+ {
+ RemoveEntry(tag);
+ }
+
+ bChanged = tag->UpdateEntry(timer);
+
+ if (bReinsert)
+ {
+ InsertEntry(tag);
+ }
+ }
+ else
+ {
+ tag.reset(new CPVRTimerInfoTag());
+ if (tag->UpdateEntry(timer))
+ {
+ tag->SetTimerID(++m_iLastId);
+ InsertEntry(tag);
+ bChanged = true;
+ }
+ }
+
+ return bChanged ? tag : std::shared_ptr<CPVRTimerInfoTag>();
+}
+
+bool CPVRTimers::AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag)
+{
+ bool bReturn = false;
+ if (tag->IsOwnedByClient())
+ {
+ bReturn = tag->AddToClient();
+ }
+ else
+ {
+ bReturn = AddLocalTimer(tag, true);
+ }
+ return bReturn;
+}
+
+TimerOperationResult CPVRTimers::DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag,
+ bool bForce /* = false */,
+ bool bDeleteRule /* = false */)
+{
+ TimerOperationResult ret = TimerOperationResult::FAILED;
+ if (!tag)
+ return ret;
+
+ std::shared_ptr<CPVRTimerInfoTag> tagToDelete = tag;
+
+ if (bDeleteRule)
+ {
+ /* delete the timer rule that scheduled this timer. */
+ const std::shared_ptr<CPVRTimerInfoTag> ruleTag = GetTimerRule(tagToDelete);
+ if (!ruleTag)
+ {
+ CLog::LogF(LOGERROR, "Unable to obtain timer rule for given timer");
+ return ret;
+ }
+
+ tagToDelete = ruleTag;
+ }
+
+ if (tagToDelete->IsOwnedByClient())
+ {
+ ret = tagToDelete->DeleteFromClient(bForce);
+ }
+ else
+ {
+ if (DeleteLocalTimer(tagToDelete, true))
+ ret = TimerOperationResult::OK;
+ }
+
+ return ret;
+}
+
+bool CPVRTimers::UpdateTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag)
+{
+ bool bReturn = false;
+ if (tag->IsOwnedByClient())
+ {
+ bReturn = tag->UpdateOnClient();
+ }
+ else
+ {
+ bReturn = UpdateLocalTimer(tag);
+ }
+ return bReturn;
+}
+
+bool CPVRTimers::AddLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, bool bNotify)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::shared_ptr<CPVRTimerInfoTag> persistedTimer = PersistAndUpdateLocalTimer(tag, nullptr);
+ bool bReturn = !!persistedTimer;
+
+ if (bReturn && persistedTimer->IsTimerRule() && persistedTimer->IsActive())
+ {
+ if (persistedTimer->IsEpgBased())
+ {
+ // create and persist children of local epg-based timer rule
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> epgTags =
+ GetEpgTagsForTimerRule(CPVRTimerRuleMatcher(persistedTimer, CDateTime::GetUTCDateTime()));
+ for (const auto& epgTag : epgTags)
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> childTimer =
+ CPVRTimerInfoTag::CreateReminderFromEpg(epgTag, persistedTimer);
+ if (childTimer)
+ {
+ PersistAndUpdateLocalTimer(childTimer, persistedTimer);
+ }
+ }
+ }
+ else
+ {
+ // create and persist children of local time-based timer rule
+ const CDateTime nextStart =
+ CPVRTimerRuleMatcher(persistedTimer, CDateTime::GetUTCDateTime()).GetNextTimerStart();
+ if (nextStart.IsValid())
+ {
+ const CDateTimeSpan duration = persistedTimer->EndAsUTC() - persistedTimer->StartAsUTC();
+ const std::shared_ptr<CPVRTimerInfoTag> childTimer =
+ CPVRTimerInfoTag::CreateReminderFromDate(nextStart, duration.GetSecondsTotal() / 60,
+ persistedTimer);
+ if (childTimer)
+ {
+ PersistAndUpdateLocalTimer(childTimer, persistedTimer);
+ }
+ }
+ }
+ }
+
+ if (bNotify && bReturn)
+ {
+ lock.unlock();
+ NotifyTimersEvent();
+ }
+
+ return bReturn;
+}
+
+bool CPVRTimers::DeleteLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, bool bNotify)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ RemoveEntry(tag);
+
+ bool bReturn = tag->DeleteFromDatabase();
+
+ if (bReturn && tag->IsTimerRule())
+ {
+ // delete children of local timer rule
+ for (auto it = m_tags.begin(); it != m_tags.end();)
+ {
+ for (auto it2 = it->second.begin(); it2 != it->second.end();)
+ {
+ std::shared_ptr<CPVRTimerInfoTag> timer = *it2;
+ if (timer->ParentClientIndex() == tag->ClientIndex())
+ {
+ tag->UpdateChildState(timer, false);
+ it2 = it->second.erase(it2);
+ timer->DeleteFromDatabase();
+ }
+ else
+ {
+ ++it2;
+ }
+ }
+
+ if (it->second.empty())
+ it = m_tags.erase(it);
+ else
+ ++it;
+ }
+ }
+
+ if (bNotify && bReturn)
+ {
+ lock.unlock();
+ NotifyTimersEvent();
+ }
+
+ return bReturn;
+}
+
+bool CPVRTimers::UpdateLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag)
+{
+ // delete and re-create timer and children, if any.
+ bool bReturn = DeleteLocalTimer(tag, false);
+
+ if (bReturn)
+ bReturn = AddLocalTimer(tag, false);
+
+ if (bReturn)
+ NotifyTimersEvent();
+
+ return bReturn;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::PersistAndUpdateLocalTimer(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer,
+ const std::shared_ptr<CPVRTimerInfoTag>& parentTimer)
+{
+ std::shared_ptr<CPVRTimerInfoTag> tag;
+ bool bReturn = timer->Persist();
+ if (bReturn)
+ {
+ tag = UpdateEntry(timer);
+ if (tag && parentTimer)
+ parentTimer->UpdateChildState(timer, true);
+ }
+ return bReturn ? tag : std::shared_ptr<CPVRTimerInfoTag>();
+}
+
+bool CPVRTimers::IsRecordingOnChannel(const CPVRChannel& channel) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ if (std::any_of(tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [&channel](const auto& timersEntry) {
+ return timersEntry->IsRecording() &&
+ timersEntry->ClientChannelUID() == channel.UniqueID() &&
+ timersEntry->ClientID() == channel.ClientID();
+ }))
+ return true;
+ }
+
+ return false;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetActiveTimerForChannel(
+ const std::shared_ptr<CPVRChannel>& channel) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tagsEntry : m_tags)
+ {
+ const auto it = std::find_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [&channel](const auto& timersEntry) {
+ return timersEntry->IsRecording() &&
+ timersEntry->ClientChannelUID() == channel->UniqueID() &&
+ timersEntry->ClientID() == channel->ClientID();
+ });
+ if (it != tagsEntry.second.cend())
+ return (*it);
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetTimerForEpgTag(
+ const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (epgTag)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ for (const auto& timersEntry : tagsEntry.second)
+ {
+ if (timersEntry->IsTimerRule())
+ continue;
+
+ if (timersEntry->GetEpgInfoTag(false) == epgTag)
+ return timersEntry;
+
+ if (timersEntry->ClientChannelUID() != PVR_CHANNEL_INVALID_UID &&
+ timersEntry->ClientChannelUID() == epgTag->UniqueChannelID() &&
+ timersEntry->ClientID() == epgTag->ClientID())
+ {
+ if (timersEntry->UniqueBroadcastID() != EPG_TAG_INVALID_UID &&
+ timersEntry->UniqueBroadcastID() == epgTag->UniqueBroadcastID())
+ return timersEntry;
+
+ if (timersEntry->IsRadio() == epgTag->IsRadio() &&
+ timersEntry->StartAsUTC() <= epgTag->StartAsUTC() &&
+ timersEntry->EndAsUTC() >= epgTag->EndAsUTC())
+ return timersEntry;
+ }
+ }
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetTimerRule(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer) const
+{
+ if (timer)
+ {
+ const int iParentClientIndex = timer->ParentClientIndex();
+ if (iParentClientIndex != PVR_TIMER_NO_PARENT)
+ {
+ int iClientId = timer->ClientID();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tagsEntry : m_tags)
+ {
+ const auto it = std::find_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [iClientId, iParentClientIndex](const auto& timersEntry) {
+ return timersEntry->ClientID() == iClientId &&
+ timersEntry->ClientIndex() == iParentClientIndex;
+ });
+ if (it != tagsEntry.second.cend())
+ return (*it);
+ }
+ }
+ }
+
+ return {};
+}
+
+void CPVRTimers::Notify(const PVREvent& event)
+{
+ switch (static_cast<PVREvent>(event))
+ {
+ case PVREvent::EpgContainer:
+ CServiceBroker::GetPVRManager().TriggerTimersUpdate();
+ break;
+ case PVREvent::Epg:
+ case PVREvent::EpgItemUpdate:
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bReminderRulesUpdatePending = true;
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+CDateTime CPVRTimers::GetNextEventTime() const
+{
+ const bool dailywakup =
+ m_settings.GetBoolValue(CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUP);
+ const CDateTime now = CDateTime::GetUTCDateTime();
+ const CDateTimeSpan prewakeup(
+ 0, 0, m_settings.GetIntValue(CSettings::SETTING_PVRPOWERMANAGEMENT_PREWAKEUP), 0);
+ const CDateTimeSpan idle(
+ 0, 0, m_settings.GetIntValue(CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME), 0);
+
+ CDateTime wakeuptime;
+
+ /* Check next active time */
+ const std::shared_ptr<CPVRTimerInfoTag> timer = GetNextActiveTimer(false);
+ if (timer)
+ {
+ const CDateTimeSpan prestart(0, 0, timer->MarginStart(), 0);
+ const CDateTime start = timer->StartAsUTC();
+ wakeuptime =
+ ((start - prestart - prewakeup - idle) > now) ? start - prestart - prewakeup : now + idle;
+ }
+
+ /* check daily wake up */
+ if (dailywakup)
+ {
+ CDateTime dailywakeuptime;
+ dailywakeuptime.SetFromDBTime(
+ m_settings.GetStringValue(CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME));
+ dailywakeuptime = dailywakeuptime.GetAsUTCDateTime();
+
+ dailywakeuptime.SetDateTime(now.GetYear(), now.GetMonth(), now.GetDay(),
+ dailywakeuptime.GetHour(), dailywakeuptime.GetMinute(),
+ dailywakeuptime.GetSecond());
+
+ if ((dailywakeuptime - idle) < now)
+ {
+ const CDateTimeSpan oneDay(1, 0, 0, 0);
+ dailywakeuptime += oneDay;
+ }
+ if (!wakeuptime.IsValid() || dailywakeuptime < wakeuptime)
+ wakeuptime = dailywakeuptime;
+ }
+
+ const CDateTime retVal(wakeuptime);
+ return retVal;
+}
+
+void CPVRTimers::UpdateChannels()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tagsEntry : m_tags)
+ {
+ for (const auto& timersEntry : tagsEntry.second)
+ timersEntry->UpdateChannel();
+ }
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetAll() const
+{
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> timers;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tagsEntry : m_tags)
+ {
+ std::copy(tagsEntry.second.cbegin(), tagsEntry.second.cend(), std::back_inserter(timers));
+ }
+
+ return timers;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetById(unsigned int iTimerId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tagsEntry : m_tags)
+ {
+ const auto it = std::find_if(
+ tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [iTimerId](const auto& timersEntry) { return timersEntry->TimerID() == iTimerId; });
+ if (it != tagsEntry.second.cend())
+ return (*it);
+ }
+
+ return {};
+}
+
+void CPVRTimers::NotifyTimersEvent(bool bAddedOrDeleted /* = true */)
+{
+ CServiceBroker::GetPVRManager().PublishEvent(bAddedOrDeleted ? PVREvent::TimersInvalidated
+ : PVREvent::Timers);
+}
diff --git a/xbmc/pvr/timers/PVRTimers.h b/xbmc/pvr/timers/PVRTimers.h
new file mode 100644
index 0000000..6b0e4e7
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimers.h
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/settings/PVRSettings.h"
+#include "threads/Thread.h"
+
+#include <map>
+#include <memory>
+#include <queue>
+#include <vector>
+
+class CDateTime;
+
+namespace PVR
+{
+enum class TimerOperationResult;
+enum class PVREvent;
+
+class CPVRChannel;
+class CPVRClient;
+class CPVREpgInfoTag;
+class CPVRTimerInfoTag;
+class CPVRTimersPath;
+
+class CPVRTimersContainer
+{
+public:
+ /*!
+ * @brief Add a timer tag to this container or update the tag if already present in this
+ * container.
+ * @param The timer tag
+ * @return True, if the update was successful. False, otherwise.
+ */
+ bool UpdateFromClient(const std::shared_ptr<CPVRTimerInfoTag>& timer);
+
+ /*!
+ * @brief Get the timer tag denoted by given client id and timer id.
+ * @param iClientId The client id.
+ * @param iClientIndex The timer id.
+ * @return the timer tag if found, null otherwise.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetByClient(int iClientId, int iClientIndex) const;
+
+ typedef std::vector<std::shared_ptr<CPVRTimerInfoTag>> VecTimerInfoTag;
+ typedef std::map<CDateTime, VecTimerInfoTag> MapTags;
+
+ /*!
+ * @brief Get the timertags map.
+ * @return The map.
+ */
+ const MapTags& GetTags() const { return m_tags; }
+
+protected:
+ void InsertEntry(const std::shared_ptr<CPVRTimerInfoTag>& newTimer);
+
+ mutable CCriticalSection m_critSection;
+ unsigned int m_iLastId = 0;
+ MapTags m_tags;
+};
+
+class CPVRTimers : public CPVRTimersContainer, private CThread
+{
+public:
+ CPVRTimers();
+ ~CPVRTimers() override = default;
+
+ /*!
+ * @brief start the timer update thread.
+ */
+ void Start();
+
+ /*!
+ * @brief stop the timer update thread.
+ */
+ void Stop();
+
+ /*!
+ * @brief Update all timers from PVR database and from given clients.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool Update(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief unload all timers.
+ */
+ void Unload();
+
+ /*!
+ * @brief Update data with recordings from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created
+ * clients.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @param bIgnoreReminders include or ignore reminders
+ * @return The tv or radio timer that will be active next (state scheduled), or nullptr if none.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer(bool bIgnoreReminders = true) const;
+
+ /*!
+ * @return The tv timer that will be active next (state scheduled), or nullptr if none.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTVTimer() const;
+
+ /*!
+ * @return The radio timer that will be active next (state scheduled), or nullptr if none.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveRadioTimer() const;
+
+ /*!
+ * @return All timers that are active (states scheduled or recording)
+ */
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveTimers() const;
+
+ /*!
+ * @return Next due reminder, if any. Removes it from the queue of due reminders.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetNextReminderToAnnnounce();
+
+ /*!
+ * Get all timers
+ * @return The list of all timers
+ */
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetAll() const;
+
+ /*!
+ * @return The amount of tv and radio timers that are active (states scheduled or recording)
+ */
+ int AmountActiveTimers() const;
+
+ /*!
+ * @return The amount of tv timers that are active (states scheduled or recording)
+ */
+ int AmountActiveTVTimers() const;
+
+ /*!
+ * @return The amount of radio timers that are active (states scheduled or recording)
+ */
+ int AmountActiveRadioTimers() const;
+
+ /*!
+ * @return All tv and radio timers that are recording
+ */
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() const;
+
+ /*!
+ * @return All tv timers that are recording
+ */
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveTVRecordings() const;
+
+ /*!
+ * @return All radio timers that are recording
+ */
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRadioRecordings() const;
+
+ /*!
+ * @return True when recording, false otherwise.
+ */
+ bool IsRecording() const;
+
+ /*!
+ * @brief Check if a recording is running on the given channel.
+ * @param channel The channel to check.
+ * @return True when recording, false otherwise.
+ */
+ bool IsRecordingOnChannel(const CPVRChannel& channel) const;
+
+ /*!
+ * @brief Obtain the active timer for a given channel.
+ * @param channel The channel to check.
+ * @return the timer, null otherwise.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetActiveTimerForChannel(
+ const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @return The amount of tv and radio timers that are currently recording
+ */
+ int AmountActiveRecordings() const;
+
+ /*!
+ * @return The amount of tv timers that are currently recording
+ */
+ int AmountActiveTVRecordings() const;
+
+ /*!
+ * @return The amount of radio timers that are currently recording
+ */
+ int AmountActiveRadioRecordings() const;
+
+ /*!
+ * @brief Delete all timers on a channel.
+ * @param channel The channel to delete the timers for.
+ * @param bDeleteTimerRules True to delete timer rules too, false otherwise.
+ * @param bCurrentlyActiveOnly True to delete timers that are currently running only.
+ * @return True if timers any were deleted, false otherwise.
+ */
+ bool DeleteTimersOnChannel(const std::shared_ptr<CPVRChannel>& channel,
+ bool bDeleteTimerRules = true,
+ bool bCurrentlyActiveOnly = false);
+
+ /*!
+ * @return Next event time (timer or daily wake up)
+ */
+ CDateTime GetNextEventTime() const;
+
+ /*!
+ * @brief Add a timer to the client. Doesn't add the timer to the container. The backend will do
+ * this.
+ * @param tag The timer to add.
+ * @return True if timer add request was sent correctly, false if not.
+ */
+ bool AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag);
+
+ /*!
+ * @brief Delete a timer on the client. Doesn't delete the timer from the container. The backend
+ * will do this.
+ * @param tag The timer to delete.
+ * @param bForce Control what to do in case the timer is currently recording.
+ * True to force to delete the timer, false to return TimerDeleteResult::RECORDING.
+ * @param bDeleteRule Also delete the timer rule that scheduled the timer instead of single timer
+ * only.
+ * @return The result.
+ */
+ TimerOperationResult DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag,
+ bool bForce = false,
+ bool bDeleteRule = false);
+
+ /*!
+ * @brief Update the timer on the client. Doesn't update the timer in the container. The backend
+ * will do this.
+ * @param tag The timer to update.
+ * @return True if timer update request was sent correctly, false if not.
+ */
+ bool UpdateTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag);
+
+ /*!
+ * @brief Get the timer tag that matches the given epg tag.
+ * @param epgTag The epg tag.
+ * @return The requested timer tag, or nullptr if none was found.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetTimerForEpgTag(
+ const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ /*!
+ * @brief Get the timer rule for a given timer tag
+ * @param timer The timer to query the timer rule for
+ * @return The timer rule, or nullptr if none was found.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetTimerRule(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer) const;
+
+ /*!
+ * @brief Update the channel pointers.
+ */
+ void UpdateChannels();
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+
+ /*!
+ * @brief Get a timer tag given it's unique ID
+ * @param iTimerId The ID to find
+ * @return The tag, or an empty one when not found
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetById(unsigned int iTimerId) const;
+
+private:
+ void Process() override;
+
+ /*!
+ * @brief Load all timers from PVR database.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ void RemoveEntry(const std::shared_ptr<CPVRTimerInfoTag>& tag);
+ bool UpdateEntries(const CPVRTimersContainer& timers, const std::vector<int>& failedClients);
+ bool UpdateEntries(int iMaxNotificationDelay);
+ std::shared_ptr<CPVRTimerInfoTag> UpdateEntry(const std::shared_ptr<CPVRTimerInfoTag>& timer);
+
+ bool AddLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, bool bNotify);
+ bool DeleteLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, bool bNotify);
+ bool UpdateLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag);
+ std::shared_ptr<CPVRTimerInfoTag> PersistAndUpdateLocalTimer(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer,
+ const std::shared_ptr<CPVRTimerInfoTag>& parentTimer);
+ void NotifyTimersEvent(bool bAddedOrDeleted = true);
+
+ enum TimerKind
+ {
+ TimerKindAny = 0,
+ TimerKindTV,
+ TimerKindRadio
+ };
+
+ bool KindMatchesTag(const TimerKind& eKind, const std::shared_ptr<CPVRTimerInfoTag>& tag) const;
+
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer(const TimerKind& eKind,
+ bool bIgnoreReminders) const;
+ int AmountActiveTimers(const TimerKind& eKind) const;
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings(const TimerKind& eKind) const;
+ int AmountActiveRecordings(const TimerKind& eKind) const;
+
+ bool CheckAndAppendTimerNotification(std::vector<std::pair<int, std::string>>& timerNotifications,
+ const std::shared_ptr<CPVRTimerInfoTag>& tag,
+ bool bDeleted) const;
+
+ bool m_bIsUpdating = false;
+ CPVRSettings m_settings;
+ std::queue<std::shared_ptr<CPVRTimerInfoTag>> m_remindersToAnnounce;
+ bool m_bReminderRulesUpdatePending = false;
+
+ bool m_bFirstUpdate = true;
+ std::vector<int> m_failedClients;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/timers/PVRTimersPath.cpp b/xbmc/pvr/timers/PVRTimersPath.cpp
new file mode 100644
index 0000000..ea2265c
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimersPath.cpp
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "PVRTimersPath.h"
+
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <cstdlib>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+const std::string CPVRTimersPath::PATH_ADDTIMER = "pvr://timers/addtimer/";
+const std::string CPVRTimersPath::PATH_NEW = "pvr://timers/new/";
+const std::string CPVRTimersPath::PATH_TV_TIMERS = "pvr://timers/tv/timers/";
+const std::string CPVRTimersPath::PATH_RADIO_TIMERS = "pvr://timers/radio/timers/";
+const std::string CPVRTimersPath::PATH_TV_TIMER_RULES = "pvr://timers/tv/rules/";
+const std::string CPVRTimersPath::PATH_RADIO_TIMER_RULES = "pvr://timers/radio/rules/";
+
+CPVRTimersPath::CPVRTimersPath(const std::string& strPath)
+{
+ Init(strPath);
+}
+
+CPVRTimersPath::CPVRTimersPath(const std::string& strPath, int iClientId, int iParentId)
+{
+ if (Init(strPath))
+ {
+ // set/replace client and parent id.
+ m_path = StringUtils::Format("pvr://timers/{}/{}/{}/{}", m_bRadio ? "radio" : "tv",
+ m_bTimerRules ? "rules" : "timers", iClientId, iParentId);
+ m_iClientId = iClientId;
+ m_iParentId = iParentId;
+ m_bRoot = false;
+ }
+}
+
+CPVRTimersPath::CPVRTimersPath(bool bRadio, bool bTimerRules)
+ : m_path(StringUtils::Format(
+ "pvr://timers/{}/{}", bRadio ? "radio" : "tv", bTimerRules ? "rules" : "timers")),
+ m_bValid(true),
+ m_bRoot(true),
+ m_bRadio(bRadio),
+ m_bTimerRules(bTimerRules)
+{
+}
+
+bool CPVRTimersPath::Init(const std::string& strPath)
+{
+ std::string strVarPath(strPath);
+ URIUtils::RemoveSlashAtEnd(strVarPath);
+
+ m_path = strVarPath;
+ const std::vector<std::string> segments = URIUtils::SplitPath(m_path);
+
+ m_bValid = (((segments.size() == 4) || (segments.size() == 6)) && (segments.at(1) == "timers") &&
+ ((segments.at(2) == "radio") || (segments.at(2) == "tv")) &&
+ ((segments.at(3) == "rules") || (segments.at(3) == "timers")));
+ m_bRoot = (m_bValid && (segments.size() == 4));
+ m_bRadio = (m_bValid && (segments.at(2) == "radio"));
+ m_bTimerRules = (m_bValid && (segments.at(3) == "rules"));
+
+ if (!m_bValid || m_bRoot)
+ {
+ m_iClientId = -1;
+ m_iParentId = 0;
+ }
+ else
+ {
+ m_iClientId = std::stoi(segments.at(4));
+ m_iParentId = std::stoi(segments.at(5));
+ }
+
+ return m_bValid;
+}
diff --git a/xbmc/pvr/timers/PVRTimersPath.h b/xbmc/pvr/timers/PVRTimersPath.h
new file mode 100644
index 0000000..01fd5f0
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimersPath.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace PVR
+{
+class CPVRTimersPath
+{
+public:
+ static const std::string PATH_ADDTIMER;
+ static const std::string PATH_NEW;
+ static const std::string PATH_TV_TIMERS;
+ static const std::string PATH_TV_TIMER_RULES;
+ static const std::string PATH_RADIO_TIMERS;
+ static const std::string PATH_RADIO_TIMER_RULES;
+
+ explicit CPVRTimersPath(const std::string& strPath);
+ CPVRTimersPath(const std::string& strPath, int iClientId, int iParentId);
+ CPVRTimersPath(bool bRadio, bool bTimerRules);
+
+ bool IsValid() const { return m_bValid; }
+
+ const std::string& GetPath() const { return m_path; }
+ bool IsTimersRoot() const { return m_bRoot; }
+ bool IsTimerRule() const { return !IsTimersRoot(); }
+ bool IsRadio() const { return m_bRadio; }
+ bool IsRules() const { return m_bTimerRules; }
+ int GetClientId() const { return m_iClientId; }
+ int GetParentId() const { return m_iParentId; }
+
+private:
+ bool Init(const std::string& strPath);
+
+ std::string m_path;
+ bool m_bValid = false;
+ bool m_bRoot = false;
+ bool m_bRadio = false;
+ bool m_bTimerRules = false;
+ int m_iClientId = -1;
+ int m_iParentId = 0;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/windows/CMakeLists.txt b/xbmc/pvr/windows/CMakeLists.txt
new file mode 100644
index 0000000..9f5097e
--- /dev/null
+++ b/xbmc/pvr/windows/CMakeLists.txt
@@ -0,0 +1,21 @@
+set(SOURCES GUIViewStatePVR.cpp
+ GUIWindowPVRBase.cpp
+ GUIWindowPVRChannels.cpp
+ GUIWindowPVRGuide.cpp
+ GUIWindowPVRRecordings.cpp
+ GUIWindowPVRSearch.cpp
+ GUIWindowPVRTimers.cpp
+ GUIWindowPVRTimersBase.cpp
+ GUIWindowPVRTimerRules.cpp)
+
+set(HEADERS GUIViewStatePVR.h
+ GUIWindowPVRBase.h
+ GUIWindowPVRChannels.h
+ GUIWindowPVRGuide.h
+ GUIWindowPVRRecordings.h
+ GUIWindowPVRSearch.h
+ GUIWindowPVRTimerRules.h
+ GUIWindowPVRTimers.h
+ GUIWindowPVRTimersBase.h)
+
+core_add_library(pvr_windows)
diff --git a/xbmc/pvr/windows/GUIViewStatePVR.cpp b/xbmc/pvr/windows/GUIViewStatePVR.cpp
new file mode 100644
index 0000000..d86f6fc
--- /dev/null
+++ b/xbmc/pvr/windows/GUIViewStatePVR.cpp
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIViewStatePVR.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/epg/EpgSearchPath.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "view/ViewStateSettings.h"
+
+using namespace PVR;
+
+CGUIViewStateWindowPVRChannels::CGUIViewStateWindowPVRChannels(const int windowId,
+ const CFileItemList& items)
+ : CGUIViewStatePVR(windowId, items)
+{
+ AddSortMethod(SortByChannelNumber, 549, // "Number"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(SortByChannel, 551, // "Name"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(
+ SortByLastPlayed, 568, // "Last played"
+ LABEL_MASKS("%L", "%p", "%L", "%p")); // Filename, LastPlayed | Foldername, LastPlayed
+ AddSortMethod(SortByClientChannelOrder, 19315, // "Backend number"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(SortByProvider, 19348, // "Provider"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+
+ // Default sorting
+ SetSortMethod(SortByChannelNumber);
+
+ LoadViewState("pvr://channels/", m_windowId);
+}
+
+void CGUIViewStateWindowPVRChannels::SaveViewState()
+{
+ SaveViewToDb("pvr://channels/", m_windowId, CViewStateSettings::GetInstance().Get("pvrchannels"));
+}
+
+CGUIViewStateWindowPVRRecordings::CGUIViewStateWindowPVRRecordings(const int windowId,
+ const CFileItemList& items)
+ : CGUIViewStatePVR(windowId, items)
+{
+ AddSortMethod(SortByLabel, 551, // "Name"
+ LABEL_MASKS("%L", "%d", "%L", ""), // Filename, DateTime | Foldername, empty
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)
+ ? SortAttributeIgnoreArticle
+ : SortAttributeNone);
+ AddSortMethod(SortByDate, 552, // "Date"
+ LABEL_MASKS("%L", "%d", "%L", "%d")); // Filename, DateTime | Foldername, DateTime
+ AddSortMethod(SortByTime, 180, // "Duration"
+ LABEL_MASKS("%L", "%D", "%L", "")); // Filename, Duration | Foldername, empty
+ AddSortMethod(SortByFile, 561, // "File"
+ LABEL_MASKS("%L", "%d", "%L", "")); // Filename, DateTime | Foldername, empty
+
+ if (CServiceBroker::GetPVRManager().Clients()->AnyClientSupportingRecordingsSize())
+ {
+ // "Size" : Filename, Size | Foldername, Size
+ AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I"));
+ }
+
+ AddSortMethod(SortByEpisodeNumber, 20359, // "Episode"
+ LABEL_MASKS("%L", "%d", "%L", "")); // Filename, DateTime | Foldername, empty
+ AddSortMethod(SortByProvider, 19348, // "Provider"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+
+ SetSortMethod(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_PVRDefaultSortOrder);
+
+ LoadViewState(items.GetPath(), m_windowId);
+}
+
+void CGUIViewStateWindowPVRRecordings::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), m_windowId,
+ CViewStateSettings::GetInstance().Get("pvrrecordings"));
+}
+
+bool CGUIViewStateWindowPVRRecordings::HideParentDirItems()
+{
+ return (CGUIViewState::HideParentDirItems() ||
+ CPVRRecordingsPath(m_items.GetPath()).IsRecordingsRoot());
+}
+
+CGUIViewStateWindowPVRGuide::CGUIViewStateWindowPVRGuide(const int windowId,
+ const CFileItemList& items)
+ : CGUIViewStatePVR(windowId, items)
+{
+ AddSortMethod(SortByChannelNumber, 549, // "Number"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(SortByChannel, 551, // "Name"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(
+ SortByLastPlayed, SortAttributeIgnoreLabel, 568, // "Last played"
+ LABEL_MASKS("%L", "%p", "%L", "%p")); // Filename, LastPlayed | Foldername, LastPlayed
+ AddSortMethod(SortByClientChannelOrder, 19315, // "Backend number"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(SortByProvider, 19348, // "Provider"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+
+ // Default sorting
+ SetSortMethod(SortByChannelNumber);
+
+ LoadViewState("pvr://guide/", m_windowId);
+}
+
+void CGUIViewStateWindowPVRGuide::SaveViewState()
+{
+ SaveViewToDb("pvr://guide/", m_windowId, CViewStateSettings::GetInstance().Get("pvrguide"));
+}
+
+CGUIViewStateWindowPVRTimers::CGUIViewStateWindowPVRTimers(const int windowId,
+ const CFileItemList& items)
+ : CGUIViewStatePVR(windowId, items)
+{
+ int sortAttributes(CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)
+ ? SortAttributeIgnoreArticle
+ : SortAttributeNone);
+ sortAttributes |= SortAttributeIgnoreFolders;
+ AddSortMethod(SortByLabel, static_cast<SortAttribute>(sortAttributes), 551, // "Name"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(SortByDate, static_cast<SortAttribute>(sortAttributes), 552, // "Date"
+ LABEL_MASKS("%L", "%d", "%L", "%d")); // Filename, DateTime | Foldername, DateTime
+
+ // Default sorting
+ SetSortMethod(SortByDate);
+
+ LoadViewState("pvr://timers/", m_windowId);
+}
+
+void CGUIViewStateWindowPVRTimers::SaveViewState()
+{
+ SaveViewToDb("pvr://timers/", m_windowId, CViewStateSettings::GetInstance().Get("pvrtimers"));
+}
+
+bool CGUIViewStateWindowPVRTimers::HideParentDirItems()
+{
+ return (CGUIViewState::HideParentDirItems() || CPVRTimersPath(m_items.GetPath()).IsTimersRoot());
+}
+
+CGUIViewStateWindowPVRSearch::CGUIViewStateWindowPVRSearch(const int windowId,
+ const CFileItemList& items)
+ : CGUIViewStatePVR(windowId, items)
+{
+ AddSortMethod(SortByLabel, 551, // "Name"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(SortByDate, 552, // "Date"
+ LABEL_MASKS("%L", "%d", "%L", "%d")); // Filename, DateTime | Foldername, DateTime
+
+ // Default sorting
+ if (CPVREpgSearchPath(m_items.GetPath()).IsSavedSearchesRoot())
+ SetSortMethod(SortByDate, SortOrderDescending);
+ else
+ SetSortMethod(SortByDate, SortOrderAscending);
+
+ LoadViewState(m_items.GetPath(), m_windowId);
+}
+
+void CGUIViewStateWindowPVRSearch::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), m_windowId, CViewStateSettings::GetInstance().Get("pvrsearch"));
+}
+
+bool CGUIViewStateWindowPVRSearch::HideParentDirItems()
+{
+ return (CGUIViewState::HideParentDirItems() ||
+ CPVREpgSearchPath(m_items.GetPath()).IsSearchRoot());
+}
diff --git a/xbmc/pvr/windows/GUIViewStatePVR.h b/xbmc/pvr/windows/GUIViewStatePVR.h
new file mode 100644
index 0000000..ce39dd7
--- /dev/null
+++ b/xbmc/pvr/windows/GUIViewStatePVR.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "view/GUIViewState.h"
+
+class CFileItemList;
+
+namespace PVR
+{
+class CGUIViewStatePVR : public CGUIViewState
+{
+public:
+ CGUIViewStatePVR(const int windowId, const CFileItemList& items) : CGUIViewState(items)
+ {
+ m_windowId = windowId;
+ }
+
+protected:
+ bool HideParentDirItems() override { return true; }
+
+ int m_windowId;
+};
+
+class CGUIViewStateWindowPVRChannels : public CGUIViewStatePVR
+{
+public:
+ CGUIViewStateWindowPVRChannels(const int windowId, const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateWindowPVRRecordings : public CGUIViewStatePVR
+{
+public:
+ CGUIViewStateWindowPVRRecordings(const int windowId, const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ bool HideParentDirItems() override;
+};
+
+class CGUIViewStateWindowPVRGuide : public CGUIViewStatePVR
+{
+public:
+ CGUIViewStateWindowPVRGuide(const int windowId, const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateWindowPVRTimers : public CGUIViewStatePVR
+{
+public:
+ CGUIViewStateWindowPVRTimers(const int windowId, const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ bool HideParentDirItems() override;
+};
+
+class CGUIViewStateWindowPVRSearch : public CGUIViewStatePVR
+{
+public:
+ CGUIViewStateWindowPVRSearch(const int windowId, const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ bool HideParentDirItems() override;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/windows/GUIWindowPVRBase.cpp b/xbmc/pvr/windows/GUIWindowPVRBase.cpp
new file mode 100644
index 0000000..96a14b5
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRBase.cpp
@@ -0,0 +1,563 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIWindowPVRBase.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/filesystem/PVRGUIDirectory.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace std::chrono_literals;
+
+#define MAX_INVALIDATION_FREQUENCY 2000ms // limit to one invalidation per X milliseconds
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+namespace PVR
+{
+
+class CGUIPVRChannelGroupsSelector
+{
+public:
+ virtual ~CGUIPVRChannelGroupsSelector() = default;
+
+ bool Initialize(CGUIWindow* parent, bool bRadio);
+
+ bool HasFocus() const;
+ std::shared_ptr<CPVRChannelGroup> GetSelectedChannelGroup() const;
+ bool SelectChannelGroup(const std::shared_ptr<CPVRChannelGroup>& newGroup);
+
+private:
+ CGUIControl* m_control = nullptr;
+ std::vector<std::shared_ptr<CPVRChannelGroup>> m_channelGroups;
+};
+
+} // namespace PVR
+
+bool CGUIPVRChannelGroupsSelector::Initialize(CGUIWindow* parent, bool bRadio)
+{
+ CGUIControl* control = parent->GetControl(CONTROL_LSTCHANNELGROUPS);
+ if (control && control->IsContainer())
+ {
+ m_control = control;
+ m_channelGroups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio)->GetMembers(true);
+
+ CFileItemList channelGroupItems;
+ CPVRGUIDirectory::GetChannelGroupsDirectory(bRadio, true, channelGroupItems);
+
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, m_control->GetID(), CONTROL_LSTCHANNELGROUPS, 0, 0, &channelGroupItems);
+ m_control->OnMessage(msg);
+ return true;
+ }
+ return false;
+}
+
+bool CGUIPVRChannelGroupsSelector::HasFocus() const
+{
+ return m_control && m_control->HasFocus();
+}
+
+std::shared_ptr<CPVRChannelGroup> CGUIPVRChannelGroupsSelector::GetSelectedChannelGroup() const
+{
+ if (m_control)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, m_control->GetID(), CONTROL_LSTCHANNELGROUPS);
+ m_control->OnMessage(msg);
+
+ const auto it = std::next(m_channelGroups.begin(), msg.GetParam1());
+ if (it != m_channelGroups.end())
+ {
+ return *it;
+ }
+ }
+ return std::shared_ptr<CPVRChannelGroup>();
+}
+
+bool CGUIPVRChannelGroupsSelector::SelectChannelGroup(const std::shared_ptr<CPVRChannelGroup>& newGroup)
+{
+ if (m_control && newGroup)
+ {
+ int iIndex = 0;
+ for (const auto& group : m_channelGroups)
+ {
+ if (*newGroup == *group)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, m_control->GetID(), CONTROL_LSTCHANNELGROUPS, iIndex);
+ m_control->OnMessage(msg);
+ return true;
+ }
+ ++iIndex;
+ }
+ }
+ return false;
+}
+
+CGUIWindowPVRBase::CGUIWindowPVRBase(bool bRadio, int id, const std::string& xmlFile) :
+ CGUIMediaWindow(id, xmlFile.c_str()),
+ m_bRadio(bRadio),
+ m_channelGroupsSelector(new CGUIPVRChannelGroupsSelector),
+ m_progressHandle(nullptr)
+{
+ // prevent removable drives to appear in directory listing (base class default behavior).
+ m_rootDir.AllowNonLocalSources(false);
+
+ CServiceBroker::GetPVRManager().Events().Subscribe(this, &CGUIWindowPVRBase::Notify);
+}
+
+CGUIWindowPVRBase::~CGUIWindowPVRBase()
+{
+ if (m_channelGroup)
+ m_channelGroup->Events().Unsubscribe(this);
+
+ CServiceBroker::GetPVRManager().Events().Unsubscribe(this);
+}
+
+void CGUIWindowPVRBase::UpdateSelectedItemPath()
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().SetSelectedChannelPath(
+ m_bRadio, m_viewControl.GetSelectedItemPath());
+}
+
+void CGUIWindowPVRBase::Notify(const PVREvent& event)
+{
+ // call virtual event handler function
+ NotifyEvent(event);
+}
+
+void CGUIWindowPVRBase::NotifyEvent(const PVREvent& event)
+{
+ if (event == PVREvent::ManagerStopped)
+ {
+ ClearData();
+ }
+ else if (m_active)
+ {
+ if (event == PVREvent::SystemSleep)
+ {
+ CGUIMessage m(GUI_MSG_SYSTEM_SLEEP, GetID(), 0, static_cast<int>(event));
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(m);
+ }
+ else if (event == PVREvent::SystemWake)
+ {
+ CGUIMessage m(GUI_MSG_SYSTEM_WAKE, GetID(), 0, static_cast<int>(event));
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(m);
+ }
+ else
+ {
+ CGUIMessage m(GUI_MSG_REFRESH_LIST, GetID(), 0, static_cast<int>(event));
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(m);
+ }
+ }
+}
+
+bool CGUIWindowPVRBase::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_PREVIOUS_CHANNELGROUP:
+ ActivatePreviousChannelGroup();
+ return true;
+
+ case ACTION_NEXT_CHANNELGROUP:
+ ActivateNextChannelGroup();
+ return true;
+
+ case ACTION_MOVE_RIGHT:
+ case ACTION_MOVE_LEFT:
+ {
+ if (m_channelGroupsSelector->HasFocus() && CGUIMediaWindow::OnAction(action))
+ {
+ SetChannelGroup(m_channelGroupsSelector->GetSelectedChannelGroup());
+ return true;
+ }
+ }
+ }
+
+ return CGUIMediaWindow::OnAction(action);
+}
+
+bool CGUIWindowPVRBase::ActivatePreviousChannelGroup()
+{
+ const std::shared_ptr<CPVRChannelGroup> channelGroup = GetChannelGroup();
+ if (channelGroup)
+ {
+ const CPVRChannelGroups* groups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(channelGroup->IsRadio());
+ if (groups)
+ {
+ SetChannelGroup(groups->GetPreviousGroup(*channelGroup));
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIWindowPVRBase::ActivateNextChannelGroup()
+{
+ const std::shared_ptr<CPVRChannelGroup> channelGroup = GetChannelGroup();
+ if (channelGroup)
+ {
+ const CPVRChannelGroups* groups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(channelGroup->IsRadio());
+ if (groups)
+ {
+ SetChannelGroup(groups->GetNextGroup(*channelGroup));
+ return true;
+ }
+ }
+ return false;
+}
+
+void CGUIWindowPVRBase::ClearData()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channelGroup.reset();
+ m_channelGroupsSelector.reset(new CGUIPVRChannelGroupsSelector);
+}
+
+void CGUIWindowPVRBase::OnInitWindow()
+{
+ SetProperty("IsRadio", m_bRadio ? "true" : "");
+
+ if (InitChannelGroup())
+ {
+ m_channelGroupsSelector->Initialize(this, m_bRadio);
+
+ CGUIMediaWindow::OnInitWindow();
+
+ // mark item as selected by channel path
+ m_viewControl.SetSelectedItem(
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetSelectedChannelPath(m_bRadio));
+
+ // This has to be done after base class OnInitWindow to restore correct selection
+ m_channelGroupsSelector->SelectChannelGroup(GetChannelGroup());
+ }
+ else
+ {
+ CGUIWindow::OnInitWindow(); // do not call CGUIMediaWindow as it will do a Refresh which in no case works in this state (no channelgroup!)
+ ShowProgressDialog(g_localizeStrings.Get(19235), 0); // PVR manager is starting up
+ }
+}
+
+void CGUIWindowPVRBase::OnDeinitWindow(int nextWindowID)
+{
+ HideProgressDialog();
+ UpdateSelectedItemPath();
+ CGUIMediaWindow::OnDeinitWindow(nextWindowID);
+}
+
+bool CGUIWindowPVRBase::OnMessage(CGUIMessage& message)
+{
+ bool bReturn = false;
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ switch (message.GetSenderId())
+ {
+ case CONTROL_BTNCHANNELGROUPS:
+ return OpenChannelGroupSelectionDialog();
+
+ case CONTROL_LSTCHANNELGROUPS:
+ {
+ switch (message.GetParam1())
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ {
+ SetChannelGroup(m_channelGroupsSelector->GetSelectedChannelGroup());
+ bReturn = true;
+ break;
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ case GUI_MSG_REFRESH_LIST:
+ {
+ switch (static_cast<PVREvent>(message.GetParam1()))
+ {
+ case PVREvent::ManagerStarted:
+ case PVREvent::ClientsInvalidated:
+ case PVREvent::ChannelGroupsInvalidated:
+ {
+ if (InitChannelGroup())
+ {
+ m_channelGroupsSelector->Initialize(this, m_bRadio);
+ m_channelGroupsSelector->SelectChannelGroup(GetChannelGroup());
+ HideProgressDialog();
+ Refresh(true);
+ m_viewControl.SetFocused();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (IsActive())
+ {
+ // Only the active window must set the selected item path which is shared
+ // between all PVR windows, not the last notified window (observer).
+ UpdateSelectedItemPath();
+ }
+ bReturn = true;
+ break;
+ }
+
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ switch (message.GetParam1())
+ {
+ case GUI_MSG_UPDATE_SOURCES:
+ {
+ // removable drive connected/disconnected. base class triggers a window
+ // content refresh, which makes no sense for pvr windows.
+ bReturn = true;
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ return bReturn || CGUIMediaWindow::OnMessage(message);
+}
+
+void CGUIWindowPVRBase::SetInvalid()
+{
+ if (m_refreshTimeout.IsTimePast())
+ {
+ for (const auto& item : *m_vecItems)
+ item->SetInvalid();
+
+ CGUIMediaWindow::SetInvalid();
+ m_refreshTimeout.Set(MAX_INVALIDATION_FREQUENCY);
+ }
+}
+
+bool CGUIWindowPVRBase::CanBeActivated() const
+{
+ // check if there is at least one enabled PVR add-on
+ if (!CServiceBroker::GetAddonMgr().HasAddons(ADDON::AddonType::PVRDLL))
+ {
+ HELPERS::ShowOKDialogText(CVariant{19296}, CVariant{19272}); // No PVR add-on enabled, You need a tuner, backend software...
+ return false;
+ }
+
+ return true;
+}
+
+bool CGUIWindowPVRBase::OpenChannelGroupSelectionDialog()
+{
+ CGUIDialogSelect* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (!dialog)
+ return false;
+
+ CFileItemList options;
+ CPVRGUIDirectory::GetChannelGroupsDirectory(m_bRadio, true, options);
+
+ dialog->Reset();
+ dialog->SetHeading(CVariant{g_localizeStrings.Get(19146)});
+ dialog->SetItems(options);
+ dialog->SetMultiSelection(false);
+ if (const std::shared_ptr<CPVRChannelGroup> channelGroup = GetChannelGroup())
+ {
+ dialog->SetSelected(channelGroup->GroupName());
+ }
+ dialog->Open();
+
+ if (!dialog->IsConfirmed())
+ return false;
+
+ const CFileItemPtr item = dialog->GetSelectedFileItem();
+ if (!item)
+ return false;
+
+ SetChannelGroup(CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bRadio)->GetByName(item->m_strTitle));
+
+ return true;
+}
+
+bool CGUIWindowPVRBase::InitChannelGroup()
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return false;
+
+ std::shared_ptr<CPVRChannelGroup> group;
+ if (m_channelGroupPath.empty())
+ {
+ group = CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(m_bRadio);
+ }
+ else
+ {
+ group = CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bRadio)->GetGroupByPath(m_channelGroupPath);
+ if (group)
+ CServiceBroker::GetPVRManager().PlaybackState()->SetActiveChannelGroup(group);
+ else
+ CLog::LogF(LOGERROR, "Found no {} channel group with path '{}'!", m_bRadio ? "radio" : "TV",
+ m_channelGroupPath);
+
+ m_channelGroupPath.clear();
+ }
+
+ if (group)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_channelGroup != group)
+ {
+ m_viewControl.SetSelectedItem(0);
+ SetChannelGroup(std::move(group), false);
+ }
+ // Path might have changed since last init. Set it always, not just on group change.
+ m_vecItems->SetPath(GetDirectoryPath());
+ return true;
+ }
+ return false;
+}
+
+std::shared_ptr<CPVRChannelGroup> CGUIWindowPVRBase::GetChannelGroup()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelGroup;
+}
+
+void CGUIWindowPVRBase::SetChannelGroup(std::shared_ptr<CPVRChannelGroup> &&group, bool bUpdate /* = true */)
+{
+ if (!group)
+ return;
+
+ std::shared_ptr<CPVRChannelGroup> updateChannelGroup;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_channelGroup != group)
+ {
+ if (m_channelGroup)
+ m_channelGroup->Events().Unsubscribe(this);
+ m_channelGroup = std::move(group);
+ // we need to register the window to receive changes from the new group
+ m_channelGroup->Events().Subscribe(this, &CGUIWindowPVRBase::Notify);
+ if (bUpdate)
+ updateChannelGroup = m_channelGroup;
+ }
+ }
+
+ if (updateChannelGroup)
+ {
+ CServiceBroker::GetPVRManager().PlaybackState()->SetActiveChannelGroup(updateChannelGroup);
+ Update(GetDirectoryPath());
+ }
+}
+
+bool CGUIWindowPVRBase::Update(const std::string& strDirectory, bool updateFilterPath /*= true*/)
+{
+ if (m_bUpdating)
+ {
+ // no concurrent updates
+ return false;
+ }
+
+ CUpdateGuard guard(m_bUpdating);
+
+ if (!GetChannelGroup())
+ {
+ // no updates before fully initialized
+ return false;
+ }
+
+ int iOldCount = m_vecItems->Size();
+ int iSelectedItem = m_viewControl.GetSelectedItem();
+ const std::string oldPath = m_vecItems->GetPath();
+
+ bool bReturn = CGUIMediaWindow::Update(strDirectory, updateFilterPath);
+
+ if (bReturn &&
+ iSelectedItem != -1) // something must have been selected
+ {
+ int iNewCount = m_vecItems->Size();
+ if (iOldCount > iNewCount && // at least one item removed by Update()
+ oldPath == m_vecItems->GetPath()) // update not due changing into another folder
+ {
+ // restore selected item if we just deleted one or more items.
+ if (iSelectedItem >= iNewCount)
+ iSelectedItem = iNewCount - 1;
+
+ m_viewControl.SetSelectedItem(iSelectedItem);
+ }
+ }
+
+ return bReturn;
+}
+
+void CGUIWindowPVRBase::UpdateButtons()
+{
+ CGUIMediaWindow::UpdateButtons();
+
+ const std::shared_ptr<CPVRChannelGroup> channelGroup = GetChannelGroup();
+ if (channelGroup)
+ {
+ SET_CONTROL_LABEL(CONTROL_BTNCHANNELGROUPS, g_localizeStrings.Get(19141) + ": " + channelGroup->GroupName());
+ }
+
+ m_channelGroupsSelector->SelectChannelGroup(channelGroup);
+}
+
+void CGUIWindowPVRBase::ShowProgressDialog(const std::string& strText, int iProgress)
+{
+ if (!m_progressHandle)
+ {
+ CGUIDialogExtendedProgressBar* loadingProgressDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS);
+ if (!loadingProgressDialog)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_EXT_PROGRESS!");
+ return;
+ }
+ m_progressHandle = loadingProgressDialog->GetHandle(g_localizeStrings.Get(19235)); // PVR manager is starting up
+ }
+
+ m_progressHandle->SetPercentage(static_cast<float>(iProgress));
+ m_progressHandle->SetText(strText);
+}
+
+void CGUIWindowPVRBase::HideProgressDialog()
+{
+ if (m_progressHandle)
+ {
+ m_progressHandle->MarkFinished();
+ m_progressHandle = nullptr;
+ }
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRBase.h b/xbmc/pvr/windows/GUIWindowPVRBase.h
new file mode 100644
index 0000000..1fe27bb
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRBase.h
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "windows/GUIMediaWindow.h"
+
+#include <atomic>
+#include <memory>
+#include <string>
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_BTNGROUPITEMS 5
+#define CONTROL_BTNSHOWHIDDEN 6
+#define CONTROL_BTNSHOWDELETED 7
+#define CONTROL_BTNHIDEDISABLEDTIMERS 8
+#define CONTROL_BTNSHOWMODE 10
+#define CONTROL_LSTCHANNELGROUPS 11
+
+#define CONTROL_BTNCHANNELGROUPS 28
+#define CONTROL_BTNFILTERCHANNELS 31
+
+#define CONTROL_LABEL_HEADER1 29
+#define CONTROL_LABEL_HEADER2 30
+
+class CGUIDialogProgressBarHandle;
+
+namespace PVR
+{
+ enum class PVREvent;
+
+ enum EPGSelectAction
+ {
+ EPG_SELECT_ACTION_CONTEXT_MENU = 0,
+ EPG_SELECT_ACTION_SWITCH = 1,
+ EPG_SELECT_ACTION_INFO = 2,
+ EPG_SELECT_ACTION_RECORD = 3,
+ EPG_SELECT_ACTION_PLAY_RECORDING = 4,
+ EPG_SELECT_ACTION_SMART_SELECT = 5
+ };
+
+ class CPVRChannelGroup;
+ class CGUIPVRChannelGroupsSelector;
+
+ class CGUIWindowPVRBase : public CGUIMediaWindow
+ {
+ public:
+ ~CGUIWindowPVRBase() override;
+
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ void UpdateButtons() override;
+ bool OnAction(const CAction& action) override;
+ void SetInvalid() override;
+ bool CanBeActivated() const override;
+
+ bool UseFileDirectories() override { return false; }
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+ virtual void NotifyEvent(const PVREvent& event);
+
+ /*!
+ * @brief Refresh window content.
+ * @return true, if refresh succeeded, false otherwise.
+ */
+ bool DoRefresh() { return Refresh(true); }
+
+ bool ActivatePreviousChannelGroup();
+ bool ActivateNextChannelGroup();
+ bool OpenChannelGroupSelectionDialog();
+
+ protected:
+ CGUIWindowPVRBase(bool bRadio, int id, const std::string& xmlFile);
+
+ virtual std::string GetDirectoryPath() = 0;
+
+ virtual void ClearData();
+
+ /*!
+ * @brief Init this window's channel group with the currently active (the "playing") channel group.
+ * @return true if group could be set, false otherwise.
+ */
+ bool InitChannelGroup();
+
+ /*!
+ * @brief Get the channel group for this window.
+ * @return the group or null, if no group set.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetChannelGroup();
+
+ /*!
+ * @brief Set a new channel group, start listening to this group, optionally update window content.
+ * @param group The new group.
+ * @param bUpdate if true, window content will be updated.
+ */
+ void SetChannelGroup(std::shared_ptr<CPVRChannelGroup> &&group, bool bUpdate = true);
+
+ virtual void UpdateSelectedItemPath();
+
+ CCriticalSection m_critSection;
+ std::string m_channelGroupPath;
+ bool m_bRadio;
+ std::atomic_bool m_bUpdating = {false};
+
+ private:
+ /*!
+ * @brief Show or update the progress dialog.
+ * @param strText The current status.
+ * @param iProgress The current progress in %.
+ */
+ void ShowProgressDialog(const std::string& strText, int iProgress);
+
+ /*!
+ * @brief Hide the progress dialog if it's visible.
+ */
+ void HideProgressDialog();
+
+ std::unique_ptr<CGUIPVRChannelGroupsSelector> m_channelGroupsSelector;
+ std::shared_ptr<CPVRChannelGroup> m_channelGroup;
+ XbmcThreads::EndTime<> m_refreshTimeout;
+ CGUIDialogProgressBarHandle* m_progressHandle; /*!< progress dialog that is displayed while the pvr manager is loading */
+ };
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRChannels.cpp b/xbmc/pvr/windows/GUIWindowPVRChannels.cpp
new file mode 100644
index 0000000..2b476f7
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRChannels.cpp
@@ -0,0 +1,414 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIWindowPVRChannels.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIRadioButtonControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "input/actions/Action.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "pvr/dialogs/GUIDialogPVRChannelManager.h"
+#include "pvr/dialogs/GUIDialogPVRGroupManager.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <mutex>
+#include <string>
+
+using namespace PVR;
+
+CGUIWindowPVRChannelsBase::CGUIWindowPVRChannelsBase(bool bRadio,
+ int id,
+ const std::string& xmlFile)
+ : CGUIWindowPVRBase(bRadio, id, xmlFile), m_bShowHiddenChannels(false)
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().RegisterChannelNumberInputHandler(this);
+}
+
+CGUIWindowPVRChannelsBase::~CGUIWindowPVRChannelsBase()
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().DeregisterChannelNumberInputHandler(
+ this);
+}
+
+std::string CGUIWindowPVRChannelsBase::GetRootPath() const
+{
+ //! @todo Would it make sense to change GetRootPath() declaration in CGUIMediaWindow
+ //! to be non-const to get rid of the const_cast's here?
+
+ CGUIWindowPVRChannelsBase* pThis = const_cast<CGUIWindowPVRChannelsBase*>(this);
+ if (pThis->InitChannelGroup())
+ return pThis->GetDirectoryPath();
+
+ return CGUIWindowPVRBase::GetRootPath();
+}
+
+void CGUIWindowPVRChannelsBase::GetContextButtons(int itemNumber, CContextButtons& buttons)
+{
+ // Add parent buttons before the Manage button
+ CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons);
+
+ buttons.Add(CONTEXT_BUTTON_EDIT, 16106); /* Manage... */
+}
+
+bool CGUIWindowPVRChannelsBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return false;
+
+ return OnContextButtonManage(m_vecItems->Get(itemNumber), button) ||
+ CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowPVRChannelsBase::Update(const std::string& strDirectory,
+ bool updateFilterPath /* = true */)
+{
+ bool bReturn = CGUIWindowPVRBase::Update(strDirectory);
+
+ if (bReturn)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ /* empty list for hidden channels */
+ if (m_vecItems->GetObjectCount() == 0 && m_bShowHiddenChannels)
+ {
+ /* show the visible channels instead */
+ m_bShowHiddenChannels = false;
+ lock.unlock();
+ Update(GetDirectoryPath());
+ }
+ }
+ return bReturn;
+}
+
+void CGUIWindowPVRChannelsBase::UpdateButtons()
+{
+ CGUIRadioButtonControl* btnShowHidden =
+ static_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_BTNSHOWHIDDEN));
+ if (btnShowHidden)
+ {
+ btnShowHidden->SetVisible(CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->GetGroupAll(m_bRadio)
+ ->GetNumHiddenChannels() > 0);
+ btnShowHidden->SetSelected(m_bShowHiddenChannels);
+ }
+
+ CGUIWindowPVRBase::UpdateButtons();
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, m_bShowHiddenChannels ? g_localizeStrings.Get(19022)
+ : GetChannelGroup()->GroupName());
+}
+
+bool CGUIWindowPVRChannelsBase::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case REMOTE_0:
+ case REMOTE_1:
+ case REMOTE_2:
+ case REMOTE_3:
+ case REMOTE_4:
+ case REMOTE_5:
+ case REMOTE_6:
+ case REMOTE_7:
+ case REMOTE_8:
+ case REMOTE_9:
+ AppendChannelNumberCharacter(static_cast<char>(action.GetID() - REMOTE_0) + '0');
+ return true;
+
+ case ACTION_CHANNEL_NUMBER_SEP:
+ AppendChannelNumberCharacter(CPVRChannelNumber::SEPARATOR);
+ return true;
+ }
+
+ return CGUIWindowPVRBase::OnAction(action);
+}
+
+bool CGUIWindowPVRChannelsBase::OnMessage(CGUIMessage& message)
+{
+ bool bReturn = false;
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ const CPVRChannelsPath path(message.GetStringParam(0));
+ if (path.IsValid() && path.IsChannelGroup())
+ {
+ // if a path to a channel group is given we must init
+ // that group instead of last played/selected group
+ m_channelGroupPath = message.GetStringParam(0);
+ }
+ break;
+ }
+
+ case GUI_MSG_CLICKED:
+ if (message.GetSenderId() == m_viewControl.GetCurrentControl())
+ {
+ if (message.GetParam1() == ACTION_SELECT_ITEM ||
+ message.GetParam1() == ACTION_MOUSE_LEFT_CLICK)
+ {
+ // If direct channel number input is active, select the entered channel.
+ if (CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNumberInputHandler()
+ .CheckInputAndExecuteAction())
+ {
+ bReturn = true;
+ break;
+ }
+ }
+
+ int iItem = m_viewControl.GetSelectedItem();
+ if (iItem >= 0 && iItem < m_vecItems->Size())
+ {
+ bReturn = true;
+ switch (message.GetParam1())
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ case ACTION_PLAYER_PLAY:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ *(m_vecItems->Get(iItem)), true);
+ break;
+ case ACTION_SHOW_INFO:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(
+ *(m_vecItems->Get(iItem)));
+ break;
+ case ACTION_DELETE_ITEM:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().HideChannel(
+ *(m_vecItems->Get(iItem)));
+ break;
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ OnPopupMenu(iItem);
+ break;
+ default:
+ bReturn = false;
+ break;
+ }
+ }
+ }
+ else if (message.GetSenderId() == CONTROL_BTNSHOWHIDDEN)
+ {
+ CGUIRadioButtonControl* radioButton =
+ static_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_BTNSHOWHIDDEN));
+ if (radioButton)
+ {
+ m_bShowHiddenChannels = radioButton->IsSelected();
+ Update(GetDirectoryPath());
+ }
+
+ bReturn = true;
+ }
+ else if (message.GetSenderId() == CONTROL_BTNFILTERCHANNELS)
+ {
+ std::string filter = GetProperty("filter").asString();
+ CGUIKeyboardFactory::ShowAndGetFilter(filter, false);
+ OnFilterItems(filter);
+ UpdateButtons();
+
+ bReturn = true;
+ }
+ break;
+
+ case GUI_MSG_REFRESH_LIST:
+ {
+ switch (static_cast<PVREvent>(message.GetParam1()))
+ {
+ case PVREvent::ChannelGroup:
+ case PVREvent::CurrentItem:
+ case PVREvent::Epg:
+ case PVREvent::EpgActiveItem:
+ case PVREvent::EpgContainer:
+ case PVREvent::RecordingsInvalidated:
+ case PVREvent::Timers:
+ SetInvalid();
+ break;
+
+ case PVREvent::ChannelGroupInvalidated:
+ Refresh(true);
+ break;
+
+ default:
+ break;
+ }
+ break;
+ }
+ }
+
+ return bReturn || CGUIWindowPVRBase::OnMessage(message);
+}
+
+bool CGUIWindowPVRChannelsBase::OnContextButtonManage(const CFileItemPtr& item,
+ CONTEXT_BUTTON button)
+{
+ bool bReturn = false;
+
+ if (button == CONTEXT_BUTTON_EDIT)
+ {
+ // Create context sub menu
+ CContextButtons buttons;
+ buttons.Add(CONTEXT_BUTTON_GROUP_MANAGER, 19048);
+ buttons.Add(CONTEXT_BUTTON_CHANNEL_MANAGER, 19199);
+ buttons.Add(CONTEXT_BUTTON_UPDATE_EPG, 19251);
+
+ // Get the response
+ int choice = CGUIDialogContextMenu::ShowAndGetChoice(buttons);
+ if (choice >= 0)
+ {
+ switch (static_cast<CONTEXT_BUTTON>(choice))
+ {
+ case CONTEXT_BUTTON_GROUP_MANAGER:
+ ShowGroupManager();
+ break;
+ case CONTEXT_BUTTON_CHANNEL_MANAGER:
+ ShowChannelManager();
+ break;
+ case CONTEXT_BUTTON_UPDATE_EPG:
+ UpdateEpg(item);
+ break;
+ default:
+ break;
+ }
+
+ // Refresh the list in case anything was changed
+ Refresh(true);
+ }
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+void CGUIWindowPVRChannelsBase::UpdateEpg(const CFileItemPtr& item)
+{
+ const std::shared_ptr<CPVRChannel> channel(item->GetPVRChannelInfoTag());
+
+ if (!CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{19251}, // "Update guide information"
+ CVariant{19252}, // "Schedule guide update for this channel?"
+ CVariant{""}, CVariant{channel->ChannelName()}))
+ return;
+
+ const std::shared_ptr<CPVREpg> epg = channel->GetEPG();
+ if (epg)
+ {
+ epg->ForceUpdate();
+
+ const std::string strMessage =
+ StringUtils::Format("{}: '{}'",
+ g_localizeStrings.Get(19253), // "Guide update scheduled for channel"
+ channel->ChannelName());
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info,
+ g_localizeStrings.Get(19166), // "PVR information"
+ strMessage);
+ }
+ else
+ {
+ const std::string strMessage =
+ StringUtils::Format("{}: '{}'",
+ g_localizeStrings.Get(19254), // "Guide update failed for channel"
+ channel->ChannelName());
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(19166), // "PVR information"
+ strMessage);
+ }
+}
+
+void CGUIWindowPVRChannelsBase::ShowChannelManager()
+{
+ CGUIDialogPVRChannelManager* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRChannelManager>(
+ WINDOW_DIALOG_PVR_CHANNEL_MANAGER);
+ if (!dialog)
+ return;
+
+ dialog->SetRadio(m_bRadio);
+
+ const int iItem = m_viewControl.GetSelectedItem();
+ dialog->Open(iItem >= 0 && iItem < m_vecItems->Size() ? m_vecItems->Get(iItem) : nullptr);
+}
+
+void CGUIWindowPVRChannelsBase::ShowGroupManager()
+{
+ /* Load group manager dialog */
+ CGUIDialogPVRGroupManager* pDlgInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGroupManager>(
+ WINDOW_DIALOG_PVR_GROUP_MANAGER);
+ if (!pDlgInfo)
+ return;
+
+ pDlgInfo->SetRadio(m_bRadio);
+ pDlgInfo->Open();
+}
+
+void CGUIWindowPVRChannelsBase::OnInputDone()
+{
+ const CPVRChannelNumber channelNumber = GetChannelNumber();
+ if (channelNumber.IsValid())
+ {
+ int itemIndex = 0;
+ for (const CFileItemPtr& channel : *m_vecItems)
+ {
+ if (channel->GetPVRChannelGroupMemberInfoTag()->ChannelNumber() == channelNumber)
+ {
+ m_viewControl.SetSelectedItem(itemIndex);
+ return;
+ }
+ ++itemIndex;
+ }
+ }
+}
+
+void CGUIWindowPVRChannelsBase::GetChannelNumbers(std::vector<std::string>& channelNumbers)
+{
+ const std::shared_ptr<CPVRChannelGroup> group = GetChannelGroup();
+ if (group)
+ group->GetChannelNumbers(channelNumbers);
+}
+
+CGUIWindowPVRTVChannels::CGUIWindowPVRTVChannels()
+ : CGUIWindowPVRChannelsBase(false, WINDOW_TV_CHANNELS, "MyPVRChannels.xml")
+{
+}
+
+std::string CGUIWindowPVRTVChannels::GetDirectoryPath()
+{
+ return CPVRChannelsPath(false, m_bShowHiddenChannels, GetChannelGroup()->GroupName());
+}
+
+CGUIWindowPVRRadioChannels::CGUIWindowPVRRadioChannels()
+ : CGUIWindowPVRChannelsBase(true, WINDOW_RADIO_CHANNELS, "MyPVRChannels.xml")
+{
+}
+
+std::string CGUIWindowPVRRadioChannels::GetDirectoryPath()
+{
+ return CPVRChannelsPath(true, m_bShowHiddenChannels, GetChannelGroup()->GroupName());
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRChannels.h b/xbmc/pvr/windows/GUIWindowPVRChannels.h
new file mode 100644
index 0000000..ef8e848
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRChannels.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "dialogs/GUIDialogContextMenu.h"
+#include "pvr/PVRChannelNumberInputHandler.h"
+#include "pvr/windows/GUIWindowPVRBase.h"
+
+#include <string>
+
+namespace PVR
+{
+class CGUIWindowPVRChannelsBase : public CGUIWindowPVRBase, public CPVRChannelNumberInputHandler
+{
+public:
+ CGUIWindowPVRChannelsBase(bool bRadio, int id, const std::string& xmlFile);
+ ~CGUIWindowPVRChannelsBase() override;
+
+ std::string GetRootPath() const override;
+ bool OnMessage(CGUIMessage& message) override;
+ void GetContextButtons(int itemNumber, CContextButtons& buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ void UpdateButtons() override;
+ bool OnAction(const CAction& action) override;
+
+ // CPVRChannelNumberInputHandler implementation
+ void GetChannelNumbers(std::vector<std::string>& channelNumbers) override;
+ void OnInputDone() override;
+
+private:
+ bool OnContextButtonManage(const CFileItemPtr& item, CONTEXT_BUTTON button);
+
+ void ShowChannelManager();
+ void ShowGroupManager();
+ void UpdateEpg(const CFileItemPtr& item);
+
+protected:
+ bool m_bShowHiddenChannels;
+};
+
+class CGUIWindowPVRTVChannels : public CGUIWindowPVRChannelsBase
+{
+public:
+ CGUIWindowPVRTVChannels();
+
+protected:
+ std::string GetDirectoryPath() override;
+};
+
+class CGUIWindowPVRRadioChannels : public CGUIWindowPVRChannelsBase
+{
+public:
+ CGUIWindowPVRRadioChannels();
+
+protected:
+ std::string GetDirectoryPath() override;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/windows/GUIWindowPVRGuide.cpp b/xbmc/pvr/windows/GUIWindowPVRGuide.cpp
new file mode 100644
index 0000000..f4c247f
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRGuide.cpp
@@ -0,0 +1,973 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIWindowPVRGuide.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "addons/Skin.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/GUIEPGGridContainer.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "view/GUIViewState.h"
+
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+using namespace KODI::MESSAGING;
+using namespace PVR;
+using namespace std::chrono_literals;
+
+CGUIWindowPVRGuideBase::CGUIWindowPVRGuideBase(bool bRadio, int id, const std::string& xmlFile)
+ : CGUIWindowPVRBase(bRadio, id, xmlFile)
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().RegisterChannelNumberInputHandler(this);
+}
+
+CGUIWindowPVRGuideBase::~CGUIWindowPVRGuideBase()
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().DeregisterChannelNumberInputHandler(
+ this);
+
+ m_bRefreshTimelineItems = false;
+ m_bSyncRefreshTimelineItems = false;
+ StopRefreshTimelineItemsThread();
+}
+
+CGUIEPGGridContainer* CGUIWindowPVRGuideBase::GetGridControl()
+{
+ return dynamic_cast<CGUIEPGGridContainer*>(GetControl(m_viewControl.GetCurrentControl()));
+}
+
+void CGUIWindowPVRGuideBase::InitEpgGridControl()
+{
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ {
+ CPVRManager& mgr = CServiceBroker::GetPVRManager();
+
+ const std::shared_ptr<CPVRChannel> channel = mgr.ChannelGroups()->GetByPath(
+ mgr.Get<PVR::GUI::Channels>().GetSelectedChannelPath(m_bRadio));
+
+ if (channel)
+ {
+ m_bChannelSelectionRestored = epgGridContainer->SetChannel(channel);
+ epgGridContainer->JumpToDate(
+ mgr.PlaybackState()->GetPlaybackTime(channel->ClientID(), channel->UniqueID()));
+ }
+ else
+ {
+ m_bChannelSelectionRestored = false;
+ epgGridContainer->JumpToNow();
+ }
+
+ if (!epgGridContainer->HasData())
+ m_bSyncRefreshTimelineItems = true; // force data update on first window open
+ }
+
+ StartRefreshTimelineItemsThread();
+}
+
+void CGUIWindowPVRGuideBase::ClearData()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_cachedChannelGroup.reset();
+ }
+
+ CGUIWindowPVRBase::ClearData();
+}
+
+void CGUIWindowPVRGuideBase::OnInitWindow()
+{
+ if (m_guiState)
+ m_viewControl.SetCurrentView(m_guiState->GetViewAsControl(), false);
+
+ if (InitChannelGroup()) // no channels -> lazy init
+ InitEpgGridControl();
+
+ CGUIWindowPVRBase::OnInitWindow();
+}
+
+void CGUIWindowPVRGuideBase::OnDeinitWindow(int nextWindowID)
+{
+ StopRefreshTimelineItemsThread();
+
+ m_bChannelSelectionRestored = false;
+
+ CGUIDialog* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_GUIDE_CONTROLS);
+ if (dialog && dialog->IsDialogRunning())
+ {
+ dialog->Close();
+ }
+
+ CGUIWindowPVRBase::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIWindowPVRGuideBase::StartRefreshTimelineItemsThread()
+{
+ StopRefreshTimelineItemsThread();
+ m_refreshTimelineItemsThread.reset(new CPVRRefreshTimelineItemsThread(this));
+ m_refreshTimelineItemsThread->Create();
+}
+
+void CGUIWindowPVRGuideBase::StopRefreshTimelineItemsThread()
+{
+ if (m_refreshTimelineItemsThread)
+ m_refreshTimelineItemsThread->Stop();
+}
+
+void CGUIWindowPVRGuideBase::NotifyEvent(const PVREvent& event)
+{
+ if (event == PVREvent::Epg || event == PVREvent::EpgContainer ||
+ event == PVREvent::ChannelGroupInvalidated || event == PVREvent::ChannelGroup)
+ {
+ m_bRefreshTimelineItems = true;
+ // no base class call => do async refresh
+ return;
+ }
+ else if (event == PVREvent::ChannelPlaybackStopped)
+ {
+ if (m_guiState && m_guiState->GetSortMethod().sortBy == SortByLastPlayed)
+ {
+ // set dirty to force sync refresh
+ m_bSyncRefreshTimelineItems = true;
+ }
+ }
+
+ // do sync refresh if dirty
+ CGUIWindowPVRBase::NotifyEvent(event);
+}
+
+void CGUIWindowPVRGuideBase::SetInvalid()
+{
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ epgGridContainer->SetInvalid();
+
+ CGUIWindowPVRBase::SetInvalid();
+}
+
+void CGUIWindowPVRGuideBase::GetContextButtons(int itemNumber, CContextButtons& buttons)
+{
+ CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons);
+
+ buttons.Add(CONTEXT_BUTTON_NAVIGATE, 19326); // Navigate...
+}
+
+void CGUIWindowPVRGuideBase::UpdateSelectedItemPath()
+{
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ epgGridContainer->GetSelectedChannelGroupMember();
+ if (groupMember)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().SetSelectedChannelPath(
+ m_bRadio, groupMember->Path());
+ }
+}
+
+void CGUIWindowPVRGuideBase::UpdateButtons()
+{
+ CGUIWindowPVRBase::UpdateButtons();
+
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, g_localizeStrings.Get(19032));
+
+ const std::shared_ptr<CPVRChannelGroup> group = GetChannelGroup();
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, group ? group->GroupName() : "");
+}
+
+bool CGUIWindowPVRGuideBase::Update(const std::string& strDirectory,
+ bool updateFilterPath /* = true */)
+{
+ if (m_bUpdating)
+ {
+ // Prevent concurrent updates. Instead, let the timeline items refresh thread pick it up later.
+ m_bRefreshTimelineItems = true;
+ return true;
+ }
+
+ bool bReturn = CGUIWindowPVRBase::Update(strDirectory, updateFilterPath);
+
+ if (bReturn && !m_bChannelSelectionRestored)
+ {
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ m_bChannelSelectionRestored = epgGridContainer->SetChannel(
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetSelectedChannelPath(
+ m_bRadio));
+ }
+
+ return bReturn;
+}
+
+bool CGUIWindowPVRGuideBase::GetDirectory(const std::string& strDirectory, CFileItemList& items)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_cachedChannelGroup && *m_cachedChannelGroup != *GetChannelGroup())
+ {
+ // channel group change and not very first open of this window. force immediate update.
+ m_bSyncRefreshTimelineItems = true;
+ }
+ }
+
+ if (m_bSyncRefreshTimelineItems)
+ m_refreshTimelineItemsThread->DoRefresh(true);
+
+ const CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ {
+ const std::unique_ptr<CFileItemList> newTimeline = GetGridControl()->GetCurrentTimeLineItems();
+ items.RemoveDiscCache(GetID());
+ items.Assign(*newTimeline, false);
+ }
+
+ return true;
+}
+
+void CGUIWindowPVRGuideBase::FormatAndSort(CFileItemList& items)
+{
+ // Speedup: Nothing to do here as sorting was already done in RefreshTimelineItems
+ return;
+}
+
+CFileItemPtr CGUIWindowPVRGuideBase::GetCurrentListItem(int offset /*= 0*/)
+{
+ const CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ return epgGridContainer->GetSelectedGridItem(offset);
+
+ return {};
+}
+
+int CGUIWindowPVRGuideBase::GetCurrentListItemIndex(const std::shared_ptr<CFileItem>& item)
+{
+ return item ? item->GetProperty("TimelineIndex").asInteger() : -1;
+}
+
+bool CGUIWindowPVRGuideBase::ShouldNavigateToGridContainer(int iAction)
+{
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ CGUIControl* control = GetControl(CONTROL_LSTCHANNELGROUPS);
+ if (epgGridContainer && control && GetFocusedControlID() == control->GetID())
+ {
+ int iNavigationId = control->GetAction(iAction).GetNavigation();
+ if (iNavigationId > 0)
+ {
+ control = epgGridContainer;
+ while (control !=
+ this) // navigation target could be the grid control or one of its parent controls.
+ {
+ if (iNavigationId == control->GetID())
+ {
+ // channel group selector control's target for the action is the grid control
+ return true;
+ }
+ control = control->GetParentControl();
+ }
+ }
+ }
+ return false;
+}
+
+bool CGUIWindowPVRGuideBase::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_MOVE_UP:
+ case ACTION_MOVE_DOWN:
+ case ACTION_MOVE_LEFT:
+ case ACTION_MOVE_RIGHT:
+ {
+ // Check whether grid container is configured as channel group selector's navigation target for the given action.
+ if (ShouldNavigateToGridContainer(action.GetID()))
+ {
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ {
+ CGUIWindowPVRBase::OnAction(action);
+
+ switch (action.GetID())
+ {
+ case ACTION_MOVE_UP:
+ epgGridContainer->GoToBottom();
+ return true;
+ case ACTION_MOVE_DOWN:
+ epgGridContainer->GoToTop();
+ return true;
+ case ACTION_MOVE_LEFT:
+ epgGridContainer->GoToMostRight();
+ return true;
+ case ACTION_MOVE_RIGHT:
+ epgGridContainer->GoToMostLeft();
+ return true;
+ default:
+ break;
+ }
+ }
+ }
+ break;
+ }
+ case REMOTE_0:
+ if (GetCurrentDigitCount() == 0)
+ {
+ // single zero input is handled by epg grid container
+ break;
+ }
+ // fall-thru is intended
+ [[fallthrough]];
+ case REMOTE_1:
+ case REMOTE_2:
+ case REMOTE_3:
+ case REMOTE_4:
+ case REMOTE_5:
+ case REMOTE_6:
+ case REMOTE_7:
+ case REMOTE_8:
+ case REMOTE_9:
+ AppendChannelNumberCharacter(static_cast<char>(action.GetID() - REMOTE_0) + '0');
+ return true;
+
+ case ACTION_CHANNEL_NUMBER_SEP:
+ AppendChannelNumberCharacter(CPVRChannelNumber::SEPARATOR);
+ return true;
+ }
+
+ return CGUIWindowPVRBase::OnAction(action);
+}
+
+void CGUIWindowPVRGuideBase::RefreshView(CGUIMessage& message, bool bInitGridControl)
+{
+ CGUIWindowPVRBase::OnMessage(message);
+
+ // force grid data update
+ m_bSyncRefreshTimelineItems = true;
+
+ if (bInitGridControl)
+ InitEpgGridControl();
+
+ Refresh(true);
+}
+
+bool CGUIWindowPVRGuideBase::OnMessage(CGUIMessage& message)
+{
+ bool bReturn = false;
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ const CPVRChannelsPath path(message.GetStringParam(0));
+ if (path.IsValid() && path.IsChannelGroup())
+ {
+ // if a path to a channel group is given we must init
+ // that group instead of last played/selected group
+ m_channelGroupPath = message.GetStringParam(0);
+ }
+ break;
+ }
+
+ case GUI_MSG_ITEM_SELECTED:
+ message.SetParam1(GetCurrentListItemIndex(GetCurrentListItem()));
+ bReturn = true;
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ if (message.GetSenderId() == m_viewControl.GetCurrentControl())
+ {
+ if (message.GetParam1() == ACTION_SELECT_ITEM ||
+ message.GetParam1() == ACTION_MOUSE_LEFT_CLICK)
+ {
+ // If direct channel number input is active, select the entered channel.
+ if (CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNumberInputHandler()
+ .CheckInputAndExecuteAction())
+ {
+ bReturn = true;
+ break;
+ }
+ }
+
+ const std::shared_ptr<CFileItem> pItem = GetCurrentListItem();
+ if (pItem)
+ {
+ switch (message.GetParam1())
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ switch (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_EPG_SELECTACTION))
+ {
+ case EPG_SELECT_ACTION_CONTEXT_MENU:
+ OnPopupMenu(GetCurrentListItemIndex(pItem));
+ bReturn = true;
+ break;
+ case EPG_SELECT_ACTION_SWITCH:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(*pItem,
+ true);
+ bReturn = true;
+ break;
+ case EPG_SELECT_ACTION_PLAY_RECORDING:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(*pItem,
+ true);
+ bReturn = true;
+ break;
+ case EPG_SELECT_ACTION_INFO:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*pItem);
+ bReturn = true;
+ break;
+ case EPG_SELECT_ACTION_RECORD:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleTimer(*pItem);
+ bReturn = true;
+ break;
+ case EPG_SELECT_ACTION_SMART_SELECT:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> tag(pItem->GetEPGInfoTag());
+ if (tag)
+ {
+ const CDateTime start(tag->StartAsUTC());
+ const CDateTime end(tag->EndAsUTC());
+ const CDateTime now(CDateTime::GetUTCDateTime());
+
+ if (start <= now && now <= end)
+ {
+ // current event
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ *pItem, true);
+ }
+ else if (now < start)
+ {
+ // future event
+ if (CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(tag))
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().EditTimer(*pItem);
+ else
+ {
+ bool bCanRecord = true;
+ const std::shared_ptr<CPVRChannel> channel = CPVRItem(pItem).GetChannel();
+ if (channel)
+ bCanRecord = channel->CanRecord();
+
+ const int iTextID =
+ bCanRecord
+ ? 19302 // "Do you want to record the selected programme or to switch to the current programme?"
+ : 19344; // "Do you want to set a reminder for the selected programme or to switch to the current programme?"
+ const int iNoButtonID = bCanRecord ? 264 // No => "Record"
+ : 826; // "Set reminder"
+
+ HELPERS::DialogResponse ret =
+ HELPERS::ShowYesNoDialogText(CVariant{19096}, // "Smart select"
+ CVariant{iTextID}, CVariant{iNoButtonID},
+ CVariant{19165}); // Yes => "Switch"
+ if (ret == HELPERS::DialogResponse::CHOICE_NO)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimer(*pItem,
+ false);
+ else if (ret == HELPERS::DialogResponse::CHOICE_YES)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ *pItem, true);
+ }
+ }
+ else
+ {
+ // past event
+ if (CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(tag))
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(
+ *pItem, true);
+ else if (tag->IsPlayable())
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayEpgTag(
+ *pItem);
+ else
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*pItem);
+ }
+ bReturn = true;
+ }
+ break;
+ }
+ }
+ break;
+ case ACTION_SHOW_INFO:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*pItem);
+ bReturn = true;
+ break;
+ case ACTION_PLAYER_PLAY:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(*pItem,
+ true);
+ bReturn = true;
+ break;
+ case ACTION_RECORD:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleTimer(*pItem);
+ bReturn = true;
+ break;
+ case ACTION_PVR_SHOW_TIMER_RULE:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimerRule(*pItem, true,
+ false);
+ bReturn = true;
+ break;
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ OnPopupMenu(GetCurrentListItemIndex(pItem));
+ bReturn = true;
+ break;
+ }
+ }
+ }
+ else if (message.GetSenderId() == CONTROL_BTNVIEWASICONS ||
+ message.GetSenderId() == CONTROL_BTNSORTBY ||
+ message.GetSenderId() == CONTROL_BTNSORTASC)
+ {
+ RefreshView(message, false);
+ bReturn = true;
+ }
+ break;
+ }
+ case GUI_MSG_CHANGE_SORT_DIRECTION:
+ case GUI_MSG_CHANGE_SORT_METHOD:
+ case GUI_MSG_CHANGE_VIEW_MODE:
+ {
+ RefreshView(message, message.GetMessage() == GUI_MSG_CHANGE_VIEW_MODE);
+ bReturn = true;
+ break;
+ }
+ case GUI_MSG_REFRESH_LIST:
+ {
+ switch (static_cast<PVREvent>(message.GetParam1()))
+ {
+ case PVREvent::ManagerStarted:
+ if (InitChannelGroup())
+ InitEpgGridControl();
+ break;
+
+ case PVREvent::ChannelGroup:
+ case PVREvent::ChannelGroupInvalidated:
+ case PVREvent::ClientsInvalidated:
+ case PVREvent::ChannelPlaybackStopped:
+ case PVREvent::Epg:
+ case PVREvent::EpgContainer:
+ if (InitChannelGroup())
+ Refresh(true);
+ break;
+
+ case PVREvent::Timers:
+ case PVREvent::TimersInvalidated:
+ SetInvalid();
+ break;
+
+ default:
+ break;
+ }
+ break;
+ }
+ case GUI_MSG_SYSTEM_WAKE:
+ GotoCurrentProgramme();
+ bReturn = true;
+ break;
+ }
+
+ return bReturn || CGUIWindowPVRBase::OnMessage(message);
+}
+
+bool CGUIWindowPVRGuideBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ if (OnContextButtonNavigate(button))
+ return true;
+
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return false;
+
+ return CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+namespace
+{
+
+template<typename A>
+class CContextMenuFunctions : public CContextButtons
+{
+public:
+ explicit CContextMenuFunctions(A* instance) : m_instance(instance) {}
+
+ void Add(bool (A::*function)(), unsigned int resId)
+ {
+ CContextButtons::Add(size(), resId);
+ m_functions.emplace_back(std::bind(function, m_instance));
+ }
+
+ bool Call(int idx)
+ {
+ if (idx < 0 || idx >= static_cast<int>(m_functions.size()))
+ return false;
+
+ return m_functions[idx]();
+ }
+
+private:
+ A* m_instance = nullptr;
+ std::vector<std::function<bool()>> m_functions;
+};
+
+} // unnamed namespace
+
+bool CGUIWindowPVRGuideBase::OnContextButtonNavigate(CONTEXT_BUTTON button)
+{
+ bool bReturn = false;
+
+ if (button == CONTEXT_BUTTON_NAVIGATE)
+ {
+ if (g_SkinInfo->HasSkinFile("DialogPVRGuideControls.xml"))
+ {
+ // use controls dialog
+ CGUIDialog* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_GUIDE_CONTROLS);
+ if (dialog && !dialog->IsDialogRunning())
+ {
+ dialog->Open();
+ }
+ }
+ else
+ {
+ // use context menu
+ CContextMenuFunctions<CGUIWindowPVRGuideBase> buttons(this);
+ buttons.Add(&CGUIWindowPVRGuideBase::GotoBegin, 19063); // First programme
+ buttons.Add(&CGUIWindowPVRGuideBase::Go12HoursBack, 19317); // 12 hours back
+ buttons.Add(&CGUIWindowPVRGuideBase::GotoCurrentProgramme, 19070); // Current programme
+ buttons.Add(&CGUIWindowPVRGuideBase::Go12HoursForward, 19318); // 12 hours forward
+ buttons.Add(&CGUIWindowPVRGuideBase::GotoEnd, 19064); // Last programme
+ buttons.Add(&CGUIWindowPVRGuideBase::OpenDateSelectionDialog, 19288); // Date selector
+ buttons.Add(&CGUIWindowPVRGuideBase::GotoFirstChannel, 19322); // First channel
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV() ||
+ CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio() ||
+ CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingEpgTag())
+ buttons.Add(&CGUIWindowPVRGuideBase::GotoPlayingChannel, 19323); // Playing channel
+ buttons.Add(&CGUIWindowPVRGuideBase::GotoLastChannel, 19324); // Last channel
+ buttons.Add(&CGUIWindowPVRBase::ActivatePreviousChannelGroup, 19319); // Previous group
+ buttons.Add(&CGUIWindowPVRBase::ActivateNextChannelGroup, 19320); // Next group
+ buttons.Add(&CGUIWindowPVRBase::OpenChannelGroupSelectionDialog, 19321); // Group selector
+
+ int buttonIdx = 0;
+ int lastButtonIdx = 2; // initially select "Current programme"
+
+ // loop until canceled
+ while (buttonIdx >= 0)
+ {
+ buttonIdx = CGUIDialogContextMenu::Show(buttons, lastButtonIdx);
+ lastButtonIdx = buttonIdx;
+ buttons.Call(buttonIdx);
+ }
+ }
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIWindowPVRGuideBase::RefreshTimelineItems()
+{
+ if (m_bRefreshTimelineItems || m_bSyncRefreshTimelineItems)
+ {
+ m_bRefreshTimelineItems = false;
+ m_bSyncRefreshTimelineItems = false;
+
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group(GetChannelGroup());
+ if (!group)
+ return false;
+
+ CPVREpgContainer& epgContainer = CServiceBroker::GetPVRManager().EpgContainer();
+
+ const std::pair<CDateTime, CDateTime> dates = epgContainer.GetFirstAndLastEPGDate();
+ CDateTime startDate = dates.first;
+ CDateTime endDate = dates.second;
+ const CDateTime currentDate = CDateTime::GetUTCDateTime();
+
+ if (!startDate.IsValid())
+ startDate = currentDate;
+
+ if (!endDate.IsValid() || endDate < startDate)
+ endDate = startDate;
+
+ // limit start to past days to display
+ const int iPastDays = epgContainer.GetPastDaysToDisplay();
+ const CDateTime maxPastDate(currentDate - CDateTimeSpan(iPastDays, 0, 0, 0));
+ if (startDate < maxPastDate)
+ startDate = maxPastDate;
+
+ // limit end to future days to display
+ const int iFutureDays = epgContainer.GetFutureDaysToDisplay();
+ const CDateTime maxFutureDate(currentDate + CDateTimeSpan(iFutureDays, 0, 0, 0));
+ if (endDate > maxFutureDate)
+ endDate = maxFutureDate;
+
+ std::unique_ptr<CFileItemList> channels(new CFileItemList);
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ group->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+
+ for (const auto& groupMember : groupMembers)
+ {
+ channels->Add(std::make_shared<CFileItem>(groupMember));
+ }
+
+ if (m_guiState)
+ channels->Sort(m_guiState->GetSortMethod());
+
+ epgGridContainer->SetTimelineItems(channels, startDate, endDate);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_cachedChannelGroup = group;
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIWindowPVRGuideBase::GotoBegin()
+{
+ GetGridControl()->GoToBegin();
+ return true;
+}
+
+bool CGUIWindowPVRGuideBase::GotoEnd()
+{
+ GetGridControl()->GoToEnd();
+ return true;
+}
+
+bool CGUIWindowPVRGuideBase::GotoCurrentProgramme()
+{
+ const CPVRManager& mgr = CServiceBroker::GetPVRManager();
+ std::shared_ptr<CPVRChannel> channel = mgr.PlaybackState()->GetPlayingChannel();
+
+ if (!channel)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> playingTag = mgr.PlaybackState()->GetPlayingEpgTag();
+ if (playingTag)
+ channel = mgr.ChannelGroups()->GetChannelForEpgTag(playingTag);
+ }
+
+ if (channel)
+ GetGridControl()->GoToDate(
+ mgr.PlaybackState()->GetPlaybackTime(channel->ClientID(), channel->UniqueID()));
+ else
+ GetGridControl()->GoToNow();
+
+ return true;
+}
+
+bool CGUIWindowPVRGuideBase::OpenDateSelectionDialog()
+{
+ bool bReturn = false;
+
+ KODI::TIME::SystemTime date;
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ epgGridContainer->GetSelectedDate().GetAsSystemTime(date);
+
+ if (CGUIDialogNumeric::ShowAndGetDate(date, g_localizeStrings.Get(19288))) /* Go to date */
+ {
+ epgGridContainer->GoToDate(CDateTime(date));
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIWindowPVRGuideBase::Go12HoursBack()
+{
+ return GotoDate(-12);
+}
+
+bool CGUIWindowPVRGuideBase::Go12HoursForward()
+{
+ return GotoDate(+12);
+}
+
+bool CGUIWindowPVRGuideBase::GotoDate(int deltaHours)
+{
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ epgGridContainer->GoToDate(epgGridContainer->GetSelectedDate() +
+ CDateTimeSpan(0, deltaHours, 0, 0));
+ return true;
+}
+
+bool CGUIWindowPVRGuideBase::GotoFirstChannel()
+{
+ GetGridControl()->GoToFirstChannel();
+ return true;
+}
+
+bool CGUIWindowPVRGuideBase::GotoLastChannel()
+{
+ GetGridControl()->GoToLastChannel();
+ return true;
+}
+
+bool CGUIWindowPVRGuideBase::GotoPlayingChannel()
+{
+ const CPVRManager& mgr = CServiceBroker::GetPVRManager();
+ std::shared_ptr<CPVRChannel> channel = mgr.PlaybackState()->GetPlayingChannel();
+
+ if (!channel)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> playingTag = mgr.PlaybackState()->GetPlayingEpgTag();
+ if (playingTag)
+ channel = mgr.ChannelGroups()->GetChannelForEpgTag(playingTag);
+ }
+
+ if (channel)
+ {
+ GetGridControl()->SetChannel(channel);
+ return true;
+ }
+ return false;
+}
+
+void CGUIWindowPVRGuideBase::OnInputDone()
+{
+ const CPVRChannelNumber channelNumber = GetChannelNumber();
+ if (channelNumber.IsValid())
+ {
+ GetGridControl()->SetChannel(channelNumber);
+ }
+}
+
+void CGUIWindowPVRGuideBase::GetChannelNumbers(std::vector<std::string>& channelNumbers)
+{
+ const std::shared_ptr<CPVRChannelGroup> group = GetChannelGroup();
+ if (group)
+ group->GetChannelNumbers(channelNumbers);
+}
+
+CPVRRefreshTimelineItemsThread::CPVRRefreshTimelineItemsThread(CGUIWindowPVRGuideBase* pGuideWindow)
+ : CThread("epg-grid-refresh-timeline-items"),
+ m_pGuideWindow(pGuideWindow),
+ m_ready(true),
+ m_done(false)
+{
+}
+
+CPVRRefreshTimelineItemsThread::~CPVRRefreshTimelineItemsThread()
+{
+ // Note: CThread dtor will also call StopThread(true), but if thread worker function exits that
+ // late, it might access member variables of this which are already destroyed. Thus, stop
+ // the thread worker here and synchronously, while all members of this are still alive.
+ StopThread(true);
+}
+
+void CPVRRefreshTimelineItemsThread::Stop()
+{
+ StopThread(false);
+ m_ready.Set(); // wake up the worker thread to let it exit
+}
+
+void CPVRRefreshTimelineItemsThread::DoRefresh(bool bWait)
+{
+ m_ready.Set(); // wake up the worker thread
+
+ if (bWait)
+ {
+ m_done.Reset();
+ CGUIDialogBusy::WaitOnEvent(m_done, 100, false);
+ }
+}
+
+void CPVRRefreshTimelineItemsThread::Process()
+{
+ static const int BOOSTED_SLEEPS_THRESHOLD = 4;
+
+ int iLastEpgItemsCount = 0;
+ int iUpdatesWithoutChange = 0;
+
+ while (!m_bStop)
+ {
+ m_done.Reset();
+
+ if (m_pGuideWindow->RefreshTimelineItems() && !m_bStop)
+ {
+ CGUIMessage m(GUI_MSG_REFRESH_LIST, m_pGuideWindow->GetID(), 0,
+ static_cast<int>(PVREvent::Epg));
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(m);
+ }
+
+ if (m_bStop)
+ break;
+
+ m_done.Set();
+
+ // in order to fill the guide window asap, use a short update interval until we the
+ // same amount of epg events for BOOSTED_SLEEPS_THRESHOLD + 1 times in a row .
+ if (iUpdatesWithoutChange < BOOSTED_SLEEPS_THRESHOLD)
+ {
+ int iCurrentEpgItemsCount = m_pGuideWindow->CurrentDirectory().Size();
+
+ if (iCurrentEpgItemsCount == iLastEpgItemsCount)
+ iUpdatesWithoutChange++;
+ else
+ iUpdatesWithoutChange = 0; // reset
+
+ iLastEpgItemsCount = iCurrentEpgItemsCount;
+
+ m_ready.Wait(1000ms); // boosted update cycle
+ }
+ else
+ {
+ m_ready.Wait(5000ms); // normal update cycle
+ }
+
+ m_ready.Reset();
+ }
+
+ m_ready.Reset();
+ m_done.Set();
+}
+
+std::string CGUIWindowPVRTVGuide::GetRootPath() const
+{
+ return "pvr://guide/tv/";
+}
+
+std::string CGUIWindowPVRRadioGuide::GetRootPath() const
+{
+ return "pvr://guide/radio/";
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRGuide.h b/xbmc/pvr/windows/GUIWindowPVRGuide.h
new file mode 100644
index 0000000..87477ca
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRGuide.h
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/PVRChannelNumberInputHandler.h"
+#include "pvr/windows/GUIWindowPVRBase.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+
+#include <atomic>
+#include <memory>
+#include <string>
+
+class CFileItemList;
+class CGUIMessage;
+
+namespace PVR
+{
+enum class PVREvent;
+
+class CPVRChannelGroup;
+class CGUIEPGGridContainer;
+class CPVRRefreshTimelineItemsThread;
+
+class CGUIWindowPVRGuideBase : public CGUIWindowPVRBase, public CPVRChannelNumberInputHandler
+{
+public:
+ CGUIWindowPVRGuideBase(bool bRadio, int id, const std::string& xmlFile);
+ ~CGUIWindowPVRGuideBase() override;
+
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void GetContextButtons(int itemNumber, CContextButtons& buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ void UpdateButtons() override;
+ void SetInvalid() override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+
+ void NotifyEvent(const PVREvent& event) override;
+
+ bool RefreshTimelineItems();
+
+ // CPVRChannelNumberInputHandler implementation
+ void GetChannelNumbers(std::vector<std::string>& channelNumbers) override;
+ void OnInputDone() override;
+
+ bool GotoBegin();
+ bool GotoEnd();
+ bool GotoCurrentProgramme();
+ bool GotoDate(int deltaHours);
+ bool OpenDateSelectionDialog();
+ bool Go12HoursBack();
+ bool Go12HoursForward();
+ bool GotoFirstChannel();
+ bool GotoLastChannel();
+ bool GotoPlayingChannel();
+
+protected:
+ void UpdateSelectedItemPath() override;
+ std::string GetDirectoryPath() override { return ""; }
+ bool GetDirectory(const std::string& strDirectory, CFileItemList& items) override;
+ void FormatAndSort(CFileItemList& items) override;
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+
+ void ClearData() override;
+
+private:
+ CGUIEPGGridContainer* GetGridControl();
+ void InitEpgGridControl();
+
+ bool OnContextButtonNavigate(CONTEXT_BUTTON button);
+
+ bool ShouldNavigateToGridContainer(int iAction);
+
+ void StartRefreshTimelineItemsThread();
+ void StopRefreshTimelineItemsThread();
+
+ void RefreshView(CGUIMessage& message, bool bInitGridControl);
+
+ int GetCurrentListItemIndex(const std::shared_ptr<CFileItem>& item);
+
+ std::unique_ptr<CPVRRefreshTimelineItemsThread> m_refreshTimelineItemsThread;
+ std::atomic_bool m_bRefreshTimelineItems{false};
+ std::atomic_bool m_bSyncRefreshTimelineItems{false};
+
+ std::shared_ptr<CPVRChannelGroup> m_cachedChannelGroup;
+
+ bool m_bChannelSelectionRestored{false};
+};
+
+class CGUIWindowPVRTVGuide : public CGUIWindowPVRGuideBase
+{
+public:
+ CGUIWindowPVRTVGuide() : CGUIWindowPVRGuideBase(false, WINDOW_TV_GUIDE, "MyPVRGuide.xml") {}
+ std::string GetRootPath() const override;
+};
+
+class CGUIWindowPVRRadioGuide : public CGUIWindowPVRGuideBase
+{
+public:
+ CGUIWindowPVRRadioGuide() : CGUIWindowPVRGuideBase(true, WINDOW_RADIO_GUIDE, "MyPVRGuide.xml") {}
+ std::string GetRootPath() const override;
+};
+
+class CPVRRefreshTimelineItemsThread : public CThread
+{
+public:
+ explicit CPVRRefreshTimelineItemsThread(CGUIWindowPVRGuideBase* pGuideWindow);
+ ~CPVRRefreshTimelineItemsThread() override;
+
+ void Process() override;
+
+ void DoRefresh(bool bWait);
+ void Stop();
+
+private:
+ CGUIWindowPVRGuideBase* m_pGuideWindow;
+ CEvent m_ready;
+ CEvent m_done;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/windows/GUIWindowPVRRecordings.cpp b/xbmc/pvr/windows/GUIWindowPVRRecordings.cpp
new file mode 100644
index 0000000..d127217
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRRecordings.cpp
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIWindowPVRRecordings.h"
+
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIRadioButtonControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsRecordings.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+#include "video/VideoLibraryQueue.h"
+#include "video/VideoUtils.h"
+#include "video/windows/GUIWindowVideoBase.h"
+#include "video/windows/GUIWindowVideoNav.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+
+using namespace PVR;
+
+CGUIWindowPVRRecordingsBase::CGUIWindowPVRRecordingsBase(bool bRadio,
+ int id,
+ const std::string& xmlFile)
+ : CGUIWindowPVRBase(bRadio, id, xmlFile),
+ m_settings(
+ {CSettings::SETTING_PVRRECORD_GROUPRECORDINGS, CSettings::SETTING_MYVIDEOS_SELECTACTION})
+{
+}
+
+CGUIWindowPVRRecordingsBase::~CGUIWindowPVRRecordingsBase() = default;
+
+void CGUIWindowPVRRecordingsBase::OnWindowLoaded()
+{
+ CONTROL_SELECT(CONTROL_BTNGROUPITEMS);
+}
+
+std::string CGUIWindowPVRRecordingsBase::GetDirectoryPath()
+{
+ const std::string basePath = CPVRRecordingsPath(m_bShowDeletedRecordings, m_bRadio);
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath()
+ : basePath;
+}
+
+void CGUIWindowPVRRecordingsBase::GetContextButtons(int itemNumber, CContextButtons& buttons)
+{
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return;
+ CFileItemPtr pItem = m_vecItems->Get(itemNumber);
+
+ if (pItem->IsParentFolder())
+ {
+ // No context menu for ".." items
+ return;
+ }
+
+ bool isDeletedRecording = false;
+
+ std::shared_ptr<CPVRRecording> recording(pItem->GetPVRRecordingInfoTag());
+ if (recording)
+ {
+ isDeletedRecording = recording->IsDeleted();
+
+ if (isDeletedRecording)
+ {
+ if (m_vecItems->GetObjectCount() > 1)
+ buttons.Add(CONTEXT_BUTTON_DELETE_ALL, 19292); /* Delete all permanently */
+ }
+ }
+
+ if (!isDeletedRecording)
+ CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons);
+}
+
+bool CGUIWindowPVRRecordingsBase::OnAction(const CAction& action)
+{
+ if (action.GetID() == ACTION_PARENT_DIR || action.GetID() == ACTION_NAV_BACK)
+ {
+ CPVRRecordingsPath path(m_vecItems->GetPath());
+ if (path.IsValid() && !path.IsRecordingsRoot())
+ {
+ GoParentFolder();
+ return true;
+ }
+ }
+ else if (action.GetID() == ACTION_TOGGLE_WATCHED)
+ {
+ const std::shared_ptr<CFileItem> pItem = m_vecItems->Get(m_viewControl.GetSelectedItem());
+ if (!pItem || pItem->IsParentFolder())
+ return false;
+
+ bool bUnWatched = false;
+ if (pItem->HasPVRRecordingInfoTag())
+ bUnWatched = pItem->GetPVRRecordingInfoTag()->GetPlayCount() == 0;
+ else if (pItem->m_bIsFolder)
+ bUnWatched = pItem->GetProperty("unwatchedepisodes").asInteger() > 0;
+ else
+ return false;
+
+ CVideoLibraryQueue::GetInstance().MarkAsWatched(pItem, bUnWatched);
+ return true;
+ }
+
+ return CGUIWindowPVRBase::OnAction(action);
+}
+
+bool CGUIWindowPVRRecordingsBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return false;
+ CFileItemPtr pItem = m_vecItems->Get(itemNumber);
+
+ return OnContextButtonDeleteAll(pItem.get(), button) ||
+ CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowPVRRecordingsBase::Update(const std::string& strDirectory,
+ bool updateFilterPath /* = true */)
+{
+ m_thumbLoader.StopThread();
+
+ int iOldCount = m_vecItems->GetObjectCount();
+ const std::string oldPath = m_vecItems->GetPath();
+
+ bool bReturn = CGUIWindowPVRBase::Update(strDirectory);
+
+ if (bReturn)
+ {
+ // TODO: does it make sense to show the non-deleted recordings, although user wants
+ // to see the deleted recordings? Or is this just another hack to avoid misbehavior
+ // of CGUIMediaWindow if it has no content?
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* empty list for deleted recordings */
+ if (m_vecItems->GetObjectCount() == 0 && m_bShowDeletedRecordings)
+ {
+ /* show the normal recordings instead */
+ m_bShowDeletedRecordings = false;
+ lock.unlock();
+ Update(GetDirectoryPath());
+ return bReturn;
+ }
+ }
+
+ if (bReturn && iOldCount > 0 && m_vecItems->GetObjectCount() == 0 &&
+ oldPath == m_vecItems->GetPath())
+ {
+ /* go to the parent folder if we're in a subdirectory and for instance just deleted the last item */
+ const CPVRRecordingsPath path(m_vecItems->GetPath());
+ if (path.IsValid() && !path.IsRecordingsRoot())
+ GoParentFolder();
+ }
+ return bReturn;
+}
+
+void CGUIWindowPVRRecordingsBase::UpdateButtons()
+{
+ int iWatchMode = CMediaSettings::GetInstance().GetWatchedMode("recordings");
+ int iStringId = 257; // "Error"
+
+ if (iWatchMode == WatchedModeAll)
+ iStringId = 22015; // "All recordings"
+ else if (iWatchMode == WatchedModeUnwatched)
+ iStringId = 16101; // "Unwatched"
+ else if (iWatchMode == WatchedModeWatched)
+ iStringId = 16102; // "Watched"
+
+ SET_CONTROL_LABEL(CONTROL_BTNSHOWMODE, g_localizeStrings.Get(iStringId));
+
+ bool bGroupRecordings = m_settings.GetBoolValue(CSettings::SETTING_PVRRECORD_GROUPRECORDINGS);
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTNGROUPITEMS, bGroupRecordings);
+
+ CGUIRadioButtonControl* btnShowDeleted =
+ static_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_BTNSHOWDELETED));
+ if (btnShowDeleted)
+ {
+ btnShowDeleted->SetVisible(
+ m_bRadio ? CServiceBroker::GetPVRManager().Recordings()->HasDeletedRadioRecordings()
+ : CServiceBroker::GetPVRManager().Recordings()->HasDeletedTVRecordings());
+ btnShowDeleted->SetSelected(m_bShowDeletedRecordings);
+ }
+
+ CGUIWindowPVRBase::UpdateButtons();
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, m_bShowDeletedRecordings
+ ? g_localizeStrings.Get(19179)
+ : ""); /* Deleted recordings trash */
+
+ const CPVRRecordingsPath path(m_vecItems->GetPath());
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2,
+ bGroupRecordings && path.IsValid() ? path.GetUnescapedDirectoryPath() : "");
+}
+
+bool CGUIWindowPVRRecordingsBase::OnMessage(CGUIMessage& message)
+{
+ bool bReturn = false;
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ if (message.GetSenderId() == m_viewControl.GetCurrentControl())
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ if (iItem >= 0 && iItem < m_vecItems->Size())
+ {
+ const CFileItemPtr item(m_vecItems->Get(iItem));
+ switch (message.GetParam1())
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ case ACTION_PLAYER_PLAY:
+ {
+ const CPVRRecordingsPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsRecordingsRoot() && item->IsParentFolder())
+ {
+ // handle special 'go home' item.
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_HOME);
+ bReturn = true;
+ break;
+ }
+
+ if (!item->IsParentFolder() && message.GetParam1() == ACTION_PLAYER_PLAY)
+ {
+ if (item->m_bIsFolder)
+ {
+ if (CGUIWindowVideoNav::ShowResumeMenu(*item))
+ VIDEO_UTILS::PlayItem(item);
+ }
+ else
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(
+ *item, true /* check resume */);
+
+ bReturn = true;
+ }
+ else if (item->m_bIsFolder)
+ {
+ // recording folders and ".." folders in subfolders are handled by base class.
+ bReturn = false;
+ }
+ else
+ {
+ switch (m_settings.GetIntValue(CSettings::SETTING_MYVIDEOS_SELECTACTION))
+ {
+ case SELECT_ACTION_CHOOSE:
+ OnPopupMenu(iItem);
+ bReturn = true;
+ break;
+ case SELECT_ACTION_PLAY_OR_RESUME:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(
+ *item, true /* check resume */);
+ bReturn = true;
+ break;
+ case SELECT_ACTION_RESUME:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().ResumePlayRecording(
+ *item, true /* fall back to play if no resume possible */);
+ bReturn = true;
+ break;
+ case SELECT_ACTION_INFO:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo(
+ *item);
+ bReturn = true;
+ break;
+ default:
+ bReturn = false;
+ break;
+ }
+ }
+ break;
+ }
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ OnPopupMenu(iItem);
+ bReturn = true;
+ break;
+ case ACTION_SHOW_INFO:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo(*item);
+ bReturn = true;
+ break;
+ case ACTION_DELETE_ITEM:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().DeleteRecording(*item);
+ bReturn = true;
+ break;
+ default:
+ bReturn = false;
+ break;
+ }
+ }
+ }
+ else if (message.GetSenderId() == CONTROL_BTNGROUPITEMS)
+ {
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->ToggleBool(CSettings::SETTING_PVRRECORD_GROUPRECORDINGS);
+ settings->Save();
+ Refresh(true);
+ }
+ else if (message.GetSenderId() == CONTROL_BTNSHOWDELETED)
+ {
+ CGUIRadioButtonControl* radioButton =
+ static_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_BTNSHOWDELETED));
+ if (radioButton)
+ {
+ m_bShowDeletedRecordings = radioButton->IsSelected();
+ Update(GetDirectoryPath());
+ }
+ bReturn = true;
+ }
+ else if (message.GetSenderId() == CONTROL_BTNSHOWMODE)
+ {
+ CMediaSettings::GetInstance().CycleWatchedMode("recordings");
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ OnFilterItems(GetProperty("filter").asString());
+ UpdateButtons();
+ return true;
+ }
+ break;
+ case GUI_MSG_REFRESH_LIST:
+ {
+ switch (static_cast<PVREvent>(message.GetParam1()))
+ {
+ case PVREvent::CurrentItem:
+ case PVREvent::Epg:
+ case PVREvent::EpgActiveItem:
+ case PVREvent::EpgContainer:
+ case PVREvent::Timers:
+ SetInvalid();
+ break;
+
+ case PVREvent::RecordingsInvalidated:
+ case PVREvent::TimersInvalidated:
+ Refresh(true);
+ break;
+
+ default:
+ break;
+ }
+ break;
+ }
+ }
+
+ return bReturn || CGUIWindowPVRBase::OnMessage(message);
+}
+
+bool CGUIWindowPVRRecordingsBase::OnContextButtonDeleteAll(CFileItem* item, CONTEXT_BUTTON button)
+{
+ if (button == CONTEXT_BUTTON_DELETE_ALL)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().DeleteAllRecordingsFromTrash();
+ return true;
+ }
+
+ return false;
+}
+
+void CGUIWindowPVRRecordingsBase::OnPrepareFileItems(CFileItemList& items)
+{
+ if (items.IsEmpty())
+ return;
+
+ CFileItemList files;
+ for (const auto& item : items)
+ {
+ if (!item->m_bIsFolder)
+ files.Add(item);
+ }
+
+ if (!files.IsEmpty())
+ {
+ if (m_database.Open())
+ {
+ CGUIWindowVideoBase::LoadVideoInfo(files, m_database, false);
+ m_database.Close();
+ }
+ m_thumbLoader.Load(files);
+ }
+
+ CGUIWindowPVRBase::OnPrepareFileItems(items);
+}
+
+bool CGUIWindowPVRRecordingsBase::GetFilteredItems(const std::string& filter, CFileItemList& items)
+{
+ bool listchanged = CGUIWindowPVRBase::GetFilteredItems(filter, items);
+
+ int watchMode = CMediaSettings::GetInstance().GetWatchedMode("recordings");
+
+ CFileItemPtr item;
+ for (int i = 0; i < items.Size(); i++)
+ {
+ item = items.Get(i);
+
+ if (item->IsParentFolder()) // Don't delete the go to parent folder
+ continue;
+
+ if (!item->HasPVRRecordingInfoTag())
+ continue;
+
+ int iPlayCount = item->GetPVRRecordingInfoTag()->GetPlayCount();
+ if ((watchMode == WatchedModeWatched && iPlayCount == 0) ||
+ (watchMode == WatchedModeUnwatched && iPlayCount > 0))
+ {
+ items.Remove(i);
+ i--;
+ listchanged = true;
+ }
+ }
+
+ // Remove the parent folder item, if it's the only item in the folder.
+ if (items.GetObjectCount() == 0 && items.GetFileCount() > 0 && items.Get(0)->IsParentFolder())
+ items.Remove(0);
+
+ return listchanged;
+}
+
+std::string CGUIWindowPVRTVRecordings::GetRootPath() const
+{
+ return CPVRRecordingsPath(m_bShowDeletedRecordings, false);
+}
+
+std::string CGUIWindowPVRRadioRecordings::GetRootPath() const
+{
+ return CPVRRecordingsPath(m_bShowDeletedRecordings, true);
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRRecordings.h b/xbmc/pvr/windows/GUIWindowPVRRecordings.h
new file mode 100644
index 0000000..5091fde
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRRecordings.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "dialogs/GUIDialogContextMenu.h"
+#include "pvr/settings/PVRSettings.h"
+#include "pvr/windows/GUIWindowPVRBase.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoThumbLoader.h"
+
+#include <string>
+
+class CFileItem;
+
+namespace PVR
+{
+class CGUIWindowPVRRecordingsBase : public CGUIWindowPVRBase
+{
+public:
+ CGUIWindowPVRRecordingsBase(bool bRadio, int id, const std::string& xmlFile);
+ ~CGUIWindowPVRRecordingsBase() override;
+
+ void OnWindowLoaded() override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void GetContextButtons(int itemNumber, CContextButtons& buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ void UpdateButtons() override;
+
+protected:
+ std::string GetDirectoryPath() override;
+ void OnPrepareFileItems(CFileItemList& items) override;
+ bool GetFilteredItems(const std::string& filter, CFileItemList& items) override;
+
+ bool m_bShowDeletedRecordings{false};
+
+private:
+ bool OnContextButtonDeleteAll(CFileItem* item, CONTEXT_BUTTON button);
+
+ CVideoThumbLoader m_thumbLoader;
+ CVideoDatabase m_database;
+ CPVRSettings m_settings;
+};
+
+class CGUIWindowPVRTVRecordings : public CGUIWindowPVRRecordingsBase
+{
+public:
+ CGUIWindowPVRTVRecordings()
+ : CGUIWindowPVRRecordingsBase(false, WINDOW_TV_RECORDINGS, "MyPVRRecordings.xml")
+ {
+ }
+ std::string GetRootPath() const override;
+};
+
+class CGUIWindowPVRRadioRecordings : public CGUIWindowPVRRecordingsBase
+{
+public:
+ CGUIWindowPVRRadioRecordings()
+ : CGUIWindowPVRRecordingsBase(true, WINDOW_RADIO_RECORDINGS, "MyPVRRecordings.xml")
+ {
+ }
+ std::string GetRootPath() const override;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/windows/GUIWindowPVRSearch.cpp b/xbmc/pvr/windows/GUIWindowPVRSearch.cpp
new file mode 100644
index 0000000..f0cd3fe
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRSearch.cpp
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIWindowPVRSearch.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/dialogs/GUIDialogPVRGuideSearch.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "pvr/epg/EpgSearchPath.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "threads/IRunnable.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+#include <memory>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+namespace
+{
+class AsyncSearchAction : private IRunnable
+{
+public:
+ AsyncSearchAction() = delete;
+ AsyncSearchAction(CFileItemList* items, CPVREpgSearchFilter* filter)
+ : m_items(items), m_filter(filter)
+ {
+ }
+ bool Execute();
+
+private:
+ // IRunnable implementation
+ void Run() override;
+
+ CFileItemList* m_items;
+ CPVREpgSearchFilter* m_filter;
+};
+
+bool AsyncSearchAction::Execute()
+{
+ CGUIDialogBusy::Wait(this, 100, false);
+ return true;
+}
+
+void AsyncSearchAction::Run()
+{
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> results =
+ CServiceBroker::GetPVRManager().EpgContainer().GetTags(m_filter->GetEpgSearchData());
+ m_filter->SetEpgSearchDataFiltered();
+
+ // Tags can still contain false positives, for search criteria that cannot be handled via
+ // database. So, run extended search filters on what we got from the database.
+ for (auto it = results.begin(); it != results.end();)
+ {
+ it = results.erase(std::remove_if(results.begin(), results.end(),
+ [this](const std::shared_ptr<CPVREpgInfoTag>& entry) {
+ return !m_filter->FilterEntry(entry);
+ }),
+ results.end());
+ }
+
+ if (m_filter->ShouldRemoveDuplicates())
+ m_filter->RemoveDuplicates(results);
+
+ m_filter->SetLastExecutedDateTime(CDateTime::GetUTCDateTime());
+
+ for (const auto& tag : results)
+ {
+ m_items->Add(std::make_shared<CFileItem>(tag));
+ }
+}
+} // unnamed namespace
+
+CGUIWindowPVRSearchBase::CGUIWindowPVRSearchBase(bool bRadio, int id, const std::string& xmlFile)
+ : CGUIWindowPVRBase(bRadio, id, xmlFile), m_bSearchConfirmed(false)
+{
+}
+
+CGUIWindowPVRSearchBase::~CGUIWindowPVRSearchBase()
+{
+}
+
+void CGUIWindowPVRSearchBase::GetContextButtons(int itemNumber, CContextButtons& buttons)
+{
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return;
+
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ const bool bIsSavedSearchesRoot = (path.IsValid() && path.IsSavedSearchesRoot());
+ if (!bIsSavedSearchesRoot)
+ buttons.Add(CONTEXT_BUTTON_CLEAR, 19232); // "Clear search results"
+
+ CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons);
+}
+
+bool CGUIWindowPVRSearchBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return false;
+ CFileItemPtr pItem = m_vecItems->Get(itemNumber);
+
+ return OnContextButtonClear(pItem.get(), button) ||
+ CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+void CGUIWindowPVRSearchBase::SetItemToSearch(const CFileItem& item)
+{
+ if (item.HasEPGSearchFilter())
+ {
+ SetSearchFilter(item.GetEPGSearchFilter());
+ }
+ else if (item.IsUsablePVRRecording())
+ {
+ SetSearchFilter(std::make_shared<CPVREpgSearchFilter>(m_bRadio));
+ m_searchfilter->SetSearchPhrase(item.GetPVRRecordingInfoTag()->m_strTitle);
+ }
+ else
+ {
+ SetSearchFilter(std::make_shared<CPVREpgSearchFilter>(m_bRadio));
+
+ const std::shared_ptr<CPVREpgInfoTag> epgTag(CPVRItem(item).GetEpgInfoTag());
+ if (epgTag && !CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ m_searchfilter->SetSearchPhrase(epgTag->Title());
+ }
+
+ ExecuteSearch();
+}
+
+void CGUIWindowPVRSearchBase::OnPrepareFileItems(CFileItemList& items)
+{
+ if (m_bSearchConfirmed)
+ {
+ items.Clear();
+ }
+
+ if (items.IsEmpty())
+ {
+ auto item = std::make_shared<CFileItem>(CPVREpgSearchPath::PATH_SEARCH_DIALOG, false);
+ item->SetLabel(g_localizeStrings.Get(
+ m_searchfilter == nullptr ? 19335 : 19336)); // "New search..." / "Edit search..."
+ item->SetLabelPreformatted(true);
+ item->SetSpecialSort(SortSpecialOnTop);
+ item->SetArt("icon", "DefaultPVRSearch.png");
+ items.Add(item);
+
+ item = std::make_shared<CFileItem>(m_bRadio ? CPVREpgSearchPath::PATH_RADIO_SAVEDSEARCHES
+ : CPVREpgSearchPath::PATH_TV_SAVEDSEARCHES,
+ true);
+ item->SetLabel(g_localizeStrings.Get(19337)); // "Saved searches"
+ item->SetLabelPreformatted(true);
+ item->SetSpecialSort(SortSpecialOnTop);
+ item->SetArt("icon", "DefaultFolder.png");
+ items.Add(item);
+ }
+
+ if (m_bSearchConfirmed)
+ {
+ const int itemCount = items.GetObjectCount();
+
+ AsyncSearchAction(&items, m_searchfilter.get()).Execute();
+
+ if (items.GetObjectCount() == itemCount)
+ {
+ HELPERS::ShowOKDialogText(CVariant{284}, // "No results found"
+ m_searchfilter->GetSearchTerm());
+ }
+ }
+}
+
+bool CGUIWindowPVRSearchBase::OnAction(const CAction& action)
+{
+ if (action.GetID() == ACTION_PARENT_DIR || action.GetID() == ACTION_NAV_BACK)
+ {
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsSavedSearchesRoot())
+ {
+ // Go to root dir and show previous search results if any
+ m_bSearchConfirmed = (m_searchfilter != nullptr);
+ GoParentFolder();
+ return true;
+ }
+ }
+ return CGUIWindowPVRBase::OnAction(action);
+}
+
+bool CGUIWindowPVRSearchBase::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_CLICKED)
+ {
+ if (message.GetSenderId() == m_viewControl.GetCurrentControl())
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ if (iItem >= 0 && iItem < m_vecItems->Size())
+ {
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+
+ /* process actions */
+ switch (message.GetParam1())
+ {
+ case ACTION_SHOW_INFO:
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ {
+ const CPVREpgSearchPath path(pItem->GetPath());
+ const bool bIsSavedSearch = (path.IsValid() && path.IsSavedSearch());
+ const bool bIsSavedSearchesRoot = (path.IsValid() && path.IsSavedSearchesRoot());
+
+ if (message.GetParam1() != ACTION_SHOW_INFO)
+ {
+ if (pItem->IsParentFolder())
+ {
+ // Go to root dir and show previous search results if any
+ m_bSearchConfirmed = (m_searchfilter != nullptr);
+ break; // handled by base class
+ }
+
+ if (bIsSavedSearchesRoot)
+ {
+ // List saved searches
+ m_bSearchConfirmed = false;
+ break; // handled by base class
+ }
+
+ if (bIsSavedSearch)
+ {
+ // Execute selected saved search
+ SetSearchFilter(pItem->GetEPGSearchFilter());
+ ExecuteSearch();
+ return true;
+ }
+ }
+
+ if (bIsSavedSearch)
+ {
+ OpenDialogSearch(*pItem);
+ }
+ else if (pItem->GetPath() == CPVREpgSearchPath::PATH_SEARCH_DIALOG)
+ {
+ OpenDialogSearch(m_searchfilter);
+ }
+ else
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*pItem);
+ }
+ return true;
+ }
+
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ OnPopupMenu(iItem);
+ return true;
+
+ case ACTION_RECORD:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleTimer(*pItem);
+ return true;
+ }
+ }
+ }
+ }
+ else if (message.GetMessage() == GUI_MSG_REFRESH_LIST)
+ {
+ if (static_cast<PVREvent>(message.GetParam1()) == PVREvent::SavedSearchesInvalidated)
+ {
+ Refresh(true);
+
+ // Refresh triggered by deleted saved search?
+ if (m_searchfilter)
+ {
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ const bool bIsSavedSearchesRoot = (path.IsValid() && path.IsSavedSearchesRoot());
+ if (bIsSavedSearchesRoot)
+ {
+ const std::string filterPath = m_searchfilter->GetPath();
+ bool bFound = false;
+ for (const auto& item : *m_vecItems)
+ {
+ const auto filter = item->GetEPGSearchFilter();
+ if (filter && filter->GetPath() == filterPath)
+ {
+ bFound = true;
+ break;
+ }
+ }
+ if (!bFound)
+ SetSearchFilter(nullptr);
+ }
+ }
+ }
+ }
+ else if (message.GetMessage() == GUI_MSG_WINDOW_INIT)
+ {
+ const CPVREpgSearchPath path(message.GetStringParam(0));
+ if (path.IsValid() && path.IsSavedSearch())
+ {
+ const std::shared_ptr<CPVREpgSearchFilter> filter =
+ CServiceBroker::GetPVRManager().EpgContainer().GetSavedSearchById(path.IsRadio(),
+ path.GetId());
+ if (filter)
+ {
+ SetSearchFilter(filter);
+ m_bSearchConfirmed = true;
+ }
+ }
+ }
+
+ return CGUIWindowPVRBase::OnMessage(message);
+}
+
+bool CGUIWindowPVRSearchBase::Update(const std::string& strDirectory,
+ bool updateFilterPath /* = true */)
+{
+ if (m_vecItems->GetObjectCount() > 0)
+ {
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsSavedSearchesRoot())
+ {
+ const std::string oldPath = m_vecItems->GetPath();
+
+ const bool bReturn = CGUIWindowPVRBase::Update(strDirectory);
+
+ if (bReturn && oldPath == m_vecItems->GetPath() && m_vecItems->GetObjectCount() == 0)
+ {
+ // Go to parent folder if we're in a subdir and for instance just deleted the last item
+ GoParentFolder();
+ }
+ return bReturn;
+ }
+ }
+ return CGUIWindowPVRBase::Update(strDirectory);
+}
+
+void CGUIWindowPVRSearchBase::UpdateButtons()
+{
+ CGUIWindowPVRBase::UpdateButtons();
+
+ bool bSavedSearchesRoot = false;
+ std::string header;
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsSavedSearchesRoot())
+ {
+ bSavedSearchesRoot = true;
+ header = g_localizeStrings.Get(19337); // "Saved searches"
+ }
+
+ if (header.empty() && m_searchfilter)
+ {
+ header = m_searchfilter->GetTitle();
+ if (header.empty())
+ {
+ header = m_searchfilter->GetSearchTerm();
+ StringUtils::Trim(header, "\"");
+ }
+ }
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, header);
+
+ if (!bSavedSearchesRoot && m_searchfilter && m_searchfilter->IsChanged())
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, g_localizeStrings.Get(19342)); // "[not saved]"
+ else if (!bSavedSearchesRoot && m_searchfilter)
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, g_localizeStrings.Get(19343)); // "[saved]"
+ else
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, "");
+}
+
+bool CGUIWindowPVRSearchBase::OnContextButtonClear(CFileItem* item, CONTEXT_BUTTON button)
+{
+ bool bReturn = false;
+
+ if (button == CONTEXT_BUTTON_CLEAR)
+ {
+ bReturn = true;
+
+ m_bSearchConfirmed = false;
+ SetSearchFilter(nullptr);
+
+ Refresh(true);
+ }
+
+ return bReturn;
+}
+
+CGUIDialogPVRGuideSearch::Result CGUIWindowPVRSearchBase::OpenDialogSearch(const CFileItem& item)
+{
+ const auto searchFilter = item.GetEPGSearchFilter();
+ if (!searchFilter)
+ return CGUIDialogPVRGuideSearch::Result::CANCEL;
+
+ return OpenDialogSearch(searchFilter);
+}
+
+CGUIDialogPVRGuideSearch::Result CGUIWindowPVRSearchBase::OpenDialogSearch(
+ const std::shared_ptr<CPVREpgSearchFilter>& searchFilter)
+{
+ CGUIDialogPVRGuideSearch* dlgSearch =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGuideSearch>(
+ WINDOW_DIALOG_PVR_GUIDE_SEARCH);
+
+ if (!dlgSearch)
+ return CGUIDialogPVRGuideSearch::Result::CANCEL;
+
+ const std::shared_ptr<CPVREpgSearchFilter> tmpSearchFilter =
+ searchFilter != nullptr ? std::make_shared<CPVREpgSearchFilter>(*searchFilter)
+ : std::make_shared<CPVREpgSearchFilter>(m_bRadio);
+
+ dlgSearch->SetFilterData(tmpSearchFilter);
+
+ /* Open dialog window */
+ dlgSearch->Open();
+
+ const CGUIDialogPVRGuideSearch::Result result = dlgSearch->GetResult();
+ if (result == CGUIDialogPVRGuideSearch::Result::SEARCH)
+ {
+ SetSearchFilter(tmpSearchFilter);
+ ExecuteSearch();
+ }
+ else if (result == CGUIDialogPVRGuideSearch::Result::SAVE)
+ {
+ CServiceBroker::GetPVRManager().EpgContainer().PersistSavedSearch(*tmpSearchFilter);
+ if (searchFilter)
+ searchFilter->SetDatabaseId(tmpSearchFilter->GetDatabaseId());
+
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsSearchRoot())
+ {
+ SetSearchFilter(tmpSearchFilter);
+ ExecuteSearch();
+ }
+ }
+
+ return result;
+}
+
+void CGUIWindowPVRSearchBase::ExecuteSearch()
+{
+ m_bSearchConfirmed = true;
+
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsSavedSearchesRoot())
+ {
+ GoParentFolder();
+ }
+ else if (IsActive())
+ {
+ Refresh(true);
+ }
+
+ // Save if not a transient search
+ if (m_searchfilter->GetDatabaseId() != -1)
+ CServiceBroker::GetPVRManager().EpgContainer().UpdateSavedSearchLastExecuted(*m_searchfilter);
+}
+
+void CGUIWindowPVRSearchBase::SetSearchFilter(
+ const std::shared_ptr<CPVREpgSearchFilter>& searchFilter)
+{
+ if (m_searchfilter && m_searchfilter->IsChanged() &&
+ (!searchFilter || m_searchfilter->GetPath() != searchFilter->GetPath()))
+ {
+ bool bCanceled = false;
+ if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{14117}, // "Warning"
+ CVariant{19341}, // "Save the current search?"
+ CVariant{},
+ CVariant{!m_searchfilter->GetTitle().empty()
+ ? m_searchfilter->GetTitle()
+ : m_searchfilter->GetSearchTerm()},
+ bCanceled, CVariant{107}, // "Yes"
+ CVariant{106}, // "No"
+ CGUIDialogYesNo::NO_TIMEOUT) &&
+ !bCanceled)
+ {
+ std::string title = m_searchfilter->GetTitle();
+ if (title.empty())
+ {
+ title = m_searchfilter->GetSearchTerm();
+ if (title.empty())
+ title = g_localizeStrings.Get(137); // "Search"
+ else
+ StringUtils::Trim(title, "\"");
+
+ m_searchfilter->SetTitle(title);
+ }
+ CServiceBroker::GetPVRManager().EpgContainer().PersistSavedSearch(*m_searchfilter);
+ }
+ }
+ m_searchfilter = searchFilter;
+}
+
+std::string CGUIWindowPVRTVSearch::GetRootPath() const
+{
+ return CPVREpgSearchPath::PATH_TV_SEARCH;
+}
+
+std::string CGUIWindowPVRTVSearch::GetStartFolder(const std::string& dir)
+{
+ return CPVREpgSearchPath::PATH_TV_SEARCH;
+}
+
+std::string CGUIWindowPVRTVSearch::GetDirectoryPath()
+{
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), CPVREpgSearchPath::PATH_TV_SEARCH)
+ ? m_vecItems->GetPath()
+ : CPVREpgSearchPath::PATH_TV_SEARCH;
+}
+
+std::string CGUIWindowPVRRadioSearch::GetRootPath() const
+{
+ return CPVREpgSearchPath::PATH_RADIO_SEARCH;
+}
+
+std::string CGUIWindowPVRRadioSearch::GetStartFolder(const std::string& dir)
+{
+ return CPVREpgSearchPath::PATH_RADIO_SEARCH;
+}
+
+std::string CGUIWindowPVRRadioSearch::GetDirectoryPath()
+{
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), CPVREpgSearchPath::PATH_RADIO_SEARCH)
+ ? m_vecItems->GetPath()
+ : CPVREpgSearchPath::PATH_RADIO_SEARCH;
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRSearch.h b/xbmc/pvr/windows/GUIWindowPVRSearch.h
new file mode 100644
index 0000000..d6f6a35
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRSearch.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "dialogs/GUIDialogContextMenu.h"
+#include "pvr/dialogs/GUIDialogPVRGuideSearch.h"
+#include "pvr/windows/GUIWindowPVRBase.h"
+
+#include <memory>
+#include <string>
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVREpgSearchFilter;
+
+class CGUIWindowPVRSearchBase : public CGUIWindowPVRBase
+{
+public:
+ CGUIWindowPVRSearchBase(bool bRadio, int id, const std::string& xmlFile);
+ ~CGUIWindowPVRSearchBase() override;
+
+ bool OnAction(const CAction& action) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void GetContextButtons(int itemNumber, CContextButtons& buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ void UpdateButtons() override;
+
+ /*!
+ * @brief set the item to search similar events for.
+ * @param item the epg event to search similar events for.
+ */
+ void SetItemToSearch(const CFileItem& item);
+
+ /*!
+ * @brief Open the search dialog for the given search filter item.
+ * @param item the epg search filter.
+ * @return The result of the dialog
+ */
+ CGUIDialogPVRGuideSearch::Result OpenDialogSearch(const CFileItem& item);
+
+protected:
+ void OnPrepareFileItems(CFileItemList& items) override;
+
+private:
+ bool OnContextButtonClear(CFileItem* item, CONTEXT_BUTTON button);
+
+ CGUIDialogPVRGuideSearch::Result OpenDialogSearch(
+ const std::shared_ptr<CPVREpgSearchFilter>& searchFilter);
+
+ void ExecuteSearch();
+
+ void SetSearchFilter(const std::shared_ptr<CPVREpgSearchFilter>& searchFilter);
+
+ bool m_bSearchConfirmed;
+ std::shared_ptr<CPVREpgSearchFilter> m_searchfilter;
+};
+
+class CGUIWindowPVRTVSearch : public CGUIWindowPVRSearchBase
+{
+public:
+ CGUIWindowPVRTVSearch() : CGUIWindowPVRSearchBase(false, WINDOW_TV_SEARCH, "MyPVRSearch.xml") {}
+
+protected:
+ std::string GetRootPath() const override;
+ std::string GetStartFolder(const std::string& dir) override;
+ std::string GetDirectoryPath() override;
+};
+
+class CGUIWindowPVRRadioSearch : public CGUIWindowPVRSearchBase
+{
+public:
+ CGUIWindowPVRRadioSearch() : CGUIWindowPVRSearchBase(true, WINDOW_RADIO_SEARCH, "MyPVRSearch.xml")
+ {
+ }
+
+protected:
+ std::string GetRootPath() const override;
+ std::string GetStartFolder(const std::string& dir) override;
+ std::string GetDirectoryPath() override;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/windows/GUIWindowPVRTimerRules.cpp b/xbmc/pvr/windows/GUIWindowPVRTimerRules.cpp
new file mode 100644
index 0000000..1b203d4
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRTimerRules.cpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIWindowPVRTimerRules.h"
+
+#include "FileItem.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "utils/URIUtils.h"
+
+using namespace PVR;
+
+CGUIWindowPVRTVTimerRules::CGUIWindowPVRTVTimerRules()
+: CGUIWindowPVRTimersBase(false, WINDOW_TV_TIMER_RULES, "MyPVRTimers.xml")
+{
+}
+
+std::string CGUIWindowPVRTVTimerRules::GetRootPath() const
+{
+ return CPVRTimersPath::PATH_TV_TIMER_RULES;
+}
+
+std::string CGUIWindowPVRTVTimerRules::GetDirectoryPath()
+{
+ const std::string basePath(CPVRTimersPath(false, true).GetPath());
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() : basePath;
+}
+
+CGUIWindowPVRRadioTimerRules::CGUIWindowPVRRadioTimerRules()
+: CGUIWindowPVRTimersBase(true, WINDOW_RADIO_TIMER_RULES, "MyPVRTimers.xml")
+{
+}
+
+std::string CGUIWindowPVRRadioTimerRules::GetRootPath() const
+{
+ return CPVRTimersPath::PATH_RADIO_TIMER_RULES;
+}
+
+std::string CGUIWindowPVRRadioTimerRules::GetDirectoryPath()
+{
+ const std::string basePath(CPVRTimersPath(true, true).GetPath());
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() : basePath;
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRTimerRules.h b/xbmc/pvr/windows/GUIWindowPVRTimerRules.h
new file mode 100644
index 0000000..dc3f050
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRTimerRules.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/windows/GUIWindowPVRTimersBase.h"
+
+#include <string>
+
+namespace PVR
+{
+ class CGUIWindowPVRTVTimerRules : public CGUIWindowPVRTimersBase
+ {
+ public:
+ CGUIWindowPVRTVTimerRules();
+ ~CGUIWindowPVRTVTimerRules() override = default;
+
+ protected:
+ std::string GetRootPath() const override;
+ std::string GetDirectoryPath() override;
+ };
+
+ class CGUIWindowPVRRadioTimerRules : public CGUIWindowPVRTimersBase
+ {
+ public:
+ CGUIWindowPVRRadioTimerRules();
+ ~CGUIWindowPVRRadioTimerRules() override = default;
+
+ protected:
+ std::string GetRootPath() const override;
+ std::string GetDirectoryPath() override;
+ };
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRTimers.cpp b/xbmc/pvr/windows/GUIWindowPVRTimers.cpp
new file mode 100644
index 0000000..610571b
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRTimers.cpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIWindowPVRTimers.h"
+
+#include "FileItem.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "utils/URIUtils.h"
+
+using namespace PVR;
+
+CGUIWindowPVRTVTimers::CGUIWindowPVRTVTimers()
+: CGUIWindowPVRTimersBase(false, WINDOW_TV_TIMERS, "MyPVRTimers.xml")
+{
+}
+
+std::string CGUIWindowPVRTVTimers::GetRootPath() const
+{
+ return CPVRTimersPath::PATH_TV_TIMERS;
+}
+
+std::string CGUIWindowPVRTVTimers::GetDirectoryPath()
+{
+ const std::string basePath(CPVRTimersPath(false, false).GetPath());
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() : basePath;
+}
+
+CGUIWindowPVRRadioTimers::CGUIWindowPVRRadioTimers()
+: CGUIWindowPVRTimersBase(true, WINDOW_RADIO_TIMERS, "MyPVRTimers.xml")
+{
+}
+
+std::string CGUIWindowPVRRadioTimers::GetRootPath() const
+{
+ return CPVRTimersPath::PATH_RADIO_TIMERS;
+}
+
+std::string CGUIWindowPVRRadioTimers::GetDirectoryPath()
+{
+ const std::string basePath(CPVRTimersPath(true, false).GetPath());
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() : basePath;
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRTimers.h b/xbmc/pvr/windows/GUIWindowPVRTimers.h
new file mode 100644
index 0000000..2193ac1
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRTimers.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/windows/GUIWindowPVRTimersBase.h"
+
+#include <string>
+
+namespace PVR
+{
+ class CGUIWindowPVRTVTimers : public CGUIWindowPVRTimersBase
+ {
+ public:
+ CGUIWindowPVRTVTimers();
+
+ protected:
+ std::string GetRootPath() const override;
+ std::string GetDirectoryPath() override;
+ };
+
+ class CGUIWindowPVRRadioTimers : public CGUIWindowPVRTimersBase
+ {
+ public:
+ CGUIWindowPVRRadioTimers();
+
+ protected:
+ std::string GetRootPath() const override;
+ std::string GetDirectoryPath() override;
+ };
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRTimersBase.cpp b/xbmc/pvr/windows/GUIWindowPVRTimersBase.cpp
new file mode 100644
index 0000000..41677eb
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRTimersBase.cpp
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIWindowPVRTimersBase.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+
+#include <memory>
+#include <string>
+
+using namespace PVR;
+
+CGUIWindowPVRTimersBase::CGUIWindowPVRTimersBase(bool bRadio, int id, const std::string& xmlFile)
+ : CGUIWindowPVRBase(bRadio, id, xmlFile)
+{
+}
+
+CGUIWindowPVRTimersBase::~CGUIWindowPVRTimersBase() = default;
+
+bool CGUIWindowPVRTimersBase::OnAction(const CAction& action)
+{
+ if (action.GetID() == ACTION_PARENT_DIR || action.GetID() == ACTION_NAV_BACK)
+ {
+ CPVRTimersPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsTimerRule())
+ {
+ m_currentFileItem.reset();
+ GoParentFolder();
+ return true;
+ }
+ }
+ return CGUIWindowPVRBase::OnAction(action);
+}
+
+void CGUIWindowPVRTimersBase::OnPrepareFileItems(CFileItemList& items)
+{
+ const CPVRTimersPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsTimersRoot())
+ {
+ const auto item = std::make_shared<CFileItem>(CPVRTimersPath::PATH_ADDTIMER, false);
+ item->SetLabel(g_localizeStrings.Get(19026)); // "Add timer..."
+ item->SetLabelPreformatted(true);
+ item->SetSpecialSort(SortSpecialOnTop);
+ item->SetArt("icon", "DefaultTVShows.png");
+
+ items.AddFront(item, 0);
+ }
+}
+
+bool CGUIWindowPVRTimersBase::Update(const std::string& strDirectory,
+ bool updateFilterPath /* = true */)
+{
+ int iOldCount = m_vecItems->GetObjectCount();
+ const std::string oldPath = m_vecItems->GetPath();
+
+ bool bReturn = CGUIWindowPVRBase::Update(strDirectory);
+
+ if (bReturn && iOldCount > 0 && m_vecItems->GetObjectCount() == 0 &&
+ oldPath == m_vecItems->GetPath())
+ {
+ /* go to the parent folder if we're in a subdirectory and for instance just deleted the last item */
+ const CPVRTimersPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsTimerRule())
+ {
+ m_currentFileItem.reset();
+ GoParentFolder();
+ }
+ }
+
+ return bReturn;
+}
+
+void CGUIWindowPVRTimersBase::UpdateButtons()
+{
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTNHIDEDISABLEDTIMERS,
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRTIMERS_HIDEDISABLEDTIMERS));
+
+ CGUIWindowPVRBase::UpdateButtons();
+
+ std::string strHeaderTitle;
+ if (m_currentFileItem && m_currentFileItem->HasPVRTimerInfoTag())
+ {
+ std::shared_ptr<CPVRTimerInfoTag> timer = m_currentFileItem->GetPVRTimerInfoTag();
+ strHeaderTitle = timer->Title();
+ }
+
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, strHeaderTitle);
+}
+
+bool CGUIWindowPVRTimersBase::OnMessage(CGUIMessage& message)
+{
+ bool bReturn = false;
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ if (message.GetSenderId() == m_viewControl.GetCurrentControl())
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ if (iItem >= 0 && iItem < m_vecItems->Size())
+ {
+ bReturn = true;
+ switch (message.GetParam1())
+ {
+ case ACTION_SHOW_INFO:
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ {
+ CFileItemPtr item(m_vecItems->Get(iItem));
+ if (item->m_bIsFolder && (message.GetParam1() != ACTION_SHOW_INFO))
+ {
+ m_currentFileItem = item;
+ bReturn = false; // folders are handled by base class
+ }
+ else
+ {
+ m_currentFileItem.reset();
+ ActionShowTimer(*item);
+ }
+ break;
+ }
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ OnPopupMenu(iItem);
+ break;
+ case ACTION_DELETE_ITEM:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().DeleteTimer(
+ *(m_vecItems->Get(iItem)));
+ break;
+ default:
+ bReturn = false;
+ break;
+ }
+ }
+ }
+ else if (message.GetSenderId() == CONTROL_BTNHIDEDISABLEDTIMERS)
+ {
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->ToggleBool(CSettings::SETTING_PVRTIMERS_HIDEDISABLEDTIMERS);
+ settings->Save();
+ Update(GetDirectoryPath());
+ bReturn = true;
+ }
+ break;
+ case GUI_MSG_REFRESH_LIST:
+ {
+ switch (static_cast<PVREvent>(message.GetParam1()))
+ {
+ case PVREvent::CurrentItem:
+ case PVREvent::Epg:
+ case PVREvent::EpgActiveItem:
+ case PVREvent::EpgContainer:
+ case PVREvent::Timers:
+ SetInvalid();
+ break;
+
+ case PVREvent::TimersInvalidated:
+ Refresh(true);
+ break;
+
+ default:
+ break;
+ }
+ break;
+ }
+ }
+
+ return bReturn || CGUIWindowPVRBase::OnMessage(message);
+}
+
+bool CGUIWindowPVRTimersBase::ActionShowTimer(const CFileItem& item)
+{
+ bool bReturn = false;
+
+ /* Check if "Add timer..." entry is selected, if yes
+ create a new timer and open settings dialog, otherwise
+ open settings for selected timer entry */
+ if (URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ bReturn = CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimer(m_bRadio);
+ else
+ bReturn = CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().EditTimer(item);
+
+ return bReturn;
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRTimersBase.h b/xbmc/pvr/windows/GUIWindowPVRTimersBase.h
new file mode 100644
index 0000000..2f4232d
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRTimersBase.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "pvr/windows/GUIWindowPVRBase.h"
+
+#include <memory>
+
+class CFileItem;
+
+namespace PVR
+{
+class CGUIWindowPVRTimersBase : public CGUIWindowPVRBase
+{
+public:
+ CGUIWindowPVRTimersBase(bool bRadio, int id, const std::string& xmlFile);
+ ~CGUIWindowPVRTimersBase() override;
+
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void OnPrepareFileItems(CFileItemList& items) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ void UpdateButtons() override;
+
+private:
+ bool ActionShowTimer(const CFileItem& item);
+
+ std::shared_ptr<CFileItem> m_currentFileItem;
+};
+} // namespace PVR