summaryrefslogtreecommitdiffstats
path: root/xbmc/pvr/epg
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/pvr/epg')
-rw-r--r--xbmc/pvr/epg/CMakeLists.txt22
-rw-r--r--xbmc/pvr/epg/Epg.cpp554
-rw-r--r--xbmc/pvr/epg/Epg.h327
-rw-r--r--xbmc/pvr/epg/EpgChannelData.cpp107
-rw-r--r--xbmc/pvr/epg/EpgChannelData.h59
-rw-r--r--xbmc/pvr/epg/EpgContainer.cpp1028
-rw-r--r--xbmc/pvr/epg/EpgContainer.h360
-rw-r--r--xbmc/pvr/epg/EpgDatabase.cpp1494
-rw-r--r--xbmc/pvr/epg/EpgDatabase.h377
-rw-r--r--xbmc/pvr/epg/EpgInfoTag.cpp681
-rw-r--r--xbmc/pvr/epg/EpgInfoTag.h513
-rw-r--r--xbmc/pvr/epg/EpgSearchData.h44
-rw-r--r--xbmc/pvr/epg/EpgSearchFilter.cpp403
-rw-r--r--xbmc/pvr/epg/EpgSearchFilter.h171
-rw-r--r--xbmc/pvr/epg/EpgSearchPath.cpp64
-rw-r--r--xbmc/pvr/epg/EpgSearchPath.h49
-rw-r--r--xbmc/pvr/epg/EpgTagsCache.cpp179
-rw-r--r--xbmc/pvr/epg/EpgTagsCache.h61
-rw-r--r--xbmc/pvr/epg/EpgTagsContainer.cpp668
-rw-r--r--xbmc/pvr/epg/EpgTagsContainer.h227
20 files changed, 7388 insertions, 0 deletions
diff --git a/xbmc/pvr/epg/CMakeLists.txt b/xbmc/pvr/epg/CMakeLists.txt
new file mode 100644
index 0000000..0ea75b7
--- /dev/null
+++ b/xbmc/pvr/epg/CMakeLists.txt
@@ -0,0 +1,22 @@
+set(SOURCES EpgContainer.cpp
+ Epg.cpp
+ EpgDatabase.cpp
+ EpgInfoTag.cpp
+ EpgSearchFilter.cpp
+ EpgSearchPath.cpp
+ EpgChannelData.cpp
+ EpgTagsCache.cpp
+ EpgTagsContainer.cpp)
+
+set(HEADERS Epg.h
+ EpgContainer.h
+ EpgDatabase.h
+ EpgInfoTag.h
+ EpgSearchData.h
+ EpgSearchFilter.h
+ EpgSearchPath.h
+ EpgChannelData.h
+ EpgTagsCache.h
+ EpgTagsContainer.h)
+
+core_add_library(pvr_epg)
diff --git a/xbmc/pvr/epg/Epg.cpp b/xbmc/pvr/epg/Epg.cpp
new file mode 100644
index 0000000..1112b2f
--- /dev/null
+++ b/xbmc/pvr/epg/Epg.cpp
@@ -0,0 +1,554 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "Epg.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+CPVREpg::CPVREpg(int iEpgID,
+ const std::string& strName,
+ const std::string& strScraperName,
+ const std::shared_ptr<CPVREpgDatabase>& database)
+ : m_iEpgID(iEpgID),
+ m_strName(strName),
+ m_strScraperName(strScraperName),
+ m_channelData(new CPVREpgChannelData),
+ m_tags(m_iEpgID, m_channelData, database)
+{
+}
+
+CPVREpg::CPVREpg(int iEpgID,
+ const std::string& strName,
+ const std::string& strScraperName,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database)
+ : m_bChanged(true),
+ m_iEpgID(iEpgID),
+ m_strName(strName),
+ m_strScraperName(strScraperName),
+ m_channelData(channelData),
+ m_tags(m_iEpgID, m_channelData, database)
+{
+}
+
+CPVREpg::~CPVREpg()
+{
+ Clear();
+}
+
+void CPVREpg::ForceUpdate()
+{
+ m_bUpdatePending = true;
+ m_events.Publish(PVREvent::EpgUpdatePending);
+}
+
+void CPVREpg::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_tags.Clear();
+}
+
+void CPVREpg::Cleanup(int iPastDays)
+{
+ const CDateTime cleanupTime = CDateTime::GetUTCDateTime() - CDateTimeSpan(iPastDays, 0, 0, 0);
+ Cleanup(cleanupTime);
+}
+
+void CPVREpg::Cleanup(const CDateTime& time)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_tags.Cleanup(time);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagNow() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetActiveTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagNext() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetNextStartingTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagPrevious() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetLastEndedTag();
+}
+
+bool CPVREpg::CheckPlayingEvent()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_tags.UpdateActiveTag())
+ {
+ m_events.Publish(PVREvent::EpgActiveItem);
+ return true;
+ }
+ return false;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagByBroadcastId(unsigned int iUniqueBroadcastId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetTag(iUniqueBroadcastId);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagByDatabaseId(int iDatabaseId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetTagByDatabaseID(iDatabaseId);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagBetween(const CDateTime& beginTime, const CDateTime& endTime, bool bUpdateFromClient /* = false */)
+{
+ std::shared_ptr<CPVREpgInfoTag> tag;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ tag = m_tags.GetTagBetween(beginTime, endTime);
+
+ if (!tag && bUpdateFromClient)
+ {
+ // not found locally; try to fetch from client
+ time_t b;
+ beginTime.GetAsTime(b);
+ time_t e;
+ endTime.GetAsTime(e);
+
+ const std::shared_ptr<CPVREpg> tmpEpg = std::make_shared<CPVREpg>(
+ m_iEpgID, m_strName, m_strScraperName, m_channelData, std::shared_ptr<CPVREpgDatabase>());
+ if (tmpEpg->UpdateFromScraper(b, e, true))
+ tag = tmpEpg->GetTagBetween(beginTime, endTime, false);
+
+ if (tag)
+ m_tags.UpdateEntry(tag);
+ }
+
+ return tag;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpg::GetTimeline(
+ const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetTimeline(timelineStart, timelineEnd, minEventEnd, maxEventStart);
+}
+
+bool CPVREpg::UpdateEntries(const CPVREpg& epg)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* copy over tags */
+ m_tags.UpdateEntries(epg.m_tags);
+
+ /* update the last scan time of this table */
+ m_lastScanTime = CDateTime::GetUTCDateTime();
+ m_bUpdateLastScanTime = true;
+
+ m_events.Publish(PVREvent::Epg);
+ return true;
+}
+
+namespace
+{
+
+bool IsTagExpired(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ // Respect epg linger time.
+ const int iPastDays = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_EPG_PAST_DAYSTODISPLAY);
+ const CDateTime cleanupTime(CDateTime::GetUTCDateTime() - CDateTimeSpan(iPastDays, 0, 0, 0));
+
+ return tag->EndAsUTC() < cleanupTime;
+}
+
+} // unnamed namespace
+
+bool CPVREpg::UpdateEntry(const EPG_TAG* data, int iClientId)
+{
+ if (!data)
+ return false;
+
+ const std::shared_ptr<CPVREpgInfoTag> tag =
+ std::make_shared<CPVREpgInfoTag>(*data, iClientId, m_channelData, m_iEpgID);
+
+ return !IsTagExpired(tag) && m_tags.UpdateEntry(tag);
+}
+
+bool CPVREpg::UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE newState)
+{
+ bool bRet = true;
+ bool bNotify = true;
+
+ if (newState == EPG_EVENT_CREATED || newState == EPG_EVENT_UPDATED)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bRet = !IsTagExpired(tag) && m_tags.UpdateEntry(tag);
+ }
+ else if (newState == EPG_EVENT_DELETED)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVREpgInfoTag> existingTag = m_tags.GetTag(tag->UniqueBroadcastID());
+ if (!existingTag)
+ {
+ bRet = false;
+ }
+ else
+ {
+ if (IsTagExpired(existingTag))
+ {
+ m_tags.DeleteEntry(existingTag);
+ }
+ else
+ {
+ bNotify = false;
+ }
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unknown epg event state value: {}", newState);
+ bRet = false;
+ }
+
+ if (bRet && bNotify)
+ m_events.Publish(PVREvent::EpgItemUpdate);
+
+ return bRet;
+}
+
+bool CPVREpg::Update(time_t start,
+ time_t end,
+ int iUpdateTime,
+ int iPastDays,
+ const std::shared_ptr<CPVREpgDatabase>& database,
+ bool bForceUpdate /* = false */)
+{
+ bool bUpdate = false;
+ std::shared_ptr<CPVREpg> tmpEpg;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_lastScanTime.IsValid())
+ {
+ database->GetLastEpgScanTime(m_iEpgID, &m_lastScanTime);
+
+ if (!m_lastScanTime.IsValid())
+ {
+ m_lastScanTime.SetFromUTCDateTime(time_t(0));
+ m_bUpdateLastScanTime = true;
+ }
+ }
+
+ // enforce advanced settings update interval override for channels with no EPG data
+ if (m_tags.IsEmpty() && m_channelData->ChannelId() > 0) //! @todo why the channelid check?
+ iUpdateTime = CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iEpgUpdateEmptyTagsInterval;
+
+ if (bForceUpdate)
+ {
+ bUpdate = true;
+ }
+ else
+ {
+ // check if we have to update
+ time_t iNow = 0;
+ CDateTime::GetUTCDateTime().GetAsTime(iNow);
+
+ time_t iLastUpdate = 0;
+ m_lastScanTime.GetAsTime(iLastUpdate);
+
+ bUpdate = (iNow > iLastUpdate + iUpdateTime);
+ }
+
+ if (bUpdate)
+ {
+ tmpEpg = std::make_shared<CPVREpg>(m_iEpgID, m_strName, m_strScraperName, m_channelData,
+ std::shared_ptr<CPVREpgDatabase>());
+ }
+ }
+
+ // remove obsolete tags
+ Cleanup(iPastDays);
+
+ bool bGrabSuccess = true;
+
+ if (bUpdate)
+ {
+ bGrabSuccess = tmpEpg->UpdateFromScraper(start, end, bForceUpdate) && UpdateEntries(*tmpEpg);
+
+ if (!bGrabSuccess)
+ CLog::LogF(LOGERROR, "Failed to update table '{}'", Name());
+ }
+
+ m_bUpdatePending = false;
+ return bGrabSuccess;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpg::GetTags() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetAllTags();
+}
+
+bool CPVREpg::QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ // Note: It is guaranteed that both this EPG instance and database instance are already
+ // locked when this method gets called! No additional locking is needed here!
+
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return false;
+ }
+
+ if (m_iEpgID <= 0 || m_bChanged)
+ {
+ const int iId = database->Persist(*this, m_iEpgID > 0);
+ if (iId > 0 && m_iEpgID != iId)
+ {
+ m_iEpgID = iId;
+ m_tags.SetEpgID(iId);
+ }
+ }
+
+ if (m_tags.NeedsSave())
+ m_tags.QueuePersistQuery();
+
+ if (m_bUpdateLastScanTime)
+ database->QueuePersistLastEpgScanTimeQuery(m_iEpgID, m_lastScanTime);
+
+ m_bChanged = false;
+ m_bUpdateLastScanTime = false;
+
+ return true;
+}
+
+bool CPVREpg::QueueDeleteQueries(const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return false;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // delete own epg db entry
+ database->QueueDeleteEpgQuery(*this);
+
+ // delete last scan time db entry for this epg
+ database->QueueDeleteLastEpgScanTimeQuery(*this);
+
+ // delete all tags for this epg from db
+ m_tags.QueueDelete();
+
+ Clear();
+
+ return true;
+}
+
+std::pair<CDateTime, CDateTime> CPVREpg::GetFirstAndLastUncommitedEPGDate() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetFirstAndLastUncommitedEPGDate();
+}
+
+bool CPVREpg::UpdateFromScraper(time_t start, time_t end, bool bForceUpdate)
+{
+ if (m_strScraperName.empty())
+ {
+ CLog::LogF(LOGERROR, "No EPG scraper defined for table '{}'", m_strName);
+ }
+ else if (m_strScraperName == "client")
+ {
+ if (!CServiceBroker::GetPVRManager().EpgsCreated())
+ return false;
+
+ if (!m_channelData->IsEPGEnabled() || m_channelData->IsHidden())
+ {
+ // ignore. not interested in any updates.
+ return true;
+ }
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
+ if (client)
+ {
+ if (!client->GetClientCapabilities().SupportsEPG())
+ {
+ CLog::LogF(LOGERROR, "The backend for channel '{}' on client '{}' does not support EPGs",
+ m_channelData->ChannelName(), m_channelData->ClientId());
+ }
+ else if (!bForceUpdate && client->GetClientCapabilities().SupportsAsyncEPGTransfer())
+ {
+ // nothing to do. client will provide epg updates asynchronously
+ return true;
+ }
+ else
+ {
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Updating EPG for channel '{}' from client '{}'",
+ m_channelData->ChannelName(), m_channelData->ClientId());
+ return (client->GetEPGForChannel(m_channelData->UniqueClientChannelId(), this, start, end) == PVR_ERROR_NO_ERROR);
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Client '{}' not found, can't update", m_channelData->ClientId());
+ }
+ }
+ else // other non-empty scraper name...
+ {
+ CLog::LogF(LOGERROR, "Loading the EPG via scraper is not yet implemented!");
+ //! @todo Add Support for Web EPG Scrapers here
+ }
+
+ return false;
+}
+
+const std::string& CPVREpg::ConvertGenreIdToString(int iID, int iSubID)
+{
+ unsigned int iLabelId = 19499;
+ switch (iID)
+ {
+ case EPG_EVENT_CONTENTMASK_MOVIEDRAMA:
+ iLabelId = (iSubID <= 8) ? 19500 + iSubID : 19500;
+ break;
+ case EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS:
+ iLabelId = (iSubID <= 4) ? 19516 + iSubID : 19516;
+ break;
+ case EPG_EVENT_CONTENTMASK_SHOW:
+ iLabelId = (iSubID <= 3) ? 19532 + iSubID : 19532;
+ break;
+ case EPG_EVENT_CONTENTMASK_SPORTS:
+ iLabelId = (iSubID <= 11) ? 19548 + iSubID : 19548;
+ break;
+ case EPG_EVENT_CONTENTMASK_CHILDRENYOUTH:
+ iLabelId = (iSubID <= 5) ? 19564 + iSubID : 19564;
+ break;
+ case EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE:
+ iLabelId = (iSubID <= 6) ? 19580 + iSubID : 19580;
+ break;
+ case EPG_EVENT_CONTENTMASK_ARTSCULTURE:
+ iLabelId = (iSubID <= 11) ? 19596 + iSubID : 19596;
+ break;
+ case EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS:
+ iLabelId = (iSubID <= 3) ? 19612 + iSubID : 19612;
+ break;
+ case EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE:
+ iLabelId = (iSubID <= 7) ? 19628 + iSubID : 19628;
+ break;
+ case EPG_EVENT_CONTENTMASK_LEISUREHOBBIES:
+ iLabelId = (iSubID <= 7) ? 19644 + iSubID : 19644;
+ break;
+ case EPG_EVENT_CONTENTMASK_SPECIAL:
+ iLabelId = (iSubID <= 3) ? 19660 + iSubID : 19660;
+ break;
+ case EPG_EVENT_CONTENTMASK_USERDEFINED:
+ iLabelId = (iSubID <= 8) ? 19676 + iSubID : 19676;
+ break;
+ default:
+ break;
+ }
+
+ return g_localizeStrings.Get(iLabelId);
+}
+
+std::shared_ptr<CPVREpgChannelData> CPVREpg::GetChannelData() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData;
+}
+
+void CPVREpg::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channelData = data;
+ m_tags.SetChannelData(data);
+}
+
+int CPVREpg::ChannelID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->ChannelId();
+}
+
+const std::string& CPVREpg::ScraperName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strScraperName;
+}
+
+const std::string& CPVREpg::Name() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strName;
+}
+
+int CPVREpg::EpgID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iEpgID;
+}
+
+bool CPVREpg::UpdatePending() const
+{
+ return m_bUpdatePending;
+}
+
+bool CPVREpg::NeedsSave() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bChanged || m_bUpdateLastScanTime || m_tags.NeedsSave();
+}
+
+bool CPVREpg::IsValid() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (ScraperName() == "client")
+ return m_channelData->ClientId() != -1 && m_channelData->UniqueClientChannelId() != PVR_CHANNEL_INVALID_UID;
+
+ return true;
+}
+
+void CPVREpg::RemovedFromContainer()
+{
+ m_events.Publish(PVREvent::EpgDeleted);
+}
+
+int CPVREpg::CleanupCachedImages(const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ const std::vector<std::string> urlsToCheck = database->GetAllIconPaths(EpgID());
+ const std::string owner = StringUtils::Format(CPVREpgInfoTag::IMAGE_OWNER_PATTERN, EpgID());
+
+ return CPVRCachedImages::Cleanup({{owner, ""}}, urlsToCheck);
+}
diff --git a/xbmc/pvr/epg/Epg.h b/xbmc/pvr/epg/Epg.h
new file mode 100644
index 0000000..31bc8d0
--- /dev/null
+++ b/xbmc/pvr/epg/Epg.h
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h"
+#include "pvr/epg/EpgTagsContainer.h"
+#include "threads/CriticalSection.h"
+#include "utils/EventStream.h"
+
+#include <atomic>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+ enum class PVREvent;
+
+ class CPVREpgChannelData;
+ class CPVREpgDatabase;
+ class CPVREpgInfoTag;
+
+ class CPVREpg
+ {
+ friend class CPVREpgDatabase;
+
+ public:
+ /*!
+ * @brief Create a new EPG instance.
+ * @param iEpgID The ID of this table or <= 0 to create a new ID.
+ * @param strName The name of this table.
+ * @param strScraperName The name of the scraper to use.
+ * @param database The EPG database
+ */
+ CPVREpg(int iEpgID,
+ const std::string& strName,
+ const std::string& strScraperName,
+ const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Create a new EPG instance.
+ * @param iEpgID The ID of this table or <= 0 to create a new ID.
+ * @param strName The name of this table.
+ * @param strScraperName The name of the scraper to use.
+ * @param channelData The channel data.
+ * @param database The EPG database
+ */
+ CPVREpg(int iEpgID,
+ const std::string& strName,
+ const std::string& strScraperName,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Destroy this EPG instance.
+ */
+ virtual ~CPVREpg();
+
+ /*!
+ * @brief Get data for the channel associated with this EPG.
+ * @return The data.
+ */
+ std::shared_ptr<CPVREpgChannelData> GetChannelData() const;
+
+ /*!
+ * @brief Set data for the channel associated with this EPG.
+ * @param data The data.
+ */
+ void SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data);
+
+ /*!
+ * @brief The id of the channel associated with this EPG.
+ * @return The channel id or -1 if no channel is associated
+ */
+ int ChannelID() const;
+
+ /*!
+ * @brief Get the name of the scraper to use for this table.
+ * @return The name of the scraper to use for this table.
+ */
+ const std::string& ScraperName() const;
+
+ /*!
+ * @brief Returns if there is a manual update pending for this EPG
+ * @return True if there is a manual update pending, false otherwise
+ */
+ bool UpdatePending() const;
+
+ /*!
+ * @brief Clear the current tags and schedule manual update
+ */
+ void ForceUpdate();
+
+ /*!
+ * @brief Get the name of this table.
+ * @return The name of this table.
+ */
+ const std::string& Name() const;
+
+ /*!
+ * @brief Get the database ID of this table.
+ * @return The database ID of this table.
+ */
+ int EpgID() const;
+
+ /*!
+ * @brief Remove all entries from this EPG that finished before the given time.
+ * @param time Delete entries with an end time before this time in UTC.
+ */
+ void Cleanup(const CDateTime& time);
+
+ /*!
+ * @brief Remove all entries from this EPG.
+ */
+ void Clear();
+
+ /*!
+ * @brief Get the event that is occurring now
+ * @return The current event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagNow() const;
+
+ /*!
+ * @brief Get the event that will occur next
+ * @return The next event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagNext() const;
+
+ /*!
+ * @brief Get the event that occurred previously
+ * @return The previous event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagPrevious() const;
+
+ /*!
+ * @brief Get the event that occurs between the given begin and end time.
+ * @param beginTime Minimum start time in UTC of the event.
+ * @param endTime Maximum end time in UTC of the event.
+ * @param bUpdateFromClient if true, try to fetch the event from the client if not found locally.
+ * @return The found tag or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagBetween(const CDateTime& beginTime, const CDateTime& endTime, bool bUpdateFromClient = false);
+
+ /*!
+ * @brief Get the event matching the given unique broadcast id
+ * @param iUniqueBroadcastId The uid to look up
+ * @return The matching event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagByBroadcastId(unsigned int iUniqueBroadcastId) const;
+
+ /*!
+ * @brief Get the event matching the given database id
+ * @param iDatabaseId The id to look up
+ * @return The matching event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagByDatabaseId(int iDatabaseId) const;
+
+ /*!
+ * @brief Update an entry in this EPG.
+ * @param data The tag to update.
+ * @param iClientId The id of the pvr client this event belongs to.
+ * @return True if it was updated successfully, false otherwise.
+ */
+ bool UpdateEntry(const EPG_TAG* data, int iClientId);
+
+ /*!
+ * @brief Update an entry in this EPG.
+ * @param tag The tag to update.
+ * @param newState the new state of the event.
+ * @return True if it was updated successfully, false otherwise.
+ */
+ bool UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE newState);
+
+ /*!
+ * @brief Update the EPG from 'start' till 'end'.
+ * @param start The start time.
+ * @param end The end time.
+ * @param iUpdateTime Update the table after the given amount of time has passed.
+ * @param iPastDays Amount of past days from now on, for which past entries are to be kept.
+ * @param database If given, the database to store the data.
+ * @param bForceUpdate Force update from client even if it's not the time to
+ * @return True if the update was successful, false otherwise.
+ */
+ bool Update(time_t start, time_t end, int iUpdateTime, int iPastDays, const std::shared_ptr<CPVREpgDatabase>& database, bool bForceUpdate = false);
+
+ /*!
+ * @brief Get all EPG tags.
+ * @return The tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetTags() const;
+
+ /*!
+ * @brief Get all EPG tags for the given time frame, including "gap" tags.
+ * @param timelineStart Start of time line
+ * @param timelineEnd End of time line
+ * @param minEventEnd The minimum end time of the events to return
+ * @param maxEventStart The maximum start time of the events to return
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetTimeline(const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const;
+
+ /*!
+ * @brief Write the query to persist data into given database's queue
+ * @param database The database.
+ * @return True on success, false otherwise.
+ */
+ bool QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Write the delete queries into the given database's queue
+ * @param database The database.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteQueries(const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Get the start and end time of the last not yet commited entry in this table.
+ * @return The times; first: start time, second: end time.
+ */
+ std::pair<CDateTime, CDateTime> GetFirstAndLastUncommitedEPGDate() const;
+
+ /*!
+ * @brief Notify observers when the currently active tag changed.
+ * @return True if the playing tag has changed, false otherwise.
+ */
+ bool CheckPlayingEvent();
+
+ /*!
+ * @brief Convert a genre id and subid to a human readable name.
+ * @param iID The genre ID.
+ * @param iSubID The genre sub ID.
+ * @return A human readable name.
+ */
+ static const std::string& ConvertGenreIdToString(int iID, int iSubID);
+
+ /*!
+ * @brief Check whether this EPG has unsaved data.
+ * @return True if this EPG contains unsaved data, false otherwise.
+ */
+ bool NeedsSave() const;
+
+ /*!
+ * @brief Check whether this EPG is valid.
+ * @return True if this EPG is valid and can be updated, false otherwise.
+ */
+ bool IsValid() const;
+
+ /*!
+ * @brief Query the events available for CEventStream
+ */
+ CEventStream<PVREvent>& Events() { return m_events; }
+
+ /*!
+ * @brief Lock the instance. No other thread gets access to this EPG until Unlock was called.
+ */
+ void Lock() { m_critSection.lock(); }
+
+ /*!
+ * @brief Unlock the instance. Other threads may get access to this EPG again.
+ */
+ void Unlock() { m_critSection.unlock(); }
+
+ /*!
+ * @brief Called to inform the EPG that it has been removed from the EPG container.
+ */
+ void RemovedFromContainer();
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @param database The EPG database
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages(const std::shared_ptr<CPVREpgDatabase>& database);
+
+ private:
+ CPVREpg() = delete;
+ CPVREpg(const CPVREpg&) = delete;
+ CPVREpg& operator =(const CPVREpg&) = delete;
+
+ /*!
+ * @brief Update the EPG from a scraper set in the channel tag.
+ * @todo not implemented yet for non-pvr EPGs
+ * @param start Get entries with a start date after this time.
+ * @param end Get entries with an end date before this time.
+ * @param bForceUpdate Force update from client even if it's not the time to
+ * @return True if the update was successful, false otherwise.
+ */
+ bool UpdateFromScraper(time_t start, time_t end, bool bForceUpdate);
+
+ /*!
+ * @brief Update the contents of this table with the contents provided in "epg"
+ * @param epg The updated contents.
+ * @return True if the update was successful, false otherwise.
+ */
+ bool UpdateEntries(const CPVREpg& epg);
+
+ /*!
+ * @brief Remove all entries from this EPG that finished before the given amount of days.
+ * @param iPastDays Delete entries with an end time before the given amount of days from now on.
+ */
+ void Cleanup(int iPastDays);
+
+ bool m_bChanged = false; /*!< true if anything changed that needs to be persisted, false otherwise */
+ std::atomic<bool> m_bUpdatePending = {false}; /*!< true if manual update is pending */
+ int m_iEpgID = 0; /*!< the database ID of this table */
+ std::string m_strName; /*!< the name of this table */
+ std::string m_strScraperName; /*!< the name of the scraper to use */
+ CDateTime m_lastScanTime; /*!< the last time the EPG has been updated */
+ mutable CCriticalSection m_critSection; /*!< critical section for changes in this table */
+ bool m_bUpdateLastScanTime = false;
+ std::shared_ptr<CPVREpgChannelData> m_channelData;
+ CPVREpgTagsContainer m_tags;
+
+ CEventSource<PVREvent> m_events;
+ };
+}
diff --git a/xbmc/pvr/epg/EpgChannelData.cpp b/xbmc/pvr/epg/EpgChannelData.cpp
new file mode 100644
index 0000000..259b296
--- /dev/null
+++ b/xbmc/pvr/epg/EpgChannelData.cpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgChannelData.h"
+
+#include "XBDateTime.h"
+#include "pvr/channels/PVRChannel.h"
+
+using namespace PVR;
+
+CPVREpgChannelData::CPVREpgChannelData(int iClientId, int iUniqueClientChannelId)
+ : m_iClientId(iClientId), m_iUniqueClientChannelId(iUniqueClientChannelId)
+{
+}
+
+CPVREpgChannelData::CPVREpgChannelData(const CPVRChannel& channel)
+ : m_bIsRadio(channel.IsRadio()),
+ m_iClientId(channel.ClientID()),
+ m_iUniqueClientChannelId(channel.UniqueID()),
+ m_bIsHidden(channel.IsHidden()),
+ m_bIsLocked(channel.IsLocked()),
+ m_bIsEPGEnabled(channel.EPGEnabled()),
+ m_iChannelId(channel.ChannelID()),
+ m_strChannelName(channel.ChannelName()),
+ m_strChannelIconPath(channel.IconPath())
+{
+}
+
+bool CPVREpgChannelData::IsRadio() const
+{
+ return m_bIsRadio;
+}
+
+int CPVREpgChannelData::ClientId() const
+{
+ return m_iClientId;
+}
+
+int CPVREpgChannelData::UniqueClientChannelId() const
+{
+ return m_iUniqueClientChannelId;
+}
+
+bool CPVREpgChannelData::IsHidden() const
+{
+ return m_bIsHidden;
+}
+
+void CPVREpgChannelData::SetHidden(bool bIsHidden)
+{
+ m_bIsHidden = bIsHidden;
+}
+
+bool CPVREpgChannelData::IsLocked() const
+{
+ return m_bIsLocked;
+}
+
+void CPVREpgChannelData::SetLocked(bool bIsLocked)
+{
+ m_bIsLocked = bIsLocked;
+}
+
+bool CPVREpgChannelData::IsEPGEnabled() const
+{
+ return m_bIsEPGEnabled;
+}
+
+void CPVREpgChannelData::SetEPGEnabled(bool bIsEPGEnabled)
+{
+ m_bIsEPGEnabled = bIsEPGEnabled;
+}
+
+int CPVREpgChannelData::ChannelId() const
+{
+ return m_iChannelId;
+}
+
+void CPVREpgChannelData::SetChannelId(int iChannelId)
+{
+ m_iChannelId = iChannelId;
+}
+
+const std::string& CPVREpgChannelData::ChannelName() const
+{
+ return m_strChannelName;
+}
+
+void CPVREpgChannelData::SetChannelName(const std::string& strChannelName)
+{
+ m_strChannelName = strChannelName;
+}
+
+const std::string& CPVREpgChannelData::ChannelIconPath() const
+{
+ return m_strChannelIconPath;
+}
+
+void CPVREpgChannelData::SetChannelIconPath(const std::string& strChannelIconPath)
+{
+ m_strChannelIconPath = strChannelIconPath;
+}
diff --git a/xbmc/pvr/epg/EpgChannelData.h b/xbmc/pvr/epg/EpgChannelData.h
new file mode 100644
index 0000000..a2a63ec
--- /dev/null
+++ b/xbmc/pvr/epg/EpgChannelData.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <ctime>
+#include <string>
+
+namespace PVR
+{
+class CPVRChannel;
+
+class CPVREpgChannelData
+{
+public:
+ CPVREpgChannelData() = default;
+ CPVREpgChannelData(int iClientId, int iUniqueClientChannelId);
+ explicit CPVREpgChannelData(const CPVRChannel& channel);
+
+ int ClientId() const;
+ int UniqueClientChannelId() const;
+ bool IsRadio() const;
+
+ bool IsHidden() const;
+ void SetHidden(bool bIsHidden);
+
+ bool IsLocked() const;
+ void SetLocked(bool bIsLocked);
+
+ bool IsEPGEnabled() const;
+ void SetEPGEnabled(bool bIsEPGEnabled);
+
+ int ChannelId() const;
+ void SetChannelId(int iChannelId);
+
+ const std::string& ChannelName() const;
+ void SetChannelName(const std::string& strChannelName);
+
+ const std::string& ChannelIconPath() const;
+ void SetChannelIconPath(const std::string& strChannelIconPath);
+
+private:
+ const bool m_bIsRadio = false;
+ const int m_iClientId = -1;
+ const int m_iUniqueClientChannelId = -1;
+
+ bool m_bIsHidden = false;
+ bool m_bIsLocked = false;
+ bool m_bIsEPGEnabled = true;
+ int m_iChannelId = -1;
+ std::string m_strChannelName;
+ std::string m_strChannelIconPath;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgContainer.cpp b/xbmc/pvr/epg/EpgContainer.cpp
new file mode 100644
index 0000000..d6f2015
--- /dev/null
+++ b/xbmc/pvr/epg/EpgContainer.cpp
@@ -0,0 +1,1028 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgContainer.h"
+
+#include "ServiceBroker.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h" // PVR_CHANNEL_INVALID_UID
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIProgressHandler.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SystemClock.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <utility>
+#include <vector>
+
+using namespace std::chrono_literals;
+
+namespace PVR
+{
+
+class CEpgUpdateRequest
+{
+public:
+ CEpgUpdateRequest() : CEpgUpdateRequest(-1, PVR_CHANNEL_INVALID_UID) {}
+ CEpgUpdateRequest(int iClientID, int iUniqueChannelID) : m_iClientID(iClientID), m_iUniqueChannelID(iUniqueChannelID) {}
+
+ void Deliver();
+
+private:
+ int m_iClientID;
+ int m_iUniqueChannelID;
+};
+
+void CEpgUpdateRequest::Deliver()
+{
+ const std::shared_ptr<CPVREpg> epg = CServiceBroker::GetPVRManager().EpgContainer().GetByChannelUid(m_iClientID, m_iUniqueChannelID);
+ if (!epg)
+ {
+ CLog::LogF(LOGERROR,
+ "Unable to obtain EPG for client {} and channel {}! Unable to deliver the epg "
+ "update request!",
+ m_iClientID, m_iUniqueChannelID);
+ return;
+ }
+
+ epg->ForceUpdate();
+}
+
+class CEpgTagStateChange
+{
+public:
+ CEpgTagStateChange() = default;
+ CEpgTagStateChange(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE eNewState) : m_epgtag(tag), m_state(eNewState) {}
+
+ void Deliver();
+
+private:
+ std::shared_ptr<CPVREpgInfoTag> m_epgtag;
+ EPG_EVENT_STATE m_state = EPG_EVENT_CREATED;
+};
+
+void CEpgTagStateChange::Deliver()
+{
+ const CPVREpgContainer& epgContainer = CServiceBroker::GetPVRManager().EpgContainer();
+
+ const std::shared_ptr<CPVREpg> epg = epgContainer.GetByChannelUid(m_epgtag->ClientID(), m_epgtag->UniqueChannelID());
+ if (!epg)
+ {
+ CLog::LogF(LOGERROR,
+ "Unable to obtain EPG for client {} and channel {}! Unable to deliver state change "
+ "for tag '{}'!",
+ m_epgtag->ClientID(), m_epgtag->UniqueChannelID(), m_epgtag->UniqueBroadcastID());
+ return;
+ }
+
+ if (m_epgtag->EpgID() < 0)
+ {
+ // now that we have the epg instance, fully initialize the tag
+ m_epgtag->SetEpgID(epg->EpgID());
+ m_epgtag->SetChannelData(epg->GetChannelData());
+ }
+
+ epg->UpdateEntry(m_epgtag, m_state);
+}
+
+CPVREpgContainer::CPVREpgContainer(CEventSource<PVREvent>& eventSource)
+ : CThread("EPGUpdater"),
+ m_database(new CPVREpgDatabase),
+ m_settings({CSettings::SETTING_EPG_EPGUPDATE, CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY,
+ CSettings::SETTING_EPG_PAST_DAYSTODISPLAY,
+ CSettings::SETTING_EPG_PREVENTUPDATESWHILEPLAYINGTV}),
+ m_events(eventSource)
+{
+ m_bStop = true; // base class member
+ m_updateEvent.Reset();
+}
+
+CPVREpgContainer::~CPVREpgContainer()
+{
+ Stop();
+ Unload();
+}
+
+std::shared_ptr<CPVREpgDatabase> CPVREpgContainer::GetEpgDatabase() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_database->IsOpen())
+ m_database->Open();
+
+ return m_database;
+}
+
+bool CPVREpgContainer::IsStarted() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bStarted;
+}
+
+int CPVREpgContainer::NextEpgId()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return ++m_iNextEpgId;
+}
+
+void CPVREpgContainer::Start()
+{
+ Stop();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bIsInitialising = true;
+
+ Create();
+ SetPriority(ThreadPriority::BELOW_NORMAL);
+
+ m_bStarted = true;
+ }
+}
+
+void CPVREpgContainer::Stop()
+{
+ StopThread();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bStarted = false;
+ }
+}
+
+bool CPVREpgContainer::Load()
+{
+ // EPGs must be loaded via PVR Manager -> channel groups -> EPG container to associate the
+ // channels with the right EPG.
+ CServiceBroker::GetPVRManager().TriggerEpgsCreate();
+ return true;
+}
+
+void CPVREpgContainer::Unload()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_updateRequestsLock);
+ m_updateRequests.clear();
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_epgTagChangesLock);
+ m_epgTagChanges.clear();
+ }
+
+ std::vector<std::shared_ptr<CPVREpg>> epgs;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* clear all epg tables and remove pointers to epg tables on channels */
+ std::transform(m_epgIdToEpgMap.cbegin(), m_epgIdToEpgMap.cend(), std::back_inserter(epgs),
+ [](const auto& epgEntry) { return epgEntry.second; });
+
+ m_epgIdToEpgMap.clear();
+ m_channelUidToEpgMap.clear();
+
+ m_iNextEpgUpdate = 0;
+ m_iNextEpgId = 0;
+ m_iNextEpgActiveTagCheck = 0;
+ m_bUpdateNotificationPending = false;
+ m_bLoaded = false;
+
+ m_database->Close();
+ }
+
+ for (const auto& epg : epgs)
+ {
+ epg->Events().Unsubscribe(this);
+ epg->RemovedFromContainer();
+ }
+}
+
+void CPVREpgContainer::Notify(const PVREvent& event)
+{
+ if (event == PVREvent::EpgItemUpdate)
+ {
+ // there can be many of these notifications during short time period. Thus, announce async and not every event.
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bUpdateNotificationPending = true;
+ return;
+ }
+ else if (event == PVREvent::EpgUpdatePending)
+ {
+ SetHasPendingUpdates(true);
+ return;
+ }
+ else if (event == PVREvent::EpgActiveItem)
+ {
+ // No need to propagate the change. See CPVREpgContainer::CheckPlayingEvents
+ return;
+ }
+
+ m_events.Publish(event);
+}
+
+void CPVREpgContainer::LoadFromDatabase()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bLoaded)
+ return;
+
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ database->Lock();
+ m_iNextEpgId = database->GetLastEPGId();
+ const std::vector<std::shared_ptr<CPVREpg>> result = database->GetAll();
+ database->Unlock();
+
+ for (const auto& entry : result)
+ InsertFromDB(entry);
+
+ m_bLoaded = true;
+}
+
+bool CPVREpgContainer::PersistAll(unsigned int iMaxTimeslice) const
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return false;
+ }
+
+ std::vector<std::shared_ptr<CPVREpg>> changedEpgs;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& epg : m_epgIdToEpgMap)
+ {
+ if (epg.second && epg.second->NeedsSave())
+ {
+ // Note: We need to obtain a lock for every epg instance before we can lock
+ // the epg db. This order is important. Otherwise deadlocks may occur.
+ epg.second->Lock();
+ changedEpgs.emplace_back(epg.second);
+ }
+ }
+ }
+
+ bool bReturn = true;
+
+ if (!changedEpgs.empty())
+ {
+ // Note: We must lock the db the whole time, otherwise races may occur.
+ database->Lock();
+
+ XbmcThreads::EndTime<> processTimeslice{std::chrono::milliseconds(iMaxTimeslice)};
+ for (const auto& epg : changedEpgs)
+ {
+ if (!processTimeslice.IsTimePast())
+ {
+ CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Container: Persisting events for channel '{}'...",
+ epg->GetChannelData()->ChannelName());
+
+ bReturn &= epg->QueuePersistQuery(database);
+
+ size_t queryCount = database->GetInsertQueriesCount() + database->GetDeleteQueriesCount();
+ if (queryCount > EPG_COMMIT_QUERY_COUNT_LIMIT)
+ {
+ CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Container: committing {} queries in loop.",
+ queryCount);
+ database->CommitDeleteQueries();
+ database->CommitInsertQueries();
+ CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Container: committed {} queries in loop.", queryCount);
+ }
+ }
+
+ epg->Unlock();
+ }
+
+ if (bReturn)
+ {
+ database->CommitDeleteQueries();
+ database->CommitInsertQueries();
+ }
+
+ database->Unlock();
+ }
+
+ return bReturn;
+}
+
+void CPVREpgContainer::Process()
+{
+ time_t iNow = 0;
+ time_t iLastSave = 0;
+
+ SetPriority(ThreadPriority::LOWEST);
+
+ while (!m_bStop)
+ {
+ time_t iLastEpgCleanup = 0;
+ bool bUpdateEpg = true;
+
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(iNow);
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bUpdateEpg = (iNow >= m_iNextEpgUpdate) && !m_bSuspended;
+ iLastEpgCleanup = m_iLastEpgCleanup;
+ }
+
+ /* update the EPG */
+ if (!InterruptUpdate() && bUpdateEpg && CServiceBroker::GetPVRManager().EpgsCreated() && UpdateEPG())
+ m_bIsInitialising = false;
+
+ /* clean up old entries */
+ if (!m_bStop && !m_bSuspended &&
+ iNow >= iLastEpgCleanup + CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iEpgCleanupInterval)
+ RemoveOldEntries();
+
+ /* check for pending manual EPG updates */
+
+ while (!m_bStop && !m_bSuspended)
+ {
+ CEpgUpdateRequest request;
+ {
+ std::unique_lock<CCriticalSection> lock(m_updateRequestsLock);
+ if (m_updateRequests.empty())
+ break;
+
+ request = m_updateRequests.front();
+ m_updateRequests.pop_front();
+ }
+
+ // do the update
+ request.Deliver();
+ }
+
+ /* check for pending EPG tag changes */
+
+ // during Kodi startup, addons may push updates very early, even before EPGs are ready to use.
+ if (!m_bStop && !m_bSuspended && CServiceBroker::GetPVRManager().EpgsCreated())
+ {
+ unsigned int iProcessed = 0;
+ XbmcThreads::EndTime<> processTimeslice(
+ 1000ms); // max 1 sec per cycle, regardless of how many events are in the queue
+
+ while (!InterruptUpdate())
+ {
+ CEpgTagStateChange change;
+ {
+ std::unique_lock<CCriticalSection> lock(m_epgTagChangesLock);
+ if (processTimeslice.IsTimePast() || m_epgTagChanges.empty())
+ {
+ if (iProcessed > 0)
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Processed {} queued epg event changes.", iProcessed);
+
+ break;
+ }
+
+ change = m_epgTagChanges.front();
+ m_epgTagChanges.pop_front();
+ }
+
+ iProcessed++;
+
+ // deliver the updated tag to the respective epg
+ change.Deliver();
+ }
+ }
+
+ if (!m_bStop && !m_bSuspended)
+ {
+ bool bHasPendingUpdates = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bHasPendingUpdates = (m_pendingUpdates > 0);
+ }
+
+ if (bHasPendingUpdates)
+ UpdateEPG(true);
+ }
+
+ /* check for updated active tag */
+ if (!m_bStop)
+ CheckPlayingEvents();
+
+ /* check for pending update notifications */
+ if (!m_bStop)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bUpdateNotificationPending)
+ {
+ m_bUpdateNotificationPending = false;
+ m_events.Publish(PVREvent::Epg);
+ }
+ }
+
+ /* check for changes that need to be saved every 60 seconds */
+ if ((iNow - iLastSave > 60) && !InterruptUpdate())
+ {
+ PersistAll(1000);
+ iLastSave = iNow;
+ }
+
+ CThread::Sleep(1000ms);
+ }
+
+ // store data on exit
+ CLog::Log(LOGINFO, "EPG Container: Persisting unsaved events...");
+ PersistAll(std::numeric_limits<unsigned int>::max());
+ CLog::Log(LOGINFO, "EPG Container: Persisting events done");
+}
+
+std::vector<std::shared_ptr<CPVREpg>> CPVREpgContainer::GetAllEpgs() const
+{
+ std::vector<std::shared_ptr<CPVREpg>> epgs;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::transform(m_epgIdToEpgMap.cbegin(), m_epgIdToEpgMap.cend(), std::back_inserter(epgs),
+ [](const auto& epgEntry) { return epgEntry.second; });
+
+ return epgs;
+}
+
+std::shared_ptr<CPVREpg> CPVREpgContainer::GetById(int iEpgId) const
+{
+ std::shared_ptr<CPVREpg> retval;
+
+ if (iEpgId < 0)
+ return retval;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto& epgEntry = m_epgIdToEpgMap.find(iEpgId);
+ if (epgEntry != m_epgIdToEpgMap.end())
+ retval = epgEntry->second;
+
+ return retval;
+}
+
+std::shared_ptr<CPVREpg> CPVREpgContainer::GetByChannelUid(int iClientId, int iChannelUid) const
+{
+ std::shared_ptr<CPVREpg> epg;
+
+ if (iClientId < 0 || iChannelUid < 0)
+ return epg;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto& epgEntry = m_channelUidToEpgMap.find(std::pair<int, int>(iClientId, iChannelUid));
+ if (epgEntry != m_channelUidToEpgMap.end())
+ epg = epgEntry->second;
+
+ return epg;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgContainer::GetTagById(const std::shared_ptr<CPVREpg>& epg, unsigned int iBroadcastId) const
+{
+ std::shared_ptr<CPVREpgInfoTag> retval;
+
+ if (iBroadcastId == EPG_TAG_INVALID_UID)
+ return retval;
+
+ if (epg)
+ {
+ retval = epg->GetTagByBroadcastId(iBroadcastId);
+ }
+
+ return retval;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgContainer::GetTagByDatabaseId(int iDatabaseId) const
+{
+ std::shared_ptr<CPVREpgInfoTag> retval;
+
+ if (iDatabaseId <= 0)
+ return retval;
+
+ m_critSection.lock();
+ const auto epgs = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ for (const auto& epgEntry : epgs)
+ {
+ retval = epgEntry.second->GetTagByDatabaseId(iDatabaseId);
+ if (retval)
+ break;
+ }
+
+ return retval;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgContainer::GetTags(
+ const PVREpgSearchData& searchData) const
+{
+ // make sure we have up-to-date data in the database.
+ PersistAll(std::numeric_limits<unsigned int>::max());
+
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> results = database->GetEpgTags(searchData);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tag : results)
+ {
+ const auto& it = m_epgIdToEpgMap.find(tag->EpgID());
+ if (it != m_epgIdToEpgMap.cend())
+ tag->SetChannelData((*it).second->GetChannelData());
+ }
+
+ return results;
+}
+
+void CPVREpgContainer::InsertFromDB(const std::shared_ptr<CPVREpg>& newEpg)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // table might already have been created when pvr channels were loaded
+ std::shared_ptr<CPVREpg> epg = GetById(newEpg->EpgID());
+ if (!epg)
+ {
+ // create a new epg table
+ epg = newEpg;
+ m_epgIdToEpgMap.insert({epg->EpgID(), epg});
+ epg->Events().Subscribe(this, &CPVREpgContainer::Notify);
+ }
+}
+
+std::shared_ptr<CPVREpg> CPVREpgContainer::CreateChannelEpg(int iEpgId, const std::string& strScraperName, const std::shared_ptr<CPVREpgChannelData>& channelData)
+{
+ std::shared_ptr<CPVREpg> epg;
+
+ WaitForUpdateFinish();
+ LoadFromDatabase();
+
+ if (iEpgId > 0)
+ epg = GetById(iEpgId);
+
+ if (!epg)
+ {
+ if (iEpgId <= 0)
+ iEpgId = NextEpgId();
+
+ epg.reset(new CPVREpg(iEpgId, channelData->ChannelName(), strScraperName, channelData,
+ GetEpgDatabase()));
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_epgIdToEpgMap.insert({iEpgId, epg});
+ m_channelUidToEpgMap.insert({{channelData->ClientId(), channelData->UniqueClientChannelId()}, epg});
+ epg->Events().Subscribe(this, &CPVREpgContainer::Notify);
+ }
+ else if (epg->ChannelID() == -1)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channelUidToEpgMap.insert({{channelData->ClientId(), channelData->UniqueClientChannelId()}, epg});
+ epg->SetChannelData(channelData);
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bPreventUpdates = false;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(m_iNextEpgUpdate);
+ }
+
+ m_events.Publish(PVREvent::EpgContainer);
+
+ return epg;
+}
+
+bool CPVREpgContainer::RemoveOldEntries()
+{
+ const CDateTime cleanupTime(CDateTime::GetUTCDateTime() - CDateTimeSpan(GetPastDaysToDisplay(), 0, 0, 0));
+
+ m_critSection.lock();
+ const auto epgs = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ for (const auto& epgEntry : epgs)
+ epgEntry.second->Cleanup(cleanupTime);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(m_iLastEpgCleanup);
+
+ return true;
+}
+
+bool CPVREpgContainer::QueueDeleteEpgs(const std::vector<std::shared_ptr<CPVREpg>>& epgs)
+{
+ if (epgs.empty())
+ return true;
+
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return false;
+ }
+
+ for (const auto& epg : epgs)
+ {
+ // Note: We need to obtain a lock for every epg instance before we can lock
+ // the epg db. This order is important. Otherwise deadlocks may occur.
+ epg->Lock();
+ }
+
+ database->Lock();
+ for (const auto& epg : epgs)
+ {
+ QueueDeleteEpg(epg, database);
+ epg->Unlock();
+
+ size_t queryCount = database->GetDeleteQueriesCount();
+ if (queryCount > EPG_COMMIT_QUERY_COUNT_LIMIT)
+ database->CommitDeleteQueries();
+ }
+ database->CommitDeleteQueries();
+ database->Unlock();
+
+ return true;
+}
+
+bool CPVREpgContainer::QueueDeleteEpg(const std::shared_ptr<CPVREpg>& epg,
+ const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ if (!epg || epg->EpgID() < 0)
+ return false;
+
+ std::shared_ptr<CPVREpg> epgToDelete;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const auto& epgEntry = m_epgIdToEpgMap.find(epg->EpgID());
+ if (epgEntry == m_epgIdToEpgMap.end())
+ return false;
+
+ const auto& epgEntry1 = m_channelUidToEpgMap.find(std::make_pair(
+ epg->GetChannelData()->ClientId(), epg->GetChannelData()->UniqueClientChannelId()));
+ if (epgEntry1 != m_channelUidToEpgMap.end())
+ m_channelUidToEpgMap.erase(epgEntry1);
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Deleting EPG table {} ({})", epg->Name(), epg->EpgID());
+ epgEntry->second->QueueDeleteQueries(database);
+
+ epgToDelete = epgEntry->second;
+ m_epgIdToEpgMap.erase(epgEntry);
+ }
+
+ epgToDelete->Events().Unsubscribe(this);
+ epgToDelete->RemovedFromContainer();
+ return true;
+}
+
+bool CPVREpgContainer::InterruptUpdate() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bStop ||
+ m_bPreventUpdates ||
+ (m_bPlaying && m_settings.GetBoolValue(CSettings::SETTING_EPG_PREVENTUPDATESWHILEPLAYINGTV));
+}
+
+void CPVREpgContainer::WaitForUpdateFinish()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bPreventUpdates = true;
+
+ if (!m_bIsUpdating)
+ return;
+
+ m_updateEvent.Reset();
+ }
+
+ m_updateEvent.Wait();
+}
+
+bool CPVREpgContainer::UpdateEPG(bool bOnlyPending /* = false */)
+{
+ bool bInterrupted = false;
+ unsigned int iUpdatedTables = 0;
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ /* set start and end time */
+ time_t start;
+ time_t end;
+ CDateTime::GetUTCDateTime().GetAsTime(start);
+ end = start + GetFutureDaysToDisplay() * 24 * 60 * 60;
+ start -= GetPastDaysToDisplay() * 24 * 60 * 60;
+
+ bool bShowProgress = (m_bIsInitialising || advancedSettings->m_bEpgDisplayIncrementalUpdatePopup) &&
+ advancedSettings->m_bEpgDisplayUpdatePopup;
+ int pendingUpdates = 0;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bIsUpdating || InterruptUpdate())
+ return false;
+
+ m_bIsUpdating = true;
+ pendingUpdates = m_pendingUpdates;
+ }
+
+ std::vector<std::shared_ptr<CPVREpg>> invalidTables;
+
+ unsigned int iCounter = 0;
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+
+ m_critSection.lock();
+ const auto epgsToUpdate = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ std::unique_ptr<CPVRGUIProgressHandler> progressHandler;
+ if (bShowProgress && !bOnlyPending && !epgsToUpdate.empty())
+ progressHandler.reset(
+ new CPVRGUIProgressHandler(g_localizeStrings.Get(19004))); // Loading programme guide
+
+ for (const auto& epgEntry : epgsToUpdate)
+ {
+ if (InterruptUpdate())
+ {
+ bInterrupted = true;
+ break;
+ }
+
+ const std::shared_ptr<CPVREpg> epg = epgEntry.second;
+ if (!epg)
+ continue;
+
+ if (progressHandler)
+ progressHandler->UpdateProgress(epg->GetChannelData()->ChannelName(), ++iCounter,
+ epgsToUpdate.size());
+
+ if ((!bOnlyPending || epg->UpdatePending()) &&
+ epg->Update(start,
+ end,
+ m_settings.GetIntValue(CSettings::SETTING_EPG_EPGUPDATE) * 60,
+ m_settings.GetIntValue(CSettings::SETTING_EPG_PAST_DAYSTODISPLAY),
+ database,
+ bOnlyPending))
+ {
+ iUpdatedTables++;
+ }
+ else if (!epg->IsValid())
+ {
+ invalidTables.push_back(epg);
+ }
+ }
+
+ progressHandler.reset();
+
+ QueueDeleteEpgs(invalidTables);
+
+ if (bInterrupted)
+ {
+ /* the update has been interrupted. try again later */
+ time_t iNow;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(iNow);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iNextEpgUpdate = iNow + advancedSettings->m_iEpgRetryInterruptedUpdateInterval;
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(m_iNextEpgUpdate);
+ m_iNextEpgUpdate += advancedSettings->m_iEpgUpdateCheckInterval;
+ if (m_pendingUpdates == pendingUpdates)
+ m_pendingUpdates = 0;
+ }
+
+ if (iUpdatedTables > 0)
+ m_events.Publish(PVREvent::EpgContainer);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bIsUpdating = false;
+ m_updateEvent.Set();
+
+ return !bInterrupted;
+}
+
+std::pair<CDateTime, CDateTime> CPVREpgContainer::GetFirstAndLastEPGDate() const
+{
+ // Get values from db
+ std::pair<CDateTime, CDateTime> dbDates;
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (database)
+ dbDates = database->GetFirstAndLastEPGDate();
+
+ // Merge not yet commited changes
+ m_critSection.lock();
+ const auto epgs = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ CDateTime first(dbDates.first);
+ CDateTime last(dbDates.second);
+
+ for (const auto& epgEntry : epgs)
+ {
+ const auto dates = epgEntry.second->GetFirstAndLastUncommitedEPGDate();
+
+ if (dates.first.IsValid() && (!first.IsValid() || dates.first < first))
+ first = dates.first;
+
+ if (dates.second.IsValid() && (!last.IsValid() || dates.second > last))
+ last = dates.second;
+ }
+
+ return {first, last};
+}
+
+bool CPVREpgContainer::CheckPlayingEvents()
+{
+ bool bReturn = false;
+ bool bFoundChanges = false;
+
+ m_critSection.lock();
+ const auto epgs = m_epgIdToEpgMap;
+ time_t iNextEpgActiveTagCheck = m_iNextEpgActiveTagCheck;
+ m_critSection.unlock();
+
+ time_t iNow;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(iNow);
+ if (iNow >= iNextEpgActiveTagCheck)
+ {
+ bFoundChanges = std::accumulate(epgs.cbegin(), epgs.cend(), bFoundChanges,
+ [](bool found, const auto& epgEntry) {
+ return epgEntry.second->CheckPlayingEvent() ? true : found;
+ });
+
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(iNextEpgActiveTagCheck);
+ iNextEpgActiveTagCheck += CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iEpgActiveTagCheckInterval;
+
+ /* pvr tags always start on the full minute */
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ iNextEpgActiveTagCheck -= iNextEpgActiveTagCheck % 60;
+
+ bReturn = true;
+ }
+
+ if (bReturn)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iNextEpgActiveTagCheck = iNextEpgActiveTagCheck;
+ }
+
+ if (bFoundChanges)
+ m_events.Publish(PVREvent::EpgActiveItem);
+
+ return bReturn;
+}
+
+void CPVREpgContainer::SetHasPendingUpdates(bool bHasPendingUpdates /* = true */)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (bHasPendingUpdates)
+ m_pendingUpdates++;
+ else
+ m_pendingUpdates = 0;
+}
+
+void CPVREpgContainer::UpdateRequest(int iClientID, int iUniqueChannelID)
+{
+ std::unique_lock<CCriticalSection> lock(m_updateRequestsLock);
+ m_updateRequests.emplace_back(CEpgUpdateRequest(iClientID, iUniqueChannelID));
+}
+
+void CPVREpgContainer::UpdateFromClient(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE eNewState)
+{
+ std::unique_lock<CCriticalSection> lock(m_epgTagChangesLock);
+ m_epgTagChanges.emplace_back(CEpgTagStateChange(tag, eNewState));
+}
+
+int CPVREpgContainer::GetPastDaysToDisplay() const
+{
+ return m_settings.GetIntValue(CSettings::SETTING_EPG_PAST_DAYSTODISPLAY);
+}
+
+int CPVREpgContainer::GetFutureDaysToDisplay() const
+{
+ return m_settings.GetIntValue(CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY);
+}
+
+void CPVREpgContainer::OnPlaybackStarted()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bPlaying = true;
+}
+
+void CPVREpgContainer::OnPlaybackStopped()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bPlaying = false;
+}
+
+void CPVREpgContainer::OnSystemSleep()
+{
+ m_bSuspended = true;
+}
+
+void CPVREpgContainer::OnSystemWake()
+{
+ m_bSuspended = false;
+}
+
+int CPVREpgContainer::CleanupCachedImages()
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return 0;
+ }
+
+ // Processing can take some time. Do not block.
+ m_critSection.lock();
+ const std::map<int, std::shared_ptr<CPVREpg>> epgIdToEpgMap = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ return std::accumulate(epgIdToEpgMap.cbegin(), epgIdToEpgMap.cend(), 0,
+ [&database](int cleanedImages, const auto& epg) {
+ return cleanedImages + epg.second->CleanupCachedImages(database);
+ });
+}
+
+std::vector<std::shared_ptr<CPVREpgSearchFilter>> CPVREpgContainer::GetSavedSearches(bool bRadio)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ return database->GetSavedSearches(bRadio);
+}
+
+std::shared_ptr<CPVREpgSearchFilter> CPVREpgContainer::GetSavedSearchById(bool bRadio, int iId)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ return database->GetSavedSearchById(bRadio, iId);
+}
+
+bool CPVREpgContainer::PersistSavedSearch(CPVREpgSearchFilter& search)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ if (database->Persist(search))
+ {
+ m_events.Publish(PVREvent::SavedSearchesInvalidated);
+ return true;
+ }
+ return false;
+}
+
+bool CPVREpgContainer::UpdateSavedSearchLastExecuted(const CPVREpgSearchFilter& epgSearch)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ return database->UpdateSavedSearchLastExecuted(epgSearch);
+}
+
+bool CPVREpgContainer::DeleteSavedSearch(const CPVREpgSearchFilter& search)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ if (database->Delete(search))
+ {
+ m_events.Publish(PVREvent::SavedSearchesInvalidated);
+ return true;
+ }
+ return false;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgContainer.h b/xbmc/pvr/epg/EpgContainer.h
new file mode 100644
index 0000000..68cf50d
--- /dev/null
+++ b/xbmc/pvr/epg/EpgContainer.h
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h"
+#include "pvr/settings/PVRSettings.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "utils/EventStream.h"
+
+#include <atomic>
+#include <list>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CDateTime;
+
+namespace PVR
+{
+ class CEpgUpdateRequest;
+ class CEpgTagStateChange;
+ class CPVREpg;
+ class CPVREpgChannelData;
+ class CPVREpgDatabase;
+ class CPVREpgInfoTag;
+ class CPVREpgSearchFilter;
+
+ enum class PVREvent;
+
+ struct PVREpgSearchData;
+
+ class CPVREpgContainer : private CThread
+ {
+ friend class CPVREpgDatabase;
+
+ public:
+ CPVREpgContainer() = delete;
+
+ /*!
+ * @brief Create a new EPG table container.
+ */
+ explicit CPVREpgContainer(CEventSource<PVREvent>& eventSource);
+
+ /*!
+ * @brief Destroy this instance.
+ */
+ ~CPVREpgContainer() override;
+
+ /*!
+ * @brief Get a pointer to the database instance.
+ * @return A pointer to the database instance.
+ */
+ std::shared_ptr<CPVREpgDatabase> GetEpgDatabase() const;
+
+ /*!
+ * @brief Start the EPG update thread.
+ */
+ void Start();
+
+ /*!
+ * @brief Stop the EPG update thread.
+ */
+ void Stop();
+
+ /**
+ * @brief (re)load EPG data.
+ * @return True if loaded successfully, false otherwise.
+ */
+ bool Load();
+
+ /**
+ * @brief unload all EPG data.
+ */
+ void Unload();
+
+ /*!
+ * @brief Check whether the EpgContainer has fully started.
+ * @return True if started, false otherwise.
+ */
+ bool IsStarted() const;
+
+ /*!
+ * @brief Queue the deletion of the given EPG tables from this container.
+ * @param epg The tables to delete.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteEpgs(const std::vector<std::shared_ptr<CPVREpg>>& epgs);
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+
+ /*!
+ * @brief Create the EPg for a given channel.
+ * @param iEpgId The EPG id.
+ * @param strScraperName The scraper name.
+ * @param channelData The channel data.
+ * @return the created EPG
+ */
+ std::shared_ptr<CPVREpg> CreateChannelEpg(int iEpgId, const std::string& strScraperName, const std::shared_ptr<CPVREpgChannelData>& channelData);
+
+ /*!
+ * @brief Get the start and end time across all EPGs.
+ * @return The times; first: start time, second: end time.
+ */
+ std::pair<CDateTime, CDateTime> GetFirstAndLastEPGDate() const;
+
+ /*!
+ * @brief Get all EPGs.
+ * @return The EPGs.
+ */
+ std::vector<std::shared_ptr<CPVREpg>> GetAllEpgs() const;
+
+ /*!
+ * @brief Get an EPG given its ID.
+ * @param iEpgId The database ID of the table.
+ * @return The EPG or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVREpg> GetById(int iEpgId) const;
+
+ /*!
+ * @brief Get an EPG given its client id and channel uid.
+ * @param iClientId the id of the pvr client providing the EPG
+ * @param iChannelUid the uid of the channel for the EPG
+ * @return The EPG or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVREpg> GetByChannelUid(int iClientId, int iChannelUid) const;
+
+ /*!
+ * @brief Get the EPG event with the given event id
+ * @param epg The epg to lookup the event.
+ * @param iBroadcastId The event id to lookup.
+ * @return The requested event, or an empty tag when not found
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagById(const std::shared_ptr<CPVREpg>& epg, unsigned int iBroadcastId) const;
+
+ /*!
+ * @brief Get the EPG event with the given database id
+ * @param iDatabaseId The id to lookup.
+ * @return The requested event, or an empty tag when not found
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagByDatabaseId(int iDatabaseId) const;
+
+ /*!
+ * @brief Get all EPG tags matching the given search criteria.
+ * @param searchData The search criteria.
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetTags(const PVREpgSearchData& searchData) const;
+
+ /*!
+ * @brief Notify EPG container that there are pending manual EPG updates
+ * @param bHasPendingUpdates The new value
+ */
+ void SetHasPendingUpdates(bool bHasPendingUpdates = true);
+
+ /*!
+ * @brief A client triggered an epg update request for a channel
+ * @param iClientID The id of the client which triggered the update request
+ * @param iUniqueChannelID The uid of the channel for which the epg shall be updated
+ */
+ void UpdateRequest(int iClientID, int iUniqueChannelID);
+
+ /*!
+ * @brief A client announced an updated epg tag for a channel
+ * @param tag The epg tag containing the updated data
+ * @param eNewState The kind of change (CREATED, UPDATED, DELETED)
+ */
+ void UpdateFromClient(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE eNewState);
+
+ /*!
+ * @brief Get the number of past days to show in the guide and to import from backends.
+ * @return the number of past epg days.
+ */
+ int GetPastDaysToDisplay() const;
+
+ /*!
+ * @brief Get the number of future days to show in the guide and to import from backends.
+ * @return the number of future epg days.
+ */
+ int GetFutureDaysToDisplay() const;
+
+ /*!
+ * @brief Inform the epg container that playback of an item just started.
+ */
+ void OnPlaybackStarted();
+
+ /*!
+ * @brief Inform the epg container that playback of an item was stopped due to user interaction.
+ */
+ void OnPlaybackStopped();
+
+ /*!
+ * @brief Inform the epg container that the system is going to sleep
+ */
+ void OnSystemSleep();
+
+ /*!
+ * @brief Inform the epg container that the system gets awake from sleep
+ */
+ void OnSystemWake();
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+ /*!
+ * @brief Get all saved searches from the database.
+ * @param bRadio Whether to fetch saved searches for radio or TV.
+ * @return The searches.
+ */
+ std::vector<std::shared_ptr<CPVREpgSearchFilter>> GetSavedSearches(bool bRadio);
+
+ /*!
+ * @brief Get the saved search matching the given id.
+ * @param bRadio Whether to fetch a TV or radio saved search.
+ * @param iId The id.
+ * @return The saved search or nullptr if not found.
+ */
+ std::shared_ptr<CPVREpgSearchFilter> GetSavedSearchById(bool bRadio, int iId);
+
+ /*!
+ * @brief Persist a saved search in the database.
+ * @param search The saved search.
+ * @return True on success, false otherwise.
+ */
+ bool PersistSavedSearch(CPVREpgSearchFilter& search);
+
+ /*!
+ * @brief Update time last executed for the given search.
+ * @param epgSearch The search.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateSavedSearchLastExecuted(const CPVREpgSearchFilter& epgSearch);
+
+ /*!
+ * @brief Delete a saved search from the database.
+ * @param search The saved search.
+ * @return True on success, false otherwise.
+ */
+ bool DeleteSavedSearch(const CPVREpgSearchFilter& search);
+
+ private:
+ /*!
+ * @brief Notify EPG table observers when the currently active tag changed.
+ * @return True if the check was done, false if it was not the right time to check
+ */
+ bool CheckPlayingEvents();
+
+ /*!
+ * @brief The next EPG ID to be given to a table when the db isn't being used.
+ * @return The next ID.
+ */
+ int NextEpgId();
+
+ /*!
+ * @brief Wait for an EPG update to finish.
+ */
+ void WaitForUpdateFinish();
+
+ /*!
+ * @brief Call Persist() on each table
+ * @param iMaxTimeslice time in milliseconds for max processing. Return after this time
+ * even if not all data was persisted, unless value is -1
+ * @return True when they all were persisted, false otherwise.
+ */
+ bool PersistAll(unsigned int iMaxTimeslice) const;
+
+ /*!
+ * @brief Remove old EPG entries.
+ * @return True if the old entries were removed successfully, false otherwise.
+ */
+ bool RemoveOldEntries();
+
+ /*!
+ * @brief Load and update the EPG data.
+ * @param bOnlyPending Only check and update EPG tables with pending manual updates
+ * @return True if the update has not been interrupted, false otherwise.
+ */
+ bool UpdateEPG(bool bOnlyPending = false);
+
+ /*!
+ * @brief Check whether a running update should be interrupted.
+ * @return True if a running update should be interrupted, false otherwise.
+ */
+ bool InterruptUpdate() const;
+
+ /*!
+ * @brief EPG update thread
+ */
+ void Process() override;
+
+ /*!
+ * @brief Load all tables from the database
+ */
+ void LoadFromDatabase();
+
+ /*!
+ * @brief Insert data from database
+ * @param newEpg the EPG containing the updated data.
+ */
+ void InsertFromDB(const std::shared_ptr<CPVREpg>& newEpg);
+
+ /*!
+ * @brief Queue the deletion of an EPG table from this container.
+ * @param epg The table to delete.
+ * @param database The database containing the epg data.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteEpg(const std::shared_ptr<CPVREpg>& epg,
+ const std::shared_ptr<CPVREpgDatabase>& database);
+
+ std::shared_ptr<CPVREpgDatabase> m_database; /*!< the EPG database */
+
+ bool m_bIsUpdating = false; /*!< true while an update is running */
+ std::atomic<bool> m_bIsInitialising = {
+ true}; /*!< true while the epg manager hasn't loaded all tables */
+ bool m_bStarted = false; /*!< true if EpgContainer has fully started */
+ bool m_bLoaded = false; /*!< true after epg data is initially loaded from the database */
+ bool m_bPreventUpdates = false; /*!< true to prevent EPG updates */
+ bool m_bPlaying = false; /*!< true if Kodi is currently playing something */
+ int m_pendingUpdates = 0; /*!< count of pending manual updates */
+ time_t m_iLastEpgCleanup = 0; /*!< the time the EPG was cleaned up */
+ time_t m_iNextEpgUpdate = 0; /*!< the time the EPG will be updated */
+ time_t m_iNextEpgActiveTagCheck = 0; /*!< the time the EPG will be checked for active tag updates */
+ int m_iNextEpgId = 0; /*!< the next epg ID that will be given to a new table when the db isn't being used */
+
+ std::map<int, std::shared_ptr<CPVREpg>> m_epgIdToEpgMap; /*!< the EPGs in this container. maps epg ids to epgs */
+ std::map<std::pair<int, int>, std::shared_ptr<CPVREpg>> m_channelUidToEpgMap; /*!< the EPGs in this container. maps channel uids to epgs */
+
+ mutable CCriticalSection m_critSection; /*!< a critical section for changes to this container */
+ CEvent m_updateEvent; /*!< trigger when an update finishes */
+
+ std::list<CEpgUpdateRequest> m_updateRequests; /*!< list of update requests triggered by addon */
+ CCriticalSection m_updateRequestsLock; /*!< protect update requests */
+
+ std::list<CEpgTagStateChange> m_epgTagChanges; /*!< list of updated epg tags announced by addon */
+ CCriticalSection m_epgTagChangesLock; /*!< protect changed epg tags list */
+
+ bool m_bUpdateNotificationPending = false; /*!< true while an epg updated notification to observers is pending. */
+ CPVRSettings m_settings;
+ CEventSource<PVREvent>& m_events;
+
+ std::atomic<bool> m_bSuspended = {false};
+ };
+}
diff --git a/xbmc/pvr/epg/EpgDatabase.cpp b/xbmc/pvr/epg/EpgDatabase.cpp
new file mode 100644
index 0000000..191f595
--- /dev/null
+++ b/xbmc/pvr/epg/EpgDatabase.cpp
@@ -0,0 +1,1494 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgDatabase.h"
+
+#include "ServiceBroker.h"
+#include "dbwrappers/dataset.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgSearchData.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace dbiplus;
+using namespace PVR;
+
+bool CPVREpgDatabase::Open()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return CDatabase::Open(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseEpg);
+}
+
+void CPVREpgDatabase::Close()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CDatabase::Close();
+}
+
+void CPVREpgDatabase::Lock()
+{
+ m_critSection.lock();
+}
+
+void CPVREpgDatabase::Unlock()
+{
+ m_critSection.unlock();
+}
+
+void CPVREpgDatabase::CreateTables()
+{
+ CLog::Log(LOGINFO, "Creating EPG database tables");
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating table 'epg'");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_pDS->exec(
+ "CREATE TABLE epg ("
+ "idEpg integer primary key, "
+ "sName varchar(64),"
+ "sScraperName varchar(32)"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating table 'epgtags'");
+ m_pDS->exec(
+ "CREATE TABLE epgtags ("
+ "idBroadcast integer primary key, "
+ "iBroadcastUid integer, "
+ "idEpg integer, "
+ "sTitle varchar(128), "
+ "sPlotOutline text, "
+ "sPlot text, "
+ "sOriginalTitle varchar(128), "
+ "sCast varchar(255), "
+ "sDirector varchar(255), "
+ "sWriter varchar(255), "
+ "iYear integer, "
+ "sIMDBNumber varchar(50), "
+ "sIconPath varchar(255), "
+ "iStartTime integer, "
+ "iEndTime integer, "
+ "iGenreType integer, "
+ "iGenreSubType integer, "
+ "sGenre varchar(128), "
+ "sFirstAired varchar(32), "
+ "iParentalRating integer, "
+ "iStarRating integer, "
+ "iSeriesId integer, "
+ "iEpisodeId integer, "
+ "iEpisodePart integer, "
+ "sEpisodeName varchar(128), "
+ "iFlags integer, "
+ "sSeriesLink varchar(255), "
+ "sParentalRatingCode varchar(64)"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating table 'lastepgscan'");
+ m_pDS->exec("CREATE TABLE lastepgscan ("
+ "idEpg integer primary key, "
+ "sLastScan varchar(20)"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating table 'savedsearches'");
+ m_pDS->exec("CREATE TABLE savedsearches ("
+ "idSearch integer primary key,"
+ "sTitle varchar(255), "
+ "sLastExecutedDateTime varchar(20), "
+ "sSearchTerm varchar(255), "
+ "bSearchInDescription bool, "
+ "iGenreType integer, "
+ "sStartDateTime varchar(20), "
+ "sEndDateTime varchar(20), "
+ "bIsCaseSensitive bool, "
+ "iMinimumDuration integer, "
+ "iMaximumDuration integer, "
+ "bIsRadio bool, "
+ "iClientId integer, "
+ "iChannelUid integer, "
+ "bIncludeUnknownGenres bool, "
+ "bRemoveDuplicates bool, "
+ "bIgnoreFinishedBroadcasts bool, "
+ "bIgnoreFutureBroadcasts bool, "
+ "bFreeToAirOnly bool, "
+ "bIgnorePresentTimers bool, "
+ "bIgnorePresentRecordings bool,"
+ "iChannelGroup integer"
+ ")");
+}
+
+void CPVREpgDatabase::CreateAnalytics()
+{
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating EPG database indices");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_pDS->exec("CREATE UNIQUE INDEX idx_epg_idEpg_iStartTime on epgtags(idEpg, iStartTime desc);");
+ m_pDS->exec("CREATE INDEX idx_epg_iEndTime on epgtags(iEndTime);");
+}
+
+void CPVREpgDatabase::UpdateTables(int iVersion)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (iVersion < 5)
+ m_pDS->exec("ALTER TABLE epgtags ADD sGenre varchar(128);");
+
+ if (iVersion < 9)
+ m_pDS->exec("ALTER TABLE epgtags ADD sIconPath varchar(255);");
+
+ if (iVersion < 10)
+ {
+ m_pDS->exec("ALTER TABLE epgtags ADD sOriginalTitle varchar(128);");
+ m_pDS->exec("ALTER TABLE epgtags ADD sCast varchar(255);");
+ m_pDS->exec("ALTER TABLE epgtags ADD sDirector varchar(255);");
+ m_pDS->exec("ALTER TABLE epgtags ADD sWriter varchar(255);");
+ m_pDS->exec("ALTER TABLE epgtags ADD iYear integer;");
+ m_pDS->exec("ALTER TABLE epgtags ADD sIMDBNumber varchar(50);");
+ }
+
+ if (iVersion < 11)
+ {
+ m_pDS->exec("ALTER TABLE epgtags ADD iFlags integer;");
+ }
+
+ if (iVersion < 12)
+ {
+ m_pDS->exec("ALTER TABLE epgtags ADD sSeriesLink varchar(255);");
+ }
+
+ if (iVersion < 13)
+ {
+ const bool isMySQL = StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseEpg.type, "mysql");
+
+ m_pDS->exec(
+ "CREATE TABLE epgtags_new ("
+ "idBroadcast integer primary key, "
+ "iBroadcastUid integer, "
+ "idEpg integer, "
+ "sTitle varchar(128), "
+ "sPlotOutline text, "
+ "sPlot text, "
+ "sOriginalTitle varchar(128), "
+ "sCast varchar(255), "
+ "sDirector varchar(255), "
+ "sWriter varchar(255), "
+ "iYear integer, "
+ "sIMDBNumber varchar(50), "
+ "sIconPath varchar(255), "
+ "iStartTime integer, "
+ "iEndTime integer, "
+ "iGenreType integer, "
+ "iGenreSubType integer, "
+ "sGenre varchar(128), "
+ "sFirstAired varchar(32), "
+ "iParentalRating integer, "
+ "iStarRating integer, "
+ "iSeriesId integer, "
+ "iEpisodeId integer, "
+ "iEpisodePart integer, "
+ "sEpisodeName varchar(128), "
+ "iFlags integer, "
+ "sSeriesLink varchar(255)"
+ ")"
+ );
+
+ m_pDS->exec(
+ "INSERT INTO epgtags_new ("
+ "idBroadcast, "
+ "iBroadcastUid, "
+ "idEpg, "
+ "sTitle, "
+ "sPlotOutline, "
+ "sPlot, "
+ "sOriginalTitle, "
+ "sCast, "
+ "sDirector, "
+ "sWriter, "
+ "iYear, "
+ "sIMDBNumber, "
+ "sIconPath, "
+ "iStartTime, "
+ "iEndTime, "
+ "iGenreType, "
+ "iGenreSubType, "
+ "sGenre, "
+ "sFirstAired, "
+ "iParentalRating, "
+ "iStarRating, "
+ "iSeriesId, "
+ "iEpisodeId, "
+ "iEpisodePart, "
+ "sEpisodeName, "
+ "iFlags, "
+ "sSeriesLink"
+ ") "
+ "SELECT "
+ "idBroadcast, "
+ "iBroadcastUid, "
+ "idEpg, "
+ "sTitle, "
+ "sPlotOutline, "
+ "sPlot, "
+ "sOriginalTitle, "
+ "sCast, "
+ "sDirector, "
+ "sWriter, "
+ "iYear, "
+ "sIMDBNumber, "
+ "sIconPath, "
+ "iStartTime, "
+ "iEndTime, "
+ "iGenreType, "
+ "iGenreSubType, "
+ "sGenre, "
+ "'' AS sFirstAired, "
+ "iParentalRating, "
+ "iStarRating, "
+ "iSeriesId, "
+ "iEpisodeId, "
+ "iEpisodePart, "
+ "sEpisodeName, "
+ "iFlags, "
+ "sSeriesLink "
+ "FROM epgtags"
+ );
+
+ if (isMySQL)
+ m_pDS->exec(
+ "UPDATE epgtags_new INNER JOIN epgtags ON epgtags_new.idBroadcast = epgtags.idBroadcast "
+ "SET epgtags_new.sFirstAired = DATE(FROM_UNIXTIME(epgtags.iFirstAired)) "
+ "WHERE epgtags.iFirstAired > 0"
+ );
+ else
+ m_pDS->exec(
+ "UPDATE epgtags_new SET sFirstAired = "
+ "COALESCE((SELECT STRFTIME('%Y-%m-%d', iFirstAired, 'UNIXEPOCH') "
+ "FROM epgtags WHERE epgtags.idBroadcast = epgtags_new.idBroadcast "
+ "AND epgtags.iFirstAired > 0), '')"
+ );
+
+ m_pDS->exec("DROP TABLE epgtags");
+ m_pDS->exec("ALTER TABLE epgtags_new RENAME TO epgtags");
+ }
+
+ if (iVersion < 14)
+ {
+ m_pDS->exec("ALTER TABLE epgtags ADD sParentalRatingCode varchar(64);");
+ }
+
+ if (iVersion < 15)
+ {
+ m_pDS->exec("CREATE TABLE savedsearches ("
+ "idSearch integer primary key,"
+ "sTitle varchar(255), "
+ "sLastExecutedDateTime varchar(20), "
+ "sSearchTerm varchar(255), "
+ "bSearchInDescription bool, "
+ "iGenreType integer, "
+ "sStartDateTime varchar(20), "
+ "sEndDateTime varchar(20), "
+ "bIsCaseSensitive bool, "
+ "iMinimumDuration integer, "
+ "iMaximumDuration integer, "
+ "bIsRadio bool, "
+ "iClientId integer, "
+ "iChannelUid integer, "
+ "bIncludeUnknownGenres bool, "
+ "bRemoveDuplicates bool, "
+ "bIgnoreFinishedBroadcasts bool, "
+ "bIgnoreFutureBroadcasts bool, "
+ "bFreeToAirOnly bool, "
+ "bIgnorePresentTimers bool, "
+ "bIgnorePresentRecordings bool"
+ ")");
+ }
+
+ if (iVersion < 16)
+ {
+ m_pDS->exec("ALTER TABLE savedsearches ADD iChannelGroup integer;");
+ m_pDS->exec("UPDATE savedsearches SET iChannelGroup = -1");
+ }
+}
+
+bool CPVREpgDatabase::DeleteEpg()
+{
+ bool bReturn(false);
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Deleting all EPG data from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bReturn = DeleteValues("epg") || bReturn;
+ bReturn = DeleteValues("epgtags") || bReturn;
+ bReturn = DeleteValues("lastepgscan") || bReturn;
+
+ return bReturn;
+}
+
+bool CPVREpgDatabase::QueueDeleteEpgQuery(const CPVREpg& table)
+{
+ /* invalid channel */
+ if (table.EpgID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel id: {}", table.EpgID());
+ return false;
+ }
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u", table.EpgID()));
+
+ std::string strQuery;
+ if (BuildSQL(PrepareSQL("DELETE FROM %s ", "epg"), filter, strQuery))
+ return QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+bool CPVREpgDatabase::QueueDeleteTagQuery(const CPVREpgInfoTag& tag)
+{
+ /* tag without a database ID was not persisted */
+ if (tag.DatabaseID() <= 0)
+ return false;
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idBroadcast = %u", tag.DatabaseID()));
+
+ std::string strQuery;
+ BuildSQL(PrepareSQL("DELETE FROM %s ", "epgtags"), filter, strQuery);
+ return QueueDeleteQuery(strQuery);
+}
+
+std::vector<std::shared_ptr<CPVREpg>> CPVREpgDatabase::GetAll()
+{
+ std::vector<std::shared_ptr<CPVREpg>> result;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string strQuery = PrepareSQL("SELECT idEpg, sName, sScraperName FROM epg;");
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ int iEpgID = m_pDS->fv("idEpg").get_asInt();
+ std::string strName = m_pDS->fv("sName").get_asString().c_str();
+ std::string strScraperName = m_pDS->fv("sScraperName").get_asString().c_str();
+
+ result.emplace_back(new CPVREpg(iEpgID, strName, strScraperName, shared_from_this()));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG data from the database");
+ }
+ }
+
+ return result;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::CreateEpgTag(
+ const std::unique_ptr<dbiplus::Dataset>& pDS)
+{
+ if (!pDS->eof())
+ {
+ std::shared_ptr<CPVREpgInfoTag> newTag(
+ new CPVREpgInfoTag(m_pDS->fv("idEpg").get_asInt(), m_pDS->fv("sIconPath").get_asString()));
+
+ time_t iStartTime;
+ iStartTime = static_cast<time_t>(m_pDS->fv("iStartTime").get_asInt());
+ const CDateTime startTime(iStartTime);
+ newTag->m_startTime = startTime;
+
+ time_t iEndTime = static_cast<time_t>(m_pDS->fv("iEndTime").get_asInt());
+ const CDateTime endTime(iEndTime);
+ newTag->m_endTime = endTime;
+
+ const std::string sFirstAired = m_pDS->fv("sFirstAired").get_asString();
+ if (sFirstAired.length() > 0)
+ newTag->m_firstAired.SetFromW3CDate(sFirstAired);
+
+ int iBroadcastUID = m_pDS->fv("iBroadcastUid").get_asInt();
+ // Compat: null value for broadcast uid changed from numerical -1 to 0 with PVR Addon API v4.0.0
+ newTag->m_iUniqueBroadcastID = iBroadcastUID == -1 ? EPG_TAG_INVALID_UID : iBroadcastUID;
+
+ newTag->m_iDatabaseID = m_pDS->fv("idBroadcast").get_asInt();
+ newTag->m_strTitle = m_pDS->fv("sTitle").get_asString();
+ newTag->m_strPlotOutline = m_pDS->fv("sPlotOutline").get_asString();
+ newTag->m_strPlot = m_pDS->fv("sPlot").get_asString();
+ newTag->m_strOriginalTitle = m_pDS->fv("sOriginalTitle").get_asString();
+ newTag->m_cast = newTag->Tokenize(m_pDS->fv("sCast").get_asString());
+ newTag->m_directors = newTag->Tokenize(m_pDS->fv("sDirector").get_asString());
+ newTag->m_writers = newTag->Tokenize(m_pDS->fv("sWriter").get_asString());
+ newTag->m_iYear = m_pDS->fv("iYear").get_asInt();
+ newTag->m_strIMDBNumber = m_pDS->fv("sIMDBNumber").get_asString();
+ newTag->m_iParentalRating = m_pDS->fv("iParentalRating").get_asInt();
+ newTag->m_iStarRating = m_pDS->fv("iStarRating").get_asInt();
+ newTag->m_iEpisodeNumber = m_pDS->fv("iEpisodeId").get_asInt();
+ newTag->m_iEpisodePart = m_pDS->fv("iEpisodePart").get_asInt();
+ newTag->m_strEpisodeName = m_pDS->fv("sEpisodeName").get_asString();
+ newTag->m_iSeriesNumber = m_pDS->fv("iSeriesId").get_asInt();
+ newTag->m_iFlags = m_pDS->fv("iFlags").get_asInt();
+ newTag->m_strSeriesLink = m_pDS->fv("sSeriesLink").get_asString();
+ newTag->m_strParentalRatingCode = m_pDS->fv("sParentalRatingCode").get_asString();
+ newTag->m_iGenreType = m_pDS->fv("iGenreType").get_asInt();
+ newTag->m_iGenreSubType = m_pDS->fv("iGenreSubType").get_asInt();
+ newTag->m_strGenreDescription = m_pDS->fv("sGenre").get_asString();
+
+ return newTag;
+ }
+ return {};
+}
+
+bool CPVREpgDatabase::HasTags(int iEpgID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT iStartTime FROM epgtags WHERE idEpg = %u LIMIT 1;", iEpgID);
+ std::string strValue = GetSingleValue(strQuery);
+ return !strValue.empty();
+}
+
+CDateTime CPVREpgDatabase::GetLastEndTime(int iEpgID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT MAX(iEndTime) FROM epgtags WHERE idEpg = %u;", iEpgID);
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ return CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ return {};
+}
+
+std::pair<CDateTime, CDateTime> CPVREpgDatabase::GetFirstAndLastEPGDate()
+{
+ CDateTime first;
+ CDateTime last;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // 1st query: get min start time
+ std::string strQuery = PrepareSQL("SELECT MIN(iStartTime) FROM epgtags;");
+
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ first = CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ // 2nd query: get max end time
+ strQuery = PrepareSQL("SELECT MAX(iEndTime) FROM epgtags;");
+
+ strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ last = CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ return {first, last};
+}
+
+CDateTime CPVREpgDatabase::GetMinStartTime(int iEpgID, const CDateTime& minStart)
+{
+ time_t t;
+ minStart.GetAsTime(t);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT MIN(iStartTime) "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iStartTime > %u;",
+ iEpgID, static_cast<unsigned int>(t));
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ return CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ return {};
+}
+
+CDateTime CPVREpgDatabase::GetMaxEndTime(int iEpgID, const CDateTime& maxEnd)
+{
+ time_t t;
+ maxEnd.GetAsTime(t);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT MAX(iEndTime) "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iEndTime <= %u;",
+ iEpgID, static_cast<unsigned int>(t));
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ return CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ return {};
+}
+
+namespace
+{
+
+class CSearchTermConverter
+{
+public:
+ explicit CSearchTermConverter(const std::string& strSearchTerm) { Parse(strSearchTerm); }
+
+ std::string ToSQL(const std::string& strFieldName) const
+ {
+ std::string result = "(";
+
+ for (auto it = m_fragments.cbegin(); it != m_fragments.cend();)
+ {
+ result += (*it);
+
+ ++it;
+ if (it != m_fragments.cend())
+ result += strFieldName;
+ }
+
+ StringUtils::TrimRight(result);
+ result += ")";
+ return result;
+ }
+
+private:
+ void Parse(const std::string& strSearchTerm)
+ {
+ std::string strParsedSearchTerm(strSearchTerm);
+ StringUtils::Trim(strParsedSearchTerm);
+
+ std::string strFragment;
+
+ bool bNextOR = false;
+ while (!strParsedSearchTerm.empty())
+ {
+ StringUtils::TrimLeft(strParsedSearchTerm);
+
+ if (StringUtils::StartsWith(strParsedSearchTerm, "!") ||
+ StringUtils::StartsWithNoCase(strParsedSearchTerm, "not"))
+ {
+ std::string strDummy;
+ GetAndCutNextTerm(strParsedSearchTerm, strDummy);
+ strFragment += " NOT ";
+ bNextOR = false;
+ }
+ else if (StringUtils::StartsWith(strParsedSearchTerm, "+") ||
+ StringUtils::StartsWithNoCase(strParsedSearchTerm, "and"))
+ {
+ std::string strDummy;
+ GetAndCutNextTerm(strParsedSearchTerm, strDummy);
+ strFragment += " AND ";
+ bNextOR = false;
+ }
+ else if (StringUtils::StartsWith(strParsedSearchTerm, "|") ||
+ StringUtils::StartsWithNoCase(strParsedSearchTerm, "or"))
+ {
+ std::string strDummy;
+ GetAndCutNextTerm(strParsedSearchTerm, strDummy);
+ strFragment += " OR ";
+ bNextOR = false;
+ }
+ else
+ {
+ std::string strTerm;
+ GetAndCutNextTerm(strParsedSearchTerm, strTerm);
+ if (!strTerm.empty())
+ {
+ if (bNextOR && !m_fragments.empty())
+ strFragment += " OR "; // default operator
+
+ strFragment += "(UPPER(";
+
+ m_fragments.emplace_back(strFragment);
+ strFragment.clear();
+
+ strFragment += ") LIKE UPPER('%";
+ StringUtils::Replace(strTerm, "'", "''"); // escape '
+ strFragment += strTerm;
+ strFragment += "%')) ";
+
+ bNextOR = true;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ StringUtils::TrimLeft(strParsedSearchTerm);
+ }
+
+ if (!strFragment.empty())
+ m_fragments.emplace_back(strFragment);
+ }
+
+ static void GetAndCutNextTerm(std::string& strSearchTerm, std::string& strNextTerm)
+ {
+ std::string strFindNext(" ");
+
+ if (StringUtils::EndsWith(strSearchTerm, "\""))
+ {
+ strSearchTerm.erase(0, 1);
+ strFindNext = "\"";
+ }
+
+ const size_t iNextPos = strSearchTerm.find(strFindNext);
+ if (iNextPos != std::string::npos)
+ {
+ strNextTerm = strSearchTerm.substr(0, iNextPos);
+ strSearchTerm.erase(0, iNextPos + 1);
+ }
+ else
+ {
+ strNextTerm = strSearchTerm;
+ strSearchTerm.clear();
+ }
+ }
+
+ std::vector<std::string> m_fragments;
+};
+
+} // unnamed namespace
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgDatabase::GetEpgTags(
+ const PVREpgSearchData& searchData)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ std::string strQuery = PrepareSQL("SELECT * FROM epgtags");
+
+ Filter filter;
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // min start datetime
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_startDateTime.IsValid())
+ {
+ time_t minStart;
+ searchData.m_startDateTime.GetAsTime(minStart);
+ filter.AppendWhere(PrepareSQL("iStartTime >= %u", static_cast<unsigned int>(minStart)));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // max end datetime
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_endDateTime.IsValid())
+ {
+ time_t maxEnd;
+ searchData.m_endDateTime.GetAsTime(maxEnd);
+ filter.AppendWhere(PrepareSQL("iEndTime <= %u", static_cast<unsigned int>(maxEnd)));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // ignore finished broadcasts
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_bIgnoreFinishedBroadcasts)
+ {
+ const time_t minEnd = std::time(nullptr); // now
+ filter.AppendWhere(PrepareSQL("iEndTime > %u", static_cast<unsigned int>(minEnd)));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // ignore future broadcasts
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_bIgnoreFutureBroadcasts)
+ {
+ const time_t maxStart = std::time(nullptr); // now
+ filter.AppendWhere(PrepareSQL("iStartTime < %u", static_cast<unsigned int>(maxStart)));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // genre type
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_iGenreType != EPG_SEARCH_UNSET)
+ {
+ if (searchData.m_bIncludeUnknownGenres)
+ {
+ // match the exact genre and everything with unknown genre
+ filter.AppendWhere(PrepareSQL("(iGenreType == %u) OR (iGenreType < %u) OR (iGenreType > %u)",
+ searchData.m_iGenreType, EPG_EVENT_CONTENTMASK_MOVIEDRAMA,
+ EPG_EVENT_CONTENTMASK_USERDEFINED));
+ }
+ else
+ {
+ // match only the exact genre
+ filter.AppendWhere(PrepareSQL("iGenreType == %u", searchData.m_iGenreType));
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // search term
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (!searchData.m_strSearchTerm.empty())
+ {
+ const CSearchTermConverter conv(searchData.m_strSearchTerm);
+
+ // title
+ std::string strWhere = conv.ToSQL("sTitle");
+
+ // plot outline
+ strWhere += " OR ";
+ strWhere += conv.ToSQL("sPlotOutline");
+
+ if (searchData.m_bSearchInDescription)
+ {
+ // plot
+ strWhere += " OR ";
+ strWhere += conv.ToSQL("sPlot");
+ }
+
+ filter.AppendWhere(strWhere);
+ }
+
+ if (BuildSQL(strQuery, filter, strQuery))
+ {
+ try
+ {
+ if (m_pDS->query(strQuery))
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ while (!m_pDS->eof())
+ {
+ tags.emplace_back(CreateEpgTag(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return tags;
+ }
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags for given search criteria");
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByUniqueBroadcastID(
+ int iEpgID, unsigned int iUniqueBroadcastId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iBroadcastUid = %u;",
+ iEpgID, iUniqueBroadcastId);
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG tag with unique broadcast ID ({}) from the database",
+ iUniqueBroadcastId);
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByDatabaseID(int iEpgID, int iDatabaseId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND idBroadcast = %u;",
+ iEpgID, iDatabaseId);
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG tag with database ID ({}) from the database",
+ iDatabaseId);
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByStartTime(int iEpgID,
+ const CDateTime& startTime)
+{
+ time_t start;
+ startTime.GetAsTime(start);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iStartTime = %u;",
+ iEpgID, static_cast<unsigned int>(start));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG tag with start time ({}) from the database",
+ startTime.GetAsDBDateTime());
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByMinStartTime(
+ int iEpgID, const CDateTime& minStartTime)
+{
+ time_t minStart;
+ minStartTime.GetAsTime(minStart);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iStartTime >= %u ORDER BY iStartTime ASC LIMIT 1;",
+ iEpgID, static_cast<unsigned int>(minStart));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags with min start time ({}) for EPG ({})",
+ minStartTime.GetAsDBDateTime(), iEpgID);
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByMaxEndTime(int iEpgID,
+ const CDateTime& maxEndTime)
+{
+ time_t maxEnd;
+ maxEndTime.GetAsTime(maxEnd);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iEndTime <= %u ORDER BY iStartTime DESC LIMIT 1;",
+ iEpgID, static_cast<unsigned int>(maxEnd));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags with max end time ({}) for EPG ({})",
+ maxEndTime.GetAsDBDateTime(), iEpgID);
+ }
+ }
+
+ return {};
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgDatabase::GetEpgTagsByMinStartMaxEndTime(
+ int iEpgID, const CDateTime& minStartTime, const CDateTime& maxEndTime)
+{
+ time_t minStart;
+ minStartTime.GetAsTime(minStart);
+
+ time_t maxEnd;
+ maxEndTime.GetAsTime(maxEnd);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iStartTime >= %u AND iEndTime <= %u ORDER BY iStartTime;",
+ iEpgID, static_cast<unsigned int>(minStart), static_cast<unsigned int>(maxEnd));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ while (!m_pDS->eof())
+ {
+ tags.emplace_back(CreateEpgTag(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return tags;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR,
+ "Could not load tags with min start time ({}) and max end time ({}) for EPG ({})",
+ minStartTime.GetAsDBDateTime(), maxEndTime.GetAsDBDateTime(), iEpgID);
+ }
+ }
+
+ return {};
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgDatabase::GetEpgTagsByMinEndMaxStartTime(
+ int iEpgID, const CDateTime& minEndTime, const CDateTime& maxStartTime)
+{
+ time_t minEnd;
+ minEndTime.GetAsTime(minEnd);
+
+ time_t maxStart;
+ maxStartTime.GetAsTime(maxStart);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iEndTime >= %u AND iStartTime <= %u ORDER BY iStartTime;",
+ iEpgID, static_cast<unsigned int>(minEnd), static_cast<unsigned int>(maxStart));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ while (!m_pDS->eof())
+ {
+ tags.emplace_back(CreateEpgTag(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return tags;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR,
+ "Could not load tags with min end time ({}) and max start time ({}) for EPG ({})",
+ minEndTime.GetAsDBDateTime(), maxStartTime.GetAsDBDateTime(), iEpgID);
+ }
+ }
+
+ return {};
+}
+
+bool CPVREpgDatabase::QueueDeleteEpgTagsByMinEndMaxStartTimeQuery(int iEpgID,
+ const CDateTime& minEndTime,
+ const CDateTime& maxStartTime)
+{
+ time_t minEnd;
+ minEndTime.GetAsTime(minEnd);
+
+ time_t maxStart;
+ maxStartTime.GetAsTime(maxStart);
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u AND iEndTime >= %u AND iStartTime <= %u", iEpgID,
+ static_cast<unsigned int>(minEnd),
+ static_cast<unsigned int>(maxStart)));
+
+ std::string strQuery;
+ if (BuildSQL("DELETE FROM epgtags", filter, strQuery))
+ return QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgDatabase::GetAllEpgTags(int iEpgID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * FROM epgtags WHERE idEpg = %u ORDER BY iStartTime;", iEpgID);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ while (!m_pDS->eof())
+ {
+ tags.emplace_back(CreateEpgTag(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return tags;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags for EPG ({})", iEpgID);
+ }
+ }
+ return {};
+}
+
+std::vector<std::string> CPVREpgDatabase::GetAllIconPaths(int iEpgID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT sIconPath FROM epgtags WHERE idEpg = %u;", iEpgID);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::vector<std::string> paths;
+ while (!m_pDS->eof())
+ {
+ paths.emplace_back(m_pDS->fv("sIconPath").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return paths;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags for EPG ({})", iEpgID);
+ }
+ }
+ return {};
+}
+
+bool CPVREpgDatabase::GetLastEpgScanTime(int iEpgId, CDateTime* lastScan)
+{
+ bool bReturn = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string strWhereClause = PrepareSQL("idEpg = %u", iEpgId);
+ std::string strValue = GetSingleValue("lastepgscan", "sLastScan", strWhereClause);
+
+ if (!strValue.empty())
+ {
+ lastScan->SetFromDBDateTime(strValue);
+ bReturn = true;
+ }
+ else
+ {
+ lastScan->SetValid(false);
+ }
+
+ return bReturn;
+}
+
+bool CPVREpgDatabase::QueuePersistLastEpgScanTimeQuery(int iEpgId, const CDateTime& lastScanTime)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string strQuery = PrepareSQL("REPLACE INTO lastepgscan(idEpg, sLastScan) VALUES (%u, '%s');",
+ iEpgId, lastScanTime.GetAsDBDateTime().c_str());
+
+ return QueueInsertQuery(strQuery);
+}
+
+bool CPVREpgDatabase::QueueDeleteLastEpgScanTimeQuery(const CPVREpg& table)
+{
+ if (table.EpgID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid EPG id: {}", table.EpgID());
+ return false;
+ }
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u", table.EpgID()));
+
+ std::string strQuery;
+ if (BuildSQL(PrepareSQL("DELETE FROM %s ", "lastepgscan"), filter, strQuery))
+ return QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+int CPVREpgDatabase::Persist(const CPVREpg& epg, bool bQueueWrite)
+{
+ int iReturn = -1;
+ std::string strQuery;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (epg.EpgID() > 0)
+ strQuery = PrepareSQL("REPLACE INTO epg (idEpg, sName, sScraperName) "
+ "VALUES (%u, '%s', '%s');",
+ epg.EpgID(), epg.Name().c_str(), epg.ScraperName().c_str());
+ else
+ strQuery = PrepareSQL("INSERT INTO epg (sName, sScraperName) "
+ "VALUES ('%s', '%s');",
+ epg.Name().c_str(), epg.ScraperName().c_str());
+
+ if (bQueueWrite)
+ {
+ if (QueueInsertQuery(strQuery))
+ iReturn = epg.EpgID() <= 0 ? 0 : epg.EpgID();
+ }
+ else
+ {
+ if (ExecuteQuery(strQuery))
+ iReturn = epg.EpgID() <= 0 ? static_cast<int>(m_pDS->lastinsertid()) : epg.EpgID();
+ }
+
+ return iReturn;
+}
+
+bool CPVREpgDatabase::DeleteEpgTags(int iEpgId, const CDateTime& maxEndTime)
+{
+ time_t iMaxEndTime;
+ maxEndTime.GetAsTime(iMaxEndTime);
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(
+ PrepareSQL("idEpg = %u AND iEndTime < %u", iEpgId, static_cast<unsigned int>(iMaxEndTime)));
+ return DeleteValues("epgtags", filter);
+}
+
+bool CPVREpgDatabase::DeleteEpgTags(int iEpgId)
+{
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u", iEpgId));
+ return DeleteValues("epgtags", filter);
+}
+
+bool CPVREpgDatabase::QueueDeleteEpgTags(int iEpgId)
+{
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u", iEpgId));
+
+ std::string strQuery;
+ BuildSQL(PrepareSQL("DELETE FROM %s ", "epgtags"), filter, strQuery);
+ return QueueDeleteQuery(strQuery);
+}
+
+bool CPVREpgDatabase::QueuePersistQuery(const CPVREpgInfoTag& tag)
+{
+ if (tag.EpgID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Tag '{}' does not have a valid table", tag.Title());
+ return false;
+ }
+
+ time_t iStartTime, iEndTime;
+ tag.StartAsUTC().GetAsTime(iStartTime);
+ tag.EndAsUTC().GetAsTime(iEndTime);
+
+ std::string sFirstAired;
+ if (tag.FirstAired().IsValid())
+ sFirstAired = tag.FirstAired().GetAsW3CDate();
+
+ int iBroadcastId = tag.DatabaseID();
+ std::string strQuery;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (iBroadcastId < 0)
+ {
+ strQuery = PrepareSQL(
+ "REPLACE INTO epgtags (idEpg, iStartTime, "
+ "iEndTime, sTitle, sPlotOutline, sPlot, sOriginalTitle, sCast, sDirector, sWriter, iYear, "
+ "sIMDBNumber, "
+ "sIconPath, iGenreType, iGenreSubType, sGenre, sFirstAired, iParentalRating, iStarRating, "
+ "iSeriesId, "
+ "iEpisodeId, iEpisodePart, sEpisodeName, iFlags, sSeriesLink, sParentalRatingCode, "
+ "iBroadcastUid) "
+ "VALUES (%u, %u, %u, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', %i, %i, "
+ "'%s', '%s', %i, %i, %i, %i, %i, '%s', %i, '%s', '%s', %i);",
+ tag.EpgID(), static_cast<unsigned int>(iStartTime), static_cast<unsigned int>(iEndTime),
+ tag.Title().c_str(), tag.PlotOutline().c_str(), tag.Plot().c_str(),
+ tag.OriginalTitle().c_str(), tag.DeTokenize(tag.Cast()).c_str(),
+ tag.DeTokenize(tag.Directors()).c_str(), tag.DeTokenize(tag.Writers()).c_str(), tag.Year(),
+ tag.IMDBNumber().c_str(), tag.ClientIconPath().c_str(), tag.GenreType(), tag.GenreSubType(),
+ tag.GenreDescription().c_str(), sFirstAired.c_str(), tag.ParentalRating(), tag.StarRating(),
+ tag.SeriesNumber(), tag.EpisodeNumber(), tag.EpisodePart(), tag.EpisodeName().c_str(),
+ tag.Flags(), tag.SeriesLink().c_str(), tag.ParentalRatingCode().c_str(),
+ tag.UniqueBroadcastID());
+ }
+ else
+ {
+ strQuery = PrepareSQL(
+ "REPLACE INTO epgtags (idEpg, iStartTime, "
+ "iEndTime, sTitle, sPlotOutline, sPlot, sOriginalTitle, sCast, sDirector, sWriter, iYear, "
+ "sIMDBNumber, "
+ "sIconPath, iGenreType, iGenreSubType, sGenre, sFirstAired, iParentalRating, iStarRating, "
+ "iSeriesId, "
+ "iEpisodeId, iEpisodePart, sEpisodeName, iFlags, sSeriesLink, sParentalRatingCode, "
+ "iBroadcastUid, idBroadcast) "
+ "VALUES (%u, %u, %u, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', %i, %i, "
+ "'%s', '%s', %i, %i, %i, %i, %i, '%s', %i, '%s', '%s', %i, %i);",
+ tag.EpgID(), static_cast<unsigned int>(iStartTime), static_cast<unsigned int>(iEndTime),
+ tag.Title().c_str(), tag.PlotOutline().c_str(), tag.Plot().c_str(),
+ tag.OriginalTitle().c_str(), tag.DeTokenize(tag.Cast()).c_str(),
+ tag.DeTokenize(tag.Directors()).c_str(), tag.DeTokenize(tag.Writers()).c_str(), tag.Year(),
+ tag.IMDBNumber().c_str(), tag.ClientIconPath().c_str(), tag.GenreType(), tag.GenreSubType(),
+ tag.GenreDescription().c_str(), sFirstAired.c_str(), tag.ParentalRating(), tag.StarRating(),
+ tag.SeriesNumber(), tag.EpisodeNumber(), tag.EpisodePart(), tag.EpisodeName().c_str(),
+ tag.Flags(), tag.SeriesLink().c_str(), tag.ParentalRatingCode().c_str(),
+ tag.UniqueBroadcastID(), iBroadcastId);
+ }
+
+ QueueInsertQuery(strQuery);
+ return true;
+}
+
+int CPVREpgDatabase::GetLastEPGId()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string strQuery = PrepareSQL("SELECT MAX(idEpg) FROM epg");
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ return std::atoi(strValue.c_str());
+ return 0;
+}
+
+/********** Saved searches methods **********/
+
+std::shared_ptr<CPVREpgSearchFilter> CPVREpgDatabase::CreateEpgSearchFilter(
+ bool bRadio, const std::unique_ptr<dbiplus::Dataset>& pDS)
+{
+ if (!pDS->eof())
+ {
+ auto newSearch = std::make_shared<CPVREpgSearchFilter>(bRadio);
+
+ newSearch->SetDatabaseId(m_pDS->fv("idSearch").get_asInt());
+ newSearch->SetTitle(m_pDS->fv("sTitle").get_asString());
+
+ const std::string lastExec = m_pDS->fv("sLastExecutedDateTime").get_asString();
+ if (!lastExec.empty())
+ newSearch->SetLastExecutedDateTime(CDateTime::FromDBDateTime(lastExec));
+
+ newSearch->SetSearchTerm(m_pDS->fv("sSearchTerm").get_asString());
+ newSearch->SetSearchInDescription(m_pDS->fv("bSearchInDescription").get_asBool());
+ newSearch->SetGenreType(m_pDS->fv("iGenreType").get_asInt());
+
+ const std::string start = m_pDS->fv("sStartDateTime").get_asString();
+ if (!start.empty())
+ newSearch->SetStartDateTime(CDateTime::FromDBDateTime(start));
+
+ const std::string end = m_pDS->fv("sEndDateTime").get_asString();
+ if (!end.empty())
+ newSearch->SetEndDateTime(CDateTime::FromDBDateTime(end));
+
+ newSearch->SetCaseSensitive(m_pDS->fv("bIsCaseSensitive").get_asBool());
+ newSearch->SetMinimumDuration(m_pDS->fv("iMinimumDuration").get_asInt());
+ newSearch->SetMaximumDuration(m_pDS->fv("iMaximumDuration").get_asInt());
+ newSearch->SetClientID(m_pDS->fv("iClientId").get_asInt());
+ newSearch->SetChannelUID(m_pDS->fv("iChannelUid").get_asInt());
+ newSearch->SetIncludeUnknownGenres(m_pDS->fv("bIncludeUnknownGenres").get_asBool());
+ newSearch->SetRemoveDuplicates(m_pDS->fv("bRemoveDuplicates").get_asBool());
+ newSearch->SetIgnoreFinishedBroadcasts(m_pDS->fv("bIgnoreFinishedBroadcasts").get_asBool());
+ newSearch->SetIgnoreFutureBroadcasts(m_pDS->fv("bIgnoreFutureBroadcasts").get_asBool());
+ newSearch->SetFreeToAirOnly(m_pDS->fv("bFreeToAirOnly").get_asBool());
+ newSearch->SetIgnorePresentTimers(m_pDS->fv("bIgnorePresentTimers").get_asBool());
+ newSearch->SetIgnorePresentRecordings(m_pDS->fv("bIgnorePresentRecordings").get_asBool());
+ newSearch->SetChannelGroupID(m_pDS->fv("iChannelGroup").get_asInt());
+
+ newSearch->SetChanged(false);
+
+ return newSearch;
+ }
+ return {};
+}
+
+std::vector<std::shared_ptr<CPVREpgSearchFilter>> CPVREpgDatabase::GetSavedSearches(bool bRadio)
+{
+ std::vector<std::shared_ptr<CPVREpgSearchFilter>> result;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * FROM savedsearches WHERE bIsRadio = %u", bRadio);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ result.emplace_back(CreateEpgSearchFilter(bRadio, m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG search data from the database");
+ }
+ }
+ return result;
+}
+
+std::shared_ptr<CPVREpgSearchFilter> CPVREpgDatabase::GetSavedSearchById(bool bRadio, int iId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * FROM savedsearches WHERE bIsRadio = %u AND idSearch = %u;", bRadio, iId);
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgSearchFilter> filter = CreateEpgSearchFilter(bRadio, m_pDS);
+ m_pDS->close();
+ return filter;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG search filter with id ({}) from the database", iId);
+ }
+ }
+
+ return {};
+}
+
+bool CPVREpgDatabase::Persist(CPVREpgSearchFilter& epgSearch)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // Insert a new entry if this is a new search, replace the existing otherwise
+ std::string strQuery;
+ if (epgSearch.GetDatabaseId() == -1)
+ strQuery = PrepareSQL(
+ "INSERT INTO savedsearches "
+ "(sTitle, sLastExecutedDateTime, sSearchTerm, bSearchInDescription, bIsCaseSensitive, "
+ "iGenreType, bIncludeUnknownGenres, sStartDateTime, sEndDateTime, iMinimumDuration, "
+ "iMaximumDuration, bIsRadio, iClientId, iChannelUid, bRemoveDuplicates, "
+ "bIgnoreFinishedBroadcasts, bIgnoreFutureBroadcasts, bFreeToAirOnly, bIgnorePresentTimers, "
+ "bIgnorePresentRecordings, iChannelGroup) "
+ "VALUES ('%s', '%s', '%s', %i, %i, %i, %i, '%s', '%s', %i, %i, %i, %i, %i, %i, %i, %i, "
+ "%i, %i, %i, %i);",
+ epgSearch.GetTitle().c_str(),
+ epgSearch.GetLastExecutedDateTime().IsValid()
+ ? epgSearch.GetLastExecutedDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetSearchTerm().c_str(), epgSearch.ShouldSearchInDescription() ? 1 : 0,
+ epgSearch.IsCaseSensitive() ? 1 : 0, epgSearch.GetGenreType(),
+ epgSearch.ShouldIncludeUnknownGenres() ? 1 : 0,
+ epgSearch.GetStartDateTime().IsValid()
+ ? epgSearch.GetStartDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetEndDateTime().IsValid() ? epgSearch.GetEndDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetMinimumDuration(), epgSearch.GetMaximumDuration(), epgSearch.IsRadio() ? 1 : 0,
+ epgSearch.GetClientID(), epgSearch.GetChannelUID(),
+ epgSearch.ShouldRemoveDuplicates() ? 1 : 0,
+ epgSearch.ShouldIgnoreFinishedBroadcasts() ? 1 : 0,
+ epgSearch.ShouldIgnoreFutureBroadcasts() ? 1 : 0, epgSearch.IsFreeToAirOnly() ? 1 : 0,
+ epgSearch.ShouldIgnorePresentTimers() ? 1 : 0,
+ epgSearch.ShouldIgnorePresentRecordings() ? 1 : 0, epgSearch.GetChannelGroupID());
+ else
+ strQuery = PrepareSQL(
+ "REPLACE INTO savedsearches "
+ "(idSearch, sTitle, sLastExecutedDateTime, sSearchTerm, bSearchInDescription, "
+ "bIsCaseSensitive, iGenreType, bIncludeUnknownGenres, sStartDateTime, sEndDateTime, "
+ "iMinimumDuration, iMaximumDuration, bIsRadio, iClientId, iChannelUid, bRemoveDuplicates, "
+ "bIgnoreFinishedBroadcasts, bIgnoreFutureBroadcasts, bFreeToAirOnly, bIgnorePresentTimers, "
+ "bIgnorePresentRecordings, iChannelGroup) "
+ "VALUES (%i, '%s', '%s', '%s', %i, %i, %i, %i, '%s', '%s', %i, %i, %i, %i, %i, %i, %i, %i, "
+ "%i, %i, %i, %i);",
+ epgSearch.GetDatabaseId(), epgSearch.GetTitle().c_str(),
+ epgSearch.GetLastExecutedDateTime().IsValid()
+ ? epgSearch.GetLastExecutedDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetSearchTerm().c_str(), epgSearch.ShouldSearchInDescription() ? 1 : 0,
+ epgSearch.IsCaseSensitive() ? 1 : 0, epgSearch.GetGenreType(),
+ epgSearch.ShouldIncludeUnknownGenres() ? 1 : 0,
+ epgSearch.GetStartDateTime().IsValid()
+ ? epgSearch.GetStartDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetEndDateTime().IsValid() ? epgSearch.GetEndDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetMinimumDuration(), epgSearch.GetMaximumDuration(), epgSearch.IsRadio() ? 1 : 0,
+ epgSearch.GetClientID(), epgSearch.GetChannelUID(),
+ epgSearch.ShouldRemoveDuplicates() ? 1 : 0,
+ epgSearch.ShouldIgnoreFinishedBroadcasts() ? 1 : 0,
+ epgSearch.ShouldIgnoreFutureBroadcasts() ? 1 : 0, epgSearch.IsFreeToAirOnly() ? 1 : 0,
+ epgSearch.ShouldIgnorePresentTimers() ? 1 : 0,
+ epgSearch.ShouldIgnorePresentRecordings() ? 1 : 0, epgSearch.GetChannelGroupID());
+
+ bool bReturn = ExecuteQuery(strQuery);
+
+ if (bReturn)
+ {
+ // Set the database id for searches persisted for the first time
+ if (epgSearch.GetDatabaseId() == -1)
+ epgSearch.SetDatabaseId(static_cast<int>(m_pDS->lastinsertid()));
+
+ epgSearch.SetChanged(false);
+ }
+
+ return bReturn;
+}
+
+bool CPVREpgDatabase::UpdateSavedSearchLastExecuted(const CPVREpgSearchFilter& epgSearch)
+{
+ if (epgSearch.GetDatabaseId() == -1)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::string strQuery = PrepareSQL(
+ "UPDATE savedsearches SET sLastExecutedDateTime = '%s' WHERE idSearch = %i",
+ epgSearch.GetLastExecutedDateTime().GetAsDBDateTime().c_str(), epgSearch.GetDatabaseId());
+ return ExecuteQuery(strQuery);
+}
+
+bool CPVREpgDatabase::Delete(const CPVREpgSearchFilter& epgSearch)
+{
+ if (epgSearch.GetDatabaseId() == -1)
+ return false;
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Deleting saved search '{}' from the database",
+ epgSearch.GetTitle());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idSearch = '%i'", epgSearch.GetDatabaseId()));
+
+ return DeleteValues("savedsearches", filter);
+}
+
+bool CPVREpgDatabase::DeleteSavedSearches()
+{
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Deleting all saved searches from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("savedsearches");
+}
diff --git a/xbmc/pvr/epg/EpgDatabase.h b/xbmc/pvr/epg/EpgDatabase.h
new file mode 100644
index 0000000..580568d
--- /dev/null
+++ b/xbmc/pvr/epg/EpgDatabase.h
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "dbwrappers/Database.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <vector>
+
+class CDateTime;
+
+namespace PVR
+{
+ class CPVREpg;
+ class CPVREpgInfoTag;
+ class CPVREpgSearchFilter;
+
+ struct PVREpgSearchData;
+
+ /** The EPG database */
+
+ static constexpr int EPG_COMMIT_QUERY_COUNT_LIMIT = 10000;
+
+ class CPVREpgDatabase : public CDatabase, public std::enable_shared_from_this<CPVREpgDatabase>
+ {
+ public:
+ /*!
+ * @brief Create a new instance of the EPG database.
+ */
+ CPVREpgDatabase() = default;
+
+ /*!
+ * @brief Destroy this instance.
+ */
+ ~CPVREpgDatabase() override = default;
+
+ /*!
+ * @brief Open the database.
+ * @return True if it was opened successfully, false otherwise.
+ */
+ bool Open() override;
+
+ /*!
+ * @brief Close the database.
+ */
+ void Close() override;
+
+ /*!
+ * @brief Lock the database.
+ */
+ void Lock();
+
+ /*!
+ * @brief Unlock the database.
+ */
+ void Unlock();
+
+ /*!
+ * @brief Get the minimal database version that is required to operate correctly.
+ * @return The minimal database version.
+ */
+ int GetSchemaVersion() const override { return 16; }
+
+ /*!
+ * @brief Get the default sqlite database filename.
+ * @return The default filename.
+ */
+ const char* GetBaseDBName() const override { return "Epg"; }
+
+ /*! @name EPG methods */
+ //@{
+
+ /*!
+ * @brief Remove all EPG information from the database
+ * @return True if the EPG information was erased, false otherwise.
+ */
+ bool DeleteEpg();
+
+ /*!
+ * @brief Queue deletionof an EPG table.
+ * @param tag The table to queue for deletion.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteEpgQuery(const CPVREpg& table);
+
+ /*!
+ * @brief Write the query to delete the given EPG tag to db query queue.
+ * @param tag The EPG tag to remove.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteTagQuery(const CPVREpgInfoTag& tag);
+
+ /*!
+ * @brief Get all EPG tables from the database. Does not get the EPG tables' entries.
+ * @return The entries.
+ */
+ std::vector<std::shared_ptr<CPVREpg>> GetAll();
+
+ /*!
+ * @brief Get all tags for a given EPG id.
+ * @param iEpgID The ID of the EPG.
+ * @return The entries.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetAllEpgTags(int iEpgID);
+
+ /*!
+ * @brief Get all icon paths for a given EPG id.
+ * @param iEpgID The ID of the EPG.
+ * @return The entries.
+ */
+ std::vector<std::string> GetAllIconPaths(int iEpgID);
+
+ /*!
+ * @brief Check whether this EPG has any tags.
+ * @param iEpgID The ID of the EPG.
+ * @return True in case there are tags, false otherwise.
+ */
+ bool HasTags(int iEpgID);
+
+ /*!
+ * @brief Get the end time of the last tag in this EPG.
+ * @param iEpgID The ID of the EPG.
+ * @return The time.
+ */
+ CDateTime GetLastEndTime(int iEpgID);
+
+ /*!
+ * @brief Get the start and end time across all EPGs.
+ * @return The times; first: start time, second: end time.
+ */
+ std::pair<CDateTime, CDateTime> GetFirstAndLastEPGDate();
+
+ /*!
+ * @brief Get the start time of the first tag with a start time greater than the given min time.
+ * @param iEpgID The ID of the EPG.
+ * @param minStart The min start time.
+ * @return The time.
+ */
+ CDateTime GetMinStartTime(int iEpgID, const CDateTime& minStart);
+
+ /*!
+ * @brief Get the end time of the first tag with an end time less than the given max time.
+ * @param iEpgID The ID of the EPG.
+ * @param maxEnd The mx end time.
+ * @return The time.
+ */
+ CDateTime GetMaxEndTime(int iEpgID, const CDateTime& maxEnd);
+
+ /*!
+ * @brief Get all EPG tags matching the given search criteria.
+ * @param searchData The search criteria.
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTags(const PVREpgSearchData& searchData);
+
+ /*!
+ * @brief Get an EPG tag given its EPG id and unique broadcast ID.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param iUniqueBroadcastId The unique broadcast ID for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByUniqueBroadcastID(int iEpgID,
+ unsigned int iUniqueBroadcastId);
+
+ /*!
+ * @brief Get an EPG tag given its EPG id and database ID.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param iDatabaseId The database ID for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByDatabaseID(int iEpgID, int iDatabaseId);
+
+ /*!
+ * @brief Get an EPG tag given its EPG ID and start time.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param startTime The start time for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByStartTime(int iEpgID, const CDateTime& startTime);
+
+ /*!
+ * @brief Get the next EPG tag matching the given EPG id and min start time.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param minStartTime The min start time for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByMinStartTime(int iEpgID,
+ const CDateTime& minStartTime);
+
+ /*!
+ * @brief Get the next EPG tag matching the given EPG id and max end time.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param maxEndTime The max end time for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByMaxEndTime(int iEpgID, const CDateTime& maxEndTime);
+
+ /*!
+ * @brief Get all EPG tags matching the given EPG id, min start time and max end time.
+ * @param iEpgID The ID of the EPG for the tags to get.
+ * @param minStartTime The min start time for the tags to get.
+ * @param maxEndTime The max end time for the tags to get.
+ * @return The tags or empty vector, if no tags were found.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTagsByMinStartMaxEndTime(
+ int iEpgID, const CDateTime& minStartTime, const CDateTime& maxEndTime);
+
+ /*!
+ * @brief Get all EPG tags matching the given EPG id, min end time and max start time.
+ * @param iEpgID The ID of the EPG for the tags to get.
+ * @param minEndTime The min end time for the tags to get.
+ * @param maxStartTime The max start time for the tags to get.
+ * @return The tags or empty vector, if no tags were found.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTagsByMinEndMaxStartTime(
+ int iEpgID, const CDateTime& minEndTime, const CDateTime& maxStartTime);
+
+ /*!
+ * @brief Write the query to delete all EPG tags in range of given EPG id, min end time and max
+ * start time to db query queue. .
+ * @param iEpgID The ID of the EPG for the tags to delete.
+ * @param minEndTime The min end time for the tags to delete.
+ * @param maxStartTime The max start time for the tags to delete.
+ * @return True if it was removed or queued successfully, false otherwise.
+ */
+ bool QueueDeleteEpgTagsByMinEndMaxStartTimeQuery(int iEpgID,
+ const CDateTime& minEndTime,
+ const CDateTime& maxStartTime);
+
+ /*!
+ * @brief Get the last stored EPG scan time.
+ * @param iEpgId The table to update the time for. Use 0 for a global value.
+ * @param lastScan The last scan time or -1 if it wasn't found.
+ * @return True if the time was fetched successfully, false otherwise.
+ */
+ bool GetLastEpgScanTime(int iEpgId, CDateTime* lastScan);
+
+ /*!
+ * @brief Write the query to update the last scan time for the given EPG to db query queue.
+ * @param iEpgId The table to update the time for.
+ * @param lastScanTime The time to write to the database.
+ * @return True on success, false otherwise.
+ */
+ bool QueuePersistLastEpgScanTimeQuery(int iEpgId, const CDateTime& lastScanTime);
+
+ /*!
+ * @brief Write the query to delete the last scan time for the given EPG to db query queue.
+ * @param iEpgId The table to delete the time for.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteLastEpgScanTimeQuery(const CPVREpg& table);
+
+ /*!
+ * @brief Persist an EPG table. It's entries are not persisted.
+ * @param epg The table to persist.
+ * @param bQueueWrite If true, don't execute the query immediately but queue it.
+ * @return The database ID of this entry or 0 if bQueueWrite is false and the query was queued.
+ */
+ int Persist(const CPVREpg& epg, bool bQueueWrite);
+
+ /*!
+ * @brief Erase all EPG tags with the given epg ID and an end time less than the given time.
+ * @param iEpgId The ID of the EPG.
+ * @param maxEndTime The maximum allowed end time.
+ * @return True if the entries were removed successfully, false otherwise.
+ */
+ bool DeleteEpgTags(int iEpgId, const CDateTime& maxEndTime);
+
+ /*!
+ * @brief Erase all EPG tags with the given epg ID.
+ * @param iEpgId The ID of the EPG.
+ * @return True if the entries were removed successfully, false otherwise.
+ */
+ bool DeleteEpgTags(int iEpgId);
+
+ /*!
+ * @brief Queue the erase all EPG tags with the given epg ID.
+ * @param iEpgId The ID of the EPG.
+ * @return True if the entries were queued successfully, false otherwise.
+ */
+ bool QueueDeleteEpgTags(int iEpgId);
+
+ /*!
+ * @brief Write the query to persist the given EPG tag to db query queue.
+ * @param tag The tag to persist.
+ * @return True on success, false otherwise.
+ */
+ bool QueuePersistQuery(const CPVREpgInfoTag& tag);
+
+ /*!
+ * @return Last EPG id in the database
+ */
+ int GetLastEPGId();
+
+ //@}
+
+ /*! @name EPG searches methods */
+ //@{
+
+ /*!
+ * @brief Get all saved searches from the database.
+ * @param bRadio Whether to fetch saved searches for radio or TV.
+ * @return The searches.
+ */
+ std::vector<std::shared_ptr<CPVREpgSearchFilter>> GetSavedSearches(bool bRadio);
+
+ /*!
+ * @brief Get the saved search matching the given id.
+ * @param bRadio Whether to fetch a TV or radio saved search.
+ * @param iId The id.
+ * @return The saved search or nullptr if not found.
+ */
+ std::shared_ptr<CPVREpgSearchFilter> GetSavedSearchById(bool bRadio, int iId);
+
+ /*!
+ * @brief Persist a search.
+ * @param epgSearch The search.
+ * @return True on success, false otherwise.
+ */
+ bool Persist(CPVREpgSearchFilter& epgSearch);
+
+ /*!
+ * @brief Update time last executed for the given search.
+ * @param epgSearch The search.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateSavedSearchLastExecuted(const CPVREpgSearchFilter& epgSearch);
+
+ /*!
+ * @brief Delete a saved search.
+ * @param epgSearch The search.
+ * @return True on success, false otherwise.
+ */
+ bool Delete(const CPVREpgSearchFilter& epgSearch);
+
+ /*!
+ * @brief Delete all saved searches.
+ * @return True on success, false otherwise.
+ */
+ bool DeleteSavedSearches();
+
+ //@}
+
+ private:
+ /*!
+ * @brief Create the EPG database tables.
+ */
+ void CreateTables() override;
+
+ /*!
+ * @brief Create the EPG database analytics.
+ */
+ void CreateAnalytics() override;
+
+ /*!
+ * @brief Update an old version of the database.
+ * @param version The version to update the database from.
+ */
+ void UpdateTables(int version) override;
+
+ int GetMinSchemaVersion() const override { return 4; }
+
+ std::shared_ptr<CPVREpgInfoTag> CreateEpgTag(const std::unique_ptr<dbiplus::Dataset>& pDS);
+
+ std::shared_ptr<CPVREpgSearchFilter> CreateEpgSearchFilter(
+ bool bRadio, const std::unique_ptr<dbiplus::Dataset>& pDS);
+
+ CCriticalSection m_critSection;
+ };
+}
diff --git a/xbmc/pvr/epg/EpgInfoTag.cpp b/xbmc/pvr/epg/EpgInfoTag.cpp
new file mode 100644
index 0000000..8b0930e
--- /dev/null
+++ b/xbmc/pvr/epg/EpgInfoTag.cpp
@@ -0,0 +1,681 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgInfoTag.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+const std::string CPVREpgInfoTag::IMAGE_OWNER_PATTERN = "epgtag_{}";
+
+CPVREpgInfoTag::CPVREpgInfoTag(int iEpgID, const std::string& iconPath)
+ : m_iUniqueBroadcastID(EPG_TAG_INVALID_UID),
+ m_iconPath(iconPath, StringUtils::Format(IMAGE_OWNER_PATTERN, iEpgID)),
+ m_iFlags(EPG_TAG_FLAG_UNDEFINED),
+ m_channelData(new CPVREpgChannelData),
+ m_iEpgID(iEpgID)
+{
+}
+
+CPVREpgInfoTag::CPVREpgInfoTag(const std::shared_ptr<CPVREpgChannelData>& channelData,
+ int iEpgID,
+ const CDateTime& start,
+ const CDateTime& end,
+ bool bIsGapTag)
+ : m_iUniqueBroadcastID(EPG_TAG_INVALID_UID),
+ m_iconPath(StringUtils::Format(IMAGE_OWNER_PATTERN, iEpgID)),
+ m_iFlags(EPG_TAG_FLAG_UNDEFINED),
+ m_bIsGapTag(bIsGapTag),
+ m_iEpgID(iEpgID)
+{
+ if (channelData)
+ m_channelData = channelData;
+ else
+ m_channelData = std::make_shared<CPVREpgChannelData>();
+
+ const CDateTimeSpan correction(
+ 0, 0, 0, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection);
+ m_startTime = start + correction;
+ m_endTime = end + correction;
+}
+
+CPVREpgInfoTag::CPVREpgInfoTag(const EPG_TAG& data,
+ int iClientId,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ int iEpgID)
+ : m_iGenreType(data.iGenreType),
+ m_iGenreSubType(data.iGenreSubType),
+ m_iParentalRating(data.iParentalRating),
+ m_iStarRating(data.iStarRating),
+ m_iSeriesNumber(data.iSeriesNumber),
+ m_iEpisodeNumber(data.iEpisodeNumber),
+ m_iEpisodePart(data.iEpisodePartNumber),
+ m_iUniqueBroadcastID(data.iUniqueBroadcastId),
+ m_iYear(data.iYear),
+ m_iconPath(data.strIconPath ? data.strIconPath : "",
+ StringUtils::Format(IMAGE_OWNER_PATTERN, iEpgID)),
+ m_startTime(
+ data.startTime +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection),
+ m_endTime(data.endTime +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection),
+ m_iFlags(data.iFlags),
+ m_iEpgID(iEpgID)
+{
+ // strFirstAired is optional, so check if supported before assigning it
+ if (data.strFirstAired && strlen(data.strFirstAired) > 0)
+ m_firstAired.SetFromW3CDate(data.strFirstAired);
+
+ if (channelData)
+ {
+ m_channelData = channelData;
+
+ if (m_channelData->ClientId() != iClientId)
+ CLog::LogF(LOGERROR, "Client id mismatch (channel: {}, epg: {})!", m_channelData->ClientId(),
+ iClientId);
+ if (m_channelData->UniqueClientChannelId() != static_cast<int>(data.iUniqueChannelId))
+ CLog::LogF(LOGERROR, "Channel uid mismatch (channel: {}, epg: {})!",
+ m_channelData->UniqueClientChannelId(), data.iUniqueChannelId);
+ }
+ else
+ {
+ // provide minimalistic channel data until we get fully initialized later
+ m_channelData = std::make_shared<CPVREpgChannelData>(iClientId, data.iUniqueChannelId);
+ }
+
+ // explicit NULL check, because there is no implicit NULL constructor for std::string
+ if (data.strTitle)
+ m_strTitle = data.strTitle;
+ if (data.strGenreDescription)
+ m_strGenreDescription = data.strGenreDescription;
+ if (data.strPlotOutline)
+ m_strPlotOutline = data.strPlotOutline;
+ if (data.strPlot)
+ m_strPlot = data.strPlot;
+ if (data.strOriginalTitle)
+ m_strOriginalTitle = data.strOriginalTitle;
+ if (data.strCast)
+ m_cast = Tokenize(data.strCast);
+ if (data.strDirector)
+ m_directors = Tokenize(data.strDirector);
+ if (data.strWriter)
+ m_writers = Tokenize(data.strWriter);
+ if (data.strIMDBNumber)
+ m_strIMDBNumber = data.strIMDBNumber;
+ if (data.strEpisodeName)
+ m_strEpisodeName = data.strEpisodeName;
+ if (data.strSeriesLink)
+ m_strSeriesLink = data.strSeriesLink;
+ if (data.strParentalRatingCode)
+ m_strParentalRatingCode = data.strParentalRatingCode;
+}
+
+void CPVREpgInfoTag::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (data)
+ m_channelData = data;
+ else
+ m_channelData.reset(new CPVREpgChannelData);
+}
+
+bool CPVREpgInfoTag::operator==(const CPVREpgInfoTag& right) const
+{
+ if (this == &right)
+ return true;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return (m_iUniqueBroadcastID == right.m_iUniqueBroadcastID && m_channelData &&
+ right.m_channelData &&
+ m_channelData->UniqueClientChannelId() == right.m_channelData->UniqueClientChannelId() &&
+ m_channelData->ClientId() == right.m_channelData->ClientId());
+}
+
+bool CPVREpgInfoTag::operator!=(const CPVREpgInfoTag& right) const
+{
+ if (this == &right)
+ return false;
+
+ return !(*this == right);
+}
+
+void CPVREpgInfoTag::Serialize(CVariant& value) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ value["broadcastid"] = m_iDatabaseID; // Use DB id here as it is unique across PVR clients
+ value["channeluid"] = m_channelData->UniqueClientChannelId();
+ value["parentalrating"] = m_iParentalRating;
+ value["parentalratingcode"] = m_strParentalRatingCode;
+ value["rating"] = m_iStarRating;
+ value["title"] = m_strTitle;
+ value["plotoutline"] = m_strPlotOutline;
+ value["plot"] = m_strPlot;
+ value["originaltitle"] = m_strOriginalTitle;
+ value["thumbnail"] = ClientIconPath();
+ value["cast"] = DeTokenize(m_cast);
+ value["director"] = DeTokenize(m_directors);
+ value["writer"] = DeTokenize(m_writers);
+ value["year"] = m_iYear;
+ value["imdbnumber"] = m_strIMDBNumber;
+ value["genre"] = Genre();
+ value["filenameandpath"] = Path();
+ value["starttime"] = m_startTime.IsValid() ? m_startTime.GetAsDBDateTime() : StringUtils::Empty;
+ value["endtime"] = m_endTime.IsValid() ? m_endTime.GetAsDBDateTime() : StringUtils::Empty;
+ value["runtime"] = GetDuration() / 60;
+ value["firstaired"] = m_firstAired.IsValid() ? m_firstAired.GetAsDBDate() : StringUtils::Empty;
+ value["progress"] = Progress();
+ value["progresspercentage"] = ProgressPercentage();
+ value["episodename"] = m_strEpisodeName;
+ value["episodenum"] = m_iEpisodeNumber;
+ value["episodepart"] = m_iEpisodePart;
+ value["seasonnum"] = m_iSeriesNumber;
+ value["isactive"] = IsActive();
+ value["wasactive"] = WasActive();
+ value["isseries"] = IsSeries();
+ value["serieslink"] = m_strSeriesLink;
+ value["clientid"] = m_channelData->ClientId();
+}
+
+int CPVREpgInfoTag::ClientID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->ClientId();
+}
+
+CDateTime CPVREpgInfoTag::GetCurrentPlayingTime() const
+{
+ return CServiceBroker::GetPVRManager().PlaybackState()->GetChannelPlaybackTime(ClientID(),
+ UniqueChannelID());
+}
+
+bool CPVREpgInfoTag::IsActive() const
+{
+ CDateTime now = GetCurrentPlayingTime();
+ return (m_startTime <= now && m_endTime > now);
+}
+
+bool CPVREpgInfoTag::WasActive() const
+{
+ CDateTime now = GetCurrentPlayingTime();
+ return (m_endTime < now);
+}
+
+bool CPVREpgInfoTag::IsUpcoming() const
+{
+ CDateTime now = GetCurrentPlayingTime();
+ return (m_startTime > now);
+}
+
+float CPVREpgInfoTag::ProgressPercentage() const
+{
+ float fReturn = 0.0f;
+
+ time_t currentTime, startTime, endTime;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(currentTime);
+ m_startTime.GetAsTime(startTime);
+ m_endTime.GetAsTime(endTime);
+ int iDuration = endTime - startTime > 0 ? endTime - startTime : 3600;
+
+ if (currentTime >= startTime && currentTime <= endTime)
+ fReturn = static_cast<float>(currentTime - startTime) * 100.0f / iDuration;
+ else if (currentTime > endTime)
+ fReturn = 100.0f;
+
+ return fReturn;
+}
+
+int CPVREpgInfoTag::Progress() const
+{
+ time_t currentTime, startTime;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(currentTime);
+ m_startTime.GetAsTime(startTime);
+ int iDuration = currentTime - startTime;
+
+ if (iDuration <= 0)
+ return 0;
+
+ return iDuration;
+}
+
+void CPVREpgInfoTag::SetUniqueBroadcastID(unsigned int iUniqueBroadcastID)
+{
+ m_iUniqueBroadcastID = iUniqueBroadcastID;
+}
+
+unsigned int CPVREpgInfoTag::UniqueBroadcastID() const
+{
+ return m_iUniqueBroadcastID;
+}
+
+int CPVREpgInfoTag::DatabaseID() const
+{
+ return m_iDatabaseID;
+}
+
+int CPVREpgInfoTag::UniqueChannelID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->UniqueClientChannelId();
+}
+
+std::string CPVREpgInfoTag::ChannelIconPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->ChannelIconPath();
+}
+
+CDateTime CPVREpgInfoTag::StartAsUTC() const
+{
+ return m_startTime;
+}
+
+CDateTime CPVREpgInfoTag::StartAsLocalTime() const
+{
+ CDateTime retVal;
+ retVal.SetFromUTCDateTime(m_startTime);
+ return retVal;
+}
+
+CDateTime CPVREpgInfoTag::EndAsUTC() const
+{
+ return m_endTime;
+}
+
+CDateTime CPVREpgInfoTag::EndAsLocalTime() const
+{
+ CDateTime retVal;
+ retVal.SetFromUTCDateTime(m_endTime);
+ return retVal;
+}
+
+void CPVREpgInfoTag::SetEndFromUTC(const CDateTime& end)
+{
+ m_endTime = end;
+}
+
+int CPVREpgInfoTag::GetDuration() const
+{
+ time_t start, end;
+ m_startTime.GetAsTime(start);
+ m_endTime.GetAsTime(end);
+ return end - start > 0 ? end - start : 3600;
+}
+
+std::string CPVREpgInfoTag::Title() const
+{
+ return m_strTitle;
+}
+
+std::string CPVREpgInfoTag::PlotOutline() const
+{
+ return m_strPlotOutline;
+}
+
+std::string CPVREpgInfoTag::Plot() const
+{
+ return m_strPlot;
+}
+
+std::string CPVREpgInfoTag::OriginalTitle() const
+{
+ return m_strOriginalTitle;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Cast() const
+{
+ return m_cast;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Directors() const
+{
+ return m_directors;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Writers() const
+{
+ return m_writers;
+}
+
+const std::string CPVREpgInfoTag::GetCastLabel() const
+{
+ // Note: see CVideoInfoTag::GetCast for reference implementation.
+ std::string strLabel;
+ for (const auto& castEntry : m_cast)
+ strLabel += StringUtils::Format("{}\n", castEntry);
+
+ return StringUtils::TrimRight(strLabel, "\n");
+}
+
+const std::string CPVREpgInfoTag::GetDirectorsLabel() const
+{
+ return StringUtils::Join(
+ m_directors,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+}
+
+const std::string CPVREpgInfoTag::GetWritersLabel() const
+{
+ return StringUtils::Join(
+ m_writers,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+}
+
+const std::string CPVREpgInfoTag::GetGenresLabel() const
+{
+ return StringUtils::Join(
+ Genre(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+}
+
+int CPVREpgInfoTag::Year() const
+{
+ return m_iYear;
+}
+
+std::string CPVREpgInfoTag::IMDBNumber() const
+{
+ return m_strIMDBNumber;
+}
+
+int CPVREpgInfoTag::GenreType() const
+{
+ return m_iGenreType;
+}
+
+int CPVREpgInfoTag::GenreSubType() const
+{
+ return m_iGenreSubType;
+}
+
+std::string CPVREpgInfoTag::GenreDescription() const
+{
+ return m_strGenreDescription;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Genre() const
+{
+ if (m_genre.empty())
+ {
+ if ((m_iGenreType == EPG_GENRE_USE_STRING || m_iGenreSubType == EPG_GENRE_USE_STRING) &&
+ !m_strGenreDescription.empty())
+ {
+ // Type and sub type are both not given. No EPG color coding possible unless sub type is
+ // used to specify EPG_GENRE_USE_STRING leaving type available for genre category, use the
+ // provided genre description for the text.
+ m_genre = Tokenize(m_strGenreDescription);
+ }
+
+ if (m_genre.empty())
+ {
+ // Determine the genre from the type and subtype IDs.
+ m_genre = Tokenize(CPVREpg::ConvertGenreIdToString(m_iGenreType, m_iGenreSubType));
+ }
+ }
+ return m_genre;
+}
+
+CDateTime CPVREpgInfoTag::FirstAired() const
+{
+ return m_firstAired;
+}
+
+int CPVREpgInfoTag::ParentalRating() const
+{
+ return m_iParentalRating;
+}
+
+std::string CPVREpgInfoTag::ParentalRatingCode() const
+{
+ return m_strParentalRatingCode;
+}
+
+int CPVREpgInfoTag::StarRating() const
+{
+ return m_iStarRating;
+}
+
+int CPVREpgInfoTag::SeriesNumber() const
+{
+ return m_iSeriesNumber;
+}
+
+std::string CPVREpgInfoTag::SeriesLink() const
+{
+ return m_strSeriesLink;
+}
+
+int CPVREpgInfoTag::EpisodeNumber() const
+{
+ return m_iEpisodeNumber;
+}
+
+int CPVREpgInfoTag::EpisodePart() const
+{
+ return m_iEpisodePart;
+}
+
+std::string CPVREpgInfoTag::EpisodeName() const
+{
+ return m_strEpisodeName;
+}
+
+std::string CPVREpgInfoTag::IconPath() const
+{
+ return m_iconPath.GetLocalImage();
+}
+
+std::string CPVREpgInfoTag::ClientIconPath() const
+{
+ return m_iconPath.GetClientImage();
+}
+
+std::string CPVREpgInfoTag::Path() const
+{
+ return StringUtils::Format("pvr://guide/{:04}/{}.epg", EpgID(), m_startTime.GetAsDBDateTime());
+}
+
+bool CPVREpgInfoTag::Update(const CPVREpgInfoTag& tag, bool bUpdateBroadcastId /* = true */)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bool bChanged =
+ (m_strTitle != tag.m_strTitle || m_strPlotOutline != tag.m_strPlotOutline ||
+ m_strPlot != tag.m_strPlot || m_strOriginalTitle != tag.m_strOriginalTitle ||
+ m_cast != tag.m_cast || m_directors != tag.m_directors || m_writers != tag.m_writers ||
+ m_iYear != tag.m_iYear || m_strIMDBNumber != tag.m_strIMDBNumber ||
+ m_startTime != tag.m_startTime || m_endTime != tag.m_endTime ||
+ m_iGenreType != tag.m_iGenreType || m_iGenreSubType != tag.m_iGenreSubType ||
+ m_strGenreDescription != tag.m_strGenreDescription || m_firstAired != tag.m_firstAired ||
+ m_iParentalRating != tag.m_iParentalRating ||
+ m_strParentalRatingCode != tag.m_strParentalRatingCode ||
+ m_iStarRating != tag.m_iStarRating || m_iEpisodeNumber != tag.m_iEpisodeNumber ||
+ m_iEpisodePart != tag.m_iEpisodePart || m_iSeriesNumber != tag.m_iSeriesNumber ||
+ m_strEpisodeName != tag.m_strEpisodeName ||
+ m_iUniqueBroadcastID != tag.m_iUniqueBroadcastID || m_iEpgID != tag.m_iEpgID ||
+ m_genre != tag.m_genre || m_iconPath != tag.m_iconPath || m_iFlags != tag.m_iFlags ||
+ m_strSeriesLink != tag.m_strSeriesLink || m_channelData != tag.m_channelData);
+
+ if (bUpdateBroadcastId)
+ bChanged |= (m_iDatabaseID != tag.m_iDatabaseID);
+
+ if (bChanged)
+ {
+ if (bUpdateBroadcastId)
+ m_iDatabaseID = tag.m_iDatabaseID;
+
+ m_strTitle = tag.m_strTitle;
+ m_strPlotOutline = tag.m_strPlotOutline;
+ m_strPlot = tag.m_strPlot;
+ m_strOriginalTitle = tag.m_strOriginalTitle;
+ m_cast = tag.m_cast;
+ m_directors = tag.m_directors;
+ m_writers = tag.m_writers;
+ m_iYear = tag.m_iYear;
+ m_strIMDBNumber = tag.m_strIMDBNumber;
+ m_startTime = tag.m_startTime;
+ m_endTime = tag.m_endTime;
+ m_iGenreType = tag.m_iGenreType;
+ m_iGenreSubType = tag.m_iGenreSubType;
+ m_strGenreDescription = tag.m_strGenreDescription;
+ m_genre = tag.m_genre;
+ m_iEpgID = tag.m_iEpgID;
+ m_iFlags = tag.m_iFlags;
+ m_strSeriesLink = tag.m_strSeriesLink;
+ m_firstAired = tag.m_firstAired;
+ m_iParentalRating = tag.m_iParentalRating;
+ m_strParentalRatingCode = tag.m_strParentalRatingCode;
+ m_iStarRating = tag.m_iStarRating;
+ m_iEpisodeNumber = tag.m_iEpisodeNumber;
+ m_iEpisodePart = tag.m_iEpisodePart;
+ m_iSeriesNumber = tag.m_iSeriesNumber;
+ m_strEpisodeName = tag.m_strEpisodeName;
+ m_iUniqueBroadcastID = tag.m_iUniqueBroadcastID;
+ m_iconPath = tag.m_iconPath;
+ m_channelData = tag.m_channelData;
+ }
+
+ return bChanged;
+}
+
+bool CPVREpgInfoTag::QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "Could not open the EPG database");
+ return false;
+ }
+
+ return database->QueuePersistQuery(*this);
+}
+
+std::vector<PVR_EDL_ENTRY> CPVREpgInfoTag::GetEdl() const
+{
+ std::vector<PVR_EDL_ENTRY> edls;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
+
+ if (client && client->GetClientCapabilities().SupportsEpgTagEdl())
+ client->GetEpgTagEdl(shared_from_this(), edls);
+
+ return edls;
+}
+
+int CPVREpgInfoTag::EpgID() const
+{
+ return m_iEpgID;
+}
+
+void CPVREpgInfoTag::SetEpgID(int iEpgID)
+{
+ m_iEpgID = iEpgID;
+ m_iconPath.SetOwner(StringUtils::Format(IMAGE_OWNER_PATTERN, m_iEpgID));
+}
+
+bool CPVREpgInfoTag::IsRecordable() const
+{
+ bool bIsRecordable = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
+ if (!client || (client->IsRecordable(shared_from_this(), bIsRecordable) != PVR_ERROR_NO_ERROR))
+ {
+ // event end time based fallback
+ bIsRecordable = EndAsLocalTime() > CDateTime::GetCurrentDateTime();
+ }
+ return bIsRecordable;
+}
+
+bool CPVREpgInfoTag::IsPlayable() const
+{
+ bool bIsPlayable = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
+ if (!client || (client->IsPlayable(shared_from_this(), bIsPlayable) != PVR_ERROR_NO_ERROR))
+ {
+ // fallback
+ bIsPlayable = false;
+ }
+ return bIsPlayable;
+}
+
+bool CPVREpgInfoTag::IsSeries() const
+{
+ if ((m_iFlags & EPG_TAG_FLAG_IS_SERIES) > 0 || SeriesNumber() >= 0 || EpisodeNumber() >= 0 ||
+ EpisodePart() >= 0)
+ return true;
+ else
+ return false;
+}
+
+bool CPVREpgInfoTag::IsRadio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->IsRadio();
+}
+
+bool CPVREpgInfoTag::IsParentalLocked() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->IsLocked();
+}
+
+bool CPVREpgInfoTag::IsGapTag() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsGapTag;
+}
+
+bool CPVREpgInfoTag::IsNew() const
+{
+ return (m_iFlags & EPG_TAG_FLAG_IS_NEW) > 0;
+}
+
+bool CPVREpgInfoTag::IsPremiere() const
+{
+ return (m_iFlags & EPG_TAG_FLAG_IS_PREMIERE) > 0;
+}
+
+bool CPVREpgInfoTag::IsFinale() const
+{
+ return (m_iFlags & EPG_TAG_FLAG_IS_FINALE) > 0;
+}
+
+bool CPVREpgInfoTag::IsLive() const
+{
+ return (m_iFlags & EPG_TAG_FLAG_IS_LIVE) > 0;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Tokenize(const std::string& str)
+{
+ return StringUtils::Split(str, EPG_STRING_TOKEN_SEPARATOR);
+}
+
+const std::string CPVREpgInfoTag::DeTokenize(const std::vector<std::string>& tokens)
+{
+ return StringUtils::Join(tokens, EPG_STRING_TOKEN_SEPARATOR);
+}
diff --git a/xbmc/pvr/epg/EpgInfoTag.h b/xbmc/pvr/epg/EpgInfoTag.h
new file mode 100644
index 0000000..89f2e0c
--- /dev/null
+++ b/xbmc/pvr/epg/EpgInfoTag.h
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "pvr/PVRCachedImage.h"
+#include "threads/CriticalSection.h"
+#include "utils/ISerializable.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+struct EPG_TAG;
+struct PVR_EDL_ENTRY;
+
+namespace PVR
+{
+class CPVREpgChannelData;
+class CPVREpgDatabase;
+
+class CPVREpgInfoTag final : public ISerializable,
+ public std::enable_shared_from_this<CPVREpgInfoTag>
+{
+ friend class CPVREpgDatabase;
+
+public:
+ static const std::string IMAGE_OWNER_PATTERN;
+
+ /*!
+ * @brief Create a new EPG infotag.
+ * @param data The tag's data.
+ * @param iClientId The client id.
+ * @param channelData The channel data.
+ * @param iEpgId The id of the EPG this tag belongs to.
+ */
+ CPVREpgInfoTag(const EPG_TAG& data,
+ int iClientId,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ int iEpgID);
+
+ /*!
+ * @brief Create a new EPG infotag.
+ * @param channelData The channel data.
+ * @param iEpgId The id of the EPG this tag belongs to.
+ * @param start The start time of the event
+ * @param end The end time of the event
+ * @param bIsGapTagTrue if this is a "gap" tag, false if this is a real EPG event
+ */
+ CPVREpgInfoTag(const std::shared_ptr<CPVREpgChannelData>& channelData,
+ int iEpgID,
+ const CDateTime& start,
+ const CDateTime& end,
+ bool bIsGapTag);
+
+ /*!
+ * @brief Set data for the channel linked to this EPG infotag.
+ * @param data The channel data.
+ */
+ void SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data);
+
+ bool operator==(const CPVREpgInfoTag& right) const;
+ bool operator!=(const CPVREpgInfoTag& right) const;
+
+ // ISerializable implementation
+ void Serialize(CVariant& value) const override;
+
+ /*!
+ * @brief Get the identifier of the client that serves this event.
+ * @return The identifier.
+ */
+ int ClientID() const;
+
+ /*!
+ * @brief Check if this event is currently active.
+ * @return True if it's active, false otherwise.
+ */
+ bool IsActive() const;
+
+ /*!
+ * @brief Check if this event is in the past.
+ * @return True when this event has already passed, false otherwise.
+ */
+ bool WasActive() const;
+
+ /*!
+ * @brief Check if this event is in the future.
+ * @return True when this event is an upcoming event, false otherwise.
+ */
+ bool IsUpcoming() const;
+
+ /*!
+ * @brief Get the progress of this tag in percent.
+ * @return The current progress of this tag.
+ */
+ float ProgressPercentage() const;
+
+ /*!
+ * @brief Get the progress of this tag in seconds.
+ * @return The current progress of this tag in seconds.
+ */
+ int Progress() const;
+
+ /*!
+ * @brief Get EPG ID of this tag.
+ * @return The epg ID.
+ */
+ int EpgID() const;
+
+ /*!
+ * @brief Sets the EPG id for this event.
+ * @param iEpgID The EPG id.
+ */
+ void SetEpgID(int iEpgID);
+
+ /*!
+ * @brief Change the unique broadcast ID of this event.
+ * @param iUniqueBroadcastId The new unique broadcast ID.
+ */
+ void SetUniqueBroadcastID(unsigned int iUniqueBroadcastID);
+
+ /*!
+ * @brief Get the unique broadcast ID.
+ * @return The unique broadcast ID.
+ */
+ unsigned int UniqueBroadcastID() const;
+
+ /*!
+ * @brief Get the event's database ID.
+ * @return The database ID.
+ */
+ int DatabaseID() const;
+
+ /*!
+ * @brief Get the unique ID of the channel associated with this event.
+ * @return The unique channel ID.
+ */
+ int UniqueChannelID() const;
+
+ /*!
+ * @brief Get the path for the icon of the channel associated with this event.
+ * @return The channel icon path.
+ */
+ std::string ChannelIconPath() const;
+
+ /*!
+ * @brief Get the event's start time.
+ * @return The start time in UTC.
+ */
+ CDateTime StartAsUTC() const;
+
+ /*!
+ * @brief Get the event's start time.
+ * @return The start time as local time.
+ */
+ CDateTime StartAsLocalTime() const;
+
+ /*!
+ * @brief Get the event's end time.
+ * @return The end time in UTC.
+ */
+ CDateTime EndAsUTC() const;
+
+ /*!
+ * @brief Get the event's end time.
+ * @return The end time as local time.
+ */
+ CDateTime EndAsLocalTime() const;
+
+ /*!
+ * @brief Change the event's end time.
+ * @param end The new end time.
+ */
+ void SetEndFromUTC(const CDateTime& end);
+
+ /*!
+ * @brief Get the duration of this event in seconds.
+ * @return The duration.
+ */
+ int GetDuration() const;
+
+ /*!
+ * @brief Get the title of this event.
+ * @return The title.
+ */
+ std::string Title() const;
+
+ /*!
+ * @brief Get the plot outline of this event.
+ * @return The plot outline.
+ */
+ std::string PlotOutline() const;
+
+ /*!
+ * @brief Get the plot of this event.
+ * @return The plot.
+ */
+ std::string Plot() const;
+
+ /*!
+ * @brief Get the original title of this event.
+ * @return The original title.
+ */
+ std::string OriginalTitle() const;
+
+ /*!
+ * @brief Get the cast of this event.
+ * @return The cast.
+ */
+ const std::vector<std::string> Cast() const;
+
+ /*!
+ * @brief Get the director(s) of this event.
+ * @return The director(s).
+ */
+ const std::vector<std::string> Directors() const;
+
+ /*!
+ * @brief Get the writer(s) of this event.
+ * @return The writer(s).
+ */
+ const std::vector<std::string> Writers() const;
+
+ /*!
+ * @brief Get the cast of this event as formatted string.
+ * @return The cast label.
+ */
+ const std::string GetCastLabel() const;
+
+ /*!
+ * @brief Get the director(s) of this event as formatted string.
+ * @return The directors label.
+ */
+ const std::string GetDirectorsLabel() const;
+
+ /*!
+ * @brief Get the writer(s) of this event as formatted string.
+ * @return The writers label.
+ */
+ const std::string GetWritersLabel() const;
+
+ /*!
+ * @brief Get the genre(s) of this event as formatted string.
+ * @return The genres label.
+ */
+ const std::string GetGenresLabel() const;
+
+ /*!
+ * @brief Get the year of this event.
+ * @return The year.
+ */
+ int Year() const;
+
+ /*!
+ * @brief Get the imdbnumber of this event.
+ * @return The imdbnumber.
+ */
+ std::string IMDBNumber() const;
+
+ /*!
+ * @brief Get the genre type ID of this event.
+ * @return The genre type ID.
+ */
+ int GenreType() const;
+
+ /*!
+ * @brief Get the genre subtype ID of this event.
+ * @return The genre subtype ID.
+ */
+ int GenreSubType() const;
+
+ /*!
+ * @brief Get the genre description of this event.
+ * @return The genre.
+ */
+ std::string GenreDescription() const;
+
+ /*!
+ * @brief Get the genre as human readable string.
+ * @return The genre.
+ */
+ const std::vector<std::string> Genre() const;
+
+ /*!
+ * @brief Get the first air date of this event.
+ * @return The first air date.
+ */
+ CDateTime FirstAired() const;
+
+ /*!
+ * @brief Get the parental rating of this event.
+ * @return The parental rating.
+ */
+ int ParentalRating() const;
+
+ /*!
+ * @brief Get the parental rating code of this event.
+ * @return The parental rating code.
+ */
+ std::string ParentalRatingCode() const;
+
+ /*!
+ * @brief Get the star rating of this event.
+ * @return The star rating.
+ */
+ int StarRating() const;
+
+ /*!
+ * @brief The series number of this event.
+ * @return The series number.
+ */
+ int SeriesNumber() const;
+
+ /*!
+ * @brief The series link for this event.
+ * @return The series link or empty string, if not available.
+ */
+ std::string SeriesLink() const;
+
+ /*!
+ * @brief The episode number of this event.
+ * @return The episode number.
+ */
+ int EpisodeNumber() const;
+
+ /*!
+ * @brief The episode part number of this event.
+ * @return The episode part number.
+ */
+ int EpisodePart() const;
+
+ /*!
+ * @brief The episode name of this event.
+ * @return The episode name.
+ */
+ std::string EpisodeName() const;
+
+ /*!
+ * @brief Get the path to the icon for this event used by Kodi.
+ * @return The path to the icon
+ */
+ std::string IconPath() const;
+
+ /*!
+ * @brief Get the path to the icon for this event as given by the client.
+ * @return The path to the icon
+ */
+ std::string ClientIconPath() const;
+
+ /*!
+ * @brief The path to this event.
+ * @return The path.
+ */
+ std::string Path() const;
+
+ /*!
+ * @brief Check if this event can be recorded.
+ * @return True if it can be recorded, false otherwise.
+ */
+ bool IsRecordable() const;
+
+ /*!
+ * @brief Check if this event can be played.
+ * @return True if it can be played, false otherwise.
+ */
+ bool IsPlayable() const;
+
+ /*!
+ * @brief Write query to persist this tag in the query queue of the given database.
+ * @param database The database.
+ * @return True on success, false otherwise.
+ */
+ bool QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Update the information in this tag with the info in the given tag.
+ * @param tag The new info.
+ * @param bUpdateBroadcastId If set to false, the tag BroadcastId (locally unique) will not be
+ * checked/updated
+ * @return True if something changed, false otherwise.
+ */
+ bool Update(const CPVREpgInfoTag& tag, bool bUpdateBroadcastId = true);
+
+ /*!
+ * @brief Retrieve the edit decision list (EDL) of an EPG tag.
+ * @return The edit decision list (empty on error)
+ */
+ std::vector<PVR_EDL_ENTRY> GetEdl() const;
+
+ /*!
+ * @brief Check whether this tag has any series attributes.
+ * @return True if this tag has any series attributes, false otherwise
+ */
+ bool IsSeries() const;
+
+ /*!
+ * @brief Check whether this tag is associated with a radion or TV channel.
+ * @return True if this tag is associated with a radio channel, false otherwise.
+ */
+ bool IsRadio() const;
+
+ /*!
+ * @brief Check whether this event is parental locked.
+ * @return True if whether this event is parental locked, false otherwise.
+ */
+ bool IsParentalLocked() const;
+
+ /*!
+ * @brief Check whether this event is a real event or a gap in the EPG timeline.
+ * @return True if this event is a gap, false otherwise.
+ */
+ bool IsGapTag() const;
+
+ /*!
+ * @brief Check whether this tag will be flagged as new.
+ * @return True if this tag will be flagged as new, false otherwise
+ */
+ bool IsNew() const;
+
+ /*!
+ * @brief Check whether this tag will be flagged as a premiere.
+ * @return True if this tag will be flagged as a premiere, false otherwise
+ */
+ bool IsPremiere() const;
+
+ /*!
+ * @brief Check whether this tag will be flagged as a finale.
+ * @return True if this tag will be flagged as a finale, false otherwise
+ */
+ bool IsFinale() const;
+
+ /*!
+ * @brief Check whether this tag will be flagged as live.
+ * @return True if this tag will be flagged as live, false otherwise
+ */
+ bool IsLive() const;
+
+ /*!
+ * @brief Return the flags (EPG_TAG_FLAG_*) of this event as a bitfield.
+ * @return the flags.
+ */
+ unsigned int Flags() const { return m_iFlags; }
+
+ /*!
+ * @brief Split the given string into tokens. Interprets occurrences of EPG_STRING_TOKEN_SEPARATOR
+ * in the string as separator.
+ * @param str The string to tokenize.
+ * @return the tokens.
+ */
+ static const std::vector<std::string> Tokenize(const std::string& str);
+
+ /*!
+ * @brief Combine the given strings to a single string. Inserts EPG_STRING_TOKEN_SEPARATOR as
+ * separator.
+ * @param tokens The tokens.
+ * @return the combined string.
+ */
+ static const std::string DeTokenize(const std::vector<std::string>& tokens);
+
+private:
+ CPVREpgInfoTag(int iEpgID, const std::string& iconPath);
+
+ CPVREpgInfoTag() = delete;
+ CPVREpgInfoTag(const CPVREpgInfoTag& tag) = delete;
+ CPVREpgInfoTag& operator=(const CPVREpgInfoTag& other) = delete;
+
+ /*!
+ * @brief Get current time, taking timeshifting into account.
+ * @return The playing time.
+ */
+ CDateTime GetCurrentPlayingTime() const;
+
+ int m_iDatabaseID = -1; /*!< database ID */
+ int m_iGenreType = 0; /*!< genre type */
+ int m_iGenreSubType = 0; /*!< genre subtype */
+ std::string m_strGenreDescription; /*!< genre description */
+ int m_iParentalRating = 0; /*!< parental rating */
+ std::string m_strParentalRatingCode; /*!< parental rating code */
+ int m_iStarRating = 0; /*!< star rating */
+ int m_iSeriesNumber = -1; /*!< series number */
+ int m_iEpisodeNumber = -1; /*!< episode number */
+ int m_iEpisodePart = -1; /*!< episode part number */
+ unsigned int m_iUniqueBroadcastID = 0; /*!< unique broadcast ID */
+ std::string m_strTitle; /*!< title */
+ std::string m_strPlotOutline; /*!< plot outline */
+ std::string m_strPlot; /*!< plot */
+ std::string m_strOriginalTitle; /*!< original title */
+ std::vector<std::string> m_cast; /*!< cast */
+ std::vector<std::string> m_directors; /*!< director(s) */
+ std::vector<std::string> m_writers; /*!< writer(s) */
+ int m_iYear = 0; /*!< year */
+ std::string m_strIMDBNumber; /*!< imdb number */
+ mutable std::vector<std::string> m_genre; /*!< genre */
+ std::string m_strEpisodeName; /*!< episode name */
+ CPVRCachedImage m_iconPath; /*!< the path to the icon */
+ CDateTime m_startTime; /*!< event start time */
+ CDateTime m_endTime; /*!< event end time */
+ CDateTime m_firstAired; /*!< first airdate */
+ unsigned int m_iFlags = 0; /*!< the flags applicable to this EPG entry */
+ std::string m_strSeriesLink; /*!< series link */
+ bool m_bIsGapTag = false;
+
+ mutable CCriticalSection m_critSection;
+ std::shared_ptr<CPVREpgChannelData> m_channelData;
+ int m_iEpgID = -1;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgSearchData.h b/xbmc/pvr/epg/EpgSearchData.h
new file mode 100644
index 0000000..e5e6ef7
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchData.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+
+#include <string>
+
+namespace PVR
+{
+
+static constexpr int EPG_SEARCH_UNSET = -1;
+
+struct PVREpgSearchData
+{
+ std::string m_strSearchTerm; /*!< The term to search for */
+ bool m_bSearchInDescription = false; /*!< Search for strSearchTerm in the description too */
+ bool m_bIncludeUnknownGenres = false; /*!< Whether to include unknown genres */
+ int m_iGenreType = EPG_SEARCH_UNSET; /*!< The genre type for an entry */
+ bool m_bIgnoreFinishedBroadcasts; /*!< True to ignore finished broadcasts, false if not */
+ bool m_bIgnoreFutureBroadcasts; /*!< True to ignore future broadcasts, false if not */
+ CDateTime m_startDateTime; /*!< The minimum start time for an entry */
+ CDateTime m_endDateTime; /*!< The maximum end time for an entry */
+
+ void Reset()
+ {
+ m_strSearchTerm.clear();
+ m_bSearchInDescription = false;
+ m_bIncludeUnknownGenres = false;
+ m_iGenreType = EPG_SEARCH_UNSET;
+ m_bIgnoreFinishedBroadcasts = true;
+ m_bIgnoreFutureBroadcasts = false;
+ m_startDateTime.SetValid(false);
+ m_endDateTime.SetValid(false);
+ }
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgSearchFilter.cpp b/xbmc/pvr/epg/EpgSearchFilter.cpp
new file mode 100644
index 0000000..e1cc35f
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchFilter.cpp
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgSearchFilter.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgSearchPath.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimers.h"
+#include "utils/TextSearch.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <memory>
+
+using namespace PVR;
+
+CPVREpgSearchFilter::CPVREpgSearchFilter(bool bRadio)
+: m_bIsRadio(bRadio)
+{
+ Reset();
+}
+
+void CPVREpgSearchFilter::Reset()
+{
+ m_searchData.Reset();
+ m_bEpgSearchDataFiltered = false;
+
+ m_bIsCaseSensitive = false;
+ m_iMinimumDuration = EPG_SEARCH_UNSET;
+ m_iMaximumDuration = EPG_SEARCH_UNSET;
+ m_bRemoveDuplicates = false;
+
+ /* pvr specific filters */
+ m_iClientID = -1;
+ m_iChannelGroupID = -1;
+ m_iChannelUID = -1;
+ m_bFreeToAirOnly = false;
+ m_bIgnorePresentTimers = true;
+ m_bIgnorePresentRecordings = true;
+
+ m_groupIdMatches.reset();
+
+ m_iDatabaseId = -1;
+ m_title.clear();
+ m_lastExecutedDateTime.SetValid(false);
+}
+
+std::string CPVREpgSearchFilter::GetPath() const
+{
+ return CPVREpgSearchPath(*this).GetPath();
+}
+
+void CPVREpgSearchFilter::SetSearchTerm(const std::string& strSearchTerm)
+{
+ if (m_searchData.m_strSearchTerm != strSearchTerm)
+ {
+ m_searchData.m_strSearchTerm = strSearchTerm;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetSearchPhrase(const std::string& strSearchPhrase)
+{
+ // match the exact phrase
+ SetSearchTerm("\"" + strSearchPhrase + "\"");
+}
+
+void CPVREpgSearchFilter::SetCaseSensitive(bool bIsCaseSensitive)
+{
+ if (m_bIsCaseSensitive != bIsCaseSensitive)
+ {
+ m_bIsCaseSensitive = bIsCaseSensitive;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetSearchInDescription(bool bSearchInDescription)
+{
+ if (m_searchData.m_bSearchInDescription != bSearchInDescription)
+ {
+ m_searchData.m_bSearchInDescription = bSearchInDescription;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetGenreType(int iGenreType)
+{
+ if (m_searchData.m_iGenreType != iGenreType)
+ {
+ m_searchData.m_iGenreType = iGenreType;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetMinimumDuration(int iMinimumDuration)
+{
+ if (m_iMinimumDuration != iMinimumDuration)
+ {
+ m_iMinimumDuration = iMinimumDuration;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetMaximumDuration(int iMaximumDuration)
+{
+ if (m_iMaximumDuration != iMaximumDuration)
+ {
+ m_iMaximumDuration = iMaximumDuration;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIgnoreFinishedBroadcasts(bool bIgnoreFinishedBroadcasts)
+{
+ if (m_searchData.m_bIgnoreFinishedBroadcasts != bIgnoreFinishedBroadcasts)
+ {
+ m_searchData.m_bIgnoreFinishedBroadcasts = bIgnoreFinishedBroadcasts;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIgnoreFutureBroadcasts(bool bIgnoreFutureBroadcasts)
+{
+ if (m_searchData.m_bIgnoreFutureBroadcasts != bIgnoreFutureBroadcasts)
+ {
+ m_searchData.m_bIgnoreFutureBroadcasts = bIgnoreFutureBroadcasts;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetStartDateTime(const CDateTime& startDateTime)
+{
+ if (m_searchData.m_startDateTime != startDateTime)
+ {
+ m_searchData.m_startDateTime = startDateTime;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetEndDateTime(const CDateTime& endDateTime)
+{
+ if (m_searchData.m_endDateTime != endDateTime)
+ {
+ m_searchData.m_endDateTime = endDateTime;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIncludeUnknownGenres(bool bIncludeUnknownGenres)
+{
+ if (m_searchData.m_bIncludeUnknownGenres != bIncludeUnknownGenres)
+ {
+ m_searchData.m_bIncludeUnknownGenres = bIncludeUnknownGenres;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetRemoveDuplicates(bool bRemoveDuplicates)
+{
+ if (m_bRemoveDuplicates != bRemoveDuplicates)
+ {
+ m_bRemoveDuplicates = bRemoveDuplicates;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetClientID(int iClientID)
+{
+ if (m_iClientID != iClientID)
+ {
+ m_iClientID = iClientID;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetChannelGroupID(int iChannelGroupID)
+{
+ if (m_iChannelGroupID != iChannelGroupID)
+ {
+ m_iChannelGroupID = iChannelGroupID;
+ m_groupIdMatches.reset();
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetChannelUID(int iChannelUID)
+{
+ if (m_iChannelUID != iChannelUID)
+ {
+ m_iChannelUID = iChannelUID;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetFreeToAirOnly(bool bFreeToAirOnly)
+{
+ if (m_bFreeToAirOnly != bFreeToAirOnly)
+ {
+ m_bFreeToAirOnly = bFreeToAirOnly;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIgnorePresentTimers(bool bIgnorePresentTimers)
+{
+ if (m_bIgnorePresentTimers != bIgnorePresentTimers)
+ {
+ m_bIgnorePresentTimers = bIgnorePresentTimers;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIgnorePresentRecordings(bool bIgnorePresentRecordings)
+{
+ if (m_bIgnorePresentRecordings != bIgnorePresentRecordings)
+ {
+ m_bIgnorePresentRecordings = bIgnorePresentRecordings;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetDatabaseId(int iDatabaseId)
+{
+ if (m_iDatabaseId != iDatabaseId)
+ {
+ m_iDatabaseId = iDatabaseId;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetTitle(const std::string& title)
+{
+ if (m_title != title)
+ {
+ m_title = title;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetLastExecutedDateTime(const CDateTime& lastExecutedDateTime)
+{
+ // Note: No need to set m_bChanged here
+ m_lastExecutedDateTime = lastExecutedDateTime;
+}
+
+bool CPVREpgSearchFilter::MatchGenre(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (m_bEpgSearchDataFiltered)
+ return true;
+
+ if (m_searchData.m_iGenreType != EPG_SEARCH_UNSET)
+ {
+ if (m_searchData.m_bIncludeUnknownGenres)
+ {
+ // match the exact genre and everything with unknown genre
+ return (tag->GenreType() == m_searchData.m_iGenreType ||
+ tag->GenreType() < EPG_EVENT_CONTENTMASK_MOVIEDRAMA ||
+ tag->GenreType() > EPG_EVENT_CONTENTMASK_USERDEFINED);
+ }
+ else
+ {
+ // match only the exact genre
+ return (tag->GenreType() == m_searchData.m_iGenreType);
+ }
+ }
+
+ // match any genre
+ return true;
+}
+
+bool CPVREpgSearchFilter::MatchDuration(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ bool bReturn(true);
+
+ if (m_iMinimumDuration != EPG_SEARCH_UNSET)
+ bReturn = (tag->GetDuration() > m_iMinimumDuration * 60);
+
+ if (bReturn && m_iMaximumDuration != EPG_SEARCH_UNSET)
+ bReturn = (tag->GetDuration() < m_iMaximumDuration * 60);
+
+ return bReturn;
+}
+
+bool CPVREpgSearchFilter::MatchStartAndEndTimes(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (m_bEpgSearchDataFiltered)
+ return true;
+
+ return ((!m_searchData.m_bIgnoreFinishedBroadcasts ||
+ tag->EndAsUTC() > CDateTime::GetUTCDateTime()) &&
+ (!m_searchData.m_bIgnoreFutureBroadcasts ||
+ tag->StartAsUTC() < CDateTime::GetUTCDateTime()) &&
+ (!m_searchData.m_startDateTime.IsValid() || // invalid => match any datetime
+ tag->StartAsUTC() >= m_searchData.m_startDateTime) &&
+ (!m_searchData.m_endDateTime.IsValid() || // invalid => match any datetime
+ tag->EndAsUTC() <= m_searchData.m_endDateTime));
+}
+
+bool CPVREpgSearchFilter::MatchSearchTerm(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ bool bReturn(true);
+
+ if (!m_searchData.m_strSearchTerm.empty())
+ {
+ bReturn = !CServiceBroker::GetPVRManager().IsParentalLocked(tag);
+ if (bReturn && (m_bIsCaseSensitive || !m_bEpgSearchDataFiltered))
+ {
+ CTextSearch search(m_searchData.m_strSearchTerm, m_bIsCaseSensitive, SEARCH_DEFAULT_OR);
+
+ bReturn = search.Search(tag->Title()) || search.Search(tag->PlotOutline()) ||
+ (m_searchData.m_bSearchInDescription && search.Search(tag->Plot()));
+ }
+ }
+
+ return bReturn;
+}
+
+bool CPVREpgSearchFilter::FilterEntry(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ return MatchGenre(tag) && MatchDuration(tag) && MatchStartAndEndTimes(tag) &&
+ MatchSearchTerm(tag) && MatchChannel(tag) && MatchChannelGroup(tag) && MatchTimers(tag) &&
+ MatchRecordings(tag) && MatchFreeToAir(tag);
+}
+
+void CPVREpgSearchFilter::RemoveDuplicates(std::vector<std::shared_ptr<CPVREpgInfoTag>>& results)
+{
+ for (auto it = results.begin(); it != results.end();)
+ {
+ it = results.erase(std::remove_if(results.begin(),
+ results.end(),
+ [&it](const std::shared_ptr<CPVREpgInfoTag>& entry)
+ {
+ return *it != entry &&
+ (*it)->Title() == entry->Title() &&
+ (*it)->Plot() == entry->Plot() &&
+ (*it)->PlotOutline() == entry->PlotOutline();
+ }),
+ results.end());
+ }
+}
+
+bool CPVREpgSearchFilter::MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ return tag && (tag->IsRadio() == m_bIsRadio) &&
+ (m_iClientID == -1 || tag->ClientID() == m_iClientID) &&
+ (m_iChannelUID == -1 || tag->UniqueChannelID() == m_iChannelUID) &&
+ CServiceBroker::GetPVRManager().Clients()->IsCreatedClient(tag->ClientID());
+}
+
+bool CPVREpgSearchFilter::MatchChannelGroup(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (m_iChannelGroupID != -1)
+ {
+ if (!m_groupIdMatches.has_value())
+ {
+ const std::shared_ptr<CPVRChannelGroup> group = CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(m_bIsRadio)
+ ->GetById(m_iChannelGroupID);
+ m_groupIdMatches =
+ group && (group->GetByUniqueID({tag->ClientID(), tag->UniqueChannelID()}) != nullptr);
+ }
+
+ return *m_groupIdMatches;
+ }
+
+ return true;
+}
+
+bool CPVREpgSearchFilter::MatchFreeToAir(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (m_bFreeToAirOnly)
+ {
+ const std::shared_ptr<CPVRChannel> channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(tag);
+ return channel && !channel->IsEncrypted();
+ }
+
+ return true;
+}
+
+bool CPVREpgSearchFilter::MatchTimers(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ return (!m_bIgnorePresentTimers || !CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(tag));
+}
+
+bool CPVREpgSearchFilter::MatchRecordings(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ return (!m_bIgnorePresentRecordings || !CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(tag));
+}
diff --git a/xbmc/pvr/epg/EpgSearchFilter.h b/xbmc/pvr/epg/EpgSearchFilter.h
new file mode 100644
index 0000000..aa6a2e5
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchFilter.h
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "pvr/epg/EpgSearchData.h"
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+ class CPVREpgInfoTag;
+
+ class CPVREpgSearchFilter
+ {
+ public:
+ CPVREpgSearchFilter() = delete;
+
+ /*!
+ * @brief ctor.
+ * @param bRadio the type of channels to search - if true, 'radio'. 'tv', otherwise.
+ */
+ explicit CPVREpgSearchFilter(bool bRadio);
+
+ /*!
+ * @brief Clear this filter.
+ */
+ void Reset();
+
+ /*!
+ * @brief Return the path for this filter.
+ * @return the path.
+ */
+ std::string GetPath() const;
+
+ /*!
+ * @brief Check if a tag will be filtered or not.
+ * @param tag The tag to check.
+ * @return True if this tag matches the filter, false if not.
+ */
+ bool FilterEntry(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+
+ /*!
+ * @brief remove duplicates from a list of epg tags.
+ * @param results The list of epg tags.
+ */
+ static void RemoveDuplicates(std::vector<std::shared_ptr<CPVREpgInfoTag>>& results);
+
+ /*!
+ * @brief Get the type of channels to search.
+ * @return true, if 'radio'. false, otherwise.
+ */
+ bool IsRadio() const { return m_bIsRadio; }
+
+ const std::string& GetSearchTerm() const { return m_searchData.m_strSearchTerm; }
+ void SetSearchTerm(const std::string& strSearchTerm);
+
+ void SetSearchPhrase(const std::string& strSearchPhrase);
+
+ bool IsCaseSensitive() const { return m_bIsCaseSensitive; }
+ void SetCaseSensitive(bool bIsCaseSensitive);
+
+ bool ShouldSearchInDescription() const { return m_searchData.m_bSearchInDescription; }
+ void SetSearchInDescription(bool bSearchInDescription);
+
+ int GetGenreType() const { return m_searchData.m_iGenreType; }
+ void SetGenreType(int iGenreType);
+
+ int GetMinimumDuration() const { return m_iMinimumDuration; }
+ void SetMinimumDuration(int iMinimumDuration);
+
+ int GetMaximumDuration() const { return m_iMaximumDuration; }
+ void SetMaximumDuration(int iMaximumDuration);
+
+ bool ShouldIgnoreFinishedBroadcasts() const { return m_searchData.m_bIgnoreFinishedBroadcasts; }
+ void SetIgnoreFinishedBroadcasts(bool bIgnoreFinishedBroadcasts);
+
+ bool ShouldIgnoreFutureBroadcasts() const { return m_searchData.m_bIgnoreFutureBroadcasts; }
+ void SetIgnoreFutureBroadcasts(bool bIgnoreFutureBroadcasts);
+
+ const CDateTime& GetStartDateTime() const { return m_searchData.m_startDateTime; }
+ void SetStartDateTime(const CDateTime& startDateTime);
+
+ const CDateTime& GetEndDateTime() const { return m_searchData.m_endDateTime; }
+ void SetEndDateTime(const CDateTime& endDateTime);
+
+ bool ShouldIncludeUnknownGenres() const { return m_searchData.m_bIncludeUnknownGenres; }
+ void SetIncludeUnknownGenres(bool bIncludeUnknownGenres);
+
+ bool ShouldRemoveDuplicates() const { return m_bRemoveDuplicates; }
+ void SetRemoveDuplicates(bool bRemoveDuplicates);
+
+ int GetClientID() const { return m_iClientID; }
+ void SetClientID(int iClientID);
+
+ int GetChannelGroupID() const { return m_iChannelGroupID; }
+ void SetChannelGroupID(int iChannelGroupID);
+
+ int GetChannelUID() const { return m_iChannelUID; }
+ void SetChannelUID(int iChannelUID);
+
+ bool IsFreeToAirOnly() const { return m_bFreeToAirOnly; }
+ void SetFreeToAirOnly(bool bFreeToAirOnly);
+
+ bool ShouldIgnorePresentTimers() const { return m_bIgnorePresentTimers; }
+ void SetIgnorePresentTimers(bool bIgnorePresentTimers);
+
+ bool ShouldIgnorePresentRecordings() const { return m_bIgnorePresentRecordings; }
+ void SetIgnorePresentRecordings(bool bIgnorePresentRecordings);
+
+ int GetDatabaseId() const { return m_iDatabaseId; }
+ void SetDatabaseId(int iDatabaseId);
+
+ const std::string& GetTitle() const { return m_title; }
+ void SetTitle(const std::string& title);
+
+ const CDateTime& GetLastExecutedDateTime() const { return m_lastExecutedDateTime; }
+ void SetLastExecutedDateTime(const CDateTime& lastExecutedDateTime);
+
+ const PVREpgSearchData& GetEpgSearchData() const { return m_searchData; }
+ void SetEpgSearchDataFiltered() { m_bEpgSearchDataFiltered = true; }
+
+ bool IsChanged() const { return m_bChanged; }
+ void SetChanged(bool bChanged) { m_bChanged = bChanged; }
+
+ private:
+ bool MatchGenre(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchDuration(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchStartAndEndTimes(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchSearchTerm(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchChannelGroup(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchFreeToAir(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchTimers(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchRecordings(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+
+ bool m_bChanged = false;
+
+ PVREpgSearchData m_searchData;
+ bool m_bEpgSearchDataFiltered = false;
+
+ bool m_bIsCaseSensitive; /*!< Do a case sensitive search */
+ int m_iMinimumDuration; /*!< The minimum duration for an entry */
+ int m_iMaximumDuration; /*!< The maximum duration for an entry */
+ bool m_bRemoveDuplicates; /*!< True to remove duplicate events, false if not */
+
+ // PVR specific filters
+ bool m_bIsRadio; /*!< True to filter radio channels only, false to tv only */
+ int m_iClientID = -1; /*!< The client id */
+ int m_iChannelGroupID{-1}; /*! The channel group id */
+ int m_iChannelUID = -1; /*!< The channel uid */
+ bool m_bFreeToAirOnly; /*!< Include free to air channels only */
+ bool m_bIgnorePresentTimers; /*!< True to ignore currently present timers (future recordings), false if not */
+ bool m_bIgnorePresentRecordings; /*!< True to ignore currently active recordings, false if not */
+
+ mutable std::optional<bool> m_groupIdMatches;
+
+ int m_iDatabaseId = -1;
+ std::string m_title;
+ CDateTime m_lastExecutedDateTime;
+ };
+}
diff --git a/xbmc/pvr/epg/EpgSearchPath.cpp b/xbmc/pvr/epg/EpgSearchPath.cpp
new file mode 100644
index 0000000..9b24e10
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchPath.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012-2021 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgSearchPath.h"
+
+#include "pvr/epg/EpgSearchFilter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <cstdlib>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+const std::string CPVREpgSearchPath::PATH_SEARCH_DIALOG = "pvr://search/search_dialog";
+const std::string CPVREpgSearchPath::PATH_TV_SEARCH = "pvr://search/tv/";
+const std::string CPVREpgSearchPath::PATH_TV_SAVEDSEARCHES = "pvr://search/tv/savedsearches/";
+const std::string CPVREpgSearchPath::PATH_RADIO_SEARCH = "pvr://search/radio/";
+const std::string CPVREpgSearchPath::PATH_RADIO_SAVEDSEARCHES = "pvr://search/radio/savedsearches/";
+
+CPVREpgSearchPath::CPVREpgSearchPath(const std::string& strPath)
+{
+ Init(strPath);
+}
+
+CPVREpgSearchPath::CPVREpgSearchPath(const CPVREpgSearchFilter& search)
+ : m_path(StringUtils::Format("pvr://search/{}/savedsearches/{}",
+ search.IsRadio() ? "radio" : "tv",
+ search.GetDatabaseId())),
+ m_bValid(true),
+ m_bRoot(false),
+ m_bRadio(search.IsRadio()),
+ m_bSavedSearchesRoot(false)
+{
+}
+
+bool CPVREpgSearchPath::Init(const std::string& strPath)
+{
+ std::string strVarPath(strPath);
+ URIUtils::RemoveSlashAtEnd(strVarPath);
+
+ m_path = strVarPath;
+ const std::vector<std::string> segments = URIUtils::SplitPath(m_path);
+
+ m_bValid =
+ ((segments.size() >= 3) && (segments.size() <= 5) && (segments.at(1) == "search") &&
+ ((segments.at(2) == "radio") || (segments.at(2) == "tv") || (segments.at(2) == "search")) &&
+ ((segments.size() == 3) || (segments.at(3) == "savedsearches")));
+ m_bRoot = (m_bValid && (segments.size() == 3) && (segments.at(2) != "search"));
+ m_bRadio = (m_bValid && (segments.at(2) == "radio"));
+ m_bSavedSearchesRoot =
+ (m_bValid && (segments.size() == 4) && (segments.at(3) == "savedsearches"));
+ m_bSavedSearch = (m_bValid && (segments.size() == 5));
+ if (m_bSavedSearch)
+ m_iId = std::stoi(segments.at(4));
+
+ return m_bValid;
+}
diff --git a/xbmc/pvr/epg/EpgSearchPath.h b/xbmc/pvr/epg/EpgSearchPath.h
new file mode 100644
index 0000000..559a3c6
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchPath.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012-2021 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace PVR
+{
+class CPVREpgSearchFilter;
+
+class CPVREpgSearchPath
+{
+public:
+ static const std::string PATH_SEARCH_DIALOG;
+ static const std::string PATH_TV_SEARCH;
+ static const std::string PATH_TV_SAVEDSEARCHES;
+ static const std::string PATH_RADIO_SEARCH;
+ static const std::string PATH_RADIO_SAVEDSEARCHES;
+
+ explicit CPVREpgSearchPath(const std::string& strPath);
+ explicit CPVREpgSearchPath(const CPVREpgSearchFilter& search);
+
+ bool IsValid() const { return m_bValid; }
+
+ const std::string& GetPath() const { return m_path; }
+ bool IsSearchRoot() const { return m_bRoot; }
+ bool IsRadio() const { return m_bRadio; }
+ bool IsSavedSearchesRoot() const { return m_bSavedSearchesRoot; }
+ bool IsSavedSearch() const { return m_bSavedSearch; }
+ int GetId() const { return m_iId; }
+
+private:
+ bool Init(const std::string& strPath);
+
+ std::string m_path;
+ bool m_bValid = false;
+ bool m_bRoot = false;
+ bool m_bRadio = false;
+ bool m_bSavedSearchesRoot = false;
+ bool m_bSavedSearch = false;
+ int m_iId = -1;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgTagsCache.cpp b/xbmc/pvr/epg/EpgTagsCache.cpp
new file mode 100644
index 0000000..2c4d111
--- /dev/null
+++ b/xbmc/pvr/epg/EpgTagsCache.cpp
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgTagsCache.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace PVR;
+
+namespace
+{
+const CDateTimeSpan ONE_SECOND(0, 0, 0, 1);
+}
+
+void CPVREpgTagsCache::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
+{
+ m_channelData = data;
+
+ if (m_lastEndedTag)
+ m_lastEndedTag->SetChannelData(data);
+ if (m_nowActiveTag)
+ m_nowActiveTag->SetChannelData(data);
+ if (m_nextStartingTag)
+ m_nextStartingTag->SetChannelData(data);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsCache::GetLastEndedTag()
+{
+ Refresh();
+ return m_lastEndedTag;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsCache::GetNowActiveTag()
+{
+ Refresh();
+ return m_nowActiveTag;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsCache::GetNextStartingTag()
+{
+ Refresh();
+ return m_nextStartingTag;
+}
+
+void CPVREpgTagsCache::Reset()
+{
+ m_lastEndedTag.reset();
+
+ m_nowActiveTag.reset();
+ m_nowActiveStart.Reset();
+ m_nowActiveEnd.Reset();
+
+ m_nextStartingTag.reset();
+}
+
+bool CPVREpgTagsCache::Refresh()
+{
+ const CDateTime activeTime =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetChannelPlaybackTime(
+ m_channelData->ClientId(), m_channelData->UniqueClientChannelId());
+
+ if (m_nowActiveStart.IsValid() && m_nowActiveEnd.IsValid() && m_nowActiveStart <= activeTime &&
+ m_nowActiveEnd > activeTime)
+ return false;
+
+ const std::shared_ptr<CPVREpgInfoTag> prevNowActiveTag = m_nowActiveTag;
+
+ m_lastEndedTag.reset();
+ m_nowActiveTag.reset();
+ m_nextStartingTag.reset();
+
+ const auto it =
+ std::find_if(m_changedTags.cbegin(), m_changedTags.cend(), [&activeTime](const auto& tag) {
+ return tag.second->StartAsUTC() <= activeTime && tag.second->EndAsUTC() > activeTime;
+ });
+
+ if (it != m_changedTags.cend())
+ {
+ m_nowActiveTag = (*it).second;
+ m_nowActiveStart = m_nowActiveTag->StartAsUTC();
+ m_nowActiveEnd = m_nowActiveTag->EndAsUTC();
+ }
+
+ if (!m_nowActiveTag && m_database)
+ {
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags =
+ m_database->GetEpgTagsByMinEndMaxStartTime(m_iEpgID, activeTime + ONE_SECOND, activeTime);
+ if (!tags.empty())
+ {
+ if (tags.size() > 1)
+ CLog::LogF(LOGWARNING, "Got multiple results. Picking up the first.");
+
+ m_nowActiveTag = tags.front();
+ m_nowActiveTag->SetChannelData(m_channelData);
+ m_nowActiveStart = m_nowActiveTag->StartAsUTC();
+ m_nowActiveEnd = m_nowActiveTag->EndAsUTC();
+ }
+ }
+
+ RefreshLastEndedTag(activeTime);
+ RefreshNextStartingTag(activeTime);
+
+ if (!m_nowActiveTag)
+ {
+ // we're in a gap. remember start and end time of that gap to avoid unneeded db load.
+ if (m_lastEndedTag)
+ m_nowActiveStart = m_lastEndedTag->EndAsUTC();
+ else
+ m_nowActiveStart = activeTime - CDateTimeSpan(1000, 0, 0, 0); // fake start far in the past
+
+ if (m_nextStartingTag)
+ m_nowActiveEnd = m_nextStartingTag->StartAsUTC();
+ else
+ m_nowActiveEnd = activeTime + CDateTimeSpan(1000, 0, 0, 0); // fake end far in the future
+ }
+
+ const bool tagChanged =
+ m_nowActiveTag && (!prevNowActiveTag || *prevNowActiveTag != *m_nowActiveTag);
+ const bool tagRemoved = !m_nowActiveTag && prevNowActiveTag;
+
+ return (tagChanged || tagRemoved);
+}
+
+void CPVREpgTagsCache::RefreshLastEndedTag(const CDateTime& activeTime)
+{
+ if (m_database)
+ {
+ m_lastEndedTag = m_database->GetEpgTagByMaxEndTime(m_iEpgID, activeTime);
+ if (m_lastEndedTag)
+ m_lastEndedTag->SetChannelData(m_channelData);
+ }
+
+ for (auto it = m_changedTags.rbegin(); it != m_changedTags.rend(); ++it)
+ {
+ if (it->second->WasActive())
+ {
+ if (!m_lastEndedTag || m_lastEndedTag->EndAsUTC() < it->second->EndAsUTC())
+ {
+ m_lastEndedTag = it->second;
+ break;
+ }
+ }
+ }
+}
+
+void CPVREpgTagsCache::RefreshNextStartingTag(const CDateTime& activeTime)
+{
+ if (m_database)
+ {
+ m_nextStartingTag = m_database->GetEpgTagByMinStartTime(m_iEpgID, activeTime + ONE_SECOND);
+ if (m_nextStartingTag)
+ m_nextStartingTag->SetChannelData(m_channelData);
+ }
+
+ for (const auto& tag : m_changedTags)
+ {
+ if (tag.second->IsUpcoming())
+ {
+ if (!m_nextStartingTag || m_nextStartingTag->StartAsUTC() > tag.second->StartAsUTC())
+ {
+ m_nextStartingTag = tag.second;
+ break;
+ }
+ }
+ }
+}
diff --git a/xbmc/pvr/epg/EpgTagsCache.h b/xbmc/pvr/epg/EpgTagsCache.h
new file mode 100644
index 0000000..f3505bc
--- /dev/null
+++ b/xbmc/pvr/epg/EpgTagsCache.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+
+#include <map>
+#include <memory>
+
+namespace PVR
+{
+class CPVREpgChannelData;
+class CPVREpgDatabase;
+class CPVREpgInfoTag;
+
+class CPVREpgTagsCache
+{
+public:
+ CPVREpgTagsCache() = delete;
+ CPVREpgTagsCache(int iEpgID,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database,
+ const std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& changedTags)
+ : m_iEpgID(iEpgID), m_channelData(channelData), m_database(database), m_changedTags(changedTags)
+ {
+ }
+
+ void SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data);
+
+ void Reset();
+
+ bool Refresh();
+
+ std::shared_ptr<CPVREpgInfoTag> GetLastEndedTag();
+ std::shared_ptr<CPVREpgInfoTag> GetNowActiveTag();
+ std::shared_ptr<CPVREpgInfoTag> GetNextStartingTag();
+
+private:
+ void RefreshLastEndedTag(const CDateTime& activeTime);
+ void RefreshNextStartingTag(const CDateTime& activeTime);
+
+ int m_iEpgID;
+ std::shared_ptr<CPVREpgChannelData> m_channelData;
+ std::shared_ptr<CPVREpgDatabase> m_database;
+ const std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& m_changedTags;
+
+ std::shared_ptr<CPVREpgInfoTag> m_lastEndedTag;
+ std::shared_ptr<CPVREpgInfoTag> m_nowActiveTag;
+ std::shared_ptr<CPVREpgInfoTag> m_nextStartingTag;
+
+ CDateTime m_nowActiveStart;
+ CDateTime m_nowActiveEnd;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgTagsContainer.cpp b/xbmc/pvr/epg/EpgTagsContainer.cpp
new file mode 100644
index 0000000..b4a778c
--- /dev/null
+++ b/xbmc/pvr/epg/EpgTagsContainer.cpp
@@ -0,0 +1,668 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EpgTagsContainer.h"
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgTagsCache.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+
+using namespace PVR;
+
+namespace
+{
+const CDateTimeSpan ONE_SECOND(0, 0, 0, 1);
+}
+
+CPVREpgTagsContainer::CPVREpgTagsContainer(int iEpgID,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database)
+ : m_iEpgID(iEpgID),
+ m_channelData(channelData),
+ m_database(database),
+ m_tagsCache(new CPVREpgTagsCache(iEpgID, channelData, database, m_changedTags))
+{
+}
+
+CPVREpgTagsContainer::~CPVREpgTagsContainer() = default;
+
+void CPVREpgTagsContainer::SetEpgID(int iEpgID)
+{
+ m_iEpgID = iEpgID;
+ for (const auto& tag : m_changedTags)
+ tag.second->SetEpgID(iEpgID);
+}
+
+void CPVREpgTagsContainer::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
+{
+ m_channelData = data;
+ m_tagsCache->SetChannelData(data);
+ for (const auto& tag : m_changedTags)
+ tag.second->SetChannelData(data);
+}
+
+namespace
+{
+
+void ResolveConflictingTags(const std::shared_ptr<CPVREpgInfoTag>& changedTag,
+ std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags)
+{
+ const CDateTime changedTagStart = changedTag->StartAsUTC();
+ const CDateTime changedTagEnd = changedTag->EndAsUTC();
+
+ for (auto it = tags.begin(); it != tags.end();)
+ {
+ bool bInsert = false;
+
+ if (changedTagEnd > (*it)->StartAsUTC() && changedTagStart < (*it)->EndAsUTC())
+ {
+ it = tags.erase(it);
+
+ if (it == tags.end())
+ {
+ bInsert = true;
+ }
+ }
+ else if ((*it)->StartAsUTC() >= changedTagEnd)
+ {
+ bInsert = true;
+ }
+ else
+ {
+ ++it;
+ }
+
+ if (bInsert)
+ {
+ tags.emplace(it, changedTag);
+ break;
+ }
+ }
+}
+
+bool FixOverlap(const std::shared_ptr<CPVREpgInfoTag>& previousTag,
+ const std::shared_ptr<CPVREpgInfoTag>& currentTag)
+{
+ if (!previousTag)
+ return true;
+
+ if (previousTag->EndAsUTC() >= currentTag->EndAsUTC())
+ {
+ // delete the current tag. it's completely overlapped
+ CLog::LogF(LOGDEBUG,
+ "Erasing completely overlapped event from EPG timeline "
+ "({} - {} - {} - {}) "
+ "({} - {} - {} - {}).",
+ previousTag->UniqueBroadcastID(), previousTag->Title(),
+ previousTag->StartAsUTC().GetAsDBDateTime(),
+ previousTag->EndAsUTC().GetAsDBDateTime(), currentTag->UniqueBroadcastID(),
+ currentTag->Title(), currentTag->StartAsUTC().GetAsDBDateTime(),
+ currentTag->EndAsUTC().GetAsDBDateTime());
+
+ return false;
+ }
+ else if (previousTag->EndAsUTC() > currentTag->StartAsUTC())
+ {
+ // fix the end time of the predecessor of the event
+ CLog::LogF(LOGDEBUG,
+ "Fixing partly overlapped event in EPG timeline "
+ "({} - {} - {} - {}) "
+ "({} - {} - {} - {}).",
+ previousTag->UniqueBroadcastID(), previousTag->Title(),
+ previousTag->StartAsUTC().GetAsDBDateTime(),
+ previousTag->EndAsUTC().GetAsDBDateTime(), currentTag->UniqueBroadcastID(),
+ currentTag->Title(), currentTag->StartAsUTC().GetAsDBDateTime(),
+ currentTag->EndAsUTC().GetAsDBDateTime());
+
+ previousTag->SetEndFromUTC(currentTag->StartAsUTC());
+ }
+ return true;
+}
+
+} // unnamed namespace
+
+bool CPVREpgTagsContainer::UpdateEntries(const CPVREpgTagsContainer& tags)
+{
+ if (tags.m_changedTags.empty())
+ return false;
+
+ if (m_database)
+ {
+ const CDateTime minEventEnd = (*tags.m_changedTags.cbegin()).second->StartAsUTC() + ONE_SECOND;
+ const CDateTime maxEventStart = (*tags.m_changedTags.crbegin()).second->EndAsUTC();
+
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> existingTags =
+ m_database->GetEpgTagsByMinEndMaxStartTime(m_iEpgID, minEventEnd, maxEventStart);
+
+ if (!m_changedTags.empty())
+ {
+ // Fix data inconsistencies
+ for (const auto& changedTagsEntry : m_changedTags)
+ {
+ const auto& changedTag = changedTagsEntry.second;
+
+ if (changedTag->EndAsUTC() > minEventEnd && changedTag->StartAsUTC() < maxEventStart)
+ {
+ // tag is in queried range, thus it could cause inconsistencies...
+ ResolveConflictingTags(changedTag, existingTags);
+ }
+ }
+ }
+
+ bool bResetCache = false;
+ for (const auto& tagsEntry : tags.m_changedTags)
+ {
+ const auto& tag = tagsEntry.second;
+
+ tag->SetChannelData(m_channelData);
+ tag->SetEpgID(m_iEpgID);
+
+ const auto it =
+ std::find_if(existingTags.cbegin(), existingTags.cend(),
+ [&tag](const auto& t) { return t->StartAsUTC() == tag->StartAsUTC(); });
+
+ if (it != existingTags.cend())
+ {
+ const std::shared_ptr<CPVREpgInfoTag>& existingTag = *it;
+
+ existingTag->SetChannelData(m_channelData);
+ existingTag->SetEpgID(m_iEpgID);
+
+ if (existingTag->Update(*tag, false))
+ {
+ // tag differs from existing tag and must be persisted
+ m_changedTags.insert({existingTag->StartAsUTC(), existingTag});
+ bResetCache = true;
+ }
+ }
+ else
+ {
+ // new tags must always be persisted
+ m_changedTags.insert({tag->StartAsUTC(), tag});
+ bResetCache = true;
+ }
+ }
+
+ if (bResetCache)
+ m_tagsCache->Reset();
+ }
+ else
+ {
+ for (const auto& tag : tags.m_changedTags)
+ UpdateEntry(tag.second);
+ }
+
+ return true;
+}
+
+void CPVREpgTagsContainer::FixOverlappingEvents(
+ std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const
+{
+ bool bResetCache = false;
+
+ std::shared_ptr<CPVREpgInfoTag> previousTag;
+ for (auto it = tags.begin(); it != tags.end();)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> currentTag = *it;
+ if (FixOverlap(previousTag, currentTag))
+ {
+ previousTag = currentTag;
+ ++it;
+ }
+ else
+ {
+ it = tags.erase(it);
+ bResetCache = true;
+ }
+ }
+
+ if (bResetCache)
+ m_tagsCache->Reset();
+}
+
+void CPVREpgTagsContainer::FixOverlappingEvents(
+ std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& tags) const
+{
+ bool bResetCache = false;
+
+ std::shared_ptr<CPVREpgInfoTag> previousTag;
+ for (auto it = tags.begin(); it != tags.end();)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> currentTag = (*it).second;
+ if (FixOverlap(previousTag, currentTag))
+ {
+ previousTag = currentTag;
+ ++it;
+ }
+ else
+ {
+ it = tags.erase(it);
+ bResetCache = true;
+ }
+ }
+
+ if (bResetCache)
+ m_tagsCache->Reset();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::CreateEntry(
+ const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (tag)
+ {
+ tag->SetChannelData(m_channelData);
+ }
+ return tag;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgTagsContainer::CreateEntries(
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const
+{
+ for (auto& tag : tags)
+ {
+ tag->SetChannelData(m_channelData);
+ }
+ return tags;
+}
+
+bool CPVREpgTagsContainer::UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ tag->SetChannelData(m_channelData);
+ tag->SetEpgID(m_iEpgID);
+
+ std::shared_ptr<CPVREpgInfoTag> existingTag = GetTag(tag->StartAsUTC());
+ if (existingTag)
+ {
+ if (existingTag->Update(*tag, false))
+ {
+ // tag differs from existing tag and must be persisted
+ m_changedTags.insert({existingTag->StartAsUTC(), existingTag});
+ m_tagsCache->Reset();
+ }
+ }
+ else
+ {
+ // new tags must always be persisted
+ m_changedTags.insert({tag->StartAsUTC(), tag});
+ m_tagsCache->Reset();
+ }
+
+ return true;
+}
+
+bool CPVREpgTagsContainer::DeleteEntry(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ m_changedTags.erase(tag->StartAsUTC());
+ m_deletedTags.insert({tag->StartAsUTC(), tag});
+ m_tagsCache->Reset();
+ return true;
+}
+
+void CPVREpgTagsContainer::Cleanup(const CDateTime& time)
+{
+ bool bResetCache = false;
+ for (auto it = m_changedTags.begin(); it != m_changedTags.end();)
+ {
+ if (it->second->EndAsUTC() < time)
+ {
+ const auto it1 = m_deletedTags.find(it->first);
+ if (it1 != m_deletedTags.end())
+ m_deletedTags.erase(it1);
+
+ it = m_changedTags.erase(it);
+ bResetCache = true;
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ if (bResetCache)
+ m_tagsCache->Reset();
+
+ if (m_database)
+ m_database->DeleteEpgTags(m_iEpgID, time);
+}
+
+void CPVREpgTagsContainer::Clear()
+{
+ m_changedTags.clear();
+ m_tagsCache->Reset();
+}
+
+bool CPVREpgTagsContainer::IsEmpty() const
+{
+ if (!m_changedTags.empty())
+ return false;
+
+ if (m_database)
+ return !m_database->HasTags(m_iEpgID);
+
+ return true;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTag(const CDateTime& startTime) const
+{
+ const auto it = m_changedTags.find(startTime);
+ if (it != m_changedTags.cend())
+ return (*it).second;
+
+ if (m_database)
+ return CreateEntry(m_database->GetEpgTagByStartTime(m_iEpgID, startTime));
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTag(unsigned int iUniqueBroadcastID) const
+{
+ if (iUniqueBroadcastID == EPG_TAG_INVALID_UID)
+ return {};
+
+ const auto it = std::find_if(m_changedTags.cbegin(), m_changedTags.cend(),
+ [iUniqueBroadcastID](const auto& tag) {
+ return tag.second->UniqueBroadcastID() == iUniqueBroadcastID;
+ });
+
+ if (it != m_changedTags.cend())
+ return (*it).second;
+
+ if (m_database)
+ return CreateEntry(m_database->GetEpgTagByUniqueBroadcastID(m_iEpgID, iUniqueBroadcastID));
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTagByDatabaseID(int iDatabaseID) const
+{
+ if (iDatabaseID <= 0)
+ return {};
+
+ const auto it =
+ std::find_if(m_changedTags.cbegin(), m_changedTags.cend(), [iDatabaseID](const auto& tag) {
+ return tag.second->DatabaseID() == iDatabaseID;
+ });
+
+ if (it != m_changedTags.cend())
+ return (*it).second;
+
+ if (m_database)
+ return CreateEntry(m_database->GetEpgTagByDatabaseID(m_iEpgID, iDatabaseID));
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTagBetween(const CDateTime& start,
+ const CDateTime& end) const
+{
+ for (const auto& tag : m_changedTags)
+ {
+ if (tag.second->StartAsUTC() >= start)
+ {
+ if (tag.second->EndAsUTC() <= end)
+ return tag.second;
+ else
+ break;
+ }
+ }
+
+ if (m_database)
+ {
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags =
+ CreateEntries(m_database->GetEpgTagsByMinStartMaxEndTime(m_iEpgID, start, end));
+ if (!tags.empty())
+ {
+ if (tags.size() > 1)
+ CLog::LogF(LOGWARNING, "Got multiple tags. Picking up the first.");
+
+ return tags.front();
+ }
+ }
+
+ return {};
+}
+
+bool CPVREpgTagsContainer::UpdateActiveTag()
+{
+ return m_tagsCache->Refresh();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetActiveTag() const
+{
+ return m_tagsCache->GetNowActiveTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetLastEndedTag() const
+{
+ return m_tagsCache->GetLastEndedTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetNextStartingTag() const
+{
+ return m_tagsCache->GetNextStartingTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::CreateGapTag(const CDateTime& start,
+ const CDateTime& end) const
+{
+ return std::make_shared<CPVREpgInfoTag>(m_channelData, m_iEpgID, start, end, true);
+}
+
+void CPVREpgTagsContainer::MergeTags(const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart,
+ std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const
+{
+ for (const auto& changedTagsEntry : m_changedTags)
+ {
+ const auto& changedTag = changedTagsEntry.second;
+
+ if (changedTag->EndAsUTC() > minEventEnd && changedTag->StartAsUTC() < maxEventStart)
+ tags.emplace_back(changedTag);
+ }
+
+ if (!tags.empty())
+ FixOverlappingEvents(tags);
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgTagsContainer::GetTimeline(
+ const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const
+{
+ if (m_database)
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+
+ bool loadFromDb = true;
+ if (!m_changedTags.empty())
+ {
+ const CDateTime lastEnd = m_database->GetLastEndTime(m_iEpgID);
+ if (!lastEnd.IsValid() || lastEnd < minEventEnd)
+ {
+ // nothing in the db yet. take what we have in memory.
+ loadFromDb = false;
+ MergeTags(minEventEnd, maxEventStart, tags);
+ }
+ }
+
+ if (loadFromDb)
+ {
+ tags = m_database->GetEpgTagsByMinEndMaxStartTime(m_iEpgID, minEventEnd, maxEventStart);
+
+ if (!m_changedTags.empty())
+ {
+ // Fix data inconsistencies
+ for (const auto& changedTagsEntry : m_changedTags)
+ {
+ const auto& changedTag = changedTagsEntry.second;
+
+ if (changedTag->EndAsUTC() > minEventEnd && changedTag->StartAsUTC() < maxEventStart)
+ {
+ // tag is in queried range, thus it could cause inconsistencies...
+ ResolveConflictingTags(changedTag, tags);
+ }
+ }
+
+ // Append missing tags
+ MergeTags(tags.empty() ? minEventEnd : tags.back()->EndAsUTC(), maxEventStart, tags);
+ }
+ }
+
+ tags = CreateEntries(tags);
+
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> result;
+
+ for (const auto& epgTag : tags)
+ {
+ if (!result.empty())
+ {
+ const CDateTime currStart = epgTag->StartAsUTC();
+ const CDateTime prevEnd = result.back()->EndAsUTC();
+ if ((currStart - prevEnd) >= ONE_SECOND)
+ {
+ // insert gap tag before current tag
+ result.emplace_back(CreateGapTag(prevEnd, currStart));
+ }
+ }
+
+ result.emplace_back(epgTag);
+ }
+
+ if (result.empty())
+ {
+ // create single gap tag
+ CDateTime maxEnd = m_database->GetMaxEndTime(m_iEpgID, minEventEnd);
+ if (!maxEnd.IsValid() || maxEnd < timelineStart)
+ maxEnd = timelineStart;
+
+ CDateTime minStart = m_database->GetMinStartTime(m_iEpgID, maxEventStart);
+ if (!minStart.IsValid() || minStart > timelineEnd)
+ minStart = timelineEnd;
+
+ result.emplace_back(CreateGapTag(maxEnd, minStart));
+ }
+ else
+ {
+ if (result.front()->StartAsUTC() > minEventEnd)
+ {
+ // prepend gap tag
+ CDateTime maxEnd = m_database->GetMaxEndTime(m_iEpgID, minEventEnd);
+ if (!maxEnd.IsValid() || maxEnd < timelineStart)
+ maxEnd = timelineStart;
+
+ result.insert(result.begin(), CreateGapTag(maxEnd, result.front()->StartAsUTC()));
+ }
+
+ if (result.back()->EndAsUTC() < maxEventStart)
+ {
+ // append gap tag
+ CDateTime minStart = m_database->GetMinStartTime(m_iEpgID, maxEventStart);
+ if (!minStart.IsValid() || minStart > timelineEnd)
+ minStart = timelineEnd;
+
+ result.emplace_back(CreateGapTag(result.back()->EndAsUTC(), minStart));
+ }
+ }
+
+ return result;
+ }
+
+ return {};
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgTagsContainer::GetAllTags() const
+{
+ if (m_database)
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ if (!m_changedTags.empty() && !m_database->HasTags(m_iEpgID))
+ {
+ // nothing in the db yet. take what we have in memory.
+ std::transform(m_changedTags.cbegin(), m_changedTags.cend(), std::back_inserter(tags),
+ [](const auto& tag) { return tag.second; });
+
+ FixOverlappingEvents(tags);
+ }
+ else
+ {
+ tags = m_database->GetAllEpgTags(m_iEpgID);
+
+ if (!m_changedTags.empty())
+ {
+ // Fix data inconsistencies
+ for (const auto& changedTagsEntry : m_changedTags)
+ {
+ ResolveConflictingTags(changedTagsEntry.second, tags);
+ }
+ }
+ }
+
+ return CreateEntries(tags);
+ }
+
+ return {};
+}
+
+std::pair<CDateTime, CDateTime> CPVREpgTagsContainer::GetFirstAndLastUncommitedEPGDate() const
+{
+ if (m_changedTags.empty())
+ return {};
+
+ return {(*m_changedTags.cbegin()).second->StartAsUTC(),
+ (*m_changedTags.crbegin()).second->EndAsUTC()};
+}
+
+bool CPVREpgTagsContainer::NeedsSave() const
+{
+ return !m_changedTags.empty() || !m_deletedTags.empty();
+}
+
+void CPVREpgTagsContainer::QueuePersistQuery()
+{
+ if (m_database)
+ {
+ m_database->Lock();
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Tags Container: Updating {}, deleting {} events...",
+ m_changedTags.size(), m_deletedTags.size());
+
+ for (const auto& tag : m_deletedTags)
+ m_database->QueueDeleteTagQuery(*tag.second);
+
+ m_deletedTags.clear();
+
+ FixOverlappingEvents(m_changedTags);
+
+ for (const auto& tag : m_changedTags)
+ {
+ // remove any conflicting events from database before persisting the new event
+ m_database->QueueDeleteEpgTagsByMinEndMaxStartTimeQuery(
+ m_iEpgID, tag.second->StartAsUTC() + ONE_SECOND, tag.second->EndAsUTC() - ONE_SECOND);
+
+ tag.second->QueuePersistQuery(m_database);
+ }
+
+ Clear();
+
+ m_database->Unlock();
+ }
+}
+
+void CPVREpgTagsContainer::QueueDelete()
+{
+ if (m_database)
+ m_database->QueueDeleteEpgTags(m_iEpgID);
+
+ Clear();
+}
diff --git a/xbmc/pvr/epg/EpgTagsContainer.h b/xbmc/pvr/epg/EpgTagsContainer.h
new file mode 100644
index 0000000..fcbe0d1
--- /dev/null
+++ b/xbmc/pvr/epg/EpgTagsContainer.h
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+
+#include <map>
+#include <memory>
+#include <vector>
+
+namespace PVR
+{
+class CPVREpgTagsCache;
+class CPVREpgChannelData;
+class CPVREpgDatabase;
+class CPVREpgInfoTag;
+
+class CPVREpgTagsContainer
+{
+public:
+ CPVREpgTagsContainer() = delete;
+ CPVREpgTagsContainer(int iEpgID,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database);
+ virtual ~CPVREpgTagsContainer();
+
+ /*!
+ * @brief Set the EPG id for this EPG.
+ * @param iEpgID The ID.
+ */
+ void SetEpgID(int iEpgID);
+
+ /*!
+ * @brief Set the channel data for this EPG.
+ * @param data The channel data.
+ */
+ void SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data);
+
+ /*!
+ * @brief Update an entry.
+ * @param tag The tag to update.
+ * @return True if it was updated successfully, false otherwise.
+ */
+ bool UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag);
+
+ /*!
+ * @brief Delete an entry.
+ * @param tag The tag to delete.
+ * @return True if it was deleted successfully, false otherwise.
+ */
+ bool DeleteEntry(const std::shared_ptr<CPVREpgInfoTag>& tag);
+
+ /*!
+ * @brief Update all entries with the provided tags.
+ * @param tags The updated tags.
+ * @return True if the update was successful, false otherwise.
+ */
+ bool UpdateEntries(const CPVREpgTagsContainer& tags);
+
+ /*!
+ * @brief Release all entries.
+ */
+ void Clear();
+
+ /*!
+ * @brief Remove all entries which were finished before the given time.
+ * @param time Delete entries with an end time before this time.
+ */
+ void Cleanup(const CDateTime& time);
+
+ /*!
+ * @brief Check whether this container is empty.
+ * @return True if the container does not contain any entries, false otherwise.
+ */
+ bool IsEmpty() const;
+
+ /*!
+ * @brief Get an EPG tag given its start time.
+ * @param startTime The start time
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTag(const CDateTime& startTime) const;
+
+ /*!
+ * @brief Get an EPG tag given its unique broadcast ID.
+ * @param iUniqueBroadcastID The ID.
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTag(unsigned int iUniqueBroadcastID) const;
+
+ /*!
+ * @brief Get an EPG tag given its database ID.
+ * @param iDatabaseID The ID.
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagByDatabaseID(int iDatabaseID) const;
+
+ /*!
+ * @brief Get the event that occurs between the given begin and end time.
+ * @param start The start of the time interval.
+ * @param end The end of the time interval.
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagBetween(const CDateTime& start, const CDateTime& end) const;
+
+ /*!
+ * @brief Update the currently active event.
+ * @return True if the active event was updated, false otherwise.
+ */
+ bool UpdateActiveTag();
+
+ /*!
+ * @brief Get the event that is occurring now
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetActiveTag() const;
+
+ /*!
+ * @brief Get the event that will occur next
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetNextStartingTag() const;
+
+ /*!
+ * @brief Get the event that occurred previously
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetLastEndedTag() const;
+
+ /*!
+ * @brief Get all EPG tags for the given time frame, including "gap" tags.
+ * @param timelineStart Start of time line
+ * @param timelineEnd End of time line
+ * @param minEventEnd The minimum end time of the events to return
+ * @param maxEventStart The maximum start time of the events to return
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetTimeline(const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const;
+
+ /*!
+ * @brief Get all EPG tags.
+ * @return The tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetAllTags() const;
+
+ /*!
+ * @brief Get the start and end time of the last not yet commited entry in this EPG.
+ * @return The times; first: start time, second: end time.
+ */
+ std::pair<CDateTime, CDateTime> GetFirstAndLastUncommitedEPGDate() const;
+
+ /*!
+ * @brief Check whether this container has unsaved data.
+ * @return True if this container contains unsaved data, false otherwise.
+ */
+ bool NeedsSave() const;
+
+ /*!
+ * @brief Write the query to persist data into database's queue
+ */
+ void QueuePersistQuery();
+
+ /*!
+ * @brief Queue the deletion of this container from its database.
+ */
+ void QueueDelete();
+
+private:
+ /*!
+ * @brief Complete the instance data for the given tags.
+ * @param tags The tags to complete.
+ * @return The completed tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> CreateEntries(
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const;
+
+ /*!
+ * @brief Complete the instance data for the given tag.
+ * @param tags The tag to complete.
+ * @return The completed tag.
+ */
+ std::shared_ptr<CPVREpgInfoTag> CreateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+
+ /*!
+ * @brief Create a "gap" tag
+ * @param start The start time of the gap.
+ * @param end The end time of the gap.
+ * @return The tag.
+ */
+ std::shared_ptr<CPVREpgInfoTag> CreateGapTag(const CDateTime& start, const CDateTime& end) const;
+
+ /*!
+ * @brief Merge m_changedTags tags into given tags, resolving conflicts.
+ * @param minEventEnd The minimum end time of the events to return
+ * @param maxEventStart The maximum start time of the events to return
+ * @param tags The merged tags.
+ */
+ void MergeTags(const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart,
+ std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const;
+
+ /*!
+ * @brief Fix overlapping events.
+ * @param tags The events to check/fix.
+ */
+ void FixOverlappingEvents(std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const;
+ void FixOverlappingEvents(std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& tags) const;
+
+ int m_iEpgID = 0;
+ std::shared_ptr<CPVREpgChannelData> m_channelData;
+ const std::shared_ptr<CPVREpgDatabase> m_database;
+ const std::unique_ptr<CPVREpgTagsCache> m_tagsCache;
+
+ std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>> m_changedTags;
+ std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>> m_deletedTags;
+};
+
+} // namespace PVR