/* * 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 #include #include #include using namespace PVR; CPVREpg::CPVREpg(int iEpgID, const std::string& strName, const std::string& strScraperName, const std::shared_ptr& 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& channelData, const std::shared_ptr& 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 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 lock(m_critSection); m_tags.Cleanup(time); } std::shared_ptr CPVREpg::GetTagNow() const { std::unique_lock lock(m_critSection); return m_tags.GetActiveTag(); } std::shared_ptr CPVREpg::GetTagNext() const { std::unique_lock lock(m_critSection); return m_tags.GetNextStartingTag(); } std::shared_ptr CPVREpg::GetTagPrevious() const { std::unique_lock lock(m_critSection); return m_tags.GetLastEndedTag(); } bool CPVREpg::CheckPlayingEvent() { std::unique_lock lock(m_critSection); if (m_tags.UpdateActiveTag()) { m_events.Publish(PVREvent::EpgActiveItem); return true; } return false; } std::shared_ptr CPVREpg::GetTagByBroadcastId(unsigned int iUniqueBroadcastId) const { std::unique_lock lock(m_critSection); return m_tags.GetTag(iUniqueBroadcastId); } std::shared_ptr CPVREpg::GetTagByDatabaseId(int iDatabaseId) const { std::unique_lock lock(m_critSection); return m_tags.GetTagByDatabaseID(iDatabaseId); } std::shared_ptr CPVREpg::GetTagBetween(const CDateTime& beginTime, const CDateTime& endTime, bool bUpdateFromClient /* = false */) { std::shared_ptr tag; std::unique_lock 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 tmpEpg = std::make_shared( m_iEpgID, m_strName, m_strScraperName, m_channelData, std::shared_ptr()); if (tmpEpg->UpdateFromScraper(b, e, true)) tag = tmpEpg->GetTagBetween(beginTime, endTime, false); if (tag) m_tags.UpdateEntry(tag); } return tag; } std::vector> CPVREpg::GetTimeline( const CDateTime& timelineStart, const CDateTime& timelineEnd, const CDateTime& minEventEnd, const CDateTime& maxEventStart) const { std::unique_lock lock(m_critSection); return m_tags.GetTimeline(timelineStart, timelineEnd, minEventEnd, maxEventStart); } bool CPVREpg::UpdateEntries(const CPVREpg& epg) { std::unique_lock 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& 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 tag = std::make_shared(*data, iClientId, m_channelData, m_iEpgID); return !IsTagExpired(tag) && m_tags.UpdateEntry(tag); } bool CPVREpg::UpdateEntry(const std::shared_ptr& tag, EPG_EVENT_STATE newState) { bool bRet = true; bool bNotify = true; if (newState == EPG_EVENT_CREATED || newState == EPG_EVENT_UPDATED) { std::unique_lock lock(m_critSection); bRet = !IsTagExpired(tag) && m_tags.UpdateEntry(tag); } else if (newState == EPG_EVENT_DELETED) { std::unique_lock lock(m_critSection); const std::shared_ptr 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& database, bool bForceUpdate /* = false */) { bool bUpdate = false; std::shared_ptr tmpEpg; { std::unique_lock 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(m_iEpgID, m_strName, m_strScraperName, m_channelData, std::shared_ptr()); } } // 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> CPVREpg::GetTags() const { std::unique_lock lock(m_critSection); return m_tags.GetAllTags(); } bool CPVREpg::QueuePersistQuery(const std::shared_ptr& 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& database) { if (!database) { CLog::LogF(LOGERROR, "No EPG database"); return false; } std::unique_lock 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 CPVREpg::GetFirstAndLastUncommitedEPGDate() const { std::unique_lock 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 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 CPVREpg::GetChannelData() const { std::unique_lock lock(m_critSection); return m_channelData; } void CPVREpg::SetChannelData(const std::shared_ptr& data) { std::unique_lock lock(m_critSection); m_channelData = data; m_tags.SetChannelData(data); } int CPVREpg::ChannelID() const { std::unique_lock lock(m_critSection); return m_channelData->ChannelId(); } const std::string& CPVREpg::ScraperName() const { std::unique_lock lock(m_critSection); return m_strScraperName; } const std::string& CPVREpg::Name() const { std::unique_lock lock(m_critSection); return m_strName; } int CPVREpg::EpgID() const { std::unique_lock lock(m_critSection); return m_iEpgID; } bool CPVREpg::UpdatePending() const { return m_bUpdatePending; } bool CPVREpg::NeedsSave() const { std::unique_lock lock(m_critSection); return m_bChanged || m_bUpdateLastScanTime || m_tags.NeedsSave(); } bool CPVREpg::IsValid() const { std::unique_lock 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& database) { const std::vector urlsToCheck = database->GetAllIconPaths(EpgID()); const std::string owner = StringUtils::Format(CPVREpgInfoTag::IMAGE_OWNER_PATTERN, EpgID()); return CPVRCachedImages::Cleanup({{owner, ""}}, urlsToCheck); }