diff options
Diffstat (limited to 'xbmc/pvr/epg')
-rw-r--r-- | xbmc/pvr/epg/CMakeLists.txt | 22 | ||||
-rw-r--r-- | xbmc/pvr/epg/Epg.cpp | 554 | ||||
-rw-r--r-- | xbmc/pvr/epg/Epg.h | 327 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgChannelData.cpp | 107 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgChannelData.h | 59 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgContainer.cpp | 1028 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgContainer.h | 360 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgDatabase.cpp | 1494 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgDatabase.h | 377 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgInfoTag.cpp | 681 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgInfoTag.h | 513 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgSearchData.h | 44 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgSearchFilter.cpp | 403 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgSearchFilter.h | 171 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgSearchPath.cpp | 64 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgSearchPath.h | 49 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgTagsCache.cpp | 179 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgTagsCache.h | 61 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgTagsContainer.cpp | 668 | ||||
-rw-r--r-- | xbmc/pvr/epg/EpgTagsContainer.h | 227 |
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 |