summaryrefslogtreecommitdiffstats
path: root/xbmc/pvr/channels
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 18:07:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 18:07:22 +0000
commitc04dcc2e7d834218ef2d4194331e383402495ae1 (patch)
tree7333e38d10d75386e60f336b80c2443c1166031d /xbmc/pvr/channels
parentInitial commit. (diff)
downloadkodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz
kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xbmc/pvr/channels')
-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
23 files changed, 6867 insertions, 0 deletions
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);
+}