summaryrefslogtreecommitdiffstats
path: root/xbmc/pvr/channels/PVRChannelGroup.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--xbmc/pvr/channels/PVRChannelGroup.cpp1176
1 files changed, 1176 insertions, 0 deletions
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);
+}