diff options
Diffstat (limited to 'xbmc/pvr')
198 files changed, 57535 insertions, 0 deletions
diff --git a/xbmc/pvr/CMakeLists.txt b/xbmc/pvr/CMakeLists.txt new file mode 100644 index 0000000..e7a95a6 --- /dev/null +++ b/xbmc/pvr/CMakeLists.txt @@ -0,0 +1,30 @@ +set(SOURCES PVRCachedImage.cpp + PVRCachedImages.cpp + PVRChannelNumberInputHandler.cpp + PVRComponentRegistration.cpp + PVRContextMenus.cpp + PVRDatabase.cpp + PVREdl.cpp + PVREventLogJob.cpp + PVRItem.cpp + PVRManager.cpp + PVRPlaybackState.cpp + PVRStreamProperties.cpp + PVRThumbLoader.cpp) + +set(HEADERS IPVRComponent.h + PVRCachedImage.h + PVRCachedImages.h + PVRChannelNumberInputHandler.h + PVRComponentRegistration.h + PVRContextMenus.h + PVRDatabase.h + PVREdl.h + PVREventLogJob.h + PVRItem.h + PVRManager.h + PVRPlaybackState.h + PVRStreamProperties.h + PVRThumbLoader.h) + +core_add_library(pvr) diff --git a/xbmc/pvr/IPVRComponent.h b/xbmc/pvr/IPVRComponent.h new file mode 100644 index 0000000..8695095 --- /dev/null +++ b/xbmc/pvr/IPVRComponent.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2022 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 + +namespace PVR +{ +class IPVRComponent +{ +public: + virtual ~IPVRComponent() = default; +}; +} // namespace PVR diff --git a/xbmc/pvr/PVRCachedImage.cpp b/xbmc/pvr/PVRCachedImage.cpp new file mode 100644 index 0000000..e336563 --- /dev/null +++ b/xbmc/pvr/PVRCachedImage.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2005-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 "PVRCachedImage.h" + +#include "TextureDatabase.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +using namespace PVR; + +CPVRCachedImage::CPVRCachedImage(const std::string& owner) : m_owner(owner) +{ +} + +CPVRCachedImage::CPVRCachedImage(const std::string& clientImage, const std::string& owner) + : m_clientImage(clientImage), m_owner(owner) +{ + UpdateLocalImage(); +} + +bool CPVRCachedImage::operator==(const CPVRCachedImage& right) const +{ + return (this == &right) || (m_clientImage == right.m_clientImage && + m_localImage == right.m_localImage && m_owner == right.m_owner); +} + +bool CPVRCachedImage::operator!=(const CPVRCachedImage& right) const +{ + return !(*this == right); +} + +void CPVRCachedImage::SetClientImage(const std::string& image) +{ + if (StringUtils::StartsWith(image, "image://")) + { + CLog::LogF(LOGERROR, "Not allowed to call this method with an image URL"); + return; + } + + if (m_owner.empty()) + { + CLog::LogF(LOGERROR, "Empty owner"); + return; + } + + m_clientImage = image; + UpdateLocalImage(); +} + +void CPVRCachedImage::SetOwner(const std::string& owner) +{ + if (m_owner != owner) + { + m_owner = owner; + UpdateLocalImage(); + } +} + +void CPVRCachedImage::UpdateLocalImage() +{ + if (m_clientImage.empty()) + m_localImage.clear(); + else + m_localImage = CTextureUtils::GetWrappedImageURL(m_clientImage, m_owner); +} diff --git a/xbmc/pvr/PVRCachedImage.h b/xbmc/pvr/PVRCachedImage.h new file mode 100644 index 0000000..c8f0210 --- /dev/null +++ b/xbmc/pvr/PVRCachedImage.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-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 CPVRCachedImage +{ +public: + CPVRCachedImage() = delete; + virtual ~CPVRCachedImage() = default; + + explicit CPVRCachedImage(const std::string& owner); + CPVRCachedImage(const std::string& clientImage, const std::string& owner); + + bool operator==(const CPVRCachedImage& right) const; + bool operator!=(const CPVRCachedImage& right) const; + + const std::string& GetClientImage() const { return m_clientImage; } + const std::string& GetLocalImage() const { return m_localImage; } + + void SetClientImage(const std::string& image); + + void SetOwner(const std::string& owner); + +private: + void UpdateLocalImage(); + + std::string m_clientImage; + std::string m_localImage; + std::string m_owner; +}; + +} // namespace PVR diff --git a/xbmc/pvr/PVRCachedImages.cpp b/xbmc/pvr/PVRCachedImages.cpp new file mode 100644 index 0000000..ea17fd7 --- /dev/null +++ b/xbmc/pvr/PVRCachedImages.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2005-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 "PVRCachedImages.h" + +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "TextureDatabase.h" +#include "URL.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> + +using namespace PVR; + +int CPVRCachedImages::Cleanup(const std::vector<PVRImagePattern>& urlPatterns, + const std::vector<std::string>& urlsToCheck, + bool clearTextureForPath /* = false */) +{ + int iCleanedImages = 0; + + if (urlPatterns.empty()) + { + CLog::LogFC(LOGERROR, LOGPVR, "No URL patterns given"); + return iCleanedImages; + } + + CTextureDatabase db; + if (!db.Open()) + { + CLog::LogFC(LOGERROR, LOGPVR, "Failed to open texture database"); + return iCleanedImages; + } + + CDatabase::Filter filter; + + for (const auto& pattern : urlPatterns) + { + const std::string encodedPattern = + StringUtils::Format("{}@{}", pattern.owner, CURL::Encode(pattern.path)); + + std::string escapedPattern; + for (size_t i = 0; i < encodedPattern.size(); ++i) + { + if (encodedPattern[i] == '%' || encodedPattern[i] == '^') + escapedPattern += '^'; + + escapedPattern += encodedPattern[i]; + } + + const std::string where = + StringUtils::Format("url LIKE 'image://{}%' ESCAPE '^'", escapedPattern); + filter.AppendWhere(where, false); // logical OR + } + + CVariant items; + if (!db.GetTextures(items, filter)) + { + CLog::LogFC(LOGERROR, LOGPVR, "Failed to get items from texture database"); + return iCleanedImages; + } + + for (unsigned int i = 0; i < items.size(); ++i) + { + // Unwrap the image:// URL returned from texture db. + const std::string textureURL = UnwrapImageURL(items[i]["url"].asString()); + + if (std::none_of(urlsToCheck.cbegin(), urlsToCheck.cend(), + [&textureURL](const std::string& url) { return url == textureURL; })) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Removing stale cached image: '{}'", textureURL); + CServiceBroker::GetTextureCache()->ClearCachedImage(items[i]["textureid"].asInteger()); + + if (clearTextureForPath) + db.ClearTextureForPath(textureURL, "thumb"); + + iCleanedImages++; + } + } + + return iCleanedImages; +} + +std::string CPVRCachedImages::UnwrapImageURL(const std::string& url) +{ + return StringUtils::StartsWith(url, "image://") ? CURL(url).GetHostName() : url; +} diff --git a/xbmc/pvr/PVRCachedImages.h b/xbmc/pvr/PVRCachedImages.h new file mode 100644 index 0000000..9dcdd24 --- /dev/null +++ b/xbmc/pvr/PVRCachedImages.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005-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> +#include <vector> + +namespace PVR +{ + +struct PVRImagePattern +{ + PVRImagePattern(const std::string& _owner, const std::string& _path) : owner(_owner), path(_path) + { + } + + std::string owner; + std::string path; +}; + +class CPVRCachedImages +{ +public: + /*! + * @brief Erase stale texture db entries and image files. + * @param urlPatterns The URL patterns to fetch from texture database. + * @param urlsToCheck The URLs to check for still being present in the texture db. + * @param clearTextureForPath Whether to clear the path in texture database. + * @return number of cleaned up images. + */ + static int Cleanup(const std::vector<PVRImagePattern>& urlPatterns, + const std::vector<std::string>& urlsToCheck, + bool clearTextureForPath = false); + + /*! + * @brief Extract the wrapped URL from an image URL. + * @param url The URL to unwrap. + * @return The unwrapped URL if url is an image URL, url otherwise. + */ + static std::string UnwrapImageURL(const std::string& url); +}; + +} // namespace PVR diff --git a/xbmc/pvr/PVRChannelNumberInputHandler.cpp b/xbmc/pvr/PVRChannelNumberInputHandler.cpp new file mode 100644 index 0000000..a57c774 --- /dev/null +++ b/xbmc/pvr/PVRChannelNumberInputHandler.cpp @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRChannelNumberInputHandler.h" + +#include "ServiceBroker.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" + +#include <algorithm> +#include <cstdlib> +#include <mutex> +#include <string> + +using namespace PVR; +using namespace std::chrono_literals; + +CPVRChannelNumberInputHandler::CPVRChannelNumberInputHandler() + : CPVRChannelNumberInputHandler(CServiceBroker::GetSettingsComponent() + ->GetAdvancedSettings() + ->m_iPVRNumericChannelSwitchTimeout, + CHANNEL_NUMBER_INPUT_MAX_DIGITS) +{ +} + +CPVRChannelNumberInputHandler::CPVRChannelNumberInputHandler( + int iDelay, int iMaxDigits /* = CHANNEL_NUMBER_INPUT_MAX_DIGITS */) + : m_iDelay(iDelay), m_iMaxDigits(iMaxDigits), m_timer(this) +{ +} + +void CPVRChannelNumberInputHandler::OnTimeout() +{ + if (m_inputBuffer.empty()) + { + std::unique_lock<CCriticalSection> lock(m_mutex); + SetLabel(""); + } + else + { + // call the overridden worker method + OnInputDone(); + + std::unique_lock<CCriticalSection> lock(m_mutex); + + // erase input buffer immediately , but... + m_inputBuffer.erase(); + + // ... display the label for another .5 secs if we stopped the timer before regular timeout. + if (m_timer.IsRunning()) + SetLabel(""); + else + m_timer.Start(500ms); + } +} + +void CPVRChannelNumberInputHandler::ExecuteAction() +{ + m_timer.Stop(true /* wait until worker thread ended */); + OnTimeout(); +} + +bool CPVRChannelNumberInputHandler::CheckInputAndExecuteAction() +{ + const CPVRChannelNumber channelNumber = GetChannelNumber(); + if (channelNumber.IsValid()) + { + // we have a valid channel number; execute the associated action now. + ExecuteAction(); + return true; + } + return false; +} + +void CPVRChannelNumberInputHandler::AppendChannelNumberCharacter(char cCharacter) +{ + if (cCharacter != CPVRChannelNumber::SEPARATOR && (cCharacter < '0' || cCharacter > '9')) + return; + + std::unique_lock<CCriticalSection> lock(m_mutex); + + if (cCharacter == CPVRChannelNumber::SEPARATOR) + { + // no leading separator + if (m_inputBuffer.empty()) + return; + + // max one separator + if (m_inputBuffer.find(CPVRChannelNumber::SEPARATOR) != std::string::npos) + return; + } + + if (m_inputBuffer.size() == static_cast<size_t>(m_iMaxDigits)) + { + m_inputBuffer.erase(m_inputBuffer.begin()); + SetLabel(m_inputBuffer); + } + else if (m_inputBuffer.empty()) + { + m_sortedChannelNumbers.clear(); + GetChannelNumbers(m_sortedChannelNumbers); + + std::sort(m_sortedChannelNumbers.begin(), m_sortedChannelNumbers.end()); + } + + m_inputBuffer.append(&cCharacter, 1); + SetLabel(m_inputBuffer); + + for (auto it = m_sortedChannelNumbers.begin(); it != m_sortedChannelNumbers.end();) + { + const std::string channel = *it; + ++it; + + if (StringUtils::StartsWith(channel, m_inputBuffer)) + { + if (it != m_sortedChannelNumbers.end() && StringUtils::StartsWith(*it, m_inputBuffer)) + { + // there are alternative numbers; wait for more input + break; + } + + // no alternatives; complete the number and fire immediately + m_inputBuffer = channel; + SetLabel(m_inputBuffer); + ExecuteAction(); + return; + } + } + + if (!m_timer.IsRunning()) + m_timer.Start(std::chrono::milliseconds(m_iDelay)); + else + m_timer.Restart(); +} + +CPVRChannelNumber CPVRChannelNumberInputHandler::GetChannelNumber() const +{ + int iChannelNumber = 0; + int iSubChannelNumber = 0; + + std::unique_lock<CCriticalSection> lock(m_mutex); + + size_t pos = m_inputBuffer.find(CPVRChannelNumber::SEPARATOR); + if (pos != std::string::npos) + { + // main + sub + if (pos != 0) + { + iChannelNumber = std::atoi(m_inputBuffer.substr(0, pos).c_str()); + if (pos != m_inputBuffer.size() - 1) + iSubChannelNumber = std::atoi(m_inputBuffer.substr(pos + 1).c_str()); + } + } + else + { + // only main + iChannelNumber = std::atoi(m_inputBuffer.c_str()); + } + + return CPVRChannelNumber(iChannelNumber, iSubChannelNumber); +} + +bool CPVRChannelNumberInputHandler::HasChannelNumber() const +{ + return !m_inputBuffer.empty(); +} + +std::string CPVRChannelNumberInputHandler::GetChannelNumberLabel() const +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + return m_label; +} + +void CPVRChannelNumberInputHandler::SetLabel(const std::string& label) +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + if (label != m_label) + { + m_label = label; + + // inform subscribers + m_events.Publish(PVRChannelNumberInputChangedEvent(m_label)); + } +} diff --git a/xbmc/pvr/PVRChannelNumberInputHandler.h b/xbmc/pvr/PVRChannelNumberInputHandler.h new file mode 100644 index 0000000..efa28ca --- /dev/null +++ b/xbmc/pvr/PVRChannelNumberInputHandler.h @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/channels/PVRChannelNumber.h" +#include "threads/CriticalSection.h" +#include "threads/Timer.h" +#include "utils/EventStream.h" + +#include <string> +#include <vector> + +namespace PVR +{ +struct PVRChannelNumberInputChangedEvent +{ + explicit PVRChannelNumberInputChangedEvent(const std::string& input) : m_input(input) {} + virtual ~PVRChannelNumberInputChangedEvent() = default; + + std::string m_input; +}; + +class CPVRChannelNumberInputHandler : private ITimerCallback +{ +public: + static const int CHANNEL_NUMBER_INPUT_MAX_DIGITS = 5; + + CPVRChannelNumberInputHandler(); + + /*! + * @brief ctor. + * @param iDelay timer delay in millisecods. + * @param iMaxDigits maximum number of display digits to use. + */ + CPVRChannelNumberInputHandler(int iDelay, int iMaxDigits = CHANNEL_NUMBER_INPUT_MAX_DIGITS); + + ~CPVRChannelNumberInputHandler() override = default; + + /*! + * @brief Get the events available for CEventStream. + * @return The events. + */ + CEventStream<PVRChannelNumberInputChangedEvent>& Events() { return m_events; } + + // implementation of ITimerCallback + void OnTimeout() override; + + /*! + * @brief Get the currently available channel numbers. + * @param channelNumbers The list to fill with the channel numbers. + */ + virtual void GetChannelNumbers(std::vector<std::string>& channelNumbers) = 0; + + /*! + * @brief This method gets called after the channel number input timer has expired. + */ + virtual void OnInputDone() = 0; + + /*! + * @brief Appends a channel number character. + * @param cCharacter The character to append. value must be CPVRChannelNumber::SEPARATOR ('.') or any char in the range from '0' to '9'. + */ + virtual void AppendChannelNumberCharacter(char cCharacter); + + /*! + * @brief Check whether a channel number was entered. + * @return True if the handler currently holds a channel number, false otherwise. + */ + bool HasChannelNumber() const; + + /*! + * @brief Get the currently entered channel number as a formatted string. + * @return the channel number string. + */ + std::string GetChannelNumberLabel() const; + + /*! + * @brief If a number was entered, execute the associated action. + * @return True, if the action was executed, false otherwise. + */ + bool CheckInputAndExecuteAction(); + +protected: + /*! + * @brief Get the currently entered channel number. + * @return the channel number. + */ + CPVRChannelNumber GetChannelNumber() const; + + /*! + * @brief Get the currently entered number of digits. + * @return the number of digits. + */ + size_t GetCurrentDigitCount() const { return m_inputBuffer.size(); } + + mutable CCriticalSection m_mutex; + +private: + void ExecuteAction(); + + void SetLabel(const std::string& label); + + std::vector<std::string> m_sortedChannelNumbers; + const int m_iDelay; + const int m_iMaxDigits; + std::string m_inputBuffer; + std::string m_label; + CTimer m_timer; + CEventSource<PVRChannelNumberInputChangedEvent> m_events; +}; + +} // namespace PVR diff --git a/xbmc/pvr/PVRComponentRegistration.cpp b/xbmc/pvr/PVRComponentRegistration.cpp new file mode 100644 index 0000000..7532283 --- /dev/null +++ b/xbmc/pvr/PVRComponentRegistration.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 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 "PVRComponentRegistration.h" + +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "pvr/guilib/PVRGUIActionsClients.h" +#include "pvr/guilib/PVRGUIActionsDatabase.h" +#include "pvr/guilib/PVRGUIActionsEPG.h" +#include "pvr/guilib/PVRGUIActionsParentalControl.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "pvr/guilib/PVRGUIActionsPowerManagement.h" +#include "pvr/guilib/PVRGUIActionsRecordings.h" +#include "pvr/guilib/PVRGUIActionsTimers.h" +#include "pvr/guilib/PVRGUIActionsUtils.h" + +#include <memory> + +using namespace PVR; + +CPVRComponentRegistration::CPVRComponentRegistration() +{ + RegisterComponent(std::make_shared<CPVRGUIActionsChannels>()); + RegisterComponent(std::make_shared<CPVRGUIActionsClients>()); + RegisterComponent(std::make_shared<CPVRGUIActionsDatabase>()); + RegisterComponent(std::make_shared<CPVRGUIActionsEPG>()); + RegisterComponent(std::make_shared<CPVRGUIActionsParentalControl>()); + RegisterComponent(std::make_shared<CPVRGUIActionsPlayback>()); + RegisterComponent(std::make_shared<CPVRGUIActionsPowerManagement>()); + RegisterComponent(std::make_shared<CPVRGUIActionsRecordings>()); + RegisterComponent(std::make_shared<CPVRGUIActionsTimers>()); + RegisterComponent(std::make_shared<CPVRGUIActionsUtils>()); +} + +CPVRComponentRegistration::~CPVRComponentRegistration() +{ + DeregisterComponent(typeid(CPVRGUIActionsUtils)); + DeregisterComponent(typeid(CPVRGUIActionsTimers)); + DeregisterComponent(typeid(CPVRGUIActionsRecordings)); + DeregisterComponent(typeid(CPVRGUIActionsPowerManagement)); + DeregisterComponent(typeid(CPVRGUIActionsPlayback)); + DeregisterComponent(typeid(CPVRGUIActionsParentalControl)); + DeregisterComponent(typeid(CPVRGUIActionsEPG)); + DeregisterComponent(typeid(CPVRGUIActionsDatabase)); + DeregisterComponent(typeid(CPVRGUIActionsClients)); + DeregisterComponent(typeid(CPVRGUIActionsChannels)); +} diff --git a/xbmc/pvr/PVRComponentRegistration.h b/xbmc/pvr/PVRComponentRegistration.h new file mode 100644 index 0000000..d81d449 --- /dev/null +++ b/xbmc/pvr/PVRComponentRegistration.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" +#include "utils/ComponentContainer.h" + +namespace PVR +{ +class CPVRComponentRegistration : public CComponentContainer<IPVRComponent> +{ +public: + CPVRComponentRegistration(); + virtual ~CPVRComponentRegistration(); +}; +} // namespace PVR diff --git a/xbmc/pvr/PVRContextMenus.cpp b/xbmc/pvr/PVRContextMenus.cpp new file mode 100644 index 0000000..724872d --- /dev/null +++ b/xbmc/pvr/PVRContextMenus.cpp @@ -0,0 +1,780 @@ +/* + * Copyright (C) 2016-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 "PVRContextMenus.h" + +#include "ContextMenuItem.h" +#include "FileItem.h" +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClientMenuHooks.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/guilib/PVRGUIActionsEPG.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "pvr/guilib/PVRGUIActionsRecordings.h" +#include "pvr/guilib/PVRGUIActionsTimers.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/recordings/PVRRecordingsPath.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "pvr/timers/PVRTimersPath.h" +#include "utils/URIUtils.h" + +#include <memory> +#include <string> + +namespace PVR +{ +namespace CONTEXTMENUITEM +{ +#define DECL_STATICCONTEXTMENUITEM(clazz) \ + class clazz : public CStaticContextMenuAction \ + { \ + public: \ + explicit clazz(uint32_t label) : CStaticContextMenuAction(label) {} \ + bool IsVisible(const CFileItem& item) const override; \ + bool Execute(const CFileItemPtr& item) const override; \ + }; + +#define DECL_CONTEXTMENUITEM(clazz) \ + class clazz : public IContextMenuItem \ + { \ + public: \ + std::string GetLabel(const CFileItem& item) const override; \ + bool IsVisible(const CFileItem& item) const override; \ + bool Execute(const CFileItemPtr& item) const override; \ + }; + +DECL_STATICCONTEXTMENUITEM(PlayEpgTag); +DECL_STATICCONTEXTMENUITEM(PlayRecording); +DECL_CONTEXTMENUITEM(ShowInformation); +DECL_STATICCONTEXTMENUITEM(ShowChannelGuide); +DECL_STATICCONTEXTMENUITEM(FindSimilar); +DECL_STATICCONTEXTMENUITEM(StartRecording); +DECL_STATICCONTEXTMENUITEM(StopRecording); +DECL_STATICCONTEXTMENUITEM(AddTimerRule); +DECL_CONTEXTMENUITEM(EditTimerRule); +DECL_STATICCONTEXTMENUITEM(DeleteTimerRule); +DECL_CONTEXTMENUITEM(EditTimer); +DECL_CONTEXTMENUITEM(DeleteTimer); +DECL_STATICCONTEXTMENUITEM(EditRecording); +DECL_CONTEXTMENUITEM(DeleteRecording); +DECL_STATICCONTEXTMENUITEM(UndeleteRecording); +DECL_STATICCONTEXTMENUITEM(DeleteWatchedRecordings); +DECL_CONTEXTMENUITEM(ToggleTimerState); +DECL_STATICCONTEXTMENUITEM(AddReminder); +DECL_STATICCONTEXTMENUITEM(ExecuteSearch); +DECL_STATICCONTEXTMENUITEM(EditSearch); +DECL_STATICCONTEXTMENUITEM(RenameSearch); +DECL_STATICCONTEXTMENUITEM(DeleteSearch); + +class PVRClientMenuHook : public IContextMenuItem +{ +public: + explicit PVRClientMenuHook(const CPVRClientMenuHook& hook) : m_hook(hook) {} + + std::string GetLabel(const CFileItem& item) const override; + bool IsVisible(const CFileItem& item) const override; + bool Execute(const CFileItemPtr& item) const override; + + const CPVRClientMenuHook& GetHook() const { return m_hook; } + +private: + const CPVRClientMenuHook m_hook; +}; + +std::shared_ptr<CPVRTimerInfoTag> GetTimerInfoTagFromItem(const CFileItem& item) +{ + std::shared_ptr<CPVRTimerInfoTag> timer; + + const std::shared_ptr<CPVREpgInfoTag> epg(item.GetEPGInfoTag()); + if (epg) + timer = CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg); + + if (!timer) + timer = item.GetPVRTimerInfoTag(); + + return timer; +} + +/////////////////////////////////////////////////////////////////////////////// +// Play epg tag + +bool PlayEpgTag::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVREpgInfoTag> epg(item.GetEPGInfoTag()); + if (epg) + return epg->IsPlayable(); + + return false; +} + +bool PlayEpgTag::Execute(const CFileItemPtr& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayEpgTag(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Play recording + +bool PlayRecording::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRRecording> recording = + CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(item.GetEPGInfoTag()); + if (recording) + return !recording->IsDeleted(); + + return false; +} + +bool PlayRecording::Execute(const CFileItemPtr& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording( + *item, true /* bCheckResume */); +} + +/////////////////////////////////////////////////////////////////////////////// +// Show information (epg, recording) + +std::string ShowInformation::GetLabel(const CFileItem& item) const +{ + if (item.GetPVRRecordingInfoTag()) + return g_localizeStrings.Get(19053); /* Recording Information */ + + return g_localizeStrings.Get(19047); /* Programme information */ +} + +bool ShowInformation::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRChannel> channel(item.GetPVRChannelInfoTag()); + if (channel) + return channel->GetEPGNow().get() != nullptr; + + if (item.HasEPGInfoTag()) + return !item.GetEPGInfoTag()->IsGapTag(); + + const std::shared_ptr<CPVRTimerInfoTag> timer(item.GetPVRTimerInfoTag()); + if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER)) + return timer->GetEpgInfoTag().get() != nullptr; + + if (item.GetPVRRecordingInfoTag()) + return true; + + return false; +} + +bool ShowInformation::Execute(const CFileItemPtr& item) const +{ + if (item->GetPVRRecordingInfoTag()) + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo(*item); + + return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Show channel guide + +bool ShowChannelGuide::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRChannel> channel(item.GetPVRChannelInfoTag()); + if (channel) + return channel->GetEPGNow().get() != nullptr; + + return false; +} + +bool ShowChannelGuide::Execute(const CFileItemPtr& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowChannelEPG(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Find similar + +bool FindSimilar::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRChannel> channel(item.GetPVRChannelInfoTag()); + if (channel) + return channel->GetEPGNow().get() != nullptr; + + if (item.HasEPGInfoTag()) + return !item.GetEPGInfoTag()->IsGapTag(); + + const std::shared_ptr<CPVRTimerInfoTag> timer(item.GetPVRTimerInfoTag()); + if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER)) + return timer->GetEpgInfoTag().get() != nullptr; + + const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag()); + if (recording) + return !recording->IsDeleted(); + + return false; +} + +bool FindSimilar::Execute(const CFileItemPtr& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().FindSimilar(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Start recording + +bool StartRecording::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(item); + + std::shared_ptr<CPVRChannel> channel = item.GetPVRChannelInfoTag(); + if (channel) + return client && client->GetClientCapabilities().SupportsTimers() && + !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel); + + const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag(); + if (epg && epg->IsRecordable()) + { + if (epg->IsGapTag()) + { + channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(epg); + if (channel) + { + return client && client->GetClientCapabilities().SupportsTimers() && + !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel); + } + } + else + { + return client && client->GetClientCapabilities().SupportsTimers() && + !CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg); + } + } + return false; +} + +bool StartRecording::Execute(const CFileItemPtr& item) const +{ + const std::shared_ptr<CPVREpgInfoTag> epgTag = item->GetEPGInfoTag(); + if (!epgTag || epgTag->IsActive()) + { + // instant recording + std::shared_ptr<CPVRChannel> channel; + if (epgTag) + channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(epgTag); + + if (!channel) + channel = item->GetPVRChannelInfoTag(); + + if (channel) + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().SetRecordingOnChannel(channel, + true); + } + + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimer(*item, false); +} + +/////////////////////////////////////////////////////////////////////////////// +// Stop recording + +bool StopRecording::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag()); + if (recording && recording->IsInProgress()) + return true; + + std::shared_ptr<CPVRChannel> channel = item.GetPVRChannelInfoTag(); + if (channel) + return CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel); + + const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item)); + if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER)) + return timer->IsRecording(); + + const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag(); + if (epg && epg->IsGapTag()) + { + channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(epg); + if (channel) + return CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel); + } + + return false; +} + +bool StopRecording::Execute(const CFileItemPtr& item) const +{ + const std::shared_ptr<CPVREpgInfoTag> epgTag = item->GetEPGInfoTag(); + if (epgTag && epgTag->IsGapTag()) + { + // instance recording + const std::shared_ptr<CPVRChannel> channel = + CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(epgTag); + if (channel) + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().SetRecordingOnChannel(channel, + false); + } + + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().StopRecording(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Edit recording + +bool EditRecording::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag()); + if (recording && !recording->IsDeleted() && !recording->IsInProgress()) + { + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().CanEditRecording(item); + } + return false; +} + +bool EditRecording::Execute(const CFileItemPtr& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().EditRecording(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Delete recording + +std::string DeleteRecording::GetLabel(const CFileItem& item) const +{ + const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag()); + if (recording && recording->IsDeleted()) + return g_localizeStrings.Get(19291); /* Delete permanently */ + + return g_localizeStrings.Get(117); /* Delete */ +} + +bool DeleteRecording::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(item); + if (client && !client->GetClientCapabilities().SupportsRecordingsDelete()) + return false; + + const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag()); + if (recording && !recording->IsInProgress()) + return true; + + // recordings folder? + if (item.m_bIsFolder && + CServiceBroker::GetPVRManager().Clients()->AnyClientSupportingRecordingsDelete()) + { + const CPVRRecordingsPath path(item.GetPath()); + return path.IsValid() && !path.IsRecordingsRoot(); + } + + return false; +} + +bool DeleteRecording::Execute(const CFileItemPtr& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().DeleteRecording(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Undelete recording + +bool UndeleteRecording::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag()); + if (recording && recording->IsDeleted()) + return true; + + return false; +} + +bool UndeleteRecording::Execute(const CFileItemPtr& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().UndeleteRecording(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Delete watched recordings + +bool DeleteWatchedRecordings::IsVisible(const CFileItem& item) const +{ + // recordings folder? + if (item.m_bIsFolder && !item.IsParentFolder()) + return CPVRRecordingsPath(item.GetPath()).IsValid(); + + return false; +} + +bool DeleteWatchedRecordings::Execute(const std::shared_ptr<CFileItem>& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().DeleteWatchedRecordings(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Add reminder + +bool AddReminder::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag(); + if (epg && !CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg) && + epg->StartAsLocalTime() > CDateTime::GetCurrentDateTime()) + return true; + + return false; +} + +bool AddReminder::Execute(const std::shared_ptr<CFileItem>& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddReminder(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Activate / deactivate timer or timer rule + +std::string ToggleTimerState::GetLabel(const CFileItem& item) const +{ + const std::shared_ptr<CPVRTimerInfoTag> timer(item.GetPVRTimerInfoTag()); + if (timer && !timer->IsDisabled()) + return g_localizeStrings.Get(844); /* Deactivate */ + + return g_localizeStrings.Get(843); /* Activate */ +} + +bool ToggleTimerState::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRTimerInfoTag> timer(item.GetPVRTimerInfoTag()); + if (!timer || URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER) || + timer->IsBroken()) + return false; + + return timer->GetTimerType()->SupportsEnableDisable(); +} + +bool ToggleTimerState::Execute(const CFileItemPtr& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleTimerState(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Add timer rule + +bool AddTimerRule::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag(); + return (epg && !epg->IsGapTag() && + !CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg)); +} + +bool AddTimerRule::Execute(const CFileItemPtr& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimerRule(*item, true, true); +} + +/////////////////////////////////////////////////////////////////////////////// +// Edit timer rule + +std::string EditTimerRule::GetLabel(const CFileItem& item) const +{ + const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item)); + if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER)) + { + const std::shared_ptr<CPVRTimerInfoTag> parentTimer( + CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer)); + if (parentTimer) + { + if (!parentTimer->GetTimerType()->IsReadOnly()) + return g_localizeStrings.Get(19243); /* Edit timer rule */ + } + } + + return g_localizeStrings.Get(19304); /* View timer rule */ +} + +bool EditTimerRule::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item)); + if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER)) + return timer->HasParent(); + + return false; +} + +bool EditTimerRule::Execute(const CFileItemPtr& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().EditTimerRule(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Delete timer rule + +bool DeleteTimerRule::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item)); + if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER)) + { + const std::shared_ptr<CPVRTimerInfoTag> parentTimer( + CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer)); + if (parentTimer) + return parentTimer->GetTimerType()->AllowsDelete(); + } + + return false; +} + +bool DeleteTimerRule::Execute(const CFileItemPtr& item) const +{ + auto& timers = CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>(); + const std::shared_ptr<CFileItem> parentTimer = timers.GetTimerRule(*item); + if (parentTimer) + return timers.DeleteTimerRule(*parentTimer); + + return false; +} + +/////////////////////////////////////////////////////////////////////////////// +// Edit / View timer + +std::string EditTimer::GetLabel(const CFileItem& item) const +{ + const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item)); + if (timer) + { + const std::shared_ptr<CPVRTimerType> timerType = timer->GetTimerType(); + if (item.GetEPGInfoTag()) + { + if (timerType->IsReminder()) + return g_localizeStrings.Get(timerType->IsReadOnly() ? 829 /* View reminder */ + : 830); /* Edit reminder */ + else + return g_localizeStrings.Get(timerType->IsReadOnly() ? 19241 /* View timer */ + : 19242); /* Edit timer */ + } + else + return g_localizeStrings.Get(timerType->IsReadOnly() ? 21483 : 21450); /* View/Edit */ + } + return g_localizeStrings.Get(19241); /* View timer */ +} + +bool EditTimer::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item)); + return timer && (!item.GetEPGInfoTag() || + !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER)); +} + +bool EditTimer::Execute(const CFileItemPtr& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().EditTimer(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Delete timer + +std::string DeleteTimer::GetLabel(const CFileItem& item) const +{ + if (item.GetPVRTimerInfoTag()) + return g_localizeStrings.Get(117); /* Delete */ + + const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag(); + if (epg) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = + CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg); + if (timer && timer->IsReminder()) + return g_localizeStrings.Get(827); /* Delete reminder */ + } + return g_localizeStrings.Get(19060); /* Delete timer */ +} + +bool DeleteTimer::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item)); + if (timer && + (!item.GetEPGInfoTag() || + !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER)) && + !timer->IsRecording()) + return timer->GetTimerType()->AllowsDelete(); + + return false; +} + +bool DeleteTimer::Execute(const CFileItemPtr& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().DeleteTimer(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// PVR Client menu hook + +std::string PVRClientMenuHook::GetLabel(const CFileItem& item) const +{ + return m_hook.GetLabel(); +} + +bool PVRClientMenuHook::IsVisible(const CFileItem& item) const +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(item); + if (!client || m_hook.GetAddonId() != client->ID()) + return false; + + if (m_hook.IsAllHook()) + return !item.m_bIsFolder && + !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER); + else if (m_hook.IsEpgHook()) + return item.IsEPG(); + else if (m_hook.IsChannelHook()) + return item.IsPVRChannel(); + else if (m_hook.IsDeletedRecordingHook()) + return item.IsDeletedPVRRecording(); + else if (m_hook.IsRecordingHook()) + return item.IsUsablePVRRecording(); + else if (m_hook.IsTimerHook()) + return item.IsPVRTimer(); + else + return false; +} + +bool PVRClientMenuHook::Execute(const CFileItemPtr& item) const +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item); + if (!client) + return false; + + if (item->IsEPG()) + return client->CallEpgTagMenuHook(m_hook, item->GetEPGInfoTag()) == PVR_ERROR_NO_ERROR; + else if (item->IsPVRChannel()) + return client->CallChannelMenuHook(m_hook, item->GetPVRChannelInfoTag()) == PVR_ERROR_NO_ERROR; + else if (item->IsDeletedPVRRecording()) + return client->CallRecordingMenuHook(m_hook, item->GetPVRRecordingInfoTag(), true) == + PVR_ERROR_NO_ERROR; + else if (item->IsUsablePVRRecording()) + return client->CallRecordingMenuHook(m_hook, item->GetPVRRecordingInfoTag(), false) == + PVR_ERROR_NO_ERROR; + else if (item->IsPVRTimer()) + return client->CallTimerMenuHook(m_hook, item->GetPVRTimerInfoTag()) == PVR_ERROR_NO_ERROR; + else + return false; +} + +/////////////////////////////////////////////////////////////////////////////// +// Execute saved search + +bool ExecuteSearch::IsVisible(const CFileItem& item) const +{ + return item.HasEPGSearchFilter(); +} + +bool ExecuteSearch::Execute(const std::shared_ptr<CFileItem>& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ExecuteSavedSearch(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Edit saved search + +bool EditSearch::IsVisible(const CFileItem& item) const +{ + return item.HasEPGSearchFilter(); +} + +bool EditSearch::Execute(const std::shared_ptr<CFileItem>& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().EditSavedSearch(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Rename saved search + +bool RenameSearch::IsVisible(const CFileItem& item) const +{ + return item.HasEPGSearchFilter(); +} + +bool RenameSearch::Execute(const std::shared_ptr<CFileItem>& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().RenameSavedSearch(*item); +} + +/////////////////////////////////////////////////////////////////////////////// +// Delete saved search + +bool DeleteSearch::IsVisible(const CFileItem& item) const +{ + return item.HasEPGSearchFilter(); +} + +bool DeleteSearch::Execute(const std::shared_ptr<CFileItem>& item) const +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().DeleteSavedSearch(*item); +} + +} // namespace CONTEXTMENUITEM + +CPVRContextMenuManager& CPVRContextMenuManager::GetInstance() +{ + static CPVRContextMenuManager instance; + return instance; +} + +CPVRContextMenuManager::CPVRContextMenuManager() + : m_items({ + std::make_shared<CONTEXTMENUITEM::PlayEpgTag>(19190), /* Play programme */ + std::make_shared<CONTEXTMENUITEM::PlayRecording>(19687), /* Play recording */ + std::make_shared<CONTEXTMENUITEM::ShowInformation>(), + std::make_shared<CONTEXTMENUITEM::ShowChannelGuide>(19686), /* Channel guide */ + std::make_shared<CONTEXTMENUITEM::FindSimilar>(19003), /* Find similar */ + std::make_shared<CONTEXTMENUITEM::ToggleTimerState>(), + std::make_shared<CONTEXTMENUITEM::AddTimerRule>(19061), /* Add timer */ + std::make_shared<CONTEXTMENUITEM::EditTimerRule>(), + std::make_shared<CONTEXTMENUITEM::DeleteTimerRule>(19295), /* Delete timer rule */ + std::make_shared<CONTEXTMENUITEM::EditTimer>(), + std::make_shared<CONTEXTMENUITEM::DeleteTimer>(), + std::make_shared<CONTEXTMENUITEM::StartRecording>(264), /* Record */ + std::make_shared<CONTEXTMENUITEM::StopRecording>(19059), /* Stop recording */ + std::make_shared<CONTEXTMENUITEM::EditRecording>(21450), /* Edit */ + std::make_shared<CONTEXTMENUITEM::DeleteRecording>(), + std::make_shared<CONTEXTMENUITEM::UndeleteRecording>(19290), /* Undelete */ + std::make_shared<CONTEXTMENUITEM::DeleteWatchedRecordings>(19327), /* Delete watched */ + std::make_shared<CONTEXTMENUITEM::AddReminder>(826), /* Set reminder */ + std::make_shared<CONTEXTMENUITEM::ExecuteSearch>(137), /* Search */ + std::make_shared<CONTEXTMENUITEM::EditSearch>(21450), /* Edit */ + std::make_shared<CONTEXTMENUITEM::RenameSearch>(118), /* Rename */ + std::make_shared<CONTEXTMENUITEM::DeleteSearch>(117), /* Delete */ + }) +{ +} + +void CPVRContextMenuManager::AddMenuHook(const CPVRClientMenuHook& hook) +{ + if (hook.IsSettingsHook()) + return; // settings hooks are not handled using context menus + + const auto item = std::make_shared<CONTEXTMENUITEM::PVRClientMenuHook>(hook); + m_items.emplace_back(item); + m_events.Publish(PVRContextMenuEvent(PVRContextMenuEventAction::ADD_ITEM, item)); +} + +void CPVRContextMenuManager::RemoveMenuHook(const CPVRClientMenuHook& hook) +{ + if (hook.IsSettingsHook()) + return; // settings hooks are not handled using context menus + + for (auto it = m_items.begin(); it < m_items.end(); ++it) + { + const CONTEXTMENUITEM::PVRClientMenuHook* cmh = + dynamic_cast<const CONTEXTMENUITEM::PVRClientMenuHook*>((*it).get()); + if (cmh && cmh->GetHook() == hook) + { + m_events.Publish(PVRContextMenuEvent(PVRContextMenuEventAction::REMOVE_ITEM, *it)); + m_items.erase(it); + return; + } + } +} + +} // namespace PVR diff --git a/xbmc/pvr/PVRContextMenus.h b/xbmc/pvr/PVRContextMenus.h new file mode 100644 index 0000000..357db17 --- /dev/null +++ b/xbmc/pvr/PVRContextMenus.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016-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 "utils/EventStream.h" + +#include <memory> +#include <vector> + +class IContextMenuItem; + +namespace PVR +{ +enum class PVRContextMenuEventAction +{ + ADD_ITEM, + REMOVE_ITEM +}; + +struct PVRContextMenuEvent +{ + PVRContextMenuEvent(const PVRContextMenuEventAction& a, + const std::shared_ptr<IContextMenuItem>& i) + : action(a), item(i) + { + } + + PVRContextMenuEventAction action; + std::shared_ptr<IContextMenuItem> item; +}; + +class CPVRClientMenuHook; + +class CPVRContextMenuManager +{ +public: + static CPVRContextMenuManager& GetInstance(); + + std::vector<std::shared_ptr<IContextMenuItem>> GetMenuItems() const { return m_items; } + + void AddMenuHook(const CPVRClientMenuHook& hook); + void RemoveMenuHook(const CPVRClientMenuHook& hook); + + /*! + * @brief Query the events available for CEventStream + */ + CEventStream<PVRContextMenuEvent>& Events() { return m_events; } + +private: + CPVRContextMenuManager(); + CPVRContextMenuManager(const CPVRContextMenuManager&) = delete; + CPVRContextMenuManager const& operator=(CPVRContextMenuManager const&) = delete; + virtual ~CPVRContextMenuManager() = default; + + std::vector<std::shared_ptr<IContextMenuItem>> m_items; + CEventSource<PVRContextMenuEvent> m_events; +}; + +} // namespace PVR diff --git a/xbmc/pvr/PVRDatabase.cpp b/xbmc/pvr/PVRDatabase.cpp new file mode 100644 index 0000000..9475c5a --- /dev/null +++ b/xbmc/pvr/PVRDatabase.cpp @@ -0,0 +1,1141 @@ +/* + * 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 "PVRDatabase.h" + +#include "ServiceBroker.h" +#include "dbwrappers/dataset.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/providers/PVRProvider.h" +#include "pvr/providers/PVRProviders.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimerType.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <cstdlib> +#include <map> +#include <memory> +#include <mutex> +#include <string> +#include <utility> +#include <vector> + +using namespace dbiplus; +using namespace PVR; + +namespace +{ +// clang-format off + + static const std::string sqlCreateTimersTable = + "CREATE TABLE timers (" + "iClientIndex integer primary key, " + "iParentClientIndex integer, " + "iClientId integer, " + "iTimerType integer, " + "iState integer, " + "sTitle varchar(255), " + "iClientChannelUid integer, " + "sSeriesLink varchar(255), " + "sStartTime varchar(20), " + "bStartAnyTime bool, " + "sEndTime varchar(20), " + "bEndAnyTime bool, " + "sFirstDay varchar(20), " + "iWeekdays integer, " + "iEpgUid integer, " + "iMarginStart integer, " + "iMarginEnd integer, " + "sEpgSearchString varchar(255), " + "bFullTextEpgSearch bool, " + "iPreventDuplicates integer," + "iPrority integer," + "iLifetime integer," + "iMaxRecordings integer," + "iRecordingGroup integer" + ")"; + + static const std::string sqlCreateChannelGroupsTable = + "CREATE TABLE channelgroups (" + "idGroup integer primary key," + "bIsRadio bool, " + "iGroupType integer, " + "sName varchar(64), " + "iLastWatched integer, " + "bIsHidden bool, " + "iPosition integer, " + "iLastOpened bigint unsigned" + ")"; + + static const std::string sqlCreateProvidersTable = + "CREATE TABLE providers (" + "idProvider integer primary key, " + "iUniqueId integer, " + "iClientId integer, " + "sName varchar(64), " + "iType integer, " + "sIconPath varchar(255), " + "sCountries varchar(64), " + "sLanguages varchar(64) " + ")"; + + // clang-format on + + std::string GetClientIdsSQL(const std::vector<std::shared_ptr<CPVRClient>>& clients) + { + if (clients.empty()) + return {}; + + std::string clientIds = "("; + for (auto it = clients.cbegin(); it != clients.cend(); ++it) + { + if (it != clients.cbegin()) + clientIds += " OR "; + + clientIds += "iClientId = "; + clientIds += std::to_string((*it)->GetID()); + } + clientIds += ")"; + return clientIds; + } + +} // unnamed namespace + +bool CPVRDatabase::Open() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return CDatabase::Open(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseTV); +} + +void CPVRDatabase::Close() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + CDatabase::Close(); +} + +void CPVRDatabase::Lock() +{ + m_critSection.lock(); +} + +void CPVRDatabase::Unlock() +{ + m_critSection.unlock(); +} + +void CPVRDatabase::CreateTables() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + CLog::LogF(LOGINFO, "Creating PVR database tables"); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'channels'"); + m_pDS->exec("CREATE TABLE channels (" + "idChannel integer primary key, " + "iUniqueId integer, " + "bIsRadio bool, " + "bIsHidden bool, " + "bIsUserSetIcon bool, " + "bIsUserSetName bool, " + "bIsLocked bool, " + "sIconPath varchar(255), " + "sChannelName varchar(64), " + "bIsVirtual bool, " + "bEPGEnabled bool, " + "sEPGScraper varchar(32), " + "iLastWatched integer, " + "iClientId integer, " //! @todo use mapping table + "idEpg integer, " + "bHasArchive bool, " + "iClientProviderUid integer, " + "bIsUserSetHidden bool" + ")"); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'channelgroups'"); + m_pDS->exec(sqlCreateChannelGroupsTable); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'map_channelgroups_channels'"); + m_pDS->exec( + "CREATE TABLE map_channelgroups_channels (" + "idChannel integer, " + "idGroup integer, " + "iChannelNumber integer, " + "iSubChannelNumber integer, " + "iOrder integer, " + "iClientChannelNumber integer, " + "iClientSubChannelNumber integer" + ")" + ); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'clients'"); + m_pDS->exec( + "CREATE TABLE clients (" + "idClient integer primary key, " + "iPriority integer" + ")" + ); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'timers'"); + m_pDS->exec(sqlCreateTimersTable); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'providers'"); + m_pDS->exec(sqlCreateProvidersTable); +} + +void CPVRDatabase::CreateAnalytics() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + CLog::LogF(LOGINFO, "Creating PVR database indices"); + m_pDS->exec("CREATE INDEX idx_clients_idClient on clients(idClient);"); + m_pDS->exec("CREATE UNIQUE INDEX idx_channels_iClientId_iUniqueId on channels(iClientId, iUniqueId);"); + m_pDS->exec("CREATE INDEX idx_channelgroups_bIsRadio on channelgroups(bIsRadio);"); + m_pDS->exec("CREATE UNIQUE INDEX idx_idGroup_idChannel on map_channelgroups_channels(idGroup, idChannel);"); + m_pDS->exec("CREATE INDEX idx_timers_iClientIndex on timers(iClientIndex);"); +} + +void CPVRDatabase::UpdateTables(int iVersion) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (iVersion < 13) + m_pDS->exec("ALTER TABLE channels ADD idEpg integer;"); + + if (iVersion < 20) + m_pDS->exec("ALTER TABLE channels ADD bIsUserSetIcon bool"); + + if (iVersion < 21) + m_pDS->exec("ALTER TABLE channelgroups ADD iGroupType integer"); + + if (iVersion < 22) + m_pDS->exec("ALTER TABLE channels ADD bIsLocked bool"); + + if (iVersion < 23) + m_pDS->exec("ALTER TABLE channelgroups ADD iLastWatched integer"); + + if (iVersion < 24) + m_pDS->exec("ALTER TABLE channels ADD bIsUserSetName bool"); + + if (iVersion < 25) + m_pDS->exec("DROP TABLE IF EXISTS channelsettings"); + + if (iVersion < 26) + { + m_pDS->exec("ALTER TABLE channels ADD iClientSubChannelNumber integer"); + m_pDS->exec("UPDATE channels SET iClientSubChannelNumber = 0"); + m_pDS->exec("ALTER TABLE map_channelgroups_channels ADD iSubChannelNumber integer"); + m_pDS->exec("UPDATE map_channelgroups_channels SET iSubChannelNumber = 0"); + } + + if (iVersion < 27) + m_pDS->exec("ALTER TABLE channelgroups ADD bIsHidden bool"); + + if (iVersion < 28) + m_pDS->exec("DROP TABLE clients"); + + if (iVersion < 29) + m_pDS->exec("ALTER TABLE channelgroups ADD iPosition integer"); + + if (iVersion < 32) + m_pDS->exec("CREATE TABLE clients (idClient integer primary key, iPriority integer)"); + + if (iVersion < 33) + m_pDS->exec(sqlCreateTimersTable); + + if (iVersion < 34) + m_pDS->exec("ALTER TABLE channels ADD bHasArchive bool"); + + if (iVersion < 35) + { + m_pDS->exec("ALTER TABLE map_channelgroups_channels ADD iOrder integer"); + m_pDS->exec("UPDATE map_channelgroups_channels SET iOrder = 0"); + } + + if (iVersion < 36) + { + m_pDS->exec("ALTER TABLE map_channelgroups_channels ADD iClientChannelNumber integer"); + m_pDS->exec("UPDATE map_channelgroups_channels SET iClientChannelNumber = 0"); + m_pDS->exec("ALTER TABLE map_channelgroups_channels ADD iClientSubChannelNumber integer"); + m_pDS->exec("UPDATE map_channelgroups_channels SET iClientSubChannelNumber = 0"); + } + + if (iVersion < 37) + m_pDS->exec("ALTER TABLE channelgroups ADD iLastOpened integer"); + + if (iVersion < 38) + { + m_pDS->exec("ALTER TABLE channelgroups " + "RENAME TO channelgroups_old"); + + m_pDS->exec(sqlCreateChannelGroupsTable); + + m_pDS->exec( + "INSERT INTO channelgroups (bIsRadio, iGroupType, sName, iLastWatched, bIsHidden, " + "iPosition, iLastOpened) " + "SELECT bIsRadio, iGroupType, sName, iLastWatched, bIsHidden, iPosition, iLastOpened " + "FROM channelgroups_old"); + + m_pDS->exec("DROP TABLE channelgroups_old"); + } + + if (iVersion < 39) + { + m_pDS->exec(sqlCreateProvidersTable); + m_pDS->exec("CREATE UNIQUE INDEX idx_iUniqueId_iClientId on providers(iUniqueId, iClientId);"); + m_pDS->exec("ALTER TABLE channels ADD iClientProviderUid integer"); + m_pDS->exec("UPDATE channels SET iClientProviderUid = -1"); + } + + if (iVersion < 40) + { + m_pDS->exec("ALTER TABLE channels ADD bIsUserSetHidden bool"); + m_pDS->exec("UPDATE channels SET bIsUserSetHidden = bIsHidden"); + } +} + +/********** Client methods **********/ + +bool CPVRDatabase::DeleteClients() +{ + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all clients from the database"); + + std::unique_lock<CCriticalSection> lock(m_critSection); + return DeleteValues("clients"); +} + +bool CPVRDatabase::Persist(const CPVRClient& client) +{ + if (client.GetID() == PVR_INVALID_CLIENT_ID) + return false; + + CLog::LogFC(LOGDEBUG, LOGPVR, "Persisting client '{}' to database", client.ID()); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + const std::string strQuery = PrepareSQL("REPLACE INTO clients (idClient, iPriority) VALUES (%i, %i);", + client.GetID(), client.GetPriority()); + + return ExecuteQuery(strQuery); +} + +bool CPVRDatabase::Delete(const CPVRClient& client) +{ + if (client.GetID() == PVR_INVALID_CLIENT_ID) + return false; + + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting client '{}' from the database", client.ID()); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + Filter filter; + filter.AppendWhere(PrepareSQL("idClient = '%i'", client.GetID())); + + return DeleteValues("clients", filter); +} + +int CPVRDatabase::GetPriority(const CPVRClient& client) +{ + if (client.GetID() == PVR_INVALID_CLIENT_ID) + return 0; + + CLog::LogFC(LOGDEBUG, LOGPVR, "Getting priority for client '{}' from the database", client.ID()); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + const std::string strWhereClause = PrepareSQL("idClient = '%i'", client.GetID()); + const std::string strValue = GetSingleValue("clients", "iPriority", strWhereClause); + + if (strValue.empty()) + return 0; + + return atoi(strValue.c_str()); +} + +/********** Channel provider methods **********/ + +bool CPVRDatabase::DeleteProviders() +{ + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all providers from the database"); + + std::unique_lock<CCriticalSection> lock(m_critSection); + return DeleteValues("providers"); +} + +bool CPVRDatabase::Persist(CPVRProvider& provider, bool updateRecord /* = false */) +{ + bool bReturn = false; + if (provider.GetName().empty()) + { + CLog::LogF(LOGERROR, "Empty provider name"); + return bReturn; + } + + std::string strQuery; + + std::unique_lock<CCriticalSection> lock(m_critSection); + { + /* insert a new entry when this is a new group, or replace the existing one otherwise */ + if (!updateRecord) + strQuery = + PrepareSQL("INSERT INTO providers (idProvider, iUniqueId, iClientId, sName, " + "iType, sIconPath, sCountries, sLanguages) " + "VALUES (%i, %i, %i, '%s', %i, '%s', '%s', '%s');", + provider.GetDatabaseId(), provider.GetUniqueId(), provider.GetClientId(), + provider.GetName().c_str(), static_cast<int>(provider.GetType()), + provider.GetClientIconPath().c_str(), provider.GetCountriesDBString().c_str(), + provider.GetLanguagesDBString().c_str()); + else + strQuery = + PrepareSQL("REPLACE INTO providers (idProvider, iUniqueId, iClientId, sName, " + "iType, sIconPath, sCountries, sLanguages) " + "VALUES (%i, %i, %i, '%s', %i, '%s', '%s', '%s');", + provider.GetDatabaseId(), provider.GetUniqueId(), provider.GetClientId(), + provider.GetName().c_str(), static_cast<int>(provider.GetType()), + provider.GetClientIconPath().c_str(), provider.GetCountriesDBString().c_str(), + provider.GetLanguagesDBString().c_str()); + + bReturn = ExecuteQuery(strQuery); + + /* set the provider id if it was <= 0 */ + if (bReturn && provider.GetDatabaseId() <= 0) + { + provider.SetDatabaseId(static_cast<int>(m_pDS->lastinsertid())); + } + } + + return bReturn; +} + +bool CPVRDatabase::Delete(const CPVRProvider& provider) +{ + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting provider '{}' from the database", + provider.GetName()); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + Filter filter; + filter.AppendWhere(PrepareSQL("idProvider = '%i'", provider.GetDatabaseId())); + + return DeleteValues("providers", filter); +} + +bool CPVRDatabase::Get(CPVRProviders& results, + const std::vector<std::shared_ptr<CPVRClient>>& clients) const +{ + bool bReturn = false; + + std::string strQuery = "SELECT * from providers "; + const std::string clientIds = GetClientIdsSQL(clients); + if (!clientIds.empty()) + strQuery += "WHERE " + clientIds + " OR iType = 1"; // always load addon providers + + std::unique_lock<CCriticalSection> lock(m_critSection); + strQuery = PrepareSQL(strQuery); + if (ResultQuery(strQuery)) + { + try + { + while (!m_pDS->eof()) + { + std::shared_ptr<CPVRProvider> provider = std::make_shared<CPVRProvider>( + m_pDS->fv("iUniqueId").get_asInt(), m_pDS->fv("iClientId").get_asInt()); + + provider->SetDatabaseId(m_pDS->fv("idProvider").get_asInt()); + provider->SetName(m_pDS->fv("sName").get_asString()); + provider->SetType( + static_cast<PVR_PROVIDER_TYPE>(m_pDS->fv("iType").get_asInt())); + provider->SetIconPath(m_pDS->fv("sIconPath").get_asString()); + provider->SetCountriesFromDBString(m_pDS->fv("sCountries").get_asString()); + provider->SetLanguagesFromDBString(m_pDS->fv("sLanguages").get_asString()); + + results.CheckAndAddEntry(provider, ProviderUpdateMode::BY_DATABASE); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Channel Provider '{}' loaded from PVR database", + provider->GetName()); + m_pDS->next(); + } + + m_pDS->close(); + bReturn = true; + } + catch (...) + { + CLog::LogF(LOGERROR, "Couldn't load providers from PVR database"); + } + } + + return bReturn; +} + +int CPVRDatabase::GetMaxProviderId() +{ + std::string strQuery = PrepareSQL("SELECT max(idProvider) as maxProviderId from providers"); + std::unique_lock<CCriticalSection> lock(m_critSection); + + return GetSingleValueInt(strQuery); +} + +/********** Channel methods **********/ + +int CPVRDatabase::Get(bool bRadio, + const std::vector<std::shared_ptr<CPVRClient>>& clients, + std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& results) const +{ + int iReturn = 0; + + std::string strQuery = "SELECT * from channels WHERE bIsRadio = %u "; + const std::string clientIds = GetClientIdsSQL(clients); + if (!clientIds.empty()) + strQuery += "AND " + clientIds; + + std::unique_lock<CCriticalSection> lock(m_critSection); + strQuery = PrepareSQL(strQuery, bRadio); + if (ResultQuery(strQuery)) + { + try + { + while (!m_pDS->eof()) + { + const std::shared_ptr<CPVRChannel> channel(new CPVRChannel( + m_pDS->fv("bIsRadio").get_asBool(), m_pDS->fv("sIconPath").get_asString())); + + channel->m_iChannelId = m_pDS->fv("idChannel").get_asInt(); + channel->m_iUniqueId = m_pDS->fv("iUniqueId").get_asInt(); + channel->m_bIsHidden = m_pDS->fv("bIsHidden").get_asBool(); + channel->m_bIsUserSetIcon = m_pDS->fv("bIsUserSetIcon").get_asBool(); + channel->m_bIsUserSetName = m_pDS->fv("bIsUserSetName").get_asBool(); + channel->m_bIsLocked = m_pDS->fv("bIsLocked").get_asBool(); + channel->m_strChannelName = m_pDS->fv("sChannelName").get_asString(); + channel->m_bEPGEnabled = m_pDS->fv("bEPGEnabled").get_asBool(); + channel->m_strEPGScraper = m_pDS->fv("sEPGScraper").get_asString(); + channel->m_iLastWatched = static_cast<time_t>(m_pDS->fv("iLastWatched").get_asInt()); + channel->m_iClientId = m_pDS->fv("iClientId").get_asInt(); + channel->m_iEpgId = m_pDS->fv("idEpg").get_asInt(); + channel->m_bHasArchive = m_pDS->fv("bHasArchive").get_asBool(); + channel->m_iClientProviderUid = m_pDS->fv("iClientProviderUid").get_asInt(); + channel->m_bIsUserSetHidden = m_pDS->fv("bIsUserSetHidden").get_asBool(); + + channel->UpdateEncryptionName(); + + results.insert({channel->StorageId(), channel}); + + m_pDS->next(); + ++iReturn; + } + m_pDS->close(); + } + catch (...) + { + CLog::LogF(LOGERROR, "Couldn't load channels from PVR database"); + } + } + else + { + CLog::LogF(LOGERROR, "PVR database query failed"); + } + + m_pDS->close(); + return iReturn; +} + +bool CPVRDatabase::DeleteChannels() +{ + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all channels from the database"); + + std::unique_lock<CCriticalSection> lock(m_critSection); + return DeleteValues("channels"); +} + +bool CPVRDatabase::QueueDeleteQuery(const CPVRChannel& channel) +{ + /* invalid channel */ + if (channel.ChannelID() <= 0) + { + CLog::LogF(LOGERROR, "Invalid channel id: {}", channel.ChannelID()); + return false; + } + + CLog::LogFC(LOGDEBUG, LOGPVR, "Queueing delete for channel '{}' from the database", + channel.ChannelName()); + + Filter filter; + filter.AppendWhere(PrepareSQL("idChannel = %i", channel.ChannelID())); + + std::string strQuery; + if (BuildSQL(PrepareSQL("DELETE FROM %s ", "channels"), filter, strQuery)) + return CDatabase::QueueDeleteQuery(strQuery); + + return false; +} + +/********** Channel group member methods **********/ + +bool CPVRDatabase::QueueDeleteQuery(const CPVRChannelGroupMember& groupMember) +{ + CLog::LogFC(LOGDEBUG, LOGPVR, "Queueing delete for channel group member '{}' from the database", + groupMember.Channel() ? groupMember.Channel()->ChannelName() + : std::to_string(groupMember.ChannelDatabaseID())); + + Filter filter; + filter.AppendWhere(PrepareSQL("idGroup = %i", groupMember.GroupID())); + filter.AppendWhere(PrepareSQL("idChannel = %i", groupMember.ChannelDatabaseID())); + + std::string strQuery; + if (BuildSQL(PrepareSQL("DELETE FROM %s ", "map_channelgroups_channels"), filter, strQuery)) + return CDatabase::QueueDeleteQuery(strQuery); + + return false; +} + +/********** Channel group methods **********/ + +bool CPVRDatabase::RemoveChannelsFromGroup(const CPVRChannelGroup& group) +{ + Filter filter; + filter.AppendWhere(PrepareSQL("idGroup = %i", group.GroupID())); + + std::unique_lock<CCriticalSection> lock(m_critSection); + return DeleteValues("map_channelgroups_channels", filter); +} + +bool CPVRDatabase::DeleteChannelGroups() +{ + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all channel groups from the database"); + + std::unique_lock<CCriticalSection> lock(m_critSection); + return DeleteValues("channelgroups") && DeleteValues("map_channelgroups_channels"); +} + +bool CPVRDatabase::Delete(const CPVRChannelGroup& group) +{ + /* invalid group id */ + if (group.GroupID() <= 0) + { + CLog::LogF(LOGERROR, "Invalid channel group id: {}", group.GroupID()); + return false; + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + + Filter filter; + filter.AppendWhere(PrepareSQL("idGroup = %i", group.GroupID())); + filter.AppendWhere(PrepareSQL("bIsRadio = %u", group.IsRadio())); + + return RemoveChannelsFromGroup(group) && DeleteValues("channelgroups", filter); +} + +int CPVRDatabase::Get(CPVRChannelGroups& results) const +{ + int iLoaded = 0; + std::unique_lock<CCriticalSection> lock(m_critSection); + + const std::string strQuery = PrepareSQL("SELECT * from channelgroups WHERE bIsRadio = %u", results.IsRadio()); + if (ResultQuery(strQuery)) + { + try + { + while (!m_pDS->eof()) + { + const std::shared_ptr<CPVRChannelGroup> group = + results.CreateChannelGroup(m_pDS->fv("iGroupType").get_asInt(), + CPVRChannelsPath(m_pDS->fv("bIsRadio").get_asBool(), + m_pDS->fv("sName").get_asString())); + + group->m_iGroupId = m_pDS->fv("idGroup").get_asInt(); + group->m_iGroupType = m_pDS->fv("iGroupType").get_asInt(); + group->m_iLastWatched = static_cast<time_t>(m_pDS->fv("iLastWatched").get_asInt()); + group->m_bHidden = m_pDS->fv("bIsHidden").get_asBool(); + group->m_iPosition = m_pDS->fv("iPosition").get_asInt(); + group->m_iLastOpened = static_cast<uint64_t>(m_pDS->fv("iLastOpened").get_asInt64()); + results.Update(group); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Group '{}' loaded from PVR database", group->GroupName()); + m_pDS->next(); + } + m_pDS->close(); + iLoaded++; + } + catch (...) + { + CLog::LogF(LOGERROR, "Couldn't load channel groups from PVR database. Exception."); + } + } + else + { + CLog::LogF(LOGERROR, "Couldn't load channel groups from PVR database. Query failed."); + } + + return iLoaded; +} + +std::vector<std::shared_ptr<CPVRChannelGroupMember>> CPVRDatabase::Get( + const CPVRChannelGroup& group, const std::vector<std::shared_ptr<CPVRClient>>& clients) const +{ + std::vector<std::shared_ptr<CPVRChannelGroupMember>> results; + + /* invalid group id */ + if (group.GroupID() < 0) + { + CLog::LogF(LOGERROR, "Invalid channel group id: {}", group.GroupID()); + return results; + } + + std::string strQuery = + "SELECT map_channelgroups_channels.idChannel, " + "map_channelgroups_channels.iChannelNumber, " + "map_channelgroups_channels.iSubChannelNumber, " + "map_channelgroups_channels.iOrder, " + "map_channelgroups_channels.iClientChannelNumber, " + "map_channelgroups_channels.iClientSubChannelNumber, " + "channels.iClientId, channels.iUniqueId, channels.bIsRadio " + "FROM map_channelgroups_channels " + "LEFT JOIN channels ON channels.idChannel = map_channelgroups_channels.idChannel " + "WHERE map_channelgroups_channels.idGroup = %i "; + const std::string clientIds = GetClientIdsSQL(clients); + if (!clientIds.empty()) + strQuery += "AND " + clientIds; + strQuery += " ORDER BY map_channelgroups_channels.iChannelNumber"; + + std::unique_lock<CCriticalSection> lock(m_critSection); + strQuery = PrepareSQL(strQuery, group.GroupID()); + if (ResultQuery(strQuery)) + { + try + { + while (!m_pDS->eof()) + { + const auto newMember = std::make_shared<CPVRChannelGroupMember>(); + newMember->m_iChannelDatabaseID = m_pDS->fv("idChannel").get_asInt(); + newMember->m_iClientID = m_pDS->fv("iClientId").get_asInt(); + newMember->m_iChannelUID = m_pDS->fv("iUniqueId").get_asInt(); + newMember->m_iGroupID = group.GroupID(); + newMember->m_bIsRadio = m_pDS->fv("bIsRadio").get_asBool(); + newMember->m_channelNumber = { + static_cast<unsigned int>(m_pDS->fv("iChannelNumber").get_asInt()), + static_cast<unsigned int>(m_pDS->fv("iSubChannelNumber").get_asInt())}; + newMember->m_clientChannelNumber = { + static_cast<unsigned int>(m_pDS->fv("iClientChannelNumber").get_asInt()), + static_cast<unsigned int>(m_pDS->fv("iClientSubChannelNumber").get_asInt())}; + newMember->m_iOrder = static_cast<int>(m_pDS->fv("iOrder").get_asInt()); + newMember->SetGroupName(group.GroupName()); + + results.emplace_back(newMember); + m_pDS->next(); + } + m_pDS->close(); + } + catch(...) + { + CLog::LogF(LOGERROR, "Failed to get channel group members"); + } + } + + return results; +} + +bool CPVRDatabase::PersistChannels(const CPVRChannelGroup& group) +{ + /* invalid group id */ + if (group.GroupID() < 0) + { + CLog::LogF(LOGERROR, "Invalid channel group id: {}", group.GroupID()); + return false; + } + + bool bReturn(true); + + std::shared_ptr<CPVRChannel> channel; + for (const auto& groupMember : group.m_members) + { + channel = groupMember.second->Channel(); + if (channel->IsChanged() || channel->IsNew()) + { + if (Persist(*channel, false)) + { + channel->Persisted(); + bReturn = true; + } + } + } + + bReturn &= CommitInsertQueries(); + + if (bReturn) + { + std::string strQuery; + std::string strValue; + for (const auto& groupMember : group.m_members) + { + channel = groupMember.second->Channel(); + strQuery = + PrepareSQL("iUniqueId = %i AND iClientId = %i", channel->UniqueID(), channel->ClientID()); + strValue = GetSingleValue("channels", "idChannel", strQuery); + if (!strValue.empty() && StringUtils::IsInteger(strValue)) + { + const int iChannelID = std::atoi(strValue.c_str()); + channel->SetChannelID(iChannelID); + groupMember.second->m_iChannelDatabaseID = iChannelID; + } + } + } + + return bReturn; +} + +bool CPVRDatabase::PersistGroupMembers(const CPVRChannelGroup& group) +{ + /* invalid group id */ + if (group.GroupID() < 0) + { + CLog::LogF(LOGERROR, "Invalid channel group id: {}", group.GroupID()); + return false; + } + + bool bReturn = true; + + if (group.HasChannels()) + { + for (const auto& groupMember : group.m_sortedMembers) + { + if (groupMember->NeedsSave()) + { + if (groupMember->ChannelDatabaseID() <= 0) + { + CLog::LogF(LOGERROR, "Invalid channel id: {}", groupMember->ChannelDatabaseID()); + continue; + } + + const std::string strWhereClause = + PrepareSQL("idChannel = %i AND idGroup = %i AND iChannelNumber = %u AND " + "iSubChannelNumber = %u AND " + "iOrder = %i AND iClientChannelNumber = %u AND iClientSubChannelNumber = %u", + groupMember->ChannelDatabaseID(), group.GroupID(), + groupMember->ChannelNumber().GetChannelNumber(), + groupMember->ChannelNumber().GetSubChannelNumber(), groupMember->Order(), + groupMember->ClientChannelNumber().GetChannelNumber(), + groupMember->ClientChannelNumber().GetSubChannelNumber()); + + const std::string strValue = + GetSingleValue("map_channelgroups_channels", "idChannel", strWhereClause); + if (strValue.empty()) + { + const std::string strQuery = + PrepareSQL("REPLACE INTO map_channelgroups_channels (" + "idGroup, idChannel, iChannelNumber, iSubChannelNumber, iOrder, " + "iClientChannelNumber, iClientSubChannelNumber) " + "VALUES (%i, %i, %i, %i, %i, %i, %i);", + group.GroupID(), groupMember->ChannelDatabaseID(), + groupMember->ChannelNumber().GetChannelNumber(), + groupMember->ChannelNumber().GetSubChannelNumber(), groupMember->Order(), + groupMember->ClientChannelNumber().GetChannelNumber(), + groupMember->ClientChannelNumber().GetSubChannelNumber()); + QueueInsertQuery(strQuery); + } + } + } + + bReturn = CommitInsertQueries(); + + if (bReturn) + { + for (const auto& groupMember : group.m_sortedMembers) + { + groupMember->SetSaved(); + } + } + } + + return bReturn; +} + +/********** Client methods **********/ + +bool CPVRDatabase::ResetEPG() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const std::string strQuery = PrepareSQL("UPDATE channels SET idEpg = 0"); + return ExecuteQuery(strQuery); +} + +bool CPVRDatabase::Persist(CPVRChannelGroup& group) +{ + bool bReturn(false); + if (group.GroupName().empty()) + { + CLog::LogF(LOGERROR, "Empty group name"); + return bReturn; + } + + std::string strQuery; + + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (group.HasChanges() || group.IsNew()) + { + /* insert a new entry when this is a new group, or replace the existing one otherwise */ + if (group.IsNew()) + strQuery = + PrepareSQL("INSERT INTO channelgroups (bIsRadio, iGroupType, sName, iLastWatched, " + "bIsHidden, iPosition, iLastOpened) VALUES (%i, %i, '%s', %u, %i, %i, %llu)", + (group.IsRadio() ? 1 : 0), group.GroupType(), group.GroupName().c_str(), + static_cast<unsigned int>(group.LastWatched()), group.IsHidden(), + group.GetPosition(), group.LastOpened()); + else + strQuery = PrepareSQL( + "REPLACE INTO channelgroups (idGroup, bIsRadio, iGroupType, sName, iLastWatched, " + "bIsHidden, iPosition, iLastOpened) VALUES (%i, %i, %i, '%s', %u, %i, %i, %llu)", + group.GroupID(), (group.IsRadio() ? 1 : 0), group.GroupType(), group.GroupName().c_str(), + static_cast<unsigned int>(group.LastWatched()), group.IsHidden(), group.GetPosition(), + group.LastOpened()); + + bReturn = ExecuteQuery(strQuery); + + // set the group ID for new groups + if (bReturn && group.IsNew()) + group.SetGroupID(static_cast<int>(m_pDS->lastinsertid())); + } + else + bReturn = true; + + /* only persist the channel data for the internal groups */ + if (group.IsInternalGroup()) + bReturn &= PersistChannels(group); + + /* persist the group member entries */ + if (bReturn) + bReturn = PersistGroupMembers(group); + + return bReturn; +} + +bool CPVRDatabase::Persist(CPVRChannel& channel, bool bCommit) +{ + bool bReturn(false); + + /* invalid channel */ + if (channel.UniqueID() <= 0) + { + CLog::LogF(LOGERROR, "Invalid channel uid: {}", channel.UniqueID()); + return bReturn; + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + + // Note: Do not use channel.ChannelID value to check presence of channel in channels table. It might not yet be set correctly. + std::string strQuery = + PrepareSQL("iUniqueId = %i AND iClientId = %i", channel.UniqueID(), channel.ClientID()); + const std::string strValue = GetSingleValue("channels", "idChannel", strQuery); + if (strValue.empty()) + { + /* new channel */ + strQuery = PrepareSQL( + "INSERT INTO channels (" + "iUniqueId, bIsRadio, bIsHidden, bIsUserSetIcon, bIsUserSetName, bIsLocked, " + "sIconPath, sChannelName, bIsVirtual, bEPGEnabled, sEPGScraper, iLastWatched, iClientId, " + "idEpg, bHasArchive, iClientProviderUid, bIsUserSetHidden) " + "VALUES (%i, %i, %i, %i, %i, %i, '%s', '%s', %i, %i, '%s', %u, %i, %i, %i, %i, %i)", + channel.UniqueID(), (channel.IsRadio() ? 1 : 0), (channel.IsHidden() ? 1 : 0), + (channel.IsUserSetIcon() ? 1 : 0), (channel.IsUserSetName() ? 1 : 0), + (channel.IsLocked() ? 1 : 0), channel.IconPath().c_str(), channel.ChannelName().c_str(), 0, + (channel.EPGEnabled() ? 1 : 0), channel.EPGScraper().c_str(), + static_cast<unsigned int>(channel.LastWatched()), channel.ClientID(), channel.EpgID(), + channel.HasArchive(), channel.ClientProviderUid(), channel.IsUserSetHidden() ? 1 : 0); + } + else + { + /* update channel */ + strQuery = PrepareSQL( + "REPLACE INTO channels (" + "iUniqueId, bIsRadio, bIsHidden, bIsUserSetIcon, bIsUserSetName, bIsLocked, " + "sIconPath, sChannelName, bIsVirtual, bEPGEnabled, sEPGScraper, iLastWatched, iClientId, " + "idChannel, idEpg, bHasArchive, iClientProviderUid, bIsUserSetHidden) " + "VALUES (%i, %i, %i, %i, %i, %i, '%s', '%s', %i, %i, '%s', %u, %i, %s, %i, %i, %i, %i)", + channel.UniqueID(), (channel.IsRadio() ? 1 : 0), (channel.IsHidden() ? 1 : 0), + (channel.IsUserSetIcon() ? 1 : 0), (channel.IsUserSetName() ? 1 : 0), + (channel.IsLocked() ? 1 : 0), channel.ClientIconPath().c_str(), + channel.ChannelName().c_str(), 0, (channel.EPGEnabled() ? 1 : 0), + channel.EPGScraper().c_str(), static_cast<unsigned int>(channel.LastWatched()), + channel.ClientID(), strValue.c_str(), channel.EpgID(), channel.HasArchive(), + channel.ClientProviderUid(), channel.IsUserSetHidden() ? 1 : 0); + } + + if (QueueInsertQuery(strQuery)) + { + bReturn = true; + + if (bCommit) + bReturn = CommitInsertQueries(); + } + + return bReturn; +} + +bool CPVRDatabase::UpdateLastWatched(const CPVRChannel& channel) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const std::string strQuery = + PrepareSQL("UPDATE channels SET iLastWatched = %u WHERE idChannel = %i", + static_cast<unsigned int>(channel.LastWatched()), channel.ChannelID()); + return ExecuteQuery(strQuery); +} + +bool CPVRDatabase::UpdateLastWatched(const CPVRChannelGroup& group) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const std::string strQuery = + PrepareSQL("UPDATE channelgroups SET iLastWatched = %u WHERE idGroup = %i", + static_cast<unsigned int>(group.LastWatched()), group.GroupID()); + return ExecuteQuery(strQuery); +} + +bool CPVRDatabase::UpdateLastOpened(const CPVRChannelGroup& group) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const std::string strQuery = + PrepareSQL("UPDATE channelgroups SET iLastOpened = %llu WHERE idGroup = %i", + group.LastOpened(), group.GroupID()); + return ExecuteQuery(strQuery); +} + +/********** Timer methods **********/ + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRDatabase::GetTimers( + CPVRTimers& timers, const std::vector<std::shared_ptr<CPVRClient>>& clients) const +{ + std::vector<std::shared_ptr<CPVRTimerInfoTag>> result; + + std::string strQuery = "SELECT * FROM timers "; + const std::string clientIds = GetClientIdsSQL(clients); + if (!clientIds.empty()) + strQuery += "WHERE " + clientIds; + + std::unique_lock<CCriticalSection> lock(m_critSection); + strQuery = PrepareSQL(strQuery); + if (ResultQuery(strQuery)) + { + try + { + while (!m_pDS->eof()) + { + std::shared_ptr<CPVRTimerInfoTag> newTag(new CPVRTimerInfoTag()); + + newTag->m_iClientIndex = -m_pDS->fv("iClientIndex").get_asInt(); + newTag->m_iParentClientIndex = m_pDS->fv("iParentClientIndex").get_asInt(); + newTag->m_iClientId = m_pDS->fv("iClientId").get_asInt(); + newTag->SetTimerType(CPVRTimerType::CreateFromIds(m_pDS->fv("iTimerType").get_asInt(), -1)); + newTag->m_state = static_cast<PVR_TIMER_STATE>(m_pDS->fv("iState").get_asInt()); + newTag->m_strTitle = m_pDS->fv("sTitle").get_asString().c_str(); + newTag->m_iClientChannelUid = m_pDS->fv("iClientChannelUid").get_asInt(); + newTag->m_strSeriesLink = m_pDS->fv("sSeriesLink").get_asString().c_str(); + newTag->SetStartFromUTC(CDateTime::FromDBDateTime(m_pDS->fv("sStartTime").get_asString().c_str())); + newTag->m_bStartAnyTime = m_pDS->fv("bStartAnyTime").get_asBool(); + newTag->SetEndFromUTC(CDateTime::FromDBDateTime(m_pDS->fv("sEndTime").get_asString().c_str())); + newTag->m_bEndAnyTime = m_pDS->fv("bEndAnyTime").get_asBool(); + newTag->SetFirstDayFromUTC(CDateTime::FromDBDateTime(m_pDS->fv("sFirstDay").get_asString().c_str())); + newTag->m_iWeekdays = m_pDS->fv("iWeekdays").get_asInt(); + newTag->m_iEpgUid = m_pDS->fv("iEpgUid").get_asInt(); + newTag->m_iMarginStart = m_pDS->fv("iMarginStart").get_asInt(); + newTag->m_iMarginEnd = m_pDS->fv("iMarginEnd").get_asInt(); + newTag->m_strEpgSearchString = m_pDS->fv("sEpgSearchString").get_asString().c_str(); + newTag->m_bFullTextEpgSearch = m_pDS->fv("bFullTextEpgSearch").get_asBool(); + newTag->m_iPreventDupEpisodes = m_pDS->fv("iPreventDuplicates").get_asInt(); + newTag->m_iPriority = m_pDS->fv("iPrority").get_asInt(); + newTag->m_iLifetime = m_pDS->fv("iLifetime").get_asInt(); + newTag->m_iMaxRecordings = m_pDS->fv("iMaxRecordings").get_asInt(); + newTag->m_iRecordingGroup = m_pDS->fv("iRecordingGroup").get_asInt(); + newTag->UpdateSummary(); + + result.emplace_back(newTag); + + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::LogF(LOGERROR, "Could not load timer data from the database"); + } + } + return result; +} + +bool CPVRDatabase::Persist(CPVRTimerInfoTag& timer) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + // insert a new entry if this is a new timer, or replace the existing one otherwise + std::string strQuery; + if (timer.m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX) + strQuery = PrepareSQL("INSERT INTO timers " + "(iParentClientIndex, iClientId, iTimerType, iState, sTitle, iClientChannelUid, sSeriesLink, sStartTime," + " bStartAnyTime, sEndTime, bEndAnyTime, sFirstDay, iWeekdays, iEpgUid, iMarginStart, iMarginEnd," + " sEpgSearchString, bFullTextEpgSearch, iPreventDuplicates, iPrority, iLifetime, iMaxRecordings, iRecordingGroup) " + "VALUES (%i, %i, %u, %i, '%s', %i, '%s', '%s', %i, '%s', %i, '%s', %i, %u, %i, %i, '%s', %i, %i, %i, %i, %i, %i);", + timer.m_iParentClientIndex, timer.m_iClientId, timer.GetTimerType()->GetTypeId(), timer.m_state, + timer.Title().c_str(), timer.m_iClientChannelUid, timer.SeriesLink().c_str(), + timer.StartAsUTC().GetAsDBDateTime().c_str(), timer.m_bStartAnyTime ? 1 : 0, + timer.EndAsUTC().GetAsDBDateTime().c_str(), timer.m_bEndAnyTime ? 1 : 0, + timer.FirstDayAsUTC().GetAsDBDateTime().c_str(), timer.m_iWeekdays, timer.UniqueBroadcastID(), + timer.m_iMarginStart, timer.m_iMarginEnd, timer.m_strEpgSearchString.c_str(), timer.m_bFullTextEpgSearch ? 1 : 0, + timer.m_iPreventDupEpisodes, timer.m_iPriority, timer.m_iLifetime, timer.m_iMaxRecordings, timer.m_iRecordingGroup); + else + strQuery = PrepareSQL("REPLACE INTO timers " + "(iClientIndex," + " iParentClientIndex, iClientId, iTimerType, iState, sTitle, iClientChannelUid, sSeriesLink, sStartTime," + " bStartAnyTime, sEndTime, bEndAnyTime, sFirstDay, iWeekdays, iEpgUid, iMarginStart, iMarginEnd," + " sEpgSearchString, bFullTextEpgSearch, iPreventDuplicates, iPrority, iLifetime, iMaxRecordings, iRecordingGroup) " + "VALUES (%i, %i, %i, %u, %i, '%s', %i, '%s', '%s', %i, '%s', %i, '%s', %i, %u, %i, %i, '%s', %i, %i, %i, %i, %i, %i);", + -timer.m_iClientIndex, + timer.m_iParentClientIndex, timer.m_iClientId, timer.GetTimerType()->GetTypeId(), timer.m_state, + timer.Title().c_str(), timer.m_iClientChannelUid, timer.SeriesLink().c_str(), + timer.StartAsUTC().GetAsDBDateTime().c_str(), timer.m_bStartAnyTime ? 1 : 0, + timer.EndAsUTC().GetAsDBDateTime().c_str(), timer.m_bEndAnyTime ? 1 : 0, + timer.FirstDayAsUTC().GetAsDBDateTime().c_str(), timer.m_iWeekdays, timer.UniqueBroadcastID(), + timer.m_iMarginStart, timer.m_iMarginEnd, timer.m_strEpgSearchString.c_str(), timer.m_bFullTextEpgSearch ? 1 : 0, + timer.m_iPreventDupEpisodes, timer.m_iPriority, timer.m_iLifetime, timer.m_iMaxRecordings, timer.m_iRecordingGroup); + + bool bReturn = ExecuteQuery(strQuery); + + // set the client index for just inserted timers + if (bReturn && timer.m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX) + { + // index must be negative for local timers! + timer.m_iClientIndex = -static_cast<int>(m_pDS->lastinsertid()); + } + + return bReturn; +} + +bool CPVRDatabase::Delete(const CPVRTimerInfoTag& timer) +{ + if (timer.m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX) + return false; + + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting timer '{}' from the database", timer.m_iClientIndex); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + Filter filter; + filter.AppendWhere(PrepareSQL("iClientIndex = '%i'", -timer.m_iClientIndex)); + + return DeleteValues("timers", filter); +} + +bool CPVRDatabase::DeleteTimers() +{ + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all timers from the database"); + + std::unique_lock<CCriticalSection> lock(m_critSection); + return DeleteValues("timers"); +} diff --git a/xbmc/pvr/PVRDatabase.h b/xbmc/pvr/PVRDatabase.h new file mode 100644 index 0000000..1e9cbf5 --- /dev/null +++ b/xbmc/pvr/PVRDatabase.h @@ -0,0 +1,320 @@ +/* + * 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 <map> +#include <vector> + +namespace PVR +{ + class CPVRChannel; + class CPVRChannelGroup; + class CPVRChannelGroupMember; + class CPVRChannelGroups; + class CPVRProvider; + class CPVRProviders; + class CPVRClient; + class CPVRTimerInfoTag; + class CPVRTimers; + + /** The PVR database */ + + static constexpr int CHANNEL_COMMIT_QUERY_COUNT_LIMIT = 10000; + + class CPVRDatabase : public CDatabase + { + public: + /*! + * @brief Create a new instance of the PVR database. + */ + CPVRDatabase() = default; + ~CPVRDatabase() 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 40; } + + /*! + * @brief Get the default sqlite database filename. + * @return The default filename. + */ + const char* GetBaseDBName() const override { return "TV"; } + + /*! @name Client methods */ + //@{ + + /*! + * @brief Remove all client entries from the database. + * @return True if all client entries were removed, false otherwise. + */ + bool DeleteClients(); + + /*! + * @brief Add or update a client entry in the database + * @param client The client to persist. + * @return True when persisted, false otherwise. + */ + bool Persist(const CPVRClient& client); + + /*! + * @brief Remove a client entry from the database + * @param client The client to remove. + * @return True if the client was removed, false otherwise. + */ + bool Delete(const CPVRClient& client); + + /*! + * @brief Get the priority for a given client from the database. + * @param client The client. + * @return The priority. + */ + int GetPriority(const CPVRClient& client); + + /*! @name Channel methods */ + //@{ + + /*! + * @brief Remove all channels from the database. + * @return True if all channels were removed, false otherwise. + */ + bool DeleteChannels(); + + /*! + * @brief Get channels from the database. + * @param bRadio Whether to fetch radio or TV channels. + * @param clients The PVR clients the channels should be loaded for. Leave empty for all clients. + * @param results The container for the channels. + * @return The number of channels loaded. + */ + int Get(bool bRadio, + const std::vector<std::shared_ptr<CPVRClient>>& clients, + std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& results) const; + + /*! + * @brief Add or update a channel entry in the database + * @param channel The channel to persist. + * @param bCommit queue only or queue and commit + * @return True when persisted or queued, false otherwise. + */ + bool Persist(CPVRChannel& channel, bool bCommit); + + /*! + * @brief Remove a channel entry from the database + * @param channel The channel to remove. + * @return True if the channel was removed, false otherwise. + */ + bool QueueDeleteQuery(const CPVRChannel& channel); + + //@} + + /*! @name Channel group member methods */ + //@{ + + /*! + * @brief Remove a channel group member entry from the database + * @param groupMember The group member to remove. + * @return True if the member was removed, false otherwise. + */ + bool QueueDeleteQuery(const CPVRChannelGroupMember& groupMember); + + //@} + + /*! @name Channel provider methods */ + //@{ + + /*! + * @brief Remove all providers from the database. + * @return True if all providers were removed, false otherwise. + */ + bool DeleteProviders(); + + /*! + * @brief Add or update a provider entry in the database + * @param provider The provider to persist. + * @param updateRecord True if record to be updated, false for insert + * @return True when persisted, false otherwise. + */ + bool Persist(CPVRProvider& provider, bool updateRecord = false); + + /*! + * @brief Remove a provider entry from the database + * @param provider The provider to remove. + * @return True if the provider was removed, false otherwise. + */ + bool Delete(const CPVRProvider& provider); + + /*! + * @brief Get the list of providers from the database + * @param results The providers to store the results in. + * @param clients The PVR clients the providers should be loaded for. Leave empty for all clients. + * @return The amount of providers that were added. + */ + bool Get(CPVRProviders& results, const std::vector<std::shared_ptr<CPVRClient>>& clients) const; + + /*! + * @brief Get the maximum provider id in the database + * @return The maximum provider id in the database + */ + int GetMaxProviderId(); + + //@} + + /*! @name Channel group methods */ + //@{ + + /*! + * @brief Remove all channel groups from the database + * @return True if all channel groups were removed. + */ + bool DeleteChannelGroups(); + + /*! + * @brief Delete a channel group and all its members from the database. + * @param group The group to delete. + * @return True if the group was deleted successfully, false otherwise. + */ + bool Delete(const CPVRChannelGroup& group); + + /*! + * @brief Get the channel groups. + * @param results The container to store the results in. + * @return The number of groups loaded. + */ + int Get(CPVRChannelGroups& results) const; + + /*! + * @brief Get the members of a channel group. + * @param group The group to get the members for. + * @param clients The PVR clients the group members should be loaded for. Leave empty for all clients. + * @return The group members. + */ + std::vector<std::shared_ptr<CPVRChannelGroupMember>> Get( + const CPVRChannelGroup& group, + const std::vector<std::shared_ptr<CPVRClient>>& clients) const; + + /*! + * @brief Add or update a channel group entry in the database. + * @param group The group to persist. + * @return True if the group was persisted successfully, false otherwise. + */ + bool Persist(CPVRChannelGroup& group); + + /*! + * @brief Reset all epg ids to 0 + * @return True when reset, false otherwise. + */ + bool ResetEPG(); + + /*! @name Timer methods */ + //@{ + + /*! + * @brief Get the timers. + * @param timers The container for the timers. + * @param clients The PVR clients the timers should be loaded for. Leave empty for all clients. + * @return The timers. + */ + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetTimers( + CPVRTimers& timers, const std::vector<std::shared_ptr<CPVRClient>>& clients) const; + + /*! + * @brief Add or update a timer entry in the database + * @param channel The timer to persist. + * @return True if persisted, false otherwise. + */ + bool Persist(CPVRTimerInfoTag& timer); + + /*! + * @brief Remove a timer from the database + * @param timer The timer to remove. + * @return True if the timer was removed, false otherwise. + */ + bool Delete(const CPVRTimerInfoTag& timer); + + /*! + * @brief Remove all timer entries from the database. + * @return True if all timer entries were removed, false otherwise. + */ + bool DeleteTimers(); + //@} + + /*! @name Client methods */ + //@{ + + /*! + * @brief Updates the last watched timestamp for the channel + * @param channel the channel + * @return whether the update was successful + */ + bool UpdateLastWatched(const CPVRChannel& channel); + + /*! + * @brief Updates the last watched timestamp for the channel group + * @param group the group + * @return whether the update was successful + */ + bool UpdateLastWatched(const CPVRChannelGroup& group); + //@} + + /*! + * @brief Updates the last opened timestamp for the channel group + * @param group the group + * @return whether the update was successful + */ + bool UpdateLastOpened(const CPVRChannelGroup& group); + //@} + + private: + /*! + * @brief Create the PVR database tables. + */ + void CreateTables() override; + 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 11; } + + bool PersistGroupMembers(const CPVRChannelGroup& group); + + bool PersistChannels(const CPVRChannelGroup& group); + + bool RemoveChannelsFromGroup(const CPVRChannelGroup& group); + + mutable CCriticalSection m_critSection; + }; +} diff --git a/xbmc/pvr/PVREdl.cpp b/xbmc/pvr/PVREdl.cpp new file mode 100644 index 0000000..72a3ee4 --- /dev/null +++ b/xbmc/pvr/PVREdl.cpp @@ -0,0 +1,68 @@ +/* + * 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 "PVREdl.h" + +#include "FileItem.h" +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_edl.h" +#include "cores/EdlEdit.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/recordings/PVRRecording.h" +#include "utils/log.h" + +namespace PVR +{ + +std::vector<EDL::Edit> CPVREdl::GetEdits(const CFileItem& item) +{ + std::vector<PVR_EDL_ENTRY> edl; + + if (item.HasPVRRecordingInfoTag()) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Reading EDL for recording: {}", + item.GetPVRRecordingInfoTag()->m_strTitle); + edl = item.GetPVRRecordingInfoTag()->GetEdl(); + } + else if (item.HasEPGInfoTag()) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Reading EDL for EPG tag: {}", item.GetEPGInfoTag()->Title()); + edl = item.GetEPGInfoTag()->GetEdl(); + } + + std::vector<EDL::Edit> editlist; + for (const auto& entry : edl) + { + EDL::Edit edit; + edit.start = entry.start; + edit.end = entry.end; + + switch (entry.type) + { + case PVR_EDL_TYPE_CUT: + edit.action = EDL::Action::CUT; + break; + case PVR_EDL_TYPE_MUTE: + edit.action = EDL::Action::MUTE; + break; + case PVR_EDL_TYPE_SCENE: + edit.action = EDL::Action::SCENE; + break; + case PVR_EDL_TYPE_COMBREAK: + edit.action = EDL::Action::COMM_BREAK; + break; + default: + CLog::LogF(LOGWARNING, "Ignoring entry of unknown EDL type: {}", entry.type); + continue; + } + + editlist.emplace_back(edit); + } + return editlist; +} + +} // namespace PVR diff --git a/xbmc/pvr/PVREdl.h b/xbmc/pvr/PVREdl.h new file mode 100644 index 0000000..7440b08 --- /dev/null +++ b/xbmc/pvr/PVREdl.h @@ -0,0 +1,34 @@ +/* + * 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 <vector> + +class CFileItem; + +namespace EDL +{ +struct Edit; +} + +namespace PVR +{ + +class CPVREdl +{ +public: + /*! + * @brief Get the EDL edits for the given item. + * @param item The item. + * @return The EDL edits or an empty vector if no edits exist. + */ + static std::vector<EDL::Edit> GetEdits(const CFileItem& item); +}; + +} // namespace PVR diff --git a/xbmc/pvr/PVREventLogJob.cpp b/xbmc/pvr/PVREventLogJob.cpp new file mode 100644 index 0000000..b70ad1c --- /dev/null +++ b/xbmc/pvr/PVREventLogJob.cpp @@ -0,0 +1,56 @@ +/* + * 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 "PVREventLogJob.h" + +#include "ServiceBroker.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "events/EventLog.h" +#include "events/NotificationEvent.h" + +namespace PVR +{ + +CPVREventLogJob::CPVREventLogJob(bool bNotifyUser, + EventLevel eLevel, + const std::string& label, + const std::string& msg, + const std::string& icon) +{ + AddEvent(bNotifyUser, eLevel, label, msg, icon); +} + +void CPVREventLogJob::AddEvent(bool bNotifyUser, + EventLevel eLevel, + const std::string& label, + const std::string& msg, + const std::string& icon) +{ + m_events.emplace_back(Event(bNotifyUser, eLevel, label, msg, icon)); +} + +bool CPVREventLogJob::DoWork() +{ + for (const auto& event : m_events) + { + if (event.m_bNotifyUser) + CGUIDialogKaiToast::QueueNotification(event.m_eLevel == EventLevel::Error + ? CGUIDialogKaiToast::Error + : CGUIDialogKaiToast::Info, + event.m_label, event.m_msg, 5000, true); + + // Write event log entry. + auto eventLog = CServiceBroker::GetEventLog(); + if (eventLog) + eventLog->Add(std::make_shared<CNotificationEvent>(event.m_label, event.m_msg, event.m_icon, + event.m_eLevel)); + } + return true; +} + +} // namespace PVR diff --git a/xbmc/pvr/PVREventLogJob.h b/xbmc/pvr/PVREventLogJob.h new file mode 100644 index 0000000..a5f0be7 --- /dev/null +++ b/xbmc/pvr/PVREventLogJob.h @@ -0,0 +1,62 @@ +/* + * 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 "events/EventLog.h" +#include "utils/Job.h" + +#include <string> +#include <vector> + +namespace PVR +{ +class CPVREventLogJob : public CJob +{ +public: + CPVREventLogJob() = default; + + CPVREventLogJob(bool bNotifyUser, + EventLevel eLevel, + const std::string& label, + const std::string& msg, + const std::string& icon); + + ~CPVREventLogJob() override = default; + const char* GetType() const override { return "pvr-eventlog-job"; } + + void AddEvent(bool bNotifyUser, + EventLevel eLevel, + const std::string& label, + const std::string& msg, + const std::string& icon); + + bool DoWork() override; + +private: + struct Event + { + bool m_bNotifyUser; + EventLevel m_eLevel{EventLevel::Information}; + std::string m_label; + std::string m_msg; + std::string m_icon; + + Event(bool bNotifyUser, + EventLevel elevel, + const std::string& label, + const std::string& msg, + const std::string& icon) + : m_bNotifyUser(bNotifyUser), m_eLevel(elevel), m_label(label), m_msg(msg), m_icon(icon) + { + } + }; + + std::vector<Event> m_events; +}; +} // namespace PVR diff --git a/xbmc/pvr/PVRItem.cpp b/xbmc/pvr/PVRItem.cpp new file mode 100644 index 0000000..5eee911 --- /dev/null +++ b/xbmc/pvr/PVRItem.cpp @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2016-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 "PVRItem.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "pvr/PVRManager.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <memory> + +namespace PVR +{ +std::shared_ptr<CPVREpgInfoTag> CPVRItem::GetEpgInfoTag() const +{ + if (m_item->IsEPG()) + { + return m_item->GetEPGInfoTag(); + } + else if (m_item->IsPVRChannel()) + { + return m_item->GetPVRChannelInfoTag()->GetEPGNow(); + } + else if (m_item->IsPVRTimer()) + { + return m_item->GetPVRTimerInfoTag()->GetEpgInfoTag(); + } + else if (URIUtils::IsPVR(m_item->GetDynPath())) + { + CLog::LogF(LOGERROR, "Unsupported item type!"); + } + return {}; +} + +std::shared_ptr<CPVREpgInfoTag> CPVRItem::GetNextEpgInfoTag() const +{ + if (m_item->IsEPG()) + { + const std::shared_ptr<CPVRChannel> channel = + CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag( + m_item->GetEPGInfoTag()); + if (channel) + return channel->GetEPGNext(); + } + else if (m_item->IsPVRChannel()) + { + return m_item->GetPVRChannelInfoTag()->GetEPGNext(); + } + else if (m_item->IsPVRTimer()) + { + const std::shared_ptr<CPVRChannel> channel = m_item->GetPVRTimerInfoTag()->Channel(); + if (channel) + return channel->GetEPGNext(); + } + else if (URIUtils::IsPVR(m_item->GetDynPath())) + { + CLog::LogF(LOGERROR, "Unsupported item type!"); + } + return {}; +} + +std::shared_ptr<CPVRChannel> CPVRItem::GetChannel() const +{ + if (m_item->IsPVRChannel()) + { + return m_item->GetPVRChannelInfoTag(); + } + else if (m_item->IsEPG()) + { + return CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag( + m_item->GetEPGInfoTag()); + } + else if (m_item->IsPVRTimer()) + { + return m_item->GetPVRTimerInfoTag()->Channel(); + } + else if (m_item->IsPVRRecording()) + { + return m_item->GetPVRRecordingInfoTag()->Channel(); + } + else if (URIUtils::IsPVR(m_item->GetDynPath())) + { + CLog::LogF(LOGERROR, "Unsupported item type!"); + } + return {}; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRItem::GetTimerInfoTag() const +{ + if (m_item->IsPVRTimer()) + { + return m_item->GetPVRTimerInfoTag(); + } + else if (m_item->IsEPG()) + { + return CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(m_item->GetEPGInfoTag()); + } + else if (m_item->IsPVRChannel()) + { + return CServiceBroker::GetPVRManager().Timers()->GetActiveTimerForChannel( + m_item->GetPVRChannelInfoTag()); + } + else if (URIUtils::IsPVR(m_item->GetDynPath())) + { + CLog::LogF(LOGERROR, "Unsupported item type!"); + } + return {}; +} + +std::shared_ptr<CPVRRecording> CPVRItem::GetRecording() const +{ + if (m_item->IsPVRRecording()) + { + return m_item->GetPVRRecordingInfoTag(); + } + else if (m_item->IsEPG()) + { + return CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag( + m_item->GetEPGInfoTag()); + } + else if (URIUtils::IsPVR(m_item->GetDynPath())) + { + CLog::LogF(LOGERROR, "Unsupported item type!"); + } + return {}; +} + +bool CPVRItem::IsRadio() const +{ + if (m_item->IsPVRChannel()) + { + return m_item->GetPVRChannelInfoTag()->IsRadio(); + } + else if (m_item->IsEPG()) + { + return m_item->GetEPGInfoTag()->IsRadio(); + } + else if (m_item->IsPVRRecording()) + { + return m_item->GetPVRRecordingInfoTag()->IsRadio(); + } + else if (m_item->IsPVRTimer()) + { + return m_item->GetPVRTimerInfoTag()->IsRadio(); + } + else if (URIUtils::IsPVR(m_item->GetDynPath())) + { + CLog::LogF(LOGERROR, "Unsupported item type!"); + } + return false; +} + +} // namespace PVR diff --git a/xbmc/pvr/PVRItem.h b/xbmc/pvr/PVRItem.h new file mode 100644 index 0000000..e41ca15 --- /dev/null +++ b/xbmc/pvr/PVRItem.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016-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 <memory> + +class CFileItem; + +namespace PVR +{ +class CPVRChannel; +class CPVREpgInfoTag; +class CPVRRecording; +class CPVRTimerInfoTag; + +class CPVRItem +{ +public: + explicit CPVRItem(const std::shared_ptr<CFileItem>& item) : m_item(item.get()) {} + explicit CPVRItem(const CFileItem* item) : m_item(item) {} + explicit CPVRItem(const CFileItem& item) : m_item(&item) {} + + std::shared_ptr<CPVREpgInfoTag> GetEpgInfoTag() const; + std::shared_ptr<CPVREpgInfoTag> GetNextEpgInfoTag() const; + std::shared_ptr<CPVRChannel> GetChannel() const; + std::shared_ptr<CPVRTimerInfoTag> GetTimerInfoTag() const; + std::shared_ptr<CPVRRecording> GetRecording() const; + + bool IsRadio() const; + +private: + const CFileItem* m_item; +}; + +} // namespace PVR diff --git a/xbmc/pvr/PVRManager.cpp b/xbmc/pvr/PVRManager.cpp new file mode 100644 index 0000000..715397e --- /dev/null +++ b/xbmc/pvr/PVRManager.cpp @@ -0,0 +1,1029 @@ +/* + * 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 "PVRManager.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "interfaces/AnnouncementManager.h" +#include "messaging/ApplicationMessenger.h" +#include "pvr/PVRComponentRegistration.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupInternal.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "pvr/guilib/PVRGUIChannelIconUpdater.h" +#include "pvr/guilib/PVRGUIProgressHandler.h" +#include "pvr/guilib/guiinfo/PVRGUIInfo.h" +#include "pvr/providers/PVRProvider.h" +#include "pvr/providers/PVRProviders.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/Settings.h" +#include "utils/JobManager.h" +#include "utils/Stopwatch.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <algorithm> +#include <memory> +#include <mutex> +#include <string> +#include <utility> +#include <vector> + +using namespace PVR; +using namespace std::chrono_literals; + +namespace +{ + +class CPVRJob +{ +public: + virtual ~CPVRJob() = default; + + virtual bool DoWork() = 0; + virtual const std::string GetType() const = 0; + +protected: +}; + +template<typename F> +class CPVRLambdaJob : public CPVRJob +{ +public: + CPVRLambdaJob() = delete; + CPVRLambdaJob(const std::string& type, F&& f) : m_type(type), m_f(std::forward<F>(f)) {} + + bool DoWork() override + { + m_f(); + return true; + } + + const std::string GetType() const override { return m_type; } + +private: + std::string m_type; + F m_f; +}; + +} // unnamed namespace + +namespace PVR +{ + +class CPVRManagerJobQueue +{ +public: + CPVRManagerJobQueue() : m_triggerEvent(false) {} + + void Start(); + void Stop(); + void Clear(); + + template<typename F> + void Append(const std::string& type, F&& f) + { + AppendJob(new CPVRLambdaJob<F>(type, std::forward<F>(f))); + } + + void ExecutePendingJobs(); + + bool WaitForJobs(unsigned int milliSeconds) + { + return m_triggerEvent.Wait(std::chrono::milliseconds(milliSeconds)); + } + +private: + void AppendJob(CPVRJob* job); + + CCriticalSection m_critSection; + CEvent m_triggerEvent; + std::vector<CPVRJob*> m_pendingUpdates; + bool m_bStopped = true; +}; + +} // namespace PVR + +void CPVRManagerJobQueue::Start() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bStopped = false; + m_triggerEvent.Set(); +} + +void CPVRManagerJobQueue::Stop() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bStopped = true; + m_triggerEvent.Reset(); +} + +void CPVRManagerJobQueue::Clear() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + for (CPVRJob* updateJob : m_pendingUpdates) + delete updateJob; + + m_pendingUpdates.clear(); + m_triggerEvent.Set(); +} + +void CPVRManagerJobQueue::AppendJob(CPVRJob* job) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + // check for another pending job of given type... + if (std::any_of(m_pendingUpdates.cbegin(), m_pendingUpdates.cend(), + [job](CPVRJob* updateJob) { return updateJob->GetType() == job->GetType(); })) + { + delete job; + return; + } + + m_pendingUpdates.push_back(job); + m_triggerEvent.Set(); +} + +void CPVRManagerJobQueue::ExecutePendingJobs() +{ + std::vector<CPVRJob*> pendingUpdates; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_bStopped) + return; + + pendingUpdates = std::move(m_pendingUpdates); + m_triggerEvent.Reset(); + } + + CPVRJob* job = nullptr; + while (!pendingUpdates.empty()) + { + job = pendingUpdates.front(); + pendingUpdates.erase(pendingUpdates.begin()); + + job->DoWork(); + delete job; + } +} + +CPVRManager::CPVRManager() + : CThread("PVRManager"), + m_providers(new CPVRProviders), + m_channelGroups(new CPVRChannelGroupsContainer), + m_recordings(new CPVRRecordings), + m_timers(new CPVRTimers), + m_addons(new CPVRClients), + m_guiInfo(new CPVRGUIInfo), + m_components(new CPVRComponentRegistration), + m_epgContainer(m_events), + m_pendingUpdates(new CPVRManagerJobQueue), + m_database(new CPVRDatabase), + m_parentalTimer(new CStopWatch), + m_playbackState(new CPVRPlaybackState), + m_settings({CSettings::SETTING_PVRPOWERMANAGEMENT_ENABLED, + CSettings::SETTING_PVRPOWERMANAGEMENT_SETWAKEUPCMD, + CSettings::SETTING_PVRPARENTAL_ENABLED, CSettings::SETTING_PVRPARENTAL_DURATION}) +{ + CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this); + m_actionListener.Init(*this); + + CLog::LogFC(LOGDEBUG, LOGPVR, "PVR Manager instance created"); +} + +CPVRManager::~CPVRManager() +{ + m_actionListener.Deinit(*this); + CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this); + + CLog::LogFC(LOGDEBUG, LOGPVR, "PVR Manager instance destroyed"); +} + +void CPVRManager::Announce(ANNOUNCEMENT::AnnouncementFlag flag, + const std::string& sender, + const std::string& message, + const CVariant& data) +{ + if (!IsStarted()) + return; + + if ((flag & (ANNOUNCEMENT::GUI))) + { + if (message == "OnScreensaverActivated") + m_addons->OnPowerSavingActivated(); + else if (message == "OnScreensaverDeactivated") + m_addons->OnPowerSavingDeactivated(); + } +} + +std::shared_ptr<CPVRDatabase> CPVRManager::GetTVDatabase() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_database || !m_database->IsOpen()) + CLog::LogF(LOGERROR, "Failed to open the PVR database"); + + return m_database; +} + +std::shared_ptr<CPVRProviders> CPVRManager::Providers() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_providers; +} + +std::shared_ptr<CPVRChannelGroupsContainer> CPVRManager::ChannelGroups() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_channelGroups; +} + +std::shared_ptr<CPVRRecordings> CPVRManager::Recordings() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_recordings; +} + +std::shared_ptr<CPVRTimers> CPVRManager::Timers() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_timers; +} + +std::shared_ptr<CPVRClients> CPVRManager::Clients() const +{ + // note: m_addons is const (only set/reset in ctor/dtor). no need for a lock here. + return m_addons; +} + +std::shared_ptr<CPVRClient> CPVRManager::GetClient(const CFileItem& item) const +{ + int iClientID = PVR_INVALID_CLIENT_ID; + + if (item.HasPVRChannelInfoTag()) + iClientID = item.GetPVRChannelInfoTag()->ClientID(); + else if (item.HasPVRRecordingInfoTag()) + iClientID = item.GetPVRRecordingInfoTag()->ClientID(); + else if (item.HasPVRTimerInfoTag()) + iClientID = item.GetPVRTimerInfoTag()->ClientID(); + else if (item.HasEPGInfoTag()) + iClientID = item.GetEPGInfoTag()->ClientID(); + else if (URIUtils::IsPVRChannel(item.GetPath())) + { + const std::shared_ptr<CPVRChannel> channel = m_channelGroups->GetByPath(item.GetPath()); + if (channel) + iClientID = channel->ClientID(); + } + else if (URIUtils::IsPVRRecording(item.GetPath())) + { + const std::shared_ptr<CPVRRecording> recording = m_recordings->GetByPath(item.GetPath()); + if (recording) + iClientID = recording->ClientID(); + } + return GetClient(iClientID); +} + +std::shared_ptr<CPVRClient> CPVRManager::GetClient(int iClientId) const +{ + return m_addons->GetCreatedClient(iClientId); +} + +std::shared_ptr<CPVRPlaybackState> CPVRManager::PlaybackState() const +{ + // note: m_playbackState is const (only set/reset in ctor/dtor). no need for a lock here. + return m_playbackState; +} + +CPVREpgContainer& CPVRManager::EpgContainer() +{ + // note: m_epgContainer is const (only set/reset in ctor/dtor). no need for a lock here. + return m_epgContainer; +} + +void CPVRManager::Clear() +{ + m_playbackState->Clear(); + m_pendingUpdates->Clear(); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_guiInfo.reset(); + m_timers.reset(); + m_recordings.reset(); + m_providers.reset(); + m_channelGroups.reset(); + m_parentalTimer.reset(); + m_database.reset(); + + m_bEpgsCreated = false; +} + +void CPVRManager::ResetProperties() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + Clear(); + + m_database.reset(new CPVRDatabase); + m_providers.reset(new CPVRProviders); + m_channelGroups.reset(new CPVRChannelGroupsContainer); + m_recordings.reset(new CPVRRecordings); + m_timers.reset(new CPVRTimers); + m_guiInfo.reset(new CPVRGUIInfo); + m_parentalTimer.reset(new CStopWatch); + m_knownClients.clear(); +} + +void CPVRManager::Init() +{ + // initial check for enabled addons + // if at least one pvr addon is enabled, PVRManager start up + CServiceBroker::GetJobManager()->Submit([this] { + Clients()->Start(); + return true; + }); +} + +void CPVRManager::Start() +{ + std::unique_lock<CCriticalSection> initLock(m_startStopMutex); + + // Prevent concurrent starts + if (IsInitialising()) + return; + + // Note: Stop() must not be called while holding pvr manager's mutex. Stop() calls + // StopThread() which can deadlock if the worker thread tries to acquire pvr manager's + // lock while StopThread() is waiting for the worker to exit. Thus, we introduce another + // lock here (m_startStopMutex), which only gets hold while starting/restarting pvr manager. + Stop(true); + + if (!m_addons->HasCreatedClients()) + return; + + CLog::Log(LOGINFO, "PVR Manager: Starting"); + SetState(ManagerState::STATE_STARTING); + + /* create the pvrmanager thread, which will ensure that all data will be loaded */ + Create(); + SetPriority(ThreadPriority::BELOW_NORMAL); +} + +void CPVRManager::Stop(bool bRestart /* = false */) +{ + std::unique_lock<CCriticalSection> initLock(m_startStopMutex); + + // Prevent concurrent stops + if (IsStopped()) + return; + + /* stop playback if needed */ + if (!bRestart && m_playbackState->IsPlaying()) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Stopping PVR playback"); + CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP); + } + + CLog::Log(LOGINFO, "PVR Manager: Stopping"); + SetState(ManagerState::STATE_SSTOPPING); + + StopThread(); +} + +void CPVRManager::Unload() +{ + // stop pvr manager thread and clear all pvr data + Stop(); + Clear(); +} + +void CPVRManager::Deinit() +{ + SetWakeupCommand(); + Unload(); + + // release addons + m_addons.reset(); +} + +CPVRManager::ManagerState CPVRManager::GetState() const +{ + std::unique_lock<CCriticalSection> lock(m_managerStateMutex); + return m_managerState; +} + +void CPVRManager::SetState(CPVRManager::ManagerState state) +{ + { + std::unique_lock<CCriticalSection> lock(m_managerStateMutex); + if (m_managerState == state) + return; + + m_managerState = state; + } + + PVREvent event; + switch (state) + { + case ManagerState::STATE_ERROR: + event = PVREvent::ManagerError; + break; + case ManagerState::STATE_STOPPED: + event = PVREvent::ManagerStopped; + break; + case ManagerState::STATE_STARTING: + event = PVREvent::ManagerStarting; + break; + case ManagerState::STATE_SSTOPPING: + event = PVREvent::ManagerStopped; + break; + case ManagerState::STATE_INTERRUPTED: + event = PVREvent::ManagerInterrupted; + break; + case ManagerState::STATE_STARTED: + event = PVREvent::ManagerStarted; + break; + default: + return; + } + + PublishEvent(event); +} + +void CPVRManager::PublishEvent(PVREvent event) +{ + m_events.Publish(event); +} + +void CPVRManager::Process() +{ + m_addons->Continue(); + m_database->Open(); + + if (!IsInitialising()) + { + CLog::Log(LOGINFO, "PVR Manager: Start aborted"); + return; + } + + UnloadComponents(); + + if (!IsInitialising()) + { + CLog::Log(LOGINFO, "PVR Manager: Start aborted"); + return; + } + + // Wait for at least one client to come up and load/update data + UpdateComponents(ManagerState::STATE_STARTING); + + if (!IsInitialising()) + { + CLog::Log(LOGINFO, "PVR Manager: Start aborted"); + return; + } + + // Load EPGs from database. + m_epgContainer.Load(); + + // Reinit playbackstate + m_playbackState->ReInit(); + + m_guiInfo->Start(); + m_epgContainer.Start(); + m_timers->Start(); + m_pendingUpdates->Start(); + + SetState(ManagerState::STATE_STARTED); + CLog::Log(LOGINFO, "PVR Manager: Started"); + + bool bRestart(false); + XbmcThreads::EndTime<> cachedImagesCleanupTimeout(30s); // first timeout after 30 secs + + while (IsStarted() && m_addons->HasCreatedClients() && !bRestart) + { + // In case any new client connected, load from db and fetch data update from new client(s) + UpdateComponents(ManagerState::STATE_STARTED); + + if (cachedImagesCleanupTimeout.IsTimePast()) + { + // We don't know for sure what to delete if there are not (yet) connected clients + if (m_addons->HasIgnoredClients()) + { + cachedImagesCleanupTimeout.Set(10s); // try again in 10 secs + } + else + { + // start a job to erase stale texture db entries and image files + TriggerCleanupCachedImages(); + cachedImagesCleanupTimeout.Set(12h); // following timeouts after 12 hours + } + } + + /* first startup */ + if (m_bFirstStart) + { + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bFirstStart = false; + } + + /* start job to search for missing channel icons */ + TriggerSearchMissingChannelIcons(); + + /* try to play channel on startup */ + TriggerPlayChannelOnStartup(); + } + + if (m_addons->AnyClientSupportingRecordingsSize()) + TriggerRecordingsSizeInProgressUpdate(); + + /* execute the next pending jobs if there are any */ + try + { + m_pendingUpdates->ExecutePendingJobs(); + } + catch (...) + { + CLog::LogF( + LOGERROR, + "An error occurred while trying to execute the last PVR update job, trying to recover"); + bRestart = true; + } + + if (IsStarted() && !bRestart) + m_pendingUpdates->WaitForJobs(1000); + } + + m_addons->Stop(); + m_pendingUpdates->Stop(); + m_timers->Stop(); + m_epgContainer.Stop(); + m_guiInfo->Stop(); + + SetState(ManagerState::STATE_INTERRUPTED); + + UnloadComponents(); + m_database->Close(); + + ResetProperties(); + + CLog::Log(LOGINFO, "PVR Manager: Stopped"); + SetState(ManagerState::STATE_STOPPED); +} + +bool CPVRManager::SetWakeupCommand() +{ +#if !defined(TARGET_DARWIN_EMBEDDED) && !defined(TARGET_WINDOWS_STORE) + if (!m_settings.GetBoolValue(CSettings::SETTING_PVRPOWERMANAGEMENT_ENABLED)) + return false; + + const std::string strWakeupCommand( + m_settings.GetStringValue(CSettings::SETTING_PVRPOWERMANAGEMENT_SETWAKEUPCMD)); + if (!strWakeupCommand.empty() && m_timers) + { + const CDateTime nextEvent = m_timers->GetNextEventTime(); + if (nextEvent.IsValid()) + { + time_t iWakeupTime; + nextEvent.GetAsTime(iWakeupTime); + + std::string strExecCommand = StringUtils::Format("{} {}", strWakeupCommand, iWakeupTime); + + const int iReturn = system(strExecCommand.c_str()); + if (iReturn != 0) + CLog::LogF(LOGERROR, "PVR Manager failed to execute wakeup command '{}': {} ({})", + strExecCommand, strerror(iReturn), iReturn); + + return iReturn == 0; + } + } +#endif + return false; +} + +void CPVRManager::OnSleep() +{ + PublishEvent(PVREvent::SystemSleep); + + SetWakeupCommand(); + + m_epgContainer.OnSystemSleep(); + + m_addons->OnSystemSleep(); +} + +void CPVRManager::OnWake() +{ + m_addons->OnSystemWake(); + + m_epgContainer.OnSystemWake(); + + PublishEvent(PVREvent::SystemWake); + + /* start job to search for missing channel icons */ + TriggerSearchMissingChannelIcons(); + + /* try to play channel on startup */ + TriggerPlayChannelOnStartup(); + + /* trigger PVR data updates */ + TriggerChannelGroupsUpdate(); + TriggerProvidersUpdate(); + TriggerChannelsUpdate(); + TriggerRecordingsUpdate(); + TriggerEpgsCreate(); + TriggerTimersUpdate(); +} + +void CPVRManager::UpdateComponents(ManagerState stateToCheck) +{ + XbmcThreads::EndTime<> progressTimeout(30s); + std::unique_ptr<CPVRGUIProgressHandler> progressHandler( + new CPVRGUIProgressHandler(g_localizeStrings.Get(19235))); // PVR manager is starting up + + // Wait for at least one client to come up and load/update data + while (!UpdateComponents(stateToCheck, progressHandler) && m_addons->HasCreatedClients() && + (stateToCheck == GetState())) + { + CThread::Sleep(1000ms); + + if (progressTimeout.IsTimePast()) + progressHandler.reset(); + } +} + +bool CPVRManager::UpdateComponents(ManagerState stateToCheck, + const std::unique_ptr<CPVRGUIProgressHandler>& progressHandler) +{ + // find clients which appeared since last check and update them + const CPVRClientMap clientMap = m_addons->GetCreatedClients(); + if (clientMap.empty()) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "All created PVR clients gone!"); + m_knownClients.clear(); // start over + PublishEvent(PVREvent::ClientsInvalidated); + return false; + } + + std::vector<std::shared_ptr<CPVRClient>> newClients; + for (const auto& entry : clientMap) + { + // skip not (yet) connected clients + if (entry.second->IgnoreClient()) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Skipping not (yet) connected PVR client '{}'", + entry.second->ID()); + continue; + } + + if (!IsKnownClient(entry.first)) + { + m_knownClients.emplace_back(entry.second); + newClients.emplace_back(entry.second); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Adding new PVR client '{}' to list of known clients", + entry.second->ID()); + } + } + + if (newClients.empty()) + return !m_knownClients.empty(); + + // Load all channels and groups + if (progressHandler) + progressHandler->UpdateProgress(g_localizeStrings.Get(19236), 0); // Loading channels and groups + + if (!m_providers->Update(newClients) || (stateToCheck != GetState())) + { + CLog::LogF(LOGERROR, "Failed to load PVR providers."); + m_knownClients.clear(); // start over + PublishEvent(PVREvent::ClientsInvalidated); + return false; + } + + if (!m_channelGroups->Update(newClients) || (stateToCheck != GetState())) + { + CLog::LogF(LOGERROR, "Failed to load PVR channels / groups."); + m_knownClients.clear(); // start over + PublishEvent(PVREvent::ClientsInvalidated); + return false; + } + + // Load all timers + if (progressHandler) + progressHandler->UpdateProgress(g_localizeStrings.Get(19237), 50); // Loading timers + + if (!m_timers->Update(newClients) || (stateToCheck != GetState())) + { + CLog::LogF(LOGERROR, "Failed to load PVR timers."); + m_knownClients.clear(); // start over + PublishEvent(PVREvent::ClientsInvalidated); + return false; + } + + // Load all recordings + if (progressHandler) + progressHandler->UpdateProgress(g_localizeStrings.Get(19238), 75); // Loading recordings + + if (!m_recordings->Update(newClients) || (stateToCheck != GetState())) + { + CLog::LogF(LOGERROR, "Failed to load PVR recordings."); + m_knownClients.clear(); // start over + PublishEvent(PVREvent::ClientsInvalidated); + return false; + } + + // reinit playbackstate as new client may provide new last opened group / last played channel + m_playbackState->ReInit(); + + PublishEvent(PVREvent::ClientsInvalidated); + return true; +} + +void CPVRManager::UnloadComponents() +{ + m_recordings->Unload(); + m_timers->Unload(); + m_channelGroups->Unload(); + m_providers->Unload(); + m_epgContainer.Unload(); +} + +bool CPVRManager::IsKnownClient(int clientID) const +{ + return std::any_of(m_knownClients.cbegin(), m_knownClients.cend(), + [clientID](const auto& client) { return client->GetID() == clientID; }); +} + +void CPVRManager::TriggerPlayChannelOnStartup() +{ + if (IsStarted()) + { + CServiceBroker::GetJobManager()->Submit( + [this] { return Get<PVR::GUI::Playback>().PlayChannelOnStartup(); }); + } +} + +void CPVRManager::RestartParentalTimer() +{ + if (m_parentalTimer) + m_parentalTimer->StartZero(); +} + +bool CPVRManager::IsParentalLocked(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + return m_channelGroups && epgTag && + IsCurrentlyParentalLocked( + m_channelGroups->GetByUniqueID(epgTag->UniqueChannelID(), epgTag->ClientID()), + epgTag->IsParentalLocked()); +} + +bool CPVRManager::IsParentalLocked(const std::shared_ptr<CPVRChannel>& channel) const +{ + return channel && IsCurrentlyParentalLocked(channel, channel->IsLocked()); +} + +bool CPVRManager::IsCurrentlyParentalLocked(const std::shared_ptr<CPVRChannel>& channel, + bool bGenerallyLocked) const +{ + bool bReturn = false; + + if (!channel || !bGenerallyLocked) + return bReturn; + + const std::shared_ptr<CPVRChannel> currentChannel = m_playbackState->GetPlayingChannel(); + + if ( // if channel in question is currently playing it must be currently unlocked. + (!currentChannel || channel != currentChannel) && + // parental control enabled + m_settings.GetBoolValue(CSettings::SETTING_PVRPARENTAL_ENABLED)) + { + float parentalDurationMs = + m_settings.GetIntValue(CSettings::SETTING_PVRPARENTAL_DURATION) * 1000.0f; + bReturn = m_parentalTimer && (!m_parentalTimer->IsRunning() || + m_parentalTimer->GetElapsedMilliseconds() > parentalDurationMs); + } + + return bReturn; +} + +void CPVRManager::OnPlaybackStarted(const CFileItem& item) +{ + m_playbackState->OnPlaybackStarted(item); + Get<PVR::GUI::Channels>().OnPlaybackStarted(item); + m_epgContainer.OnPlaybackStarted(); +} + +void CPVRManager::OnPlaybackStopped(const CFileItem& item) +{ + // Playback ended due to user interaction + if (m_playbackState->OnPlaybackStopped(item)) + PublishEvent(PVREvent::ChannelPlaybackStopped); + + Get<PVR::GUI::Channels>().OnPlaybackStopped(item); + m_epgContainer.OnPlaybackStopped(); +} + +void CPVRManager::OnPlaybackEnded(const CFileItem& item) +{ + // Playback ended, but not due to user interaction + OnPlaybackStopped(item); +} + +void CPVRManager::LocalizationChanged() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (IsStarted()) + { + static_cast<CPVRChannelGroupInternal*>(m_channelGroups->GetGroupAllRadio().get()) + ->CheckGroupName(); + static_cast<CPVRChannelGroupInternal*>(m_channelGroups->GetGroupAllTV().get()) + ->CheckGroupName(); + } +} + +bool CPVRManager::EpgsCreated() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bEpgsCreated; +} + +void CPVRManager::TriggerEpgsCreate() +{ + m_pendingUpdates->Append("pvr-create-epgs", [this]() { return CreateChannelEpgs(); }); +} + +void CPVRManager::TriggerRecordingsSizeInProgressUpdate() +{ + m_pendingUpdates->Append("pvr-update-recordings-size", + [this]() { return Recordings()->UpdateInProgressSize(); }); +} + +void CPVRManager::TriggerRecordingsUpdate(int clientId) +{ + m_pendingUpdates->Append("pvr-update-recordings-" + std::to_string(clientId), [this, clientId]() { + if (!IsKnownClient(clientId)) + return; + + const std::shared_ptr<CPVRClient> client = GetClient(clientId); + if (client) + Recordings()->UpdateFromClients({client}); + }); +} + +void CPVRManager::TriggerRecordingsUpdate() +{ + m_pendingUpdates->Append("pvr-update-recordings", + [this]() { Recordings()->UpdateFromClients({}); }); +} + +void CPVRManager::TriggerTimersUpdate(int clientId) +{ + m_pendingUpdates->Append("pvr-update-timers-" + std::to_string(clientId), [this, clientId]() { + if (!IsKnownClient(clientId)) + return; + + const std::shared_ptr<CPVRClient> client = GetClient(clientId); + if (client) + Timers()->UpdateFromClients({client}); + }); +} + +void CPVRManager::TriggerTimersUpdate() +{ + m_pendingUpdates->Append("pvr-update-timers", [this]() { Timers()->UpdateFromClients({}); }); +} + +void CPVRManager::TriggerProvidersUpdate(int clientId) +{ + m_pendingUpdates->Append("pvr-update-channel-providers-" + std::to_string(clientId), + [this, clientId]() { + if (!IsKnownClient(clientId)) + return; + + const std::shared_ptr<CPVRClient> client = GetClient(clientId); + if (client) + Providers()->UpdateFromClients({client}); + }); +} + +void CPVRManager::TriggerProvidersUpdate() +{ + m_pendingUpdates->Append("pvr-update-channel-providers", + [this]() { Providers()->UpdateFromClients({}); }); +} + +void CPVRManager::TriggerChannelsUpdate(int clientId) +{ + m_pendingUpdates->Append("pvr-update-channels-" + std::to_string(clientId), [this, clientId]() { + if (!IsKnownClient(clientId)) + return; + + const std::shared_ptr<CPVRClient> client = GetClient(clientId); + if (client) + ChannelGroups()->UpdateFromClients({client}, true); + }); +} + +void CPVRManager::TriggerChannelsUpdate() +{ + m_pendingUpdates->Append("pvr-update-channels", + [this]() { ChannelGroups()->UpdateFromClients({}, true); }); +} + +void CPVRManager::TriggerChannelGroupsUpdate(int clientId) +{ + m_pendingUpdates->Append("pvr-update-channelgroups-" + std::to_string(clientId), + [this, clientId]() { + if (!IsKnownClient(clientId)) + return; + + const std::shared_ptr<CPVRClient> client = GetClient(clientId); + if (client) + ChannelGroups()->UpdateFromClients({client}, false); + }); +} + +void CPVRManager::TriggerChannelGroupsUpdate() +{ + m_pendingUpdates->Append("pvr-update-channelgroups", + [this]() { ChannelGroups()->UpdateFromClients({}, false); }); +} + +void CPVRManager::TriggerSearchMissingChannelIcons() +{ + m_pendingUpdates->Append("pvr-search-missing-channel-icons", [this]() { + CPVRGUIChannelIconUpdater updater( + {ChannelGroups()->GetGroupAllTV(), ChannelGroups()->GetGroupAllRadio()}, true); + updater.SearchAndUpdateMissingChannelIcons(); + return true; + }); +} + +void CPVRManager::TriggerSearchMissingChannelIcons(const std::shared_ptr<CPVRChannelGroup>& group) +{ + m_pendingUpdates->Append("pvr-search-missing-channel-icons-" + std::to_string(group->GroupID()), + [group]() { + CPVRGUIChannelIconUpdater updater({group}, false); + updater.SearchAndUpdateMissingChannelIcons(); + return true; + }); +} + +void CPVRManager::TriggerCleanupCachedImages() +{ + m_pendingUpdates->Append("pvr-cleanup-cached-images", [this]() { + int iCleanedImages = 0; + CLog::Log(LOGINFO, "PVR Manager: Starting cleanup of cached images."); + iCleanedImages += Recordings()->CleanupCachedImages(); + iCleanedImages += ChannelGroups()->CleanupCachedImages(); + iCleanedImages += Providers()->CleanupCachedImages(); + iCleanedImages += EpgContainer().CleanupCachedImages(); + CLog::Log(LOGINFO, "PVR Manager: Cleaned up {} cached images.", iCleanedImages); + return true; + }); +} + +void CPVRManager::ConnectionStateChange(CPVRClient* client, + const std::string& connectString, + PVR_CONNECTION_STATE state, + const std::string& message) +{ + CServiceBroker::GetJobManager()->Submit([this, client, connectString, state, message] { + Clients()->ConnectionStateChange(client, connectString, state, message); + return true; + }); +} + +bool CPVRManager::CreateChannelEpgs() +{ + if (EpgsCreated()) + return true; + + bool bEpgsCreated = m_channelGroups->CreateChannelEpgs(); + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bEpgsCreated = bEpgsCreated; + return m_bEpgsCreated; +} diff --git a/xbmc/pvr/PVRManager.h b/xbmc/pvr/PVRManager.h new file mode 100644 index 0000000..06bcb44 --- /dev/null +++ b/xbmc/pvr/PVRManager.h @@ -0,0 +1,482 @@ +/* + * 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_general.h" +#include "interfaces/IAnnouncer.h" +#include "pvr/PVRComponentRegistration.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/guilib/PVRGUIActionListener.h" +#include "pvr/settings/PVRSettings.h" +#include "threads/CriticalSection.h" +#include "threads/Event.h" +#include "threads/Thread.h" +#include "utils/EventStream.h" + +#include <memory> +#include <string> +#include <vector> + +class CFileItem; +class CStopWatch; + +namespace PVR +{ +class CPVRChannel; +class CPVRChannelGroup; +class CPVRChannelGroupsContainer; +class CPVRProviders; +class CPVRClient; +class CPVRClients; +class CPVRDatabase; +class CPVRGUIInfo; +class CPVRGUIProgressHandler; +class CPVRManagerJobQueue; +class CPVRPlaybackState; +class CPVRRecording; +class CPVRRecordings; +class CPVRTimers; + +enum class PVREvent +{ + // PVR Manager states + ManagerError = 0, + ManagerStopped, + ManagerStarting, + ManagerStopping, + ManagerInterrupted, + ManagerStarted, + + // Channel events + ChannelPlaybackStopped, + + // Channel group events + ChannelGroup, + ChannelGroupInvalidated, + ChannelGroupsInvalidated, + + // Recording events + RecordingsInvalidated, + + // Timer events + AnnounceReminder, + Timers, + TimersInvalidated, + + // Client events + ClientsInvalidated, + + // EPG events + Epg, + EpgActiveItem, + EpgContainer, + EpgItemUpdate, + EpgUpdatePending, + EpgDeleted, + + // Saved searches events + SavedSearchesInvalidated, + + // Item events + CurrentItem, + + // System events + SystemSleep, + SystemWake, +}; + +class CPVRManager : private CThread, public ANNOUNCEMENT::IAnnouncer +{ +public: + /*! + * @brief Create a new CPVRManager instance, which handles all PVR related operations in XBMC. + */ + CPVRManager(); + + /*! + * @brief Stop the PVRManager and destroy all objects it created. + */ + ~CPVRManager() override; + + void Announce(ANNOUNCEMENT::AnnouncementFlag flag, + const std::string& sender, + const std::string& message, + const CVariant& data) override; + + /*! + * @brief Get a PVR component. + * @return The component. + */ + template<class T> + T& Get() + { + return *m_components->GetComponent<T>(); + } + + /*! + * @brief Get the providers container. + * @return The providers container. + */ + std::shared_ptr<CPVRProviders> Providers() const; + + /*! + * @brief Get the channel groups container. + * @return The groups container. + */ + std::shared_ptr<CPVRChannelGroupsContainer> ChannelGroups() const; + + /*! + * @brief Get the recordings container. + * @return The recordings container. + */ + std::shared_ptr<CPVRRecordings> Recordings() const; + + /*! + * @brief Get the timers container. + * @return The timers container. + */ + std::shared_ptr<CPVRTimers> Timers() const; + + /*! + * @brief Get the timers container. + * @return The timers container. + */ + std::shared_ptr<CPVRClients> Clients() const; + + /*! + * @brief Get the instance of a client that matches the given item. + * @param item The item containing a PVR recording, a PVR channel, a PVR timer or a PVR EPG event. + * @return the requested client on success, nullptr otherwise. + */ + std::shared_ptr<CPVRClient> GetClient(const CFileItem& item) const; + + /*! + * @brief Get the instance of a client that matches the given id. + * @param iClientId The id of a PVR client. + * @return the requested client on success, nullptr otherwise. + */ + std::shared_ptr<CPVRClient> GetClient(int iClientId) const; + + /*! + * @brief Get access to the pvr playback state. + * @return The playback state. + */ + std::shared_ptr<CPVRPlaybackState> PlaybackState() const; + + /*! + * @brief Get access to the epg container. + * @return The epg container. + */ + CPVREpgContainer& EpgContainer(); + + /*! + * @brief Init PVRManager. + */ + void Init(); + + /*! + * @brief Start the PVRManager, which loads all PVR data and starts some threads to update the PVR data. + */ + void Start(); + + /*! + * @brief Stop PVRManager. + */ + void Stop(bool bRestart = false); + + /*! + * @brief Stop PVRManager, unload data. + */ + void Unload(); + + /*! + * @brief Deinit PVRManager, unload data, unload addons. + */ + void Deinit(); + + /*! + * @brief Propagate event on system sleep + */ + void OnSleep(); + + /*! + * @brief Propagate event on system wake + */ + void OnWake(); + + /*! + * @brief Get the TV database. + * @return The TV database. + */ + std::shared_ptr<CPVRDatabase> GetTVDatabase() const; + + /*! + * @brief Check whether the PVRManager has fully started. + * @return True if started, false otherwise. + */ + bool IsStarted() const { return GetState() == ManagerState::STATE_STARTED; } + + /*! + * @brief Check whether EPG tags for channels have been created. + * @return True if EPG tags have been created, false otherwise. + */ + bool EpgsCreated() const; + + /*! + * @brief Inform PVR manager that playback of an item just started. + * @param item The item that started to play. + */ + void OnPlaybackStarted(const CFileItem& item); + + /*! + * @brief Inform PVR manager that playback of an item was stopped due to user interaction. + * @param item The item that stopped to play. + */ + void OnPlaybackStopped(const CFileItem& item); + + /*! + * @brief Inform PVR manager that playback of an item has stopped without user interaction. + * @param item The item that ended to play. + */ + void OnPlaybackEnded(const CFileItem& item); + + /*! + * @brief Let the background thread create epg tags for all channels. + */ + void TriggerEpgsCreate(); + + /*! + * @brief Let the background thread update the recordings list. + * @param clientId The id of the PVR client to update. + */ + void TriggerRecordingsUpdate(int clientId); + void TriggerRecordingsUpdate(); + + /*! + * @brief Let the background thread update the size for any in progress recordings. + */ + void TriggerRecordingsSizeInProgressUpdate(); + + /*! + * @brief Let the background thread update the timer list. + * @param clientId The id of the PVR client to update. + */ + void TriggerTimersUpdate(int clientId); + void TriggerTimersUpdate(); + + /*! + * @brief Let the background thread update the channel list. + * @param clientId The id of the PVR client to update. + */ + void TriggerChannelsUpdate(int clientId); + void TriggerChannelsUpdate(); + + /*! + * @brief Let the background thread update the provider list. + * @param clientId The id of the PVR client to update. + */ + void TriggerProvidersUpdate(int clientId); + void TriggerProvidersUpdate(); + + /*! + * @brief Let the background thread update the channel groups list. + * @param clientId The id of the PVR client to update. + */ + void TriggerChannelGroupsUpdate(int clientId); + void TriggerChannelGroupsUpdate(); + + /*! + * @brief Let the background thread search for all missing channel icons. + */ + void TriggerSearchMissingChannelIcons(); + + /*! + * @brief Let the background thread erase stale texture db entries and image files. + */ + void TriggerCleanupCachedImages(); + + /*! + * @brief Let the background thread search for missing channel icons for channels contained in the given group. + * @param group The channel group. + */ + void TriggerSearchMissingChannelIcons(const std::shared_ptr<CPVRChannelGroup>& group); + + /*! + * @brief Check whether names are still correct after the language settings changed. + */ + void LocalizationChanged(); + + /*! + * @brief Check if parental lock is overridden at the given moment. + * @param channel The channel to check. + * @return True if parental lock is overridden, false otherwise. + */ + bool IsParentalLocked(const std::shared_ptr<CPVRChannel>& channel) const; + + /*! + * @brief Check if parental lock is overridden at the given moment. + * @param epgTag The epg tag to check. + * @return True if parental lock is overridden, false otherwise. + */ + bool IsParentalLocked(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + + /*! + * @brief Restart the parental timer. + */ + void RestartParentalTimer(); + + /*! + * @brief Create EPG tags for all channels in internal channel groups + * @return True if EPG tags where created successfully, false otherwise + */ + bool CreateChannelEpgs(); + + /*! + * @brief Signal a connection change of a client + */ + void ConnectionStateChange(CPVRClient* client, + const std::string& connectString, + PVR_CONNECTION_STATE state, + const std::string& message); + + /*! + * @brief Query the events available for CEventStream + */ + CEventStream<PVREvent>& Events() { return m_events; } + + /*! + * @brief Publish an event + * @param state the event + */ + void PublishEvent(PVREvent state); + +protected: + /*! + * @brief PVR update and control thread. + */ + void Process() override; + +private: + /*! + * @brief Executes "pvrpowermanagement.setwakeupcmd" + */ + bool SetWakeupCommand(); + + enum class ManagerState + { + STATE_ERROR = 0, + STATE_STOPPED, + STATE_STARTING, + STATE_SSTOPPING, + STATE_INTERRUPTED, + STATE_STARTED + }; + + /*! + * @return True while the PVRManager is initialising. + */ + bool IsInitialising() const { return GetState() == ManagerState::STATE_STARTING; } + + /*! + * @brief Check whether the PVRManager has been stopped. + * @return True if stopped, false otherwise. + */ + bool IsStopped() const { return GetState() == ManagerState::STATE_STOPPED; } + + /*! + * @brief Get the current state of the PVR manager. + * @return the state. + */ + ManagerState GetState() const; + + /*! + * @brief Set the current state of the PVR manager. + * @param state the new state. + */ + void SetState(ManagerState state); + + /*! + * @brief Wait until at least one client is up. Update all data from database and the given PVR clients. + * @param stateToCheck Required state of the PVR manager while this method gets called. + */ + void UpdateComponents(ManagerState stateToCheck); + + /*! + * @brief Update all data from database and the given PVR clients. + * @param stateToCheck Required state of the PVR manager while this method gets called. + * @param progressHandler The progress handler to use for showing the different stages. + * @return True if at least one client is known and successfully loaded, false otherwise. + */ + bool UpdateComponents(ManagerState stateToCheck, + const std::unique_ptr<CPVRGUIProgressHandler>& progressHandler); + + /*! + * @brief Unload all PVR data (recordings, timers, channelgroups). + */ + void UnloadComponents(); + + /*! + * @brief Check whether the given client id belongs to a known client. + * @return True if the client is known, false otherwise. + */ + bool IsKnownClient(int clientID) const; + + /*! + * @brief Reset all properties. + */ + void ResetProperties(); + + /*! + * @brief Destroy PVRManager's objects. + */ + void Clear(); + + /*! + * @brief Continue playback on the last played channel. + */ + void TriggerPlayChannelOnStartup(); + + bool IsCurrentlyParentalLocked(const std::shared_ptr<CPVRChannel>& channel, + bool bGenerallyLocked) const; + + CEventSource<PVREvent> m_events; + + /** @name containers */ + //@{ + std::shared_ptr<CPVRProviders> m_providers; /*!< pointer to the providers container */ + std::shared_ptr<CPVRChannelGroupsContainer> + m_channelGroups; /*!< pointer to the channel groups container */ + std::shared_ptr<CPVRRecordings> m_recordings; /*!< pointer to the recordings container */ + std::shared_ptr<CPVRTimers> m_timers; /*!< pointer to the timers container */ + std::shared_ptr<CPVRClients> m_addons; /*!< pointer to the pvr addon container */ + std::unique_ptr<CPVRGUIInfo> m_guiInfo; /*!< pointer to the guiinfo data */ + std::shared_ptr<CPVRComponentRegistration> m_components; /*!< pointer to the PVR components */ + CPVREpgContainer m_epgContainer; /*!< the epg container */ + //@} + + std::vector<std::shared_ptr<CPVRClient>> m_knownClients; /*!< vector with all known clients */ + std::unique_ptr<CPVRManagerJobQueue> m_pendingUpdates; /*!< vector of pending pvr updates */ + std::shared_ptr<CPVRDatabase> m_database; /*!< the database for all PVR related data */ + mutable CCriticalSection + m_critSection; /*!< critical section for all changes to this class, except for changes to triggers */ + bool m_bFirstStart = true; /*!< true when the PVR manager was started first, false otherwise */ + bool m_bEpgsCreated = false; /*!< true if epg data for channels has been created */ + + mutable CCriticalSection m_managerStateMutex; + ManagerState m_managerState = ManagerState::STATE_STOPPED; + std::unique_ptr<CStopWatch> m_parentalTimer; + + CCriticalSection + m_startStopMutex; // mutex for protecting pvr manager's start/restart/stop sequence */ + + const std::shared_ptr<CPVRPlaybackState> m_playbackState; + CPVRGUIActionListener m_actionListener; + CPVRSettings m_settings; +}; +} // namespace PVR diff --git a/xbmc/pvr/PVRPlaybackState.cpp b/xbmc/pvr/PVRPlaybackState.cpp new file mode 100644 index 0000000..56bbd5b --- /dev/null +++ b/xbmc/pvr/PVRPlaybackState.cpp @@ -0,0 +1,566 @@ +/* + * 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 "PVRPlaybackState.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "XBDateTime.h" +#include "cores/DataCacheCore.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/Epg.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/Timer.h" +#include "utils/log.h" + +#include <algorithm> +#include <mutex> + +using namespace PVR; + +class CPVRPlaybackState::CLastWatchedUpdateTimer : public CTimer, private ITimerCallback +{ +public: + CLastWatchedUpdateTimer(CPVRPlaybackState& state, + const std::shared_ptr<CPVRChannelGroupMember>& channel, + const CDateTime& time) + : CTimer(this), m_state(state), m_channel(channel), m_time(time) + { + } + + // ITimerCallback implementation + void OnTimeout() override { m_state.UpdateLastWatched(m_channel, m_time); } + +private: + CLastWatchedUpdateTimer() = delete; + + CPVRPlaybackState& m_state; + const std::shared_ptr<CPVRChannelGroupMember> m_channel; + const CDateTime m_time; +}; + +CPVRPlaybackState::CPVRPlaybackState() = default; + +CPVRPlaybackState::~CPVRPlaybackState() = default; + +void CPVRPlaybackState::ReInit() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + Clear(); + + if (m_playingClientId != -1) + { + if (m_playingChannelUniqueId != -1) + { + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().ChannelGroups()->GetByIdFromAll(m_playingGroupId); + if (group) + m_playingChannel = group->GetByUniqueID({m_playingClientId, m_playingChannelUniqueId}); + else + CLog::LogFC(LOGERROR, LOGPVR, "Failed to obtain group with id '{}'", m_playingGroupId); + } + else if (!m_strPlayingRecordingUniqueId.empty()) + { + m_playingRecording = CServiceBroker::GetPVRManager().Recordings()->GetById( + m_playingClientId, m_strPlayingRecordingUniqueId); + } + else if (m_playingEpgTagChannelUniqueId != -1 && m_playingEpgTagUniqueId != 0) + { + const std::shared_ptr<CPVREpg> epg = + CServiceBroker::GetPVRManager().EpgContainer().GetByChannelUid( + m_playingClientId, m_playingEpgTagChannelUniqueId); + if (epg) + m_playingEpgTag = epg->GetTagByBroadcastId(m_playingEpgTagUniqueId); + } + } + + const std::shared_ptr<CPVRChannelGroupsContainer> groups = + CServiceBroker::GetPVRManager().ChannelGroups(); + const CPVRChannelGroups* groupsTV = groups->GetTV(); + const CPVRChannelGroups* groupsRadio = groups->GetRadio(); + + m_activeGroupTV = groupsTV->GetLastOpenedGroup(); + m_activeGroupRadio = groupsRadio->GetLastOpenedGroup(); + if (!m_activeGroupTV) + m_activeGroupTV = groupsTV->GetGroupAll(); + if (!m_activeGroupRadio) + m_activeGroupRadio = groupsRadio->GetGroupAll(); + + GroupMemberPair lastPlayed = groupsTV->GetLastAndPreviousToLastPlayedChannelGroupMember(); + m_lastPlayedChannelTV = lastPlayed.first; + m_previousToLastPlayedChannelTV = lastPlayed.second; + + lastPlayed = groupsRadio->GetLastAndPreviousToLastPlayedChannelGroupMember(); + m_lastPlayedChannelRadio = lastPlayed.first; + m_previousToLastPlayedChannelRadio = lastPlayed.second; +} + +void CPVRPlaybackState::ClearData() +{ + m_playingGroupId = -1; + m_playingChannelUniqueId = -1; + m_strPlayingRecordingUniqueId.clear(); + m_playingEpgTagChannelUniqueId = -1; + m_playingEpgTagUniqueId = 0; + m_playingClientId = -1; + m_strPlayingClientName.clear(); +} + +void CPVRPlaybackState::Clear() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_playingChannel.reset(); + m_playingRecording.reset(); + m_playingEpgTag.reset(); + m_lastPlayedChannelTV.reset(); + m_lastPlayedChannelRadio.reset(); + m_previousToLastPlayedChannelTV.reset(); + m_previousToLastPlayedChannelRadio.reset(); + m_lastWatchedUpdateTimer.reset(); + m_activeGroupTV.reset(); + m_activeGroupRadio.reset(); +} + +void CPVRPlaybackState::OnPlaybackStarted(const CFileItem& item) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_playingChannel.reset(); + m_playingRecording.reset(); + m_playingEpgTag.reset(); + ClearData(); + + if (item.HasPVRChannelGroupMemberInfoTag()) + { + const std::shared_ptr<CPVRChannelGroupMember> channel = item.GetPVRChannelGroupMemberInfoTag(); + + m_playingChannel = channel; + m_playingGroupId = m_playingChannel->GroupID(); + m_playingClientId = m_playingChannel->Channel()->ClientID(); + m_playingChannelUniqueId = m_playingChannel->Channel()->UniqueID(); + + SetActiveChannelGroup(channel); + + if (channel->Channel()->IsRadio()) + { + if (m_lastPlayedChannelRadio != channel) + { + m_previousToLastPlayedChannelRadio = m_lastPlayedChannelRadio; + m_lastPlayedChannelRadio = channel; + } + } + else + { + if (m_lastPlayedChannelTV != channel) + { + m_previousToLastPlayedChannelTV = m_lastPlayedChannelTV; + m_lastPlayedChannelTV = channel; + } + } + + int iLastWatchedDelay = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_PVRPLAYBACK_DELAYMARKLASTWATCHED) * + 1000; + if (iLastWatchedDelay > 0) + { + // Insert new / replace existing last watched update timer + if (m_lastWatchedUpdateTimer) + m_lastWatchedUpdateTimer->Stop(true); + + m_lastWatchedUpdateTimer.reset( + new CLastWatchedUpdateTimer(*this, channel, CDateTime::GetUTCDateTime())); + m_lastWatchedUpdateTimer->Start(std::chrono::milliseconds(iLastWatchedDelay)); + } + else + { + // Store last watched timestamp immediately + UpdateLastWatched(channel, CDateTime::GetUTCDateTime()); + } + } + else if (item.HasPVRRecordingInfoTag()) + { + m_playingRecording = item.GetPVRRecordingInfoTag(); + m_playingClientId = m_playingRecording->ClientID(); + m_strPlayingRecordingUniqueId = m_playingRecording->ClientRecordingID(); + } + else if (item.HasEPGInfoTag()) + { + m_playingEpgTag = item.GetEPGInfoTag(); + m_playingClientId = m_playingEpgTag->ClientID(); + m_playingEpgTagChannelUniqueId = m_playingEpgTag->UniqueChannelID(); + m_playingEpgTagUniqueId = m_playingEpgTag->UniqueBroadcastID(); + } + else if (item.HasPVRChannelInfoTag()) + { + CLog::LogFC(LOGERROR, LOGPVR, "Channel item without channel group member!"); + } + + if (m_playingClientId != -1) + { + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(m_playingClientId); + if (client) + m_strPlayingClientName = client->GetFriendlyName(); + } +} + +bool CPVRPlaybackState::OnPlaybackStopped(const CFileItem& item) +{ + // Playback ended due to user interaction + + std::unique_lock<CCriticalSection> lock(m_critSection); + + bool bChanged = false; + + if (item.HasPVRChannelGroupMemberInfoTag() && + item.GetPVRChannelGroupMemberInfoTag()->Channel()->ClientID() == m_playingClientId && + item.GetPVRChannelGroupMemberInfoTag()->Channel()->UniqueID() == m_playingChannelUniqueId) + { + bool bUpdateLastWatched = true; + + if (m_lastWatchedUpdateTimer) + { + if (m_lastWatchedUpdateTimer->IsRunning()) + { + // If last watched timer is still running, cancel it. Channel was not watched long enough to store the value. + m_lastWatchedUpdateTimer->Stop(true); + bUpdateLastWatched = false; + } + m_lastWatchedUpdateTimer.reset(); + } + + if (bUpdateLastWatched) + { + // If last watched timer is not running (any more), channel was watched long enough to store the value. + UpdateLastWatched(m_playingChannel, CDateTime::GetUTCDateTime()); + } + + bChanged = true; + m_playingChannel.reset(); + ClearData(); + } + else if (item.HasPVRRecordingInfoTag() && + item.GetPVRRecordingInfoTag()->ClientID() == m_playingClientId && + item.GetPVRRecordingInfoTag()->ClientRecordingID() == m_strPlayingRecordingUniqueId) + { + bChanged = true; + m_playingRecording.reset(); + ClearData(); + } + else if (item.HasEPGInfoTag() && item.GetEPGInfoTag()->ClientID() == m_playingClientId && + item.GetEPGInfoTag()->UniqueChannelID() == m_playingEpgTagChannelUniqueId && + item.GetEPGInfoTag()->UniqueBroadcastID() == m_playingEpgTagUniqueId) + { + bChanged = true; + m_playingEpgTag.reset(); + ClearData(); + } + else if (item.HasPVRChannelInfoTag()) + { + CLog::LogFC(LOGERROR, LOGPVR, "Channel item without channel group member!"); + } + + return bChanged; +} + +void CPVRPlaybackState::OnPlaybackEnded(const CFileItem& item) +{ + // Playback ended, but not due to user interaction + OnPlaybackStopped(item); +} + +bool CPVRPlaybackState::IsPlaying() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingChannel != nullptr || m_playingRecording != nullptr || m_playingEpgTag != nullptr; +} + +bool CPVRPlaybackState::IsPlayingTV() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingChannel && !m_playingChannel->Channel()->IsRadio(); +} + +bool CPVRPlaybackState::IsPlayingRadio() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingChannel && m_playingChannel->Channel()->IsRadio(); +} + +bool CPVRPlaybackState::IsPlayingEncryptedChannel() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingChannel && m_playingChannel->Channel()->IsEncrypted(); +} + +bool CPVRPlaybackState::IsPlayingRecording() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingRecording != nullptr; +} + +bool CPVRPlaybackState::IsPlayingEpgTag() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingEpgTag != nullptr; +} + +bool CPVRPlaybackState::IsPlayingChannel(int iClientID, int iUniqueChannelID) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingClientId == iClientID && m_playingChannelUniqueId == iUniqueChannelID; +} + +bool CPVRPlaybackState::IsPlayingChannel(const std::shared_ptr<CPVRChannel>& channel) const +{ + if (channel) + { + const std::shared_ptr<CPVRChannel> current = GetPlayingChannel(); + if (current && current->ClientID() == channel->ClientID() && + current->UniqueID() == channel->UniqueID()) + return true; + } + + return false; +} + +bool CPVRPlaybackState::IsPlayingRecording(const std::shared_ptr<CPVRRecording>& recording) const +{ + if (recording) + { + const std::shared_ptr<CPVRRecording> current = GetPlayingRecording(); + if (current && current->ClientID() == recording->ClientID() && + current->ClientRecordingID() == recording->ClientRecordingID()) + return true; + } + + return false; +} + +bool CPVRPlaybackState::IsPlayingEpgTag(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + if (epgTag) + { + const std::shared_ptr<CPVREpgInfoTag> current = GetPlayingEpgTag(); + if (current && current->ClientID() == epgTag->ClientID() && + current->UniqueChannelID() == epgTag->UniqueChannelID() && + current->UniqueBroadcastID() == epgTag->UniqueBroadcastID()) + return true; + } + + return false; +} + +std::shared_ptr<CPVRChannel> CPVRPlaybackState::GetPlayingChannel() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingChannel ? m_playingChannel->Channel() : std::shared_ptr<CPVRChannel>(); +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRPlaybackState::GetPlayingChannelGroupMember() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingChannel; +} + +std::shared_ptr<CPVRRecording> CPVRPlaybackState::GetPlayingRecording() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingRecording; +} + +std::shared_ptr<CPVREpgInfoTag> CPVRPlaybackState::GetPlayingEpgTag() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingEpgTag; +} + +int CPVRPlaybackState::GetPlayingChannelUniqueID() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingChannelUniqueId; +} + +std::string CPVRPlaybackState::GetPlayingClientName() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strPlayingClientName; +} + +int CPVRPlaybackState::GetPlayingClientID() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_playingClientId; +} + +bool CPVRPlaybackState::IsRecording() const +{ + return CServiceBroker::GetPVRManager().Timers()->IsRecording(); +} + +bool CPVRPlaybackState::IsRecordingOnPlayingChannel() const +{ + const std::shared_ptr<CPVRChannel> currentChannel = GetPlayingChannel(); + return currentChannel && + CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*currentChannel); +} + +bool CPVRPlaybackState::IsPlayingActiveRecording() const +{ + const std::shared_ptr<CPVRRecording> currentRecording = GetPlayingRecording(); + return currentRecording && currentRecording->IsInProgress(); +} + +bool CPVRPlaybackState::CanRecordOnPlayingChannel() const +{ + const std::shared_ptr<CPVRChannel> currentChannel = GetPlayingChannel(); + return currentChannel && currentChannel->CanRecord(); +} + +void CPVRPlaybackState::SetActiveChannelGroup(const std::shared_ptr<CPVRChannelGroup>& group) +{ + if (group) + { + if (group->IsRadio()) + m_activeGroupRadio = group; + else + m_activeGroupTV = group; + + auto duration = std::chrono::system_clock::now().time_since_epoch(); + uint64_t tsMillis = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count(); + group->SetLastOpened(tsMillis); + } +} + +void CPVRPlaybackState::SetActiveChannelGroup( + const std::shared_ptr<CPVRChannelGroupMember>& channel) +{ + const bool bRadio = channel->Channel()->IsRadio(); + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio)->GetById(channel->GroupID()); + + SetActiveChannelGroup(group); +} + +namespace +{ +std::shared_ptr<CPVRChannelGroup> GetFirstNonDeletedAndNonHiddenChannelGroup(bool bRadio) +{ + CPVRChannelGroups* groups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio); + if (groups) + { + const std::vector<std::shared_ptr<CPVRChannelGroup>> members = + groups->GetMembers(true); // exclude hidden + + const auto it = std::find_if(members.cbegin(), members.cend(), + [](const auto& group) { return !group->IsDeleted(); }); + if (it != members.cend()) + return (*it); + } + + CLog::LogFC(LOGERROR, LOGPVR, "Failed to obtain any non-deleted and non-hidden group"); + return {}; +} +} // unnamed namespace + +std::shared_ptr<CPVRChannelGroup> CPVRPlaybackState::GetActiveChannelGroup(bool bRadio) const +{ + if (bRadio) + { + if (m_activeGroupRadio && (m_activeGroupRadio->IsDeleted() || m_activeGroupRadio->IsHidden())) + { + // switch to first non-deleted and non-hidden group + const auto group = GetFirstNonDeletedAndNonHiddenChannelGroup(bRadio); + if (group) + const_cast<CPVRPlaybackState*>(this)->SetActiveChannelGroup(group); + } + return m_activeGroupRadio; + } + else + { + if (m_activeGroupTV && (m_activeGroupTV->IsDeleted() || m_activeGroupTV->IsHidden())) + { + // switch to first non-deleted and non-hidden group + const auto group = GetFirstNonDeletedAndNonHiddenChannelGroup(bRadio); + if (group) + const_cast<CPVRPlaybackState*>(this)->SetActiveChannelGroup(group); + } + return m_activeGroupTV; + } +} + +CDateTime CPVRPlaybackState::GetPlaybackTime(int iClientID, int iUniqueChannelID) const +{ + const std::shared_ptr<CPVREpgInfoTag> epgTag = GetPlayingEpgTag(); + if (epgTag && iClientID == epgTag->ClientID() && iUniqueChannelID == epgTag->UniqueChannelID()) + { + // playing an epg tag on requested channel + return epgTag->StartAsUTC() + + CDateTimeSpan(0, 0, 0, CServiceBroker::GetDataCacheCore().GetPlayTime() / 1000); + } + + // not playing / playing live / playing timeshifted + return GetChannelPlaybackTime(iClientID, iUniqueChannelID); +} + +CDateTime CPVRPlaybackState::GetChannelPlaybackTime(int iClientID, int iUniqueChannelID) const +{ + if (IsPlayingChannel(iClientID, iUniqueChannelID)) + { + // start time valid? + time_t startTime = CServiceBroker::GetDataCacheCore().GetStartTime(); + if (startTime > 0) + return CDateTime(startTime + CServiceBroker::GetDataCacheCore().GetPlayTime() / 1000); + } + + // not playing / playing live + return CDateTime::GetUTCDateTime(); +} + +void CPVRPlaybackState::UpdateLastWatched(const std::shared_ptr<CPVRChannelGroupMember>& channel, + const CDateTime& time) +{ + time_t iTime; + time.GetAsTime(iTime); + + channel->Channel()->SetLastWatched(iTime); + + // update last watched timestamp for group + const bool bRadio = channel->Channel()->IsRadio(); + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio)->GetById(channel->GroupID()); + if (group) + group->SetLastWatched(iTime); +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRPlaybackState::GetLastPlayedChannelGroupMember( + bool bRadio) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return bRadio ? m_lastPlayedChannelRadio : m_lastPlayedChannelTV; +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRPlaybackState:: + GetPreviousToLastPlayedChannelGroupMember(bool bRadio) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return bRadio ? m_previousToLastPlayedChannelRadio : m_previousToLastPlayedChannelTV; +} diff --git a/xbmc/pvr/PVRPlaybackState.h b/xbmc/pvr/PVRPlaybackState.h new file mode 100644 index 0000000..2ef7500 --- /dev/null +++ b/xbmc/pvr/PVRPlaybackState.h @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2012-2019 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" + +#include <memory> +#include <string> + +class CDateTime; +class CFileItem; + +namespace PVR +{ +class CPVRChannel; +class CPVRChannelGroup; +class CPVRChannelGroupMember; +class CPVREpgInfoTag; +class CPVRRecording; + +class CPVRPlaybackState +{ +public: + /*! + * @brief ctor. + */ + CPVRPlaybackState(); + + /*! + * @brief dtor. + */ + virtual ~CPVRPlaybackState(); + + /*! + * @brief clear instances, keep stored UIDs. + */ + void Clear(); + + /*! + * @brief re-init using stored UIDs. + */ + void ReInit(); + + /*! + * @brief Inform that playback of an item just started. + * @param item The item that started to play. + */ + void OnPlaybackStarted(const CFileItem& item); + + /*! + * @brief Inform that playback of an item was stopped due to user interaction. + * @param item The item that stopped to play. + * @return True, if the state has changed, false otherwise + */ + bool OnPlaybackStopped(const CFileItem& item); + + /*! + * @brief Inform that playback of an item has stopped without user interaction. + * @param item The item that ended to play. + */ + void OnPlaybackEnded(const CFileItem& item); + + /*! + * @brief Check if a TV channel, radio channel or recording is playing. + * @return True if it's playing, false otherwise. + */ + bool IsPlaying() const; + + /*! + * @brief Check if a TV channel is playing. + * @return True if it's playing, false otherwise. + */ + bool IsPlayingTV() const; + + /*! + * @brief Check if a radio channel is playing. + * @return True if it's playing, false otherwise. + */ + bool IsPlayingRadio() const; + + /*! + * @brief Check if a an encrypted TV or radio channel is playing. + * @return True if it's playing, false otherwise. + */ + bool IsPlayingEncryptedChannel() const; + + /*! + * @brief Check if a recording is playing. + * @return True if it's playing, false otherwise. + */ + bool IsPlayingRecording() const; + + /*! + * @brief Check if an epg tag is playing. + * @return True if it's playing, false otherwise. + */ + bool IsPlayingEpgTag() const; + + /*! + * @brief Check whether playing channel matches given uids. + * @param iClientID The client id. + * @param iUniqueChannelID The channel uid. + * @return True on match, false if there is no match or no channel is playing. + */ + bool IsPlayingChannel(int iClientID, int iUniqueChannelID) const; + + /*! + * @brief Check if the given channel is playing. + * @param channel The channel to check. + * @return True if it's playing, false otherwise. + */ + bool IsPlayingChannel(const std::shared_ptr<CPVRChannel>& channel) const; + + /*! + * @brief Check if the given recording is playing. + * @param recording The recording to check. + * @return True if it's playing, false otherwise. + */ + bool IsPlayingRecording(const std::shared_ptr<CPVRRecording>& recording) const; + + /*! + * @brief Check if the given epg tag is playing. + * @param epgTag The tag to check. + * @return True if it's playing, false otherwise. + */ + bool IsPlayingEpgTag(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + + /*! + * @brief Return the channel that is currently playing. + * @return The channel or nullptr if none is playing. + */ + std::shared_ptr<CPVRChannel> GetPlayingChannel() const; + + /*! + * @brief Return the channel group member that is currently playing. + * @return The channel group member or nullptr if none is playing. + */ + std::shared_ptr<CPVRChannelGroupMember> GetPlayingChannelGroupMember() const; + + /*! + * @brief Return the recording that is currently playing. + * @return The recording or nullptr if none is playing. + */ + std::shared_ptr<CPVRRecording> GetPlayingRecording() const; + + /*! + * @brief Return the epg tag that is currently playing. + * @return The tag or nullptr if none is playing. + */ + std::shared_ptr<CPVREpgInfoTag> GetPlayingEpgTag() const; + + /*! + * @brief Return playing channel unique identifier + * @return The channel id or -1 if not present + */ + int GetPlayingChannelUniqueID() const; + + /*! + * @brief Get the name of the playing client, if there is one. + * @return The name of the client or an empty string if nothing is playing. + */ + std::string GetPlayingClientName() const; + + /*! + * @brief Get the ID of the playing client, if there is one. + * @return The ID or -1 if no client is playing. + */ + int GetPlayingClientID() const; + + /*! + * @brief Check whether there are active recordings. + * @return True if there are active recordings, false otherwise. + */ + bool IsRecording() const; + + /*! + * @brief Check whether there is an active recording on the currenlyt playing channel. + * @return True if there is a playing channel and there is an active recording on that channel, false otherwise. + */ + bool IsRecordingOnPlayingChannel() const; + + /*! + * @brief Check if an active recording is playing. + * @return True if an in-progress (active) recording is playing, false otherwise. + */ + bool IsPlayingActiveRecording() const; + + /*! + * @brief Check whether the currently playing channel can be recorded. + * @return True if there is a playing channel that can be recorded, false otherwise. + */ + bool CanRecordOnPlayingChannel() const; + + /*! + * @brief Set the active channel group. + * @param group The new group. + */ + void SetActiveChannelGroup(const std::shared_ptr<CPVRChannelGroup>& group); + + /*! + * @brief Get the active channel group. + * @param bRadio True to get the active radio group, false to get the active TV group. + * @return The current group or the group containing all channels if it's not set. + */ + std::shared_ptr<CPVRChannelGroup> GetActiveChannelGroup(bool bRadio) const; + + /*! + * @brief Get the last played channel group member. + * @param bRadio True to get the radio group member, false to get the TV group member. + * @return The last played group member or nullptr if it's not available. + */ + std::shared_ptr<CPVRChannelGroupMember> GetLastPlayedChannelGroupMember(bool bRadio) const; + + /*! + * @brief Get the channel group member that was played before the last played member. + * @param bRadio True to get the radio group member, false to get the TV group member. + * @return The previous played group member or nullptr if it's not available. + */ + std::shared_ptr<CPVRChannelGroupMember> GetPreviousToLastPlayedChannelGroupMember( + bool bRadio) const; + + /*! + * @brief Get current playback time for the given channel, taking timeshifting and playing + * epg tags into account. + * @param iClientID The client id. + * @param iUniqueChannelID The channel uid. + * @return The playback time or 'now' if not playing. + */ + CDateTime GetPlaybackTime(int iClientID, int iUniqueChannelID) const; + + /*! + * @brief Get current playback time for the given channel, taking timeshifting into account. + * @param iClientID The client id. + * @param iUniqueChannelID The channel uid. + * @return The playback time or 'now' if not playing. + */ + CDateTime GetChannelPlaybackTime(int iClientID, int iUniqueChannelID) const; + +private: + void ClearData(); + + /*! + * @brief Set the active group to the group of the supplied channel group member. + * @param channel The channel group member + */ + void SetActiveChannelGroup(const std::shared_ptr<CPVRChannelGroupMember>& channel); + + /*! + * @brief Updates the last watched timestamps of the channel and group which are currently playing. + * @param channel The channel which is updated + * @param time The last watched time to set + */ + void UpdateLastWatched(const std::shared_ptr<CPVRChannelGroupMember>& channel, + const CDateTime& time); + + mutable CCriticalSection m_critSection; + + std::shared_ptr<CPVRChannelGroupMember> m_playingChannel; + std::shared_ptr<CPVRRecording> m_playingRecording; + std::shared_ptr<CPVREpgInfoTag> m_playingEpgTag; + std::shared_ptr<CPVRChannelGroupMember> m_lastPlayedChannelTV; + std::shared_ptr<CPVRChannelGroupMember> m_lastPlayedChannelRadio; + std::shared_ptr<CPVRChannelGroupMember> m_previousToLastPlayedChannelTV; + std::shared_ptr<CPVRChannelGroupMember> m_previousToLastPlayedChannelRadio; + std::string m_strPlayingClientName; + int m_playingGroupId = -1; + int m_playingClientId = -1; + int m_playingChannelUniqueId = -1; + std::string m_strPlayingRecordingUniqueId; + int m_playingEpgTagChannelUniqueId = -1; + unsigned int m_playingEpgTagUniqueId = 0; + std::shared_ptr<CPVRChannelGroup> m_activeGroupTV; + std::shared_ptr<CPVRChannelGroup> m_activeGroupRadio; + + class CLastWatchedUpdateTimer; + std::unique_ptr<CLastWatchedUpdateTimer> m_lastWatchedUpdateTimer; +}; +} // namespace PVR diff --git a/xbmc/pvr/PVRStreamProperties.cpp b/xbmc/pvr/PVRStreamProperties.cpp new file mode 100644 index 0000000..272b33a --- /dev/null +++ b/xbmc/pvr/PVRStreamProperties.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012-2019 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 "PVRStreamProperties.h" + +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_general.h" +#include "utils/StringUtils.h" + +#include <algorithm> + +using namespace PVR; + +std::string CPVRStreamProperties::GetStreamURL() const +{ + const auto it = std::find_if(cbegin(), cend(), [](const auto& prop) { + return prop.first == PVR_STREAM_PROPERTY_STREAMURL; + }); + return it != cend() ? (*it).second : std::string(); +} + +std::string CPVRStreamProperties::GetStreamMimeType() const +{ + const auto it = std::find_if(cbegin(), cend(), [](const auto& prop) { + return prop.first == PVR_STREAM_PROPERTY_MIMETYPE; + }); + return it != cend() ? (*it).second : std::string(); +} + +bool CPVRStreamProperties::EPGPlaybackAsLive() const +{ + const auto it = std::find_if(cbegin(), cend(), [](const auto& prop) { + return prop.first == PVR_STREAM_PROPERTY_EPGPLAYBACKASLIVE; + }); + return it != cend() ? StringUtils::EqualsNoCase((*it).second, "true") : false; +} diff --git a/xbmc/pvr/PVRStreamProperties.h b/xbmc/pvr/PVRStreamProperties.h new file mode 100644 index 0000000..6690660 --- /dev/null +++ b/xbmc/pvr/PVRStreamProperties.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2012-2019 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> +#include <utility> +#include <vector> + +namespace PVR +{ + +class CPVRStreamProperties : public std::vector<std::pair<std::string, std::string>> +{ +public: + CPVRStreamProperties() = default; + virtual ~CPVRStreamProperties() = default; + + /*! + * @brief Obtain the URL of the stream. + * @return The stream URL or empty string, if not found. + */ + std::string GetStreamURL() const; + + /*! + * @brief Obtain the MIME type of the stream. + * @return The stream's MIME type or empty string, if not found. + */ + std::string GetStreamMimeType() const; + + /*! + * @brief If props are from an EPG tag indicates if playback should be as live playback would be + * @return true if it should be played back as live, false otherwise. + */ + bool EPGPlaybackAsLive() const; +}; + +} // namespace PVR diff --git a/xbmc/pvr/PVRThumbLoader.cpp b/xbmc/pvr/PVRThumbLoader.cpp new file mode 100644 index 0000000..4b3544c --- /dev/null +++ b/xbmc/pvr/PVRThumbLoader.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRThumbLoader.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "TextureCacheJob.h" +#include "pictures/Picture.h" +#include "pvr/PVRCachedImages.h" +#include "pvr/PVRManager.h" +#include "pvr/filesystem/PVRGUIDirectory.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <ctime> + +using namespace PVR; + +bool CPVRThumbLoader::LoadItem(CFileItem* item) +{ + bool result = LoadItemCached(item); + result |= LoadItemLookup(item); + return result; +} + +bool CPVRThumbLoader::LoadItemCached(CFileItem* item) +{ + return FillThumb(*item); +} + +bool CPVRThumbLoader::LoadItemLookup(CFileItem* item) +{ + return false; +} + +void CPVRThumbLoader::OnLoaderFinish() +{ + if (m_bInvalidated) + { + m_bInvalidated = false; + CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated); + } + CThumbLoader::OnLoaderFinish(); +} + +void CPVRThumbLoader::ClearCachedImage(CFileItem& item) +{ + const std::string thumb = item.GetArt("thumb"); + if (!thumb.empty()) + { + CServiceBroker::GetTextureCache()->ClearCachedImage(thumb); + if (m_textureDatabase->Open()) + { + m_textureDatabase->ClearTextureForPath(item.GetPath(), "thumb"); + m_textureDatabase->Close(); + } + item.SetArt("thumb", ""); + m_bInvalidated = true; + } +} + +void CPVRThumbLoader::ClearCachedImages(const CFileItemList& items) +{ + for (auto& item : items) + ClearCachedImage(*item); +} + +bool CPVRThumbLoader::FillThumb(CFileItem& item) +{ + // see whether we have a cached image for this item + std::string thumb = GetCachedImage(item, "thumb"); + if (thumb.empty()) + { + if (item.IsPVRChannelGroup()) + thumb = CreateChannelGroupThumb(item); + else + CLog::LogF(LOGERROR, "Unsupported PVR item '{}'", item.GetPath()); + + if (!thumb.empty()) + { + SetCachedImage(item, "thumb", thumb); + m_bInvalidated = true; + } + } + + if (thumb.empty()) + return false; + + item.SetArt("thumb", thumb); + return true; +} + +std::string CPVRThumbLoader::CreateChannelGroupThumb(const CFileItem& channelGroupItem) +{ + const CPVRGUIDirectory channelGroupDir(channelGroupItem.GetPath()); + CFileItemList channels; + if (channelGroupDir.GetChannelsDirectory(channels)) + { + std::vector<std::string> channelIcons; + for (const auto& channel : channels) + { + const std::string& icon = channel->GetArt("icon"); + if (!icon.empty()) + channelIcons.emplace_back(CPVRCachedImages::UnwrapImageURL(icon)); + + if (channelIcons.size() == 9) // limit number of tiles + break; + } + + std::string thumb = StringUtils::Format( + "{}?ts={}", // append timestamp to Thumb URL to enforce texture refresh + CTextureUtils::GetWrappedImageURL(channelGroupItem.GetPath(), "pvr"), std::time(nullptr)); + const std::string relativeCacheFile = CTextureCache::GetCacheFile(thumb) + ".png"; + if (CPicture::CreateTiledThumb(channelIcons, CTextureCache::GetCachedPath(relativeCacheFile))) + { + CTextureDetails details; + details.file = relativeCacheFile; + details.width = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes; + details.height = details.width; + CServiceBroker::GetTextureCache()->AddCachedTexture(thumb, details); + return thumb; + } + } + + return {}; +} diff --git a/xbmc/pvr/PVRThumbLoader.h b/xbmc/pvr/PVRThumbLoader.h new file mode 100644 index 0000000..33e1bd4 --- /dev/null +++ b/xbmc/pvr/PVRThumbLoader.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ThumbLoader.h" + +#include <string> + +namespace PVR +{ + +class CPVRThumbLoader : public CThumbLoader +{ +public: + CPVRThumbLoader() = default; + ~CPVRThumbLoader() override = default; + + bool LoadItem(CFileItem* item) override; + bool LoadItemCached(CFileItem* item) override; + bool LoadItemLookup(CFileItem* item) override; + + void ClearCachedImage(CFileItem& item); + void ClearCachedImages(const CFileItemList& items); + +protected: + void OnLoaderFinish() override; + +private: + bool FillThumb(CFileItem& item); + std::string CreateChannelGroupThumb(const CFileItem& channelGroupItem); + + bool m_bInvalidated = false; +}; + +} diff --git a/xbmc/pvr/addons/CMakeLists.txt b/xbmc/pvr/addons/CMakeLists.txt new file mode 100644 index 0000000..e8a0a51 --- /dev/null +++ b/xbmc/pvr/addons/CMakeLists.txt @@ -0,0 +1,13 @@ +set(SOURCES PVRClient.cpp + PVRClientCapabilities.cpp + PVRClientMenuHooks.cpp + PVRClientUID.cpp + PVRClients.cpp) + +set(HEADERS PVRClient.h + PVRClientCapabilities.h + PVRClientMenuHooks.h + PVRClientUID.h + PVRClients.h) + +core_add_library(pvr_addons) diff --git a/xbmc/pvr/addons/PVRClient.cpp b/xbmc/pvr/addons/PVRClient.cpp new file mode 100644 index 0000000..2a09277 --- /dev/null +++ b/xbmc/pvr/addons/PVRClient.cpp @@ -0,0 +1,2023 @@ +/* + * 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 "PVRClient.h" + +#include "ServiceBroker.h" +#include "addons/AddonManager.h" +#include "addons/binary-addons/AddonDll.h" +#include "cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.h" +#include "dialogs/GUIDialogKaiToast.h" //! @todo get rid of GUI in core +#include "events/EventLog.h" +#include "events/NotificationEvent.h" +#include "filesystem/SpecialProtocol.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRStreamProperties.h" +#include "pvr/addons/PVRClientMenuHooks.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupInternal.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/Epg.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/providers/PVRProvider.h" +#include "pvr/providers/PVRProviders.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimerType.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <map> +#include <memory> +#include <mutex> +#include <string> +#include <utility> + +extern "C" +{ +#include <libavcodec/avcodec.h> +} + +using namespace ADDON; + +namespace PVR +{ + +#define DEFAULT_INFO_STRING_VALUE "unknown" + +CPVRClient::CPVRClient(const ADDON::AddonInfoPtr& addonInfo, + ADDON::AddonInstanceId instanceId, + int clientId) + : IAddonInstanceHandler(ADDON_INSTANCE_PVR, addonInfo, instanceId), m_iClientId(clientId) +{ + // Create all interface parts independent to make API changes easier if + // something is added + m_ifc.pvr = new AddonInstance_PVR; + m_ifc.pvr->props = new AddonProperties_PVR(); + m_ifc.pvr->toKodi = new AddonToKodiFuncTable_PVR(); + m_ifc.pvr->toAddon = new KodiToAddonFuncTable_PVR(); + + ResetProperties(); +} + +CPVRClient::~CPVRClient() +{ + Destroy(); + + if (m_ifc.pvr) + { + delete m_ifc.pvr->props; + delete m_ifc.pvr->toKodi; + delete m_ifc.pvr->toAddon; + } + delete m_ifc.pvr; +} + +void CPVRClient::StopRunningInstance() +{ + // stop the pvr manager and stop and unload the running pvr addon. pvr manager will be restarted on demand. + CServiceBroker::GetPVRManager().Stop(); + CServiceBroker::GetPVRManager().Clients()->StopClient(m_iClientId, false); +} + +void CPVRClient::OnPreInstall() +{ + // note: this method is also called on update; thus stop and unload possibly running instance + StopRunningInstance(); +} + +void CPVRClient::OnPreUnInstall() +{ + StopRunningInstance(); +} + +void CPVRClient::ResetProperties() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + /* initialise members */ + m_strUserPath = CSpecialProtocol::TranslatePath(Profile()); + m_strClientPath = CSpecialProtocol::TranslatePath(Path()); + m_bReadyToUse = false; + m_bBlockAddonCalls = false; + m_iAddonCalls = 0; + m_allAddonCallsFinished.Set(); + m_connectionState = PVR_CONNECTION_STATE_UNKNOWN; + m_prevConnectionState = PVR_CONNECTION_STATE_UNKNOWN; + m_ignoreClient = false; + m_iPriority = 0; + m_bPriorityFetched = false; + m_strBackendVersion = DEFAULT_INFO_STRING_VALUE; + m_strConnectionString = DEFAULT_INFO_STRING_VALUE; + m_strBackendName = DEFAULT_INFO_STRING_VALUE; + m_strBackendHostname.clear(); + m_menuhooks.reset(); + m_timertypes.clear(); + m_clientCapabilities.clear(); + + m_ifc.pvr->props->strUserPath = m_strUserPath.c_str(); + m_ifc.pvr->props->strClientPath = m_strClientPath.c_str(); + m_ifc.pvr->props->iEpgMaxPastDays = + CServiceBroker::GetPVRManager().EpgContainer().GetPastDaysToDisplay(); + m_ifc.pvr->props->iEpgMaxFutureDays = + CServiceBroker::GetPVRManager().EpgContainer().GetFutureDaysToDisplay(); + + m_ifc.pvr->toKodi->kodiInstance = this; + m_ifc.pvr->toKodi->TransferEpgEntry = cb_transfer_epg_entry; + m_ifc.pvr->toKodi->TransferChannelEntry = cb_transfer_channel_entry; + m_ifc.pvr->toKodi->TransferProviderEntry = cb_transfer_provider_entry; + m_ifc.pvr->toKodi->TransferTimerEntry = cb_transfer_timer_entry; + m_ifc.pvr->toKodi->TransferRecordingEntry = cb_transfer_recording_entry; + m_ifc.pvr->toKodi->AddMenuHook = cb_add_menu_hook; + m_ifc.pvr->toKodi->RecordingNotification = cb_recording_notification; + m_ifc.pvr->toKodi->TriggerChannelUpdate = cb_trigger_channel_update; + m_ifc.pvr->toKodi->TriggerProvidersUpdate = cb_trigger_provider_update; + m_ifc.pvr->toKodi->TriggerChannelGroupsUpdate = cb_trigger_channel_groups_update; + m_ifc.pvr->toKodi->TriggerTimerUpdate = cb_trigger_timer_update; + m_ifc.pvr->toKodi->TriggerRecordingUpdate = cb_trigger_recording_update; + m_ifc.pvr->toKodi->TriggerEpgUpdate = cb_trigger_epg_update; + m_ifc.pvr->toKodi->FreeDemuxPacket = cb_free_demux_packet; + m_ifc.pvr->toKodi->AllocateDemuxPacket = cb_allocate_demux_packet; + m_ifc.pvr->toKodi->TransferChannelGroup = cb_transfer_channel_group; + m_ifc.pvr->toKodi->TransferChannelGroupMember = cb_transfer_channel_group_member; + m_ifc.pvr->toKodi->ConnectionStateChange = cb_connection_state_change; + m_ifc.pvr->toKodi->EpgEventStateChange = cb_epg_event_state_change; + m_ifc.pvr->toKodi->GetCodecByName = cb_get_codec_by_name; + + // Clear function addresses to have NULL if not set by addon + memset(m_ifc.pvr->toAddon, 0, sizeof(KodiToAddonFuncTable_PVR)); +} + +ADDON_STATUS CPVRClient::Create() +{ + ADDON_STATUS status(ADDON_STATUS_UNKNOWN); + + ResetProperties(); + + /* initialise the add-on */ + CLog::LogFC(LOGDEBUG, LOGPVR, "Creating PVR add-on instance '{}'", ID()); + + bool bReadyToUse = false; + if ((status = CreateInstance()) == ADDON_STATUS_OK) + bReadyToUse = GetAddonProperties(); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Created PVR add-on instance '{}'. readytouse={} ", ID(), + bReadyToUse); + + m_bReadyToUse = bReadyToUse; + return status; +} + +void CPVRClient::Destroy() +{ + if (!m_bReadyToUse) + return; + + m_bReadyToUse = false; + + CLog::LogFC(LOGDEBUG, LOGPVR, "Destroying PVR add-on instance '{}'", ID()); + + m_bBlockAddonCalls = true; + m_allAddonCallsFinished.Wait(); + + DestroyInstance(); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Destroyed PVR add-on instance '{}'", ID()); + + if (m_menuhooks) + m_menuhooks->Clear(); + + ResetProperties(); +} + +void CPVRClient::Stop() +{ + m_bBlockAddonCalls = true; + m_bPriorityFetched = false; +} + +void CPVRClient::Continue() +{ + m_bBlockAddonCalls = false; +} + +void CPVRClient::ReCreate() +{ + Destroy(); + Create(); +} + +bool CPVRClient::ReadyToUse() const +{ + return m_bReadyToUse; +} + +PVR_CONNECTION_STATE CPVRClient::GetConnectionState() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_connectionState; +} + +void CPVRClient::SetConnectionState(PVR_CONNECTION_STATE state) +{ + if (state == PVR_CONNECTION_STATE_CONNECTED) + { + // update properties - some will only be available after add-on is connected to backend + if (!GetAddonProperties()) + CLog::LogF(LOGERROR, "Error reading PVR client properties"); + } + else + { + if (!GetAddonNameStringProperties()) + CLog::LogF(LOGERROR, "Cannot read PVR client name string properties"); + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_prevConnectionState = m_connectionState; + m_connectionState = state; + + if (m_connectionState == PVR_CONNECTION_STATE_CONNECTED) + m_ignoreClient = false; + else if (m_connectionState == PVR_CONNECTION_STATE_CONNECTING && + m_prevConnectionState == PVR_CONNECTION_STATE_UNKNOWN) + m_ignoreClient = true; // ignore until connected +} + +PVR_CONNECTION_STATE CPVRClient::GetPreviousConnectionState() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_prevConnectionState; +} + +bool CPVRClient::IgnoreClient() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_ignoreClient; +} + +bool CPVRClient::IsEnabled() const +{ + if (InstanceId() == ADDON_SINGLETON_INSTANCE_ID) + { + return !CServiceBroker::GetAddonMgr().IsAddonDisabled(ID()); + } + else + { + bool instanceEnabled{false}; + Addon()->ReloadSettings(InstanceId()); + Addon()->GetSettingBool(ADDON_SETTING_INSTANCE_ENABLED_VALUE, instanceEnabled, InstanceId()); + return instanceEnabled; + } +} + +int CPVRClient::GetID() const +{ + return m_iClientId; +} + +bool CPVRClient::GetAddonProperties() +{ + if (!GetAddonNameStringProperties()) + return false; + + PVR_ADDON_CAPABILITIES addonCapabilities = {}; + std::vector<std::shared_ptr<CPVRTimerType>> timerTypes; + + /* get the capabilities */ + PVR_ERROR retVal = DoAddonCall( + __func__, + [&addonCapabilities](const AddonInstance* addon) { + return addon->toAddon->GetCapabilities(addon, &addonCapabilities); + }, + true, false); + + if (retVal != PVR_ERROR_NO_ERROR) + return false; + + /* timer types */ + retVal = DoAddonCall( + __func__, + [this, &addonCapabilities, &timerTypes](const AddonInstance* addon) { + std::unique_ptr<PVR_TIMER_TYPE[]> types_array( + new PVR_TIMER_TYPE[PVR_ADDON_TIMERTYPE_ARRAY_SIZE]); + int size = PVR_ADDON_TIMERTYPE_ARRAY_SIZE; + + PVR_ERROR retval = addon->toAddon->GetTimerTypes(addon, types_array.get(), &size); + + if (retval == PVR_ERROR_NOT_IMPLEMENTED) + { + // begin compat section + CLog::LogF(LOGWARNING, + "Add-on {} does not support timer types. It will work, but not benefit from " + "the timer features introduced with PVR Addon API 2.0.0", + GetFriendlyName()); + + // Create standard timer types (mostly) matching the timer functionality available in Isengard. + // This is for migration only and does not make changes to the addons obsolete. Addons should + // work and benefit from some UI changes (e.g. some of the timer settings dialog enhancements), + // but all old problems/bugs due to static attributes and values will remain the same as in + // Isengard. Also, new features (like epg search) are not available to addons automatically. + // This code can be removed once all addons actually support the respective PVR Addon API version. + + size = 0; + // manual one time + memset(&types_array[size], 0, sizeof(types_array[size])); + types_array[size].iId = size + 1; + types_array[size].iAttributes = + PVR_TIMER_TYPE_IS_MANUAL | PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE | + PVR_TIMER_TYPE_SUPPORTS_CHANNELS | PVR_TIMER_TYPE_SUPPORTS_START_TIME | + PVR_TIMER_TYPE_SUPPORTS_END_TIME | PVR_TIMER_TYPE_SUPPORTS_PRIORITY | + PVR_TIMER_TYPE_SUPPORTS_LIFETIME | PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS; + ++size; + + // manual timer rule + memset(&types_array[size], 0, sizeof(types_array[size])); + types_array[size].iId = size + 1; + types_array[size].iAttributes = + PVR_TIMER_TYPE_IS_MANUAL | PVR_TIMER_TYPE_IS_REPEATING | + PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE | PVR_TIMER_TYPE_SUPPORTS_CHANNELS | + PVR_TIMER_TYPE_SUPPORTS_START_TIME | PVR_TIMER_TYPE_SUPPORTS_END_TIME | + PVR_TIMER_TYPE_SUPPORTS_PRIORITY | PVR_TIMER_TYPE_SUPPORTS_LIFETIME | + PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY | PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS | + PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS; + ++size; + + if (addonCapabilities.bSupportsEPG) + { + // One-shot epg-based + memset(&types_array[size], 0, sizeof(types_array[size])); + types_array[size].iId = size + 1; + types_array[size].iAttributes = + PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE | PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE | + PVR_TIMER_TYPE_SUPPORTS_CHANNELS | PVR_TIMER_TYPE_SUPPORTS_START_TIME | + PVR_TIMER_TYPE_SUPPORTS_END_TIME | PVR_TIMER_TYPE_SUPPORTS_PRIORITY | + PVR_TIMER_TYPE_SUPPORTS_LIFETIME | PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS; + ++size; + } + + retval = PVR_ERROR_NO_ERROR; + // end compat section + } + + if (retval == PVR_ERROR_NO_ERROR) + { + timerTypes.reserve(size); + for (int i = 0; i < size; ++i) + { + if (types_array[i].iId == PVR_TIMER_TYPE_NONE) + { + CLog::LogF(LOGERROR, "Invalid timer type supplied by add-on '{}'.", ID()); + continue; + } + timerTypes.emplace_back( + std::shared_ptr<CPVRTimerType>(new CPVRTimerType(types_array[i], m_iClientId))); + } + } + return retval; + }, + addonCapabilities.bSupportsTimers, false); + + if (retVal == PVR_ERROR_NOT_IMPLEMENTED) + retVal = PVR_ERROR_NO_ERROR; // timer support is optional. + + /* update the members */ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_clientCapabilities = addonCapabilities; + m_timertypes = timerTypes; + + return retVal == PVR_ERROR_NO_ERROR; +} + +bool CPVRClient::GetAddonNameStringProperties() +{ + char strBackendName[PVR_ADDON_NAME_STRING_LENGTH] = {}; + char strConnectionString[PVR_ADDON_NAME_STRING_LENGTH] = {}; + char strBackendVersion[PVR_ADDON_NAME_STRING_LENGTH] = {}; + char strBackendHostname[PVR_ADDON_NAME_STRING_LENGTH] = {}; + + /* get the name of the backend */ + PVR_ERROR retVal = DoAddonCall( + __func__, + [&strBackendName](const AddonInstance* addon) { + return addon->toAddon->GetBackendName(addon, strBackendName, sizeof(strBackendName)); + }, + true, false); + + if (retVal != PVR_ERROR_NO_ERROR) + return false; + + /* get the connection string */ + retVal = DoAddonCall( + __func__, + [&strConnectionString](const AddonInstance* addon) { + return addon->toAddon->GetConnectionString(addon, strConnectionString, + sizeof(strConnectionString)); + }, + true, false); + + if (retVal != PVR_ERROR_NO_ERROR && retVal != PVR_ERROR_NOT_IMPLEMENTED) + return false; + + /* backend version number */ + retVal = DoAddonCall( + __func__, + [&strBackendVersion](const AddonInstance* addon) { + return addon->toAddon->GetBackendVersion(addon, strBackendVersion, + sizeof(strBackendVersion)); + }, + true, false); + + if (retVal != PVR_ERROR_NO_ERROR) + return false; + + /* backend hostname */ + retVal = DoAddonCall( + __func__, + [&strBackendHostname](const AddonInstance* addon) { + return addon->toAddon->GetBackendHostname(addon, strBackendHostname, + sizeof(strBackendHostname)); + }, + true, false); + + if (retVal != PVR_ERROR_NO_ERROR && retVal != PVR_ERROR_NOT_IMPLEMENTED) + return false; + + /* update the members */ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strBackendName = strBackendName; + m_strConnectionString = strConnectionString; + m_strBackendVersion = strBackendVersion; + m_strBackendHostname = strBackendHostname; + + return true; +} + +const std::string& CPVRClient::GetBackendName() const +{ + return m_strBackendName; +} + +const std::string& CPVRClient::GetBackendVersion() const +{ + return m_strBackendVersion; +} + +const std::string& CPVRClient::GetBackendHostname() const +{ + return m_strBackendHostname; +} + +const std::string& CPVRClient::GetConnectionString() const +{ + return m_strConnectionString; +} + +const std::string CPVRClient::GetFriendlyName() const +{ + + if (Addon()->SupportsInstanceSettings()) + { + std::string instanceName; + Addon()->GetSettingString(ADDON_SETTING_INSTANCE_NAME_VALUE, instanceName, InstanceId()); + if (!instanceName.empty()) + return StringUtils::Format("{} ({})", Name(), instanceName); + } + return Name(); +} + +PVR_ERROR CPVRClient::GetDriveSpace(uint64_t& iTotal, uint64_t& iUsed) +{ + /* default to 0 in case of error */ + iTotal = 0; + iUsed = 0; + + return DoAddonCall(__func__, [&iTotal, &iUsed](const AddonInstance* addon) { + uint64_t iTotalSpace = 0; + uint64_t iUsedSpace = 0; + PVR_ERROR error = addon->toAddon->GetDriveSpace(addon, &iTotalSpace, &iUsedSpace); + if (error == PVR_ERROR_NO_ERROR) + { + iTotal = iTotalSpace; + iUsed = iUsedSpace; + } + return error; + }); +} + +PVR_ERROR CPVRClient::StartChannelScan() +{ + return DoAddonCall( + __func__, + [](const AddonInstance* addon) { return addon->toAddon->OpenDialogChannelScan(addon); }, + m_clientCapabilities.SupportsChannelScan()); +} + +PVR_ERROR CPVRClient::OpenDialogChannelAdd(const std::shared_ptr<CPVRChannel>& channel) +{ + return DoAddonCall( + __func__, + [channel](const AddonInstance* addon) { + PVR_CHANNEL addonChannel; + channel->FillAddonData(addonChannel); + return addon->toAddon->OpenDialogChannelAdd(addon, &addonChannel); + }, + m_clientCapabilities.SupportsChannelSettings()); +} + +PVR_ERROR CPVRClient::OpenDialogChannelSettings(const std::shared_ptr<CPVRChannel>& channel) +{ + return DoAddonCall( + __func__, + [channel](const AddonInstance* addon) { + PVR_CHANNEL addonChannel; + channel->FillAddonData(addonChannel); + return addon->toAddon->OpenDialogChannelSettings(addon, &addonChannel); + }, + m_clientCapabilities.SupportsChannelSettings()); +} + +PVR_ERROR CPVRClient::DeleteChannel(const std::shared_ptr<CPVRChannel>& channel) +{ + return DoAddonCall( + __func__, + [channel](const AddonInstance* addon) { + PVR_CHANNEL addonChannel; + channel->FillAddonData(addonChannel); + return addon->toAddon->DeleteChannel(addon, &addonChannel); + }, + m_clientCapabilities.SupportsChannelSettings()); +} + +PVR_ERROR CPVRClient::RenameChannel(const std::shared_ptr<CPVRChannel>& channel) +{ + return DoAddonCall( + __func__, + [channel](const AddonInstance* addon) { + PVR_CHANNEL addonChannel; + channel->FillAddonData(addonChannel); + strncpy(addonChannel.strChannelName, channel->ChannelName().c_str(), + sizeof(addonChannel.strChannelName) - 1); + return addon->toAddon->RenameChannel(addon, &addonChannel); + }, + m_clientCapabilities.SupportsChannelSettings()); +} + +PVR_ERROR CPVRClient::GetEPGForChannel(int iChannelUid, CPVREpg* epg, time_t start, time_t end) +{ + return DoAddonCall( + __func__, + [this, iChannelUid, epg, start, end](const AddonInstance* addon) { + PVR_HANDLE_STRUCT handle = {}; + handle.callerAddress = this; + handle.dataAddress = epg; + + int iPVRTimeCorrection = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection; + + return addon->toAddon->GetEPGForChannel(addon, &handle, iChannelUid, + start ? start - iPVRTimeCorrection : 0, + end ? end - iPVRTimeCorrection : 0); + }, + m_clientCapabilities.SupportsEPG()); +} + +PVR_ERROR CPVRClient::SetEPGMaxPastDays(int iPastDays) +{ + return DoAddonCall( + __func__, + [iPastDays](const AddonInstance* addon) { + return addon->toAddon->SetEPGMaxPastDays(addon, iPastDays); + }, + m_clientCapabilities.SupportsEPG()); +} + +PVR_ERROR CPVRClient::SetEPGMaxFutureDays(int iFutureDays) +{ + return DoAddonCall( + __func__, + [iFutureDays](const AddonInstance* addon) { + return addon->toAddon->SetEPGMaxFutureDays(addon, iFutureDays); + }, + m_clientCapabilities.SupportsEPG()); +} + +// This class wraps an EPG_TAG (PVR Addon API struct) to ensure that the string members of +// that struct, which are const char pointers, stay valid until the EPG_TAG gets destructed. +// Please note that this struct is also used to transfer huge amount of EPG_TAGs from +// addon to Kodi. Thus, changing the struct to contain char arrays is not recommended, +// because this would lead to huge amount of string copies when transferring epg data +// from addon to Kodi. +class CAddonEpgTag : public EPG_TAG +{ +public: + CAddonEpgTag() = delete; + explicit CAddonEpgTag(const std::shared_ptr<const CPVREpgInfoTag>& kodiTag) + : m_strTitle(kodiTag->Title()), + m_strPlotOutline(kodiTag->PlotOutline()), + m_strPlot(kodiTag->Plot()), + m_strOriginalTitle(kodiTag->OriginalTitle()), + m_strCast(kodiTag->DeTokenize(kodiTag->Cast())), + m_strDirector(kodiTag->DeTokenize(kodiTag->Directors())), + m_strWriter(kodiTag->DeTokenize(kodiTag->Writers())), + m_strIMDBNumber(kodiTag->IMDBNumber()), + m_strEpisodeName(kodiTag->EpisodeName()), + m_strIconPath(kodiTag->ClientIconPath()), + m_strSeriesLink(kodiTag->SeriesLink()), + m_strGenreDescription(kodiTag->GenreDescription()), + m_strParentalRatingCode(kodiTag->ParentalRatingCode()) + { + time_t t; + kodiTag->StartAsUTC().GetAsTime(t); + startTime = t; + kodiTag->EndAsUTC().GetAsTime(t); + endTime = t; + + const CDateTime firstAired = kodiTag->FirstAired(); + if (firstAired.IsValid()) + m_strFirstAired = firstAired.GetAsW3CDate(); + + iUniqueBroadcastId = kodiTag->UniqueBroadcastID(); + iUniqueChannelId = kodiTag->UniqueChannelID(); + iParentalRating = kodiTag->ParentalRating(); + iSeriesNumber = kodiTag->SeriesNumber(); + iEpisodeNumber = kodiTag->EpisodeNumber(); + iEpisodePartNumber = kodiTag->EpisodePart(); + iStarRating = kodiTag->StarRating(); + iYear = kodiTag->Year(); + iFlags = kodiTag->Flags(); + iGenreType = kodiTag->GenreType(); + iGenreSubType = kodiTag->GenreSubType(); + strTitle = m_strTitle.c_str(); + strPlotOutline = m_strPlotOutline.c_str(); + strPlot = m_strPlot.c_str(); + strOriginalTitle = m_strOriginalTitle.c_str(); + strCast = m_strCast.c_str(); + strDirector = m_strDirector.c_str(); + strWriter = m_strWriter.c_str(); + strIMDBNumber = m_strIMDBNumber.c_str(); + strEpisodeName = m_strEpisodeName.c_str(); + strIconPath = m_strIconPath.c_str(); + strSeriesLink = m_strSeriesLink.c_str(); + strGenreDescription = m_strGenreDescription.c_str(); + strFirstAired = m_strFirstAired.c_str(); + strParentalRatingCode = m_strParentalRatingCode.c_str(); + } + + virtual ~CAddonEpgTag() = default; + +private: + std::string m_strTitle; + std::string m_strPlotOutline; + std::string m_strPlot; + std::string m_strOriginalTitle; + std::string m_strCast; + std::string m_strDirector; + std::string m_strWriter; + std::string m_strIMDBNumber; + std::string m_strEpisodeName; + std::string m_strIconPath; + std::string m_strSeriesLink; + std::string m_strGenreDescription; + std::string m_strFirstAired; + std::string m_strParentalRatingCode; +}; + +PVR_ERROR CPVRClient::IsRecordable(const std::shared_ptr<const CPVREpgInfoTag>& tag, + bool& bIsRecordable) const +{ + return DoAddonCall( + __func__, + [tag, &bIsRecordable](const AddonInstance* addon) { + CAddonEpgTag addonTag(tag); + return addon->toAddon->IsEPGTagRecordable(addon, &addonTag, &bIsRecordable); + }, + m_clientCapabilities.SupportsRecordings() && m_clientCapabilities.SupportsEPG()); +} + +PVR_ERROR CPVRClient::IsPlayable(const std::shared_ptr<const CPVREpgInfoTag>& tag, + bool& bIsPlayable) const +{ + return DoAddonCall( + __func__, + [tag, &bIsPlayable](const AddonInstance* addon) { + CAddonEpgTag addonTag(tag); + return addon->toAddon->IsEPGTagPlayable(addon, &addonTag, &bIsPlayable); + }, + m_clientCapabilities.SupportsEPG()); +} + +void CPVRClient::WriteStreamProperties(const PVR_NAMED_VALUE* properties, + unsigned int iPropertyCount, + CPVRStreamProperties& props) +{ + for (unsigned int i = 0; i < iPropertyCount; ++i) + { + props.emplace_back(std::make_pair(properties[i].strName, properties[i].strValue)); + } +} + +PVR_ERROR CPVRClient::GetEpgTagStreamProperties(const std::shared_ptr<CPVREpgInfoTag>& tag, + CPVRStreamProperties& props) +{ + return DoAddonCall(__func__, [&tag, &props](const AddonInstance* addon) { + CAddonEpgTag addonTag(tag); + + unsigned int iPropertyCount = STREAM_MAX_PROPERTY_COUNT; + std::unique_ptr<PVR_NAMED_VALUE[]> properties(new PVR_NAMED_VALUE[iPropertyCount]); + memset(properties.get(), 0, iPropertyCount * sizeof(PVR_NAMED_VALUE)); + + PVR_ERROR error = addon->toAddon->GetEPGTagStreamProperties(addon, &addonTag, properties.get(), + &iPropertyCount); + if (error == PVR_ERROR_NO_ERROR) + WriteStreamProperties(properties.get(), iPropertyCount, props); + + return error; + }); +} + +PVR_ERROR CPVRClient::GetEpgTagEdl(const std::shared_ptr<const CPVREpgInfoTag>& epgTag, + std::vector<PVR_EDL_ENTRY>& edls) +{ + edls.clear(); + return DoAddonCall( + __func__, + [&epgTag, &edls](const AddonInstance* addon) { + CAddonEpgTag addonTag(epgTag); + + PVR_EDL_ENTRY edl_array[PVR_ADDON_EDL_LENGTH]; + int size = PVR_ADDON_EDL_LENGTH; + PVR_ERROR error = addon->toAddon->GetEPGTagEdl(addon, &addonTag, edl_array, &size); + if (error == PVR_ERROR_NO_ERROR) + { + edls.reserve(size); + for (int i = 0; i < size; ++i) + edls.emplace_back(edl_array[i]); + } + return error; + }, + m_clientCapabilities.SupportsEpgTagEdl()); +} + +PVR_ERROR CPVRClient::GetChannelGroupsAmount(int& iGroups) +{ + iGroups = -1; + return DoAddonCall( + __func__, + [&iGroups](const AddonInstance* addon) { + return addon->toAddon->GetChannelGroupsAmount(addon, &iGroups); + }, + m_clientCapabilities.SupportsChannelGroups()); +} + +PVR_ERROR CPVRClient::GetChannelGroups(CPVRChannelGroups* groups) +{ + return DoAddonCall(__func__, + [this, groups](const AddonInstance* addon) { + PVR_HANDLE_STRUCT handle = {}; + handle.callerAddress = this; + handle.dataAddress = groups; + return addon->toAddon->GetChannelGroups(addon, &handle, groups->IsRadio()); + }, + m_clientCapabilities.SupportsChannelGroups()); +} + +PVR_ERROR CPVRClient::GetChannelGroupMembers( + CPVRChannelGroup* group, std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers) +{ + return DoAddonCall(__func__, + [this, group, &groupMembers](const AddonInstance* addon) { + PVR_HANDLE_STRUCT handle = {}; + handle.callerAddress = this; + handle.dataAddress = &groupMembers; + + PVR_CHANNEL_GROUP tag; + group->FillAddonData(tag); + return addon->toAddon->GetChannelGroupMembers(addon, &handle, &tag); + }, + m_clientCapabilities.SupportsChannelGroups()); +} + +PVR_ERROR CPVRClient::GetProvidersAmount(int& iProviders) +{ + iProviders = -1; + return DoAddonCall(__func__, [&iProviders](const AddonInstance* addon) { + return addon->toAddon->GetProvidersAmount(addon, &iProviders); + }); +} + +PVR_ERROR CPVRClient::GetProviders(CPVRProvidersContainer& providers) +{ + return DoAddonCall(__func__, + [this, &providers](const AddonInstance* addon) { + PVR_HANDLE_STRUCT handle = {}; + handle.callerAddress = this; + handle.dataAddress = &providers; + return addon->toAddon->GetProviders(addon, &handle); + }, + m_clientCapabilities.SupportsProviders()); +} + +PVR_ERROR CPVRClient::GetChannelsAmount(int& iChannels) +{ + iChannels = -1; + return DoAddonCall(__func__, [&iChannels](const AddonInstance* addon) { + return addon->toAddon->GetChannelsAmount(addon, &iChannels); + }); +} + +PVR_ERROR CPVRClient::GetChannels(bool radio, std::vector<std::shared_ptr<CPVRChannel>>& channels) +{ + return DoAddonCall(__func__, + [this, radio, &channels](const AddonInstance* addon) { + PVR_HANDLE_STRUCT handle = {}; + handle.callerAddress = this; + handle.dataAddress = &channels; + return addon->toAddon->GetChannels(addon, &handle, radio); + }, + (radio && m_clientCapabilities.SupportsRadio()) || + (!radio && m_clientCapabilities.SupportsTV())); +} + +PVR_ERROR CPVRClient::GetRecordingsAmount(bool deleted, int& iRecordings) +{ + iRecordings = -1; + return DoAddonCall( + __func__, + [deleted, &iRecordings](const AddonInstance* addon) { + return addon->toAddon->GetRecordingsAmount(addon, deleted, &iRecordings); + }, + m_clientCapabilities.SupportsRecordings() && + (!deleted || m_clientCapabilities.SupportsRecordingsUndelete())); +} + +PVR_ERROR CPVRClient::GetRecordings(CPVRRecordings* results, bool deleted) +{ + return DoAddonCall(__func__, + [this, results, deleted](const AddonInstance* addon) { + PVR_HANDLE_STRUCT handle = {}; + handle.callerAddress = this; + handle.dataAddress = results; + return addon->toAddon->GetRecordings(addon, &handle, deleted); + }, + m_clientCapabilities.SupportsRecordings() && + (!deleted || m_clientCapabilities.SupportsRecordingsUndelete())); +} + +PVR_ERROR CPVRClient::DeleteRecording(const CPVRRecording& recording) +{ + return DoAddonCall( + __func__, + [&recording](const AddonInstance* addon) { + PVR_RECORDING tag; + recording.FillAddonData(tag); + return addon->toAddon->DeleteRecording(addon, &tag); + }, + m_clientCapabilities.SupportsRecordings() && m_clientCapabilities.SupportsRecordingsDelete()); +} + +PVR_ERROR CPVRClient::UndeleteRecording(const CPVRRecording& recording) +{ + return DoAddonCall( + __func__, + [&recording](const AddonInstance* addon) { + PVR_RECORDING tag; + recording.FillAddonData(tag); + return addon->toAddon->UndeleteRecording(addon, &tag); + }, + m_clientCapabilities.SupportsRecordingsUndelete()); +} + +PVR_ERROR CPVRClient::DeleteAllRecordingsFromTrash() +{ + return DoAddonCall( + __func__, + [](const AddonInstance* addon) { + return addon->toAddon->DeleteAllRecordingsFromTrash(addon); + }, + m_clientCapabilities.SupportsRecordingsUndelete()); +} + +PVR_ERROR CPVRClient::RenameRecording(const CPVRRecording& recording) +{ + return DoAddonCall( + __func__, + [&recording](const AddonInstance* addon) { + PVR_RECORDING tag; + recording.FillAddonData(tag); + return addon->toAddon->RenameRecording(addon, &tag); + }, + m_clientCapabilities.SupportsRecordings()); +} + +PVR_ERROR CPVRClient::SetRecordingLifetime(const CPVRRecording& recording) +{ + return DoAddonCall( + __func__, + [&recording](const AddonInstance* addon) { + PVR_RECORDING tag; + recording.FillAddonData(tag); + return addon->toAddon->SetRecordingLifetime(addon, &tag); + }, + m_clientCapabilities.SupportsRecordingsLifetimeChange()); +} + +PVR_ERROR CPVRClient::SetRecordingPlayCount(const CPVRRecording& recording, int count) +{ + return DoAddonCall( + __func__, + [&recording, count](const AddonInstance* addon) { + PVR_RECORDING tag; + recording.FillAddonData(tag); + return addon->toAddon->SetRecordingPlayCount(addon, &tag, count); + }, + m_clientCapabilities.SupportsRecordingsPlayCount()); +} + +PVR_ERROR CPVRClient::SetRecordingLastPlayedPosition(const CPVRRecording& recording, + int lastplayedposition) +{ + return DoAddonCall( + __func__, + [&recording, lastplayedposition](const AddonInstance* addon) { + PVR_RECORDING tag; + recording.FillAddonData(tag); + return addon->toAddon->SetRecordingLastPlayedPosition(addon, &tag, lastplayedposition); + }, + m_clientCapabilities.SupportsRecordingsLastPlayedPosition()); +} + +PVR_ERROR CPVRClient::GetRecordingLastPlayedPosition(const CPVRRecording& recording, int& iPosition) +{ + iPosition = -1; + return DoAddonCall( + __func__, + [&recording, &iPosition](const AddonInstance* addon) { + PVR_RECORDING tag; + recording.FillAddonData(tag); + return addon->toAddon->GetRecordingLastPlayedPosition(addon, &tag, &iPosition); + }, + m_clientCapabilities.SupportsRecordingsLastPlayedPosition()); +} + +PVR_ERROR CPVRClient::GetRecordingEdl(const CPVRRecording& recording, + std::vector<PVR_EDL_ENTRY>& edls) +{ + edls.clear(); + return DoAddonCall( + __func__, + [&recording, &edls](const AddonInstance* addon) { + PVR_RECORDING tag; + recording.FillAddonData(tag); + + PVR_EDL_ENTRY edl_array[PVR_ADDON_EDL_LENGTH]; + int size = PVR_ADDON_EDL_LENGTH; + PVR_ERROR error = addon->toAddon->GetRecordingEdl(addon, &tag, edl_array, &size); + if (error == PVR_ERROR_NO_ERROR) + { + edls.reserve(size); + for (int i = 0; i < size; ++i) + edls.emplace_back(edl_array[i]); + } + return error; + }, + m_clientCapabilities.SupportsRecordingsEdl()); +} + +PVR_ERROR CPVRClient::GetRecordingSize(const CPVRRecording& recording, int64_t& sizeInBytes) +{ + return DoAddonCall( + __func__, + [&recording, &sizeInBytes](const AddonInstance* addon) { + PVR_RECORDING tag; + recording.FillAddonData(tag); + return addon->toAddon->GetRecordingSize(addon, &tag, &sizeInBytes); + }, + m_clientCapabilities.SupportsRecordingsSize()); +} + +PVR_ERROR CPVRClient::GetTimersAmount(int& iTimers) +{ + iTimers = -1; + return DoAddonCall( + __func__, + [&iTimers](const AddonInstance* addon) { + return addon->toAddon->GetTimersAmount(addon, &iTimers); + }, + m_clientCapabilities.SupportsTimers()); +} + +PVR_ERROR CPVRClient::GetTimers(CPVRTimersContainer* results) +{ + return DoAddonCall(__func__, + [this, results](const AddonInstance* addon) { + PVR_HANDLE_STRUCT handle = {}; + handle.callerAddress = this; + handle.dataAddress = results; + return addon->toAddon->GetTimers(addon, &handle); + }, + m_clientCapabilities.SupportsTimers()); +} + +PVR_ERROR CPVRClient::AddTimer(const CPVRTimerInfoTag& timer) +{ + return DoAddonCall( + __func__, + [&timer](const AddonInstance* addon) { + PVR_TIMER tag; + timer.FillAddonData(tag); + return addon->toAddon->AddTimer(addon, &tag); + }, + m_clientCapabilities.SupportsTimers()); +} + +PVR_ERROR CPVRClient::DeleteTimer(const CPVRTimerInfoTag& timer, bool bForce /* = false */) +{ + return DoAddonCall( + __func__, + [&timer, bForce](const AddonInstance* addon) { + PVR_TIMER tag; + timer.FillAddonData(tag); + return addon->toAddon->DeleteTimer(addon, &tag, bForce); + }, + m_clientCapabilities.SupportsTimers()); +} + +PVR_ERROR CPVRClient::UpdateTimer(const CPVRTimerInfoTag& timer) +{ + return DoAddonCall( + __func__, + [&timer](const AddonInstance* addon) { + PVR_TIMER tag; + timer.FillAddonData(tag); + return addon->toAddon->UpdateTimer(addon, &tag); + }, + m_clientCapabilities.SupportsTimers()); +} + +PVR_ERROR CPVRClient::GetTimerTypes(std::vector<std::shared_ptr<CPVRTimerType>>& results) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + results = m_timertypes; + return PVR_ERROR_NO_ERROR; +} + +PVR_ERROR CPVRClient::GetStreamReadChunkSize(int& iChunkSize) +{ + return DoAddonCall( + __func__, + [&iChunkSize](const AddonInstance* addon) { + return addon->toAddon->GetStreamReadChunkSize(addon, &iChunkSize); + }, + m_clientCapabilities.SupportsRecordings() || m_clientCapabilities.HandlesInputStream()); +} + +PVR_ERROR CPVRClient::ReadLiveStream(void* lpBuf, int64_t uiBufSize, int& iRead) +{ + iRead = -1; + return DoAddonCall(__func__, [&lpBuf, uiBufSize, &iRead](const AddonInstance* addon) { + iRead = addon->toAddon->ReadLiveStream(addon, static_cast<unsigned char*>(lpBuf), + static_cast<int>(uiBufSize)); + return (iRead == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::ReadRecordedStream(void* lpBuf, int64_t uiBufSize, int& iRead) +{ + iRead = -1; + return DoAddonCall(__func__, [&lpBuf, uiBufSize, &iRead](const AddonInstance* addon) { + iRead = addon->toAddon->ReadRecordedStream(addon, static_cast<unsigned char*>(lpBuf), + static_cast<int>(uiBufSize)); + return (iRead == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::SeekLiveStream(int64_t iFilePosition, int iWhence, int64_t& iPosition) +{ + iPosition = -1; + return DoAddonCall(__func__, [iFilePosition, iWhence, &iPosition](const AddonInstance* addon) { + iPosition = addon->toAddon->SeekLiveStream(addon, iFilePosition, iWhence); + return (iPosition == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::SeekRecordedStream(int64_t iFilePosition, int iWhence, int64_t& iPosition) +{ + iPosition = -1; + return DoAddonCall(__func__, [iFilePosition, iWhence, &iPosition](const AddonInstance* addon) { + iPosition = addon->toAddon->SeekRecordedStream(addon, iFilePosition, iWhence); + return (iPosition == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::SeekTime(double time, bool backwards, double* startpts) +{ + return DoAddonCall(__func__, [time, backwards, &startpts](const AddonInstance* addon) { + return addon->toAddon->SeekTime(addon, time, backwards, startpts) ? PVR_ERROR_NO_ERROR + : PVR_ERROR_NOT_IMPLEMENTED; + }); +} + +PVR_ERROR CPVRClient::GetLiveStreamLength(int64_t& iLength) +{ + iLength = -1; + return DoAddonCall(__func__, [&iLength](const AddonInstance* addon) { + iLength = addon->toAddon->LengthLiveStream(addon); + return (iLength == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::GetRecordedStreamLength(int64_t& iLength) +{ + iLength = -1; + return DoAddonCall(__func__, [&iLength](const AddonInstance* addon) { + iLength = addon->toAddon->LengthRecordedStream(addon); + return (iLength == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::SignalQuality(int channelUid, PVR_SIGNAL_STATUS& qualityinfo) +{ + return DoAddonCall(__func__, [channelUid, &qualityinfo](const AddonInstance* addon) { + return addon->toAddon->GetSignalStatus(addon, channelUid, &qualityinfo); + }); +} + +PVR_ERROR CPVRClient::GetDescrambleInfo(int channelUid, PVR_DESCRAMBLE_INFO& descrambleinfo) const +{ + return DoAddonCall( + __func__, + [channelUid, &descrambleinfo](const AddonInstance* addon) { + return addon->toAddon->GetDescrambleInfo(addon, channelUid, &descrambleinfo); + }, + m_clientCapabilities.SupportsDescrambleInfo()); +} + +PVR_ERROR CPVRClient::GetChannelStreamProperties(const std::shared_ptr<CPVRChannel>& channel, + CPVRStreamProperties& props) +{ + return DoAddonCall(__func__, [this, &channel, &props](const AddonInstance* addon) { + if (!CanPlayChannel(channel)) + return PVR_ERROR_NO_ERROR; // no error, but no need to obtain the values from the addon + + PVR_CHANNEL tag = {}; + channel->FillAddonData(tag); + + unsigned int iPropertyCount = STREAM_MAX_PROPERTY_COUNT; + std::unique_ptr<PVR_NAMED_VALUE[]> properties(new PVR_NAMED_VALUE[iPropertyCount]); + memset(properties.get(), 0, iPropertyCount * sizeof(PVR_NAMED_VALUE)); + + PVR_ERROR error = + addon->toAddon->GetChannelStreamProperties(addon, &tag, properties.get(), &iPropertyCount); + if (error == PVR_ERROR_NO_ERROR) + WriteStreamProperties(properties.get(), iPropertyCount, props); + + return error; + }); +} + +PVR_ERROR CPVRClient::GetRecordingStreamProperties(const std::shared_ptr<CPVRRecording>& recording, + CPVRStreamProperties& props) +{ + return DoAddonCall(__func__, [this, &recording, &props](const AddonInstance* addon) { + if (!m_clientCapabilities.SupportsRecordings()) + return PVR_ERROR_NO_ERROR; // no error, but no need to obtain the values from the addon + + PVR_RECORDING tag = {}; + recording->FillAddonData(tag); + + unsigned int iPropertyCount = STREAM_MAX_PROPERTY_COUNT; + std::unique_ptr<PVR_NAMED_VALUE[]> properties(new PVR_NAMED_VALUE[iPropertyCount]); + memset(properties.get(), 0, iPropertyCount * sizeof(PVR_NAMED_VALUE)); + + PVR_ERROR error = addon->toAddon->GetRecordingStreamProperties(addon, &tag, properties.get(), + &iPropertyCount); + if (error == PVR_ERROR_NO_ERROR) + WriteStreamProperties(properties.get(), iPropertyCount, props); + + return error; + }); +} + +PVR_ERROR CPVRClient::GetStreamProperties(PVR_STREAM_PROPERTIES* props) +{ + return DoAddonCall(__func__, [&props](const AddonInstance* addon) { + return addon->toAddon->GetStreamProperties(addon, props); + }); +} + +PVR_ERROR CPVRClient::DemuxReset() +{ + return DoAddonCall( + __func__, + [](const AddonInstance* addon) { + addon->toAddon->DemuxReset(addon); + return PVR_ERROR_NO_ERROR; + }, + m_clientCapabilities.HandlesDemuxing()); +} + +PVR_ERROR CPVRClient::DemuxAbort() +{ + return DoAddonCall( + __func__, + [](const AddonInstance* addon) { + addon->toAddon->DemuxAbort(addon); + return PVR_ERROR_NO_ERROR; + }, + m_clientCapabilities.HandlesDemuxing()); +} + +PVR_ERROR CPVRClient::DemuxFlush() +{ + return DoAddonCall( + __func__, + [](const AddonInstance* addon) { + addon->toAddon->DemuxFlush(addon); + return PVR_ERROR_NO_ERROR; + }, + m_clientCapabilities.HandlesDemuxing()); +} + +PVR_ERROR CPVRClient::DemuxRead(DemuxPacket*& packet) +{ + return DoAddonCall( + __func__, + [&packet](const AddonInstance* addon) { + packet = static_cast<DemuxPacket*>(addon->toAddon->DemuxRead(addon)); + return packet ? PVR_ERROR_NO_ERROR : PVR_ERROR_NOT_IMPLEMENTED; + }, + m_clientCapabilities.HandlesDemuxing()); +} + +const char* CPVRClient::ToString(const PVR_ERROR error) +{ + switch (error) + { + case PVR_ERROR_NO_ERROR: + return "no error"; + case PVR_ERROR_NOT_IMPLEMENTED: + return "not implemented"; + case PVR_ERROR_SERVER_ERROR: + return "server error"; + case PVR_ERROR_SERVER_TIMEOUT: + return "server timeout"; + case PVR_ERROR_RECORDING_RUNNING: + return "recording already running"; + case PVR_ERROR_ALREADY_PRESENT: + return "already present"; + case PVR_ERROR_REJECTED: + return "rejected by the backend"; + case PVR_ERROR_INVALID_PARAMETERS: + return "invalid parameters for this method"; + case PVR_ERROR_FAILED: + return "the command failed"; + case PVR_ERROR_UNKNOWN: + default: + return "unknown error"; + } +} + +PVR_ERROR CPVRClient::DoAddonCall(const char* strFunctionName, + const std::function<PVR_ERROR(const AddonInstance*)>& function, + bool bIsImplemented /* = true */, + bool bCheckReadyToUse /* = true */) const +{ + // Check preconditions. + if (!bIsImplemented) + return PVR_ERROR_NOT_IMPLEMENTED; + + if (m_bBlockAddonCalls) + { + CLog::Log(LOGWARNING, "{}: Blocking call to add-on '{}'.", strFunctionName, ID()); + return PVR_ERROR_SERVER_ERROR; + } + + if (bCheckReadyToUse && IgnoreClient()) + { + CLog::Log(LOGWARNING, "{}: Blocking call to add-on '{}'. Add-on not (yet) connected.", + strFunctionName, ID()); + return PVR_ERROR_SERVER_ERROR; + } + + if (bCheckReadyToUse && !ReadyToUse()) + { + CLog::Log(LOGWARNING, "{}: Blocking call to add-on '{}'. Add-on not ready to use.", + strFunctionName, ID()); + return PVR_ERROR_SERVER_ERROR; + } + + // Call. + m_allAddonCallsFinished.Reset(); + m_iAddonCalls++; + + const PVR_ERROR error = function(m_ifc.pvr); + + m_iAddonCalls--; + if (m_iAddonCalls == 0) + m_allAddonCallsFinished.Set(); + + // Log error, if any. + if (error != PVR_ERROR_NO_ERROR && error != PVR_ERROR_NOT_IMPLEMENTED) + CLog::Log(LOGERROR, "{}: Add-on '{}' returned an error: {}", strFunctionName, ID(), + ToString(error)); + + return error; +} + +bool CPVRClient::CanPlayChannel(const std::shared_ptr<CPVRChannel>& channel) const +{ + return (m_bReadyToUse && ((m_clientCapabilities.SupportsTV() && !channel->IsRadio()) || + (m_clientCapabilities.SupportsRadio() && channel->IsRadio()))); +} + +PVR_ERROR CPVRClient::OpenLiveStream(const std::shared_ptr<CPVRChannel>& channel) +{ + if (!channel) + return PVR_ERROR_INVALID_PARAMETERS; + + return DoAddonCall(__func__, [this, channel](const AddonInstance* addon) { + CloseLiveStream(); + + if (!CanPlayChannel(channel)) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Add-on '{}' can not play channel '{}'", ID(), + channel->ChannelName()); + return PVR_ERROR_SERVER_ERROR; + } + else + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Opening live stream for channel '{}'", channel->ChannelName()); + PVR_CHANNEL tag; + channel->FillAddonData(tag); + return addon->toAddon->OpenLiveStream(addon, &tag) ? PVR_ERROR_NO_ERROR + : PVR_ERROR_NOT_IMPLEMENTED; + } + }); +} + +PVR_ERROR CPVRClient::OpenRecordedStream(const std::shared_ptr<CPVRRecording>& recording) +{ + if (!recording) + return PVR_ERROR_INVALID_PARAMETERS; + + return DoAddonCall( + __func__, + [this, recording](const AddonInstance* addon) { + CloseRecordedStream(); + + PVR_RECORDING tag; + recording->FillAddonData(tag); + CLog::LogFC(LOGDEBUG, LOGPVR, "Opening stream for recording '{}'", recording->m_strTitle); + return addon->toAddon->OpenRecordedStream(addon, &tag) ? PVR_ERROR_NO_ERROR + : PVR_ERROR_NOT_IMPLEMENTED; + }, + m_clientCapabilities.SupportsRecordings()); +} + +PVR_ERROR CPVRClient::CloseLiveStream() +{ + return DoAddonCall(__func__, [](const AddonInstance* addon) { + addon->toAddon->CloseLiveStream(addon); + return PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::CloseRecordedStream() +{ + return DoAddonCall(__func__, [](const AddonInstance* addon) { + addon->toAddon->CloseRecordedStream(addon); + return PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::PauseStream(bool bPaused) +{ + return DoAddonCall(__func__, [bPaused](const AddonInstance* addon) { + addon->toAddon->PauseStream(addon, bPaused); + return PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::SetSpeed(int speed) +{ + return DoAddonCall(__func__, [speed](const AddonInstance* addon) { + addon->toAddon->SetSpeed(addon, speed); + return PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::FillBuffer(bool mode) +{ + return DoAddonCall(__func__, [mode](const AddonInstance* addon) { + addon->toAddon->FillBuffer(addon, mode); + return PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::CanPauseStream(bool& bCanPause) const +{ + bCanPause = false; + return DoAddonCall(__func__, [&bCanPause](const AddonInstance* addon) { + bCanPause = addon->toAddon->CanPauseStream(addon); + return PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::CanSeekStream(bool& bCanSeek) const +{ + bCanSeek = false; + return DoAddonCall(__func__, [&bCanSeek](const AddonInstance* addon) { + bCanSeek = addon->toAddon->CanSeekStream(addon); + return PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::GetStreamTimes(PVR_STREAM_TIMES* times) +{ + return DoAddonCall(__func__, [×](const AddonInstance* addon) { + return addon->toAddon->GetStreamTimes(addon, times); + }); +} + +PVR_ERROR CPVRClient::IsRealTimeStream(bool& bRealTime) const +{ + bRealTime = false; + return DoAddonCall(__func__, [&bRealTime](const AddonInstance* addon) { + bRealTime = addon->toAddon->IsRealTimeStream(addon); + return PVR_ERROR_NO_ERROR; + }); +} + +PVR_ERROR CPVRClient::OnSystemSleep() +{ + return DoAddonCall( + __func__, [](const AddonInstance* addon) { return addon->toAddon->OnSystemSleep(addon); }); +} + +PVR_ERROR CPVRClient::OnSystemWake() +{ + return DoAddonCall( + __func__, [](const AddonInstance* addon) { return addon->toAddon->OnSystemWake(addon); }); +} + +PVR_ERROR CPVRClient::OnPowerSavingActivated() +{ + return DoAddonCall(__func__, [](const AddonInstance* addon) { + return addon->toAddon->OnPowerSavingActivated(addon); + }); +} + +PVR_ERROR CPVRClient::OnPowerSavingDeactivated() +{ + return DoAddonCall(__func__, [](const AddonInstance* addon) { + return addon->toAddon->OnPowerSavingDeactivated(addon); + }); +} + +std::shared_ptr<CPVRClientMenuHooks> CPVRClient::GetMenuHooks() +{ + if (!m_menuhooks) + m_menuhooks.reset(new CPVRClientMenuHooks(ID())); + + return m_menuhooks; +} + +PVR_ERROR CPVRClient::CallEpgTagMenuHook(const CPVRClientMenuHook& hook, + const std::shared_ptr<CPVREpgInfoTag>& tag) +{ + return DoAddonCall(__func__, [&hook, &tag](const AddonInstance* addon) { + CAddonEpgTag addonTag(tag); + + PVR_MENUHOOK menuHook; + menuHook.category = PVR_MENUHOOK_EPG; + menuHook.iHookId = hook.GetId(); + menuHook.iLocalizedStringId = hook.GetLabelId(); + + return addon->toAddon->CallEPGMenuHook(addon, &menuHook, &addonTag); + }); +} + +PVR_ERROR CPVRClient::CallChannelMenuHook(const CPVRClientMenuHook& hook, + const std::shared_ptr<CPVRChannel>& channel) +{ + return DoAddonCall(__func__, [&hook, &channel](const AddonInstance* addon) { + PVR_CHANNEL tag; + channel->FillAddonData(tag); + + PVR_MENUHOOK menuHook; + menuHook.category = PVR_MENUHOOK_CHANNEL; + menuHook.iHookId = hook.GetId(); + menuHook.iLocalizedStringId = hook.GetLabelId(); + + return addon->toAddon->CallChannelMenuHook(addon, &menuHook, &tag); + }); +} + +PVR_ERROR CPVRClient::CallRecordingMenuHook(const CPVRClientMenuHook& hook, + const std::shared_ptr<CPVRRecording>& recording, + bool bDeleted) +{ + return DoAddonCall(__func__, [&hook, &recording, &bDeleted](const AddonInstance* addon) { + PVR_RECORDING tag; + recording->FillAddonData(tag); + + PVR_MENUHOOK menuHook; + menuHook.category = bDeleted ? PVR_MENUHOOK_DELETED_RECORDING : PVR_MENUHOOK_RECORDING; + menuHook.iHookId = hook.GetId(); + menuHook.iLocalizedStringId = hook.GetLabelId(); + + return addon->toAddon->CallRecordingMenuHook(addon, &menuHook, &tag); + }); +} + +PVR_ERROR CPVRClient::CallTimerMenuHook(const CPVRClientMenuHook& hook, + const std::shared_ptr<CPVRTimerInfoTag>& timer) +{ + return DoAddonCall(__func__, [&hook, &timer](const AddonInstance* addon) { + PVR_TIMER tag; + timer->FillAddonData(tag); + + PVR_MENUHOOK menuHook; + menuHook.category = PVR_MENUHOOK_TIMER; + menuHook.iHookId = hook.GetId(); + menuHook.iLocalizedStringId = hook.GetLabelId(); + + return addon->toAddon->CallTimerMenuHook(addon, &menuHook, &tag); + }); +} + +PVR_ERROR CPVRClient::CallSettingsMenuHook(const CPVRClientMenuHook& hook) +{ + return DoAddonCall(__func__, [&hook](const AddonInstance* addon) { + PVR_MENUHOOK menuHook; + menuHook.category = PVR_MENUHOOK_SETTING; + menuHook.iHookId = hook.GetId(); + menuHook.iLocalizedStringId = hook.GetLabelId(); + + return addon->toAddon->CallSettingsMenuHook(addon, &menuHook); + }); +} + +void CPVRClient::SetPriority(int iPriority) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_iPriority != iPriority) + { + m_iPriority = iPriority; + if (m_iClientId > PVR_INVALID_CLIENT_ID) + { + CServiceBroker::GetPVRManager().GetTVDatabase()->Persist(*this); + m_bPriorityFetched = true; + } + } +} + +int CPVRClient::GetPriority() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_bPriorityFetched && m_iClientId > PVR_INVALID_CLIENT_ID) + { + m_iPriority = CServiceBroker::GetPVRManager().GetTVDatabase()->GetPriority(*this); + m_bPriorityFetched = true; + } + return m_iPriority; +} + +void CPVRClient::HandleAddonCallback(const char* strFunctionName, + void* kodiInstance, + const std::function<void(CPVRClient* client)>& function, + bool bForceCall /* = false */) +{ + // Check preconditions. + CPVRClient* client = static_cast<CPVRClient*>(kodiInstance); + if (!client) + { + CLog::Log(LOGERROR, "{}: No instance pointer given!", strFunctionName); + return; + } + + if (!bForceCall && client->m_bBlockAddonCalls && client->m_iAddonCalls == 0) + { + CLog::Log(LOGWARNING, LOGPVR, "{}: Ignoring callback from PVR client '{}'", strFunctionName, + client->ID()); + return; + } + + // Call. + function(client); +} + +void CPVRClient::cb_transfer_channel_group(void* kodiInstance, + const PVR_HANDLE handle, + const PVR_CHANNEL_GROUP* group) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + if (!handle || !group) + { + CLog::LogF(LOGERROR, "Invalid callback parameter(s)"); + return; + } + + if (strlen(group->strGroupName) == 0) + { + CLog::LogF(LOGERROR, "Empty group name"); + return; + } + + // transfer this entry to the groups container + CPVRChannelGroups* kodiGroups = static_cast<CPVRChannelGroups*>(handle->dataAddress); + const auto transferGroup = + std::make_shared<CPVRChannelGroup>(*group, kodiGroups->GetGroupAll()); + kodiGroups->UpdateFromClient(transferGroup); + }); +} + +void CPVRClient::cb_transfer_channel_group_member(void* kodiInstance, + const PVR_HANDLE handle, + const PVR_CHANNEL_GROUP_MEMBER* member) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + if (!handle || !member) + { + CLog::LogF(LOGERROR, "Invalid callback parameter(s)"); + return; + } + + const std::shared_ptr<CPVRChannel> channel = + CServiceBroker::GetPVRManager().ChannelGroups()->GetByUniqueID(member->iChannelUniqueId, + client->GetID()); + if (!channel) + { + CLog::LogF(LOGERROR, "Cannot find group '{}' or channel '{}'", member->strGroupName, + member->iChannelUniqueId); + } + else + { + auto* groupMembers = + static_cast<std::vector<std::shared_ptr<CPVRChannelGroupMember>>*>(handle->dataAddress); + groupMembers->emplace_back( + std::make_shared<CPVRChannelGroupMember>(member->strGroupName, member->iOrder, channel)); + } + }); +} + +void CPVRClient::cb_transfer_epg_entry(void* kodiInstance, + const PVR_HANDLE handle, + const EPG_TAG* epgentry) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + if (!handle || !epgentry) + { + CLog::LogF(LOGERROR, "Invalid callback parameter(s)"); + return; + } + + // transfer this entry to the epg + CPVREpg* epg = static_cast<CPVREpg*>(handle->dataAddress); + epg->UpdateEntry(epgentry, client->GetID()); + }); +} + +void CPVRClient::cb_transfer_provider_entry(void* kodiInstance, + const PVR_HANDLE handle, + const PVR_PROVIDER* provider) +{ + if (!handle) + { + CLog::LogF(LOGERROR, "Invalid handler data"); + return; + } + + CPVRClient* client = static_cast<CPVRClient*>(kodiInstance); + CPVRProvidersContainer* kodiProviders = static_cast<CPVRProvidersContainer*>(handle->dataAddress); + if (!provider || !client || !kodiProviders) + { + CLog::LogF(LOGERROR, "Invalid handler data"); + return; + } + + /* transfer this entry to the internal channels group */ + std::shared_ptr<CPVRProvider> transferProvider( + std::make_shared<CPVRProvider>(*provider, client->GetID())); + kodiProviders->UpdateFromClient(transferProvider); +} + +void CPVRClient::cb_transfer_channel_entry(void* kodiInstance, + const PVR_HANDLE handle, + const PVR_CHANNEL* channel) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + if (!handle || !channel) + { + CLog::LogF(LOGERROR, "Invalid callback parameter(s)"); + return; + } + + auto* channels = static_cast<std::vector<std::shared_ptr<CPVRChannel>>*>(handle->dataAddress); + channels->emplace_back(std::make_shared<CPVRChannel>(*channel, client->GetID())); + }); +} + +void CPVRClient::cb_transfer_recording_entry(void* kodiInstance, + const PVR_HANDLE handle, + const PVR_RECORDING* recording) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + if (!handle || !recording) + { + CLog::LogF(LOGERROR, "Invalid callback parameter(s)"); + return; + } + + // transfer this entry to the recordings container + const std::shared_ptr<CPVRRecording> transferRecording = + std::make_shared<CPVRRecording>(*recording, client->GetID()); + CPVRRecordings* recordings = static_cast<CPVRRecordings*>(handle->dataAddress); + recordings->UpdateFromClient(transferRecording, *client); + }); +} + +void CPVRClient::cb_transfer_timer_entry(void* kodiInstance, + const PVR_HANDLE handle, + const PVR_TIMER* timer) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + if (!handle || !timer) + { + CLog::LogF(LOGERROR, "Invalid callback parameter(s)"); + return; + } + + // Note: channel can be nullptr here, for instance for epg-based timer rules + // ("record on any channel" condition) + const std::shared_ptr<CPVRChannel> channel = + CServiceBroker::GetPVRManager().ChannelGroups()->GetByUniqueID(timer->iClientChannelUid, + client->GetID()); + + // transfer this entry to the timers container + const std::shared_ptr<CPVRTimerInfoTag> transferTimer = + std::make_shared<CPVRTimerInfoTag>(*timer, channel, client->GetID()); + CPVRTimersContainer* timers = static_cast<CPVRTimersContainer*>(handle->dataAddress); + timers->UpdateFromClient(transferTimer); + }); +} + +void CPVRClient::cb_add_menu_hook(void* kodiInstance, const PVR_MENUHOOK* hook) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + if (!hook) + { + CLog::LogF(LOGERROR, "Invalid callback parameter(s)"); + return; + } + + client->GetMenuHooks()->AddHook(*hook); + }); +} + +void CPVRClient::cb_recording_notification(void* kodiInstance, + const char* strName, + const char* strFileName, + bool bOnOff) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + if (!strFileName) + { + CLog::LogF(LOGERROR, "Invalid callback parameter(s)"); + return; + } + + const std::string strLine1 = StringUtils::Format(g_localizeStrings.Get(bOnOff ? 19197 : 19198), + client->GetFriendlyName()); + std::string strLine2; + if (strName) + strLine2 = strName; + else + strLine2 = strFileName; + + // display a notification for 5 seconds + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, strLine1, strLine2, 5000, + false); + auto eventLog = CServiceBroker::GetEventLog(); + if (eventLog) + eventLog->Add(EventPtr( + new CNotificationEvent(client->GetFriendlyName(), strLine1, client->Icon(), strLine2))); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Recording {} on client '{}'. name='{}' filename='{}'", + bOnOff ? "started" : "finished", client->ID(), strName, strFileName); + }); +} + +void CPVRClient::cb_trigger_channel_update(void* kodiInstance) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + // update channels in the next iteration of the pvrmanager's main loop + CServiceBroker::GetPVRManager().TriggerChannelsUpdate(client->GetID()); + }); +} + +void CPVRClient::cb_trigger_provider_update(void* kodiInstance) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + /* update the providers table in the next iteration of the pvrmanager's main loop */ + CServiceBroker::GetPVRManager().TriggerProvidersUpdate(client->GetID()); + }); +} + +void CPVRClient::cb_trigger_timer_update(void* kodiInstance) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + // update timers in the next iteration of the pvrmanager's main loop + CServiceBroker::GetPVRManager().TriggerTimersUpdate(client->GetID()); + }); +} + +void CPVRClient::cb_trigger_recording_update(void* kodiInstance) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + // update recordings in the next iteration of the pvrmanager's main loop + CServiceBroker::GetPVRManager().TriggerRecordingsUpdate(client->GetID()); + }); +} + +void CPVRClient::cb_trigger_channel_groups_update(void* kodiInstance) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + // update all channel groups in the next iteration of the pvrmanager's main loop + CServiceBroker::GetPVRManager().TriggerChannelGroupsUpdate(client->GetID()); + }); +} + +void CPVRClient::cb_trigger_epg_update(void* kodiInstance, unsigned int iChannelUid) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + CServiceBroker::GetPVRManager().EpgContainer().UpdateRequest(client->GetID(), iChannelUid); + }); +} + +void CPVRClient::cb_free_demux_packet(void* kodiInstance, DEMUX_PACKET* pPacket) +{ + HandleAddonCallback(__func__, kodiInstance, + [&](CPVRClient* client) { + CDVDDemuxUtils::FreeDemuxPacket(static_cast<DemuxPacket*>(pPacket)); + }, + true); +} + +DEMUX_PACKET* CPVRClient::cb_allocate_demux_packet(void* kodiInstance, int iDataSize) +{ + DEMUX_PACKET* result = nullptr; + + HandleAddonCallback( + __func__, kodiInstance, + [&](CPVRClient* client) { result = CDVDDemuxUtils::AllocateDemuxPacket(iDataSize); }, true); + + return result; +} + +void CPVRClient::cb_connection_state_change(void* kodiInstance, + const char* strConnectionString, + PVR_CONNECTION_STATE newState, + const char* strMessage) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + if (!strConnectionString) + { + CLog::LogF(LOGERROR, "Invalid callback parameter(s)"); + return; + } + + const PVR_CONNECTION_STATE prevState(client->GetConnectionState()); + if (prevState == newState) + return; + + CLog::LogFC(LOGDEBUG, LOGPVR, + "State for connection '{}' on client '{}' changed from '{}' to '{}'", + strConnectionString, client->ID(), prevState, newState); + + client->SetConnectionState(newState); + + std::string msg; + if (strMessage) + msg = strMessage; + + CServiceBroker::GetPVRManager().ConnectionStateChange(client, std::string(strConnectionString), + newState, msg); + }); +} + +void CPVRClient::cb_epg_event_state_change(void* kodiInstance, + EPG_TAG* tag, + EPG_EVENT_STATE newState) +{ + HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) { + if (!tag) + { + CLog::LogF(LOGERROR, "Invalid callback parameter(s)"); + return; + } + + // Note: channel data and epg id may not yet be available. Tag will be fully initialized later. + const std::shared_ptr<CPVREpgInfoTag> epgTag = + std::make_shared<CPVREpgInfoTag>(*tag, client->GetID(), nullptr, -1); + CServiceBroker::GetPVRManager().EpgContainer().UpdateFromClient(epgTag, newState); + }); +} + +class CCodecIds +{ +public: + virtual ~CCodecIds() = default; + + static CCodecIds& GetInstance() + { + static CCodecIds _instance; + return _instance; + } + + PVR_CODEC GetCodecByName(const char* strCodecName) + { + PVR_CODEC retVal = PVR_INVALID_CODEC; + if (strlen(strCodecName) == 0) + return retVal; + + std::string strUpperCodecName = strCodecName; + StringUtils::ToUpper(strUpperCodecName); + + std::map<std::string, PVR_CODEC>::const_iterator it = m_lookup.find(strUpperCodecName); + if (it != m_lookup.end()) + retVal = it->second; + + return retVal; + } + +private: + CCodecIds() + { + // get ids and names + const AVCodec* codec = nullptr; + void* i = nullptr; + PVR_CODEC tmp; + while ((codec = av_codec_iterate(&i))) + { + if (av_codec_is_decoder(codec)) + { + tmp.codec_type = static_cast<PVR_CODEC_TYPE>(codec->type); + tmp.codec_id = codec->id; + + std::string strUpperCodecName = codec->name; + StringUtils::ToUpper(strUpperCodecName); + + m_lookup.insert(std::make_pair(strUpperCodecName, tmp)); + } + } + + // teletext is not returned by av_codec_next. we got our own decoder + tmp.codec_type = PVR_CODEC_TYPE_SUBTITLE; + tmp.codec_id = AV_CODEC_ID_DVB_TELETEXT; + m_lookup.insert(std::make_pair("TELETEXT", tmp)); + + // rds is not returned by av_codec_next. we got our own decoder + tmp.codec_type = PVR_CODEC_TYPE_RDS; + tmp.codec_id = AV_CODEC_ID_NONE; + m_lookup.insert(std::make_pair("RDS", tmp)); + + // ID3 is not returned by av_codec_next. we got our own decoder + tmp.codec_type = PVR_CODEC_TYPE_ID3; + tmp.codec_id = AV_CODEC_ID_NONE; + m_lookup.insert({"ID3", tmp}); + } + + std::map<std::string, PVR_CODEC> m_lookup; +}; + +PVR_CODEC CPVRClient::cb_get_codec_by_name(const void* kodiInstance, const char* strCodecName) +{ + PVR_CODEC result = PVR_INVALID_CODEC; + + HandleAddonCallback( + __func__, const_cast<void*>(kodiInstance), + [&](CPVRClient* client) { result = CCodecIds::GetInstance().GetCodecByName(strCodecName); }, + true); + + return result; +} + +} // namespace PVR diff --git a/xbmc/pvr/addons/PVRClient.h b/xbmc/pvr/addons/PVRClient.h new file mode 100644 index 0000000..10203cf --- /dev/null +++ b/xbmc/pvr/addons/PVRClient.h @@ -0,0 +1,1062 @@ +/* + * 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/binary-addons/AddonInstanceHandler.h" +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr.h" +#include "pvr/addons/PVRClientCapabilities.h" +#include "threads/Event.h" + +#include <atomic> +#include <functional> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +struct DemuxPacket; + +namespace PVR +{ +class CPVRChannel; +class CPVRChannelGroup; +class CPVRChannelGroupMember; +class CPVRChannelGroups; +class CPVRProvider; +class CPVRProvidersContainer; +class CPVRClientMenuHook; +class CPVRClientMenuHooks; +class CPVREpg; +class CPVREpgInfoTag; +class CPVRRecording; +class CPVRRecordings; +class CPVRStreamProperties; +class CPVRTimerInfoTag; +class CPVRTimerType; +class CPVRTimersContainer; + +#define PVR_INVALID_CLIENT_ID (-2) + +/*! + * Interface from Kodi to a PVR add-on. + * + * Also translates Kodi's C++ structures to the add-on's C structures. + */ +class CPVRClient : public ADDON::IAddonInstanceHandler +{ +public: + CPVRClient(const ADDON::AddonInfoPtr& addonInfo, ADDON::AddonInstanceId instanceId, int clientId); + ~CPVRClient() override; + + void OnPreInstall() override; + void OnPreUnInstall() override; + + /** @name PVR add-on methods */ + //@{ + + /*! + * @brief Initialise the instance of this add-on. + */ + ADDON_STATUS Create(); + + /*! + * @brief Stop this add-on instance. No more client add-on access after this call. + */ + void Stop(); + + /*! + * @brief Continue this add-on instance. Client add-on access is okay again after this call. + */ + void Continue(); + + /*! + * @brief Destroy the instance of this add-on. + */ + void Destroy(); + + /*! + * @brief Destroy and recreate this add-on. + */ + void ReCreate(); + + /*! + * @return True if this instance is initialised (ADDON_Create returned true), false otherwise. + */ + bool ReadyToUse() const; + + /*! + * @brief Gets the backend connection state. + * @return the backend connection state. + */ + PVR_CONNECTION_STATE GetConnectionState() const; + + /*! + * @brief Sets the backend connection state. + * @param state the new backend connection state. + */ + void SetConnectionState(PVR_CONNECTION_STATE state); + + /*! + * @brief Gets the backend's previous connection state. + * @return the backend's previous connection state. + */ + PVR_CONNECTION_STATE GetPreviousConnectionState() const; + + /*! + * @brief Check whether this client should be ignored. + * @return True if this client should be ignored, false otherwise. + */ + bool IgnoreClient() const; + + /*! + * @brief Check whether this client is enabled, according to its instance/add-on configuration. + * @return True if this client is enabled, false otherwise. + */ + bool IsEnabled() const; + + /*! + * @return The ID of this instance. + */ + int GetID() const; + + //@} + /** @name PVR server methods */ + //@{ + + /*! + * @brief Query this add-on's capabilities. + * @return The add-on's capabilities. + */ + const CPVRClientCapabilities& GetClientCapabilities() const { return m_clientCapabilities; } + + /*! + * @brief Get the stream properties of the stream that's currently being read. + * @param pProperties The properties. + * @return PVR_ERROR_NO_ERROR if the properties have been fetched successfully. + */ + PVR_ERROR GetStreamProperties(PVR_STREAM_PROPERTIES* pProperties); + + /*! + * @return The name reported by the backend. + */ + const std::string& GetBackendName() const; + + /*! + * @return The version string reported by the backend. + */ + const std::string& GetBackendVersion() const; + + /*! + * @brief the ip address or alias of the pvr backend server + */ + const std::string& GetBackendHostname() const; + + /*! + * @return The connection string reported by the backend. + */ + const std::string& GetConnectionString() const; + + /*! + * @brief A friendly name used to uniquely identify the addon instance + * @return string that can be used in log messages and the GUI. + */ + const std::string GetFriendlyName() const; + + /*! + * @brief Get the disk space reported by the server. + * @param iTotal The total disk space. + * @param iUsed The used disk space. + * @return PVR_ERROR_NO_ERROR if the drive space has been fetched successfully. + */ + PVR_ERROR GetDriveSpace(uint64_t& iTotal, uint64_t& iUsed); + + /*! + * @brief Start a channel scan on the server. + * @return PVR_ERROR_NO_ERROR if the channel scan has been started successfully. + */ + PVR_ERROR StartChannelScan(); + + /*! + * @brief Request the client to open dialog about given channel to add + * @param channel The channel to add + * @return PVR_ERROR_NO_ERROR if the add has been fetched successfully. + */ + PVR_ERROR OpenDialogChannelAdd(const std::shared_ptr<CPVRChannel>& channel); + + /*! + * @brief Request the client to open dialog about given channel settings + * @param channel The channel to edit + * @return PVR_ERROR_NO_ERROR if the edit has been fetched successfully. + */ + PVR_ERROR OpenDialogChannelSettings(const std::shared_ptr<CPVRChannel>& channel); + + /*! + * @brief Request the client to delete given channel + * @param channel The channel to delete + * @return PVR_ERROR_NO_ERROR if the delete has been fetched successfully. + */ + PVR_ERROR DeleteChannel(const std::shared_ptr<CPVRChannel>& channel); + + /*! + * @brief Request the client to rename given channel + * @param channel The channel to rename + * @return PVR_ERROR_NO_ERROR if the rename has been fetched successfully. + */ + PVR_ERROR RenameChannel(const std::shared_ptr<CPVRChannel>& channel); + + /* + * @brief Check if an epg tag can be recorded + * @param tag The epg tag + * @param bIsRecordable Set to true if the tag can be recorded + * @return PVR_ERROR_NO_ERROR if bIsRecordable has been set successfully. + */ + PVR_ERROR IsRecordable(const std::shared_ptr<const CPVREpgInfoTag>& tag, + bool& bIsRecordable) const; + + /* + * @brief Check if an epg tag can be played + * @param tag The epg tag + * @param bIsPlayable Set to true if the tag can be played + * @return PVR_ERROR_NO_ERROR if bIsPlayable has been set successfully. + */ + PVR_ERROR IsPlayable(const std::shared_ptr<const CPVREpgInfoTag>& tag, bool& bIsPlayable) const; + + /*! + * @brief Fill the given container with the properties required for playback + * of the given EPG tag. Values are obtained from the PVR backend. + * + * @param tag The EPG tag. + * @param props The container to be filled with the stream properties. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetEpgTagStreamProperties(const std::shared_ptr<CPVREpgInfoTag>& tag, + CPVRStreamProperties& props); + + //@} + /** @name PVR EPG methods */ + //@{ + + /*! + * @brief Request an EPG table for a channel from the client. + * @param iChannelUid The UID of the channel to get the EPG table for. + * @param epg The table to write the data to. + * @param start The start time to use. + * @param end The end time to use. + * @return PVR_ERROR_NO_ERROR if the table has been fetched successfully. + */ + PVR_ERROR GetEPGForChannel(int iChannelUid, CPVREpg* epg, time_t start, time_t end); + + /*! + * @brief Tell the client the past time frame to use when notifying epg events back + * to Kodi. + * + * The client might push epg events asynchronously to Kodi using the callback + * function EpgEventStateChange. To be able to only push events that are + * actually of interest for Kodi, client needs to know about the past epg time + * frame Kodi uses. + * + * @param[in] iPastDays number of days before "now". + @ref EPG_TIMEFRAME_UNLIMITED means that Kodi is interested in all epg events, + regardless of event times. + * @return PVR_ERROR_NO_ERROR if new value was successfully set. + */ + PVR_ERROR SetEPGMaxPastDays(int iPastDays); + + /*! + * @brief Tell the client the future time frame to use when notifying epg events back + * to Kodi. + * + * The client might push epg events asynchronously to Kodi using the callback + * function EpgEventStateChange. To be able to only push events that are + * actually of interest for Kodi, client needs to know about the future epg time + * frame Kodi uses. + * + * @param[in] iFutureDays number of days after "now". + @ref EPG_TIMEFRAME_UNLIMITED means that Kodi is interested in all epg events, + regardless of event times. + * @return PVR_ERROR_NO_ERROR if new value was successfully set. + */ + PVR_ERROR SetEPGMaxFutureDays(int iFutureDays); + + //@} + /** @name PVR channel group methods */ + //@{ + + /*! + * @brief Get the total amount of channel groups from the backend. + * @param iGroups The total amount of channel groups on the server or -1 on error. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetChannelGroupsAmount(int& iGroups); + + /*! + * @brief Request the list of all channel groups from the backend. + * @param groups The groups container to get the groups for. + * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully. + */ + PVR_ERROR GetChannelGroups(CPVRChannelGroups* groups); + + /*! + * @brief Request the list of all group members from the backend. + * @param group The group to get the members for. + * @param groupMembers The container for the group members. + * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully. + */ + PVR_ERROR GetChannelGroupMembers( + CPVRChannelGroup* group, std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers); + + //@} + /** @name PVR channel methods */ + //@{ + + /*! + * @brief Get the total amount of channels from the backend. + * @param iChannels The total amount of channels on the server or -1 on error. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetChannelsAmount(int& iChannels); + + /*! + * @brief Request the list of all channels from the backend. + * @param bRadio True to get the radio channels, false to get the TV channels. + * @param channels The container for the channels. + * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully. + */ + PVR_ERROR GetChannels(bool bRadio, std::vector<std::shared_ptr<CPVRChannel>>& channels); + + /*! + * @brief Get the total amount of providers from the backend. + * @param iChannels The total amount of channels on the server or -1 on error. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetProvidersAmount(int& iProviders); + + /*! + * @brief Request the list of all providers from the backend. + * @param providers The providers list to add the providers to. + * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully. + */ + PVR_ERROR GetProviders(CPVRProvidersContainer& providers); + + //@} + /** @name PVR recording methods */ + //@{ + + /*! + * @brief Get the total amount of recordings from the backend. + * @param deleted True to return deleted recordings. + * @param iRecordings The total amount of recordings on the server or -1 on error. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetRecordingsAmount(bool deleted, int& iRecordings); + + /*! + * @brief Request the list of all recordings from the backend. + * @param results The container to add the recordings to. + * @param deleted True to return deleted recordings. + * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully. + */ + PVR_ERROR GetRecordings(CPVRRecordings* results, bool deleted); + + /*! + * @brief Delete a recording on the backend. + * @param recording The recording to delete. + * @return PVR_ERROR_NO_ERROR if the recording has been deleted successfully. + */ + PVR_ERROR DeleteRecording(const CPVRRecording& recording); + + /*! + * @brief Undelete a recording on the backend. + * @param recording The recording to undelete. + * @return PVR_ERROR_NO_ERROR if the recording has been undeleted successfully. + */ + PVR_ERROR UndeleteRecording(const CPVRRecording& recording); + + /*! + * @brief Delete all recordings permanent which in the deleted folder on the backend. + * @return PVR_ERROR_NO_ERROR if the recordings has been deleted successfully. + */ + PVR_ERROR DeleteAllRecordingsFromTrash(); + + /*! + * @brief Rename a recording on the backend. + * @param recording The recording to rename. + * @return PVR_ERROR_NO_ERROR if the recording has been renamed successfully. + */ + PVR_ERROR RenameRecording(const CPVRRecording& recording); + + /*! + * @brief Set the lifetime of a recording on the backend. + * @param recording The recording to set the lifetime for. recording.m_iLifetime contains the new lifetime value. + * @return PVR_ERROR_NO_ERROR if the recording's lifetime has been set successfully. + */ + PVR_ERROR SetRecordingLifetime(const CPVRRecording& recording); + + /*! + * @brief Set the play count of a recording on the backend. + * @param recording The recording to set the play count. + * @param count Play count. + * @return PVR_ERROR_NO_ERROR if the recording's play count has been set successfully. + */ + PVR_ERROR SetRecordingPlayCount(const CPVRRecording& recording, int count); + + /*! + * @brief Set the last watched position of a recording on the backend. + * @param recording The recording. + * @param lastplayedposition The last watched position in seconds + * @return PVR_ERROR_NO_ERROR if the position has been stored successfully. + */ + PVR_ERROR SetRecordingLastPlayedPosition(const CPVRRecording& recording, int lastplayedposition); + + /*! + * @brief Retrieve the last watched position of a recording on the backend. + * @param recording The recording. + * @param iPosition The last watched position in seconds or -1 on error + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetRecordingLastPlayedPosition(const CPVRRecording& recording, int& iPosition); + + /*! + * @brief Retrieve the edit decision list (EDL) from the backend. + * @param recording The recording. + * @param edls The edit decision list (empty on error). + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetRecordingEdl(const CPVRRecording& recording, std::vector<PVR_EDL_ENTRY>& edls); + + /*! + * @brief Retrieve the size of a recording on the backend. + * @param recording The recording. + * @param sizeInBytes The size in bytes + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetRecordingSize(const CPVRRecording& recording, int64_t& sizeInBytes); + + /*! + * @brief Retrieve the edit decision list (EDL) from the backend. + * @param epgTag The EPG tag. + * @param edls The edit decision list (empty on error). + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetEpgTagEdl(const std::shared_ptr<const CPVREpgInfoTag>& epgTag, + std::vector<PVR_EDL_ENTRY>& edls); + + //@} + /** @name PVR timer methods */ + //@{ + + /*! + * @brief Get the total amount of timers from the backend. + * @param iTimers The total amount of timers on the backend or -1 on error. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetTimersAmount(int& iTimers); + + /*! + * @brief Request the list of all timers from the backend. + * @param results The container to store the result in. + * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully. + */ + PVR_ERROR GetTimers(CPVRTimersContainer* results); + + /*! + * @brief Add a timer on the backend. + * @param timer The timer to add. + * @return PVR_ERROR_NO_ERROR if the timer has been added successfully. + */ + PVR_ERROR AddTimer(const CPVRTimerInfoTag& timer); + + /*! + * @brief Delete a timer on the backend. + * @param timer The timer to delete. + * @param bForce Set to true to delete a timer that is currently recording a program. + * @return PVR_ERROR_NO_ERROR if the timer has been deleted successfully. + */ + PVR_ERROR DeleteTimer(const CPVRTimerInfoTag& timer, bool bForce = false); + + /*! + * @brief Update the timer information on the server. + * @param timer The timer to update. + * @return PVR_ERROR_NO_ERROR if the timer has been updated successfully. + */ + PVR_ERROR UpdateTimer(const CPVRTimerInfoTag& timer); + + /*! + * @brief Get all timer types supported by the backend. + * @param results The container to store the result in. + * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully. + */ + PVR_ERROR GetTimerTypes(std::vector<std::shared_ptr<CPVRTimerType>>& results) const; + + //@} + /** @name PVR live stream methods */ + //@{ + + /*! + * @brief Open a live stream on the server. + * @param channel The channel to stream. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR OpenLiveStream(const std::shared_ptr<CPVRChannel>& channel); + + /*! + * @brief Close an open live stream. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR CloseLiveStream(); + + /*! + * @brief Read from an open live stream. + * @param lpBuf The buffer to store the data in. + * @param uiBufSize The amount of bytes to read. + * @param iRead The amount of bytes that were actually read from the stream. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR ReadLiveStream(void* lpBuf, int64_t uiBufSize, int& iRead); + + /*! + * @brief Seek in a live stream on a backend. + * @param iFilePosition The position to seek to. + * @param iWhence ? + * @param iPosition The new position or -1 on error. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR SeekLiveStream(int64_t iFilePosition, int iWhence, int64_t& iPosition); + + /*! + * @brief Get the length of the currently playing live stream, if any. + * @param iLength The total length of the stream that's currently being read or -1 on error. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetLiveStreamLength(int64_t& iLength); + + /*! + * @brief (Un)Pause a stream. + * @param bPaused True to pause the stream, false to unpause. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR PauseStream(bool bPaused); + + /*! + * @brief Get the signal quality of the stream that's currently open. + * @param channelUid Channel unique identifier + * @param qualityinfo The signal quality. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR SignalQuality(int channelUid, PVR_SIGNAL_STATUS& qualityinfo); + + /*! + * @brief Get the descramble information of the stream that's currently open. + * @param channelUid Channel unique identifier + * @param descrambleinfo The descramble information. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetDescrambleInfo(int channelUid, PVR_DESCRAMBLE_INFO& descrambleinfo) const; + + /*! + * @brief Fill the given container with the properties required for playback of the given channel. Values are obtained from the PVR backend. + * @param channel The channel. + * @param props The container to be filled with the stream properties. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetChannelStreamProperties(const std::shared_ptr<CPVRChannel>& channel, + CPVRStreamProperties& props); + + /*! + * @brief Check whether PVR backend supports pausing the currently playing stream + * @param bCanPause True if the stream can be paused, false otherwise. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR CanPauseStream(bool& bCanPause) const; + + /*! + * @brief Check whether PVR backend supports seeking for the currently playing stream + * @param bCanSeek True if the stream can be seeked, false otherwise. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR CanSeekStream(bool& bCanSeek) const; + + /*! + * @brief Notify the pvr addon/demuxer that Kodi wishes to seek the stream by time + * @param time The absolute time since stream start + * @param backwards True to seek to keyframe BEFORE time, else AFTER + * @param startpts can be updated to point to where display should start + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + * @remarks Optional, and only used if addon has its own demuxer. + */ + PVR_ERROR SeekTime(double time, bool backwards, double* startpts); + + /*! + * @brief Notify the pvr addon/demuxer that Kodi wishes to change playback speed + * @param speed The requested playback speed + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + * @remarks Optional, and only used if addon has its own demuxer. + */ + PVR_ERROR SetSpeed(int speed); + + /*! + * @brief Notify the pvr addon/demuxer that Kodi wishes to fill demux queue + * @param mode for setting on/off + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + * @remarks Optional, and only used if addon has its own demuxer. + */ + PVR_ERROR FillBuffer(bool mode); + + //@} + /** @name PVR recording stream methods */ + //@{ + + /*! + * @brief Open a recording on the server. + * @param recording The recording to open. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR OpenRecordedStream(const std::shared_ptr<CPVRRecording>& recording); + + /*! + * @brief Close an open recording stream. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR CloseRecordedStream(); + + /*! + * @brief Read from an open recording stream. + * @param lpBuf The buffer to store the data in. + * @param uiBufSize The amount of bytes to read. + * @param iRead The amount of bytes that were actually read from the stream. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR ReadRecordedStream(void* lpBuf, int64_t uiBufSize, int& iRead); + + /*! + * @brief Seek in a recording stream on a backend. + * @param iFilePosition The position to seek to. + * @param iWhence ? + * @param iPosition The new position or -1 on error. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR SeekRecordedStream(int64_t iFilePosition, int iWhence, int64_t& iPosition); + + /*! + * @brief Get the length of the currently playing recording stream, if any. + * @param iLength The total length of the stream that's currently being read or -1 on error. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetRecordedStreamLength(int64_t& iLength); + + /*! + * @brief Fill the given container with the properties required for playback of the given recording. Values are obtained from the PVR backend. + * @param recording The recording. + * @param props The container to be filled with the stream properties. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetRecordingStreamProperties(const std::shared_ptr<CPVRRecording>& recording, + CPVRStreamProperties& props); + + //@} + /** @name PVR demultiplexer methods */ + //@{ + + /*! + * @brief Reset the demultiplexer in the add-on. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR DemuxReset(); + + /*! + * @brief Abort the demultiplexer thread in the add-on. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR DemuxAbort(); + + /*! + * @brief Flush all data that's currently in the demultiplexer buffer in the add-on. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR DemuxFlush(); + + /*! + * @brief Read a packet from the demultiplexer. + * @param packet The packet read. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR DemuxRead(DemuxPacket*& packet); + + static const char* ToString(const PVR_ERROR error); + + /*! + * @brief Check whether the currently playing stream, if any, is a real-time stream. + * @param bRealTime True if real-time, false otherwise. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR IsRealTimeStream(bool& bRealTime) const; + + /*! + * @brief Get Stream times for the currently playing stream, if any (will be moved to inputstream). + * @param times The stream times. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetStreamTimes(PVR_STREAM_TIMES* times); + + /*! + * @brief Get the client's menu hooks. + * @return The hooks. Guaranteed never to be nullptr. + */ + std::shared_ptr<CPVRClientMenuHooks> GetMenuHooks(); + + /*! + * @brief Call one of the EPG tag menu hooks of the client. + * @param hook The hook to call. + * @param tag The EPG tag associated with the hook to be called. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR CallEpgTagMenuHook(const CPVRClientMenuHook& hook, + const std::shared_ptr<CPVREpgInfoTag>& tag); + + /*! + * @brief Call one of the channel menu hooks of the client. + * @param hook The hook to call. + * @param tag The channel associated with the hook to be called. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR CallChannelMenuHook(const CPVRClientMenuHook& hook, + const std::shared_ptr<CPVRChannel>& channel); + + /*! + * @brief Call one of the recording menu hooks of the client. + * @param hook The hook to call. + * @param tag The recording associated with the hook to be called. + * @param bDeleted True, if the recording is deleted (trashed), false otherwise + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR CallRecordingMenuHook(const CPVRClientMenuHook& hook, + const std::shared_ptr<CPVRRecording>& recording, + bool bDeleted); + + /*! + * @brief Call one of the timer menu hooks of the client. + * @param hook The hook to call. + * @param tag The timer associated with the hook to be called. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR CallTimerMenuHook(const CPVRClientMenuHook& hook, + const std::shared_ptr<CPVRTimerInfoTag>& timer); + + /*! + * @brief Call one of the settings menu hooks of the client. + * @param hook The hook to call. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR CallSettingsMenuHook(const CPVRClientMenuHook& hook); + + /*! + * @brief Propagate power management events to this add-on + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR OnSystemSleep(); + PVR_ERROR OnSystemWake(); + PVR_ERROR OnPowerSavingActivated(); + PVR_ERROR OnPowerSavingDeactivated(); + + /*! + * @brief Get the priority of this client. Larger value means higher priority. + * @return The priority. + */ + int GetPriority() const; + + /*! + * @brief Set a new priority for this client. + * @param iPriority The new priority. + */ + void SetPriority(int iPriority); + + /*! + * @brief Obtain the chunk size to use when reading streams. + * @param iChunkSize the chunk size in bytes. + * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise. + */ + PVR_ERROR GetStreamReadChunkSize(int& iChunkSize); + + /*! + * @brief Get the interface table used between addon and Kodi. + * @todo This function will be removed after old callback library system is removed. + */ + AddonInstance_PVR* GetInstanceInterface() { return m_ifc.pvr; } + +private: + /*! + * @brief Resets all class members to their defaults, accept the client id. + */ + void ResetProperties(); + + /*! + * @brief reads the client's properties. + * @return True on success, false otherwise. + */ + bool GetAddonProperties(); + + /*! + * @brief reads the client's name string properties + * @return True on success, false otherwise. + */ + bool GetAddonNameStringProperties(); + + /*! + * @brief Write the given addon properties to the given properties container. + * @param properties Pointer to an array of addon properties. + * @param iPropertyCount The number of properties contained in the addon properties array. + * @param props The container the addon properties shall be written to. + */ + static void WriteStreamProperties(const PVR_NAMED_VALUE* properties, + unsigned int iPropertyCount, + CPVRStreamProperties& props); + + /*! + * @brief Whether a channel can be played by this add-on + * @param channel The channel to check. + * @return True when it can be played, false otherwise. + */ + bool CanPlayChannel(const std::shared_ptr<CPVRChannel>& channel) const; + + /*! + * @brief Stop this instance, if it is currently running. + */ + void StopRunningInstance(); + + /*! + * @brief Wraps an addon function call in order to do common pre and post function invocation actions. + * @param strFunctionName The function name, for logging purposes. + * @param function The function to wrap. It has to have return type PVR_ERROR and must take one parameter of type const AddonInstance*. + * @param bIsImplemented If false, this method will return PVR_ERROR_NOT_IMPLEMENTED. + * @param bCheckReadyToUse If true, this method will check whether this instance is ready for use and return PVR_ERROR_SERVER_ERROR if it is not. + * @return PVR_ERROR_NO_ERROR on success, any other PVR_ERROR_* value otherwise. + */ + typedef AddonInstance_PVR AddonInstance; + PVR_ERROR DoAddonCall(const char* strFunctionName, + const std::function<PVR_ERROR(const AddonInstance*)>& function, + bool bIsImplemented = true, + bool bCheckReadyToUse = true) const; + + /*! + * @brief Wraps an addon callback function call in order to do common pre and post function invocation actions. + * @param strFunctionName The function name, for logging purposes. + * @param kodiInstance The addon instance pointer. + * @param function The function to wrap. It must take one parameter of type CPVRClient*. + * @param bForceCall If true, make the call, ignoring client's state. + */ + static void HandleAddonCallback(const char* strFunctionName, + void* kodiInstance, + const std::function<void(CPVRClient* client)>& function, + bool bForceCall = false); + + /*! + * @brief Callback functions from addon to kodi + */ + //@{ + + /*! + * @brief Transfer a channel group from the add-on to Kodi. The group will be created if it doesn't exist. + * @param kodiInstance Pointer to Kodi's CPVRClient class + * @param handle The handle parameter that Kodi used when requesting the channel groups list + * @param entry The entry to transfer to Kodi + */ + static void cb_transfer_channel_group(void* kodiInstance, + const PVR_HANDLE handle, + const PVR_CHANNEL_GROUP* entry); + + /*! + * @brief Transfer a channel group member entry from the add-on to Kodi. The channel will be added to the group if the group can be found. + * @param kodiInstance Pointer to Kodi's CPVRClient class + * @param handle The handle parameter that Kodi used when requesting the channel group members list + * @param entry The entry to transfer to Kodi + */ + static void cb_transfer_channel_group_member(void* kodiInstance, + const PVR_HANDLE handle, + const PVR_CHANNEL_GROUP_MEMBER* entry); + + /*! + * @brief Transfer an EPG tag from the add-on to Kodi + * @param kodiInstance Pointer to Kodi's CPVRClient class + * @param handle The handle parameter that Kodi used when requesting the EPG data + * @param entry The entry to transfer to Kodi + */ + static void cb_transfer_epg_entry(void* kodiInstance, + const PVR_HANDLE handle, + const EPG_TAG* entry); + + /*! + * @brief Transfer a channel entry from the add-on to Kodi + * @param kodiInstance Pointer to Kodi's CPVRClient class + * @param handle The handle parameter that Kodi used when requesting the channel list + * @param entry The entry to transfer to Kodi + */ + static void cb_transfer_channel_entry(void* kodiInstance, + const PVR_HANDLE handle, + const PVR_CHANNEL* entry); + + /*! + * @brief Transfer a provider entry from the add-on to Kodi + * @param kodiInstance Pointer to Kodi's CPVRClient class + * @param handle The handle parameter that Kodi used when requesting the channel list + * @param entry The entry to transfer to Kodi + */ + static void cb_transfer_provider_entry(void* kodiInstance, + const PVR_HANDLE handle, + const PVR_PROVIDER* entry); + + /*! + * @brief Transfer a timer entry from the add-on to Kodi + * @param kodiInstance Pointer to Kodi's CPVRClient class + * @param handle The handle parameter that Kodi used when requesting the timers list + * @param entry The entry to transfer to Kodi + */ + static void cb_transfer_timer_entry(void* kodiInstance, + const PVR_HANDLE handle, + const PVR_TIMER* entry); + + /*! + * @brief Transfer a recording entry from the add-on to Kodi + * @param kodiInstance Pointer to Kodi's CPVRClient class + * @param handle The handle parameter that Kodi used when requesting the recordings list + * @param entry The entry to transfer to Kodi + */ + static void cb_transfer_recording_entry(void* kodiInstance, + const PVR_HANDLE handle, + const PVR_RECORDING* entry); + + /*! + * @brief Add or replace a menu hook for the context menu for this add-on + * @param kodiInstance Pointer to Kodi's CPVRClient class + * @param hook The hook to add. + */ + static void cb_add_menu_hook(void* kodiInstance, const PVR_MENUHOOK* hook); + + /*! + * @brief Display a notification in Kodi that a recording started or stopped on the server + * @param kodiInstance Pointer to Kodi's CPVRClient class + * @param strName The name of the recording to display + * @param strFileName The filename of the recording + * @param bOnOff True when recording started, false when it stopped + */ + static void cb_recording_notification(void* kodiInstance, + const char* strName, + const char* strFileName, + bool bOnOff); + + /*! + * @brief Request Kodi to update it's list of channels + * @param kodiInstance Pointer to Kodi's CPVRClient class + */ + static void cb_trigger_channel_update(void* kodiInstance); + + /*! + * @brief Request Kodi to update it's list of providers + * @param kodiInstance Pointer to Kodi's CPVRClient class + */ + static void cb_trigger_provider_update(void* kodiInstance); + + /*! + * @brief Request Kodi to update it's list of timers + * @param kodiInstance Pointer to Kodi's CPVRClient class + */ + static void cb_trigger_timer_update(void* kodiInstance); + + /*! + * @brief Request Kodi to update it's list of recordings + * @param kodiInstance Pointer to Kodi's CPVRClient class + */ + static void cb_trigger_recording_update(void* kodiInstance); + + /*! + * @brief Request Kodi to update it's list of channel groups + * @param kodiInstance Pointer to Kodi's CPVRClient class + */ + static void cb_trigger_channel_groups_update(void* kodiInstance); + + /*! + * @brief Schedule an EPG update for the given channel channel + * @param kodiInstance A pointer to the add-on + * @param iChannelUid The unique id of the channel for this add-on + */ + static void cb_trigger_epg_update(void* kodiInstance, unsigned int iChannelUid); + + /*! + * @brief Free a packet that was allocated with AllocateDemuxPacket + * @param kodiInstance Pointer to Kodi's CPVRClient class + * @param pPacket The packet to free. + */ + static void cb_free_demux_packet(void* kodiInstance, DEMUX_PACKET* pPacket); + + /*! + * @brief Allocate a demux packet. Free with FreeDemuxPacket + * @param kodiInstance Pointer to Kodi's CPVRClient class. + * @param iDataSize The size of the data that will go into the packet + * @return The allocated packet. + */ + static DEMUX_PACKET* cb_allocate_demux_packet(void* kodiInstance, int iDataSize = 0); + + /*! + * @brief Notify a state change for a PVR backend connection + * @param kodiInstance Pointer to Kodi's CPVRClient class + * @param strConnectionString The connection string reported by the backend that can be displayed in the UI. + * @param newState The new state. + * @param strMessage A localized addon-defined string representing the new state, that can be displayed + * in the UI or NULL if the Kodi-defined default string for the new state shall be displayed. + */ + static void cb_connection_state_change(void* kodiInstance, + const char* strConnectionString, + PVR_CONNECTION_STATE newState, + const char* strMessage); + + /*! + * @brief Notify a state change for an EPG event + * @param kodiInstance Pointer to Kodi's CPVRClient class + * @param tag The EPG event. + * @param newState The new state. + * @param newState The new state. For EPG_EVENT_CREATED and EPG_EVENT_UPDATED, tag must be filled with all available + * event data, not just a delta. For EPG_EVENT_DELETED, it is sufficient to fill EPG_TAG.iUniqueBroadcastId + */ + static void cb_epg_event_state_change(void* kodiInstance, EPG_TAG* tag, EPG_EVENT_STATE newState); + + /*! @todo remove the use complete from them, or add as generl function?! + * Returns the ffmpeg codec id from given ffmpeg codec string name + */ + static PVR_CODEC cb_get_codec_by_name(const void* kodiInstance, const char* strCodecName); + //@} + + const int m_iClientId; /*!< unique ID of the client */ + std::atomic<bool> + m_bReadyToUse; /*!< true if this add-on is initialised (ADDON_Create returned true), false otherwise */ + std::atomic<bool> m_bBlockAddonCalls; /*!< true if no add-on API calls are allowed */ + mutable std::atomic<int> m_iAddonCalls; /*!< number of in-progress addon calls */ + mutable CEvent m_allAddonCallsFinished; /*!< fires after last in-progress addon call finished */ + PVR_CONNECTION_STATE m_connectionState; /*!< the backend connection state */ + PVR_CONNECTION_STATE m_prevConnectionState; /*!< the previous backend connection state */ + bool + m_ignoreClient; /*!< signals to PVRManager to ignore this client until it has been connected */ + std::vector<std::shared_ptr<CPVRTimerType>> + m_timertypes; /*!< timer types supported by this backend */ + mutable int m_iPriority; /*!< priority of the client */ + mutable bool m_bPriorityFetched; + + /* cached data */ + std::string m_strBackendName; /*!< the cached backend version */ + std::string m_strBackendVersion; /*!< the cached backend version */ + std::string m_strConnectionString; /*!< the cached connection string */ + std::string m_strBackendHostname; /*!< the cached backend hostname */ + CPVRClientCapabilities m_clientCapabilities; /*!< the cached add-on's capabilities */ + std::shared_ptr<CPVRClientMenuHooks> m_menuhooks; /*!< the menu hooks for this add-on */ + + /* stored strings to make sure const char* members in AddonProperties_PVR stay valid */ + std::string m_strUserPath; /*!< @brief translated path to the user profile */ + std::string m_strClientPath; /*!< @brief translated path to this add-on */ + + mutable CCriticalSection m_critSection; +}; +} // namespace PVR diff --git a/xbmc/pvr/addons/PVRClientCapabilities.cpp b/xbmc/pvr/addons/PVRClientCapabilities.cpp new file mode 100644 index 0000000..8650465 --- /dev/null +++ b/xbmc/pvr/addons/PVRClientCapabilities.cpp @@ -0,0 +1,85 @@ +/* + * 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 "PVRClientCapabilities.h" + +#include "guilib/LocalizeStrings.h" +#include "utils/StringUtils.h" + +#include <algorithm> +#include <iterator> + +using namespace PVR; + +CPVRClientCapabilities::CPVRClientCapabilities(const CPVRClientCapabilities& other) +{ + if (other.m_addonCapabilities) + m_addonCapabilities.reset(new PVR_ADDON_CAPABILITIES(*other.m_addonCapabilities)); + InitRecordingsLifetimeValues(); +} + +const CPVRClientCapabilities& CPVRClientCapabilities::operator=(const CPVRClientCapabilities& other) +{ + if (other.m_addonCapabilities) + m_addonCapabilities.reset(new PVR_ADDON_CAPABILITIES(*other.m_addonCapabilities)); + InitRecordingsLifetimeValues(); + return *this; +} + +const CPVRClientCapabilities& CPVRClientCapabilities::operator=( + const PVR_ADDON_CAPABILITIES& addonCapabilities) +{ + m_addonCapabilities.reset(new PVR_ADDON_CAPABILITIES(addonCapabilities)); + InitRecordingsLifetimeValues(); + return *this; +} + +void CPVRClientCapabilities::clear() +{ + m_recordingsLifetimeValues.clear(); + m_addonCapabilities.reset(); +} + +void CPVRClientCapabilities::InitRecordingsLifetimeValues() +{ + m_recordingsLifetimeValues.clear(); + if (m_addonCapabilities && m_addonCapabilities->iRecordingsLifetimesSize > 0) + { + for (unsigned int i = 0; i < m_addonCapabilities->iRecordingsLifetimesSize; ++i) + { + int iValue = m_addonCapabilities->recordingsLifetimeValues[i].iValue; + std::string strDescr(m_addonCapabilities->recordingsLifetimeValues[i].strDescription); + if (strDescr.empty()) + { + // No description given by addon. Create one from value. + strDescr = std::to_string(iValue); + } + m_recordingsLifetimeValues.emplace_back(strDescr, iValue); + } + } + else if (SupportsRecordingsLifetimeChange()) + { + // No values given by addon, but lifetime supported. Use default values 1..365 + for (int i = 1; i < 366; ++i) + { + m_recordingsLifetimeValues.emplace_back(StringUtils::Format(g_localizeStrings.Get(17999), i), + i); // "{} days" + } + } + else + { + // No lifetime supported. + } +} + +void CPVRClientCapabilities::GetRecordingsLifetimeValues( + std::vector<std::pair<std::string, int>>& list) const +{ + std::copy(m_recordingsLifetimeValues.cbegin(), m_recordingsLifetimeValues.cend(), + std::back_inserter(list)); +} diff --git a/xbmc/pvr/addons/PVRClientCapabilities.h b/xbmc/pvr/addons/PVRClientCapabilities.h new file mode 100644 index 0000000..edf2090 --- /dev/null +++ b/xbmc/pvr/addons/PVRClientCapabilities.h @@ -0,0 +1,277 @@ +/* + * 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.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +namespace PVR +{ + +class CPVRClientCapabilities +{ +public: + CPVRClientCapabilities() = default; + virtual ~CPVRClientCapabilities() = default; + + CPVRClientCapabilities(const CPVRClientCapabilities& other); + const CPVRClientCapabilities& operator=(const CPVRClientCapabilities& other); + + const CPVRClientCapabilities& operator=(const PVR_ADDON_CAPABILITIES& addonCapabilities); + + void clear(); + + ///////////////////////////////////////////////////////////////////////////////// + // + // Channels + // + ///////////////////////////////////////////////////////////////////////////////// + + /*! + * @brief Check whether this add-on supports TV channels. + * @return True if supported, false otherwise. + */ + bool SupportsTV() const { return m_addonCapabilities && m_addonCapabilities->bSupportsTV; } + + /*! + * @brief Check whether this add-on supports radio channels. + * @return True if supported, false otherwise. + */ + bool SupportsRadio() const { return m_addonCapabilities && m_addonCapabilities->bSupportsRadio; } + + /*! + * @brief Check whether this add-on supports providers. + * @return True if supported, false otherwise. + */ + bool SupportsProviders() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsProviders; + } + + /*! + * @brief Check whether this add-on supports channel groups. + * @return True if supported, false otherwise. + */ + bool SupportsChannelGroups() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsChannelGroups; + } + + /*! + * @brief Check whether this add-on supports scanning for new channels on the backend. + * @return True if supported, false otherwise. + */ + bool SupportsChannelScan() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsChannelScan; + } + + /*! + * @brief Check whether this add-on supports the following functions: + * DeleteChannel, RenameChannel, DialogChannelSettings and DialogAddChannel. + * + * @return True if supported, false otherwise. + */ + bool SupportsChannelSettings() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsChannelSettings; + } + + /*! + * @brief Check whether this add-on supports descramble information for playing channels. + * @return True if supported, false otherwise. + */ + bool SupportsDescrambleInfo() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsDescrambleInfo; + } + + ///////////////////////////////////////////////////////////////////////////////// + // + // EPG + // + ///////////////////////////////////////////////////////////////////////////////// + + /*! + * @brief Check whether this add-on provides EPG information. + * @return True if supported, false otherwise. + */ + bool SupportsEPG() const { return m_addonCapabilities && m_addonCapabilities->bSupportsEPG; } + + /*! + * @brief Check whether this add-on supports asynchronous transfer of epg events. + * @return True if supported, false otherwise. + */ + bool SupportsAsyncEPGTransfer() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsAsyncEPGTransfer; + } + + ///////////////////////////////////////////////////////////////////////////////// + // + // Timers + // + ///////////////////////////////////////////////////////////////////////////////// + + /*! + * @brief Check whether this add-on supports the creation and editing of timers. + * @return True if supported, false otherwise. + */ + bool SupportsTimers() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsTimers; + } + + ///////////////////////////////////////////////////////////////////////////////// + // + // Recordings + // + ///////////////////////////////////////////////////////////////////////////////// + + /*! + * @brief Check whether this add-on supports recordings. + * @return True if supported, false otherwise. + */ + bool SupportsRecordings() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings; + } + + /*! + * @brief Check whether this add-on supports undelete of deleted recordings. + * @return True if supported, false otherwise. + */ + bool SupportsRecordingsUndelete() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings && + m_addonCapabilities->bSupportsRecordingsUndelete; + } + + /*! + * @brief Check whether this add-on supports play count for recordings. + * @return True if supported, false otherwise. + */ + bool SupportsRecordingsPlayCount() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings && + m_addonCapabilities->bSupportsRecordingPlayCount; + } + + /*! + * @brief Check whether this add-on supports store/retrieve of last played position for recordings.. + * @return True if supported, false otherwise. + */ + bool SupportsRecordingsLastPlayedPosition() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings && + m_addonCapabilities->bSupportsLastPlayedPosition; + } + + /*! + * @brief Check whether this add-on supports retrieving an edit decision list for recordings. + * @return True if supported, false otherwise. + */ + bool SupportsRecordingsEdl() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings && + m_addonCapabilities->bSupportsRecordingEdl; + } + + /*! + * @brief Check whether this add-on supports retrieving an edit decision list for epg tags. + * @return True if supported, false otherwise. + */ + bool SupportsEpgTagEdl() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsEPG && + m_addonCapabilities->bSupportsEPGEdl; + } + + /*! + * @brief Check whether this add-on supports renaming recordings.. + * @return True if supported, false otherwise. + */ + bool SupportsRecordingsRename() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings && + m_addonCapabilities->bSupportsRecordingsRename; + } + + /*! + * @brief Check whether this add-on supports changing lifetime of recording. + * @return True if supported, false otherwise. + */ + bool SupportsRecordingsLifetimeChange() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings && + m_addonCapabilities->bSupportsRecordingsLifetimeChange; + } + + /*! + * @brief Obtain a list with all possible values for recordings lifetime. + * @param list out, the list with the values or an empty list, if lifetime is not supported. + */ + void GetRecordingsLifetimeValues(std::vector<std::pair<std::string, int>>& list) const; + + /*! + * @brief Check whether this add-on supports retrieving the size recordings.. + * @return True if supported, false otherwise. + */ + bool SupportsRecordingsSize() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings && + m_addonCapabilities->bSupportsRecordingSize; + } + + /*! + * @brief Check whether this add-on supports deleting recordings. + * @return True if supported, false otherwise. + */ + bool SupportsRecordingsDelete() const + { + return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings && + m_addonCapabilities->bSupportsRecordingsDelete; + } + + ///////////////////////////////////////////////////////////////////////////////// + // + // Streams + // + ///////////////////////////////////////////////////////////////////////////////// + + /*! + * @brief Check whether this add-on provides an input stream. false if Kodi handles the stream. + * @return True if supported, false otherwise. + */ + bool HandlesInputStream() const + { + return m_addonCapabilities && m_addonCapabilities->bHandlesInputStream; + } + + /*! + * @brief Check whether this add-on demultiplexes packets. + * @return True if supported, false otherwise. + */ + bool HandlesDemuxing() const + { + return m_addonCapabilities && m_addonCapabilities->bHandlesDemuxing; + } + +private: + void InitRecordingsLifetimeValues(); + + std::unique_ptr<PVR_ADDON_CAPABILITIES> m_addonCapabilities; + std::vector<std::pair<std::string, int>> m_recordingsLifetimeValues; +}; + +} // namespace PVR diff --git a/xbmc/pvr/addons/PVRClientMenuHooks.cpp b/xbmc/pvr/addons/PVRClientMenuHooks.cpp new file mode 100644 index 0000000..12f150a --- /dev/null +++ b/xbmc/pvr/addons/PVRClientMenuHooks.cpp @@ -0,0 +1,185 @@ +/* + * 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 "PVRClientMenuHooks.h" + +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_menu_hook.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRContextMenus.h" +#include "utils/log.h" + +namespace PVR +{ + +CPVRClientMenuHook::CPVRClientMenuHook(const std::string& addonId, const PVR_MENUHOOK& hook) +: m_addonId(addonId), + m_hook(new PVR_MENUHOOK(hook)) +{ + if (hook.category != PVR_MENUHOOK_UNKNOWN && + hook.category != PVR_MENUHOOK_ALL && + hook.category != PVR_MENUHOOK_CHANNEL && + hook.category != PVR_MENUHOOK_TIMER && + hook.category != PVR_MENUHOOK_EPG && + hook.category != PVR_MENUHOOK_RECORDING && + hook.category != PVR_MENUHOOK_DELETED_RECORDING && + hook.category != PVR_MENUHOOK_SETTING) + CLog::LogF(LOGERROR, "Unknown PVR_MENUHOOK_CAT value: {}", hook.category); +} + +bool CPVRClientMenuHook::operator ==(const CPVRClientMenuHook& right) const +{ + if (this == &right) + return true; + + return m_addonId == right.m_addonId && + m_hook->iHookId == right.m_hook->iHookId && + m_hook->iLocalizedStringId == right.m_hook->iLocalizedStringId && + m_hook->category == right.m_hook->category; +} + +bool CPVRClientMenuHook::IsAllHook() const +{ + return m_hook->category == PVR_MENUHOOK_ALL; +} + +bool CPVRClientMenuHook::IsChannelHook() const +{ + return m_hook->category == PVR_MENUHOOK_CHANNEL; +} + +bool CPVRClientMenuHook::IsTimerHook() const +{ + return m_hook->category == PVR_MENUHOOK_TIMER; +} + +bool CPVRClientMenuHook::IsEpgHook() const +{ + return m_hook->category == PVR_MENUHOOK_EPG; +} + +bool CPVRClientMenuHook::IsRecordingHook() const +{ + return m_hook->category == PVR_MENUHOOK_RECORDING; +} + +bool CPVRClientMenuHook::IsDeletedRecordingHook() const +{ + return m_hook->category == PVR_MENUHOOK_DELETED_RECORDING; +} + +bool CPVRClientMenuHook::IsSettingsHook() const +{ + return m_hook->category == PVR_MENUHOOK_SETTING; +} + +std::string CPVRClientMenuHook::GetAddonId() const +{ + return m_addonId; +} + +unsigned int CPVRClientMenuHook::GetId() const +{ + return m_hook->iHookId; +} + +unsigned int CPVRClientMenuHook::GetLabelId() const +{ + return m_hook->iLocalizedStringId; +} + +std::string CPVRClientMenuHook::GetLabel() const +{ + return g_localizeStrings.GetAddonString(m_addonId, m_hook->iLocalizedStringId); +} + +void CPVRClientMenuHooks::AddHook(const PVR_MENUHOOK& addonHook) +{ + if (!m_hooks) + m_hooks.reset(new std::vector<CPVRClientMenuHook>()); + + const CPVRClientMenuHook hook(m_addonId, addonHook); + m_hooks->emplace_back(hook); + CPVRContextMenuManager::GetInstance().AddMenuHook(hook); +} + +void CPVRClientMenuHooks::Clear() +{ + if (!m_hooks) + return; + + for (const auto& hook : *m_hooks) + CPVRContextMenuManager::GetInstance().RemoveMenuHook(hook); + + m_hooks.reset(); +} + +std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetHooks( + const std::function<bool(const CPVRClientMenuHook& hook)>& function) const +{ + std::vector<CPVRClientMenuHook> hooks; + + if (!m_hooks) + return hooks; + + for (const CPVRClientMenuHook& hook : *m_hooks) + { + if (function(hook) || hook.IsAllHook()) + hooks.emplace_back(hook); + } + return hooks; +} + +std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetChannelHooks() const +{ + return GetHooks([](const CPVRClientMenuHook& hook) + { + return hook.IsChannelHook(); + }); +} + +std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetTimerHooks() const +{ + return GetHooks([](const CPVRClientMenuHook& hook) + { + return hook.IsTimerHook(); + }); +} + +std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetEpgHooks() const +{ + return GetHooks([](const CPVRClientMenuHook& hook) + { + return hook.IsEpgHook(); + }); +} + +std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetRecordingHooks() const +{ + return GetHooks([](const CPVRClientMenuHook& hook) + { + return hook.IsRecordingHook(); + }); +} + +std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetDeletedRecordingHooks() const +{ + return GetHooks([](const CPVRClientMenuHook& hook) + { + return hook.IsDeletedRecordingHook(); + }); +} + +std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetSettingsHooks() const +{ + return GetHooks([](const CPVRClientMenuHook& hook) + { + return hook.IsSettingsHook(); + }); +} + +} // namespace PVR diff --git a/xbmc/pvr/addons/PVRClientMenuHooks.h b/xbmc/pvr/addons/PVRClientMenuHooks.h new file mode 100644 index 0000000..69d7f79 --- /dev/null +++ b/xbmc/pvr/addons/PVRClientMenuHooks.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <functional> +#include <memory> +#include <string> +#include <vector> + +struct PVR_MENUHOOK; + +namespace PVR +{ + class CPVRClientMenuHook + { + public: + CPVRClientMenuHook() = delete; + virtual ~CPVRClientMenuHook() = default; + + CPVRClientMenuHook(const std::string& addonId, const PVR_MENUHOOK& hook); + + bool operator ==(const CPVRClientMenuHook& right) const; + + bool IsAllHook() const; + bool IsChannelHook() const; + bool IsTimerHook() const; + bool IsEpgHook() const; + bool IsRecordingHook() const; + bool IsDeletedRecordingHook() const; + bool IsSettingsHook() const; + + std::string GetAddonId() const; + unsigned int GetId() const; + unsigned int GetLabelId() const; + std::string GetLabel() const; + + private: + std::string m_addonId; + std::shared_ptr<PVR_MENUHOOK> m_hook; + }; + + class CPVRClientMenuHooks + { + public: + CPVRClientMenuHooks() = default; + virtual ~CPVRClientMenuHooks() = default; + + explicit CPVRClientMenuHooks(const std::string& addonId) : m_addonId(addonId) {} + + void AddHook(const PVR_MENUHOOK& addonHook); + void Clear(); + + std::vector<CPVRClientMenuHook> GetChannelHooks() const; + std::vector<CPVRClientMenuHook> GetTimerHooks() const; + std::vector<CPVRClientMenuHook> GetEpgHooks() const; + std::vector<CPVRClientMenuHook> GetRecordingHooks() const; + std::vector<CPVRClientMenuHook> GetDeletedRecordingHooks() const; + std::vector<CPVRClientMenuHook> GetSettingsHooks() const; + + private: + std::vector<CPVRClientMenuHook> GetHooks( + const std::function<bool(const CPVRClientMenuHook& hook)>& function) const; + + std::string m_addonId; + std::unique_ptr<std::vector<CPVRClientMenuHook>> m_hooks; + }; +} diff --git a/xbmc/pvr/addons/PVRClientUID.cpp b/xbmc/pvr/addons/PVRClientUID.cpp new file mode 100644 index 0000000..8788385 --- /dev/null +++ b/xbmc/pvr/addons/PVRClientUID.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012-2022 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 "PVRClientUID.h" + +#include <functional> + +using namespace PVR; + +int CPVRClientUID::GetUID() const +{ + if (!m_uidCreated) + { + std::hash<std::string> hasher; + + // Note: For database backwards compatibility reasons the hash of the first instance + // must be calculated just from the addonId, not from addonId and instanceId. + m_uid = static_cast<int>(hasher( + (m_instanceID > ADDON::ADDON_FIRST_INSTANCE_ID ? std::to_string(m_instanceID) + "@" : "") + + m_addonID)); + if (m_uid < 0) + m_uid = -m_uid; + + m_uidCreated = true; + } + + return m_uid; +} diff --git a/xbmc/pvr/addons/PVRClientUID.h b/xbmc/pvr/addons/PVRClientUID.h new file mode 100644 index 0000000..5b5d1c0 --- /dev/null +++ b/xbmc/pvr/addons/PVRClientUID.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2012-2022 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/IAddon.h" + +#include <string> + +namespace PVR +{ +class CPVRClientUID final +{ +public: + CPVRClientUID(const std::string& addonID, ADDON::AddonInstanceId instanceID) + : m_addonID(addonID), m_instanceID(instanceID) + { + } + + virtual ~CPVRClientUID() = default; + + /*! + * @brief Return the numeric UID. + * @return The numeric UID. + */ + int GetUID() const; + +private: + CPVRClientUID() = delete; + + std::string m_addonID; + ADDON::AddonInstanceId m_instanceID{ADDON::ADDON_SINGLETON_INSTANCE_ID}; + + mutable bool m_uidCreated{false}; + mutable int m_uid{0}; +}; +} // namespace PVR diff --git a/xbmc/pvr/addons/PVRClients.cpp b/xbmc/pvr/addons/PVRClients.cpp new file mode 100644 index 0000000..8cda035 --- /dev/null +++ b/xbmc/pvr/addons/PVRClients.cpp @@ -0,0 +1,977 @@ +/* + * 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 "PVRClients.h" + +#include "ServiceBroker.h" +#include "addons/AddonEvents.h" +#include "addons/AddonManager.h" +#include "addons/addoninfo/AddonInfo.h" +#include "addons/addoninfo/AddonType.h" +#include "guilib/LocalizeStrings.h" +#include "messaging/ApplicationMessenger.h" +#include "pvr/PVREventLogJob.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClientUID.h" +#include "pvr/channels/PVRChannelGroupInternal.h" +#include "pvr/guilib/PVRGUIProgressHandler.h" +#include "utils/JobManager.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <algorithm> +#include <functional> +#include <memory> +#include <mutex> +#include <string> +#include <utility> +#include <vector> + +using namespace ADDON; +using namespace PVR; + +CPVRClients::CPVRClients() +{ + CServiceBroker::GetAddonMgr().RegisterAddonMgrCallback(AddonType::PVRDLL, this); + CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CPVRClients::OnAddonEvent); +} + +CPVRClients::~CPVRClients() +{ + CServiceBroker::GetAddonMgr().Events().Unsubscribe(this); + CServiceBroker::GetAddonMgr().UnregisterAddonMgrCallback(AddonType::PVRDLL); + + for (const auto& client : m_clientMap) + { + client.second->Destroy(); + } +} + +void CPVRClients::Start() +{ + UpdateClients(); +} + +void CPVRClients::Stop() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& client : m_clientMap) + { + client.second->Stop(); + } +} + +void CPVRClients::Continue() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& client : m_clientMap) + { + client.second->Continue(); + } +} + +void CPVRClients::UpdateClients( + const std::string& changedAddonId /* = "" */, + ADDON::AddonInstanceId changedInstanceId /* = ADDON::ADDON_SINGLETON_INSTANCE_ID */) +{ + std::vector<std::pair<AddonInfoPtr, bool>> addonsWithStatus; + if (!GetAddonsWithStatus(changedAddonId, addonsWithStatus)) + return; + + std::vector<std::shared_ptr<CPVRClient>> clientsToCreate; // client + std::vector<std::pair<int, std::string>> clientsToReCreate; // client id, addon name + std::vector<int> clientsToDestroy; // client id + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& addonWithStatus : addonsWithStatus) + { + const AddonInfoPtr addon = addonWithStatus.first; + const std::vector<std::pair<ADDON::AddonInstanceId, bool>> instanceIdsWithStatus = + GetInstanceIdsWithStatus(addon, addonWithStatus.second); + + for (const auto& instanceIdWithStatus : instanceIdsWithStatus) + { + const ADDON::AddonInstanceId instanceId = instanceIdWithStatus.first; + bool instanceEnabled = instanceIdWithStatus.second; + const CPVRClientUID clientUID(addon->ID(), instanceId); + const int clientId = clientUID.GetUID(); + + if (instanceEnabled && (!IsKnownClient(clientId) || !IsCreatedClient(clientId))) + { + std::shared_ptr<CPVRClient> client; + const bool isKnownClient = IsKnownClient(clientId); + if (isKnownClient) + { + client = GetClient(clientId); + } + else + { + client = std::make_shared<CPVRClient>(addon, instanceId, clientId); + if (!client) + { + CLog::LogF(LOGERROR, "Severe error, incorrect add-on type"); + continue; + } + } + + // determine actual enabled state of instance + if (instanceId != ADDON_SINGLETON_INSTANCE_ID) + instanceEnabled = client->IsEnabled(); + + if (instanceEnabled) + clientsToCreate.emplace_back(client); + else if (isKnownClient) + clientsToDestroy.emplace_back(clientId); + } + else if (IsCreatedClient(clientId)) + { + // determine actual enabled state of instance + if (instanceEnabled && instanceId != ADDON_SINGLETON_INSTANCE_ID) + { + const std::shared_ptr<CPVRClient> client = GetClient(clientId); + instanceEnabled = client ? client->IsEnabled() : false; + } + + if (instanceEnabled) + clientsToReCreate.emplace_back(clientId, addon->Name()); + else + clientsToDestroy.emplace_back(clientId); + } + } + } + } + + if (!clientsToCreate.empty() || !clientsToReCreate.empty() || !clientsToDestroy.empty()) + { + CServiceBroker::GetPVRManager().Stop(); + + auto progressHandler = std::make_unique<CPVRGUIProgressHandler>( + g_localizeStrings.Get(19239)); // Creating PVR clients + + unsigned int i = 0; + for (const auto& client : clientsToCreate) + { + progressHandler->UpdateProgress(client->Name(), i++, + clientsToCreate.size() + clientsToReCreate.size()); + + const ADDON_STATUS status = client->Create(); + + if (status != ADDON_STATUS_OK) + { + CLog::LogF(LOGERROR, "Failed to create add-on {}, status = {}", client->ID(), status); + if (status == ADDON_STATUS_PERMANENT_FAILURE) + { + CServiceBroker::GetAddonMgr().DisableAddon(client->ID(), + AddonDisabledReason::PERMANENT_FAILURE); + CServiceBroker::GetJobManager()->AddJob( + new CPVREventLogJob(true, EventLevel::Error, client->Name(), + g_localizeStrings.Get(24070), client->Icon()), + nullptr); + } + } + } + + for (const auto& clientInfo : clientsToReCreate) + { + progressHandler->UpdateProgress(clientInfo.second, i++, + clientsToCreate.size() + clientsToReCreate.size()); + + // stop and recreate client + StopClient(clientInfo.first, true /* restart */); + } + + progressHandler.reset(); + + for (const auto& client : clientsToDestroy) + { + // destroy client + StopClient(client, false /* no restart */); + } + + if (!clientsToCreate.empty()) + { + // update created clients map + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& client : clientsToCreate) + { + if (m_clientMap.find(client->GetID()) == m_clientMap.end()) + { + m_clientMap.insert({client->GetID(), client}); + } + } + } + + CServiceBroker::GetPVRManager().Start(); + } +} + +bool CPVRClients::RequestRestart(const std::string& addonId, + ADDON::AddonInstanceId instanceId, + bool bDataChanged) +{ + CServiceBroker::GetJobManager()->Submit([this, addonId, instanceId] { + UpdateClients(addonId, instanceId); + return true; + }); + return true; +} + +bool CPVRClients::StopClient(int clientId, bool restart) +{ + // stop playback if needed + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying()) + CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + const std::shared_ptr<CPVRClient> client = GetClient(clientId); + if (client) + { + if (restart) + { + client->ReCreate(); + } + else + { + const auto it = m_clientMap.find(clientId); + if (it != m_clientMap.end()) + m_clientMap.erase(it); + + client->Destroy(); + } + return true; + } + + return false; +} + +void CPVRClients::OnAddonEvent(const AddonEvent& event) +{ + if (typeid(event) == typeid(AddonEvents::Enabled) || // also called on install, + typeid(event) == typeid(AddonEvents::Disabled) || // not called on uninstall + typeid(event) == typeid(AddonEvents::UnInstalled) || + typeid(event) == typeid(AddonEvents::ReInstalled) || + typeid(event) == typeid(AddonEvents::InstanceAdded) || + typeid(event) == typeid(AddonEvents::InstanceRemoved)) + { + // update addons + const std::string addonId = event.addonId; + const ADDON::AddonInstanceId instanceId = event.instanceId; + if (CServiceBroker::GetAddonMgr().HasType(addonId, AddonType::PVRDLL)) + { + CServiceBroker::GetJobManager()->Submit([this, addonId, instanceId] { + UpdateClients(addonId, instanceId); + return true; + }); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// client access +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +std::shared_ptr<CPVRClient> CPVRClients::GetClient(int clientId) const +{ + if (clientId <= PVR_INVALID_CLIENT_ID) + return {}; + + std::unique_lock<CCriticalSection> lock(m_critSection); + const auto it = m_clientMap.find(clientId); + if (it != m_clientMap.end()) + return it->second; + + return {}; +} + +int CPVRClients::CreatedClientAmount() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::count_if(m_clientMap.cbegin(), m_clientMap.cend(), + [](const auto& client) { return client.second->ReadyToUse(); }); +} + +bool CPVRClients::HasCreatedClients() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), + [](const auto& client) { return client.second->ReadyToUse(); }); +} + +bool CPVRClients::IsKnownClient(int clientId) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + // valid client IDs start at 1 + const auto it = m_clientMap.find(clientId); + return (it != m_clientMap.end() && (*it).second->GetID() > 0); +} + +bool CPVRClients::IsCreatedClient(int iClientId) const +{ + return GetCreatedClient(iClientId) != nullptr; +} + +std::shared_ptr<CPVRClient> CPVRClients::GetCreatedClient(int clientId) const +{ + std::shared_ptr<CPVRClient> client = GetClient(clientId); + if (client && client->ReadyToUse()) + return client; + + return {}; +} + +CPVRClientMap CPVRClients::GetCreatedClients() const +{ + CPVRClientMap clients; + + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& client : m_clientMap) + { + if (client.second->ReadyToUse()) + { + clients.insert(std::make_pair(client.second->GetID(), client.second)); + } + } + + return clients; +} + +std::vector<CVariant> CPVRClients::GetClientProviderInfos() const +{ + std::vector<AddonInfoPtr> addonInfos; + // Get enabled and disabled PVR client addon infos + CServiceBroker::GetAddonMgr().GetAddonInfos(addonInfos, false, AddonType::PVRDLL); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + std::vector<CVariant> clientProviderInfos; + for (const auto& addonInfo : addonInfos) + { + std::vector<ADDON::AddonInstanceId> instanceIds = addonInfo->GetKnownInstanceIds(); + for (const auto& instanceId : instanceIds) + { + CVariant clientProviderInfo(CVariant::VariantTypeObject); + clientProviderInfo["clientid"] = CPVRClientUID(addonInfo->ID(), instanceId).GetUID(); + clientProviderInfo["addonid"] = addonInfo->ID(); + clientProviderInfo["instanceid"] = instanceId; + clientProviderInfo["enabled"] = + !CServiceBroker::GetAddonMgr().IsAddonDisabled(addonInfo->ID()); + clientProviderInfo["name"] = addonInfo->Name(); + clientProviderInfo["icon"] = addonInfo->Icon(); + auto& artMap = addonInfo->Art(); + auto thumbEntry = artMap.find("thumb"); + if (thumbEntry != artMap.end()) + clientProviderInfo["thumb"] = thumbEntry->second; + + clientProviderInfos.emplace_back(clientProviderInfo); + } + } + + return clientProviderInfos; +} + +int CPVRClients::GetFirstCreatedClientID() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const auto it = std::find_if(m_clientMap.cbegin(), m_clientMap.cend(), + [](const auto& client) { return client.second->ReadyToUse(); }); + return it != m_clientMap.cend() ? (*it).second->GetID() : -1; +} + +PVR_ERROR CPVRClients::GetCallableClients(CPVRClientMap& clientsReady, + std::vector<int>& clientsNotReady) const +{ + clientsNotReady.clear(); + + std::vector<AddonInfoPtr> addons; + CServiceBroker::GetAddonMgr().GetAddonInfos(addons, true, AddonType::PVRDLL); + + for (const auto& addon : addons) + { + std::vector<ADDON::AddonInstanceId> instanceIds = addon->GetKnownInstanceIds(); + for (const auto& instanceId : instanceIds) + { + const int clientId = CPVRClientUID(addon->ID(), instanceId).GetUID(); + const std::shared_ptr<CPVRClient> client = GetClient(clientId); + + if (client && client->ReadyToUse() && !client->IgnoreClient()) + { + clientsReady.insert(std::make_pair(clientId, client)); + } + else + { + clientsNotReady.emplace_back(clientId); + } + } + } + + return clientsNotReady.empty() ? PVR_ERROR_NO_ERROR : PVR_ERROR_SERVER_ERROR; +} + +int CPVRClients::EnabledClientAmount() const +{ + CPVRClientMap clientMap; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + clientMap = m_clientMap; + } + + ADDON::CAddonMgr& addonMgr = CServiceBroker::GetAddonMgr(); + return std::count_if(clientMap.cbegin(), clientMap.cend(), [&addonMgr](const auto& client) { + return !addonMgr.IsAddonDisabled(client.second->ID()); + }); +} + +bool CPVRClients::IsEnabledClient(int clientId) const +{ + const std::shared_ptr<CPVRClient> client = GetClient(clientId); + return client && !CServiceBroker::GetAddonMgr().IsAddonDisabled(client->ID()); +} + +std::vector<CVariant> CPVRClients::GetEnabledClientInfos() const +{ + std::vector<CVariant> clientInfos; + + CPVRClientMap clientMap; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + clientMap = m_clientMap; + } + + for (const auto& client : clientMap) + { + const auto& addonInfo = + CServiceBroker::GetAddonMgr().GetAddonInfo(client.second->ID(), AddonType::PVRDLL); + + if (addonInfo) + { + // This will be the same variant structure used in the json api + CVariant clientInfo(CVariant::VariantTypeObject); + clientInfo["clientid"] = client.first; + clientInfo["addonid"] = client.second->ID(); + clientInfo["instanceid"] = client.second->InstanceId(); + clientInfo["label"] = addonInfo->Name(); // Note that this is called label instead of name + + const auto& capabilities = client.second->GetClientCapabilities(); + clientInfo["supportstv"] = capabilities.SupportsTV(); + clientInfo["supportsradio"] = capabilities.SupportsRadio(); + clientInfo["supportsepg"] = capabilities.SupportsEPG(); + clientInfo["supportsrecordings"] = capabilities.SupportsRecordings(); + clientInfo["supportstimers"] = capabilities.SupportsTimers(); + clientInfo["supportschannelgroups"] = capabilities.SupportsChannelGroups(); + clientInfo["supportschannelscan"] = capabilities.SupportsChannelScan(); + clientInfo["supportchannelproviders"] = capabilities.SupportsProviders(); + + clientInfos.push_back(clientInfo); + } + } + + return clientInfos; +} + +bool CPVRClients::HasIgnoredClients() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), + [](const auto& client) { return client.second->IgnoreClient(); }); +} + +std::vector<ADDON::AddonInstanceId> CPVRClients::GetKnownInstanceIds( + const std::string& addonID) const +{ + std::vector<ADDON::AddonInstanceId> instanceIds; + + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& entry : m_clientMap) + { + if (entry.second->ID() == addonID) + instanceIds.emplace_back(entry.second->InstanceId()); + } + + return instanceIds; +} + +bool CPVRClients::GetAddonsWithStatus( + const std::string& changedAddonId, + std::vector<std::pair<AddonInfoPtr, bool>>& addonsWithStatus) const +{ + std::vector<AddonInfoPtr> addons; + CServiceBroker::GetAddonMgr().GetAddonInfos(addons, false, AddonType::PVRDLL); + + if (addons.empty()) + return false; + + bool foundChangedAddon = changedAddonId.empty(); + for (const auto& addon : addons) + { + bool enabled = !CServiceBroker::GetAddonMgr().IsAddonDisabled(addon->ID()); + addonsWithStatus.emplace_back(std::make_pair(addon, enabled)); + + if (!foundChangedAddon && addon->ID() == changedAddonId) + foundChangedAddon = true; + } + + return foundChangedAddon; +} + +std::vector<std::pair<ADDON::AddonInstanceId, bool>> CPVRClients::GetInstanceIdsWithStatus( + const AddonInfoPtr& addon, bool addonIsEnabled) const +{ + std::vector<std::pair<ADDON::AddonInstanceId, bool>> instanceIdsWithStatus; + + std::vector<ADDON::AddonInstanceId> instanceIds = addon->GetKnownInstanceIds(); + std::transform(instanceIds.cbegin(), instanceIds.cend(), + std::back_inserter(instanceIdsWithStatus), [addonIsEnabled](const auto& id) { + return std::pair<ADDON::AddonInstanceId, bool>(id, addonIsEnabled); + }); + + // find removed instances + const std::vector<ADDON::AddonInstanceId> knownInstanceIds = GetKnownInstanceIds(addon->ID()); + for (const auto& knownInstanceId : knownInstanceIds) + { + if (std::find(instanceIds.begin(), instanceIds.end(), knownInstanceId) == instanceIds.end()) + { + // instance was removed + instanceIdsWithStatus.emplace_back( + std::pair<ADDON::AddonInstanceId, bool>(knownInstanceId, false)); + } + } + + return instanceIdsWithStatus; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// client API calls +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +std::vector<SBackend> CPVRClients::GetBackendProperties() const +{ + std::vector<SBackend> backendProperties; + + ForCreatedClients(__FUNCTION__, [&backendProperties](const std::shared_ptr<CPVRClient>& client) { + SBackend properties; + + if (client->GetDriveSpace(properties.diskTotal, properties.diskUsed) == PVR_ERROR_NO_ERROR) + { + properties.diskTotal *= 1024; + properties.diskUsed *= 1024; + } + + int iAmount = 0; + if (client->GetProvidersAmount(iAmount) == PVR_ERROR_NO_ERROR) + properties.numProviders = iAmount; + if (client->GetChannelGroupsAmount(iAmount) == PVR_ERROR_NO_ERROR) + properties.numChannelGroups = iAmount; + if (client->GetChannelsAmount(iAmount) == PVR_ERROR_NO_ERROR) + properties.numChannels = iAmount; + if (client->GetTimersAmount(iAmount) == PVR_ERROR_NO_ERROR) + properties.numTimers = iAmount; + if (client->GetRecordingsAmount(false, iAmount) == PVR_ERROR_NO_ERROR) + properties.numRecordings = iAmount; + if (client->GetRecordingsAmount(true, iAmount) == PVR_ERROR_NO_ERROR) + properties.numDeletedRecordings = iAmount; + properties.name = client->GetBackendName(); + properties.version = client->GetBackendVersion(); + properties.host = client->GetConnectionString(); + + backendProperties.emplace_back(properties); + return PVR_ERROR_NO_ERROR; + }); + + return backendProperties; +} + +bool CPVRClients::GetTimers(const std::vector<std::shared_ptr<CPVRClient>>& clients, + CPVRTimersContainer* timers, + std::vector<int>& failedClients) +{ + return ForClients(__FUNCTION__, clients, + [timers](const std::shared_ptr<CPVRClient>& client) { + return client->GetTimers(timers); + }, + failedClients) == PVR_ERROR_NO_ERROR; +} + +PVR_ERROR CPVRClients::GetTimerTypes(std::vector<std::shared_ptr<CPVRTimerType>>& results) const +{ + return ForCreatedClients(__FUNCTION__, [&results](const std::shared_ptr<CPVRClient>& client) { + std::vector<std::shared_ptr<CPVRTimerType>> types; + PVR_ERROR ret = client->GetTimerTypes(types); + if (ret == PVR_ERROR_NO_ERROR) + results.insert(results.end(), types.begin(), types.end()); + return ret; + }); +} + +PVR_ERROR CPVRClients::GetRecordings(const std::vector<std::shared_ptr<CPVRClient>>& clients, + CPVRRecordings* recordings, + bool deleted, + std::vector<int>& failedClients) +{ + return ForClients(__FUNCTION__, clients, + [recordings, deleted](const std::shared_ptr<CPVRClient>& client) { + return client->GetRecordings(recordings, deleted); + }, + failedClients); +} + +PVR_ERROR CPVRClients::DeleteAllRecordingsFromTrash() +{ + return ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) { + return client->DeleteAllRecordingsFromTrash(); + }); +} + +PVR_ERROR CPVRClients::SetEPGMaxPastDays(int iPastDays) +{ + return ForCreatedClients(__FUNCTION__, [iPastDays](const std::shared_ptr<CPVRClient>& client) { + return client->SetEPGMaxPastDays(iPastDays); + }); +} + +PVR_ERROR CPVRClients::SetEPGMaxFutureDays(int iFutureDays) +{ + return ForCreatedClients(__FUNCTION__, [iFutureDays](const std::shared_ptr<CPVRClient>& client) { + return client->SetEPGMaxFutureDays(iFutureDays); + }); +} + +PVR_ERROR CPVRClients::GetChannels(const std::vector<std::shared_ptr<CPVRClient>>& clients, + bool bRadio, + std::vector<std::shared_ptr<CPVRChannel>>& channels, + std::vector<int>& failedClients) +{ + return ForClients(__FUNCTION__, clients, + [bRadio, &channels](const std::shared_ptr<CPVRClient>& client) { + return client->GetChannels(bRadio, channels); + }, + failedClients); +} + +PVR_ERROR CPVRClients::GetProviders(const std::vector<std::shared_ptr<CPVRClient>>& clients, + CPVRProvidersContainer* providers, + std::vector<int>& failedClients) +{ + return ForClients(__FUNCTION__, clients, + [providers](const std::shared_ptr<CPVRClient>& client) { + return client->GetProviders(*providers); + }, + failedClients); +} + +PVR_ERROR CPVRClients::GetChannelGroups(const std::vector<std::shared_ptr<CPVRClient>>& clients, + CPVRChannelGroups* groups, + std::vector<int>& failedClients) +{ + return ForClients(__FUNCTION__, clients, + [groups](const std::shared_ptr<CPVRClient>& client) { + return client->GetChannelGroups(groups); + }, + failedClients); +} + +PVR_ERROR CPVRClients::GetChannelGroupMembers( + const std::vector<std::shared_ptr<CPVRClient>>& clients, + CPVRChannelGroup* group, + std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers, + std::vector<int>& failedClients) +{ + return ForClients(__FUNCTION__, clients, + [group, &groupMembers](const std::shared_ptr<CPVRClient>& client) { + return client->GetChannelGroupMembers(group, groupMembers); + }, + failedClients); +} + +std::vector<std::shared_ptr<CPVRClient>> CPVRClients::GetClientsSupportingChannelScan() const +{ + std::vector<std::shared_ptr<CPVRClient>> possibleScanClients; + + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& entry : m_clientMap) + { + const auto& client = entry.second; + if (client->ReadyToUse() && !client->IgnoreClient() && + client->GetClientCapabilities().SupportsChannelScan()) + possibleScanClients.emplace_back(client); + } + + return possibleScanClients; +} + +std::vector<std::shared_ptr<CPVRClient>> CPVRClients::GetClientsSupportingChannelSettings(bool bRadio) const +{ + std::vector<std::shared_ptr<CPVRClient>> possibleSettingsClients; + + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& entry : m_clientMap) + { + const auto& client = entry.second; + if (client->ReadyToUse() && !client->IgnoreClient()) + { + const CPVRClientCapabilities& caps = client->GetClientCapabilities(); + if (caps.SupportsChannelSettings() && + ((bRadio && caps.SupportsRadio()) || (!bRadio && caps.SupportsTV()))) + possibleSettingsClients.emplace_back(client); + } + } + + return possibleSettingsClients; +} + +bool CPVRClients::AnyClientSupportingRecordingsSize() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), [](const auto& entry) { + const auto& client = entry.second; + return client->ReadyToUse() && !client->IgnoreClient() && + client->GetClientCapabilities().SupportsRecordingsSize(); + }); +} + +bool CPVRClients::AnyClientSupportingEPG() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), [](const auto& entry) { + const auto& client = entry.second; + return client->ReadyToUse() && !client->IgnoreClient() && + client->GetClientCapabilities().SupportsEPG(); + }); +} + +bool CPVRClients::AnyClientSupportingRecordings() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), [](const auto& entry) { + const auto& client = entry.second; + return client->ReadyToUse() && !client->IgnoreClient() && + client->GetClientCapabilities().SupportsRecordings(); + }); +} + +bool CPVRClients::AnyClientSupportingRecordingsDelete() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), [](const auto& entry) { + const auto& client = entry.second; + return client->ReadyToUse() && !client->IgnoreClient() && + client->GetClientCapabilities().SupportsRecordingsDelete(); + }); +} + +void CPVRClients::OnSystemSleep() +{ + ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) { + client->OnSystemSleep(); + return PVR_ERROR_NO_ERROR; + }); +} + +void CPVRClients::OnSystemWake() +{ + ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) { + client->OnSystemWake(); + return PVR_ERROR_NO_ERROR; + }); +} + +void CPVRClients::OnPowerSavingActivated() +{ + ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) { + client->OnPowerSavingActivated(); + return PVR_ERROR_NO_ERROR; + }); +} + +void CPVRClients::OnPowerSavingDeactivated() +{ + ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) { + client->OnPowerSavingDeactivated(); + return PVR_ERROR_NO_ERROR; + }); +} + +void CPVRClients::ConnectionStateChange(CPVRClient* client, + const std::string& strConnectionString, + PVR_CONNECTION_STATE newState, + const std::string& strMessage) +{ + if (!client) + return; + + int iMsg = -1; + EventLevel eLevel = EventLevel::Error; + bool bNotify = true; + + switch (newState) + { + case PVR_CONNECTION_STATE_SERVER_UNREACHABLE: + iMsg = 35505; // Server is unreachable + if (client->GetPreviousConnectionState() == PVR_CONNECTION_STATE_UNKNOWN || + client->GetPreviousConnectionState() == PVR_CONNECTION_STATE_CONNECTING) + { + // Make our users happy. There were so many complaints about this notification because their TV backend + // was not up quick enough after Kodi start. So, ignore the very first 'server not reachable' notification. + bNotify = false; + } + break; + case PVR_CONNECTION_STATE_SERVER_MISMATCH: + iMsg = 35506; // Server does not respond properly + break; + case PVR_CONNECTION_STATE_VERSION_MISMATCH: + iMsg = 35507; // Server version is not compatible + break; + case PVR_CONNECTION_STATE_ACCESS_DENIED: + iMsg = 35508; // Access denied + break; + case PVR_CONNECTION_STATE_CONNECTED: + eLevel = EventLevel::Basic; + iMsg = 36034; // Connection established + if (client->GetPreviousConnectionState() == PVR_CONNECTION_STATE_UNKNOWN || + client->GetPreviousConnectionState() == PVR_CONNECTION_STATE_CONNECTING) + bNotify = false; + break; + case PVR_CONNECTION_STATE_DISCONNECTED: + iMsg = 36030; // Connection lost + break; + case PVR_CONNECTION_STATE_CONNECTING: + eLevel = EventLevel::Information; + iMsg = 35509; // Connecting + bNotify = false; + break; + default: + CLog::LogF(LOGERROR, "Unknown connection state"); + return; + } + + // Use addon-supplied message, if present + std::string strMsg; + if (!strMessage.empty()) + strMsg = strMessage; + else + strMsg = g_localizeStrings.Get(iMsg); + + if (!strConnectionString.empty()) + strMsg = StringUtils::Format("{} ({})", strMsg, strConnectionString); + + // Notify user. + CServiceBroker::GetJobManager()->AddJob( + new CPVREventLogJob(bNotify, eLevel, client->GetFriendlyName(), strMsg, client->Icon()), + nullptr); +} + +namespace +{ + +void LogClientWarning(const char* strFunctionName, const std::shared_ptr<CPVRClient>& client) +{ + if (client->IgnoreClient()) + CLog::Log(LOGWARNING, "{}: Not calling add-on '{}'. Add-on not (yet) connected.", + strFunctionName, client->ID()); + else if (!client->ReadyToUse()) + CLog::Log(LOGWARNING, "{}: Not calling add-on '{}'. Add-on not ready to use.", strFunctionName, + client->ID()); + else + CLog::Log(LOGERROR, "{}: Not calling add-on '{}' for unexpected reason.", strFunctionName, + client->ID()); +} + +} // unnamed namespace + +PVR_ERROR CPVRClients::ForCreatedClients(const char* strFunctionName, + const PVRClientFunction& function) const +{ + std::vector<int> failedClients; + return ForCreatedClients(strFunctionName, function, failedClients); +} + +PVR_ERROR CPVRClients::ForCreatedClients(const char* strFunctionName, + const PVRClientFunction& function, + std::vector<int>& failedClients) const +{ + PVR_ERROR lastError = PVR_ERROR_NO_ERROR; + + CPVRClientMap clients; + GetCallableClients(clients, failedClients); + + if (!failedClients.empty()) + { + std::shared_ptr<CPVRClient> client; + for (int id : failedClients) + { + client = GetClient(id); + if (client) + LogClientWarning(strFunctionName, client); + } + } + + for (const auto& clientEntry : clients) + { + PVR_ERROR currentError = function(clientEntry.second); + + if (currentError != PVR_ERROR_NO_ERROR && currentError != PVR_ERROR_NOT_IMPLEMENTED) + { + lastError = currentError; + failedClients.emplace_back(clientEntry.first); + } + } + return lastError; +} + +PVR_ERROR CPVRClients::ForClients(const char* strFunctionName, + const std::vector<std::shared_ptr<CPVRClient>>& clients, + const PVRClientFunction& function, + std::vector<int>& failedClients) const +{ + if (clients.empty()) + return ForCreatedClients(strFunctionName, function, failedClients); + + PVR_ERROR lastError = PVR_ERROR_NO_ERROR; + + failedClients.clear(); + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& entry : m_clientMap) + { + if (entry.second->ReadyToUse() && !entry.second->IgnoreClient() && + std::any_of(clients.cbegin(), clients.cend(), + [&entry](const auto& client) { return client->GetID() == entry.first; })) + { + // Allow ready to use clients that shall be called + continue; + } + + failedClients.emplace_back(entry.first); + } + } + + for (const auto& client : clients) + { + if (std::none_of(failedClients.cbegin(), failedClients.cend(), + [&client](int failedClientId) { return failedClientId == client->GetID(); })) + { + PVR_ERROR currentError = function(client); + + if (currentError != PVR_ERROR_NO_ERROR && currentError != PVR_ERROR_NOT_IMPLEMENTED) + { + lastError = currentError; + failedClients.emplace_back(client->GetID()); + } + } + else + { + LogClientWarning(strFunctionName, client); + } + } + return lastError; +} diff --git a/xbmc/pvr/addons/PVRClients.h b/xbmc/pvr/addons/PVRClients.h new file mode 100644 index 0000000..32248fa --- /dev/null +++ b/xbmc/pvr/addons/PVRClients.h @@ -0,0 +1,485 @@ +/* + * 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/IAddonManagerCallback.h" +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_general.h" +#include "threads/CriticalSection.h" + +#include <functional> +#include <map> +#include <memory> +#include <string> +#include <vector> + +class CVariant; + +namespace ADDON +{ + struct AddonEvent; + class CAddonInfo; +} + +namespace PVR +{ +class CPVRChannel; +class CPVRChannelGroupInternal; +class CPVRChannelGroup; +class CPVRChannelGroupMember; +class CPVRChannelGroups; +class CPVRProvidersContainer; +class CPVRClient; +class CPVREpg; +class CPVRRecordings; +class CPVRTimerType; +class CPVRTimersContainer; + +typedef std::map<int, std::shared_ptr<CPVRClient>> CPVRClientMap; + +/** + * Holds generic data about a backend (number of channels etc.) + */ +struct SBackend +{ + std::string name; + std::string version; + std::string host; + int numTimers = 0; + int numRecordings = 0; + int numDeletedRecordings = 0; + int numProviders = 0; + int numChannelGroups = 0; + int numChannels = 0; + uint64_t diskUsed = 0; + uint64_t diskTotal = 0; +}; + + class CPVRClients : public ADDON::IAddonMgrCallback + { + public: + CPVRClients(); + ~CPVRClients() override; + + /*! + * @brief Start all clients. + */ + void Start(); + + /*! + * @brief Stop all clients. + */ + void Stop(); + + /*! + * @brief Continue all clients. + */ + void Continue(); + + /*! + * @brief Update all clients, sync with Addon Manager state (start, restart, shutdown clients). + * @param changedAddonId The id of the changed addon, empty string denotes 'any addon'. + * @param changedInstanceId The Identifier of the changed add-on instance + */ + void UpdateClients( + const std::string& changedAddonId = "", + ADDON::AddonInstanceId changedInstanceId = ADDON::ADDON_SINGLETON_INSTANCE_ID); + + /*! + * @brief Restart a single client add-on. + * @param addonId The add-on to restart. + * @param instanceId Instance identifier to use + * @param bDataChanged True if the client's data changed, false otherwise (unused). + * @return True if the client was found and restarted, false otherwise. + */ + bool RequestRestart(const std::string& addonId, + ADDON::AddonInstanceId instanceId, + bool bDataChanged) override; + + /*! + * @brief Stop a client. + * @param clientId The id of the client to stop. + * @param restart If true, restart the client. + * @return True if the client was found, false otherwise. + */ + bool StopClient(int clientId, bool restart); + + /*! + * @brief Handle addon events (enable, disable, ...). + * @param event The addon event. + */ + void OnAddonEvent(const ADDON::AddonEvent& event); + + /*! + * @brief Get the number of created clients. + * @return The amount of created clients. + */ + int CreatedClientAmount() const; + + /*! + * @brief Check whether there are any created clients. + * @return True if at least one client is created. + */ + bool HasCreatedClients() const; + + /*! + * @brief Check whether a given client ID points to a created client. + * @param iClientId The client ID. + * @return True if the the client ID represents a created client, false otherwise. + */ + bool IsCreatedClient(int iClientId) const; + + /*! + * @brief Get the the client for the given client id, if it is created. + * @param clientId The ID of the client to get. + * @return The client if found, nullptr otherwise. + */ + std::shared_ptr<CPVRClient> GetCreatedClient(int clientId) const; + + /*! + * @brief Get all created clients. + * @return All created clients. + */ + CPVRClientMap GetCreatedClients() const; + + /*! + * @brief Get the ID of the first created client. + * @return the ID or -1 if no clients are created; + */ + int GetFirstCreatedClientID(); + + /*! + * @brief Check whether there are any created, but not (yet) connected clients. + * @return True if at least one client is ignored. + */ + bool HasIgnoredClients() const; + + /*! + * @brief Get the number of enabled clients. + * @return The amount of enabled clients. + */ + int EnabledClientAmount() const; + + /*! + * @brief Check whether a given client ID points to an enabled client. + * @param clientId The client ID. + * @return True if the the client ID represents an enabled client, false otherwise. + */ + bool IsEnabledClient(int clientId) const; + + /*! + * @brief Get a list of the enabled client infos. + * @return A list of enabled client infos. + */ + std::vector<CVariant> GetEnabledClientInfos() const; + + /*! + * @brief Get info required for providers. Include both enabled and disabled PVR add-ons + * @return A list containing the information required to create client providers. + */ + std::vector<CVariant> GetClientProviderInfos() const; + + //@} + + /*! @name general methods */ + //@{ + + /*! + * @brief Returns properties about all created clients + * @return the properties + */ + std::vector<SBackend> GetBackendProperties() const; + + //@} + + /*! @name Timer methods */ + //@{ + + /*! + * @brief Get all timers from the given clients + * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients. + * @param timers Store the timers in this container. + * @param failedClients in case of errors will contain the ids of the clients for which the timers could not be obtained. + * @return true on success for all clients, false in case of error for at least one client. + */ + bool GetTimers(const std::vector<std::shared_ptr<CPVRClient>>& clients, + CPVRTimersContainer* timers, + std::vector<int>& failedClients); + + /*! + * @brief Get all supported timer types. + * @param results The container to store the result in. + * @return PVR_ERROR_NO_ERROR if the operation succeeded, the respective PVR_ERROR value otherwise. + */ + PVR_ERROR GetTimerTypes(std::vector<std::shared_ptr<CPVRTimerType>>& results) const; + + //@} + + /*! @name Recording methods */ + //@{ + + /*! + * @brief Get all recordings from the given clients + * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients. + * @param recordings Store the recordings in this container. + * @param deleted If true, return deleted recordings, return not deleted recordings otherwise. + * @param failedClients in case of errors will contain the ids of the clients for which the recordings could not be obtained. + * @return PVR_ERROR_NO_ERROR if the operation succeeded, the respective PVR_ERROR value otherwise. + */ + PVR_ERROR GetRecordings(const std::vector<std::shared_ptr<CPVRClient>>& clients, + CPVRRecordings* recordings, + bool deleted, + std::vector<int>& failedClients); + + /*! + * @brief Delete all "soft" deleted recordings permanently on the backend. + * @return PVR_ERROR_NO_ERROR if the operation succeeded, the respective PVR_ERROR value otherwise. + */ + PVR_ERROR DeleteAllRecordingsFromTrash(); + + //@} + + /*! @name EPG methods */ + //@{ + + /*! + * @brief Tell all clients the past time frame to use when notifying epg events back to Kodi. + * + * The clients might push epg events asynchronously to Kodi using the callback function + * EpgEventStateChange. To be able to only push events that are actually of interest for Kodi, + * clients need to know about the future epg time frame Kodi uses. + * + * @param[in] iPastDays number of days before "now". + * @ref EPG_TIMEFRAME_UNLIMITED means that Kodi is interested in all + * epg events, regardless of event times. + * @return @ref PVR_ERROR_NO_ERROR if the operation succeeded, the respective @ref PVR_ERROR + * value otherwise. + */ + PVR_ERROR SetEPGMaxPastDays(int iPastDays); + + /*! + * @brief Tell all clients the future time frame to use when notifying epg events back to Kodi. + * + * The clients might push epg events asynchronously to Kodi using the callback function + * EpgEventStateChange. To be able to only push events that are actually of interest for Kodi, + * clients need to know about the future epg time frame Kodi uses. + * + * @param[in] iFutureDays number of days from "now". + * @ref EPG_TIMEFRAME_UNLIMITED means that Kodi is interested in all + * epg events, regardless of event times. + * @return @ref PVR_ERROR_NO_ERROR if the operation succeeded, the respective @ref PVR_ERROR + * value otherwise. + */ + PVR_ERROR SetEPGMaxFutureDays(int iFutureDays); + + //@} + + /*! @name Channel methods */ + //@{ + + /*! + * @brief Get all channels from the given clients. + * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients. + * @param bRadio Whether to fetch radio or TV channels. + * @param channels The container to store the channels. + * @param failedClients in case of errors will contain the ids of the clients for which the channels could not be obtained. + * @return PVR_ERROR_NO_ERROR if the channels were fetched successfully, last error otherwise. + */ + PVR_ERROR GetChannels(const std::vector<std::shared_ptr<CPVRClient>>& clients, + bool bRadio, + std::vector<std::shared_ptr<CPVRChannel>>& channels, + std::vector<int>& failedClients); + + /*! + * @brief Get all providers from backends. + * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients. + * @param group The container to store the providers in. + * @param failedClients in case of errors will contain the ids of the clients for which the providers could not be obtained. + * @return PVR_ERROR_NO_ERROR if the providers were fetched successfully, last error otherwise. + */ + PVR_ERROR GetProviders(const std::vector<std::shared_ptr<CPVRClient>>& clients, + CPVRProvidersContainer* providers, + std::vector<int>& failedClients); + + /*! + * @brief Get all channel groups from the given clients. + * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients. + * @param groups Store the channel groups in this container. + * @param failedClients in case of errors will contain the ids of the clients for which the channel groups could not be obtained. + * @return PVR_ERROR_NO_ERROR if the channel groups were fetched successfully, last error otherwise. + */ + PVR_ERROR GetChannelGroups(const std::vector<std::shared_ptr<CPVRClient>>& clients, + CPVRChannelGroups* groups, + std::vector<int>& failedClients); + + /*! + * @brief Get all group members of a channel group from the given clients. + * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients. + * @param group The group to get the member for. + * @param groupMembers The container for the group members. + * @param failedClients in case of errors will contain the ids of the clients for which the channel group members could not be obtained. + * @return PVR_ERROR_NO_ERROR if the channel group members were fetched successfully, last error otherwise. + */ + PVR_ERROR GetChannelGroupMembers( + const std::vector<std::shared_ptr<CPVRClient>>& clients, + CPVRChannelGroup* group, + std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers, + std::vector<int>& failedClients); + + /*! + * @brief Get a list of clients providing a channel scan dialog. + * @return All clients supporting channel scan. + */ + std::vector<std::shared_ptr<CPVRClient>> GetClientsSupportingChannelScan() const; + + /*! + * @brief Get a list of clients providing a channel settings dialog. + * @return All clients supporting channel settings. + */ + std::vector<std::shared_ptr<CPVRClient>> GetClientsSupportingChannelSettings(bool bRadio) const; + + /*! + * @brief Get whether or not any client supports recording size. + * @return True if any client supports recording size. + */ + bool AnyClientSupportingRecordingsSize() const; + + /*! + * @brief Get whether or not any client supports EPG. + * @return True if any client supports EPG. + */ + bool AnyClientSupportingEPG() const; + + /*! + * @brief Get whether or not any client supports recordings. + * @return True if any client supports recordings. + */ + bool AnyClientSupportingRecordings() const; + //@} + + /*! + * @brief Get whether or not any client supports recordings delete. + * @return True if any client supports recordings delete. + */ + bool AnyClientSupportingRecordingsDelete() const; + //@} + + /*! @name Power management methods */ + //@{ + + /*! + * @brief Propagate "system sleep" event to clients + */ + void OnSystemSleep(); + + /*! + * @brief Propagate "system wakeup" event to clients + */ + void OnSystemWake(); + + /*! + * @brief Propagate "power saving activated" event to clients + */ + void OnPowerSavingActivated(); + + /*! + * @brief Propagate "power saving deactivated" event to clients + */ + void OnPowerSavingDeactivated(); + + //@} + + /*! + * @brief Notify a change of an addon connection state. + * @param client The changed client. + * @param strConnectionString A human-readable string providing additional information. + * @param newState The new connection state. + * @param strMessage A human readable string replacing default state message. + */ + void ConnectionStateChange(CPVRClient* client, + const std::string& strConnectionString, + PVR_CONNECTION_STATE newState, + const std::string& strMessage); + + private: + /*! + * @brief Get the known instance ids for a given addon id. + * @param addonID The addon id. + * @return The list of known instance ids. + */ + std::vector<ADDON::AddonInstanceId> GetKnownInstanceIds(const std::string& addonID) const; + + bool GetAddonsWithStatus( + const std::string& changedAddonId, + std::vector<std::pair<std::shared_ptr<ADDON::CAddonInfo>, bool>>& addonsWithStatus) const; + + std::vector<std::pair<ADDON::AddonInstanceId, bool>> GetInstanceIdsWithStatus( + const std::shared_ptr<ADDON::CAddonInfo>& addon, bool addonIsEnabled) const; + + /*! + * @brief Get the client instance for a given client id. + * @param clientId The id of the client to get. + * @return The client if found, nullptr otherwise. + */ + std::shared_ptr<CPVRClient> GetClient(int clientId) const; + + /*! + * @brief Check whether a client is known. + * @param iClientId The id of the client to check. + * @return True if this client is known, false otherwise. + */ + bool IsKnownClient(int iClientId) const; + + /*! + * @brief Get all created clients and clients not (yet) ready to use. + * @param clientsReady Store the created clients in this map. + * @param clientsNotReady Store the the ids of the not (yet) ready clients in this list. + * @return PVR_ERROR_NO_ERROR in case all clients are ready, PVR_ERROR_SERVER_ERROR otherwise. + */ + PVR_ERROR GetCallableClients(CPVRClientMap& clientsReady, + std::vector<int>& clientsNotReady) const; + + typedef std::function<PVR_ERROR(const std::shared_ptr<CPVRClient>&)> PVRClientFunction; + + /*! + * @brief Wraps calls to the given clients in order to do common pre and post function invocation actions. + * @param strFunctionName The function name, for logging purposes. + * @param clients The clients to wrap. + * @param function The function to wrap. It has to have return type PVR_ERROR and must take a const reference to a std::shared_ptr<CPVRClient> as parameter. + * @param failedClients Contains a list of the ids of clients for that the call failed, if any. + * @return PVR_ERROR_NO_ERROR on success, any other PVR_ERROR_* value otherwise. + */ + PVR_ERROR ForClients(const char* strFunctionName, + const std::vector<std::shared_ptr<CPVRClient>>& clients, + const PVRClientFunction& function, + std::vector<int>& failedClients) const; + + /*! + * @brief Wraps calls to all created clients in order to do common pre and post function invocation actions. + * @param strFunctionName The function name, for logging purposes. + * @param function The function to wrap. It has to have return type PVR_ERROR and must take a const reference to a std::shared_ptr<CPVRClient> as parameter. + * @return PVR_ERROR_NO_ERROR on success, any other PVR_ERROR_* value otherwise. + */ + PVR_ERROR ForCreatedClients(const char* strFunctionName, + const PVRClientFunction& function) const; + + /*! + * @brief Wraps calls to all created clients in order to do common pre and post function invocation actions. + * @param strFunctionName The function name, for logging purposes. + * @param function The function to wrap. It has to have return type PVR_ERROR and must take a const reference to a std::shared_ptr<CPVRClient> as parameter. + * @param failedClients Contains a list of the ids of clients for that the call failed, if any. + * @return PVR_ERROR_NO_ERROR on success, any other PVR_ERROR_* value otherwise. + */ + PVR_ERROR ForCreatedClients(const char* strFunctionName, + const PVRClientFunction& function, + std::vector<int>& failedClients) const; + + mutable CCriticalSection m_critSection; + CPVRClientMap m_clientMap; + }; +} diff --git a/xbmc/pvr/channels/CMakeLists.txt b/xbmc/pvr/channels/CMakeLists.txt new file mode 100644 index 0000000..714f232 --- /dev/null +++ b/xbmc/pvr/channels/CMakeLists.txt @@ -0,0 +1,23 @@ +set(SOURCES PVRChannel.cpp + PVRChannelGroup.cpp + PVRChannelGroupInternal.cpp + PVRChannelGroupMember.cpp + PVRChannelGroupSettings.cpp + PVRChannelGroups.cpp + PVRChannelGroupsContainer.cpp + PVRChannelNumber.cpp + PVRRadioRDSInfoTag.cpp + PVRChannelsPath.cpp) + +set(HEADERS PVRChannel.h + PVRChannelGroup.h + PVRChannelGroupInternal.h + PVRChannelGroupMember.h + PVRChannelGroupSettings.h + PVRChannelGroups.h + PVRChannelGroupsContainer.h + PVRChannelNumber.h + PVRRadioRDSInfoTag.h + PVRChannelsPath.h) + +core_add_library(pvr_channels) diff --git a/xbmc/pvr/channels/PVRChannel.cpp b/xbmc/pvr/channels/PVRChannel.cpp new file mode 100644 index 0000000..135719f --- /dev/null +++ b/xbmc/pvr/channels/PVRChannel.cpp @@ -0,0 +1,843 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRChannel.h" + +#include "ServiceBroker.h" +#include "XBDateTime.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/epg/Epg.h" +#include "pvr/epg/EpgChannelData.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/providers/PVRProviders.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <memory> +#include <mutex> +#include <string> + +using namespace PVR; + +const std::string CPVRChannel::IMAGE_OWNER_PATTERN = "pvrchannel_{}"; + +bool CPVRChannel::operator==(const CPVRChannel& right) const +{ + return (m_bIsRadio == right.m_bIsRadio && m_iUniqueId == right.m_iUniqueId && + m_iClientId == right.m_iClientId); +} + +bool CPVRChannel::operator!=(const CPVRChannel& right) const +{ + return !(*this == right); +} + +CPVRChannel::CPVRChannel(bool bRadio) + : m_bIsRadio(bRadio), + m_iconPath("", StringUtils::Format(IMAGE_OWNER_PATTERN, bRadio ? "radio" : "tv")) +{ + UpdateEncryptionName(); +} + +CPVRChannel::CPVRChannel(bool bRadio, const std::string& iconPath) + : m_bIsRadio(bRadio), + m_iconPath(iconPath, StringUtils::Format(IMAGE_OWNER_PATTERN, bRadio ? "radio" : "tv")) +{ + UpdateEncryptionName(); +} + +CPVRChannel::CPVRChannel(const PVR_CHANNEL& channel, unsigned int iClientId) + : m_bIsRadio(channel.bIsRadio), + m_bIsHidden(channel.bIsHidden), + m_iconPath(channel.strIconPath, + StringUtils::Format(IMAGE_OWNER_PATTERN, channel.bIsRadio ? "radio" : "tv")), + m_strChannelName(channel.strChannelName), + m_bHasArchive(channel.bHasArchive), + m_bEPGEnabled(!channel.bIsHidden), + m_iUniqueId(channel.iUniqueId), + m_iClientId(iClientId), + m_clientChannelNumber(channel.iChannelNumber, channel.iSubChannelNumber), + m_strClientChannelName(channel.strChannelName), + m_strMimeType(channel.strMimeType), + m_iClientEncryptionSystem(channel.iEncryptionSystem), + m_iClientOrder(channel.iOrder), + m_iClientProviderUid(channel.iClientProviderUid) +{ + if (m_strChannelName.empty()) + m_strChannelName = StringUtils::Format("{} {}", g_localizeStrings.Get(19029), m_iUniqueId); + + UpdateEncryptionName(); +} + +CPVRChannel::~CPVRChannel() +{ + ResetEPG(); +} + +void CPVRChannel::FillAddonData(PVR_CHANNEL& channel) const +{ + channel = {}; + channel.iUniqueId = UniqueID(); + channel.iChannelNumber = ClientChannelNumber().GetChannelNumber(); + channel.iSubChannelNumber = ClientChannelNumber().GetSubChannelNumber(); + strncpy(channel.strChannelName, ClientChannelName().c_str(), sizeof(channel.strChannelName) - 1); + strncpy(channel.strIconPath, ClientIconPath().c_str(), sizeof(channel.strIconPath) - 1); + channel.iEncryptionSystem = EncryptionSystem(); + channel.bIsRadio = IsRadio(); + channel.bIsHidden = IsHidden(); + strncpy(channel.strMimeType, MimeType().c_str(), sizeof(channel.strMimeType) - 1); + channel.iClientProviderUid = ClientProviderUid(); + channel.bHasArchive = HasArchive(); +} + +void CPVRChannel::Serialize(CVariant& value) const +{ + value["channelid"] = m_iChannelId; + value["channeltype"] = m_bIsRadio ? "radio" : "tv"; + value["hidden"] = m_bIsHidden; + value["locked"] = m_bIsLocked; + value["icon"] = ClientIconPath(); + value["channel"] = m_strChannelName; + value["uniqueid"] = m_iUniqueId; + CDateTime lastPlayed(m_iLastWatched); + value["lastplayed"] = lastPlayed.IsValid() ? lastPlayed.GetAsDBDate() : ""; + + std::shared_ptr<CPVREpgInfoTag> epg = GetEPGNow(); + if (epg) + { + // add the properties of the current EPG item to the main object + epg->Serialize(value); + // and add an extra sub-object with only the current EPG details + epg->Serialize(value["broadcastnow"]); + } + + epg = GetEPGNext(); + if (epg) + epg->Serialize(value["broadcastnext"]); + + value["hasarchive"] = m_bHasArchive; + value["clientid"] = m_iClientId; +} + +bool CPVRChannel::QueueDelete() +{ + bool bReturn = false; + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (!database) + return bReturn; + + const std::shared_ptr<CPVREpg> epg = GetEPG(); + if (epg) + ResetEPG(); + + bReturn = database->QueueDeleteQuery(*this); + return bReturn; +} + +std::shared_ptr<CPVREpg> CPVRChannel::GetEPG() const +{ + const_cast<CPVRChannel*>(this)->CreateEPG(); + + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_bIsHidden && m_bEPGEnabled) + return m_epg; + + return {}; +} + +bool CPVRChannel::CreateEPG() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_epg) + { + m_epg = CServiceBroker::GetPVRManager().EpgContainer().CreateChannelEpg( + m_iEpgId, m_strEPGScraper, std::make_shared<CPVREpgChannelData>(*this)); + if (m_epg) + { + if (m_epg->EpgID() != m_iEpgId) + { + m_iEpgId = m_epg->EpgID(); + m_bChanged = true; + } + + // Subscribe for EPG delete event + m_epg->Events().Subscribe(this, &CPVRChannel::Notify); + return true; + } + } + return false; +} + +void CPVRChannel::Notify(const PVREvent& event) +{ + if (event == PVREvent::EpgDeleted) + { + ResetEPG(); + } +} + +void CPVRChannel::ResetEPG() +{ + std::shared_ptr<CPVREpg> epgToUnsubscribe; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_epg) + { + epgToUnsubscribe = m_epg; + m_epg.reset(); + } + } + + if (epgToUnsubscribe) + epgToUnsubscribe->Events().Unsubscribe(this); +} + +bool CPVRChannel::UpdateFromClient(const std::shared_ptr<CPVRChannel>& channel) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + SetClientID(channel->ClientID()); + SetArchive(channel->HasArchive()); + SetClientProviderUid(channel->ClientProviderUid()); + + m_clientChannelNumber = channel->m_clientChannelNumber; + m_strMimeType = channel->MimeType(); + m_iClientEncryptionSystem = channel->EncryptionSystem(); + m_strClientChannelName = channel->ClientChannelName(); + + UpdateEncryptionName(); + + // only update the channel name, icon, and hidden flag if the user hasn't changed them manually + if (m_strChannelName.empty() || !IsUserSetName()) + SetChannelName(channel->ClientChannelName()); + if (IconPath().empty() || !IsUserSetIcon()) + SetIconPath(channel->ClientIconPath()); + if (!IsUserSetHidden()) + SetHidden(channel->IsHidden()); + + return m_bChanged; +} + +bool CPVRChannel::Persist() +{ + { + // not changed + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_bChanged && m_iChannelId > 0) + return true; + } + + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (database) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Persisting channel '{}'", m_strChannelName); + + bool bReturn = database->Persist(*this, true); + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bChanged = !bReturn; + return bReturn; + } + + return false; +} + +bool CPVRChannel::SetChannelID(int iChannelId) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_iChannelId != iChannelId) + { + m_iChannelId = iChannelId; + + const std::shared_ptr<CPVREpg> epg = GetEPG(); + if (epg) + epg->GetChannelData()->SetChannelId(m_iChannelId); + + m_bChanged = true; + return true; + } + + return false; +} + +bool CPVRChannel::SetHidden(bool bIsHidden, bool bIsUserSetHidden /*= false*/) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_bIsHidden != bIsHidden) + { + m_bIsHidden = bIsHidden; + m_bIsUserSetHidden = bIsUserSetHidden; + + if (m_epg) + m_epg->GetChannelData()->SetHidden(m_bIsHidden); + + m_bChanged = true; + return true; + } + + return false; +} + +bool CPVRChannel::SetLocked(bool bIsLocked) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_bIsLocked != bIsLocked) + { + m_bIsLocked = bIsLocked; + + const std::shared_ptr<CPVREpg> epg = GetEPG(); + if (epg) + epg->GetChannelData()->SetLocked(m_bIsLocked); + + m_bChanged = true; + return true; + } + + return false; +} + +std::shared_ptr<CPVRRadioRDSInfoTag> CPVRChannel::GetRadioRDSInfoTag() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_rdsTag; +} + +void CPVRChannel::SetRadioRDSInfoTag(const std::shared_ptr<CPVRRadioRDSInfoTag>& tag) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_rdsTag = tag; +} + +bool CPVRChannel::HasArchive() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bHasArchive; +} + +bool CPVRChannel::SetArchive(bool bHasArchive) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_bHasArchive != bHasArchive) + { + m_bHasArchive = bHasArchive; + m_bChanged = true; + return true; + } + + return false; +} + +bool CPVRChannel::SetIconPath(const std::string& strIconPath, bool bIsUserSetIcon /* = false */) +{ + if (StringUtils::StartsWith(strIconPath, "image://")) + { + CLog::LogF(LOGERROR, "Not allowed to call this method with an image URL"); + return false; + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + if (ClientIconPath() == strIconPath) + return false; + + m_iconPath.SetClientImage(strIconPath); + + const std::shared_ptr<CPVREpg> epg = GetEPG(); + if (epg) + epg->GetChannelData()->SetChannelIconPath(strIconPath); + + m_bChanged = true; + m_bIsUserSetIcon = bIsUserSetIcon && !IconPath().empty(); + return true; +} + +bool CPVRChannel::SetChannelName(const std::string& strChannelName, bool bIsUserSetName /*= false*/) +{ + std::string strName(strChannelName); + + if (strName.empty()) + strName = StringUtils::Format(g_localizeStrings.Get(19085), + m_clientChannelNumber.FormattedChannelNumber()); + + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_strChannelName != strName) + { + m_strChannelName = strName; + m_bIsUserSetName = bIsUserSetName; + + /* if the user changes the name manually to an empty string we reset the + flag and use the name from the client instead */ + if (bIsUserSetName && strChannelName.empty()) + { + m_bIsUserSetName = false; + m_strChannelName = ClientChannelName(); + } + + const std::shared_ptr<CPVREpg> epg = GetEPG(); + if (epg) + epg->GetChannelData()->SetChannelName(m_strChannelName); + + m_bChanged = true; + return true; + } + + return false; +} + +bool CPVRChannel::SetLastWatched(time_t iLastWatched) +{ + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_iLastWatched = iLastWatched; + } + + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (database) + return database->UpdateLastWatched(*this); + + return false; +} + +/********** Client related channel methods **********/ + +bool CPVRChannel::SetClientID(int iClientId) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iClientId != iClientId) + { + m_iClientId = iClientId; + m_bChanged = true; + return true; + } + + return false; +} + +std::string CPVRChannel::GetEncryptionName(int iCaid) +{ + // http://www.dvb.org/index.php?id=174 + // http://en.wikipedia.org/wiki/Conditional_access_system + std::string strName(g_localizeStrings.Get(13205)); /* Unknown */ + + if (iCaid == 0x0000) + strName = g_localizeStrings.Get(19013); /* Free To Air */ + else if (iCaid >= 0x0001 && iCaid <= 0x009F) + strName = g_localizeStrings.Get(19014); /* Fixed */ + else if (iCaid >= 0x00A0 && iCaid <= 0x00A1) + strName = g_localizeStrings.Get(338); /* Analog */ + else if (iCaid >= 0x00A2 && iCaid <= 0x00FF) + strName = g_localizeStrings.Get(19014); /* Fixed */ + else if (iCaid >= 0x0100 && iCaid <= 0x01FF) + strName = "SECA Mediaguard"; + else if (iCaid == 0x0464) + strName = "EuroDec"; + else if (iCaid >= 0x0500 && iCaid <= 0x05FF) + strName = "Viaccess"; + else if (iCaid >= 0x0600 && iCaid <= 0x06FF) + strName = "Irdeto"; + else if (iCaid >= 0x0900 && iCaid <= 0x09FF) + strName = "NDS Videoguard"; + else if (iCaid >= 0x0B00 && iCaid <= 0x0BFF) + strName = "Conax"; + else if (iCaid >= 0x0D00 && iCaid <= 0x0DFF) + strName = "CryptoWorks"; + else if (iCaid >= 0x0E00 && iCaid <= 0x0EFF) + strName = "PowerVu"; + else if (iCaid == 0x1000) + strName = "RAS"; + else if (iCaid >= 0x1200 && iCaid <= 0x12FF) + strName = "NagraVision"; + else if (iCaid >= 0x1700 && iCaid <= 0x17FF) + strName = "BetaCrypt"; + else if (iCaid >= 0x1800 && iCaid <= 0x18FF) + strName = "NagraVision"; + else if (iCaid == 0x22F0) + strName = "Codicrypt"; + else if (iCaid == 0x2600) + strName = "BISS"; + else if (iCaid == 0x4347) + strName = "CryptOn"; + else if (iCaid == 0x4800) + strName = "Accessgate"; + else if (iCaid == 0x4900) + strName = "China Crypt"; + else if (iCaid == 0x4A10) + strName = "EasyCas"; + else if (iCaid == 0x4A20) + strName = "AlphaCrypt"; + else if (iCaid == 0x4A70) + strName = "DreamCrypt"; + else if (iCaid == 0x4A60) + strName = "SkyCrypt"; + else if (iCaid == 0x4A61) + strName = "Neotioncrypt"; + else if (iCaid == 0x4A62) + strName = "SkyCrypt"; + else if (iCaid == 0x4A63) + strName = "Neotion SHL"; + else if (iCaid >= 0x4A64 && iCaid <= 0x4A6F) + strName = "SkyCrypt"; + else if (iCaid == 0x4A80) + strName = "ThalesCrypt"; + else if (iCaid == 0x4AA1) + strName = "KeyFly"; + else if (iCaid == 0x4ABF) + strName = "DG-Crypt"; + else if (iCaid >= 0x4AD0 && iCaid <= 0x4AD1) + strName = "X-Crypt"; + else if (iCaid == 0x4AD4) + strName = "OmniCrypt"; + else if (iCaid == 0x4AE0) + strName = "RossCrypt"; + else if (iCaid == 0x5500) + strName = "Z-Crypt"; + else if (iCaid == 0x5501) + strName = "Griffin"; + else if (iCaid == 0x5601) + strName = "Verimatrix"; + + if (iCaid >= 0) + strName += StringUtils::Format(" ({:04X})", iCaid); + + return strName; +} + +void CPVRChannel::UpdateEncryptionName() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strClientEncryptionName = GetEncryptionName(m_iClientEncryptionSystem); +} + +bool CPVRChannel::SetClientProviderUid(int iClientProviderUid) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iClientProviderUid != iClientProviderUid) + { + m_iClientProviderUid = iClientProviderUid; + m_bChanged = true; + return true; + } + + return false; +} + +/********** EPG methods **********/ + +std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVRChannel::GetEpgTags() const +{ + const std::shared_ptr<CPVREpg> epg = GetEPG(); + if (!epg) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Cannot get EPG for channel '{}'", m_strChannelName); + return {}; + } + + return epg->GetTags(); +} + +std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVRChannel::GetEPGTimeline( + const CDateTime& timelineStart, + const CDateTime& timelineEnd, + const CDateTime& minEventEnd, + const CDateTime& maxEventStart) const +{ + const std::shared_ptr<CPVREpg> epg = GetEPG(); + if (epg) + { + return epg->GetTimeline(timelineStart, timelineEnd, minEventEnd, maxEventStart); + } + else + { + // return single gap tag spanning whole timeline + return std::vector<std::shared_ptr<CPVREpgInfoTag>>{ + CreateEPGGapTag(timelineStart, timelineEnd)}; + } +} + +std::shared_ptr<CPVREpgInfoTag> CPVRChannel::CreateEPGGapTag(const CDateTime& start, + const CDateTime& end) const +{ + const std::shared_ptr<CPVREpg> epg = GetEPG(); + if (epg) + return std::make_shared<CPVREpgInfoTag>(epg->GetChannelData(), epg->EpgID(), start, end, true); + else + return std::make_shared<CPVREpgInfoTag>(std::make_shared<CPVREpgChannelData>(*this), -1, start, + end, true); +} + +std::shared_ptr<CPVREpgInfoTag> CPVRChannel::GetEPGNow() const +{ + std::shared_ptr<CPVREpgInfoTag> tag; + const std::shared_ptr<CPVREpg> epg = GetEPG(); + if (epg) + tag = epg->GetTagNow(); + + return tag; +} + +std::shared_ptr<CPVREpgInfoTag> CPVRChannel::GetEPGNext() const +{ + std::shared_ptr<CPVREpgInfoTag> tag; + const std::shared_ptr<CPVREpg> epg = GetEPG(); + if (epg) + tag = epg->GetTagNext(); + + return tag; +} + +std::shared_ptr<CPVREpgInfoTag> CPVRChannel::GetEPGPrevious() const +{ + std::shared_ptr<CPVREpgInfoTag> tag; + const std::shared_ptr<CPVREpg> epg = GetEPG(); + if (epg) + tag = epg->GetTagPrevious(); + + return tag; +} + +bool CPVRChannel::SetEPGEnabled(bool bEPGEnabled) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_bEPGEnabled != bEPGEnabled) + { + m_bEPGEnabled = bEPGEnabled; + + if (m_epg) + { + m_epg->GetChannelData()->SetEPGEnabled(m_bEPGEnabled); + + if (m_bEPGEnabled) + m_epg->ForceUpdate(); + else + m_epg->Clear(); + } + + m_bChanged = true; + return true; + } + + return false; +} + +bool CPVRChannel::SetEPGScraper(const std::string& strScraper) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_strEPGScraper != strScraper) + { + bool bCleanEPG = !m_strEPGScraper.empty() || strScraper.empty(); + + m_strEPGScraper = strScraper; + + if (bCleanEPG && m_epg) + m_epg->Clear(); + + m_bChanged = true; + return true; + } + + return false; +} + +void CPVRChannel::ToSortable(SortItem& sortable, Field field) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (field == FieldChannelName) + sortable[FieldChannelName] = m_strChannelName; + else if (field == FieldLastPlayed) + { + const CDateTime lastWatched(m_iLastWatched); + sortable[FieldLastPlayed] = + lastWatched.IsValid() ? lastWatched.GetAsDBDateTime() : StringUtils::Empty; + } + else if (field == FieldProvider) + sortable[FieldProvider] = StringUtils::Format("{} {}", m_iClientId, m_iClientProviderUid); +} + +int CPVRChannel::ChannelID() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iChannelId; +} + +bool CPVRChannel::IsNew() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iChannelId <= 0; +} + +bool CPVRChannel::IsHidden() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bIsHidden; +} + +bool CPVRChannel::IsLocked() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bIsLocked; +} + +std::string CPVRChannel::ClientIconPath() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iconPath.GetClientImage(); +} + +std::string CPVRChannel::IconPath() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iconPath.GetLocalImage(); +} + +bool CPVRChannel::IsUserSetIcon() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bIsUserSetIcon; +} + +bool CPVRChannel::IsUserSetName() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bIsUserSetName; +} + +bool CPVRChannel::IsUserSetHidden() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bIsUserSetHidden; +} + +std::string CPVRChannel::ChannelName() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strChannelName; +} + +time_t CPVRChannel::LastWatched() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iLastWatched; +} + +bool CPVRChannel::IsChanged() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bChanged; +} + +void CPVRChannel::Persisted() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bChanged = false; +} + +int CPVRChannel::UniqueID() const +{ + return m_iUniqueId; +} + +int CPVRChannel::ClientID() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iClientId; +} + +const CPVRChannelNumber& CPVRChannel::ClientChannelNumber() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_clientChannelNumber; +} + +std::string CPVRChannel::ClientChannelName() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strClientChannelName; +} + +std::string CPVRChannel::MimeType() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strMimeType; +} + +bool CPVRChannel::IsEncrypted() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iClientEncryptionSystem > 0; +} + +int CPVRChannel::EncryptionSystem() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iClientEncryptionSystem; +} + +std::string CPVRChannel::EncryptionName() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strClientEncryptionName; +} + +int CPVRChannel::EpgID() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iEpgId; +} + +bool CPVRChannel::EPGEnabled() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bEPGEnabled; +} + +std::string CPVRChannel::EPGScraper() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strEPGScraper; +} + +bool CPVRChannel::CanRecord() const +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + return client && client->GetClientCapabilities().SupportsRecordings() && + client->GetClientCapabilities().SupportsTimers(); +} + +std::shared_ptr<CPVRProvider> CPVRChannel::GetDefaultProvider() const +{ + return CServiceBroker::GetPVRManager().Providers()->GetByClient(m_iClientId, + PVR_PROVIDER_INVALID_UID); +} + +bool CPVRChannel::HasClientProvider() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iClientProviderUid != PVR_PROVIDER_INVALID_UID; +} + +std::shared_ptr<CPVRProvider> CPVRChannel::GetProvider() const +{ + auto provider = + CServiceBroker::GetPVRManager().Providers()->GetByClient(m_iClientId, m_iClientProviderUid); + + if (!provider) + provider = GetDefaultProvider(); + + return provider; +} diff --git a/xbmc/pvr/channels/PVRChannel.h b/xbmc/pvr/channels/PVRChannel.h new file mode 100644 index 0000000..8ff5054 --- /dev/null +++ b/xbmc/pvr/channels/PVRChannel.h @@ -0,0 +1,550 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h" +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_providers.h" +#include "pvr/PVRCachedImage.h" +#include "pvr/channels/PVRChannelNumber.h" +#include "threads/CriticalSection.h" +#include "utils/ISerializable.h" +#include "utils/ISortable.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +class CDateTime; + +namespace PVR +{ +enum class PVREvent; + +class CPVRProvider; +class CPVREpg; +class CPVREpgInfoTag; +class CPVRRadioRDSInfoTag; + +class CPVRChannel : public ISerializable, public ISortable +{ + friend class CPVRDatabase; + +public: + static const std::string IMAGE_OWNER_PATTERN; + + explicit CPVRChannel(bool bRadio); + CPVRChannel(bool bRadio, const std::string& iconPath); + CPVRChannel(const PVR_CHANNEL& channel, unsigned int iClientId); + + virtual ~CPVRChannel(); + + bool operator==(const CPVRChannel& right) const; + bool operator!=(const CPVRChannel& right) const; + + /*! + * @brief Copy over data to the given PVR_CHANNEL instance. + * @param channel The channel instance to fill. + */ + void FillAddonData(PVR_CHANNEL& channel) const; + + void Serialize(CVariant& value) const override; + + /*! @name Kodi related channel methods + */ + //@{ + + /*! + * @brief Delete this channel from the database. + * @return True if it was deleted successfully, false otherwise. + */ + bool QueueDelete(); + + /*! + * @brief Update this channel tag with the data of the given channel tag. + * @param channel The new channel data. + * @return True if something changed, false otherwise. + */ + bool UpdateFromClient(const std::shared_ptr<CPVRChannel>& channel); + + /*! + * @brief Persists the changes in the database. + * @return True if the changes were saved successfully, false otherwise. + */ + bool Persist(); + + /*! + * @return The identifier given to this channel by the TV database. + */ + int ChannelID() const; + + /*! + * @return True when not persisted yet, false otherwise. + */ + bool IsNew() const; + + /*! + * @brief Set the identifier for this channel. + * @param iDatabaseId The new channel ID + * @return True if the something changed, false otherwise. + */ + bool SetChannelID(int iDatabaseId); + + /*! + * @return True if this channel is a radio channel, false if not. + */ + bool IsRadio() const { return m_bIsRadio; } + + /*! + * @return True if this channel is hidden. False if not. + */ + bool IsHidden() const; + + /*! + * @brief Set to true to hide this channel. Set to false to unhide it. + * + * Set to true to hide this channel. Set to false to unhide it. + * The EPG of hidden channels won't be updated. + * @param bIsHidden The new setting. + * @param bIsUserSetIcon true if user changed the hidden flag via GUI, false otherwise. + * @return True if the something changed, false otherwise. + */ + bool SetHidden(bool bIsHidden, bool bIsUserSetHidden = false); + + /*! + * @return True if this channel is locked. False if not. + */ + bool IsLocked() const; + + /*! + * @brief Set to true to lock this channel. Set to false to unlock it. + * + * Set to true to lock this channel. Set to false to unlock it. + * Locked channels need can only be viewed if parental PIN entered. + * @param bIsLocked The new setting. + * @return True if the something changed, false otherwise. + */ + bool SetLocked(bool bIsLocked); + + /*! + * @brief Obtain the Radio RDS data for this channel, if available. + * @return The Radio RDS data or nullptr. + */ + std::shared_ptr<CPVRRadioRDSInfoTag> GetRadioRDSInfoTag() const; + + /*! + * @brief Set the Radio RDS data for the channel. + * @param tag The RDS data. + */ + void SetRadioRDSInfoTag(const std::shared_ptr<CPVRRadioRDSInfoTag>& tag); + + /*! + * @return True if this channel has archive support, false otherwise + */ + bool HasArchive() const; + + /*! + * @brief Set the archive support flag for this channel. + * @param bHasArchive True to set the flag, false to reset. + * @return True if the flag was changed, false otherwise. + */ + bool SetArchive(bool bHasArchive); + + /*! + * @return The path to the icon for this channel as given by the client. + */ + std::string ClientIconPath() const; + + /*! + * @return The path to the icon for this channel. + */ + std::string IconPath() const; + + /*! + * @return True if this user changed icon via GUI. False if not. + */ + bool IsUserSetIcon() const; + + /*! + * @return whether the user has changed the channel name through the GUI + */ + bool IsUserSetName() const; + + /*! + * @return True if user changed the hidden flag via GUI, False if not + */ + bool IsUserSetHidden() const; + + /*! + * @brief Set the path to the icon for this channel. + * @param strIconPath The new path. + * @param bIsUserSetIcon true if user changed the icon via GUI, false otherwise. + * @return True if the something changed, false otherwise. + */ + bool SetIconPath(const std::string& strIconPath, bool bIsUserSetIcon = false); + + /*! + * @return The name for this channel used by XBMC. + */ + std::string ChannelName() const; + + /*! + * @brief Set the name for this channel used by XBMC. + * @param strChannelName The new channel name. + * @param bIsUserSetName whether the change was triggered by the user directly + * @return True if the something changed, false otherwise. + */ + bool SetChannelName(const std::string& strChannelName, bool bIsUserSetName = false); + + /*! + * @return Time channel has been watched last. + */ + time_t LastWatched() const; + + /*! + * @brief Last time channel has been watched + * @param iLastWatched The new value. + * @return True if the something changed, false otherwise. + */ + bool SetLastWatched(time_t iLastWatched); + + /*! + * @brief Check whether this channel has unpersisted data changes. + * @return True if this channel has changes to persist, false otherwise + */ + bool IsChanged() const; + + /*! + * @brief reset changed flag after persist + */ + void Persisted(); + //@} + + /*! @name Client related channel methods + */ + //@{ + + /*! + * @brief A unique identifier for this channel. + * + * A unique identifier for this channel. + * It can be used to find the same channel on different providers + * + * @return The Unique ID. + */ + int UniqueID() const; + + /*! + * @return The identifier of the client that serves this channel. + */ + int ClientID() const; + + /*! + * @brief Set the identifier of the client that serves this channel. + * @param iClientId The new ID. + * @return True if the something changed, false otherwise. + */ + bool SetClientID(int iClientId); + + /*! + * Get the channel number on the client. + * @return The channel number on the client. + */ + const CPVRChannelNumber& ClientChannelNumber() const; + + /*! + * @return The name of this channel on the client. + */ + std::string ClientChannelName() const; + + /*! + * @brief The stream input mime type + * + * The stream input type + * If it is empty, ffmpeg will try to scan the stream to find the right input format. + * See https://www.iana.org/assignments/media-types/media-types.xhtml for a + * list of the input formats. + * + * @return The stream input type + */ + std::string MimeType() const; + + // ISortable implementation + void ToSortable(SortItem& sortable, Field field) const override; + + /*! + * @return Storage id for this channel in CPVRChannelGroup + */ + std::pair<int, int> StorageId() const { return std::make_pair(m_iClientId, m_iUniqueId); } + + /*! + * @brief Return true if this channel is encrypted. + * + * Return true if this channel is encrypted. Does not inform whether XBMC can play the file. + * Decryption should be done by the client. + * + * @return Return true if this channel is encrypted. + */ + bool IsEncrypted() const; + + /*! + * @brief Return the encryption system ID for this channel. 0 for FTA. + * + * Return the encryption system ID for this channel. 0 for FTA. + * The values are documented on: http://www.dvb.org/index.php?id=174. + * + * @return Return the encryption system ID for this channel. + */ + int EncryptionSystem() const; + + /*! + * @return A friendly name for the used encryption system. + */ + std::string EncryptionName() const; + //@} + + /*! @name EPG methods + */ + //@{ + + /*! + * @return The ID of the EPG table to use for this channel or -1 if it isn't set. + */ + int EpgID() const; + + /*! + * @brief Create the EPG for this channel, if it does not yet exist + * @return true if a new epg was created, false otherwise. + */ + bool CreateEPG(); + + /*! + * @brief Get the EPG table for this channel. + * @return The EPG for this channel. + */ + std::shared_ptr<CPVREpg> GetEPG() const; + + /*! + * @brief Get the EPG tags for this channel. + * @return The tags. + */ + std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTags() const; + + /*! + * @brief Get all EPG tags for the given time frame, including "gap" tags. + * @param timelineStart Start of time line. + * @param timelineEnd End of time line. + * @param minEventEnd The minimum end time of the events to return. + * @param maxEventStart The maximum start time of the events to return. + * @return The matching tags. + */ + std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEPGTimeline(const CDateTime& timelineStart, + const CDateTime& timelineEnd, + const CDateTime& minEventEnd, + const CDateTime& maxEventStart) const; + + /*! + * @brief Create a "gap" EPG tag. + * @param start Start of gap. + * @param end End of gap. + * @return The tag. + */ + std::shared_ptr<CPVREpgInfoTag> CreateEPGGapTag(const CDateTime& start, + const CDateTime& end) const; + + /*! + * @brief Get the EPG tag that is now active on this channel. + * + * Get the EPG tag that is now active on this channel. + * Will return an empty tag if there is none. + * + * @return The EPG tag that is now active. + */ + std::shared_ptr<CPVREpgInfoTag> GetEPGNow() const; + + /*! + * @brief Get the EPG tag that was previously active on this channel. + * + * Get the EPG tag that was previously active on this channel. + * Will return an empty tag if there is none. + * + * @return The EPG tag that was previously active. + */ + std::shared_ptr<CPVREpgInfoTag> GetEPGPrevious() const; + + /*! + * @brief Get the EPG tag that will be next active on this channel. + * + * Get the EPG tag that will be next active on this channel. + * Will return an empty tag if there is none. + * + * @return The EPG tag that will be next active. + */ + std::shared_ptr<CPVREpgInfoTag> GetEPGNext() const; + + /*! + * @return Don't use an EPG for this channel if set to false. + */ + bool EPGEnabled() const; + + /*! + * @brief Set to true if an EPG should be used for this channel. Set to false otherwise. + * @param bEPGEnabled The new value. + * @return True if the something changed, false otherwise. + */ + bool SetEPGEnabled(bool bEPGEnabled); + + /*! + * @brief Get the name of the scraper to be used for this channel. + * + * Get the name of the scraper to be used for this channel. + * The default is 'client', which means the EPG should be loaded from the backend. + * + * @return The name of the scraper to be used for this channel. + */ + std::string EPGScraper() const; + + /*! + * @brief Set the name of the scraper to be used for this channel. + * + * Set the name of the scraper to be used for this channel. + * Set to "client" to load the EPG from the backend + * + * @param strScraper The new scraper name. + * @return True if the something changed, false otherwise. + */ + bool SetEPGScraper(const std::string& strScraper); + + bool CanRecord() const; + + static std::string GetEncryptionName(int iCaid); + + /*! + * @brief Get the client order for this channel + * @return iOrder The order for this channel + */ + int ClientOrder() const { return m_iClientOrder; } + + /*! + * @brief Get the client provider Uid for this channel + * @return m_iClientProviderUid The provider Uid for this channel + */ + int ClientProviderUid() const { return m_iClientProviderUid; } + + /*! + * @brief CEventStream callback for PVR events. + * @param event The event. + */ + void Notify(const PVREvent& event); + + /*! + * @brief Lock the instance. No other thread gets access to this channel until Unlock was called. + */ + void Lock() { m_critSection.lock(); } + + /*! + * @brief Unlock the instance. Other threads may get access to this channel again. + */ + void Unlock() { m_critSection.unlock(); } + + /*! + * @brief Get the default provider of this channel. The default + * provider represents the PVR add-on itself. + * @return The default provider of this channel + */ + std::shared_ptr<CPVRProvider> GetDefaultProvider() const; + + /*! + * @brief Whether or not this channel has a provider set by the client. + * @return True if a provider was set by the client, false otherwise. + */ + bool HasClientProvider() const; + + /*! + * @brief Get the provider of this channel. This may be the default provider or a + * custom provider set by the client. If @ref "HasClientProvider()" returns true + * the provider will be custom from the client, otherwise the default provider. + * @return The provider of this channel + */ + std::shared_ptr<CPVRProvider> GetProvider() const; + + //@} +private: + CPVRChannel() = delete; + CPVRChannel(const CPVRChannel& tag) = delete; + CPVRChannel& operator=(const CPVRChannel& channel) = delete; + + /*! + * @brief Update the encryption name after SetEncryptionSystem() has been called. + */ + void UpdateEncryptionName(); + + /*! + * @brief Reset the EPG instance pointer. + */ + void ResetEPG(); + + /*! + * @brief Set the client provider Uid for this channel + * @param iClientProviderUid The provider Uid for this channel + * @return True if the something changed, false otherwise. + */ + bool SetClientProviderUid(int iClientProviderUid); + + /*! @name Kodi related channel data + */ + //@{ + int m_iChannelId = -1; /*!< the identifier given to this channel by the TV database */ + bool m_bIsRadio = false; /*!< true if this channel is a radio channel, false if not */ + bool m_bIsHidden = false; /*!< true if this channel is hidden, false if not */ + bool m_bIsUserSetName = false; /*!< true if user set the channel name via GUI, false if not */ + bool m_bIsUserSetIcon = false; /*!< true if user set the icon via GUI, false if not */ + bool m_bIsUserSetHidden = false; /*!< true if user set the hidden flag via GUI, false if not */ + bool m_bIsLocked = false; /*!< true if channel is locked, false if not */ + CPVRCachedImage m_iconPath; /*!< the path to the icon for this channel */ + std::string m_strChannelName; /*!< the name for this channel used by Kodi */ + time_t m_iLastWatched = 0; /*!< last time channel has been watched */ + bool m_bChanged = + false; /*!< true if anything in this entry was changed that needs to be persisted */ + std::shared_ptr<CPVRRadioRDSInfoTag> + m_rdsTag; /*! < the radio rds data, if available for the channel. */ + bool m_bHasArchive = false; /*!< true if this channel supports archive */ + //@} + + /*! @name EPG related channel data + */ + //@{ + int m_iEpgId = -1; /*!< the id of the EPG for this channel */ + bool m_bEPGEnabled = false; /*!< don't use an EPG for this channel if set to false */ + std::string m_strEPGScraper = + "client"; /*!< the name of the scraper to be used for this channel */ + std::shared_ptr<CPVREpg> m_epg; + //@} + + /*! @name Client related channel data + */ + //@{ + int m_iUniqueId = -1; /*!< the unique identifier for this channel */ + int m_iClientId = -1; /*!< the identifier of the client that serves this channel */ + CPVRChannelNumber m_clientChannelNumber; /*!< the channel number on the client */ + std::string m_strClientChannelName; /*!< the name of this channel on the client */ + std::string + m_strMimeType; /*!< the stream input type based mime type, see @ref https://www.iana.org/assignments/media-types/media-types.xhtml#video */ + int m_iClientEncryptionSystem = + -1; /*!< the encryption system used by this channel. 0 for FreeToAir, -1 for unknown */ + std::string + m_strClientEncryptionName; /*!< the name of the encryption system used by this channel */ + int m_iClientOrder = 0; /*!< the order from this channels group member */ + int m_iClientProviderUid = + PVR_PROVIDER_INVALID_UID; /*!< the unique id for this provider from the client */ + //@} + + mutable CCriticalSection m_critSection; +}; +} // namespace PVR diff --git a/xbmc/pvr/channels/PVRChannelGroup.cpp b/xbmc/pvr/channels/PVRChannelGroup.cpp new file mode 100644 index 0000000..7f03430 --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelGroup.cpp @@ -0,0 +1,1176 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +//! @todo use Observable here, so we can use event driven operations later + +#include "PVRChannelGroup.h" + +#include "ServiceBroker.h" +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channel_groups.h" +#include "pvr/PVRCachedImages.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelsPath.h" +#include "pvr/epg/Epg.h" +#include "pvr/epg/EpgChannelData.h" +#include "pvr/epg/EpgInfoTag.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <algorithm> +#include <iterator> +#include <memory> +#include <mutex> +#include <numeric> +#include <string> +#include <utility> +#include <vector> + +using namespace PVR; + +CPVRChannelGroup::CPVRChannelGroup(const CPVRChannelsPath& path, + const std::shared_ptr<CPVRChannelGroup>& allChannelsGroup) + : m_allChannelsGroup(allChannelsGroup), m_path(path) +{ + GetSettings()->RegisterCallback(this); +} + +CPVRChannelGroup::CPVRChannelGroup(const PVR_CHANNEL_GROUP& group, + const std::shared_ptr<CPVRChannelGroup>& allChannelsGroup) + : m_iPosition(group.iPosition), + m_allChannelsGroup(allChannelsGroup), + m_path(group.bIsRadio, group.strGroupName) +{ + GetSettings()->RegisterCallback(this); +} + +CPVRChannelGroup::~CPVRChannelGroup() +{ + GetSettings()->UnregisterCallback(this); +} + +bool CPVRChannelGroup::operator==(const CPVRChannelGroup& right) const +{ + return (m_iGroupType == right.m_iGroupType && m_iGroupId == right.m_iGroupId && + m_iPosition == right.m_iPosition && m_path == right.m_path); +} + +bool CPVRChannelGroup::operator!=(const CPVRChannelGroup& right) const +{ + return !(*this == right); +} + +void CPVRChannelGroup::FillAddonData(PVR_CHANNEL_GROUP& group) const +{ + group = {}; + group.bIsRadio = IsRadio(); + strncpy(group.strGroupName, GroupName().c_str(), sizeof(group.strGroupName) - 1); + group.iPosition = GetPosition(); +} + +CCriticalSection CPVRChannelGroup::m_settingsSingletonCritSection; +std::weak_ptr<CPVRChannelGroupSettings> CPVRChannelGroup::m_settingsSingleton; + +std::shared_ptr<CPVRChannelGroupSettings> CPVRChannelGroup::GetSettings() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_settings) + { + std::unique_lock<CCriticalSection> singletonLock(m_settingsSingletonCritSection); + const std::shared_ptr<CPVRChannelGroupSettings> settings = m_settingsSingleton.lock(); + if (settings) + { + m_settings = settings; + } + else + { + m_settings = std::make_shared<CPVRChannelGroupSettings>(); + m_settingsSingleton = m_settings; + } + } + return m_settings; +} + +bool CPVRChannelGroup::LoadFromDatabase( + const std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& channels, + const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + const int iChannelCount = m_iGroupId > 0 ? LoadFromDatabase(clients) : 0; + CLog::LogFC(LOGDEBUG, LOGPVR, "Fetched {} {} group members from the database for group '{}'", + iChannelCount, IsRadio() ? "radio" : "TV", GroupName()); + + for (const auto& groupMember : m_members) + { + if (groupMember.second->Channel()) + continue; + + auto channel = channels.find(groupMember.first); + if (channel == channels.end()) + { + CLog::Log(LOGERROR, "Cannot find group member '{},{}' in channels!", groupMember.first.first, + groupMember.first.second); + // No workaround here, please. We need to find and fix the root cause of this case! + } + groupMember.second->SetChannel((*channel).second); + } + + m_bLoaded = true; + return true; +} + +void CPVRChannelGroup::Unload() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_sortedMembers.clear(); + m_members.clear(); + m_failedClients.clear(); +} + +bool CPVRChannelGroup::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + if (GroupType() == PVR_GROUP_TYPE_USER_DEFINED || !GetSettings()->SyncChannelGroups()) + return true; + + // get the channel group members from the backends. + std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers; + CServiceBroker::GetPVRManager().Clients()->GetChannelGroupMembers(clients, this, groupMembers, + m_failedClients); + return UpdateGroupEntries(groupMembers); +} + +const CPVRChannelsPath& CPVRChannelGroup::GetPath() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_path; +} + +void CPVRChannelGroup::SetPath(const CPVRChannelsPath& path) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_path != path) + { + m_path = path; + if (m_bLoaded) + { + // note: path contains both the radio flag and the group name, which are stored in the db + m_bChanged = true; + Persist(); //! @todo why must we persist immediately? + } + } +} + +bool CPVRChannelGroup::SetChannelNumber(const std::shared_ptr<CPVRChannel>& channel, + const CPVRChannelNumber& channelNumber) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const auto it = + std::find_if(m_sortedMembers.cbegin(), m_sortedMembers.cend(), + [&channel](const auto& member) { return *member->Channel() == *channel; }); + + if (it != m_sortedMembers.cend() && (*it)->ChannelNumber() != channelNumber) + { + (*it)->SetChannelNumber(channelNumber); + return true; + } + + return false; +} + +/********** sort methods **********/ + +struct sortByClientChannelNumber +{ + bool operator()(const std::shared_ptr<CPVRChannelGroupMember>& channel1, + const std::shared_ptr<CPVRChannelGroupMember>& channel2) const + { + if (channel1->ClientPriority() == channel2->ClientPriority()) + { + if (channel1->ClientChannelNumber() == channel2->ClientChannelNumber()) + return channel1->Channel()->ChannelName() < channel2->Channel()->ChannelName(); + + return channel1->ClientChannelNumber() < channel2->ClientChannelNumber(); + } + return channel1->ClientPriority() > channel2->ClientPriority(); + } +}; + +struct sortByChannelNumber +{ + bool operator()(const std::shared_ptr<CPVRChannelGroupMember>& channel1, + const std::shared_ptr<CPVRChannelGroupMember>& channel2) const + { + return channel1->ChannelNumber() < channel2->ChannelNumber(); + } +}; + +void CPVRChannelGroup::Sort() +{ + if (GetSettings()->UseBackendChannelOrder()) + SortByClientChannelNumber(); + else + SortByChannelNumber(); +} + +bool CPVRChannelGroup::SortAndRenumber() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + Sort(); + return Renumber(); +} + +void CPVRChannelGroup::SortByClientChannelNumber() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + std::sort(m_sortedMembers.begin(), m_sortedMembers.end(), sortByClientChannelNumber()); +} + +void CPVRChannelGroup::SortByChannelNumber() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + std::sort(m_sortedMembers.begin(), m_sortedMembers.end(), sortByChannelNumber()); +} + +bool CPVRChannelGroup::UpdateClientPriorities() +{ + const std::shared_ptr<CPVRClients> clients = CServiceBroker::GetPVRManager().Clients(); + bool bChanged = false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + + const bool bUseBackendChannelOrder = GetSettings()->UseBackendChannelOrder(); + for (auto& member : m_sortedMembers) + { + int iNewPriority = 0; + + if (bUseBackendChannelOrder) + { + const std::shared_ptr<CPVRClient> client = + clients->GetCreatedClient(member->Channel()->ClientID()); + if (!client) + continue; + + iNewPriority = client->GetPriority(); + } + else + { + iNewPriority = 0; + } + + bChanged |= (member->ClientPriority() != iNewPriority); + member->SetClientPriority(iNewPriority); + } + + return bChanged; +} + +/********** getters **********/ +std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetByUniqueID( + const std::pair<int, int>& id) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const auto it = m_members.find(id); + return it != m_members.end() ? it->second : std::shared_ptr<CPVRChannelGroupMember>(); +} + +std::shared_ptr<CPVRChannel> CPVRChannelGroup::GetByUniqueID(int iUniqueChannelId, + int iClientID) const +{ + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + GetByUniqueID(std::make_pair(iClientID, iUniqueChannelId)); + return groupMember ? groupMember->Channel() : std::shared_ptr<CPVRChannel>(); +} + +std::shared_ptr<CPVRChannel> CPVRChannelGroup::GetByChannelID(int iChannelID) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const auto it = + std::find_if(m_members.cbegin(), m_members.cend(), [iChannelID](const auto& member) { + return member.second->Channel()->ChannelID() == iChannelID; + }); + return it != m_members.cend() ? (*it).second->Channel() : std::shared_ptr<CPVRChannel>(); +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetLastPlayedChannelGroupMember( + int iCurrentChannel /* = -1 */) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + std::shared_ptr<CPVRChannelGroupMember> groupMember; + for (const auto& memberPair : m_members) + { + const std::shared_ptr<CPVRChannel> channel = memberPair.second->Channel(); + if (channel->ChannelID() != iCurrentChannel && + CServiceBroker::GetPVRManager().Clients()->IsCreatedClient(channel->ClientID()) && + channel->LastWatched() > 0 && + (!groupMember || channel->LastWatched() > groupMember->Channel()->LastWatched())) + { + groupMember = memberPair.second; + } + } + + return groupMember; +} + +GroupMemberPair CPVRChannelGroup::GetLastAndPreviousToLastPlayedChannelGroupMember() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_sortedMembers.empty()) + return {}; + + auto members = m_sortedMembers; + lock.unlock(); + + std::sort(members.begin(), members.end(), [](const auto& a, const auto& b) { + return a->Channel()->LastWatched() > b->Channel()->LastWatched(); + }); + + std::shared_ptr<CPVRChannelGroupMember> last; + std::shared_ptr<CPVRChannelGroupMember> previousToLast; + if (members[0]->Channel()->LastWatched()) + { + last = members[0]; + if (members.size() > 1 && members[1]->Channel()->LastWatched()) + previousToLast = members[1]; + } + + return {last, previousToLast}; +} + +CPVRChannelNumber CPVRChannelGroup::GetChannelNumber( + const std::shared_ptr<CPVRChannel>& channel) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const std::shared_ptr<CPVRChannelGroupMember> member = GetByUniqueID(channel->StorageId()); + return member ? member->ChannelNumber() : CPVRChannelNumber(); +} + +CPVRChannelNumber CPVRChannelGroup::GetClientChannelNumber( + const std::shared_ptr<CPVRChannel>& channel) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const std::shared_ptr<CPVRChannelGroupMember> member = GetByUniqueID(channel->StorageId()); + return member ? member->ClientChannelNumber() : CPVRChannelNumber(); +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetByChannelNumber( + const CPVRChannelNumber& channelNumber) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const bool bUseBackendChannelNumbers = GetSettings()->UseBackendChannelNumbers(); + for (const auto& member : m_sortedMembers) + { + CPVRChannelNumber activeChannelNumber = + bUseBackendChannelNumbers ? member->ClientChannelNumber() : member->ChannelNumber(); + if (activeChannelNumber == channelNumber) + return member; + } + + return {}; +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetNextChannelGroupMember( + const std::shared_ptr<CPVRChannelGroupMember>& groupMember) const +{ + std::shared_ptr<CPVRChannelGroupMember> nextMember; + + if (groupMember) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + for (auto it = m_sortedMembers.cbegin(); !nextMember && it != m_sortedMembers.cend(); ++it) + { + if (*it == groupMember) + { + do + { + if ((++it) == m_sortedMembers.cend()) + it = m_sortedMembers.cbegin(); + if ((*it)->Channel() && !(*it)->Channel()->IsHidden()) + nextMember = *it; + } while (!nextMember && *it != groupMember); + + break; + } + } + } + + return nextMember; +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetPreviousChannelGroupMember( + const std::shared_ptr<CPVRChannelGroupMember>& groupMember) const +{ + std::shared_ptr<CPVRChannelGroupMember> previousMember; + + if (groupMember) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + for (auto it = m_sortedMembers.crbegin(); !previousMember && it != m_sortedMembers.crend(); + ++it) + { + if (*it == groupMember) + { + do + { + if ((++it) == m_sortedMembers.crend()) + it = m_sortedMembers.crbegin(); + if ((*it)->Channel() && !(*it)->Channel()->IsHidden()) + previousMember = *it; + } while (!previousMember && *it != groupMember); + + break; + } + } + } + return previousMember; +} + +std::vector<std::shared_ptr<CPVRChannelGroupMember>> CPVRChannelGroup::GetMembers( + Include eFilter /* = Include::ALL */) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (eFilter == Include::ALL) + return m_sortedMembers; + + std::vector<std::shared_ptr<CPVRChannelGroupMember>> members; + for (const auto& member : m_sortedMembers) + { + switch (eFilter) + { + case Include::ONLY_HIDDEN: + if (!member->Channel()->IsHidden()) + continue; + break; + case Include::ONLY_VISIBLE: + if (member->Channel()->IsHidden()) + continue; + break; + default: + break; + } + + members.emplace_back(member); + } + + return members; +} + +void CPVRChannelGroup::GetChannelNumbers(std::vector<std::string>& channelNumbers) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const bool bUseBackendChannelNumbers = GetSettings()->UseBackendChannelNumbers(); + for (const auto& member : m_sortedMembers) + { + CPVRChannelNumber activeChannelNumber = + bUseBackendChannelNumbers ? member->ClientChannelNumber() : member->ChannelNumber(); + channelNumbers.emplace_back(activeChannelNumber.FormattedChannelNumber()); + } +} + +int CPVRChannelGroup::LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase()); + if (!database) + return -1; + + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> results = + database->Get(*this, clients); + + std::vector<std::shared_ptr<CPVRChannelGroupMember>> membersToDelete; + if (!results.empty()) + { + const std::shared_ptr<CPVRClients> allClients = CServiceBroker::GetPVRManager().Clients(); + + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& member : results) + { + // Consistency check. + if (member->ClientID() > 0 && member->ChannelUID() > 0 && member->IsRadio() == IsRadio()) + { + // Ignore data from unknown/disabled clients + if (allClients->IsEnabledClient(member->ClientID())) + { + m_sortedMembers.emplace_back(member); + m_members.emplace(std::make_pair(member->ClientID(), member->ChannelUID()), member); + } + } + else + { + CLog::LogF(LOGWARNING, + "Skipping member with channel database id {} of {} channel group '{}'. " + "Channel not found in the database or radio flag changed.", + member->ChannelDatabaseID(), IsRadio() ? "radio" : "TV", GroupName()); + membersToDelete.emplace_back(member); + } + } + + SortByChannelNumber(); + } + + DeleteGroupMembersFromDb(membersToDelete); + + return results.size() - membersToDelete.size(); +} + +void CPVRChannelGroup::DeleteGroupMembersFromDb( + const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& membersToDelete) +{ + if (!membersToDelete.empty()) + { + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (!database) + { + CLog::LogF(LOGERROR, "No TV database"); + return; + } + + // Note: We must lock the db the whole time, otherwise races may occur. + database->Lock(); + + bool commitPending = false; + + for (const auto& member : membersToDelete) + { + commitPending |= member->QueueDelete(); + + size_t queryCount = database->GetDeleteQueriesCount(); + if (queryCount > CHANNEL_COMMIT_QUERY_COUNT_LIMIT) + database->CommitDeleteQueries(); + } + + if (commitPending) + database->CommitDeleteQueries(); + + database->Unlock(); + } +} + +bool CPVRChannelGroup::UpdateFromClient(const std::shared_ptr<CPVRChannelGroupMember>& groupMember) +{ + bool bChanged = false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + + const std::shared_ptr<CPVRChannel> channel = groupMember->Channel(); + const std::shared_ptr<CPVRChannelGroupMember> existingMember = + GetByUniqueID(channel->StorageId()); + if (existingMember) + { + // update existing channel + if (IsInternalGroup() && existingMember->Channel()->UpdateFromClient(channel)) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Updated {} channel '{}' from PVR client", + IsRadio() ? "radio" : "TV", channel->ChannelName()); + bChanged = true; + } + + existingMember->SetClientChannelNumber(channel->ClientChannelNumber()); + existingMember->SetOrder(groupMember->Order()); + + if (existingMember->NeedsSave()) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Updated {} channel group member '{}' in group '{}'", + IsRadio() ? "radio" : "TV", channel->ChannelName(), GroupName()); + bChanged = true; + } + } + else + { + if (groupMember->GroupID() == -1) + groupMember->SetGroupID(GroupID()); + + m_sortedMembers.emplace_back(groupMember); + m_members.emplace(channel->StorageId(), groupMember); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Added {} channel group member '{}' to group '{}'", + IsRadio() ? "radio" : "TV", channel->ChannelName(), GroupName()); + + // create EPG for new channel + if (IsInternalGroup() && channel->CreateEPG()) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Created EPG for {} channel '{}' from PVR client", + IsRadio() ? "radio" : "TV", channel->ChannelName()); + } + + bChanged = true; + } + + return bChanged; +} + +bool CPVRChannelGroup::AddAndUpdateGroupMembers( + const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers) +{ + return std::accumulate(groupMembers.cbegin(), groupMembers.cend(), false, + [this](bool changed, const auto& groupMember) { + return UpdateFromClient(groupMember) ? true : changed; + }); +} + +bool CPVRChannelGroup::HasValidDataForClient(int iClientId) const +{ + return std::find(m_failedClients.begin(), m_failedClients.end(), iClientId) == + m_failedClients.end(); +} + +bool CPVRChannelGroup::HasValidDataForClients( + const std::vector<std::shared_ptr<CPVRClient>>& clients) const +{ + return m_failedClients.empty() || std::none_of(clients.cbegin(), clients.cend(), + [this](const std::shared_ptr<CPVRClient>& client) { + return !HasValidDataForClient(client->GetID()); + }); +} + +bool CPVRChannelGroup::UpdateChannelNumbersFromAllChannelsGroup() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + bool bChanged = false; + + if (!IsInternalGroup()) + { + // If we don't sync channel groups make sure the channel numbers are set from + // the all channels group using the non default renumber call before sorting + if (Renumber(IGNORE_NUMBERING_FROM_ONE) || SortAndRenumber()) + bChanged = true; + } + + m_events.Publish(IsInternalGroup() || bChanged ? PVREvent::ChannelGroupInvalidated + : PVREvent::ChannelGroup); + + return bChanged; +} + +std::vector<std::shared_ptr<CPVRChannelGroupMember>> CPVRChannelGroup::RemoveDeletedGroupMembers( + const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers) +{ + std::vector<std::shared_ptr<CPVRChannelGroupMember>> membersToRemove; + + std::unique_lock<CCriticalSection> lock(m_critSection); + + // put group members into map to speedup the following lookups + std::map<std::pair<int, int>, std::shared_ptr<CPVRChannelGroupMember>> membersMap; + std::transform(groupMembers.begin(), groupMembers.end(), + std::inserter(membersMap, membersMap.end()), + [](const std::shared_ptr<CPVRChannelGroupMember>& member) { + return std::make_pair(member->Channel()->StorageId(), member); + }); + + // check for deleted channels + for (auto it = m_sortedMembers.begin(); it != m_sortedMembers.end();) + { + const std::shared_ptr<CPVRChannel> channel = (*it)->Channel(); + auto mapIt = membersMap.find(channel->StorageId()); + if (mapIt == membersMap.end()) + { + if (HasValidDataForClient(channel->ClientID())) + { + CLog::Log(LOGINFO, "Removed stale {} channel '{}' from group '{}'", + IsRadio() ? "radio" : "TV", channel->ChannelName(), GroupName()); + membersToRemove.emplace_back(*it); + + m_members.erase(channel->StorageId()); + it = m_sortedMembers.erase(it); + continue; + } + } + else + { + membersMap.erase(mapIt); + } + ++it; + } + + DeleteGroupMembersFromDb(membersToRemove); + + return membersToRemove; +} + +bool CPVRChannelGroup::UpdateGroupEntries( + const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers) +{ + bool bReturn = false; + bool bChanged = false; + bool bRemoved = false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + + bRemoved = !RemoveDeletedGroupMembers(groupMembers).empty(); + bChanged = AddAndUpdateGroupMembers(groupMembers) || bRemoved; + bChanged |= UpdateClientPriorities(); + + if (bChanged) + { + // renumber to make sure all group members have a channel number. New members were added at the + // back, so they'll get the highest numbers + bool bRenumbered = SortAndRenumber(); + bReturn = Persist(); + m_events.Publish(HasNewChannels() || bRemoved || bRenumbered ? PVREvent::ChannelGroupInvalidated + : PVREvent::ChannelGroup); + } + else + { + bReturn = true; + } + + return bReturn; +} + +bool CPVRChannelGroup::RemoveFromGroup(const std::shared_ptr<CPVRChannel>& channel) +{ + bool bReturn = false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (auto it = m_sortedMembers.begin(); it != m_sortedMembers.end(); ++it) + { + const auto storageId = (*it)->Channel()->StorageId(); + if (channel->StorageId() == storageId) + { + m_members.erase(storageId); + m_sortedMembers.erase(it); + bReturn = true; + break; + } + } + + // no need to renumber if nothing was removed + if (bReturn) + Renumber(); + + return bReturn; +} + +bool CPVRChannelGroup::AppendToGroup(const std::shared_ptr<CPVRChannel>& channel) +{ + bool bReturn = false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (!CPVRChannelGroup::IsGroupMember(channel)) + { + const std::shared_ptr<CPVRChannelGroupMember> allGroupMember = + m_allChannelsGroup->GetByUniqueID(channel->StorageId()); + + if (allGroupMember) + { + unsigned int channelNumberMax = + std::accumulate(m_sortedMembers.cbegin(), m_sortedMembers.cend(), 0, + [](unsigned int last, const auto& member) { + return (member->ChannelNumber().GetChannelNumber() > last) + ? member->ChannelNumber().GetChannelNumber() + : last; + }); + + const auto newMember = std::make_shared<CPVRChannelGroupMember>(GroupID(), GroupName(), + allGroupMember->Channel()); + newMember->SetChannelNumber(CPVRChannelNumber(channelNumberMax + 1, 0)); + newMember->SetClientPriority(allGroupMember->ClientPriority()); + + m_sortedMembers.emplace_back(newMember); + m_members.emplace(allGroupMember->Channel()->StorageId(), newMember); + + SortAndRenumber(); + bReturn = true; + } + } + return bReturn; +} + +bool CPVRChannelGroup::IsGroupMember(const std::shared_ptr<CPVRChannel>& channel) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_members.find(channel->StorageId()) != m_members.end(); +} + +bool CPVRChannelGroup::Persist() +{ + bool bReturn(true); + const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase()); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + // do not persist if the group is not fully loaded and was saved before. + if (!m_bLoaded && m_iGroupId != INVALID_GROUP_ID) + return bReturn; + + // Mark newly created groups as loaded so future updates will also be persisted... + if (m_iGroupId == INVALID_GROUP_ID) + m_bLoaded = true; + + if (database) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Persisting channel group '{}' with {} channels", GroupName(), + static_cast<int>(m_members.size())); + + bReturn = database->Persist(*this); + m_bChanged = false; + } + else + { + bReturn = false; + } + + return bReturn; +} + +void CPVRChannelGroup::Delete() +{ + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (!database) + { + CLog::LogF(LOGERROR, "No TV database"); + return; + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iGroupId > 0) + { + if (database->Delete(*this)) + m_bDeleted = true; + } +} + +bool CPVRChannelGroup::Renumber(RenumberMode mode /* = NORMAL */) +{ + bool bReturn(false); + unsigned int iChannelNumber(0); + + std::unique_lock<CCriticalSection> lock(m_critSection); + const bool bUseBackendChannelNumbers = GetSettings()->UseBackendChannelNumbers(); + const bool bStartGroupChannelNumbersFromOne = GetSettings()->StartGroupChannelNumbersFromOne(); + + CPVRChannelNumber currentChannelNumber; + CPVRChannelNumber currentClientChannelNumber; + for (auto& sortedMember : m_sortedMembers) + { + currentClientChannelNumber = sortedMember->ClientChannelNumber(); + if (m_allChannelsGroup && !currentClientChannelNumber.IsValid()) + currentClientChannelNumber = + m_allChannelsGroup->GetClientChannelNumber(sortedMember->Channel()); + + if (bUseBackendChannelNumbers) + { + currentChannelNumber = currentClientChannelNumber; + } + else if (sortedMember->Channel()->IsHidden()) + { + currentChannelNumber = CPVRChannelNumber(0, 0); + } + else + { + if (IsInternalGroup()) + { + currentChannelNumber = CPVRChannelNumber(++iChannelNumber, 0); + } + else + { + if (bStartGroupChannelNumbersFromOne && mode != IGNORE_NUMBERING_FROM_ONE) + currentChannelNumber = CPVRChannelNumber(++iChannelNumber, 0); + else + currentChannelNumber = m_allChannelsGroup->GetChannelNumber(sortedMember->Channel()); + } + } + + if (sortedMember->ChannelNumber() != currentChannelNumber || + sortedMember->ClientChannelNumber() != currentClientChannelNumber) + { + bReturn = true; + sortedMember->SetChannelNumber(currentChannelNumber); + sortedMember->SetClientChannelNumber(currentClientChannelNumber); + } + } + + if (bReturn) + Sort(); + + return bReturn; +} + +bool CPVRChannelGroup::HasNewChannels() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::any_of(m_members.cbegin(), m_members.cend(), + [](const auto& member) { return member.second->Channel()->ChannelID() <= 0; }); +} + +bool CPVRChannelGroup::HasChanges() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bChanged; +} + +bool CPVRChannelGroup::IsNew() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iGroupId <= 0; +} + +void CPVRChannelGroup::UseBackendChannelOrderChanged() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + UpdateClientPriorities(); + OnSettingChanged(); +} + +void CPVRChannelGroup::UseBackendChannelNumbersChanged() +{ + OnSettingChanged(); +} + +void CPVRChannelGroup::StartGroupChannelNumbersFromOneChanged() +{ + OnSettingChanged(); +} + +void CPVRChannelGroup::OnSettingChanged() +{ + //! @todo while pvr manager is starting up do accept setting changes. + if (!CServiceBroker::GetPVRManager().IsStarted()) + { + CLog::Log(LOGWARNING, "Channel group setting change ignored while PVR Manager is starting"); + return; + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + + CLog::LogFC(LOGDEBUG, LOGPVR, + "Renumbering channel group '{}' to use the backend channel order and/or numbers", + GroupName()); + + // If we don't sync channel groups make sure the channel numbers are set from + // the all channels group using the non default renumber call before sorting + if (!GetSettings()->SyncChannelGroups()) + Renumber(IGNORE_NUMBERING_FROM_ONE); + + const bool bRenumbered = SortAndRenumber(); + Persist(); + + m_events.Publish(bRenumbered ? PVREvent::ChannelGroupInvalidated : PVREvent::ChannelGroup); +} + +int CPVRChannelGroup::GroupID() const +{ + return m_iGroupId; +} + +void CPVRChannelGroup::SetGroupID(int iGroupId) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (iGroupId >= 0 && m_iGroupId != iGroupId) + { + m_iGroupId = iGroupId; + + // propagate the new id to the group members + for (const auto& member : m_members) + member.second->SetGroupID(iGroupId); + } +} + +void CPVRChannelGroup::SetGroupType(int iGroupType) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_iGroupType != iGroupType) + { + m_iGroupType = iGroupType; + if (m_bLoaded) + m_bChanged = true; + } +} + +int CPVRChannelGroup::GroupType() const +{ + return m_iGroupType; +} + +std::string CPVRChannelGroup::GroupName() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_path.GetGroupName(); +} + +void CPVRChannelGroup::SetGroupName(const std::string& strGroupName) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_path.GetGroupName() != strGroupName) + { + m_path = CPVRChannelsPath(m_path.IsRadio(), strGroupName); + if (m_bLoaded) + { + m_bChanged = true; + Persist(); //! @todo why must we persist immediately? + } + } +} + +bool CPVRChannelGroup::IsRadio() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_path.IsRadio(); +} + +time_t CPVRChannelGroup::LastWatched() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iLastWatched; +} + +void CPVRChannelGroup::SetLastWatched(time_t iLastWatched) +{ + const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase()); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iLastWatched != iLastWatched) + { + m_iLastWatched = iLastWatched; + if (m_bLoaded && database) + database->UpdateLastWatched(*this); + } +} + +uint64_t CPVRChannelGroup::LastOpened() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iLastOpened; +} + +void CPVRChannelGroup::SetLastOpened(uint64_t iLastOpened) +{ + const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase()); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iLastOpened != iLastOpened) + { + m_iLastOpened = iLastOpened; + if (m_bLoaded && database) + database->UpdateLastOpened(*this); + } +} + +bool CPVRChannelGroup::UpdateChannel(const std::pair<int, int>& storageId, + const std::string& strChannelName, + const std::string& strIconPath, + int iEPGSource, + int iChannelNumber, + bool bHidden, + bool bEPGEnabled, + bool bParentalLocked, + bool bUserSetIcon, + bool bUserSetHidden) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + /* get the real channel from the group */ + const std::shared_ptr<CPVRChannel> channel = GetByUniqueID(storageId)->Channel(); + if (!channel) + return false; + + channel->SetChannelName(strChannelName, true); + channel->SetHidden(bHidden, bUserSetHidden); + channel->SetLocked(bParentalLocked); + channel->SetIconPath(strIconPath, bUserSetIcon); + + if (iEPGSource == 0) + channel->SetEPGScraper("client"); + + //! @todo add other scrapers + channel->SetEPGEnabled(bEPGEnabled); + + /* set new values in the channel tag */ + if (bHidden) + { + // sort or previous changes will be overwritten + Sort(); + + RemoveFromGroup(channel); + } + else if (iChannelNumber > 0) + { + SetChannelNumber(channel, CPVRChannelNumber(iChannelNumber, 0)); + } + + return true; +} + +size_t CPVRChannelGroup::Size() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_members.size(); +} + +bool CPVRChannelGroup::HasChannels() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return !m_members.empty(); +} + +bool CPVRChannelGroup::CreateChannelEpgs(bool bForce /* = false */) +{ + /* used only by internal channel groups */ + return true; +} + +bool CPVRChannelGroup::SetHidden(bool bHidden) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_bHidden != bHidden) + { + m_bHidden = bHidden; + if (m_bLoaded) + m_bChanged = true; + } + + return m_bChanged; +} + +bool CPVRChannelGroup::IsHidden() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bHidden; +} + +int CPVRChannelGroup::GetPosition() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iPosition; +} + +void CPVRChannelGroup::SetPosition(int iPosition) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iPosition != iPosition) + { + m_iPosition = iPosition; + if (m_bLoaded) + m_bChanged = true; + } +} + +int CPVRChannelGroup::CleanupCachedImages() +{ + std::vector<std::string> urlsToCheck; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + std::transform( + m_members.cbegin(), m_members.cend(), std::back_inserter(urlsToCheck), + [](const auto& groupMember) { return groupMember.second->Channel()->ClientIconPath(); }); + } + + const std::string owner = + StringUtils::Format(CPVRChannel::IMAGE_OWNER_PATTERN, IsRadio() ? "radio" : "tv"); + return CPVRCachedImages::Cleanup({{owner, ""}}, urlsToCheck); +} diff --git a/xbmc/pvr/channels/PVRChannelGroup.h b/xbmc/pvr/channels/PVRChannelGroup.h new file mode 100644 index 0000000..66c45d6 --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelGroup.h @@ -0,0 +1,548 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/channels/PVRChannelGroupSettings.h" +#include "pvr/channels/PVRChannelNumber.h" +#include "pvr/channels/PVRChannelsPath.h" +#include "utils/EventStream.h" + +#include <map> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +struct PVR_CHANNEL_GROUP; + +namespace PVR +{ +#define PVR_GROUP_TYPE_DEFAULT 0 +#define PVR_GROUP_TYPE_INTERNAL 1 +#define PVR_GROUP_TYPE_USER_DEFINED 2 + +enum class PVREvent; + +class CPVRChannel; +class CPVRChannelGroupMember; +class CPVRClient; +class CPVREpgInfoTag; + +enum RenumberMode +{ + NORMAL = 0, + IGNORE_NUMBERING_FROM_ONE = 1 +}; + +using GroupMemberPair = + std::pair<std::shared_ptr<CPVRChannelGroupMember>, std::shared_ptr<CPVRChannelGroupMember>>; + +class CPVRChannelGroup : public IChannelGroupSettingsCallback +{ + friend class CPVRDatabase; + +public: + static const int INVALID_GROUP_ID = -1; + + /*! + * @brief Create a new channel group instance. + * @param path The channel group path. + * @param allChannelsGroup The channel group containing all TV or radio channels. + */ + CPVRChannelGroup(const CPVRChannelsPath& path, + const std::shared_ptr<CPVRChannelGroup>& allChannelsGroup); + + /*! + * @brief Create a new channel group instance from a channel group provided by an add-on. + * @param group The channel group provided by the add-on. + * @param allChannelsGroup The channel group containing all TV or radio channels. + */ + CPVRChannelGroup(const PVR_CHANNEL_GROUP& group, + const std::shared_ptr<CPVRChannelGroup>& allChannelsGroup); + + ~CPVRChannelGroup() override; + + bool operator==(const CPVRChannelGroup& right) const; + bool operator!=(const CPVRChannelGroup& right) const; + + /*! + * @brief Copy over data to the given PVR_CHANNEL_GROUP instance. + * @param group The group instance to fill. + */ + void FillAddonData(PVR_CHANNEL_GROUP& group) const; + + /*! + * @brief Query the events available for CEventStream + */ + CEventStream<PVREvent>& Events() { return m_events; } + + /*! + * @brief Load the channels from the database. + * @param channels All available channels. + * @param clients The PVR clients data should be loaded for. Leave empty for all clients. + * @return True when loaded successfully, false otherwise. + */ + virtual bool LoadFromDatabase( + const std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& channels, + const std::vector<std::shared_ptr<CPVRClient>>& clients); + + /*! + * @brief Clear all data. + */ + virtual void Unload(); + + /*! + * @return The amount of group members + */ + size_t Size() const; + + /*! + * @brief Update data with channel group members from the given clients, sync with local data. + * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients. + * @return True on success, false otherwise. + */ + virtual bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + /*! + * @brief Get the path of this group. + * @return the path. + */ + const CPVRChannelsPath& GetPath() const; + + /*! + * @brief Set the path of this group. + * @param the path. + */ + void SetPath(const CPVRChannelsPath& path); + + /*! + * @brief Change the channelnumber of a group. Used by CGUIDialogPVRChannelManager. + * Call SortByChannelNumber() and Renumber() after all changes are done. + * @param channel The channel to change the channel number for. + * @param channelNumber The new channel number. + */ + bool SetChannelNumber(const std::shared_ptr<CPVRChannel>& channel, + const CPVRChannelNumber& channelNumber); + + /*! + * @brief Remove a channel from this container. + * @param channel The channel to remove. + * @return True if the channel was found and removed, false otherwise. + */ + virtual bool RemoveFromGroup(const std::shared_ptr<CPVRChannel>& channel); + + /*! + * @brief Append a channel to this container. + * @param channel The channel to append. + * @return True if the channel was appended, false otherwise. + */ + virtual bool AppendToGroup(const std::shared_ptr<CPVRChannel>& channel); + + /*! + * @brief Change the name of this group. + * @param strGroupName The new group name. + */ + void SetGroupName(const std::string& strGroupName); + + /*! + * @brief Persist changed or new data. + * @return True if the channel was persisted, false otherwise. + */ + bool Persist(); + + /*! + * @brief Check whether a channel is in this container. + * @param channel The channel to find. + * @return True if the channel was found, false otherwise. + */ + virtual bool IsGroupMember(const std::shared_ptr<CPVRChannel>& channel) const; + + /*! + * @brief Check if this group is the internal group containing all channels. + * @return True if it's the internal group, false otherwise. + */ + bool IsInternalGroup() const { return m_iGroupType == PVR_GROUP_TYPE_INTERNAL; } + + /*! + * @brief True if this group holds radio channels, false if it holds TV channels. + * @return True if this group holds radio channels, false if it holds TV channels. + */ + bool IsRadio() const; + + /*! + * @brief The database ID of this group. + * @return The database ID of this group. + */ + int GroupID() const; + + /*! + * @brief Set the database ID of this group. + * @param iGroupId The new database ID. + */ + void SetGroupID(int iGroupId); + + /*! + * @brief Set the type of this group. + * @param the new type for this group. + */ + void SetGroupType(int iGroupType); + + /*! + * @brief Return the type of this group. + */ + int GroupType() const; + + /*! + * @return Time group has been watched last. + */ + time_t LastWatched() const; + + /*! + * @brief Last time group has been watched + * @param iLastWatched The new value. + */ + void SetLastWatched(time_t iLastWatched); + + /*! + * @return Time in milliseconds from epoch this group was last opened. + */ + uint64_t LastOpened() const; + + /*! + * @brief Set the time in milliseconds from epoch this group was last opened. + * @param iLastOpened The new value. + */ + void SetLastOpened(uint64_t iLastOpened); + + /*! + * @brief The name of this group. + * @return The name of this group. + */ + std::string GroupName() const; + + /*! @name Sort methods + */ + //@{ + + /*! + * @brief Sort the group. + */ + void Sort(); + + /*! + * @brief Sort the group and fix up channel numbers. + * @return True when numbering changed, false otherwise + */ + bool SortAndRenumber(); + + /*! + * @brief Remove invalid channels and updates the channel numbers. + * @param mode the numbering mode to use + * @return True if something changed, false otherwise. + */ + bool Renumber(RenumberMode mode = NORMAL); + + //@} + + // IChannelGroupSettingsCallback implementation + void UseBackendChannelOrderChanged() override; + void UseBackendChannelNumbersChanged() override; + void StartGroupChannelNumbersFromOneChanged() override; + + /*! + * @brief Get the channel group member that was played last. + * @param iCurrentChannel The channelid of the current channel that is playing, or -1 if none + * @return The requested channel group member or nullptr. + */ + std::shared_ptr<CPVRChannelGroupMember> GetLastPlayedChannelGroupMember( + int iCurrentChannel = -1) const; + + /*! + * @brief Get the last and previous to last played channel group members. + * @return The members. pair.first contains the last, pair.second the previous to last member. + */ + GroupMemberPair GetLastAndPreviousToLastPlayedChannelGroupMember() const; + + /*! + * @brief Get a channel group member given it's active channel number + * @param channelNumber The channel number. + * @return The channel group member or nullptr if it wasn't found. + */ + std::shared_ptr<CPVRChannelGroupMember> GetByChannelNumber( + const CPVRChannelNumber& channelNumber) const; + + /*! + * @brief Get the channel number in this group of the given channel. + * @param channel The channel to get the channel number for. + * @return The channel number in this group. + */ + CPVRChannelNumber GetChannelNumber(const std::shared_ptr<CPVRChannel>& channel) const; + + /*! + * @brief Get the client channel number in this group of the given channel. + * @param channel The channel to get the channel number for. + * @return The client channel number in this group. + */ + CPVRChannelNumber GetClientChannelNumber(const std::shared_ptr<CPVRChannel>& channel) const; + + /*! + * @brief Get the next channel group member in this group. + * @param groupMember The current channel group member. + * @return The channel group member or nullptr if it wasn't found. + */ + std::shared_ptr<CPVRChannelGroupMember> GetNextChannelGroupMember( + const std::shared_ptr<CPVRChannelGroupMember>& groupMember) const; + + /*! + * @brief Get the previous channel group member in this group. + * @param groupMember The current channel group member. + * @return The channel group member or nullptr if it wasn't found. + */ + std::shared_ptr<CPVRChannelGroupMember> GetPreviousChannelGroupMember( + const std::shared_ptr<CPVRChannelGroupMember>& groupMember) const; + + /*! + * @brief Get a channel given it's channel ID. + * @param iChannelID The channel ID. + * @return The channel or NULL if it wasn't found. + */ + std::shared_ptr<CPVRChannel> GetByChannelID(int iChannelID) const; + + enum class Include + { + ALL, + ONLY_HIDDEN, + ONLY_VISIBLE + }; + + /*! + * @brief Get the current members of this group + * @param eFilter A filter to apply. + * @return The group members + */ + std::vector<std::shared_ptr<CPVRChannelGroupMember>> GetMembers( + Include eFilter = Include::ALL) const; + + /*! + * @brief Get the list of active channel numbers in a group. + * @param channelNumbers The list to store the numbers in. + */ + void GetChannelNumbers(std::vector<std::string>& channelNumbers) const; + + /*! + * @brief The amount of hidden channels in this container. + * @return The amount of hidden channels in this container. + */ + virtual size_t GetNumHiddenChannels() const { return 0; } + + /*! + * @brief Does this container holds channels. + * @return True if there is at least one channel in this container, otherwise false. + */ + bool HasChannels() const; + + /*! + * @return True if there is at least one new channel in this group that hasn't been persisted, false otherwise. + */ + bool HasNewChannels() const; + + /*! + * @return True if anything changed in this group that hasn't been persisted, false otherwise. + */ + bool HasChanges() const; + + /*! + * @return True if the group was never persisted, false otherwise. + */ + bool IsNew() const; + + /*! + * @brief Create an EPG table for each channel. + * @brief bForce Create the tables, even if they already have been created before. + * @return True if all tables were created successfully, false otherwise. + */ + virtual bool CreateChannelEpgs(bool bForce = false); + + /*! + * @brief Update a channel group member with given data. + * @param storageId The storage id of the channel. + * @param strChannelName The channel name to set. + * @param strIconPath The icon path to set. + * @param iEPGSource The EPG id. + * @param iChannelNumber The channel number to set. + * @param bHidden Set/Remove hidden flag for the channel group member identified by storage id. + * @param bEPGEnabled Set/Remove EPG enabled flag for the channel group member identified by storage id. + * @param bParentalLocked Set/Remove parental locked flag for the channel group member identified by storage id. + * @param bUserSetIcon Set/Remove user set icon flag for the channel group member identified by storage id. + * @param bUserSetHidden Set/Remove user set hidden flag for the channel group member identified by storage id. + * @return True on success, false otherwise. + */ + bool UpdateChannel(const std::pair<int, int>& storageId, + const std::string& strChannelName, + const std::string& strIconPath, + int iEPGSource, + int iChannelNumber, + bool bHidden, + bool bEPGEnabled, + bool bParentalLocked, + bool bUserSetIcon, + bool bUserSetHidden); + + /*! + * @brief Get a channel given the channel number on the client. + * @param iUniqueChannelId The unique channel id on the client. + * @param iClientID The ID of the client. + * @return The channel or NULL if it wasn't found. + */ + std::shared_ptr<CPVRChannel> GetByUniqueID(int iUniqueChannelId, int iClientID) const; + + /*! + * @brief Get a channel group member given its storage id. + * @param id The storage id (a pair of client id and unique channel id). + * @return The channel group member or nullptr if it wasn't found. + */ + std::shared_ptr<CPVRChannelGroupMember> GetByUniqueID(const std::pair<int, int>& id) const; + + bool SetHidden(bool bHidden); + bool IsHidden() const; + + int GetPosition() const; + void SetPosition(int iPosition); + + /*! + * @brief Check, whether data for a given pvr client are currently valid. For instance, data + * can be invalid because the client's backend was offline when data was last queried. + * @param iClientId The id of the client. + * @return True, if data is currently valid, false otherwise. + */ + bool HasValidDataForClient(int iClientId) const; + + /*! + * @brief Check, whether data for given pvr clients are currently valid. For instance, data + * can be invalid because the client's backend was offline when data was last queried. + * @param clients The clients to check. Check all active clients if vector is empty. + * @return True, if data is currently valid, false otherwise. + */ + bool HasValidDataForClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) const; + + /*! + * @brief Update the channel numbers according to the all channels group and publish event. + * @return True, if a channel number was changed, false otherwise. + */ + bool UpdateChannelNumbersFromAllChannelsGroup(); + + /*! + * @brief Remove this group from database. + */ + void Delete(); + + /*! + * @brief Whether this group is deleted. + * @return True, if deleted, false otherwise. + */ + bool IsDeleted() const { return m_bDeleted; } + + /*! + * @brief Erase stale texture db entries and image files. + * @return number of cleaned up images. + */ + int CleanupCachedImages(); + +protected: + /*! + * @brief Remove deleted group members from this group. + * @param groupMembers The group members to use to update this list. + * @return The removed members . + */ + virtual std::vector<std::shared_ptr<CPVRChannelGroupMember>> RemoveDeletedGroupMembers( + const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers); + + /*! + * @brief Update the current channel group members with the given list. + * @param groupMembers The group members to use to update this list. + * @return True if everything went well, false otherwise. + */ + bool UpdateGroupEntries(const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers); + + /*! + * @brief Sort the current channel list by client channel number. + */ + void SortByClientChannelNumber(); + + /*! + * @brief Sort the current channel list by channel number. + */ + void SortByChannelNumber(); + + /*! + * @brief Update the priority for all members of all channel groups. + */ + bool UpdateClientPriorities(); + + std::shared_ptr<CPVRChannelGroupSettings> GetSettings() const; + + int m_iGroupType = PVR_GROUP_TYPE_DEFAULT; /*!< The type of this group */ + int m_iGroupId = INVALID_GROUP_ID; /*!< The ID of this group in the database */ + bool m_bLoaded = false; /*!< True if this container is loaded, false otherwise */ + bool m_bChanged = + false; /*!< true if anything changed in this group that hasn't been persisted, false otherwise */ + time_t m_iLastWatched = 0; /*!< last time group has been watched */ + uint64_t m_iLastOpened = 0; /*!< time in milliseconds from epoch this group was last opened */ + bool m_bHidden = false; /*!< true if this group is hidden, false otherwise */ + int m_iPosition = 0; /*!< the position of this group within the group list */ + std::vector<std::shared_ptr<CPVRChannelGroupMember>> + m_sortedMembers; /*!< members sorted by channel number */ + std::map<std::pair<int, int>, std::shared_ptr<CPVRChannelGroupMember>> + m_members; /*!< members with key clientid+uniqueid */ + mutable CCriticalSection m_critSection; + std::vector<int> m_failedClients; + CEventSource<PVREvent> m_events; + mutable std::shared_ptr<CPVRChannelGroupSettings> m_settings; + + // the settings singleton shared between all group instances + static CCriticalSection m_settingsSingletonCritSection; + static std::weak_ptr<CPVRChannelGroupSettings> m_settingsSingleton; + +private: + /*! + * @brief Load the channel group members stored in the database. + * @param clients The PVR clients to load data for. Leave empty for all clients. + * @return The amount of channel group members that were added. + */ + int LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + /*! + * @brief Delete channel group members from database. + * @param membersToDelete The channel group members. + */ + void DeleteGroupMembersFromDb( + const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& membersToDelete); + + /*! + * @brief Update this group's data with a channel group member provided by a client. + * @param groupMember The updated group member. + * @return True if group member data were changed, false otherwise. + */ + bool UpdateFromClient(const std::shared_ptr<CPVRChannelGroupMember>& groupMember); + + /*! + * @brief Add new channel group members to this group; update data. + * @param groupMembers The group members to use to update this list. + * @return True if group member data were changed, false otherwise. + */ + bool AddAndUpdateGroupMembers( + const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers); + + void OnSettingChanged(); + + std::shared_ptr<CPVRChannelGroup> m_allChannelsGroup; + CPVRChannelsPath m_path; + bool m_bDeleted = false; +}; +} // namespace PVR diff --git a/xbmc/pvr/channels/PVRChannelGroupInternal.cpp b/xbmc/pvr/channels/PVRChannelGroupInternal.cpp new file mode 100644 index 0000000..f79e02f --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelGroupInternal.cpp @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRChannelGroupInternal.h" + +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/epg/EpgContainer.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <iterator> +#include <mutex> +#include <string> +#include <utility> +#include <vector> + +using namespace PVR; + +CPVRChannelGroupInternal::CPVRChannelGroupInternal(bool bRadio) + : CPVRChannelGroup(CPVRChannelsPath(bRadio, g_localizeStrings.Get(19287)), nullptr), + m_iHiddenChannels(0) +{ + m_iGroupType = PVR_GROUP_TYPE_INTERNAL; +} + +CPVRChannelGroupInternal::CPVRChannelGroupInternal(const CPVRChannelsPath& path) + : CPVRChannelGroup(path, nullptr), m_iHiddenChannels(0) +{ + m_iGroupType = PVR_GROUP_TYPE_INTERNAL; +} + +CPVRChannelGroupInternal::~CPVRChannelGroupInternal() +{ + CServiceBroker::GetPVRManager().Events().Unsubscribe(this); +} + +bool CPVRChannelGroupInternal::LoadFromDatabase( + const std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& channels, + const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + if (CPVRChannelGroup::LoadFromDatabase(channels, clients)) + { + for (const auto& groupMember : m_members) + { + const std::shared_ptr<CPVRChannel> channel = groupMember.second->Channel(); + + // create the EPG for the channel + if (channel->CreateEPG()) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Created EPG for {} channel '{}'", IsRadio() ? "radio" : "TV", + channel->ChannelName()); + } + } + + UpdateChannelPaths(); + CServiceBroker::GetPVRManager().Events().Subscribe(this, &CPVRChannelGroupInternal::OnPVRManagerEvent); + return true; + } + + CLog::LogF(LOGERROR, "Failed to load channels"); + return false; +} + +void CPVRChannelGroupInternal::Unload() +{ + CServiceBroker::GetPVRManager().Events().Unsubscribe(this); + CPVRChannelGroup::Unload(); +} + +void CPVRChannelGroupInternal::CheckGroupName() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + /* check whether the group name is still correct, or channels will fail to load after the language setting changed */ + const std::string& strNewGroupName = g_localizeStrings.Get(19287); + if (GroupName() != strNewGroupName) + { + SetGroupName(strNewGroupName); + UpdateChannelPaths(); + } +} + +void CPVRChannelGroupInternal::UpdateChannelPaths() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_iHiddenChannels = 0; + for (auto& groupMemberPair : m_members) + { + if (groupMemberPair.second->Channel()->IsHidden()) + ++m_iHiddenChannels; + else + groupMemberPair.second->SetGroupName(GroupName()); + } +} + +bool CPVRChannelGroupInternal::UpdateFromClients( + const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + // get the channels from the given clients + std::vector<std::shared_ptr<CPVRChannel>> channels; + CServiceBroker::GetPVRManager().Clients()->GetChannels(clients, IsRadio(), channels, + m_failedClients); + + // create group members for the channels + std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers; + std::transform(channels.cbegin(), channels.cend(), std::back_inserter(groupMembers), + [this](const auto& channel) { + return std::make_shared<CPVRChannelGroupMember>(GroupID(), GroupName(), channel); + }); + + return UpdateGroupEntries(groupMembers); +} + +std::vector<std::shared_ptr<CPVRChannelGroupMember>> CPVRChannelGroupInternal:: + RemoveDeletedGroupMembers( + const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers) +{ + std::vector<std::shared_ptr<CPVRChannelGroupMember>> removedMembers = + CPVRChannelGroup::RemoveDeletedGroupMembers(groupMembers); + if (!removedMembers.empty()) + { + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (!database) + { + CLog::LogF(LOGERROR, "No TV database"); + } + else + { + std::vector<std::shared_ptr<CPVREpg>> epgsToRemove; + for (const auto& member : removedMembers) + { + const auto channel = member->Channel(); + const auto epg = channel->GetEPG(); + if (epg) + epgsToRemove.emplace_back(epg); + + // Note: We need to obtain a lock for every channel instance before we can lock + // the TV db. This order is important. Otherwise deadlocks may occur. + channel->Lock(); + } + + // Note: We must lock the db the whole time, otherwise races may occur. + database->Lock(); + + bool commitPending = false; + + for (const auto& member : removedMembers) + { + // since channel was not found in the internal group, it was deleted from the backend + + const auto channel = member->Channel(); + commitPending |= channel->QueueDelete(); + channel->Unlock(); + + size_t queryCount = database->GetDeleteQueriesCount(); + if (queryCount > CHANNEL_COMMIT_QUERY_COUNT_LIMIT) + database->CommitDeleteQueries(); + } + + if (commitPending) + database->CommitDeleteQueries(); + + database->Unlock(); + + // delete the EPG data for the removed channels + CServiceBroker::GetPVRManager().EpgContainer().QueueDeleteEpgs(epgsToRemove); + } + } + return removedMembers; +} + +bool CPVRChannelGroupInternal::AppendToGroup(const std::shared_ptr<CPVRChannel>& channel) +{ + if (IsGroupMember(channel)) + return false; + + const std::shared_ptr<CPVRChannelGroupMember> groupMember = GetByUniqueID(channel->StorageId()); + if (!groupMember) + return false; + + channel->SetHidden(false, true); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iHiddenChannels > 0) + m_iHiddenChannels--; + + const unsigned int iChannelNumber = m_members.size() - m_iHiddenChannels; + groupMember->SetChannelNumber(CPVRChannelNumber(iChannelNumber, 0)); + + SortAndRenumber(); + return true; +} + +bool CPVRChannelGroupInternal::RemoveFromGroup(const std::shared_ptr<CPVRChannel>& channel) +{ + if (!IsGroupMember(channel)) + return false; + + channel->SetHidden(true, true); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + ++m_iHiddenChannels; + + SortAndRenumber(); + return true; +} + +bool CPVRChannelGroupInternal::IsGroupMember(const std::shared_ptr<CPVRChannel>& channel) const +{ + return !channel->IsHidden(); +} + +bool CPVRChannelGroupInternal::CreateChannelEpgs(bool bForce /* = false */) +{ + if (!CServiceBroker::GetPVRManager().EpgContainer().IsStarted()) + return false; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + for (auto& groupMemberPair : m_members) + groupMemberPair.second->Channel()->CreateEPG(); + } + + return Persist(); +} + +void CPVRChannelGroupInternal::OnPVRManagerEvent(const PVR::PVREvent& event) +{ + if (event == PVREvent::ManagerStarted) + CServiceBroker::GetPVRManager().TriggerEpgsCreate(); +} diff --git a/xbmc/pvr/channels/PVRChannelGroupInternal.h b/xbmc/pvr/channels/PVRChannelGroupInternal.h new file mode 100644 index 0000000..5f8a06d --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelGroupInternal.h @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/channels/PVRChannelGroup.h" + +#include <memory> +#include <vector> + +namespace PVR +{ + enum class PVREvent; + + class CPVRChannel; + class CPVRChannelNumber; + + class CPVRChannelGroupInternal : public CPVRChannelGroup + { + public: + CPVRChannelGroupInternal() = delete; + + /*! + * @brief Create a new internal channel group. + * @param bRadio True if this group holds radio channels. + */ + explicit CPVRChannelGroupInternal(bool bRadio); + + /*! + * @brief Create a new internal channel group. + * @param path The path for the new group. + */ + explicit CPVRChannelGroupInternal(const CPVRChannelsPath& path); + + ~CPVRChannelGroupInternal() override; + + /** + * @brief The amount of channels in this container. + * @return The amount of channels in this container. + */ + size_t GetNumHiddenChannels() const override { return m_iHiddenChannels; } + + /*! + * @see CPVRChannelGroup::IsGroupMember + */ + bool IsGroupMember(const std::shared_ptr<CPVRChannel>& channel) const override; + + /*! + * @see CPVRChannelGroup::AppendToGroup + */ + bool AppendToGroup(const std::shared_ptr<CPVRChannel>& channel) override; + + /*! + * @see CPVRChannelGroup::RemoveFromGroup + */ + bool RemoveFromGroup(const std::shared_ptr<CPVRChannel>& channel) override; + + /*! + * @brief Check whether the group name is still correct after the language setting changed. + */ + void CheckGroupName(); + + /*! + * @brief Create an EPG table for each channel. + * @brief bForce Create the tables, even if they already have been created before. + * @return True if all tables were created successfully, false otherwise. + */ + bool CreateChannelEpgs(bool bForce = false) override; + + protected: + /*! + * @brief Remove deleted group members from this group. Delete stale channels. + * @param groupMembers The group members to use to update this list. + * @return The removed members . + */ + std::vector<std::shared_ptr<CPVRChannelGroupMember>> RemoveDeletedGroupMembers( + const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers) override; + + /*! + * @brief Update data with 'all channels' group members from the given clients, sync with local data. + * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients. + * @return True on success, false otherwise. + */ + bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) override; + + /*! + * @brief Load the channels from the database. + * @param channels All available channels. + * @param clients The PVR clients data should be loaded for. Leave empty for all clients. + * @return True when loaded successfully, false otherwise. + */ + bool LoadFromDatabase( + const std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& channels, + const std::vector<std::shared_ptr<CPVRClient>>& clients) override; + + /*! + * @brief Clear all data. + */ + void Unload() override; + + /*! + * @brief Update the vfs paths of all channels. + */ + void UpdateChannelPaths(); + + size_t m_iHiddenChannels; /*!< the amount of hidden channels in this container */ + + private: + void OnPVRManagerEvent(const PVREvent& event); + }; +} diff --git a/xbmc/pvr/channels/PVRChannelGroupMember.cpp b/xbmc/pvr/channels/PVRChannelGroupMember.cpp new file mode 100644 index 0000000..27592b5 --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelGroupMember.cpp @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2012-2021 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRChannelGroupMember.h" + +#include "ServiceBroker.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelsPath.h" +#include "utils/DatabaseUtils.h" +#include "utils/SortUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +using namespace PVR; + +CPVRChannelGroupMember::CPVRChannelGroupMember(const std::string& groupName, + int order, + const std::shared_ptr<CPVRChannel>& channel) + : m_clientChannelNumber(channel->ClientChannelNumber()), m_iOrder(order) +{ + SetChannel(channel); + SetGroupName(groupName); +} + +CPVRChannelGroupMember::CPVRChannelGroupMember(int iGroupID, + const std::string& groupName, + const std::shared_ptr<CPVRChannel>& channel) + : m_iGroupID(iGroupID), + m_clientChannelNumber(channel->ClientChannelNumber()), + m_iOrder(channel->ClientOrder()) +{ + SetChannel(channel); + SetGroupName(groupName); +} + +void CPVRChannelGroupMember::Serialize(CVariant& value) const +{ + value["channelnumber"] = m_channelNumber.GetChannelNumber(); + value["subchannelnumber"] = m_channelNumber.GetSubChannelNumber(); +} + +void CPVRChannelGroupMember::SetChannel(const std::shared_ptr<CPVRChannel>& channel) +{ + // note: no need to set m_bChanged, as these values are not persisted in the db + m_channel = channel; + m_iClientID = channel->ClientID(); + m_iChannelUID = channel->UniqueID(); + m_iChannelDatabaseID = channel->ChannelID(); + m_bIsRadio = channel->IsRadio(); +} + +void CPVRChannelGroupMember::ToSortable(SortItem& sortable, Field field) const +{ + if (field == FieldChannelNumber) + { + sortable[FieldChannelNumber] = m_channelNumber.SortableChannelNumber(); + } + else if (field == FieldClientChannelOrder) + { + if (m_iOrder) + sortable[FieldClientChannelOrder] = m_iOrder; + else + sortable[FieldClientChannelOrder] = m_clientChannelNumber.SortableChannelNumber(); + } +} + +void CPVRChannelGroupMember::SetGroupID(int iGroupID) +{ + if (m_iGroupID != iGroupID) + { + m_iGroupID = iGroupID; + m_bNeedsSave = true; + } +} + +void CPVRChannelGroupMember::SetGroupName(const std::string& groupName) +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientID); + if (client) + m_path = + CPVRChannelsPath(m_bIsRadio, groupName, client->ID(), client->InstanceId(), m_iChannelUID); + else + CLog::LogF(LOGERROR, "Unable to obtain instance for client id: {}", m_iClientID); +} + +void CPVRChannelGroupMember::SetChannelNumber(const CPVRChannelNumber& channelNumber) +{ + if (m_channelNumber != channelNumber) + { + m_channelNumber = channelNumber; + m_bNeedsSave = true; + } +} + +void CPVRChannelGroupMember::SetClientChannelNumber(const CPVRChannelNumber& clientChannelNumber) +{ + if (m_clientChannelNumber != clientChannelNumber) + { + m_clientChannelNumber = clientChannelNumber; + m_bNeedsSave = true; + } +} + +void CPVRChannelGroupMember::SetClientPriority(int iClientPriority) +{ + if (m_iClientPriority != iClientPriority) + { + m_iClientPriority = iClientPriority; + // Note: do not set m_bNeedsSave here as priority is not stored in database + } +} + +void CPVRChannelGroupMember::SetOrder(int iOrder) +{ + if (m_iOrder != iOrder) + { + m_iOrder = iOrder; + m_bNeedsSave = true; + } +} + +bool CPVRChannelGroupMember::QueueDelete() +{ + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (!database) + return false; + + return database->QueueDeleteQuery(*this); +} diff --git a/xbmc/pvr/channels/PVRChannelGroupMember.h b/xbmc/pvr/channels/PVRChannelGroupMember.h new file mode 100644 index 0000000..cb3ac83 --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelGroupMember.h @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2012-2021 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/channels/PVRChannelNumber.h" +#include "utils/ISerializable.h" +#include "utils/ISortable.h" + +#include <memory> +#include <string> + +namespace PVR +{ + +class CPVRChannel; + +class CPVRChannelGroupMember : public ISerializable, public ISortable +{ + friend class CPVRDatabase; + +public: + CPVRChannelGroupMember() : m_bNeedsSave(false) {} + + CPVRChannelGroupMember(const std::string& groupName, + int order, + const std::shared_ptr<CPVRChannel>& channel); + + CPVRChannelGroupMember(int iGroupID, + const std::string& groupName, + const std::shared_ptr<CPVRChannel>& channel); + + virtual ~CPVRChannelGroupMember() = default; + + // ISerializable implementation + void Serialize(CVariant& value) const override; + + // ISortable implementation + void ToSortable(SortItem& sortable, Field field) const override; + + std::shared_ptr<CPVRChannel> Channel() const { return m_channel; } + void SetChannel(const std::shared_ptr<CPVRChannel>& channel); + + int GroupID() const { return m_iGroupID; } + void SetGroupID(int iGroupID); + + const std::string& Path() const { return m_path; } + void SetGroupName(const std::string& groupName); + + const CPVRChannelNumber& ChannelNumber() const { return m_channelNumber; } + void SetChannelNumber(const CPVRChannelNumber& channelNumber); + + const CPVRChannelNumber& ClientChannelNumber() const { return m_clientChannelNumber; } + void SetClientChannelNumber(const CPVRChannelNumber& clientChannelNumber); + + int ClientPriority() const { return m_iClientPriority; } + void SetClientPriority(int iClientPriority); + + int Order() const { return m_iOrder; } + void SetOrder(int iOrder); + + bool NeedsSave() const { return m_bNeedsSave; } + void SetSaved() { m_bNeedsSave = false; } + + int ClientID() const { return m_iClientID; } + + int ChannelUID() const { return m_iChannelUID; } + + int ChannelDatabaseID() const { return m_iChannelDatabaseID; } + + bool IsRadio() const { return m_bIsRadio; } + + /*! + * @brief Delete this group member from the database. + * @return True if it was deleted successfully, false otherwise. + */ + bool QueueDelete(); + +private: + int m_iGroupID = -1; + int m_iClientID = -1; + int m_iChannelUID = -1; + int m_iChannelDatabaseID = -1; + bool m_bIsRadio = false; + std::shared_ptr<CPVRChannel> m_channel; + std::string m_path; + CPVRChannelNumber m_channelNumber; // the channel number this channel has in the group + CPVRChannelNumber + m_clientChannelNumber; // the client channel number this channel has in the group + int m_iClientPriority = 0; + int m_iOrder = 0; // The value denoting the order of this member in the group + + bool m_bNeedsSave = true; +}; + +} // namespace PVR diff --git a/xbmc/pvr/channels/PVRChannelGroupSettings.cpp b/xbmc/pvr/channels/PVRChannelGroupSettings.cpp new file mode 100644 index 0000000..3cbc0bd --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelGroupSettings.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2012-2021 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRChannelGroupSettings.h" + +#include "ServiceBroker.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClients.h" +#include "settings/Settings.h" +#include "settings/lib/Setting.h" + +using namespace PVR; + +CPVRChannelGroupSettings::CPVRChannelGroupSettings() + : m_settings({CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS, + CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER, + CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS, + CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS, + CSettings::SETTING_PVRMANAGER_STARTGROUPCHANNELNUMBERSFROMONE}) +{ + UpdateSyncChannelGroups(); + UpdateUseBackendChannelOrder(); + UpdateUseBackendChannelNumbers(); + UpdateStartGroupChannelNumbersFromOne(); + + m_settings.RegisterCallback(this); +} + +CPVRChannelGroupSettings::~CPVRChannelGroupSettings() +{ + m_settings.UnregisterCallback(this); +} + +void CPVRChannelGroupSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting) +{ + if (!setting) + return; + + const std::string& settingId = setting->GetId(); + if (settingId == CSettings::CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS) + { + if (SyncChannelGroups() != UpdateSyncChannelGroups()) + { + for (const auto& callback : m_callbacks) + callback->SyncChannelGroupsChanged(); + } + } + else if (settingId == CSettings::CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER) + { + if (UseBackendChannelOrder() != UpdateUseBackendChannelOrder()) + { + for (const auto& callback : m_callbacks) + callback->UseBackendChannelOrderChanged(); + } + } + else if (settingId == CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS || + settingId == CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS) + { + if (UseBackendChannelNumbers() != UpdateUseBackendChannelNumbers()) + { + for (const auto& callback : m_callbacks) + callback->UseBackendChannelNumbersChanged(); + } + } + else if (settingId == CSettings::SETTING_PVRMANAGER_STARTGROUPCHANNELNUMBERSFROMONE) + { + if (StartGroupChannelNumbersFromOne() != UpdateStartGroupChannelNumbersFromOne()) + { + for (const auto& callback : m_callbacks) + callback->StartGroupChannelNumbersFromOneChanged(); + } + } +} + +void CPVRChannelGroupSettings::RegisterCallback(IChannelGroupSettingsCallback* callback) +{ + m_callbacks.insert(callback); +} + +void CPVRChannelGroupSettings::UnregisterCallback(IChannelGroupSettingsCallback* callback) +{ + m_callbacks.erase(callback); +} + +bool CPVRChannelGroupSettings::UpdateSyncChannelGroups() +{ + m_bSyncChannelGroups = m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS); + return m_bSyncChannelGroups; +} + +bool CPVRChannelGroupSettings::UpdateUseBackendChannelOrder() +{ + m_bUseBackendChannelOrder = + m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER); + return m_bUseBackendChannelOrder; +} + +bool CPVRChannelGroupSettings::UpdateUseBackendChannelNumbers() +{ + const int enabledClientAmount = CServiceBroker::GetPVRManager().Clients()->EnabledClientAmount(); + m_bUseBackendChannelNumbers = + m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS) && + (enabledClientAmount == 1 || + (m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS) && + enabledClientAmount > 1)); + return m_bUseBackendChannelNumbers; +} + +bool CPVRChannelGroupSettings::UpdateStartGroupChannelNumbersFromOne() +{ + m_bStartGroupChannelNumbersFromOne = + m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_STARTGROUPCHANNELNUMBERSFROMONE) && + !UseBackendChannelNumbers(); + return m_bStartGroupChannelNumbersFromOne; +} diff --git a/xbmc/pvr/channels/PVRChannelGroupSettings.h b/xbmc/pvr/channels/PVRChannelGroupSettings.h new file mode 100644 index 0000000..abf842a --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelGroupSettings.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2012-2021 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/settings/PVRSettings.h" +#include "settings/lib/ISettingCallback.h" + +#include <memory> +#include <set> + +namespace PVR +{ + +class IChannelGroupSettingsCallback +{ +public: + virtual ~IChannelGroupSettingsCallback() = default; + + virtual void SyncChannelGroupsChanged() {} + virtual void UseBackendChannelOrderChanged() {} + virtual void UseBackendChannelNumbersChanged() {} + virtual void StartGroupChannelNumbersFromOneChanged() {} +}; + +class CPVRChannelGroupSettings : public ISettingCallback +{ +public: + CPVRChannelGroupSettings(); + virtual ~CPVRChannelGroupSettings(); + + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + + void RegisterCallback(IChannelGroupSettingsCallback* callback); + void UnregisterCallback(IChannelGroupSettingsCallback* callback); + + bool SyncChannelGroups() const { return m_bSyncChannelGroups; } + bool UseBackendChannelOrder() const { return m_bUseBackendChannelOrder; } + bool UseBackendChannelNumbers() const { return m_bUseBackendChannelNumbers; } + bool StartGroupChannelNumbersFromOne() const { return m_bStartGroupChannelNumbersFromOne; } + +private: + bool UpdateSyncChannelGroups(); + bool UpdateUseBackendChannelOrder(); + bool UpdateUseBackendChannelNumbers(); + bool UpdateStartGroupChannelNumbersFromOne(); + + bool m_bSyncChannelGroups = false; + bool m_bUseBackendChannelOrder = false; + bool m_bUseBackendChannelNumbers = false; + bool m_bStartGroupChannelNumbersFromOne = false; + + CPVRSettings m_settings; + std::set<IChannelGroupSettingsCallback*> m_callbacks; +}; + +} // namespace PVR diff --git a/xbmc/pvr/channels/PVRChannelGroups.cpp b/xbmc/pvr/channels/PVRChannelGroups.cpp new file mode 100644 index 0000000..77cc2b5 --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelGroups.cpp @@ -0,0 +1,621 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRChannelGroups.h" + +#include "ServiceBroker.h" +#include "pvr/PVRCachedImages.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClientUID.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupInternal.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelsPath.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <algorithm> +#include <chrono> +#include <iterator> +#include <memory> +#include <mutex> +#include <numeric> +#include <string> +#include <vector> + +using namespace PVR; + +CPVRChannelGroups::CPVRChannelGroups(bool bRadio) : + m_bRadio(bRadio) +{ +} + +CPVRChannelGroups::~CPVRChannelGroups() +{ + Unload(); +} + +void CPVRChannelGroups::Unload() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& group : m_groups) + group->Unload(); + + m_groups.clear(); + m_failedClientsForChannelGroups.clear(); +} + +bool CPVRChannelGroups::Update(const std::shared_ptr<CPVRChannelGroup>& group, + bool bUpdateFromClient /* = false */) +{ + if (group->GroupName().empty() && group->GroupID() <= 0) + return true; + + std::shared_ptr<CPVRChannelGroup> updateGroup; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + // There can be only one internal group! Make sure we never push a new one! + if (group->IsInternalGroup()) + updateGroup = GetGroupAll(); + + // try to find the group by id + if (!updateGroup && group->GroupID() > 0) + updateGroup = GetById(group->GroupID()); + + // try to find the group by name if we didn't find it yet + if (!updateGroup) + updateGroup = GetByName(group->GroupName()); + + if (updateGroup) + { + updateGroup->SetPath(group->GetPath()); + updateGroup->SetGroupID(group->GroupID()); + updateGroup->SetGroupType(group->GroupType()); + updateGroup->SetPosition(group->GetPosition()); + + // don't override properties we only store locally in our PVR database + if (!bUpdateFromClient) + { + updateGroup->SetLastWatched(group->LastWatched()); + updateGroup->SetHidden(group->IsHidden()); + updateGroup->SetLastOpened(group->LastOpened()); + } + } + else + { + updateGroup = group; + m_groups.emplace_back(updateGroup); + } + } + + // sort groups + SortGroups(); + + // persist changes + if (bUpdateFromClient) + return updateGroup->Persist(); + + return true; +} + +void CPVRChannelGroups::SortGroups() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + // check if one of the group holds a valid sort position + const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), + [](const auto& group) { return (group->GetPosition() > 0); }); + + // sort by position if we found a valid sort position + if (it != m_groups.cend()) + { + std::sort(m_groups.begin(), m_groups.end(), [](const auto& group1, const auto& group2) { + return group1->GetPosition() < group2->GetPosition(); + }); + } +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroups::GetChannelGroupMemberByPath( + const CPVRChannelsPath& path) const +{ + if (path.IsChannel()) + { + const std::shared_ptr<CPVRChannelGroup> group = GetByName(path.GetGroupName()); + if (group) + return group->GetByUniqueID( + {CPVRClientUID(path.GetAddonID(), path.GetInstanceID()).GetUID(), path.GetChannelUID()}); + } + + return {}; +} + +std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetById(int iGroupId) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), [iGroupId](const auto& group) { + return group->GroupID() == iGroupId; + }); + return (it != m_groups.cend()) ? (*it) : std::shared_ptr<CPVRChannelGroup>(); +} + +std::vector<std::shared_ptr<CPVRChannelGroup>> CPVRChannelGroups::GetGroupsByChannel(const std::shared_ptr<CPVRChannel>& channel, bool bExcludeHidden /* = false */) const +{ + std::vector<std::shared_ptr<CPVRChannelGroup>> groups; + + std::unique_lock<CCriticalSection> lock(m_critSection); + std::copy_if(m_groups.cbegin(), m_groups.cend(), std::back_inserter(groups), + [bExcludeHidden, &channel](const auto& group) { + return (!bExcludeHidden || !group->IsHidden()) && group->IsGroupMember(channel); + }); + return groups; +} + +std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetGroupByPath(const std::string& strInPath) const +{ + const CPVRChannelsPath path(strInPath); + if (path.IsChannelGroup()) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), + [&path](const auto& group) { return group->GetPath() == path; }); + if (it != m_groups.cend()) + return (*it); + } + return {}; +} + +std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetByName(const std::string& strName) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), [&strName](const auto& group) { + return group->GroupName() == strName; + }); + return (it != m_groups.cend()) ? (*it) : std::shared_ptr<CPVRChannelGroup>(); +} + +bool CPVRChannelGroups::HasValidDataForClients( + const std::vector<std::shared_ptr<CPVRClient>>& clients) const +{ + return m_failedClientsForChannelGroups.empty() || + std::none_of(clients.cbegin(), clients.cend(), + [this](const std::shared_ptr<CPVRClient>& client) { + return std::find(m_failedClientsForChannelGroups.cbegin(), + m_failedClientsForChannelGroups.cend(), + client->GetID()) != m_failedClientsForChannelGroups.cend(); + }); +} + +bool CPVRChannelGroups::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients, + bool bChannelsOnly /* = false */) +{ + bool bSyncWithBackends = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS); + bool bUpdateAllGroups = !bChannelsOnly && bSyncWithBackends; + bool bReturn = true; + + // sync groups + const int iSize = m_groups.size(); + if (bUpdateAllGroups) + { + // get channel groups from the clients + CServiceBroker::GetPVRManager().Clients()->GetChannelGroups(clients, this, + m_failedClientsForChannelGroups); + CLog::LogFC(LOGDEBUG, LOGPVR, "{} new user defined {} channel groups fetched from clients", + (m_groups.size() - iSize), m_bRadio ? "radio" : "TV"); + } + else if (!bSyncWithBackends) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "'sync channelgroups' is disabled; skipping groups from clients"); + } + + // sync channels in groups + std::vector<std::shared_ptr<CPVRChannelGroup>> groups; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + groups = m_groups; + } + + std::vector<std::shared_ptr<CPVRChannelGroup>> emptyGroups; + + for (const auto& group : groups) + { + if (bUpdateAllGroups || group->IsInternalGroup()) + { + const int iMemberCount = group->Size(); + if (!group->UpdateFromClients(clients)) + { + CLog::LogFC(LOGERROR, LOGPVR, "Failed to update channel group '{}'", group->GroupName()); + bReturn = false; + } + + const int iChangedMembersCount = static_cast<int>(group->Size()) - iMemberCount; + if (iChangedMembersCount > 0) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "{} channel group members added to group '{}'", + iChangedMembersCount, group->GroupName()); + } + else if (iChangedMembersCount < 0) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "{} channel group members removed from group '{}'", + -iChangedMembersCount, group->GroupName()); + } + else + { + // could still be changed if same amount of members was removed as was added, but too + // complicated to calculate just for debug logging + } + } + + // remove empty groups if sync with backend is enabled and we have valid data from all clients + if (bSyncWithBackends && group->Size() == 0 && !group->IsInternalGroup() && + HasValidDataForClients(clients) && group->HasValidDataForClients(clients)) + { + emptyGroups.emplace_back(group); + } + + if (bReturn && + group->IsInternalGroup() && + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bPVRChannelIconsAutoScan) + { + CServiceBroker::GetPVRManager().TriggerSearchMissingChannelIcons(group); + } + } + + for (const auto& group : emptyGroups) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting empty channel group '{}'", group->GroupName()); + DeleteGroup(group); + } + + if (bChannelsOnly) + { + // changes in the all channels group may require resorting/renumbering of other groups. + // if we updated all groups this already has been done while updating the single groups. + UpdateChannelNumbersFromAllChannelsGroup(); + } + + CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated); + + // persist changes + return PersistAll() && bReturn; +} + +bool CPVRChannelGroups::UpdateChannelNumbersFromAllChannelsGroup() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::accumulate( + m_groups.cbegin(), m_groups.cend(), false, [](bool changed, const auto& group) { + return group->UpdateChannelNumbersFromAllChannelsGroup() ? true : changed; + }); +} + +std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::CreateChannelGroup( + int iType, const CPVRChannelsPath& path) +{ + if (iType == PVR_GROUP_TYPE_INTERNAL) + return std::make_shared<CPVRChannelGroupInternal>(path); + else + return std::make_shared<CPVRChannelGroup>(path, GetGroupAll()); +} + +bool CPVRChannelGroups::LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase()); + if (!database) + return false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + + // Ensure we have an internal group. It is important that the internal group is created before + // loading contents from database and that it gets inserted in front of m_groups. Look at + // GetGroupAll() implementation to see why. + if (m_groups.empty()) + { + const auto internalGroup = std::make_shared<CPVRChannelGroupInternal>(m_bRadio); + m_groups.emplace_back(internalGroup); + } + + CLog::LogFC(LOGDEBUG, LOGPVR, "Loading all {} channel groups and members", + m_bRadio ? "radio" : "TV"); + + // load all channels from the database + std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>> channels; + database->Get(m_bRadio, clients, channels); + CLog::LogFC(LOGDEBUG, LOGPVR, "Fetched {} {} channels from the database", channels.size(), + m_bRadio ? "radio" : "TV"); + + // load all groups from the database + const int iLoaded = database->Get(*this); + CLog::LogFC(LOGDEBUG, LOGPVR, "Fetched {} {} groups from the database", iLoaded, + m_bRadio ? "radio" : "TV"); + + // load all group members from the database + for (const auto& group : m_groups) + { + if (!group->LoadFromDatabase(channels, clients)) + { + CLog::LogFC(LOGERROR, LOGPVR, + "Failed to load members of {} channel group '{}' from the database", + m_bRadio ? "radio" : "TV", group->GroupName()); + } + } + + // Hide empty groups + for (auto it = m_groups.begin(); it != m_groups.end();) + { + if ((*it)->Size() == 0 && !(*it)->IsInternalGroup()) + it = m_groups.erase(it); + else + ++it; + } + + return true; +} + +bool CPVRChannelGroups::PersistAll() +{ + CLog::LogFC(LOGDEBUG, LOGPVR, "Persisting all channel group changes"); + + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::accumulate( + m_groups.cbegin(), m_groups.cend(), true, + [](bool success, const auto& group) { return !group->Persist() ? false : success; }); +} + +std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetGroupAll() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_groups.empty()) + return m_groups.front(); + + return std::shared_ptr<CPVRChannelGroup>(); +} + +std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetLastGroup() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_groups.empty()) + return m_groups.back(); + + return std::shared_ptr<CPVRChannelGroup>(); +} + +GroupMemberPair CPVRChannelGroups::GetLastAndPreviousToLastPlayedChannelGroupMember() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_groups.empty()) + return {}; + + auto groups = m_groups; + lock.unlock(); + + std::sort(groups.begin(), groups.end(), + [](const auto& a, const auto& b) { return a->LastWatched() > b->LastWatched(); }); + + // Last is always 'first' of last played group. + const GroupMemberPair members = groups[0]->GetLastAndPreviousToLastPlayedChannelGroupMember(); + std::shared_ptr<CPVRChannelGroupMember> last = members.first; + + // Previous to last is either 'second' of first group or 'first' of second group. + std::shared_ptr<CPVRChannelGroupMember> previousToLast = members.second; + if (groups.size() > 1 && groups[0]->LastWatched() && groups[1]->LastWatched() && members.second && + members.second->Channel()->LastWatched()) + { + if (groups[1]->LastWatched() >= members.second->Channel()->LastWatched()) + { + const GroupMemberPair membersPreviousToLastPlayedGroup = + groups[1]->GetLastAndPreviousToLastPlayedChannelGroupMember(); + previousToLast = membersPreviousToLastPlayedGroup.first; + } + } + + return {last, previousToLast}; +} + +std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetLastOpenedGroup() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::accumulate( + m_groups.cbegin(), m_groups.cend(), std::shared_ptr<CPVRChannelGroup>{}, + [](const std::shared_ptr<CPVRChannelGroup>& last, + const std::shared_ptr<CPVRChannelGroup>& group) + { + return group->LastOpened() > 0 && (!last || group->LastOpened() > last->LastOpened()) + ? group + : last; + }); +} + +std::vector<std::shared_ptr<CPVRChannelGroup>> CPVRChannelGroups::GetMembers(bool bExcludeHidden /* = false */) const +{ + std::vector<std::shared_ptr<CPVRChannelGroup>> groups; + + std::unique_lock<CCriticalSection> lock(m_critSection); + std::copy_if( + m_groups.cbegin(), m_groups.cend(), std::back_inserter(groups), + [bExcludeHidden](const auto& group) { return (!bExcludeHidden || !group->IsHidden()); }); + return groups; +} + +std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetPreviousGroup(const CPVRChannelGroup& group) const +{ + { + bool bReturnNext = false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + for (std::vector<std::shared_ptr<CPVRChannelGroup>>::const_reverse_iterator it = m_groups.rbegin(); it != m_groups.rend(); ++it) + { + // return this entry + if (bReturnNext && !(*it)->IsHidden()) + return *it; + + // return the next entry + if ((*it)->GroupID() == group.GroupID()) + bReturnNext = true; + } + + // no match return last visible group + for (std::vector<std::shared_ptr<CPVRChannelGroup>>::const_reverse_iterator it = m_groups.rbegin(); it != m_groups.rend(); ++it) + { + if (!(*it)->IsHidden()) + return *it; + } + } + + // no match + return GetLastGroup(); +} + +std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetNextGroup(const CPVRChannelGroup& group) const +{ + { + bool bReturnNext = false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + for (std::vector<std::shared_ptr<CPVRChannelGroup>>::const_iterator it = m_groups.begin(); it != m_groups.end(); ++it) + { + // return this entry + if (bReturnNext && !(*it)->IsHidden()) + return *it; + + // return the next entry + if ((*it)->GroupID() == group.GroupID()) + bReturnNext = true; + } + + // no match return first visible group + for (std::vector<std::shared_ptr<CPVRChannelGroup>>::const_iterator it = m_groups.begin(); it != m_groups.end(); ++it) + { + if (!(*it)->IsHidden()) + return *it; + } + } + + // no match + return GetFirstGroup(); +} + +bool CPVRChannelGroups::AddGroup(const std::string& strName) +{ + bool bPersist(false); + std::shared_ptr<CPVRChannelGroup> group; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + // check if there's no group with the same name yet + group = GetByName(strName); + if (!group) + { + // create a new group + group.reset(new CPVRChannelGroup(CPVRChannelsPath(m_bRadio, strName), GetGroupAll())); + + m_groups.push_back(group); + bPersist = true; + + CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated); + } + } + + // persist in the db if a new group was added + return bPersist ? group->Persist() : true; +} + +bool CPVRChannelGroups::DeleteGroup(const std::shared_ptr<CPVRChannelGroup>& group) +{ + // don't delete internal groups + if (group->IsInternalGroup()) + { + CLog::LogF(LOGERROR, "Internal channel group cannot be deleted"); + return false; + } + + bool bFound(false); + + // delete the group in this container + { + std::unique_lock<CCriticalSection> lock(m_critSection); + for (auto it = m_groups.begin(); it != m_groups.end(); ++it) + { + if (*it == group || (group->GroupID() > 0 && (*it)->GroupID() == group->GroupID())) + { + m_groups.erase(it); + bFound = true; + break; + } + } + } + + if (bFound && group->GroupID() > 0) + { + // delete the group from the database + group->Delete(); + CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated); + } + return bFound; +} + +bool CPVRChannelGroups::HideGroup(const std::shared_ptr<CPVRChannelGroup>& group, bool bHide) +{ + bool bReturn = false; + + if (group) + { + if (group->SetHidden(bHide)) + { + // state changed + CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated); + } + bReturn = true; + } + return bReturn; +} + +bool CPVRChannelGroups::CreateChannelEpgs() +{ + bool bReturn(false); + + std::unique_lock<CCriticalSection> lock(m_critSection); + for (std::vector<std::shared_ptr<CPVRChannelGroup>>::iterator it = m_groups.begin(); it != m_groups.end(); ++it) + { + /* Only create EPGs for the internal groups */ + if ((*it)->IsInternalGroup()) + bReturn = (*it)->CreateChannelEpgs(); + } + return bReturn; +} + +int CPVRChannelGroups::CleanupCachedImages() +{ + int iCleanedImages = 0; + + // cleanup channels + iCleanedImages += GetGroupAll()->CleanupCachedImages(); + + // cleanup groups + std::vector<std::string> urlsToCheck; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + std::transform(m_groups.cbegin(), m_groups.cend(), std::back_inserter(urlsToCheck), + [](const auto& group) { return group->GetPath(); }); + } + + // kodi-generated thumbnail (see CPVRThumbLoader) + const std::string path = StringUtils::Format("pvr://channels/{}/", IsRadio() ? "radio" : "tv"); + iCleanedImages += CPVRCachedImages::Cleanup({{"pvr", path}}, urlsToCheck, true); + + return iCleanedImages; +} diff --git a/xbmc/pvr/channels/PVRChannelGroups.h b/xbmc/pvr/channels/PVRChannelGroups.h new file mode 100644 index 0000000..709dcfb --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelGroups.h @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/channels/PVRChannelGroup.h" +#include "threads/CriticalSection.h" + +#include <memory> +#include <mutex> +#include <string> +#include <vector> + +namespace PVR +{ + class CPVRChannel; + class CPVRClient; + + /** A container class for channel groups */ + + class CPVRChannelGroups + { + public: + /*! + * @brief Create a new group container. + * @param bRadio True if this is a container for radio channels, false if it is for tv channels. + */ + explicit CPVRChannelGroups(bool bRadio); + virtual ~CPVRChannelGroups(); + + /*! + * @brief Remove all groups from this container. + */ + void Unload(); + + /*! + * @brief Load all channel groups and all channels from PVR database. + * @param clients The PVR clients data should be loaded for. Leave empty for all clients. + * @return True on success, false otherwise. + */ + bool LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + /*! + * @brief Create a channel group matching the given type. + * @param iType The type for the group. + * @param path The path of the group. + * @return The new group. + */ + std::shared_ptr<CPVRChannelGroup> CreateChannelGroup(int iType, const CPVRChannelsPath& path); + + /*! + * @return Amount of groups in this container + */ + size_t Size() const + { + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_groups.size(); + } + + /*! + * @brief Update a group or add it if it's not in here yet. + * @param group The group to update. + * @param bUpdateFromClient True to save the changes in the db. + * @return True if the group was added or update successfully, false otherwise. + */ + bool Update(const std::shared_ptr<CPVRChannelGroup>& group, bool bUpdateFromClient = false); + + /*! + * @brief Called by the add-on callback to add a new group + * @param group The group to add + * @return True when updated, false otherwise + */ + bool UpdateFromClient(const std::shared_ptr<CPVRChannelGroup>& group) + { + return Update(group, true); + } + + /*! + * @brief Get a channel group member given its path + * @param strPath The path to the channel group member + * @return The channel group member, or nullptr if not found + */ + std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMemberByPath( + const CPVRChannelsPath& path) const; + + /*! + * @brief Get a pointer to a channel group given its ID. + * @param iGroupId The ID of the group. + * @return The group or NULL if it wasn't found. + */ + std::shared_ptr<CPVRChannelGroup> GetById(int iGroupId) const; + + /*! + * @brief Get all groups the given channel is a member. + * @param channel The channel. + * @param bExcludeHidden Whenever to exclude hidden channel groups. + * @return A list of groups the channel is a member. + */ + std::vector<std::shared_ptr<CPVRChannelGroup>> GetGroupsByChannel(const std::shared_ptr<CPVRChannel>& channel, bool bExcludeHidden = false) const; + + /*! + * @brief Get a channel group given its path + * @param strPath The path to the channel group + * @return The channel group, or nullptr if not found + */ + std::shared_ptr<CPVRChannelGroup> GetGroupByPath(const std::string& strPath) const; + + /*! + * @brief Get a group given its name. + * @param strName The name. + * @return The group or NULL if it wasn't found. + */ + std::shared_ptr<CPVRChannelGroup> GetByName(const std::string& strName) const; + + /*! + * @brief Get the group that contains all channels. + * @return The group that contains all channels. + */ + std::shared_ptr<CPVRChannelGroup> GetGroupAll() const; + + /*! + * @return The first group in this container, which always is the group with all channels. + */ + std::shared_ptr<CPVRChannelGroup> GetFirstGroup() const { return GetGroupAll(); } + + /*! + * @return The last group in this container. + */ + std::shared_ptr<CPVRChannelGroup> GetLastGroup() const; + + /*! + * @return The last and previous to last played channel group members. pair.first contains the last, pair.second the previous to last member. + */ + GroupMemberPair GetLastAndPreviousToLastPlayedChannelGroupMember() const; + + /*! + * @return The last opened group. + */ + std::shared_ptr<CPVRChannelGroup> GetLastOpenedGroup() const; + + /*! + * @brief Get the list of groups. + * @param groups The list to store the results in. + * @param bExcludeHidden Whenever to exclude hidden channel groups. + * @return The amount of items that were added. + */ + std::vector<std::shared_ptr<CPVRChannelGroup>> GetMembers(bool bExcludeHidden = false) const; + + /*! + * @brief Get the previous group in this container. + * @param group The current group. + * @return The previous group or the group containing all channels if it wasn't found. + */ + std::shared_ptr<CPVRChannelGroup> GetPreviousGroup(const CPVRChannelGroup& group) const; + + /*! + * @brief Get the next group in this container. + * @param group The current group. + * @return The next group or the group containing all channels if it wasn't found. + */ + std::shared_ptr<CPVRChannelGroup> GetNextGroup(const CPVRChannelGroup& group) const; + + /*! + * @brief Add a group to this container. + * @param strName The name of the group. + * @return True if the group was added, false otherwise. + */ + bool AddGroup(const std::string& strName); + + /*! + * @brief Remove a group from this container and delete it from the database. + * @param group The group to delete. + * @return True if it was deleted successfully, false if not. + */ + bool DeleteGroup(const std::shared_ptr<CPVRChannelGroup>& group); + + /*! + * @brief Hide/unhide a group in this container. + * @param group The group to hide/unhide. + * @param bHide True to hide the group, false to unhide it. + * @return True on success, false otherwise. + */ + bool HideGroup(const std::shared_ptr<CPVRChannelGroup>& group, bool bHide); + + /*! + * @brief Create EPG tags for all channels of the internal group. + * @return True if EPG tags where created successfully, false if not. + */ + bool CreateChannelEpgs(); + + /*! + * @brief Persist all changes in channel groups. + * @return True if everything was persisted, false otherwise. + */ + bool PersistAll(); + + /*! + * @return True when this container contains radio groups, false otherwise + */ + bool IsRadio() const { return m_bRadio; } + + /*! + * @brief Update data with groups and channels from the given clients, sync with local data. + * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients. + * @param bChannelsOnly Set to true to only update channels, not the groups themselves. + * @return True on success, false otherwise. + */ + bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients, + bool bChannelsOnly = false); + + /*! + * @brief Update the channel numbers across the channel groups from the all channels group + * @return True if any channel number was changed, false otherwise. + */ + bool UpdateChannelNumbersFromAllChannelsGroup(); + + /*! + * @brief Erase stale texture db entries and image files. + * @return number of cleaned up images. + */ + int CleanupCachedImages(); + + private: + void SortGroups(); + + /*! + * @brief Check, whether data for given pvr clients are currently valid. For instance, data + * can be invalid because the client's backend was offline when data was last queried. + * @param clients The clients to check. Check all active clients if vector is empty. + * @return True, if data is currently valid, false otherwise. + */ + bool HasValidDataForClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) const; + + bool m_bRadio; /*!< true if this is a container for radio channels, false if it is for tv channels */ + std::vector<std::shared_ptr<CPVRChannelGroup>> m_groups; /*!< the groups in this container */ + mutable CCriticalSection m_critSection; + std::vector<int> m_failedClientsForChannelGroups; + }; +} diff --git a/xbmc/pvr/channels/PVRChannelGroupsContainer.cpp b/xbmc/pvr/channels/PVRChannelGroupsContainer.cpp new file mode 100644 index 0000000..5f65cdd --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelGroupsContainer.cpp @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRChannelGroupsContainer.h" + +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/epg/EpgInfoTag.h" +#include "utils/log.h" + +#include <memory> +#include <mutex> + +using namespace PVR; + +CPVRChannelGroupsContainer::CPVRChannelGroupsContainer() : + m_groupsRadio(new CPVRChannelGroups(true)), + m_groupsTV(new CPVRChannelGroups(false)) +{ +} + +CPVRChannelGroupsContainer::~CPVRChannelGroupsContainer() +{ + Unload(); + delete m_groupsRadio; + delete m_groupsTV; +} + +bool CPVRChannelGroupsContainer::Update(const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + return LoadFromDatabase(clients) && UpdateFromClients(clients); +} + +bool CPVRChannelGroupsContainer::LoadFromDatabase( + const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + return m_groupsTV->LoadFromDatabase(clients) && m_groupsRadio->LoadFromDatabase(clients); +} + +bool CPVRChannelGroupsContainer::UpdateFromClients( + const std::vector<std::shared_ptr<CPVRClient>>& clients, bool bChannelsOnly /* = false */) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_bIsUpdating) + return false; + m_bIsUpdating = true; + lock.unlock(); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Updating {}", bChannelsOnly ? "channels" : "channel groups"); + bool bReturn = m_groupsTV->UpdateFromClients(clients, bChannelsOnly) && + m_groupsRadio->UpdateFromClients(clients, bChannelsOnly); + + lock.lock(); + m_bIsUpdating = false; + lock.unlock(); + + return bReturn; +} + +void CPVRChannelGroupsContainer::Unload() +{ + m_groupsRadio->Unload(); + m_groupsTV->Unload(); +} + +CPVRChannelGroups* CPVRChannelGroupsContainer::Get(bool bRadio) const +{ + return bRadio ? m_groupsRadio : m_groupsTV; +} + +std::shared_ptr<CPVRChannelGroup> CPVRChannelGroupsContainer::GetGroupAll(bool bRadio) const +{ + return Get(bRadio)->GetGroupAll(); +} + +std::shared_ptr<CPVRChannelGroup> CPVRChannelGroupsContainer::GetByIdFromAll(int iGroupId) const +{ + std::shared_ptr<CPVRChannelGroup> group = m_groupsTV->GetById(iGroupId); + if (!group) + group = m_groupsRadio->GetById(iGroupId); + + return group; +} + +std::shared_ptr<CPVRChannel> CPVRChannelGroupsContainer::GetChannelById(int iChannelId) const +{ + std::shared_ptr<CPVRChannel> channel = m_groupsTV->GetGroupAll()->GetByChannelID(iChannelId); + if (!channel) + channel = m_groupsRadio->GetGroupAll()->GetByChannelID(iChannelId); + + return channel; +} + +std::shared_ptr<CPVRChannel> CPVRChannelGroupsContainer::GetChannelForEpgTag(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + if (!epgTag) + return {}; + + return Get(epgTag->IsRadio())->GetGroupAll()->GetByUniqueID(epgTag->UniqueChannelID(), epgTag->ClientID()); +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroupsContainer::GetChannelGroupMemberByPath( + const std::string& strPath) const +{ + const CPVRChannelsPath path(strPath); + if (path.IsValid()) + return Get(path.IsRadio())->GetChannelGroupMemberByPath(path); + + return {}; +} + +std::shared_ptr<CPVRChannel> CPVRChannelGroupsContainer::GetByPath(const std::string& strPath) const +{ + const std::shared_ptr<CPVRChannelGroupMember> groupMember = GetChannelGroupMemberByPath(strPath); + if (groupMember) + return groupMember->Channel(); + + return {}; +} + +std::shared_ptr<CPVRChannel> CPVRChannelGroupsContainer::GetByUniqueID(int iUniqueChannelId, int iClientID) const +{ + std::shared_ptr<CPVRChannel> channel; + std::shared_ptr<CPVRChannelGroup> channelgroup = GetGroupAllTV(); + if (channelgroup) + channel = channelgroup->GetByUniqueID(iUniqueChannelId, iClientID); + + if (!channelgroup || !channel) + channelgroup = GetGroupAllRadio(); + if (channelgroup) + channel = channelgroup->GetByUniqueID(iUniqueChannelId, iClientID); + + return channel; +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroupsContainer:: + GetLastPlayedChannelGroupMember() const +{ + std::shared_ptr<CPVRChannelGroupMember> channelTV = + m_groupsTV->GetGroupAll()->GetLastPlayedChannelGroupMember(); + std::shared_ptr<CPVRChannelGroupMember> channelRadio = + m_groupsRadio->GetGroupAll()->GetLastPlayedChannelGroupMember(); + + if (!channelTV || (channelRadio && + channelRadio->Channel()->LastWatched() > channelTV->Channel()->LastWatched())) + return channelRadio; + + return channelTV; +} + +bool CPVRChannelGroupsContainer::CreateChannelEpgs() +{ + bool bReturn = m_groupsTV->CreateChannelEpgs(); + bReturn &= m_groupsRadio->CreateChannelEpgs(); + return bReturn; +} + +int CPVRChannelGroupsContainer::CleanupCachedImages() +{ + return m_groupsTV->CleanupCachedImages() + m_groupsRadio->CleanupCachedImages(); +} diff --git a/xbmc/pvr/channels/PVRChannelGroupsContainer.h b/xbmc/pvr/channels/PVRChannelGroupsContainer.h new file mode 100644 index 0000000..d7b7f57 --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelGroupsContainer.h @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" + +#include <memory> +#include <vector> + +namespace PVR +{ + class CPVRChannel; + class CPVRChannelGroup; + class CPVRChannelGroupMember; + class CPVRChannelGroups; + class CPVRClient; + class CPVREpgInfoTag; + + class CPVRChannelGroupsContainer + { + public: + /*! + * @brief Create a new container for all channel groups + */ + CPVRChannelGroupsContainer(); + + /*! + * @brief Destroy this container. + */ + virtual ~CPVRChannelGroupsContainer(); + + /*! + * @brief Update all channel groups and all channels from PVR database and from given clients. + * @param clients The PVR clients data should be loaded for. Leave empty for all clients. + * @return True on success, false otherwise. + */ + bool Update(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + /*! + * @brief Update data with groups and channels from the given clients, sync with local data. + * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients. + * @param bChannelsOnly Set to true to only update channels, not the groups themselves. + * @return True on success, false otherwise. + */ + bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients, + bool bChannelsOnly = false); + + /*! + * @brief Unload and destruct all channel groups and all channels in them. + */ + void Unload(); + + /*! + * @brief Get the TV channel groups. + * @return The TV channel groups. + */ + CPVRChannelGroups* GetTV() const { return Get(false); } + + /*! + * @brief Get the radio channel groups. + * @return The radio channel groups. + */ + CPVRChannelGroups* GetRadio() const { return Get(true); } + + /*! + * @brief Get the radio or TV channel groups. + * @param bRadio If true, get the radio channel groups. Get the TV channel groups otherwise. + * @return The requested groups. + */ + CPVRChannelGroups* Get(bool bRadio) const; + + /*! + * @brief Get the group containing all TV channels. + * @return The group containing all TV channels. + */ + std::shared_ptr<CPVRChannelGroup> GetGroupAllTV() const { return GetGroupAll(false); } + + /*! + * @brief Get the group containing all radio channels. + * @return The group containing all radio channels. + */ + std::shared_ptr<CPVRChannelGroup> GetGroupAllRadio() const { return GetGroupAll(true); } + + /*! + * @brief Get the group containing all TV or radio channels. + * @param bRadio If true, get the group containing all radio channels. Get the group containing all TV channels otherwise. + * @return The requested group. + */ + std::shared_ptr<CPVRChannelGroup> GetGroupAll(bool bRadio) const; + + /*! + * @brief Get a group given it's ID. + * @param iGroupId The ID of the group. + * @return The requested group or NULL if it wasn't found. + */ + std::shared_ptr<CPVRChannelGroup> GetByIdFromAll(int iGroupId) const; + + /*! + * @brief Get a channel given it's database ID. + * @param iChannelId The ID of the channel. + * @return The channel or NULL if it wasn't found. + */ + std::shared_ptr<CPVRChannel> GetChannelById(int iChannelId) const; + + /*! + * @brief Get the channel for the given epg tag. + * @param epgTag The epg tag. + * @return The channel. + */ + std::shared_ptr<CPVRChannel> GetChannelForEpgTag(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + + /*! + * @brief Get a channel given it's path. + * @param strPath The path. + * @return The channel or nullptr if it wasn't found. + */ + std::shared_ptr<CPVRChannel> GetByPath(const std::string& strPath) const; + + /*! + * @brief Get a channel group member given it's path. + * @param strPath The path. + * @return The channel group member or nullptr if it wasn't found. + */ + std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMemberByPath( + const std::string& strPath) const; + + /*! + * @brief Get a channel given it's channel ID from all containers. + * @param iUniqueChannelId The unique channel id on the client. + * @param iClientID The ID of the client. + * @return The channel or NULL if it wasn't found. + */ + std::shared_ptr<CPVRChannel> GetByUniqueID(int iUniqueChannelId, int iClientID) const; + + /*! + * @brief Get the channel group member that was played last. + * @return The requested channel group member or nullptr. + */ + std::shared_ptr<CPVRChannelGroupMember> GetLastPlayedChannelGroupMember() const; + + /*! + * @brief Create EPG tags for channels in all internal channel groups. + * @return True if EPG tags were created successfully. + */ + bool CreateChannelEpgs(); + + /*! + * @brief Erase stale texture db entries and image files. + * @return number of cleaned up images. + */ + int CleanupCachedImages(); + + private: + CPVRChannelGroupsContainer& operator=(const CPVRChannelGroupsContainer&) = delete; + CPVRChannelGroupsContainer(const CPVRChannelGroupsContainer&) = delete; + + /*! + * @brief Load all channel groups and all channels from PVR database. + * @param clients The PVR clients data should be loaded for. Leave empty for all clients. + * @return True on success, false otherwise. + */ + bool LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + CPVRChannelGroups* m_groupsRadio; /*!< all radio channel groups */ + CPVRChannelGroups* m_groupsTV; /*!< all TV channel groups */ + CCriticalSection m_critSection; + bool m_bIsUpdating = false; + }; +} diff --git a/xbmc/pvr/channels/PVRChannelNumber.cpp b/xbmc/pvr/channels/PVRChannelNumber.cpp new file mode 100644 index 0000000..403938e --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelNumber.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRChannelNumber.h" + +#include "utils/StringUtils.h" + +using namespace PVR; + +const char CPVRChannelNumber::SEPARATOR = '.'; + +std::string CPVRChannelNumber::FormattedChannelNumber() const +{ + return ToString(SEPARATOR); +} + +std::string CPVRChannelNumber::SortableChannelNumber() const +{ + // Note: The subchannel separator is a character that does not work for a + // SortItem (at least not on all platforms). See SortUtils::Sort for + // details. Only numbers, letters and the blank are safe to use. + return ToString(' '); +} + +std::string CPVRChannelNumber::ToString(char separator) const +{ + if (m_iSubChannelNumber == 0) + return std::to_string(m_iChannelNumber); + else + return StringUtils::Format("{}{}{}", m_iChannelNumber, separator, m_iSubChannelNumber); +} diff --git a/xbmc/pvr/channels/PVRChannelNumber.h b/xbmc/pvr/channels/PVRChannelNumber.h new file mode 100644 index 0000000..b6e2efc --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelNumber.h @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <string> + +namespace PVR +{ + class CPVRChannelNumber + { + public: + CPVRChannelNumber() = default; + + constexpr CPVRChannelNumber(unsigned int iChannelNumber, unsigned int iSubChannelNumber) + : m_iChannelNumber(iChannelNumber), m_iSubChannelNumber(iSubChannelNumber) {} + + constexpr bool operator ==(const CPVRChannelNumber& right) const + { + return (m_iChannelNumber == right.m_iChannelNumber && + m_iSubChannelNumber == right.m_iSubChannelNumber); + } + + constexpr bool operator !=(const CPVRChannelNumber& right) const + { + return !(*this == right); + } + + constexpr bool operator <(const CPVRChannelNumber& right) const + { + return m_iChannelNumber == right.m_iChannelNumber + ? m_iSubChannelNumber < right.m_iSubChannelNumber + : m_iChannelNumber < right.m_iChannelNumber; + } + + /*! + * @brief Check whether this channel number is valid (main channel number > 0). + * @return True if valid, false otherwise.. + */ + constexpr bool IsValid() const { return m_iChannelNumber > 0; } + + /*! + * @brief Set the primary channel number. + * @return The channel number. + */ + constexpr unsigned int GetChannelNumber() const + { + return m_iChannelNumber; + } + + /*! + * @brief Set the sub channel number. + * @return The sub channel number (ATSC). + */ + constexpr unsigned int GetSubChannelNumber() const + { + return m_iSubChannelNumber; + } + + /*! + * @brief The character used to separate channel and subchannel number. + */ + static const char SEPARATOR; // '.' + + /*! + * @brief Get a string representation for the channel number. + * @return The formatted string in the form <channel>SEPARATOR<subchannel>. + */ + std::string FormattedChannelNumber() const; + + /*! + * @brief Get a string representation for the channel number that can be used for SortItems. + * @return The sortable string in the form <channel> <subchannel>. + */ + std::string SortableChannelNumber() const; + + private: + std::string ToString(char separator) const; + + unsigned int m_iChannelNumber = 0; + unsigned int m_iSubChannelNumber = 0; + }; +} diff --git a/xbmc/pvr/channels/PVRChannelsPath.cpp b/xbmc/pvr/channels/PVRChannelsPath.cpp new file mode 100644 index 0000000..4ae2e28 --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelsPath.cpp @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRChannelsPath.h" + +#include "URL.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <string> +#include <vector> + +using namespace PVR; + +const std::string CPVRChannelsPath::PATH_TV_CHANNELS = "pvr://channels/tv/"; +const std::string CPVRChannelsPath::PATH_RADIO_CHANNELS = "pvr://channels/radio/"; + + +CPVRChannelsPath::CPVRChannelsPath(const std::string& strPath) +{ + std::string strVarPath = TrimSlashes(strPath); + const std::vector<std::string> segments = URIUtils::SplitPath(strVarPath); + + for (const std::string& segment : segments) + { + switch (m_kind) + { + case Kind::INVALID: + if (segment == "pvr://") + m_kind = Kind::PROTO; // pvr:// followed by something => go on + else if (segment == "pvr:" && segments.size() == 1) // just pvr:// => invalid + strVarPath = "pvr:/"; + break; + + case Kind::PROTO: + if (segment == "channels") + m_kind = Kind::EMPTY; // pvr://channels + else + m_kind = Kind::INVALID; + break; + + case Kind::EMPTY: + if (segment == "tv" || segment == "radio") + { + m_kind = Kind::ROOT; // pvr://channels/(tv|radio) + m_bRadio = (segment == "radio"); + } + else + { + CLog::LogF(LOGERROR, "Invalid channels path '{}' - channel root segment syntax error.", + strPath); + m_kind = Kind::INVALID; + } + break; + + case Kind::ROOT: + m_kind = Kind::GROUP; // pvr://channels/(tv|radio)/<groupname> + m_group = CURL::Decode(segment); + break; + + case Kind::GROUP: + { + std::vector<std::string> tokens = StringUtils::Split(segment, "_"); + if (tokens.size() == 2) + { + std::vector<std::string> instance = StringUtils::Split(tokens[0], "@"); + if (instance.size() == 2) + { + m_instanceID = std::atoi(instance[0].c_str()); + m_addonID = instance[1]; + } + else + { + m_instanceID = ADDON::ADDON_SINGLETON_INSTANCE_ID; + m_addonID = tokens[0]; + } + + tokens = StringUtils::Split(tokens[1], "."); + if (tokens.size() == 2 && tokens[1] == "pvr") + { + std::string channelUID = tokens[0]; + if (!channelUID.empty() && channelUID.find_first_not_of("0123456789") == std::string::npos) + m_iChannelUID = std::atoi(channelUID.c_str()); + } + } + + if (!m_addonID.empty() && m_iChannelUID >= 0) + { + m_kind = Kind:: + CHANNEL; // pvr://channels/(tv|radio)/<groupname>/<instanceid>@<addonid>_<channeluid>.pvr + } + else + { + CLog::LogF(LOGERROR, "Invalid channels path '{}' - channel segment syntax error.", + strPath); + m_kind = Kind::INVALID; + } + break; + } + + case Kind::CHANNEL: + CLog::LogF(LOGERROR, "Invalid channels path '{}' - too many path segments.", strPath); + m_kind = Kind::INVALID; // too many segments + break; + } + + if (m_kind == Kind::INVALID) + break; + } + + // append slash to all folders + if (m_kind < Kind::CHANNEL) + strVarPath.append("/"); + + m_path = strVarPath; +} + +CPVRChannelsPath::CPVRChannelsPath(bool bRadio, bool bHidden, const std::string& strGroupName) + : m_bRadio(bRadio) +{ + if (!bHidden && strGroupName.empty()) + m_kind = Kind::EMPTY; + else + m_kind = Kind::GROUP; + + m_group = bHidden ? ".hidden" : strGroupName; + m_path = + StringUtils::Format("pvr://channels/{}/{}", bRadio ? "radio" : "tv", CURL::Encode(m_group)); + + if (!m_group.empty()) + m_path.append("/"); +} + +CPVRChannelsPath::CPVRChannelsPath(bool bRadio, const std::string& strGroupName) + : m_bRadio(bRadio) +{ + if (strGroupName.empty()) + m_kind = Kind::EMPTY; + else + m_kind = Kind::GROUP; + + m_group = strGroupName; + m_path = + StringUtils::Format("pvr://channels/{}/{}", bRadio ? "radio" : "tv", CURL::Encode(m_group)); + + if (!m_group.empty()) + m_path.append("/"); +} + +CPVRChannelsPath::CPVRChannelsPath(bool bRadio, + const std::string& strGroupName, + const std::string& strAddonID, + ADDON::AddonInstanceId instanceID, + int iChannelUID) + : m_bRadio(bRadio) +{ + if (!strGroupName.empty() && !strAddonID.empty() && iChannelUID >= 0) + { + m_kind = Kind::CHANNEL; + m_group = strGroupName; + m_addonID = strAddonID; + m_instanceID = instanceID; + m_iChannelUID = iChannelUID; + m_path = StringUtils::Format("pvr://channels/{}/{}/{}@{}_{}.pvr", bRadio ? "radio" : "tv", + CURL::Encode(m_group), m_instanceID, m_addonID, m_iChannelUID); + } +} + +bool CPVRChannelsPath::IsHiddenChannelGroup() const +{ + return m_kind == Kind::GROUP && m_group == ".hidden"; +} + +std::string CPVRChannelsPath::TrimSlashes(const std::string& strString) +{ + std::string strTrimmed = strString; + while (!strTrimmed.empty() && strTrimmed.front() == '/') + strTrimmed.erase(0, 1); + + while (!strTrimmed.empty() && strTrimmed.back() == '/') + strTrimmed.pop_back(); + + return strTrimmed; +} diff --git a/xbmc/pvr/channels/PVRChannelsPath.h b/xbmc/pvr/channels/PVRChannelsPath.h new file mode 100644 index 0000000..7b825c1 --- /dev/null +++ b/xbmc/pvr/channels/PVRChannelsPath.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/IAddon.h" + +class CDateTime; + +namespace PVR +{ + class CPVRChannelsPath + { + public: + static const std::string PATH_TV_CHANNELS; + static const std::string PATH_RADIO_CHANNELS; + + explicit CPVRChannelsPath(const std::string& strPath); + CPVRChannelsPath(bool bRadio, const std::string& strGroupName); + CPVRChannelsPath(bool bRadio, bool bHidden, const std::string& strGroupName); + CPVRChannelsPath(bool bRadio, + const std::string& strGroupName, + const std::string& strAddonID, + ADDON::AddonInstanceId instanceID, + int iChannelUID); + + operator std::string() const { return m_path; } + bool operator ==(const CPVRChannelsPath& right) const { return m_path == right.m_path; } + bool operator !=(const CPVRChannelsPath& right) const { return !(*this == right); } + + bool IsValid() const { return m_kind > Kind::PROTO; } + + bool IsEmpty() const { return m_kind == Kind::EMPTY; } + bool IsChannelsRoot() const { return m_kind == Kind::ROOT; } + bool IsChannelGroup() const { return m_kind == Kind::GROUP; } + bool IsChannel() const { return m_kind == Kind::CHANNEL; } + + bool IsHiddenChannelGroup() const; + + bool IsRadio() const { return m_bRadio; } + + const std::string& GetGroupName() const { return m_group; } + const std::string& GetAddonID() const { return m_addonID; } + ADDON::AddonInstanceId GetInstanceID() const { return m_instanceID; } + int GetChannelUID() const { return m_iChannelUID; } + + private: + static std::string TrimSlashes(const std::string& strString); + + enum class Kind + { + INVALID, + PROTO, + EMPTY, + ROOT, + GROUP, + CHANNEL, + }; + + Kind m_kind = Kind::INVALID; + bool m_bRadio = false;; + std::string m_path; + std::string m_group; + std::string m_addonID; + ADDON::AddonInstanceId m_instanceID{ADDON::ADDON_SINGLETON_INSTANCE_ID}; + int m_iChannelUID = -1; + }; +} diff --git a/xbmc/pvr/channels/PVRRadioRDSInfoTag.cpp b/xbmc/pvr/channels/PVRRadioRDSInfoTag.cpp new file mode 100644 index 0000000..e1ee649 --- /dev/null +++ b/xbmc/pvr/channels/PVRRadioRDSInfoTag.cpp @@ -0,0 +1,736 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRRadioRDSInfoTag.h" + +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIMessage.h" +#include "guilib/GUIWindowManager.h" +#include "utils/Archive.h" +#include "utils/CharsetConverter.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +#include <algorithm> +#include <mutex> +#include <string> +#include <utility> + +using namespace PVR; + +CPVRRadioRDSInfoTag::CPVRRadioRDSInfoTag() +{ + Clear(); +} + +void CPVRRadioRDSInfoTag::Serialize(CVariant& value) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + value["strLanguage"] = m_strLanguage; + value["strCountry"] = m_strCountry; + value["strTitle"] = m_strTitle; + value["strBand"] = m_strBand; + value["strArtist"] = m_strArtist; + value["strComposer"] = m_strComposer; + value["strConductor"] = m_strConductor; + value["strAlbum"] = m_strAlbum; + value["iAlbumTracknumber"] = m_iAlbumTracknumber; + value["strProgStation"] = m_strProgStation; + value["strProgStyle"] = m_strProgStyle; + value["strProgHost"] = m_strProgHost; + value["strProgWebsite"] = m_strProgWebsite; + value["strProgNow"] = m_strProgNow; + value["strProgNext"] = m_strProgNext; + value["strPhoneHotline"] = m_strPhoneHotline; + value["strEMailHotline"] = m_strEMailHotline; + value["strPhoneStudio"] = m_strPhoneStudio; + value["strEMailStudio"] = m_strEMailStudio; + value["strSMSStudio"] = m_strSMSStudio; + value["strRadioStyle"] = m_strRadioStyle; +} + +void CPVRRadioRDSInfoTag::Archive(CArchive& ar) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (ar.IsStoring()) + { + ar << m_strLanguage; + ar << m_strCountry; + ar << m_strTitle; + ar << m_strBand; + ar << m_strArtist; + ar << m_strComposer; + ar << m_strConductor; + ar << m_strAlbum; + ar << m_iAlbumTracknumber; + ar << m_strProgStation; + ar << m_strProgStyle; + ar << m_strProgHost; + ar << m_strProgWebsite; + ar << m_strProgNow; + ar << m_strProgNext; + ar << m_strPhoneHotline; + ar << m_strEMailHotline; + ar << m_strPhoneStudio; + ar << m_strEMailStudio; + ar << m_strSMSStudio; + ar << m_strRadioStyle; + } + else + { + ar >> m_strLanguage; + ar >> m_strCountry; + ar >> m_strTitle; + ar >> m_strBand; + ar >> m_strArtist; + ar >> m_strComposer; + ar >> m_strConductor; + ar >> m_strAlbum; + ar >> m_iAlbumTracknumber; + ar >> m_strProgStation; + ar >> m_strProgStyle; + ar >> m_strProgHost; + ar >> m_strProgWebsite; + ar >> m_strProgNow; + ar >> m_strProgNext; + ar >> m_strPhoneHotline; + ar >> m_strEMailHotline; + ar >> m_strPhoneStudio; + ar >> m_strEMailStudio; + ar >> m_strSMSStudio; + ar >> m_strRadioStyle; + } +} + +bool CPVRRadioRDSInfoTag::operator==(const CPVRRadioRDSInfoTag& right) const +{ + if (this == &right) + return true; + + std::unique_lock<CCriticalSection> lock(m_critSection); + return ( + m_strLanguage == right.m_strLanguage && m_strCountry == right.m_strCountry && + m_strTitle == right.m_strTitle && m_strBand == right.m_strBand && + m_strArtist == right.m_strArtist && m_strComposer == right.m_strComposer && + m_strConductor == right.m_strConductor && m_strAlbum == right.m_strAlbum && + m_iAlbumTracknumber == right.m_iAlbumTracknumber && m_strInfoNews == right.m_strInfoNews && + m_strInfoNewsLocal == right.m_strInfoNewsLocal && m_strInfoSport == right.m_strInfoSport && + m_strInfoStock == right.m_strInfoStock && m_strInfoWeather == right.m_strInfoWeather && + m_strInfoLottery == right.m_strInfoLottery && m_strInfoOther == right.m_strInfoOther && + m_strProgStyle == right.m_strProgStyle && m_strProgHost == right.m_strProgHost && + m_strProgStation == right.m_strProgStation && m_strProgWebsite == right.m_strProgWebsite && + m_strProgNow == right.m_strProgNow && m_strProgNext == right.m_strProgNext && + m_strPhoneHotline == right.m_strPhoneHotline && + m_strEMailHotline == right.m_strEMailHotline && m_strPhoneStudio == right.m_strPhoneStudio && + m_strEMailStudio == right.m_strEMailStudio && m_strSMSStudio == right.m_strSMSStudio && + m_strRadioStyle == right.m_strRadioStyle && m_strInfoHoroscope == right.m_strInfoHoroscope && + m_strInfoCinema == right.m_strInfoCinema && m_strComment == right.m_strComment && + m_strEditorialStaff == right.m_strEditorialStaff && m_strRadioText == right.m_strRadioText && + m_strProgramServiceText == right.m_strProgramServiceText && + m_strProgramServiceLine0 == right.m_strProgramServiceLine0 && + m_strProgramServiceLine1 == right.m_strProgramServiceLine1 && + m_bHaveRadioText == right.m_bHaveRadioText && + m_bHaveRadioTextPlus == right.m_bHaveRadioTextPlus); +} + +bool CPVRRadioRDSInfoTag::operator !=(const CPVRRadioRDSInfoTag& right) const +{ + if (this == &right) + return false; + + return !(*this == right); +} + +void CPVRRadioRDSInfoTag::Clear() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_RDS_SpeechActive = false; + + ResetSongInformation(); + + m_strLanguage.erase(); + m_strCountry.erase(); + m_strComment.erase(); + m_strInfoNews.Clear(); + m_strInfoNewsLocal.Clear(); + m_strInfoSport.Clear(); + m_strInfoStock.Clear(); + m_strInfoWeather.Clear(); + m_strInfoLottery.Clear(); + m_strInfoOther.Clear(); + m_strInfoHoroscope.Clear(); + m_strInfoCinema.Clear(); + m_strProgStyle.erase(); + m_strProgHost.erase(); + m_strProgStation.erase(); + m_strProgWebsite.erase(); + m_strPhoneHotline.erase(); + m_strEMailHotline.erase(); + m_strPhoneStudio.erase(); + m_strEMailStudio.erase(); + m_strSMSStudio.erase(); + m_strRadioStyle = "unknown"; + m_strEditorialStaff.Clear(); + m_strRadioText.Clear(); + m_strProgramServiceText.Clear(); + m_strProgramServiceLine0.erase(); + m_strProgramServiceLine1.erase(); + + m_bHaveRadioText = false; + m_bHaveRadioTextPlus = false; +} + +void CPVRRadioRDSInfoTag::ResetSongInformation() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_strTitle.erase(); + m_strBand.erase(); + m_strArtist.erase(); + m_strComposer.erase(); + m_strConductor.erase(); + m_strAlbum.erase(); + m_iAlbumTracknumber = 0; +} + +void CPVRRadioRDSInfoTag::SetSpeechActive(bool active) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_RDS_SpeechActive = active; +} + +void CPVRRadioRDSInfoTag::SetLanguage(const std::string& strLanguage) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strLanguage = Trim(strLanguage); +} + +const std::string& CPVRRadioRDSInfoTag::GetLanguage() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strLanguage; +} + +void CPVRRadioRDSInfoTag::SetCountry(const std::string& strCountry) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strCountry = Trim(strCountry); +} + +const std::string& CPVRRadioRDSInfoTag::GetCountry() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strCountry; +} + +void CPVRRadioRDSInfoTag::SetTitle(const std::string& strTitle) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strTitle = Trim(strTitle); +} + +void CPVRRadioRDSInfoTag::SetArtist(const std::string& strArtist) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strArtist = Trim(strArtist); +} + +void CPVRRadioRDSInfoTag::SetBand(const std::string& strBand) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strBand = Trim(strBand); + g_charsetConverter.unknownToUTF8(m_strBand); +} + +void CPVRRadioRDSInfoTag::SetComposer(const std::string& strComposer) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strComposer = Trim(strComposer); + g_charsetConverter.unknownToUTF8(m_strComposer); +} + +void CPVRRadioRDSInfoTag::SetConductor(const std::string& strConductor) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strConductor = Trim(strConductor); + g_charsetConverter.unknownToUTF8(m_strConductor); +} + +void CPVRRadioRDSInfoTag::SetAlbum(const std::string& strAlbum) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strAlbum = Trim(strAlbum); + g_charsetConverter.unknownToUTF8(m_strAlbum); +} + +void CPVRRadioRDSInfoTag::SetAlbumTrackNumber(int track) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_iAlbumTracknumber = track; +} + +void CPVRRadioRDSInfoTag::SetComment(const std::string& strComment) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strComment = Trim(strComment); + g_charsetConverter.unknownToUTF8(m_strComment); +} + +const std::string& CPVRRadioRDSInfoTag::GetTitle() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strTitle; +} + +const std::string& CPVRRadioRDSInfoTag::GetArtist() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strArtist; +} + +const std::string& CPVRRadioRDSInfoTag::GetBand() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strBand; +} + +const std::string& CPVRRadioRDSInfoTag::GetComposer() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strComposer; +} + +const std::string& CPVRRadioRDSInfoTag::GetConductor() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strConductor; +} + +const std::string& CPVRRadioRDSInfoTag::GetAlbum() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strAlbum; +} + +int CPVRRadioRDSInfoTag::GetAlbumTrackNumber() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iAlbumTracknumber; +} + +const std::string& CPVRRadioRDSInfoTag::GetComment() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strComment; +} + +void CPVRRadioRDSInfoTag::SetInfoNews(const std::string& strNews) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strInfoNews.Add(strNews); +} + +const std::string CPVRRadioRDSInfoTag::GetInfoNews() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strInfoNews.GetText(); +} + +void CPVRRadioRDSInfoTag::SetInfoNewsLocal(const std::string& strNews) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strInfoNewsLocal.Add(strNews); +} + +const std::string CPVRRadioRDSInfoTag::GetInfoNewsLocal() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strInfoNewsLocal.GetText(); +} + +void CPVRRadioRDSInfoTag::SetInfoSport(const std::string& strSport) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strInfoSport.Add(strSport); +} + +const std::string CPVRRadioRDSInfoTag::GetInfoSport() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strInfoSport.GetText(); +} + +void CPVRRadioRDSInfoTag::SetInfoStock(const std::string& strStock) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strInfoStock.Add(strStock); +} + +const std::string CPVRRadioRDSInfoTag::GetInfoStock() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strInfoStock.GetText(); +} + +void CPVRRadioRDSInfoTag::SetInfoWeather(const std::string& strWeather) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strInfoWeather.Add(strWeather); +} + +const std::string CPVRRadioRDSInfoTag::GetInfoWeather() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strInfoWeather.GetText(); +} + +void CPVRRadioRDSInfoTag::SetInfoLottery(const std::string& strLottery) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strInfoLottery.Add(strLottery); +} + +const std::string CPVRRadioRDSInfoTag::GetInfoLottery() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strInfoLottery.GetText(); +} + +void CPVRRadioRDSInfoTag::SetEditorialStaff(const std::string& strEditorialStaff) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strEditorialStaff.Add(strEditorialStaff); +} + +const std::string CPVRRadioRDSInfoTag::GetEditorialStaff() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strEditorialStaff.GetText(); +} + +void CPVRRadioRDSInfoTag::SetInfoHoroscope(const std::string& strHoroscope) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strInfoHoroscope.Add(strHoroscope); +} + +const std::string CPVRRadioRDSInfoTag::GetInfoHoroscope() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strInfoHoroscope.GetText(); +} + +void CPVRRadioRDSInfoTag::SetInfoCinema(const std::string& strCinema) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strInfoCinema.Add(strCinema); +} + +const std::string CPVRRadioRDSInfoTag::GetInfoCinema() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strInfoCinema.GetText(); +} + +void CPVRRadioRDSInfoTag::SetInfoOther(const std::string& strOther) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strInfoOther.Add(strOther); +} + +const std::string CPVRRadioRDSInfoTag::GetInfoOther() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strInfoOther.GetText(); +} + +void CPVRRadioRDSInfoTag::SetProgStation(const std::string& strProgStation) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strProgStation = Trim(strProgStation); + g_charsetConverter.unknownToUTF8(m_strProgStation); +} + +void CPVRRadioRDSInfoTag::SetProgHost(const std::string& strProgHost) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strProgHost = Trim(strProgHost); + g_charsetConverter.unknownToUTF8(m_strProgHost); +} + +void CPVRRadioRDSInfoTag::SetProgStyle(const std::string& strProgStyle) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strProgStyle = Trim(strProgStyle); + g_charsetConverter.unknownToUTF8(m_strProgStyle); +} + +void CPVRRadioRDSInfoTag::SetProgWebsite(const std::string& strWebsite) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strProgWebsite = Trim(strWebsite); + g_charsetConverter.unknownToUTF8(m_strProgWebsite); +} + +void CPVRRadioRDSInfoTag::SetProgNow(const std::string& strNow) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strProgNow = Trim(strNow); + g_charsetConverter.unknownToUTF8(m_strProgNow); +} + +void CPVRRadioRDSInfoTag::SetProgNext(const std::string& strNext) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strProgNext = Trim(strNext); + g_charsetConverter.unknownToUTF8(m_strProgNext); +} + +void CPVRRadioRDSInfoTag::SetPhoneHotline(const std::string& strHotline) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strPhoneHotline = Trim(strHotline); + g_charsetConverter.unknownToUTF8(m_strPhoneHotline); +} + +void CPVRRadioRDSInfoTag::SetEMailHotline(const std::string& strHotline) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strEMailHotline = Trim(strHotline); + g_charsetConverter.unknownToUTF8(m_strEMailHotline); +} + +void CPVRRadioRDSInfoTag::SetPhoneStudio(const std::string& strPhone) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strPhoneStudio = Trim(strPhone); + g_charsetConverter.unknownToUTF8(m_strPhoneStudio); +} + +void CPVRRadioRDSInfoTag::SetEMailStudio(const std::string& strEMail) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strEMailStudio = Trim(strEMail); + g_charsetConverter.unknownToUTF8(m_strEMailStudio); +} + +void CPVRRadioRDSInfoTag::SetSMSStudio(const std::string& strSMS) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strSMSStudio = Trim(strSMS); + g_charsetConverter.unknownToUTF8(m_strSMSStudio); +} + +const std::string& CPVRRadioRDSInfoTag::GetProgStyle() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strProgStyle; +} + +const std::string& CPVRRadioRDSInfoTag::GetProgHost() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strProgHost; +} + +const std::string& CPVRRadioRDSInfoTag::GetProgStation() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strProgStation; +} + +const std::string& CPVRRadioRDSInfoTag::GetProgWebsite() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strProgWebsite; +} + +const std::string& CPVRRadioRDSInfoTag::GetProgNow() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strProgNow; +} + +const std::string& CPVRRadioRDSInfoTag::GetProgNext() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strProgNext; +} + +const std::string& CPVRRadioRDSInfoTag::GetPhoneHotline() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strPhoneHotline; +} + +const std::string& CPVRRadioRDSInfoTag::GetEMailHotline() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strEMailHotline; +} + +const std::string& CPVRRadioRDSInfoTag::GetPhoneStudio() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strPhoneStudio; +} + +const std::string& CPVRRadioRDSInfoTag::GetEMailStudio() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strEMailStudio; +} + +const std::string& CPVRRadioRDSInfoTag::GetSMSStudio() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strSMSStudio; +} + +void CPVRRadioRDSInfoTag::SetRadioStyle(const std::string& style) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strRadioStyle = style; +} + +const std::string CPVRRadioRDSInfoTag::GetRadioStyle() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strRadioStyle; +} + +void CPVRRadioRDSInfoTag::SetRadioText(const std::string& strRadioText) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strRadioText.Add(strRadioText); +} + +std::string CPVRRadioRDSInfoTag::GetRadioText(unsigned int line) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_strRadioText.Size() > 0) + { + return m_strRadioText.GetLine(line); + } + else if (line == 0) + { + return m_strProgramServiceLine0; + } + else if (line == 1) + { + return m_strProgramServiceLine1; + } + return {}; +} + +void CPVRRadioRDSInfoTag::SetProgramServiceText(const std::string& strPSText) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strProgramServiceText.Add(strPSText); + + m_strProgramServiceLine0.erase(); + for (size_t i = m_strProgramServiceText.MaxSize() / 2 + 1; i < m_strProgramServiceText.MaxSize(); + ++i) + { + m_strProgramServiceLine0 += m_strProgramServiceText.GetLine(i); + m_strProgramServiceLine0 += ' '; + } + + m_strProgramServiceLine1.erase(); + for (size_t i = 0; i < m_strProgramServiceText.MaxSize() / 2; ++i) + { + m_strProgramServiceLine1 += m_strProgramServiceText.GetLine(i); + m_strProgramServiceLine1 += ' '; + } +} + +void CPVRRadioRDSInfoTag::SetPlayingRadioText(bool yesNo) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bHaveRadioText = yesNo; +} + +bool CPVRRadioRDSInfoTag::IsPlayingRadioText() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bHaveRadioText; +} + +void CPVRRadioRDSInfoTag::SetPlayingRadioTextPlus(bool yesNo) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bHaveRadioTextPlus = yesNo; +} + +bool CPVRRadioRDSInfoTag::IsPlayingRadioTextPlus() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bHaveRadioTextPlus; +} + +std::string CPVRRadioRDSInfoTag::Trim(const std::string& value) +{ + std::string trimmedValue(value); + StringUtils::TrimLeft(trimmedValue); + StringUtils::TrimRight(trimmedValue, " \n\r"); + return trimmedValue; +} + +bool CPVRRadioRDSInfoTag::Info::operator==(const CPVRRadioRDSInfoTag::Info& right) const +{ + if (this == &right) + return true; + + return (m_infoText == right.m_infoText && m_data == right.m_data && + m_maxSize == right.m_maxSize && m_prependData == right.m_prependData); +} + +void CPVRRadioRDSInfoTag::Info::Clear() +{ + m_data.clear(); + m_infoText.clear(); +} + +void CPVRRadioRDSInfoTag::Info::Add(const std::string& text) +{ + std::string tmp = Trim(text); + g_charsetConverter.unknownToUTF8(tmp); + + if (std::find(m_data.begin(), m_data.end(), tmp) != m_data.end()) + return; + + if (m_data.size() >= m_maxSize) + { + if (m_prependData) + m_data.pop_back(); + else + m_data.pop_front(); + } + + if (m_prependData) + m_data.emplace_front(std::move(tmp)); + else + m_data.emplace_back(std::move(tmp)); + + m_infoText.clear(); + for (const std::string& data : m_data) + { + m_infoText += data; + m_infoText += '\n'; + } + + // send a message to all windows to tell them to update the radiotext + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_RADIOTEXT); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); +} diff --git a/xbmc/pvr/channels/PVRRadioRDSInfoTag.h b/xbmc/pvr/channels/PVRRadioRDSInfoTag.h new file mode 100644 index 0000000..bc66989 --- /dev/null +++ b/xbmc/pvr/channels/PVRRadioRDSInfoTag.h @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "utils/IArchivable.h" +#include "utils/ISerializable.h" + +#include <deque> +#include <string> + +namespace PVR +{ + +class CPVRRadioRDSInfoTag final : public IArchivable, public ISerializable +{ +public: + CPVRRadioRDSInfoTag(); + + bool operator ==(const CPVRRadioRDSInfoTag& right) const; + bool operator !=(const CPVRRadioRDSInfoTag& right) const; + + void Archive(CArchive& ar) override; + void Serialize(CVariant& value) const override; + + void Clear(); + void ResetSongInformation(); + + /**! Basic RDS related information */ + void SetSpeechActive(bool active); + void SetLanguage(const std::string& strLanguage); + const std::string& GetLanguage() const; + void SetCountry(const std::string& strCountry); + const std::string& GetCountry() const; + void SetRadioText(const std::string& strRadioText); + std::string GetRadioText(unsigned int line) const; + void SetProgramServiceText(const std::string& strPSText); + + /**! RDS RadioText related information */ + void SetTitle(const std::string& strTitle); + void SetBand(const std::string& strBand); + void SetArtist(const std::string& strArtist); + void SetComposer(const std::string& strComposer); + void SetConductor(const std::string& strConductor); + void SetAlbum(const std::string& strAlbum); + void SetComment(const std::string& strComment); + void SetAlbumTrackNumber(int track); + + const std::string& GetTitle() const; + const std::string& GetBand() const; + const std::string& GetArtist() const; + const std::string& GetComposer() const; + const std::string& GetConductor() const; + const std::string& GetAlbum() const; + const std::string& GetComment() const; + int GetAlbumTrackNumber() const; + + void SetProgStation(const std::string& strProgStation); + void SetProgStyle(const std::string& strProgStyle); + void SetProgHost(const std::string& strProgHost); + void SetProgWebsite(const std::string& strWebsite); + void SetProgNow(const std::string& strNow); + void SetProgNext(const std::string& strNext); + void SetPhoneHotline(const std::string& strHotline); + void SetEMailHotline(const std::string& strHotline); + void SetPhoneStudio(const std::string& strPhone); + void SetEMailStudio(const std::string& strEMail); + void SetSMSStudio(const std::string& strSMS); + + const std::string& GetProgStation() const; + const std::string& GetProgStyle() const; + const std::string& GetProgHost() const; + const std::string& GetProgWebsite() const; + const std::string& GetProgNow() const; + const std::string& GetProgNext() const; + const std::string& GetPhoneHotline() const; + const std::string& GetEMailHotline() const; + const std::string& GetPhoneStudio() const; + const std::string& GetEMailStudio() const; + const std::string& GetSMSStudio() const; + + void SetInfoNews(const std::string& strNews); + const std::string GetInfoNews() const; + + void SetInfoNewsLocal(const std::string& strNews); + const std::string GetInfoNewsLocal() const; + + void SetInfoSport(const std::string& strSport); + const std::string GetInfoSport() const; + + void SetInfoStock(const std::string& strSport); + const std::string GetInfoStock() const; + + void SetInfoWeather(const std::string& strWeather); + const std::string GetInfoWeather() const; + + void SetInfoHoroscope(const std::string& strHoroscope); + const std::string GetInfoHoroscope() const; + + void SetInfoCinema(const std::string& strCinema); + const std::string GetInfoCinema() const; + + void SetInfoLottery(const std::string& strLottery); + const std::string GetInfoLottery() const; + + void SetInfoOther(const std::string& strOther); + const std::string GetInfoOther() const; + + void SetEditorialStaff(const std::string& strEditorialStaff); + const std::string GetEditorialStaff() const; + + void SetRadioStyle(const std::string& style); + const std::string GetRadioStyle() const; + + void SetPlayingRadioText(bool yesNo); + bool IsPlayingRadioText() const; + + void SetPlayingRadioTextPlus(bool yesNo); + bool IsPlayingRadioTextPlus() const; + +private: + CPVRRadioRDSInfoTag(const CPVRRadioRDSInfoTag& tag) = delete; + const CPVRRadioRDSInfoTag& operator =(const CPVRRadioRDSInfoTag& tag) = delete; + + static std::string Trim(const std::string& value); + + mutable CCriticalSection m_critSection; + + bool m_RDS_SpeechActive; + + std::string m_strLanguage; + std::string m_strCountry; + std::string m_strTitle; + std::string m_strBand; + std::string m_strArtist; + std::string m_strComposer; + std::string m_strConductor; + std::string m_strAlbum; + std::string m_strComment; + int m_iAlbumTracknumber; + std::string m_strRadioStyle; + + class Info + { + public: + Info() = delete; + Info(size_t maxSize, bool prependData) : m_maxSize(maxSize), m_prependData(prependData) {} + + bool operator==(const Info& right) const; + + size_t Size() const { return m_data.size(); } + size_t MaxSize() const { return m_maxSize; } + + void Clear(); + void Add(const std::string& text); + + const std::string& GetText() const { return m_infoText; } + std::string GetLine(unsigned int line) const + { + return line < m_data.size() ? m_data.at(line) : ""; + } + + private: + const size_t m_maxSize = 10; + const bool m_prependData = false; + std::deque<std::string> m_data; + std::string m_infoText; + }; + + Info m_strInfoNews{10, false}; + Info m_strInfoNewsLocal{10, false}; + Info m_strInfoSport{10, false}; + Info m_strInfoStock{10, false}; + Info m_strInfoWeather{10, false}; + Info m_strInfoLottery{10, false}; + Info m_strInfoOther{10, false}; + Info m_strInfoHoroscope{10, false}; + Info m_strInfoCinema{10, false}; + Info m_strEditorialStaff{10, false}; + + Info m_strRadioText{6, true}; + + Info m_strProgramServiceText{12, false}; + std::string m_strProgramServiceLine0; + std::string m_strProgramServiceLine1; + + std::string m_strProgStyle; + std::string m_strProgHost; + std::string m_strProgStation; + std::string m_strProgWebsite; + std::string m_strProgNow; + std::string m_strProgNext; + std::string m_strPhoneHotline; + std::string m_strEMailHotline; + std::string m_strPhoneStudio; + std::string m_strEMailStudio; + std::string m_strSMSStudio; + + bool m_bHaveRadioText; + bool m_bHaveRadioTextPlus; +}; +} diff --git a/xbmc/pvr/channels/test/CMakeLists.txt b/xbmc/pvr/channels/test/CMakeLists.txt new file mode 100644 index 0000000..d88bab8 --- /dev/null +++ b/xbmc/pvr/channels/test/CMakeLists.txt @@ -0,0 +1,4 @@ +set(SOURCES TestPVRChannelsPath.cpp) +set(HEADERS) + +core_add_test_library(pvrchannels_test) diff --git a/xbmc/pvr/channels/test/TestPVRChannelsPath.cpp b/xbmc/pvr/channels/test/TestPVRChannelsPath.cpp new file mode 100644 index 0000000..c7232db --- /dev/null +++ b/xbmc/pvr/channels/test/TestPVRChannelsPath.cpp @@ -0,0 +1,404 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "pvr/channels/PVRChannelsPath.h" + +#include <gtest/gtest.h> + +TEST(TestPVRChannelsPath, Parse_Protocol) +{ + // pvr protocol is generally fine, but not sufficient for channels pvr paths - component is missing for that. + PVR::CPVRChannelsPath path("pvr://"); + + EXPECT_FALSE(path.IsValid()); +} + +TEST(TestPVRChannelsPath, Parse_Component_1) +{ + PVR::CPVRChannelsPath path("pvr://channels"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_TRUE(path.IsEmpty()); +} + +TEST(TestPVRChannelsPath, Parse_Component_2) +{ + PVR::CPVRChannelsPath path("pvr://channels/"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_TRUE(path.IsEmpty()); +} + +TEST(TestPVRChannelsPath, Parse_Invalid_Component) +{ + PVR::CPVRChannelsPath path("pvr://foo/"); + + EXPECT_FALSE(path.IsValid()); +} + +TEST(TestPVRChannelsPath, Parse_TV_Root_1) +{ + PVR::CPVRChannelsPath path("pvr://channels/tv"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_FALSE(path.IsRadio()); + EXPECT_TRUE(path.IsChannelsRoot()); + EXPECT_FALSE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), ""); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, Parse_TV_Root_2) +{ + PVR::CPVRChannelsPath path("pvr://channels/tv/"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_FALSE(path.IsRadio()); + EXPECT_TRUE(path.IsChannelsRoot()); + EXPECT_FALSE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), ""); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, Parse_Radio_Root_1) +{ + PVR::CPVRChannelsPath path("pvr://channels/radio"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_TRUE(path.IsRadio()); + EXPECT_TRUE(path.IsChannelsRoot()); + EXPECT_FALSE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), ""); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, Parse_Radio_Root_2) +{ + PVR::CPVRChannelsPath path("pvr://channels/radio/"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_TRUE(path.IsRadio()); + EXPECT_TRUE(path.IsChannelsRoot()); + EXPECT_FALSE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), ""); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, Parse_Invalid_Root) +{ + PVR::CPVRChannelsPath path("pvr://channels/foo"); + + EXPECT_FALSE(path.IsValid()); +} + +TEST(TestPVRChannelsPath, Parse_TV_Group_1) +{ + PVR::CPVRChannelsPath path("pvr://channels/tv/Group1"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_FALSE(path.IsRadio()); + EXPECT_FALSE(path.IsChannelsRoot()); + EXPECT_TRUE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), "Group1"); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, Parse_TV_Group_2) +{ + PVR::CPVRChannelsPath path("pvr://channels/tv/Group1/"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_FALSE(path.IsRadio()); + EXPECT_FALSE(path.IsChannelsRoot()); + EXPECT_TRUE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), "Group1"); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, Parse_Hidden_TV_Group) +{ + PVR::CPVRChannelsPath path("pvr://channels/tv/.hidden"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/.hidden/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_FALSE(path.IsRadio()); + EXPECT_FALSE(path.IsChannelsRoot()); + EXPECT_TRUE(path.IsChannelGroup()); + EXPECT_TRUE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), ".hidden"); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, Parse_Special_TV_Group) +{ + PVR::CPVRChannelsPath path("pvr://channels/tv/foo%2Fbar%20baz"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/foo%2Fbar%20baz/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_FALSE(path.IsRadio()); + EXPECT_FALSE(path.IsChannelsRoot()); + EXPECT_TRUE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), "foo/bar baz"); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, Parse_Invalid_Special_TV_Group) +{ + // special chars in group name not escaped + PVR::CPVRChannelsPath path("pvr://channels/tv/foo/bar baz"); + + EXPECT_FALSE(path.IsValid()); +} + +TEST(TestPVRChannelsPath, Parse_Radio_Group) +{ + PVR::CPVRChannelsPath path("pvr://channels/radio/Group1"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/Group1/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_TRUE(path.IsRadio()); + EXPECT_FALSE(path.IsChannelsRoot()); + EXPECT_TRUE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), "Group1"); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, Parse_TV_Channel) +{ + PVR::CPVRChannelsPath path("pvr://channels/tv/Group1/5@pvr.demo_4711.pvr"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/5@pvr.demo_4711.pvr"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_FALSE(path.IsRadio()); + EXPECT_FALSE(path.IsChannelsRoot()); + EXPECT_FALSE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_TRUE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), "Group1"); + EXPECT_EQ(path.GetAddonID(), "pvr.demo"); + EXPECT_EQ(path.GetInstanceID(), 5); + EXPECT_EQ(path.GetChannelUID(), 4711); +} + +TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_1) +{ + // trailing ".pvr" missing + PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo_4711"); + + EXPECT_FALSE(path.IsValid()); +} + +TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_2) +{ + // '-' instead of '_' as clientid / channeluid delimiter + PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo-4711.pvr"); + + EXPECT_FALSE(path.IsValid()); +} + +TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_3) +{ + // channeluid not numerical + PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo_abc4711.pvr"); + + EXPECT_FALSE(path.IsValid()); +} + +TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_4) +{ + // channeluid not positive or zero + PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo_-4711.pvr"); + + EXPECT_FALSE(path.IsValid()); +} + +TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_5) +{ + // empty clientid + PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/_4711.pvr"); + + EXPECT_FALSE(path.IsValid()); +} + +TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_6) +{ + // empty channeluid + PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo_.pvr"); + + EXPECT_FALSE(path.IsValid()); +} + +TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_7) +{ + // empty clientid and empty channeluid + PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/_.pvr"); + + EXPECT_FALSE(path.IsValid()); +} + +TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_8) +{ + // empty clientid and empty channeluid, only extension ".pvr" given + PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/.pvr"); + + EXPECT_FALSE(path.IsValid()); +} + +TEST(TestPVRChannelsPath, TV_Channelgroup) +{ + PVR::CPVRChannelsPath path(false, "Group1"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_FALSE(path.IsRadio()); + EXPECT_FALSE(path.IsChannelsRoot()); + EXPECT_TRUE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), "Group1"); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, Radio_Channelgroup) +{ + PVR::CPVRChannelsPath path(true, "Group1"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/Group1/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_TRUE(path.IsRadio()); + EXPECT_FALSE(path.IsChannelsRoot()); + EXPECT_TRUE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), "Group1"); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, Hidden_TV_Channelgroup) +{ + PVR::CPVRChannelsPath path(false, true, "Group1"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/.hidden/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_FALSE(path.IsRadio()); + EXPECT_FALSE(path.IsChannelsRoot()); + EXPECT_TRUE(path.IsChannelGroup()); + EXPECT_TRUE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), ".hidden"); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, Hidden_Radio_Channelgroup) +{ + PVR::CPVRChannelsPath path(true, true, "Group1"); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/.hidden/"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_TRUE(path.IsRadio()); + EXPECT_FALSE(path.IsChannelsRoot()); + EXPECT_TRUE(path.IsChannelGroup()); + EXPECT_TRUE(path.IsHiddenChannelGroup()); + EXPECT_FALSE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), ".hidden"); + EXPECT_EQ(path.GetAddonID(), ""); + EXPECT_EQ(path.GetChannelUID(), -1); +} + +TEST(TestPVRChannelsPath, TV_Channel) +{ + PVR::CPVRChannelsPath path(false, "Group1", "pvr.demo", ADDON::ADDON_SINGLETON_INSTANCE_ID, 4711); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/0@pvr.demo_4711.pvr"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_FALSE(path.IsRadio()); + EXPECT_FALSE(path.IsChannelsRoot()); + EXPECT_FALSE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_TRUE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), "Group1"); + EXPECT_EQ(path.GetAddonID(), "pvr.demo"); + EXPECT_EQ(path.GetChannelUID(), 4711); +} + +TEST(TestPVRChannelsPath, Radio_Channel) +{ + PVR::CPVRChannelsPath path(true, "Group1", "pvr.demo", ADDON::ADDON_SINGLETON_INSTANCE_ID, 4711); + + EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/Group1/0@pvr.demo_4711.pvr"); + EXPECT_TRUE(path.IsValid()); + EXPECT_FALSE(path.IsEmpty()); + EXPECT_TRUE(path.IsRadio()); + EXPECT_FALSE(path.IsChannelsRoot()); + EXPECT_FALSE(path.IsChannelGroup()); + EXPECT_FALSE(path.IsHiddenChannelGroup()); + EXPECT_TRUE(path.IsChannel()); + EXPECT_EQ(path.GetGroupName(), "Group1"); + EXPECT_EQ(path.GetAddonID(), "pvr.demo"); + EXPECT_EQ(path.GetChannelUID(), 4711); +} + +TEST(TestPVRChannelsPath, Operator_Equals) +{ + PVR::CPVRChannelsPath path2(true, "Group1"); + PVR::CPVRChannelsPath path(static_cast<std::string>(path2)); + + EXPECT_EQ(path, path2); +} diff --git a/xbmc/pvr/dialogs/CMakeLists.txt b/xbmc/pvr/dialogs/CMakeLists.txt new file mode 100644 index 0000000..39b88ed --- /dev/null +++ b/xbmc/pvr/dialogs/CMakeLists.txt @@ -0,0 +1,29 @@ +set(SOURCES GUIDialogPVRChannelManager.cpp + GUIDialogPVRChannelsOSD.cpp + GUIDialogPVRGroupManager.cpp + GUIDialogPVRGuideInfo.cpp + GUIDialogPVRChannelGuide.cpp + GUIDialogPVRGuideControls.cpp + GUIDialogPVRGuideSearch.cpp + GUIDialogPVRRadioRDSInfo.cpp + GUIDialogPVRRecordingInfo.cpp + GUIDialogPVRRecordingSettings.cpp + GUIDialogPVRTimerSettings.cpp + GUIDialogPVRClientPriorities.cpp + GUIDialogPVRItemsViewBase.cpp) + +set(HEADERS GUIDialogPVRChannelManager.h + GUIDialogPVRChannelsOSD.h + GUIDialogPVRGroupManager.h + GUIDialogPVRGuideInfo.h + GUIDialogPVRChannelGuide.h + GUIDialogPVRGuideControls.h + GUIDialogPVRGuideSearch.h + GUIDialogPVRRadioRDSInfo.h + GUIDialogPVRRecordingInfo.h + GUIDialogPVRRecordingSettings.h + GUIDialogPVRTimerSettings.h + GUIDialogPVRClientPriorities.h + GUIDialogPVRItemsViewBase.h) + +core_add_library(pvr_dialogs) diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.cpp b/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.cpp new file mode 100644 index 0000000..10b2623 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.cpp @@ -0,0 +1,76 @@ +/* + * 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 "GUIDialogPVRChannelGuide.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/epg/EpgInfoTag.h" + +#include <memory> +#include <vector> + +using namespace PVR; + +CGUIDialogPVRChannelGuide::CGUIDialogPVRChannelGuide() + : CGUIDialogPVRItemsViewBase(WINDOW_DIALOG_PVR_CHANNEL_GUIDE, "DialogPVRChannelGuide.xml") +{ +} + +void CGUIDialogPVRChannelGuide::Open(const std::shared_ptr<CPVRChannel>& channel) +{ + m_channel = channel; + CGUIDialogPVRItemsViewBase::Open(); +} + +void CGUIDialogPVRChannelGuide::OnInitWindow() +{ + // no user-specific channel is set; use current playing channel + if (!m_channel) + m_channel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + + if (!m_channel) + { + Close(); + return; + } + + Init(); + + const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags = m_channel->GetEpgTags(); + for (const auto& tag : tags) + { + m_vecItems->Add(std::make_shared<CFileItem>(tag)); + } + + m_viewControl.SetItems(*m_vecItems); + + CGUIDialogPVRItemsViewBase::OnInitWindow(); + + // select the active entry + unsigned int iSelectedItem = 0; + for (int iEpgPtr = 0; iEpgPtr < m_vecItems->Size(); ++iEpgPtr) + { + const CFileItemPtr entry = m_vecItems->Get(iEpgPtr); + if (entry->HasEPGInfoTag() && entry->GetEPGInfoTag()->IsActive()) + { + iSelectedItem = iEpgPtr; + break; + } + } + m_viewControl.SetSelectedItem(iSelectedItem); +} + +void CGUIDialogPVRChannelGuide::OnDeinitWindow(int nextWindowID) +{ + CGUIDialogPVRItemsViewBase::OnDeinitWindow(nextWindowID); + m_channel.reset(); +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.h b/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.h new file mode 100644 index 0000000..b348a9e --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/dialogs/GUIDialogPVRItemsViewBase.h" + +#include <memory> + +namespace PVR +{ + class CPVRChannel; + + class CGUIDialogPVRChannelGuide : public CGUIDialogPVRItemsViewBase + { + public: + CGUIDialogPVRChannelGuide(); + ~CGUIDialogPVRChannelGuide() override = default; + + void Open(const std::shared_ptr<CPVRChannel>& channel); + + protected: + void OnInitWindow() override; + void OnDeinitWindow(int nextWindowID) override; + + private: + std::shared_ptr<CPVRChannel> m_channel; + }; +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.cpp b/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.cpp new file mode 100644 index 0000000..267b3c6 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.cpp @@ -0,0 +1,1076 @@ +/* + * 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 "GUIDialogPVRChannelManager.h" + +#include "FileItem.h" +#include "GUIPassword.h" +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "dialogs/GUIDialogFileBrowser.h" +#include "dialogs/GUIDialogProgress.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIEditControl.h" +#include "guilib/GUIMessage.h" +#include "guilib/GUISpinControlEx.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "profiles/ProfileManager.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/dialogs/GUIDialogPVRGroupManager.h" +#include "pvr/guilib/PVRGUIActionsParentalControl.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "storage/MediaManager.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +#include <algorithm> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#define BUTTON_OK 4 +#define BUTTON_APPLY 5 +#define BUTTON_CANCEL 6 +#define RADIOBUTTON_ACTIVE 7 +#define EDIT_NAME 8 +#define BUTTON_CHANNEL_LOGO 9 +#define IMAGE_CHANNEL_LOGO 10 +#define RADIOBUTTON_USEEPG 12 +#define SPIN_EPGSOURCE_SELECTION 13 +#define RADIOBUTTON_PARENTAL_LOCK 14 +#define CONTROL_LIST_CHANNELS 20 +#define BUTTON_GROUP_MANAGER 30 +#define BUTTON_NEW_CHANNEL 31 +#define BUTTON_RADIO_TV 34 +#define BUTTON_REFRESH_LOGOS 35 + +namespace +{ +constexpr const char* LABEL_CHANNEL_DISABLED = "0"; + +// Note: strings must not be changed; they are part of the public skinning API for this dialog. +constexpr const char* PROPERTY_CHANNEL_NUMBER = "Number"; +constexpr const char* PROPERTY_CHANNEL_ENABLED = "ActiveChannel"; +constexpr const char* PROPERTY_CHANNEL_USER_SET_HIDDEN = "UserSetHidden"; +constexpr const char* PROPERTY_CHANNEL_LOCKED = "ParentalLocked"; +constexpr const char* PROPERTY_CHANNEL_ICON = "Icon"; +constexpr const char* PROPERTY_CHANNEL_CUSTOM_ICON = "UserSetIcon"; +constexpr const char* PROPERTY_CHANNEL_NAME = "Name"; +constexpr const char* PROPERTY_CHANNEL_EPG_ENABLED = "UseEPG"; +constexpr const char* PROPERTY_CHANNEL_EPG_SOURCE = "EPGSource"; +constexpr const char* PROPERTY_CLIENT_SUPPORTS_SETTINGS = "SupportsSettings"; +constexpr const char* PROPERTY_CLIENT_NAME = "ClientName"; +constexpr const char* PROPERTY_ITEM_CHANGED = "Changed"; + +} // namespace + +using namespace PVR; +using namespace KODI::MESSAGING; + +CGUIDialogPVRChannelManager::CGUIDialogPVRChannelManager() : + CGUIDialog(WINDOW_DIALOG_PVR_CHANNEL_MANAGER, "DialogPVRChannelManager.xml"), + m_channelItems(new CFileItemList) +{ + SetRadio(false); +} + +CGUIDialogPVRChannelManager::~CGUIDialogPVRChannelManager() +{ + delete m_channelItems; +} + +bool CGUIDialogPVRChannelManager::OnActionMove(const CAction& action) +{ + bool bReturn(false); + int iActionId = action.GetID(); + + if (GetFocusedControlID() == CONTROL_LIST_CHANNELS) + { + if (iActionId == ACTION_MOUSE_MOVE) + { + int iSelected = m_viewControl.GetSelectedItem(); + if (m_iSelected < iSelected) + { + iActionId = ACTION_MOVE_DOWN; + } + else if (m_iSelected > iSelected) + { + iActionId = ACTION_MOVE_UP; + } + else + { + return bReturn; + } + } + + if (iActionId == ACTION_MOVE_DOWN || iActionId == ACTION_MOVE_UP || + iActionId == ACTION_PAGE_DOWN || iActionId == ACTION_PAGE_UP || + iActionId == ACTION_FIRST_PAGE || iActionId == ACTION_LAST_PAGE) + { + CGUIDialog::OnAction(action); + int iSelected = m_viewControl.GetSelectedItem(); + + bReturn = true; + if (!m_bMovingMode) + { + if (iSelected != m_iSelected) + { + m_iSelected = iSelected; + SetData(m_iSelected); + } + } + else + { + bool bMoveUp = iActionId == ACTION_PAGE_UP || iActionId == ACTION_MOVE_UP || iActionId == ACTION_FIRST_PAGE; + unsigned int iLines = bMoveUp ? abs(m_iSelected - iSelected) : 1; + bool bOutOfBounds = bMoveUp ? m_iSelected <= 0 : m_iSelected >= m_channelItems->Size() - 1; + if (bOutOfBounds) + { + bMoveUp = !bMoveUp; + iLines = m_channelItems->Size() - 1; + } + for (unsigned int iLine = 0; iLine < iLines; ++iLine) + { + unsigned int iNewSelect = bMoveUp ? m_iSelected - 1 : m_iSelected + 1; + + const CFileItemPtr newItem = m_channelItems->Get(iNewSelect); + const std::string number = newItem->GetProperty(PROPERTY_CHANNEL_NUMBER).asString(); + if (number != LABEL_CHANNEL_DISABLED) + { + // Swap channel numbers + const CFileItemPtr item = m_channelItems->Get(m_iSelected); + newItem->SetProperty(PROPERTY_CHANNEL_NUMBER, + item->GetProperty(PROPERTY_CHANNEL_NUMBER)); + SetItemChanged(newItem); + item->SetProperty(PROPERTY_CHANNEL_NUMBER, number); + SetItemChanged(item); + } + + // swap items + m_channelItems->Swap(iNewSelect, m_iSelected); + m_iSelected = iNewSelect; + } + + m_viewControl.SetItems(*m_channelItems); + m_viewControl.SetSelectedItem(m_iSelected); + } + } + } + + return bReturn; +} + +bool CGUIDialogPVRChannelManager::OnAction(const CAction& action) +{ + return OnActionMove(action) || + CGUIDialog::OnAction(action); +} + +void CGUIDialogPVRChannelManager::OnInitWindow() +{ + CGUIDialog::OnInitWindow(); + + m_iSelected = 0; + m_bMovingMode = false; + m_bAllowNewChannel = false; + + EnableChannelOptions(false); + CONTROL_DISABLE(BUTTON_APPLY); + + // prevent resorting channels if backend channel numbers or backend channel order shall be used + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + m_bAllowRenumber = !settings->GetBool(CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS); + m_bAllowReorder = + m_bAllowRenumber && !settings->GetBool(CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER); + + Update(); + + if (m_initialSelection) + { + // set initial selection + const std::shared_ptr<CPVRChannel> channel = m_initialSelection->GetPVRChannelInfoTag(); + for (int i = 0; i < m_channelItems->Size(); ++i) + { + if (m_channelItems->Get(i)->GetPVRChannelInfoTag() == channel) + { + m_iSelected = i; + m_viewControl.SetSelectedItem(m_iSelected); + break; + } + } + m_initialSelection.reset(); + } + SetData(m_iSelected); +} + +void CGUIDialogPVRChannelManager::OnDeinitWindow(int nextWindowID) +{ + Clear(); + + CGUIDialog::OnDeinitWindow(nextWindowID); +} + +void CGUIDialogPVRChannelManager::SetRadio(bool bIsRadio) +{ + m_bIsRadio = bIsRadio; + SetProperty("IsRadio", m_bIsRadio ? "true" : ""); +} + +void CGUIDialogPVRChannelManager::Open(const std::shared_ptr<CFileItem>& initialSelection) +{ + m_initialSelection = initialSelection; + CGUIDialog::Open(); +} + +bool CGUIDialogPVRChannelManager::OnClickListChannels(const CGUIMessage& message) +{ + if (!m_bMovingMode) + { + int iAction = message.GetParam1(); + int iItem = m_viewControl.GetSelectedItem(); + + /* Check file item is in list range and get his pointer */ + if (iItem < 0 || iItem >= m_channelItems->Size()) return true; + + /* Process actions */ + if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_CONTEXT_MENU || iAction == ACTION_MOUSE_RIGHT_CLICK) + { + /* Show Contextmenu */ + OnPopupMenu(iItem); + + return true; + } + } + else + { + CFileItemPtr pItem = m_channelItems->Get(m_iSelected); + if (pItem) + { + pItem->Select(false); + m_bMovingMode = false; + SetItemChanged(pItem); + return true; + } + } + + return false; +} + +bool CGUIDialogPVRChannelManager::OnClickButtonOK() +{ + SaveList(); + Close(); + return true; +} + +bool CGUIDialogPVRChannelManager::OnClickButtonApply() +{ + SaveList(); + return true; +} + +bool CGUIDialogPVRChannelManager::OnClickButtonCancel() +{ + Close(); + return true; +} + +bool CGUIDialogPVRChannelManager::OnClickButtonRadioTV() +{ + PromptAndSaveList(); + + m_iSelected = 0; + m_bMovingMode = false; + m_bAllowNewChannel = false; + m_bIsRadio = !m_bIsRadio; + SetProperty("IsRadio", m_bIsRadio ? "true" : ""); + Update(); + return true; +} + +bool CGUIDialogPVRChannelManager::OnClickButtonRadioActive() +{ + CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), RADIOBUTTON_ACTIVE); + if (OnMessage(msg)) + { + CFileItemPtr pItem = m_channelItems->Get(m_iSelected); + if (pItem) + { + const bool selected = (msg.GetParam1() == 1); + if (pItem->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean() != selected) + { + pItem->SetProperty(PROPERTY_CHANNEL_ENABLED, selected); + pItem->SetProperty(PROPERTY_CHANNEL_USER_SET_HIDDEN, true); + SetItemChanged(pItem); + Renumber(); + } + return true; + } + } + + return false; +} + +bool CGUIDialogPVRChannelManager::OnClickButtonRadioParentalLocked() +{ + CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), RADIOBUTTON_PARENTAL_LOCK); + if (!OnMessage(msg)) + return false; + + bool selected(msg.GetParam1() == 1); + + // ask for PIN first + if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalPIN() != + ParentalCheckResult::SUCCESS) + { // failed - reset to previous + SET_CONTROL_SELECTED(GetID(), RADIOBUTTON_PARENTAL_LOCK, !selected); + return false; + } + + CFileItemPtr pItem = m_channelItems->Get(m_iSelected); + if (pItem) + { + if (pItem->GetProperty(PROPERTY_CHANNEL_LOCKED).asBoolean() != selected) + { + pItem->SetProperty(PROPERTY_CHANNEL_LOCKED, selected); + SetItemChanged(pItem); + Renumber(); + } + return true; + } + + return false; +} + +bool CGUIDialogPVRChannelManager::OnClickButtonEditName() +{ + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), EDIT_NAME); + if (OnMessage(msg)) + { + CFileItemPtr pItem = m_channelItems->Get(m_iSelected); + if (pItem) + { + if (pItem->GetProperty(PROPERTY_CHANNEL_NAME).asString() != msg.GetLabel()) + { + pItem->SetProperty(PROPERTY_CHANNEL_NAME, msg.GetLabel()); + SetItemChanged(pItem); + } + return true; + } + } + + return false; +} + +bool CGUIDialogPVRChannelManager::OnClickButtonChannelLogo() +{ + CFileItemPtr pItem = m_channelItems->Get(m_iSelected); + if (!pItem) + return false; + + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + if (profileManager->GetCurrentProfile().canWriteSources() && !g_passwordManager.IsProfileLockUnlocked()) + return false; + + // setup our thumb list + CFileItemList items; + + // add the current thumb, if available + if (!pItem->GetProperty(PROPERTY_CHANNEL_ICON).asString().empty()) + { + CFileItemPtr current(new CFileItem("thumb://Current", false)); + current->SetArt("thumb", pItem->GetPVRChannelInfoTag()->IconPath()); + current->SetLabel(g_localizeStrings.Get(19282)); + items.Add(current); + } + else if (pItem->HasArt("thumb")) + { // already have a thumb that the share doesn't know about - must be a local one, so we mayaswell reuse it. + CFileItemPtr current(new CFileItem("thumb://Current", false)); + current->SetArt("thumb", pItem->GetArt("thumb")); + current->SetLabel(g_localizeStrings.Get(19282)); + items.Add(current); + } + + // and add a "no thumb" entry as well + CFileItemPtr nothumb(new CFileItem("thumb://None", false)); + nothumb->SetArt("icon", pItem->GetArt("icon")); + nothumb->SetLabel(g_localizeStrings.Get(19283)); + items.Add(nothumb); + + std::string strThumb; + VECSOURCES shares; + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + if (settings->GetString(CSettings::SETTING_PVRMENU_ICONPATH) != "") + { + CMediaSource share1; + share1.strPath = settings->GetString(CSettings::SETTING_PVRMENU_ICONPATH); + share1.strName = g_localizeStrings.Get(19066); + shares.push_back(share1); + } + CServiceBroker::GetMediaManager().GetLocalDrives(shares); + if (!CGUIDialogFileBrowser::ShowAndGetImage(items, shares, g_localizeStrings.Get(19285), strThumb, NULL, 19285)) + return false; + + if (strThumb == "thumb://Current") + return true; + + if (strThumb == "thumb://None") + strThumb = ""; + + if (pItem->GetProperty(PROPERTY_CHANNEL_ICON).asString() != strThumb) + { + pItem->SetProperty(PROPERTY_CHANNEL_ICON, strThumb); + pItem->SetProperty(PROPERTY_CHANNEL_CUSTOM_ICON, true); + SetItemChanged(pItem); + } + + return true; +} + +bool CGUIDialogPVRChannelManager::OnClickButtonUseEPG() +{ + CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), RADIOBUTTON_USEEPG); + if (OnMessage(msg)) + { + CFileItemPtr pItem = m_channelItems->Get(m_iSelected); + if (pItem) + { + const bool selected = (msg.GetParam1() == 1); + if (pItem->GetProperty(PROPERTY_CHANNEL_EPG_ENABLED).asBoolean() != selected) + { + pItem->SetProperty(PROPERTY_CHANNEL_EPG_ENABLED, selected); + SetItemChanged(pItem); + } + return true; + } + } + + return false; +} + +bool CGUIDialogPVRChannelManager::OnClickEPGSourceSpin() +{ + //! @todo Add EPG scraper support + return true; + // CGUISpinControlEx* pSpin = static_cast<CGUISpinControlEx*>(GetControl(SPIN_EPGSOURCE_SELECTION)); + // if (pSpin) + // { + // CFileItemPtr pItem = m_channelItems->Get(m_iSelected); + // if (pItem) + // { + // if (pItem->GetProperty(PROPERTY_CHANNEL_EPG_SOURCE).asInteger() != 0) + // { + // pItem->SetProperty(PROPERTY_CHANNEL_EPG_SOURCE, static_cast<int>(0)); + // SetItemChanged(pItem); + // } + // return true; + // } + // } +} + +bool CGUIDialogPVRChannelManager::OnClickButtonGroupManager() +{ + PromptAndSaveList(); + + /* Load group manager dialog */ + CGUIDialogPVRGroupManager* pDlgInfo = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGroupManager>(WINDOW_DIALOG_PVR_GROUP_MANAGER); + if (!pDlgInfo) + return false; + + pDlgInfo->SetRadio(m_bIsRadio); + + /* Open dialog window */ + pDlgInfo->Open(); + + Update(); + return true; +} + +bool CGUIDialogPVRChannelManager::OnClickButtonNewChannel() +{ + PromptAndSaveList(); + + int iSelection = 0; + if (m_clientsWithSettingsList.size() > 1) + { + CGUIDialogSelect* pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (!pDlgSelect) + return false; + + pDlgSelect->SetHeading(CVariant{19213}); // Select Client + + for (const auto& client : m_clientsWithSettingsList) + pDlgSelect->Add(client->Name()); + pDlgSelect->Open(); + + iSelection = pDlgSelect->GetSelectedItem(); + } + + if (iSelection >= 0 && iSelection < static_cast<int>(m_clientsWithSettingsList.size())) + { + int iClientID = m_clientsWithSettingsList[iSelection]->GetID(); + + std::shared_ptr<CPVRChannel> channel(new CPVRChannel(m_bIsRadio)); + channel->SetChannelName(g_localizeStrings.Get(19204)); // New channel + channel->SetClientID(iClientID); + + PVR_ERROR ret = PVR_ERROR_UNKNOWN; + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(iClientID); + if (client) + { + channel->SetEPGEnabled(client->GetClientCapabilities().SupportsEPG()); + ret = client->OpenDialogChannelAdd(channel); + } + + if (ret == PVR_ERROR_NO_ERROR) + { + CFileItemList prevChannelItems; + prevChannelItems.Assign(*m_channelItems); + + Update(); + + for (int index = 0; index < m_channelItems->Size(); ++index) + { + if (!prevChannelItems.Contains(m_channelItems->Get(index)->GetPath())) + { + m_iSelected = index; + m_viewControl.SetSelectedItem(m_iSelected); + SetData(m_iSelected); + break; + } + } + } + else if (ret == PVR_ERROR_NOT_IMPLEMENTED) + HELPERS::ShowOKDialogText(CVariant{19033}, CVariant{19038}); // "Information", "Not supported by the PVR backend." + else + HELPERS::ShowOKDialogText(CVariant{2103}, CVariant{16029}); // "Add-on error", "Check the log for more information about this message." + } + return true; +} + +bool CGUIDialogPVRChannelManager::OnClickButtonRefreshChannelLogos() +{ + for (const auto& item : *m_channelItems) + { + const std::string thumb = item->GetArt("thumb"); + if (!thumb.empty()) + { + // clear current cached image + CServiceBroker::GetTextureCache()->ClearCachedImage(thumb); + item->SetArt("thumb", ""); + } + } + + m_iSelected = 0; + Update(); + + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_REFRESH_THUMBS); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + + return true; +} + +bool CGUIDialogPVRChannelManager::OnMessageClick(const CGUIMessage& message) +{ + int iControl = message.GetSenderId(); + switch(iControl) + { + case CONTROL_LIST_CHANNELS: + return OnClickListChannels(message); + case BUTTON_OK: + return OnClickButtonOK(); + case BUTTON_APPLY: + return OnClickButtonApply(); + case BUTTON_CANCEL: + return OnClickButtonCancel(); + case BUTTON_RADIO_TV: + return OnClickButtonRadioTV(); + case RADIOBUTTON_ACTIVE: + return OnClickButtonRadioActive(); + case RADIOBUTTON_PARENTAL_LOCK: + return OnClickButtonRadioParentalLocked(); + case EDIT_NAME: + return OnClickButtonEditName(); + case BUTTON_CHANNEL_LOGO: + return OnClickButtonChannelLogo(); + case RADIOBUTTON_USEEPG: + return OnClickButtonUseEPG(); + case SPIN_EPGSOURCE_SELECTION: + return OnClickEPGSourceSpin(); + case BUTTON_GROUP_MANAGER: + return OnClickButtonGroupManager(); + case BUTTON_NEW_CHANNEL: + return OnClickButtonNewChannel(); + case BUTTON_REFRESH_LOGOS: + return OnClickButtonRefreshChannelLogos(); + default: + return false; + } +} + +bool CGUIDialogPVRChannelManager::OnMessage(CGUIMessage& message) +{ + unsigned int iMessage = message.GetMessage(); + + switch (iMessage) + { + case GUI_MSG_CLICKED: + return OnMessageClick(message); + } + + return CGUIDialog::OnMessage(message); +} + +void CGUIDialogPVRChannelManager::OnWindowLoaded() +{ + CGUIDialog::OnWindowLoaded(); + + m_viewControl.Reset(); + m_viewControl.SetParentWindow(GetID()); + m_viewControl.AddView(GetControl(CONTROL_LIST_CHANNELS)); +} + +void CGUIDialogPVRChannelManager::OnWindowUnload() +{ + CGUIDialog::OnWindowUnload(); + m_viewControl.Reset(); +} + +CFileItemPtr CGUIDialogPVRChannelManager::GetCurrentListItem(int offset) +{ + return m_channelItems->Get(m_iSelected); +} + +bool CGUIDialogPVRChannelManager::OnPopupMenu(int iItem) +{ + // popup the context menu + // grab our context menu + CContextButtons buttons; + + // mark the item + if (iItem >= 0 && iItem < m_channelItems->Size()) + m_channelItems->Get(iItem)->Select(true); + else + return false; + + CFileItemPtr pItem = m_channelItems->Get(iItem); + if (!pItem) + return false; + + if (m_bAllowReorder && + pItem->GetProperty(PROPERTY_CHANNEL_NUMBER).asString() != LABEL_CHANNEL_DISABLED) + buttons.Add(CONTEXT_BUTTON_MOVE, 116); /* Move channel up or down */ + + if (pItem->GetProperty(PROPERTY_CLIENT_SUPPORTS_SETTINGS).asBoolean()) + { + buttons.Add(CONTEXT_BUTTON_SETTINGS, 10004); /* Open add-on channel settings dialog */ + buttons.Add(CONTEXT_BUTTON_DELETE, 117); /* Delete add-on channel */ + } + + int choice = CGUIDialogContextMenu::ShowAndGetChoice(buttons); + + // deselect our item + if (iItem >= 0 && iItem < m_channelItems->Size()) + m_channelItems->Get(iItem)->Select(false); + + if (choice < 0) + return false; + + return OnContextButton(iItem, (CONTEXT_BUTTON)choice); +} + +bool CGUIDialogPVRChannelManager::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + /* Check file item is in list range and get his pointer */ + if (itemNumber < 0 || itemNumber >= m_channelItems->Size()) return false; + + CFileItemPtr pItem = m_channelItems->Get(itemNumber); + if (!pItem) + return false; + + if (button == CONTEXT_BUTTON_MOVE) + { + m_bMovingMode = true; + pItem->Select(true); + } + else if (button == CONTEXT_BUTTON_SETTINGS) + { + PromptAndSaveList(); + + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*pItem); + PVR_ERROR ret = PVR_ERROR_UNKNOWN; + if (client) + ret = client->OpenDialogChannelSettings(pItem->GetPVRChannelInfoTag()); + + if (ret == PVR_ERROR_NO_ERROR) + { + Update(); + SetData(m_iSelected); + } + else if (ret == PVR_ERROR_NOT_IMPLEMENTED) + HELPERS::ShowOKDialogText(CVariant{19033}, CVariant{19038}); // "Information", "Not supported by the PVR backend." + else + HELPERS::ShowOKDialogText(CVariant{2103}, CVariant{16029}); // "Add-on error", "Check the log for more information about this message." + } + else if (button == CONTEXT_BUTTON_DELETE) + { + CGUIDialogYesNo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO); + if (!pDialog) + return true; + + pDialog->SetHeading(CVariant{19211}); // Delete channel + pDialog->SetText(CVariant{750}); // Are you sure? + pDialog->Open(); + + if (pDialog->IsConfirmed()) + { + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*pItem); + if (client) + { + const std::shared_ptr<CPVRChannel> channel = pItem->GetPVRChannelInfoTag(); + PVR_ERROR ret = client->DeleteChannel(channel); + if (ret == PVR_ERROR_NO_ERROR) + { + CPVRChannelGroups* groups = + CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio); + if (groups) + { + groups->UpdateFromClients({}); + Update(); + } + } + else if (ret == PVR_ERROR_NOT_IMPLEMENTED) + HELPERS::ShowOKDialogText(CVariant{19033}, CVariant{19038}); // "Information", "Not supported by the PVR backend." + else + HELPERS::ShowOKDialogText(CVariant{2103}, CVariant{16029}); // "Add-on error", "Check the log for more information about this message." + } + } + } + return true; +} + +void CGUIDialogPVRChannelManager::SetData(int iItem) +{ + if (iItem < 0 || iItem >= m_channelItems->Size()) + { + ClearChannelOptions(); + EnableChannelOptions(false); + return; + } + + CFileItemPtr pItem = m_channelItems->Get(iItem); + if (!pItem) + return; + + SET_CONTROL_LABEL2(EDIT_NAME, pItem->GetProperty(PROPERTY_CHANNEL_NAME).asString()); + CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), EDIT_NAME, CGUIEditControl::INPUT_TYPE_TEXT, 19208); + OnMessage(msg); + + SET_CONTROL_SELECTED(GetID(), RADIOBUTTON_ACTIVE, + pItem->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean()); + SET_CONTROL_SELECTED(GetID(), RADIOBUTTON_USEEPG, + pItem->GetProperty(PROPERTY_CHANNEL_EPG_ENABLED).asBoolean()); + SET_CONTROL_SELECTED(GetID(), RADIOBUTTON_PARENTAL_LOCK, + pItem->GetProperty(PROPERTY_CHANNEL_LOCKED).asBoolean()); + + EnableChannelOptions(true); +} + +void CGUIDialogPVRChannelManager::Update() +{ + m_viewControl.SetCurrentView(CONTROL_LIST_CHANNELS); + + // empty the lists ready for population + Clear(); + + std::shared_ptr<CPVRChannelGroup> channels = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_bIsRadio); + + // No channels available, nothing to do. + if (!channels) + return; + + channels->UpdateFromClients({}); + + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers = channels->GetMembers(); + std::shared_ptr<CFileItem> channelFile; + for (const auto& member : groupMembers) + { + channelFile = std::make_shared<CFileItem>(member); + if (!channelFile) + continue; + const std::shared_ptr<CPVRChannel> channel(channelFile->GetPVRChannelInfoTag()); + + channelFile->SetProperty(PROPERTY_CHANNEL_ENABLED, !channel->IsHidden()); + channelFile->SetProperty(PROPERTY_CHANNEL_USER_SET_HIDDEN, channel->IsUserSetHidden()); + channelFile->SetProperty(PROPERTY_CHANNEL_NAME, channel->ChannelName()); + channelFile->SetProperty(PROPERTY_CHANNEL_EPG_ENABLED, channel->EPGEnabled()); + channelFile->SetProperty(PROPERTY_CHANNEL_ICON, channel->ClientIconPath()); + channelFile->SetProperty(PROPERTY_CHANNEL_CUSTOM_ICON, channel->IsUserSetIcon()); + channelFile->SetProperty(PROPERTY_CHANNEL_EPG_SOURCE, 0); + channelFile->SetProperty(PROPERTY_CHANNEL_LOCKED, channel->IsLocked()); + channelFile->SetProperty(PROPERTY_CHANNEL_NUMBER, + member->ChannelNumber().FormattedChannelNumber()); + + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*channelFile); + if (client) + { + channelFile->SetProperty(PROPERTY_CLIENT_NAME, client->GetFriendlyName()); + channelFile->SetProperty(PROPERTY_CLIENT_SUPPORTS_SETTINGS, + client->GetClientCapabilities().SupportsChannelSettings()); + } + + m_channelItems->Add(channelFile); + } + + { + std::vector< std::pair<std::string, int> > labels; + labels.emplace_back(g_localizeStrings.Get(19210), 0); + //! @todo Add Labels for EPG scrapers here + SET_CONTROL_LABELS(SPIN_EPGSOURCE_SELECTION, 0, &labels); + } + + m_clientsWithSettingsList = CServiceBroker::GetPVRManager().Clients()->GetClientsSupportingChannelSettings(m_bIsRadio); + if (!m_clientsWithSettingsList.empty()) + m_bAllowNewChannel = true; + + if (m_bAllowNewChannel) + CONTROL_ENABLE(BUTTON_NEW_CHANNEL); + else + CONTROL_DISABLE(BUTTON_NEW_CHANNEL); + + Renumber(); + m_viewControl.SetItems(*m_channelItems); + if (m_iSelected >= m_channelItems->Size()) + m_iSelected = m_channelItems->Size() - 1; + m_viewControl.SetSelectedItem(m_iSelected); + SetData(m_iSelected); +} + +void CGUIDialogPVRChannelManager::Clear() +{ + m_viewControl.Clear(); + m_channelItems->Clear(); + + ClearChannelOptions(); + EnableChannelOptions(false); + + CONTROL_DISABLE(BUTTON_APPLY); +} + +void CGUIDialogPVRChannelManager::ClearChannelOptions() +{ + CONTROL_DESELECT(RADIOBUTTON_ACTIVE); + SET_CONTROL_LABEL2(EDIT_NAME, ""); + SET_CONTROL_FILENAME(BUTTON_CHANNEL_LOGO, ""); + CONTROL_DESELECT(RADIOBUTTON_USEEPG); + + std::vector<std::pair<std::string, int>> labels = {{g_localizeStrings.Get(19210), 0}}; + SET_CONTROL_LABELS(SPIN_EPGSOURCE_SELECTION, 0, &labels); + + CONTROL_DESELECT(RADIOBUTTON_PARENTAL_LOCK); +} + +void CGUIDialogPVRChannelManager::EnableChannelOptions(bool bEnable) +{ + if (bEnable) + { + CONTROL_ENABLE(RADIOBUTTON_ACTIVE); + CONTROL_ENABLE(EDIT_NAME); + CONTROL_ENABLE(BUTTON_CHANNEL_LOGO); + CONTROL_ENABLE(IMAGE_CHANNEL_LOGO); + CONTROL_ENABLE(RADIOBUTTON_USEEPG); + CONTROL_ENABLE(SPIN_EPGSOURCE_SELECTION); + CONTROL_ENABLE(RADIOBUTTON_PARENTAL_LOCK); + } + else + { + CONTROL_DISABLE(RADIOBUTTON_ACTIVE); + CONTROL_DISABLE(EDIT_NAME); + CONTROL_DISABLE(BUTTON_CHANNEL_LOGO); + CONTROL_DISABLE(IMAGE_CHANNEL_LOGO); + CONTROL_DISABLE(RADIOBUTTON_USEEPG); + CONTROL_DISABLE(SPIN_EPGSOURCE_SELECTION); + CONTROL_DISABLE(RADIOBUTTON_PARENTAL_LOCK); + } +} + +void CGUIDialogPVRChannelManager::RenameChannel(const CFileItemPtr& pItem) +{ + std::string strChannelName = pItem->GetProperty(PROPERTY_CHANNEL_NAME).asString(); + if (strChannelName != pItem->GetPVRChannelInfoTag()->ChannelName()) + { + std::shared_ptr<CPVRChannel> channel = pItem->GetPVRChannelInfoTag(); + channel->SetChannelName(strChannelName); + + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*pItem); + if (!client || (client->RenameChannel(channel) != PVR_ERROR_NO_ERROR)) + HELPERS::ShowOKDialogText(CVariant{2103}, CVariant{16029}); // Add-on error;Check the log file for details. + } +} + +bool CGUIDialogPVRChannelManager::PersistChannel(const CFileItemPtr& pItem, + const std::shared_ptr<CPVRChannelGroup>& group) +{ + if (!pItem || !group) + return false; + + return group->UpdateChannel( + pItem->GetPVRChannelInfoTag()->StorageId(), + pItem->GetProperty(PROPERTY_CHANNEL_NAME).asString(), + pItem->GetProperty(PROPERTY_CHANNEL_ICON).asString(), + static_cast<int>(pItem->GetProperty(PROPERTY_CHANNEL_EPG_SOURCE).asInteger()), + m_bAllowRenumber ? pItem->GetProperty(PROPERTY_CHANNEL_NUMBER).asInteger() : 0, + !pItem->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean(), // hidden + pItem->GetProperty(PROPERTY_CHANNEL_EPG_ENABLED).asBoolean(), + pItem->GetProperty(PROPERTY_CHANNEL_LOCKED).asBoolean(), + pItem->GetProperty(PROPERTY_CHANNEL_CUSTOM_ICON).asBoolean(), + pItem->GetProperty(PROPERTY_CHANNEL_USER_SET_HIDDEN).asBoolean()); +} + +void CGUIDialogPVRChannelManager::PromptAndSaveList() +{ + if (!HasChangedItems()) + return; + + CGUIDialogYesNo* pDialogYesNo = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO); + if (pDialogYesNo) + { + pDialogYesNo->SetHeading(CVariant{20052}); + pDialogYesNo->SetLine(0, CVariant{""}); + pDialogYesNo->SetLine(1, CVariant{19212}); + pDialogYesNo->SetLine(2, CVariant{20103}); + pDialogYesNo->Open(); + + if (pDialogYesNo->IsConfirmed()) + SaveList(); + else + Update(); + } +} + +void CGUIDialogPVRChannelManager::SaveList() +{ + if (!HasChangedItems()) + return; + + /* display the progress dialog */ + CGUIDialogProgress* pDlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + pDlgProgress->SetHeading(CVariant{190}); + pDlgProgress->SetLine(0, CVariant{""}); + pDlgProgress->SetLine(1, CVariant{328}); + pDlgProgress->SetLine(2, CVariant{""}); + pDlgProgress->Open(); + pDlgProgress->Progress(); + pDlgProgress->SetPercentage(0); + + /* persist all channels */ + std::shared_ptr<CPVRChannelGroup> group = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_bIsRadio); + if (!group) + return; + + for (int iListPtr = 0; iListPtr < m_channelItems->Size(); ++iListPtr) + { + CFileItemPtr pItem = m_channelItems->Get(iListPtr); + if (pItem && pItem->GetProperty(PROPERTY_ITEM_CHANGED).asBoolean()) + { + if (pItem->GetProperty(PROPERTY_CLIENT_SUPPORTS_SETTINGS).asBoolean()) + RenameChannel(pItem); + + if (PersistChannel(pItem, group)) + pItem->SetProperty(PROPERTY_ITEM_CHANGED, false); + } + + pDlgProgress->SetPercentage(iListPtr * 100 / m_channelItems->Size()); + } + + group->SortAndRenumber(); + + auto channelGroups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio); + channelGroups->UpdateChannelNumbersFromAllChannelsGroup(); + channelGroups->PersistAll(); + pDlgProgress->Close(); + + CONTROL_DISABLE(BUTTON_APPLY); +} + +bool CGUIDialogPVRChannelManager::HasChangedItems() const +{ + return std::any_of(m_channelItems->cbegin(), m_channelItems->cend(), [](const auto& item) { + return item && item->GetProperty(PROPERTY_ITEM_CHANGED).asBoolean(); + }); +} + +namespace +{ + +bool IsItemChanged(const std::shared_ptr<CFileItem>& item) +{ + const std::shared_ptr<CPVRChannelGroupMember> member = item->GetPVRChannelGroupMemberInfoTag(); + const std::shared_ptr<CPVRChannel> channel = member->Channel(); + + return item->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean() == channel->IsHidden() || + item->GetProperty(PROPERTY_CHANNEL_USER_SET_HIDDEN).asBoolean() != + channel->IsUserSetHidden() || + item->GetProperty(PROPERTY_CHANNEL_NAME).asString() != channel->ChannelName() || + item->GetProperty(PROPERTY_CHANNEL_EPG_ENABLED).asBoolean() != channel->EPGEnabled() || + item->GetProperty(PROPERTY_CHANNEL_ICON).asString() != channel->ClientIconPath() || + item->GetProperty(PROPERTY_CHANNEL_CUSTOM_ICON).asBoolean() != channel->IsUserSetIcon() || + item->GetProperty(PROPERTY_CHANNEL_EPG_SOURCE).asInteger() != 0 || + item->GetProperty(PROPERTY_CHANNEL_LOCKED).asBoolean() != channel->IsLocked() || + item->GetProperty(PROPERTY_CHANNEL_NUMBER).asString() != + member->ChannelNumber().FormattedChannelNumber(); +} + +} // namespace + +void CGUIDialogPVRChannelManager::SetItemChanged(const CFileItemPtr& pItem) +{ + const bool changed = IsItemChanged(pItem); + pItem->SetProperty(PROPERTY_ITEM_CHANGED, changed); + + if (changed || HasChangedItems()) + CONTROL_ENABLE(BUTTON_APPLY); + else + CONTROL_DISABLE(BUTTON_APPLY); +} + +void CGUIDialogPVRChannelManager::Renumber() +{ + if (!m_bAllowRenumber) + return; + + int iNextChannelNumber = 0; + for (const auto& item : *m_channelItems) + { + const std::string number = item->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean() + ? std::to_string(++iNextChannelNumber) + : LABEL_CHANNEL_DISABLED; + + if (item->GetProperty(PROPERTY_CHANNEL_NUMBER).asString() != number) + { + item->SetProperty(PROPERTY_CHANNEL_NUMBER, number); + SetItemChanged(item); + } + } +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.h b/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.h new file mode 100644 index 0000000..0716436 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.h @@ -0,0 +1,95 @@ +/* + * 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 "dialogs/GUIDialogContextMenu.h" +#include "guilib/GUIDialog.h" +#include "view/GUIViewControl.h" + +#include <memory> +#include <vector> + +class CAction; +class CFileItemList; +class CGUIMessage; + +namespace PVR +{ + class CPVRChannelGroup; + class CPVRClient; + + class CGUIDialogPVRChannelManager : public CGUIDialog + { + public: + CGUIDialogPVRChannelManager(); + ~CGUIDialogPVRChannelManager() override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction& action) override; + void OnWindowLoaded() override; + void OnWindowUnload() override; + bool HasListItems() const override{ return true; } + CFileItemPtr GetCurrentListItem(int offset = 0) override; + + void Open(const std::shared_ptr<CFileItem>& initialSelection); + void SetRadio(bool bIsRadio); + + protected: + void OnInitWindow() override; + void OnDeinitWindow(int nextWindowID) override; + + private: + void Clear(); + void Update(); + void PromptAndSaveList(); + void SaveList(); + void Renumber(); + void SetData(int iItem); + void RenameChannel(const CFileItemPtr& pItem); + + void ClearChannelOptions(); + void EnableChannelOptions(bool bEnable); + + bool OnPopupMenu(int iItem); + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button); + bool OnActionMove(const CAction& action); + bool OnMessageClick(const CGUIMessage& message); + bool OnClickListChannels(const CGUIMessage& message); + bool OnClickButtonOK(); + bool OnClickButtonApply(); + bool OnClickButtonCancel(); + bool OnClickButtonRadioTV(); + bool OnClickButtonRadioActive(); + bool OnClickButtonRadioParentalLocked(); + bool OnClickButtonEditName(); + bool OnClickButtonChannelLogo(); + bool OnClickButtonUseEPG(); + bool OnClickEPGSourceSpin(); + bool OnClickButtonGroupManager(); + bool OnClickButtonNewChannel(); + bool OnClickButtonRefreshChannelLogos(); + + bool PersistChannel(const CFileItemPtr& pItem, const std::shared_ptr<CPVRChannelGroup>& group); + + bool HasChangedItems() const; + void SetItemChanged(const CFileItemPtr& pItem); + + bool m_bIsRadio = false; + bool m_bMovingMode = false; + bool m_bAllowNewChannel = false; + bool m_bAllowRenumber = false; + bool m_bAllowReorder = false; + + std::shared_ptr<CFileItem> m_initialSelection; + int m_iSelected = 0; + CFileItemList* m_channelItems; + CGUIViewControl m_viewControl; + + std::vector<std::shared_ptr<CPVRClient>> m_clientsWithSettingsList; + }; +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.cpp b/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.cpp new file mode 100644 index 0000000..b5830d4 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.cpp @@ -0,0 +1,299 @@ +/* + * 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 "GUIDialogPVRChannelsOSD.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIMessage.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" + +#include <memory> +#include <string> +#include <vector> + +using namespace PVR; + +using namespace std::chrono_literals; + +#define MAX_INVALIDATION_FREQUENCY 2000ms // limit to one invalidation per X milliseconds + +CGUIDialogPVRChannelsOSD::CGUIDialogPVRChannelsOSD() + : CGUIDialogPVRItemsViewBase(WINDOW_DIALOG_PVR_OSD_CHANNELS, "DialogPVRChannelsOSD.xml") +{ + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().RegisterChannelNumberInputHandler(this); +} + +CGUIDialogPVRChannelsOSD::~CGUIDialogPVRChannelsOSD() +{ + auto& mgr = CServiceBroker::GetPVRManager(); + mgr.Events().Unsubscribe(this); + mgr.Get<PVR::GUI::Channels>().DeregisterChannelNumberInputHandler(this); +} + +bool CGUIDialogPVRChannelsOSD::OnMessage(CGUIMessage& message) +{ + if (message.GetMessage() == GUI_MSG_REFRESH_LIST) + { + switch (static_cast<PVREvent>(message.GetParam1())) + { + case PVREvent::CurrentItem: + m_viewControl.SetItems(*m_vecItems); + return true; + + case PVREvent::Epg: + case PVREvent::EpgContainer: + case PVREvent::EpgActiveItem: + if (IsActive()) + SetInvalid(); + return true; + + default: + break; + } + } + return CGUIDialogPVRItemsViewBase::OnMessage(message); +} + +void CGUIDialogPVRChannelsOSD::OnInitWindow() +{ + if (!CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV() && + !CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio()) + { + Close(); + return; + } + + Init(); + Update(); + CGUIDialogPVRItemsViewBase::OnInitWindow(); +} + +void CGUIDialogPVRChannelsOSD::OnDeinitWindow(int nextWindowID) +{ + if (m_group) + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().SetSelectedChannelPath( + m_group->IsRadio(), m_viewControl.GetSelectedItemPath()); + + // next OnInitWindow will set the group which is then selected + m_group.reset(); + } + + CGUIDialogPVRItemsViewBase::OnDeinitWindow(nextWindowID); +} + +bool CGUIDialogPVRChannelsOSD::OnAction(const CAction& action) +{ + switch (action.GetID()) + { + case ACTION_SELECT_ITEM: + case ACTION_MOUSE_LEFT_CLICK: + { + // If direct channel number input is active, select the entered channel. + if (CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNumberInputHandler() + .CheckInputAndExecuteAction()) + return true; + + if (m_viewControl.HasControl(GetFocusedControlID())) + { + // Switch to channel + GotoChannel(m_viewControl.GetSelectedItem()); + return true; + } + break; + } + case ACTION_PREVIOUS_CHANNELGROUP: + case ACTION_NEXT_CHANNELGROUP: + { + // save control states and currently selected item of group + SaveControlStates(); + + // switch to next or previous group + const CPVRChannelGroups* groups = + CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_group->IsRadio()); + const std::shared_ptr<CPVRChannelGroup> nextGroup = action.GetID() == ACTION_NEXT_CHANNELGROUP + ? groups->GetNextGroup(*m_group) + : groups->GetPreviousGroup(*m_group); + CServiceBroker::GetPVRManager().PlaybackState()->SetActiveChannelGroup(nextGroup); + m_group = nextGroup; + Init(); + Update(); + + // restore control states and previously selected item of group + RestoreControlStates(); + return true; + } + case REMOTE_0: + case REMOTE_1: + case REMOTE_2: + case REMOTE_3: + case REMOTE_4: + case REMOTE_5: + case REMOTE_6: + case REMOTE_7: + case REMOTE_8: + case REMOTE_9: + { + AppendChannelNumberCharacter(static_cast<char>(action.GetID() - REMOTE_0) + '0'); + return true; + } + case ACTION_CHANNEL_NUMBER_SEP: + { + AppendChannelNumberCharacter(CPVRChannelNumber::SEPARATOR); + return true; + } + default: + break; + } + + return CGUIDialogPVRItemsViewBase::OnAction(action); +} + +void CGUIDialogPVRChannelsOSD::Update() +{ + CPVRManager& pvrMgr = CServiceBroker::GetPVRManager(); + pvrMgr.Events().Subscribe(this, &CGUIDialogPVRChannelsOSD::Notify); + + const std::shared_ptr<CPVRChannel> channel = pvrMgr.PlaybackState()->GetPlayingChannel(); + if (channel) + { + const std::shared_ptr<CPVRChannelGroup> group = + pvrMgr.PlaybackState()->GetActiveChannelGroup(channel->IsRadio()); + if (group) + { + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers = + group->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE); + for (const auto& groupMember : groupMembers) + { + m_vecItems->Add(std::make_shared<CFileItem>(groupMember)); + } + + m_viewControl.SetItems(*m_vecItems); + + if (!m_group) + { + m_group = group; + m_viewControl.SetSelectedItem( + pvrMgr.Get<PVR::GUI::Channels>().GetSelectedChannelPath(channel->IsRadio())); + SaveSelectedItemPath(group->GroupID()); + } + } + } +} + +void CGUIDialogPVRChannelsOSD::SetInvalid() +{ + if (m_refreshTimeout.IsTimePast()) + { + for (const auto& item : *m_vecItems) + item->SetInvalid(); + + CGUIDialogPVRItemsViewBase::SetInvalid(); + m_refreshTimeout.Set(MAX_INVALIDATION_FREQUENCY); + } +} + +void CGUIDialogPVRChannelsOSD::SaveControlStates() +{ + CGUIDialogPVRItemsViewBase::SaveControlStates(); + + if (m_group) + SaveSelectedItemPath(m_group->GroupID()); +} + +void CGUIDialogPVRChannelsOSD::RestoreControlStates() +{ + CGUIDialogPVRItemsViewBase::RestoreControlStates(); + + if (m_group) + { + const std::string path = GetLastSelectedItemPath(m_group->GroupID()); + if (path.empty()) + m_viewControl.SetSelectedItem(0); + else + m_viewControl.SetSelectedItem(path); + } +} + +void CGUIDialogPVRChannelsOSD::GotoChannel(int iItem) +{ + if (iItem < 0 || iItem >= m_vecItems->Size()) + return; + + // Preserve the item before closing self, because this will clear m_vecItems + const std::shared_ptr<CFileItem> item = m_vecItems->Get(iItem); + + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_PVRMENU_CLOSECHANNELOSDONSWITCH)) + Close(); + + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + *item, true /* bCheckResume */); +} + +void CGUIDialogPVRChannelsOSD::Notify(const PVREvent& event) +{ + const CGUIMessage m(GUI_MSG_REFRESH_LIST, GetID(), 0, static_cast<int>(event)); + CServiceBroker::GetAppMessenger()->SendGUIMessage(m); +} + +void CGUIDialogPVRChannelsOSD::SaveSelectedItemPath(int iGroupID) +{ + m_groupSelectedItemPaths[iGroupID] = m_viewControl.GetSelectedItemPath(); +} + +std::string CGUIDialogPVRChannelsOSD::GetLastSelectedItemPath(int iGroupID) const +{ + const auto it = m_groupSelectedItemPaths.find(iGroupID); + if (it != m_groupSelectedItemPaths.end()) + return it->second; + + return std::string(); +} + +void CGUIDialogPVRChannelsOSD::GetChannelNumbers(std::vector<std::string>& channelNumbers) +{ + if (m_group) + m_group->GetChannelNumbers(channelNumbers); +} + +void CGUIDialogPVRChannelsOSD::OnInputDone() +{ + const CPVRChannelNumber channelNumber = GetChannelNumber(); + if (channelNumber.IsValid()) + { + int itemIndex = 0; + for (const CFileItemPtr& channel : *m_vecItems) + { + if (channel->GetPVRChannelGroupMemberInfoTag()->ChannelNumber() == channelNumber) + { + m_viewControl.SetSelectedItem(itemIndex); + return; + } + ++itemIndex; + } + } +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.h b/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.h new file mode 100644 index 0000000..c23fb92 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.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 "pvr/PVRChannelNumberInputHandler.h" +#include "pvr/dialogs/GUIDialogPVRItemsViewBase.h" +#include "threads/SystemClock.h" + +#include <map> +#include <memory> +#include <string> + +namespace PVR +{ +enum class PVREvent; + +class CPVRChannelGroup; + +class CGUIDialogPVRChannelsOSD : public CGUIDialogPVRItemsViewBase, + public CPVRChannelNumberInputHandler +{ +public: + CGUIDialogPVRChannelsOSD(); + ~CGUIDialogPVRChannelsOSD() override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction& action) override; + + /*! + * @brief CEventStream callback for PVR events. + * @param event The event. + */ + void Notify(const PVREvent& event); + + // CPVRChannelNumberInputHandler implementation + void GetChannelNumbers(std::vector<std::string>& channelNumbers) override; + void OnInputDone() override; + +protected: + void OnInitWindow() override; + void OnDeinitWindow(int nextWindowID) override; + void RestoreControlStates() override; + void SaveControlStates() override; + void SetInvalid() override; + +private: + void GotoChannel(int iItem); + void Update(); + void SaveSelectedItemPath(int iGroupID); + std::string GetLastSelectedItemPath(int iGroupID) const; + + std::shared_ptr<CPVRChannelGroup> m_group; + std::map<int, std::string> m_groupSelectedItemPaths; + XbmcThreads::EndTime<> m_refreshTimeout; +}; +} // namespace PVR diff --git a/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.cpp b/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.cpp new file mode 100644 index 0000000..ca83ba1 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIDialogPVRClientPriorities.h" + +#include "ServiceBroker.h" +#include "guilib/GUIMessage.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "settings/lib/Setting.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <cstdlib> +#include <memory> + +using namespace PVR; + +CGUIDialogPVRClientPriorities::CGUIDialogPVRClientPriorities() : + CGUIDialogSettingsManualBase(WINDOW_DIALOG_PVR_CLIENT_PRIORITIES, "DialogSettings.xml") +{ + m_loadType = LOAD_EVERY_TIME; +} + +void CGUIDialogPVRClientPriorities::SetupView() +{ + CGUIDialogSettingsManualBase::SetupView(); + + SetHeading(19240); // Client priorities + SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON); + SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186); // OK + SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222); // Cancel +} + +std::string CGUIDialogPVRClientPriorities::GetSettingsLabel( + const std::shared_ptr<ISetting>& pSetting) +{ + int iClientId = std::atoi(pSetting->GetId().c_str()); + auto clientEntry = m_clients.find(iClientId); + if (clientEntry != m_clients.end()) + return clientEntry->second->GetFriendlyName(); + + CLog::LogF(LOGERROR, "Unable to obtain pvr client with id '{}'", iClientId); + return CGUIDialogSettingsManualBase::GetLocalizedString(13205); // Unknown +} + +void CGUIDialogPVRClientPriorities::InitializeSettings() +{ + CGUIDialogSettingsManualBase::InitializeSettings(); + + const std::shared_ptr<CSettingCategory> category = AddCategory("pvrclientpriorities", -1); + if (category == nullptr) + { + CLog::LogF(LOGERROR, "Unable to add settings category"); + return; + } + + const std::shared_ptr<CSettingGroup> group = AddGroup(category); + if (group == nullptr) + { + CLog::LogF(LOGERROR, "Unable to add settings group"); + return; + } + + m_clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients(); + for (const auto& client : m_clients) + { + AddEdit(group, std::to_string(client.second->GetID()), 13205 /* Unknown */, SettingLevel::Basic, + client.second->GetPriority()); + } +} + +void CGUIDialogPVRClientPriorities::OnSettingChanged(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == nullptr) + { + CLog::LogF(LOGERROR, "No setting"); + return; + } + + CGUIDialogSettingsManualBase::OnSettingChanged(setting); + + m_changedValues[setting->GetId()] = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); +} + +bool CGUIDialogPVRClientPriorities::Save() +{ + for (const auto& changedClient : m_changedValues) + { + int iClientId = std::atoi(changedClient.first.c_str()); + auto clientEntry = m_clients.find(iClientId); + if (clientEntry != m_clients.end()) + clientEntry->second->SetPriority(changedClient.second); + } + + return true; +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.h b/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.h new file mode 100644 index 0000000..a6ca62d --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/addons/PVRClients.h" +#include "settings/dialogs/GUIDialogSettingsManualBase.h" + +#include <map> +#include <string> + +namespace PVR +{ + class CGUIDialogPVRClientPriorities : public CGUIDialogSettingsManualBase + { + public: + CGUIDialogPVRClientPriorities(); + + protected: + // implementation of ISettingCallback + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + + // specialization of CGUIDialogSettingsBase + std::string GetSettingsLabel(const std::shared_ptr<ISetting>& pSetting) override; + bool AllowResettingSettings() const override { return false; } + bool Save() override; + void SetupView() override; + + // specialization of CGUIDialogSettingsManualBase + void InitializeSettings() override; + + private: + CPVRClientMap m_clients; + std::map<std::string, int> m_changedValues; + }; +} // namespace PVR diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.cpp b/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.cpp new file mode 100644 index 0000000..dbc37f3 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.cpp @@ -0,0 +1,546 @@ +/* + * 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 "GUIDialogPVRGroupManager.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIMessage.h" +#include "guilib/GUIRadioButtonControl.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/helpers//DialogOKHelper.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/filesystem/PVRGUIDirectory.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +#include <memory> +#include <string> +#include <vector> + +using namespace KODI::MESSAGING; +using namespace PVR; + +#define CONTROL_LIST_CHANNELS_LEFT 11 +#define CONTROL_LIST_CHANNELS_RIGHT 12 +#define CONTROL_LIST_CHANNEL_GROUPS 13 +#define CONTROL_CURRENT_GROUP_LABEL 20 +#define CONTROL_UNGROUPED_LABEL 21 +#define CONTROL_IN_GROUP_LABEL 22 +#define BUTTON_HIDE_GROUP 25 +#define BUTTON_NEWGROUP 26 +#define BUTTON_RENAMEGROUP 27 +#define BUTTON_DELGROUP 28 +#define BUTTON_OK 29 +#define BUTTON_TOGGLE_RADIO_TV 34 +#define BUTTON_RECREATE_GROUP_THUMB 35 + +CGUIDialogPVRGroupManager::CGUIDialogPVRGroupManager() : + CGUIDialog(WINDOW_DIALOG_PVR_GROUP_MANAGER, "DialogPVRGroupManager.xml") +{ + m_ungroupedChannels = new CFileItemList; + m_groupMembers = new CFileItemList; + m_channelGroups = new CFileItemList; + + SetRadio(false); +} + +CGUIDialogPVRGroupManager::~CGUIDialogPVRGroupManager() +{ + delete m_ungroupedChannels; + delete m_groupMembers; + delete m_channelGroups; +} + +void CGUIDialogPVRGroupManager::SetRadio(bool bIsRadio) +{ + m_bIsRadio = bIsRadio; + SetProperty("IsRadio", m_bIsRadio ? "true" : ""); +} + +bool CGUIDialogPVRGroupManager::PersistChanges() +{ + return CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio)->PersistAll(); +} + +bool CGUIDialogPVRGroupManager::ActionButtonOk(const CGUIMessage& message) +{ + bool bReturn = false; + unsigned int iControl = message.GetSenderId(); + + if (iControl == BUTTON_OK) + { + PersistChanges(); + Close(); + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRGroupManager::ActionButtonNewGroup(const CGUIMessage& message) +{ + bool bReturn = false; + unsigned int iControl = message.GetSenderId(); + + if (iControl == BUTTON_NEWGROUP) + { + std::string strGroupName = ""; + /* prompt for a group name */ + if (CGUIKeyboardFactory::ShowAndGetInput(strGroupName, CVariant{g_localizeStrings.Get(19139)}, false)) + { + if (strGroupName != "") + { + /* add the group if it doesn't already exist */ + CPVRChannelGroups* groups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio); + if (groups->AddGroup(strGroupName)) + { + CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio)->GetByName(strGroupName)->SetGroupType(PVR_GROUP_TYPE_USER_DEFINED); + m_iSelectedChannelGroup = groups->Size() - 1; + Update(); + } + } + } + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRGroupManager::ActionButtonDeleteGroup(const CGUIMessage& message) +{ + bool bReturn = false; + unsigned int iControl = message.GetSenderId(); + + if (iControl == BUTTON_DELGROUP) + { + if (!m_selectedGroup) + return bReturn; + + CGUIDialogYesNo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO); + if (!pDialog) + return bReturn; + + pDialog->SetHeading(CVariant{117}); + pDialog->SetLine(0, CVariant{""}); + pDialog->SetLine(1, CVariant{m_selectedGroup->GroupName()}); + pDialog->SetLine(2, CVariant{""}); + pDialog->Open(); + + if (pDialog->IsConfirmed()) + { + ClearSelectedGroupsThumbnail(); + if (CServiceBroker::GetPVRManager() + .ChannelGroups() + ->Get(m_bIsRadio) + ->DeleteGroup(m_selectedGroup)) + Update(); + } + + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRGroupManager::ActionButtonRenameGroup(const CGUIMessage& message) +{ + bool bReturn = false; + unsigned int iControl = message.GetSenderId(); + + if (iControl == BUTTON_RENAMEGROUP) + { + if (!m_selectedGroup) + return bReturn; + + std::string strGroupName(m_selectedGroup->GroupName()); + if (CGUIKeyboardFactory::ShowAndGetInput(strGroupName, CVariant{g_localizeStrings.Get(19139)}, false)) + { + if (!strGroupName.empty()) + { + ClearSelectedGroupsThumbnail(); + m_selectedGroup->SetGroupName(strGroupName); + Update(); + } + } + + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRGroupManager::ActionButtonUngroupedChannels(const CGUIMessage& message) +{ + bool bReturn = false; + unsigned int iControl = message.GetSenderId(); + + if (m_viewUngroupedChannels.HasControl(iControl)) // list/thumb control + { + m_iSelectedUngroupedChannel = m_viewUngroupedChannels.GetSelectedItem(); + int iAction = message.GetParam1(); + + if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK) + { + if (m_channelGroups->GetFolderCount() == 0) + { + HELPERS::ShowOKDialogText(CVariant{19033}, CVariant{19137}); + } + else if (m_ungroupedChannels->GetFileCount() > 0) + { + CFileItemPtr pItemChannel = m_ungroupedChannels->Get(m_iSelectedUngroupedChannel); + + if (m_selectedGroup->AppendToGroup(pItemChannel->GetPVRChannelInfoTag())) + { + ClearSelectedGroupsThumbnail(); + Update(); + } + } + } + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRGroupManager::ActionButtonGroupMembers(const CGUIMessage& message) +{ + bool bReturn = false; + unsigned int iControl = message.GetSenderId(); + + if (m_viewGroupMembers.HasControl(iControl)) // list/thumb control + { + m_iSelectedGroupMember = m_viewGroupMembers.GetSelectedItem(); + int iAction = message.GetParam1(); + + if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK) + { + if (m_selectedGroup && m_groupMembers->GetFileCount() > 0) + { + CFileItemPtr pItemChannel = m_groupMembers->Get(m_iSelectedGroupMember); + m_selectedGroup->RemoveFromGroup(pItemChannel->GetPVRChannelInfoTag()); + ClearSelectedGroupsThumbnail(); + Update(); + } + } + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRGroupManager::ActionButtonChannelGroups(const CGUIMessage& message) +{ + bool bReturn = false; + unsigned int iControl = message.GetSenderId(); + + if (m_viewChannelGroups.HasControl(iControl)) // list/thumb control + { + int iAction = message.GetParam1(); + + if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK) + { + m_iSelectedChannelGroup = m_viewChannelGroups.GetSelectedItem(); + Update(); + } + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRGroupManager::ActionButtonHideGroup(const CGUIMessage& message) +{ + bool bReturn = false; + + if (message.GetSenderId() == BUTTON_HIDE_GROUP && m_selectedGroup) + { + CGUIRadioButtonControl* button = static_cast<CGUIRadioButtonControl*>(GetControl(message.GetSenderId())); + if (button) + { + CServiceBroker::GetPVRManager() + .ChannelGroups() + ->Get(m_bIsRadio) + ->HideGroup(m_selectedGroup, button->IsSelected()); + Update(); + } + + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRGroupManager::ActionButtonToggleRadioTV(const CGUIMessage& message) +{ + bool bReturn = false; + + if (message.GetSenderId() == BUTTON_TOGGLE_RADIO_TV) + { + PersistChanges(); + SetRadio(!m_bIsRadio); + Update(); + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRGroupManager::ActionButtonRecreateThumbnail(const CGUIMessage& message) +{ + bool bReturn = false; + + if (message.GetSenderId() == BUTTON_RECREATE_GROUP_THUMB) + { + m_thumbLoader.ClearCachedImages(*m_channelGroups); + Update(); + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRGroupManager::OnMessageClick(const CGUIMessage& message) +{ + return ActionButtonOk(message) || + ActionButtonNewGroup(message) || + ActionButtonDeleteGroup(message) || + ActionButtonRenameGroup(message) || + ActionButtonUngroupedChannels(message) || + ActionButtonGroupMembers(message) || + ActionButtonChannelGroups(message) || + ActionButtonHideGroup(message) || + ActionButtonToggleRadioTV(message) || + ActionButtonRecreateThumbnail(message); +} + +bool CGUIDialogPVRGroupManager::OnMessage(CGUIMessage& message) +{ + unsigned int iMessage = message.GetMessage(); + + switch (iMessage) + { + case GUI_MSG_CLICKED: + { + OnMessageClick(message); + } + break; + } + + return CGUIDialog::OnMessage(message); +} + +bool CGUIDialogPVRGroupManager::OnActionMove(const CAction& action) +{ + bool bReturn = false; + int iActionId = action.GetID(); + + if (GetFocusedControlID() == CONTROL_LIST_CHANNEL_GROUPS) + { + if (iActionId == ACTION_MOUSE_MOVE) + { + int iSelected = m_viewChannelGroups.GetSelectedItem(); + if (m_iSelectedChannelGroup < iSelected) + { + iActionId = ACTION_MOVE_DOWN; + } + else if (m_iSelectedChannelGroup > iSelected) + { + iActionId = ACTION_MOVE_UP; + } + else + { + return bReturn; + } + } + + if (iActionId == ACTION_MOVE_DOWN || iActionId == ACTION_MOVE_UP || + iActionId == ACTION_PAGE_DOWN || iActionId == ACTION_PAGE_UP || + iActionId == ACTION_FIRST_PAGE || iActionId == ACTION_LAST_PAGE) + { + CGUIDialog::OnAction(action); + int iSelected = m_viewChannelGroups.GetSelectedItem(); + + bReturn = true; + if (iSelected != m_iSelectedChannelGroup) + { + m_iSelectedChannelGroup = iSelected; + Update(); + } + } + } + + return bReturn; +} + +bool CGUIDialogPVRGroupManager::OnAction(const CAction& action) +{ + return OnActionMove(action) || + CGUIDialog::OnAction(action); +} + +void CGUIDialogPVRGroupManager::OnInitWindow() +{ + CGUIDialog::OnInitWindow(); + m_iSelectedUngroupedChannel = 0; + m_iSelectedGroupMember = 0; + m_iSelectedChannelGroup = 0; + Update(); +} + +void CGUIDialogPVRGroupManager::OnDeinitWindow(int nextWindowID) +{ + Clear(); + CGUIDialog::OnDeinitWindow(nextWindowID); +} + +void CGUIDialogPVRGroupManager::OnWindowLoaded() +{ + CGUIDialog::OnWindowLoaded(); + + m_viewUngroupedChannels.Reset(); + m_viewUngroupedChannels.SetParentWindow(GetID()); + m_viewUngroupedChannels.AddView(GetControl(CONTROL_LIST_CHANNELS_LEFT)); + + m_viewGroupMembers.Reset(); + m_viewGroupMembers.SetParentWindow(GetID()); + m_viewGroupMembers.AddView(GetControl(CONTROL_LIST_CHANNELS_RIGHT)); + + m_viewChannelGroups.Reset(); + m_viewChannelGroups.SetParentWindow(GetID()); + m_viewChannelGroups.AddView(GetControl(CONTROL_LIST_CHANNEL_GROUPS)); +} + +void CGUIDialogPVRGroupManager::OnWindowUnload() +{ + CGUIDialog::OnWindowUnload(); + m_viewUngroupedChannels.Reset(); + m_viewGroupMembers.Reset(); + m_viewChannelGroups.Reset(); +} + +void CGUIDialogPVRGroupManager::Update() +{ + m_viewUngroupedChannels.SetCurrentView(CONTROL_LIST_CHANNELS_LEFT); + m_viewGroupMembers.SetCurrentView(CONTROL_LIST_CHANNELS_RIGHT); + m_viewChannelGroups.SetCurrentView(CONTROL_LIST_CHANNEL_GROUPS); + + Clear(); + + // get the groups list + CPVRGUIDirectory::GetChannelGroupsDirectory(m_bIsRadio, false, *m_channelGroups); + + // Load group thumbnails + m_thumbLoader.Load(*m_channelGroups); + + m_viewChannelGroups.SetItems(*m_channelGroups); + m_viewChannelGroups.SetSelectedItem(m_iSelectedChannelGroup); + + /* select a group or select the default group if no group was selected */ + CFileItemPtr pItem = m_channelGroups->Get(m_viewChannelGroups.GetSelectedItem()); + m_selectedGroup = CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio)->GetByName(pItem->m_strTitle); + if (m_selectedGroup) + { + /* set this group in the pvrmanager, so it becomes the selected group in other dialogs too */ + CServiceBroker::GetPVRManager().PlaybackState()->SetActiveChannelGroup(m_selectedGroup); + SET_CONTROL_LABEL(CONTROL_CURRENT_GROUP_LABEL, m_selectedGroup->GroupName()); + SET_CONTROL_SELECTED(GetID(), BUTTON_HIDE_GROUP, m_selectedGroup->IsHidden()); + + CONTROL_ENABLE_ON_CONDITION(BUTTON_DELGROUP, !m_selectedGroup->IsInternalGroup()); + + if (m_selectedGroup->IsInternalGroup()) + { + std::string strNewLabel = StringUtils::Format("{} {}", g_localizeStrings.Get(19022), + m_bIsRadio ? g_localizeStrings.Get(19024) + : g_localizeStrings.Get(19023)); + SET_CONTROL_LABEL(CONTROL_UNGROUPED_LABEL, strNewLabel); + + strNewLabel = StringUtils::Format("{} {}", g_localizeStrings.Get(19218), + m_bIsRadio ? g_localizeStrings.Get(19024) + : g_localizeStrings.Get(19023)); + SET_CONTROL_LABEL(CONTROL_IN_GROUP_LABEL, strNewLabel); + } + else + { + std::string strNewLabel = g_localizeStrings.Get(19219); + SET_CONTROL_LABEL(CONTROL_UNGROUPED_LABEL, strNewLabel); + + strNewLabel = + StringUtils::Format("{} {}", g_localizeStrings.Get(19220), m_selectedGroup->GroupName()); + SET_CONTROL_LABEL(CONTROL_IN_GROUP_LABEL, strNewLabel); + } + + // Slightly different handling for "all" group... + if (m_selectedGroup->IsInternalGroup()) + { + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers = + m_selectedGroup->GetMembers(CPVRChannelGroup::Include::ALL); + for (const auto& groupMember : groupMembers) + { + if (groupMember->Channel()->IsHidden()) + m_ungroupedChannels->Add(std::make_shared<CFileItem>(groupMember)); + else + m_groupMembers->Add(std::make_shared<CFileItem>(groupMember)); + } + } + else + { + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers = + m_selectedGroup->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE); + for (const auto& groupMember : groupMembers) + { + m_groupMembers->Add(std::make_shared<CFileItem>(groupMember)); + } + + /* for the center part, get all channels of the "all" channels group that are not in this group */ + const std::shared_ptr<CPVRChannelGroup> allGroup = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_bIsRadio); + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> allGroupMembers = + allGroup->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE); + for (const auto& groupMember : allGroupMembers) + { + if (!m_selectedGroup->IsGroupMember(groupMember->Channel())) + m_ungroupedChannels->Add(std::make_shared<CFileItem>(groupMember)); + } + } + m_viewGroupMembers.SetItems(*m_groupMembers); + m_viewGroupMembers.SetSelectedItem(m_iSelectedGroupMember); + + m_viewUngroupedChannels.SetItems(*m_ungroupedChannels); + m_viewUngroupedChannels.SetSelectedItem(m_iSelectedUngroupedChannel); + } +} + +void CGUIDialogPVRGroupManager::Clear() +{ + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + + m_viewUngroupedChannels.Clear(); + m_viewGroupMembers.Clear(); + m_viewChannelGroups.Clear(); + + m_ungroupedChannels->Clear(); + m_groupMembers->Clear(); + m_channelGroups->Clear(); +} + +void CGUIDialogPVRGroupManager::ClearSelectedGroupsThumbnail() +{ + m_thumbLoader.ClearCachedImage(*m_channelGroups->Get(m_iSelectedChannelGroup)); +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.h b/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.h new file mode 100644 index 0000000..524205a --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.h @@ -0,0 +1,75 @@ +/* + * 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 "guilib/GUIDialog.h" +#include "pvr/PVRThumbLoader.h" +#include "view/GUIViewControl.h" + +#include <memory> + +class CFileItemList; +class CGUIMessage; + +namespace PVR +{ + class CPVRChannelGroup; + + class CGUIDialogPVRGroupManager : public CGUIDialog + { + public: + CGUIDialogPVRGroupManager(); + ~CGUIDialogPVRGroupManager() override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction& action) override; + void OnWindowLoaded() override; + void OnWindowUnload() override; + + void SetRadio(bool bIsRadio); + + protected: + void OnInitWindow() override; + void OnDeinitWindow(int nextWindowID) override; + + private: + void Clear(); + void ClearSelectedGroupsThumbnail(); + void Update(); + bool PersistChanges(); + bool ActionButtonOk(const CGUIMessage& message); + bool ActionButtonNewGroup(const CGUIMessage& message); + bool ActionButtonDeleteGroup(const CGUIMessage& message); + bool ActionButtonRenameGroup(const CGUIMessage& message); + bool ActionButtonUngroupedChannels(const CGUIMessage& message); + bool ActionButtonGroupMembers(const CGUIMessage& message); + bool ActionButtonChannelGroups(const CGUIMessage& message); + bool ActionButtonHideGroup(const CGUIMessage& message); + bool ActionButtonToggleRadioTV(const CGUIMessage& message); + bool ActionButtonRecreateThumbnail(const CGUIMessage& message); + bool OnMessageClick(const CGUIMessage& message); + bool OnActionMove(const CAction& action); + + std::shared_ptr<CPVRChannelGroup> m_selectedGroup; + bool m_bIsRadio; + + int m_iSelectedUngroupedChannel = 0; + int m_iSelectedGroupMember = 0; + int m_iSelectedChannelGroup = 0; + + CFileItemList * m_ungroupedChannels; + CFileItemList * m_groupMembers; + CFileItemList * m_channelGroups; + + CGUIViewControl m_viewUngroupedChannels; + CGUIViewControl m_viewGroupMembers; + CGUIViewControl m_viewChannelGroups; + + CPVRThumbLoader m_thumbLoader; + }; +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.cpp b/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.cpp new file mode 100644 index 0000000..2f41af5 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.cpp @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIDialogPVRGuideControls.h" + +using namespace PVR; + +CGUIDialogPVRGuideControls::CGUIDialogPVRGuideControls() + : CGUIDialog(WINDOW_DIALOG_PVR_GUIDE_CONTROLS, "DialogPVRGuideControls.xml") +{ + m_loadType = KEEP_IN_MEMORY; +} + +CGUIDialogPVRGuideControls::~CGUIDialogPVRGuideControls() = default; diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.h b/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.h new file mode 100644 index 0000000..9b81f2b --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "guilib/GUIDialog.h" + +namespace PVR +{ +class CGUIDialogPVRGuideControls : public CGUIDialog +{ +public: + CGUIDialogPVRGuideControls(); + ~CGUIDialogPVRGuideControls() override; +}; +} // namespace PVR diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.cpp b/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.cpp new file mode 100644 index 0000000..9bce21e --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.cpp @@ -0,0 +1,255 @@ +/* + * 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 "GUIDialogPVRGuideInfo.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "guilib/GUIMessage.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/guilib/PVRGUIActionsEPG.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "pvr/guilib/PVRGUIActionsTimers.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" + +#include <memory> + +using namespace PVR; + +#define CONTROL_BTN_FIND 4 +#define CONTROL_BTN_SWITCH 5 +#define CONTROL_BTN_RECORD 6 +#define CONTROL_BTN_OK 7 +#define CONTROL_BTN_PLAY_RECORDING 8 +#define CONTROL_BTN_ADD_TIMER 9 +#define CONTROL_BTN_PLAY_EPGTAG 10 +#define CONTROL_BTN_SET_REMINDER 11 + +CGUIDialogPVRGuideInfo::CGUIDialogPVRGuideInfo() + : CGUIDialog(WINDOW_DIALOG_PVR_GUIDE_INFO, "DialogPVRInfo.xml") +{ +} + +CGUIDialogPVRGuideInfo::~CGUIDialogPVRGuideInfo() = default; + +bool CGUIDialogPVRGuideInfo::OnClickButtonOK(const CGUIMessage& message) +{ + bool bReturn = false; + + if (message.GetSenderId() == CONTROL_BTN_OK) + { + Close(); + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRGuideInfo::OnClickButtonRecord(const CGUIMessage& message) +{ + bool bReturn = false; + + if (message.GetSenderId() == CONTROL_BTN_RECORD) + { + auto& mgr = CServiceBroker::GetPVRManager(); + + const std::shared_ptr<CPVRTimerInfoTag> timerTag = + mgr.Timers()->GetTimerForEpgTag(m_progItem->GetEPGInfoTag()); + if (timerTag) + { + if (timerTag->IsRecording()) + bReturn = mgr.Get<PVR::GUI::Timers>().StopRecording(CFileItem(timerTag)); + else + bReturn = mgr.Get<PVR::GUI::Timers>().DeleteTimer(CFileItem(timerTag)); + } + else + { + bReturn = mgr.Get<PVR::GUI::Timers>().AddTimer(*m_progItem, false); + } + } + + if (bReturn) + Close(); + + return bReturn; +} + +bool CGUIDialogPVRGuideInfo::OnClickButtonAddTimer(const CGUIMessage& message) +{ + bool bReturn = false; + + if (message.GetSenderId() == CONTROL_BTN_ADD_TIMER) + { + auto& mgr = CServiceBroker::GetPVRManager(); + if (m_progItem && !mgr.Timers()->GetTimerForEpgTag(m_progItem->GetEPGInfoTag())) + { + bReturn = mgr.Get<PVR::GUI::Timers>().AddTimerRule(*m_progItem, true, true); + } + } + + if (bReturn) + Close(); + + return bReturn; +} + +bool CGUIDialogPVRGuideInfo::OnClickButtonSetReminder(const CGUIMessage& message) +{ + bool bReturn = false; + + if (message.GetSenderId() == CONTROL_BTN_SET_REMINDER) + { + auto& mgr = CServiceBroker::GetPVRManager(); + if (m_progItem && !mgr.Timers()->GetTimerForEpgTag(m_progItem->GetEPGInfoTag())) + { + bReturn = mgr.Get<PVR::GUI::Timers>().AddReminder(*m_progItem); + } + } + + if (bReturn) + Close(); + + return bReturn; +} + +bool CGUIDialogPVRGuideInfo::OnClickButtonPlay(const CGUIMessage& message) +{ + bool bReturn = false; + + if (message.GetSenderId() == CONTROL_BTN_SWITCH || + message.GetSenderId() == CONTROL_BTN_PLAY_RECORDING || + message.GetSenderId() == CONTROL_BTN_PLAY_EPGTAG) + { + Close(); + + if (m_progItem) + { + if (message.GetSenderId() == CONTROL_BTN_PLAY_RECORDING) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording( + *m_progItem, true /* bCheckResume */); + else if (message.GetSenderId() == CONTROL_BTN_PLAY_EPGTAG && + m_progItem->GetEPGInfoTag()->IsPlayable()) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayEpgTag(*m_progItem); + else + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + *m_progItem, true /* bCheckResume */); + + bReturn = true; + } + } + + return bReturn; +} + +bool CGUIDialogPVRGuideInfo::OnClickButtonFind(const CGUIMessage& message) +{ + bool bReturn = false; + + if (message.GetSenderId() == CONTROL_BTN_FIND) + { + Close(); + if (m_progItem) + return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().FindSimilar(*m_progItem); + } + + return bReturn; +} + +bool CGUIDialogPVRGuideInfo::OnMessage(CGUIMessage& message) +{ + switch (message.GetMessage()) + { + case GUI_MSG_CLICKED: + return OnClickButtonOK(message) || OnClickButtonRecord(message) || + OnClickButtonPlay(message) || OnClickButtonFind(message) || + OnClickButtonAddTimer(message) || OnClickButtonSetReminder(message); + } + + return CGUIDialog::OnMessage(message); +} + +bool CGUIDialogPVRGuideInfo::OnInfo(int actionID) +{ + Close(); + return true; +} + +void CGUIDialogPVRGuideInfo::SetProgInfo(const std::shared_ptr<CFileItem>& item) +{ + m_progItem = item; +} + +CFileItemPtr CGUIDialogPVRGuideInfo::GetCurrentListItem(int offset) +{ + return m_progItem; +} + +void CGUIDialogPVRGuideInfo::OnInitWindow() +{ + CGUIDialog::OnInitWindow(); + + if (!m_progItem) + { + /* no epg event selected */ + return; + } + + auto& mgr = CServiceBroker::GetPVRManager(); + const auto epgTag = m_progItem->GetEPGInfoTag(); + + if (!mgr.Recordings()->GetRecordingForEpgTag(epgTag)) + { + /* not recording. hide the play recording button */ + SET_CONTROL_HIDDEN(CONTROL_BTN_PLAY_RECORDING); + } + + bool bHideRecord = true; + bool bHideAddTimer = true; + const std::shared_ptr<CPVRTimerInfoTag> timer = mgr.Timers()->GetTimerForEpgTag(epgTag); + bool bHideSetReminder = timer || (epgTag->StartAsLocalTime() <= CDateTime::GetCurrentDateTime()); + + if (timer) + { + if (timer->IsRecording()) + { + SET_CONTROL_LABEL(CONTROL_BTN_RECORD, 19059); /* Stop recording */ + bHideRecord = false; + } + else if (!timer->GetTimerType()->IsReadOnly()) + { + SET_CONTROL_LABEL(CONTROL_BTN_RECORD, 19060); /* Delete timer */ + bHideRecord = false; + } + } + else if (epgTag->IsRecordable()) + { + const std::shared_ptr<CPVRClient> client = mgr.GetClient(epgTag->ClientID()); + if (client && client->GetClientCapabilities().SupportsTimers()) + { + SET_CONTROL_LABEL(CONTROL_BTN_RECORD, 264); /* Record */ + bHideRecord = false; + bHideAddTimer = false; + } + } + + if (!epgTag->IsPlayable()) + SET_CONTROL_HIDDEN(CONTROL_BTN_PLAY_EPGTAG); + + if (bHideRecord) + SET_CONTROL_HIDDEN(CONTROL_BTN_RECORD); + + if (bHideAddTimer) + SET_CONTROL_HIDDEN(CONTROL_BTN_ADD_TIMER); + + if (bHideSetReminder) + SET_CONTROL_HIDDEN(CONTROL_BTN_SET_REMINDER); +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.h b/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.h new file mode 100644 index 0000000..8d9bda5 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.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 "guilib/GUIDialog.h" + +#include <memory> + +class CGUIMessage; + +namespace PVR +{ +class CGUIDialogPVRGuideInfo : public CGUIDialog +{ +public: + CGUIDialogPVRGuideInfo(); + ~CGUIDialogPVRGuideInfo() override; + bool OnMessage(CGUIMessage& message) override; + bool OnInfo(int actionID) override; + bool HasListItems() const override { return true; } + CFileItemPtr GetCurrentListItem(int offset = 0) override; + + void SetProgInfo(const std::shared_ptr<CFileItem>& item); + +protected: + void OnInitWindow() override; + +private: + bool OnClickButtonOK(const CGUIMessage& message); + bool OnClickButtonRecord(const CGUIMessage& message); + bool OnClickButtonPlay(const CGUIMessage& message); + bool OnClickButtonFind(const CGUIMessage& message); + bool OnClickButtonAddTimer(const CGUIMessage& message); + bool OnClickButtonSetReminder(const CGUIMessage& message); + + std::shared_ptr<CFileItem> m_progItem; +}; +} // namespace PVR diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.cpp b/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.cpp new file mode 100644 index 0000000..4cf7f2f --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.cpp @@ -0,0 +1,391 @@ +/* + * 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 "GUIDialogPVRGuideSearch.h" + +#include "ServiceBroker.h" +#include "guilib/GUIEditControl.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIMessage.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRManager.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/epg/EpgSearchFilter.h" +#include "utils/StringUtils.h" + +#include <string> +#include <utility> +#include <vector> + +using namespace PVR; + +#define CONTROL_EDIT_SEARCH 9 +#define CONTROL_BTN_INC_DESC 10 +#define CONTROL_BTN_CASE_SENS 11 +#define CONTROL_SPIN_MIN_DURATION 12 +#define CONTROL_SPIN_MAX_DURATION 13 +#define CONTROL_EDIT_START_DATE 14 +#define CONTROL_EDIT_STOP_DATE 15 +#define CONTROL_EDIT_START_TIME 16 +#define CONTROL_EDIT_STOP_TIME 17 +#define CONTROL_SPIN_GENRE 18 +#define CONTROL_SPIN_NO_REPEATS 19 +#define CONTROL_BTN_UNK_GENRE 20 +#define CONTROL_SPIN_GROUPS 21 +#define CONTROL_BTN_FTA_ONLY 22 +#define CONTROL_SPIN_CHANNELS 23 +#define CONTROL_BTN_IGNORE_TMR 24 +#define CONTROL_BTN_CANCEL 25 +#define CONTROL_BTN_SEARCH 26 +#define CONTROL_BTN_IGNORE_REC 27 +#define CONTROL_BTN_DEFAULTS 28 +static constexpr int CONTROL_BTN_SAVE = 29; +static constexpr int CONTROL_BTN_IGNORE_FINISHED = 30; +static constexpr int CONTROL_BTN_IGNORE_FUTURE = 31; + +CGUIDialogPVRGuideSearch::CGUIDialogPVRGuideSearch() + : CGUIDialog(WINDOW_DIALOG_PVR_GUIDE_SEARCH, "DialogPVRGuideSearch.xml") +{ +} + +void CGUIDialogPVRGuideSearch::SetFilterData( + const std::shared_ptr<CPVREpgSearchFilter>& searchFilter) +{ + m_searchFilter = searchFilter; +} + +void CGUIDialogPVRGuideSearch::UpdateChannelSpin() +{ + int iChannelGroup = GetSpinValue(CONTROL_SPIN_GROUPS); + + std::vector< std::pair<std::string, int> > labels; + if (m_searchFilter->IsRadio()) + labels.emplace_back(g_localizeStrings.Get(19216), EPG_SEARCH_UNSET); // All radio channels + else + labels.emplace_back(g_localizeStrings.Get(19217), EPG_SEARCH_UNSET); // All TV channels + + std::shared_ptr<CPVRChannelGroup> group; + if (iChannelGroup != EPG_SEARCH_UNSET) + group = CServiceBroker::GetPVRManager().ChannelGroups()->GetByIdFromAll(iChannelGroup); + + if (!group) + group = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_searchFilter->IsRadio()); + + m_channelsMap.clear(); + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers = + group->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE); + int iIndex = 0; + int iSelectedChannel = EPG_SEARCH_UNSET; + for (const auto& groupMember : groupMembers) + { + labels.emplace_back(std::make_pair(groupMember->Channel()->ChannelName(), iIndex)); + m_channelsMap.insert(std::make_pair(iIndex, groupMember)); + + if (iSelectedChannel == EPG_SEARCH_UNSET && + groupMember->ChannelUID() == m_searchFilter->GetChannelUID() && + groupMember->ClientID() == m_searchFilter->GetClientID()) + iSelectedChannel = iIndex; + + ++iIndex; + } + + SET_CONTROL_LABELS(CONTROL_SPIN_CHANNELS, iSelectedChannel, &labels); +} + +void CGUIDialogPVRGuideSearch::UpdateGroupsSpin() +{ + std::vector<std::pair<std::string, int>> labels; + const std::vector<std::shared_ptr<CPVRChannelGroup>> groups = + CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_searchFilter->IsRadio())->GetMembers(); + int selectedGroup = EPG_SEARCH_UNSET; + for (const auto& group : groups) + { + labels.emplace_back(group->GroupName(), group->GroupID()); + + if (selectedGroup == EPG_SEARCH_UNSET && + group->GroupID() == m_searchFilter->GetChannelGroupID()) + selectedGroup = group->GroupID(); + } + + SET_CONTROL_LABELS(CONTROL_SPIN_GROUPS, selectedGroup, &labels); +} + +void CGUIDialogPVRGuideSearch::UpdateGenreSpin() +{ + std::vector< std::pair<std::string, int> > labels; + labels.emplace_back(g_localizeStrings.Get(593), EPG_SEARCH_UNSET); + labels.emplace_back(g_localizeStrings.Get(19500), EPG_EVENT_CONTENTMASK_MOVIEDRAMA); + labels.emplace_back(g_localizeStrings.Get(19516), EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS); + labels.emplace_back(g_localizeStrings.Get(19532), EPG_EVENT_CONTENTMASK_SHOW); + labels.emplace_back(g_localizeStrings.Get(19548), EPG_EVENT_CONTENTMASK_SPORTS); + labels.emplace_back(g_localizeStrings.Get(19564), EPG_EVENT_CONTENTMASK_CHILDRENYOUTH); + labels.emplace_back(g_localizeStrings.Get(19580), EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE); + labels.emplace_back(g_localizeStrings.Get(19596), EPG_EVENT_CONTENTMASK_ARTSCULTURE); + labels.emplace_back(g_localizeStrings.Get(19612), EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS); + labels.emplace_back(g_localizeStrings.Get(19628), EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE); + labels.emplace_back(g_localizeStrings.Get(19644), EPG_EVENT_CONTENTMASK_LEISUREHOBBIES); + labels.emplace_back(g_localizeStrings.Get(19660), EPG_EVENT_CONTENTMASK_SPECIAL); + labels.emplace_back(g_localizeStrings.Get(19499), EPG_EVENT_CONTENTMASK_USERDEFINED); + + SET_CONTROL_LABELS(CONTROL_SPIN_GENRE, m_searchFilter->GetGenreType(), &labels); +} + +void CGUIDialogPVRGuideSearch::UpdateDurationSpin() +{ + /* minimum duration */ + std::vector< std::pair<std::string, int> > labels; + + labels.emplace_back("-", EPG_SEARCH_UNSET); + for (int i = 1; i < 12*60/5; ++i) + labels.emplace_back(StringUtils::Format(g_localizeStrings.Get(14044), i * 5), i * 5); + + SET_CONTROL_LABELS(CONTROL_SPIN_MIN_DURATION, m_searchFilter->GetMinimumDuration(), &labels); + + /* maximum duration */ + labels.clear(); + + labels.emplace_back("-", EPG_SEARCH_UNSET); + for (int i = 1; i < 12*60/5; ++i) + labels.emplace_back(StringUtils::Format(g_localizeStrings.Get(14044), i * 5), i * 5); + + SET_CONTROL_LABELS(CONTROL_SPIN_MAX_DURATION, m_searchFilter->GetMaximumDuration(), &labels); +} + +bool CGUIDialogPVRGuideSearch::OnMessage(CGUIMessage& message) +{ + CGUIDialog::OnMessage(message); + + switch (message.GetMessage()) + { + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_BTN_SEARCH) + { + // Read data from controls, update m_searchfilter accordingly + UpdateSearchFilter(); + + m_result = Result::SEARCH; + Close(); + return true; + } + else if (iControl == CONTROL_BTN_CANCEL) + { + m_result = Result::CANCEL; + Close(); + return true; + } + else if (iControl == CONTROL_BTN_DEFAULTS) + { + if (m_searchFilter) + { + m_searchFilter->Reset(); + Update(); + } + return true; + } + else if (iControl == CONTROL_BTN_SAVE) + { + // Read data from controls, update m_searchfilter accordingly + UpdateSearchFilter(); + + std::string title = m_searchFilter->GetTitle(); + if (title.empty()) + { + title = m_searchFilter->GetSearchTerm(); + if (title.empty()) + title = g_localizeStrings.Get(137); // "Search" + else + StringUtils::Trim(title, "\""); + + if (!CGUIKeyboardFactory::ShowAndGetInput( + title, CVariant{g_localizeStrings.Get(528)}, // "Enter title" + false)) + { + return false; + } + m_searchFilter->SetTitle(title); + } + + m_result = Result::SAVE; + Close(); + return true; + } + else if (iControl == CONTROL_SPIN_GROUPS) + { + UpdateChannelSpin(); + return true; + } + } + break; + } + + return false; +} + +void CGUIDialogPVRGuideSearch::OnInitWindow() +{ + CGUIDialog::OnInitWindow(); + + m_result = Result::CANCEL; +} + +void CGUIDialogPVRGuideSearch::OnWindowLoaded() +{ + Update(); + return CGUIDialog::OnWindowLoaded(); +} + +CDateTime CGUIDialogPVRGuideSearch::ReadDateTime(const std::string& strDate, const std::string& strTime) const +{ + CDateTime dateTime; + int iHours, iMinutes; + sscanf(strTime.c_str(), "%d:%d", &iHours, &iMinutes); + dateTime.SetFromDBDate(strDate); + dateTime.SetDateTime(dateTime.GetYear(), dateTime.GetMonth(), dateTime.GetDay(), iHours, iMinutes, 0); + return dateTime.GetAsUTCDateTime(); +} + +bool CGUIDialogPVRGuideSearch::IsRadioSelected(int controlID) +{ + CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), controlID); + OnMessage(msg); + return (msg.GetParam1() == 1); +} + +int CGUIDialogPVRGuideSearch::GetSpinValue(int controlID) +{ + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), controlID); + OnMessage(msg); + return msg.GetParam1(); +} + +std::string CGUIDialogPVRGuideSearch::GetEditValue(int controlID) +{ + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), controlID); + OnMessage(msg); + return msg.GetLabel(); +} + +void CGUIDialogPVRGuideSearch::UpdateSearchFilter() +{ + if (!m_searchFilter) + return; + + m_searchFilter->SetSearchTerm(GetEditValue(CONTROL_EDIT_SEARCH)); + + m_searchFilter->SetSearchInDescription(IsRadioSelected(CONTROL_BTN_INC_DESC)); + m_searchFilter->SetCaseSensitive(IsRadioSelected(CONTROL_BTN_CASE_SENS)); + m_searchFilter->SetFreeToAirOnly(IsRadioSelected(CONTROL_BTN_FTA_ONLY)); + m_searchFilter->SetIncludeUnknownGenres(IsRadioSelected(CONTROL_BTN_UNK_GENRE)); + m_searchFilter->SetIgnorePresentRecordings(IsRadioSelected(CONTROL_BTN_IGNORE_REC)); + m_searchFilter->SetIgnorePresentTimers(IsRadioSelected(CONTROL_BTN_IGNORE_TMR)); + m_searchFilter->SetRemoveDuplicates(IsRadioSelected(CONTROL_SPIN_NO_REPEATS)); + m_searchFilter->SetIgnoreFinishedBroadcasts(IsRadioSelected(CONTROL_BTN_IGNORE_FINISHED)); + m_searchFilter->SetIgnoreFutureBroadcasts(IsRadioSelected(CONTROL_BTN_IGNORE_FUTURE)); + m_searchFilter->SetGenreType(GetSpinValue(CONTROL_SPIN_GENRE)); + m_searchFilter->SetMinimumDuration(GetSpinValue(CONTROL_SPIN_MIN_DURATION)); + m_searchFilter->SetMaximumDuration(GetSpinValue(CONTROL_SPIN_MAX_DURATION)); + + auto it = m_channelsMap.find(GetSpinValue(CONTROL_SPIN_CHANNELS)); + m_searchFilter->SetClientID(it == m_channelsMap.end() ? -1 : (*it).second->ClientID()); + m_searchFilter->SetChannelUID(it == m_channelsMap.end() ? -1 : (*it).second->ChannelUID()); + m_searchFilter->SetChannelGroupID(GetSpinValue(CONTROL_SPIN_GROUPS)); + + const CDateTime start = + ReadDateTime(GetEditValue(CONTROL_EDIT_START_DATE), GetEditValue(CONTROL_EDIT_START_TIME)); + if (start != m_startDateTime) + { + m_searchFilter->SetStartDateTime(start); + m_startDateTime = start; + } + const CDateTime end = + ReadDateTime(GetEditValue(CONTROL_EDIT_STOP_DATE), GetEditValue(CONTROL_EDIT_STOP_TIME)); + if (end != m_endDateTime) + { + m_searchFilter->SetEndDateTime(end); + m_endDateTime = end; + } +} + +void CGUIDialogPVRGuideSearch::Update() +{ + if (!m_searchFilter) + return; + + SET_CONTROL_LABEL2(CONTROL_EDIT_SEARCH, m_searchFilter->GetSearchTerm()); + { + CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_SEARCH, CGUIEditControl::INPUT_TYPE_TEXT, 16017); + OnMessage(msg); + } + + SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_CASE_SENS, m_searchFilter->IsCaseSensitive()); + SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_INC_DESC, m_searchFilter->ShouldSearchInDescription()); + SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_FTA_ONLY, m_searchFilter->IsFreeToAirOnly()); + SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_UNK_GENRE, m_searchFilter->ShouldIncludeUnknownGenres()); + SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_IGNORE_REC, m_searchFilter->ShouldIgnorePresentRecordings()); + SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_IGNORE_TMR, m_searchFilter->ShouldIgnorePresentTimers()); + SET_CONTROL_SELECTED(GetID(), CONTROL_SPIN_NO_REPEATS, m_searchFilter->ShouldRemoveDuplicates()); + SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_IGNORE_FINISHED, + m_searchFilter->ShouldIgnoreFinishedBroadcasts()); + SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_IGNORE_FUTURE, + m_searchFilter->ShouldIgnoreFutureBroadcasts()); + + // Set start/end datetime fields + m_startDateTime = m_searchFilter->GetStartDateTime(); + m_endDateTime = m_searchFilter->GetEndDateTime(); + if (!m_startDateTime.IsValid() || !m_endDateTime.IsValid()) + { + const auto dates = CServiceBroker::GetPVRManager().EpgContainer().GetFirstAndLastEPGDate(); + if (!m_startDateTime.IsValid()) + m_startDateTime = dates.first; + if (!m_endDateTime.IsValid()) + m_endDateTime = dates.second; + } + + if (!m_startDateTime.IsValid()) + m_startDateTime = CDateTime::GetUTCDateTime(); + + if (!m_endDateTime.IsValid()) + m_endDateTime = m_startDateTime + CDateTimeSpan(10, 0, 0, 0); // default to start + 10 days + + CDateTime startLocal; + startLocal.SetFromUTCDateTime(m_startDateTime); + CDateTime endLocal; + endLocal.SetFromUTCDateTime(m_endDateTime); + + SET_CONTROL_LABEL2(CONTROL_EDIT_START_TIME, startLocal.GetAsLocalizedTime("", false)); + { + CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_START_TIME, CGUIEditControl::INPUT_TYPE_TIME, 14066); + OnMessage(msg); + } + SET_CONTROL_LABEL2(CONTROL_EDIT_STOP_TIME, endLocal.GetAsLocalizedTime("", false)); + { + CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_STOP_TIME, CGUIEditControl::INPUT_TYPE_TIME, 14066); + OnMessage(msg); + } + SET_CONTROL_LABEL2(CONTROL_EDIT_START_DATE, startLocal.GetAsDBDate()); + { + CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_START_DATE, CGUIEditControl::INPUT_TYPE_DATE, 14067); + OnMessage(msg); + } + SET_CONTROL_LABEL2(CONTROL_EDIT_STOP_DATE, endLocal.GetAsDBDate()); + { + CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_STOP_DATE, CGUIEditControl::INPUT_TYPE_DATE, 14067); + OnMessage(msg); + } + + UpdateDurationSpin(); + UpdateGroupsSpin(); + UpdateChannelSpin(); + UpdateGenreSpin(); +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.h b/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.h new file mode 100644 index 0000000..4d02167 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.h @@ -0,0 +1,64 @@ +/* + * 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 "guilib/GUIDialog.h" + +#include <map> +#include <memory> +#include <string> + +namespace PVR +{ + class CPVREpgSearchFilter; + class CPVRChannelGroupMember; + + class CGUIDialogPVRGuideSearch : public CGUIDialog + { + public: + CGUIDialogPVRGuideSearch(); + ~CGUIDialogPVRGuideSearch() override = default; + bool OnMessage(CGUIMessage& message) override; + void OnWindowLoaded() override; + + void SetFilterData(const std::shared_ptr<CPVREpgSearchFilter>& searchFilter); + + enum class Result + { + SEARCH, + SAVE, + CANCEL + }; + Result GetResult() const { return m_result; } + + protected: + void OnInitWindow() override; + + private: + void UpdateSearchFilter(); + void UpdateChannelSpin(); + void UpdateGroupsSpin(); + void UpdateGenreSpin(); + void UpdateDurationSpin(); + CDateTime ReadDateTime(const std::string& strDate, const std::string& strTime) const; + void Update(); + + bool IsRadioSelected(int controlID); + int GetSpinValue(int controlID); + std::string GetEditValue(int controlID); + + Result m_result = Result::CANCEL; + std::shared_ptr<CPVREpgSearchFilter> m_searchFilter; + std::map<int, std::shared_ptr<CPVRChannelGroupMember>> m_channelsMap; + + CDateTime m_startDateTime; + CDateTime m_endDateTime; + }; +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.cpp b/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.cpp new file mode 100644 index 0000000..2add783 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.cpp @@ -0,0 +1,154 @@ +/* + * 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 "GUIDialogPVRItemsViewBase.h" + +#include "ContextMenuManager.h" +#include "FileItem.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogContextMenu.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "pvr/PVRManager.h" +#include "pvr/guilib/PVRGUIActionsEPG.h" +#include "view/ViewState.h" + +#include <utility> + +#define CONTROL_LIST 11 + +using namespace PVR; + +CGUIDialogPVRItemsViewBase::CGUIDialogPVRItemsViewBase(int id, const std::string& xmlFile) + : CGUIDialog(id, xmlFile), m_vecItems(new CFileItemList) +{ +} + +void CGUIDialogPVRItemsViewBase::OnWindowLoaded() +{ + CGUIDialog::OnWindowLoaded(); + m_viewControl.Reset(); + m_viewControl.SetParentWindow(GetID()); + m_viewControl.AddView(GetControl(CONTROL_LIST)); +} + +void CGUIDialogPVRItemsViewBase::OnWindowUnload() +{ + CGUIDialog::OnWindowUnload(); + m_viewControl.Reset(); +} + +void CGUIDialogPVRItemsViewBase::OnInitWindow() +{ + CGUIDialog::OnInitWindow(); +} + +void CGUIDialogPVRItemsViewBase::OnDeinitWindow(int nextWindowID) +{ + CGUIDialog::OnDeinitWindow(nextWindowID); + Clear(); +} + +bool CGUIDialogPVRItemsViewBase::OnAction(const CAction& action) +{ + if (m_viewControl.HasControl(GetFocusedControlID())) + { + switch (action.GetID()) + { + case ACTION_SHOW_INFO: + case ACTION_SELECT_ITEM: + case ACTION_MOUSE_LEFT_CLICK: + ShowInfo(m_viewControl.GetSelectedItem()); + return true; + + case ACTION_CONTEXT_MENU: + case ACTION_MOUSE_RIGHT_CLICK: + return ContextMenu(m_viewControl.GetSelectedItem()); + + default: + break; + } + } + return CGUIDialog::OnAction(action); +} + +CGUIControl* CGUIDialogPVRItemsViewBase::GetFirstFocusableControl(int id) +{ + if (m_viewControl.HasControl(id)) + id = m_viewControl.GetCurrentControl(); + + return CGUIDialog::GetFirstFocusableControl(id); +} + +void CGUIDialogPVRItemsViewBase::ShowInfo(int itemIdx) +{ + if (itemIdx < 0 || itemIdx >= m_vecItems->Size()) + return; + + const std::shared_ptr<CFileItem> item = m_vecItems->Get(itemIdx); + if (!item) + return; + + CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*item); +} + +bool CGUIDialogPVRItemsViewBase::ContextMenu(int itemIdx) +{ + auto InRange = [](size_t i, std::pair<size_t, size_t> range) { + return i >= range.first && i < range.second; + }; + + if (itemIdx < 0 || itemIdx >= m_vecItems->Size()) + return false; + + const CFileItemPtr item = m_vecItems->Get(itemIdx); + if (!item) + return false; + + CContextButtons buttons; + + // Add the global menu + const ContextMenuView globalMenu = + CServiceBroker::GetContextMenuManager().GetItems(*item, CContextMenuManager::MAIN); + auto globalMenuRange = std::make_pair(buttons.size(), buttons.size() + globalMenu.size()); + for (const auto& menu : globalMenu) + buttons.emplace_back(~buttons.size(), menu->GetLabel(*item)); + + // Add addon menus + const ContextMenuView addonMenu = + CServiceBroker::GetContextMenuManager().GetAddonItems(*item, CContextMenuManager::MAIN); + auto addonMenuRange = std::make_pair(buttons.size(), buttons.size() + addonMenu.size()); + for (const auto& menu : addonMenu) + buttons.emplace_back(~buttons.size(), menu->GetLabel(*item)); + + if (buttons.empty()) + return true; + + int idx = CGUIDialogContextMenu::Show(buttons); + if (idx < 0 || idx >= static_cast<int>(buttons.size())) + return false; + + Close(); + + if (InRange(idx, globalMenuRange)) + return CONTEXTMENU::LoopFrom(*globalMenu[idx - globalMenuRange.first], item); + + return CONTEXTMENU::LoopFrom(*addonMenu[idx - addonMenuRange.first], item); +} + +void CGUIDialogPVRItemsViewBase::Init() +{ + m_viewControl.SetCurrentView(DEFAULT_VIEW_LIST); + Clear(); +} + +void CGUIDialogPVRItemsViewBase::Clear() +{ + m_viewControl.Clear(); + m_vecItems->Clear(); +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.h b/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.h new file mode 100644 index 0000000..66530e4 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.h @@ -0,0 +1,47 @@ +/* + * 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 "guilib/GUIDialog.h" +#include "view/GUIViewControl.h" + +#include <memory> +#include <string> + +class CFileItemList; + +namespace PVR +{ +class CGUIDialogPVRItemsViewBase : public CGUIDialog +{ +public: + CGUIDialogPVRItemsViewBase() = delete; + CGUIDialogPVRItemsViewBase(int id, const std::string& xmlFile); + ~CGUIDialogPVRItemsViewBase() override = default; + + void OnWindowLoaded() override; + void OnWindowUnload() override; + bool OnAction(const CAction& action) override; + +protected: + void Init(); + + void OnInitWindow() override; + void OnDeinitWindow(int nextWindowID) override; + CGUIControl* GetFirstFocusableControl(int id) override; + + std::unique_ptr<CFileItemList> m_vecItems; + CGUIViewControl m_viewControl; + +private: + void Clear(); + void ShowInfo(int itemIdx); + bool ContextMenu(int iItemIdx); +}; +} // namespace PVR diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.cpp b/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.cpp new file mode 100644 index 0000000..ead92d2 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.cpp @@ -0,0 +1,216 @@ +/* + * 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 "GUIDialogPVRRadioRDSInfo.h" + +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "guilib/GUIMessage.h" +#include "guilib/GUISpinControl.h" +#include "guilib/GUITextBox.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRRadioRDSInfoTag.h" + +using namespace PVR; + +#define CONTROL_BTN_OK 10 +#define SPIN_CONTROL_INFO 21 +#define TEXT_INFO 22 +#define CONTROL_NEXT_PAGE 60 +#define CONTROL_INFO_LIST 70 + +#define INFO_NEWS 1 +#define INFO_NEWS_LOCAL 2 +#define INFO_SPORT 3 +#define INFO_WEATHER 4 +#define INFO_LOTTERY 5 +#define INFO_STOCK 6 +#define INFO_OTHER 7 +#define INFO_CINEMA 8 +#define INFO_HOROSCOPE 9 + +CGUIDialogPVRRadioRDSInfo::CGUIDialogPVRRadioRDSInfo() + : CGUIDialog(WINDOW_DIALOG_PVR_RADIO_RDS_INFO, "DialogPVRRadioRDSInfo.xml") + , m_InfoNews(29916, INFO_NEWS) + , m_InfoNewsLocal(29917, INFO_NEWS_LOCAL) + , m_InfoSport(29918, INFO_SPORT) + , m_InfoWeather(400, INFO_WEATHER) + , m_InfoLottery(29919, INFO_LOTTERY) + , m_InfoStock(29920, INFO_STOCK) + , m_InfoOther(29921, INFO_OTHER) + , m_InfoCinema(19602, INFO_CINEMA) + , m_InfoHoroscope(29922, INFO_HOROSCOPE) +{ +} + +bool CGUIDialogPVRRadioRDSInfo::OnMessage(CGUIMessage& message) +{ + if (message.GetMessage() == GUI_MSG_CLICKED) + { + int iControl = message.GetSenderId(); + + if (iControl == CONTROL_BTN_OK) + { + Close(); + return true; + } + else if (iControl == SPIN_CONTROL_INFO) + { + const std::shared_ptr<CPVRChannel> channel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + if (!channel) + return false; + + const std::shared_ptr<CPVRRadioRDSInfoTag> currentRDS = channel->GetRadioRDSInfoTag(); + if (!currentRDS) + return false; + + const CGUISpinControl* spin = static_cast<CGUISpinControl*>(GetControl(SPIN_CONTROL_INFO)); + if (!spin) + return false; + + CGUITextBox* textbox = static_cast<CGUITextBox*>(GetControl(TEXT_INFO)); + if (!textbox) + return false; + + switch (spin->GetValue()) + { + case INFO_NEWS: + textbox->SetInfo(currentRDS->GetInfoNews()); + break; + case INFO_NEWS_LOCAL: + textbox->SetInfo(currentRDS->GetInfoNewsLocal()); + break; + case INFO_SPORT: + textbox->SetInfo(currentRDS->GetInfoSport()); + break; + case INFO_WEATHER: + textbox->SetInfo(currentRDS->GetInfoWeather()); + break; + case INFO_LOTTERY: + textbox->SetInfo(currentRDS->GetInfoLottery()); + break; + case INFO_STOCK: + textbox->SetInfo(currentRDS->GetInfoStock()); + break; + case INFO_OTHER: + textbox->SetInfo(currentRDS->GetInfoOther()); + break; + case INFO_CINEMA: + textbox->SetInfo(currentRDS->GetInfoCinema()); + break; + case INFO_HOROSCOPE: + textbox->SetInfo(currentRDS->GetInfoHoroscope()); + break; + } + + SET_CONTROL_VISIBLE(CONTROL_INFO_LIST); + } + } + else if (message.GetMessage() == GUI_MSG_NOTIFY_ALL) + { + if (message.GetParam1() == GUI_MSG_UPDATE_RADIOTEXT && IsActive()) + { + UpdateInfoControls(); + } + } + + return CGUIDialog::OnMessage(message); +} + +void CGUIDialogPVRRadioRDSInfo::OnInitWindow() +{ + CGUIDialog::OnInitWindow(); + + InitInfoControls(); +} + +void CGUIDialogPVRRadioRDSInfo::InitInfoControls() +{ + SET_CONTROL_HIDDEN(CONTROL_INFO_LIST); + + CGUISpinControl* spin = static_cast<CGUISpinControl*>(GetControl(SPIN_CONTROL_INFO)); + if (spin) + spin->Clear(); + + CGUITextBox* textbox = static_cast<CGUITextBox*>(GetControl(TEXT_INFO)); + + m_InfoNews.Init(spin, textbox); + m_InfoNewsLocal.Init(spin, textbox); + m_InfoSport.Init(spin, textbox); + m_InfoWeather.Init(spin, textbox); + m_InfoLottery.Init(spin, textbox); + m_InfoStock.Init(spin, textbox); + m_InfoOther.Init(spin, textbox); + m_InfoCinema.Init(spin, textbox); + m_InfoHoroscope.Init(spin, textbox); + + if (spin && textbox) + UpdateInfoControls(); +} + +void CGUIDialogPVRRadioRDSInfo::UpdateInfoControls() +{ + const std::shared_ptr<CPVRChannel> channel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + if (!channel) + return; + + const std::shared_ptr<CPVRRadioRDSInfoTag> currentRDS = channel->GetRadioRDSInfoTag(); + if (!currentRDS) + return; + + bool bInfoPresent = m_InfoNews.Update(currentRDS->GetInfoNews()); + bInfoPresent |= m_InfoNewsLocal.Update(currentRDS->GetInfoNewsLocal()); + bInfoPresent |= m_InfoSport.Update(currentRDS->GetInfoSport()); + bInfoPresent |= m_InfoWeather.Update(currentRDS->GetInfoWeather()); + bInfoPresent |= m_InfoLottery.Update(currentRDS->GetInfoLottery()); + bInfoPresent |= m_InfoStock.Update(currentRDS->GetInfoStock()); + bInfoPresent |= m_InfoOther.Update(currentRDS->GetInfoOther()); + bInfoPresent |= m_InfoCinema.Update(currentRDS->GetInfoCinema()); + bInfoPresent |= m_InfoHoroscope.Update(currentRDS->GetInfoHoroscope()); + + if (bInfoPresent) + SET_CONTROL_VISIBLE(CONTROL_INFO_LIST); +} + +CGUIDialogPVRRadioRDSInfo::InfoControl::InfoControl(uint32_t iSpinLabelId, uint32_t iSpinControlId) +: m_iSpinLabelId(iSpinLabelId), + m_iSpinControlId(iSpinControlId) +{ +} + +void CGUIDialogPVRRadioRDSInfo::InfoControl::Init(CGUISpinControl* spin, CGUITextBox* textbox) +{ + m_spinControl = spin; + m_textbox = textbox; + m_bSpinLabelPresent = false; + m_textboxValue.clear(); +} + +bool CGUIDialogPVRRadioRDSInfo::InfoControl::Update(const std::string& textboxValue) +{ + if (m_spinControl && m_textbox && !textboxValue.empty()) + { + if (!m_bSpinLabelPresent) + { + m_spinControl->AddLabel(g_localizeStrings.Get(m_iSpinLabelId), m_iSpinControlId); + m_bSpinLabelPresent = true; + } + + if (m_textboxValue != textboxValue) + { + m_spinControl->SetValue(m_iSpinControlId); + m_textboxValue = textboxValue; + m_textbox->SetInfo(textboxValue); + return true; + } + } + return false; +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.h b/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.h new file mode 100644 index 0000000..4ae1b0b --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.h @@ -0,0 +1,60 @@ +/* + * 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 "guilib/GUIDialog.h" + +#include <string> + +class CGUISpinControl; +class CGUITextBox; + +namespace PVR +{ + class CGUIDialogPVRRadioRDSInfo : public CGUIDialog + { + public: + CGUIDialogPVRRadioRDSInfo(); + ~CGUIDialogPVRRadioRDSInfo() override = default; + bool OnMessage(CGUIMessage& message) override; + + protected: + void OnInitWindow() override; + + private: + class InfoControl + { + public: + InfoControl(uint32_t iSpinLabelId, uint32_t iSpinControlId); + void Init(CGUISpinControl* spin, CGUITextBox* textbox); + bool Update(const std::string& textboxValue); + + private: + CGUISpinControl* m_spinControl = nullptr; + uint32_t m_iSpinLabelId = 0; + uint32_t m_iSpinControlId = 0; + CGUITextBox* m_textbox = nullptr; + bool m_bSpinLabelPresent = false; + std::string m_textboxValue; + }; + + void InitInfoControls(); + void UpdateInfoControls(); + + InfoControl m_InfoNews; + InfoControl m_InfoNewsLocal; + InfoControl m_InfoSport; + InfoControl m_InfoWeather; + InfoControl m_InfoLottery; + InfoControl m_InfoStock; + InfoControl m_InfoOther; + InfoControl m_InfoCinema; + InfoControl m_InfoHoroscope; + }; +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.cpp b/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.cpp new file mode 100644 index 0000000..9396083 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.cpp @@ -0,0 +1,102 @@ +/* + * 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 "GUIDialogPVRRecordingInfo.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "guilib/GUIMessage.h" +#include "pvr/PVRManager.h" +#include "pvr/guilib/PVRGUIActionsEPG.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" + +using namespace PVR; + +#define CONTROL_BTN_FIND 4 +#define CONTROL_BTN_OK 7 +#define CONTROL_BTN_PLAY_RECORDING 8 + +CGUIDialogPVRRecordingInfo::CGUIDialogPVRRecordingInfo() + : CGUIDialog(WINDOW_DIALOG_PVR_RECORDING_INFO, "DialogPVRInfo.xml"), m_recordItem(new CFileItem) +{ +} + +bool CGUIDialogPVRRecordingInfo::OnMessage(CGUIMessage& message) +{ + switch (message.GetMessage()) + { + case GUI_MSG_CLICKED: + return OnClickButtonOK(message) || OnClickButtonPlay(message) || OnClickButtonFind(message); + } + + return CGUIDialog::OnMessage(message); +} + +bool CGUIDialogPVRRecordingInfo::OnClickButtonOK(const CGUIMessage& message) +{ + bool bReturn = false; + + if (message.GetSenderId() == CONTROL_BTN_OK) + { + Close(); + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRRecordingInfo::OnClickButtonPlay(const CGUIMessage& message) +{ + bool bReturn = false; + + if (message.GetSenderId() == CONTROL_BTN_PLAY_RECORDING) + { + Close(); + + if (m_recordItem) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording( + *m_recordItem, true /* check resume */); + + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRRecordingInfo::OnClickButtonFind(const CGUIMessage& message) +{ + bool bReturn = false; + + if (message.GetSenderId() == CONTROL_BTN_FIND) + { + Close(); + + if (m_recordItem) + CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().FindSimilar(*m_recordItem); + + bReturn = true; + } + + return bReturn; +} + +bool CGUIDialogPVRRecordingInfo::OnInfo(int actionID) +{ + Close(); + return true; +} + +void CGUIDialogPVRRecordingInfo::SetRecording(const CFileItem& item) +{ + m_recordItem = std::make_shared<CFileItem>(item); +} + +CFileItemPtr CGUIDialogPVRRecordingInfo::GetCurrentListItem(int offset) +{ + return m_recordItem; +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.h b/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.h new file mode 100644 index 0000000..b22e0d7 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.h @@ -0,0 +1,37 @@ +/* + * 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 "guilib/GUIDialog.h" + +class CFileItem; +class CGUIMessage; + +namespace PVR +{ +class CGUIDialogPVRRecordingInfo : public CGUIDialog +{ +public: + CGUIDialogPVRRecordingInfo(); + ~CGUIDialogPVRRecordingInfo() override = default; + bool OnMessage(CGUIMessage& message) override; + bool OnInfo(int actionID) override; + bool HasListItems() const override { return true; } + CFileItemPtr GetCurrentListItem(int offset = 0) override; + + void SetRecording(const CFileItem& item); + +private: + bool OnClickButtonFind(const CGUIMessage& message); + bool OnClickButtonOK(const CGUIMessage& message); + bool OnClickButtonPlay(const CGUIMessage& message); + + CFileItemPtr m_recordItem; +}; +} // namespace PVR diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.cpp b/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.cpp new file mode 100644 index 0000000..ed8cd7b --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.cpp @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIDialogPVRRecordingSettings.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "guilib/GUIMessage.h" +#include "guilib/LocalizeStrings.h" +#include "messaging/helpers/DialogHelper.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/recordings/PVRRecording.h" +#include "settings/dialogs/GUIDialogSettingsBase.h" +#include "settings/lib/Setting.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <iterator> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +using namespace PVR; +using namespace KODI::MESSAGING; + +#define SETTING_RECORDING_NAME "recording.name" +#define SETTING_RECORDING_PLAYCOUNT "recording.playcount" +#define SETTING_RECORDING_LIFETIME "recording.lifetime" + +CGUIDialogPVRRecordingSettings::CGUIDialogPVRRecordingSettings() + : CGUIDialogSettingsManualBase(WINDOW_DIALOG_PVR_RECORDING_SETTING, "DialogSettings.xml") +{ + m_loadType = LOAD_EVERY_TIME; +} + +void CGUIDialogPVRRecordingSettings::SetRecording(const std::shared_ptr<CPVRRecording>& recording) +{ + if (!recording) + { + CLog::LogF(LOGERROR, "No recording given"); + return; + } + + m_recording = recording; + + // Copy data we need from tag. Do not modify the tag itself until Save()! + m_strTitle = m_recording->m_strTitle; + m_iPlayCount = m_recording->GetLocalPlayCount(); + m_iLifetime = m_recording->LifeTime(); +} + +void CGUIDialogPVRRecordingSettings::SetupView() +{ + CGUIDialogSettingsManualBase::SetupView(); + SetHeading(19068); // Recording settings + SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON); + SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186); // OK + SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222); // Cancel +} + +void CGUIDialogPVRRecordingSettings::InitializeSettings() +{ + CGUIDialogSettingsManualBase::InitializeSettings(); + + const std::shared_ptr<CSettingCategory> category = AddCategory("pvrrecordingsettings", -1); + if (category == nullptr) + { + CLog::LogF(LOGERROR, "Unable to add settings category"); + return; + } + + const std::shared_ptr<CSettingGroup> group = AddGroup(category); + if (group == nullptr) + { + CLog::LogF(LOGERROR, "Unable to add settings group"); + return; + } + + std::shared_ptr<CSetting> setting = nullptr; + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(m_recording->ClientID()); + + // Name + setting = AddEdit(group, SETTING_RECORDING_NAME, 19075, SettingLevel::Basic, m_strTitle); + setting->SetEnabled(client && client->GetClientCapabilities().SupportsRecordingsRename()); + + // Play count + if (client && client->GetClientCapabilities().SupportsRecordingsPlayCount()) + setting = AddEdit(group, SETTING_RECORDING_PLAYCOUNT, 567, SettingLevel::Basic, + m_recording->GetLocalPlayCount()); + + // Lifetime + if (client && client->GetClientCapabilities().SupportsRecordingsLifetimeChange()) + setting = AddList(group, SETTING_RECORDING_LIFETIME, 19083, SettingLevel::Basic, m_iLifetime, + LifetimesFiller, 19083); +} + +bool CGUIDialogPVRRecordingSettings::CanEditRecording(const CFileItem& item) +{ + if (!item.HasPVRRecordingInfoTag()) + return false; + + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(item.GetPVRRecordingInfoTag()->ClientID()); + + if (!client) + return false; + + const CPVRClientCapabilities& capabilities = client->GetClientCapabilities(); + + return capabilities.SupportsRecordingsRename() || capabilities.SupportsRecordingsPlayCount() || + capabilities.SupportsRecordingsLifetimeChange(); +} + +bool CGUIDialogPVRRecordingSettings::OnSettingChanging( + const std::shared_ptr<const CSetting>& setting) +{ + if (setting == nullptr) + { + CLog::LogF(LOGERROR, "No setting"); + return false; + } + + const std::string& settingId = setting->GetId(); + + if (settingId == SETTING_RECORDING_LIFETIME) + { + int iNewLifetime = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + if (m_recording->WillBeExpiredWithNewLifetime(iNewLifetime)) + { + if (HELPERS::ShowYesNoDialogText( + CVariant{19068}, // "Recording settings" + StringUtils::Format(g_localizeStrings.Get(19147), + iNewLifetime)) // "Setting the lifetime..." + != HELPERS::DialogResponse::CHOICE_YES) + return false; + } + } + + return CGUIDialogSettingsManualBase::OnSettingChanging(setting); +} + +void CGUIDialogPVRRecordingSettings::OnSettingChanged( + const std::shared_ptr<const CSetting>& setting) +{ + if (setting == nullptr) + { + CLog::LogF(LOGERROR, "No setting"); + return; + } + + CGUIDialogSettingsManualBase::OnSettingChanged(setting); + + const std::string& settingId = setting->GetId(); + + if (settingId == SETTING_RECORDING_NAME) + { + m_strTitle = std::static_pointer_cast<const CSettingString>(setting)->GetValue(); + } + else if (settingId == SETTING_RECORDING_PLAYCOUNT) + { + m_iPlayCount = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + } + else if (settingId == SETTING_RECORDING_LIFETIME) + { + m_iLifetime = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + } +} + +bool CGUIDialogPVRRecordingSettings::Save() +{ + // Name + m_recording->m_strTitle = m_strTitle; + + // Play count + m_recording->SetLocalPlayCount(m_iPlayCount); + + // Lifetime + m_recording->SetLifeTime(m_iLifetime); + + return true; +} + +void CGUIDialogPVRRecordingSettings::LifetimesFiller(const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + CGUIDialogPVRRecordingSettings* pThis = static_cast<CGUIDialogPVRRecordingSettings*>(data); + if (pThis) + { + list.clear(); + + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(pThis->m_recording->ClientID()); + if (client) + { + std::vector<std::pair<std::string, int>> values; + client->GetClientCapabilities().GetRecordingsLifetimeValues(values); + std::transform( + values.cbegin(), values.cend(), std::back_inserter(list), + [](const auto& value) { return IntegerSettingOption(value.first, value.second); }); + } + + current = pThis->m_iLifetime; + + auto it = list.begin(); + while (it != list.end()) + { + if (it->value == current) + break; // value already in list + + ++it; + } + + if (it == list.end()) + { + // PVR backend supplied value is not in the list of predefined values. Insert it. + list.insert(it, IntegerSettingOption( + StringUtils::Format(g_localizeStrings.Get(17999), current) /* {} days */, + current)); + } + } + else + CLog::LogF(LOGERROR, "No dialog"); +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.h b/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.h new file mode 100644 index 0000000..f0de299 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "settings/dialogs/GUIDialogSettingsManualBase.h" + +#include <memory> +#include <string> +#include <vector> + +class CFileItem; +class CSetting; + +struct IntegerSettingOption; + +namespace PVR +{ +class CPVRRecording; + +class CGUIDialogPVRRecordingSettings : public CGUIDialogSettingsManualBase +{ +public: + CGUIDialogPVRRecordingSettings(); + + void SetRecording(const std::shared_ptr<CPVRRecording>& recording); + static bool CanEditRecording(const CFileItem& item); + +protected: + // implementation of ISettingCallback + bool OnSettingChanging(const std::shared_ptr<const CSetting>& setting) override; + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + + // specialization of CGUIDialogSettingsBase + bool AllowResettingSettings() const override { return false; } + bool Save() override; + void SetupView() override; + + // specialization of CGUIDialogSettingsManualBase + void InitializeSettings() override; + +private: + static void LifetimesFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + + std::shared_ptr<CPVRRecording> m_recording; + std::string m_strTitle; + int m_iPlayCount = 0; + int m_iLifetime = 0; +}; +} // namespace PVR diff --git a/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.cpp b/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.cpp new file mode 100644 index 0000000..f786627 --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.cpp @@ -0,0 +1,1513 @@ +/* + * 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 "GUIDialogPVRTimerSettings.h" + +#include "ServiceBroker.h" +#include "dialogs/GUIDialogNumeric.h" +#include "guilib/GUIMessage.h" +#include "guilib/LocalizeStrings.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimerType.h" +#include "settings/SettingUtils.h" +#include "settings/dialogs/GUIDialogSettingsBase.h" +#include "settings/lib/Setting.h" +#include "settings/lib/SettingsManager.h" +#include "settings/windows/GUIControlSettings.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <iterator> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +using namespace PVR; +using namespace KODI::MESSAGING; + +#define SETTING_TMR_TYPE "timer.type" +#define SETTING_TMR_ACTIVE "timer.active" +#define SETTING_TMR_NAME "timer.name" +#define SETTING_TMR_EPGSEARCH "timer.epgsearch" +#define SETTING_TMR_FULLTEXT "timer.fulltext" +#define SETTING_TMR_CHANNEL "timer.channel" +#define SETTING_TMR_START_ANYTIME "timer.startanytime" +#define SETTING_TMR_END_ANYTIME "timer.endanytime" +#define SETTING_TMR_START_DAY "timer.startday" +#define SETTING_TMR_END_DAY "timer.endday" +#define SETTING_TMR_BEGIN "timer.begin" +#define SETTING_TMR_END "timer.end" +#define SETTING_TMR_WEEKDAYS "timer.weekdays" +#define SETTING_TMR_FIRST_DAY "timer.firstday" +#define SETTING_TMR_NEW_EPISODES "timer.newepisodes" +#define SETTING_TMR_BEGIN_PRE "timer.startmargin" +#define SETTING_TMR_END_POST "timer.endmargin" +#define SETTING_TMR_PRIORITY "timer.priority" +#define SETTING_TMR_LIFETIME "timer.lifetime" +#define SETTING_TMR_MAX_REC "timer.maxrecordings" +#define SETTING_TMR_DIR "timer.directory" +#define SETTING_TMR_REC_GROUP "timer.recgroup" + +#define TYPE_DEP_VISIBI_COND_ID_POSTFIX "visibi.typedep" +#define TYPE_DEP_ENABLE_COND_ID_POSTFIX "enable.typedep" +#define CHANNEL_DEP_VISIBI_COND_ID_POSTFIX "visibi.channeldep" +#define START_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX "visibi.startanytimedep" +#define END_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX "visibi.endanytimedep" + +CGUIDialogPVRTimerSettings::CGUIDialogPVRTimerSettings() + : CGUIDialogSettingsManualBase(WINDOW_DIALOG_PVR_TIMER_SETTING, "DialogSettings.xml"), + m_iWeekdays(PVR_WEEKDAY_NONE) +{ + m_loadType = LOAD_EVERY_TIME; +} + +CGUIDialogPVRTimerSettings::~CGUIDialogPVRTimerSettings() = default; + +bool CGUIDialogPVRTimerSettings::CanBeActivated() const +{ + if (!m_timerInfoTag) + { + CLog::LogF(LOGERROR, "No timer info tag"); + return false; + } + return true; +} + +void CGUIDialogPVRTimerSettings::SetTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer) +{ + if (!timer) + { + CLog::LogF(LOGERROR, "No timer given"); + return; + } + + m_timerInfoTag = timer; + + // Copy data we need from tag. Do not modify the tag itself until Save()! + m_timerType = m_timerInfoTag->GetTimerType(); + m_bIsRadio = m_timerInfoTag->m_bIsRadio; + m_bIsNewTimer = m_timerInfoTag->m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX; + m_bTimerActive = m_bIsNewTimer || !m_timerType->SupportsEnableDisable() || + !(m_timerInfoTag->m_state == PVR_TIMER_STATE_DISABLED); + m_bStartAnyTime = + m_bIsNewTimer || !m_timerType->SupportsStartAnyTime() || m_timerInfoTag->m_bStartAnyTime; + m_bEndAnyTime = + m_bIsNewTimer || !m_timerType->SupportsEndAnyTime() || m_timerInfoTag->m_bEndAnyTime; + m_strTitle = m_timerInfoTag->m_strTitle; + + m_startLocalTime = m_timerInfoTag->StartAsLocalTime(); + m_endLocalTime = m_timerInfoTag->EndAsLocalTime(); + + m_timerStartTimeStr = m_startLocalTime.GetAsLocalizedTime("", false); + m_timerEndTimeStr = m_endLocalTime.GetAsLocalizedTime("", false); + m_firstDayLocalTime = m_timerInfoTag->FirstDayAsLocalTime(); + + m_strEpgSearchString = m_timerInfoTag->m_strEpgSearchString; + if ((m_bIsNewTimer || !m_timerType->SupportsEpgTitleMatch()) && m_strEpgSearchString.empty()) + m_strEpgSearchString = m_strTitle; + + m_bFullTextEpgSearch = m_timerInfoTag->m_bFullTextEpgSearch; + + m_iWeekdays = m_timerInfoTag->m_iWeekdays; + if ((m_bIsNewTimer || !m_timerType->SupportsWeekdays()) && m_iWeekdays == PVR_WEEKDAY_NONE) + m_iWeekdays = PVR_WEEKDAY_ALLDAYS; + + m_iPreventDupEpisodes = m_timerInfoTag->m_iPreventDupEpisodes; + m_iMarginStart = m_timerInfoTag->m_iMarginStart; + m_iMarginEnd = m_timerInfoTag->m_iMarginEnd; + m_iPriority = m_timerInfoTag->m_iPriority; + m_iLifetime = m_timerInfoTag->m_iLifetime; + m_iMaxRecordings = m_timerInfoTag->m_iMaxRecordings; + + if (m_bIsNewTimer && m_timerInfoTag->m_strDirectory.empty() && + m_timerType->SupportsRecordingFolders()) + m_strDirectory = m_strTitle; + else + m_strDirectory = m_timerInfoTag->m_strDirectory; + + m_iRecordingGroup = m_timerInfoTag->m_iRecordingGroup; + + InitializeChannelsList(); + InitializeTypesList(); + + // Channel + m_channel = ChannelDescriptor(); + + if (m_timerInfoTag->m_iClientChannelUid == PVR_CHANNEL_INVALID_UID) + { + if (m_timerType->SupportsAnyChannel()) + { + // Select first matching "Any channel" entry. + const auto it = std::find_if(m_channelEntries.cbegin(), m_channelEntries.cend(), + [this](const auto& channel) { + return channel.second.channelUid == PVR_CHANNEL_INVALID_UID && + channel.second.clientId == m_timerInfoTag->m_iClientId; + }); + + if (it != m_channelEntries.cend()) + { + m_channel = (*it).second; + } + else + { + CLog::LogF(LOGERROR, "Unable to map PVR_CHANNEL_INVALID_UID to channel entry!"); + } + } + else if (m_bIsNewTimer) + { + // Select first matching regular (not "Any channel") entry. + const auto it = std::find_if(m_channelEntries.cbegin(), m_channelEntries.cend(), + [this](const auto& channel) { + return channel.second.channelUid != PVR_CHANNEL_INVALID_UID && + channel.second.clientId == m_timerInfoTag->m_iClientId; + }); + + if (it != m_channelEntries.cend()) + { + m_channel = (*it).second; + } + else + { + CLog::LogF(LOGERROR, "Unable to map PVR_CHANNEL_INVALID_UID to channel entry!"); + } + } + } + else + { + // Find matching channel entry + const auto it = std::find_if( + m_channelEntries.cbegin(), m_channelEntries.cend(), [this](const auto& channel) { + return channel.second.channelUid == m_timerInfoTag->m_iClientChannelUid && + channel.second.clientId == m_timerInfoTag->m_iClientId; + }); + + if (it != m_channelEntries.cend()) + { + m_channel = (*it).second; + } + else + { + CLog::LogF(LOGERROR, "Unable to map channel uid to channel entry!"); + } + } +} + +void CGUIDialogPVRTimerSettings::SetupView() +{ + CGUIDialogSettingsManualBase::SetupView(); + SetHeading(19065); + SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON); + SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186); + SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222); + SetButtonLabels(); +} + +void CGUIDialogPVRTimerSettings::InitializeSettings() +{ + CGUIDialogSettingsManualBase::InitializeSettings(); + + const std::shared_ptr<CSettingCategory> category = AddCategory("pvrtimersettings", -1); + if (category == NULL) + { + CLog::LogF(LOGERROR, "Unable to add settings category"); + return; + } + + const std::shared_ptr<CSettingGroup> group = AddGroup(category); + if (group == NULL) + { + CLog::LogF(LOGERROR, "Unable to add settings group"); + return; + } + + std::shared_ptr<CSetting> setting = NULL; + + // Timer type + bool useDetails = false; + bool foundClientSupportingTimers = false; + + const CPVRClientMap clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients(); + for (const auto& client : clients) + { + if (client.second->GetClientCapabilities().SupportsTimers()) + { + if (foundClientSupportingTimers) + { + // found second client supporting timers, use detailed timer type list layout + useDetails = true; + break; + } + foundClientSupportingTimers = true; + } + } + + setting = AddList(group, SETTING_TMR_TYPE, 803, SettingLevel::Basic, 0, TypesFiller, 803, true, + -1, useDetails); + AddTypeDependentEnableCondition(setting, SETTING_TMR_TYPE); + + // Timer enabled/disabled + setting = AddToggle(group, SETTING_TMR_ACTIVE, 305, SettingLevel::Basic, m_bTimerActive); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_ACTIVE); + AddTypeDependentEnableCondition(setting, SETTING_TMR_ACTIVE); + + // Name + setting = + AddEdit(group, SETTING_TMR_NAME, 19075, SettingLevel::Basic, m_strTitle, true, false, 19097); + AddTypeDependentEnableCondition(setting, SETTING_TMR_NAME); + + // epg search string (only for epg-based timer rules) + setting = AddEdit(group, SETTING_TMR_EPGSEARCH, 804, SettingLevel::Basic, m_strEpgSearchString, + true, false, 805); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_EPGSEARCH); + AddTypeDependentEnableCondition(setting, SETTING_TMR_EPGSEARCH); + + // epg fulltext search (only for epg-based timer rules) + setting = AddToggle(group, SETTING_TMR_FULLTEXT, 806, SettingLevel::Basic, m_bFullTextEpgSearch); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_FULLTEXT); + AddTypeDependentEnableCondition(setting, SETTING_TMR_FULLTEXT); + + // Channel + setting = + AddList(group, SETTING_TMR_CHANNEL, 19078, SettingLevel::Basic, 0, ChannelsFiller, 19078); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_CHANNEL); + AddTypeDependentEnableCondition(setting, SETTING_TMR_CHANNEL); + + // Days of week (only for timer rules) + std::vector<int> weekdaysPreselect; + if (m_iWeekdays & PVR_WEEKDAY_MONDAY) + weekdaysPreselect.push_back(PVR_WEEKDAY_MONDAY); + if (m_iWeekdays & PVR_WEEKDAY_TUESDAY) + weekdaysPreselect.push_back(PVR_WEEKDAY_TUESDAY); + if (m_iWeekdays & PVR_WEEKDAY_WEDNESDAY) + weekdaysPreselect.push_back(PVR_WEEKDAY_WEDNESDAY); + if (m_iWeekdays & PVR_WEEKDAY_THURSDAY) + weekdaysPreselect.push_back(PVR_WEEKDAY_THURSDAY); + if (m_iWeekdays & PVR_WEEKDAY_FRIDAY) + weekdaysPreselect.push_back(PVR_WEEKDAY_FRIDAY); + if (m_iWeekdays & PVR_WEEKDAY_SATURDAY) + weekdaysPreselect.push_back(PVR_WEEKDAY_SATURDAY); + if (m_iWeekdays & PVR_WEEKDAY_SUNDAY) + weekdaysPreselect.push_back(PVR_WEEKDAY_SUNDAY); + + setting = AddList(group, SETTING_TMR_WEEKDAYS, 19079, SettingLevel::Basic, weekdaysPreselect, + WeekdaysFiller, 19079, 1, -1, true, -1, WeekdaysValueFormatter); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_WEEKDAYS); + AddTypeDependentEnableCondition(setting, SETTING_TMR_WEEKDAYS); + + // "Start any time" (only for timer rules) + setting = AddToggle(group, SETTING_TMR_START_ANYTIME, 810, SettingLevel::Basic, m_bStartAnyTime); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_START_ANYTIME); + AddTypeDependentEnableCondition(setting, SETTING_TMR_START_ANYTIME); + + // Start day (day + month + year only, no hours, minutes) + setting = AddSpinner(group, SETTING_TMR_START_DAY, 19128, SettingLevel::Basic, + GetDateAsIndex(m_startLocalTime), DaysFiller); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_START_DAY); + AddTypeDependentEnableCondition(setting, SETTING_TMR_START_DAY); + AddStartAnytimeDependentVisibilityCondition(setting, SETTING_TMR_START_DAY); + + // Start time (hours + minutes only, no day, month, year) + setting = AddButton(group, SETTING_TMR_BEGIN, 19126, SettingLevel::Basic); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_BEGIN); + AddTypeDependentEnableCondition(setting, SETTING_TMR_BEGIN); + AddStartAnytimeDependentVisibilityCondition(setting, SETTING_TMR_BEGIN); + + // "End any time" (only for timer rules) + setting = AddToggle(group, SETTING_TMR_END_ANYTIME, 817, SettingLevel::Basic, m_bEndAnyTime); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_END_ANYTIME); + AddTypeDependentEnableCondition(setting, SETTING_TMR_END_ANYTIME); + + // End day (day + month + year only, no hours, minutes) + setting = AddSpinner(group, SETTING_TMR_END_DAY, 19129, SettingLevel::Basic, + GetDateAsIndex(m_endLocalTime), DaysFiller); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_END_DAY); + AddTypeDependentEnableCondition(setting, SETTING_TMR_END_DAY); + AddEndAnytimeDependentVisibilityCondition(setting, SETTING_TMR_END_DAY); + + // End time (hours + minutes only, no day, month, year) + setting = AddButton(group, SETTING_TMR_END, 19127, SettingLevel::Basic); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_END); + AddTypeDependentEnableCondition(setting, SETTING_TMR_END); + AddEndAnytimeDependentVisibilityCondition(setting, SETTING_TMR_END); + + // First day (only for timer rules) + setting = AddSpinner(group, SETTING_TMR_FIRST_DAY, 19084, SettingLevel::Basic, + GetDateAsIndex(m_firstDayLocalTime), DaysFiller); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_FIRST_DAY); + AddTypeDependentEnableCondition(setting, SETTING_TMR_FIRST_DAY); + + // "Prevent duplicate episodes" (only for timer rules) + setting = AddList(group, SETTING_TMR_NEW_EPISODES, 812, SettingLevel::Basic, + m_iPreventDupEpisodes, DupEpisodesFiller, 812); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_NEW_EPISODES); + AddTypeDependentEnableCondition(setting, SETTING_TMR_NEW_EPISODES); + + // Pre and post record time + setting = AddList(group, SETTING_TMR_BEGIN_PRE, 813, SettingLevel::Basic, m_iMarginStart, + MarginTimeFiller, 813); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_BEGIN_PRE); + AddTypeDependentEnableCondition(setting, SETTING_TMR_BEGIN_PRE); + + setting = AddList(group, SETTING_TMR_END_POST, 814, SettingLevel::Basic, m_iMarginEnd, + MarginTimeFiller, 814); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_END_POST); + AddTypeDependentEnableCondition(setting, SETTING_TMR_END_POST); + + // Priority + setting = AddList(group, SETTING_TMR_PRIORITY, 19082, SettingLevel::Basic, m_iPriority, + PrioritiesFiller, 19082); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_PRIORITY); + AddTypeDependentEnableCondition(setting, SETTING_TMR_PRIORITY); + + // Lifetime + setting = AddList(group, SETTING_TMR_LIFETIME, 19083, SettingLevel::Basic, m_iLifetime, + LifetimesFiller, 19083); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_LIFETIME); + AddTypeDependentEnableCondition(setting, SETTING_TMR_LIFETIME); + + // MaxRecordings + setting = AddList(group, SETTING_TMR_MAX_REC, 818, SettingLevel::Basic, m_iMaxRecordings, + MaxRecordingsFiller, 818); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_MAX_REC); + AddTypeDependentEnableCondition(setting, SETTING_TMR_MAX_REC); + + // Recording folder + setting = AddEdit(group, SETTING_TMR_DIR, 19076, SettingLevel::Basic, m_strDirectory, true, false, + 19104); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_DIR); + AddTypeDependentEnableCondition(setting, SETTING_TMR_DIR); + + // Recording group + setting = AddList(group, SETTING_TMR_REC_GROUP, 811, SettingLevel::Basic, m_iRecordingGroup, + RecordingGroupFiller, 811); + AddTypeDependentVisibilityCondition(setting, SETTING_TMR_REC_GROUP); + AddTypeDependentEnableCondition(setting, SETTING_TMR_REC_GROUP); +} + +int CGUIDialogPVRTimerSettings::GetWeekdaysFromSetting(const SettingConstPtr& setting) +{ + std::shared_ptr<const CSettingList> settingList = + std::static_pointer_cast<const CSettingList>(setting); + if (settingList->GetElementType() != SettingType::Integer) + { + CLog::LogF(LOGERROR, "Wrong weekdays element type"); + return 0; + } + int weekdays = 0; + std::vector<CVariant> list = CSettingUtils::GetList(settingList); + for (const auto& value : list) + { + if (!value.isInteger()) + { + CLog::LogF(LOGERROR, "Wrong weekdays value type"); + return 0; + } + weekdays += static_cast<int>(value.asInteger()); + } + + return weekdays; +} + +void CGUIDialogPVRTimerSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == NULL) + { + CLog::LogF(LOGERROR, "No setting"); + return; + } + + CGUIDialogSettingsManualBase::OnSettingChanged(setting); + + const std::string& settingId = setting->GetId(); + + if (settingId == SETTING_TMR_TYPE) + { + int idx = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + const auto it = m_typeEntries.find(idx); + if (it != m_typeEntries.end()) + { + m_timerType = it->second; + + // reset certain settings to the defaults of the new timer type + + if (m_timerType->SupportsPriority()) + m_iPriority = m_timerType->GetPriorityDefault(); + + if (m_timerType->SupportsLifetime()) + m_iLifetime = m_timerType->GetLifetimeDefault(); + + if (m_timerType->SupportsMaxRecordings()) + m_iMaxRecordings = m_timerType->GetMaxRecordingsDefault(); + + if (m_timerType->SupportsRecordingGroup()) + m_iRecordingGroup = m_timerType->GetRecordingGroupDefault(); + + if (m_timerType->SupportsRecordOnlyNewEpisodes()) + m_iPreventDupEpisodes = m_timerType->GetPreventDuplicateEpisodesDefault(); + + if (m_timerType->IsTimerRule() && (m_iWeekdays == PVR_WEEKDAY_ALLDAYS)) + SetButtonLabels(); // update "Any day" vs. "Every day" + } + else + { + CLog::LogF(LOGERROR, "Unable to get 'type' value"); + } + } + else if (settingId == SETTING_TMR_ACTIVE) + { + m_bTimerActive = std::static_pointer_cast<const CSettingBool>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_NAME) + { + m_strTitle = std::static_pointer_cast<const CSettingString>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_EPGSEARCH) + { + m_strEpgSearchString = std::static_pointer_cast<const CSettingString>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_FULLTEXT) + { + m_bFullTextEpgSearch = std::static_pointer_cast<const CSettingBool>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_CHANNEL) + { + int idx = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + const auto it = m_channelEntries.find(idx); + if (it != m_channelEntries.end()) + { + m_channel = it->second; + } + else + { + CLog::LogF(LOGERROR, "Unable to get 'type' value"); + } + } + else if (settingId == SETTING_TMR_WEEKDAYS) + { + m_iWeekdays = GetWeekdaysFromSetting(setting); + } + else if (settingId == SETTING_TMR_START_ANYTIME) + { + m_bStartAnyTime = std::static_pointer_cast<const CSettingBool>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_END_ANYTIME) + { + m_bEndAnyTime = std::static_pointer_cast<const CSettingBool>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_START_DAY) + { + SetDateFromIndex(m_startLocalTime, + std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + } + else if (settingId == SETTING_TMR_END_DAY) + { + SetDateFromIndex(m_endLocalTime, + std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + } + else if (settingId == SETTING_TMR_FIRST_DAY) + { + SetDateFromIndex(m_firstDayLocalTime, + std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + } + else if (settingId == SETTING_TMR_NEW_EPISODES) + { + m_iPreventDupEpisodes = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_BEGIN_PRE) + { + m_iMarginStart = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_END_POST) + { + m_iMarginEnd = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_PRIORITY) + { + m_iPriority = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_LIFETIME) + { + m_iLifetime = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_MAX_REC) + { + m_iMaxRecordings = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_DIR) + { + m_strDirectory = std::static_pointer_cast<const CSettingString>(setting)->GetValue(); + } + else if (settingId == SETTING_TMR_REC_GROUP) + { + m_iRecordingGroup = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + } +} + +void CGUIDialogPVRTimerSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == NULL) + { + CLog::LogF(LOGERROR, "No setting"); + return; + } + + CGUIDialogSettingsManualBase::OnSettingAction(setting); + + const std::string& settingId = setting->GetId(); + if (settingId == SETTING_TMR_BEGIN) + { + KODI::TIME::SystemTime timerStartTime; + m_startLocalTime.GetAsSystemTime(timerStartTime); + if (CGUIDialogNumeric::ShowAndGetTime(timerStartTime, g_localizeStrings.Get(14066))) + { + SetTimeFromSystemTime(m_startLocalTime, timerStartTime); + m_timerStartTimeStr = m_startLocalTime.GetAsLocalizedTime("", false); + SetButtonLabels(); + } + } + else if (settingId == SETTING_TMR_END) + { + KODI::TIME::SystemTime timerEndTime; + m_endLocalTime.GetAsSystemTime(timerEndTime); + if (CGUIDialogNumeric::ShowAndGetTime(timerEndTime, g_localizeStrings.Get(14066))) + { + SetTimeFromSystemTime(m_endLocalTime, timerEndTime); + m_timerEndTimeStr = m_endLocalTime.GetAsLocalizedTime("", false); + SetButtonLabels(); + } + } +} + +bool CGUIDialogPVRTimerSettings::Validate() +{ + // @todo: Timer rules may have no date (time-only), so we can't check those for now. + // We need to extend the api with additional attributes to properly fix this + if (m_timerType->IsTimerRule()) + return true; + + bool bStartAnyTime = m_bStartAnyTime; + bool bEndAnyTime = m_bEndAnyTime; + + if (!m_timerType->SupportsStartAnyTime() || + !m_timerType->IsEpgBased()) // Start anytime toggle is not displayed + bStartAnyTime = false; // Assume start time change needs checking for + + if (!m_timerType->SupportsEndAnyTime() || + !m_timerType->IsEpgBased()) // End anytime toggle is not displayed + bEndAnyTime = false; // Assume end time change needs checking for + + // Begin and end time + if (!bStartAnyTime && !bEndAnyTime) + { + if (m_timerType->SupportsStartTime() && m_timerType->SupportsEndTime() && + m_endLocalTime < m_startLocalTime) + { + HELPERS::ShowOKDialogText(CVariant{19065}, // "Timer settings" + CVariant{19072}); // In order to add/update a timer + return false; + } + } + + return true; +} + +bool CGUIDialogPVRTimerSettings::Save() +{ + if (!Validate()) + return false; + + // Timer type + m_timerInfoTag->SetTimerType(m_timerType); + + // Timer active/inactive + m_timerInfoTag->m_state = m_bTimerActive ? PVR_TIMER_STATE_SCHEDULED : PVR_TIMER_STATE_DISABLED; + + // Name + m_timerInfoTag->m_strTitle = m_strTitle; + + // epg search string (only for epg-based timer rules) + m_timerInfoTag->m_strEpgSearchString = m_strEpgSearchString; + + // epg fulltext search, instead of just title match. (only for epg-based timer rules) + m_timerInfoTag->m_bFullTextEpgSearch = m_bFullTextEpgSearch; + + // Channel + m_timerInfoTag->m_iClientChannelUid = m_channel.channelUid; + m_timerInfoTag->m_iClientId = m_channel.clientId; + m_timerInfoTag->m_bIsRadio = m_bIsRadio; + m_timerInfoTag->UpdateChannel(); + + if (!m_timerType->SupportsStartAnyTime() || + !m_timerType->IsEpgBased()) // Start anytime toggle is not displayed + m_bStartAnyTime = false; // Assume start time change needs checking for + m_timerInfoTag->m_bStartAnyTime = m_bStartAnyTime; + + if (!m_timerType->SupportsEndAnyTime() || + !m_timerType->IsEpgBased()) // End anytime toggle is not displayed + m_bEndAnyTime = false; // Assume end time change needs checking for + m_timerInfoTag->m_bEndAnyTime = m_bEndAnyTime; + + // Begin and end time + if (!m_bStartAnyTime && !m_bEndAnyTime) + { + if (m_timerType->SupportsStartTime() && // has start clock entry + m_timerType->SupportsEndTime() && // and end clock entry + m_timerType->IsTimerRule()) // but no associated start/end day spinners + { + if (m_endLocalTime < m_startLocalTime) // And the end clock is earlier than the start clock + { + CLog::LogFC(LOGDEBUG, LOGPVR, "End before start, adding a day."); + m_endLocalTime += CDateTimeSpan(1, 0, 0, 0); + if (m_endLocalTime < m_startLocalTime) + { + CLog::Log(LOGWARNING, + "Timer settings dialog: End before start. Setting end time to start time."); + m_endLocalTime = m_startLocalTime; + } + } + else if (m_endLocalTime > + (m_startLocalTime + CDateTimeSpan(1, 0, 0, 0))) // Or the duration is more than a day + { + CLog::LogFC(LOGDEBUG, LOGPVR, "End > 1 day after start, removing a day."); + m_endLocalTime -= CDateTimeSpan(1, 0, 0, 0); + if (m_endLocalTime > (m_startLocalTime + CDateTimeSpan(1, 0, 0, 0))) + { + CLog::Log( + LOGWARNING, + "Timer settings dialog: End > 1 day after start. Setting end time to start time."); + m_endLocalTime = m_startLocalTime; + } + } + } + else if (m_endLocalTime < m_startLocalTime) + { + // this case will fail validation so this can't be reached. + } + m_timerInfoTag->SetStartFromLocalTime(m_startLocalTime); + m_timerInfoTag->SetEndFromLocalTime(m_endLocalTime); + } + else if (!m_bStartAnyTime) + m_timerInfoTag->SetStartFromLocalTime(m_startLocalTime); + else if (!m_bEndAnyTime) + m_timerInfoTag->SetEndFromLocalTime(m_endLocalTime); + + // Days of week (only for timer rules) + if (m_timerType->IsTimerRule()) + m_timerInfoTag->m_iWeekdays = m_iWeekdays; + else + m_timerInfoTag->m_iWeekdays = PVR_WEEKDAY_NONE; + + // First day (only for timer rules) + m_timerInfoTag->SetFirstDayFromLocalTime(m_firstDayLocalTime); + + // "New episodes only" (only for timer rules) + m_timerInfoTag->m_iPreventDupEpisodes = m_iPreventDupEpisodes; + + // Pre and post record time + m_timerInfoTag->m_iMarginStart = m_iMarginStart; + m_timerInfoTag->m_iMarginEnd = m_iMarginEnd; + + // Priority + m_timerInfoTag->m_iPriority = m_iPriority; + + // Lifetime + m_timerInfoTag->m_iLifetime = m_iLifetime; + + // MaxRecordings + m_timerInfoTag->m_iMaxRecordings = m_iMaxRecordings; + + // Recording folder + m_timerInfoTag->m_strDirectory = m_strDirectory; + + // Recording group + m_timerInfoTag->m_iRecordingGroup = m_iRecordingGroup; + + // Set the timer's title to the channel name if it's empty or 'New Timer' + if (m_strTitle.empty() || m_strTitle == g_localizeStrings.Get(19056)) + { + const std::string channelName = m_timerInfoTag->ChannelName(); + if (!channelName.empty()) + m_timerInfoTag->m_strTitle = channelName; + } + + // Update summary + m_timerInfoTag->UpdateSummary(); + + return true; +} + +void CGUIDialogPVRTimerSettings::SetButtonLabels() +{ + // timer start time + BaseSettingControlPtr settingControl = GetSettingControl(SETTING_TMR_BEGIN); + if (settingControl != NULL && settingControl->GetControl() != NULL) + { + SET_CONTROL_LABEL2(settingControl->GetID(), m_timerStartTimeStr); + } + + // timer end time + settingControl = GetSettingControl(SETTING_TMR_END); + if (settingControl != NULL && settingControl->GetControl() != NULL) + { + SET_CONTROL_LABEL2(settingControl->GetID(), m_timerEndTimeStr); + } +} + +void CGUIDialogPVRTimerSettings::AddCondition(const std::shared_ptr<CSetting>& setting, + const std::string& identifier, + SettingConditionCheck condition, + SettingDependencyType depType, + const std::string& settingId) +{ + GetSettingsManager()->AddDynamicCondition(identifier, condition, this); + CSettingDependency dep(depType, GetSettingsManager()); + dep.And()->Add(CSettingDependencyConditionPtr( + new CSettingDependencyCondition(identifier, "true", settingId, false, GetSettingsManager()))); + SettingDependencies deps(setting->GetDependencies()); + deps.push_back(dep); + setting->SetDependencies(deps); +} + +int CGUIDialogPVRTimerSettings::GetDateAsIndex(const CDateTime& datetime) +{ + const CDateTime date(datetime.GetYear(), datetime.GetMonth(), datetime.GetDay(), 0, 0, 0); + time_t t(0); + date.GetAsTime(t); + return static_cast<int>(t); +} + +void CGUIDialogPVRTimerSettings::SetDateFromIndex(CDateTime& datetime, int date) +{ + const CDateTime newDate(static_cast<time_t>(date)); + datetime.SetDateTime(newDate.GetYear(), newDate.GetMonth(), newDate.GetDay(), datetime.GetHour(), + datetime.GetMinute(), datetime.GetSecond()); +} + +void CGUIDialogPVRTimerSettings::SetTimeFromSystemTime(CDateTime& datetime, + const KODI::TIME::SystemTime& time) +{ + const CDateTime newTime(time); + datetime.SetDateTime(datetime.GetYear(), datetime.GetMonth(), datetime.GetDay(), + newTime.GetHour(), newTime.GetMinute(), newTime.GetSecond()); +} + +void CGUIDialogPVRTimerSettings::InitializeTypesList() +{ + m_typeEntries.clear(); + + // If timer is read-only or was created by a timer rule, only add current type, for information. Type can't be changed. + if (m_timerType->IsReadOnly() || m_timerInfoTag->HasParent()) + { + m_typeEntries.insert(std::make_pair(0, m_timerType)); + return; + } + + bool bFoundThisType(false); + int idx(0); + const std::vector<std::shared_ptr<CPVRTimerType>> types(CPVRTimerType::GetAllTypes()); + for (const auto& type : types) + { + // Type definition prohibits created of new instances. + // But the dialog can act as a viewer for these types. + if (type->ForbidsNewInstances()) + continue; + + // Read-only timers cannot be created using this dialog. + // But the dialog can act as a viewer for read-only types. + if (type->IsReadOnly()) + continue; + + // Drop TimerTypes that require EPGInfo, if none is populated + if (type->RequiresEpgTagOnCreate() && !m_timerInfoTag->GetEpgInfoTag()) + continue; + + // Drop TimerTypes without 'Series' EPG attributes if none are set + if (type->RequiresEpgSeriesOnCreate()) + { + const std::shared_ptr<CPVREpgInfoTag> epgTag(m_timerInfoTag->GetEpgInfoTag()); + if (epgTag && !epgTag->IsSeries()) + continue; + } + + // Drop TimerTypes which need series link if none is set + if (type->RequiresEpgSeriesLinkOnCreate()) + { + const std::shared_ptr<CPVREpgInfoTag> epgTag(m_timerInfoTag->GetEpgInfoTag()); + if (!epgTag || epgTag->SeriesLink().empty()) + continue; + } + + // Drop TimerTypes that forbid EPGInfo, if it is populated + if (type->ForbidsEpgTagOnCreate() && m_timerInfoTag->GetEpgInfoTag()) + continue; + + // Drop TimerTypes that aren't rules and cannot be recorded + if (!type->IsTimerRule()) + { + const std::shared_ptr<CPVREpgInfoTag> epgTag(m_timerInfoTag->GetEpgInfoTag()); + bool bCanRecord = epgTag ? epgTag->IsRecordable() + : m_timerInfoTag->EndAsLocalTime() > CDateTime::GetCurrentDateTime(); + if (!bCanRecord) + continue; + } + + if (!bFoundThisType && *type == *m_timerType) + bFoundThisType = true; + + m_typeEntries.insert(std::make_pair(idx++, type)); + } + + if (!bFoundThisType) + m_typeEntries.insert(std::make_pair(idx++, m_timerType)); +} + +void CGUIDialogPVRTimerSettings::InitializeChannelsList() +{ + m_channelEntries.clear(); + + int index = 0; + + // Add special "any channel" entries - one for every client (used for epg-based timer rules). + const CPVRClientMap clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients(); + for (const auto& client : clients) + { + m_channelEntries.insert( + {index, ChannelDescriptor(PVR_CHANNEL_INVALID_UID, client.second->GetID(), + g_localizeStrings.Get(809))}); // "Any channel" + ++index; + } + + // Add regular channels + const std::shared_ptr<CPVRChannelGroup> allGroup = + CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_bIsRadio); + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers = + allGroup->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE); + for (const auto& groupMember : groupMembers) + { + const std::shared_ptr<CPVRChannel> channel = groupMember->Channel(); + const std::string channelDescription = StringUtils::Format( + "{} {}", groupMember->ChannelNumber().FormattedChannelNumber(), channel->ChannelName()); + m_channelEntries.insert( + {index, ChannelDescriptor(channel->UniqueID(), channel->ClientID(), channelDescription)}); + ++index; + } +} + +void CGUIDialogPVRTimerSettings::TypesFiller(const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis) + { + list.clear(); + current = 0; + + static const std::vector<std::pair<std::string, CVariant>> reminderTimerProps{ + std::make_pair("PVR.IsRemindingTimer", CVariant{true})}; + static const std::vector<std::pair<std::string, CVariant>> recordingTimerProps{ + std::make_pair("PVR.IsRecordingTimer", CVariant{true})}; + + const auto clients = CServiceBroker::GetPVRManager().Clients(); + + bool foundCurrent(false); + for (const auto& typeEntry : pThis->m_typeEntries) + { + std::string clientName; + + const auto client = clients->GetCreatedClient(typeEntry.second->GetClientId()); + if (client) + clientName = client->GetFriendlyName(); + + list.emplace_back(typeEntry.second->GetDescription(), clientName, typeEntry.first, + typeEntry.second->IsReminder() ? reminderTimerProps : recordingTimerProps); + + if (!foundCurrent && (*(pThis->m_timerType) == *(typeEntry.second))) + { + current = typeEntry.first; + foundCurrent = true; + } + } + } + else + CLog::LogF(LOGERROR, "No dialog"); +} + +void CGUIDialogPVRTimerSettings::ChannelsFiller(const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis) + { + list.clear(); + current = 0; + + bool foundCurrent(false); + for (const auto& channelEntry : pThis->m_channelEntries) + { + // Only include channels for the currently selected timer type or all channels if type is client-independent. + if (pThis->m_timerType->GetClientId() == -1 || // client-independent + pThis->m_timerType->GetClientId() == channelEntry.second.clientId) + { + // Do not add "any channel" entry if not supported by selected timer type. + if (channelEntry.second.channelUid == PVR_CHANNEL_INVALID_UID && + !pThis->m_timerType->SupportsAnyChannel()) + continue; + + list.emplace_back( + IntegerSettingOption(channelEntry.second.description, channelEntry.first)); + } + + if (!foundCurrent && (pThis->m_channel == channelEntry.second)) + { + current = channelEntry.first; + foundCurrent = true; + } + } + } + else + CLog::LogF(LOGERROR, "No dialog"); +} + +void CGUIDialogPVRTimerSettings::DaysFiller(const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis) + { + list.clear(); + current = 0; + + // Data range: "today" until "yesterday next year" + const CDateTime now(CDateTime::GetCurrentDateTime()); + CDateTime time(now.GetYear(), now.GetMonth(), now.GetDay(), 0, 0, 0); + const CDateTime yesterdayPlusOneYear(CDateTime(time.GetYear() + 1, time.GetMonth(), + time.GetDay(), time.GetHour(), time.GetMinute(), + time.GetSecond()) - + CDateTimeSpan(1, 0, 0, 0)); + + CDateTime oldCDateTime; + if (setting->GetId() == SETTING_TMR_FIRST_DAY) + oldCDateTime = pThis->m_timerInfoTag->FirstDayAsLocalTime(); + else if (setting->GetId() == SETTING_TMR_START_DAY) + oldCDateTime = pThis->m_timerInfoTag->StartAsLocalTime(); + else + oldCDateTime = pThis->m_timerInfoTag->EndAsLocalTime(); + const CDateTime oldCDate(oldCDateTime.GetYear(), oldCDateTime.GetMonth(), oldCDateTime.GetDay(), + 0, 0, 0); + + if ((oldCDate < time) || (oldCDate > yesterdayPlusOneYear)) + list.emplace_back(oldCDate.GetAsLocalizedDate(true /*long date*/), GetDateAsIndex(oldCDate)); + + while (time <= yesterdayPlusOneYear) + { + list.emplace_back(time.GetAsLocalizedDate(true /*long date*/), GetDateAsIndex(time)); + time += CDateTimeSpan(1, 0, 0, 0); + } + + if (setting->GetId() == SETTING_TMR_FIRST_DAY) + current = GetDateAsIndex(pThis->m_firstDayLocalTime); + else if (setting->GetId() == SETTING_TMR_START_DAY) + current = GetDateAsIndex(pThis->m_startLocalTime); + else + current = GetDateAsIndex(pThis->m_endLocalTime); + } + else + CLog::LogF(LOGERROR, "No dialog"); +} + +void CGUIDialogPVRTimerSettings::DupEpisodesFiller(const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis) + { + list.clear(); + + std::vector<std::pair<std::string, int>> values; + pThis->m_timerType->GetPreventDuplicateEpisodesValues(values); + std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) { + return IntegerSettingOption(value.first, value.second); + }); + + current = pThis->m_iPreventDupEpisodes; + } + else + CLog::LogF(LOGERROR, "No dialog"); +} + +void CGUIDialogPVRTimerSettings::WeekdaysFiller(const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis) + { + list.clear(); + list.emplace_back(g_localizeStrings.Get(831), PVR_WEEKDAY_MONDAY); // "Mondays" + list.emplace_back(g_localizeStrings.Get(832), PVR_WEEKDAY_TUESDAY); // "Tuesdays" + list.emplace_back(g_localizeStrings.Get(833), PVR_WEEKDAY_WEDNESDAY); // "Wednesdays" + list.emplace_back(g_localizeStrings.Get(834), PVR_WEEKDAY_THURSDAY); // "Thursdays" + list.emplace_back(g_localizeStrings.Get(835), PVR_WEEKDAY_FRIDAY); // "Fridays" + list.emplace_back(g_localizeStrings.Get(836), PVR_WEEKDAY_SATURDAY); // "Saturdays" + list.emplace_back(g_localizeStrings.Get(837), PVR_WEEKDAY_SUNDAY); // "Sundays" + + current = pThis->m_iWeekdays; + } + else + CLog::LogF(LOGERROR, "No dialog"); +} + +void CGUIDialogPVRTimerSettings::PrioritiesFiller(const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis) + { + list.clear(); + + std::vector<std::pair<std::string, int>> values; + pThis->m_timerType->GetPriorityValues(values); + std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) { + return IntegerSettingOption(value.first, value.second); + }); + + current = pThis->m_iPriority; + + auto it = list.begin(); + while (it != list.end()) + { + if (it->value == current) + break; // value already in list + + ++it; + } + + if (it == list.end()) + { + // PVR backend supplied value is not in the list of predefined values. Insert it. + list.insert(it, IntegerSettingOption(std::to_string(current), current)); + } + } + else + CLog::LogF(LOGERROR, "No dialog"); +} + +void CGUIDialogPVRTimerSettings::LifetimesFiller(const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis) + { + list.clear(); + + std::vector<std::pair<std::string, int>> values; + pThis->m_timerType->GetLifetimeValues(values); + std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) { + return IntegerSettingOption(value.first, value.second); + }); + + current = pThis->m_iLifetime; + + auto it = list.begin(); + while (it != list.end()) + { + if (it->value == current) + break; // value already in list + + ++it; + } + + if (it == list.end()) + { + // PVR backend supplied value is not in the list of predefined values. Insert it. + list.insert(it, IntegerSettingOption( + StringUtils::Format(g_localizeStrings.Get(17999), current) /* {} days */, + current)); + } + } + else + CLog::LogF(LOGERROR, "No dialog"); +} + +void CGUIDialogPVRTimerSettings::MaxRecordingsFiller(const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis) + { + list.clear(); + + std::vector<std::pair<std::string, int>> values; + pThis->m_timerType->GetMaxRecordingsValues(values); + std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) { + return IntegerSettingOption(value.first, value.second); + }); + + current = pThis->m_iMaxRecordings; + + auto it = list.begin(); + while (it != list.end()) + { + if (it->value == current) + break; // value already in list + + ++it; + } + + if (it == list.end()) + { + // PVR backend supplied value is not in the list of predefined values. Insert it. + list.insert(it, IntegerSettingOption(std::to_string(current), current)); + } + } + else + CLog::LogF(LOGERROR, "No dialog"); +} + +void CGUIDialogPVRTimerSettings::RecordingGroupFiller(const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis) + { + list.clear(); + + std::vector<std::pair<std::string, int>> values; + pThis->m_timerType->GetRecordingGroupValues(values); + std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) { + return IntegerSettingOption(value.first, value.second); + }); + + current = pThis->m_iRecordingGroup; + } + else + CLog::LogF(LOGERROR, "No dialog"); +} + +void CGUIDialogPVRTimerSettings::MarginTimeFiller(const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis) + { + list.clear(); + + // Get global settings values + CPVRSettings::MarginTimeFiller(setting, list, current, data); + + if (setting->GetId() == SETTING_TMR_BEGIN_PRE) + current = pThis->m_iMarginStart; + else + current = pThis->m_iMarginEnd; + + bool bInsertValue = true; + auto it = list.begin(); + while (it != list.end()) + { + if (it->value == current) + { + bInsertValue = false; + break; // value already in list + } + + if (it->value > current) + break; + + ++it; + } + + if (bInsertValue) + { + // PVR backend supplied value is not in the list of predefined values. Insert it. + list.insert(it, IntegerSettingOption( + StringUtils::Format(g_localizeStrings.Get(14044), current) /* {} min */, + current)); + } + } + else + CLog::LogF(LOGERROR, "No dialog"); +} + +std::string CGUIDialogPVRTimerSettings::WeekdaysValueFormatter(const SettingConstPtr& setting) +{ + return CPVRTimerInfoTag::GetWeekdaysString(GetWeekdaysFromSetting(setting), true, true); +} + +void CGUIDialogPVRTimerSettings::AddTypeDependentEnableCondition( + const std::shared_ptr<CSetting>& setting, const std::string& identifier) +{ + // Enable setting depending on read-only attribute of the selected timer type + std::string id(identifier); + id.append(TYPE_DEP_ENABLE_COND_ID_POSTFIX); + AddCondition(setting, id, TypeReadOnlyCondition, SettingDependencyType::Enable, SETTING_TMR_TYPE); +} + +bool CGUIDialogPVRTimerSettings::TypeReadOnlyCondition(const std::string& condition, + const std::string& value, + const SettingConstPtr& setting, + void* data) +{ + if (setting == NULL) + return false; + + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis == NULL) + { + CLog::LogF(LOGERROR, "No dialog"); + return false; + } + + if (!StringUtils::EqualsNoCase(value, "true")) + return false; + + std::string cond(condition); + cond.erase(cond.find(TYPE_DEP_ENABLE_COND_ID_POSTFIX)); + + // If only one type is available, disable type selector. + if (pThis->m_typeEntries.size() == 1) + { + if (cond == SETTING_TMR_TYPE) + return false; + } + + // For existing one time epg-based timers, disable editing of epg-filled data. + if (!pThis->m_bIsNewTimer && pThis->m_timerType->IsEpgBasedOnetime()) + { + if ((cond == SETTING_TMR_NAME) || (cond == SETTING_TMR_CHANNEL) || + (cond == SETTING_TMR_START_DAY) || (cond == SETTING_TMR_END_DAY) || + (cond == SETTING_TMR_BEGIN) || (cond == SETTING_TMR_END)) + return false; + } + + /* Always enable enable/disable, if supported by the timer type. */ + if (pThis->m_timerType->SupportsEnableDisable() && !pThis->m_timerInfoTag->IsBroken()) + { + if (cond == SETTING_TMR_ACTIVE) + return true; + } + + // Let the PVR client decide... + int idx = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + const auto entry = pThis->m_typeEntries.find(idx); + if (entry != pThis->m_typeEntries.end()) + return !entry->second->IsReadOnly(); + else + CLog::LogF(LOGERROR, "No type entry"); + + return false; +} + +void CGUIDialogPVRTimerSettings::AddTypeDependentVisibilityCondition( + const std::shared_ptr<CSetting>& setting, const std::string& identifier) +{ + // Show or hide setting depending on attributes of the selected timer type + std::string id(identifier); + id.append(TYPE_DEP_VISIBI_COND_ID_POSTFIX); + AddCondition(setting, id, TypeSupportsCondition, SettingDependencyType::Visible, + SETTING_TMR_TYPE); +} + +bool CGUIDialogPVRTimerSettings::TypeSupportsCondition(const std::string& condition, + const std::string& value, + const SettingConstPtr& setting, + void* data) +{ + if (setting == NULL) + return false; + + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis == NULL) + { + CLog::LogF(LOGERROR, "No dialog"); + return false; + } + + if (!StringUtils::EqualsNoCase(value, "true")) + return false; + + int idx = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + const auto entry = pThis->m_typeEntries.find(idx); + if (entry != pThis->m_typeEntries.end()) + { + std::string cond(condition); + cond.erase(cond.find(TYPE_DEP_VISIBI_COND_ID_POSTFIX)); + + if (cond == SETTING_TMR_EPGSEARCH) + return entry->second->SupportsEpgTitleMatch() || entry->second->SupportsEpgFulltextMatch(); + else if (cond == SETTING_TMR_FULLTEXT) + return entry->second->SupportsEpgFulltextMatch(); + else if (cond == SETTING_TMR_ACTIVE) + return entry->second->SupportsEnableDisable(); + else if (cond == SETTING_TMR_CHANNEL) + return entry->second->SupportsChannels(); + else if (cond == SETTING_TMR_START_ANYTIME) + return entry->second->SupportsStartAnyTime() && entry->second->IsEpgBased(); + else if (cond == SETTING_TMR_END_ANYTIME) + return entry->second->SupportsEndAnyTime() && entry->second->IsEpgBased(); + else if (cond == SETTING_TMR_START_DAY) + return entry->second->SupportsStartTime() && entry->second->IsOnetime(); + else if (cond == SETTING_TMR_END_DAY) + return entry->second->SupportsEndTime() && entry->second->IsOnetime(); + else if (cond == SETTING_TMR_BEGIN) + return entry->second->SupportsStartTime(); + else if (cond == SETTING_TMR_END) + return entry->second->SupportsEndTime(); + else if (cond == SETTING_TMR_WEEKDAYS) + return entry->second->SupportsWeekdays(); + else if (cond == SETTING_TMR_FIRST_DAY) + return entry->second->SupportsFirstDay(); + else if (cond == SETTING_TMR_NEW_EPISODES) + return entry->second->SupportsRecordOnlyNewEpisodes(); + else if (cond == SETTING_TMR_BEGIN_PRE) + return entry->second->SupportsStartMargin(); + else if (cond == SETTING_TMR_END_POST) + return entry->second->SupportsEndMargin(); + else if (cond == SETTING_TMR_PRIORITY) + return entry->second->SupportsPriority(); + else if (cond == SETTING_TMR_LIFETIME) + return entry->second->SupportsLifetime(); + else if (cond == SETTING_TMR_MAX_REC) + return entry->second->SupportsMaxRecordings(); + else if (cond == SETTING_TMR_DIR) + return entry->second->SupportsRecordingFolders(); + else if (cond == SETTING_TMR_REC_GROUP) + return entry->second->SupportsRecordingGroup(); + else + CLog::LogF(LOGERROR, "Unknown condition"); + } + else + { + CLog::LogF(LOGERROR, "No type entry"); + } + return false; +} + +void CGUIDialogPVRTimerSettings::AddStartAnytimeDependentVisibilityCondition( + const std::shared_ptr<CSetting>& setting, const std::string& identifier) +{ + // Show or hide setting depending on value of setting "any time" + std::string id(identifier); + id.append(START_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX); + AddCondition(setting, id, StartAnytimeSetCondition, SettingDependencyType::Visible, + SETTING_TMR_START_ANYTIME); +} + +bool CGUIDialogPVRTimerSettings::StartAnytimeSetCondition(const std::string& condition, + const std::string& value, + const SettingConstPtr& setting, + void* data) +{ + if (setting == NULL) + return false; + + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis == NULL) + { + CLog::LogF(LOGERROR, "No dialog"); + return false; + } + + if (!StringUtils::EqualsNoCase(value, "true")) + return false; + + // "any time" setting is only relevant for epg-based timers. + if (!pThis->m_timerType->IsEpgBased()) + return true; + + // If 'Start anytime' option isn't supported, don't hide start time + if (!pThis->m_timerType->SupportsStartAnyTime()) + return true; + + std::string cond(condition); + cond.erase(cond.find(START_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX)); + + if ((cond == SETTING_TMR_START_DAY) || (cond == SETTING_TMR_BEGIN)) + { + bool bAnytime = std::static_pointer_cast<const CSettingBool>(setting)->GetValue(); + return !bAnytime; + } + return false; +} + +void CGUIDialogPVRTimerSettings::AddEndAnytimeDependentVisibilityCondition( + const std::shared_ptr<CSetting>& setting, const std::string& identifier) +{ + // Show or hide setting depending on value of setting "any time" + std::string id(identifier); + id.append(END_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX); + AddCondition(setting, id, EndAnytimeSetCondition, SettingDependencyType::Visible, + SETTING_TMR_END_ANYTIME); +} + +bool CGUIDialogPVRTimerSettings::EndAnytimeSetCondition(const std::string& condition, + const std::string& value, + const SettingConstPtr& setting, + void* data) +{ + if (setting == NULL) + return false; + + CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data); + if (pThis == NULL) + { + CLog::LogF(LOGERROR, "No dialog"); + return false; + } + + if (!StringUtils::EqualsNoCase(value, "true")) + return false; + + // "any time" setting is only relevant for epg-based timers. + if (!pThis->m_timerType->IsEpgBased()) + return true; + + // If 'End anytime' option isn't supported, don't hide end time + if (!pThis->m_timerType->SupportsEndAnyTime()) + return true; + + std::string cond(condition); + cond.erase(cond.find(END_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX)); + + if ((cond == SETTING_TMR_END_DAY) || (cond == SETTING_TMR_END)) + { + bool bAnytime = std::static_pointer_cast<const CSettingBool>(setting)->GetValue(); + return !bAnytime; + } + return false; +} diff --git a/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.h b/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.h new file mode 100644 index 0000000..d3e9e3e --- /dev/null +++ b/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.h @@ -0,0 +1,196 @@ +/* + * 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_channels.h" // PVR_CHANNEL_INVALID_UID +#include "settings/SettingConditions.h" +#include "settings/dialogs/GUIDialogSettingsManualBase.h" +#include "settings/lib/SettingDependency.h" + +#include <map> +#include <memory> +#include <string> +#include <vector> + +class CSetting; + +struct IntegerSettingOption; + +namespace PVR +{ +class CPVRTimerInfoTag; +class CPVRTimerType; + +class CGUIDialogPVRTimerSettings : public CGUIDialogSettingsManualBase +{ +public: + CGUIDialogPVRTimerSettings(); + ~CGUIDialogPVRTimerSettings() override; + + bool CanBeActivated() const override; + + void SetTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer); + +protected: + // implementation of ISettingCallback + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override; + + // specialization of CGUIDialogSettingsBase + bool AllowResettingSettings() const override { return false; } + bool Save() override; + void SetupView() override; + + // specialization of CGUIDialogSettingsManualBase + void InitializeSettings() override; + +private: + bool Validate(); + void InitializeTypesList(); + void InitializeChannelsList(); + void SetButtonLabels(); + + static int GetDateAsIndex(const CDateTime& datetime); + static void SetDateFromIndex(CDateTime& datetime, int date); + static void SetTimeFromSystemTime(CDateTime& datetime, const KODI::TIME::SystemTime& time); + + static int GetWeekdaysFromSetting(const std::shared_ptr<const CSetting>& setting); + + static void TypesFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + static void ChannelsFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + static void DaysFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + static void DupEpisodesFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + static void WeekdaysFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + static void PrioritiesFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + static void LifetimesFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + static void MaxRecordingsFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + static void RecordingGroupFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + static void MarginTimeFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + + static std::string WeekdaysValueFormatter(const std::shared_ptr<const CSetting>& setting); + + void AddCondition(const std::shared_ptr<CSetting>& setting, + const std::string& identifier, + SettingConditionCheck condition, + SettingDependencyType depType, + const std::string& settingId); + + void AddTypeDependentEnableCondition(const std::shared_ptr<CSetting>& setting, + const std::string& identifier); + static bool TypeReadOnlyCondition(const std::string& condition, + const std::string& value, + const std::shared_ptr<const CSetting>& setting, + void* data); + + void AddTypeDependentVisibilityCondition(const std::shared_ptr<CSetting>& setting, + const std::string& identifier); + static bool TypeSupportsCondition(const std::string& condition, + const std::string& value, + const std::shared_ptr<const CSetting>& setting, + void* data); + + void AddStartAnytimeDependentVisibilityCondition(const std::shared_ptr<CSetting>& setting, + const std::string& identifier); + static bool StartAnytimeSetCondition(const std::string& condition, + const std::string& value, + const std::shared_ptr<const CSetting>& setting, + void* data); + void AddEndAnytimeDependentVisibilityCondition(const std::shared_ptr<CSetting>& setting, + const std::string& identifier); + static bool EndAnytimeSetCondition(const std::string& condition, + const std::string& value, + const std::shared_ptr<const CSetting>& setting, + void* data); + + typedef std::map<int, std::shared_ptr<CPVRTimerType>> TypeEntriesMap; + + typedef struct ChannelDescriptor + { + int channelUid; + int clientId; + std::string description; + + ChannelDescriptor(int _channelUid = PVR_CHANNEL_INVALID_UID, + int _clientId = -1, + const std::string& _description = "") + : channelUid(_channelUid), clientId(_clientId), description(_description) + { + } + + inline bool operator==(const ChannelDescriptor& right) const + { + return (channelUid == right.channelUid && clientId == right.clientId && + description == right.description); + } + + } ChannelDescriptor; + + typedef std::map<int, ChannelDescriptor> ChannelEntriesMap; + + std::shared_ptr<CPVRTimerInfoTag> m_timerInfoTag; + TypeEntriesMap m_typeEntries; + ChannelEntriesMap m_channelEntries; + std::string m_timerStartTimeStr; + std::string m_timerEndTimeStr; + + std::shared_ptr<CPVRTimerType> m_timerType; + bool m_bIsRadio = false; + bool m_bIsNewTimer = true; + bool m_bTimerActive = false; + std::string m_strTitle; + std::string m_strEpgSearchString; + bool m_bFullTextEpgSearch = true; + ChannelDescriptor m_channel; + CDateTime m_startLocalTime; + CDateTime m_endLocalTime; + bool m_bStartAnyTime = false; + bool m_bEndAnyTime = false; + unsigned int m_iWeekdays; + CDateTime m_firstDayLocalTime; + unsigned int m_iPreventDupEpisodes = 0; + unsigned int m_iMarginStart = 0; + unsigned int m_iMarginEnd = 0; + int m_iPriority = 0; + int m_iLifetime = 0; + int m_iMaxRecordings = 0; + std::string m_strDirectory; + unsigned int m_iRecordingGroup = 0; +}; +} // namespace PVR 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 diff --git a/xbmc/pvr/filesystem/CMakeLists.txt b/xbmc/pvr/filesystem/CMakeLists.txt new file mode 100644 index 0000000..823d783 --- /dev/null +++ b/xbmc/pvr/filesystem/CMakeLists.txt @@ -0,0 +1,5 @@ +set(SOURCES PVRGUIDirectory.cpp) + +set(HEADERS PVRGUIDirectory.h) + +core_add_library(pvr_filesystem) diff --git a/xbmc/pvr/filesystem/PVRGUIDirectory.cpp b/xbmc/pvr/filesystem/PVRGUIDirectory.cpp new file mode 100644 index 0000000..1ef7810 --- /dev/null +++ b/xbmc/pvr/filesystem/PVRGUIDirectory.cpp @@ -0,0 +1,625 @@ +/* + * Copyright (C) 2012-2019 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 "PVRGUIDirectory.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "input/WindowTranslator.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/channels/PVRChannelsPath.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/epg/EpgSearchFilter.h" +#include "pvr/epg/EpgSearchPath.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/recordings/PVRRecordingsPath.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "pvr/timers/PVRTimersPath.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <memory> +#include <set> +#include <string> +#include <vector> + +using namespace PVR; + +bool CPVRGUIDirectory::Exists() const +{ + if (!CServiceBroker::GetPVRManager().IsStarted()) + return false; + + return m_url.IsProtocol("pvr") && StringUtils::StartsWith(m_url.GetFileName(), "recordings"); +} + +bool CPVRGUIDirectory::SupportsWriteFileOperations() const +{ + if (!CServiceBroker::GetPVRManager().IsStarted()) + return false; + + const std::string filename = m_url.GetFileName(); + return URIUtils::IsPVRRecording(filename); +} + +namespace +{ + +bool GetRootDirectory(bool bRadio, CFileItemList& results) +{ + std::shared_ptr<CFileItem> item; + + const std::shared_ptr<CPVRClients> clients = CServiceBroker::GetPVRManager().Clients(); + + // EPG + const bool bAnyClientSupportingEPG = clients->AnyClientSupportingEPG(); + if (bAnyClientSupportingEPG) + { + item.reset( + new CFileItem(StringUtils::Format("pvr://guide/{}/", bRadio ? "radio" : "tv"), true)); + item->SetLabel(g_localizeStrings.Get(19069)); // Guide + item->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio ? WINDOW_RADIO_GUIDE + : WINDOW_TV_GUIDE)); + item->SetArt("icon", "DefaultPVRGuide.png"); + results.Add(item); + } + + // Channels + item.reset(new CFileItem( + bRadio ? CPVRChannelsPath::PATH_RADIO_CHANNELS : CPVRChannelsPath::PATH_TV_CHANNELS, true)); + item->SetLabel(g_localizeStrings.Get(19019)); // Channels + item->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio ? WINDOW_RADIO_CHANNELS + : WINDOW_TV_CHANNELS)); + item->SetArt("icon", "DefaultPVRChannels.png"); + results.Add(item); + + // Recordings + if (clients->AnyClientSupportingRecordings()) + { + item.reset(new CFileItem(bRadio ? CPVRRecordingsPath::PATH_ACTIVE_RADIO_RECORDINGS + : CPVRRecordingsPath::PATH_ACTIVE_TV_RECORDINGS, + true)); + item->SetLabel(g_localizeStrings.Get(19017)); // Recordings + item->SetProperty("node.target", CWindowTranslator::TranslateWindow( + bRadio ? WINDOW_RADIO_RECORDINGS : WINDOW_TV_RECORDINGS)); + item->SetArt("icon", "DefaultPVRRecordings.png"); + results.Add(item); + } + + // Timers/Timer rules + // - always present, because Reminders are always available, no client support needed for this + item.reset(new CFileItem( + bRadio ? CPVRTimersPath::PATH_RADIO_TIMERS : CPVRTimersPath::PATH_TV_TIMERS, true)); + item->SetLabel(g_localizeStrings.Get(19040)); // Timers + item->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio ? WINDOW_RADIO_TIMERS + : WINDOW_TV_TIMERS)); + item->SetArt("icon", "DefaultPVRTimers.png"); + results.Add(item); + + item.reset(new CFileItem( + bRadio ? CPVRTimersPath::PATH_RADIO_TIMER_RULES : CPVRTimersPath::PATH_TV_TIMER_RULES, true)); + item->SetLabel(g_localizeStrings.Get(19138)); // Timer rules + item->SetProperty("node.target", CWindowTranslator::TranslateWindow( + bRadio ? WINDOW_RADIO_TIMER_RULES : WINDOW_TV_TIMER_RULES)); + item->SetArt("icon", "DefaultPVRTimerRules.png"); + results.Add(item); + + // Search + if (bAnyClientSupportingEPG) + { + item.reset(new CFileItem( + bRadio ? CPVREpgSearchPath::PATH_RADIO_SEARCH : CPVREpgSearchPath::PATH_TV_SEARCH, true)); + item->SetLabel(g_localizeStrings.Get(137)); // Search + item->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio ? WINDOW_RADIO_SEARCH + : WINDOW_TV_SEARCH)); + item->SetArt("icon", "DefaultPVRSearch.png"); + results.Add(item); + } + + return true; +} + +} // unnamed namespace + +bool CPVRGUIDirectory::GetDirectory(CFileItemList& results) const +{ + std::string base = m_url.Get(); + URIUtils::RemoveSlashAtEnd(base); + + std::string fileName = m_url.GetFileName(); + URIUtils::RemoveSlashAtEnd(fileName); + + results.SetCacheToDisc(CFileItemList::CACHE_NEVER); + + if (fileName.empty()) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + std::shared_ptr<CFileItem> item; + + item.reset(new CFileItem(base + "channels/", true)); + item->SetLabel(g_localizeStrings.Get(19019)); // Channels + item->SetLabelPreformatted(true); + results.Add(item); + + item.reset(new CFileItem(base + "recordings/active/", true)); + item->SetLabel(g_localizeStrings.Get(19017)); // Recordings + item->SetLabelPreformatted(true); + results.Add(item); + + item.reset(new CFileItem(base + "recordings/deleted/", true)); + item->SetLabel(g_localizeStrings.Get(19184)); // Deleted recordings + item->SetLabelPreformatted(true); + results.Add(item); + + // Sort by name only. Labels are preformatted. + results.AddSortMethod(SortByLabel, 551 /* Name */, LABEL_MASKS("%L", "", "%L", "")); + } + return true; + } + else if (StringUtils::StartsWith(fileName, "tv")) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + return GetRootDirectory(false, results); + } + return true; + } + else if (StringUtils::StartsWith(fileName, "radio")) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + return GetRootDirectory(true, results); + } + return true; + } + else if (StringUtils::StartsWith(fileName, "recordings")) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + return GetRecordingsDirectory(results); + } + return true; + } + else if (StringUtils::StartsWith(fileName, "channels")) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + return GetChannelsDirectory(results); + } + return true; + } + else if (StringUtils::StartsWith(fileName, "timers")) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + return GetTimersDirectory(results); + } + return true; + } + + const CPVREpgSearchPath path(m_url.Get()); + if (path.IsValid()) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + if (path.IsSavedSearchesRoot()) + return GetSavedSearchesDirectory(path.IsRadio(), results); + } + return true; + } + + return false; +} + +bool CPVRGUIDirectory::HasTVRecordings() +{ + return CServiceBroker::GetPVRManager().IsStarted() && + CServiceBroker::GetPVRManager().Recordings()->GetNumTVRecordings() > 0; +} + +bool CPVRGUIDirectory::HasDeletedTVRecordings() +{ + return CServiceBroker::GetPVRManager().IsStarted() && + CServiceBroker::GetPVRManager().Recordings()->HasDeletedTVRecordings(); +} + +bool CPVRGUIDirectory::HasRadioRecordings() +{ + return CServiceBroker::GetPVRManager().IsStarted() && + CServiceBroker::GetPVRManager().Recordings()->GetNumRadioRecordings() > 0; +} + +bool CPVRGUIDirectory::HasDeletedRadioRecordings() +{ + return CServiceBroker::GetPVRManager().IsStarted() && + CServiceBroker::GetPVRManager().Recordings()->HasDeletedRadioRecordings(); +} + +namespace +{ + +std::string TrimSlashes(const std::string& strOrig) +{ + std::string strReturn = strOrig; + while (strReturn[0] == '/') + strReturn.erase(0, 1); + + URIUtils::RemoveSlashAtEnd(strReturn); + return strReturn; +} + +bool IsDirectoryMember(const std::string& strDirectory, + const std::string& strEntryDirectory, + bool bGrouped) +{ + const std::string strUseDirectory = TrimSlashes(strDirectory); + const std::string strUseEntryDirectory = TrimSlashes(strEntryDirectory); + + // Case-insensitive comparison since sub folders are created with case-insensitive matching (GetSubDirectories) + if (bGrouped) + return StringUtils::EqualsNoCase(strUseDirectory, strUseEntryDirectory); + else + return StringUtils::StartsWithNoCase(strUseEntryDirectory, strUseDirectory); +} + +void GetSubDirectories(const CPVRRecordingsPath& recParentPath, + const std::vector<std::shared_ptr<CPVRRecording>>& recordings, + CFileItemList& results) +{ + // Only active recordings are fetched to provide sub directories. + // Not applicable for deleted view which is supposed to be flattened. + std::set<std::shared_ptr<CFileItem>> unwatchedFolders; + bool bRadio = recParentPath.IsRadio(); + + for (const auto& recording : recordings) + { + if (recording->IsDeleted()) + continue; + + if (recording->IsRadio() != bRadio) + continue; + + const std::string strCurrent = + recParentPath.GetUnescapedSubDirectoryPath(recording->Directory()); + if (strCurrent.empty()) + continue; + + CPVRRecordingsPath recChildPath(recParentPath); + recChildPath.AppendSegment(strCurrent); + const std::string strFilePath = recChildPath; + + std::shared_ptr<CFileItem> item; + if (!results.Contains(strFilePath)) + { + item.reset(new CFileItem(strCurrent, true)); + item->SetPath(strFilePath); + item->SetLabel(strCurrent); + item->SetLabelPreformatted(true); + item->m_dateTime = recording->RecordingTimeAsLocalTime(); + item->SetProperty("totalepisodes", 0); + item->SetProperty("watchedepisodes", 0); + item->SetProperty("unwatchedepisodes", 0); + item->SetProperty("sizeinbytes", UINT64_C(0)); + + // Assume all folders are watched, we'll change the overlay later + item->SetOverlayImage(CGUIListItem::ICON_OVERLAY_WATCHED, false); + results.Add(item); + } + else + { + item = results.Get(strFilePath); + if (item->m_dateTime < recording->RecordingTimeAsLocalTime()) + item->m_dateTime = recording->RecordingTimeAsLocalTime(); + } + + item->IncrementProperty("totalepisodes", 1); + if (recording->GetPlayCount() == 0) + { + unwatchedFolders.insert(item); + item->IncrementProperty("unwatchedepisodes", 1); + } + else + { + item->IncrementProperty("watchedepisodes", 1); + } + item->SetLabel2(StringUtils::Format("{} / {}", item->GetProperty("watchedepisodes").asString(), + item->GetProperty("totalepisodes").asString())); + + item->IncrementProperty("sizeinbytes", recording->GetSizeInBytes()); + } + + // Replace the incremental size of the recordings with a string equivalent + for (auto& item : results.GetList()) + { + int64_t size = item->GetProperty("sizeinbytes").asInteger(); + item->ClearProperty("sizeinbytes"); + item->m_dwSize = size; // We'll also sort recording folders by size + if (size > 0) + item->SetProperty("recordingsize", StringUtils::SizeToString(size)); + } + + // Change the watched overlay to unwatched for folders containing unwatched entries + for (auto& item : unwatchedFolders) + item->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, false); +} + +} // unnamed namespace + +bool CPVRGUIDirectory::GetRecordingsDirectory(CFileItemList& results) const +{ + results.SetContent("recordings"); + + bool bGrouped = false; + const std::vector<std::shared_ptr<CPVRRecording>> recordings = + CServiceBroker::GetPVRManager().Recordings()->GetAll(); + + if (m_url.HasOption("view")) + { + const std::string view = m_url.GetOption("view"); + if (view == "grouped") + bGrouped = true; + else if (view == "flat") + bGrouped = false; + else + { + CLog::LogF(LOGERROR, "Unsupported value '{}' for url parameter 'view'", view); + return false; + } + } + else + { + bGrouped = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_PVRRECORD_GROUPRECORDINGS); + } + + const CPVRRecordingsPath recPath(m_url.GetWithoutOptions()); + if (recPath.IsValid()) + { + // Get the directory structure if in non-flatten mode + // Deleted view is always flatten. So only for an active view + const std::string strDirectory = recPath.GetUnescapedDirectoryPath(); + if (!recPath.IsDeleted() && bGrouped) + GetSubDirectories(recPath, recordings, results); + + // get all files of the current directory or recursively all files starting at the current directory if in flatten mode + std::shared_ptr<CFileItem> item; + for (const auto& recording : recordings) + { + // Omit recordings not matching criteria + if (recording->IsDeleted() != recPath.IsDeleted() || + recording->IsRadio() != recPath.IsRadio() || + !IsDirectoryMember(strDirectory, recording->Directory(), bGrouped)) + continue; + + item = std::make_shared<CFileItem>(recording); + item->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, recording->GetPlayCount() > 0); + results.Add(item); + } + } + + return recPath.IsValid(); +} + +bool CPVRGUIDirectory::GetSavedSearchesDirectory(bool bRadio, CFileItemList& results) const +{ + const std::vector<std::shared_ptr<CPVREpgSearchFilter>> searches = + CServiceBroker::GetPVRManager().EpgContainer().GetSavedSearches(bRadio); + + for (const auto& search : searches) + { + results.Add(std::make_shared<CFileItem>(search)); + } + return true; +} + +bool CPVRGUIDirectory::GetChannelGroupsDirectory(bool bRadio, + bool bExcludeHidden, + CFileItemList& results) +{ + const CPVRChannelGroups* channelGroups = + CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio); + if (channelGroups) + { + std::shared_ptr<CFileItem> item; + const std::vector<std::shared_ptr<CPVRChannelGroup>> groups = + channelGroups->GetMembers(bExcludeHidden); + for (const auto& group : groups) + { + item = std::make_shared<CFileItem>(group->GetPath(), true); + item->m_strTitle = group->GroupName(); + item->SetLabel(group->GroupName()); + results.Add(item); + } + return true; + } + return false; +} + +bool CPVRGUIDirectory::GetChannelsDirectory(CFileItemList& results) const +{ + const CPVRChannelsPath path(m_url.GetWithoutOptions()); + if (path.IsValid()) + { + if (path.IsEmpty()) + { + std::shared_ptr<CFileItem> item; + + // all tv channels + item.reset(new CFileItem(CPVRChannelsPath::PATH_TV_CHANNELS, true)); + item->SetLabel(g_localizeStrings.Get(19020)); // TV + item->SetLabelPreformatted(true); + results.Add(item); + + // all radio channels + item.reset(new CFileItem(CPVRChannelsPath::PATH_RADIO_CHANNELS, true)); + item->SetLabel(g_localizeStrings.Get(19021)); // Radio + item->SetLabelPreformatted(true); + results.Add(item); + + return true; + } + else if (path.IsChannelsRoot()) + { + return GetChannelGroupsDirectory(path.IsRadio(), true, results); + } + else if (path.IsChannelGroup()) + { + const std::string& strGroupName = path.GetGroupName(); + bool bShowHiddenChannels = path.IsHiddenChannelGroup(); + + std::shared_ptr<CPVRChannelGroup> group; + if (bShowHiddenChannels || strGroupName == "*") // all channels + { + group = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(path.IsRadio()); + } + else + { + group = CServiceBroker::GetPVRManager() + .ChannelGroups() + ->Get(path.IsRadio()) + ->GetByName(strGroupName); + } + + if (group) + { + const bool playedOnly = + (m_url.HasOption("view") && (m_url.GetOption("view") == "lastplayed")); + + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers = + group->GetMembers(); + for (const auto& groupMember : groupMembers) + { + if (bShowHiddenChannels != groupMember->Channel()->IsHidden()) + continue; + + if (playedOnly && !groupMember->Channel()->LastWatched()) + continue; + + results.Add(std::make_shared<CFileItem>(groupMember)); + } + } + else + { + CLog::LogF(LOGERROR, "Unable to obtain members of channel group '{}'", strGroupName); + return false; + } + + return true; + } + } + return false; +} + +namespace +{ + +bool GetTimersRootDirectory(const CPVRTimersPath& path, + bool bHideDisabled, + const std::vector<std::shared_ptr<CPVRTimerInfoTag>>& timers, + CFileItemList& results) +{ + bool bRadio = path.IsRadio(); + bool bRules = path.IsRules(); + + for (const auto& timer : timers) + { + if ((bRadio == timer->IsRadio() || + (bRules && timer->ClientChannelUID() == PVR_TIMER_ANY_CHANNEL)) && + (bRules == timer->IsTimerRule()) && (!bHideDisabled || !timer->IsDisabled())) + { + const auto item = std::make_shared<CFileItem>(timer); + const CPVRTimersPath timersPath(path.GetPath(), timer->ClientID(), timer->ClientIndex()); + item->SetPath(timersPath.GetPath()); + results.Add(item); + } + } + return true; +} + +bool GetTimersSubDirectory(const CPVRTimersPath& path, + bool bHideDisabled, + const std::vector<std::shared_ptr<CPVRTimerInfoTag>>& timers, + CFileItemList& results) +{ + bool bRadio = path.IsRadio(); + int iParentId = path.GetParentId(); + int iClientId = path.GetClientId(); + + std::shared_ptr<CFileItem> item; + + for (const auto& timer : timers) + { + if ((timer->IsRadio() == bRadio) && timer->HasParent() && (timer->ClientID() == iClientId) && + (timer->ParentClientIndex() == iParentId) && (!bHideDisabled || !timer->IsDisabled())) + { + item.reset(new CFileItem(timer)); + const CPVRTimersPath timersPath(path.GetPath(), timer->ClientID(), timer->ClientIndex()); + item->SetPath(timersPath.GetPath()); + results.Add(item); + } + } + return true; +} + +} // unnamed namespace + +bool CPVRGUIDirectory::GetTimersDirectory(CFileItemList& results) const +{ + const CPVRTimersPath path(m_url.GetWithoutOptions()); + if (path.IsValid() && (path.IsTimersRoot() || path.IsTimerRule())) + { + bool bHideDisabled = false; + if (m_url.HasOption("view")) + { + const std::string view = m_url.GetOption("view"); + if (view == "hidedisabled") + { + bHideDisabled = true; + } + else + { + CLog::LogF(LOGERROR, "Unsupported value '{}' for url parameter 'view'", view); + return false; + } + } + else + { + bHideDisabled = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_PVRTIMERS_HIDEDISABLEDTIMERS); + } + + const std::vector<std::shared_ptr<CPVRTimerInfoTag>> timers = + CServiceBroker::GetPVRManager().Timers()->GetAll(); + + if (path.IsTimersRoot()) + { + /* Root folder containing either timer rules or timers. */ + return GetTimersRootDirectory(path, bHideDisabled, timers, results); + } + else if (path.IsTimerRule()) + { + /* Sub folder containing the timers scheduled by the given timer rule. */ + return GetTimersSubDirectory(path, bHideDisabled, timers, results); + } + } + + return false; +} diff --git a/xbmc/pvr/filesystem/PVRGUIDirectory.h b/xbmc/pvr/filesystem/PVRGUIDirectory.h new file mode 100644 index 0000000..462c553 --- /dev/null +++ b/xbmc/pvr/filesystem/PVRGUIDirectory.h @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2012-2019 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 "URL.h" + +#include <string> + +class CFileItemList; + +namespace PVR +{ + +class CPVRGUIDirectory +{ +public: + /*! + * @brief PVR GUI directory ctor. + * @param url The directory's URL. + */ + explicit CPVRGUIDirectory(const CURL& url) : m_url(url) {} + + /*! + * @brief PVR GUI directory ctor. + * @param path The directory's path. + */ + explicit CPVRGUIDirectory(const std::string& path) : m_url(path) {} + + /*! + * @brief PVR GUI directory dtor. + */ + virtual ~CPVRGUIDirectory() = default; + + /*! + * @brief Check existence of this directory. + * @return True if the directory exists, false otherwise. + */ + bool Exists() const; + + /*! + * @brief Obtain the directory listing. + * @param results The list to fill with the results. + * @return True on success, false otherwise. + */ + bool GetDirectory(CFileItemList& results) const; + + /*! + * @brief Check if this directory supports file write operations. + * @return True if the directory supports file write operations, false otherwise. + */ + bool SupportsWriteFileOperations() const; + + /*! + * @brief Check if any TV recordings are existing. + * @return True if TV recordings exists, false otherwise. + */ + static bool HasTVRecordings(); + + /*! + * @brief Check if any deleted TV recordings are existing. + * @return True if deleted TV recordings exists, false otherwise. + */ + static bool HasDeletedTVRecordings(); + + /*! + * @brief Check if any radio recordings are existing. + * @return True if radio recordings exists, false otherwise. + */ + static bool HasRadioRecordings(); + + /*! + * @brief Check if any deleted radio recordings are existing. + * @return True if deleted radio recordings exists, false otherwise. + */ + static bool HasDeletedRadioRecordings(); + + /*! + * @brief Get the list of channel groups. + * @param bRadio If true, obtain radio groups, tv groups otherwise. + * @param bExcludeHidden If true exclude hidden groups, include hidden groups otherwise. + * @param results The file list to store the results in. + * @return True on success, false otherwise.. + */ + static bool GetChannelGroupsDirectory(bool bRadio, bool bExcludeHidden, CFileItemList& results); + + /*! + * @brief Get the list of channels. + * @param results The file list to store the results in. + * @return True on success, false otherwise.. + */ + bool GetChannelsDirectory(CFileItemList& results) const; + +private: + bool GetTimersDirectory(CFileItemList& results) const; + bool GetRecordingsDirectory(CFileItemList& results) const; + bool GetSavedSearchesDirectory(bool bRadio, CFileItemList& results) const; + + const CURL m_url; +}; + +} // namespace PVR diff --git a/xbmc/pvr/guilib/CMakeLists.txt b/xbmc/pvr/guilib/CMakeLists.txt new file mode 100644 index 0000000..3a73a68 --- /dev/null +++ b/xbmc/pvr/guilib/CMakeLists.txt @@ -0,0 +1,35 @@ +set(SOURCES GUIEPGGridContainer.cpp + GUIEPGGridContainerModel.cpp + PVRGUIActionListener.cpp + PVRGUIActionsChannels.cpp + PVRGUIActionsClients.cpp + PVRGUIActionsDatabase.cpp + PVRGUIActionsEPG.cpp + PVRGUIActionsUtils.cpp + PVRGUIActionsParentalControl.cpp + PVRGUIActionsPlayback.cpp + PVRGUIActionsPowerManagement.cpp + PVRGUIActionsRecordings.cpp + PVRGUIActionsTimers.cpp + PVRGUIChannelIconUpdater.cpp + PVRGUIChannelNavigator.cpp + PVRGUIProgressHandler.cpp) + +set(HEADERS GUIEPGGridContainer.h + GUIEPGGridContainerModel.h + PVRGUIActionListener.h + PVRGUIActionsChannels.h + PVRGUIActionsClients.h + PVRGUIActionsDatabase.h + PVRGUIActionsEPG.h + PVRGUIActionsUtils.h + PVRGUIActionsParentalControl.h + PVRGUIActionsPlayback.h + PVRGUIActionsPowerManagement.h + PVRGUIActionsRecordings.h + PVRGUIActionsTimers.h + PVRGUIChannelIconUpdater.h + PVRGUIChannelNavigator.h + PVRGUIProgressHandler.h) + +core_add_library(pvr_guilib) diff --git a/xbmc/pvr/guilib/GUIEPGGridContainer.cpp b/xbmc/pvr/guilib/GUIEPGGridContainer.cpp new file mode 100644 index 0000000..d88dc86 --- /dev/null +++ b/xbmc/pvr/guilib/GUIEPGGridContainer.cpp @@ -0,0 +1,2549 @@ +/* + * 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 "GUIEPGGridContainer.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "guilib/DirtyRegion.h" +#include "guilib/GUIAction.h" +#include "guilib/GUIMessage.h" +#include "guilib/guiinfo/GUIInfoLabels.h" +#include "input/Key.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "pvr/PVRManager.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/guilib/GUIEPGGridContainerModel.h" +#include "utils/MathUtils.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +#include <algorithm> +#include <memory> +#include <mutex> +#include <string> +#include <utility> + +#include <tinyxml.h> + +using namespace PVR; + +#define BLOCKJUMP 4 // how many blocks are jumped with each analogue scroll action +static const int BLOCK_SCROLL_OFFSET = 60 / CGUIEPGGridContainerModel::MINSPERBLOCK; // how many blocks are jumped if we are at left/right edge of grid + +CGUIEPGGridContainer::CGUIEPGGridContainer(int parentID, + int controlID, + float posX, + float posY, + float width, + float height, + ORIENTATION orientation, + int scrollTime, + int preloadItems, + int timeBlocks, + int rulerUnit, + const CTextureInfo& progressIndicatorTexture) + : IGUIContainer(parentID, controlID, posX, posY, width, height), + m_orientation(orientation), + m_channelLayout(nullptr), + m_focusedChannelLayout(nullptr), + m_programmeLayout(nullptr), + m_focusedProgrammeLayout(nullptr), + m_rulerLayout(nullptr), + m_rulerDateLayout(nullptr), + m_pageControl(0), + m_rulerUnit(rulerUnit), + m_channelsPerPage(0), + m_programmesPerPage(0), + m_channelCursor(0), + m_channelOffset(0), + m_blocksPerPage(timeBlocks), + m_blockCursor(0), + m_blockOffset(0), + m_blockTravelAxis(0), + m_cacheChannelItems(preloadItems), + m_cacheProgrammeItems(preloadItems), + m_cacheRulerItems(preloadItems), + m_rulerDateHeight(0), + m_rulerDateWidth(0), + m_rulerPosX(0), + m_rulerPosY(0), + m_rulerHeight(0), + m_rulerWidth(0), + m_channelPosX(0), + m_channelPosY(0), + m_channelHeight(0), + m_channelWidth(0), + m_gridPosX(0), + m_gridPosY(0), + m_gridWidth(0), + m_gridHeight(0), + m_blockSize(0), + m_analogScrollCount(0), + m_guiProgressIndicatorTexture( + CGUITexture::CreateTexture(posX, posY, width, height, progressIndicatorTexture)), + m_scrollTime(scrollTime ? scrollTime : 1), + m_programmeScrollLastTime(0), + m_programmeScrollSpeed(0), + m_programmeScrollOffset(0), + m_channelScrollLastTime(0), + m_channelScrollSpeed(0), + m_channelScrollOffset(0), + m_gridModel(new CGUIEPGGridContainerModel) +{ + ControlType = GUICONTAINER_EPGGRID; +} + +CGUIEPGGridContainer::CGUIEPGGridContainer(const CGUIEPGGridContainer& other) + : IGUIContainer(other), + m_renderOffset(other.m_renderOffset), + m_orientation(other.m_orientation), + m_channelLayouts(other.m_channelLayouts), + m_focusedChannelLayouts(other.m_focusedChannelLayouts), + m_focusedProgrammeLayouts(other.m_focusedProgrammeLayouts), + m_programmeLayouts(other.m_programmeLayouts), + m_rulerLayouts(other.m_rulerLayouts), + m_rulerDateLayouts(other.m_rulerDateLayouts), + m_channelLayout(other.m_channelLayout), + m_focusedChannelLayout(other.m_focusedChannelLayout), + m_programmeLayout(other.m_programmeLayout), + m_focusedProgrammeLayout(other.m_focusedProgrammeLayout), + m_rulerLayout(other.m_rulerLayout), + m_rulerDateLayout(other.m_rulerDateLayout), + m_pageControl(other.m_pageControl), + m_rulerUnit(other.m_rulerUnit), + m_channelsPerPage(other.m_channelsPerPage), + m_programmesPerPage(other.m_programmesPerPage), + m_channelCursor(other.m_channelCursor), + m_channelOffset(other.m_channelOffset), + m_blocksPerPage(other.m_blocksPerPage), + m_blockCursor(other.m_blockCursor), + m_blockOffset(other.m_blockOffset), + m_blockTravelAxis(other.m_blockTravelAxis), + m_cacheChannelItems(other.m_cacheChannelItems), + m_cacheProgrammeItems(other.m_cacheProgrammeItems), + m_cacheRulerItems(other.m_cacheRulerItems), + m_rulerDateHeight(other.m_rulerDateHeight), + m_rulerDateWidth(other.m_rulerDateWidth), + m_rulerPosX(other.m_rulerPosX), + m_rulerPosY(other.m_rulerPosY), + m_rulerHeight(other.m_rulerHeight), + m_rulerWidth(other.m_rulerWidth), + m_channelPosX(other.m_channelPosX), + m_channelPosY(other.m_channelPosY), + m_channelHeight(other.m_channelHeight), + m_channelWidth(other.m_channelWidth), + m_gridPosX(other.m_gridPosX), + m_gridPosY(other.m_gridPosY), + m_gridWidth(other.m_gridWidth), + m_gridHeight(other.m_gridHeight), + m_blockSize(other.m_blockSize), + m_analogScrollCount(other.m_analogScrollCount), + m_guiProgressIndicatorTexture(other.m_guiProgressIndicatorTexture->Clone()), + m_lastItem(other.m_lastItem), + m_lastChannel(other.m_lastChannel), + m_scrollTime(other.m_scrollTime), + m_programmeScrollLastTime(other.m_programmeScrollLastTime), + m_programmeScrollSpeed(other.m_programmeScrollSpeed), + m_programmeScrollOffset(other.m_programmeScrollOffset), + m_channelScrollLastTime(other.m_channelScrollLastTime), + m_channelScrollSpeed(other.m_channelScrollSpeed), + m_channelScrollOffset(other.m_channelScrollOffset), + m_gridModel(new CGUIEPGGridContainerModel(*other.m_gridModel)), + m_updatedGridModel(other.m_updatedGridModel + ? new CGUIEPGGridContainerModel(*other.m_updatedGridModel) + : nullptr), + m_itemStartBlock(other.m_itemStartBlock) +{ +} + +bool CGUIEPGGridContainer::HasData() const +{ + return m_gridModel && m_gridModel->HasChannelItems(); +} + +void CGUIEPGGridContainer::AllocResources() +{ + IGUIContainer::AllocResources(); + m_guiProgressIndicatorTexture->AllocResources(); +} + +void CGUIEPGGridContainer::FreeResources(bool immediately) +{ + m_guiProgressIndicatorTexture->FreeResources(immediately); + IGUIContainer::FreeResources(immediately); +} + +void CGUIEPGGridContainer::SetPageControl(int id) +{ + m_pageControl = id; +} + +void CGUIEPGGridContainer::Process(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + ValidateOffset(); + + if (m_bInvalidated) + { + UpdateLayout(); + + if (m_pageControl) + { + int iItemsPerPage; + int iTotalItems; + + if (m_orientation == VERTICAL) + { + iItemsPerPage = m_channelsPerPage; + iTotalItems = m_gridModel->ChannelItemsSize(); + } + else + { + iItemsPerPage = m_blocksPerPage; + iTotalItems = m_gridModel->GridItemsSize(); + } + + CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), m_pageControl, iItemsPerPage, iTotalItems); + SendWindowMessage(msg); + } + } + + UpdateScrollOffset(currentTime); + ProcessChannels(currentTime, dirtyregions); + ProcessRulerDate(currentTime, dirtyregions); + ProcessRuler(currentTime, dirtyregions); + ProcessProgrammeGrid(currentTime, dirtyregions); + ProcessProgressIndicator(currentTime, dirtyregions); + + if (m_pageControl) + { + int iItem = + (m_orientation == VERTICAL) + ? MathUtils::round_int(static_cast<double>(m_channelScrollOffset / m_channelHeight)) + : MathUtils::round_int( + static_cast<double>(m_programmeScrollOffset / (m_gridHeight / m_blocksPerPage))); + + CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), m_pageControl, iItem); + SendWindowMessage(msg); + } + + CGUIControl::Process(currentTime, dirtyregions); +} + +void CGUIEPGGridContainer::Render() +{ + RenderChannels(); + RenderRulerDate(); + RenderRuler(); + RenderProgrammeGrid(); + RenderProgressIndicator(); + + CGUIControl::Render(); +} + +void CGUIEPGGridContainer::ProcessChannels(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + HandleChannels(false, currentTime, dirtyregions); +} + +void CGUIEPGGridContainer::RenderChannels() +{ + // params not needed for render. + unsigned int dummyTime = 0; + CDirtyRegionList dummyRegions; + HandleChannels(true, dummyTime, dummyRegions); +} + +void CGUIEPGGridContainer::ProcessRulerDate(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + HandleRulerDate(false, currentTime, dirtyregions); +} + +void CGUIEPGGridContainer::RenderRulerDate() +{ + // params not needed for render. + unsigned int dummyTime = 0; + CDirtyRegionList dummyRegions; + HandleRulerDate(true, dummyTime, dummyRegions); +} + +void CGUIEPGGridContainer::ProcessRuler(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + HandleRuler(false, currentTime, dirtyregions); +} + +void CGUIEPGGridContainer::RenderRuler() +{ + // params not needed for render. + unsigned int dummyTime = 0; + CDirtyRegionList dummyRegions; + HandleRuler(true, dummyTime, dummyRegions); +} + +void CGUIEPGGridContainer::ProcessProgrammeGrid(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + HandleProgrammeGrid(false, currentTime, dirtyregions); +} + +void CGUIEPGGridContainer::RenderProgrammeGrid() +{ + // params not needed for render. + unsigned int dummyTime = 0; + CDirtyRegionList dummyRegions; + HandleProgrammeGrid(true, dummyTime, dummyRegions); +} + +float CGUIEPGGridContainer::GetCurrentTimePositionOnPage() const +{ + if (!m_gridModel->GetGridStart().IsValid()) + return -1.0f; + + const CDateTimeSpan startDelta(CDateTime::GetUTCDateTime() - m_gridModel->GetGridStart()); + const float fPos = (startDelta.GetSecondsTotal() * m_blockSize) / + (CGUIEPGGridContainerModel::MINSPERBLOCK * 60) - + GetProgrammeScrollOffsetPos(); + return std::min(fPos, m_orientation == VERTICAL ? m_gridWidth : m_gridHeight); +} + +float CGUIEPGGridContainer::GetProgressIndicatorWidth() const +{ + return (m_orientation == VERTICAL) ? GetCurrentTimePositionOnPage() : m_rulerWidth + m_gridWidth; +} + +float CGUIEPGGridContainer::GetProgressIndicatorHeight() const +{ + return (m_orientation == VERTICAL) ? m_rulerHeight + m_gridHeight : GetCurrentTimePositionOnPage(); +} + +void CGUIEPGGridContainer::ProcessProgressIndicator(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + float width = GetProgressIndicatorWidth(); + float height = GetProgressIndicatorHeight(); + + if (width > 0 && height > 0) + { + m_guiProgressIndicatorTexture->SetVisible(true); + m_guiProgressIndicatorTexture->SetPosition(m_rulerPosX + m_renderOffset.x, + m_rulerPosY + m_renderOffset.y); + m_guiProgressIndicatorTexture->SetWidth(width); + m_guiProgressIndicatorTexture->SetHeight(height); + } + else + { + m_guiProgressIndicatorTexture->SetVisible(false); + } + + m_guiProgressIndicatorTexture->Process(currentTime); +} + +void CGUIEPGGridContainer::RenderProgressIndicator() +{ + if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_rulerPosX, m_rulerPosY, GetProgressIndicatorWidth(), GetProgressIndicatorHeight())) + { + m_guiProgressIndicatorTexture->SetDiffuseColor(m_diffuseColor); + m_guiProgressIndicatorTexture->Render(); + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + } +} + +void CGUIEPGGridContainer::ProcessItem(float posX, float posY, const CFileItemPtr& item, CFileItemPtr& lastitem, + bool focused, CGUIListItemLayout* normallayout, CGUIListItemLayout* focusedlayout, + unsigned int currentTime, CDirtyRegionList& dirtyregions, float resize /* = -1.0f */) +{ + if (!normallayout || !focusedlayout) + return; + + // set the origin + CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(posX, posY); + + if (m_bInvalidated) + item->SetInvalid(); + + if (focused) + { + if (!item->GetFocusedLayout()) + { + item->SetFocusedLayout(std::make_unique<CGUIListItemLayout>(*focusedlayout, this)); + } + + if (resize != -1.0f) + { + if (m_orientation == VERTICAL) + item->GetFocusedLayout()->SetWidth(resize); + else + item->GetFocusedLayout()->SetHeight(resize); + } + + if (item != lastitem || !HasFocus()) + item->GetFocusedLayout()->SetFocusedItem(0); + + if (item != lastitem && HasFocus()) + { + item->GetFocusedLayout()->ResetAnimation(ANIM_TYPE_UNFOCUS); + + unsigned int subItem = 1; + if (lastitem && lastitem->GetFocusedLayout()) + subItem = lastitem->GetFocusedLayout()->GetFocusedItem(); + + item->GetFocusedLayout()->SetFocusedItem(subItem ? subItem : 1); + } + + item->GetFocusedLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions); + lastitem = item; + } + else + { + if (!item->GetLayout()) + { + item->SetLayout(std::make_unique<CGUIListItemLayout>(*normallayout, this)); + } + + if (resize != -1.0f) + { + if (m_orientation == VERTICAL) + item->GetLayout()->SetWidth(resize); + else + item->GetLayout()->SetHeight(resize); + } + + if (item->GetFocusedLayout()) + item->GetFocusedLayout()->SetFocusedItem(0); + + if (item->GetFocusedLayout() && item->GetFocusedLayout()->IsAnimating(ANIM_TYPE_UNFOCUS)) + item->GetFocusedLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions); + else + item->GetLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions); + } + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin(); +} + +void CGUIEPGGridContainer::RenderItem(float posX, float posY, CGUIListItem* item, bool focused) +{ + // set the origin + CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(posX, posY); + + if (focused) + { + if (item->GetFocusedLayout()) + item->GetFocusedLayout()->Render(item, m_parentID); + } + else + { + if (item->GetFocusedLayout() && item->GetFocusedLayout()->IsAnimating(ANIM_TYPE_UNFOCUS)) + item->GetFocusedLayout()->Render(item, m_parentID); + else if (item->GetLayout()) + item->GetLayout()->Render(item, m_parentID); + } + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin(); +} + +bool CGUIEPGGridContainer::OnAction(const CAction& action) +{ + switch (action.GetID()) + { + case ACTION_MOVE_LEFT: + case ACTION_MOVE_RIGHT: + case ACTION_MOVE_DOWN: + case ACTION_MOVE_UP: + case ACTION_NAV_BACK: + // use base class implementation + return CGUIControl::OnAction(action); + + case ACTION_NEXT_ITEM: + // skip +12h + ScrollToBlockOffset(m_blockOffset + (12 * 60 / CGUIEPGGridContainerModel::MINSPERBLOCK)); + SetBlock(m_blockCursor); + return true; + + case ACTION_PREV_ITEM: + // skip -12h + ScrollToBlockOffset(m_blockOffset - (12 * 60 / CGUIEPGGridContainerModel::MINSPERBLOCK)); + SetBlock(m_blockCursor); + return true; + + case REMOTE_0: + GoToNow(); + return true; + + case ACTION_PAGE_UP: + if (m_orientation == VERTICAL) + { + if (m_channelOffset == 0) + { + // already on the first page, so move to the first item + SetChannel(0); + } + else + { + // scroll up to the previous page + ChannelScroll(-m_channelsPerPage); + } + } + else + { + if (m_blockOffset == 0) + { + // already on the first page, so move to the first item + SetBlock(0); + } + else + { + // scroll up to the previous page + ProgrammesScroll(-m_blocksPerPage); + } + } + return true; + + case ACTION_PAGE_DOWN: + if (m_orientation == VERTICAL) + { + if (m_channelOffset == m_gridModel->ChannelItemsSize() - m_channelsPerPage || + m_gridModel->ChannelItemsSize() < m_channelsPerPage) + { + // already at the last page, so move to the last item. + SetChannel(m_gridModel->GetLastChannel() - m_channelOffset); + } + else + { + // scroll down to the next page + ChannelScroll(m_channelsPerPage); + } + } + else + { + if (m_blockOffset == m_gridModel->GridItemsSize() - m_blocksPerPage || + m_gridModel->GridItemsSize() < m_blocksPerPage) + { + // already at the last page, so move to the last item. + SetBlock(m_gridModel->GetLastBlock() - m_blockOffset); + } + else + { + // scroll down to the next page + ProgrammesScroll(m_blocksPerPage); + } + } + + return true; + + // smooth scrolling (for analog controls) + case ACTION_TELETEXT_RED: + case ACTION_TELETEXT_GREEN: + case ACTION_SCROLL_UP: // left horizontal scrolling + if (m_orientation == VERTICAL) + { + int blocksToJump = action.GetID() == ACTION_TELETEXT_RED ? m_blocksPerPage / 2 : m_blocksPerPage / 4; + + m_analogScrollCount += action.GetAmount() * action.GetAmount(); + bool handled = false; + + while (m_analogScrollCount > 0.4f) + { + handled = true; + m_analogScrollCount -= 0.4f; + + if (m_blockOffset > 0 && m_blockCursor <= m_blocksPerPage / 2) + ProgrammesScroll(-blocksToJump); + else if (m_blockCursor > blocksToJump) + SetBlock(m_blockCursor - blocksToJump); + } + return handled; + } + else + { + int channelsToJump = action.GetID() == ACTION_TELETEXT_RED ? m_channelsPerPage / 2 : m_channelsPerPage / 4; + + m_analogScrollCount += action.GetAmount() * action.GetAmount(); + bool handled = false; + + while (m_analogScrollCount > 0.4f) + { + handled = true; + m_analogScrollCount -= 0.4f; + + if (m_channelOffset > 0 && m_channelCursor <= m_channelsPerPage / 2) + ChannelScroll(-channelsToJump); + else if (m_channelCursor > channelsToJump) + SetChannel(m_channelCursor - channelsToJump); + } + return handled; + } + break; + + case ACTION_TELETEXT_BLUE: + case ACTION_TELETEXT_YELLOW: + case ACTION_SCROLL_DOWN: // right horizontal scrolling + if (m_orientation == VERTICAL) + { + int blocksToJump = action.GetID() == ACTION_TELETEXT_BLUE ? m_blocksPerPage / 2 : m_blocksPerPage / 4; + + m_analogScrollCount += action.GetAmount() * action.GetAmount(); + bool handled = false; + + while (m_analogScrollCount > 0.4f) + { + handled = true; + m_analogScrollCount -= 0.4f; + + if (m_blockOffset + m_blocksPerPage < m_gridModel->GridItemsSize() && + m_blockCursor >= m_blocksPerPage / 2) + ProgrammesScroll(blocksToJump); + else if (m_blockCursor < m_blocksPerPage - blocksToJump && + m_blockOffset + m_blockCursor < m_gridModel->GridItemsSize() - blocksToJump) + SetBlock(m_blockCursor + blocksToJump); + } + return handled; + } + else + { + int channelsToJump = action.GetID() == ACTION_TELETEXT_BLUE ? m_channelsPerPage / 2 : m_channelsPerPage / 4; + + m_analogScrollCount += action.GetAmount() * action.GetAmount(); + bool handled = false; + + while (m_analogScrollCount > 0.4f) + { + handled = true; + m_analogScrollCount -= 0.4f; + + if (m_channelOffset + m_channelsPerPage < m_gridModel->ChannelItemsSize() && m_channelCursor >= m_channelsPerPage / 2) + ChannelScroll(channelsToJump); + else if (m_channelCursor < m_channelsPerPage - channelsToJump && m_channelOffset + m_channelCursor < m_gridModel->ChannelItemsSize() - channelsToJump) + SetChannel(m_channelCursor + channelsToJump); + } + return handled; + } + break; + + default: + if (action.GetID()) + return OnClick(action.GetID()); + + break; + } + + return false; +} + +bool CGUIEPGGridContainer::OnMessage(CGUIMessage& message) +{ + if (message.GetControlId() == GetID()) + { + switch (message.GetMessage()) + { + case GUI_MSG_PAGE_CHANGE: + if (message.GetSenderId() == m_pageControl && IsVisible()) + { + if (m_orientation == VERTICAL) + { + ScrollToChannelOffset(message.GetParam1()); + SetChannel(m_channelCursor); + } + else + { + ScrollToBlockOffset(message.GetParam1()); + SetBlock(m_blockCursor); + } + return true; + } + break; + + case GUI_MSG_LABEL_BIND: + UpdateItems(); + return true; + + case GUI_MSG_REFRESH_LIST: + // update our list contents + m_gridModel->SetInvalid(); + break; + } + } + + return CGUIControl::OnMessage(message); +} + +void CGUIEPGGridContainer::UpdateItems() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (!m_updatedGridModel) + return; + + // Save currently selected epg tag and grid coordinates. Selection shall be restored after update. + std::shared_ptr<CPVREpgInfoTag> prevSelectedEpgTag; + if (HasData()) + prevSelectedEpgTag = + m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset) + ->GetEPGInfoTag(); + + const int oldChannelIndex = m_channelOffset + m_channelCursor; + const int oldBlockIndex = m_blockOffset + m_blockCursor; + const CDateTime oldGridStart(m_gridModel->GetGridStart()); + int eventOffset = oldBlockIndex; + int newChannelIndex = oldChannelIndex; + int newBlockIndex = oldBlockIndex; + int channelUid = -1; + unsigned int broadcastUid = 0; + + if (prevSelectedEpgTag) + { + // get the block offset relative to the first block of the selected event + eventOffset = + oldBlockIndex - m_gridModel->GetGridItemStartBlock(oldChannelIndex, oldBlockIndex); + + if (!prevSelectedEpgTag->IsGapTag()) // "normal" tag selected + { + if (oldGridStart >= prevSelectedEpgTag->StartAsUTC()) + { + // start of previously selected event is before grid start + newBlockIndex = eventOffset; + } + else + { + newBlockIndex = m_gridModel->GetFirstEventBlock(prevSelectedEpgTag) + eventOffset; + } + + channelUid = prevSelectedEpgTag->UniqueChannelID(); + broadcastUid = prevSelectedEpgTag->UniqueBroadcastID(); + } + else // "gap" tag selected + { + channelUid = prevSelectedEpgTag->UniqueChannelID(); + + // As gap tags do not have a unique broadcast id, we will look for the real tag preceding + // the gap tag and add the respective offset to restore the gap tag selection, assuming that + // the real tag is still the predecessor of the gap tag after the grid model update. + + const std::shared_ptr<CFileItem> prevItem = GetPrevItem().first; + if (prevItem) + { + const std::shared_ptr<CPVREpgInfoTag> tag = prevItem->GetEPGInfoTag(); + if (tag && !tag->IsGapTag()) + { + if (oldGridStart >= tag->StartAsUTC()) + { + // start of previously selected event is before grid start + newBlockIndex = eventOffset; + } + else + { + newBlockIndex = m_gridModel->GetFirstEventBlock(tag); + eventOffset += m_gridModel->GetFirstEventBlock(prevSelectedEpgTag) - newBlockIndex; + } + + broadcastUid = tag->UniqueBroadcastID(); + } + } + } + } + + m_lastItem = nullptr; + m_lastChannel = nullptr; + + // always use asynchronously precalculated grid data. + m_gridModel = std::move(m_updatedGridModel); + + if (prevSelectedEpgTag) + { + if (oldGridStart != m_gridModel->GetGridStart()) + { + // grid start changed. block offset for selected event might have changed. + newBlockIndex += m_gridModel->GetBlock(oldGridStart); + if (newBlockIndex < 0 || newBlockIndex > m_gridModel->GetLastBlock()) + { + // previous selection is no longer in grid. + SetInvalid(); + m_bEnableChannelScrolling = false; + GoToChannel(newChannelIndex); + m_bEnableProgrammeScrolling = false; + GoToNow(); + return; + } + } + + if (newChannelIndex >= m_gridModel->ChannelItemsSize() || + newBlockIndex >= m_gridModel->GridItemsSize() || + m_gridModel->GetGridItem(newChannelIndex, newBlockIndex)->GetEPGInfoTag() != + prevSelectedEpgTag) + { + int iChannelIndex = CGUIEPGGridContainerModel::INVALID_INDEX; + int iBlockIndex = CGUIEPGGridContainerModel::INVALID_INDEX; + m_gridModel->FindChannelAndBlockIndex(channelUid, broadcastUid, eventOffset, iChannelIndex, iBlockIndex); + + if (iBlockIndex != CGUIEPGGridContainerModel::INVALID_INDEX) + { + newBlockIndex = iBlockIndex; + } + else if (newBlockIndex > m_gridModel->GetLastBlock()) + { + // default to now + newBlockIndex = m_gridModel->GetNowBlock(); + + if (newBlockIndex > m_gridModel->GetLastBlock()) + { + // last block is in the past. default to last block + newBlockIndex = m_gridModel->GetLastBlock(); + } + } + + if (iChannelIndex != CGUIEPGGridContainerModel::INVALID_INDEX) + { + newChannelIndex = iChannelIndex; + } + else if (newChannelIndex >= m_gridModel->ChannelItemsSize() || + (m_gridModel->GetGridItem(newChannelIndex, newBlockIndex)->GetEPGInfoTag()->UniqueChannelID() != prevSelectedEpgTag->UniqueChannelID() && + m_gridModel->GetGridItem(newChannelIndex, newBlockIndex)->GetEPGInfoTag()->ClientID() != prevSelectedEpgTag->ClientID())) + { + // default to first channel + newChannelIndex = 0; + } + } + + // restore previous selection. + if (newChannelIndex == oldChannelIndex && newBlockIndex == oldBlockIndex) + { + // same coordinates, keep current grid view port + UpdateItem(); + } + else + { + // new coordinates, move grid view port accordingly + SetInvalid(); + + if (newBlockIndex != oldBlockIndex) + { + m_bEnableProgrammeScrolling = false; + GoToBlock(newBlockIndex); + } + + if (newChannelIndex != oldChannelIndex) + { + m_bEnableChannelScrolling = false; + GoToChannel(newChannelIndex); + } + } + } + else + { + // no previous selection, goto now + SetInvalid(); + m_bEnableProgrammeScrolling = false; + GoToNow(); + } +} + +float CGUIEPGGridContainer::GetChannelScrollOffsetPos() const +{ + if (m_bEnableChannelScrolling) + return m_channelScrollOffset; + else + return m_channelOffset * m_channelLayout->Size(m_orientation); +} + +float CGUIEPGGridContainer::GetProgrammeScrollOffsetPos() const +{ + if (m_bEnableProgrammeScrolling) + return m_programmeScrollOffset; + else + return m_blockOffset * m_blockSize; +} + +int CGUIEPGGridContainer::GetChannelScrollOffset(CGUIListItemLayout* layout) const +{ + if (m_bEnableChannelScrolling) + return MathUtils::round_int( + static_cast<double>(m_channelScrollOffset / layout->Size(m_orientation))); + else + return m_channelOffset; +} + +int CGUIEPGGridContainer::GetProgrammeScrollOffset() const +{ + if (m_bEnableProgrammeScrolling) + return MathUtils::round_int(static_cast<double>(m_programmeScrollOffset / m_blockSize)); + else + return m_blockOffset; +} + +void CGUIEPGGridContainer::ChannelScroll(int amount) +{ + // increase or decrease the vertical offset + int offset = m_channelOffset + amount; + + if (offset > m_gridModel->ChannelItemsSize() - m_channelsPerPage) + offset = m_gridModel->ChannelItemsSize() - m_channelsPerPage; + + if (offset < 0) + offset = 0; + + ScrollToChannelOffset(offset); + SetChannel(m_channelCursor); +} + +void CGUIEPGGridContainer::ProgrammesScroll(int amount) +{ + // increase or decrease the horizontal offset + ScrollToBlockOffset(m_blockOffset + amount); + SetBlock(m_blockCursor); +} + +void CGUIEPGGridContainer::OnUp() +{ + if (!HasData()) + return CGUIControl::OnUp(); + + if (m_orientation == VERTICAL) + { + CGUIAction action = GetAction(ACTION_MOVE_UP); + if (m_channelCursor > 0) + { + SetChannel(m_channelCursor - 1); + } + else if (m_channelCursor == 0 && m_channelOffset) + { + ScrollToChannelOffset(m_channelOffset - 1); + SetChannel(0); + } + else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around + { + int offset = m_gridModel->ChannelItemsSize() - m_channelsPerPage; + + if (offset < 0) + offset = 0; + + SetChannel(m_gridModel->GetLastChannel() - offset); + ScrollToChannelOffset(offset); + } + else + CGUIControl::OnUp(); + } + else + { + if (m_gridModel->GetGridItemStartBlock(m_channelCursor + m_channelOffset, + m_blockCursor + m_blockOffset) > m_blockOffset) + { + // this is not first item on page + SetItem(GetPrevItem()); + UpdateBlock(); + return; + } + else if (m_blockCursor <= 0 && m_blockOffset && m_blockOffset - BLOCK_SCROLL_OFFSET >= 0) + { + // this is the first item on page + ScrollToBlockOffset(m_blockOffset - BLOCK_SCROLL_OFFSET); + UpdateBlock(); + return; + } + + CGUIControl::OnUp(); + } +} + +void CGUIEPGGridContainer::OnDown() +{ + if (!HasData()) + return CGUIControl::OnDown(); + + if (m_orientation == VERTICAL) + { + CGUIAction action = GetAction(ACTION_MOVE_DOWN); + if (m_channelOffset + m_channelCursor < m_gridModel->GetLastChannel()) + { + if (m_channelCursor + 1 < m_channelsPerPage) + { + SetChannel(m_channelCursor + 1); + } + else + { + ScrollToChannelOffset(m_channelOffset + 1); + SetChannel(m_channelsPerPage - 1); + } + } + else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around + { + ScrollToChannelOffset(0); + SetChannel(0); + } + else + CGUIControl::OnDown(); + } + else + { + if (m_gridModel->GetGridItemEndBlock(m_channelCursor + m_channelOffset, + m_blockCursor + m_blockOffset) < + (m_blockOffset + m_blocksPerPage - 1)) + { + // this is not last item on page + SetItem(GetNextItem()); + UpdateBlock(); + return; + } + else if ((m_blockOffset != m_gridModel->GridItemsSize() - m_blocksPerPage) && + m_gridModel->GridItemsSize() > m_blocksPerPage && + m_blockOffset + BLOCK_SCROLL_OFFSET < m_gridModel->GetLastBlock()) + { + // this is the last item on page + ScrollToBlockOffset(m_blockOffset + BLOCK_SCROLL_OFFSET); + UpdateBlock(); + return; + } + + CGUIControl::OnDown(); + } +} + +void CGUIEPGGridContainer::OnLeft() +{ + if (!HasData()) + return CGUIControl::OnLeft(); + + if (m_orientation == VERTICAL) + { + if (m_gridModel->GetGridItemStartBlock(m_channelCursor + m_channelOffset, + m_blockCursor + m_blockOffset) > m_blockOffset) + { + // this is not first item on page + SetItem(GetPrevItem()); + UpdateBlock(); + return; + } + else if (m_blockCursor <= 0 && m_blockOffset && m_blockOffset - BLOCK_SCROLL_OFFSET >= 0) + { + // this is the first item on page + ScrollToBlockOffset(m_blockOffset - BLOCK_SCROLL_OFFSET); + UpdateBlock(); + return; + } + + CGUIControl::OnLeft(); + } + else + { + CGUIAction action = GetAction(ACTION_MOVE_LEFT); + if (m_channelCursor > 0) + { + SetChannel(m_channelCursor - 1); + } + else if (m_channelCursor == 0 && m_channelOffset) + { + ScrollToChannelOffset(m_channelOffset - 1); + SetChannel(0); + } + else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around + { + int offset = m_gridModel->ChannelItemsSize() - m_channelsPerPage; + + if (offset < 0) + offset = 0; + + SetChannel(m_gridModel->GetLastChannel() - offset); + ScrollToChannelOffset(offset); + } + else + CGUIControl::OnLeft(); + } +} + +void CGUIEPGGridContainer::OnRight() +{ + if (!HasData()) + return CGUIControl::OnRight(); + + if (m_orientation == VERTICAL) + { + if (m_gridModel->GetGridItemEndBlock(m_channelCursor + m_channelOffset, + m_blockCursor + m_blockOffset) < + (m_blockOffset + m_blocksPerPage - 1)) + { + // this is not last item on page + SetItem(GetNextItem()); + UpdateBlock(); + return; + } + else if ((m_blockOffset != m_gridModel->GridItemsSize() - m_blocksPerPage) && + m_gridModel->GridItemsSize() > m_blocksPerPage && + m_blockOffset + BLOCK_SCROLL_OFFSET < m_gridModel->GetLastBlock()) + { + // this is the last item on page + ScrollToBlockOffset(m_blockOffset + BLOCK_SCROLL_OFFSET); + UpdateBlock(); + return; + } + + CGUIControl::OnRight(); + } + else + { + CGUIAction action = GetAction(ACTION_MOVE_RIGHT); + if (m_channelOffset + m_channelCursor < m_gridModel->GetLastChannel()) + { + if (m_channelCursor + 1 < m_channelsPerPage) + { + SetChannel(m_channelCursor + 1); + } + else + { + ScrollToChannelOffset(m_channelOffset + 1); + SetChannel(m_channelsPerPage - 1); + } + } + else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around + { + SetChannel(0); + ScrollToChannelOffset(0); + } + else + CGUIControl::OnRight(); + } +} + +bool CGUIEPGGridContainer::SetChannel(const std::string& channel) +{ + for (int iIndex = 0; iIndex < m_gridModel->ChannelItemsSize(); iIndex++) + { + std::string strPath = m_gridModel->GetChannelItem(iIndex)->GetProperty("path").asString(); + if (strPath == channel) + { + GoToChannel(iIndex); + return true; + } + } + return false; +} + +bool CGUIEPGGridContainer::SetChannel(const std::shared_ptr<CPVRChannel>& channel) +{ + for (int iIndex = 0; iIndex < m_gridModel->ChannelItemsSize(); iIndex++) + { + int iChannelId = static_cast<int>(m_gridModel->GetChannelItem(iIndex)->GetProperty("channelid").asInteger(-1)); + if (iChannelId == channel->ChannelID()) + { + GoToChannel(iIndex); + return true; + } + } + return false; +} + +bool CGUIEPGGridContainer::SetChannel(const CPVRChannelNumber& channelNumber) +{ + for (int iIndex = 0; iIndex < m_gridModel->ChannelItemsSize(); iIndex++) + { + const CPVRChannelNumber& number = + m_gridModel->GetChannelItem(iIndex)->GetPVRChannelGroupMemberInfoTag()->ChannelNumber(); + if (number == channelNumber) + { + GoToChannel(iIndex); + return true; + } + } + return false; +} + +void CGUIEPGGridContainer::SetChannel(int channel) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + int channelIndex = channel + m_channelOffset; + int blockIndex = m_blockCursor + m_blockOffset; + if (channelIndex < m_gridModel->ChannelItemsSize() && blockIndex < m_gridModel->GridItemsSize()) + { + if (SetItem(m_gridModel->GetGridItem(channelIndex, m_blockTravelAxis), channelIndex, + m_blockTravelAxis)) + { + m_channelCursor = channel; + MarkDirtyRegion(); + UpdateBlock(false); + } + } +} + +void CGUIEPGGridContainer::SetBlock(int block, bool bUpdateBlockTravelAxis /* = true */) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (block < 0) + m_blockCursor = 0; + else if (block > m_blocksPerPage - 1) + m_blockCursor = m_blocksPerPage - 1; + else + m_blockCursor = block; + + if (bUpdateBlockTravelAxis) + m_blockTravelAxis = m_blockOffset + m_blockCursor; + + UpdateItem(); + MarkDirtyRegion(); +} + +void CGUIEPGGridContainer::UpdateBlock(bool bUpdateBlockTravelAxis /* = true */) +{ + SetBlock(m_itemStartBlock > 0 ? m_itemStartBlock - m_blockOffset : 0, bUpdateBlockTravelAxis); +} + +CGUIListItemLayout* CGUIEPGGridContainer::GetFocusedLayout() const +{ + CGUIListItemPtr item = GetListItem(0); + + if (item) + return item->GetFocusedLayout(); + + return nullptr; +} + +bool CGUIEPGGridContainer::SelectItemFromPoint(const CPoint& point, bool justGrid /* = false */) +{ + /* point has already had origin set to m_posX, m_posY */ + if (!m_focusedProgrammeLayout || !m_programmeLayout || (justGrid && point.x < 0)) + return false; + + int channel; + int block; + + if (m_orientation == VERTICAL) + { + channel = point.y / m_channelHeight; + block = point.x / m_blockSize; + } + else + { + channel = point.x / m_channelWidth; + block = point.y / m_blockSize; + } + + if (channel > m_channelsPerPage) + channel = m_channelsPerPage - 1; + + if (channel >= m_gridModel->ChannelItemsSize()) + channel = m_gridModel->GetLastChannel(); + + if (channel < 0) + channel = 0; + + if (block > m_blocksPerPage) + block = m_blocksPerPage - 1; + + if (block < 0) + block = 0; + + int channelIndex = channel + m_channelOffset; + int blockIndex = block + m_blockOffset; + + // bail if out of range + if (channelIndex >= m_gridModel->ChannelItemsSize() || blockIndex >= m_gridModel->GridItemsSize()) + return false; + + // bail if block isn't occupied + if (!m_gridModel->GetGridItem(channelIndex, blockIndex)) + return false; + + SetChannel(channel); + SetBlock(block); + return true; +} + +EVENT_RESULT CGUIEPGGridContainer::OnMouseEvent(const CPoint& point, const CMouseEvent& event) +{ + switch (event.m_id) + { + case ACTION_MOUSE_LEFT_CLICK: + OnMouseClick(0, point); + return EVENT_RESULT_HANDLED; + case ACTION_MOUSE_RIGHT_CLICK: + OnMouseClick(1, point); + return EVENT_RESULT_HANDLED; + case ACTION_MOUSE_DOUBLE_CLICK: + OnMouseDoubleClick(0, point); + return EVENT_RESULT_HANDLED; + case ACTION_MOUSE_WHEEL_UP: + OnMouseWheel(-1, point); + return EVENT_RESULT_HANDLED; + case ACTION_MOUSE_WHEEL_DOWN: + OnMouseWheel(1, point); + return EVENT_RESULT_HANDLED; + case ACTION_GESTURE_BEGIN: + { + // we want exclusive access + CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID()); + SendWindowMessage(msg); + return EVENT_RESULT_HANDLED; + } + case ACTION_GESTURE_END: + case ACTION_GESTURE_ABORT: + { + // we're done with exclusive access + CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID()); + SendWindowMessage(msg); + ScrollToChannelOffset(MathUtils::round_int( + static_cast<double>(m_channelScrollOffset / m_channelLayout->Size(m_orientation)))); + SetChannel(m_channelCursor); + ScrollToBlockOffset( + MathUtils::round_int(static_cast<double>(m_programmeScrollOffset / m_blockSize))); + SetBlock(m_blockCursor); + return EVENT_RESULT_HANDLED; + } + case ACTION_GESTURE_PAN: + { + m_programmeScrollOffset -= event.m_offsetX; + m_channelScrollOffset -= event.m_offsetY; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_channelOffset = MathUtils::round_int( + static_cast<double>(m_channelScrollOffset / m_channelLayout->Size(m_orientation))); + m_blockOffset = + MathUtils::round_int(static_cast<double>(m_programmeScrollOffset / m_blockSize)); + ValidateOffset(); + } + return EVENT_RESULT_HANDLED; + } + default: + return EVENT_RESULT_UNHANDLED; + } +} + +bool CGUIEPGGridContainer::OnMouseOver(const CPoint& point) +{ + // select the item under the pointer + SelectItemFromPoint(point - CPoint(m_gridPosX, m_gridPosY), false); + return CGUIControl::OnMouseOver(point); +} + +bool CGUIEPGGridContainer::OnMouseClick(int dwButton, const CPoint& point) +{ + if (SelectItemFromPoint(point - CPoint(m_gridPosX, m_gridPosY))) + { + // send click message to window + OnClick(ACTION_MOUSE_LEFT_CLICK + dwButton); + return true; + } + return false; +} + +bool CGUIEPGGridContainer::OnMouseDoubleClick(int dwButton, const CPoint& point) +{ + if (SelectItemFromPoint(point - CPoint(m_gridPosX, m_gridPosY))) + { + // send double click message to window + OnClick(ACTION_MOUSE_DOUBLE_CLICK + dwButton); + return true; + } + return false; +} + +bool CGUIEPGGridContainer::OnClick(int actionID) +{ + int subItem = 0; + + if (actionID == ACTION_SELECT_ITEM || actionID == ACTION_MOUSE_LEFT_CLICK) + { + // grab the currently focused subitem (if applicable) + CGUIListItemLayout* focusedLayout = GetFocusedLayout(); + + if (focusedLayout) + subItem = focusedLayout->GetFocusedItem(); + } + + // Don't know what to do, so send to our parent window. + CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID(), actionID, subItem); + return SendWindowMessage(msg); +} + +bool CGUIEPGGridContainer::OnMouseWheel(char wheel, const CPoint& point) +{ + // doesn't work while an item is selected? + ProgrammesScroll(-wheel); + return true; +} + +std::shared_ptr<CPVRChannelGroupMember> CGUIEPGGridContainer::GetSelectedChannelGroupMember() const +{ + CFileItemPtr fileItem; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_channelCursor + m_channelOffset < m_gridModel->ChannelItemsSize()) + fileItem = m_gridModel->GetChannelItem(m_channelCursor + m_channelOffset); + } + + if (fileItem) + return fileItem->GetPVRChannelGroupMemberInfoTag(); + + return {}; +} + +CDateTime CGUIEPGGridContainer::GetSelectedDate() const +{ + return m_gridModel->GetStartTimeForBlock(m_blockOffset + m_blockCursor); +} + +CFileItemPtr CGUIEPGGridContainer::GetSelectedGridItem(int offset /*= 0*/) const +{ + CFileItemPtr item; + + if (m_channelCursor + m_channelOffset + offset < m_gridModel->ChannelItemsSize() && + m_blockCursor + m_blockOffset < m_gridModel->GridItemsSize()) + item = m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset); + + return item; +} + + +CGUIListItemPtr CGUIEPGGridContainer::GetListItem(int offset, unsigned int flag) const +{ + if (!m_gridModel->HasChannelItems()) + return CGUIListItemPtr(); + + int item = m_channelCursor + m_channelOffset + offset; + if (flag & INFOFLAG_LISTITEM_POSITION) + item = GetChannelScrollOffset(m_channelLayout); + + if (flag & INFOFLAG_LISTITEM_WRAP) + { + item %= m_gridModel->ChannelItemsSize(); + if (item < 0) + item += m_gridModel->ChannelItemsSize(); + + return m_gridModel->GetChannelItem(item); + } + else + { + if (item >= 0 && item < m_gridModel->ChannelItemsSize()) + return m_gridModel->GetChannelItem(item); + } + return CGUIListItemPtr(); +} + +std::string CGUIEPGGridContainer::GetLabel(int info) const +{ + std::string label; + switch (info) + { + case CONTAINER_NUM_PAGES: + if (m_channelsPerPage > 0) + label = std::to_string((m_gridModel->ChannelItemsSize() + m_channelsPerPage - 1) / + m_channelsPerPage); + else + label = std::to_string(0); + break; + case CONTAINER_CURRENT_PAGE: + if (m_channelsPerPage > 0) + label = std::to_string(1 + (m_channelCursor + m_channelOffset) / m_channelsPerPage); + else + label = std::to_string(1); + break; + case CONTAINER_POSITION: + label = std::to_string(1 + m_channelCursor + m_channelOffset); + break; + case CONTAINER_NUM_ITEMS: + label = std::to_string(m_gridModel->ChannelItemsSize()); + break; + default: + break; + } + return label; +} + +void CGUIEPGGridContainer::SetItem(const std::pair<std::shared_ptr<CFileItem>, int>& itemInfo) +{ + SetItem(itemInfo.first, m_channelCursor + m_channelOffset, itemInfo.second); +} + +bool CGUIEPGGridContainer::SetItem(const std::shared_ptr<CFileItem>& item, + int channelIndex, + int blockIndex) +{ + if (item && channelIndex < m_gridModel->ChannelItemsSize() && + blockIndex < m_gridModel->GridItemsSize()) + { + m_itemStartBlock = m_gridModel->GetGridItemStartBlock(channelIndex, blockIndex); + return true; + } + else + { + m_itemStartBlock = 0; + return false; + } +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainer::GetItem() const +{ + const int channelIndex = m_channelCursor + m_channelOffset; + const int blockIndex = m_blockCursor + m_blockOffset; + + if (channelIndex >= m_gridModel->ChannelItemsSize() || blockIndex >= m_gridModel->GridItemsSize()) + return {}; + + return m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset); +} + +std::pair<std::shared_ptr<CFileItem>, int> CGUIEPGGridContainer::GetNextItem() const +{ + int block = m_gridModel->GetGridItemEndBlock(m_channelCursor + m_channelOffset, + m_blockCursor + m_blockOffset); + if (block < m_gridModel->GridItemsSize()) + { + // first block of next event is one block after end block of selected event + block += 1; + } + + return {m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, block), block}; +} + +std::pair<std::shared_ptr<CFileItem>, int> CGUIEPGGridContainer::GetPrevItem() const +{ + int block = m_gridModel->GetGridItemStartBlock(m_channelCursor + m_channelOffset, + m_blockCursor + m_blockOffset); + if (block > 0) + { + // last block of previous event is one block before start block of selected event + block -= 1; + } + + return {m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, block), block}; +} + +void CGUIEPGGridContainer::UpdateItem() +{ + SetItem(GetItem(), m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset); +} + +void CGUIEPGGridContainer::SetFocus(bool focus) +{ + if (focus != HasFocus()) + SetInvalid(); + + CGUIControl::SetFocus(focus); +} + +void CGUIEPGGridContainer::ScrollToChannelOffset(int offset) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + float size = m_programmeLayout->Size(m_orientation); + int range = m_channelsPerPage / 4; + + if (range <= 0) + range = 1; + + if (offset * size < m_channelScrollOffset && m_channelScrollOffset - offset * size > size * range) + { + // scrolling up, and we're jumping more than 0.5 of a screen + m_channelScrollOffset = (offset + range) * size; + } + + if (offset * size > m_channelScrollOffset && offset * size - m_channelScrollOffset > size * range) + { + // scrolling down, and we're jumping more than 0.5 of a screen + m_channelScrollOffset = (offset - range) * size; + } + + m_channelScrollSpeed = (offset * size - m_channelScrollOffset) / m_scrollTime; + m_channelOffset = offset; + MarkDirtyRegion(); +} + +void CGUIEPGGridContainer::ScrollToBlockOffset(int offset) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + // make sure offset is in valid range + offset = std::max(0, std::min(offset, m_gridModel->GridItemsSize() - m_blocksPerPage)); + + float size = m_blockSize; + int range = m_blocksPerPage / 1; + + if (range <= 0) + range = 1; + + if (offset * size < m_programmeScrollOffset && m_programmeScrollOffset - offset * size > size * range) + { + // scrolling left, and we're jumping more than 0.5 of a screen + m_programmeScrollOffset = (offset + range) * size; + } + + if (offset * size > m_programmeScrollOffset && offset * size - m_programmeScrollOffset > size * range) + { + // scrolling right, and we're jumping more than 0.5 of a screen + m_programmeScrollOffset = (offset - range) * size; + } + + m_programmeScrollSpeed = (offset * size - m_programmeScrollOffset) / m_scrollTime; + m_blockOffset = offset; + MarkDirtyRegion(); +} + +void CGUIEPGGridContainer::ValidateOffset() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (!m_programmeLayout) + return; + + float pos = (m_orientation == VERTICAL) ? m_channelHeight : m_channelWidth; + + if (m_gridModel->ChannelItemsSize() && + (m_channelOffset > m_gridModel->ChannelItemsSize() - m_channelsPerPage || + m_channelScrollOffset > (m_gridModel->ChannelItemsSize() - m_channelsPerPage) * pos)) + { + m_channelOffset = m_gridModel->ChannelItemsSize() - m_channelsPerPage; + m_channelScrollOffset = m_channelOffset * pos; + } + + if (m_channelOffset < 0 || m_channelScrollOffset < 0) + { + m_channelOffset = 0; + m_channelScrollOffset = 0; + } + + if (m_gridModel->GridItemsSize() && + (m_blockOffset > m_gridModel->GridItemsSize() - m_blocksPerPage || + m_programmeScrollOffset > (m_gridModel->GridItemsSize() - m_blocksPerPage) * m_blockSize)) + { + m_blockOffset = m_gridModel->GridItemsSize() - m_blocksPerPage; + m_programmeScrollOffset = m_blockOffset * m_blockSize; + } + + if (m_blockOffset < 0 || m_programmeScrollOffset < 0) + { + m_blockOffset = 0; + m_programmeScrollOffset = 0; + } +} + +void CGUIEPGGridContainer::LoadLayout(TiXmlElement* layout) +{ + /* layouts for the channel column */ + TiXmlElement* itemElement = layout->FirstChildElement("channellayout"); + while (itemElement) + { + m_channelLayouts.emplace_back(); + m_channelLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height); + itemElement = itemElement->NextSiblingElement("channellayout"); + } + itemElement = layout->FirstChildElement("focusedchannellayout"); + while (itemElement) + { + m_focusedChannelLayouts.emplace_back(); + m_focusedChannelLayouts.back().LoadLayout(itemElement, GetParentID(), true, m_width, m_height); + itemElement = itemElement->NextSiblingElement("focusedchannellayout"); + } + + /* layouts for the grid items */ + itemElement = layout->FirstChildElement("focusedlayout"); + while (itemElement) + { + m_focusedProgrammeLayouts.emplace_back(); + m_focusedProgrammeLayouts.back().LoadLayout(itemElement, GetParentID(), true, m_width, m_height); + itemElement = itemElement->NextSiblingElement("focusedlayout"); + } + itemElement = layout->FirstChildElement("itemlayout"); + while (itemElement) + { + m_programmeLayouts.emplace_back(); + m_programmeLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height); + itemElement = itemElement->NextSiblingElement("itemlayout"); + } + + /* layout for the date label for the grid */ + itemElement = layout->FirstChildElement("rulerdatelayout"); + while (itemElement) + { + m_rulerDateLayouts.emplace_back(); + m_rulerDateLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height); + itemElement = itemElement->NextSiblingElement("rulerdatelayout"); + } + + /* layout for the timeline for the grid */ + itemElement = layout->FirstChildElement("rulerlayout"); + while (itemElement) + { + m_rulerLayouts.emplace_back(); + m_rulerLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height); + itemElement = itemElement->NextSiblingElement("rulerlayout"); + } + + UpdateLayout(); +} + +std::string CGUIEPGGridContainer::GetDescription() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + const int channelIndex = m_channelCursor + m_channelOffset; + const int blockIndex = m_blockCursor + m_blockOffset; + + if (channelIndex < m_gridModel->ChannelItemsSize() && blockIndex < m_gridModel->GridItemsSize()) + { + const std::shared_ptr<CFileItem> item = m_gridModel->GetGridItem(channelIndex, blockIndex); + if (item) + return item->GetLabel(); + } + + return {}; +} + +void CGUIEPGGridContainer::JumpToNow() +{ + m_bEnableProgrammeScrolling = false; + GoToNow(); +} + +void CGUIEPGGridContainer::JumpToDate(const CDateTime& date) +{ + m_bEnableProgrammeScrolling = false; + GoToDate(date); +} + +void CGUIEPGGridContainer::GoToBegin() +{ + ScrollToBlockOffset(0); + SetBlock(0); +} + +void CGUIEPGGridContainer::GoToEnd() +{ + ScrollToBlockOffset(m_gridModel->GetLastBlock() - m_blocksPerPage + 1); + SetBlock(m_blocksPerPage - 1); +} + +void CGUIEPGGridContainer::GoToNow() +{ + GoToDate(CDateTime::GetUTCDateTime()); +} + +void CGUIEPGGridContainer::GoToDate(const CDateTime& date) +{ + unsigned int offset = m_gridModel->GetPageNowOffset(); + ScrollToBlockOffset(m_gridModel->GetBlock(date) - offset); + + // ensure we're selecting the active event, not its predecessor. + const int iChannel = m_channelOffset + m_channelCursor; + const int iBlock = m_blockOffset + offset; + if (iChannel >= m_gridModel->ChannelItemsSize() || iBlock >= m_gridModel->GridItemsSize() || + m_gridModel->GetGridItemEndTime(iChannel, iBlock) > date) + { + SetBlock(offset); + } + else + { + SetBlock(offset + 1); + } +} + +void CGUIEPGGridContainer::GoToFirstChannel() +{ + GoToChannel(0); +} + +void CGUIEPGGridContainer::GoToLastChannel() +{ + if (m_gridModel->ChannelItemsSize()) + GoToChannel(m_gridModel->GetLastChannel()); + else + GoToChannel(0); +} + +void CGUIEPGGridContainer::GoToTop() +{ + if (m_orientation == VERTICAL) + { + GoToChannel(0); + } + else + { + GoToBlock(0); + } +} + +void CGUIEPGGridContainer::GoToBottom() +{ + if (m_orientation == VERTICAL) + { + if (m_gridModel->HasChannelItems()) + GoToChannel(m_gridModel->GetLastChannel()); + else + GoToChannel(0); + } + else + { + if (m_gridModel->GridItemsSize()) + GoToBlock(m_gridModel->GetLastBlock()); + else + GoToBlock(0); + } +} + +void CGUIEPGGridContainer::GoToMostLeft() +{ + if (m_orientation == VERTICAL) + { + GoToBlock(0); + } + else + { + GoToChannel(0); + } +} + +void CGUIEPGGridContainer::GoToMostRight() +{ + if (m_orientation == VERTICAL) + { + if (m_gridModel->GridItemsSize()) + GoToBlock(m_gridModel->GetLastBlock()); + else + GoToBlock(0); + } + else + { + if (m_gridModel->HasChannelItems()) + GoToChannel(m_gridModel->GetLastChannel()); + else + GoToChannel(0); + } +} + +void CGUIEPGGridContainer::SetTimelineItems(const std::unique_ptr<CFileItemList>& items, + const CDateTime& gridStart, + const CDateTime& gridEnd) +{ + int iRulerUnit; + int iFirstChannel; + int iChannelsPerPage; + int iBlocksPerPage; + int iFirstBlock; + float fBlockSize; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + iRulerUnit = m_rulerUnit; + iFirstChannel = m_channelOffset; + iChannelsPerPage = m_channelsPerPage; + iFirstBlock = m_blockOffset; + iBlocksPerPage = m_blocksPerPage; + fBlockSize = m_blockSize; + } + + std::unique_ptr<CGUIEPGGridContainerModel> oldUpdatedGridModel; + std::unique_ptr<CGUIEPGGridContainerModel> newUpdatedGridModel(new CGUIEPGGridContainerModel); + + newUpdatedGridModel->Initialize(items, gridStart, gridEnd, iFirstChannel, iChannelsPerPage, + iFirstBlock, iBlocksPerPage, iRulerUnit, fBlockSize); + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + // grid contains CFileItem instances. CFileItem dtor locks global graphics mutex. + // by increasing its refcount make sure, old data are not deleted while we're holding own mutex. + oldUpdatedGridModel = std::move(m_updatedGridModel); + + m_updatedGridModel = std::move(newUpdatedGridModel); + } +} + +std::unique_ptr<CFileItemList> CGUIEPGGridContainer::GetCurrentTimeLineItems() const +{ + return m_gridModel->GetCurrentTimeLineItems(m_channelOffset, m_channelsPerPage); +} + +void CGUIEPGGridContainer::GoToChannel(int channelIndex) +{ + if (channelIndex < m_channelsPerPage) + { + // first page + ScrollToChannelOffset(0); + SetChannel(channelIndex); + } + else if (channelIndex > m_gridModel->ChannelItemsSize() - m_channelsPerPage) + { + // last page + ScrollToChannelOffset(m_gridModel->ChannelItemsSize() - m_channelsPerPage); + SetChannel(channelIndex - (m_gridModel->ChannelItemsSize() - m_channelsPerPage)); + } + else + { + ScrollToChannelOffset(channelIndex - m_channelCursor); + SetChannel(m_channelCursor); + } +} + +void CGUIEPGGridContainer::GoToBlock(int blockIndex) +{ + int lastPage = m_gridModel->GridItemsSize() - m_blocksPerPage; + if (blockIndex > lastPage) + { + // last page + ScrollToBlockOffset(lastPage); + SetBlock(blockIndex - lastPage); + } + else + { + ScrollToBlockOffset(blockIndex - m_blockCursor); + SetBlock(m_blockCursor); + } +} + +void CGUIEPGGridContainer::UpdateLayout() +{ + CGUIListItemLayout* oldFocusedChannelLayout = m_focusedChannelLayout; + CGUIListItemLayout* oldChannelLayout = m_channelLayout; + CGUIListItemLayout* oldFocusedProgrammeLayout = m_focusedProgrammeLayout; + CGUIListItemLayout* oldProgrammeLayout = m_programmeLayout; + CGUIListItemLayout* oldRulerLayout = m_rulerLayout; + CGUIListItemLayout* oldRulerDateLayout = m_rulerDateLayout; + + GetCurrentLayouts(); + + // Note: m_rulerDateLayout is optional + if (!m_focusedProgrammeLayout || !m_programmeLayout || !m_focusedChannelLayout || !m_channelLayout || !m_rulerLayout) + return; + + if (oldChannelLayout == m_channelLayout && oldFocusedChannelLayout == m_focusedChannelLayout && + oldProgrammeLayout == m_programmeLayout && oldFocusedProgrammeLayout == m_focusedProgrammeLayout && + oldRulerLayout == m_rulerLayout && oldRulerDateLayout == m_rulerDateLayout) + return; // nothing has changed, so don't update stuff + + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_channelHeight = m_channelLayout->Size(VERTICAL); + m_channelWidth = m_channelLayout->Size(HORIZONTAL); + + m_rulerDateHeight = m_rulerDateLayout ? m_rulerDateLayout->Size(VERTICAL) : 0; + m_rulerDateWidth = m_rulerDateLayout ? m_rulerDateLayout->Size(HORIZONTAL) : 0; + + if (m_orientation == VERTICAL) + { + m_rulerHeight = m_rulerLayout->Size(VERTICAL); + m_gridPosX = m_posX + m_channelWidth; + m_gridPosY = m_posY + m_rulerHeight + m_rulerDateHeight; + m_gridWidth = m_width - m_channelWidth; + m_gridHeight = m_height - m_rulerHeight - m_rulerDateHeight; + m_blockSize = m_gridWidth / m_blocksPerPage; + m_rulerWidth = m_rulerUnit * m_blockSize; + m_channelPosX = m_posX; + m_channelPosY = m_posY + m_rulerHeight + m_rulerDateHeight; + m_rulerPosX = m_posX + m_channelWidth; + m_rulerPosY = m_posY + m_rulerDateHeight; + m_channelsPerPage = m_gridHeight / m_channelHeight; + m_programmesPerPage = (m_gridWidth / m_blockSize) + 1; + + m_programmeLayout->SetHeight(m_channelHeight); + m_focusedProgrammeLayout->SetHeight(m_channelHeight); + } + else + { + m_rulerWidth = m_rulerLayout->Size(HORIZONTAL); + m_gridPosX = m_posX + m_rulerWidth; + m_gridPosY = m_posY + m_channelHeight + m_rulerDateHeight; + m_gridWidth = m_width - m_rulerWidth; + m_gridHeight = m_height - m_channelHeight - m_rulerDateHeight; + m_blockSize = m_gridHeight / m_blocksPerPage; + m_rulerHeight = m_rulerUnit * m_blockSize; + m_channelPosX = m_posX + m_rulerWidth; + m_channelPosY = m_posY + m_rulerDateHeight; + m_rulerPosX = m_posX; + m_rulerPosY = m_posY + m_channelHeight + m_rulerDateHeight; + m_channelsPerPage = m_gridWidth / m_channelWidth; + m_programmesPerPage = (m_gridHeight / m_blockSize) + 1; + + m_programmeLayout->SetWidth(m_channelWidth); + m_focusedProgrammeLayout->SetWidth(m_channelWidth); + } + + // ensure that the scroll offsets are a multiple of our sizes + m_channelScrollOffset = m_channelOffset * m_programmeLayout->Size(m_orientation); + m_programmeScrollOffset = m_blockOffset * m_blockSize; +} + +void CGUIEPGGridContainer::UpdateScrollOffset(unsigned int currentTime) +{ + if (!m_programmeLayout) + return; + + m_channelScrollOffset += m_channelScrollSpeed * (currentTime - m_channelScrollLastTime); + if ((m_channelScrollSpeed < 0 && m_channelScrollOffset < m_channelOffset * m_programmeLayout->Size(m_orientation)) || + (m_channelScrollSpeed > 0 && m_channelScrollOffset > m_channelOffset * m_programmeLayout->Size(m_orientation))) + { + m_channelScrollOffset = m_channelOffset * m_programmeLayout->Size(m_orientation); + m_channelScrollSpeed = 0; + m_bEnableChannelScrolling = true; + } + + m_channelScrollLastTime = currentTime; + m_programmeScrollOffset += m_programmeScrollSpeed * (currentTime - m_programmeScrollLastTime); + + if ((m_programmeScrollSpeed < 0 && m_programmeScrollOffset < m_blockOffset * m_blockSize) || + (m_programmeScrollSpeed > 0 && m_programmeScrollOffset > m_blockOffset * m_blockSize)) + { + m_programmeScrollOffset = m_blockOffset * m_blockSize; + m_programmeScrollSpeed = 0; + m_bEnableProgrammeScrolling = true; + } + + m_programmeScrollLastTime = currentTime; + + if (m_channelScrollSpeed || m_programmeScrollSpeed) + MarkDirtyRegion(); +} + +void CGUIEPGGridContainer::GetCurrentLayouts() +{ + m_channelLayout = nullptr; + + for (unsigned int i = 0; i < m_channelLayouts.size(); i++) + { + if (m_channelLayouts[i].CheckCondition()) + { + m_channelLayout = &m_channelLayouts[i]; + break; + } + } + + if (!m_channelLayout && !m_channelLayouts.empty()) + m_channelLayout = &m_channelLayouts[0]; // failsafe + + m_focusedChannelLayout = nullptr; + + for (unsigned int i = 0; i < m_focusedChannelLayouts.size(); i++) + { + if (m_focusedChannelLayouts[i].CheckCondition()) + { + m_focusedChannelLayout = &m_focusedChannelLayouts[i]; + break; + } + } + + if (!m_focusedChannelLayout && !m_focusedChannelLayouts.empty()) + m_focusedChannelLayout = &m_focusedChannelLayouts[0]; // failsafe + + m_programmeLayout = nullptr; + + for (unsigned int i = 0; i < m_programmeLayouts.size(); i++) + { + if (m_programmeLayouts[i].CheckCondition()) + { + m_programmeLayout = &m_programmeLayouts[i]; + break; + } + } + + if (!m_programmeLayout && !m_programmeLayouts.empty()) + m_programmeLayout = &m_programmeLayouts[0]; // failsafe + + m_focusedProgrammeLayout = nullptr; + + for (unsigned int i = 0; i < m_focusedProgrammeLayouts.size(); i++) + { + if (m_focusedProgrammeLayouts[i].CheckCondition()) + { + m_focusedProgrammeLayout = &m_focusedProgrammeLayouts[i]; + break; + } + } + + if (!m_focusedProgrammeLayout && !m_focusedProgrammeLayouts.empty()) + m_focusedProgrammeLayout = &m_focusedProgrammeLayouts[0]; // failsafe + + m_rulerLayout = nullptr; + + for (unsigned int i = 0; i < m_rulerLayouts.size(); i++) + { + if (m_rulerLayouts[i].CheckCondition()) + { + m_rulerLayout = &m_rulerLayouts[i]; + break; + } + } + + if (!m_rulerLayout && !m_rulerLayouts.empty()) + m_rulerLayout = &m_rulerLayouts[0]; // failsafe + + m_rulerDateLayout = nullptr; + + for (unsigned int i = 0; i < m_rulerDateLayouts.size(); i++) + { + if (m_rulerDateLayouts[i].CheckCondition()) + { + m_rulerDateLayout = &m_rulerDateLayouts[i]; + break; + } + } + + // Note: m_rulerDateLayout is optional; so no "failsafe" logic here (see above) +} + +void CGUIEPGGridContainer::SetRenderOffset(const CPoint& offset) +{ + m_renderOffset = offset; +} + +void CGUIEPGGridContainer::GetChannelCacheOffsets(int& cacheBefore, int& cacheAfter) +{ + if (m_channelScrollSpeed > 0) + { + cacheBefore = 0; + cacheAfter = m_cacheChannelItems; + } + else if (m_channelScrollSpeed < 0) + { + cacheBefore = m_cacheChannelItems; + cacheAfter = 0; + } + else + { + cacheBefore = m_cacheChannelItems / 2; + cacheAfter = m_cacheChannelItems / 2; + } +} + +void CGUIEPGGridContainer::GetProgrammeCacheOffsets(int& cacheBefore, int& cacheAfter) +{ + if (m_programmeScrollSpeed > 0) + { + cacheBefore = 0; + cacheAfter = m_cacheProgrammeItems; + } + else if (m_programmeScrollSpeed < 0) + { + cacheBefore = m_cacheProgrammeItems; + cacheAfter = 0; + } + else + { + cacheBefore = m_cacheProgrammeItems / 2; + cacheAfter = m_cacheProgrammeItems / 2; + } +} + +void CGUIEPGGridContainer::HandleChannels(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + if (!m_focusedChannelLayout || !m_channelLayout) + return; + + const int chanOffset = GetChannelScrollOffset(m_programmeLayout); + + int cacheBeforeChannel, cacheAfterChannel; + GetChannelCacheOffsets(cacheBeforeChannel, cacheAfterChannel); + + if (bRender) + { + if (m_orientation == VERTICAL) + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_channelPosX, m_channelPosY, m_channelWidth, m_gridHeight); + else + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_channelPosX, m_channelPosY, m_gridWidth, m_channelHeight); + } + else + { + // Free memory not used on screen + if (m_gridModel->ChannelItemsSize() > m_channelsPerPage + cacheBeforeChannel + cacheAfterChannel) + m_gridModel->FreeChannelMemory(chanOffset - cacheBeforeChannel, + chanOffset + m_channelsPerPage - 1 + cacheAfterChannel); + } + + CPoint originChannel = CPoint(m_channelPosX, m_channelPosY) + m_renderOffset; + float pos; + float end; + + if (m_orientation == VERTICAL) + { + pos = originChannel.y; + end = m_posY + m_height; + } + else + { + pos = originChannel.x; + end = m_posX + m_width; + } + + // we offset our draw position to take into account scrolling and whether or not our focused + // item is offscreen "above" the list. + float drawOffset = (chanOffset - cacheBeforeChannel) * m_channelLayout->Size(m_orientation) - + GetChannelScrollOffsetPos(); + if (m_channelOffset + m_channelCursor < chanOffset) + drawOffset += m_focusedChannelLayout->Size(m_orientation) - m_channelLayout->Size(m_orientation); + + pos += drawOffset; + end += cacheAfterChannel * m_channelLayout->Size(m_orientation); + + float focusedPos = 0; + CGUIListItemPtr focusedItem; + + CFileItemPtr item; + int current = chanOffset - cacheBeforeChannel; + while (pos < end && m_gridModel->HasChannelItems()) + { + int itemNo = current; + if (itemNo >= m_gridModel->ChannelItemsSize()) + break; + + bool focused = (current == m_channelOffset + m_channelCursor); + if (itemNo >= 0) + { + item = m_gridModel->GetChannelItem(itemNo); + if (bRender) + { + // render our item + if (focused) + { + focusedPos = pos; + focusedItem = item; + } + else + { + if (m_orientation == VERTICAL) + RenderItem(originChannel.x, pos, item.get(), false); + else + RenderItem(pos, originChannel.y, item.get(), false); + } + } + else + { + // process our item + if (m_orientation == VERTICAL) + ProcessItem(originChannel.x, pos, item, m_lastItem, focused, m_channelLayout, m_focusedChannelLayout, currentTime, dirtyregions); + else + ProcessItem(pos, originChannel.y, item, m_lastItem, focused, m_channelLayout, m_focusedChannelLayout, currentTime, dirtyregions); + } + } + // increment our position + pos += focused ? m_focusedChannelLayout->Size(m_orientation) : m_channelLayout->Size(m_orientation); + current++; + } + + if (bRender) + { + // render focused item last so it can overlap other items + if (focusedItem) + { + if (m_orientation == VERTICAL) + RenderItem(originChannel.x, focusedPos, focusedItem.get(), true); + else + RenderItem(focusedPos, originChannel.y, focusedItem.get(), true); + } + + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + } +} + +void CGUIEPGGridContainer::HandleRulerDate(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + if (!m_rulerDateLayout || m_gridModel->RulerItemsSize() <= 1 || m_gridModel->IsZeroGridDuration()) + return; + + CFileItemPtr item(m_gridModel->GetRulerItem(0)); + + if (bRender) + { + // Render single ruler item with date of selected programme + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_rulerDateWidth, m_rulerDateHeight); + RenderItem(m_posX, m_posY, item.get(), false); + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + } + else + { + const int rulerOffset = GetProgrammeScrollOffset(); + item->SetLabel(m_gridModel->GetRulerItem(rulerOffset / m_rulerUnit + 1)->GetLabel2()); + + CFileItemPtr lastitem; + ProcessItem(m_posX, m_posY, item, lastitem, false, m_rulerDateLayout, m_rulerDateLayout, currentTime, dirtyregions); + } +} + +void CGUIEPGGridContainer::HandleRuler(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + if (!m_rulerLayout || m_gridModel->RulerItemsSize() <= 1 || m_gridModel->IsZeroGridDuration()) + return; + + int rulerOffset = GetProgrammeScrollOffset(); + + CFileItemPtr item(m_gridModel->GetRulerItem(0)); + CFileItemPtr lastitem; + int cacheBeforeRuler, cacheAfterRuler; + + if (bRender) + { + if (!m_rulerDateLayout) + { + // Render single ruler item with date of selected programme + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height); + RenderItem(m_posX, m_posY, item.get(), false); + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + } + + // render ruler items + GetProgrammeCacheOffsets(cacheBeforeRuler, cacheAfterRuler); + + if (m_orientation == VERTICAL) + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_rulerPosX, m_rulerPosY, m_gridWidth, m_rulerHeight); + else + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_rulerPosX, m_rulerPosY, m_rulerWidth, m_gridHeight); + } + else + { + if (!m_rulerDateLayout) + { + item->SetLabel(m_gridModel->GetRulerItem(rulerOffset / m_rulerUnit + 1)->GetLabel2()); + ProcessItem(m_posX, m_posY, item, lastitem, false, m_rulerLayout, m_rulerLayout, currentTime, dirtyregions, m_channelWidth); + } + + GetProgrammeCacheOffsets(cacheBeforeRuler, cacheAfterRuler); + + // Free memory not used on screen + if (m_gridModel->RulerItemsSize() > m_blocksPerPage + cacheBeforeRuler + cacheAfterRuler) + m_gridModel->FreeRulerMemory(rulerOffset / m_rulerUnit + 1 - cacheBeforeRuler, + rulerOffset / m_rulerUnit + 1 + m_blocksPerPage - 1 + + cacheAfterRuler); + } + + CPoint originRuler = CPoint(m_rulerPosX, m_rulerPosY) + m_renderOffset; + float pos; + float end; + + if (m_orientation == VERTICAL) + { + pos = originRuler.x; + end = m_posX + m_width; + } + else + { + pos = originRuler.y; + end = m_posY + m_height; + } + + const float drawOffset = + (rulerOffset - cacheBeforeRuler) * m_blockSize - GetProgrammeScrollOffsetPos(); + pos += drawOffset; + end += cacheAfterRuler * m_rulerLayout->Size(m_orientation == VERTICAL ? HORIZONTAL : VERTICAL); + + if (rulerOffset % m_rulerUnit != 0) + { + /* first ruler marker starts before current view */ + int startBlock = rulerOffset - 1; + + while (startBlock % m_rulerUnit != 0) + startBlock--; + + int missingSection = rulerOffset - startBlock; + + pos -= missingSection * m_blockSize; + } + + while (pos < end && (rulerOffset / m_rulerUnit + 1) < m_gridModel->RulerItemsSize()) + { + item = m_gridModel->GetRulerItem(rulerOffset / m_rulerUnit + 1); + + if (m_orientation == VERTICAL) + { + if (bRender) + RenderItem(pos, originRuler.y, item.get(), false); + else + ProcessItem(pos, originRuler.y, item, lastitem, false, m_rulerLayout, m_rulerLayout, currentTime, dirtyregions, m_rulerWidth); + + pos += m_rulerWidth; + } + else + { + if (bRender) + RenderItem(originRuler.x, pos, item.get(), false); + else + ProcessItem(originRuler.x, pos, item, lastitem, false, m_rulerLayout, m_rulerLayout, currentTime, dirtyregions, m_rulerHeight); + + pos += m_rulerHeight; + } + + rulerOffset += m_rulerUnit; + } + + if (bRender) + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); +} + +void CGUIEPGGridContainer::HandleProgrammeGrid(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + if (!m_focusedProgrammeLayout || !m_programmeLayout || m_gridModel->RulerItemsSize() <= 1 || m_gridModel->IsZeroGridDuration()) + return; + + const int blockOffset = GetProgrammeScrollOffset(); + const int chanOffset = GetChannelScrollOffset(m_programmeLayout); + + int cacheBeforeProgramme, cacheAfterProgramme; + GetProgrammeCacheOffsets(cacheBeforeProgramme, cacheAfterProgramme); + + if (bRender) + { + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_gridPosX, m_gridPosY, m_gridWidth, m_gridHeight); + } + else + { + int cacheBeforeChannel, cacheAfterChannel; + GetChannelCacheOffsets(cacheBeforeChannel, cacheAfterChannel); + + // Free memory not used on screen + int firstChannel = chanOffset - cacheBeforeChannel; + if (firstChannel < 0) + firstChannel = 0; + int lastChannel = chanOffset + m_channelsPerPage - 1 + cacheAfterChannel; + if (lastChannel > m_gridModel->GetLastChannel()) + lastChannel = m_gridModel->GetLastChannel(); + int firstBlock = blockOffset - cacheBeforeProgramme; + if (firstBlock < 0) + firstBlock = 0; + int lastBlock = blockOffset + m_programmesPerPage - 1 + cacheAfterProgramme; + if (lastBlock > m_gridModel->GetLastBlock()) + lastBlock = m_gridModel->GetLastBlock(); + + if (m_gridModel->FreeProgrammeMemory(firstChannel, lastChannel, firstBlock, lastBlock)) + { + // announce changed viewport + const CGUIMessage msg( + GUI_MSG_REFRESH_LIST, GetParentID(), GetID(), static_cast<int>(PVREvent::Epg)); + CServiceBroker::GetAppMessenger()->SendGUIMessage(msg); + } + } + + CPoint originProgramme = CPoint(m_gridPosX, m_gridPosY) + m_renderOffset; + float posA; + float endA; + float posB; + float endB; + + if (m_orientation == VERTICAL) + { + posA = originProgramme.x; + endA = m_posX + m_width; + posB = originProgramme.y; + endB = m_gridPosY + m_gridHeight; + } + else + { + posA = originProgramme.y; + endA = m_posY + m_height; + posB = originProgramme.x; + endB = m_gridPosX + m_gridWidth; + } + + endA += cacheAfterProgramme * m_blockSize; + + const float drawOffsetA = blockOffset * m_blockSize - GetProgrammeScrollOffsetPos(); + posA += drawOffsetA; + const float drawOffsetB = + (chanOffset - cacheBeforeProgramme) * m_channelLayout->Size(m_orientation) - + GetChannelScrollOffsetPos(); + posB += drawOffsetB; + + int channel = chanOffset - cacheBeforeProgramme; + + float focusedPosX = 0; + float focusedPosY = 0; + CFileItemPtr focusedItem; + CFileItemPtr item; + + const int lastChannel = m_gridModel->GetLastChannel(); + while (posB < endB && HasData() && channel <= lastChannel) + { + if (channel >= 0) + { + int block = blockOffset; + float posA2 = posA; + + const int startBlock = blockOffset == 0 ? 0 : blockOffset - 1; + if (startBlock == 0 || m_gridModel->IsSameGridItem(channel, block, startBlock)) + { + // First program starts before current view + block = m_gridModel->GetGridItemStartBlock(channel, startBlock); + const int missingSection = blockOffset - block; + posA2 -= missingSection * m_blockSize; + } + + const int lastBlock = m_gridModel->GetLastBlock(); + while (posA2 < endA && HasData() && block <= lastBlock) + { + item = m_gridModel->GetGridItem(channel, block); + + bool focused = (channel == m_channelOffset + m_channelCursor) && + m_gridModel->IsSameGridItem(m_channelOffset + m_channelCursor, + m_blockOffset + m_blockCursor, block); + + if (bRender) + { + // reset to grid start position if first item is out of grid view + if (posA2 < posA) + posA2 = posA; + + // render our item + if (focused) + { + focusedPosX = posA2; + focusedPosY = posB; + focusedItem = item; + } + else + { + if (m_orientation == VERTICAL) + RenderItem(posA2, posB, item.get(), focused); + else + RenderItem(posB, posA2, item.get(), focused); + } + } + else + { + // calculate the size to truncate if item is out of grid view + float truncateSize = 0; + if (posA2 < posA) + { + truncateSize = posA - posA2; + posA2 = posA; // reset to grid start position + } + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + // truncate item's width + m_gridModel->DecreaseGridItemWidth(channel, block, truncateSize); + } + + if (m_orientation == VERTICAL) + ProcessItem(posA2, posB, item, m_lastChannel, focused, m_programmeLayout, + m_focusedProgrammeLayout, currentTime, dirtyregions, + m_gridModel->GetGridItemWidth(channel, block)); + else + ProcessItem(posB, posA2, item, m_lastChannel, focused, m_programmeLayout, + m_focusedProgrammeLayout, currentTime, dirtyregions, + m_gridModel->GetGridItemWidth(channel, block)); + } + + // increment our X position + posA2 += m_gridModel->GetGridItemWidth( + channel, block); // assumes focused & unfocused layouts have equal length + block += MathUtils::round_int( + static_cast<double>(m_gridModel->GetGridItemOriginWidth(channel, block) / m_blockSize)); + } + } + + // increment our Y position + channel++; + posB += (m_orientation == VERTICAL) ? m_channelHeight : m_channelWidth; + } + + if (bRender) + { + // and render the focused item last (for overlapping purposes) + if (focusedItem) + { + if (m_orientation == VERTICAL) + RenderItem(focusedPosX, focusedPosY, focusedItem.get(), true); + else + RenderItem(focusedPosY, focusedPosX, focusedItem.get(), true); + } + + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + } +} diff --git a/xbmc/pvr/guilib/GUIEPGGridContainer.dox b/xbmc/pvr/guilib/GUIEPGGridContainer.dox new file mode 100644 index 0000000..1916cde --- /dev/null +++ b/xbmc/pvr/guilib/GUIEPGGridContainer.dox @@ -0,0 +1,245 @@ +/*! + +\page EPGGrid_control EPGGrid control +\brief **Used to display the EPG guide in the PVR section.** + +\tableofcontents + +The epggrid control is used for creating an epg timeline in Kodi. You can choose +the position, size, and look of the grid and it's contents. + + +-------------------------------------------------------------------------------- +\section EPGGrid_control_sect1 Example + +~~~~~~~~~~~~~ +<control type="epggrid" id="10"> + <description>EPG Grid</description> + <posx>80</posx> + <posy>81</posy> + <width>1120</width> + <height>555</height> + <pagecontrol>10</pagecontrol> + <scrolltime>350</scrolltime> + <timeblocks>40</timeblocks> + <rulerunit>6</rulerunit> + <progresstexture border="5">PVR-EpgProgressIndicator.png</progresstexture> + <orienttation>vertical</orientation> + <onleft>31</onleft> + <onright>31</onright> + <onup>10</onup> + <ondown>10</ondown> + <rulerlayout height="35" width="40"> + <control type="image" id="1"> + <width>40</width> + <height>29</height> + <posx>0</posx> + <posy>0</posy> + <texture border="5">button-nofocus.png</texture> + </control> + <control type="label" id="2"> + <posx>10</posx> + <posy>0</posy> + <width>34</width> + <height>29</height> + <font>font12</font> + <aligny>center</aligny> + <selectedcolor>selected</selectedcolor> + <align>left</align> + <label>$INFO[ListItem.Label]</label> + </control> + </rulerlayout> + <channellayout height="52" width="280"> + <animation effect="fade" start="110" time="200">UnFocus</animation> + <control type="image" id="1"> + <posx>0</posx> + <posy>0</posy> + <width>270</width> + <height>52</height> + <texture border="5">button-nofocus.png</texture> + </control> + <control type="label"> + <posx>5</posx> + <posy>5</posy> + <width>40</width> + <height>35</height> + <font>font12</font> + <align>left</align> + <aligny>center</aligny> + <textcolor>grey</textcolor> + <selectedcolor>grey</selectedcolor> + <info>ListItem.ChannelNumber</info> + </control> + <control type="image"> + <posx>45</posx> + <posy>4</posy> + <width>45</width> + <height>44</height> + <texture>$INFO[ListItem.Icon]</texture> + </control> + <control type="label" id="1"> + <posx>94</posx> + <posy>0</posy> + <width>160</width> + <height>52</height> + <font>special12</font> + <aligny>center</aligny> + <selectedcolor>selected</selectedcolor> + <align>left</align> + <label>$INFO[ListItem.ChannelName]</label> + </control> + </channellayout> + <focusedchannellayout height="52" width="280"> + <animation effect="fade" start="110" time="200">OnFocus</animation> + <control type="image" id="1"> + <posx>0</posx> + <posy>0</posy> + <width>270</width> + <height>52</height> + <texture border="5">button-focus.png</texture> + </control> + <control type="label"> + <posx>5</posx> + <posy>5</posy> + <width>40</width> + <height>35</height> + <font>font12</font> + <align>left</align> + <aligny>center</aligny> + <textcolor>grey</textcolor> + <selectedcolor>grey</selectedcolor> + <info>ListItem.ChannelNumber</info> + </control> + <control type="image"> + <posx>45</posx> + <posy>4</posy> + <width>45</width> + <height>44</height> + <texture>$INFO[ListItem.Icon]</texture> + </control> + <control type="label" id="1"> + <posx>94</posx> + <posy>0</posy> + <width>160</width> + <height>52</height> + <font>special12</font> + <aligny>center</aligny> + <selectedcolor>selected</selectedcolor> + <align>left</align> + <label>$INFO[ListItem.ChannelName]</label> + </control> + </focusedchannellayout> + <itemlayout height="52" width="40"> + <control type="image" id="2"> + <width>40</width> + <height>52</height> + <posx>0</posx> + <posy>0</posy> + <aspectratio>stretch</aspectratio> + <texture border="3">epg-genres/$INFO[ListItem.Property(GenreType)].png</texture> + </control> + <control type="label" id="1"> + <posx>6</posx> + <posy>3</posy> + <width>30</width> + <height>25</height> + <font>font12</font> + <aligny>center</aligny> + <selectedcolor>selected</selectedcolor> + <align>left</align> + <info>ListItem.Label</info> + </control> + <control type="image"> + <posx>5</posx> + <posy>28</posy> + <width>30</width> + <height>20</height> + <texture>PVR-IsRecording.png</texture> + <visible>ListItem.IsRecording</visible> + </control> + <control type="image"> + <posx>5</posx> + <posy>28</posy> + <width>20</width> + <height>20</height> + <texture>PVR-HasTimer.png</texture> + <visible>ListItem.HasTimer + !ListItem.IsRecording</visible> + </control> + </itemlayout> + <focusedlayout height="52" width="40"> + <control type="image" id="14"> + <width>40</width> + <height>52</height> + <posx>0</posx> + <posy>0</posy> + <texture border="5">folder-focus.png</texture> + </control> + <control type="image" id="2"> + <width>40</width> + <height>52</height> + <posx>0</posx> + <posy>0</posy> + <aspectratio>stretch</aspectratio> + <texture border="3">epg-genres/$INFO[ListItem.Property(GenreType)].png</texture> + </control> + <control type="label" id="1"> + <posx>6</posx> + <posy>3</posy> + <width>30</width> + <height>25</height> + <font>font12</font> + <aligny>center</aligny> + <selectedcolor>selected</selectedcolor> + <align>left</align> + <info>ListItem.Label</info> + </control> + <control type="image"> + <posx>5</posx> + <posy>28</posy> + <width>30</width> + <height>20</height> + <texture>PVR-IsRecording.png</texture> + <visible>ListItem.IsRecording</visible> + </control> + <control type="image"> + <posx>5</posx> + <posy>28</posy> + <width>20</width> + <height>20</height> + <texture>PVR-HasTimer.png</texture> + <visible>ListItem.HasTimer + !ListItem.IsRecording</visible> + </control> + </focusedlayout> +</control> +~~~~~~~~~~~~~ + + +-------------------------------------------------------------------------------- +\section EPGGrid_control_sect2 Available tags + +In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags) +the following tags are available. Note that each tag is **lower case** only. This is +important, as `xml` tags are case-sensitive. + +| Tag | Description | +|----------------------:|:--------------------------------------------------------------| +| timeblocks | The number of timeframes on the ruler. +| rulerunit | Timeframe of each unit on ruler. 1 unit equals 5 minutes. +| rulerdatelayout | The layout of a ruler date item (usually used to display the start date of current epg page). +| rulerlayout | The layout of a ruler item. +| progresstexture | A texture which indicates the current progress time +| channellayout | The layout of a channel item. +| focusedchannellayout | The focused layout of a channel item. +| itemlayout | The layout of the grid +| focusedlayout | The focused layout of the grid + + +-------------------------------------------------------------------------------- +\section EPGGrid_control_sect3 See also + +#### Development: + +- [Add-on development](http://kodi.wiki/view/Add-on_development) +- [Skinning](http://kodi.wiki/view/Skinning) + +*/ diff --git a/xbmc/pvr/guilib/GUIEPGGridContainer.h b/xbmc/pvr/guilib/GUIEPGGridContainer.h new file mode 100644 index 0000000..360ade9 --- /dev/null +++ b/xbmc/pvr/guilib/GUIEPGGridContainer.h @@ -0,0 +1,274 @@ +/* + * 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 "guilib/DirtyRegion.h" +#include "guilib/GUIControl.h" +#include "guilib/GUIListItemLayout.h" +#include "guilib/GUITexture.h" +#include "guilib/IGUIContainer.h" +#include "threads/CriticalSection.h" +#include "utils/Geometry.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +class CDateTime; +class CFileItem; +class CFileItemList; +class CGUIListItem; +class CGUIListItemLayout; + +namespace PVR +{ + class CPVRChannel; + class CPVRChannelGroupMember; + class CPVRChannelNumber; + + class CGUIEPGGridContainerModel; + + class CGUIEPGGridContainer : public IGUIContainer + { + public: + CGUIEPGGridContainer(int parentID, int controlID, float posX, float posY, float width, float height, + ORIENTATION orientation, int scrollTime, int preloadItems, int minutesPerPage, + int rulerUnit, const CTextureInfo& progressIndicatorTexture); + CGUIEPGGridContainer(const CGUIEPGGridContainer& other); + + CGUIEPGGridContainer* Clone() const override { return new CGUIEPGGridContainer(*this); } + + /*! + * @brief Check whether the control currently holds data. + * @return true if the control has data, false otherwise. + */ + bool HasData() const; + + void AllocResources() override; + void FreeResources(bool immediately) override; + + bool OnAction(const CAction& action) override; + void OnDown() override; + void OnUp() override; + void OnLeft() override; + void OnRight() override; + bool OnMouseOver(const CPoint& point) override; + bool OnMessage(CGUIMessage& message) override; + void SetFocus(bool focus) override; + std::string GetDescription() const override; + EVENT_RESULT OnMouseEvent(const CPoint& point, const CMouseEvent& event) override; + + void Process(unsigned int currentTime, CDirtyRegionList& dirtyregions) override; + void Render() override; + + CGUIListItemPtr GetListItem(int offset, unsigned int flag = 0) const override; + std::string GetLabel(int info) const override; + + std::shared_ptr<CFileItem> GetSelectedGridItem(int offset = 0) const; + std::shared_ptr<CPVRChannelGroupMember> GetSelectedChannelGroupMember() const; + CDateTime GetSelectedDate() const; + + void LoadLayout(TiXmlElement* layout); + void SetPageControl(int id); + + /*! \brief Set the offset of the first item in the container from the container's position + Useful for lists/panels where the focused item may be larger than the non-focused items and thus + normally cut off from the clipping window defined by the container's position + size. + \param offset CPoint holding the offset in skin coordinates. + */ + void SetRenderOffset(const CPoint& offset); + + void JumpToNow(); + void JumpToDate(const CDateTime& date); + + void GoToBegin(); + void GoToEnd(); + void GoToNow(); + void GoToDate(const CDateTime& date); + + void GoToFirstChannel(); + void GoToLastChannel(); + + void GoToTop(); + void GoToBottom(); + void GoToMostLeft(); + void GoToMostRight(); + + void SetTimelineItems(const std::unique_ptr<CFileItemList>& items, + const CDateTime& gridStart, + const CDateTime& gridEnd); + + std::unique_ptr<CFileItemList> GetCurrentTimeLineItems() const; + + /*! + * @brief Set the control's selection to the given channel and set the control's view port to show the channel. + * @param channel the channel. + * @return true if the selection was set to the given channel, false otherwise. + */ + bool SetChannel(const std::shared_ptr<CPVRChannel>& channel); + + /*! + * @brief Set the control's selection to the given channel and set the control's view port to show the channel. + * @param channel the channel's path. + * @return true if the selection was set to the given channel, false otherwise. + */ + bool SetChannel(const std::string& channel); + + /*! + * @brief Set the control's selection to the given channel and set the control's view port to show the channel. + * @param channelNumber the channel's number. + * @return true if the selection was set to the given channel, false otherwise. + */ + bool SetChannel(const CPVRChannelNumber& channelNumber); + + private: + bool OnClick(int actionID); + bool SelectItemFromPoint(const CPoint& point, bool justGrid = true); + + void SetChannel(int channel); + + void SetBlock(int block, bool bUpdateBlockTravelAxis = true); + void UpdateBlock(bool bUpdateBlockTravelAxis = true); + + void ChannelScroll(int amount); + void ProgrammesScroll(int amount); + void ValidateOffset(); + void UpdateLayout(); + + void SetItem(const std::pair<std::shared_ptr<CFileItem>, int>& itemInfo); + bool SetItem(const std::shared_ptr<CFileItem>& item, int channelIndex, int blockIndex); + std::shared_ptr<CFileItem> GetItem() const; + std::pair<std::shared_ptr<CFileItem>, int> GetNextItem() const; + std::pair<std::shared_ptr<CFileItem>, int> GetPrevItem() const; + void UpdateItem(); + + void MoveToRow(int row); + + CGUIListItemLayout* GetFocusedLayout() const; + + void ScrollToBlockOffset(int offset); + void ScrollToChannelOffset(int offset); + void GoToBlock(int blockIndex); + void GoToChannel(int channelIndex); + void UpdateScrollOffset(unsigned int currentTime); + void ProcessItem(float posX, float posY, const std::shared_ptr<CFileItem>& item, std::shared_ptr<CFileItem>& lastitem, bool focused, CGUIListItemLayout* normallayout, CGUIListItemLayout* focusedlayout, unsigned int currentTime, CDirtyRegionList& dirtyregions, float resize = -1.0f); + void RenderItem(float posX, float posY, CGUIListItem* item, bool focused); + void GetCurrentLayouts(); + + void ProcessChannels(unsigned int currentTime, CDirtyRegionList& dirtyregions); + void ProcessRuler(unsigned int currentTime, CDirtyRegionList& dirtyregions); + void ProcessRulerDate(unsigned int currentTime, CDirtyRegionList& dirtyregions); + void ProcessProgrammeGrid(unsigned int currentTime, CDirtyRegionList& dirtyregions); + void ProcessProgressIndicator(unsigned int currentTime, CDirtyRegionList& dirtyregions); + void RenderChannels(); + void RenderRulerDate(); + void RenderRuler(); + void RenderProgrammeGrid(); + void RenderProgressIndicator(); + + CPoint m_renderOffset; ///< \brief render offset of the first item in the list \sa SetRenderOffset + + ORIENTATION m_orientation; + + std::vector<CGUIListItemLayout> m_channelLayouts; + std::vector<CGUIListItemLayout> m_focusedChannelLayouts; + std::vector<CGUIListItemLayout> m_focusedProgrammeLayouts; + std::vector<CGUIListItemLayout> m_programmeLayouts; + std::vector<CGUIListItemLayout> m_rulerLayouts; + std::vector<CGUIListItemLayout> m_rulerDateLayouts; + + CGUIListItemLayout* m_channelLayout; + CGUIListItemLayout* m_focusedChannelLayout; + CGUIListItemLayout* m_programmeLayout; + CGUIListItemLayout* m_focusedProgrammeLayout; + CGUIListItemLayout* m_rulerLayout; + CGUIListItemLayout* m_rulerDateLayout; + + int m_pageControl; + + void GetChannelCacheOffsets(int& cacheBefore, int& cacheAfter); + void GetProgrammeCacheOffsets(int& cacheBefore, int& cacheAfter); + + private: + bool OnMouseClick(int dwButton, const CPoint& point); + bool OnMouseDoubleClick(int dwButton, const CPoint& point); + bool OnMouseWheel(char wheel, const CPoint& point); + + void HandleChannels(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions); + void HandleRuler(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions); + void HandleRulerDate(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions); + void HandleProgrammeGrid(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions); + + float GetCurrentTimePositionOnPage() const; + float GetProgressIndicatorWidth() const; + float GetProgressIndicatorHeight() const; + + void UpdateItems(); + + float GetChannelScrollOffsetPos() const; + float GetProgrammeScrollOffsetPos() const; + int GetChannelScrollOffset(CGUIListItemLayout* layout) const; + int GetProgrammeScrollOffset() const; + + int m_rulerUnit; //! number of blocks that makes up one element of the ruler + int m_channelsPerPage; + int m_programmesPerPage; + int m_channelCursor; + int m_channelOffset; + int m_blocksPerPage; + int m_blockCursor; + int m_blockOffset; + int m_blockTravelAxis; + int m_cacheChannelItems; + int m_cacheProgrammeItems; + int m_cacheRulerItems; + + float m_rulerDateHeight; //! height of ruler date item + float m_rulerDateWidth; //! width of ruler date item + float m_rulerPosX; //! X position of first ruler item + float m_rulerPosY; //! Y position of first ruler item + float m_rulerHeight; //! height of the scrolling timeline above the ruler items + float m_rulerWidth; //! width of each element of the ruler + float m_channelPosX; //! X position of first channel row + float m_channelPosY; //! Y position of first channel row + float m_channelHeight; //! height of the channel item + float m_channelWidth; //! width of the channel item + float m_gridPosX; //! X position of first grid item + float m_gridPosY; //! Y position of first grid item + float m_gridWidth; //! width of the epg grid control + float m_gridHeight; //! height of the epg grid control + float m_blockSize; //! a block's width in pixels + float m_analogScrollCount; + + std::unique_ptr<CGUITexture> m_guiProgressIndicatorTexture; + + std::shared_ptr<CFileItem> m_lastItem; + std::shared_ptr<CFileItem> m_lastChannel; + + bool m_bEnableProgrammeScrolling = true; + bool m_bEnableChannelScrolling = true; + + int m_scrollTime; + + int m_programmeScrollLastTime; + float m_programmeScrollSpeed; + float m_programmeScrollOffset; + + int m_channelScrollLastTime; + float m_channelScrollSpeed; + float m_channelScrollOffset; + + mutable CCriticalSection m_critSection; + std::unique_ptr<CGUIEPGGridContainerModel> m_gridModel; + std::unique_ptr<CGUIEPGGridContainerModel> m_updatedGridModel; + + int m_itemStartBlock = 0; + }; +} diff --git a/xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp b/xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp new file mode 100644 index 0000000..e8fac01 --- /dev/null +++ b/xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp @@ -0,0 +1,723 @@ +/* + * 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 "GUIEPGGridContainerModel.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "pvr/PVRManager.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/epg/Epg.h" +#include "pvr/epg/EpgChannelData.h" +#include "pvr/epg/EpgInfoTag.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <cmath> +#include <iterator> +#include <memory> +#include <vector> + +using namespace PVR; + +static const unsigned int GRID_START_PADDING = 30; // minutes + +void CGUIEPGGridContainerModel::SetInvalid() +{ + for (const auto& gridItem : m_gridIndex) + gridItem.second.item->SetInvalid(); + for (const auto& channel : m_channelItems) + channel->SetInvalid(); + for (const auto& ruler : m_rulerItems) + ruler->SetInvalid(); +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::CreateGapItem(int iChannel) const +{ + const std::shared_ptr<CPVRChannel> channel = m_channelItems[iChannel]->GetPVRChannelInfoTag(); + const std::shared_ptr<CPVREpgInfoTag> gapTag = channel->CreateEPGGapTag(m_gridStart, m_gridEnd); + return std::make_shared<CFileItem>(gapTag); +} + +std::vector<std::shared_ptr<CPVREpgInfoTag>> CGUIEPGGridContainerModel::GetEPGTimeline( + int iChannel, const CDateTime& minEventEnd, const CDateTime& maxEventStart) const +{ + CDateTime min = minEventEnd - CDateTimeSpan(0, 0, MINSPERBLOCK, 0) + CDateTimeSpan(0, 0, 0, 1); + CDateTime max = maxEventStart + CDateTimeSpan(0, 0, MINSPERBLOCK, 0); + + if (min < m_gridStart) + min = m_gridStart; + + if (max > m_gridEnd) + max = m_gridEnd; + + return m_channelItems[iChannel]->GetPVRChannelInfoTag()->GetEPGTimeline(m_gridStart, m_gridEnd, + min, max); +} + +void CGUIEPGGridContainerModel::Initialize(const std::unique_ptr<CFileItemList>& items, + const CDateTime& gridStart, + const CDateTime& gridEnd, + int iFirstChannel, + int iChannelsPerPage, + int iFirstBlock, + int iBlocksPerPage, + int iRulerUnit, + float fBlockSize) +{ + if (!m_channelItems.empty()) + { + CLog::LogF(LOGERROR, "Already initialized!"); + return; + } + + m_fBlockSize = fBlockSize; + + //////////////////////////////////////////////////////////////////////// + // Create channel items + std::copy(items->cbegin(), items->cend(), std::back_inserter(m_channelItems)); + + /* check for invalid start and end time */ + if (gridStart >= gridEnd) + { + // default to start "now minus GRID_START_PADDING minutes" and end "start plus one page". + m_gridStart = CDateTime::GetUTCDateTime() - CDateTimeSpan(0, 0, GetGridStartPadding(), 0); + m_gridEnd = m_gridStart + CDateTimeSpan(0, 0, iBlocksPerPage * MINSPERBLOCK, 0); + } + else if (gridStart > + (CDateTime::GetUTCDateTime() - CDateTimeSpan(0, 0, GetGridStartPadding(), 0))) + { + // adjust to start "now minus GRID_START_PADDING minutes". + m_gridStart = CDateTime::GetUTCDateTime() - CDateTimeSpan(0, 0, GetGridStartPadding(), 0); + m_gridEnd = gridEnd; + } + else + { + m_gridStart = gridStart; + m_gridEnd = gridEnd; + } + + // roundup + m_gridStart = CDateTime(m_gridStart.GetYear(), m_gridStart.GetMonth(), m_gridStart.GetDay(), + m_gridStart.GetHour(), m_gridStart.GetMinute() >= 30 ? 30 : 0, 0); + m_gridEnd = CDateTime(m_gridEnd.GetYear(), m_gridEnd.GetMonth(), m_gridEnd.GetDay(), + m_gridEnd.GetHour(), m_gridEnd.GetMinute() >= 30 ? 30 : 0, 0); + + m_blocks = GetBlock(m_gridEnd) + 1; + + const int iBlocksLastPage = m_blocks % iBlocksPerPage; + if (iBlocksLastPage > 0) + { + m_gridEnd += CDateTimeSpan(0, 0, (iBlocksPerPage - iBlocksLastPage) * MINSPERBLOCK, 0); + m_blocks += (iBlocksPerPage - iBlocksLastPage); + } + + //////////////////////////////////////////////////////////////////////// + // Create ruler items + CDateTime ruler; + ruler.SetFromUTCDateTime(m_gridStart); + CDateTime rulerEnd; + rulerEnd.SetFromUTCDateTime(m_gridEnd); + CFileItemPtr rulerItem(new CFileItem(ruler.GetAsLocalizedDate(true))); + rulerItem->SetProperty("DateLabel", true); + m_rulerItems.emplace_back(rulerItem); + + const CDateTimeSpan unit(0, 0, iRulerUnit * MINSPERBLOCK, 0); + for (; ruler < rulerEnd; ruler += unit) + { + rulerItem.reset(new CFileItem(ruler.GetAsLocalizedTime("", false))); + rulerItem->SetLabel2(ruler.GetAsLocalizedDate(true)); + m_rulerItems.emplace_back(rulerItem); + } + + m_firstActiveChannel = iFirstChannel; + m_lastActiveChannel = iFirstChannel + iChannelsPerPage - 1; + m_firstActiveBlock = iFirstBlock; + m_lastActiveBlock = iFirstBlock + iBlocksPerPage - 1; +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::CreateEpgTags(int iChannel, int iBlock) const +{ + std::shared_ptr<CFileItem> result; + + const int firstBlock = iBlock < m_firstActiveBlock ? iBlock : m_firstActiveBlock; + const int lastBlock = iBlock > m_lastActiveBlock ? iBlock : m_lastActiveBlock; + + const auto tags = + GetEPGTimeline(iChannel, GetStartTimeForBlock(firstBlock), GetStartTimeForBlock(lastBlock)); + + const int firstResultBlock = GetFirstEventBlock(tags.front()); + const int lastResultBlock = GetLastEventBlock(tags.back()); + if (firstResultBlock > lastResultBlock) + return result; + + auto it = m_epgItems.insert({iChannel, EpgTags()}).first; + EpgTags& epgTags = (*it).second; + + epgTags.firstBlock = firstResultBlock; + epgTags.lastBlock = lastResultBlock; + + for (const auto& tag : tags) + { + if (GetFirstEventBlock(tag) > GetLastEventBlock(tag)) + continue; + + const std::shared_ptr<CFileItem> item = std::make_shared<CFileItem>(tag); + if (!result && IsEventMemberOfBlock(tag, iBlock)) + result = item; + + epgTags.tags.emplace_back(item); + } + + return result; +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetEpgTags(EpgTagsMap::iterator& itEpg, + int iChannel, + int iBlock) const +{ + std::shared_ptr<CFileItem> result; + + EpgTags& epgTags = (*itEpg).second; + + if (iBlock < epgTags.firstBlock) + { + result = GetEpgTagsBefore(epgTags, iChannel, iBlock); + } + else if (iBlock > epgTags.lastBlock) + { + result = GetEpgTagsAfter(epgTags, iChannel, iBlock); + } + else + { + const auto it = + std::find_if(epgTags.tags.cbegin(), epgTags.tags.cend(), [this, iBlock](const auto& item) { + return IsEventMemberOfBlock(item->GetEPGInfoTag(), iBlock); + }); + if (it != epgTags.tags.cend()) + result = (*it); + } + + return result; +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetEpgTagsBefore(EpgTags& epgTags, + int iChannel, + int iBlock) const +{ + std::shared_ptr<CFileItem> result; + + int lastBlock = epgTags.firstBlock - 1; + if (lastBlock < 0) + lastBlock = 0; + + const auto tags = + GetEPGTimeline(iChannel, GetStartTimeForBlock(iBlock), GetStartTimeForBlock(lastBlock)); + + if (epgTags.lastBlock == -1) + epgTags.lastBlock = lastBlock; + + if (tags.empty()) + { + epgTags.firstBlock = iBlock; + } + else + { + const int firstResultBlock = GetFirstEventBlock(tags.front()); + const int lastResultBlock = GetLastEventBlock(tags.back()); + if (firstResultBlock > lastResultBlock) + return result; + + // insert before the existing tags + epgTags.firstBlock = firstResultBlock; + + auto it = tags.crbegin(); + if (!epgTags.tags.empty()) + { + // ptr comp does not work for gap tags! + // if ((*it) == epgTags.tags.front()->GetEPGInfoTag()) + + const std::shared_ptr<CPVREpgInfoTag> t = epgTags.tags.front()->GetEPGInfoTag(); + if ((*it)->StartAsUTC() == t->StartAsUTC() && (*it)->EndAsUTC() == t->EndAsUTC()) + { + if (!result && IsEventMemberOfBlock(*it, iBlock)) + result = epgTags.tags.front(); + + ++it; // skip, because we already have that epg tag + } + } + + for (; it != tags.crend(); ++it) + { + if (GetFirstEventBlock(*it) > GetLastEventBlock(*it)) + continue; + + const std::shared_ptr<CFileItem> item = std::make_shared<CFileItem>(*it); + if (!result && IsEventMemberOfBlock(*it, iBlock)) + result = item; + + epgTags.tags.insert(epgTags.tags.begin(), item); + } + } + + return result; +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetEpgTagsAfter(EpgTags& epgTags, + int iChannel, + int iBlock) const +{ + std::shared_ptr<CFileItem> result; + + int firstBlock = epgTags.lastBlock + 1; + if (firstBlock >= GetLastBlock()) + firstBlock = GetLastBlock(); + + const auto tags = + GetEPGTimeline(iChannel, GetStartTimeForBlock(firstBlock), GetStartTimeForBlock(iBlock)); + + if (epgTags.firstBlock == -1) + epgTags.firstBlock = firstBlock; + + if (tags.empty()) + { + epgTags.lastBlock = iBlock; + } + else + { + const int firstResultBlock = GetFirstEventBlock(tags.front()); + const int lastResultBlock = GetLastEventBlock(tags.back()); + if (firstResultBlock > lastResultBlock) + return result; + + // append to the existing tags + epgTags.lastBlock = lastResultBlock; + + auto it = tags.cbegin(); + if (!epgTags.tags.empty()) + { + // ptr comp does not work for gap tags! + // if ((*it) == epgTags.tags.back()->GetEPGInfoTag()) + + const std::shared_ptr<CPVREpgInfoTag> t = epgTags.tags.back()->GetEPGInfoTag(); + if ((*it)->StartAsUTC() == t->StartAsUTC() && (*it)->EndAsUTC() == t->EndAsUTC()) + { + if (!result && IsEventMemberOfBlock(*it, iBlock)) + result = epgTags.tags.back(); + + ++it; // skip, because we already have that epg tag + } + } + + for (; it != tags.cend(); ++it) + { + if (GetFirstEventBlock(*it) > GetLastEventBlock(*it)) + continue; + + const std::shared_ptr<CFileItem> item = std::make_shared<CFileItem>(*it); + if (!result && IsEventMemberOfBlock(*it, iBlock)) + result = item; + + epgTags.tags.emplace_back(item); + } + } + + return result; +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetItem(int iChannel, int iBlock) const +{ + std::shared_ptr<CFileItem> result; + + auto itEpg = m_epgItems.find(iChannel); + if (itEpg == m_epgItems.end()) + { + result = CreateEpgTags(iChannel, iBlock); + } + else + { + result = GetEpgTags(itEpg, iChannel, iBlock); + } + + if (!result) + { + // Must never happen. if it does, fix the root cause, don't tolerate nullptr! + CLog::LogF(LOGERROR, "EPG tag ({}, {}) not found!", iChannel, iBlock); + } + + return result; +} + +void CGUIEPGGridContainerModel::FindChannelAndBlockIndex(int channelUid, + unsigned int broadcastUid, + int eventOffset, + int& newChannelIndex, + int& newBlockIndex) const +{ + newChannelIndex = INVALID_INDEX; + newBlockIndex = INVALID_INDEX; + + // find the new channel index + int iCurrentChannel = 0; + for (const auto& channel : m_channelItems) + { + if (channel->GetPVRChannelInfoTag()->UniqueID() == channelUid) + { + newChannelIndex = iCurrentChannel; + + // find the new block index + const std::shared_ptr<CPVREpg> epg = channel->GetPVRChannelInfoTag()->GetEPG(); + if (epg) + { + const std::shared_ptr<CPVREpgInfoTag> tag = epg->GetTagByBroadcastId(broadcastUid); + if (tag) + newBlockIndex = GetFirstEventBlock(tag) + eventOffset; + } + break; // done + } + iCurrentChannel++; + } +} + +GridItem* CGUIEPGGridContainerModel::GetGridItemPtr(int iChannel, int iBlock) const +{ + auto it = m_gridIndex.find({iChannel, iBlock}); + if (it == m_gridIndex.end()) + { + const CDateTime startTime = GetStartTimeForBlock(iBlock); + if (startTime < m_gridStart || m_gridEnd < startTime) + { + CLog::LogF(LOGERROR, "Requested EPG tag ({}, {}) outside grid boundaries!", iChannel, iBlock); + return nullptr; + } + + const std::shared_ptr<CFileItem> item = GetItem(iChannel, iBlock); + if (!item) + { + CLog::LogF(LOGERROR, "Got no EPG tag ({}, {})!", iChannel, iBlock); + return nullptr; + } + + const std::shared_ptr<CPVREpgInfoTag> epgTag = item->GetEPGInfoTag(); + + const int startBlock = GetFirstEventBlock(epgTag); + const int endBlock = GetLastEventBlock(epgTag); + + //! @todo it seems that this should be done somewhere else. CFileItem ctor maybe. + item->SetProperty("GenreType", epgTag->GenreType()); + + const float fItemWidth = (endBlock - startBlock + 1) * m_fBlockSize; + it = m_gridIndex.insert({{iChannel, iBlock}, {item, fItemWidth, startBlock, endBlock}}).first; + } + + return &(*it).second; +} + +bool CGUIEPGGridContainerModel::IsSameGridItem(int iChannel, int iBlock1, int iBlock2) const +{ + if (iBlock1 == iBlock2) + return true; + + const GridItem* item1 = GetGridItemPtr(iChannel, iBlock1); + const GridItem* item2 = GetGridItemPtr(iChannel, iBlock2); + + // compare the instances, not instance pointers, pointers are not unique. + return *item1 == *item2; +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetGridItem(int iChannel, int iBlock) const +{ + return GetGridItemPtr(iChannel, iBlock)->item; +} + +int CGUIEPGGridContainerModel::GetGridItemStartBlock(int iChannel, int iBlock) const +{ + return GetGridItemPtr(iChannel, iBlock)->startBlock; +} + +int CGUIEPGGridContainerModel::GetGridItemEndBlock(int iChannel, int iBlock) const +{ + return GetGridItemPtr(iChannel, iBlock)->endBlock; +} + +CDateTime CGUIEPGGridContainerModel::GetGridItemEndTime(int iChannel, int iBlock) const +{ + return GetGridItemPtr(iChannel, iBlock)->item->GetEPGInfoTag()->EndAsUTC(); +} + +float CGUIEPGGridContainerModel::GetGridItemWidth(int iChannel, int iBlock) const +{ + return GetGridItemPtr(iChannel, iBlock)->width; +} + +float CGUIEPGGridContainerModel::GetGridItemOriginWidth(int iChannel, int iBlock) const +{ + return GetGridItemPtr(iChannel, iBlock)->originWidth; +} + +void CGUIEPGGridContainerModel::DecreaseGridItemWidth(int iChannel, int iBlock, float fSize) +{ + auto it = m_gridIndex.find({iChannel, iBlock}); + if (it != m_gridIndex.end() && (*it).second.width != ((*it).second.originWidth - fSize)) + (*it).second.width = (*it).second.originWidth - fSize; +} + +unsigned int CGUIEPGGridContainerModel::GetGridStartPadding() const +{ + unsigned int iPastMinutes = + CServiceBroker::GetPVRManager().EpgContainer().GetPastDaysToDisplay() * 24 * 60; + + if (iPastMinutes < GRID_START_PADDING) + return iPastMinutes; + + return GRID_START_PADDING; // minutes +} + +void CGUIEPGGridContainerModel::FreeChannelMemory(int keepStart, int keepEnd) +{ + if (keepStart < keepEnd) + { + // remove before keepStart and after keepEnd + for (int i = 0; i < keepStart && i < ChannelItemsSize(); ++i) + m_channelItems[i]->FreeMemory(); + for (int i = keepEnd + 1; i < ChannelItemsSize(); ++i) + m_channelItems[i]->FreeMemory(); + } + else + { + // wrapping + for (int i = keepEnd + 1; i < keepStart && i < ChannelItemsSize(); ++i) + m_channelItems[i]->FreeMemory(); + } +} + +bool CGUIEPGGridContainerModel::FreeProgrammeMemory(int firstChannel, + int lastChannel, + int firstBlock, + int lastBlock) +{ + const bool channelsChanged = + (firstChannel != m_firstActiveChannel || lastChannel != m_lastActiveChannel); + const bool blocksChanged = (firstBlock != m_firstActiveBlock || lastBlock != m_lastActiveBlock); + if (!channelsChanged && !blocksChanged) + return false; + + // clear the grid. it will be recreated on-demand. + m_gridIndex.clear(); + + bool newChannels = false; + + if (channelsChanged) + { + // purge epg tags for inactive channels + for (auto it = m_epgItems.begin(); it != m_epgItems.end();) + { + if ((*it).first < firstChannel || (*it).first > lastChannel) + { + it = m_epgItems.erase(it); + continue; // next channel + } + ++it; + } + + newChannels = (firstChannel < m_firstActiveChannel) || (lastChannel > m_lastActiveChannel); + } + + if (blocksChanged || newChannels) + { + // clear and refetch epg tags for active channels + const CDateTime maxEnd = GetStartTimeForBlock(firstBlock); + const CDateTime minStart = GetStartTimeForBlock(lastBlock); + std::vector<std::shared_ptr<CPVREpgInfoTag>> tags; + for (int i = firstChannel; i <= lastChannel; ++i) + { + auto it = m_epgItems.find(i); + if (it == m_epgItems.end()) + it = m_epgItems.insert({i, EpgTags()}).first; + + if (blocksChanged || i < m_firstActiveChannel || i > m_lastActiveChannel) + { + EpgTags& epgTags = (*it).second; + + (*it).second.tags.clear(); + + tags = GetEPGTimeline(i, maxEnd, minStart); + const int firstResultBlock = GetFirstEventBlock(tags.front()); + const int lastResultBlock = GetLastEventBlock(tags.back()); + if (firstResultBlock > lastResultBlock) + continue; + + epgTags.firstBlock = firstResultBlock; + epgTags.lastBlock = lastResultBlock; + + for (const auto& tag : tags) + { + if (GetFirstEventBlock(tag) > GetLastEventBlock(tag)) + continue; + + epgTags.tags.emplace_back(std::make_shared<CFileItem>(tag)); + } + } + } + } + + m_firstActiveChannel = firstChannel; + m_lastActiveChannel = lastChannel; + m_firstActiveBlock = firstBlock; + m_lastActiveBlock = lastBlock; + + return true; +} + +void CGUIEPGGridContainerModel::FreeRulerMemory(int keepStart, int keepEnd) +{ + if (keepStart < keepEnd) + { + // remove before keepStart and after keepEnd + for (int i = 1; i < keepStart && i < RulerItemsSize(); ++i) + m_rulerItems[i]->FreeMemory(); + for (int i = keepEnd + 1; i < RulerItemsSize(); ++i) + m_rulerItems[i]->FreeMemory(); + } + else + { + // wrapping + for (int i = keepEnd + 1; i < keepStart && i < RulerItemsSize(); ++i) + { + if (i == 0) + continue; + + m_rulerItems[i]->FreeMemory(); + } + } +} + +unsigned int CGUIEPGGridContainerModel::GetPageNowOffset() const +{ + return GetGridStartPadding() / MINSPERBLOCK; // this is the 'now' block relative to page start +} + +CDateTime CGUIEPGGridContainerModel::GetStartTimeForBlock(int block) const +{ + if (block < 0) + block = 0; + else if (block >= GridItemsSize()) + block = GetLastBlock(); + + return m_gridStart + CDateTimeSpan(0, 0, block * MINSPERBLOCK, 0); +} + +int CGUIEPGGridContainerModel::GetBlock(const CDateTime& datetime) const +{ + int diff; + + if (m_gridStart == datetime) + return 0; // block is at grid start + else if (m_gridStart > datetime) + diff = -1 * (m_gridStart - datetime).GetSecondsTotal(); // block is before grid start + else + diff = (datetime - m_gridStart).GetSecondsTotal(); // block is after grid start + + // Note: Subtract 1 second from diff to ensure that events ending exactly at block boundary + // are unambiguous. Example: An event ending at 5:00:00 shall be mapped to block 9 and + // an event starting at 5:00:00 shall be mapped to block 10, not both at block 10. + // Only exception is grid end, because there is no successor. + if (datetime >= m_gridEnd) + return diff / 60 / MINSPERBLOCK; // block is equal or after grid end + else + return (diff - 1) / 60 / MINSPERBLOCK; +} + +int CGUIEPGGridContainerModel::GetNowBlock() const +{ + return GetBlock(CDateTime::GetUTCDateTime()) - GetPageNowOffset(); +} + +int CGUIEPGGridContainerModel::GetFirstEventBlock( + const std::shared_ptr<CPVREpgInfoTag>& event) const +{ + const CDateTime eventStart = event->StartAsUTC(); + int diff; + + if (m_gridStart == eventStart) + return 0; // block is at grid start + else if (m_gridStart > eventStart) + diff = -1 * (m_gridStart - eventStart).GetSecondsTotal(); + else + diff = (eventStart - m_gridStart).GetSecondsTotal(); + + // First block of a tag is always the block calculated using event's start time, rounded up. + float fBlockIndex = diff / 60.0f / MINSPERBLOCK; + return static_cast<int>(std::ceil(fBlockIndex)); +} + +int CGUIEPGGridContainerModel::GetLastEventBlock(const std::shared_ptr<CPVREpgInfoTag>& event) const +{ + // Last block of a tag is always the block calculated using event's end time, not rounded up. + return GetBlock(event->EndAsUTC()); +} + +bool CGUIEPGGridContainerModel::IsEventMemberOfBlock(const std::shared_ptr<CPVREpgInfoTag>& event, + int iBlock) const +{ + const int iFirstBlock = GetFirstEventBlock(event); + const int iLastBlock = GetLastEventBlock(event); + + if (iFirstBlock > iLastBlock) + { + return false; + } + else if (iFirstBlock == iBlock) + { + return true; + } + else if (iFirstBlock < iBlock) + { + return (iBlock <= iLastBlock); + } + return false; +} + +std::unique_ptr<CFileItemList> CGUIEPGGridContainerModel::GetCurrentTimeLineItems( + int firstChannel, int numChannels) const +{ + // Note: No need to keep this in a member. Gets generally not called multiple times for the + // same timeline, but content must be synced with m_epgItems, which changes quite often. + + std::unique_ptr<CFileItemList> items(new CFileItemList); + + if (numChannels > ChannelItemsSize()) + numChannels = ChannelItemsSize(); + + int i = 0; + for (int channel = firstChannel; channel < (firstChannel + numChannels); ++channel) + { + // m_epgItems is not sorted, fileitemlist must be sorted, so we have to 'find' the channel + const auto itEpg = m_epgItems.find(channel); + if (itEpg != m_epgItems.end()) + { + // tags are sorted, so we can iterate and append + for (const auto& tag : (*itEpg).second.tags) + { + tag->SetProperty("TimelineIndex", i); + items->Add(tag); + ++i; + } + } + else + { + // fake empty EPG + const std::shared_ptr<CFileItem> tag = CreateGapItem(channel); + tag->SetProperty("TimelineIndex", i); + items->Add(tag); + ++i; + } + } + return items; +} diff --git a/xbmc/pvr/guilib/GUIEPGGridContainerModel.h b/xbmc/pvr/guilib/GUIEPGGridContainerModel.h new file mode 100644 index 0000000..2e0a6d6 --- /dev/null +++ b/xbmc/pvr/guilib/GUIEPGGridContainerModel.h @@ -0,0 +1,178 @@ +/* + * 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 <functional> +#include <map> +#include <memory> +#include <unordered_map> +#include <utility> +#include <vector> + +class CFileItem; +class CFileItemList; + +namespace PVR +{ +struct GridItem +{ + GridItem(const std::shared_ptr<CFileItem>& _item, float _width, int _startBlock, int _endBlock) + : item(_item), originWidth(_width), width(_width), startBlock(_startBlock), endBlock(_endBlock) + { + } + + bool operator==(const GridItem& other) const + { + return (startBlock == other.startBlock && endBlock == other.endBlock); + } + + std::shared_ptr<CFileItem> item; + float originWidth = 0.0f; + float width = 0.0f; + int startBlock = 0; + int endBlock = 0; +}; + +class CPVREpgInfoTag; + +class CGUIEPGGridContainerModel +{ +public: + static constexpr int MINSPERBLOCK = 5; // minutes + + CGUIEPGGridContainerModel() = default; + virtual ~CGUIEPGGridContainerModel() = default; + + void Initialize(const std::unique_ptr<CFileItemList>& items, + const CDateTime& gridStart, + const CDateTime& gridEnd, + int iFirstChannel, + int iChannelsPerPage, + int iFirstBlock, + int iBlocksPerPage, + int iRulerUnit, + float fBlockSize); + void SetInvalid(); + + static const int INVALID_INDEX = -1; + void FindChannelAndBlockIndex(int channelUid, + unsigned int broadcastUid, + int eventOffset, + int& newChannelIndex, + int& newBlockIndex) const; + + void FreeChannelMemory(int keepStart, int keepEnd); + bool FreeProgrammeMemory(int firstChannel, int lastChannel, int firstBlock, int lastBlock); + void FreeRulerMemory(int keepStart, int keepEnd); + + std::shared_ptr<CFileItem> GetChannelItem(int iIndex) const { return m_channelItems[iIndex]; } + bool HasChannelItems() const { return !m_channelItems.empty(); } + int ChannelItemsSize() const { return static_cast<int>(m_channelItems.size()); } + int GetLastChannel() const + { + return m_channelItems.empty() ? -1 : static_cast<int>(m_channelItems.size()) - 1; + } + + std::shared_ptr<CFileItem> GetRulerItem(int iIndex) const { return m_rulerItems[iIndex]; } + int RulerItemsSize() const { return static_cast<int>(m_rulerItems.size()); } + + int GridItemsSize() const { return m_blocks; } + bool IsSameGridItem(int iChannel, int iBlock1, int iBlock2) const; + std::shared_ptr<CFileItem> GetGridItem(int iChannel, int iBlock) const; + int GetGridItemStartBlock(int iChannel, int iBlock) const; + int GetGridItemEndBlock(int iChannel, int iBlock) const; + CDateTime GetGridItemEndTime(int iChannel, int iBlock) const; + float GetGridItemWidth(int iChannel, int iBlock) const; + float GetGridItemOriginWidth(int iChannel, int iBlock) const; + void DecreaseGridItemWidth(int iChannel, int iBlock, float fSize); + + bool IsZeroGridDuration() const { return (m_gridEnd - m_gridStart) == CDateTimeSpan(0, 0, 0, 0); } + const CDateTime& GetGridStart() const { return m_gridStart; } + const CDateTime& GetGridEnd() const { return m_gridEnd; } + unsigned int GetGridStartPadding() const; + + unsigned int GetPageNowOffset() const; + int GetNowBlock() const; + int GetLastBlock() const { return m_blocks - 1; } + + CDateTime GetStartTimeForBlock(int block) const; + int GetBlock(const CDateTime& datetime) const; + int GetFirstEventBlock(const std::shared_ptr<CPVREpgInfoTag>& event) const; + int GetLastEventBlock(const std::shared_ptr<CPVREpgInfoTag>& event) const; + bool IsEventMemberOfBlock(const std::shared_ptr<CPVREpgInfoTag>& event, int iBlock) const; + + std::unique_ptr<CFileItemList> GetCurrentTimeLineItems(int firstChannel, int numChannels) const; + +private: + GridItem* GetGridItemPtr(int iChannel, int iBlock) const; + std::shared_ptr<CFileItem> CreateGapItem(int iChannel) const; + std::shared_ptr<CFileItem> GetItem(int iChannel, int iBlock) const; + + std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEPGTimeline(int iChannel, + const CDateTime& minEventEnd, + const CDateTime& maxEventStart) const; + + struct EpgTags + { + std::vector<std::shared_ptr<CFileItem>> tags; + int firstBlock = -1; + int lastBlock = -1; + }; + + using EpgTagsMap = std::unordered_map<int, EpgTags>; + + std::shared_ptr<CFileItem> CreateEpgTags(int iChannel, int iBlock) const; + std::shared_ptr<CFileItem> GetEpgTags(EpgTagsMap::iterator& itEpg, + int iChannel, + int iBlock) const; + std::shared_ptr<CFileItem> GetEpgTagsBefore(EpgTags& epgTags, int iChannel, int iBlock) const; + std::shared_ptr<CFileItem> GetEpgTagsAfter(EpgTags& epgTags, int iChannel, int iBlock) const; + + mutable EpgTagsMap m_epgItems; + + CDateTime m_gridStart; + CDateTime m_gridEnd; + + std::vector<std::shared_ptr<CFileItem>> m_channelItems; + std::vector<std::shared_ptr<CFileItem>> m_rulerItems; + + struct GridCoordinates + { + GridCoordinates(int _channel, int _block) : channel(_channel), block(_block) {} + + bool operator==(const GridCoordinates& other) const + { + return (channel == other.channel && block == other.block); + } + + int channel = 0; + int block = 0; + }; + + struct GridCoordinatesHash + { + std::size_t operator()(const GridCoordinates& coordinates) const + { + return std::hash<int>()(coordinates.channel) ^ std::hash<int>()(coordinates.block); + } + }; + + mutable std::unordered_map<GridCoordinates, GridItem, GridCoordinatesHash> m_gridIndex; + + int m_blocks = 0; + float m_fBlockSize = 0.0f; + + int m_firstActiveChannel = 0; + int m_lastActiveChannel = 0; + int m_firstActiveBlock = 0; + int m_lastActiveBlock = 0; +}; +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionListener.cpp b/xbmc/pvr/guilib/PVRGUIActionListener.cpp new file mode 100644 index 0000000..3e68386 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionListener.cpp @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIActionListener.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "application/Application.h" +#include "application/ApplicationActionListeners.h" +#include "application/ApplicationComponents.h" +#include "dialogs/GUIDialogNumeric.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/WindowIDs.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.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/guilib/PVRGUIActionsChannels.h" +#include "pvr/guilib/PVRGUIActionsClients.h" +#include "pvr/guilib/PVRGUIActionsDatabase.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "pvr/guilib/PVRGUIActionsTimers.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" + +#include <memory> +#include <string> + +namespace PVR +{ + +CPVRGUIActionListener::CPVRGUIActionListener() +{ + auto& components = CServiceBroker::GetAppComponents(); + const auto appListener = components.GetComponent<CApplicationActionListeners>(); + appListener->RegisterActionListener(this); + CServiceBroker::GetSettingsComponent()->GetSettings()->RegisterCallback( + this, + {CSettings::SETTING_PVRPARENTAL_ENABLED, CSettings::SETTING_PVRMANAGER_RESETDB, + CSettings::SETTING_EPG_RESETEPG, CSettings::SETTING_PVRMANAGER_ADDONS, + CSettings::SETTING_PVRMANAGER_CLIENTPRIORITIES, CSettings::SETTING_PVRMANAGER_CHANNELMANAGER, + CSettings::SETTING_PVRMANAGER_GROUPMANAGER, CSettings::SETTING_PVRMANAGER_CHANNELSCAN, + CSettings::SETTING_PVRMENU_SEARCHICONS, CSettings::SETTING_PVRCLIENT_MENUHOOK, + CSettings::SETTING_EPG_PAST_DAYSTODISPLAY, CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY}); +} + +CPVRGUIActionListener::~CPVRGUIActionListener() +{ + CServiceBroker::GetSettingsComponent()->GetSettings()->UnregisterCallback(this); + auto& components = CServiceBroker::GetAppComponents(); + const auto appListener = components.GetComponent<CApplicationActionListeners>(); + appListener->UnregisterActionListener(this); +} + +void CPVRGUIActionListener::Init(CPVRManager& mgr) +{ + mgr.Events().Subscribe(this, &CPVRGUIActionListener::OnPVRManagerEvent); +} + +void CPVRGUIActionListener::Deinit(CPVRManager& mgr) +{ + mgr.Events().Unsubscribe(this); +} + +void CPVRGUIActionListener::OnPVRManagerEvent(const PVREvent& event) +{ + if (event == PVREvent::AnnounceReminder) + { + if (g_application.IsInitialized()) + { + // if GUI is ready, dispatch to GUI thread and handle the action there + CServiceBroker::GetAppMessenger()->PostMsg( + TMSG_GUI_ACTION, WINDOW_INVALID, -1, + static_cast<void*>(new CAction(ACTION_PVR_ANNOUNCE_REMINDERS))); + } + } +} + +ChannelSwitchMode CPVRGUIActionListener::GetChannelSwitchMode(int iAction) +{ + if ((iAction == ACTION_MOVE_UP || iAction == ACTION_MOVE_DOWN) && + CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_PVRPLAYBACK_CONFIRMCHANNELSWITCH)) + return ChannelSwitchMode::NO_SWITCH; + + return ChannelSwitchMode::INSTANT_OR_DELAYED_SWITCH; +} + +bool CPVRGUIActionListener::OnAction(const CAction& action) +{ + bool bIsJumpSMS = false; + bool bIsPlayingPVR = CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying() && + g_application.CurrentFileItem().HasPVRChannelInfoTag(); + + switch (action.GetID()) + { + case ACTION_PVR_PLAY: + case ACTION_PVR_PLAY_TV: + case ACTION_PVR_PLAY_RADIO: + { + // see if we're already playing a PVR stream and if not or the stream type + // doesn't match the demanded type, start playback of according type + switch (action.GetID()) + { + case ACTION_PVR_PLAY: + if (!bIsPlayingPVR) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + PlaybackTypeAny); + break; + case ACTION_PVR_PLAY_TV: + if (!bIsPlayingPVR || g_application.CurrentFileItem().GetPVRChannelInfoTag()->IsRadio()) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + PlaybackTypeTV); + break; + case ACTION_PVR_PLAY_RADIO: + if (!bIsPlayingPVR || !g_application.CurrentFileItem().GetPVRChannelInfoTag()->IsRadio()) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + PlaybackTypeRadio); + break; + } + return true; + } + + case ACTION_JUMP_SMS2: + case ACTION_JUMP_SMS3: + case ACTION_JUMP_SMS4: + case ACTION_JUMP_SMS5: + case ACTION_JUMP_SMS6: + case ACTION_JUMP_SMS7: + case ACTION_JUMP_SMS8: + case ACTION_JUMP_SMS9: + bIsJumpSMS = true; + // fallthru is intended + [[fallthrough]]; + case REMOTE_0: + case REMOTE_1: + case REMOTE_2: + case REMOTE_3: + case REMOTE_4: + case REMOTE_5: + case REMOTE_6: + case REMOTE_7: + case REMOTE_8: + case REMOTE_9: + case ACTION_CHANNEL_NUMBER_SEP: + { + if (!bIsPlayingPVR) + return false; + + if (CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_FULLSCREEN_VIDEO) || + CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_VISUALISATION)) + { + // do not consume action if a python modal is the top most dialog + // as a python modal can't return that it consumed the action. + if (CServiceBroker::GetGUI()->GetWindowManager().IsPythonWindow( + CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog())) + return false; + + char cCharacter; + if (action.GetID() == ACTION_CHANNEL_NUMBER_SEP) + { + cCharacter = CPVRChannelNumber::SEPARATOR; + } + else + { + int iRemote = + bIsJumpSMS ? action.GetID() - (ACTION_JUMP_SMS2 - REMOTE_2) : action.GetID(); + cCharacter = static_cast<char>(iRemote - REMOTE_0) + '0'; + } + CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNumberInputHandler() + .AppendChannelNumberCharacter(cCharacter); + return true; + } + return false; + } + + case ACTION_SHOW_INFO: + { + if (!bIsPlayingPVR) + return false; + + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelNavigator().ToggleInfo(); + return true; + } + + case ACTION_SELECT_ITEM: + { + if (!bIsPlayingPVR) + return false; + + // If the button that caused this action matches action "Select" ... + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_PVRPLAYBACK_CONFIRMCHANNELSWITCH) && + CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNavigator() + .IsPreview()) + { + // ... and if "confirm channel switch" setting is active and a channel + // preview is currently shown, switch to the currently previewed channel. + CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNavigator() + .SwitchToCurrentChannel(); + return true; + } + else if (CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNumberInputHandler() + .CheckInputAndExecuteAction()) + { + // ... or if the action was processed by direct channel number input, we're done. + return true; + } + return false; + } + + case ACTION_NEXT_ITEM: + { + if (!bIsPlayingPVR) + return false; + + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SeekForward(); + return true; + } + + case ACTION_PREV_ITEM: + { + if (!bIsPlayingPVR) + return false; + + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SeekBackward( + CApplication::ACTION_PREV_ITEM_THRESHOLD); + return true; + } + + case ACTION_MOVE_UP: + case ACTION_CHANNEL_UP: + { + if (!bIsPlayingPVR) + return false; + + CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNavigator() + .SelectNextChannel(GetChannelSwitchMode(action.GetID())); + return true; + } + + case ACTION_MOVE_DOWN: + case ACTION_CHANNEL_DOWN: + { + if (!bIsPlayingPVR) + return false; + + CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNavigator() + .SelectPreviousChannel(GetChannelSwitchMode(action.GetID())); + return true; + } + + case ACTION_CHANNEL_SWITCH: + { + if (!bIsPlayingPVR) + return false; + + int iChannelNumber = static_cast<int>(action.GetAmount(0)); + int iSubChannelNumber = static_cast<int>(action.GetAmount(1)); + + const std::shared_ptr<CPVRPlaybackState> playbackState = + CServiceBroker::GetPVRManager().PlaybackState(); + const std::shared_ptr<CPVRChannelGroup> activeGroup = + playbackState->GetActiveChannelGroup(playbackState->IsPlayingRadio()); + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + activeGroup->GetByChannelNumber(CPVRChannelNumber(iChannelNumber, iSubChannelNumber)); + + if (!groupMember) + return false; + + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + CFileItem(groupMember), false); + return true; + } + + case ACTION_RECORD: + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleRecordingOnPlayingChannel(); + return true; + } + + case ACTION_PVR_ANNOUNCE_REMINDERS: + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AnnounceReminders(); + return true; + } + } + return false; +} + +void CPVRGUIActionListener::OnSettingChanged(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == nullptr) + return; + + const std::string& settingId = setting->GetId(); + if (settingId == CSettings::SETTING_PVRPARENTAL_ENABLED) + { + if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue() && + CServiceBroker::GetSettingsComponent() + ->GetSettings() + ->GetString(CSettings::SETTING_PVRPARENTAL_PIN) + .empty()) + { + std::string newPassword = ""; + // password set... save it + if (CGUIDialogNumeric::ShowAndVerifyNewPassword(newPassword)) + CServiceBroker::GetSettingsComponent()->GetSettings()->SetString( + CSettings::SETTING_PVRPARENTAL_PIN, newPassword); + // password not set... disable parental + else + std::static_pointer_cast<CSettingBool>(std::const_pointer_cast<CSetting>(setting)) + ->SetValue(false); + } + } + else if (settingId == CSettings::SETTING_EPG_PAST_DAYSTODISPLAY) + { + CServiceBroker::GetPVRManager().Clients()->SetEPGMaxPastDays( + std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + } + else if (settingId == CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY) + { + CServiceBroker::GetPVRManager().Clients()->SetEPGMaxFutureDays( + std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + } +} + +void CPVRGUIActionListener::OnSettingAction(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == nullptr) + return; + + const std::string& settingId = setting->GetId(); + if (settingId == CSettings::SETTING_PVRMANAGER_RESETDB) + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Database>().ResetDatabase(false); + } + else if (settingId == CSettings::SETTING_EPG_RESETEPG) + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Database>().ResetDatabase(true); + } + else if (settingId == CSettings::SETTING_PVRMANAGER_CLIENTPRIORITIES) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + CGUIDialog* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetDialog( + WINDOW_DIALOG_PVR_CLIENT_PRIORITIES); + if (dialog) + { + dialog->Open(); + CServiceBroker::GetPVRManager().ChannelGroups()->UpdateFromClients({}); + } + } + } + else if (settingId == CSettings::SETTING_PVRMANAGER_CHANNELMANAGER) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + CGUIDialog* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_CHANNEL_MANAGER); + if (dialog) + dialog->Open(); + } + } + else if (settingId == CSettings::SETTING_PVRMANAGER_GROUPMANAGER) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + CGUIDialog* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_GROUP_MANAGER); + if (dialog) + dialog->Open(); + } + } + else if (settingId == CSettings::SETTING_PVRMANAGER_CHANNELSCAN) + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().StartChannelScan(); + } + else if (settingId == CSettings::SETTING_PVRMENU_SEARCHICONS) + { + CServiceBroker::GetPVRManager().TriggerSearchMissingChannelIcons(); + } + else if (settingId == CSettings::SETTING_PVRCLIENT_MENUHOOK) + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Clients>().ProcessSettingsMenuHooks(); + } + else if (settingId == CSettings::SETTING_PVRMANAGER_ADDONS) + { + const std::vector<std::string> params{"addons://default_binary_addons_source/kodi.pvrclient", + "return"}; + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_ADDON_BROWSER, params); + } +} + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionListener.h b/xbmc/pvr/guilib/PVRGUIActionListener.h new file mode 100644 index 0000000..d24818b --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionListener.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/IActionListener.h" +#include "settings/lib/ISettingCallback.h" + +namespace PVR +{ + +class CPVRManager; +enum class ChannelSwitchMode; +enum class PVREvent; + +class CPVRGUIActionListener : public IActionListener, public ISettingCallback +{ +public: + CPVRGUIActionListener(); + ~CPVRGUIActionListener() override; + + void Init(CPVRManager& mgr); + void Deinit(CPVRManager& mgr); + + // IActionListener implementation + bool OnAction(const CAction& action) override; + + // ISettingCallback implementation + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override; + + void OnPVRManagerEvent(const PVREvent& event); + +private: + CPVRGUIActionListener(const CPVRGUIActionListener&) = delete; + CPVRGUIActionListener& operator=(const CPVRGUIActionListener&) = delete; + + static ChannelSwitchMode GetChannelSwitchMode(int iAction); +}; + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsChannels.cpp b/xbmc/pvr/guilib/PVRGUIActionsChannels.cpp new file mode 100644 index 0000000..80fb90e --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsChannels.cpp @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2016-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 "PVRGUIActionsChannels.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/WindowIDs.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/windows/GUIWindowPVRBase.h" +#include "settings/Settings.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <chrono> +#include <memory> +#include <mutex> +#include <string> +#include <vector> + +using namespace PVR; +using namespace KODI::MESSAGING; + +void CPVRChannelSwitchingInputHandler::AppendChannelNumberCharacter(char cCharacter) +{ + // special case. if only a single zero was typed in, switch to previously played channel. + if (GetCurrentDigitCount() == 0 && cCharacter == '0') + { + SwitchToPreviousChannel(); + return; + } + + CPVRChannelNumberInputHandler::AppendChannelNumberCharacter(cCharacter); +} + +void CPVRChannelSwitchingInputHandler::GetChannelNumbers(std::vector<std::string>& channelNumbers) +{ + const CPVRManager& pvrMgr = CServiceBroker::GetPVRManager(); + const std::shared_ptr<CPVRChannel> playingChannel = pvrMgr.PlaybackState()->GetPlayingChannel(); + if (playingChannel) + { + const std::shared_ptr<CPVRChannelGroup> group = + pvrMgr.ChannelGroups()->GetGroupAll(playingChannel->IsRadio()); + if (group) + group->GetChannelNumbers(channelNumbers); + } +} + +void CPVRChannelSwitchingInputHandler::OnInputDone() +{ + CPVRChannelNumber channelNumber = GetChannelNumber(); + if (channelNumber.GetChannelNumber()) + SwitchToChannel(channelNumber); +} + +void CPVRChannelSwitchingInputHandler::SwitchToChannel(const CPVRChannelNumber& channelNumber) +{ + if (channelNumber.IsValid() && CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying()) + { + const std::shared_ptr<CPVRChannel> playingChannel = + CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + if (playingChannel) + { + bool bRadio = playingChannel->IsRadio(); + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(bRadio); + + if (channelNumber != group->GetChannelNumber(playingChannel)) + { + // channel number present in active group? + std::shared_ptr<CPVRChannelGroupMember> groupMember = + group->GetByChannelNumber(channelNumber); + + if (!groupMember) + { + // channel number present in any group? + const CPVRChannelGroups* groupAccess = + CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio); + const std::vector<std::shared_ptr<CPVRChannelGroup>> groups = + groupAccess->GetMembers(true); + for (const auto& currentGroup : groups) + { + if (currentGroup == group) // we have already checked this group + continue; + + groupMember = currentGroup->GetByChannelNumber(channelNumber); + if (groupMember) + break; + } + } + + if (groupMember) + { + CServiceBroker::GetAppMessenger()->PostMsg( + TMSG_GUI_ACTION, WINDOW_INVALID, -1, + static_cast<void*>(new CAction( + ACTION_CHANNEL_SWITCH, static_cast<float>(channelNumber.GetChannelNumber()), + static_cast<float>(channelNumber.GetSubChannelNumber())))); + } + } + } + } +} + +void CPVRChannelSwitchingInputHandler::SwitchToPreviousChannel() +{ + const std::shared_ptr<CPVRPlaybackState> playbackState = + CServiceBroker::GetPVRManager().PlaybackState(); + if (playbackState->IsPlaying()) + { + const std::shared_ptr<CPVRChannel> playingChannel = playbackState->GetPlayingChannel(); + if (playingChannel) + { + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + playbackState->GetPreviousToLastPlayedChannelGroupMember(playingChannel->IsRadio()); + if (groupMember) + { + const CPVRChannelNumber channelNumber = groupMember->ChannelNumber(); + CServiceBroker::GetAppMessenger()->SendMsg( + TMSG_GUI_ACTION, WINDOW_INVALID, -1, + static_cast<void*>(new CAction( + ACTION_CHANNEL_SWITCH, static_cast<float>(channelNumber.GetChannelNumber()), + static_cast<float>(channelNumber.GetSubChannelNumber())))); + } + } + } +} + +CPVRGUIActionsChannels::CPVRGUIActionsChannels() + : m_settings({CSettings::SETTING_PVRMANAGER_PRESELECTPLAYINGCHANNEL}) +{ + RegisterChannelNumberInputHandler(&m_channelNumberInputHandler); +} + +CPVRGUIActionsChannels::~CPVRGUIActionsChannels() +{ + DeregisterChannelNumberInputHandler(&m_channelNumberInputHandler); +} + +void CPVRGUIActionsChannels::RegisterChannelNumberInputHandler( + CPVRChannelNumberInputHandler* handler) +{ + if (handler) + handler->Events().Subscribe(this, &CPVRGUIActionsChannels::Notify); +} + +void CPVRGUIActionsChannels::DeregisterChannelNumberInputHandler( + CPVRChannelNumberInputHandler* handler) +{ + if (handler) + handler->Events().Unsubscribe(this); +} + +void CPVRGUIActionsChannels::Notify(const PVRChannelNumberInputChangedEvent& event) +{ + m_events.Publish(event); +} + +bool CPVRGUIActionsChannels::HideChannel(const CFileItem& item) const +{ + const std::shared_ptr<CPVRChannel> channel = item.GetPVRChannelInfoTag(); + + if (!channel) + return false; + + if (!CGUIDialogYesNo::ShowAndGetInput( + CVariant{19054}, // "Hide channel" + CVariant{19039}, // "Are you sure you want to hide this channel?" + CVariant{""}, CVariant{channel->ChannelName()})) + return false; + + if (!CServiceBroker::GetPVRManager() + .ChannelGroups() + ->GetGroupAll(channel->IsRadio()) + ->RemoveFromGroup(channel)) + return false; + + CGUIWindowPVRBase* pvrWindow = + dynamic_cast<CGUIWindowPVRBase*>(CServiceBroker::GetGUI()->GetWindowManager().GetWindow( + CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow())); + if (pvrWindow) + pvrWindow->DoRefresh(); + else + CLog::LogF(LOGERROR, "Called on non-pvr window. No refresh possible."); + + return true; +} + +bool CPVRGUIActionsChannels::StartChannelScan() +{ + return StartChannelScan(PVR_INVALID_CLIENT_ID); +} + +bool CPVRGUIActionsChannels::StartChannelScan(int clientId) +{ + if (!CServiceBroker::GetPVRManager().IsStarted() || IsRunningChannelScan()) + return false; + + std::shared_ptr<CPVRClient> scanClient; + std::vector<std::shared_ptr<CPVRClient>> possibleScanClients = + CServiceBroker::GetPVRManager().Clients()->GetClientsSupportingChannelScan(); + m_bChannelScanRunning = true; + + if (clientId != PVR_INVALID_CLIENT_ID) + { + const auto it = + std::find_if(possibleScanClients.cbegin(), possibleScanClients.cend(), + [clientId](const auto& client) { return client->GetID() == clientId; }); + + if (it != possibleScanClients.cend()) + scanClient = (*it); + + if (!scanClient) + { + CLog::LogF(LOGERROR, + "Provided client id '{}' could not be found in list of possible scan clients!", + clientId); + m_bChannelScanRunning = false; + return false; + } + } + /* multiple clients found */ + else if (possibleScanClients.size() > 1) + { + CGUIDialogSelect* pDialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + if (!pDialog) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT!"); + m_bChannelScanRunning = false; + return false; + } + + pDialog->Reset(); + pDialog->SetHeading(CVariant{19119}); // "On which backend do you want to search?" + + for (const auto& client : possibleScanClients) + pDialog->Add(client->GetFriendlyName()); + + pDialog->Open(); + + int selection = pDialog->GetSelectedItem(); + if (selection >= 0) + scanClient = possibleScanClients[selection]; + } + /* one client found */ + else if (possibleScanClients.size() == 1) + { + scanClient = possibleScanClients[0]; + } + /* no clients found */ + else if (!scanClient) + { + HELPERS::ShowOKDialogText( + CVariant{19033}, // "Information" + CVariant{19192}); // "None of the connected PVR backends supports scanning for channels." + m_bChannelScanRunning = false; + return false; + } + + /* start the channel scan */ + CLog::LogFC(LOGDEBUG, LOGPVR, "Starting to scan for channels on client {}", + scanClient->GetFriendlyName()); + auto start = std::chrono::steady_clock::now(); + + /* do the scan */ + if (scanClient->StartChannelScan() != PVR_ERROR_NO_ERROR) + HELPERS::ShowOKDialogText( + CVariant{257}, // "Error" + CVariant{ + 19193}); // "The channel scan can't be started. Check the log for more information about this message." + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Channel scan finished after {} ms", duration.count()); + + m_bChannelScanRunning = false; + return true; +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRGUIActionsChannels::GetChannelGroupMember( + const std::shared_ptr<CPVRChannel>& channel) const +{ + if (!channel) + return {}; + + std::shared_ptr<CPVRChannelGroupMember> groupMember; + + // first, try whether the channel is contained in the active channel group, except + // if a window is active which never uses the active channel group, e.g. Timers window + const int activeWindowID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(); + + static std::vector<int> windowIDs = { + WINDOW_TV_RECORDINGS, WINDOW_TV_TIMERS, WINDOW_TV_TIMER_RULES, WINDOW_TV_SEARCH, + WINDOW_RADIO_RECORDINGS, WINDOW_RADIO_TIMERS, WINDOW_RADIO_TIMER_RULES, WINDOW_RADIO_SEARCH, + }; + + if (std::find(windowIDs.cbegin(), windowIDs.cend(), activeWindowID) == windowIDs.cend()) + { + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(channel->IsRadio()); + if (group) + groupMember = group->GetByUniqueID(channel->StorageId()); + } + + // as fallback, obtain the member from the 'all channels' group + if (!groupMember) + { + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(channel->IsRadio()); + if (group) + groupMember = group->GetByUniqueID(channel->StorageId()); + } + + return groupMember; +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRGUIActionsChannels::GetChannelGroupMember( + const CFileItem& item) const +{ + std::shared_ptr<CPVRChannelGroupMember> groupMember = item.GetPVRChannelGroupMemberInfoTag(); + + if (!groupMember) + groupMember = GetChannelGroupMember(CPVRItem(std::make_shared<CFileItem>(item)).GetChannel()); + + return groupMember; +} + +CPVRChannelNumberInputHandler& CPVRGUIActionsChannels::GetChannelNumberInputHandler() +{ + // window/dialog specific input handler + CPVRChannelNumberInputHandler* windowInputHandler = dynamic_cast<CPVRChannelNumberInputHandler*>( + CServiceBroker::GetGUI()->GetWindowManager().GetWindow( + CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog())); + if (windowInputHandler) + return *windowInputHandler; + + // default + return m_channelNumberInputHandler; +} + +CPVRGUIChannelNavigator& CPVRGUIActionsChannels::GetChannelNavigator() +{ + return m_channelNavigator; +} + +void CPVRGUIActionsChannels::OnPlaybackStarted(const CFileItem& item) +{ + const std::shared_ptr<CPVRChannelGroupMember> groupMember = GetChannelGroupMember(item); + if (groupMember) + { + m_channelNavigator.SetPlayingChannel(groupMember); + SetSelectedChannelPath(groupMember->Channel()->IsRadio(), groupMember->Path()); + } +} + +void CPVRGUIActionsChannels::OnPlaybackStopped(const CFileItem& item) +{ + if (item.HasPVRChannelInfoTag() || item.HasEPGInfoTag()) + { + m_channelNavigator.ClearPlayingChannel(); + } +} + +void CPVRGUIActionsChannels::SetSelectedChannelPath(bool bRadio, const std::string& path) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (bRadio) + m_selectedChannelPathRadio = path; + else + m_selectedChannelPathTV = path; +} + +std::string CPVRGUIActionsChannels::GetSelectedChannelPath(bool bRadio) const +{ + if (m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_PRESELECTPLAYINGCHANNEL)) + { + CPVRManager& mgr = CServiceBroker::GetPVRManager(); + + // if preselect playing channel is activated, return the path of the playing channel, if any. + const std::shared_ptr<CPVRChannelGroupMember> playingChannel = + mgr.PlaybackState()->GetPlayingChannelGroupMember(); + if (playingChannel && playingChannel->IsRadio() == bRadio) + return playingChannel->Path(); + + const std::shared_ptr<CPVREpgInfoTag> playingTag = mgr.PlaybackState()->GetPlayingEpgTag(); + if (playingTag && playingTag->IsRadio() == bRadio) + { + const std::shared_ptr<CPVRChannel> channel = + mgr.ChannelGroups()->GetChannelForEpgTag(playingTag); + if (channel) + return GetChannelGroupMember(channel)->Path(); + } + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + return bRadio ? m_selectedChannelPathRadio : m_selectedChannelPathTV; +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsChannels.h b/xbmc/pvr/guilib/PVRGUIActionsChannels.h new file mode 100644 index 0000000..a7656fe --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsChannels.h @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" +#include "pvr/PVRChannelNumberInputHandler.h" +#include "pvr/guilib/PVRGUIChannelNavigator.h" +#include "pvr/settings/PVRSettings.h" +#include "threads/CriticalSection.h" + +#include <memory> +#include <string> +#include <vector> + +class CFileItem; + +namespace PVR +{ +class CPVRChannel; +class CPVRChannelGroupMember; + +class CPVRChannelSwitchingInputHandler : public CPVRChannelNumberInputHandler +{ +public: + // CPVRChannelNumberInputHandler implementation + void GetChannelNumbers(std::vector<std::string>& channelNumbers) override; + void AppendChannelNumberCharacter(char cCharacter) override; + void OnInputDone() override; + +private: + /*! + * @brief Switch to the channel with the given number. + * @param channelNumber the channel number + */ + void SwitchToChannel(const CPVRChannelNumber& channelNumber); + + /*! + * @brief Switch to the previously played channel. + */ + void SwitchToPreviousChannel(); +}; + +class CPVRGUIActionsChannels : public IPVRComponent +{ +public: + CPVRGUIActionsChannels(); + ~CPVRGUIActionsChannels() override; + + /*! + * @brief Get the events available for CEventStream. + * @return The events. + */ + CEventStream<PVRChannelNumberInputChangedEvent>& Events() { return m_events; } + + /*! + * @brief Register a handler for channel number input. + * @param handler The handler to register. + */ + void RegisterChannelNumberInputHandler(CPVRChannelNumberInputHandler* handler); + + /*! + * @brief Deregister a handler for channel number input. + * @param handler The handler to deregister. + */ + void DeregisterChannelNumberInputHandler(CPVRChannelNumberInputHandler* handler); + + /*! + * @brief CEventStream callback for channel number input changes. + * @param event The event. + */ + void Notify(const PVRChannelNumberInputChangedEvent& event); + + /*! + * @brief Hide a channel, always showing a confirmation dialog. + * @param item containing a channel or an epg tag. + * @return true on success, false otherwise. + */ + bool HideChannel(const CFileItem& item) const; + + /*! + * @brief Open a selection dialog and start a channel scan on the selected client. + * @return true on success, false otherwise. + */ + bool StartChannelScan(); + + /*! + * @brief Start a channel scan on the specified client or open a dialog to select a client + * @param clientId the id of client to scan or PVR_INVALID_CLIENT_ID if a dialog will be opened + * @return true on success, false otherwise. + */ + bool StartChannelScan(int clientId); + + /*! + * @return True when a channel scan is currently running, false otherwise. + */ + bool IsRunningChannelScan() const { return m_bChannelScanRunning; } + + /*! + * @brief Get a channel group member for the given channel, either from the currently active + * group or if not found there, from the 'all channels' group. + * @param channel the channel. + * @return the group member or nullptr if not found. + */ + std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMember( + const std::shared_ptr<CPVRChannel>& channel) const; + + /*! + * @brief Get a channel group member for the given item, either from the currently active group + * or if not found there, from the 'all channels' group. + * @param item the item containing a channel, channel group, recording, timer or epg tag. + * @return the group member or nullptr if not found. + */ + std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMember(const CFileItem& item) const; + + /*! + * @brief Get the currently active channel number input handler. + * @return the handler. + */ + CPVRChannelNumberInputHandler& GetChannelNumberInputHandler(); + + /*! + * @brief Get the channel navigator. + * @return the navigator. + */ + CPVRGUIChannelNavigator& GetChannelNavigator(); + + /*! + * @brief Inform GUI actions that playback of an item just started. + * @param item The item that started to play. + */ + void OnPlaybackStarted(const CFileItem& item); + + /*! + * @brief Inform GUI actions that playback of an item was stopped due to user interaction. + * @param item The item that stopped to play. + */ + void OnPlaybackStopped(const CFileItem& item); + + /*! + * @brief Get the currently selected channel item path; used across several windows/dialogs to + * share item selection. + * @param bRadio True to query the selected path for PVR radio, false for Live TV. + * @return the path. + */ + std::string GetSelectedChannelPath(bool bRadio) const; + + /*! + * @brief Set the currently selected channel item path; used across several windows/dialogs to + * share item selection. + * @param bRadio True to set the selected path for PVR radio, false for Live TV. + * @param path The new path to set. + */ + void SetSelectedChannelPath(bool bRadio, const std::string& path); + +private: + CPVRGUIActionsChannels(const CPVRGUIActionsChannels&) = delete; + CPVRGUIActionsChannels const& operator=(CPVRGUIActionsChannels const&) = delete; + + CPVRChannelSwitchingInputHandler m_channelNumberInputHandler; + bool m_bChannelScanRunning{false}; + CPVRGUIChannelNavigator m_channelNavigator; + CEventSource<PVRChannelNumberInputChangedEvent> m_events; + + mutable CCriticalSection m_critSection; + CPVRSettings m_settings; + std::string m_selectedChannelPathTV; + std::string m_selectedChannelPathRadio; +}; + +namespace GUI +{ +// pretty scope and name +using Channels = CPVRGUIActionsChannels; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsClients.cpp b/xbmc/pvr/guilib/PVRGUIActionsClients.cpp new file mode 100644 index 0000000..b9c598c --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsClients.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2016-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 "PVRGUIActionsClients.h" + +#include "ServiceBroker.h" +#include "dialogs/GUIDialogSelect.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/WindowIDs.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClientMenuHooks.h" +#include "pvr/addons/PVRClients.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <iterator> +#include <memory> +#include <utility> +#include <vector> + +using namespace KODI::MESSAGING; + +using namespace PVR; + +bool CPVRGUIActionsClients::ProcessSettingsMenuHooks() +{ + const CPVRClientMap clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients(); + + std::vector<std::pair<std::shared_ptr<CPVRClient>, CPVRClientMenuHook>> settingsHooks; + for (const auto& client : clients) + { + const auto hooks = client.second->GetMenuHooks()->GetSettingsHooks(); + std::transform(hooks.cbegin(), hooks.cend(), std::back_inserter(settingsHooks), + [&client](const auto& hook) { return std::make_pair(client.second, hook); }); + } + + if (settingsHooks.empty()) + { + HELPERS::ShowOKDialogText( + CVariant{19033}, // "Information" + CVariant{19347}); // "None of the active PVR clients does provide client-specific settings." + return true; // no settings hooks, no error + } + + auto selectedHook = settingsHooks.begin(); + + // if there is only one settings hook, execute it directly, otherwise let the user select + if (settingsHooks.size() > 1) + { + CGUIDialogSelect* pDialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + if (!pDialog) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT!"); + return false; + } + + pDialog->Reset(); + pDialog->SetHeading(CVariant{19196}); // "PVR client specific actions" + + for (const auto& hook : settingsHooks) + { + if (clients.size() == 1) + pDialog->Add(hook.second.GetLabel()); + else + pDialog->Add(hook.first->GetFriendlyName() + ": " + hook.second.GetLabel()); + } + + pDialog->Open(); + + int selection = pDialog->GetSelectedItem(); + if (selection < 0) + return true; // cancelled + + std::advance(selectedHook, selection); + } + return selectedHook->first->CallSettingsMenuHook(selectedHook->second) == PVR_ERROR_NO_ERROR; +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsClients.h b/xbmc/pvr/guilib/PVRGUIActionsClients.h new file mode 100644 index 0000000..7c48274 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsClients.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" + +namespace PVR +{ +class CPVRGUIActionsClients : public IPVRComponent +{ +public: + CPVRGUIActionsClients() = default; + ~CPVRGUIActionsClients() override = default; + + /*! + * @brief Select and invoke client-specific settings actions + * @return true on success, false otherwise. + */ + bool ProcessSettingsMenuHooks(); + +private: + CPVRGUIActionsClients(const CPVRGUIActionsClients&) = delete; + CPVRGUIActionsClients const& operator=(CPVRGUIActionsClients const&) = delete; +}; + +namespace GUI +{ +// pretty scope and name +using Clients = CPVRGUIActionsClients; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp b/xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp new file mode 100644 index 0000000..53bbf81 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2016-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 "PVRGUIActionsDatabase.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogProgress.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/epg/EpgDatabase.h" +#include "pvr/guilib/PVRGUIActionsParentalControl.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/recordings/PVRRecordingsPath.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" + +#include <memory> +#include <string> + +using namespace PVR; + +namespace +{ +class CPVRGUIDatabaseResetComponentsSelector +{ +public: + CPVRGUIDatabaseResetComponentsSelector() = default; + virtual ~CPVRGUIDatabaseResetComponentsSelector() = default; + + bool Select() + { + CGUIDialogSelect* pDlgSelect = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + if (!pDlgSelect) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT!"); + return false; + } + + CFileItemList options; + + const std::shared_ptr<CFileItem> itemAll = + std::make_shared<CFileItem>(StringUtils::Format(g_localizeStrings.Get(593))); // All + itemAll->SetPath("all"); + options.Add(itemAll); + + // if channels are cleared, groups, EPG data and providers must also be cleared + const std::shared_ptr<CFileItem> itemChannels = + std::make_shared<CFileItem>(StringUtils::Format("{}, {}, {}, {}", + g_localizeStrings.Get(19019), // Channels + g_localizeStrings.Get(19146), // Groups + g_localizeStrings.Get(19069), // Guide + g_localizeStrings.Get(19334))); // Providers + itemChannels->SetPath("channels"); + itemChannels->Select(true); // preselect this item in dialog + options.Add(itemChannels); + + const std::shared_ptr<CFileItem> itemGroups = + std::make_shared<CFileItem>(g_localizeStrings.Get(19146)); // Groups + itemGroups->SetPath("groups"); + options.Add(itemGroups); + + const std::shared_ptr<CFileItem> itemGuide = + std::make_shared<CFileItem>(g_localizeStrings.Get(19069)); // Guide + itemGuide->SetPath("guide"); + options.Add(itemGuide); + + const std::shared_ptr<CFileItem> itemProviders = + std::make_shared<CFileItem>(g_localizeStrings.Get(19334)); // Providers + itemProviders->SetPath("providers"); + options.Add(itemProviders); + + const std::shared_ptr<CFileItem> itemReminders = + std::make_shared<CFileItem>(g_localizeStrings.Get(19215)); // Reminders + itemReminders->SetPath("reminders"); + options.Add(itemReminders); + + const std::shared_ptr<CFileItem> itemRecordings = + std::make_shared<CFileItem>(g_localizeStrings.Get(19017)); // Recordings + itemRecordings->SetPath("recordings"); + options.Add(itemRecordings); + + const std::shared_ptr<CFileItem> itemClients = + std::make_shared<CFileItem>(g_localizeStrings.Get(24019)); // PVR clients + itemClients->SetPath("clients"); + options.Add(itemClients); + + pDlgSelect->Reset(); + pDlgSelect->SetHeading(CVariant{g_localizeStrings.Get(19185)}); // "Clear data" + pDlgSelect->SetItems(options); + pDlgSelect->SetMultiSelection(true); + pDlgSelect->Open(); + + if (!pDlgSelect->IsConfirmed()) + return false; + + for (int i : pDlgSelect->GetSelectedItems()) + { + const std::string path = options.Get(i)->GetPath(); + + m_bResetChannels |= (path == "channels" || path == "all"); + m_bResetGroups |= (path == "groups" || path == "all"); + m_bResetGuide |= (path == "guide" || path == "all"); + m_bResetProviders |= (path == "providers" || path == "all"); + m_bResetReminders |= (path == "reminders" || path == "all"); + m_bResetRecordings |= (path == "recordings" || path == "all"); + m_bResetClients |= (path == "clients" || path == "all"); + } + + m_bResetGroups |= m_bResetChannels; + m_bResetGuide |= m_bResetChannels; + m_bResetProviders |= m_bResetChannels; + + return (m_bResetChannels || m_bResetGroups || m_bResetGuide || m_bResetProviders || + m_bResetReminders || m_bResetRecordings || m_bResetClients); + } + + bool IsResetChannelsSelected() const { return m_bResetChannels; } + bool IsResetGroupsSelected() const { return m_bResetGroups; } + bool IsResetGuideSelected() const { return m_bResetGuide; } + bool IsResetProvidersSelected() const { return m_bResetProviders; } + bool IsResetRemindersSelected() const { return m_bResetReminders; } + bool IsResetRecordingsSelected() const { return m_bResetRecordings; } + bool IsResetClientsSelected() const { return m_bResetClients; } + +private: + bool m_bResetChannels = false; + bool m_bResetGroups = false; + bool m_bResetGuide = false; + bool m_bResetProviders = false; + bool m_bResetReminders = false; + bool m_bResetRecordings = false; + bool m_bResetClients = false; +}; + +} // unnamed namespace + +bool CPVRGUIActionsDatabase::ResetDatabase(bool bResetEPGOnly) +{ + CGUIDialogProgress* pDlgProgress = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>( + WINDOW_DIALOG_PROGRESS); + if (!pDlgProgress) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PROGRESS!"); + return false; + } + + bool bResetChannels = false; + bool bResetGroups = false; + bool bResetGuide = false; + bool bResetProviders = false; + bool bResetReminders = false; + bool bResetRecordings = false; + bool bResetClients = false; + + if (bResetEPGOnly) + { + if (!CGUIDialogYesNo::ShowAndGetInput( + CVariant{19098}, // "Warning!" + CVariant{19188})) // "All guide data will be cleared. Are you sure?" + return false; + + bResetGuide = true; + } + else + { + if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalPIN() != + ParentalCheckResult::SUCCESS) + return false; + + CPVRGUIDatabaseResetComponentsSelector selector; + if (!selector.Select()) + return false; + + if (!CGUIDialogYesNo::ShowAndGetInput( + CVariant{19098}, // "Warning!" + CVariant{19186})) // "All selected data will be cleared. ... Are you sure?" + return false; + + bResetChannels = selector.IsResetChannelsSelected(); + bResetGroups = selector.IsResetGroupsSelected(); + bResetGuide = selector.IsResetGuideSelected(); + bResetProviders = selector.IsResetProvidersSelected(); + bResetReminders = selector.IsResetRemindersSelected(); + bResetRecordings = selector.IsResetRecordingsSelected(); + bResetClients = selector.IsResetClientsSelected(); + } + + CDateTime::ResetTimezoneBias(); + + CLog::LogFC(LOGDEBUG, LOGPVR, "PVR clearing {} database", bResetEPGOnly ? "EPG" : "PVR and EPG"); + + pDlgProgress->SetHeading(CVariant{313}); // "Cleaning database" + pDlgProgress->SetLine(0, CVariant{g_localizeStrings.Get(19187)}); // "Clearing all related data." + pDlgProgress->SetLine(1, CVariant{""}); + pDlgProgress->SetLine(2, CVariant{""}); + + pDlgProgress->Open(); + pDlgProgress->Progress(); + + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying()) + { + CLog::Log(LOGINFO, "PVR is stopping playback for {} database reset", + bResetEPGOnly ? "EPG" : "PVR and EPG"); + CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP); + } + + const std::shared_ptr<CPVRDatabase> pvrDatabase(CServiceBroker::GetPVRManager().GetTVDatabase()); + const std::shared_ptr<CPVREpgDatabase> epgDatabase( + CServiceBroker::GetPVRManager().EpgContainer().GetEpgDatabase()); + + // increase db open refcounts, so they don't get closed during following pvr manager shutdown + pvrDatabase->Open(); + epgDatabase->Open(); + + // stop pvr manager; close both pvr and epg databases + CServiceBroker::GetPVRManager().Stop(); + + const int iProgressStepPercentage = + 100 / ((2 * bResetChannels) + bResetGroups + bResetGuide + bResetProviders + bResetReminders + + bResetRecordings + bResetClients + 1); + int iProgressStepsDone = 0; + + if (bResetProviders) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all providers + pvrDatabase->DeleteProviders(); + } + + if (bResetGuide) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // reset channel's EPG pointers + pvrDatabase->ResetEPG(); + + // delete all entries from the EPG database + epgDatabase->DeleteEpg(); + } + + if (bResetGroups) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all channel groups (including data only available locally, like user defined groups) + pvrDatabase->DeleteChannelGroups(); + } + + if (bResetChannels) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all channels (including data only available locally, like user set icons) + pvrDatabase->DeleteChannels(); + } + + if (bResetReminders) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all timers data (e.g. all reminders, which are only stored locally) + pvrDatabase->DeleteTimers(); + } + + if (bResetClients) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all clients data (e.g priorities, which are only stored locally) + pvrDatabase->DeleteClients(); + } + + if (bResetChannels || bResetRecordings) + { + CVideoDatabase videoDatabase; + + if (videoDatabase.Open()) + { + if (bResetChannels) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all channel's entries (e.g. settings, bookmarks, stream details) + videoDatabase.EraseAllForPath("pvr://channels/"); + } + + if (bResetRecordings) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all recording's entries (e.g. settings, bookmarks, stream details) + videoDatabase.EraseAllForPath(CPVRRecordingsPath::PATH_RECORDINGS); + } + + videoDatabase.Close(); + } + } + + // decrease db open refcounts; this actually closes dbs because refcounts drops to zero + pvrDatabase->Close(); + epgDatabase->Close(); + + CLog::LogFC(LOGDEBUG, LOGPVR, "{} database cleared", bResetEPGOnly ? "EPG" : "PVR and EPG"); + + CLog::Log(LOGINFO, "Restarting the PVR Manager after {} database reset", + bResetEPGOnly ? "EPG" : "PVR and EPG"); + CServiceBroker::GetPVRManager().Start(); + + pDlgProgress->SetPercentage(100); + pDlgProgress->Close(); + return true; +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsDatabase.h b/xbmc/pvr/guilib/PVRGUIActionsDatabase.h new file mode 100644 index 0000000..62eb484 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsDatabase.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" + +namespace PVR +{ +class CPVRGUIActionsDatabase : public IPVRComponent +{ +public: + CPVRGUIActionsDatabase() = default; + ~CPVRGUIActionsDatabase() override = default; + + /*! + * @brief Reset the TV database to it's initial state and delete all the data. + * @param bResetEPGOnly True to only reset the EPG database, false to reset both PVR and EPG + * database. + * @return true on success, false otherwise. + */ + bool ResetDatabase(bool bResetEPGOnly); + +private: + CPVRGUIActionsDatabase(const CPVRGUIActionsDatabase&) = delete; + CPVRGUIActionsDatabase const& operator=(CPVRGUIActionsDatabase const&) = delete; +}; + +namespace GUI +{ +// pretty scope and name +using Database = CPVRGUIActionsDatabase; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsEPG.cpp b/xbmc/pvr/guilib/PVRGUIActionsEPG.cpp new file mode 100644 index 0000000..8b5cc9b --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsEPG.cpp @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2016-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 "PVRGUIActionsEPG.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/dialogs/GUIDialogPVRChannelGuide.h" +#include "pvr/dialogs/GUIDialogPVRGuideInfo.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/epg/EpgSearchFilter.h" +#include "pvr/guilib/PVRGUIActionsParentalControl.h" +#include "pvr/windows/GUIWindowPVRSearch.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <memory> +#include <string> + +using namespace PVR; + +namespace +{ +PVR::CGUIWindowPVRSearchBase* GetSearchWindow(bool bRadio) +{ + const int windowSearchId = bRadio ? WINDOW_RADIO_SEARCH : WINDOW_TV_SEARCH; + + PVR::CGUIWindowPVRSearchBase* windowSearch; + + CGUIWindowManager& windowMgr = CServiceBroker::GetGUI()->GetWindowManager(); + if (bRadio) + windowSearch = windowMgr.GetWindow<PVR::CGUIWindowPVRRadioSearch>(windowSearchId); + else + windowSearch = windowMgr.GetWindow<PVR::CGUIWindowPVRTVSearch>(windowSearchId); + + if (!windowSearch) + CLog::LogF(LOGERROR, "Unable to get {}!", bRadio ? "WINDOW_RADIO_SEARCH" : "WINDOW_TV_SEARCH"); + + return windowSearch; +} +} // unnamed namespace + +bool CPVRGUIActionsEPG::ShowEPGInfo(const CFileItem& item) const +{ + const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel()); + if (channel && CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock( + channel) != ParentalCheckResult::SUCCESS) + return false; + + const std::shared_ptr<CPVREpgInfoTag> epgTag(CPVRItem(item).GetEpgInfoTag()); + if (!epgTag) + { + CLog::LogF(LOGERROR, "No epg tag!"); + return false; + } + + CGUIDialogPVRGuideInfo* pDlgInfo = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGuideInfo>( + WINDOW_DIALOG_PVR_GUIDE_INFO); + if (!pDlgInfo) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_GUIDE_INFO!"); + return false; + } + + pDlgInfo->SetProgInfo(std::make_shared<CFileItem>(epgTag)); + pDlgInfo->Open(); + return true; +} + +bool CPVRGUIActionsEPG::ShowChannelEPG(const CFileItem& item) const +{ + const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel()); + if (channel && CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock( + channel) != ParentalCheckResult::SUCCESS) + return false; + + CGUIDialogPVRChannelGuide* pDlgInfo = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRChannelGuide>( + WINDOW_DIALOG_PVR_CHANNEL_GUIDE); + if (!pDlgInfo) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_CHANNEL_GUIDE!"); + return false; + } + + pDlgInfo->Open(channel); + return true; +} + +bool CPVRGUIActionsEPG::FindSimilar(const CFileItem& item) const +{ + CGUIWindowPVRSearchBase* windowSearch = GetSearchWindow(CPVRItem(item).IsRadio()); + if (!windowSearch) + return false; + + //! @todo If we want dialogs to spawn program search in a clean way - without having to force-close any + // other dialogs - we must introduce a search dialog with functionality similar to the search window. + + for (int iId = CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog( + true /* ignoreClosing */); + iId != WINDOW_INVALID; + iId = CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog( + true /* ignoreClosing */)) + { + CLog::LogF(LOGWARNING, + "Have to close modal dialog with id {} before search window can be opened.", iId); + + CGUIWindow* window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(iId); + if (window) + { + window->Close(); + } + else + { + CLog::LogF(LOGERROR, "Unable to get window instance {}! Cannot open search window.", iId); + return false; // return, otherwise we run into an endless loop + } + } + + windowSearch->SetItemToSearch(item); + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowSearch->GetID()); + return true; +}; + +bool CPVRGUIActionsEPG::ExecuteSavedSearch(const CFileItem& item) +{ + const auto searchFilter = item.GetEPGSearchFilter(); + + if (!searchFilter) + { + CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present."); + return false; + } + + CGUIWindowPVRSearchBase* windowSearch = GetSearchWindow(searchFilter->IsRadio()); + if (!windowSearch) + return false; + + windowSearch->SetItemToSearch(item); + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowSearch->GetID()); + return true; +} + +bool CPVRGUIActionsEPG::EditSavedSearch(const CFileItem& item) +{ + const auto searchFilter = item.GetEPGSearchFilter(); + + if (!searchFilter) + { + CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present."); + return false; + } + + CGUIWindowPVRSearchBase* windowSearch = GetSearchWindow(searchFilter->IsRadio()); + if (!windowSearch) + return false; + + if (windowSearch->OpenDialogSearch(item) == CGUIDialogPVRGuideSearch::Result::SEARCH) + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowSearch->GetID()); + + return true; +} + +bool CPVRGUIActionsEPG::RenameSavedSearch(const CFileItem& item) +{ + const auto searchFilter = item.GetEPGSearchFilter(); + + if (!searchFilter) + { + CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present."); + return false; + } + + std::string title = searchFilter->GetTitle(); + if (CGUIKeyboardFactory::ShowAndGetInput(title, + CVariant{g_localizeStrings.Get(528)}, // "Enter title" + false)) + { + searchFilter->SetTitle(title); + CServiceBroker::GetPVRManager().EpgContainer().PersistSavedSearch(*searchFilter); + return true; + } + return false; +} + +bool CPVRGUIActionsEPG::DeleteSavedSearch(const CFileItem& item) +{ + const auto searchFilter = item.GetEPGSearchFilter(); + + if (!searchFilter) + { + CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present."); + return false; + } + + if (CGUIDialogYesNo::ShowAndGetInput(CVariant{122}, // "Confirm delete" + CVariant{19338}, // "Delete this saved search?" + CVariant{""}, CVariant{item.GetLabel()})) + { + return CServiceBroker::GetPVRManager().EpgContainer().DeleteSavedSearch(*searchFilter); + } + return false; +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsEPG.h b/xbmc/pvr/guilib/PVRGUIActionsEPG.h new file mode 100644 index 0000000..e0a3edc --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsEPG.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" + +class CFileItem; + +namespace PVR +{ +class CPVRGUIActionsEPG : public IPVRComponent +{ +public: + CPVRGUIActionsEPG() = default; + ~CPVRGUIActionsEPG() override = default; + + /*! + * @brief Open a dialog with epg information for a given item. + * @param item containing epg data to show. item must be an epg tag, a channel or a timer. + * @return true on success, false otherwise. + */ + bool ShowEPGInfo(const CFileItem& item) const; + + /*! + * @brief Open a dialog with the epg list for a given item. + * @param item containing channel info. item must be an epg tag, a channel or a timer. + * @return true on success, false otherwise. + */ + bool ShowChannelEPG(const CFileItem& item) const; + + /*! + * @brief Open a window containing a list of epg tags 'similar' to a given item. + * @param item containing epg data for matching. item must be an epg tag, a channel or a + * recording. + * @return true on success, false otherwise. + */ + bool FindSimilar(const CFileItem& item) const; + + /*! + * @brief Execute a saved search. Displays result in search window if it is open. + * @param item The item containing a search filter. + * @return True on success, false otherwise. + */ + bool ExecuteSavedSearch(const CFileItem& item); + + /*! + * @brief Edit a saved search. Opens the search dialog. + * @param item The item containing a search filter. + * @return True on success, false otherwise. + */ + bool EditSavedSearch(const CFileItem& item); + + /*! + * @brief Rename a saved search. Opens a title input dialog. + * @param item The item containing a search filter. + * @return True on success, false otherwise. + */ + bool RenameSavedSearch(const CFileItem& item); + + /*! + * @brief Delete a saved search. Opens confirmation dialog before deleting. + * @param item The item containing a search filter. + * @return True on success, false otherwise. + */ + bool DeleteSavedSearch(const CFileItem& item); + +private: + CPVRGUIActionsEPG(const CPVRGUIActionsEPG&) = delete; + CPVRGUIActionsEPG const& operator=(CPVRGUIActionsEPG const&) = delete; +}; + +namespace GUI +{ +// pretty scope and name +using EPG = CPVRGUIActionsEPG; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp new file mode 100644 index 0000000..96947c4 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016-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 "PVRGUIActionsParentalControl.h" + +#include "ServiceBroker.h" +#include "dialogs/GUIDialogNumeric.h" +#include "guilib/LocalizeStrings.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVRManager.h" +#include "pvr/channels/PVRChannel.h" +#include "settings/Settings.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <memory> +#include <string> + +using namespace PVR; +using namespace KODI::MESSAGING; + +CPVRGUIActionsParentalControl::CPVRGUIActionsParentalControl() + : m_settings({CSettings::SETTING_PVRPARENTAL_PIN, CSettings::SETTING_PVRPARENTAL_ENABLED}) +{ +} + +ParentalCheckResult CPVRGUIActionsParentalControl::CheckParentalLock( + const std::shared_ptr<CPVRChannel>& channel) const +{ + if (!CServiceBroker::GetPVRManager().IsParentalLocked(channel)) + return ParentalCheckResult::SUCCESS; + + ParentalCheckResult ret = CheckParentalPIN(); + + if (ret == ParentalCheckResult::FAILED) + CLog::LogF(LOGERROR, "Parental lock verification failed for channel '{}': wrong PIN entered.", + channel->ChannelName()); + + return ret; +} + +ParentalCheckResult CPVRGUIActionsParentalControl::CheckParentalPIN() const +{ + if (!m_settings.GetBoolValue(CSettings::SETTING_PVRPARENTAL_ENABLED)) + return ParentalCheckResult::SUCCESS; + + std::string pinCode = m_settings.GetStringValue(CSettings::SETTING_PVRPARENTAL_PIN); + if (pinCode.empty()) + return ParentalCheckResult::SUCCESS; + + InputVerificationResult ret = CGUIDialogNumeric::ShowAndVerifyInput( + pinCode, g_localizeStrings.Get(19262), true); // "Parental control. Enter PIN:" + + if (ret == InputVerificationResult::SUCCESS) + { + CServiceBroker::GetPVRManager().RestartParentalTimer(); + return ParentalCheckResult::SUCCESS; + } + else if (ret == InputVerificationResult::FAILED) + { + HELPERS::ShowOKDialogText(CVariant{19264}, + CVariant{19265}); // "Incorrect PIN", "The entered PIN was incorrect." + return ParentalCheckResult::FAILED; + } + else + { + return ParentalCheckResult::CANCELED; + } +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsParentalControl.h b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.h new file mode 100644 index 0000000..921f597 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" +#include "pvr/settings/PVRSettings.h" + +#include <memory> + +namespace PVR +{ +enum class ParentalCheckResult +{ + CANCELED, + FAILED, + SUCCESS +}; + +class CPVRChannel; + +class CPVRGUIActionsParentalControl : public IPVRComponent +{ +public: + CPVRGUIActionsParentalControl(); + ~CPVRGUIActionsParentalControl() override = default; + + /*! + * @brief Check if channel is parental locked. Ask for PIN if necessary. + * @param channel The channel to do the check for. + * @return the result of the check (success, failed, or canceled by user). + */ + ParentalCheckResult CheckParentalLock(const std::shared_ptr<CPVRChannel>& channel) const; + + /*! + * @brief Open Numeric dialog to check for parental PIN. + * @return the result of the check (success, failed, or canceled by user). + */ + ParentalCheckResult CheckParentalPIN() const; + +private: + CPVRGUIActionsParentalControl(const CPVRGUIActionsParentalControl&) = delete; + CPVRGUIActionsParentalControl const& operator=(CPVRGUIActionsParentalControl const&) = delete; + + CPVRSettings m_settings; +}; + +namespace GUI +{ +// pretty scope and name +using Parental = CPVRGUIActionsParentalControl; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp b/xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp new file mode 100644 index 0000000..66fdb80 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp @@ -0,0 +1,564 @@ +/* + * Copyright (C) 2016-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 "PVRGUIActionsPlayback.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "application/ApplicationEnums.h" +#include "cores/DataCacheCore.h" +#include "dialogs/GUIDialogContextMenu.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/PVRStreamProperties.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "pvr/guilib/PVRGUIActionsParentalControl.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/recordings/PVRRecordings.h" +#include "settings/MediaSettings.h" +#include "settings/Settings.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <memory> +#include <string> +#include <vector> + +using namespace PVR; +using namespace KODI::MESSAGING; + +CPVRGUIActionsPlayback::CPVRGUIActionsPlayback() + : m_settings({CSettings::SETTING_LOOKANDFEEL_STARTUPACTION, + CSettings::SETTING_PVRPLAYBACK_SWITCHTOFULLSCREENCHANNELTYPES}) +{ +} + +std::string CPVRGUIActionsPlayback::GetResumeLabel(const CFileItem& item) const +{ + std::string resumeString; + + const std::shared_ptr<CPVRRecording> recording( + CPVRItem(CFileItemPtr(new CFileItem(item))).GetRecording()); + if (recording && !recording->IsDeleted()) + { + int positionInSeconds = lrint(recording->GetResumePoint().timeInSeconds); + if (positionInSeconds > 0) + resumeString = StringUtils::Format( + g_localizeStrings.Get(12022), + StringUtils::SecondsToTimeString(positionInSeconds, TIME_FORMAT_HH_MM_SS)); + } + return resumeString; +} + +bool CPVRGUIActionsPlayback::CheckResumeRecording(const CFileItem& item) const +{ + bool bPlayIt(true); + std::string resumeString(GetResumeLabel(item)); + if (!resumeString.empty()) + { + CContextButtons choices; + choices.Add(CONTEXT_BUTTON_RESUME_ITEM, resumeString); + choices.Add(CONTEXT_BUTTON_PLAY_ITEM, 12021); // Play from beginning + int choice = CGUIDialogContextMenu::ShowAndGetChoice(choices); + if (choice > 0) + const_cast<CFileItem*>(&item)->SetStartOffset( + choice == CONTEXT_BUTTON_RESUME_ITEM ? STARTOFFSET_RESUME : 0); + else + bPlayIt = false; // context menu cancelled + } + return bPlayIt; +} + +bool CPVRGUIActionsPlayback::ResumePlayRecording(const CFileItem& item, bool bFallbackToPlay) const +{ + bool bCanResume = !GetResumeLabel(item).empty(); + if (bCanResume) + { + const_cast<CFileItem*>(&item)->SetStartOffset(STARTOFFSET_RESUME); + } + else + { + if (bFallbackToPlay) + const_cast<CFileItem*>(&item)->SetStartOffset(0); + else + return false; + } + + return PlayRecording(item, false); +} + +void CPVRGUIActionsPlayback::CheckAndSwitchToFullscreen(bool bFullscreen) const +{ + CMediaSettings::GetInstance().SetMediaStartWindowed(!bFullscreen); + + if (bFullscreen) + { + CGUIMessage msg(GUI_MSG_FULLSCREEN, 0, + CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow()); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + } +} + +void CPVRGUIActionsPlayback::StartPlayback(CFileItem* item, + bool bFullscreen, + const CPVRStreamProperties* epgProps) const +{ + // Obtain dynamic playback url and properties from the respective pvr client + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item); + if (client) + { + CPVRStreamProperties props; + + if (item->IsPVRChannel()) + { + // If this was an EPG Tag to be played as live then PlayEpgTag() will create a channel + // fileitem instead and pass the epg tags props so we use those and skip the client call + if (epgProps) + props = *epgProps; + else + client->GetChannelStreamProperties(item->GetPVRChannelInfoTag(), props); + } + else if (item->IsPVRRecording()) + { + client->GetRecordingStreamProperties(item->GetPVRRecordingInfoTag(), props); + } + else if (item->IsEPG()) + { + if (epgProps) // we already have props from PlayEpgTag() + props = *epgProps; + else + client->GetEpgTagStreamProperties(item->GetEPGInfoTag(), props); + } + + if (props.size()) + { + const std::string url = props.GetStreamURL(); + if (!url.empty()) + item->SetDynPath(url); + + const std::string mime = props.GetStreamMimeType(); + if (!mime.empty()) + { + item->SetMimeType(mime); + item->SetContentLookup(false); + } + + for (const auto& prop : props) + item->SetProperty(prop.first, prop.second); + } + } + + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 0, 0, static_cast<void*>(item)); + CheckAndSwitchToFullscreen(bFullscreen); +} + +bool CPVRGUIActionsPlayback::PlayRecording(const CFileItem& item, bool bCheckResume) const +{ + const std::shared_ptr<CPVRRecording> recording(CPVRItem(item).GetRecording()); + if (!recording) + return false; + + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRecording(recording)) + { + CGUIMessage msg(GUI_MSG_FULLSCREEN, 0, + CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow()); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + return true; + } + + if (!bCheckResume || CheckResumeRecording(item)) + { + CFileItem* itemToPlay = new CFileItem(recording); + itemToPlay->SetStartOffset(item.GetStartOffset()); + StartPlayback(itemToPlay, true); + } + return true; +} + +bool CPVRGUIActionsPlayback::PlayEpgTag(const CFileItem& item) const +{ + const std::shared_ptr<CPVREpgInfoTag> epgTag(CPVRItem(item).GetEpgInfoTag()); + if (!epgTag) + return false; + + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingEpgTag(epgTag)) + { + CGUIMessage msg(GUI_MSG_FULLSCREEN, 0, + CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow()); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + return true; + } + + // Obtain dynamic playback url and properties from the respective pvr client + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(epgTag->ClientID()); + if (!client) + return false; + + CPVRStreamProperties props; + client->GetEpgTagStreamProperties(epgTag, props); + + CFileItem* itemToPlay = nullptr; + if (props.EPGPlaybackAsLive()) + { + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(item); + if (!groupMember) + return false; + + itemToPlay = new CFileItem(groupMember); + } + else + { + itemToPlay = new CFileItem(epgTag); + } + + StartPlayback(itemToPlay, true, &props); + return true; +} + +bool CPVRGUIActionsPlayback::SwitchToChannel(const CFileItem& item, bool bCheckResume) const +{ + if (item.m_bIsFolder) + return false; + + std::shared_ptr<CPVRRecording> recording; + const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel()); + if (channel) + { + bool bSwitchToFullscreen = + CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingChannel(channel); + + if (!bSwitchToFullscreen) + { + recording = + CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(channel->GetEPGNow()); + bSwitchToFullscreen = + recording && + CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRecording(recording); + } + + if (bSwitchToFullscreen) + { + CGUIMessage msg(GUI_MSG_FULLSCREEN, 0, + CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow()); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + return true; + } + } + + ParentalCheckResult result = + channel ? CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(channel) + : ParentalCheckResult::FAILED; + if (result == ParentalCheckResult::SUCCESS) + { + // switch to channel or if recording present, ask whether to switch or play recording... + if (!recording) + recording = + CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(channel->GetEPGNow()); + + if (recording) + { + bool bCancel(false); + bool bPlayRecording = CGUIDialogYesNo::ShowAndGetInput( + CVariant{19687}, // "Play recording" + CVariant{""}, CVariant{12021}, // "Play from beginning" + CVariant{recording->m_strTitle}, bCancel, CVariant{19000}, // "Switch to channel" + CVariant{19687}, // "Play recording" + 0); // no autoclose + if (bCancel) + return false; + + if (bPlayRecording) + return PlayRecording(CFileItem(recording), bCheckResume); + } + + bool bFullscreen; + switch (m_settings.GetIntValue(CSettings::SETTING_PVRPLAYBACK_SWITCHTOFULLSCREENCHANNELTYPES)) + { + case 0: // never + bFullscreen = false; + break; + case 1: // TV channels + bFullscreen = !channel->IsRadio(); + break; + case 2: // Radio channels + bFullscreen = channel->IsRadio(); + break; + case 3: // TV and radio channels + default: + bFullscreen = true; + break; + } + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(item); + if (!groupMember) + return false; + + StartPlayback(new CFileItem(groupMember), bFullscreen); + return true; + } + else if (result == ParentalCheckResult::FAILED) + { + const std::string channelName = + channel ? channel->ChannelName() : g_localizeStrings.Get(19029); // Channel + const std::string msg = StringUtils::Format( + g_localizeStrings.Get(19035), + channelName); // CHANNELNAME could not be played. Check the log for details. + + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(19166), + msg); // PVR information + } + + return false; +} + +bool CPVRGUIActionsPlayback::SwitchToChannel(PlaybackType type) const +{ + std::shared_ptr<CPVRChannelGroupMember> groupMember; + bool bIsRadio(false); + + // check if the desired PlaybackType is already playing, + // and if not, try to grab the last played channel of this type + switch (type) + { + case PlaybackTypeRadio: + { + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio()) + return true; + + const std::shared_ptr<CPVRChannelGroup> allGroup = + CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAllRadio(); + if (allGroup) + groupMember = allGroup->GetLastPlayedChannelGroupMember(); + + bIsRadio = true; + break; + } + case PlaybackTypeTV: + { + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV()) + return true; + + const std::shared_ptr<CPVRChannelGroup> allGroup = + CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAllTV(); + if (allGroup) + groupMember = allGroup->GetLastPlayedChannelGroupMember(); + + break; + } + default: + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying()) + return true; + + groupMember = + CServiceBroker::GetPVRManager().ChannelGroups()->GetLastPlayedChannelGroupMember(); + break; + } + + // if we have a last played channel, start playback + if (groupMember) + { + return SwitchToChannel(CFileItem(groupMember), true); + } + else + { + // if we don't, find the active channel group of the demanded type and play it's first channel + const std::shared_ptr<CPVRChannelGroup> channelGroup = + CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(bIsRadio); + if (channelGroup) + { + // try to start playback of first channel in this group + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers = + channelGroup->GetMembers(); + if (!groupMembers.empty()) + { + return SwitchToChannel(CFileItem(*groupMembers.begin()), true); + } + } + } + + CLog::LogF(LOGERROR, + "Could not determine {} channel to playback. No last played channel found, and " + "first channel of active group could also not be determined.", + bIsRadio ? "Radio" : "TV"); + + CGUIDialogKaiToast::QueueNotification( + CGUIDialogKaiToast::Error, + g_localizeStrings.Get(19166), // PVR information + StringUtils::Format( + g_localizeStrings.Get(19035), + g_localizeStrings.Get( + bIsRadio ? 19021 + : 19020))); // Radio/TV could not be played. Check the log for details. + return false; +} + +bool CPVRGUIActionsPlayback::PlayChannelOnStartup() const +{ + int iAction = m_settings.GetIntValue(CSettings::SETTING_LOOKANDFEEL_STARTUPACTION); + if (iAction != STARTUP_ACTION_PLAY_TV && iAction != STARTUP_ACTION_PLAY_RADIO) + return false; + + bool playRadio = (iAction == STARTUP_ACTION_PLAY_RADIO); + + // get the last played channel or fallback to first channel of all channels group + std::shared_ptr<CPVRChannelGroupMember> groupMember = + CServiceBroker::GetPVRManager().PlaybackState()->GetLastPlayedChannelGroupMember(playRadio); + + if (!groupMember) + { + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().ChannelGroups()->Get(playRadio)->GetGroupAll(); + auto channels = group->GetMembers(); + if (channels.empty()) + return false; + + groupMember = channels.front(); + if (!groupMember) + return false; + } + + CLog::Log(LOGINFO, "PVR is starting playback of channel '{}'", + groupMember->Channel()->ChannelName()); + return SwitchToChannel(CFileItem(groupMember), true); +} + +bool CPVRGUIActionsPlayback::PlayMedia(const CFileItem& item) const +{ + std::unique_ptr<CFileItem> pvrItem = std::make_unique<CFileItem>(item); + if (URIUtils::IsPVRChannel(item.GetPath()) && !item.HasPVRChannelInfoTag()) + { + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelGroupMemberByPath( + item.GetPath()); + if (groupMember) + pvrItem = std::make_unique<CFileItem>(groupMember); + } + else if (URIUtils::IsPVRRecording(item.GetPath()) && !item.HasPVRRecordingInfoTag()) + { + const std::shared_ptr<CPVRRecording> recording = + CServiceBroker::GetPVRManager().Recordings()->GetByPath(item.GetPath()); + if (recording) + { + pvrItem = std::make_unique<CFileItem>(recording); + pvrItem->SetStartOffset(item.GetStartOffset()); + } + } + bool bCheckResume = true; + if (item.HasProperty("check_resume")) + bCheckResume = item.GetProperty("check_resume").asBoolean(); + + if (pvrItem && pvrItem->HasPVRChannelInfoTag()) + { + return SwitchToChannel(*pvrItem, bCheckResume); + } + else if (pvrItem && pvrItem->HasPVRRecordingInfoTag()) + { + return PlayRecording(*pvrItem, bCheckResume); + } + + return false; +} + +void CPVRGUIActionsPlayback::SeekForward() +{ + time_t playbackStartTime = CServiceBroker::GetDataCacheCore().GetStartTime(); + if (playbackStartTime > 0) + { + const std::shared_ptr<CPVRChannel> playingChannel = + CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + if (playingChannel) + { + time_t nextTime = 0; + std::shared_ptr<CPVREpgInfoTag> next = playingChannel->GetEPGNext(); + if (next) + { + next->StartAsUTC().GetAsTime(nextTime); + } + else + { + // if there is no next event, jump to end of currently playing event + next = playingChannel->GetEPGNow(); + if (next) + next->EndAsUTC().GetAsTime(nextTime); + } + + int64_t seekTime = 0; + if (nextTime != 0) + { + seekTime = (nextTime - playbackStartTime) * 1000; + } + else + { + // no epg; jump to end of buffer + seekTime = CServiceBroker::GetDataCacheCore().GetMaxTime(); + } + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_SEEK_TIME, seekTime); + } + } +} + +void CPVRGUIActionsPlayback::SeekBackward(unsigned int iThreshold) +{ + time_t playbackStartTime = CServiceBroker::GetDataCacheCore().GetStartTime(); + if (playbackStartTime > 0) + { + const std::shared_ptr<CPVRChannel> playingChannel = + CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + if (playingChannel) + { + time_t prevTime = 0; + std::shared_ptr<CPVREpgInfoTag> prev = playingChannel->GetEPGNow(); + if (prev) + { + prev->StartAsUTC().GetAsTime(prevTime); + + // if playback time of current event is above threshold jump to start of current event + int64_t playTime = CServiceBroker::GetDataCacheCore().GetPlayTime() / 1000; + if ((playbackStartTime + playTime - prevTime) <= iThreshold) + { + // jump to start of previous event + prevTime = 0; + prev = playingChannel->GetEPGPrevious(); + if (prev) + prev->StartAsUTC().GetAsTime(prevTime); + } + } + + int64_t seekTime = 0; + if (prevTime != 0) + { + seekTime = (prevTime - playbackStartTime) * 1000; + } + else + { + // no epg; jump to begin of buffer + seekTime = CServiceBroker::GetDataCacheCore().GetMinTime(); + } + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_SEEK_TIME, seekTime); + } + } +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsPlayback.h b/xbmc/pvr/guilib/PVRGUIActionsPlayback.h new file mode 100644 index 0000000..8b0ab55 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsPlayback.h @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" +#include "pvr/settings/PVRSettings.h" + +#include <string> + +class CFileItem; + +namespace PVR +{ +enum PlaybackType +{ + PlaybackTypeAny = 0, + PlaybackTypeTV, + PlaybackTypeRadio +}; + +class CPVRStreamProperties; + +class CPVRGUIActionsPlayback : public IPVRComponent +{ +public: + CPVRGUIActionsPlayback(); + ~CPVRGUIActionsPlayback() override = default; + + /*! + * @brief Get a localized resume play label, if the given item can be resumed. + * @param item containing a recording or an epg tag. + * @return the localized resume play label that can be used for instance as context menu item + * label or an empty string if resume is not possible. + */ + std::string GetResumeLabel(const CFileItem& item) const; + + /*! + * @brief Resume a previously not completely played recording. + * @param item containing a recording or an epg tag. + * @param bFallbackToPlay controls whether playback of the recording should be started at the + * beginning ig no resume data are available. + * @return true on success, false otherwise. + */ + bool ResumePlayRecording(const CFileItem& item, bool bFallbackToPlay) const; + + /*! + * @brief Play recording. + * @param item containing a recording or an epg tag. + * @param bCheckResume controls resume check. + * @return true on success, false otherwise. + */ + bool PlayRecording(const CFileItem& item, bool bCheckResume) const; + + /*! + * @brief Play EPG tag. + * @param item containing an epg tag. + * @return true on success, false otherwise. + */ + bool PlayEpgTag(const CFileItem& item) const; + + /*! + * @brief Switch channel. + * @param item containing a channel or an epg tag. + * @param bCheckResume controls resume check in case a recording for the current epg event is + * present. + * @return true on success, false otherwise. + */ + bool SwitchToChannel(const CFileItem& item, bool bCheckResume) const; + + /*! + * @brief Playback the given file item. + * @param item containing a channel or a recording. + * @return True if the item could be played, false otherwise. + */ + bool PlayMedia(const CFileItem& item) const; + + /*! + * @brief Start playback of the last played channel, and if there is none, play first channel in + * the current channelgroup. + * @param type The type of playback to be started (any, radio, tv). See PlaybackType enum + * @return True if playback was started, false otherwise. + */ + bool SwitchToChannel(PlaybackType type) const; + + /*! + * @brief Plays the last played channel or the first channel of TV or Radio on startup. + * @return True if playback was started, false otherwise. + */ + bool PlayChannelOnStartup() const; + + /*! + * @brief Seek to the start of the next epg event in timeshift buffer, relative to the currently + * playing event. If there is no next event, seek to the end of the currently playing event (to + * the 'live' position). + */ + void SeekForward(); + + /*! + * @brief Seek to the start of the previous epg event in timeshift buffer, relative to the + * currently playing event or if there is no previous event or if playback time is greater than + * given threshold, seek to the start of the playing event. + * @param iThreshold the value in seconds to trigger seek to start of current event instead of + * start of previous event. + */ + void SeekBackward(unsigned int iThreshold); + +private: + CPVRGUIActionsPlayback(const CPVRGUIActionsPlayback&) = delete; + CPVRGUIActionsPlayback const& operator=(CPVRGUIActionsPlayback const&) = delete; + + /*! + * @brief Check whether resume play is possible for a given item, display "resume from ..."/"play + * from start" context menu in case. + * @param item containing a recording or an epg tag. + * @return true, to play/resume the item, false otherwise. + */ + bool CheckResumeRecording(const CFileItem& item) const; + + /*! + * @brief Check "play minimized" settings value and switch to fullscreen if not set. + * @param bFullscreen switch to fullscreen or set windowed playback. + */ + void CheckAndSwitchToFullscreen(bool bFullscreen) const; + + /*! + * @brief Start playback of the given item. + * @param bFullscreen start playback fullscreen or not. + * @param epgProps properties to be used instead of calling to the client if supplied. + * @param item containing a channel or a recording. + */ + void StartPlayback(CFileItem* item, + bool bFullscreen, + const CPVRStreamProperties* epgProps = nullptr) const; + + CPVRSettings m_settings; +}; + +namespace GUI +{ +// pretty scope and name +using Playback = CPVRGUIActionsPlayback; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp new file mode 100644 index 0000000..1a0a99a --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2016-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 "PVRGUIActionsPowerManagement.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "messaging/helpers/DialogHelper.h" +#include "network/Network.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/Settings.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +#include <memory> +#include <string> +#include <vector> + +using namespace PVR; +using namespace KODI::MESSAGING; + +CPVRGUIActionsPowerManagement::CPVRGUIActionsPowerManagement() + : m_settings({CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME, + CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME}) +{ +} + +bool CPVRGUIActionsPowerManagement::CanSystemPowerdown(bool bAskUser /*= true*/) const +{ + bool bReturn(true); + if (CServiceBroker::GetPVRManager().IsStarted()) + { + std::shared_ptr<CPVRTimerInfoTag> cause; + if (!AllLocalBackendsIdle(cause)) + { + if (bAskUser) + { + std::string text; + + if (cause) + { + if (cause->IsRecording()) + { + text = StringUtils::Format( + g_localizeStrings.Get(19691), // "PVR is currently recording...." + cause->Title(), cause->ChannelName()); + } + else + { + // Next event is due to a local recording or reminder. + const CDateTime now(CDateTime::GetUTCDateTime()); + const CDateTime start(cause->StartAsUTC()); + const CDateTimeSpan prestart(0, 0, cause->MarginStart(), 0); + + CDateTimeSpan diff(start - now); + diff -= prestart; + int mins = diff.GetSecondsTotal() / 60; + + std::string dueStr; + if (mins > 1) + { + // "%d minutes" + dueStr = StringUtils::Format(g_localizeStrings.Get(19694), mins); + } + else + { + // "about a minute" + dueStr = g_localizeStrings.Get(19695); + } + + text = StringUtils::Format( + cause->IsReminder() + ? g_localizeStrings.Get(19690) // "PVR has scheduled a reminder...." + : g_localizeStrings.Get(19692), // "PVR will start recording...." + cause->Title(), cause->ChannelName(), dueStr); + } + } + else + { + // Next event is due to automatic daily wakeup of PVR. + const CDateTime now(CDateTime::GetUTCDateTime()); + + CDateTime dailywakeuptime; + dailywakeuptime.SetFromDBTime( + m_settings.GetStringValue(CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME)); + dailywakeuptime = dailywakeuptime.GetAsUTCDateTime(); + + const CDateTimeSpan diff(dailywakeuptime - now); + int mins = diff.GetSecondsTotal() / 60; + + std::string dueStr; + if (mins > 1) + { + // "%d minutes" + dueStr = StringUtils::Format(g_localizeStrings.Get(19694), mins); + } + else + { + // "about a minute" + dueStr = g_localizeStrings.Get(19695); + } + + text = StringUtils::Format(g_localizeStrings.Get(19693), // "Daily wakeup is due in...." + dueStr); + } + + // Inform user about PVR being busy. Ask if user wants to powerdown anyway. + bReturn = HELPERS::ShowYesNoDialogText(CVariant{19685}, // "Confirm shutdown" + CVariant{text}, CVariant{222}, // "Shutdown anyway", + CVariant{19696}, // "Cancel" + 10000) // timeout value before closing + == HELPERS::DialogResponse::CHOICE_YES; + } + else + bReturn = false; // do not powerdown (busy, but no user interaction requested). + } + } + return bReturn; +} + +bool CPVRGUIActionsPowerManagement::AllLocalBackendsIdle( + std::shared_ptr<CPVRTimerInfoTag>& causingEvent) const +{ + // active recording on local backend? + const std::vector<std::shared_ptr<CPVRTimerInfoTag>> activeRecordings = + CServiceBroker::GetPVRManager().Timers()->GetActiveRecordings(); + for (const auto& timer : activeRecordings) + { + if (EventOccursOnLocalBackend(timer)) + { + causingEvent = timer; + return false; + } + } + + // soon recording on local backend? + if (IsNextEventWithinBackendIdleTime()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = + CServiceBroker::GetPVRManager().Timers()->GetNextActiveTimer(false); + if (!timer) + { + // Next event is due to automatic daily wakeup of PVR! + causingEvent.reset(); + return false; + } + + if (EventOccursOnLocalBackend(timer)) + { + causingEvent = timer; + return false; + } + } + return true; +} + +bool CPVRGUIActionsPowerManagement::EventOccursOnLocalBackend( + const std::shared_ptr<CPVRTimerInfoTag>& event) const +{ + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(CFileItem(event)); + if (client) + { + const std::string hostname = client->GetBackendHostname(); + if (!hostname.empty() && CServiceBroker::GetNetwork().IsLocalHost(hostname)) + return true; + } + return false; +} + +bool CPVRGUIActionsPowerManagement::IsNextEventWithinBackendIdleTime() const +{ + // timers going off soon? + const CDateTime now(CDateTime::GetUTCDateTime()); + const CDateTimeSpan idle( + 0, 0, m_settings.GetIntValue(CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME), 0); + const CDateTime next(CServiceBroker::GetPVRManager().Timers()->GetNextEventTime()); + const CDateTimeSpan delta(next - now); + + return (delta <= idle); +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h new file mode 100644 index 0000000..bf99819 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" +#include "pvr/settings/PVRSettings.h" + +#include <memory> + +class CFileItem; + +namespace PVR +{ +class CPVRTimerInfoTag; + +class CPVRGUIActionsPowerManagement : public IPVRComponent +{ +public: + CPVRGUIActionsPowerManagement(); + ~CPVRGUIActionsPowerManagement() override = default; + + /*! + * @brief Check whether the system Kodi is running on can be powered down + * (shutdown/reboot/suspend/hibernate) without stopping any active recordings and/or without + * preventing the start of recordings scheduled for now + pvrpowermanagement.backendidletime. + * @param bAskUser True to informs user in case of potential data loss. User can decide to allow + * powerdown anyway. False to not to ask user and to not confirm power down. + * @return True if system can be safely powered down, false otherwise. + */ + bool CanSystemPowerdown(bool bAskUser = true) const; + +private: + CPVRGUIActionsPowerManagement(const CPVRGUIActionsPowerManagement&) = delete; + CPVRGUIActionsPowerManagement const& operator=(CPVRGUIActionsPowerManagement const&) = delete; + + bool AllLocalBackendsIdle(std::shared_ptr<CPVRTimerInfoTag>& causingEvent) const; + bool EventOccursOnLocalBackend(const std::shared_ptr<CPVRTimerInfoTag>& event) const; + bool IsNextEventWithinBackendIdleTime() const; + + CPVRSettings m_settings; +}; + +namespace GUI +{ +// pretty scope and name +using PowerManagement = CPVRGUIActionsPowerManagement; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp b/xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp new file mode 100644 index 0000000..df23f38 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2016-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 "PVRGUIActionsRecordings.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "dialogs/GUIDialogBusy.h" +#include "dialogs/GUIDialogYesNo.h" +#include "filesystem/IDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/WindowIDs.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/dialogs/GUIDialogPVRRecordingInfo.h" +#include "pvr/dialogs/GUIDialogPVRRecordingSettings.h" +#include "pvr/recordings/PVRRecording.h" +#include "settings/Settings.h" +#include "threads/IRunnable.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <memory> +#include <numeric> +#include <string> + +using namespace PVR; +using namespace KODI::MESSAGING; + +namespace +{ +class AsyncRecordingAction : private IRunnable +{ +public: + bool Execute(const CFileItem& item); + +protected: + AsyncRecordingAction() = default; + +private: + // IRunnable implementation + void Run() override; + + // the worker function + virtual bool DoRun(const std::shared_ptr<CFileItem>& item) = 0; + + std::shared_ptr<CFileItem> m_item; + bool m_bSuccess = false; +}; + +bool AsyncRecordingAction::Execute(const CFileItem& item) +{ + m_item = std::make_shared<CFileItem>(item); + CGUIDialogBusy::Wait(this, 100, false); + return m_bSuccess; +} + +void AsyncRecordingAction::Run() +{ + m_bSuccess = DoRun(m_item); + + if (m_bSuccess) + CServiceBroker::GetPVRManager().TriggerRecordingsUpdate(); +} + +class AsyncRenameRecording : public AsyncRecordingAction +{ +public: + explicit AsyncRenameRecording(const std::string& strNewName) : m_strNewName(strNewName) {} + +private: + bool DoRun(const std::shared_ptr<CFileItem>& item) override + { + if (item->IsUsablePVRRecording()) + { + return item->GetPVRRecordingInfoTag()->Rename(m_strNewName); + } + else + { + CLog::LogF(LOGERROR, "Cannot rename item '{}': no valid recording tag", item->GetPath()); + return false; + } + } + std::string m_strNewName; +}; + +class AsyncDeleteRecording : public AsyncRecordingAction +{ +public: + explicit AsyncDeleteRecording(bool bWatchedOnly = false) : m_bWatchedOnly(bWatchedOnly) {} + +private: + bool DoRun(const std::shared_ptr<CFileItem>& item) override + { + CFileItemList items; + if (item->m_bIsFolder) + { + CUtil::GetRecursiveListing(item->GetPath(), items, "", XFILE::DIR_FLAG_NO_FILE_INFO); + } + else + { + items.Add(item); + } + + return std::accumulate( + items.cbegin(), items.cend(), true, [this](bool success, const auto& itemToDelete) { + return (itemToDelete->IsPVRRecording() && + (!m_bWatchedOnly || itemToDelete->GetPVRRecordingInfoTag()->GetPlayCount() > 0) && + !itemToDelete->GetPVRRecordingInfoTag()->Delete()) + ? false + : success; + }); + } + bool m_bWatchedOnly = false; +}; + +class AsyncEmptyRecordingsTrash : public AsyncRecordingAction +{ +private: + bool DoRun(const std::shared_ptr<CFileItem>& item) override + { + return CServiceBroker::GetPVRManager().Clients()->DeleteAllRecordingsFromTrash() == + PVR_ERROR_NO_ERROR; + } +}; + +class AsyncUndeleteRecording : public AsyncRecordingAction +{ +private: + bool DoRun(const std::shared_ptr<CFileItem>& item) override + { + if (item->IsDeletedPVRRecording()) + { + return item->GetPVRRecordingInfoTag()->Undelete(); + } + else + { + CLog::LogF(LOGERROR, "Cannot undelete item '{}': no valid recording tag", item->GetPath()); + return false; + } + } +}; + +class AsyncSetRecordingPlayCount : public AsyncRecordingAction +{ +private: + bool DoRun(const std::shared_ptr<CFileItem>& item) override + { + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item); + if (client) + { + const std::shared_ptr<CPVRRecording> recording = item->GetPVRRecordingInfoTag(); + return client->SetRecordingPlayCount(*recording, recording->GetLocalPlayCount()) == + PVR_ERROR_NO_ERROR; + } + return false; + } +}; + +class AsyncSetRecordingLifetime : public AsyncRecordingAction +{ +private: + bool DoRun(const std::shared_ptr<CFileItem>& item) override + { + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item); + if (client) + return client->SetRecordingLifetime(*item->GetPVRRecordingInfoTag()) == PVR_ERROR_NO_ERROR; + return false; + } +}; + +} // unnamed namespace + +bool CPVRGUIActionsRecordings::ShowRecordingInfo(const CFileItem& item) const +{ + if (!item.IsPVRRecording()) + { + CLog::LogF(LOGERROR, "No recording!"); + return false; + } + + CGUIDialogPVRRecordingInfo* pDlgInfo = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRRecordingInfo>( + WINDOW_DIALOG_PVR_RECORDING_INFO); + if (!pDlgInfo) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_RECORDING_INFO!"); + return false; + } + + pDlgInfo->SetRecording(item); + pDlgInfo->Open(); + return true; +} + +bool CPVRGUIActionsRecordings::EditRecording(const CFileItem& item) const +{ + const std::shared_ptr<CPVRRecording> recording = CPVRItem(item).GetRecording(); + if (!recording) + { + CLog::LogF(LOGERROR, "No recording!"); + return false; + } + + std::shared_ptr<CPVRRecording> origRecording(new CPVRRecording); + origRecording->Update(*recording, + *CServiceBroker::GetPVRManager().GetClient(recording->ClientID())); + + if (!ShowRecordingSettings(recording)) + return false; + + if (origRecording->m_strTitle != recording->m_strTitle) + { + if (!AsyncRenameRecording(recording->m_strTitle).Execute(item)) + CLog::LogF(LOGERROR, "Renaming recording failed!"); + } + + if (origRecording->GetLocalPlayCount() != recording->GetLocalPlayCount()) + { + if (!AsyncSetRecordingPlayCount().Execute(item)) + CLog::LogF(LOGERROR, "Setting recording playcount failed!"); + } + + if (origRecording->LifeTime() != recording->LifeTime()) + { + if (!AsyncSetRecordingLifetime().Execute(item)) + CLog::LogF(LOGERROR, "Setting recording lifetime failed!"); + } + + return true; +} + +bool CPVRGUIActionsRecordings::CanEditRecording(const CFileItem& item) const +{ + return CGUIDialogPVRRecordingSettings::CanEditRecording(item); +} + +bool CPVRGUIActionsRecordings::DeleteRecording(const CFileItem& item) const +{ + if ((!item.IsPVRRecording() && !item.m_bIsFolder) || item.IsParentFolder()) + return false; + + if (!ConfirmDeleteRecording(item)) + return false; + + if (!AsyncDeleteRecording().Execute(item)) + { + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19111}); // "Error", "PVR backend error. Check the log for more information about this message." + return false; + } + + return true; +} + +bool CPVRGUIActionsRecordings::ConfirmDeleteRecording(const CFileItem& item) const +{ + return CGUIDialogYesNo::ShowAndGetInput( + CVariant{122}, // "Confirm delete" + item.m_bIsFolder + ? CVariant{19113} // "Delete all recordings in this folder?" + : item.GetPVRRecordingInfoTag()->IsDeleted() + ? CVariant{19294} + // "Remove this deleted recording from trash? This operation cannot be reverted." + : CVariant{19112}, // "Delete this recording?" + CVariant{""}, CVariant{item.GetLabel()}); +} + +bool CPVRGUIActionsRecordings::DeleteWatchedRecordings(const CFileItem& item) const +{ + if (!item.m_bIsFolder || item.IsParentFolder()) + return false; + + if (!ConfirmDeleteWatchedRecordings(item)) + return false; + + if (!AsyncDeleteRecording(true).Execute(item)) + { + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19111}); // "Error", "PVR backend error. Check the log for more information about this message." + return false; + } + + return true; +} + +bool CPVRGUIActionsRecordings::ConfirmDeleteWatchedRecordings(const CFileItem& item) const +{ + return CGUIDialogYesNo::ShowAndGetInput( + CVariant{122}, // "Confirm delete" + CVariant{19328}, // "Delete all watched recordings in this folder?" + CVariant{""}, CVariant{item.GetLabel()}); +} + +bool CPVRGUIActionsRecordings::DeleteAllRecordingsFromTrash() const +{ + if (!ConfirmDeleteAllRecordingsFromTrash()) + return false; + + if (!AsyncEmptyRecordingsTrash().Execute({})) + return false; + + return true; +} + +bool CPVRGUIActionsRecordings::ConfirmDeleteAllRecordingsFromTrash() const +{ + return CGUIDialogYesNo::ShowAndGetInput( + CVariant{19292}, // "Delete all permanently" + CVariant{ + 19293}); // "Remove all deleted recordings from trash? This operation cannot be reverted." +} + +bool CPVRGUIActionsRecordings::UndeleteRecording(const CFileItem& item) const +{ + if (!item.IsDeletedPVRRecording()) + return false; + + if (!AsyncUndeleteRecording().Execute(item)) + { + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19111}); // "Error", "PVR backend error. Check the log for more information about this message." + return false; + } + + return true; +} + +bool CPVRGUIActionsRecordings::ShowRecordingSettings( + const std::shared_ptr<CPVRRecording>& recording) const +{ + CGUIDialogPVRRecordingSettings* pDlgInfo = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRRecordingSettings>( + WINDOW_DIALOG_PVR_RECORDING_SETTING); + if (!pDlgInfo) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_RECORDING_SETTING!"); + return false; + } + + pDlgInfo->SetRecording(recording); + pDlgInfo->Open(); + + return pDlgInfo->IsConfirmed(); +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsRecordings.h b/xbmc/pvr/guilib/PVRGUIActionsRecordings.h new file mode 100644 index 0000000..795a2c7 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsRecordings.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" + +#include <memory> + +class CFileItem; + +namespace PVR +{ +class CPVRRecording; + +class CPVRGUIActionsRecordings : public IPVRComponent +{ +public: + CPVRGUIActionsRecordings() = default; + ~CPVRGUIActionsRecordings() override = default; + + /*! + * @brief Open a dialog with information for a given recording. + * @param item containing a recording. + * @return true on success, false otherwise. + */ + bool ShowRecordingInfo(const CFileItem& item) const; + + /*! + * @brief Open the recording settings dialog to edit a recording. + * @param item containing the recording to edit. + * @return true on success, false otherwise. + */ + bool EditRecording(const CFileItem& item) const; + + /*! + * @brief Check if any recording settings can be edited. + * @param item containing the recording to edit. + * @return true on success, false otherwise. + */ + bool CanEditRecording(const CFileItem& item) const; + + /*! + * @brief Delete a recording, always showing a confirmation dialog. + * @param item containing a recording to delete. + * @return true, if the recording was deleted successfully, false otherwise. + */ + bool DeleteRecording(const CFileItem& item) const; + + /*! + * @brief Delete all watched recordings contained in the given folder, always showing a + * confirmation dialog. + * @param item containing a recording folder containing the items to delete. + * @return true, if the recordings were deleted successfully, false otherwise. + */ + bool DeleteWatchedRecordings(const CFileItem& item) const; + + /*! + * @brief Delete all recordings from trash, always showing a confirmation dialog. + * @return true, if the recordings were permanently deleted successfully, false otherwise. + */ + bool DeleteAllRecordingsFromTrash() const; + + /*! + * @brief Undelete a recording. + * @param item containing a recording to undelete. + * @return true, if the recording was undeleted successfully, false otherwise. + */ + bool UndeleteRecording(const CFileItem& item) const; + +private: + CPVRGUIActionsRecordings(const CPVRGUIActionsRecordings&) = delete; + CPVRGUIActionsRecordings const& operator=(CPVRGUIActionsRecordings const&) = delete; + + /*! + * @brief Open a dialog to confirm to delete a recording. + * @param item the recording to delete. + * @return true, to proceed with delete, false otherwise. + */ + bool ConfirmDeleteRecording(const CFileItem& item) const; + + /*! + * @brief Open a dialog to confirm delete all watched recordings contained in the given folder. + * @param item containing a recording folder containing the items to delete. + * @return true, to proceed with delete, false otherwise. + */ + bool ConfirmDeleteWatchedRecordings(const CFileItem& item) const; + + /*! + * @brief Open a dialog to confirm to permanently remove all deleted recordings. + * @return true, to proceed with delete, false otherwise. + */ + bool ConfirmDeleteAllRecordingsFromTrash() const; + + /*! + * @brief Open the recording settings dialog. + * @param recording containing the recording the settings shall be displayed for. + * @return true, if the dialog was ended successfully, false otherwise. + */ + bool ShowRecordingSettings(const std::shared_ptr<CPVRRecording>& recording) const; +}; + +namespace GUI +{ +// pretty scope and name +using Recordings = CPVRGUIActionsRecordings; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsTimers.cpp b/xbmc/pvr/guilib/PVRGUIActionsTimers.cpp new file mode 100644 index 0000000..be8e313 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsTimers.cpp @@ -0,0 +1,1009 @@ +/* + * Copyright (C) 2016-2022 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 "PVRGUIActionsTimers.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogProgress.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "messaging/helpers/DialogHelper.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVREventLogJob.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/dialogs/GUIDialogPVRTimerSettings.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "pvr/guilib/PVRGUIActionsParentalControl.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/Settings.h" +#include "utils/StringUtils.h" +#include "utils/SystemInfo.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <map> +#include <memory> +#include <string> +#include <thread> +#include <utility> + +using namespace PVR; +using namespace KODI::MESSAGING; + +CPVRGUIActionsTimers::CPVRGUIActionsTimers() + : m_settings({CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME, + CSettings::SETTING_PVRRECORD_INSTANTRECORDACTION, + CSettings::SETTING_PVRREMINDERS_AUTOCLOSEDELAY, + CSettings::SETTING_PVRREMINDERS_AUTORECORD, + CSettings::SETTING_PVRREMINDERS_AUTOSWITCH}) +{ +} + +bool CPVRGUIActionsTimers::ShowTimerSettings(const std::shared_ptr<CPVRTimerInfoTag>& timer) const +{ + CGUIDialogPVRTimerSettings* pDlgInfo = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRTimerSettings>( + WINDOW_DIALOG_PVR_TIMER_SETTING); + if (!pDlgInfo) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_TIMER_SETTING!"); + return false; + } + + pDlgInfo->SetTimer(timer); + pDlgInfo->Open(); + + return pDlgInfo->IsConfirmed(); +} + +bool CPVRGUIActionsTimers::AddReminder(const CFileItem& item) const +{ + const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag(); + if (!epgTag) + { + CLog::LogF(LOGERROR, "No epg tag!"); + return false; + } + + if (CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epgTag)) + { + HELPERS::ShowOKDialogText(CVariant{19033}, // "Information" + CVariant{19034}); // "There is already a timer set for this event" + return false; + } + + const std::shared_ptr<CPVRTimerInfoTag> newTimer = + CPVRTimerInfoTag::CreateReminderFromEpg(epgTag); + if (!newTimer) + { + HELPERS::ShowOKDialogText(CVariant{19033}, // "Information" + CVariant{19094}); // Timer creation failed. Unsupported timer type. + return false; + } + + return AddTimer(newTimer); +} + +bool CPVRGUIActionsTimers::AddTimer(bool bRadio) const +{ + const std::shared_ptr<CPVRTimerInfoTag> newTimer(new CPVRTimerInfoTag(bRadio)); + if (ShowTimerSettings(newTimer)) + { + return AddTimer(newTimer); + } + return false; +} + +bool CPVRGUIActionsTimers::AddTimer(const CFileItem& item, bool bShowTimerSettings) const +{ + return AddTimer(item, false, bShowTimerSettings, false); +} + +bool CPVRGUIActionsTimers::AddTimerRule(const CFileItem& item, + bool bShowTimerSettings, + bool bFallbackToOneShotTimer) const +{ + return AddTimer(item, true, bShowTimerSettings, bFallbackToOneShotTimer); +} + +bool CPVRGUIActionsTimers::AddTimer(const CFileItem& item, + bool bCreateRule, + bool bShowTimerSettings, + bool bFallbackToOneShotTimer) const +{ + const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel()); + if (!channel) + { + CLog::LogF(LOGERROR, "No channel!"); + return false; + } + + if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(channel) != + ParentalCheckResult::SUCCESS) + return false; + + std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag(); + if (epgTag) + { + if (epgTag->IsGapTag()) + epgTag.reset(); // for gap tags, we can only create instant timers + } + else if (bCreateRule) + { + CLog::LogF(LOGERROR, "No epg tag!"); + return false; + } + + std::shared_ptr<CPVRTimerInfoTag> timer( + bCreateRule || !epgTag ? nullptr + : CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epgTag)); + std::shared_ptr<CPVRTimerInfoTag> rule( + bCreateRule ? CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer) : nullptr); + if (timer || rule) + { + HELPERS::ShowOKDialogText( + CVariant{19033}, + CVariant{19034}); // "Information", "There is already a timer set for this event" + return false; + } + + std::shared_ptr<CPVRTimerInfoTag> newTimer( + epgTag ? CPVRTimerInfoTag::CreateFromEpg(epgTag, bCreateRule) + : CPVRTimerInfoTag::CreateInstantTimerTag(channel)); + if (!newTimer) + { + if (bCreateRule && bFallbackToOneShotTimer) + newTimer = CPVRTimerInfoTag::CreateFromEpg(epgTag, false); + + if (!newTimer) + { + HELPERS::ShowOKDialogText( + CVariant{19033}, // "Information" + bCreateRule ? CVariant{19095} // Timer rule creation failed. Unsupported timer type. + : CVariant{19094}); // Timer creation failed. Unsupported timer type. + return false; + } + } + + if (bShowTimerSettings) + { + if (!ShowTimerSettings(newTimer)) + return false; + } + + return AddTimer(newTimer); +} + +bool CPVRGUIActionsTimers::AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& item) const +{ + if (!item->Channel() && !item->GetTimerType()->IsEpgBasedTimerRule()) + { + CLog::LogF(LOGERROR, "No channel given"); + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19109}); // "Error", "Could not save the timer. Check the log for more information about this message." + return false; + } + + if (!item->IsTimerRule() && item->GetEpgInfoTag() && !item->GetEpgInfoTag()->IsRecordable()) + { + HELPERS::ShowOKDialogText( + CVariant{19033}, + CVariant{19189}); // "Information", "The PVR backend does not allow to record this event." + return false; + } + + if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock( + item->Channel()) != ParentalCheckResult::SUCCESS) + return false; + + if (!CServiceBroker::GetPVRManager().Timers()->AddTimer(item)) + { + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19109}); // "Error", "Could not save the timer. Check the log for more information about this message." + return false; + } + + return true; +} + +namespace +{ +enum PVRRECORD_INSTANTRECORDACTION +{ + NONE = -1, + RECORD_CURRENT_SHOW = 0, + RECORD_INSTANTRECORDTIME = 1, + ASK = 2, + RECORD_30_MINUTES = 3, + RECORD_60_MINUTES = 4, + RECORD_120_MINUTES = 5, + RECORD_NEXT_SHOW = 6 +}; + +class InstantRecordingActionSelector +{ +public: + explicit InstantRecordingActionSelector(int iInstantRecordTime); + virtual ~InstantRecordingActionSelector() = default; + + void AddAction(PVRRECORD_INSTANTRECORDACTION eAction, const std::string& title); + void PreSelectAction(PVRRECORD_INSTANTRECORDACTION eAction); + PVRRECORD_INSTANTRECORDACTION Select(); + +private: + int m_iInstantRecordTime; + CGUIDialogSelect* m_pDlgSelect; // not owner! + std::map<PVRRECORD_INSTANTRECORDACTION, int> m_actions; +}; + +InstantRecordingActionSelector::InstantRecordingActionSelector(int iInstantRecordTime) + : m_iInstantRecordTime(iInstantRecordTime), + m_pDlgSelect(CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT)) +{ + if (m_pDlgSelect) + { + m_pDlgSelect->Reset(); + m_pDlgSelect->SetMultiSelection(false); + m_pDlgSelect->SetHeading(CVariant{19086}); // Instant recording action + } + else + { + CLog::LogF(LOGERROR, "Unable to obtain WINDOW_DIALOG_SELECT instance"); + } +} + +void InstantRecordingActionSelector::AddAction(PVRRECORD_INSTANTRECORDACTION eAction, + const std::string& title) +{ + if (m_actions.find(eAction) == m_actions.end()) + { + switch (eAction) + { + case RECORD_INSTANTRECORDTIME: + m_pDlgSelect->Add( + StringUtils::Format(g_localizeStrings.Get(19090), + m_iInstantRecordTime)); // Record next <default duration> minutes + break; + case RECORD_30_MINUTES: + m_pDlgSelect->Add( + StringUtils::Format(g_localizeStrings.Get(19090), 30)); // Record next 30 minutes + break; + case RECORD_60_MINUTES: + m_pDlgSelect->Add( + StringUtils::Format(g_localizeStrings.Get(19090), 60)); // Record next 60 minutes + break; + case RECORD_120_MINUTES: + m_pDlgSelect->Add( + StringUtils::Format(g_localizeStrings.Get(19090), 120)); // Record next 120 minutes + break; + case RECORD_CURRENT_SHOW: + m_pDlgSelect->Add(StringUtils::Format(g_localizeStrings.Get(19091), + title)); // Record current show (<title>) + break; + case RECORD_NEXT_SHOW: + m_pDlgSelect->Add(StringUtils::Format(g_localizeStrings.Get(19092), + title)); // Record next show (<title>) + break; + case NONE: + case ASK: + default: + return; + } + + m_actions.insert(std::make_pair(eAction, static_cast<int>(m_actions.size()))); + } +} + +void InstantRecordingActionSelector::PreSelectAction(PVRRECORD_INSTANTRECORDACTION eAction) +{ + const auto& it = m_actions.find(eAction); + if (it != m_actions.end()) + m_pDlgSelect->SetSelected(it->second); +} + +PVRRECORD_INSTANTRECORDACTION InstantRecordingActionSelector::Select() +{ + PVRRECORD_INSTANTRECORDACTION eAction = NONE; + + m_pDlgSelect->Open(); + + if (m_pDlgSelect->IsConfirmed()) + { + int iSelection = m_pDlgSelect->GetSelectedItem(); + const auto it = + std::find_if(m_actions.cbegin(), m_actions.cend(), + [iSelection](const auto& action) { return action.second == iSelection; }); + + if (it != m_actions.cend()) + eAction = (*it).first; + } + + return eAction; +} + +} // unnamed namespace + +bool CPVRGUIActionsTimers::ToggleRecordingOnPlayingChannel() +{ + const std::shared_ptr<CPVRChannel> channel = + CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + if (channel && channel->CanRecord()) + return SetRecordingOnChannel( + channel, !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel)); + + return false; +} + +bool CPVRGUIActionsTimers::SetRecordingOnChannel(const std::shared_ptr<CPVRChannel>& channel, + bool bOnOff) +{ + bool bReturn = false; + + if (!channel) + return bReturn; + + if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(channel) != + ParentalCheckResult::SUCCESS) + return bReturn; + + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(channel->ClientID()); + if (client && client->GetClientCapabilities().SupportsTimers()) + { + /* timers are supported on this channel */ + if (bOnOff && !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel)) + { + std::shared_ptr<CPVREpgInfoTag> epgTag; + int iDuration = m_settings.GetIntValue(CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME); + + int iAction = m_settings.GetIntValue(CSettings::SETTING_PVRRECORD_INSTANTRECORDACTION); + switch (iAction) + { + case RECORD_CURRENT_SHOW: + epgTag = channel->GetEPGNow(); + break; + + case RECORD_INSTANTRECORDTIME: + epgTag.reset(); + break; + + case ASK: + { + PVRRECORD_INSTANTRECORDACTION ePreselect = RECORD_INSTANTRECORDTIME; + const int iDurationDefault = + m_settings.GetIntValue(CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME); + InstantRecordingActionSelector selector(iDurationDefault); + std::shared_ptr<CPVREpgInfoTag> epgTagNext; + + // fixed length recordings + selector.AddAction(RECORD_30_MINUTES, ""); + selector.AddAction(RECORD_60_MINUTES, ""); + selector.AddAction(RECORD_120_MINUTES, ""); + + if (iDurationDefault != 30 && iDurationDefault != 60 && iDurationDefault != 120) + selector.AddAction(RECORD_INSTANTRECORDTIME, ""); + + // epg-based recordings + epgTag = channel->GetEPGNow(); + if (epgTag) + { + bool bLocked = CServiceBroker::GetPVRManager().IsParentalLocked(epgTag); + + // "now" + const std::string currentTitle = + bLocked ? g_localizeStrings.Get(19266) /* Parental locked */ : epgTag->Title(); + selector.AddAction(RECORD_CURRENT_SHOW, currentTitle); + ePreselect = RECORD_CURRENT_SHOW; + + // "next" + epgTagNext = channel->GetEPGNext(); + if (epgTagNext) + { + const std::string nextTitle = bLocked + ? g_localizeStrings.Get(19266) /* Parental locked */ + : epgTagNext->Title(); + selector.AddAction(RECORD_NEXT_SHOW, nextTitle); + + // be smart. if current show is almost over, preselect next show. + if (epgTag->ProgressPercentage() > 90.0f) + ePreselect = RECORD_NEXT_SHOW; + } + } + + if (ePreselect == RECORD_INSTANTRECORDTIME) + { + if (iDurationDefault == 30) + ePreselect = RECORD_30_MINUTES; + else if (iDurationDefault == 60) + ePreselect = RECORD_60_MINUTES; + else if (iDurationDefault == 120) + ePreselect = RECORD_120_MINUTES; + } + + selector.PreSelectAction(ePreselect); + + PVRRECORD_INSTANTRECORDACTION eSelected = selector.Select(); + switch (eSelected) + { + case NONE: + return false; // dialog canceled + + case RECORD_30_MINUTES: + iDuration = 30; + epgTag.reset(); + break; + + case RECORD_60_MINUTES: + iDuration = 60; + epgTag.reset(); + break; + + case RECORD_120_MINUTES: + iDuration = 120; + epgTag.reset(); + break; + + case RECORD_INSTANTRECORDTIME: + iDuration = iDurationDefault; + epgTag.reset(); + break; + + case RECORD_CURRENT_SHOW: + break; + + case RECORD_NEXT_SHOW: + epgTag = epgTagNext; + break; + + default: + CLog::LogF(LOGERROR, + "Unknown instant record action selection ({}), defaulting to fixed " + "length recording.", + static_cast<int>(eSelected)); + epgTag.reset(); + break; + } + break; + } + + default: + CLog::LogF(LOGERROR, + "Unknown instant record action setting value ({}), defaulting to fixed " + "length recording.", + iAction); + break; + } + + const std::shared_ptr<CPVRTimerInfoTag> newTimer( + epgTag ? CPVRTimerInfoTag::CreateFromEpg(epgTag, false) + : CPVRTimerInfoTag::CreateInstantTimerTag(channel, iDuration)); + + if (newTimer) + bReturn = CServiceBroker::GetPVRManager().Timers()->AddTimer(newTimer); + + if (!bReturn) + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19164}); // "Error", "Could not start recording. Check the log for more information about this message." + } + else if (!bOnOff && CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel)) + { + /* delete active timers */ + bReturn = + CServiceBroker::GetPVRManager().Timers()->DeleteTimersOnChannel(channel, true, true); + + if (!bReturn) + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19170}); // "Error", "Could not stop recording. Check the log for more information about this message." + } + } + + return bReturn; +} + +bool CPVRGUIActionsTimers::ToggleTimer(const CFileItem& item) const +{ + if (!item.HasEPGInfoTag()) + return false; + + const std::shared_ptr<CPVRTimerInfoTag> timer(CPVRItem(item).GetTimerInfoTag()); + if (timer) + { + if (timer->IsRecording()) + return StopRecording(item); + else + return DeleteTimer(item); + } + else + return AddTimer(item, false); +} + +bool CPVRGUIActionsTimers::ToggleTimerState(const CFileItem& item) const +{ + if (!item.HasPVRTimerInfoTag()) + return false; + + const std::shared_ptr<CPVRTimerInfoTag> timer = item.GetPVRTimerInfoTag(); + if (timer->IsDisabled()) + timer->SetState(PVR_TIMER_STATE_SCHEDULED); + else + timer->SetState(PVR_TIMER_STATE_DISABLED); + + if (CServiceBroker::GetPVRManager().Timers()->UpdateTimer(timer)) + return true; + + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19263}); // "Error", "Could not update the timer. Check the log for more information about this message." + return false; +} + +bool CPVRGUIActionsTimers::EditTimer(const CFileItem& item) const +{ + const std::shared_ptr<CPVRTimerInfoTag> timer(CPVRItem(item).GetTimerInfoTag()); + if (!timer) + { + CLog::LogF(LOGERROR, "No timer!"); + return false; + } + + // clone the timer. + const std::shared_ptr<CPVRTimerInfoTag> newTimer(new CPVRTimerInfoTag); + newTimer->UpdateEntry(timer); + + if (ShowTimerSettings(newTimer) && + (!timer->GetTimerType()->IsReadOnly() || timer->GetTimerType()->SupportsEnableDisable())) + { + if (newTimer->GetTimerType() == timer->GetTimerType()) + { + if (CServiceBroker::GetPVRManager().Timers()->UpdateTimer(newTimer)) + return true; + + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19263}); // "Error", "Could not update the timer. Check the log for more information about this message." + return false; + } + else + { + // timer type changed. delete the original timer, then create the new timer. this order is + // important. for instance, the new timer might be a rule which schedules the original timer. + // deleting the original timer after creating the rule would do literally this and we would + // end up with one timer missing wrt to the rule defined by the new timer. + if (DeleteTimer(timer, timer->IsRecording(), false)) + { + if (AddTimer(newTimer)) + return true; + + // rollback. + return AddTimer(timer); + } + } + } + return false; +} + +bool CPVRGUIActionsTimers::EditTimerRule(const CFileItem& item) const +{ + const std::shared_ptr<CFileItem> parentTimer = GetTimerRule(item); + if (parentTimer) + return EditTimer(*parentTimer); + + return false; +} + +std::shared_ptr<CFileItem> CPVRGUIActionsTimers::GetTimerRule(const CFileItem& item) const +{ + std::shared_ptr<CPVRTimerInfoTag> timer; + if (item.HasEPGInfoTag()) + timer = CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(item.GetEPGInfoTag()); + else if (item.HasPVRTimerInfoTag()) + timer = item.GetPVRTimerInfoTag(); + + if (timer) + { + timer = CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer); + if (timer) + return std::make_shared<CFileItem>(timer); + } + return {}; +} + +bool CPVRGUIActionsTimers::DeleteTimer(const CFileItem& item) const +{ + return DeleteTimer(item, false, false); +} + +bool CPVRGUIActionsTimers::DeleteTimerRule(const CFileItem& item) const +{ + return DeleteTimer(item, false, true); +} + +bool CPVRGUIActionsTimers::DeleteTimer(const CFileItem& item, + bool bIsRecording, + bool bDeleteRule) const +{ + std::shared_ptr<CPVRTimerInfoTag> timer; + const std::shared_ptr<CPVRRecording> recording(CPVRItem(item).GetRecording()); + if (recording) + timer = recording->GetRecordingTimer(); + + if (!timer) + timer = CPVRItem(item).GetTimerInfoTag(); + + if (!timer) + { + CLog::LogF(LOGERROR, "No timer!"); + return false; + } + + if (bDeleteRule && !timer->IsTimerRule()) + timer = CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer); + + if (!timer) + { + CLog::LogF(LOGERROR, "No timer rule!"); + return false; + } + + if (bIsRecording) + { + if (ConfirmStopRecording(timer)) + { + if (CServiceBroker::GetPVRManager().Timers()->DeleteTimer(timer, true, false) == + TimerOperationResult::OK) + return true; + + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19170}); // "Error", "Could not stop recording. Check the log for more information about this message." + return false; + } + } + else if (!timer->GetTimerType()->AllowsDelete()) + { + return false; + } + else + { + bool bAlsoDeleteRule(false); + if (ConfirmDeleteTimer(timer, bAlsoDeleteRule)) + return DeleteTimer(timer, false, bAlsoDeleteRule); + } + return false; +} + +bool CPVRGUIActionsTimers::DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer, + bool bIsRecording, + bool bDeleteRule) const +{ + TimerOperationResult result = + CServiceBroker::GetPVRManager().Timers()->DeleteTimer(timer, bIsRecording, bDeleteRule); + switch (result) + { + case TimerOperationResult::RECORDING: + { + // recording running. ask the user if it should be deleted anyway + if (HELPERS::ShowYesNoDialogText( + CVariant{122}, // "Confirm delete" + CVariant{ + 19122}) // "This timer is still recording. Are you sure you want to delete this timer?" + != HELPERS::DialogResponse::CHOICE_YES) + return false; + + return DeleteTimer(timer, true, bDeleteRule); + } + case TimerOperationResult::OK: + { + return true; + } + case TimerOperationResult::FAILED: + { + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19110}); // "Error", "Could not delete the timer. Check the log for more information about this message." + return false; + } + default: + { + CLog::LogF(LOGERROR, "Unhandled TimerOperationResult ({})!", static_cast<int>(result)); + break; + } + } + return false; +} + +bool CPVRGUIActionsTimers::ConfirmDeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer, + bool& bDeleteRule) const +{ + bool bConfirmed(false); + const std::shared_ptr<CPVRTimerInfoTag> parentTimer( + CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer)); + + if (parentTimer && parentTimer->GetTimerType()->AllowsDelete()) + { + // timer was scheduled by a deletable timer rule. prompt user for confirmation for deleting the timer rule, including scheduled timers. + bool bCancel(false); + bDeleteRule = CGUIDialogYesNo::ShowAndGetInput( + CVariant{122}, // "Confirm delete" + CVariant{ + 840}, // "Do you want to delete only this timer or also the timer rule that has scheduled it?" + CVariant{""}, CVariant{timer->Title()}, bCancel, CVariant{841}, // "Only this" + CVariant{593}, // "All" + 0); // no autoclose + bConfirmed = !bCancel; + } + else + { + bDeleteRule = false; + + // prompt user for confirmation for deleting the timer + bConfirmed = CGUIDialogYesNo::ShowAndGetInput( + CVariant{122}, // "Confirm delete" + timer->IsTimerRule() + ? CVariant{845} + // "Are you sure you want to delete this timer rule and all timers it has scheduled?" + : CVariant{846}, // "Are you sure you want to delete this timer?" + CVariant{""}, CVariant{timer->Title()}); + } + + return bConfirmed; +} + +bool CPVRGUIActionsTimers::StopRecording(const CFileItem& item) const +{ + if (!DeleteTimer(item, true, false)) + return false; + + CServiceBroker::GetPVRManager().TriggerRecordingsUpdate(); + return true; +} + +bool CPVRGUIActionsTimers::ConfirmStopRecording( + const std::shared_ptr<CPVRTimerInfoTag>& timer) const +{ + return CGUIDialogYesNo::ShowAndGetInput( + CVariant{847}, // "Confirm stop recording" + CVariant{848}, // "Are you sure you want to stop this recording?" + CVariant{""}, CVariant{timer->Title()}); +} + +namespace +{ +std::string GetAnnouncerText(const std::shared_ptr<CPVRTimerInfoTag>& timer, int idEpg, int idNoEpg) +{ + std::string text; + if (timer->IsEpgBased()) + { + text = StringUtils::Format(g_localizeStrings.Get(idEpg), + timer->Title(), // tv show title + timer->ChannelName(), + timer->StartAsLocalTime().GetAsLocalizedDateTime(false, false)); + } + else + { + text = StringUtils::Format(g_localizeStrings.Get(idNoEpg), timer->ChannelName(), + timer->StartAsLocalTime().GetAsLocalizedDateTime(false, false)); + } + return text; +} + +void AddEventLogEntry(const std::shared_ptr<CPVRTimerInfoTag>& timer, int idEpg, int idNoEpg) +{ + std::string name; + std::string icon; + + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(timer->GetTimerType()->GetClientId()); + if (client) + { + name = client->GetFriendlyName(); + icon = client->Icon(); + } + else + { + name = g_sysinfo.GetAppName(); + icon = "special://xbmc/media/icon256x256.png"; + } + + CPVREventLogJob* job = new CPVREventLogJob; + job->AddEvent(false, // do not display a toast, only log event + EventLevel::Information, // info, no error + name, GetAnnouncerText(timer, idEpg, idNoEpg), icon); + CServiceBroker::GetJobManager()->AddJob(job, nullptr); +} +} // unnamed namespace + +void CPVRGUIActionsTimers::AnnounceReminder(const std::shared_ptr<CPVRTimerInfoTag>& timer) const +{ + if (!timer->IsReminder()) + { + CLog::LogF(LOGERROR, "No reminder timer!"); + return; + } + + if (timer->EndAsUTC() < CDateTime::GetUTCDateTime()) + { + // expired. timer end is in the past. write event log entry. + AddEventLogEntry(timer, 19305, 19306); // Deleted missed PVR reminder ... + return; + } + + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingChannel(timer->Channel())) + { + // no need for an announcement. channel in question is already playing. + return; + } + + // show the reminder dialog + CGUIDialogProgress* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>( + WINDOW_DIALOG_PROGRESS); + if (!dialog) + return; + + dialog->Reset(); + + dialog->SetHeading(CVariant{19312}); // "PVR reminder" + dialog->ShowChoice(0, CVariant{19165}); // "Switch" + + std::string text = GetAnnouncerText(timer, 19307, 19308); // Reminder for ... + + bool bCanRecord = false; + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(timer->ClientID()); + if (client && client->GetClientCapabilities().SupportsTimers()) + { + bCanRecord = true; + dialog->ShowChoice(1, CVariant{264}); // "Record" + dialog->ShowChoice(2, CVariant{222}); // "Cancel" + + if (m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTORECORD)) + text += "\n\n" + g_localizeStrings.Get( + 19309); // (Auto-close of this reminder will schedule a recording...) + else if (m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTOSWITCH)) + text += "\n\n" + g_localizeStrings.Get( + 19331); // (Auto-close of this reminder will switch to channel...) + } + else + { + dialog->ShowChoice(1, CVariant{222}); // "Cancel" + } + + dialog->SetText(text); + dialog->SetPercentage(100); + + dialog->Open(); + + int result = CGUIDialogProgress::CHOICE_NONE; + + static constexpr int PROGRESS_TIMESLICE_MILLISECS = 50; + + const int iWait = m_settings.GetIntValue(CSettings::SETTING_PVRREMINDERS_AUTOCLOSEDELAY) * 1000; + int iRemaining = iWait; + while (iRemaining > 0) + { + result = dialog->GetChoice(); + if (result != CGUIDialogProgress::CHOICE_NONE) + break; + + std::this_thread::sleep_for(std::chrono::milliseconds(PROGRESS_TIMESLICE_MILLISECS)); + + iRemaining -= PROGRESS_TIMESLICE_MILLISECS; + dialog->SetPercentage(iRemaining * 100 / iWait); + dialog->Progress(); + } + + dialog->Close(); + + bool bAutoClosed = (iRemaining <= 0); + bool bSwitch = (result == 0); + bool bRecord = (result == 1); + + if (bAutoClosed) + { + bRecord = (bCanRecord && m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTORECORD)); + bSwitch = m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTOSWITCH); + } + + if (bRecord) + { + std::shared_ptr<CPVRTimerInfoTag> newTimer; + + std::shared_ptr<CPVREpgInfoTag> epgTag = timer->GetEpgInfoTag(); + if (epgTag) + { + newTimer = CPVRTimerInfoTag::CreateFromEpg(epgTag, false); + if (newTimer) + { + // an epgtag can only have max one timer - we need to clear the reminder to be able to + // attach the recording timer + DeleteTimer(timer, false, false); + } + } + else + { + int iDuration = (timer->EndAsUTC() - timer->StartAsUTC()).GetSecondsTotal() / 60; + newTimer = CPVRTimerInfoTag::CreateTimerTag(timer->Channel(), timer->StartAsUTC(), iDuration); + } + + if (newTimer) + { + // schedule recording + AddTimer(CFileItem(newTimer), false); + } + + if (bAutoClosed) + { + AddEventLogEntry(timer, 19310, + 19311); // Scheduled recording for auto-closed PVR reminder ... + } + } + + if (bSwitch) + { + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember( + timer->Channel()); + if (groupMember) + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + CFileItem(groupMember), false); + + if (bAutoClosed) + { + AddEventLogEntry(timer, 19332, + 19333); // Switched channel for auto-closed PVR reminder ... + } + } + } +} + +void CPVRGUIActionsTimers::AnnounceReminders() const +{ + // Prevent multiple yesno dialogs, all on same call stack, due to gui message processing while dialog is open. + if (m_bReminderAnnouncementRunning) + return; + + m_bReminderAnnouncementRunning = true; + std::shared_ptr<CPVRTimerInfoTag> timer = + CServiceBroker::GetPVRManager().Timers()->GetNextReminderToAnnnounce(); + while (timer) + { + AnnounceReminder(timer); + timer = CServiceBroker::GetPVRManager().Timers()->GetNextReminderToAnnnounce(); + } + m_bReminderAnnouncementRunning = false; +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsTimers.h b/xbmc/pvr/guilib/PVRGUIActionsTimers.h new file mode 100644 index 0000000..f6ae8fe --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsTimers.h @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2016-2022 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" +#include "pvr/settings/PVRSettings.h" + +#include <memory> + +class CFileItem; + +namespace PVR +{ +class CPVRChannel; +class CPVRTimerInfoTag; + +class CPVRGUIActionsTimers : public IPVRComponent +{ +public: + CPVRGUIActionsTimers(); + ~CPVRGUIActionsTimers() override = default; + + /*! + * @brief Open the timer settings dialog to create a new tv or radio timer. + * @param bRadio indicates whether a radio or tv timer shall be created. + * @return true on success, false otherwise. + */ + bool AddTimer(bool bRadio) const; + + /*! + * @brief Create a new timer, either interactive or non-interactive. + * @param item containing epg data to create a timer for. item must be an epg tag or a channel. + * @param bShowTimerSettings is used to control whether a settings dialog will be opened prior + * creating the timer. + * @return true, if the timer was created successfully, false otherwise. + */ + bool AddTimer(const CFileItem& item, bool bShowTimerSettings) const; + + /*! + * @brief Add a timer to the client. Doesn't add the timer to the container. The backend will + * do this. + * @return True if it was sent correctly, false if not. + */ + bool AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& item) const; + + /*! + * @brief Create a new timer rule, either interactive or non-interactive. + * @param item containing epg data to create a timer rule for. item must be an epg tag or a + * channel. + * @param bShowTimerSettings is used to control whether a settings dialog will be opened prior + * creating the timer rule. + * @param bFallbackToOneShotTimer if no timer rule can be created, try to create a one-shot + * timer instead. + * @return true, if the timer rule was created successfully, false otherwise. + */ + bool AddTimerRule(const CFileItem& item, + bool bShowTimerSettings, + bool bFallbackToOneShotTimer) const; + + /*! + * @brief Creates or deletes a timer for the given epg tag. + * @param item containing an epg tag. + * @return true on success, false otherwise. + */ + bool ToggleTimer(const CFileItem& item) const; + + /*! + * @brief Toggles a given timer's enabled/disabled state. + * @param item containing a timer. + * @return true on success, false otherwise. + */ + bool ToggleTimerState(const CFileItem& item) const; + + /*! + * @brief Open the timer settings dialog to edit an existing timer. + * @param item containing an epg tag or a timer. + * @return true on success, false otherwise. + */ + bool EditTimer(const CFileItem& item) const; + + /*! + * @brief Open the timer settings dialog to edit an existing timer rule. + * @param item containing an epg tag or a timer. + * @return true on success, false otherwise. + */ + bool EditTimerRule(const CFileItem& item) const; + + /*! + * @brief Get the timer rule for a given timer + * @param item containing an item to query the timer rule for. item must be a timer or an epg tag. + * @return The timer rule item, or nullptr if none was found. + */ + std::shared_ptr<CFileItem> GetTimerRule(const CFileItem& item) const; + + /*! + * @brief Delete a timer, always showing a confirmation dialog. + * @param item containing a timer to delete. item must be a timer, an epg tag or a channel. + * @return true, if the timer was deleted successfully, false otherwise. + */ + bool DeleteTimer(const CFileItem& item) const; + + /*! + * @brief Delete a timer rule, always showing a confirmation dialog. + * @param item containing a timer rule to delete. item must be a timer, an epg tag or a channel. + * @return true, if the timer rule was deleted successfully, false otherwise. + */ + bool DeleteTimerRule(const CFileItem& item) const; + + /*! + * @brief Toggle recording on the currently playing channel, if any. + * @return True if the recording was started or stopped successfully, false otherwise. + */ + bool ToggleRecordingOnPlayingChannel(); + + /*! + * @brief Start or stop recording on a given channel. + * @param channel the channel to start/stop recording. + * @param bOnOff True to start recording, false to stop. + * @return True if the recording was started or stopped successfully, false otherwise. + */ + bool SetRecordingOnChannel(const std::shared_ptr<CPVRChannel>& channel, bool bOnOff); + + /*! + * @brief Stop a currently active recording, always showing a confirmation dialog. + * @param item containing a recording to stop. item must be a timer, an epg tag or a channel. + * @return true, if the recording was stopped successfully, false otherwise. + */ + bool StopRecording(const CFileItem& item) const; + + /*! + * @brief Create a new reminder timer, non-interactive. + * @param item containing epg data to create a reminder timer for. item must be an epg tag. + * @return true, if the timer was created successfully, false otherwise. + */ + bool AddReminder(const CFileItem& item) const; + + /*! + * @brief Announce due reminders, if any. + */ + void AnnounceReminders() const; + +private: + CPVRGUIActionsTimers(const CPVRGUIActionsTimers&) = delete; + CPVRGUIActionsTimers const& operator=(CPVRGUIActionsTimers const&) = delete; + + /*! + * @brief Open the timer settings dialog. + * @param timer containing the timer the settings shall be displayed for. + * @return true, if the dialog was ended successfully, false otherwise. + */ + bool ShowTimerSettings(const std::shared_ptr<CPVRTimerInfoTag>& timer) const; + + /*! + * @brief Add a timer or timer rule, either interactive or non-interactive. + * @param item containing epg data to create a timer or timer rule for. item must be an epg tag + * or a channel. + * @param bCreateteRule denotes whether to create a one-shot timer or a timer rule. + * @param bShowTimerSettings is used to control whether a settings dialog will be opened prior + * creating the timer or timer rule. + * @param bFallbackToOneShotTimer if bCreateteRule is true and no timer rule can be created, try + * to create a one-shot timer instead. + * @return true, if the timer or timer rule was created successfully, false otherwise. + */ + bool AddTimer(const CFileItem& item, + bool bCreateRule, + bool bShowTimerSettings, + bool bFallbackToOneShotTimer) const; + + /*! + * @brief Delete a timer or timer rule, always showing a confirmation dialog. + * @param item containing a timer or timer rule to delete. item must be a timer, an epg tag or + * a channel. + * @param bIsRecording denotes whether the timer is currently recording (controls correct + * confirmation dialog). + * @param bDeleteRule denotes to delete a timer rule. For convenience, one can pass a timer + * created by a rule. + * @return true, if the timer or timer rule was deleted successfully, false otherwise. + */ + bool DeleteTimer(const CFileItem& item, bool bIsRecording, bool bDeleteRule) const; + + /*! + * @brief Delete a timer or timer rule, showing a confirmation dialog in case a timer currently + * recording shall be deleted. + * @param timer containing a timer or timer rule to delete. + * @param bIsRecording denotes whether the timer is currently recording (controls correct + * confirmation dialog). + * @param bDeleteRule denotes to delete a timer rule. For convenience, one can pass a timer + * created by a rule. + * @return true, if the timer or timer rule was deleted successfully, false otherwise. + */ + bool DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer, + bool bIsRecording, + bool bDeleteRule) const; + + /*! + * @brief Open a dialog to confirm timer delete. + * @param timer the timer to delete. + * @param bDeleteRule in: ignored. out, for one shot timer scheduled by a timer rule: true to + * also delete the timer rule that has scheduled this timer, false to only delete the one shot + * timer. out, for one shot timer not scheduled by a timer rule: ignored + * @return true, to proceed with delete, false otherwise. + */ + bool ConfirmDeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer, bool& bDeleteRule) const; + + /*! + * @brief Open a dialog to confirm stop recording. + * @param timer the recording to stop (actually the timer to delete). + * @return true, to proceed with delete, false otherwise. + */ + bool ConfirmStopRecording(const std::shared_ptr<CPVRTimerInfoTag>& timer) const; + + /*! + * @brief Announce and process a reminder timer. + * @param timer The reminder timer. + */ + void AnnounceReminder(const std::shared_ptr<CPVRTimerInfoTag>& timer) const; + + CPVRSettings m_settings; + mutable bool m_bReminderAnnouncementRunning{false}; +}; + +namespace GUI +{ +// pretty scope and name +using Timers = CPVRGUIActionsTimers; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsUtils.cpp b/xbmc/pvr/guilib/PVRGUIActionsUtils.cpp new file mode 100644 index 0000000..28e5582 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsUtils.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016-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 "PVRGUIActionsUtils.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "pvr/PVRManager.h" +#include "pvr/guilib/PVRGUIActionsEPG.h" +#include "pvr/guilib/PVRGUIActionsRecordings.h" + +namespace PVR +{ +bool CPVRGUIActionsUtils::OnInfo(const CFileItem& item) +{ + if (item.HasPVRRecordingInfoTag()) + { + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo(item); + } + else if (item.HasPVRChannelInfoTag() || item.HasPVRTimerInfoTag()) + { + return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(item); + } + else if (item.HasEPGSearchFilter()) + { + return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().EditSavedSearch(item); + } + return false; +} + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsUtils.h b/xbmc/pvr/guilib/PVRGUIActionsUtils.h new file mode 100644 index 0000000..a880548 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsUtils.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" + +class CFileItem; + +namespace PVR +{ +class CPVRGUIActionsUtils : public IPVRComponent +{ +public: + CPVRGUIActionsUtils() = default; + ~CPVRGUIActionsUtils() override = default; + + /*! + * @brief Process info action for the given item. + * @param item The item. + */ + bool OnInfo(const CFileItem& item); + +private: + CPVRGUIActionsUtils(const CPVRGUIActionsUtils&) = delete; + CPVRGUIActionsUtils const& operator=(CPVRGUIActionsUtils const&) = delete; +}; + +namespace GUI +{ +// pretty scope and name +using Utils = CPVRGUIActionsUtils; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp new file mode 100644 index 0000000..a239fe2 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2012-2019 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 "PVRGUIChannelIconUpdater.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "filesystem/Directory.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/guilib/PVRGUIProgressHandler.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/FileUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <map> +#include <string> +#include <vector> + +using namespace PVR; + +void CPVRGUIChannelIconUpdater::SearchAndUpdateMissingChannelIcons() const +{ + const std::string iconPath = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_PVRMENU_ICONPATH); + if (iconPath.empty()) + return; + + // fetch files in icon path for fast lookup + CFileItemList fileItemList; + XFILE::CDirectory::GetDirectory(iconPath, fileItemList, ".jpg|.png|.tbn", XFILE::DIR_FLAG_DEFAULTS); + + if (fileItemList.IsEmpty()) + return; + + CLog::Log(LOGINFO, "Starting PVR channel icon search"); + + // create a map for fast lookup of normalized file base name + std::map<std::string, std::string> fileItemMap; + for (const auto& item : fileItemList) + { + std::string baseName = URIUtils::GetFileName(item->GetPath()); + URIUtils::RemoveExtension(baseName); + StringUtils::ToLower(baseName); + fileItemMap.insert({baseName, item->GetPath()}); + } + + std::unique_ptr<CPVRGUIProgressHandler> progressHandler; + if (!m_groups.empty()) + progressHandler.reset( + new CPVRGUIProgressHandler(g_localizeStrings.Get(19286))); // Searching for channel icons + + for (const auto& group : m_groups) + { + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> members = group->GetMembers(); + int channelIndex = 0; + for (const auto& member : members) + { + const std::shared_ptr<CPVRChannel> channel = member->Channel(); + + progressHandler->UpdateProgress(channel->ChannelName(), channelIndex++, members.size()); + + // skip if an icon is already set and exists + if (CFileUtils::Exists(channel->IconPath())) + continue; + + // reset icon before searching for a new one + channel->SetIconPath(""); + + const std::string strChannelUid = StringUtils::Format("{:08}", channel->UniqueID()); + std::string strLegalClientChannelName = + CUtil::MakeLegalFileName(channel->ClientChannelName()); + StringUtils::ToLower(strLegalClientChannelName); + std::string strLegalChannelName = CUtil::MakeLegalFileName(channel->ChannelName()); + StringUtils::ToLower(strLegalChannelName); + + std::map<std::string, std::string>::iterator itItem; + if ((itItem = fileItemMap.find(strLegalClientChannelName)) != fileItemMap.end() || + (itItem = fileItemMap.find(strLegalChannelName)) != fileItemMap.end() || + (itItem = fileItemMap.find(strChannelUid)) != fileItemMap.end()) + { + channel->SetIconPath(itItem->second, CServiceBroker::GetSettingsComponent() + ->GetAdvancedSettings() + ->m_bPVRAutoScanIconsUserSet); + } + + if (m_bUpdateDb) + channel->Persist(); + } + } +} diff --git a/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h new file mode 100644 index 0000000..d64a41f --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012-2019 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 <memory> +#include <vector> + +namespace PVR +{ + +class CPVRChannelGroup; + +class CPVRGUIChannelIconUpdater +{ +public: + /*! + * @brief ctor. + * @param groups The channel groups for which the channel icons shall be updated. + * @param bUpdateDb If true, persist the changed values in the PVR database. + */ + CPVRGUIChannelIconUpdater(const std::vector<std::shared_ptr<CPVRChannelGroup>>& groups, bool bUpdateDb) + : m_groups(groups), m_bUpdateDb(bUpdateDb) {} + + CPVRGUIChannelIconUpdater() = delete; + CPVRGUIChannelIconUpdater(const CPVRGUIChannelIconUpdater&) = delete; + CPVRGUIChannelIconUpdater& operator=(const CPVRGUIChannelIconUpdater&) = delete; + + virtual ~CPVRGUIChannelIconUpdater() = default; + + /*! + * @brief Search and update missing channel icons. + */ + void SearchAndUpdateMissingChannelIcons() const; + +private: + const std::vector<std::shared_ptr<CPVRChannelGroup>> m_groups; + const bool m_bUpdateDb = false; +}; + +} diff --git a/xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp b/xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp new file mode 100644 index 0000000..0ccfdf8 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp @@ -0,0 +1,373 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIChannelNavigator.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/SystemClock.h" +#include "utils/Job.h" +#include "utils/JobManager.h" +#include "utils/XTimeUtils.h" + +#include <mutex> + +using namespace KODI::GUILIB::GUIINFO; +using namespace PVR; +using namespace std::chrono_literals; + +namespace +{ +class CPVRChannelTimeoutJobBase : public CJob, public IJobCallback +{ +public: + CPVRChannelTimeoutJobBase() = delete; + CPVRChannelTimeoutJobBase(PVR::CPVRGUIChannelNavigator& channelNavigator, + std::chrono::milliseconds timeout) + : m_channelNavigator(channelNavigator) + { + m_delayTimer.Set(timeout); + } + + ~CPVRChannelTimeoutJobBase() override = default; + + virtual void OnTimeout() = 0; + + void OnJobComplete(unsigned int iJobID, bool bSuccess, CJob* job) override {} + + bool DoWork() override + { + while (!ShouldCancel(0, 0)) + { + if (m_delayTimer.IsTimePast()) + { + OnTimeout(); + return true; + } + KODI::TIME::Sleep(10ms); + } + return false; + } + +protected: + PVR::CPVRGUIChannelNavigator& m_channelNavigator; + +private: + XbmcThreads::EndTime<> m_delayTimer; +}; + +class CPVRChannelEntryTimeoutJob : public CPVRChannelTimeoutJobBase +{ +public: + CPVRChannelEntryTimeoutJob(PVR::CPVRGUIChannelNavigator& channelNavigator, + std::chrono::milliseconds timeout) + : CPVRChannelTimeoutJobBase(channelNavigator, timeout) + { + } + ~CPVRChannelEntryTimeoutJob() override = default; + const char* GetType() const override { return "pvr-channel-entry-timeout-job"; } + void OnTimeout() override { m_channelNavigator.SwitchToCurrentChannel(); } +}; + +class CPVRChannelInfoTimeoutJob : public CPVRChannelTimeoutJobBase +{ +public: + CPVRChannelInfoTimeoutJob(PVR::CPVRGUIChannelNavigator& channelNavigator, + std::chrono::milliseconds timeout) + : CPVRChannelTimeoutJobBase(channelNavigator, timeout) + { + } + ~CPVRChannelInfoTimeoutJob() override = default; + const char* GetType() const override { return "pvr-channel-info-timeout-job"; } + void OnTimeout() override { m_channelNavigator.HideInfo(); } +}; +} // unnamed namespace + +CPVRGUIChannelNavigator::CPVRGUIChannelNavigator() +{ + // Note: we cannot subscribe to PlayerInfoProvider here, as we're getting constructed + // before the info providers. We will subscribe once our first subscriber appears. +} + +CPVRGUIChannelNavigator::~CPVRGUIChannelNavigator() +{ + const auto gui = CServiceBroker::GetGUI(); + if (!gui) + return; + + gui->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().Events().Unsubscribe(this); +} + +void CPVRGUIChannelNavigator::SubscribeToShowInfoEventStream() +{ + CServiceBroker::GetGUI() + ->GetInfoManager() + .GetInfoProviders() + .GetPlayerInfoProvider() + .Events() + .Subscribe(this, &CPVRGUIChannelNavigator::Notify); +} + +void CPVRGUIChannelNavigator::CheckAndPublishPreviewAndPlayerShowInfoChangedEvent() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + const bool currentValue = IsPreview() && m_playerShowInfo; + if (m_previewAndPlayerShowInfo != currentValue) + { + m_previewAndPlayerShowInfo = currentValue; + + // inform subscribers + m_events.Publish(PVRPreviewAndPlayerShowInfoChangedEvent(currentValue)); + } +} + +void CPVRGUIChannelNavigator::Notify(const PlayerShowInfoChangedEvent& event) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_playerShowInfo = event.m_showInfo; + CheckAndPublishPreviewAndPlayerShowInfoChangedEvent(); +} + +void CPVRGUIChannelNavigator::SelectNextChannel(ChannelSwitchMode eSwitchMode) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (!m_playerShowInfo && eSwitchMode == ChannelSwitchMode::NO_SWITCH) + { + // show info for current channel on first next channel selection. + ShowInfo(false); + return; + } + + const std::shared_ptr<CPVRChannelGroupMember> nextMember = GetNextOrPrevChannel(true); + if (nextMember) + SelectChannel(nextMember, eSwitchMode); +} + +void CPVRGUIChannelNavigator::SelectPreviousChannel(ChannelSwitchMode eSwitchMode) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (!m_playerShowInfo && eSwitchMode == ChannelSwitchMode::NO_SWITCH) + { + // show info for current channel on first previous channel selection. + ShowInfo(false); + return; + } + + const std::shared_ptr<CPVRChannelGroupMember> prevMember = GetNextOrPrevChannel(false); + if (prevMember) + SelectChannel(prevMember, eSwitchMode); +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRGUIChannelNavigator::GetNextOrPrevChannel(bool bNext) +{ + const bool bPlayingRadio = CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio(); + const bool bPlayingTV = CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV(); + + if (bPlayingTV || bPlayingRadio) + { + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(bPlayingRadio); + if (group) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + return bNext ? group->GetNextChannelGroupMember(m_currentChannel) + : group->GetPreviousChannelGroupMember(m_currentChannel); + } + } + return {}; +} + +void CPVRGUIChannelNavigator::SelectChannel( + const std::shared_ptr<CPVRChannelGroupMember>& groupMember, ChannelSwitchMode eSwitchMode) +{ + CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(CFileItem(groupMember)); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_currentChannel = groupMember; + ShowInfo(false); + + CheckAndPublishPreviewAndPlayerShowInfoChangedEvent(); + + if (IsPreview() && eSwitchMode == ChannelSwitchMode::INSTANT_OR_DELAYED_SWITCH) + { + auto timeout = + std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_PVRPLAYBACK_CHANNELENTRYTIMEOUT)); + if (timeout > 0ms) + { + // delayed switch + if (m_iChannelEntryJobId >= 0) + CServiceBroker::GetJobManager()->CancelJob(m_iChannelEntryJobId); + + CPVRChannelEntryTimeoutJob* job = new CPVRChannelEntryTimeoutJob(*this, timeout); + m_iChannelEntryJobId = + CServiceBroker::GetJobManager()->AddJob(job, dynamic_cast<IJobCallback*>(job)); + } + else + { + // instant switch + SwitchToCurrentChannel(); + } + } +} + +void CPVRGUIChannelNavigator::SwitchToCurrentChannel() +{ + std::unique_ptr<CFileItem> item; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iChannelEntryJobId >= 0) + { + CServiceBroker::GetJobManager()->CancelJob(m_iChannelEntryJobId); + m_iChannelEntryJobId = -1; + } + + item = std::make_unique<CFileItem>(m_currentChannel); + } + + if (item) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(*item, false); +} + +bool CPVRGUIChannelNavigator::IsPreview() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_currentChannel != m_playingChannel; +} + +bool CPVRGUIChannelNavigator::IsPreviewAndShowInfo() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_previewAndPlayerShowInfo; +} + +void CPVRGUIChannelNavigator::ShowInfo() +{ + ShowInfo(true); +} + +void CPVRGUIChannelNavigator::ShowInfo(bool bForce) +{ + auto timeout = std::chrono::seconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_PVRMENU_DISPLAYCHANNELINFO)); + + if (bForce || timeout > 0s) + { + CServiceBroker::GetGUI() + ->GetInfoManager() + .GetInfoProviders() + .GetPlayerInfoProvider() + .SetShowInfo(true); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iChannelInfoJobId >= 0) + { + CServiceBroker::GetJobManager()->CancelJob(m_iChannelInfoJobId); + m_iChannelInfoJobId = -1; + } + + if (!bForce && timeout > 0s) + { + CPVRChannelInfoTimeoutJob* job = new CPVRChannelInfoTimeoutJob(*this, timeout); + m_iChannelInfoJobId = + CServiceBroker::GetJobManager()->AddJob(job, dynamic_cast<IJobCallback*>(job)); + } + } +} + +void CPVRGUIChannelNavigator::HideInfo() +{ + CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().SetShowInfo( + false); + + CFileItemPtr item; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iChannelInfoJobId >= 0) + { + CServiceBroker::GetJobManager()->CancelJob(m_iChannelInfoJobId); + m_iChannelInfoJobId = -1; + } + + if (m_currentChannel != m_playingChannel) + { + m_currentChannel = m_playingChannel; + if (m_playingChannel) + item.reset(new CFileItem(m_playingChannel)); + } + + CheckAndPublishPreviewAndPlayerShowInfoChangedEvent(); + } + + if (item) + CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*item); +} + +void CPVRGUIChannelNavigator::ToggleInfo() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_playerShowInfo) + HideInfo(); + else + ShowInfo(); +} + +void CPVRGUIChannelNavigator::SetPlayingChannel( + const std::shared_ptr<CPVRChannelGroupMember>& groupMember) +{ + CFileItemPtr item; + + if (groupMember) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_playingChannel = groupMember; + if (m_currentChannel != m_playingChannel) + { + m_currentChannel = m_playingChannel; + if (m_playingChannel) + item.reset(new CFileItem(m_playingChannel)); + } + + CheckAndPublishPreviewAndPlayerShowInfoChangedEvent(); + } + + if (item) + CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*item); + + ShowInfo(false); +} + +void CPVRGUIChannelNavigator::ClearPlayingChannel() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_playingChannel.reset(); + HideInfo(); + + CheckAndPublishPreviewAndPlayerShowInfoChangedEvent(); +} diff --git a/xbmc/pvr/guilib/PVRGUIChannelNavigator.h b/xbmc/pvr/guilib/PVRGUIChannelNavigator.h new file mode 100644 index 0000000..5c27074 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIChannelNavigator.h @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "utils/EventStream.h" + +#include <memory> + +namespace KODI +{ +namespace GUILIB +{ +namespace GUIINFO +{ +struct PlayerShowInfoChangedEvent; +} +} // namespace GUILIB +} // namespace KODI + +namespace PVR +{ +enum class ChannelSwitchMode +{ + NO_SWITCH, // no channel switch + INSTANT_OR_DELAYED_SWITCH // switch according to SETTING_PVRPLAYBACK_CHANNELENTRYTIMEOUT +}; + +struct PVRPreviewAndPlayerShowInfoChangedEvent +{ + explicit PVRPreviewAndPlayerShowInfoChangedEvent(bool previewAndPlayerShowInfo) + : m_previewAndPlayerShowInfo(previewAndPlayerShowInfo) + { + } + virtual ~PVRPreviewAndPlayerShowInfoChangedEvent() = default; + + bool m_previewAndPlayerShowInfo{false}; +}; + +class CPVRChannelGroupMember; + +class CPVRGUIChannelNavigator +{ +public: + CPVRGUIChannelNavigator(); + virtual ~CPVRGUIChannelNavigator(); + + /*! + * @brief Subscribe to the event stream for changes of channel preview and player show info. + * @param owner The subscriber. + * @param fn The callback function of the subscriber for the events. + */ + template<typename A> + void Subscribe(A* owner, void (A::*fn)(const PVRPreviewAndPlayerShowInfoChangedEvent&)) + { + SubscribeToShowInfoEventStream(); + m_events.Subscribe(owner, fn); + } + + /*! + * @brief Unsubscribe from the event stream for changes of channel preview and player show info. + * @param obj The subscriber. + */ + template<typename A> + void Unsubscribe(A* obj) + { + m_events.Unsubscribe(obj); + } + + /*! + * @brief CEventStream callback for player show info flag changes. + * @param event The event. + */ + void Notify(const KODI::GUILIB::GUIINFO::PlayerShowInfoChangedEvent& event); + + /*! + * @brief Select the next channel in currently playing channel group, relative to the currently + * selected channel. + * @param eSwitchMode controls whether only the channel info OSD is triggered or whether + * additionally a (delayed) channel switch will be done. + */ + void SelectNextChannel(ChannelSwitchMode eSwitchMode); + + /*! + * @brief Select the previous channel in currently playing channel group, relative to the + * currently selected channel. + * @param eSwitchMode controls whether only the channel info OSD is triggered or whether + * additionally a (delayed) channel switch will be done. + */ + void SelectPreviousChannel(ChannelSwitchMode eSwitchMode); + + /*! + * @brief Switch to the currently selected channel. + */ + void SwitchToCurrentChannel(); + + /*! + * @brief Query the state of channel preview. + * @return True, if the currently selected channel is different from the currently playing + * channel, False otherwise. + */ + bool IsPreview() const; + + /*! + * @brief Query the state of channel preview and channel info OSD. + * @return True, if the currently selected channel is different from the currently playing channel + * and channel info OSD is active, False otherwise. + */ + bool IsPreviewAndShowInfo() const; + + /*! + * @brief Show the channel info OSD. + */ + void ShowInfo(); + + /*! + * @brief Hide the channel info OSD. + */ + void HideInfo(); + + /*! + * @brief Toggle the channel info OSD visibility. + */ + void ToggleInfo(); + + /*! + * @brief Set a new playing channel group member and show the channel info OSD for the new + * channel. + * @param groupMember The new playing channel group member + */ + void SetPlayingChannel(const std::shared_ptr<CPVRChannelGroupMember>& groupMember); + + /*! + * @brief Clear the currently playing channel and hide the channel info OSD. + */ + void ClearPlayingChannel(); + +private: + /*! + * @brief Get next or previous channel group member of the playing channel group, relative to the + * currently selected channel group member. + * @param bNext True to get the next channel group member, false to get the previous channel group + * member. + * @param return The channel or nullptr if not found. + */ + std::shared_ptr<CPVRChannelGroupMember> GetNextOrPrevChannel(bool bNext); + + /*! + * @brief Select a given channel group member, display channel info OSD, switch according to given + * switch mode. + * @param groupMember The channel group member to select. + * @param eSwitchMode The channel switch mode. + */ + void SelectChannel(const std::shared_ptr<CPVRChannelGroupMember>& groupMember, + ChannelSwitchMode eSwitchMode); + + /*! + * @brief Show the channel info OSD. + * @param bForce True ignores value of SETTING_PVRMENU_DISPLAYCHANNELINFO and always activates the + * info, False acts aaccording settings value. + */ + void ShowInfo(bool bForce); + + /*! + * @brief Subscribe to the event stream for changes of player show info. + */ + void SubscribeToShowInfoEventStream(); + + /*! + * @brief Check if property preview and show info value changed, inform subscribers in case. + */ + void CheckAndPublishPreviewAndPlayerShowInfoChangedEvent(); + + mutable CCriticalSection m_critSection; + std::shared_ptr<CPVRChannelGroupMember> m_playingChannel; + std::shared_ptr<CPVRChannelGroupMember> m_currentChannel; + int m_iChannelEntryJobId = -1; + int m_iChannelInfoJobId = -1; + CEventSource<PVRPreviewAndPlayerShowInfoChangedEvent> m_events; + bool m_playerShowInfo{false}; + bool m_previewAndPlayerShowInfo{false}; +}; +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIProgressHandler.cpp b/xbmc/pvr/guilib/PVRGUIProgressHandler.cpp new file mode 100644 index 0000000..f876522 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIProgressHandler.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIProgressHandler.h" + +#include "ServiceBroker.h" +#include "dialogs/GUIDialogExtendedProgressBar.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/WindowIDs.h" + +#include <algorithm> +#include <cmath> +#include <mutex> +#include <string> + +using namespace std::chrono_literals; + +namespace PVR +{ + +CPVRGUIProgressHandler::CPVRGUIProgressHandler(const std::string& strTitle) + : CThread("PVRGUIProgressHandler"), m_strTitle(strTitle) +{ +} + +void CPVRGUIProgressHandler::UpdateProgress(const std::string& strText, float fProgress) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bChanged = true; + m_strText = strText; + m_fProgress = fProgress; + + if (!m_bCreated) + { + m_bCreated = true; + Create(); + } +} + +void CPVRGUIProgressHandler::UpdateProgress(const std::string& strText, int iCurrent, int iMax) +{ + float fPercentage = (iCurrent * 100.0f) / iMax; + if (!std::isnan(fPercentage)) + fPercentage = std::min(100.0f, fPercentage); + + UpdateProgress(strText, fPercentage); +} + +void CPVRGUIProgressHandler::Process() +{ + CGUIDialogExtendedProgressBar* progressBar = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>( + WINDOW_DIALOG_EXT_PROGRESS); + if (m_bStop || !progressBar) + return; + + CGUIDialogProgressBarHandle* progressHandle = progressBar->GetHandle(m_strTitle); + if (!progressHandle) + return; + + while (!m_bStop) + { + float fProgress = 0.0; + std::string strText; + bool bUpdate = false; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_bChanged) + { + m_bChanged = false; + fProgress = m_fProgress; + strText = m_strText; + bUpdate = true; + } + } + + if (bUpdate) + { + progressHandle->SetPercentage(fProgress); + progressHandle->SetText(strText); + } + + // Intentionally ignore some changes that come in too fast. Humans cannot read as fast as Mr. Data ;-) + CThread::Sleep(100ms); + } + + progressHandle->MarkFinished(); +} + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIProgressHandler.h b/xbmc/pvr/guilib/PVRGUIProgressHandler.h new file mode 100644 index 0000000..6e7b72f --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIProgressHandler.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "threads/Thread.h" + +#include <string> + +namespace PVR +{ + class CPVRGUIProgressHandler : private CThread + { + public: + CPVRGUIProgressHandler() = delete; + + /*! + * @brief Creates and asynchronously shows a progress dialog with the given title. + * @param strTitle The title for the progress dialog. + */ + explicit CPVRGUIProgressHandler(const std::string& strTitle); + + ~CPVRGUIProgressHandler() override = default; + + /*! + * @brief Update the progress dialogs's content. + * @param strText The new progress text. + * @param fProgress The new progress value, in a range from 0.0 to 100.0. + */ + void UpdateProgress(const std::string& strText, float fProgress); + + /*! + * @brief Update the progress dialogs's content. + * @param strText The new progress text. + * @param iCurrent The new current progress value, must be less or equal iMax. + * @param iMax The new maximum progress value, must be greater or equal iCurrent. + */ + void UpdateProgress(const std::string& strText, int iCurrent, int iMax); + + protected: + // CThread implementation + void Process() override; + + private: + CCriticalSection m_critSection; + const std::string m_strTitle; + std::string m_strText; + float m_fProgress{0.0f}; + bool m_bChanged{false}; + bool m_bCreated{false}; + }; + +} // namespace PVR diff --git a/xbmc/pvr/guilib/guiinfo/CMakeLists.txt b/xbmc/pvr/guilib/guiinfo/CMakeLists.txt new file mode 100644 index 0000000..18491ee --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/CMakeLists.txt @@ -0,0 +1,9 @@ +set(SOURCES PVRGUIInfo.cpp + PVRGUITimerInfo.cpp + PVRGUITimesInfo.cpp) + +set(HEADERS PVRGUIInfo.h + PVRGUITimerInfo.h + PVRGUITimesInfo.h) + +core_add_library(pvr_guilib_guiinfo) diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp new file mode 100644 index 0000000..9cffb9a --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp @@ -0,0 +1,2017 @@ +/* + * 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 "PVRGUIInfo.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/guiinfo/GUIInfo.h" +#include "guilib/guiinfo/GUIInfoLabels.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/channels/PVRRadioRDSInfoTag.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/epg/EpgSearchFilter.h" +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "pvr/providers/PVRProvider.h" +#include "pvr/providers/PVRProviders.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/SingleLock.h" +#include "threads/SystemClock.h" +#include "utils/StringUtils.h" + +#include <cmath> +#include <ctime> +#include <memory> +#include <mutex> +#include <string> +#include <vector> + +using namespace PVR; +using namespace KODI::GUILIB::GUIINFO; +using namespace std::chrono_literals; + +CPVRGUIInfo::CPVRGUIInfo() : CThread("PVRGUIInfo") +{ + ResetProperties(); +} + +void CPVRGUIInfo::ResetProperties() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_anyTimersInfo.ResetProperties(); + m_tvTimersInfo.ResetProperties(); + m_radioTimersInfo.ResetProperties(); + m_timesInfo.Reset(); + m_bHasTVRecordings = false; + m_bHasRadioRecordings = false; + m_iCurrentActiveClient = 0; + m_strPlayingClientName.clear(); + m_strBackendName.clear(); + m_strBackendVersion.clear(); + m_strBackendHost.clear(); + m_strBackendTimers.clear(); + m_strBackendRecordings.clear(); + m_strBackendDeletedRecordings.clear(); + m_strBackendProviders.clear(); + m_strBackendChannelGroups.clear(); + m_strBackendChannels.clear(); + m_iBackendDiskTotal = 0; + m_iBackendDiskUsed = 0; + m_bIsPlayingTV = false; + m_bIsPlayingRadio = false; + m_bIsPlayingRecording = false; + m_bIsPlayingEpgTag = false; + m_bIsPlayingEncryptedStream = false; + m_bIsRecordingPlayingChannel = false; + m_bCanRecordPlayingChannel = false; + m_bIsPlayingActiveRecording = false; + m_bHasTVChannels = false; + m_bHasRadioChannels = false; + + ClearQualityInfo(m_qualityInfo); + ClearDescrambleInfo(m_descrambleInfo); + + m_updateBackendCacheRequested = false; + m_bRegistered = false; +} + +void CPVRGUIInfo::ClearQualityInfo(PVR_SIGNAL_STATUS& qualityInfo) +{ + memset(&qualityInfo, 0, sizeof(qualityInfo)); + strncpy(qualityInfo.strAdapterName, g_localizeStrings.Get(13106).c_str(), + PVR_ADDON_NAME_STRING_LENGTH - 1); + strncpy(qualityInfo.strAdapterStatus, g_localizeStrings.Get(13106).c_str(), + PVR_ADDON_NAME_STRING_LENGTH - 1); +} + +void CPVRGUIInfo::ClearDescrambleInfo(PVR_DESCRAMBLE_INFO& descrambleInfo) +{ + descrambleInfo = {}; +} + +void CPVRGUIInfo::Start() +{ + ResetProperties(); + Create(); + SetPriority(ThreadPriority::BELOW_NORMAL); +} + +void CPVRGUIInfo::Stop() +{ + StopThread(); + + auto& mgr = CServiceBroker::GetPVRManager(); + auto& channels = mgr.Get<PVR::GUI::Channels>(); + channels.GetChannelNavigator().Unsubscribe(this); + channels.Events().Unsubscribe(this); + mgr.Events().Unsubscribe(this); + + CGUIComponent* gui = CServiceBroker::GetGUI(); + if (gui) + { + gui->GetInfoManager().UnregisterInfoProvider(this); + m_bRegistered = false; + } +} + +void CPVRGUIInfo::Notify(const PVREvent& event) +{ + if (event == PVREvent::Timers || event == PVREvent::TimersInvalidated) + UpdateTimersCache(); +} + +void CPVRGUIInfo::Notify(const PVRChannelNumberInputChangedEvent& event) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_channelNumberInput = event.m_input; +} + +void CPVRGUIInfo::Notify(const PVRPreviewAndPlayerShowInfoChangedEvent& event) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_previewAndPlayerShowInfo = event.m_previewAndPlayerShowInfo; +} + +void CPVRGUIInfo::Process() +{ + auto toggleIntervalMs = std::chrono::milliseconds( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRInfoToggleInterval); + XbmcThreads::EndTime<> cacheTimer(toggleIntervalMs); + + auto& mgr = CServiceBroker::GetPVRManager(); + mgr.Events().Subscribe(this, &CPVRGUIInfo::Notify); + + auto& channels = mgr.Get<PVR::GUI::Channels>(); + channels.Events().Subscribe(this, &CPVRGUIInfo::Notify); + channels.GetChannelNavigator().Subscribe(this, &CPVRGUIInfo::Notify); + + /* updated on request */ + UpdateTimersCache(); + + /* update the backend cache once initially */ + m_updateBackendCacheRequested = true; + + while (!g_application.m_bStop && !m_bStop) + { + if (!m_bRegistered) + { + CGUIComponent* gui = CServiceBroker::GetGUI(); + if (gui) + { + gui->GetInfoManager().RegisterInfoProvider(this); + m_bRegistered = true; + } + } + + if (!m_bStop) + UpdateQualityData(); + std::this_thread::yield(); + + if (!m_bStop) + UpdateDescrambleData(); + std::this_thread::yield(); + + if (!m_bStop) + UpdateMisc(); + std::this_thread::yield(); + + if (!m_bStop) + UpdateTimeshiftData(); + std::this_thread::yield(); + + if (!m_bStop) + UpdateTimersToggle(); + std::this_thread::yield(); + + if (!m_bStop) + UpdateNextTimer(); + std::this_thread::yield(); + + // Update the backend cache every toggleInterval seconds + if (!m_bStop && cacheTimer.IsTimePast()) + { + UpdateBackendCache(); + cacheTimer.Set(toggleIntervalMs); + } + + if (!m_bStop) + CThread::Sleep(500ms); + } +} + +void CPVRGUIInfo::UpdateQualityData() +{ + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_PVRPLAYBACK_SIGNALQUALITY)) + return; + + const std::shared_ptr<CPVRPlaybackState> playbackState = + CServiceBroker::GetPVRManager().PlaybackState(); + if (!playbackState) + return; + + PVR_SIGNAL_STATUS qualityInfo; + ClearQualityInfo(qualityInfo); + + const int channelUid = playbackState->GetPlayingChannelUniqueID(); + if (channelUid > 0) + { + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().Clients()->GetCreatedClient( + playbackState->GetPlayingClientID()); + if (client) + client->SignalQuality(channelUid, qualityInfo); + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_qualityInfo = qualityInfo; +} + +void CPVRGUIInfo::UpdateDescrambleData() +{ + const std::shared_ptr<CPVRPlaybackState> playbackState = + CServiceBroker::GetPVRManager().PlaybackState(); + if (!playbackState) + return; + + PVR_DESCRAMBLE_INFO descrambleInfo; + ClearDescrambleInfo(descrambleInfo); + + const int channelUid = playbackState->GetPlayingChannelUniqueID(); + if (channelUid > 0) + { + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().Clients()->GetCreatedClient( + playbackState->GetPlayingClientID()); + if (client) + client->GetDescrambleInfo(channelUid, descrambleInfo); + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_descrambleInfo = descrambleInfo; +} + +void CPVRGUIInfo::UpdateMisc() +{ + const CPVRManager& mgr = CServiceBroker::GetPVRManager(); + bool bStarted = mgr.IsStarted(); + const std::shared_ptr<CPVRPlaybackState> state = mgr.PlaybackState(); + + /* safe to fetch these unlocked, since they're updated from the same thread as this one */ + const std::string strPlayingClientName = bStarted ? state->GetPlayingClientName() : ""; + const bool bHasTVRecordings = bStarted && mgr.Recordings()->GetNumTVRecordings() > 0; + const bool bHasRadioRecordings = bStarted && mgr.Recordings()->GetNumRadioRecordings() > 0; + const bool bIsPlayingTV = bStarted && state->IsPlayingTV(); + const bool bIsPlayingRadio = bStarted && state->IsPlayingRadio(); + const bool bIsPlayingRecording = bStarted && state->IsPlayingRecording(); + const bool bIsPlayingEpgTag = bStarted && state->IsPlayingEpgTag(); + const bool bIsPlayingEncryptedStream = bStarted && state->IsPlayingEncryptedChannel(); + const bool bHasTVChannels = bStarted && mgr.ChannelGroups()->GetGroupAllTV()->HasChannels(); + const bool bHasRadioChannels = bStarted && mgr.ChannelGroups()->GetGroupAllRadio()->HasChannels(); + const bool bCanRecordPlayingChannel = bStarted && state->CanRecordOnPlayingChannel(); + const bool bIsRecordingPlayingChannel = bStarted && state->IsRecordingOnPlayingChannel(); + const bool bIsPlayingActiveRecording = bStarted && state->IsPlayingActiveRecording(); + const std::string strPlayingTVGroup = + (bStarted && bIsPlayingTV) ? state->GetActiveChannelGroup(false)->GroupName() : ""; + const std::string strPlayingRadioGroup = + (bStarted && bIsPlayingRadio) ? state->GetActiveChannelGroup(true)->GroupName() : ""; + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strPlayingClientName = strPlayingClientName; + m_bHasTVRecordings = bHasTVRecordings; + m_bHasRadioRecordings = bHasRadioRecordings; + m_bIsPlayingTV = bIsPlayingTV; + m_bIsPlayingRadio = bIsPlayingRadio; + m_bIsPlayingRecording = bIsPlayingRecording; + m_bIsPlayingEpgTag = bIsPlayingEpgTag; + m_bIsPlayingEncryptedStream = bIsPlayingEncryptedStream; + m_bHasTVChannels = bHasTVChannels; + m_bHasRadioChannels = bHasRadioChannels; + m_strPlayingTVGroup = strPlayingTVGroup; + m_strPlayingRadioGroup = strPlayingRadioGroup; + m_bCanRecordPlayingChannel = bCanRecordPlayingChannel; + m_bIsRecordingPlayingChannel = bIsRecordingPlayingChannel; + m_bIsPlayingActiveRecording = bIsPlayingActiveRecording; +} + +void CPVRGUIInfo::UpdateTimeshiftData() +{ + m_timesInfo.Update(); +} + +bool CPVRGUIInfo::InitCurrentItem(CFileItem* item) +{ + CServiceBroker::GetPVRManager().PublishEvent(PVREvent::CurrentItem); + return false; +} + +bool CPVRGUIInfo::GetLabel(std::string& value, + const CFileItem* item, + int contextWindow, + const CGUIInfo& info, + std::string* fallback) const +{ + return GetListItemAndPlayerLabel(item, info, value) || GetPVRLabel(item, info, value) || + GetRadioRDSLabel(item, info, value); +} + +namespace +{ +std::string GetAsLocalizedDateString(const CDateTime& datetime, bool bLongDate) +{ + return datetime.IsValid() ? datetime.GetAsLocalizedDate(bLongDate) : ""; +} + +std::string GetAsLocalizedTimeString(const CDateTime& datetime) +{ + return datetime.IsValid() ? datetime.GetAsLocalizedTime("", false) : ""; +} + +std::string GetAsLocalizedDateTimeString(const CDateTime& datetime) +{ + return datetime.IsValid() ? datetime.GetAsLocalizedDateTime(false, false) : ""; +} + +std::string GetEpgTagTitle(const std::shared_ptr<CPVREpgInfoTag>& epgTag) +{ + if (epgTag) + { + if (CServiceBroker::GetPVRManager().IsParentalLocked(epgTag)) + return g_localizeStrings.Get(19266); // Parental locked + else if (!epgTag->Title().empty()) + return epgTag->Title(); + } + + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_EPG_HIDENOINFOAVAILABLE)) + return g_localizeStrings.Get(19055); // no information available + + return {}; +} + +} // unnamed namespace + +bool CPVRGUIInfo::GetListItemAndPlayerLabel(const CFileItem* item, + const CGUIInfo& info, + std::string& strValue) const +{ + const std::shared_ptr<CPVRTimerInfoTag> timer = item->GetPVRTimerInfoTag(); + if (timer) + { + switch (info.m_info) + { + case LISTITEM_DATE: + strValue = timer->Summary(); + return true; + case LISTITEM_STARTDATE: + strValue = GetAsLocalizedDateString(timer->StartAsLocalTime(), true); + return true; + case LISTITEM_STARTTIME: + strValue = GetAsLocalizedTimeString(timer->StartAsLocalTime()); + return true; + case LISTITEM_ENDDATE: + strValue = GetAsLocalizedDateString(timer->EndAsLocalTime(), true); + return true; + case LISTITEM_ENDTIME: + strValue = GetAsLocalizedTimeString(timer->EndAsLocalTime()); + return true; + case LISTITEM_DURATION: + if (timer->GetDuration() > 0) + { + strValue = StringUtils::SecondsToTimeString(timer->GetDuration(), + static_cast<TIME_FORMAT>(info.GetData4())); + return true; + } + return false; + case LISTITEM_TITLE: + strValue = timer->Title(); + return true; + case LISTITEM_COMMENT: + strValue = + timer->GetStatus(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == + WINDOW_RADIO_TIMER_RULES); + return true; + case LISTITEM_TIMERTYPE: + strValue = timer->GetTypeAsString(); + return true; + case LISTITEM_CHANNEL_NAME: + strValue = timer->ChannelName(); + return true; + case LISTITEM_EPG_EVENT_TITLE: + case LISTITEM_EPG_EVENT_ICON: + case LISTITEM_GENRE: + case LISTITEM_PLOT: + case LISTITEM_PLOT_OUTLINE: + case LISTITEM_ORIGINALTITLE: + case LISTITEM_YEAR: + case LISTITEM_SEASON: + case LISTITEM_EPISODE: + case LISTITEM_EPISODENAME: + case LISTITEM_DIRECTOR: + case LISTITEM_CHANNEL_NUMBER: + case LISTITEM_PREMIERED: + break; // obtain value from channel/epg + default: + return false; + } + } + + const std::shared_ptr<CPVRRecording> recording(item->GetPVRRecordingInfoTag()); + if (recording) + { + // Note: CPVRRecoding is derived from CVideoInfoTag. All base class properties will be handled + // by CVideoGUIInfoProvider. Only properties introduced by CPVRRecording need to be handled here. + switch (info.m_info) + { + case LISTITEM_DATE: + strValue = GetAsLocalizedDateTimeString(recording->RecordingTimeAsLocalTime()); + return true; + case LISTITEM_STARTDATE: + strValue = GetAsLocalizedDateString(recording->RecordingTimeAsLocalTime(), true); + return true; + case VIDEOPLAYER_STARTTIME: + case LISTITEM_STARTTIME: + strValue = GetAsLocalizedTimeString(recording->RecordingTimeAsLocalTime()); + return true; + case LISTITEM_ENDDATE: + strValue = GetAsLocalizedDateString(recording->EndTimeAsLocalTime(), true); + return true; + case VIDEOPLAYER_ENDTIME: + case LISTITEM_ENDTIME: + strValue = GetAsLocalizedTimeString(recording->EndTimeAsLocalTime()); + return true; + case LISTITEM_EXPIRATION_DATE: + if (recording->HasExpirationTime()) + { + strValue = GetAsLocalizedDateString(recording->ExpirationTimeAsLocalTime(), false); + return true; + } + break; + case LISTITEM_EXPIRATION_TIME: + if (recording->HasExpirationTime()) + { + strValue = GetAsLocalizedTimeString(recording->ExpirationTimeAsLocalTime()); + ; + return true; + } + break; + case VIDEOPLAYER_EPISODENAME: + case LISTITEM_EPISODENAME: + strValue = recording->EpisodeName(); + // fixup multiline episode name strings (which do not fit in any way in our GUI) + StringUtils::Replace(strValue, "\n", ", "); + return true; + case VIDEOPLAYER_CHANNEL_NAME: + case LISTITEM_CHANNEL_NAME: + strValue = recording->ChannelName(); + if (strValue.empty()) + { + if (recording->ProviderName().empty()) + { + const auto& provider = recording->GetProvider(); + strValue = provider->GetName(); + } + else + { + strValue = recording->ProviderName(); + } + } + return true; + case VIDEOPLAYER_CHANNEL_NUMBER: + case LISTITEM_CHANNEL_NUMBER: + { + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(*item); + if (groupMember) + { + strValue = groupMember->ChannelNumber().FormattedChannelNumber(); + return true; + } + break; + } + case LISTITEM_ICON: + if (recording->ClientIconPath().empty() && recording->ClientThumbnailPath().empty() && + // Only use a fallback if there is more than a single provider available + // Note: an add-on itself is a provider, hence we don't use > 0 + CServiceBroker::GetPVRManager().Providers()->GetNumProviders() > 1 && + !recording->Channel()) + { + auto provider = recording->GetProvider(); + if (!provider->GetIconPath().empty()) + { + strValue = provider->GetIconPath(); + return true; + } + } + return false; + case VIDEOPLAYER_CHANNEL_GROUP: + { + std::unique_lock<CCriticalSection> lock(m_critSection); + strValue = recording->IsRadio() ? m_strPlayingRadioGroup : m_strPlayingTVGroup; + return true; + } + case VIDEOPLAYER_PREMIERED: + case LISTITEM_PREMIERED: + if (recording->FirstAired().IsValid()) + { + strValue = recording->FirstAired().GetAsLocalizedDate(); + return true; + } + else if (recording->HasYear()) + { + strValue = std::to_string(recording->GetYear()); + return true; + } + return false; + case LISTITEM_SIZE: + if (recording->GetSizeInBytes() > 0) + { + strValue = StringUtils::SizeToString(recording->GetSizeInBytes()); + return true; + } + return false; + } + return false; + } + + const std::shared_ptr<CPVREpgSearchFilter> filter = item->GetEPGSearchFilter(); + if (filter) + { + switch (info.m_info) + { + case LISTITEM_DATE: + { + CDateTime lastExecLocal; + lastExecLocal.SetFromUTCDateTime(filter->GetLastExecutedDateTime()); + strValue = GetAsLocalizedDateTimeString(lastExecLocal); + if (strValue.empty()) + strValue = g_localizeStrings.Get(10006); // "N/A" + return true; + } + } + return false; + } + + std::shared_ptr<CPVREpgInfoTag> epgTag; + std::shared_ptr<CPVRChannel> channel; + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + CPVRItem pvrItem(item); + channel = pvrItem.GetChannel(); + + switch (info.m_info) + { + case VIDEOPLAYER_NEXT_TITLE: + case VIDEOPLAYER_NEXT_GENRE: + case VIDEOPLAYER_NEXT_PLOT: + case VIDEOPLAYER_NEXT_PLOT_OUTLINE: + case VIDEOPLAYER_NEXT_STARTTIME: + case VIDEOPLAYER_NEXT_ENDTIME: + case VIDEOPLAYER_NEXT_DURATION: + case LISTITEM_NEXT_TITLE: + case LISTITEM_NEXT_GENRE: + case LISTITEM_NEXT_PLOT: + case LISTITEM_NEXT_PLOT_OUTLINE: + case LISTITEM_NEXT_STARTDATE: + case LISTITEM_NEXT_STARTTIME: + case LISTITEM_NEXT_ENDDATE: + case LISTITEM_NEXT_ENDTIME: + case LISTITEM_NEXT_DURATION: + // next playing event + epgTag = pvrItem.GetNextEpgInfoTag(); + break; + default: + // now playing event + epgTag = pvrItem.GetEpgInfoTag(); + break; + } + + switch (info.m_info) + { + // special handling for channels without epg or with radio rds data + case PLAYER_TITLE: + case LISTITEM_TITLE: + case VIDEOPLAYER_NEXT_TITLE: + case LISTITEM_NEXT_TITLE: + case LISTITEM_EPG_EVENT_TITLE: + // Note: in difference to LISTITEM_TITLE, LISTITEM_EPG_EVENT_TITLE returns the title + // associated with the epg event of a timer, if any, and not the title of the timer. + strValue = GetEpgTagTitle(epgTag); + return true; + } + } + + if (epgTag) + { + switch (info.m_info) + { + case VIDEOPLAYER_GENRE: + case LISTITEM_GENRE: + case VIDEOPLAYER_NEXT_GENRE: + case LISTITEM_NEXT_GENRE: + strValue = epgTag->GetGenresLabel(); + return true; + case VIDEOPLAYER_PLOT: + case LISTITEM_PLOT: + case VIDEOPLAYER_NEXT_PLOT: + case LISTITEM_NEXT_PLOT: + if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag)) + strValue = epgTag->Plot(); + return true; + case VIDEOPLAYER_PLOT_OUTLINE: + case LISTITEM_PLOT_OUTLINE: + case VIDEOPLAYER_NEXT_PLOT_OUTLINE: + case LISTITEM_NEXT_PLOT_OUTLINE: + if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag)) + strValue = epgTag->PlotOutline(); + return true; + case LISTITEM_DATE: + strValue = GetAsLocalizedDateTimeString(epgTag->StartAsLocalTime()); + return true; + case LISTITEM_STARTDATE: + case LISTITEM_NEXT_STARTDATE: + strValue = GetAsLocalizedDateString(epgTag->StartAsLocalTime(), true); + return true; + case VIDEOPLAYER_STARTTIME: + case VIDEOPLAYER_NEXT_STARTTIME: + case LISTITEM_STARTTIME: + case LISTITEM_NEXT_STARTTIME: + strValue = GetAsLocalizedTimeString(epgTag->StartAsLocalTime()); + return true; + case LISTITEM_ENDDATE: + case LISTITEM_NEXT_ENDDATE: + strValue = GetAsLocalizedDateString(epgTag->EndAsLocalTime(), true); + return true; + case VIDEOPLAYER_ENDTIME: + case VIDEOPLAYER_NEXT_ENDTIME: + case LISTITEM_ENDTIME: + case LISTITEM_NEXT_ENDTIME: + strValue = GetAsLocalizedTimeString(epgTag->EndAsLocalTime()); + return true; + // note: for some reason, there is no VIDEOPLAYER_DURATION + case LISTITEM_DURATION: + case VIDEOPLAYER_NEXT_DURATION: + case LISTITEM_NEXT_DURATION: + if (epgTag->GetDuration() > 0) + { + strValue = StringUtils::SecondsToTimeString(epgTag->GetDuration(), + static_cast<TIME_FORMAT>(info.GetData4())); + return true; + } + return false; + case VIDEOPLAYER_IMDBNUMBER: + case LISTITEM_IMDBNUMBER: + strValue = epgTag->IMDBNumber(); + return true; + case VIDEOPLAYER_ORIGINALTITLE: + case LISTITEM_ORIGINALTITLE: + if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag)) + strValue = epgTag->OriginalTitle(); + return true; + case VIDEOPLAYER_YEAR: + case LISTITEM_YEAR: + if (epgTag->Year() > 0) + { + strValue = std::to_string(epgTag->Year()); + return true; + } + return false; + case VIDEOPLAYER_SEASON: + case LISTITEM_SEASON: + if (epgTag->SeriesNumber() >= 0) + { + strValue = std::to_string(epgTag->SeriesNumber()); + return true; + } + return false; + case VIDEOPLAYER_EPISODE: + case LISTITEM_EPISODE: + if (epgTag->EpisodeNumber() >= 0) + { + strValue = std::to_string(epgTag->EpisodeNumber()); + return true; + } + return false; + case VIDEOPLAYER_EPISODENAME: + case LISTITEM_EPISODENAME: + if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag)) + { + strValue = epgTag->EpisodeName(); + // fixup multiline episode name strings (which do not fit in any way in our GUI) + StringUtils::Replace(strValue, "\n", ", "); + } + return true; + case VIDEOPLAYER_CAST: + case LISTITEM_CAST: + strValue = epgTag->GetCastLabel(); + return true; + case VIDEOPLAYER_DIRECTOR: + case LISTITEM_DIRECTOR: + strValue = epgTag->GetDirectorsLabel(); + return true; + case VIDEOPLAYER_WRITER: + case LISTITEM_WRITER: + strValue = epgTag->GetWritersLabel(); + return true; + case LISTITEM_EPG_EVENT_ICON: + strValue = epgTag->IconPath(); + return true; + case VIDEOPLAYER_PARENTAL_RATING: + case LISTITEM_PARENTAL_RATING: + if (epgTag->ParentalRating() > 0) + { + strValue = std::to_string(epgTag->ParentalRating()); + return true; + } + return false; + case VIDEOPLAYER_PREMIERED: + case LISTITEM_PREMIERED: + if (epgTag->FirstAired().IsValid()) + { + strValue = epgTag->FirstAired().GetAsLocalizedDate(); + return true; + } + else if (epgTag->Year() > 0) + { + strValue = std::to_string(epgTag->Year()); + return true; + } + return false; + case VIDEOPLAYER_RATING: + case LISTITEM_RATING: + { + int iStarRating = epgTag->StarRating(); + if (iStarRating > 0) + { + strValue = StringUtils::FormatNumber(iStarRating); + return true; + } + return false; + } + } + } + + if (channel) + { + switch (info.m_info) + { + case MUSICPLAYER_CHANNEL_NAME: + { + const std::shared_ptr<CPVRRadioRDSInfoTag> rdsTag = channel->GetRadioRDSInfoTag(); + if (rdsTag) + { + strValue = rdsTag->GetProgStation(); + if (!strValue.empty()) + return true; + } + // fall-thru is intended + [[fallthrough]]; + } + case VIDEOPLAYER_CHANNEL_NAME: + case LISTITEM_CHANNEL_NAME: + strValue = channel->ChannelName(); + return true; + case MUSICPLAYER_CHANNEL_NUMBER: + case VIDEOPLAYER_CHANNEL_NUMBER: + case LISTITEM_CHANNEL_NUMBER: + { + auto groupMember = item->GetPVRChannelGroupMemberInfoTag(); + if (!groupMember) + groupMember = + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember( + *item); + if (groupMember) + { + strValue = groupMember->ChannelNumber().FormattedChannelNumber(); + return true; + } + break; + } + case MUSICPLAYER_CHANNEL_GROUP: + case VIDEOPLAYER_CHANNEL_GROUP: + { + std::unique_lock<CCriticalSection> lock(m_critSection); + strValue = channel->IsRadio() ? m_strPlayingRadioGroup : m_strPlayingTVGroup; + return true; + } + } + } + + return false; +} + +bool CPVRGUIInfo::GetPVRLabel(const CFileItem* item, + const CGUIInfo& info, + std::string& strValue) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + switch (info.m_info) + { + case PVR_EPG_EVENT_ICON: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + if (epgTag) + { + strValue = epgTag->IconPath(); + } + return true; + } + case PVR_EPG_EVENT_DURATION: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + strValue = m_timesInfo.GetEpgEventDuration(epgTag, static_cast<TIME_FORMAT>(info.GetData1())); + return true; + } + case PVR_EPG_EVENT_ELAPSED_TIME: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + strValue = + m_timesInfo.GetEpgEventElapsedTime(epgTag, static_cast<TIME_FORMAT>(info.GetData1())); + return true; + } + case PVR_EPG_EVENT_REMAINING_TIME: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + strValue = + m_timesInfo.GetEpgEventRemainingTime(epgTag, static_cast<TIME_FORMAT>(info.GetData1())); + return true; + } + case PVR_EPG_EVENT_FINISH_TIME: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + strValue = + m_timesInfo.GetEpgEventFinishTime(epgTag, static_cast<TIME_FORMAT>(info.GetData1())); + return true; + } + case PVR_TIMESHIFT_START_TIME: + strValue = m_timesInfo.GetTimeshiftStartTime(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_TIMESHIFT_END_TIME: + strValue = m_timesInfo.GetTimeshiftEndTime(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_TIMESHIFT_PLAY_TIME: + strValue = m_timesInfo.GetTimeshiftPlayTime(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_TIMESHIFT_OFFSET: + strValue = m_timesInfo.GetTimeshiftOffset(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_TIMESHIFT_PROGRESS_DURATION: + strValue = + m_timesInfo.GetTimeshiftProgressDuration(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_TIMESHIFT_PROGRESS_START_TIME: + strValue = + m_timesInfo.GetTimeshiftProgressStartTime(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_TIMESHIFT_PROGRESS_END_TIME: + strValue = m_timesInfo.GetTimeshiftProgressEndTime(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_EPG_EVENT_SEEK_TIME: + { + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + strValue = m_timesInfo.GetEpgEventSeekTime(appPlayer->GetSeekHandler().GetSeekSize(), + static_cast<TIME_FORMAT>(info.GetData1())); + return true; + } + case PVR_NOW_RECORDING_TITLE: + strValue = m_anyTimersInfo.GetActiveTimerTitle(); + return true; + case PVR_NOW_RECORDING_CHANNEL: + strValue = m_anyTimersInfo.GetActiveTimerChannelName(); + return true; + case PVR_NOW_RECORDING_CHAN_ICO: + strValue = m_anyTimersInfo.GetActiveTimerChannelIcon(); + return true; + case PVR_NOW_RECORDING_DATETIME: + strValue = m_anyTimersInfo.GetActiveTimerDateTime(); + return true; + case PVR_NEXT_RECORDING_TITLE: + strValue = m_anyTimersInfo.GetNextTimerTitle(); + return true; + case PVR_NEXT_RECORDING_CHANNEL: + strValue = m_anyTimersInfo.GetNextTimerChannelName(); + return true; + case PVR_NEXT_RECORDING_CHAN_ICO: + strValue = m_anyTimersInfo.GetNextTimerChannelIcon(); + return true; + case PVR_NEXT_RECORDING_DATETIME: + strValue = m_anyTimersInfo.GetNextTimerDateTime(); + return true; + case PVR_TV_NOW_RECORDING_TITLE: + strValue = m_tvTimersInfo.GetActiveTimerTitle(); + return true; + case PVR_TV_NOW_RECORDING_CHANNEL: + strValue = m_tvTimersInfo.GetActiveTimerChannelName(); + return true; + case PVR_TV_NOW_RECORDING_CHAN_ICO: + strValue = m_tvTimersInfo.GetActiveTimerChannelIcon(); + return true; + case PVR_TV_NOW_RECORDING_DATETIME: + strValue = m_tvTimersInfo.GetActiveTimerDateTime(); + return true; + case PVR_TV_NEXT_RECORDING_TITLE: + strValue = m_tvTimersInfo.GetNextTimerTitle(); + return true; + case PVR_TV_NEXT_RECORDING_CHANNEL: + strValue = m_tvTimersInfo.GetNextTimerChannelName(); + return true; + case PVR_TV_NEXT_RECORDING_CHAN_ICO: + strValue = m_tvTimersInfo.GetNextTimerChannelIcon(); + return true; + case PVR_TV_NEXT_RECORDING_DATETIME: + strValue = m_tvTimersInfo.GetNextTimerDateTime(); + return true; + case PVR_RADIO_NOW_RECORDING_TITLE: + strValue = m_radioTimersInfo.GetActiveTimerTitle(); + return true; + case PVR_RADIO_NOW_RECORDING_CHANNEL: + strValue = m_radioTimersInfo.GetActiveTimerChannelName(); + return true; + case PVR_RADIO_NOW_RECORDING_CHAN_ICO: + strValue = m_radioTimersInfo.GetActiveTimerChannelIcon(); + return true; + case PVR_RADIO_NOW_RECORDING_DATETIME: + strValue = m_radioTimersInfo.GetActiveTimerDateTime(); + return true; + case PVR_RADIO_NEXT_RECORDING_TITLE: + strValue = m_radioTimersInfo.GetNextTimerTitle(); + return true; + case PVR_RADIO_NEXT_RECORDING_CHANNEL: + strValue = m_radioTimersInfo.GetNextTimerChannelName(); + return true; + case PVR_RADIO_NEXT_RECORDING_CHAN_ICO: + strValue = m_radioTimersInfo.GetNextTimerChannelIcon(); + return true; + case PVR_RADIO_NEXT_RECORDING_DATETIME: + strValue = m_radioTimersInfo.GetNextTimerDateTime(); + return true; + case PVR_NEXT_TIMER: + strValue = m_anyTimersInfo.GetNextTimer(); + return true; + case PVR_ACTUAL_STREAM_SIG: + CharInfoSignal(strValue); + return true; + case PVR_ACTUAL_STREAM_SNR: + CharInfoSNR(strValue); + return true; + case PVR_ACTUAL_STREAM_BER: + CharInfoBER(strValue); + return true; + case PVR_ACTUAL_STREAM_UNC: + CharInfoUNC(strValue); + return true; + case PVR_ACTUAL_STREAM_CLIENT: + CharInfoPlayingClientName(strValue); + return true; + case PVR_ACTUAL_STREAM_DEVICE: + CharInfoFrontendName(strValue); + return true; + case PVR_ACTUAL_STREAM_STATUS: + CharInfoFrontendStatus(strValue); + return true; + case PVR_ACTUAL_STREAM_CRYPTION: + CharInfoEncryption(strValue); + return true; + case PVR_ACTUAL_STREAM_SERVICE: + CharInfoService(strValue); + return true; + case PVR_ACTUAL_STREAM_MUX: + CharInfoMux(strValue); + return true; + case PVR_ACTUAL_STREAM_PROVIDER: + CharInfoProvider(strValue); + return true; + case PVR_BACKEND_NAME: + CharInfoBackendName(strValue); + return true; + case PVR_BACKEND_VERSION: + CharInfoBackendVersion(strValue); + return true; + case PVR_BACKEND_HOST: + CharInfoBackendHost(strValue); + return true; + case PVR_BACKEND_DISKSPACE: + CharInfoBackendDiskspace(strValue); + return true; + case PVR_BACKEND_PROVIDERS: + CharInfoBackendProviders(strValue); + return true; + case PVR_BACKEND_CHANNEL_GROUPS: + CharInfoBackendChannelGroups(strValue); + return true; + case PVR_BACKEND_CHANNELS: + CharInfoBackendChannels(strValue); + return true; + case PVR_BACKEND_TIMERS: + CharInfoBackendTimers(strValue); + return true; + case PVR_BACKEND_RECORDINGS: + CharInfoBackendRecordings(strValue); + return true; + case PVR_BACKEND_DELETED_RECORDINGS: + CharInfoBackendDeletedRecordings(strValue); + return true; + case PVR_BACKEND_NUMBER: + CharInfoBackendNumber(strValue); + return true; + case PVR_TOTAL_DISKSPACE: + CharInfoTotalDiskSpace(strValue); + return true; + case PVR_CHANNEL_NUMBER_INPUT: + strValue = m_channelNumberInput; + return true; + } + + return false; +} + +bool CPVRGUIInfo::GetRadioRDSLabel(const CFileItem* item, + const CGUIInfo& info, + std::string& strValue) const +{ + if (!item->HasPVRChannelInfoTag()) + return false; + + const std::shared_ptr<CPVRRadioRDSInfoTag> tag = + item->GetPVRChannelInfoTag()->GetRadioRDSInfoTag(); + if (tag) + { + switch (info.m_info) + { + case RDS_CHANNEL_COUNTRY: + strValue = tag->GetCountry(); + return true; + case RDS_TITLE: + strValue = tag->GetTitle(); + return true; + case RDS_ARTIST: + strValue = tag->GetArtist(); + return true; + case RDS_BAND: + strValue = tag->GetBand(); + return true; + case RDS_COMPOSER: + strValue = tag->GetComposer(); + return true; + case RDS_CONDUCTOR: + strValue = tag->GetConductor(); + return true; + case RDS_ALBUM: + strValue = tag->GetAlbum(); + return true; + case RDS_ALBUM_TRACKNUMBER: + if (tag->GetAlbumTrackNumber() > 0) + { + strValue = std::to_string(tag->GetAlbumTrackNumber()); + return true; + } + break; + case RDS_GET_RADIO_STYLE: + strValue = tag->GetRadioStyle(); + return true; + case RDS_COMMENT: + strValue = tag->GetComment(); + return true; + case RDS_INFO_NEWS: + strValue = tag->GetInfoNews(); + return true; + case RDS_INFO_NEWS_LOCAL: + strValue = tag->GetInfoNewsLocal(); + return true; + case RDS_INFO_STOCK: + strValue = tag->GetInfoStock(); + return true; + case RDS_INFO_STOCK_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoStock().size())); + return true; + case RDS_INFO_SPORT: + strValue = tag->GetInfoSport(); + return true; + case RDS_INFO_SPORT_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoSport().size())); + return true; + case RDS_INFO_LOTTERY: + strValue = tag->GetInfoLottery(); + return true; + case RDS_INFO_LOTTERY_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoLottery().size())); + return true; + case RDS_INFO_WEATHER: + strValue = tag->GetInfoWeather(); + return true; + case RDS_INFO_WEATHER_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoWeather().size())); + return true; + case RDS_INFO_HOROSCOPE: + strValue = tag->GetInfoHoroscope(); + return true; + case RDS_INFO_HOROSCOPE_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoHoroscope().size())); + return true; + case RDS_INFO_CINEMA: + strValue = tag->GetInfoCinema(); + return true; + case RDS_INFO_CINEMA_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoCinema().size())); + return true; + case RDS_INFO_OTHER: + strValue = tag->GetInfoOther(); + return true; + case RDS_INFO_OTHER_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoOther().size())); + return true; + case RDS_PROG_HOST: + strValue = tag->GetProgHost(); + return true; + case RDS_PROG_EDIT_STAFF: + strValue = tag->GetEditorialStaff(); + return true; + case RDS_PROG_HOMEPAGE: + strValue = tag->GetProgWebsite(); + return true; + case RDS_PROG_STYLE: + strValue = tag->GetProgStyle(); + return true; + case RDS_PHONE_HOTLINE: + strValue = tag->GetPhoneHotline(); + return true; + case RDS_PHONE_STUDIO: + strValue = tag->GetPhoneStudio(); + return true; + case RDS_SMS_STUDIO: + strValue = tag->GetSMSStudio(); + return true; + case RDS_EMAIL_HOTLINE: + strValue = tag->GetEMailHotline(); + return true; + case RDS_EMAIL_STUDIO: + strValue = tag->GetEMailStudio(); + return true; + case RDS_PROG_STATION: + strValue = tag->GetProgStation(); + return true; + case RDS_PROG_NOW: + strValue = tag->GetProgNow(); + return true; + case RDS_PROG_NEXT: + strValue = tag->GetProgNext(); + return true; + case RDS_AUDIO_LANG: + strValue = tag->GetLanguage(); + return true; + case RDS_GET_RADIOTEXT_LINE: + strValue = tag->GetRadioText(info.GetData1()); + return true; + } + } + return false; +} + +bool CPVRGUIInfo::GetFallbackLabel(std::string& value, + const CFileItem* item, + int contextWindow, + const CGUIInfo& info, + std::string* fallback) +{ + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + switch (info.m_info) + { + ///////////////////////////////////////////////////////////////////////////////////////////// + // VIDEOPLAYER_*, MUSICPLAYER_* + ///////////////////////////////////////////////////////////////////////////////////////////// + case VIDEOPLAYER_TITLE: + case MUSICPLAYER_TITLE: + value = GetEpgTagTitle(CPVRItem(item).GetEpgInfoTag()); + return !value.empty(); + default: + break; + } + } + return false; +} + +bool CPVRGUIInfo::GetInt(int& value, + const CGUIListItem* item, + int contextWindow, + const CGUIInfo& info) const +{ + if (!item->IsFileItem()) + return false; + + const CFileItem* fitem = static_cast<const CFileItem*>(item); + return GetListItemAndPlayerInt(fitem, info, value) || GetPVRInt(fitem, info, value); +} + +bool CPVRGUIInfo::GetListItemAndPlayerInt(const CFileItem* item, + const CGUIInfo& info, + int& iValue) const +{ + switch (info.m_info) + { + case LISTITEM_PROGRESS: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag(); + if (epgTag) + iValue = static_cast<int>(epgTag->ProgressPercentage()); + } + return true; + } + return false; +} + +bool CPVRGUIInfo::GetPVRInt(const CFileItem* item, const CGUIInfo& info, int& iValue) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + switch (info.m_info) + { + case PVR_EPG_EVENT_DURATION: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + iValue = m_timesInfo.GetEpgEventDuration(epgTag); + return true; + } + case PVR_EPG_EVENT_PROGRESS: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + iValue = m_timesInfo.GetEpgEventProgress(epgTag); + return true; + } + case PVR_TIMESHIFT_PROGRESS: + iValue = m_timesInfo.GetTimeshiftProgress(); + return true; + case PVR_TIMESHIFT_PROGRESS_DURATION: + iValue = m_timesInfo.GetTimeshiftProgressDuration(); + return true; + case PVR_TIMESHIFT_PROGRESS_PLAY_POS: + iValue = m_timesInfo.GetTimeshiftProgressPlayPosition(); + return true; + case PVR_TIMESHIFT_PROGRESS_EPG_START: + iValue = m_timesInfo.GetTimeshiftProgressEpgStart(); + return true; + case PVR_TIMESHIFT_PROGRESS_EPG_END: + iValue = m_timesInfo.GetTimeshiftProgressEpgEnd(); + return true; + case PVR_TIMESHIFT_PROGRESS_BUFFER_START: + iValue = m_timesInfo.GetTimeshiftProgressBufferStart(); + return true; + case PVR_TIMESHIFT_PROGRESS_BUFFER_END: + iValue = m_timesInfo.GetTimeshiftProgressBufferEnd(); + return true; + case PVR_TIMESHIFT_SEEKBAR: + iValue = GetTimeShiftSeekPercent(); + return true; + case PVR_ACTUAL_STREAM_SIG_PROGR: + iValue = std::lrintf(static_cast<float>(m_qualityInfo.iSignal) / 0xFFFF * 100); + return true; + case PVR_ACTUAL_STREAM_SNR_PROGR: + iValue = std::lrintf(static_cast<float>(m_qualityInfo.iSNR) / 0xFFFF * 100); + return true; + case PVR_BACKEND_DISKSPACE_PROGR: + if (m_iBackendDiskTotal > 0) + iValue = std::lrintf(static_cast<float>(m_iBackendDiskUsed) / m_iBackendDiskTotal * 100); + else + iValue = 0xFF; + return true; + } + return false; +} + +bool CPVRGUIInfo::GetBool(bool& value, + const CGUIListItem* item, + int contextWindow, + const CGUIInfo& info) const +{ + if (!item->IsFileItem()) + return false; + + const CFileItem* fitem = static_cast<const CFileItem*>(item); + return GetListItemAndPlayerBool(fitem, info, value) || GetPVRBool(fitem, info, value) || + GetRadioRDSBool(fitem, info, value); +} + +bool CPVRGUIInfo::GetListItemAndPlayerBool(const CFileItem* item, + const CGUIInfo& info, + bool& bValue) const +{ + switch (info.m_info) + { + case LISTITEM_HASARCHIVE: + if (item->IsPVRChannel()) + { + bValue = item->GetPVRChannelInfoTag()->HasArchive(); + return true; + } + break; + case LISTITEM_ISPLAYABLE: + if (item->IsEPG()) + { + bValue = item->GetEPGInfoTag()->IsPlayable(); + return true; + } + break; + case LISTITEM_ISRECORDING: + if (item->IsPVRChannel()) + { + bValue = CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel( + *item->GetPVRChannelInfoTag()); + return true; + } + else if (item->IsEPG() || item->IsPVRTimer()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = timer->IsRecording(); + return true; + } + else if (item->IsPVRRecording()) + { + bValue = item->GetPVRRecordingInfoTag()->IsInProgress(); + return true; + } + break; + case LISTITEM_INPROGRESS: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag(); + if (epgTag) + bValue = epgTag->IsActive(); + return true; + } + break; + case LISTITEM_HASTIMER: + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = true; + return true; + } + break; + case LISTITEM_HASTIMERSCHEDULE: + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = timer->HasParent(); + return true; + } + break; + case LISTITEM_HASREMINDER: + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = timer->IsReminder(); + return true; + } + break; + case LISTITEM_HASREMINDERRULE: + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = timer->IsReminder() && timer->HasParent(); + return true; + } + break; + case LISTITEM_TIMERISACTIVE: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = timer->IsActive(); + break; + } + break; + case LISTITEM_TIMERHASCONFLICT: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = timer->HasConflict(); + return true; + } + break; + case LISTITEM_TIMERHASERROR: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = (timer->IsBroken() && !timer->HasConflict()); + return true; + } + break; + case LISTITEM_HASRECORDING: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag(); + if (epgTag) + bValue = !!CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(epgTag); + return true; + } + break; + case LISTITEM_HAS_EPG: + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag(); + bValue = (epgTag != nullptr); + return true; + } + break; + case LISTITEM_ISENCRYPTED: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVRChannel> channel = CPVRItem(item).GetChannel(); + if (channel) + bValue = channel->IsEncrypted(); + return true; + } + break; + case LISTITEM_IS_NEW: + if (item->IsEPG()) + { + if (item->GetEPGInfoTag()) + { + bValue = item->GetEPGInfoTag()->IsNew(); + return true; + } + } + else if (item->IsPVRRecording()) + { + bValue = item->GetPVRRecordingInfoTag()->IsNew(); + return true; + } + else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag()) + { + bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsNew(); + return true; + } + else if (item->IsPVRChannel()) + { + const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow(); + bValue = epgNow ? epgNow->IsNew() : false; + return true; + } + break; + case LISTITEM_IS_PREMIERE: + if (item->IsEPG()) + { + bValue = item->GetEPGInfoTag()->IsPremiere(); + return true; + } + else if (item->IsPVRRecording()) + { + bValue = item->GetPVRRecordingInfoTag()->IsPremiere(); + return true; + } + else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag()) + { + bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsPremiere(); + return true; + } + else if (item->IsPVRChannel()) + { + const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow(); + bValue = epgNow ? epgNow->IsPremiere() : false; + return true; + } + break; + case LISTITEM_IS_FINALE: + if (item->IsEPG()) + { + bValue = item->GetEPGInfoTag()->IsFinale(); + return true; + } + else if (item->IsPVRRecording()) + { + bValue = item->GetPVRRecordingInfoTag()->IsFinale(); + return true; + } + else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag()) + { + bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsFinale(); + return true; + } + else if (item->IsPVRChannel()) + { + const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow(); + bValue = epgNow ? epgNow->IsFinale() : false; + return true; + } + break; + case LISTITEM_IS_LIVE: + if (item->IsEPG()) + { + bValue = item->GetEPGInfoTag()->IsLive(); + return true; + } + else if (item->IsPVRRecording()) + { + bValue = item->GetPVRRecordingInfoTag()->IsLive(); + return true; + } + else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag()) + { + bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsLive(); + return true; + } + else if (item->IsPVRChannel()) + { + const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow(); + bValue = epgNow ? epgNow->IsLive() : false; + return true; + } + break; + case MUSICPLAYER_CONTENT: + case VIDEOPLAYER_CONTENT: + if (item->IsPVRChannel()) + { + bValue = StringUtils::EqualsNoCase(info.GetData3(), "livetv"); + return bValue; // if no match for this provider, other providers shall be asked. + } + break; + case VIDEOPLAYER_HAS_INFO: + if (item->IsPVRChannel()) + { + bValue = !item->GetPVRChannelInfoTag()->ChannelName().empty(); + return true; + } + break; + case VIDEOPLAYER_HAS_EPG: + if (item->IsPVRChannel()) + { + bValue = (item->GetPVRChannelInfoTag()->GetEPGNow() != nullptr); + return true; + } + break; + case VIDEOPLAYER_CAN_RESUME_LIVE_TV: + if (item->IsPVRRecording()) + { + const std::shared_ptr<CPVRRecording> recording = item->GetPVRRecordingInfoTag(); + const std::shared_ptr<CPVREpg> epg = + recording->Channel() ? recording->Channel()->GetEPG() : nullptr; + const std::shared_ptr<CPVREpgInfoTag> epgTag = + CServiceBroker::GetPVRManager().EpgContainer().GetTagById(epg, + recording->BroadcastUid()); + bValue = (epgTag && epgTag->IsActive()); + return true; + } + break; + case PLAYER_IS_CHANNEL_PREVIEW_ACTIVE: + if (item->IsPVRChannel()) + { + if (m_previewAndPlayerShowInfo) + { + bValue = true; + } + else + { + bValue = !m_videoInfo.valid; + if (bValue && item->GetPVRChannelInfoTag()->IsRadio()) + bValue = !m_audioInfo.valid; + } + return true; + } + break; + } + return false; +} + +bool CPVRGUIInfo::GetPVRBool(const CFileItem* item, const CGUIInfo& info, bool& bValue) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + switch (info.m_info) + { + case PVR_IS_RECORDING: + bValue = m_anyTimersInfo.HasRecordingTimers(); + return true; + case PVR_IS_RECORDING_TV: + bValue = m_tvTimersInfo.HasRecordingTimers(); + return true; + case PVR_IS_RECORDING_RADIO: + bValue = m_radioTimersInfo.HasRecordingTimers(); + return true; + case PVR_HAS_TIMER: + bValue = m_anyTimersInfo.HasTimers(); + return true; + case PVR_HAS_TV_TIMER: + bValue = m_tvTimersInfo.HasTimers(); + return true; + case PVR_HAS_RADIO_TIMER: + bValue = m_radioTimersInfo.HasTimers(); + return true; + case PVR_HAS_TV_CHANNELS: + bValue = m_bHasTVChannels; + return true; + case PVR_HAS_RADIO_CHANNELS: + bValue = m_bHasRadioChannels; + return true; + case PVR_HAS_NONRECORDING_TIMER: + bValue = m_anyTimersInfo.HasNonRecordingTimers(); + return true; + case PVR_HAS_NONRECORDING_TV_TIMER: + bValue = m_tvTimersInfo.HasNonRecordingTimers(); + return true; + case PVR_HAS_NONRECORDING_RADIO_TIMER: + bValue = m_radioTimersInfo.HasNonRecordingTimers(); + return true; + case PVR_IS_PLAYING_TV: + bValue = m_bIsPlayingTV; + return true; + case PVR_IS_PLAYING_RADIO: + bValue = m_bIsPlayingRadio; + return true; + case PVR_IS_PLAYING_RECORDING: + bValue = m_bIsPlayingRecording; + return true; + case PVR_IS_PLAYING_EPGTAG: + bValue = m_bIsPlayingEpgTag; + return true; + case PVR_ACTUAL_STREAM_ENCRYPTED: + bValue = m_bIsPlayingEncryptedStream; + return true; + case PVR_IS_TIMESHIFTING: + bValue = m_timesInfo.IsTimeshifting(); + return true; + case PVR_CAN_RECORD_PLAYING_CHANNEL: + bValue = m_bCanRecordPlayingChannel; + return true; + case PVR_IS_RECORDING_PLAYING_CHANNEL: + bValue = m_bIsRecordingPlayingChannel; + return true; + case PVR_IS_PLAYING_ACTIVE_RECORDING: + bValue = m_bIsPlayingActiveRecording; + return true; + } + return false; +} + +bool CPVRGUIInfo::GetRadioRDSBool(const CFileItem* item, const CGUIInfo& info, bool& bValue) const +{ + if (!item->HasPVRChannelInfoTag()) + return false; + + const std::shared_ptr<CPVRRadioRDSInfoTag> tag = + item->GetPVRChannelInfoTag()->GetRadioRDSInfoTag(); + if (tag) + { + switch (info.m_info) + { + case RDS_HAS_RADIOTEXT: + bValue = tag->IsPlayingRadioText(); + return true; + case RDS_HAS_RADIOTEXT_PLUS: + bValue = tag->IsPlayingRadioTextPlus(); + return true; + case RDS_HAS_HOTLINE_DATA: + bValue = (!tag->GetEMailHotline().empty() || !tag->GetPhoneHotline().empty()); + return true; + case RDS_HAS_STUDIO_DATA: + bValue = (!tag->GetEMailStudio().empty() || !tag->GetSMSStudio().empty() || + !tag->GetPhoneStudio().empty()); + return true; + } + } + + switch (info.m_info) + { + case RDS_HAS_RDS: + { + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + bValue = appPlayer->IsPlayingRDS(); + return true; + } + } + + return false; +} + +void CPVRGUIInfo::CharInfoBackendNumber(std::string& strValue) const +{ + size_t numBackends = m_backendProperties.size(); + + if (numBackends > 0) + strValue = StringUtils::Format("{0} {1} {2}", m_iCurrentActiveClient + 1, + g_localizeStrings.Get(20163), numBackends); + else + strValue = g_localizeStrings.Get(14023); +} + +void CPVRGUIInfo::CharInfoTotalDiskSpace(std::string& strValue) const +{ + strValue = StringUtils::SizeToString(m_iBackendDiskTotal).c_str(); +} + +void CPVRGUIInfo::CharInfoSignal(std::string& strValue) const +{ + strValue = StringUtils::Format("{} %", m_qualityInfo.iSignal / 655); +} + +void CPVRGUIInfo::CharInfoSNR(std::string& strValue) const +{ + strValue = StringUtils::Format("{} %", m_qualityInfo.iSNR / 655); +} + +void CPVRGUIInfo::CharInfoBER(std::string& strValue) const +{ + strValue = StringUtils::Format("{:08X}", m_qualityInfo.iBER); +} + +void CPVRGUIInfo::CharInfoUNC(std::string& strValue) const +{ + strValue = StringUtils::Format("{:08X}", m_qualityInfo.iUNC); +} + +void CPVRGUIInfo::CharInfoFrontendName(std::string& strValue) const +{ + if (!strlen(m_qualityInfo.strAdapterName)) + strValue = g_localizeStrings.Get(13205); + else + strValue = m_qualityInfo.strAdapterName; +} + +void CPVRGUIInfo::CharInfoFrontendStatus(std::string& strValue) const +{ + if (!strlen(m_qualityInfo.strAdapterStatus)) + strValue = g_localizeStrings.Get(13205); + else + strValue = m_qualityInfo.strAdapterStatus; +} + +void CPVRGUIInfo::CharInfoBackendName(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendName; +} + +void CPVRGUIInfo::CharInfoBackendVersion(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendVersion; +} + +void CPVRGUIInfo::CharInfoBackendHost(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendHost; +} + +void CPVRGUIInfo::CharInfoBackendDiskspace(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + + auto diskTotal = m_iBackendDiskTotal; + auto diskUsed = m_iBackendDiskUsed; + + if (diskTotal > 0) + { + strValue = StringUtils::Format(g_localizeStrings.Get(802), + StringUtils::SizeToString(diskTotal - diskUsed), + StringUtils::SizeToString(diskTotal)); + } + else + strValue = g_localizeStrings.Get(13205); +} + +void CPVRGUIInfo::CharInfoBackendProviders(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendProviders; +} + +void CPVRGUIInfo::CharInfoBackendChannelGroups(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendChannelGroups; +} + +void CPVRGUIInfo::CharInfoBackendChannels(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendChannels; +} + +void CPVRGUIInfo::CharInfoBackendTimers(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendTimers; +} + +void CPVRGUIInfo::CharInfoBackendRecordings(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendRecordings; +} + +void CPVRGUIInfo::CharInfoBackendDeletedRecordings(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendDeletedRecordings; +} + +void CPVRGUIInfo::CharInfoPlayingClientName(std::string& strValue) const +{ + if (m_strPlayingClientName.empty()) + strValue = g_localizeStrings.Get(13205); + else + strValue = m_strPlayingClientName; +} + +void CPVRGUIInfo::CharInfoEncryption(std::string& strValue) const +{ + if (m_descrambleInfo.iCaid != PVR_DESCRAMBLE_INFO_NOT_AVAILABLE) + { + // prefer dynamically updated info, if available + strValue = CPVRChannel::GetEncryptionName(m_descrambleInfo.iCaid); + return; + } + else + { + const std::shared_ptr<CPVRChannel> channel = + CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + if (channel) + { + strValue = channel->EncryptionName(); + return; + } + } + + strValue.clear(); +} + +void CPVRGUIInfo::CharInfoService(std::string& strValue) const +{ + if (!strlen(m_qualityInfo.strServiceName)) + strValue = g_localizeStrings.Get(13205); + else + strValue = m_qualityInfo.strServiceName; +} + +void CPVRGUIInfo::CharInfoMux(std::string& strValue) const +{ + if (!strlen(m_qualityInfo.strMuxName)) + strValue = g_localizeStrings.Get(13205); + else + strValue = m_qualityInfo.strMuxName; +} + +void CPVRGUIInfo::CharInfoProvider(std::string& strValue) const +{ + if (!strlen(m_qualityInfo.strProviderName)) + strValue = g_localizeStrings.Get(13205); + else + strValue = m_qualityInfo.strProviderName; +} + +void CPVRGUIInfo::UpdateBackendCache() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + // Update the backend information for all backends if + // an update has been requested + if (m_iCurrentActiveClient == 0 && m_updateBackendCacheRequested) + { + std::vector<SBackend> backendProperties; + { + CSingleExit exit(m_critSection); + backendProperties = CServiceBroker::GetPVRManager().Clients()->GetBackendProperties(); + } + + m_backendProperties = backendProperties; + m_updateBackendCacheRequested = false; + } + + // Store some defaults + m_strBackendName = g_localizeStrings.Get(13205); + m_strBackendVersion = g_localizeStrings.Get(13205); + m_strBackendHost = g_localizeStrings.Get(13205); + m_strBackendProviders = g_localizeStrings.Get(13205); + m_strBackendChannelGroups = g_localizeStrings.Get(13205); + m_strBackendChannels = g_localizeStrings.Get(13205); + m_strBackendTimers = g_localizeStrings.Get(13205); + m_strBackendRecordings = g_localizeStrings.Get(13205); + m_strBackendDeletedRecordings = g_localizeStrings.Get(13205); + m_iBackendDiskTotal = 0; + m_iBackendDiskUsed = 0; + + // Update with values from the current client when we have at least one + if (!m_backendProperties.empty()) + { + const auto& backend = m_backendProperties[m_iCurrentActiveClient]; + + m_strBackendName = backend.name; + m_strBackendVersion = backend.version; + m_strBackendHost = backend.host; + + // We always display one extra as the add-on itself counts as a provider + if (backend.numProviders >= 0) + m_strBackendProviders = std::to_string(backend.numProviders + 1); + + if (backend.numChannelGroups >= 0) + m_strBackendChannelGroups = std::to_string(backend.numChannelGroups); + + if (backend.numChannels >= 0) + m_strBackendChannels = std::to_string(backend.numChannels); + + if (backend.numTimers >= 0) + m_strBackendTimers = std::to_string(backend.numTimers); + + if (backend.numRecordings >= 0) + m_strBackendRecordings = std::to_string(backend.numRecordings); + + if (backend.numDeletedRecordings >= 0) + m_strBackendDeletedRecordings = std::to_string(backend.numDeletedRecordings); + + m_iBackendDiskTotal = backend.diskTotal; + m_iBackendDiskUsed = backend.diskUsed; + } + + // Update the current active client, eventually wrapping around + if (++m_iCurrentActiveClient >= m_backendProperties.size()) + m_iCurrentActiveClient = 0; +} + +void CPVRGUIInfo::UpdateTimersCache() +{ + m_anyTimersInfo.UpdateTimersCache(); + m_tvTimersInfo.UpdateTimersCache(); + m_radioTimersInfo.UpdateTimersCache(); +} + +void CPVRGUIInfo::UpdateTimersToggle() +{ + m_anyTimersInfo.UpdateTimersToggle(); + m_tvTimersInfo.UpdateTimersToggle(); + m_radioTimersInfo.UpdateTimersToggle(); +} + +void CPVRGUIInfo::UpdateNextTimer() +{ + m_anyTimersInfo.UpdateNextTimer(); + m_tvTimersInfo.UpdateNextTimer(); + m_radioTimersInfo.UpdateNextTimer(); +} + +int CPVRGUIInfo::GetTimeShiftSeekPercent() const +{ + int progress = m_timesInfo.GetTimeshiftProgressPlayPosition(); + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + int seekSize = appPlayer->GetSeekHandler().GetSeekSize(); + if (seekSize != 0) + { + int total = m_timesInfo.GetTimeshiftProgressDuration(); + + float totalTime = static_cast<float>(total); + if (totalTime == 0.0f) + return 0; + + float percentPerSecond = 100.0f / totalTime; + float percent = progress + percentPerSecond * seekSize; + percent = std::max(0.0f, std::min(percent, 100.0f)); + return std::lrintf(percent); + } + return progress; +} diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h new file mode 100644 index 0000000..5591353 --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h" +#include "guilib/guiinfo/GUIInfoProvider.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/guilib/guiinfo/PVRGUITimerInfo.h" +#include "pvr/guilib/guiinfo/PVRGUITimesInfo.h" +#include "threads/CriticalSection.h" +#include "threads/Thread.h" + +#include <atomic> +#include <string> +#include <vector> + +class CFileItem; + +namespace KODI +{ +namespace GUILIB +{ +namespace GUIINFO +{ +class CGUIInfo; +} +} // namespace GUILIB +} // namespace KODI + +namespace PVR +{ +enum class PVREvent; +struct PVRChannelNumberInputChangedEvent; +struct PVRPreviewAndPlayerShowInfoChangedEvent; + +class CPVRGUIInfo : public KODI::GUILIB::GUIINFO::CGUIInfoProvider, private CThread +{ +public: + CPVRGUIInfo(); + ~CPVRGUIInfo() override = default; + + void Start(); + void Stop(); + + /*! + * @brief CEventStream callback for PVR events. + * @param event The event. + */ + void Notify(const PVREvent& event); + + /*! + * @brief CEventStream callback for channel number input changes. + * @param event The event. + */ + void Notify(const PVRChannelNumberInputChangedEvent& event); + + /*! + * @brief CEventStream callback for channel preview and player show info changes. + * @param event The event. + */ + void Notify(const PVRPreviewAndPlayerShowInfoChangedEvent& event); + + // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation + bool InitCurrentItem(CFileItem* item) override; + bool GetLabel(std::string& value, + const CFileItem* item, + int contextWindow, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + std::string* fallback) const override; + bool GetFallbackLabel(std::string& value, + const CFileItem* item, + int contextWindow, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + std::string* fallback) override; + bool GetInt(int& value, + const CGUIListItem* item, + int contextWindow, + const KODI::GUILIB::GUIINFO::CGUIInfo& info) const override; + bool GetBool(bool& value, + const CGUIListItem* item, + int contextWindow, + const KODI::GUILIB::GUIINFO::CGUIInfo& info) const override; + +private: + void ResetProperties(); + void ClearQualityInfo(PVR_SIGNAL_STATUS& qualityInfo); + void ClearDescrambleInfo(PVR_DESCRAMBLE_INFO& descrambleInfo); + + void Process() override; + + void UpdateTimersCache(); + void UpdateBackendCache(); + void UpdateQualityData(); + void UpdateDescrambleData(); + void UpdateMisc(); + void UpdateNextTimer(); + void UpdateTimeshiftData(); + void UpdateTimeshiftProgressData(); + + void UpdateTimersToggle(); + + bool GetListItemAndPlayerLabel(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + std::string& strValue) const; + bool GetPVRLabel(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + std::string& strValue) const; + bool GetRadioRDSLabel(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + std::string& strValue) const; + + bool GetListItemAndPlayerInt(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + int& iValue) const; + bool GetPVRInt(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + int& iValue) const; + int GetTimeShiftSeekPercent() const; + + bool GetListItemAndPlayerBool(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + bool& bValue) const; + bool GetPVRBool(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + bool& bValue) const; + bool GetRadioRDSBool(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + bool& bValue) const; + + void CharInfoBackendNumber(std::string& strValue) const; + void CharInfoTotalDiskSpace(std::string& strValue) const; + void CharInfoSignal(std::string& strValue) const; + void CharInfoSNR(std::string& strValue) const; + void CharInfoBER(std::string& strValue) const; + void CharInfoUNC(std::string& strValue) const; + void CharInfoFrontendName(std::string& strValue) const; + void CharInfoFrontendStatus(std::string& strValue) const; + void CharInfoBackendName(std::string& strValue) const; + void CharInfoBackendVersion(std::string& strValue) const; + void CharInfoBackendHost(std::string& strValue) const; + void CharInfoBackendDiskspace(std::string& strValue) const; + void CharInfoBackendProviders(std::string& strValue) const; + void CharInfoBackendChannelGroups(std::string& strValue) const; + void CharInfoBackendChannels(std::string& strValue) const; + void CharInfoBackendTimers(std::string& strValue) const; + void CharInfoBackendRecordings(std::string& strValue) const; + void CharInfoBackendDeletedRecordings(std::string& strValue) const; + void CharInfoPlayingClientName(std::string& strValue) const; + void CharInfoEncryption(std::string& strValue) const; + void CharInfoService(std::string& strValue) const; + void CharInfoMux(std::string& strValue) const; + void CharInfoProvider(std::string& strValue) const; + + /** @name PVRGUIInfo data */ + //@{ + CPVRGUIAnyTimerInfo m_anyTimersInfo; // tv + radio + CPVRGUITVTimerInfo m_tvTimersInfo; + CPVRGUIRadioTimerInfo m_radioTimersInfo; + + CPVRGUITimesInfo m_timesInfo; + + bool m_bHasTVRecordings; + bool m_bHasRadioRecordings; + unsigned int m_iCurrentActiveClient; + std::string m_strPlayingClientName; + std::string m_strBackendName; + std::string m_strBackendVersion; + std::string m_strBackendHost; + std::string m_strBackendTimers; + std::string m_strBackendRecordings; + std::string m_strBackendDeletedRecordings; + std::string m_strBackendProviders; + std::string m_strBackendChannelGroups; + std::string m_strBackendChannels; + long long m_iBackendDiskTotal; + long long m_iBackendDiskUsed; + bool m_bIsPlayingTV; + bool m_bIsPlayingRadio; + bool m_bIsPlayingRecording; + bool m_bIsPlayingEpgTag; + bool m_bIsPlayingEncryptedStream; + bool m_bHasTVChannels; + bool m_bHasRadioChannels; + bool m_bCanRecordPlayingChannel; + bool m_bIsRecordingPlayingChannel; + bool m_bIsPlayingActiveRecording; + std::string m_strPlayingTVGroup; + std::string m_strPlayingRadioGroup; + + //@} + + PVR_SIGNAL_STATUS m_qualityInfo; /*!< stream quality information */ + PVR_DESCRAMBLE_INFO m_descrambleInfo; /*!< stream descramble information */ + std::vector<SBackend> m_backendProperties; + + std::string m_channelNumberInput; + bool m_previewAndPlayerShowInfo{false}; + + mutable CCriticalSection m_critSection; + + /** + * The various backend-related fields will only be updated when this + * flag is set. This is done to limit the amount of unnecessary + * backend querying when we're not displaying any of the queried + * information. + */ + mutable std::atomic<bool> m_updateBackendCacheRequested; + + bool m_bRegistered; +}; +} // namespace PVR diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp new file mode 100644 index 0000000..24a468a --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp @@ -0,0 +1,270 @@ +/* + * 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 "PVRGUITimerInfo.h" + +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRManager.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" + +#include <memory> +#include <mutex> +#include <string> +#include <vector> + +using namespace PVR; + +CPVRGUITimerInfo::CPVRGUITimerInfo() +{ + ResetProperties(); +} + +void CPVRGUITimerInfo::ResetProperties() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strActiveTimerTitle.clear(); + m_strActiveTimerChannelName.clear(); + m_strActiveTimerChannelIcon.clear(); + m_strActiveTimerTime.clear(); + m_strNextTimerInfo.clear(); + m_strNextRecordingTitle.clear(); + m_strNextRecordingChannelName.clear(); + m_strNextRecordingChannelIcon.clear(); + m_strNextRecordingTime.clear(); + m_iTimerAmount = 0; + m_iRecordingTimerAmount = 0; + m_iTimerInfoToggleStart = {}; + m_iTimerInfoToggleCurrent = 0; +} + +bool CPVRGUITimerInfo::TimerInfoToggle() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_iTimerInfoToggleStart.time_since_epoch().count() == 0) + { + m_iTimerInfoToggleStart = std::chrono::steady_clock::now(); + m_iTimerInfoToggleCurrent = 0; + return true; + } + + auto now = std::chrono::steady_clock::now(); + auto duration = + std::chrono::duration_cast<std::chrono::milliseconds>(now - m_iTimerInfoToggleStart); + + if (duration.count() > + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRInfoToggleInterval) + { + unsigned int iPrevious = m_iTimerInfoToggleCurrent; + unsigned int iBoundary = m_iRecordingTimerAmount > 0 ? m_iRecordingTimerAmount : m_iTimerAmount; + if (++m_iTimerInfoToggleCurrent > iBoundary - 1) + m_iTimerInfoToggleCurrent = 0; + + if (m_iTimerInfoToggleCurrent != iPrevious) + { + m_iTimerInfoToggleStart = std::chrono::steady_clock::now(); + return true; + } + } + + return false; +} + +void CPVRGUITimerInfo::UpdateTimersToggle() +{ + if (!TimerInfoToggle()) + return; + + std::string strActiveTimerTitle; + std::string strActiveTimerChannelName; + std::string strActiveTimerChannelIcon; + std::string strActiveTimerTime; + + /* safe to fetch these unlocked, since they're updated from the same thread as this one */ + if (m_iRecordingTimerAmount > 0) + { + std::vector<std::shared_ptr<CPVRTimerInfoTag>> activeTags = GetActiveRecordings(); + if (m_iTimerInfoToggleCurrent < activeTags.size()) + { + const std::shared_ptr<CPVRTimerInfoTag> tag = activeTags.at(m_iTimerInfoToggleCurrent); + strActiveTimerTitle = tag->Title(); + strActiveTimerChannelName = tag->ChannelName(); + strActiveTimerChannelIcon = tag->ChannelIcon(); + strActiveTimerTime = tag->StartAsLocalTime().GetAsLocalizedDateTime(false, false); + } + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strActiveTimerTitle = strActiveTimerTitle; + m_strActiveTimerChannelName = strActiveTimerChannelName; + m_strActiveTimerChannelIcon = strActiveTimerChannelIcon; + m_strActiveTimerTime = strActiveTimerTime; +} + +void CPVRGUITimerInfo::UpdateTimersCache() +{ + int iTimerAmount = AmountActiveTimers(); + int iRecordingTimerAmount = AmountActiveRecordings(); + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_iTimerAmount = iTimerAmount; + m_iRecordingTimerAmount = iRecordingTimerAmount; + m_iTimerInfoToggleStart = {}; + } + + UpdateTimersToggle(); +} + +void CPVRGUITimerInfo::UpdateNextTimer() +{ + std::string strNextRecordingTitle; + std::string strNextRecordingChannelName; + std::string strNextRecordingChannelIcon; + std::string strNextRecordingTime; + std::string strNextTimerInfo; + + const std::shared_ptr<CPVRTimerInfoTag> timer = GetNextActiveTimer(); + if (timer) + { + strNextRecordingTitle = timer->Title(); + strNextRecordingChannelName = timer->ChannelName(); + strNextRecordingChannelIcon = timer->ChannelIcon(); + strNextRecordingTime = timer->StartAsLocalTime().GetAsLocalizedDateTime(false, false); + + strNextTimerInfo = StringUtils::Format("{} {} {} {}", g_localizeStrings.Get(19106), + timer->StartAsLocalTime().GetAsLocalizedDate(true), + g_localizeStrings.Get(19107), + timer->StartAsLocalTime().GetAsLocalizedTime("", false)); + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strNextRecordingTitle = strNextRecordingTitle; + m_strNextRecordingChannelName = strNextRecordingChannelName; + m_strNextRecordingChannelIcon = strNextRecordingChannelIcon; + m_strNextRecordingTime = strNextRecordingTime; + m_strNextTimerInfo = strNextTimerInfo; +} + +const std::string& CPVRGUITimerInfo::GetActiveTimerTitle() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strActiveTimerTitle; +} + +const std::string& CPVRGUITimerInfo::GetActiveTimerChannelName() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strActiveTimerChannelName; +} + +const std::string& CPVRGUITimerInfo::GetActiveTimerChannelIcon() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strActiveTimerChannelIcon; +} + +const std::string& CPVRGUITimerInfo::GetActiveTimerDateTime() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strActiveTimerTime; +} + +const std::string& CPVRGUITimerInfo::GetNextTimerTitle() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strNextRecordingTitle; +} + +const std::string& CPVRGUITimerInfo::GetNextTimerChannelName() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strNextRecordingChannelName; +} + +const std::string& CPVRGUITimerInfo::GetNextTimerChannelIcon() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strNextRecordingChannelIcon; +} + +const std::string& CPVRGUITimerInfo::GetNextTimerDateTime() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strNextRecordingTime; +} + +const std::string& CPVRGUITimerInfo::GetNextTimer() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strNextTimerInfo; +} + +int CPVRGUIAnyTimerInfo::AmountActiveTimers() +{ + return CServiceBroker::GetPVRManager().Timers()->AmountActiveTimers(); +} + +int CPVRGUIAnyTimerInfo::AmountActiveRecordings() +{ + return CServiceBroker::GetPVRManager().Timers()->AmountActiveRecordings(); +} + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRGUIAnyTimerInfo::GetActiveRecordings() +{ + return CServiceBroker::GetPVRManager().Timers()->GetActiveRecordings(); +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRGUIAnyTimerInfo::GetNextActiveTimer() +{ + return CServiceBroker::GetPVRManager().Timers()->GetNextActiveTimer(); +} + +int CPVRGUITVTimerInfo::AmountActiveTimers() +{ + return CServiceBroker::GetPVRManager().Timers()->AmountActiveTVTimers(); +} + +int CPVRGUITVTimerInfo::AmountActiveRecordings() +{ + return CServiceBroker::GetPVRManager().Timers()->AmountActiveTVRecordings(); +} + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRGUITVTimerInfo::GetActiveRecordings() +{ + return CServiceBroker::GetPVRManager().Timers()->GetActiveTVRecordings(); +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRGUITVTimerInfo::GetNextActiveTimer() +{ + return CServiceBroker::GetPVRManager().Timers()->GetNextActiveTVTimer(); +} + +int CPVRGUIRadioTimerInfo::AmountActiveTimers() +{ + return CServiceBroker::GetPVRManager().Timers()->AmountActiveRadioTimers(); +} + +int CPVRGUIRadioTimerInfo::AmountActiveRecordings() +{ + return CServiceBroker::GetPVRManager().Timers()->AmountActiveRadioRecordings(); +} + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRGUIRadioTimerInfo::GetActiveRecordings() +{ + return CServiceBroker::GetPVRManager().Timers()->GetActiveRadioRecordings(); +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRGUIRadioTimerInfo::GetNextActiveTimer() +{ + return CServiceBroker::GetPVRManager().Timers()->GetNextActiveRadioTimer(); +} diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h new file mode 100644 index 0000000..260db6c --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" + +#include <chrono> +#include <memory> +#include <string> +#include <vector> + +namespace PVR +{ + class CPVRTimerInfoTag; + + class CPVRGUITimerInfo + { + public: + CPVRGUITimerInfo(); + virtual ~CPVRGUITimerInfo() = default; + + void ResetProperties(); + + void UpdateTimersCache(); + void UpdateTimersToggle(); + void UpdateNextTimer(); + + const std::string& GetActiveTimerTitle() const; + const std::string& GetActiveTimerChannelName() const; + const std::string& GetActiveTimerChannelIcon() const; + const std::string& GetActiveTimerDateTime() const; + const std::string& GetNextTimerTitle() const; + const std::string& GetNextTimerChannelName() const; + const std::string& GetNextTimerChannelIcon() const; + const std::string& GetNextTimerDateTime() const; + const std::string& GetNextTimer() const; + + bool HasTimers() const { return m_iTimerAmount > 0; } + bool HasRecordingTimers() const { return m_iRecordingTimerAmount > 0; } + bool HasNonRecordingTimers() const { return m_iTimerAmount - m_iRecordingTimerAmount > 0; } + + private: + bool TimerInfoToggle(); + + virtual int AmountActiveTimers() = 0; + virtual int AmountActiveRecordings() = 0; + virtual std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() = 0; + virtual std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() = 0; + + unsigned int m_iTimerAmount; + unsigned int m_iRecordingTimerAmount; + + std::string m_strActiveTimerTitle; + std::string m_strActiveTimerChannelName; + std::string m_strActiveTimerChannelIcon; + std::string m_strActiveTimerTime; + std::string m_strNextRecordingTitle; + std::string m_strNextRecordingChannelName; + std::string m_strNextRecordingChannelIcon; + std::string m_strNextRecordingTime; + std::string m_strNextTimerInfo; + + std::chrono::time_point<std::chrono::steady_clock> m_iTimerInfoToggleStart; + unsigned int m_iTimerInfoToggleCurrent; + + mutable CCriticalSection m_critSection; + }; + + class CPVRGUIAnyTimerInfo : public CPVRGUITimerInfo + { + public: + CPVRGUIAnyTimerInfo() = default; + + private: + int AmountActiveTimers() override; + int AmountActiveRecordings() override; + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() override; + std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() override; + }; + + class CPVRGUITVTimerInfo : public CPVRGUITimerInfo + { + public: + CPVRGUITVTimerInfo() = default; + + private: + int AmountActiveTimers() override; + int AmountActiveRecordings() override; + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() override; + std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() override; + }; + + class CPVRGUIRadioTimerInfo : public CPVRGUITimerInfo + { + public: + CPVRGUIRadioTimerInfo() = default; + + private: + int AmountActiveTimers() override; + int AmountActiveRecordings() override; + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() override; + std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() override; + }; + +} // namespace PVR diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp new file mode 100644 index 0000000..e5dfdb9 --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp @@ -0,0 +1,424 @@ +/* + * 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 "PVRGUITimesInfo.h" + +#include "ServiceBroker.h" +#include "cores/DataCacheCore.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/recordings/PVRRecording.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" + +#include <cmath> +#include <ctime> +#include <memory> +#include <mutex> + +using namespace PVR; + +CPVRGUITimesInfo::CPVRGUITimesInfo() +{ + Reset(); +} + +void CPVRGUITimesInfo::Reset() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_iStartTime = 0; + m_iDuration = 0; + m_iTimeshiftStartTime = 0; + m_iTimeshiftEndTime = 0; + m_iTimeshiftPlayTime = 0; + m_iTimeshiftOffset = 0; + + m_iTimeshiftProgressStartTime = 0; + m_iTimeshiftProgressEndTime = 0; + m_iTimeshiftProgressDuration = 0; + + m_playingEpgTag.reset(); + m_playingChannel.reset(); +} + +void CPVRGUITimesInfo::UpdatePlayingTag() +{ + const std::shared_ptr<CPVRChannel> currentChannel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + std::shared_ptr<CPVREpgInfoTag> currentTag = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingEpgTag(); + + if (currentChannel || currentTag) + { + if (currentChannel && !currentTag) + currentTag = currentChannel->GetEPGNow(); + + const std::shared_ptr<CPVRChannelGroupsContainer> groups = CServiceBroker::GetPVRManager().ChannelGroups(); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + const std::shared_ptr<CPVRChannel> playingChannel = + m_playingEpgTag ? groups->GetChannelForEpgTag(m_playingEpgTag) : nullptr; + + if (!m_playingEpgTag || !currentTag || !playingChannel || !currentChannel || + m_playingEpgTag->StartAsUTC() != currentTag->StartAsUTC() || + m_playingEpgTag->EndAsUTC() != currentTag->EndAsUTC() || *playingChannel != *currentChannel) + { + if (currentTag) + { + m_playingEpgTag = currentTag; + m_iDuration = m_playingEpgTag->GetDuration(); + } + else if (m_iTimeshiftEndTime > m_iTimeshiftStartTime) + { + m_playingEpgTag.reset(); + m_iDuration = m_iTimeshiftEndTime - m_iTimeshiftStartTime; + } + else + { + m_playingEpgTag.reset(); + m_iDuration = 0; + } + } + } + else + { + const std::shared_ptr<CPVRRecording> recording = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingRecording(); + if (recording) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_playingEpgTag.reset(); + m_iDuration = recording->GetDuration(); + } + } +} + +void CPVRGUITimesInfo::UpdateTimeshiftData() +{ + if (!CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV() && !CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio()) + { + // If nothing is playing (anymore), there is no need to update data. + Reset(); + return; + } + + time_t now = std::time(nullptr); + time_t iStartTime; + int64_t iPlayTime, iMinTime, iMaxTime; + CServiceBroker::GetDataCacheCore().GetPlayTimes(iStartTime, iPlayTime, iMinTime, iMaxTime); + bool bPlaying = CServiceBroker::GetDataCacheCore().GetSpeed() == 1.0f; + const std::shared_ptr<CPVRChannel> playingChannel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (playingChannel != m_playingChannel) + { + // playing channel changed. we need to reset offset and playtime. + m_iTimeshiftOffset = 0; + m_iTimeshiftPlayTime = 0; + m_playingChannel = playingChannel; + } + + if (!iStartTime) + { + if (m_iStartTime == 0) + iStartTime = now; + else + iStartTime = m_iStartTime; + + iMinTime = iPlayTime; + iMaxTime = iPlayTime; + } + + m_iStartTime = iStartTime; + m_iTimeshiftStartTime = iStartTime + iMinTime / 1000; + m_iTimeshiftEndTime = iStartTime + iMaxTime / 1000; + + if (m_iTimeshiftEndTime > m_iTimeshiftStartTime) + { + // timeshifting supported + m_iTimeshiftPlayTime = iStartTime + iPlayTime / 1000; + if (iMaxTime > iPlayTime) + m_iTimeshiftOffset = (iMaxTime - iPlayTime) / 1000; + else + m_iTimeshiftOffset = 0; + } + else + { + // timeshifting not supported + if (bPlaying) + m_iTimeshiftPlayTime = now - m_iTimeshiftOffset; + + m_iTimeshiftOffset = now - m_iTimeshiftPlayTime; + } + + UpdateTimeshiftProgressData(); +} + +void CPVRGUITimesInfo::UpdateTimeshiftProgressData() +{ + // Note: General idea of the ts progress is always to be able to visualise both the complete + // ts buffer and the complete playing epg event (if any) side by side with the same time + // scale. TS progress start and end times will be calculated accordingly. + // + Start is usually ts buffer start, except if start time of playing epg event is + // before ts buffer start, then progress start is epg event start. + // + End is usually ts buffer end, except if end time of playing epg event is + // after ts buffer end, then progress end is epg event end. + // In simple timeshift mode (settings value), progress start is always the start time of + // playing epg event and progress end is always the end time of playing epg event. + + std::unique_lock<CCriticalSection> lock(m_critSection); + + ////////////////////////////////////////////////////////////////////////////////////// + // start time + ////////////////////////////////////////////////////////////////////////////////////// + bool bUpdatedStartTime = false; + if (m_playingEpgTag) + { + time_t start = 0; + m_playingEpgTag->StartAsUTC().GetAsTime(start); + if (start < m_iTimeshiftStartTime || + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bPVRTimeshiftSimpleOSD) + { + // playing event started before start of ts buffer or simple ts osd to be used + m_iTimeshiftProgressStartTime = start; + bUpdatedStartTime = true; + } + } + + if (!bUpdatedStartTime) + { + // default to ts buffer start + m_iTimeshiftProgressStartTime = m_iTimeshiftStartTime; + } + + ////////////////////////////////////////////////////////////////////////////////////// + // end time + ////////////////////////////////////////////////////////////////////////////////////// + bool bUpdatedEndTime = false; + if (m_playingEpgTag) + { + time_t end = 0; + m_playingEpgTag->EndAsUTC().GetAsTime(end); + if (end > m_iTimeshiftEndTime || + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bPVRTimeshiftSimpleOSD) + { + // playing event will end after end of ts buffer or simple ts osd to be used + m_iTimeshiftProgressEndTime = end; + bUpdatedEndTime = true; + } + } + + if (!bUpdatedEndTime) + { + // default to ts buffer end + m_iTimeshiftProgressEndTime = m_iTimeshiftEndTime; + } + + ////////////////////////////////////////////////////////////////////////////////////// + // duration + ////////////////////////////////////////////////////////////////////////////////////// + m_iTimeshiftProgressDuration = m_iTimeshiftProgressEndTime - m_iTimeshiftProgressStartTime; +} + +void CPVRGUITimesInfo::Update() +{ + UpdatePlayingTag(); + UpdateTimeshiftData(); +} + +std::string CPVRGUITimesInfo::TimeToTimeString(time_t datetime, TIME_FORMAT format, bool withSeconds) +{ + CDateTime time; + time.SetFromUTCDateTime(datetime); + return time.GetAsLocalizedTime(format, withSeconds); +} + +std::string CPVRGUITimesInfo::GetTimeshiftStartTime(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return TimeToTimeString(m_iTimeshiftStartTime, format, false); +} + +std::string CPVRGUITimesInfo::GetTimeshiftEndTime(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return TimeToTimeString(m_iTimeshiftEndTime, format, false); +} + +std::string CPVRGUITimesInfo::GetTimeshiftPlayTime(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return TimeToTimeString(m_iTimeshiftPlayTime, format, true); +} + +std::string CPVRGUITimesInfo::GetTimeshiftOffset(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return StringUtils::SecondsToTimeString(m_iTimeshiftOffset, format); +} + +std::string CPVRGUITimesInfo::GetTimeshiftProgressDuration(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return StringUtils::SecondsToTimeString(m_iTimeshiftProgressDuration, format); +} + +std::string CPVRGUITimesInfo::GetTimeshiftProgressStartTime(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return TimeToTimeString(m_iTimeshiftProgressStartTime, format, false); +} + +std::string CPVRGUITimesInfo::GetTimeshiftProgressEndTime(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return TimeToTimeString(m_iTimeshiftProgressEndTime, format, false); +} + +std::string CPVRGUITimesInfo::GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return StringUtils::SecondsToTimeString(GetEpgEventDuration(epgTag), format); +} + +std::string CPVRGUITimesInfo::GetEpgEventElapsedTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const +{ + int iElapsed = 0; + std::unique_lock<CCriticalSection> lock(m_critSection); + if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag) + iElapsed = epgTag->Progress(); + else + iElapsed = GetElapsedTime(); + + return StringUtils::SecondsToTimeString(iElapsed, format); +} + +std::string CPVRGUITimesInfo::GetEpgEventRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return StringUtils::SecondsToTimeString(GetRemainingTime(epgTag), format); +} + +std::string CPVRGUITimesInfo::GetEpgEventFinishTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const +{ + CDateTime finish = CDateTime::GetCurrentDateTime(); + finish += CDateTimeSpan(0, 0, 0, GetRemainingTime(epgTag)); + return finish.GetAsLocalizedTime(format); +} + +std::string CPVRGUITimesInfo::GetEpgEventSeekTime(int iSeekSize, TIME_FORMAT format) const +{ + return StringUtils::SecondsToTimeString(GetElapsedTime() + iSeekSize, format); +} + +int CPVRGUITimesInfo::GetElapsedTime() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_playingEpgTag || m_iTimeshiftStartTime) + { + CDateTime current(m_iTimeshiftPlayTime); + CDateTime start = m_playingEpgTag ? m_playingEpgTag->StartAsUTC() : CDateTime(m_iTimeshiftStartTime); + CDateTimeSpan time = current > start ? current - start : CDateTimeSpan(0, 0, 0, 0); + return time.GetSecondsTotal(); + } + else + { + return 0; + } +} + +int CPVRGUITimesInfo::GetRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag) + return epgTag->GetDuration() - epgTag->Progress(); + else + return m_iDuration - GetElapsedTime(); +} + +int CPVRGUITimesInfo::GetTimeshiftProgress() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::lrintf(static_cast<float>(m_iTimeshiftPlayTime - m_iTimeshiftStartTime) / (m_iTimeshiftEndTime - m_iTimeshiftStartTime) * 100); +} + +int CPVRGUITimesInfo::GetTimeshiftProgressDuration() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iTimeshiftProgressDuration; +} + +int CPVRGUITimesInfo::GetTimeshiftProgressPlayPosition() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::lrintf(static_cast<float>(m_iTimeshiftPlayTime - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100); +} + +int CPVRGUITimesInfo::GetTimeshiftProgressEpgStart() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_playingEpgTag) + { + time_t epgStart = 0; + m_playingEpgTag->StartAsUTC().GetAsTime(epgStart); + return std::lrintf(static_cast<float>(epgStart - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100); + } + return 0; +} + +int CPVRGUITimesInfo::GetTimeshiftProgressEpgEnd() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_playingEpgTag) + { + time_t epgEnd = 0; + m_playingEpgTag->EndAsUTC().GetAsTime(epgEnd); + return std::lrintf(static_cast<float>(epgEnd - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100); + } + return 0; +} + +int CPVRGUITimesInfo::GetTimeshiftProgressBufferStart() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::lrintf(static_cast<float>(m_iTimeshiftStartTime - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100); +} + +int CPVRGUITimesInfo::GetTimeshiftProgressBufferEnd() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::lrintf(static_cast<float>(m_iTimeshiftEndTime - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100); +} + +int CPVRGUITimesInfo::GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag) + return epgTag->GetDuration(); + else + return m_iDuration; +} + +int CPVRGUITimesInfo::GetEpgEventProgress(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag) + return std::lrintf(epgTag->ProgressPercentage()); + else + return std::lrintf(static_cast<float>(GetElapsedTime()) / m_iDuration * 100); +} + +bool CPVRGUITimesInfo::IsTimeshifting() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return (m_iTimeshiftOffset > static_cast<unsigned int>(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeshiftThreshold)); +} diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h new file mode 100644 index 0000000..47dd9f5 --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "utils/TimeFormat.h" + +#include <memory> + +namespace PVR +{ + class CPVRChannel; + class CPVREpgInfoTag; + + class CPVRGUITimesInfo + { + public: + CPVRGUITimesInfo(); + virtual ~CPVRGUITimesInfo() = default; + + void Reset(); + void Update(); + + // GUI info labels + std::string GetTimeshiftStartTime(TIME_FORMAT format) const; + std::string GetTimeshiftEndTime(TIME_FORMAT format) const; + std::string GetTimeshiftPlayTime(TIME_FORMAT format) const; + std::string GetTimeshiftOffset(TIME_FORMAT format) const; + std::string GetTimeshiftProgressDuration(TIME_FORMAT format) const; + std::string GetTimeshiftProgressStartTime(TIME_FORMAT format) const; + std::string GetTimeshiftProgressEndTime(TIME_FORMAT format) const; + + std::string GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const; + std::string GetEpgEventElapsedTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const; + std::string GetEpgEventRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const; + std::string GetEpgEventFinishTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const; + std::string GetEpgEventSeekTime(int iSeekSize, TIME_FORMAT format) const; + + // GUI info ints + int GetTimeshiftProgress() const; + int GetTimeshiftProgressDuration() const; + int GetTimeshiftProgressPlayPosition() const; + int GetTimeshiftProgressEpgStart() const; + int GetTimeshiftProgressEpgEnd() const; + int GetTimeshiftProgressBufferStart() const; + int GetTimeshiftProgressBufferEnd() const; + + int GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + int GetEpgEventProgress(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + + // GUI info bools + bool IsTimeshifting() const; + + private: + void UpdatePlayingTag(); + void UpdateTimeshiftData(); + void UpdateTimeshiftProgressData(); + + static std::string TimeToTimeString(time_t datetime, TIME_FORMAT format, bool withSeconds); + + int GetElapsedTime() const; + int GetRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + + mutable CCriticalSection m_critSection; + + std::shared_ptr<CPVREpgInfoTag> m_playingEpgTag; + std::shared_ptr<CPVRChannel> m_playingChannel; + + time_t m_iStartTime; + unsigned int m_iDuration; + time_t m_iTimeshiftStartTime; + time_t m_iTimeshiftEndTime; + time_t m_iTimeshiftPlayTime; + unsigned int m_iTimeshiftOffset; + + time_t m_iTimeshiftProgressStartTime; + time_t m_iTimeshiftProgressEndTime; + unsigned int m_iTimeshiftProgressDuration; + }; + +} // namespace PVR diff --git a/xbmc/pvr/providers/CMakeLists.txt b/xbmc/pvr/providers/CMakeLists.txt new file mode 100644 index 0000000..f01a35a --- /dev/null +++ b/xbmc/pvr/providers/CMakeLists.txt @@ -0,0 +1,7 @@ +set(SOURCES PVRProvider.cpp + PVRProviders.cpp) + +set(HEADERS PVRProvider.h + PVRProviders.h) + +core_add_library(pvr_providers) diff --git a/xbmc/pvr/providers/PVRProvider.cpp b/xbmc/pvr/providers/PVRProvider.cpp new file mode 100644 index 0000000..ab2e7a3 --- /dev/null +++ b/xbmc/pvr/providers/PVRProvider.cpp @@ -0,0 +1,393 @@ +/* + * 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 "PVRProvider.h" + +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <memory> +#include <mutex> +#include <string> + +using namespace PVR; + + +const std::string CPVRProvider::IMAGE_OWNER_PATTERN = "pvrprovider"; + +CPVRProvider::CPVRProvider(int iUniqueId, int iClientId) + : m_iUniqueId(iUniqueId), + m_iClientId(iClientId), + m_iconPath(IMAGE_OWNER_PATTERN), + m_thumbPath(IMAGE_OWNER_PATTERN) +{ +} + +CPVRProvider::CPVRProvider(const PVR_PROVIDER& provider, int iClientId) + : m_iUniqueId(provider.iUniqueId), + m_iClientId(iClientId), + m_strName(provider.strName), + m_type(provider.type), + m_iconPath(provider.strIconPath, IMAGE_OWNER_PATTERN), + m_strCountries(provider.strCountries), + m_strLanguages(provider.strLanguages), + m_thumbPath(IMAGE_OWNER_PATTERN) +{ +} + +CPVRProvider::CPVRProvider(int iClientId, + const std::string& addonProviderName, + const std::string& addonIconPath, + const std::string& addonThumbPath) + : m_iClientId(iClientId), + m_strName(addonProviderName), + m_type(PVR_PROVIDER_TYPE_ADDON), + m_iconPath(addonIconPath, IMAGE_OWNER_PATTERN), + m_bIsClientProvider(true), + m_thumbPath(addonThumbPath, IMAGE_OWNER_PATTERN) +{ +} + +bool CPVRProvider::operator==(const CPVRProvider& right) const +{ + return (m_iUniqueId == right.m_iUniqueId && m_iClientId == right.m_iClientId); +} + +bool CPVRProvider::operator!=(const CPVRProvider& right) const +{ + return !(*this == right); +} + +void CPVRProvider::Serialize(CVariant& value) const +{ + value["providerid"] = m_iDatabaseId; + value["clientid"] = m_iClientId; + value["providername"] = m_strName; + switch (m_type) + { + case PVR_PROVIDER_TYPE_ADDON: + value["providertype"] = "addon"; + break; + case PVR_PROVIDER_TYPE_SATELLITE: + value["providertype"] = "satellite"; + break; + case PVR_PROVIDER_TYPE_CABLE: + value["providertype"] = "cable"; + break; + case PVR_PROVIDER_TYPE_AERIAL: + value["providertype"] = "aerial"; + break; + case PVR_PROVIDER_TYPE_IPTV: + value["providertype"] = "iptv"; + break; + case PVR_PROVIDER_TYPE_OTHER: + value["providertype"] = "other"; + break; + default: + value["state"] = "unknown"; + break; + } + value["iconpath"] = GetClientIconPath(); + value["countries"] = m_strCountries; + value["languages"] = m_strLanguages; +} + +int CPVRProvider::GetDatabaseId() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iDatabaseId; +} + +bool CPVRProvider::SetDatabaseId(int iDatabaseId) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iDatabaseId != iDatabaseId) + { + m_iDatabaseId = iDatabaseId; + return true; + } + + return false; +} + +int CPVRProvider::GetUniqueId() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iUniqueId; +} + +int CPVRProvider::GetClientId() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iClientId; +} + +std::string CPVRProvider::GetName() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strName; +} + +bool CPVRProvider::SetName(const std::string& strName) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_strName != strName) + { + m_strName = strName; + return true; + } + + return false; +} + +PVR_PROVIDER_TYPE CPVRProvider::GetType() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_type; +} + +bool CPVRProvider::SetType(PVR_PROVIDER_TYPE type) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_type != type) + { + m_type = type; + return true; + } + + return false; +} + +std::string CPVRProvider::GetClientIconPath() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iconPath.GetClientImage(); +} + +std::string CPVRProvider::GetIconPath() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iconPath.GetLocalImage(); +} + +bool CPVRProvider::SetIconPath(const std::string& strIconPath) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (GetClientIconPath() != strIconPath) + { + m_iconPath.SetClientImage(strIconPath); + return true; + } + + return false; +} + +namespace +{ + +const std::vector<std::string> Tokenize(const std::string& str) +{ + return StringUtils::Split(str, PROVIDER_STRING_TOKEN_SEPARATOR); +} + +const std::string DeTokenize(const std::vector<std::string>& tokens) +{ + return StringUtils::Join(tokens, PROVIDER_STRING_TOKEN_SEPARATOR); +} + +} // unnamed namespace + +std::vector<std::string> CPVRProvider::GetCountries() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + return Tokenize(m_strCountries); +} + +bool CPVRProvider::SetCountries(const std::vector<std::string>& countries) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const std::string strCountries = DeTokenize(countries); + if (m_strCountries != strCountries) + { + m_strCountries = strCountries; + return true; + } + + return false; +} + +std::string CPVRProvider::GetCountriesDBString() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strCountries; +} + +bool CPVRProvider::SetCountriesFromDBString(const std::string& strCountries) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_strCountries != strCountries) + { + m_strCountries = strCountries; + return true; + } + + return false; +} + +std::vector<std::string> CPVRProvider::GetLanguages() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return Tokenize(m_strLanguages); +} + +bool CPVRProvider::SetLanguages(const std::vector<std::string>& languages) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const std::string strLanguages = DeTokenize(languages); + if (m_strLanguages != strLanguages) + { + m_strLanguages = strLanguages; + return true; + } + + return false; +} + +std::string CPVRProvider::GetLanguagesDBString() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strLanguages; +} + +bool CPVRProvider::SetLanguagesFromDBString(const std::string& strLanguages) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_strLanguages != strLanguages) + { + m_strLanguages = strLanguages; + return true; + } + + return false; +} + +bool CPVRProvider::Persist(bool updateRecord /* = false */) +{ + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (database) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + return database->Persist(*this, updateRecord); + } + + return false; +} + +bool CPVRProvider::DeleteFromDatabase() +{ + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (database) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + return database->Delete(*this); + } + + return false; +} + +bool CPVRProvider::UpdateEntry(const std::shared_ptr<CPVRProvider>& fromProvider, + ProviderUpdateMode updateMode) +{ + bool bChanged = false; + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (updateMode == ProviderUpdateMode::BY_DATABASE) + { + m_iDatabaseId = fromProvider->m_iDatabaseId; + + m_strName = fromProvider->m_strName; + m_type = fromProvider->m_type; + m_iconPath = fromProvider->m_iconPath; + + if (fromProvider->m_bIsClientProvider) + { + m_thumbPath = fromProvider->m_thumbPath; + m_bIsClientProvider = fromProvider->m_bIsClientProvider; + } + + m_strCountries = fromProvider->m_strCountries; + m_strLanguages = fromProvider->m_strLanguages; + } + else if (updateMode == ProviderUpdateMode::BY_CLIENT) + { + if (m_strName != fromProvider->m_strName) + { + m_strName = fromProvider->m_strName; + bChanged = true; + } + + if (m_type != fromProvider->m_type) + { + m_type = fromProvider->m_type; + bChanged = true; + } + + if (m_iconPath != fromProvider->m_iconPath) + { + m_iconPath = fromProvider->m_iconPath; + bChanged = true; + } + + if (fromProvider->m_bIsClientProvider) + { + m_thumbPath = fromProvider->m_thumbPath; + m_bIsClientProvider = fromProvider->m_bIsClientProvider; + } + + if (m_strCountries != fromProvider->m_strCountries) + { + m_strCountries = fromProvider->m_strCountries; + bChanged = true; + } + + if (m_strLanguages != fromProvider->m_strLanguages) + { + m_strLanguages = fromProvider->m_strLanguages; + bChanged = true; + } + } + + return bChanged; +} + +bool CPVRProvider::HasThumbPath() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return (m_type == PVR_PROVIDER_TYPE_ADDON && !m_thumbPath.GetLocalImage().empty()); +} + +std::string CPVRProvider::GetThumbPath() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_thumbPath.GetLocalImage(); +} + +std::string CPVRProvider::GetClientThumbPath() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_thumbPath.GetClientImage(); +} diff --git a/xbmc/pvr/providers/PVRProvider.h b/xbmc/pvr/providers/PVRProvider.h new file mode 100644 index 0000000..1e8d835 --- /dev/null +++ b/xbmc/pvr/providers/PVRProvider.h @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_providers.h" +#include "pvr/PVRCachedImage.h" +#include "threads/CriticalSection.h" +#include "utils/ISerializable.h" + +#include <memory> +#include <string> +#include <vector> + +namespace PVR +{ + +enum class ProviderUpdateMode +{ + BY_CLIENT, + BY_DATABASE +}; + +static constexpr int PVR_PROVIDER_ADDON_UID = -1; +static constexpr int PVR_PROVIDER_INVALID_DB_ID = -1; + +class CPVRProvider final : public ISerializable +{ +public: + static const std::string IMAGE_OWNER_PATTERN; + + CPVRProvider(int iUniqueId, int iClientId); + CPVRProvider(const PVR_PROVIDER& provider, int iClientId); + CPVRProvider(int iClientId, + const std::string& addonProviderName, + const std::string& addonIconPath, + const std::string& addonThumbPath); + + bool operator==(const CPVRProvider& right) const; + bool operator!=(const CPVRProvider& right) const; + + void Serialize(CVariant& value) const override; + + /*! + * @brief The database id of this provider + * + * A unique identifier for this provider. + * It can be used to find the same provider on this clients backend + * + * @return The database id of this provider + */ + int GetDatabaseId() const; + + /*! + * @brief Set the database id of this provider + * @param iDatabaseId The new ID. + * @return True if the something changed, false otherwise. + */ + bool SetDatabaseId(int iDatabaseId); + + /*! + * @brief A unique identifier for this provider. + * + * A unique identifier for this provider. + * It can be used to find the same provider on this clients backend + * + * @return The Unique ID. + */ + int GetUniqueId() const; + + /*! + * @return The identifier of the client that supplies this provider. + */ + int GetClientId() const; + + /*! + * @return The name of the provider. Can be user provided or the backend name + */ + std::string GetName() const; + + /*! + * @brief Set the name of the provider. + * @param name The new name of the provider. + * @return True if the something changed, false otherwise. + */ + bool SetName(const std::string& iName); + + /*! + * @brief Checks whether this provider has a known type + * @return True if this provider has a type other than unknown, false otherwise + */ + bool HasType() const { return m_type != PVR_PROVIDER_TYPE_UNKNOWN; } + + /*! + * @brief Gets the type of this provider. + * @return the type of this provider. + */ + PVR_PROVIDER_TYPE GetType() const; + + /*! + * @brief Sets the type of this provider. + * @param type the new provider type. + * @return True if the something changed, false otherwise. + */ + bool SetType(PVR_PROVIDER_TYPE type); + + /*! + * @brief Get the path for this provider's icon + * @return iconpath for this provider's icon + */ + std::string GetIconPath() const; + + /*! + * @brief Set the path for this icon + * @param strIconPath The new path of the icon. + * @return true if the icon path was updated successfully + */ + bool SetIconPath(const std::string& strIconPath); + + /*! + * @return Get the path to the icon for this provider as given by the client. + */ + std::string GetClientIconPath() const; + + /*! + * @brief Get this provider's country codes (ISO 3166). + * @return This provider's country codes. + */ + std::vector<std::string> GetCountries() const; + + /*! + * @brief Set the country codes for this provider + * @param countries The new ISO 3166 country codes for this provider. + * @return true if the country codes were updated successfully + */ + bool SetCountries(const std::vector<std::string>& countries); + + /*! + * @brief Get this provider's country codes (ISO 3166) as a string. + * @return This provider's country codes. + */ + std::string GetCountriesDBString() const; + + /*! + * @brief Set the country codes for this provider from a string + * @param strCountries The new ISO 3166 country codes for this provider. + * @return true if the country codes were updated successfully + */ + bool SetCountriesFromDBString(const std::string& strCountries); + + /*! + * @brief Get this provider's language codes (RFC 5646). + * @return This provider's language codes + */ + std::vector<std::string> GetLanguages() const; + + /*! + * @brief Set the language codes for this provider + * @param languages The new RFC 5646 language codes for this provider. + * @return true if the language codes were updated successfully + */ + bool SetLanguages(const std::vector<std::string>& languages); + + /*! + * @brief Get this provider's language codes (RFC 5646) as a string. + * @return This provider's language codes. + */ + std::string GetLanguagesDBString() const; + + /*! + * @brief Set the language codes for this provider from a string + * @param strLanguages The new RFC 5646 language codes for this provider. + * @return true if the language codes were updated successfully + */ + bool SetLanguagesFromDBString(const std::string& strLanguages); + + /*! + * @brief Get if this provider has a thumb image path. + * @return True if this add-on provider has a thumb image path, false otherwise. + */ + bool HasThumbPath() const; + + /*! + * @brief Get this provider's thumb image path. Note only PVR add-on providers will set this value. + * @return This add-on provider's thumb image path. + */ + std::string GetThumbPath() const; + + /*! + * @return Get the path to the thumb for this provider as given by the client. + */ + std::string GetClientThumbPath() const; + + /*! + * @brief Whether a provider is a default provider of a PVR Client add-on or not + * @return True if this provider is of a PVR Client add-on, false otherwise. + */ + bool IsClientProvider() const { return m_bIsClientProvider; } + + /*! + * @brief updates this provider from the provided entry + * @param fromProvider A provider containing the data that shall be merged into this provider's data. + * @param updateMode update as User, Client or DB + * @return true if the provider was updated successfully + */ + bool UpdateEntry(const std::shared_ptr<CPVRProvider>& fromProvider, + ProviderUpdateMode updateMode); + + /*! + * @brief Persist this provider in the local database. + * @param updateRecord True if an existing record should be updated, false for an insert + * @return True on success, false otherwise. + */ + bool Persist(bool updateRecord = false); + + /*! + * @brief Delete this provider from the local database. + * @return True on success, false otherwise. + */ + bool DeleteFromDatabase(); + +private: + CPVRProvider(const CPVRProvider& provider) = delete; + CPVRProvider& operator=(const CPVRProvider& orig) = delete; + + int m_iDatabaseId = PVR_PROVIDER_INVALID_DB_ID; /*!< the identifier given to this provider by the TV database */ + + int m_iUniqueId = PVR_PROVIDER_ADDON_UID; /*!< @brief unique ID of the provider on the backend */ + int m_iClientId; /*!< @brief ID of the backend */ + std::string m_strName; /*!< @brief name of this provider */ + PVR_PROVIDER_TYPE m_type = PVR_PROVIDER_TYPE_UNKNOWN; /*!< @brief service type for this provider */ + CPVRCachedImage m_iconPath; /*!< @brief the path to the icon for this provider */ + std::string m_strCountries; /*!< @brief the country codes for this provider (empty if undefined) */ + std::string m_strLanguages; /*!< @brief the language codes for this provider (empty if undefined) */ + bool m_bIsClientProvider = false; /*!< the provider is a default provider of a PVR Client add-on */ + CPVRCachedImage m_thumbPath; /*!< a thumb image path for providers that are PVR add-ons */ + + mutable CCriticalSection m_critSection; +}; +} // namespace PVR diff --git a/xbmc/pvr/providers/PVRProviders.cpp b/xbmc/pvr/providers/PVRProviders.cpp new file mode 100644 index 0000000..6d751a2 --- /dev/null +++ b/xbmc/pvr/providers/PVRProviders.cpp @@ -0,0 +1,380 @@ +/* + * 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 "PVRProviders.h" + +#include "ServiceBroker.h" +#include "pvr/PVRCachedImages.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/providers/PVRProvider.h" +#include "settings/Settings.h" +#include "utils/log.h" + +#include <algorithm> +#include <memory> +#include <mutex> +#include <numeric> +#include <string> +#include <vector> + +using namespace PVR; + +bool CPVRProvidersContainer::UpdateFromClient(const std::shared_ptr<CPVRProvider>& provider) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const std::shared_ptr<CPVRProvider> providerToUpdate = + GetByClient(provider->GetClientId(), provider->GetUniqueId()); + if (providerToUpdate) + { + return providerToUpdate->UpdateEntry(provider, ProviderUpdateMode::BY_CLIENT); + } + else + { + provider->SetDatabaseId(++m_iLastId); + InsertEntry(provider, ProviderUpdateMode::BY_CLIENT); + } + + return true; +} + +std::shared_ptr<CPVRProvider> CPVRProvidersContainer::GetByClient(int iClientId, + int iUniqueId) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const auto it = std::find_if( + m_providers.cbegin(), m_providers.cend(), [iClientId, iUniqueId](const auto& provider) { + return provider->GetClientId() == iClientId && provider->GetUniqueId() == iUniqueId; + }); + return it != m_providers.cend() ? (*it) : std::shared_ptr<CPVRProvider>(); +} + +void CPVRProvidersContainer::InsertEntry(const std::shared_ptr<CPVRProvider>& newProvider, + ProviderUpdateMode updateMode) +{ + bool found = false; + for (auto& provider : m_providers) + { + if (provider->GetClientId() == newProvider->GetClientId() && + provider->GetUniqueId() == newProvider->GetUniqueId()) + { + found = true; + provider->UpdateEntry(newProvider, updateMode); + } + } + + if (!found) + { + m_providers.emplace_back(newProvider); + } +} + +std::vector<std::shared_ptr<CPVRProvider>> CPVRProvidersContainer::GetProvidersList() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_providers; +} + +std::size_t CPVRProvidersContainer::GetNumProviders() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_providers.size(); +} + +bool CPVRProviders::Update(const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + return LoadFromDatabase(clients) && UpdateFromClients(clients); +} + +void CPVRProviders::Unload() +{ + // remove all tags + std::unique_lock<CCriticalSection> lock(m_critSection); + m_providers.clear(); +} + +bool CPVRProviders::LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (database) + { + m_iLastId = database->GetMaxProviderId(); + + CPVRProviders providers; + database->Get(providers, clients); + + for (auto& provider : providers.GetProvidersList()) + CheckAndAddEntry(provider, ProviderUpdateMode::BY_DATABASE); + } + return true; +} + +bool CPVRProviders::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_bIsUpdating) + return false; + m_bIsUpdating = true; + } + + // Default client providers are the add-ons themselves, we retrieve both enabled + // and disabled add-ons as we don't want them removed from the DB + CPVRProviders newAddonProviderList; + std::vector<int> disabledClients; + std::vector<CVariant> clientProviderInfos = + CServiceBroker::GetPVRManager().Clients()->GetClientProviderInfos(); + CLog::LogFC(LOGDEBUG, LOGPVR, "Adding default providers, found {} PVR add-ons", + clientProviderInfos.size()); + for (const auto& clientInfo : clientProviderInfos) + { + auto addonProvider = std::make_shared<CPVRProvider>( + clientInfo["clientid"].asInteger32(), clientInfo["name"].asString(), + clientInfo["icon"].asString(), clientInfo["thumb"].asString()); + + newAddonProviderList.CheckAndAddEntry(addonProvider, ProviderUpdateMode::BY_CLIENT); + + if (!clientInfo["enabled"].asBoolean()) + disabledClients.emplace_back(clientInfo["clientid"].asInteger32()); + } + UpdateDefaultEntries(newAddonProviderList); + + // Client providers are retrieved from the clients + CLog::LogFC(LOGDEBUG, LOGPVR, "Updating providers"); + CPVRProvidersContainer newProviderList; + std::vector<int> failedClients; + CServiceBroker::GetPVRManager().Clients()->GetProviders(clients, &newProviderList, failedClients); + return UpdateClientEntries(newProviderList, failedClients, disabledClients); +} + +bool CPVRProviders::UpdateDefaultEntries(const CPVRProvidersContainer& newProviders) +{ + bool bChanged = false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + + // go through the provider list and check for updated or new providers + const auto newProviderList = newProviders.GetProvidersList(); + bChanged = std::accumulate( + newProviderList.cbegin(), newProviderList.cend(), false, + [this](bool changed, const auto& newProvider) { + return (CheckAndPersistEntry(newProvider, ProviderUpdateMode::BY_CLIENT) != nullptr) + ? true + : changed; + }); + + // check for deleted providers + for (std::vector<std::shared_ptr<CPVRProvider>>::iterator it = m_providers.begin(); + it != m_providers.end();) + { + const std::shared_ptr<CPVRProvider> provider = *it; + if (!newProviders.GetByClient(provider->GetClientId(), provider->GetUniqueId())) + { + // provider was not found + bool bIgnoreProvider = false; + + // ignore add-on any providers that are no PVR Client addon providers + if (!provider->IsClientProvider()) + bIgnoreProvider = true; + + if (bIgnoreProvider) + { + ++it; + continue; + } + + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleted provider {} on client {}", provider->GetUniqueId(), + provider->GetClientId()); + + (*it)->DeleteFromDatabase(); + it = m_providers.erase(it); + + bChanged |= true; + } + else + { + ++it; + } + } + + return bChanged; +} + +bool CPVRProviders::UpdateClientEntries(const CPVRProvidersContainer& newProviders, + const std::vector<int>& failedClients, + const std::vector<int>& disabledClients) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + // go through the provider list and check for updated or new providers + for (const auto& newProvider : newProviders.GetProvidersList()) + { + CheckAndPersistEntry(newProvider, ProviderUpdateMode::BY_CLIENT); + } + + // check for deleted providers + for (auto it = m_providers.begin(); it != m_providers.end();) + { + const std::shared_ptr<CPVRProvider> provider = *it; + if (!newProviders.GetByClient(provider->GetClientId(), provider->GetUniqueId())) + { + const bool bIgnoreProvider = + (provider->IsClientProvider() || // ignore add-on providers as they are a special case + std::any_of(failedClients.cbegin(), failedClients.cend(), + [&provider](const auto& failedClient) { + return failedClient == provider->GetClientId(); + }) || + std::any_of(disabledClients.cbegin(), disabledClients.cend(), + [&provider](const auto& disabledClient) { + return disabledClient == provider->GetClientId(); + })); + if (bIgnoreProvider) + { + ++it; + continue; + } + + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleted provider {} on client {}", provider->GetUniqueId(), + provider->GetClientId()); + + (*it)->DeleteFromDatabase(); + it = m_providers.erase(it); + } + else + { + ++it; + } + } + + m_bIsUpdating = false; + + return true; +} + +std::shared_ptr<CPVRProvider> CPVRProviders::CheckAndAddEntry( + const std::shared_ptr<CPVRProvider>& newProvider, ProviderUpdateMode updateMode) +{ + bool bChanged = false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + std::shared_ptr<CPVRProvider> provider = + GetByClient(newProvider->GetClientId(), newProvider->GetUniqueId()); + if (provider) + { + bChanged = provider->UpdateEntry(newProvider, updateMode); + } + else + { + // We don't set an id as this came from the DB so it has one already + InsertEntry(newProvider, updateMode); + + if (newProvider->GetDatabaseId() > m_iLastId) + m_iLastId = newProvider->GetDatabaseId(); + + provider = newProvider; + bChanged = true; + } + + if (bChanged) + return provider; + + return {}; +} + +std::shared_ptr<CPVRProvider> CPVRProviders::CheckAndPersistEntry( + const std::shared_ptr<CPVRProvider>& newProvider, ProviderUpdateMode updateMode) +{ + bool bChanged = false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + std::shared_ptr<CPVRProvider> provider = + GetByClient(newProvider->GetClientId(), newProvider->GetUniqueId()); + if (provider) + { + bChanged = provider->UpdateEntry(newProvider, updateMode); + + if (bChanged) + provider->Persist(true); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Updated provider {} on client {}", newProvider->GetUniqueId(), + newProvider->GetClientId()); + } + else + { + newProvider->SetDatabaseId(++m_iLastId); + InsertEntry(newProvider, updateMode); + + newProvider->Persist(); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Added provider {} on client {}", newProvider->GetUniqueId(), + newProvider->GetClientId()); + + provider = newProvider; + bChanged = true; + } + + if (bChanged) + return provider; + + return {}; +} + +bool CPVRProviders::PersistUserChanges(const std::vector<std::shared_ptr<CPVRProvider>>& providers) +{ + for (const auto& provider : providers) + { + provider->Persist(true); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Updated provider {} on client {}", provider->GetUniqueId(), + provider->GetClientId()); + } + + return true; +} + +std::shared_ptr<CPVRProvider> CPVRProviders::GetById(int iProviderId) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const auto it = + std::find_if(m_providers.cbegin(), m_providers.cend(), [iProviderId](const auto& provider) { + return provider->GetDatabaseId() == iProviderId; + }); + return it != m_providers.cend() ? (*it) : std::shared_ptr<CPVRProvider>(); +} + +void CPVRProviders::RemoveEntry(const std::shared_ptr<CPVRProvider>& provider) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_providers.erase( + std::remove_if(m_providers.begin(), m_providers.end(), + [&provider](const std::shared_ptr<CPVRProvider>& providerToRemove) { + return provider->GetClientId() == providerToRemove->GetClientId() && + provider->GetUniqueId() == providerToRemove->GetUniqueId(); + }), + m_providers.end()); +} + +int CPVRProviders::CleanupCachedImages() +{ + std::vector<std::string> urlsToCheck; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (const auto& provider : m_providers) + { + urlsToCheck.emplace_back(provider->GetClientIconPath()); + urlsToCheck.emplace_back(provider->GetClientThumbPath()); + } + } + + static const std::vector<PVRImagePattern> urlPatterns = {{CPVRProvider::IMAGE_OWNER_PATTERN, ""}}; + return CPVRCachedImages::Cleanup(urlPatterns, urlsToCheck); +} diff --git a/xbmc/pvr/providers/PVRProviders.h b/xbmc/pvr/providers/PVRProviders.h new file mode 100644 index 0000000..8196d3b --- /dev/null +++ b/xbmc/pvr/providers/PVRProviders.h @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" + +#include <memory> +#include <vector> + +namespace PVR +{ +class CPVRClient; +class CPVRProvider; + +enum class ProviderUpdateMode; + +class CPVRProvidersContainer +{ +public: + /*! + * @brief Add a provider to this container or update the provider if already present in this container. + * @param The provider + * @return True, if the update was successful. False, otherwise. + */ + bool UpdateFromClient(const std::shared_ptr<CPVRProvider>& provider); + + /*! + * @brief Get the provider denoted by given client id and unique client provider id. + * @param iClientId The client id. + * @param iUniqueId The client provider id. + * @return the provider if found, null otherwise. + */ + std::shared_ptr<CPVRProvider> GetByClient(int iClientId, int iUniqueId) const; + + /*! + * Get all providers in this container + * @return The list of all providers + */ + std::vector<std::shared_ptr<CPVRProvider>> GetProvidersList() const; + + /*! + * Get the number of providers in this container + * @return The total number of providers + */ + std::size_t GetNumProviders() const; + +protected: + void InsertEntry(const std::shared_ptr<CPVRProvider>& newProvider, ProviderUpdateMode updateMode); + + mutable CCriticalSection m_critSection; + int m_iLastId = 0; + std::vector<std::shared_ptr<CPVRProvider>> m_providers; +}; + +class CPVRProviders : public CPVRProvidersContainer +{ +public: + CPVRProviders() = default; + ~CPVRProviders() = default; + + /** + * @brief Update all providers from PVR database and from given clients. + * @param clients The PVR clients data should be loaded for. Leave empty for all clients. + * @return True on success, false otherwise. + */ + bool Update(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + /** + * @brief unload all providers. + */ + void Unload(); + + /** + * @brief Update data with providers from the given clients, sync with local data. + * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients. + * @return True on success, false otherwise. + */ + bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + /** + * @brief Load all local providers from PVR database. + * @param clients The PVR clients data should be loaded for. Leave empty for all clients. + * @return True on success, false otherwise. + */ + bool LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + /*! + * @brief Check if the entry exists in the container, if it does update it otherwise add it + * @param newProvider The provider entry to update/add in/to the container + * @param updateMode update as Client (respect User set values) or DB (update all values) + * @return The provider if updated or added, otherwise an empty object (nullptr) + */ + std::shared_ptr<CPVRProvider> CheckAndAddEntry(const std::shared_ptr<CPVRProvider>& newProvider, + ProviderUpdateMode updateMode); + + /*! + * @brief Check if the entry exists in the container, if it does update it and persist + * it in the DB otherwise add it and persist it in the DB. + * @param newProvider The provider entry to update/add in/to the container and DB + * @param updateMode update as Client (respect User set values) or DB (update all values) + * @return The provider if updated or added, otherwise an empty object (nullptr) + */ + std::shared_ptr<CPVRProvider> CheckAndPersistEntry( + const std::shared_ptr<CPVRProvider>& newProvider, ProviderUpdateMode updateMode); + + /** + * @brief Persist user changes to the current state of the providers in the DB. + */ + bool PersistUserChanges(const std::vector<std::shared_ptr<CPVRProvider>>& providers); + + /*! + * @brief Get a provider given it's database ID + * @param iProviderId The ID to find + * @return The provider, or an empty one when not found + */ + std::shared_ptr<CPVRProvider> GetById(int iProviderId) const; + + /*! + * @brief Erase stale texture db entries and image files. + * @return number of cleaned up images. + */ + int CleanupCachedImages(); + +private: + void RemoveEntry(const std::shared_ptr<CPVRProvider>& provider); + bool UpdateDefaultEntries(const CPVRProvidersContainer& newProviders); + bool UpdateClientEntries(const CPVRProvidersContainer& newProviders, + const std::vector<int>& failedClients, + const std::vector<int>& disabledClients); + + bool m_bIsUpdating = false; +}; +} // namespace PVR diff --git a/xbmc/pvr/recordings/CMakeLists.txt b/xbmc/pvr/recordings/CMakeLists.txt new file mode 100644 index 0000000..55f7028 --- /dev/null +++ b/xbmc/pvr/recordings/CMakeLists.txt @@ -0,0 +1,9 @@ +set(SOURCES PVRRecording.cpp + PVRRecordings.cpp + PVRRecordingsPath.cpp) + +set(HEADERS PVRRecording.h + PVRRecordings.h + PVRRecordingsPath.h) + +core_add_library(pvr_recordings) diff --git a/xbmc/pvr/recordings/PVRRecording.cpp b/xbmc/pvr/recordings/PVRRecording.cpp new file mode 100644 index 0000000..a2e1cbd --- /dev/null +++ b/xbmc/pvr/recordings/PVRRecording.cpp @@ -0,0 +1,730 @@ +/* + * 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 "PVRRecording.h" + +#include "ServiceBroker.h" +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_recordings.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/Epg.h" +#include "pvr/providers/PVRProvider.h" +#include "pvr/providers/PVRProviders.h" +#include "pvr/recordings/PVRRecordingsPath.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" + +#include <memory> +#include <mutex> +#include <string> +#include <vector> + +using namespace PVR; + +using namespace std::chrono_literals; + +CPVRRecordingUid::CPVRRecordingUid(int iClientId, const std::string& strRecordingId) + : m_iClientId(iClientId), m_strRecordingId(strRecordingId) +{ +} + +bool CPVRRecordingUid::operator>(const CPVRRecordingUid& right) const +{ + return (m_iClientId == right.m_iClientId) ? m_strRecordingId > right.m_strRecordingId + : m_iClientId > right.m_iClientId; +} + +bool CPVRRecordingUid::operator<(const CPVRRecordingUid& right) const +{ + return (m_iClientId == right.m_iClientId) ? m_strRecordingId < right.m_strRecordingId + : m_iClientId < right.m_iClientId; +} + +bool CPVRRecordingUid::operator==(const CPVRRecordingUid& right) const +{ + return m_iClientId == right.m_iClientId && m_strRecordingId == right.m_strRecordingId; +} + +bool CPVRRecordingUid::operator!=(const CPVRRecordingUid& right) const +{ + return m_iClientId != right.m_iClientId || m_strRecordingId != right.m_strRecordingId; +} + +const std::string CPVRRecording::IMAGE_OWNER_PATTERN = "pvrrecording"; + +CPVRRecording::CPVRRecording() + : m_iconPath(IMAGE_OWNER_PATTERN), + m_thumbnailPath(IMAGE_OWNER_PATTERN), + m_fanartPath(IMAGE_OWNER_PATTERN) +{ + Reset(); +} + +CPVRRecording::CPVRRecording(const PVR_RECORDING& recording, unsigned int iClientId) + : m_iconPath(recording.strIconPath, IMAGE_OWNER_PATTERN), + m_thumbnailPath(recording.strThumbnailPath, IMAGE_OWNER_PATTERN), + m_fanartPath(recording.strFanartPath, IMAGE_OWNER_PATTERN) +{ + Reset(); + + m_strRecordingId = recording.strRecordingId; + m_strTitle = recording.strTitle; + m_strShowTitle = recording.strEpisodeName; + m_iSeason = recording.iSeriesNumber; + m_iEpisode = recording.iEpisodeNumber; + if (recording.iYear > 0) + SetYear(recording.iYear); + m_iClientId = iClientId; + m_recordingTime = + recording.recordingTime + + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection; + m_iPriority = recording.iPriority; + m_iLifetime = recording.iLifetime; + // Deleted recording is placed at the root of the deleted view + m_strDirectory = recording.bIsDeleted ? "" : recording.strDirectory; + m_strPlot = recording.strPlot; + m_strPlotOutline = recording.strPlotOutline; + m_strChannelName = recording.strChannelName; + m_bIsDeleted = recording.bIsDeleted; + m_iEpgEventId = recording.iEpgEventId; + m_iChannelUid = recording.iChannelUid; + if (strlen(recording.strFirstAired) > 0) + m_firstAired.SetFromW3CDateTime(recording.strFirstAired); + m_iFlags = recording.iFlags; + if (recording.sizeInBytes >= 0) + m_sizeInBytes = recording.sizeInBytes; + m_strProviderName = recording.strProviderName; + m_iClientProviderUniqueId = recording.iClientProviderUid; + + SetGenre(recording.iGenreType, recording.iGenreSubType, recording.strGenreDescription); + CVideoInfoTag::SetPlayCount(recording.iPlayCount); + if (recording.iLastPlayedPosition > 0 && recording.iDuration > recording.iLastPlayedPosition) + CVideoInfoTag::SetResumePoint(recording.iLastPlayedPosition, recording.iDuration, ""); + SetDuration(recording.iDuration); + + // As the channel a recording was done on (probably long time ago) might no longer be + // available today prefer addon-supplied channel type (tv/radio) over channel attribute. + if (recording.channelType != PVR_RECORDING_CHANNEL_TYPE_UNKNOWN) + { + m_bRadio = recording.channelType == PVR_RECORDING_CHANNEL_TYPE_RADIO; + } + else + { + const std::shared_ptr<CPVRChannel> channel(Channel()); + if (channel) + { + m_bRadio = channel->IsRadio(); + } + else + { + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(m_iClientId); + bool bSupportsRadio = client && client->GetClientCapabilities().SupportsRadio(); + if (bSupportsRadio && client && client->GetClientCapabilities().SupportsTV()) + { + CLog::Log(LOGWARNING, "Unable to determine channel type. Defaulting to TV."); + m_bRadio = false; // Assume TV. + } + else + { + m_bRadio = bSupportsRadio; + } + } + } + + UpdatePath(); +} + +bool CPVRRecording::operator==(const CPVRRecording& right) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return (this == &right) || + (m_strRecordingId == right.m_strRecordingId && m_iClientId == right.m_iClientId && + m_strChannelName == right.m_strChannelName && m_recordingTime == right.m_recordingTime && + GetDuration() == right.GetDuration() && m_strPlotOutline == right.m_strPlotOutline && + m_strPlot == right.m_strPlot && m_iPriority == right.m_iPriority && + m_iLifetime == right.m_iLifetime && m_strDirectory == right.m_strDirectory && + m_strFileNameAndPath == right.m_strFileNameAndPath && m_strTitle == right.m_strTitle && + m_strShowTitle == right.m_strShowTitle && m_iSeason == right.m_iSeason && + m_iEpisode == right.m_iEpisode && GetPremiered() == right.GetPremiered() && + m_iconPath == right.m_iconPath && m_thumbnailPath == right.m_thumbnailPath && + m_fanartPath == right.m_fanartPath && m_iRecordingId == right.m_iRecordingId && + m_bIsDeleted == right.m_bIsDeleted && m_iEpgEventId == right.m_iEpgEventId && + m_iChannelUid == right.m_iChannelUid && m_bRadio == right.m_bRadio && + m_genre == right.m_genre && m_iGenreType == right.m_iGenreType && + m_iGenreSubType == right.m_iGenreSubType && m_firstAired == right.m_firstAired && + m_iFlags == right.m_iFlags && m_sizeInBytes == right.m_sizeInBytes && + m_strProviderName == right.m_strProviderName && + m_iClientProviderUniqueId == right.m_iClientProviderUniqueId); +} + +bool CPVRRecording::operator!=(const CPVRRecording& right) const +{ + return !(*this == right); +} + +void CPVRRecording::FillAddonData(PVR_RECORDING& recording) const +{ + time_t recTime; + RecordingTimeAsUTC().GetAsTime(recTime); + + recording = {}; + strncpy(recording.strRecordingId, ClientRecordingID().c_str(), + sizeof(recording.strRecordingId) - 1); + strncpy(recording.strTitle, m_strTitle.c_str(), sizeof(recording.strTitle) - 1); + strncpy(recording.strEpisodeName, m_strShowTitle.c_str(), sizeof(recording.strEpisodeName) - 1); + recording.iSeriesNumber = m_iSeason; + recording.iEpisodeNumber = m_iEpisode; + recording.iYear = GetYear(); + strncpy(recording.strDirectory, Directory().c_str(), sizeof(recording.strDirectory) - 1); + strncpy(recording.strPlotOutline, m_strPlotOutline.c_str(), sizeof(recording.strPlotOutline) - 1); + strncpy(recording.strPlot, m_strPlot.c_str(), sizeof(recording.strPlot) - 1); + strncpy(recording.strGenreDescription, GetGenresLabel().c_str(), + sizeof(recording.strGenreDescription) - 1); + strncpy(recording.strChannelName, ChannelName().c_str(), sizeof(recording.strChannelName) - 1); + strncpy(recording.strIconPath, ClientIconPath().c_str(), sizeof(recording.strIconPath) - 1); + strncpy(recording.strThumbnailPath, ClientThumbnailPath().c_str(), + sizeof(recording.strThumbnailPath) - 1); + strncpy(recording.strFanartPath, ClientFanartPath().c_str(), sizeof(recording.strFanartPath) - 1); + recording.recordingTime = + recTime - CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection; + recording.iDuration = GetDuration(); + recording.iPriority = Priority(); + recording.iLifetime = LifeTime(); + recording.iGenreType = GenreType(); + recording.iGenreSubType = GenreSubType(); + recording.iPlayCount = GetLocalPlayCount(); + recording.iLastPlayedPosition = std::lrint(GetLocalResumePoint().timeInSeconds); + recording.bIsDeleted = IsDeleted(); + recording.iEpgEventId = m_iEpgEventId; + recording.iChannelUid = ChannelUid(); + recording.channelType = + IsRadio() ? PVR_RECORDING_CHANNEL_TYPE_RADIO : PVR_RECORDING_CHANNEL_TYPE_TV; + if (FirstAired().IsValid()) + strncpy(recording.strFirstAired, FirstAired().GetAsW3CDate().c_str(), + sizeof(recording.strFirstAired) - 1); + recording.iFlags = Flags(); + recording.sizeInBytes = GetSizeInBytes(); + strncpy(recording.strProviderName, ProviderName().c_str(), sizeof(recording.strProviderName) - 1); + recording.iClientProviderUid = ClientProviderUniqueId(); +} + +void CPVRRecording::Serialize(CVariant& value) const +{ + CVideoInfoTag::Serialize(value); + + value["channel"] = m_strChannelName; + value["lifetime"] = m_iLifetime; + value["directory"] = m_strDirectory; + value["icon"] = ClientIconPath(); + value["starttime"] = m_recordingTime.IsValid() ? m_recordingTime.GetAsDBDateTime() : ""; + value["endtime"] = m_recordingTime.IsValid() ? EndTimeAsUTC().GetAsDBDateTime() : ""; + value["recordingid"] = m_iRecordingId; + value["isdeleted"] = m_bIsDeleted; + value["epgeventid"] = m_iEpgEventId; + value["channeluid"] = m_iChannelUid; + value["radio"] = m_bRadio; + value["genre"] = m_genre; + + if (!value.isMember("art")) + value["art"] = CVariant(CVariant::VariantTypeObject); + if (!ClientThumbnailPath().empty()) + value["art"]["thumb"] = ClientThumbnailPath(); + if (!ClientFanartPath().empty()) + value["art"]["fanart"] = ClientFanartPath(); + + value["clientid"] = m_iClientId; +} + +void CPVRRecording::ToSortable(SortItem& sortable, Field field) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (field == FieldSize) + sortable[FieldSize] = m_sizeInBytes; + else if (field == FieldProvider) + sortable[FieldProvider] = StringUtils::Format("{} {}", m_iClientId, m_iClientProviderUniqueId); + else + CVideoInfoTag::ToSortable(sortable, field); +} + +void CPVRRecording::Reset() +{ + m_strRecordingId.clear(); + m_iClientId = -1; + m_strChannelName.clear(); + m_strDirectory.clear(); + m_iPriority = -1; + m_iLifetime = -1; + m_strFileNameAndPath.clear(); + m_bGotMetaData = false; + m_iRecordingId = 0; + m_bIsDeleted = false; + m_bInProgress = true; + m_iEpgEventId = EPG_TAG_INVALID_UID; + m_iSeason = -1; + m_iEpisode = -1; + m_iChannelUid = PVR_CHANNEL_INVALID_UID; + m_bRadio = false; + m_iFlags = PVR_RECORDING_FLAG_UNDEFINED; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_sizeInBytes = 0; + } + m_strProviderName.clear(); + m_iClientProviderUniqueId = PVR_PROVIDER_INVALID_UID; + + m_recordingTime.Reset(); + CVideoInfoTag::Reset(); +} + +bool CPVRRecording::Delete() +{ + std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + return client && (client->DeleteRecording(*this) == PVR_ERROR_NO_ERROR); +} + +bool CPVRRecording::Undelete() +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + return client && (client->UndeleteRecording(*this) == PVR_ERROR_NO_ERROR); +} + +bool CPVRRecording::Rename(const std::string& strNewName) +{ + m_strTitle = strNewName; + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + return client && (client->RenameRecording(*this) == PVR_ERROR_NO_ERROR); +} + +bool CPVRRecording::SetPlayCount(int count) +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + if (client && client->GetClientCapabilities().SupportsRecordingsPlayCount()) + { + if (client->SetRecordingPlayCount(*this, count) != PVR_ERROR_NO_ERROR) + return false; + } + + return CVideoInfoTag::SetPlayCount(count); +} + +bool CPVRRecording::IncrementPlayCount() +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + if (client && client->GetClientCapabilities().SupportsRecordingsPlayCount()) + { + if (client->SetRecordingPlayCount(*this, CVideoInfoTag::GetPlayCount() + 1) != + PVR_ERROR_NO_ERROR) + return false; + } + + return CVideoInfoTag::IncrementPlayCount(); +} + +bool CPVRRecording::SetResumePoint(const CBookmark& resumePoint) +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + if (client && client->GetClientCapabilities().SupportsRecordingsLastPlayedPosition()) + { + if (client->SetRecordingLastPlayedPosition(*this, lrint(resumePoint.timeInSeconds)) != + PVR_ERROR_NO_ERROR) + return false; + } + + return CVideoInfoTag::SetResumePoint(resumePoint); +} + +bool CPVRRecording::SetResumePoint(double timeInSeconds, + double totalTimeInSeconds, + const std::string& playerState /* = "" */) +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + if (client && client->GetClientCapabilities().SupportsRecordingsLastPlayedPosition()) + { + if (client->SetRecordingLastPlayedPosition(*this, lrint(timeInSeconds)) != PVR_ERROR_NO_ERROR) + return false; + } + + return CVideoInfoTag::SetResumePoint(timeInSeconds, totalTimeInSeconds, playerState); +} + +CBookmark CPVRRecording::GetResumePoint() const +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + if (client && client->GetClientCapabilities().SupportsRecordingsLastPlayedPosition() && + m_resumePointRefetchTimeout.IsTimePast()) + { + // @todo: root cause should be fixed. details: https://github.com/xbmc/xbmc/pull/14961 + m_resumePointRefetchTimeout.Set(10s); // update resume point from backend at most every 10 secs + + int pos = -1; + client->GetRecordingLastPlayedPosition(*this, pos); + + if (pos >= 0) + { + CBookmark resumePoint(CVideoInfoTag::GetResumePoint()); + resumePoint.timeInSeconds = pos; + resumePoint.totalTimeInSeconds = (pos == 0) ? 0 : m_duration; + CPVRRecording* pThis = const_cast<CPVRRecording*>(this); + pThis->CVideoInfoTag::SetResumePoint(resumePoint); + } + } + return CVideoInfoTag::GetResumePoint(); +} + +bool CPVRRecording::UpdateRecordingSize() +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + if (client && client->GetClientCapabilities().SupportsRecordingsSize() && + m_recordingSizeRefetchTimeout.IsTimePast()) + { + // @todo: root cause should be fixed. details: https://github.com/xbmc/xbmc/pull/14961 + m_recordingSizeRefetchTimeout.Set(10s); // update size from backend at most every 10 secs + + int64_t sizeInBytes = -1; + client->GetRecordingSize(*this, sizeInBytes); + + std::unique_lock<CCriticalSection> lock(m_critSection); + if (sizeInBytes >= 0 && sizeInBytes != m_sizeInBytes) + { + m_sizeInBytes = sizeInBytes; + return true; + } + } + + return false; +} + +void CPVRRecording::UpdateMetadata(CVideoDatabase& db, const CPVRClient& client) +{ + if (m_bGotMetaData || !db.IsOpen()) + return; + + if (!client.GetClientCapabilities().SupportsRecordingsPlayCount()) + CVideoInfoTag::SetPlayCount(db.GetPlayCount(m_strFileNameAndPath)); + + if (!client.GetClientCapabilities().SupportsRecordingsLastPlayedPosition()) + { + CBookmark resumePoint; + if (db.GetResumeBookMark(m_strFileNameAndPath, resumePoint)) + CVideoInfoTag::SetResumePoint(resumePoint); + } + + m_lastPlayed = db.GetLastPlayed(m_strFileNameAndPath); + + m_bGotMetaData = true; +} + +std::vector<PVR_EDL_ENTRY> CPVRRecording::GetEdl() const +{ + std::vector<PVR_EDL_ENTRY> edls; + + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + if (client && client->GetClientCapabilities().SupportsRecordingsEdl()) + client->GetRecordingEdl(*this, edls); + + return edls; +} + +void CPVRRecording::Update(const CPVRRecording& tag, const CPVRClient& client) +{ + m_strRecordingId = tag.m_strRecordingId; + m_iClientId = tag.m_iClientId; + m_strTitle = tag.m_strTitle; + m_strShowTitle = tag.m_strShowTitle; + m_iSeason = tag.m_iSeason; + m_iEpisode = tag.m_iEpisode; + SetPremiered(tag.GetPremiered()); + m_recordingTime = tag.m_recordingTime; + m_iPriority = tag.m_iPriority; + m_iLifetime = tag.m_iLifetime; + m_strDirectory = tag.m_strDirectory; + m_strPlot = tag.m_strPlot; + m_strPlotOutline = tag.m_strPlotOutline; + m_strChannelName = tag.m_strChannelName; + m_genre = tag.m_genre; + m_iconPath = tag.m_iconPath; + m_thumbnailPath = tag.m_thumbnailPath; + m_fanartPath = tag.m_fanartPath; + m_bIsDeleted = tag.m_bIsDeleted; + m_iEpgEventId = tag.m_iEpgEventId; + m_iChannelUid = tag.m_iChannelUid; + m_bRadio = tag.m_bRadio; + m_firstAired = tag.m_firstAired; + m_iFlags = tag.m_iFlags; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_sizeInBytes = tag.m_sizeInBytes; + m_strProviderName = tag.m_strProviderName; + m_iClientProviderUniqueId = tag.m_iClientProviderUniqueId; + } + + if (client.GetClientCapabilities().SupportsRecordingsPlayCount()) + CVideoInfoTag::SetPlayCount(tag.GetLocalPlayCount()); + + if (client.GetClientCapabilities().SupportsRecordingsLastPlayedPosition()) + CVideoInfoTag::SetResumePoint(tag.GetLocalResumePoint()); + + SetDuration(tag.GetDuration()); + + if (m_iGenreType == EPG_GENRE_USE_STRING || m_iGenreSubType == EPG_GENRE_USE_STRING) + { + /* No type/subtype. Use the provided description */ + m_genre = tag.m_genre; + } + else + { + /* Determine genre description by type/subtype */ + m_genre = StringUtils::Split( + CPVREpg::ConvertGenreIdToString(tag.m_iGenreType, tag.m_iGenreSubType), + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); + } + + //Old Method of identifying TV show title and subtitle using m_strDirectory and strPlotOutline (deprecated) + std::string strShow = StringUtils::Format("{} - ", g_localizeStrings.Get(20364)); + if (StringUtils::StartsWithNoCase(m_strPlotOutline, strShow)) + { + CLog::Log(LOGWARNING, "PVR addon provides episode name in strPlotOutline which is deprecated"); + std::string strEpisode = m_strPlotOutline; + std::string strTitle = m_strDirectory; + + size_t pos = strTitle.rfind('/'); + strTitle.erase(0, pos + 1); + strEpisode.erase(0, strShow.size()); + m_strTitle = strTitle; + pos = strEpisode.find('-'); + strEpisode.erase(0, pos + 2); + m_strShowTitle = strEpisode; + } + + UpdatePath(); +} + +void CPVRRecording::UpdatePath() +{ + m_strFileNameAndPath = CPVRRecordingsPath(m_bIsDeleted, m_bRadio, m_strDirectory, m_strTitle, + m_iSeason, m_iEpisode, GetYear(), m_strShowTitle, + m_strChannelName, m_recordingTime, m_strRecordingId); +} + +const CDateTime& CPVRRecording::RecordingTimeAsLocalTime() const +{ + static CDateTime tmp; + tmp.SetFromUTCDateTime(m_recordingTime); + + return tmp; +} + +CDateTime CPVRRecording::EndTimeAsUTC() const +{ + unsigned int duration = GetDuration(); + return m_recordingTime + CDateTimeSpan(0, 0, duration / 60, duration % 60); +} + +CDateTime CPVRRecording::EndTimeAsLocalTime() const +{ + CDateTime ret; + ret.SetFromUTCDateTime(EndTimeAsUTC()); + return ret; +} + +bool CPVRRecording::WillBeExpiredWithNewLifetime(int iLifetime) const +{ + if (iLifetime > 0) + return (EndTimeAsUTC() + CDateTimeSpan(iLifetime, 0, 0, 0)) <= CDateTime::GetUTCDateTime(); + + return false; +} + +CDateTime CPVRRecording::ExpirationTimeAsLocalTime() const +{ + CDateTime ret; + if (m_iLifetime > 0) + ret = EndTimeAsLocalTime() + CDateTimeSpan(m_iLifetime, 0, 0, 0); + + return ret; +} + +std::string CPVRRecording::GetTitleFromURL(const std::string& url) +{ + return CPVRRecordingsPath(url).GetTitle(); +} + +std::shared_ptr<CPVRChannel> CPVRRecording::Channel() const +{ + if (m_iChannelUid != PVR_CHANNEL_INVALID_UID) + return CServiceBroker::GetPVRManager().ChannelGroups()->GetByUniqueID(m_iChannelUid, + m_iClientId); + + return std::shared_ptr<CPVRChannel>(); +} + +int CPVRRecording::ChannelUid() const +{ + return m_iChannelUid; +} + +int CPVRRecording::ClientID() const +{ + return m_iClientId; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRRecording::GetRecordingTimer() const +{ + const std::vector<std::shared_ptr<CPVRTimerInfoTag>> recordingTimers = + CServiceBroker::GetPVRManager().Timers()->GetActiveRecordings(); + + for (const auto& timer : recordingTimers) + { + if (timer->ClientID() == ClientID() && timer->ClientChannelUID() == ChannelUid()) + { + // first, match epg event uids, if available + if (timer->UniqueBroadcastID() == BroadcastUid() && + timer->UniqueBroadcastID() != EPG_TAG_INVALID_UID) + return timer; + + // alternatively, match start and end times + const CDateTime timerStart = + timer->StartAsUTC() - CDateTimeSpan(0, 0, timer->MarginStart(), 0); + const CDateTime timerEnd = timer->EndAsUTC() + CDateTimeSpan(0, 0, timer->MarginEnd(), 0); + if (timerStart <= RecordingTimeAsUTC() && timerEnd >= EndTimeAsUTC()) + return timer; + } + } + return {}; +} + +bool CPVRRecording::IsInProgress() const +{ + // Note: It is not enough to only check recording time and duration against 'now'. + // Only the state of the related timer is a safe indicator that the backend + // actually is recording this. + // Once the recording is known to not be in progress that will never change. + if (m_bInProgress) + m_bInProgress = GetRecordingTimer() != nullptr; + return m_bInProgress; +} + +void CPVRRecording::SetGenre(int iGenreType, int iGenreSubType, const std::string& strGenre) +{ + m_iGenreType = iGenreType; + m_iGenreSubType = iGenreSubType; + + if ((iGenreType == EPG_GENRE_USE_STRING || iGenreSubType == EPG_GENRE_USE_STRING) && + !strGenre.empty()) + { + /* Type and sub type are not given. Use the provided genre description if available. */ + m_genre = StringUtils::Split( + strGenre, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); + } + else + { + /* Determine the genre description from the type and subtype IDs */ + m_genre = StringUtils::Split( + CPVREpg::ConvertGenreIdToString(iGenreType, iGenreSubType), + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); + } +} + +const std::string CPVRRecording::GetGenresLabel() const +{ + return StringUtils::Join( + m_genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); +} + +CDateTime CPVRRecording::FirstAired() const +{ + return m_firstAired; +} + +void CPVRRecording::SetYear(int year) +{ + if (year > 0) + m_premiered = CDateTime(year, 1, 1, 0, 0, 0); +} + +int CPVRRecording::GetYear() const +{ + return m_premiered.IsValid() ? m_premiered.GetYear() : 0; +} + +bool CPVRRecording::HasYear() const +{ + return m_premiered.IsValid(); +} + +bool CPVRRecording::IsNew() const +{ + return (m_iFlags & PVR_RECORDING_FLAG_IS_NEW) > 0; +} + +bool CPVRRecording::IsPremiere() const +{ + return (m_iFlags & PVR_RECORDING_FLAG_IS_PREMIERE) > 0; +} + +bool CPVRRecording::IsLive() const +{ + return (m_iFlags & PVR_RECORDING_FLAG_IS_LIVE) > 0; +} + +bool CPVRRecording::IsFinale() const +{ + return (m_iFlags & PVR_RECORDING_FLAG_IS_FINALE) > 0; +} + +int64_t CPVRRecording::GetSizeInBytes() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_sizeInBytes; +} + +int CPVRRecording::ClientProviderUniqueId() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iClientProviderUniqueId; +} + +std::string CPVRRecording::ProviderName() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strProviderName; +} + +std::shared_ptr<CPVRProvider> CPVRRecording::GetDefaultProvider() const +{ + return CServiceBroker::GetPVRManager().Providers()->GetByClient(m_iClientId, + PVR_PROVIDER_INVALID_UID); +} + +bool CPVRRecording::HasClientProvider() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iClientProviderUniqueId != PVR_PROVIDER_INVALID_UID; +} + +std::shared_ptr<CPVRProvider> CPVRRecording::GetProvider() const +{ + auto provider = CServiceBroker::GetPVRManager().Providers()->GetByClient( + m_iClientId, m_iClientProviderUniqueId); + + if (!provider) + provider = GetDefaultProvider(); + + return provider; +} diff --git a/xbmc/pvr/recordings/PVRRecording.h b/xbmc/pvr/recordings/PVRRecording.h new file mode 100644 index 0000000..bdd8378 --- /dev/null +++ b/xbmc/pvr/recordings/PVRRecording.h @@ -0,0 +1,541 @@ +/* + * 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_providers.h" +#include "pvr/PVRCachedImage.h" +#include "threads/CriticalSection.h" +#include "threads/SystemClock.h" +#include "video/Bookmark.h" +#include "video/VideoInfoTag.h" + +#include <memory> +#include <string> +#include <vector> + +class CVideoDatabase; + +struct PVR_EDL_ENTRY; +struct PVR_RECORDING; + +namespace PVR +{ +class CPVRChannel; +class CPVRClient; +class CPVRProvider; +class CPVRTimerInfoTag; + +/*! + * @brief Representation of a CPVRRecording unique ID. + */ +class CPVRRecordingUid final +{ +public: + int m_iClientId; /*!< ID of the backend */ + std::string m_strRecordingId; /*!< unique ID of the recording on the client */ + + CPVRRecordingUid(int iClientId, const std::string& strRecordingId); + + bool operator>(const CPVRRecordingUid& right) const; + bool operator<(const CPVRRecordingUid& right) const; + bool operator==(const CPVRRecordingUid& right) const; + bool operator!=(const CPVRRecordingUid& right) const; +}; + +class CPVRRecording final : public CVideoInfoTag +{ +public: + static const std::string IMAGE_OWNER_PATTERN; + + CPVRRecording(); + CPVRRecording(const PVR_RECORDING& recording, unsigned int iClientId); + + bool operator==(const CPVRRecording& right) const; + bool operator!=(const CPVRRecording& right) const; + + /*! + * @brief Copy over data to the given PVR_RECORDING instance. + * @param recording The recording instance to fill. + */ + void FillAddonData(PVR_RECORDING& recording) const; + + void Serialize(CVariant& value) const override; + + // ISortable implementation + void ToSortable(SortItem& sortable, Field field) const override; + + /*! + * @brief Reset this tag to it's initial state. + */ + void Reset(); + + /*! + * @brief Delete this recording on the client (if supported). + * @return True if it was deleted successfully, false otherwise. + */ + bool Delete(); + + /*! + * @brief Undelete this recording on the client (if supported). + * @return True if it was undeleted successfully, false otherwise. + */ + bool Undelete(); + + /*! + * @brief Rename this recording on the client (if supported). + * @param strNewName The new name. + * @return True if it was renamed successfully, false otherwise. + */ + bool Rename(const std::string& strNewName); + + /*! + * @brief Set this recording's play count. The value will be transferred to the backend if it supports server-side play counts. + * @param count play count. + * @return True if play count was set successfully, false otherwise. + */ + bool SetPlayCount(int count) override; + + /*! + * @brief Increment this recording's play count. The value will be transferred to the backend if it supports server-side play counts. + * @return True if play count was increased successfully, false otherwise. + */ + bool IncrementPlayCount() override; + + /*! + * @brief Set this recording's play count without transferring the value to the backend, even if it supports server-side play counts. + * @param count play count. + * @return True if play count was set successfully, false otherwise. + */ + bool SetLocalPlayCount(int count) { return CVideoInfoTag::SetPlayCount(count); } + + /*! + * @brief Get this recording's local play count. The value will not be obtained from the backend, even if it supports server-side play counts. + * @return the play count. + */ + int GetLocalPlayCount() const { return CVideoInfoTag::GetPlayCount(); } + + /*! + * @brief Set this recording's resume point. The value will be transferred to the backend if it supports server-side resume points. + * @param resumePoint resume point. + * @return True if resume point was set successfully, false otherwise. + */ + bool SetResumePoint(const CBookmark& resumePoint) override; + + /*! + * @brief Set this recording's resume point. The value will be transferred to the backend if it supports server-side resume points. + * @param timeInSeconds the time of the resume point + * @param totalTimeInSeconds the total time of the video + * @param playerState the player state + * @return True if resume point was set successfully, false otherwise. + */ + bool SetResumePoint(double timeInSeconds, + double totalTimeInSeconds, + const std::string& playerState = "") override; + + /*! + * @brief Get this recording's resume point. The value will be obtained from the backend if it supports server-side resume points. + * @return the resume point. + */ + CBookmark GetResumePoint() const override; + + /*! + * @brief Update this recording's size. The value will be obtained from the backend if it supports server-side size retrieval. + * @return true if the the updated value is different, false otherwise. + */ + bool UpdateRecordingSize(); + + /*! + * @brief Get this recording's local resume point. The value will not be obtained from the backend even if it supports server-side resume points. + * @return the resume point. + */ + CBookmark GetLocalResumePoint() const { return CVideoInfoTag::GetResumePoint(); } + + /*! + * @brief Retrieve the edit decision list (EDL) of a recording on the backend. + * @return The edit decision list (empty on error) + */ + std::vector<PVR_EDL_ENTRY> GetEdl() const; + + /*! + * @brief Get the resume point and play count from the database if the + * client doesn't handle it itself. + * @param db The database to read the data from. + * @param client The client this recording belongs to. + */ + void UpdateMetadata(CVideoDatabase& db, const CPVRClient& client); + + /*! + * @brief Update this tag with the contents of the given tag. + * @param tag The new tag info. + * @param client The client this recording belongs to. + */ + void Update(const CPVRRecording& tag, const CPVRClient& client); + + /*! + * @brief Retrieve the recording start as UTC time + * @return the recording start time + */ + const CDateTime& RecordingTimeAsUTC() const { return m_recordingTime; } + + /*! + * @brief Retrieve the recording start as local time + * @return the recording start time + */ + const CDateTime& RecordingTimeAsLocalTime() const; + + /*! + * @brief Retrieve the recording end as UTC time + * @return the recording end time + */ + CDateTime EndTimeAsUTC() const; + + /*! + * @brief Retrieve the recording end as local time + * @return the recording end time + */ + CDateTime EndTimeAsLocalTime() const; + + /*! + * @brief Check whether this recording has an expiration time + * @return True if the recording has an expiration time, false otherwise + */ + bool HasExpirationTime() const { return m_iLifetime > 0; } + + /*! + * @brief Retrieve the recording expiration time as local time + * @return the recording expiration time + */ + CDateTime ExpirationTimeAsLocalTime() const; + + /*! + * @brief Check whether this recording will immediately expire if the given lifetime value would be set + * @param iLifetime The lifetime value to check + * @return True if the recording would immediately expire, false otherwiese + */ + bool WillBeExpiredWithNewLifetime(int iLifetime) const; + + /*! + * @brief Retrieve the recording title from the URL path + * @param url the URL for the recording + * @return Title of the recording + */ + static std::string GetTitleFromURL(const std::string& url); + + /*! + * @brief If deleted but can be undeleted it is true + */ + bool IsDeleted() const { return m_bIsDeleted; } + + /*! + * @brief Check whether this is a tv or radio recording + * @return true if this is a radio recording, false if this is a tv recording + */ + bool IsRadio() const { return m_bRadio; } + + /*! + * @return Broadcast id of the EPG event associated with this recording or EPG_TAG_INVALID_UID + */ + unsigned int BroadcastUid() const { return m_iEpgEventId; } + + /*! + * @return Get the channel on which this recording is/was running + * @note Only works if the recording has a channel uid provided by the add-on + */ + std::shared_ptr<CPVRChannel> Channel() const; + + /*! + * @brief Get the uid of the channel on which this recording is/was running + * @return the uid of the channel or PVR_CHANNEL_INVALID_UID + */ + int ChannelUid() const; + + /*! + * @brief the identifier of the client that serves this recording + * @return the client identifier + */ + int ClientID() const; + + /*! + * @brief Get the recording ID as upplied by the client + * @return the recording identifier + */ + std::string ClientRecordingID() const { return m_strRecordingId; } + + /*! + * @brief Get the recording ID as upplied by the client + * @return the recording identifier + */ + unsigned int RecordingID() const { return m_iRecordingId; } + + /*! + * @brief Set the recording ID + * @param recordingId The new identifier + */ + void SetRecordingID(unsigned int recordingId) { m_iRecordingId = recordingId; } + + /*! + * @brief Get the directory for this recording + * @return the directory + */ + std::string Directory() const { return m_strDirectory; } + + /*! + * @brief Get the priority for this recording + * @return the priority + */ + int Priority() const { return m_iPriority; } + + /*! + * @brief Get the lifetime for this recording + * @return the lifetime + */ + int LifeTime() const { return m_iLifetime; } + + /*! + * @brief Set the lifetime for this recording + * @param lifeTime The lifetime + */ + void SetLifeTime(int lifeTime) { m_iLifetime = lifeTime; } + + /*! + * @brief Get the channel name for this recording + * @return the channel name + */ + std::string ChannelName() const { return m_strChannelName; } + + /*! + * @brief Return the icon path as given by the client. + * @return The path. + */ + const std::string& ClientIconPath() const { return m_iconPath.GetClientImage(); } + + /*! + * @brief Return the thumbnail path as given by the client. + * @return The path. + */ + const std::string& ClientThumbnailPath() const { return m_thumbnailPath.GetClientImage(); } + + /*! + * @brief Return the fanart path as given by the client. + * @return The path. + */ + const std::string& ClientFanartPath() const { return m_fanartPath.GetClientImage(); } + + /*! + * @brief Return the icon path used by Kodi. + * @return The path. + */ + const std::string& IconPath() const { return m_iconPath.GetLocalImage(); } + + /*! + * @brief Return the thumnail path used by Kodi. + * @return The path. + */ + const std::string& ThumbnailPath() const { return m_thumbnailPath.GetLocalImage(); } + + /*! + * @brief Return the fanart path used by Kodi. + * @return The path. + */ + const std::string& FanartPath() const { return m_fanartPath.GetLocalImage(); } + + /*! + * @brief Retrieve the recording Episode Name + * @note Returns an empty string if no Episode Name was provided by the PVR client + */ + std::string EpisodeName() const { return m_strShowTitle; } + + /*! + * @brief check whether this recording is currently in progress + * @return true if the recording is in progress, false otherwise + */ + bool IsInProgress() const; + + /*! + * @brief return the timer for an in-progress recording, if any + * @return the timer if the recording is in progress, nullptr otherwise + */ + std::shared_ptr<CPVRTimerInfoTag> GetRecordingTimer() const; + + /*! + * @brief set the genre for this recording. + * @param iGenreType The genre type ID. If set to EPG_GENRE_USE_STRING, set genre to the value provided with strGenre. Otherwise, compile the genre string from the values given by iGenreType and iGenreSubType + * @param iGenreSubType The genre subtype ID + * @param strGenre The genre + */ + void SetGenre(int iGenreType, int iGenreSubType, const std::string& strGenre); + + /*! + * @brief Get the genre type ID of this recording. + * @return The genre type ID. + */ + int GenreType() const { return m_iGenreType; } + + /*! + * @brief Get the genre subtype ID of this recording. + * @return The genre subtype ID. + */ + int GenreSubType() const { return m_iGenreSubType; } + + /*! + * @brief Get the genre as human readable string. + * @return The genre. + */ + const std::vector<std::string> Genre() const { return m_genre; } + + /*! + * @brief Get the genre(s) of this recording as formatted string. + * @return The genres label. + */ + const std::string GetGenresLabel() const; + + /*! + * @brief Get the first air date of this recording. + * @return The first air date. + */ + CDateTime FirstAired() const; + + /*! + * @brief Get the premiere year of this recording. + * @return The premiere year + */ + int GetYear() const override; + + /*! + * @brief Set the premiere year of this recording. + * @param year The premiere year + */ + void SetYear(int year) override; + + /*! + * @brief Check if the premiere year of this recording is valid + * @return True if the recording has as valid premiere date, false otherwise + */ + bool HasYear() const override; + + /*! + * @brief Check whether this recording will be flagged as new. + * @return True if this recording will be flagged as new, false otherwise + */ + bool IsNew() const; + + /*! + * @brief Check whether this recording will be flagged as a premiere. + * @return True if this recording will be flagged as a premiere, false otherwise + */ + bool IsPremiere() const; + + /*! + * @brief Check whether this recording will be flagged as a finale. + * @return True if this recording will be flagged as a finale, false otherwise + */ + bool IsFinale() const; + + /*! + * @brief Check whether this recording will be flagged as live. + * @return True if this recording will be flagged as live, false otherwise + */ + bool IsLive() const; + + /*! + * @brief Return the flags (PVR_RECORDING_FLAG_*) of this recording as a bitfield. + * @return the flags. + */ + unsigned int Flags() const { return m_iFlags; } + + /*! + * @brief Return the size of this recording in bytes. + * @return the size in bytes. + */ + int64_t GetSizeInBytes() const; + + /*! + * @brief Mark a recording as dirty/clean. + * @param bDirty true to mark as dirty, false to mark as clean. + */ + void SetDirty(bool bDirty) { m_bDirty = bDirty; } + + /*! + * @brief Return whether the recording is marked dirty. + * @return true if dirty, false otherwise. + */ + bool IsDirty() const { return m_bDirty; } + + /*! + * @brief Get the uid of the provider on the client which this recording is from + * @return the client uid of the provider or PVR_PROVIDER_INVALID_UID + */ + int ClientProviderUniqueId() const; + + /*! + * @brief Get the client provider name for this recording + * @return m_strProviderName The name for this recording provider + */ + std::string ProviderName() const; + + /*! + * @brief Get the default provider of this recording. The default + * provider represents the PVR add-on itself. + * @return The default provider of this recording + */ + std::shared_ptr<CPVRProvider> GetDefaultProvider() const; + + /*! + * @brief Whether or not this recording has a provider set by the client. + * @return True if a provider was set by the client, false otherwise. + */ + bool HasClientProvider() const; + + /*! + * @brief Get the provider of this recording. This may be the default provider or a + * custom provider set by the client. If @ref "HasClientProvider()" returns true + * the provider will be custom from the client, otherwise the default provider. + * @return The provider of this recording + */ + std::shared_ptr<CPVRProvider> GetProvider() const; + +private: + CPVRRecording(const CPVRRecording& tag) = delete; + CPVRRecording& operator=(const CPVRRecording& other) = delete; + + void UpdatePath(); + + int m_iClientId; /*!< ID of the backend */ + std::string m_strRecordingId; /*!< unique ID of the recording on the client */ + std::string m_strChannelName; /*!< name of the channel this was recorded from */ + int m_iPriority; /*!< priority of this recording */ + int m_iLifetime; /*!< lifetime of this recording */ + std::string m_strDirectory; /*!< directory of this recording on the client */ + unsigned int m_iRecordingId; /*!< id that won't change while xbmc is running */ + + CPVRCachedImage m_iconPath; /*!< icon path */ + CPVRCachedImage m_thumbnailPath; /*!< thumbnail path */ + CPVRCachedImage m_fanartPath; /*!< fanart path */ + CDateTime m_recordingTime; /*!< start time of the recording */ + bool m_bGotMetaData; + bool m_bIsDeleted; /*!< set if entry is a deleted recording which can be undelete */ + mutable bool m_bInProgress; /*!< set if recording might be in progress */ + unsigned int m_iEpgEventId; /*!< epg broadcast id associated with this recording */ + int m_iChannelUid; /*!< channel uid associated with this recording */ + bool m_bRadio; /*!< radio or tv recording */ + int m_iGenreType = 0; /*!< genre type */ + int m_iGenreSubType = 0; /*!< genre subtype */ + mutable XbmcThreads::EndTime<> m_resumePointRefetchTimeout; + unsigned int m_iFlags = 0; /*!< the flags applicable to this recording */ + mutable XbmcThreads::EndTime<> m_recordingSizeRefetchTimeout; + int64_t m_sizeInBytes = 0; /*!< the size of the recording in bytes */ + bool m_bDirty = false; + std::string m_strProviderName; /*!< name of the provider this recording is from */ + int m_iClientProviderUniqueId = + PVR_PROVIDER_INVALID_UID; /*!< provider uid associated with this recording on the client */ + + mutable CCriticalSection m_critSection; +}; +} // namespace PVR diff --git a/xbmc/pvr/recordings/PVRRecordings.cpp b/xbmc/pvr/recordings/PVRRecordings.cpp new file mode 100644 index 0000000..d6fd480 --- /dev/null +++ b/xbmc/pvr/recordings/PVRRecordings.cpp @@ -0,0 +1,361 @@ +/* + * 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 "PVRRecordings.h" + +#include "ServiceBroker.h" +#include "pvr/PVRCachedImages.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/recordings/PVRRecordingsPath.h" +#include "utils/URIUtils.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" + +#include <algorithm> +#include <iterator> +#include <memory> +#include <mutex> +#include <utility> +#include <vector> + +using namespace PVR; + +CPVRRecordings::CPVRRecordings() = default; + +CPVRRecordings::~CPVRRecordings() +{ + if (m_database && m_database->IsOpen()) + m_database->Close(); +} + +bool CPVRRecordings::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_bIsUpdating) + return false; + + m_bIsUpdating = true; + + for (const auto& recording : m_recordings) + recording.second->SetDirty(true); + + std::vector<int> failedClients; + CServiceBroker::GetPVRManager().Clients()->GetRecordings(clients, this, false, failedClients); + CServiceBroker::GetPVRManager().Clients()->GetRecordings(clients, this, true, failedClients); + + // remove recordings that were deleted at the backend + for (auto it = m_recordings.begin(); it != m_recordings.end();) + { + if ((*it).second->IsDirty() && std::find(failedClients.begin(), failedClients.end(), + (*it).second->ClientID()) == failedClients.end()) + it = m_recordings.erase(it); + else + ++it; + } + + m_bIsUpdating = false; + CServiceBroker::GetPVRManager().PublishEvent(PVREvent::RecordingsInvalidated); + return true; +} + +bool CPVRRecordings::Update(const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + return UpdateFromClients(clients); +} + +void CPVRRecordings::Unload() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bDeletedTVRecordings = false; + m_bDeletedRadioRecordings = false; + m_iTVRecordings = 0; + m_iRadioRecordings = 0; + m_recordings.clear(); +} + +void CPVRRecordings::UpdateInProgressSize() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_bIsUpdating) + return; + m_bIsUpdating = true; + + CLog::LogFC(LOGDEBUG, LOGPVR, "Updating recordings size"); + bool bHaveUpdatedInProgessRecording = false; + for (auto& recording : m_recordings) + { + if (recording.second->IsInProgress()) + { + if (recording.second->UpdateRecordingSize()) + bHaveUpdatedInProgessRecording = true; + } + } + + m_bIsUpdating = false; + + if (bHaveUpdatedInProgessRecording) + CServiceBroker::GetPVRManager().PublishEvent(PVREvent::RecordingsInvalidated); +} + +int CPVRRecordings::GetNumTVRecordings() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iTVRecordings; +} + +bool CPVRRecordings::HasDeletedTVRecordings() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bDeletedTVRecordings; +} + +int CPVRRecordings::GetNumRadioRecordings() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iRadioRecordings; +} + +bool CPVRRecordings::HasDeletedRadioRecordings() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bDeletedRadioRecordings; +} + +std::vector<std::shared_ptr<CPVRRecording>> CPVRRecordings::GetAll() const +{ + std::vector<std::shared_ptr<CPVRRecording>> recordings; + + std::unique_lock<CCriticalSection> lock(m_critSection); + std::transform(m_recordings.cbegin(), m_recordings.cend(), std::back_inserter(recordings), + [](const auto& recordingEntry) { return recordingEntry.second; }); + + return recordings; +} + +std::shared_ptr<CPVRRecording> CPVRRecordings::GetById(unsigned int iId) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + const auto it = + std::find_if(m_recordings.cbegin(), m_recordings.cend(), + [iId](const auto& recording) { return recording.second->RecordingID() == iId; }); + return it != m_recordings.cend() ? (*it).second : std::shared_ptr<CPVRRecording>(); +} + +std::shared_ptr<CPVRRecording> CPVRRecordings::GetByPath(const std::string& path) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + CPVRRecordingsPath recPath(path); + if (recPath.IsValid()) + { + bool bDeleted = recPath.IsDeleted(); + bool bRadio = recPath.IsRadio(); + + for (const auto& recording : m_recordings) + { + std::shared_ptr<CPVRRecording> current = recording.second; + // Omit recordings not matching criteria + if (!URIUtils::PathEquals(path, current->m_strFileNameAndPath) || + bDeleted != current->IsDeleted() || bRadio != current->IsRadio()) + continue; + + return current; + } + } + + return {}; +} + +std::shared_ptr<CPVRRecording> CPVRRecordings::GetById(int iClientId, + const std::string& strRecordingId) const +{ + std::shared_ptr<CPVRRecording> retVal; + std::unique_lock<CCriticalSection> lock(m_critSection); + const auto it = m_recordings.find(CPVRRecordingUid(iClientId, strRecordingId)); + if (it != m_recordings.end()) + retVal = it->second; + + return retVal; +} + +void CPVRRecordings::UpdateFromClient(const std::shared_ptr<CPVRRecording>& tag, + const CPVRClient& client) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (tag->IsDeleted()) + { + if (tag->IsRadio()) + m_bDeletedRadioRecordings = true; + else + m_bDeletedTVRecordings = true; + } + + std::shared_ptr<CPVRRecording> existingTag = GetById(tag->ClientID(), tag->ClientRecordingID()); + if (existingTag) + { + existingTag->Update(*tag, client); + existingTag->SetDirty(false); + } + else + { + tag->UpdateMetadata(GetVideoDatabase(), client); + tag->SetRecordingID(++m_iLastId); + m_recordings.insert({CPVRRecordingUid(tag->ClientID(), tag->ClientRecordingID()), tag}); + if (tag->IsRadio()) + ++m_iRadioRecordings; + else + ++m_iTVRecordings; + } +} + +std::shared_ptr<CPVRRecording> CPVRRecordings::GetRecordingForEpgTag( + const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + if (!epgTag) + return {}; + + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (const auto& recording : m_recordings) + { + if (recording.second->IsDeleted()) + continue; + + if (recording.second->ClientID() != epgTag->ClientID()) + continue; + + if (recording.second->ChannelUid() != epgTag->UniqueChannelID()) + continue; + + unsigned int iEpgEvent = recording.second->BroadcastUid(); + if (iEpgEvent != EPG_TAG_INVALID_UID) + { + if (iEpgEvent == epgTag->UniqueBroadcastID()) + return recording.second; + } + else + { + if (recording.second->RecordingTimeAsUTC() <= epgTag->StartAsUTC() && + recording.second->EndTimeAsUTC() >= epgTag->EndAsUTC()) + return recording.second; + } + } + + return std::shared_ptr<CPVRRecording>(); +} + +bool CPVRRecordings::SetRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording, + int count) +{ + return ChangeRecordingsPlayCount(recording, count); +} + +bool CPVRRecordings::IncrementRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording) +{ + return ChangeRecordingsPlayCount(recording, INCREMENT_PLAY_COUNT); +} + +bool CPVRRecordings::ChangeRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording, + int count) +{ + if (recording) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + CVideoDatabase& db = GetVideoDatabase(); + if (db.IsOpen()) + { + if (count == INCREMENT_PLAY_COUNT) + recording->IncrementPlayCount(); + else + recording->SetPlayCount(count); + + // Clear resume bookmark + if (recording->GetPlayCount() > 0) + { + db.ClearBookMarksOfFile(recording->m_strFileNameAndPath, CBookmark::RESUME); + recording->SetResumePoint(CBookmark()); + } + + CServiceBroker::GetPVRManager().PublishEvent(PVREvent::RecordingsInvalidated); + return true; + } + } + + return false; +} + +bool CPVRRecordings::MarkWatched(const std::shared_ptr<CPVRRecording>& recording, bool bWatched) +{ + if (bWatched) + return IncrementRecordingsPlayCount(recording); + else + return SetRecordingsPlayCount(recording, 0); +} + +bool CPVRRecordings::ResetResumePoint(const std::shared_ptr<CPVRRecording>& recording) +{ + bool bResult = false; + + if (recording) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + CVideoDatabase& db = GetVideoDatabase(); + if (db.IsOpen()) + { + bResult = true; + + db.ClearBookMarksOfFile(recording->m_strFileNameAndPath, CBookmark::RESUME); + recording->SetResumePoint(CBookmark()); + + CServiceBroker::GetPVRManager().PublishEvent(PVREvent::RecordingsInvalidated); + } + } + return bResult; +} + +CVideoDatabase& CPVRRecordings::GetVideoDatabase() +{ + if (!m_database) + { + m_database.reset(new CVideoDatabase()); + m_database->Open(); + + if (!m_database->IsOpen()) + CLog::LogF(LOGERROR, "Failed to open the video database"); + } + + return *m_database; +} + +int CPVRRecordings::CleanupCachedImages() +{ + std::vector<std::string> urlsToCheck; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& recording : m_recordings) + { + urlsToCheck.emplace_back(recording.second->ClientIconPath()); + urlsToCheck.emplace_back(recording.second->ClientThumbnailPath()); + urlsToCheck.emplace_back(recording.second->ClientFanartPath()); + urlsToCheck.emplace_back(recording.second->m_strFileNameAndPath); + } + } + + static const std::vector<PVRImagePattern> urlPatterns = { + {CPVRRecording::IMAGE_OWNER_PATTERN, ""}, // client-supplied icon, thumbnail, fanart + {"video", "pvr://recordings/"}, // kodi-generated video thumbnail + }; + return CPVRCachedImages::Cleanup(urlPatterns, urlsToCheck); +} diff --git a/xbmc/pvr/recordings/PVRRecordings.h b/xbmc/pvr/recordings/PVRRecordings.h new file mode 100644 index 0000000..1a7753f --- /dev/null +++ b/xbmc/pvr/recordings/PVRRecordings.h @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" + +#include <map> +#include <memory> +#include <string> +#include <vector> + +class CVideoDatabase; + +namespace PVR +{ +class CPVRClient; +class CPVREpgInfoTag; +class CPVRRecording; +class CPVRRecordingUid; +class CPVRRecordingsPath; + +class CPVRRecordings +{ +public: + CPVRRecordings(); + virtual ~CPVRRecordings(); + + /*! + * @brief Update all recordings from the given PVR clients. + * @param clients The PVR clients data should be loaded for. Leave empty for all clients. + * @return True on success, false otherwise. + */ + bool Update(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + /*! + * @brief unload all recordings. + */ + void Unload(); + + /*! + * @brief Update data with recordings from the given clients, sync with local data. + * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients. + * @return True on success, false otherwise. + */ + bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + /*! + * @brief client has delivered a new/updated recording. + * @param tag The recording + * @param client The client the recording belongs to. + */ + void UpdateFromClient(const std::shared_ptr<CPVRRecording>& tag, const CPVRClient& client); + + /*! + * @brief refresh the size of any in progress recordings from the clients. + */ + void UpdateInProgressSize(); + + int GetNumTVRecordings() const; + bool HasDeletedTVRecordings() const; + int GetNumRadioRecordings() const; + bool HasDeletedRadioRecordings() const; + + /*! + * @brief Set a recording's watched state + * @param recording The recording + * @param bWatched True to set watched, false to set unwatched state + * @return True on success, false otherwise + */ + bool MarkWatched(const std::shared_ptr<CPVRRecording>& recording, bool bWatched); + + /*! + * @brief Reset a recording's resume point, if any + * @param recording The recording + * @return True on success, false otherwise + */ + bool ResetResumePoint(const std::shared_ptr<CPVRRecording>& recording); + + /*! + * @brief Get a list of all recordings + * @return the list of all recordings + */ + std::vector<std::shared_ptr<CPVRRecording>> GetAll() const; + + std::shared_ptr<CPVRRecording> GetByPath(const std::string& path) const; + std::shared_ptr<CPVRRecording> GetById(int iClientId, const std::string& strRecordingId) const; + std::shared_ptr<CPVRRecording> GetById(unsigned int iId) const; + + /*! + * @brief Get the recording for the given epg tag, if any. + * @param epgTag The epg tag. + * @return The requested recording, or an empty recordingptr if none was found. + */ + std::shared_ptr<CPVRRecording> GetRecordingForEpgTag( + const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + + /*! + * @brief Erase stale texture db entries and image files. + * @return number of cleaned up images. + */ + int CleanupCachedImages(); + +private: + /*! + * @brief Get/Open the video database. + * @return A reference to the video database. + */ + CVideoDatabase& GetVideoDatabase(); + + /*! + * @brief Set a recording's play count + * @param recording The recording + * @param count The new play count + * @return True on success, false otherwise + */ + bool SetRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording, int count); + + /*! + * @brief Increment a recording's play count + * @param recording The recording + * @return True on success, false otherwise + */ + bool IncrementRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording); + + /*! + * @brief special value for parameter count of method ChangeRecordingsPlayCount + */ + static const int INCREMENT_PLAY_COUNT = -1; + + /*! + * @brief change the play count of the given recording + * @param recording The recording + * @param count The new play count or INCREMENT_PLAY_COUNT to denote that the current play count is to be incremented by one + * @return true if the play count was changed successfully + */ + bool ChangeRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording, int count); + + mutable CCriticalSection m_critSection; + bool m_bIsUpdating = false; + std::map<CPVRRecordingUid, std::shared_ptr<CPVRRecording>> m_recordings; + unsigned int m_iLastId = 0; + std::unique_ptr<CVideoDatabase> m_database; + bool m_bDeletedTVRecordings = false; + bool m_bDeletedRadioRecordings = false; + unsigned int m_iTVRecordings = 0; + unsigned int m_iRadioRecordings = 0; +}; +} // namespace PVR diff --git a/xbmc/pvr/recordings/PVRRecordingsPath.cpp b/xbmc/pvr/recordings/PVRRecordingsPath.cpp new file mode 100644 index 0000000..470d674 --- /dev/null +++ b/xbmc/pvr/recordings/PVRRecordingsPath.cpp @@ -0,0 +1,258 @@ +/* + * 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 "PVRRecordingsPath.h" + +#include "URL.h" +#include "XBDateTime.h" +#include "utils/RegExp.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +#include <string> +#include <vector> + +using namespace PVR; + +const std::string CPVRRecordingsPath::PATH_RECORDINGS = "pvr://recordings/"; +const std::string CPVRRecordingsPath::PATH_ACTIVE_TV_RECORDINGS = "pvr://recordings/tv/active/"; +const std::string CPVRRecordingsPath::PATH_ACTIVE_RADIO_RECORDINGS = + "pvr://recordings/radio/active/"; +const std::string CPVRRecordingsPath::PATH_DELETED_TV_RECORDINGS = "pvr://recordings/tv/deleted/"; +const std::string CPVRRecordingsPath::PATH_DELETED_RADIO_RECORDINGS = + "pvr://recordings/radio/deleted/"; + +CPVRRecordingsPath::CPVRRecordingsPath(const std::string& strPath) +{ + std::string strVarPath(TrimSlashes(strPath)); + const std::vector<std::string> segments = URIUtils::SplitPath(strVarPath); + + m_bValid = ((segments.size() >= 4) && // at least pvr://recordings/[tv|radio]/[active|deleted] + StringUtils::StartsWith(strVarPath, "pvr://") && (segments.at(1) == "recordings") && + ((segments.at(2) == "tv") || (segments.at(2) == "radio")) && + ((segments.at(3) == "active") || (segments.at(3) == "deleted"))); + if (m_bValid) + { + m_bRoot = (segments.size() == 4); + m_bRadio = (segments.at(2) == "radio"); + m_bActive = (segments.at(3) == "active"); + + if (m_bRoot) + strVarPath.append("/"); + else + { + size_t paramStart = m_path.find(", TV"); + if (paramStart == std::string::npos) + m_directoryPath = strVarPath.substr(GetDirectoryPathPosition()); + else + { + size_t dirStart = GetDirectoryPathPosition(); + m_directoryPath = strVarPath.substr(dirStart, paramStart - dirStart); + m_params = strVarPath.substr(paramStart); + } + } + + m_path = strVarPath; + } + else + { + m_bRoot = false; + m_bActive = false; + m_bRadio = false; + } +} + +CPVRRecordingsPath::CPVRRecordingsPath(bool bDeleted, bool bRadio) + : m_bValid(true), + m_bRoot(true), + m_bActive(!bDeleted), + m_bRadio(bRadio), + m_path(StringUtils::Format( + "pvr://recordings/{}/{}/", bRadio ? "radio" : "tv", bDeleted ? "deleted" : "active")) +{ +} + +CPVRRecordingsPath::CPVRRecordingsPath(bool bDeleted, + bool bRadio, + const std::string& strDirectory, + const std::string& strTitle, + int iSeason, + int iEpisode, + int iYear, + const std::string& strSubtitle, + const std::string& strChannelName, + const CDateTime& recordingTime, + const std::string& strId) + : m_bValid(true), m_bRoot(false), m_bActive(!bDeleted), m_bRadio(bRadio) +{ + std::string strDirectoryN(TrimSlashes(strDirectory)); + if (!strDirectoryN.empty()) + strDirectoryN = StringUtils::Format("{}/", strDirectoryN); + + std::string strTitleN(strTitle); + strTitleN = CURL::Encode(strTitleN); + + std::string strSeasonEpisodeN; + if ((iSeason > -1 && iEpisode > -1 && (iSeason > 0 || iEpisode > 0))) + strSeasonEpisodeN = StringUtils::Format("s{:02}e{:02}", iSeason, iEpisode); + if (!strSeasonEpisodeN.empty()) + strSeasonEpisodeN = StringUtils::Format(" {}", strSeasonEpisodeN); + + std::string strYearN(iYear > 0 ? StringUtils::Format(" ({})", iYear) : ""); + + std::string strSubtitleN; + if (!strSubtitle.empty()) + { + strSubtitleN = StringUtils::Format(" {}", strSubtitle); + strSubtitleN = CURL::Encode(strSubtitleN); + } + + std::string strChannelNameN; + if (!strChannelName.empty()) + { + strChannelNameN = StringUtils::Format(" ({})", strChannelName); + strChannelNameN = CURL::Encode(strChannelNameN); + } + + m_directoryPath = StringUtils::Format("{}{}{}{}{}", strDirectoryN, strTitleN, strSeasonEpisodeN, + strYearN, strSubtitleN); + m_params = StringUtils::Format(", TV{}, {}, {}.pvr", strChannelNameN, + recordingTime.GetAsSaveString(), strId); + m_path = StringUtils::Format("pvr://recordings/{}/{}/{}{}", bRadio ? "radio" : "tv", + bDeleted ? "deleted" : "active", m_directoryPath, m_params); +} + +std::string CPVRRecordingsPath::GetUnescapedDirectoryPath() const +{ + return CURL::Decode(m_directoryPath); +} + +namespace +{ +bool PathHasParent(const std::string& path, const std::string& parent) +{ + if (path == parent) + return true; + + if (!parent.empty() && parent.back() != '/') + return StringUtils::StartsWith(path, parent + '/'); + + return StringUtils::StartsWith(path, parent); +} +} // unnamed namespace + +std::string CPVRRecordingsPath::GetUnescapedSubDirectoryPath(const std::string& strPath) const +{ + // note: strPath must be unescaped. + + std::string strReturn; + std::string strUsePath(TrimSlashes(strPath)); + + const std::string strUnescapedDirectoryPath(GetUnescapedDirectoryPath()); + + /* adding "/" to make sure that base matches the complete folder name and not only parts of it */ + if (!strUnescapedDirectoryPath.empty() && + (strUsePath.size() <= strUnescapedDirectoryPath.size() || + !PathHasParent(strUsePath, strUnescapedDirectoryPath))) + return strReturn; + + strUsePath.erase(0, strUnescapedDirectoryPath.size()); + strUsePath = TrimSlashes(strUsePath); + + /* check for more occurrences */ + size_t iDelimiter = strUsePath.find('/'); + if (iDelimiter == std::string::npos) + strReturn = strUsePath; + else + strReturn = strUsePath.substr(0, iDelimiter); + + return strReturn; +} + +const std::string CPVRRecordingsPath::GetTitle() const +{ + if (m_bValid) + { + CRegExp reg(true); + if (reg.RegComp("pvr://recordings/(.*/)*(.*), TV( \\(.*\\))?, " + "(19[0-9][0-9]|20[0-9][0-9])[0-9][0-9][0-9][0-9]_[0-9][0-9][0-9][0-9][0-9][0-9]" + ", (.*).pvr")) + { + if (reg.RegFind(m_path.c_str()) >= 0) + return reg.GetMatch(2); + } + } + return ""; +} + +void CPVRRecordingsPath::AppendSegment(const std::string& strSegment) +{ + if (!m_bValid || strSegment.empty() || strSegment == "/") + return; + + std::string strVarSegment(TrimSlashes(strSegment)); + strVarSegment = CURL::Encode(strVarSegment); + + if (!m_directoryPath.empty() && m_directoryPath.back() != '/') + m_directoryPath.push_back('/'); + + m_directoryPath += strVarSegment; + m_directoryPath.push_back('/'); + + size_t paramStart = m_path.find(", TV"); + if (paramStart == std::string::npos) + { + if (!m_path.empty() && m_path.back() != '/') + m_path.push_back('/'); + + // append the segment + m_path += strVarSegment; + m_path.push_back('/'); + } + else + { + if (m_path.back() != '/') + m_path.insert(paramStart, "/"); + + // insert the segment between end of current directory path and parameters + m_path.insert(paramStart, strVarSegment); + } + + m_bRoot = false; +} + +std::string CPVRRecordingsPath::TrimSlashes(const std::string& strString) +{ + std::string strTrimmed(strString); + while (!strTrimmed.empty() && strTrimmed.front() == '/') + strTrimmed.erase(0, 1); + + while (!strTrimmed.empty() && strTrimmed.back() == '/') + strTrimmed.pop_back(); + + return strTrimmed; +} + +size_t CPVRRecordingsPath::GetDirectoryPathPosition() const +{ + if (m_bActive) + { + if (m_bRadio) + return PATH_ACTIVE_RADIO_RECORDINGS.size(); + else + return PATH_ACTIVE_TV_RECORDINGS.size(); + } + else + { + if (m_bRadio) + return PATH_DELETED_RADIO_RECORDINGS.size(); + else + return PATH_DELETED_TV_RECORDINGS.size(); + } + // unreachable +} diff --git a/xbmc/pvr/recordings/PVRRecordingsPath.h b/xbmc/pvr/recordings/PVRRecordingsPath.h new file mode 100644 index 0000000..e95dc92 --- /dev/null +++ b/xbmc/pvr/recordings/PVRRecordingsPath.h @@ -0,0 +1,68 @@ +/* + * 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 <string> + +class CDateTime; + +namespace PVR +{ +class CPVRRecordingsPath +{ +public: + static const std::string PATH_RECORDINGS; + static const std::string PATH_ACTIVE_TV_RECORDINGS; + static const std::string PATH_ACTIVE_RADIO_RECORDINGS; + static const std::string PATH_DELETED_TV_RECORDINGS; + static const std::string PATH_DELETED_RADIO_RECORDINGS; + + explicit CPVRRecordingsPath(const std::string& strPath); + CPVRRecordingsPath(bool bDeleted, bool bRadio); + CPVRRecordingsPath(bool bDeleted, + bool bRadio, + const std::string& strDirectory, + const std::string& strTitle, + int iSeason, + int iEpisode, + int iYear, + const std::string& strSubtitle, + const std::string& strChannelName, + const CDateTime& recordingTime, + const std::string& strId); + + operator std::string() const { return m_path; } + + bool IsValid() const { return m_bValid; } + + const std::string& GetPath() const { return m_path; } + bool IsRecordingsRoot() const { return m_bRoot; } + bool IsActive() const { return m_bActive; } + bool IsDeleted() const { return !IsActive(); } + bool IsRadio() const { return m_bRadio; } + bool IsTV() const { return !IsRadio(); } + std::string GetUnescapedDirectoryPath() const; + std::string GetUnescapedSubDirectoryPath(const std::string& strPath) const; + + const std::string GetTitle() const; + void AppendSegment(const std::string& strSegment); + +private: + static std::string TrimSlashes(const std::string& strString); + size_t GetDirectoryPathPosition() const; + + bool m_bValid; + bool m_bRoot; + bool m_bActive; + bool m_bRadio; + std::string m_directoryPath; + std::string m_params; + std::string m_path; +}; +} // namespace PVR diff --git a/xbmc/pvr/settings/CMakeLists.txt b/xbmc/pvr/settings/CMakeLists.txt new file mode 100644 index 0000000..6b32503 --- /dev/null +++ b/xbmc/pvr/settings/CMakeLists.txt @@ -0,0 +1,5 @@ +set(SOURCES PVRSettings.cpp) + +set(HEADERS PVRSettings.h) + +core_add_library(pvr_settings) diff --git a/xbmc/pvr/settings/PVRSettings.cpp b/xbmc/pvr/settings/PVRSettings.cpp new file mode 100644 index 0000000..f28651a --- /dev/null +++ b/xbmc/pvr/settings/PVRSettings.cpp @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2015-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 "PVRSettings.h" + +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/guilib/PVRGUIActionsParentalControl.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/SettingsManager.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <memory> +#include <mutex> +#include <set> +#include <string> +#include <utility> + +using namespace PVR; + +unsigned int CPVRSettings::m_iInstances = 0; + +CPVRSettings::CPVRSettings(const std::set<std::string>& settingNames) +{ + Init(settingNames); + + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + settings->GetSettingsManager()->RegisterSettingsHandler(this); + settings->RegisterCallback(this, settingNames); + + if (m_iInstances == 0) + { + // statics must only be registered once, not per instance + settings->GetSettingsManager()->RegisterSettingOptionsFiller("pvrrecordmargins", MarginTimeFiller); + settings->GetSettingsManager()->AddDynamicCondition("pvrsettingvisible", IsSettingVisible); + settings->GetSettingsManager()->AddDynamicCondition("checkpvrparentalpin", CheckParentalPin); + } + m_iInstances++; +} + +CPVRSettings::~CPVRSettings() +{ + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + + m_iInstances--; + if (m_iInstances == 0) + { + settings->GetSettingsManager()->RemoveDynamicCondition("checkpvrparentalpin"); + settings->GetSettingsManager()->RemoveDynamicCondition("pvrsettingvisible"); + settings->GetSettingsManager()->UnregisterSettingOptionsFiller("pvrrecordmargins"); + } + + settings->UnregisterCallback(this); + settings->GetSettingsManager()->UnregisterSettingsHandler(this); +} + +void CPVRSettings::RegisterCallback(ISettingCallback* callback) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_callbacks.insert(callback); +} + +void CPVRSettings::UnregisterCallback(ISettingCallback* callback) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_callbacks.erase(callback); +} + +void CPVRSettings::Init(const std::set<std::string>& settingNames) +{ + for (const auto& settingName : settingNames) + { + SettingPtr setting = CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(settingName); + if (!setting) + { + CLog::LogF(LOGERROR, "Unknown PVR setting '{}'", settingName); + continue; + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_settings.insert(std::make_pair(settingName, setting->Clone(settingName))); + } +} + +void CPVRSettings::OnSettingsLoaded() +{ + std::set<std::string> settingNames; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& settingName : m_settings) + settingNames.insert(settingName.first); + + m_settings.clear(); + } + + Init(settingNames); +} + +void CPVRSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == nullptr) + return; + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_settings[setting->GetId()] = setting->Clone(setting->GetId()); + const auto callbacks(m_callbacks); + lock.unlock(); + + for (const auto& callback : callbacks) + callback->OnSettingChanged(setting); +} + +bool CPVRSettings::GetBoolValue(const std::string& settingName) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + auto settingIt = m_settings.find(settingName); + if (settingIt != m_settings.end() && (*settingIt).second->GetType() == SettingType::Boolean) + { + std::shared_ptr<const CSettingBool> setting = std::dynamic_pointer_cast<const CSettingBool>((*settingIt).second); + if (setting) + return setting->GetValue(); + } + + CLog::LogF(LOGERROR, "PVR setting '{}' not found or wrong type given", settingName); + return false; +} + +int CPVRSettings::GetIntValue(const std::string& settingName) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + auto settingIt = m_settings.find(settingName); + if (settingIt != m_settings.end() && (*settingIt).second->GetType() == SettingType::Integer) + { + std::shared_ptr<const CSettingInt> setting = std::dynamic_pointer_cast<const CSettingInt>((*settingIt).second); + if (setting) + return setting->GetValue(); + } + + CLog::LogF(LOGERROR, "PVR setting '{}' not found or wrong type given", settingName); + return -1; +} + +std::string CPVRSettings::GetStringValue(const std::string& settingName) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + auto settingIt = m_settings.find(settingName); + if (settingIt != m_settings.end() && (*settingIt).second->GetType() == SettingType::String) + { + std::shared_ptr<const CSettingString> setting = std::dynamic_pointer_cast<const CSettingString>((*settingIt).second); + if (setting) + return setting->GetValue(); + } + + CLog::LogF(LOGERROR, "PVR setting '{}' not found or wrong type given", settingName); + return ""; +} + +void CPVRSettings::MarginTimeFiller(const SettingConstPtr& /*setting*/, + std::vector<IntegerSettingOption>& list, + int& current, + void* /*data*/) +{ + list.clear(); + + static const int marginTimeValues[] = + { + 0, 1, 3, 5, 10, 15, 20, 30, 60, 90, 120, 180 // minutes + }; + + for (int iValue : marginTimeValues) + { + list.emplace_back(StringUtils::Format(g_localizeStrings.Get(14044), iValue) /* {} min */, + iValue); + } +} + +bool CPVRSettings::IsSettingVisible(const std::string& condition, + const std::string& value, + const std::shared_ptr<const CSetting>& setting, + void* data) +{ + if (setting == nullptr) + return false; + + const std::string& settingId = setting->GetId(); + + if (settingId == CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS) + { + // Setting is only visible if exactly one PVR client is enabled or + // the expert setting to always use backend numbers is enabled + const auto& settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + int enabledClientAmount = CServiceBroker::GetPVRManager().Clients()->EnabledClientAmount(); + + return enabledClientAmount == 1 || + (settings->GetBool(CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS) && + enabledClientAmount > 1); + } + else if (settingId == CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS) + { + // Setting is only visible if more than one PVR client is enabled. + return CServiceBroker::GetPVRManager().Clients()->EnabledClientAmount() > 1; + } + else if (settingId == CSettings::SETTING_PVRMANAGER_CLIENTPRIORITIES) + { + // Setting is only visible if more than one PVR client is enabeld. + return CServiceBroker::GetPVRManager().Clients()->EnabledClientAmount() > 1; + } + else + { + // Show all other settings unconditionally. + return true; + } +} + +bool CPVRSettings::CheckParentalPin(const std::string& condition, + const std::string& value, + const std::shared_ptr<const CSetting>& setting, + void* data) +{ + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalPIN() == + ParentalCheckResult::SUCCESS; +} diff --git a/xbmc/pvr/settings/PVRSettings.h b/xbmc/pvr/settings/PVRSettings.h new file mode 100644 index 0000000..7cab9d6 --- /dev/null +++ b/xbmc/pvr/settings/PVRSettings.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2015-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 "settings/lib/ISettingCallback.h" +#include "settings/lib/ISettingsHandler.h" +#include "threads/CriticalSection.h" + +#include <map> +#include <memory> +#include <set> +#include <string> +#include <vector> + +class CSetting; + +struct IntegerSettingOption; + +namespace PVR +{ + class CPVRSettings : private ISettingsHandler, private ISettingCallback + { + public: + explicit CPVRSettings(const std::set<std::string>& settingNames); + ~CPVRSettings() override; + + void RegisterCallback(ISettingCallback* callback); + void UnregisterCallback(ISettingCallback* callback); + + // ISettingsHandler implementation + void OnSettingsLoaded() override; + + // ISettingCallback implementation + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + + bool GetBoolValue(const std::string& settingName) const; + int GetIntValue(const std::string& settingName) const; + std::string GetStringValue(const std::string& settingName) const; + + // settings value filler for start/end recording margin time for PVR timers. + static void MarginTimeFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + + // Dynamically hide or show settings. + static bool IsSettingVisible(const std::string& condition, + const std::string& value, + const std::shared_ptr<const CSetting>& setting, + void* data); + + // Do parental PIN check. + static bool CheckParentalPin(const std::string& condition, + const std::string& value, + const std::shared_ptr<const CSetting>& setting, + void* data); + + private: + CPVRSettings(const CPVRSettings&) = delete; + CPVRSettings& operator=(CPVRSettings const&) = delete; + + void Init(const std::set<std::string>& settingNames); + + mutable CCriticalSection m_critSection; + std::map<std::string, std::shared_ptr<CSetting>> m_settings; + std::set<ISettingCallback*> m_callbacks; + + static unsigned int m_iInstances; + }; +} diff --git a/xbmc/pvr/timers/CMakeLists.txt b/xbmc/pvr/timers/CMakeLists.txt new file mode 100644 index 0000000..e1031b4 --- /dev/null +++ b/xbmc/pvr/timers/CMakeLists.txt @@ -0,0 +1,13 @@ +set(SOURCES PVRTimerInfoTag.cpp + PVRTimerRuleMatcher.cpp + PVRTimers.cpp + PVRTimersPath.cpp + PVRTimerType.cpp) + +set(HEADERS PVRTimerInfoTag.h + PVRTimerRuleMatcher.h + PVRTimers.h + PVRTimersPath.h + PVRTimerType.h) + +core_add_library(pvr_timers) diff --git a/xbmc/pvr/timers/PVRTimerInfoTag.cpp b/xbmc/pvr/timers/PVRTimerInfoTag.cpp new file mode 100644 index 0000000..24df4cc --- /dev/null +++ b/xbmc/pvr/timers/PVRTimerInfoTag.cpp @@ -0,0 +1,1389 @@ +/* + * 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 "PVRTimerInfoTag.h" + +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/Epg.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/timers/PVRTimersPath.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <ctime> +#include <memory> +#include <mutex> +#include <stdexcept> +#include <string> + +using namespace PVR; + +CPVRTimerInfoTag::CPVRTimerInfoTag(bool bRadio /* = false */) + : m_strTitle(g_localizeStrings.Get(19056)), // New Timer + m_iClientId(CServiceBroker::GetPVRManager().Clients()->GetFirstCreatedClientID()), + m_iClientIndex(PVR_TIMER_NO_CLIENT_INDEX), + m_iParentClientIndex(PVR_TIMER_NO_PARENT), + m_iClientChannelUid(PVR_CHANNEL_INVALID_UID), + m_iPriority(DEFAULT_RECORDING_PRIORITY), + m_iLifetime(DEFAULT_RECORDING_LIFETIME), + m_iPreventDupEpisodes(DEFAULT_RECORDING_DUPLICATEHANDLING), + m_bIsRadio(bRadio), + m_iMarginStart(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_PVRRECORD_MARGINSTART)), + m_iMarginEnd(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_PVRRECORD_MARGINEND)), + m_iEpgUid(EPG_TAG_INVALID_UID), + m_StartTime(CDateTime::GetUTCDateTime()), + m_StopTime(m_StartTime + CDateTimeSpan(0, 2, 0, 0)), + m_FirstDay(m_StartTime) +{ + static const uint64_t iMustHaveAttr = PVR_TIMER_TYPE_IS_MANUAL; + static const uint64_t iMustNotHaveAttr = + PVR_TIMER_TYPE_IS_REPEATING | PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES; + + std::shared_ptr<CPVRTimerType> type; + + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + if (client && client->GetClientCapabilities().SupportsTimers()) + { + // default to first available type for given client + type = CPVRTimerType::GetFirstAvailableType(client); + } + + // fallback to manual one-shot reminder, which is always available + if (!type) + type = CPVRTimerType::CreateFromAttributes(iMustHaveAttr | PVR_TIMER_TYPE_IS_REMINDER, + iMustNotHaveAttr, m_iClientId); + + if (type) + SetTimerType(type); + else + { + CLog::LogF(LOGERROR, "Unable to obtain timer type!"); + throw std::logic_error("CPVRTimerInfoTag::CPVRTimerInfoTag - Unable to obtain timer type!"); + } + + m_iWeekdays = m_timerType->IsTimerRule() ? PVR_WEEKDAY_ALLDAYS : PVR_WEEKDAY_NONE; + + UpdateSummary(); +} + +CPVRTimerInfoTag::CPVRTimerInfoTag(const PVR_TIMER& timer, + const std::shared_ptr<CPVRChannel>& channel, + unsigned int iClientId) + : m_strTitle(timer.strTitle), + m_strEpgSearchString(timer.strEpgSearchString), + m_bFullTextEpgSearch(timer.bFullTextEpgSearch), + m_strDirectory(timer.strDirectory), + m_state(timer.state), + m_iClientId(iClientId), + m_iClientIndex(timer.iClientIndex), + m_iParentClientIndex(timer.iParentClientIndex), + m_iClientChannelUid(channel ? channel->UniqueID() + : (timer.iClientChannelUid > 0) ? timer.iClientChannelUid + : PVR_CHANNEL_INVALID_UID), + m_bStartAnyTime(timer.bStartAnyTime), + m_bEndAnyTime(timer.bEndAnyTime), + m_iPriority(timer.iPriority), + m_iLifetime(timer.iLifetime), + m_iMaxRecordings(timer.iMaxRecordings), + m_iWeekdays(timer.iWeekdays), + m_iPreventDupEpisodes(timer.iPreventDuplicateEpisodes), + m_iRecordingGroup(timer.iRecordingGroup), + m_strFileNameAndPath( + StringUtils::Format("pvr://client{}/timers/{}", m_iClientId, m_iClientIndex)), + m_bIsRadio(channel && channel->IsRadio()), + m_iMarginStart(timer.iMarginStart), + m_iMarginEnd(timer.iMarginEnd), + m_iEpgUid(timer.iEpgUid), + m_strSeriesLink(timer.strSeriesLink), + m_StartTime( + timer.startTime + + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection), + m_StopTime(timer.endTime + + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection), + m_channel(channel) +{ + if (timer.firstDay) + m_FirstDay = CDateTime( + timer.firstDay + + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection); + else + m_FirstDay = CDateTime::GetUTCDateTime(); + + if (m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX) + CLog::LogF(LOGERROR, "Invalid client index supplied by client {} (must be > 0)!", m_iClientId); + + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + if (client && client->GetClientCapabilities().SupportsTimers()) + { + // begin compat section + + // Create timer type according to certain timer values already available in Isengard. + // This is for migration only and does not make changes to the addons obsolete. Addons should + // work and benefit from some UI changes (e.g. some of the timer settings dialog enhancements), + // but all old problems/bugs due to static attributes and values will remain the same as in + // Isengard. Also, new features (like epg search) are not available to addons automatically. + // This code can be removed once all addons actually support the respective PVR Addon API version. + if (timer.iTimerType == PVR_TIMER_TYPE_NONE) + { + // Create type according to certain timer values. + uint64_t iMustHave = PVR_TIMER_TYPE_ATTRIBUTE_NONE; + uint64_t iMustNotHave = PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES; + + if (timer.iEpgUid == PVR_TIMER_NO_EPG_UID && timer.iWeekdays != PVR_WEEKDAY_NONE) + iMustHave |= PVR_TIMER_TYPE_IS_REPEATING; + else + iMustNotHave |= PVR_TIMER_TYPE_IS_REPEATING; + + if (timer.iEpgUid == PVR_TIMER_NO_EPG_UID) + iMustHave |= PVR_TIMER_TYPE_IS_MANUAL; + else + iMustNotHave |= PVR_TIMER_TYPE_IS_MANUAL; + + const std::shared_ptr<CPVRTimerType> type = + CPVRTimerType::CreateFromAttributes(iMustHave, iMustNotHave, m_iClientId); + + if (type) + SetTimerType(type); + } + // end compat section + else + { + SetTimerType(CPVRTimerType::CreateFromIds(timer.iTimerType, m_iClientId)); + } + + if (!m_timerType) + { + CLog::LogF(LOGERROR, "No timer type, although timers are supported by client {}!", + m_iClientId); + throw std::logic_error("CPVRTimerInfoTag::CPVRTimerInfoTag - Unable to obtain timer type!"); + } + else if (m_iEpgUid == EPG_TAG_INVALID_UID && m_timerType->IsEpgBasedOnetime()) + { + CLog::LogF(LOGERROR, "No epg tag given for epg based timer type ({})!", + m_timerType->GetTypeId()); + } + } + + UpdateSummary(); + UpdateEpgInfoTag(); +} + +bool CPVRTimerInfoTag::operator==(const CPVRTimerInfoTag& right) const +{ + bool bChannelsMatch = true; + if (m_channel && right.m_channel) + bChannelsMatch = *m_channel == *right.m_channel; + else if (m_channel != right.m_channel) + bChannelsMatch = false; + + return (bChannelsMatch && m_iClientIndex == right.m_iClientIndex && + m_iParentClientIndex == right.m_iParentClientIndex && + m_strSummary == right.m_strSummary && m_iClientChannelUid == right.m_iClientChannelUid && + m_bIsRadio == right.m_bIsRadio && m_iPreventDupEpisodes == right.m_iPreventDupEpisodes && + m_iRecordingGroup == right.m_iRecordingGroup && m_StartTime == right.m_StartTime && + m_StopTime == right.m_StopTime && m_bStartAnyTime == right.m_bStartAnyTime && + m_bEndAnyTime == right.m_bEndAnyTime && m_FirstDay == right.m_FirstDay && + m_iWeekdays == right.m_iWeekdays && m_iPriority == right.m_iPriority && + m_iLifetime == right.m_iLifetime && m_iMaxRecordings == right.m_iMaxRecordings && + m_strFileNameAndPath == right.m_strFileNameAndPath && m_strTitle == right.m_strTitle && + m_strEpgSearchString == right.m_strEpgSearchString && + m_bFullTextEpgSearch == right.m_bFullTextEpgSearch && + m_strDirectory == right.m_strDirectory && m_iClientId == right.m_iClientId && + m_iMarginStart == right.m_iMarginStart && m_iMarginEnd == right.m_iMarginEnd && + m_state == right.m_state && m_timerType == right.m_timerType && + m_iTimerId == right.m_iTimerId && m_strSeriesLink == right.m_strSeriesLink && + m_iEpgUid == right.m_iEpgUid && m_iTVChildTimersActive == right.m_iTVChildTimersActive && + m_iTVChildTimersConflictNOK == right.m_iTVChildTimersConflictNOK && + m_iTVChildTimersRecording == right.m_iTVChildTimersRecording && + m_iTVChildTimersErrors == right.m_iTVChildTimersErrors && + m_iRadioChildTimersActive == right.m_iRadioChildTimersActive && + m_iRadioChildTimersConflictNOK == right.m_iRadioChildTimersConflictNOK && + m_iRadioChildTimersRecording == right.m_iRadioChildTimersRecording && + m_iRadioChildTimersErrors == right.m_iRadioChildTimersErrors); +} + +bool CPVRTimerInfoTag::operator!=(const CPVRTimerInfoTag& right) const +{ + return !(*this == right); +} + +void CPVRTimerInfoTag::FillAddonData(PVR_TIMER& timer) const +{ + time_t start, end, firstDay; + StartAsUTC().GetAsTime(start); + EndAsUTC().GetAsTime(end); + FirstDayAsUTC().GetAsTime(firstDay); + const std::shared_ptr<CPVREpgInfoTag> epgTag = GetEpgInfoTag(); + const int iPVRTimeCorrection = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection; + + timer = {}; + timer.iClientIndex = m_iClientIndex; + timer.iParentClientIndex = m_iParentClientIndex; + timer.state = m_state; + timer.iTimerType = GetTimerType()->GetTypeId(); + timer.iClientChannelUid = m_iClientChannelUid; + strncpy(timer.strTitle, m_strTitle.c_str(), sizeof(timer.strTitle) - 1); + strncpy(timer.strEpgSearchString, m_strEpgSearchString.c_str(), + sizeof(timer.strEpgSearchString) - 1); + timer.bFullTextEpgSearch = m_bFullTextEpgSearch; + strncpy(timer.strDirectory, m_strDirectory.c_str(), sizeof(timer.strDirectory) - 1); + timer.iPriority = m_iPriority; + timer.iLifetime = m_iLifetime; + timer.iMaxRecordings = m_iMaxRecordings; + timer.iPreventDuplicateEpisodes = m_iPreventDupEpisodes; + timer.iRecordingGroup = m_iRecordingGroup; + timer.iWeekdays = m_iWeekdays; + timer.startTime = start - iPVRTimeCorrection; + timer.endTime = end - iPVRTimeCorrection; + timer.bStartAnyTime = m_bStartAnyTime; + timer.bEndAnyTime = m_bEndAnyTime; + timer.firstDay = firstDay - iPVRTimeCorrection; + timer.iEpgUid = epgTag ? epgTag->UniqueBroadcastID() : PVR_TIMER_NO_EPG_UID; + strncpy(timer.strSummary, m_strSummary.c_str(), sizeof(timer.strSummary) - 1); + timer.iMarginStart = m_iMarginStart; + timer.iMarginEnd = m_iMarginEnd; + timer.iGenreType = epgTag ? epgTag->GenreType() : 0; + timer.iGenreSubType = epgTag ? epgTag->GenreSubType() : 0; + strncpy(timer.strSeriesLink, SeriesLink().c_str(), sizeof(timer.strSeriesLink) - 1); +} + +void CPVRTimerInfoTag::Serialize(CVariant& value) const +{ + value["channelid"] = m_channel != NULL ? m_channel->ChannelID() : -1; + value["summary"] = m_strSummary; + value["isradio"] = m_bIsRadio; + value["preventduplicateepisodes"] = m_iPreventDupEpisodes; + value["starttime"] = m_StartTime.IsValid() ? m_StartTime.GetAsDBDateTime() : ""; + value["endtime"] = m_StopTime.IsValid() ? m_StopTime.GetAsDBDateTime() : ""; + value["startanytime"] = m_bStartAnyTime; + value["endanytime"] = m_bEndAnyTime; + value["runtime"] = m_StartTime.IsValid() && m_StopTime.IsValid() + ? (m_StopTime - m_StartTime).GetSecondsTotal() + : 0; + value["firstday"] = m_FirstDay.IsValid() ? m_FirstDay.GetAsDBDate() : ""; + + CVariant weekdays(CVariant::VariantTypeArray); + if (m_iWeekdays & PVR_WEEKDAY_MONDAY) + weekdays.push_back("monday"); + if (m_iWeekdays & PVR_WEEKDAY_TUESDAY) + weekdays.push_back("tuesday"); + if (m_iWeekdays & PVR_WEEKDAY_WEDNESDAY) + weekdays.push_back("wednesday"); + if (m_iWeekdays & PVR_WEEKDAY_THURSDAY) + weekdays.push_back("thursday"); + if (m_iWeekdays & PVR_WEEKDAY_FRIDAY) + weekdays.push_back("friday"); + if (m_iWeekdays & PVR_WEEKDAY_SATURDAY) + weekdays.push_back("saturday"); + if (m_iWeekdays & PVR_WEEKDAY_SUNDAY) + weekdays.push_back("sunday"); + value["weekdays"] = weekdays; + + value["priority"] = m_iPriority; + value["lifetime"] = m_iLifetime; + value["title"] = m_strTitle; + value["directory"] = m_strDirectory; + value["startmargin"] = m_iMarginStart; + value["endmargin"] = m_iMarginEnd; + + value["timerid"] = m_iTimerId; + + switch (m_state) + { + case PVR_TIMER_STATE_NEW: + value["state"] = "new"; + break; + case PVR_TIMER_STATE_SCHEDULED: + value["state"] = "scheduled"; + break; + case PVR_TIMER_STATE_RECORDING: + value["state"] = "recording"; + break; + case PVR_TIMER_STATE_COMPLETED: + value["state"] = "completed"; + break; + case PVR_TIMER_STATE_ABORTED: + value["state"] = "aborted"; + break; + case PVR_TIMER_STATE_CANCELLED: + value["state"] = "cancelled"; + break; + case PVR_TIMER_STATE_CONFLICT_OK: + value["state"] = "conflict_ok"; + break; + case PVR_TIMER_STATE_CONFLICT_NOK: + value["state"] = "conflict_notok"; + break; + case PVR_TIMER_STATE_ERROR: + value["state"] = "error"; + break; + case PVR_TIMER_STATE_DISABLED: + value["state"] = "disabled"; + break; + default: + value["state"] = "unknown"; + break; + } + + value["istimerrule"] = m_timerType->IsTimerRule(); + value["ismanual"] = m_timerType->IsManual(); + value["isreadonly"] = m_timerType->IsReadOnly(); + value["isreminder"] = m_timerType->IsReminder(); + + value["epgsearchstring"] = m_strEpgSearchString; + value["fulltextepgsearch"] = m_bFullTextEpgSearch; + value["recordinggroup"] = m_iRecordingGroup; + value["maxrecordings"] = m_iMaxRecordings; + value["epguid"] = m_iEpgUid; + value["broadcastid"] = m_epgTag ? m_epgTag->DatabaseID() : -1; + value["serieslink"] = m_strSeriesLink; + + value["clientid"] = m_iClientId; +} + +void CPVRTimerInfoTag::UpdateSummary() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strSummary.clear(); + + const std::string startDate(StartAsLocalTime().GetAsLocalizedDate()); + const std::string endDate(EndAsLocalTime().GetAsLocalizedDate()); + + if (m_bEndAnyTime) + { + m_strSummary = StringUtils::Format( + "{} {} {}", + m_iWeekdays != PVR_WEEKDAY_NONE ? GetWeekdaysString() + : startDate, //for "Any day" set PVR_WEEKDAY_ALLDAYS + g_localizeStrings.Get(19107), // "at" + m_bStartAnyTime ? g_localizeStrings.Get(19161) /* "any time" */ + : StartAsLocalTime().GetAsLocalizedTime("", false)); + } + else if ((m_iWeekdays != PVR_WEEKDAY_NONE) || (startDate == endDate)) + { + m_strSummary = StringUtils::Format( + "{} {} {} {} {}", + m_iWeekdays != PVR_WEEKDAY_NONE ? GetWeekdaysString() + : startDate, //for "Any day" set PVR_WEEKDAY_ALLDAYS + g_localizeStrings.Get(19159), // "from" + m_bStartAnyTime ? g_localizeStrings.Get(19161) /* "any time" */ + : StartAsLocalTime().GetAsLocalizedTime("", false), + g_localizeStrings.Get(19160), // "to" + m_bEndAnyTime ? g_localizeStrings.Get(19161) /* "any time" */ + : EndAsLocalTime().GetAsLocalizedTime("", false)); + } + else + { + m_strSummary = + StringUtils::Format("{} {} {} {} {} {}", startDate, + g_localizeStrings.Get(19159), // "from" + m_bStartAnyTime ? g_localizeStrings.Get(19161) /* "any time" */ + : StartAsLocalTime().GetAsLocalizedTime("", false), + g_localizeStrings.Get(19160), // "to" + endDate, + m_bEndAnyTime ? g_localizeStrings.Get(19161) /* "any time" */ + : EndAsLocalTime().GetAsLocalizedTime("", false)); + } +} + +void CPVRTimerInfoTag::SetTimerType(const std::shared_ptr<CPVRTimerType>& type) +{ + if (!type) + throw std::logic_error("CPVRTimerInfoTag::SetTimerType - Attempt to set 'null' timer type!"); + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_timerType = type; + + if (m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX) + { + m_iPriority = m_timerType->GetPriorityDefault(); + m_iLifetime = m_timerType->GetLifetimeDefault(); + m_iMaxRecordings = m_timerType->GetMaxRecordingsDefault(); + m_iPreventDupEpisodes = m_timerType->GetPreventDuplicateEpisodesDefault(); + m_iRecordingGroup = m_timerType->GetRecordingGroupDefault(); + } + + if (!m_timerType->IsTimerRule()) + m_iWeekdays = PVR_WEEKDAY_NONE; +} + +std::string CPVRTimerInfoTag::GetStatus(bool bRadio) const +{ + std::string strReturn = g_localizeStrings.Get(305); + std::unique_lock<CCriticalSection> lock(m_critSection); + if (URIUtils::PathEquals(m_strFileNameAndPath, CPVRTimersPath::PATH_ADDTIMER)) + strReturn = g_localizeStrings.Get(19026); + else if (m_state == PVR_TIMER_STATE_CANCELLED || m_state == PVR_TIMER_STATE_ABORTED) + strReturn = g_localizeStrings.Get(13106); + else if (m_state == PVR_TIMER_STATE_RECORDING) + strReturn = g_localizeStrings.Get(19162); + else if (m_state == PVR_TIMER_STATE_CONFLICT_OK) + strReturn = g_localizeStrings.Get(19275); + else if (m_state == PVR_TIMER_STATE_CONFLICT_NOK) + strReturn = g_localizeStrings.Get(19276); + else if (m_state == PVR_TIMER_STATE_ERROR) + strReturn = g_localizeStrings.Get(257); + else if (m_state == PVR_TIMER_STATE_DISABLED) + strReturn = g_localizeStrings.Get(13106); + else if (m_state == PVR_TIMER_STATE_COMPLETED) + { + if ((m_iTVChildTimersRecording > 0 && !bRadio) || (m_iRadioChildTimersRecording > 0 && bRadio)) + strReturn = g_localizeStrings.Get(19162); // "Recording active" + else + strReturn = g_localizeStrings.Get(19256); // "Completed" + } + else if (m_state == PVR_TIMER_STATE_SCHEDULED || m_state == PVR_TIMER_STATE_NEW) + { + if ((m_iTVChildTimersRecording > 0 && !bRadio) || (m_iRadioChildTimersRecording > 0 && bRadio)) + strReturn = g_localizeStrings.Get(19162); // "Recording active" + else if ((m_iTVChildTimersErrors > 0 && !bRadio) || (m_iRadioChildTimersErrors > 0 && bRadio)) + strReturn = g_localizeStrings.Get(257); // "Error" + else if ((m_iTVChildTimersConflictNOK > 0 && !bRadio) || + (m_iRadioChildTimersConflictNOK > 0 && bRadio)) + strReturn = g_localizeStrings.Get(19276); // "Conflict error" + else if ((m_iTVChildTimersActive > 0 && !bRadio) || (m_iRadioChildTimersActive > 0 && bRadio)) + strReturn = StringUtils::Format(g_localizeStrings.Get(19255), + bRadio ? m_iRadioChildTimersActive + : m_iTVChildTimersActive); // "{} scheduled" + } + + return strReturn; +} + +/** + * Get the type string of this timer + */ +std::string CPVRTimerInfoTag::GetTypeAsString() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_timerType->GetDescription(); +} + +namespace +{ +void AppendDay(std::string& strReturn, unsigned int iId) +{ + if (!strReturn.empty()) + strReturn += "-"; + + if (iId > 0) + strReturn += g_localizeStrings.Get(iId).c_str(); + else + strReturn += "__"; +} +} // unnamed namespace + +std::string CPVRTimerInfoTag::GetWeekdaysString(unsigned int iWeekdays, + bool bEpgBased, + bool bLongMultiDaysFormat) +{ + std::string strReturn; + + if (iWeekdays == PVR_WEEKDAY_NONE) + return strReturn; + else if (iWeekdays == PVR_WEEKDAY_ALLDAYS) + strReturn = bEpgBased ? g_localizeStrings.Get(807) // "Any day" + : g_localizeStrings.Get(808); // "Every day" + else if (iWeekdays == PVR_WEEKDAY_MONDAY) + strReturn = g_localizeStrings.Get(831); // "Mondays" + else if (iWeekdays == PVR_WEEKDAY_TUESDAY) + strReturn = g_localizeStrings.Get(832); // "Tuesdays" + else if (iWeekdays == PVR_WEEKDAY_WEDNESDAY) + strReturn = g_localizeStrings.Get(833); // "Wednesdays" + else if (iWeekdays == PVR_WEEKDAY_THURSDAY) + strReturn = g_localizeStrings.Get(834); // "Thursdays" + else if (iWeekdays == PVR_WEEKDAY_FRIDAY) + strReturn = g_localizeStrings.Get(835); // "Fridays" + else if (iWeekdays == PVR_WEEKDAY_SATURDAY) + strReturn = g_localizeStrings.Get(836); // "Saturdays" + else if (iWeekdays == PVR_WEEKDAY_SUNDAY) + strReturn = g_localizeStrings.Get(837); // "Sundays" + else + { + // Any other combination. Assemble custom string. + if (iWeekdays & PVR_WEEKDAY_MONDAY) + AppendDay(strReturn, 19149); // Mo + else if (bLongMultiDaysFormat) + AppendDay(strReturn, 0); + + if (iWeekdays & PVR_WEEKDAY_TUESDAY) + AppendDay(strReturn, 19150); // Tu + else if (bLongMultiDaysFormat) + AppendDay(strReturn, 0); + + if (iWeekdays & PVR_WEEKDAY_WEDNESDAY) + AppendDay(strReturn, 19151); // We + else if (bLongMultiDaysFormat) + AppendDay(strReturn, 0); + + if (iWeekdays & PVR_WEEKDAY_THURSDAY) + AppendDay(strReturn, 19152); // Th + else if (bLongMultiDaysFormat) + AppendDay(strReturn, 0); + + if (iWeekdays & PVR_WEEKDAY_FRIDAY) + AppendDay(strReturn, 19153); // Fr + else if (bLongMultiDaysFormat) + AppendDay(strReturn, 0); + + if (iWeekdays & PVR_WEEKDAY_SATURDAY) + AppendDay(strReturn, 19154); // Sa + else if (bLongMultiDaysFormat) + AppendDay(strReturn, 0); + + if (iWeekdays & PVR_WEEKDAY_SUNDAY) + AppendDay(strReturn, 19155); // So + else if (bLongMultiDaysFormat) + AppendDay(strReturn, 0); + } + return strReturn; +} + +std::string CPVRTimerInfoTag::GetWeekdaysString() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return GetWeekdaysString(m_iWeekdays, m_timerType->IsEpgBased(), false); +} + +bool CPVRTimerInfoTag::IsOwnedByClient() const +{ + return m_timerType->GetClientId() > -1; +} + +bool CPVRTimerInfoTag::AddToClient() const +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + if (client) + return client->AddTimer(*this) == PVR_ERROR_NO_ERROR; + return false; +} + +TimerOperationResult CPVRTimerInfoTag::DeleteFromClient(bool bForce /* = false */) const +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + PVR_ERROR error = PVR_ERROR_UNKNOWN; + + if (client) + error = client->DeleteTimer(*this, bForce); + + if (error == PVR_ERROR_RECORDING_RUNNING) + return TimerOperationResult::RECORDING; + + return (error == PVR_ERROR_NO_ERROR) ? TimerOperationResult::OK : TimerOperationResult::FAILED; +} + +bool CPVRTimerInfoTag::Persist() +{ + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (database) + return database->Persist(*this); + + return false; +} + +bool CPVRTimerInfoTag::DeleteFromDatabase() +{ + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (database) + return database->Delete(*this); + + return false; +} + +bool CPVRTimerInfoTag::UpdateEntry(const std::shared_ptr<CPVRTimerInfoTag>& tag) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_iClientId = tag->m_iClientId; + m_iClientIndex = tag->m_iClientIndex; + m_iParentClientIndex = tag->m_iParentClientIndex; + m_strTitle = tag->m_strTitle; + m_strEpgSearchString = tag->m_strEpgSearchString; + m_bFullTextEpgSearch = tag->m_bFullTextEpgSearch; + m_strDirectory = tag->m_strDirectory; + m_iClientChannelUid = tag->m_iClientChannelUid; + m_StartTime = tag->m_StartTime; + m_StopTime = tag->m_StopTime; + m_bStartAnyTime = tag->m_bStartAnyTime; + m_bEndAnyTime = tag->m_bEndAnyTime; + m_FirstDay = tag->m_FirstDay; + m_iPriority = tag->m_iPriority; + m_iLifetime = tag->m_iLifetime; + m_iMaxRecordings = tag->m_iMaxRecordings; + m_state = tag->m_state; + m_iPreventDupEpisodes = tag->m_iPreventDupEpisodes; + m_iRecordingGroup = tag->m_iRecordingGroup; + m_iWeekdays = tag->m_iWeekdays; + m_bIsRadio = tag->m_bIsRadio; + m_iMarginStart = tag->m_iMarginStart; + m_iMarginEnd = tag->m_iMarginEnd; + m_strSeriesLink = tag->m_strSeriesLink; + m_iEpgUid = tag->m_iEpgUid; + m_epgTag = tag->m_epgTag; + m_strSummary = tag->m_strSummary; + m_channel = tag->m_channel; + m_bProbedEpgTag = tag->m_bProbedEpgTag; + + m_iTVChildTimersActive = tag->m_iTVChildTimersActive; + m_iTVChildTimersConflictNOK = tag->m_iTVChildTimersConflictNOK; + m_iTVChildTimersRecording = tag->m_iTVChildTimersRecording; + m_iTVChildTimersErrors = tag->m_iTVChildTimersErrors; + m_iRadioChildTimersActive = tag->m_iRadioChildTimersActive; + m_iRadioChildTimersConflictNOK = tag->m_iRadioChildTimersConflictNOK; + m_iRadioChildTimersRecording = tag->m_iRadioChildTimersRecording; + m_iRadioChildTimersErrors = tag->m_iRadioChildTimersErrors; + + SetTimerType(tag->m_timerType); + + if (m_strSummary.empty()) + UpdateSummary(); + + UpdateEpgInfoTag(); + + return true; +} + +bool CPVRTimerInfoTag::UpdateChildState(const std::shared_ptr<CPVRTimerInfoTag>& childTimer, + bool bAdd) +{ + if (!childTimer || childTimer->m_iParentClientIndex != m_iClientIndex) + return false; + + int iDelta = bAdd ? +1 : -1; + switch (childTimer->m_state) + { + case PVR_TIMER_STATE_NEW: + case PVR_TIMER_STATE_SCHEDULED: + case PVR_TIMER_STATE_CONFLICT_OK: + if (childTimer->m_bIsRadio) + m_iRadioChildTimersActive += iDelta; + else + m_iTVChildTimersActive += iDelta; + break; + case PVR_TIMER_STATE_RECORDING: + if (childTimer->m_bIsRadio) + { + m_iRadioChildTimersActive += iDelta; + m_iRadioChildTimersRecording += iDelta; + } + else + { + m_iTVChildTimersActive += iDelta; + m_iTVChildTimersRecording += iDelta; + } + break; + case PVR_TIMER_STATE_CONFLICT_NOK: + if (childTimer->m_bIsRadio) + m_iRadioChildTimersConflictNOK += iDelta; + else + m_iTVChildTimersConflictNOK += iDelta; + break; + case PVR_TIMER_STATE_ERROR: + if (childTimer->m_bIsRadio) + m_iRadioChildTimersErrors += iDelta; + else + m_iTVChildTimersErrors += iDelta; + break; + case PVR_TIMER_STATE_COMPLETED: + case PVR_TIMER_STATE_ABORTED: + case PVR_TIMER_STATE_CANCELLED: + case PVR_TIMER_STATE_DISABLED: + //these are not the child timers we are looking for + break; + } + return true; +} + +void CPVRTimerInfoTag::ResetChildState() +{ + m_iTVChildTimersActive = 0; + m_iTVChildTimersRecording = 0; + m_iTVChildTimersConflictNOK = 0; + m_iTVChildTimersErrors = 0; + m_iRadioChildTimersActive = 0; + m_iRadioChildTimersRecording = 0; + m_iRadioChildTimersConflictNOK = 0; + m_iRadioChildTimersErrors = 0; +} + +bool CPVRTimerInfoTag::UpdateOnClient() +{ + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId); + return client && (client->UpdateTimer(*this) == PVR_ERROR_NO_ERROR); +} + +std::string CPVRTimerInfoTag::ChannelName() const +{ + std::string strReturn; + std::shared_ptr<CPVRChannel> channeltag = Channel(); + if (channeltag) + strReturn = channeltag->ChannelName(); + else if (m_timerType->IsEpgBasedTimerRule()) + strReturn = StringUtils::Format("({})", g_localizeStrings.Get(809)); // "Any channel" + + return strReturn; +} + +std::string CPVRTimerInfoTag::ChannelIcon() const +{ + std::string strReturn; + std::shared_ptr<CPVRChannel> channeltag = Channel(); + if (channeltag) + strReturn = channeltag->IconPath(); + return strReturn; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateReminderFromDate( + const CDateTime& start, + int iDuration, + const std::shared_ptr<CPVRTimerInfoTag>& parent /* = std::shared_ptr<CPVRTimerInfoTag>() */) +{ + bool bReadOnly = !!parent; // children of reminder rules are always read-only + std::shared_ptr<CPVRTimerInfoTag> newTimer = + CreateFromDate(parent->Channel(), start, iDuration, true, bReadOnly); + if (newTimer && parent) + { + // set parent + newTimer->m_iParentClientIndex = parent->m_iClientIndex; + + // set relevant props for the new one-time reminder timer + newTimer->m_strTitle = parent->Title(); + } + return newTimer; +} + +static const time_t INSTANT_TIMER_START = + 0; // PVR addon API: special start time value to denote an instant timer + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateInstantTimerTag( + const std::shared_ptr<CPVRChannel>& channel, + int iDuration /* = DEFAULT_PVRRECORD_INSTANTRECORDTIME */) +{ + return CreateFromDate(channel, CDateTime(INSTANT_TIMER_START), iDuration, false, false); +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateTimerTag( + const std::shared_ptr<CPVRChannel>& channel, const CDateTime& start, int iDuration) +{ + return CreateFromDate(channel, start, iDuration, false, false); +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateFromDate( + const std::shared_ptr<CPVRChannel>& channel, + const CDateTime& start, + int iDuration, + bool bCreateReminder, + bool bReadOnly) +{ + if (!channel) + { + CLog::LogF(LOGERROR, "No channel"); + return std::shared_ptr<CPVRTimerInfoTag>(); + } + + bool bInstantStart = (start == CDateTime(INSTANT_TIMER_START)); + + std::shared_ptr<CPVREpgInfoTag> epgTag; + if (!bCreateReminder) // time-based reminders never have epg tags + { + if (bInstantStart) + epgTag = channel->GetEPGNow(); + else if (channel->GetEPG()) + epgTag = channel->GetEPG()->GetTagBetween(start, start + CDateTimeSpan(0, 0, iDuration, 0)); + } + + std::shared_ptr<CPVRTimerInfoTag> newTimer; + if (epgTag) + { + if (epgTag->IsRecordable()) + { + newTimer = CreateFromEpg(epgTag, false, bCreateReminder, bReadOnly); + } + else + { + CLog::LogF(LOGERROR, "EPG tag is not recordable"); + return std::shared_ptr<CPVRTimerInfoTag>(); + } + } + + if (!newTimer) + { + newTimer.reset(new CPVRTimerInfoTag); + + newTimer->m_iClientIndex = PVR_TIMER_NO_CLIENT_INDEX; + newTimer->m_iParentClientIndex = PVR_TIMER_NO_PARENT; + newTimer->m_channel = channel; + newTimer->m_strTitle = channel->ChannelName(); + newTimer->m_iClientChannelUid = channel->UniqueID(); + newTimer->m_iClientId = channel->ClientID(); + newTimer->m_bIsRadio = channel->IsRadio(); + + uint64_t iMustHaveAttribs = PVR_TIMER_TYPE_IS_MANUAL; + if (bCreateReminder) + iMustHaveAttribs |= PVR_TIMER_TYPE_IS_REMINDER; + if (bReadOnly) + iMustHaveAttribs |= PVR_TIMER_TYPE_IS_READONLY; + + // timertype: manual one-shot timer for given client + const std::shared_ptr<CPVRTimerType> timerType = CPVRTimerType::CreateFromAttributes( + iMustHaveAttribs, PVR_TIMER_TYPE_IS_REPEATING | PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES, + channel->ClientID()); + if (!timerType) + { + CLog::LogF(LOGERROR, "Unable to create one shot manual timer type"); + return std::shared_ptr<CPVRTimerInfoTag>(); + } + + newTimer->SetTimerType(timerType); + + if (epgTag) + newTimer->SetEpgInfoTag(epgTag); + else + newTimer->UpdateEpgInfoTag(); + } + + /* no matter the timer was created from an epg tag, overwrite timer start and end times. */ + CDateTime now(CDateTime::GetUTCDateTime()); + if (bInstantStart) + newTimer->SetStartFromUTC(now); + else + newTimer->SetStartFromUTC(start); + + if (iDuration == DEFAULT_PVRRECORD_INSTANTRECORDTIME) + iDuration = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME); + + if (bInstantStart) + { + CDateTime endTime = now + CDateTimeSpan(0, 0, iDuration ? iDuration : 120, 0); + newTimer->SetEndFromUTC(endTime); + } + else + { + CDateTime endTime = start + CDateTimeSpan(0, 0, iDuration ? iDuration : 120, 0); + newTimer->SetEndFromUTC(endTime); + } + + /* update summary string according to instant recording start/end time */ + newTimer->UpdateSummary(); + + if (bInstantStart) + { + // "Instant recording: <summary> + newTimer->m_strSummary = StringUtils::Format(g_localizeStrings.Get(19093), newTimer->Summary()); + + // now that we have a nice summary, we can set the "special" start time value that indicates an instant recording + newTimer->SetStartFromUTC(start); + } + + newTimer->m_iMarginStart = 0; + newTimer->m_iMarginEnd = 0; + + /* unused only for reference */ + newTimer->m_strFileNameAndPath = CPVRTimersPath::PATH_NEW; + + return newTimer; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateReminderFromEpg( + const std::shared_ptr<CPVREpgInfoTag>& tag, + const std::shared_ptr<CPVRTimerInfoTag>& parent /* = std::shared_ptr<CPVRTimerInfoTag>() */) +{ + bool bReadOnly = !!parent; // children of reminder rules are always read-only + std::shared_ptr<CPVRTimerInfoTag> newTimer = CreateFromEpg(tag, false, true, bReadOnly); + if (newTimer && parent) + { + // set parent + newTimer->m_iParentClientIndex = parent->m_iClientIndex; + + // set relevant props for the new one-time reminder timer (everything not epg-related) + newTimer->m_iMarginStart = parent->m_iMarginStart; + newTimer->m_iMarginEnd = parent->m_iMarginEnd; + } + return newTimer; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateFromEpg( + const std::shared_ptr<CPVREpgInfoTag>& tag, bool bCreateRule /* = false */) +{ + return CreateFromEpg(tag, bCreateRule, false, false); +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateFromEpg( + const std::shared_ptr<CPVREpgInfoTag>& tag, + bool bCreateRule, + bool bCreateReminder, + bool bReadOnly /* = false */) +{ + std::shared_ptr<CPVRTimerInfoTag> newTag(new CPVRTimerInfoTag()); + + /* check if a valid channel is set */ + const std::shared_ptr<CPVRChannel> channel = + CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(tag); + if (!channel) + { + CLog::LogF(LOGERROR, "EPG tag has no channel"); + return std::shared_ptr<CPVRTimerInfoTag>(); + } + + newTag->m_iClientIndex = PVR_TIMER_NO_CLIENT_INDEX; + newTag->m_iParentClientIndex = PVR_TIMER_NO_PARENT; + if (!CServiceBroker::GetPVRManager().IsParentalLocked(tag)) + newTag->m_strTitle = tag->Title(); + if (newTag->m_strTitle.empty()) + newTag->m_strTitle = channel->ChannelName(); + newTag->m_iClientChannelUid = channel->UniqueID(); + newTag->m_iClientId = channel->ClientID(); + newTag->m_bIsRadio = channel->IsRadio(); + newTag->m_channel = channel; + newTag->m_strSeriesLink = tag->SeriesLink(); + newTag->m_iEpgUid = tag->UniqueBroadcastID(); + newTag->SetStartFromUTC(tag->StartAsUTC()); + newTag->SetEndFromUTC(tag->EndAsUTC()); + + const uint64_t iMustNotHaveAttribs = PVR_TIMER_TYPE_IS_MANUAL | + PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES | + PVR_TIMER_TYPE_FORBIDS_EPG_TAG_ON_CREATE; + std::shared_ptr<CPVRTimerType> timerType; + if (bCreateRule) + { + // create epg-based timer rule, prefer rule using series link if available. + + uint64_t iMustHaveAttribs = PVR_TIMER_TYPE_IS_REPEATING; + if (bCreateReminder) + iMustHaveAttribs |= PVR_TIMER_TYPE_IS_REMINDER; + if (bReadOnly) + iMustHaveAttribs |= PVR_TIMER_TYPE_IS_READONLY; + + if (!tag->SeriesLink().empty()) + timerType = CPVRTimerType::CreateFromAttributes( + iMustHaveAttribs | PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE, iMustNotHaveAttribs, + channel->ClientID()); + if (!timerType) + timerType = CPVRTimerType::CreateFromAttributes( + iMustHaveAttribs, iMustNotHaveAttribs | PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE, + channel->ClientID()); + if (timerType) + { + if (timerType->SupportsEpgTitleMatch()) + newTag->m_strEpgSearchString = newTag->m_strTitle; + + if (timerType->SupportsWeekdays()) + newTag->m_iWeekdays = PVR_WEEKDAY_ALLDAYS; + + if (timerType->SupportsStartAnyTime()) + newTag->m_bStartAnyTime = true; + + if (timerType->SupportsEndAnyTime()) + newTag->m_bEndAnyTime = true; + } + } + else + { + // create one-shot epg-based timer + + uint64_t iMustHaveAttribs = PVR_TIMER_TYPE_ATTRIBUTE_NONE; + if (bCreateReminder) + iMustHaveAttribs |= PVR_TIMER_TYPE_IS_REMINDER; + if (bReadOnly) + iMustHaveAttribs |= PVR_TIMER_TYPE_IS_READONLY; + + timerType = CPVRTimerType::CreateFromAttributes( + iMustHaveAttribs, PVR_TIMER_TYPE_IS_REPEATING | iMustNotHaveAttribs, channel->ClientID()); + } + + if (!timerType) + { + CLog::LogF(LOGERROR, "Unable to create any epg-based timer type"); + return std::shared_ptr<CPVRTimerInfoTag>(); + } + + newTag->SetTimerType(timerType); + newTag->UpdateSummary(); + newTag->SetEpgInfoTag(tag); + + /* unused only for reference */ + newTag->m_strFileNameAndPath = CPVRTimersPath::PATH_NEW; + + return newTag; +} + +//! @todo CDateTime class does not handle daylight saving timezone bias correctly (and cannot easily +// be changed to do so due to performance and platform specific issues). In most cases this only +// causes GUI presentation glitches, but reminder timer rules rely on correct local time values. + +namespace +{ +#define IsLeapYear(y) ((y % 4 == 0) && (y % 100 != 0 || y % 400 == 0)) + +int days_from_0(int year) +{ + year--; + return 365 * year + (year / 400) - (year / 100) + (year / 4); +} + +int days_from_1970(int32_t year) +{ + static const int days_from_0_to_1970 = days_from_0(1970); + return days_from_0(year) - days_from_0_to_1970; +} + +int days_from_1jan(int year, int month, int day) +{ + static const int days[2][12] = {{0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}, + {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}}; + return days[IsLeapYear(year)][month - 1] + day - 1; +} + +time_t mytimegm(struct tm* time) +{ + int year = time->tm_year + 1900; + int month = time->tm_mon; + + if (month > 11) + { + year += month / 12; + month %= 12; + } + else if (month < 0) + { + int years_diff = (-month + 11) / 12; + year -= years_diff; + month += 12 * years_diff; + } + + month++; + + int day_of_year = days_from_1jan(year, month, time->tm_mday); + int days_since_epoch = days_from_1970(year) + day_of_year; + + return 3600 * 24 * days_since_epoch + 3600 * time->tm_hour + 60 * time->tm_min + time->tm_sec; +} +} // namespace + +CDateTime CPVRTimerInfoTag::ConvertUTCToLocalTime(const CDateTime& utc) +{ + time_t time = 0; + utc.GetAsTime(time); + + struct tm* tms; +#ifdef HAVE_LOCALTIME_R + struct tm gbuf; + tms = localtime_r(&time, &gbuf); +#else + tms = localtime(&time); +#endif + + if (!tms) + { + CLog::LogF(LOGWARNING, "localtime() returned NULL!"); + return {}; + } + + return CDateTime(mytimegm(tms)); +} + +CDateTime CPVRTimerInfoTag::ConvertLocalTimeToUTC(const CDateTime& local) +{ + time_t time = 0; + local.GetAsTime(time); + + struct tm* tms; + + // obtain dst flag for given datetime +#ifdef HAVE_LOCALTIME_R + struct tm loc_buf; + tms = localtime_r(&time, &loc_buf); +#else + tms = localtime(&time); +#endif + + if (!tms) + { + CLog::LogF(LOGWARNING, "localtime() returned NULL!"); + return {}; + } + + int isdst = tms->tm_isdst; + +#ifdef HAVE_GMTIME_R + struct tm gm_buf; + tms = gmtime_r(&time, &gm_buf); +#else + tms = gmtime(&time); +#endif + + if (!tms) + { + CLog::LogF(LOGWARNING, "gmtime() returned NULL!"); + return {}; + } + + tms->tm_isdst = isdst; + return CDateTime(mktime(tms)); +} + +CDateTime CPVRTimerInfoTag::StartAsUTC() const +{ + return m_StartTime; +} + +CDateTime CPVRTimerInfoTag::StartAsLocalTime() const +{ + return ConvertUTCToLocalTime(m_StartTime); +} + +void CPVRTimerInfoTag::SetStartFromUTC(const CDateTime& start) +{ + m_StartTime = start; +} + +void CPVRTimerInfoTag::SetStartFromLocalTime(const CDateTime& start) +{ + m_StartTime = ConvertLocalTimeToUTC(start); +} + +CDateTime CPVRTimerInfoTag::EndAsUTC() const +{ + return m_StopTime; +} + +CDateTime CPVRTimerInfoTag::EndAsLocalTime() const +{ + return ConvertUTCToLocalTime(m_StopTime); +} + +void CPVRTimerInfoTag::SetEndFromUTC(const CDateTime& end) +{ + m_StopTime = end; +} + +void CPVRTimerInfoTag::SetEndFromLocalTime(const CDateTime& end) +{ + m_StopTime = ConvertLocalTimeToUTC(end); +} + +int CPVRTimerInfoTag::GetDuration() const +{ + time_t start, end; + m_StartTime.GetAsTime(start); + m_StopTime.GetAsTime(end); + return end - start > 0 ? end - start : 3600; +} + +CDateTime CPVRTimerInfoTag::FirstDayAsUTC() const +{ + return m_FirstDay; +} + +CDateTime CPVRTimerInfoTag::FirstDayAsLocalTime() const +{ + return ConvertUTCToLocalTime(m_FirstDay); +} + +void CPVRTimerInfoTag::SetFirstDayFromUTC(const CDateTime& firstDay) +{ + m_FirstDay = firstDay; +} + +void CPVRTimerInfoTag::SetFirstDayFromLocalTime(const CDateTime& firstDay) +{ + m_FirstDay = ConvertLocalTimeToUTC(firstDay); +} + +std::string CPVRTimerInfoTag::GetNotificationText() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + int stringID = 0; + + switch (m_state) + { + case PVR_TIMER_STATE_ABORTED: + case PVR_TIMER_STATE_CANCELLED: + stringID = 19224; // Recording aborted + break; + case PVR_TIMER_STATE_SCHEDULED: + if (IsTimerRule()) + stringID = 19058; // Timer enabled + else + stringID = 19225; // Recording scheduled + break; + case PVR_TIMER_STATE_RECORDING: + stringID = 19226; // Recording started + break; + case PVR_TIMER_STATE_COMPLETED: + stringID = 19227; // Recording completed + break; + case PVR_TIMER_STATE_CONFLICT_OK: + case PVR_TIMER_STATE_CONFLICT_NOK: + stringID = 19277; // Recording conflict + break; + case PVR_TIMER_STATE_ERROR: + stringID = 19278; // Recording error + break; + case PVR_TIMER_STATE_DISABLED: + stringID = 19057; // Timer disabled + break; + default: + break; + } + + if (stringID != 0) + return StringUtils::Format("{}: '{}'", g_localizeStrings.Get(stringID), m_strTitle); + + return {}; +} + +std::string CPVRTimerInfoTag::GetDeletedNotificationText() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + int stringID = 0; + // The state in this case is the state the timer had when it was last seen + switch (m_state) + { + case PVR_TIMER_STATE_RECORDING: + stringID = 19227; // Recording completed + break; + case PVR_TIMER_STATE_SCHEDULED: + default: + if (IsTimerRule()) + stringID = 828; // Timer rule deleted + else + stringID = 19228; // Timer deleted + } + + return StringUtils::Format("{}: '{}'", g_localizeStrings.Get(stringID), m_strTitle); +} + +void CPVRTimerInfoTag::SetEpgInfoTag(const std::shared_ptr<CPVREpgInfoTag>& tag) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_epgTag = tag; + m_bProbedEpgTag = true; +} + +void CPVRTimerInfoTag::UpdateEpgInfoTag() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_epgTag.reset(); + m_bProbedEpgTag = false; + GetEpgInfoTag(); +} + +std::shared_ptr<CPVREpgInfoTag> CPVRTimerInfoTag::GetEpgInfoTag(bool bCreate /* = true */) const +{ + if (!m_epgTag && !m_bProbedEpgTag && bCreate && CServiceBroker::GetPVRManager().EpgsCreated()) + { + std::shared_ptr<CPVRChannel> channel(m_channel); + if (!channel) + { + channel = CServiceBroker::GetPVRManager() + .ChannelGroups() + ->Get(m_bIsRadio) + ->GetGroupAll() + ->GetByUniqueID(m_iClientChannelUid, m_iClientId); + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_channel = channel; + } + + if (channel) + { + const std::shared_ptr<CPVREpg> epg(channel->GetEPG()); + if (epg) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_epgTag && m_iEpgUid != EPG_TAG_INVALID_UID) + { + m_epgTag = epg->GetTagByBroadcastId(m_iEpgUid); + } + + if (!m_epgTag && !IsTimerRule() && IsOwnedByClient()) + { + time_t startTime = 0; + time_t endTime = 0; + + StartAsUTC().GetAsTime(startTime); + if (startTime > 0) + EndAsUTC().GetAsTime(endTime); + + if (startTime > 0 && endTime > 0) + { + // try to fetch missing epg tag from backend + m_epgTag = epg->GetTagBetween(StartAsUTC() - CDateTimeSpan(0, 0, 2, 0), + EndAsUTC() + CDateTimeSpan(0, 0, 2, 0), true); + if (m_epgTag) + m_iEpgUid = m_epgTag->UniqueBroadcastID(); + } + } + } + } + m_bProbedEpgTag = true; + } + return m_epgTag; +} + +bool CPVRTimerInfoTag::HasChannel() const +{ + return m_channel.get() != nullptr; +} + +std::shared_ptr<CPVRChannel> CPVRTimerInfoTag::Channel() const +{ + return m_channel; +} + +void CPVRTimerInfoTag::UpdateChannel() +{ + const std::shared_ptr<CPVRChannel> channel(CServiceBroker::GetPVRManager() + .ChannelGroups() + ->Get(m_bIsRadio) + ->GetGroupAll() + ->GetByUniqueID(m_iClientChannelUid, m_iClientId)); + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_channel = channel; +} + +const std::string& CPVRTimerInfoTag::Title() const +{ + return m_strTitle; +} + +const std::string& CPVRTimerInfoTag::Summary() const +{ + return m_strSummary; +} + +const std::string& CPVRTimerInfoTag::Path() const +{ + return m_strFileNameAndPath; +} + +const std::string& CPVRTimerInfoTag::SeriesLink() const +{ + return m_strSeriesLink; +} diff --git a/xbmc/pvr/timers/PVRTimerInfoTag.h b/xbmc/pvr/timers/PVRTimerInfoTag.h new file mode 100644 index 0000000..ad2d3d8 --- /dev/null +++ b/xbmc/pvr/timers/PVRTimerInfoTag.h @@ -0,0 +1,644 @@ +/* + * 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/timers/PVRTimerType.h" +#include "threads/CriticalSection.h" +#include "utils/ISerializable.h" + +#include <memory> +#include <string> + +struct PVR_TIMER; + +namespace PVR +{ +class CPVRChannel; +class CPVREpgInfoTag; + +enum class TimerOperationResult +{ + OK = 0, + FAILED, + RECORDING // The timer was not deleted because it is currently recording (see DeleteTimer). +}; + +class CPVRTimerInfoTag final : public ISerializable +{ + // allow these classes direct access to members as they act as timer tag instance factories. + friend class CGUIDialogPVRTimerSettings; + friend class CPVRDatabase; + +public: + explicit CPVRTimerInfoTag(bool bRadio = false); + CPVRTimerInfoTag(const PVR_TIMER& timer, + const std::shared_ptr<CPVRChannel>& channel, + unsigned int iClientId); + + bool operator==(const CPVRTimerInfoTag& right) const; + bool operator!=(const CPVRTimerInfoTag& right) const; + + /*! + * @brief Copy over data to the given PVR_TIMER instance. + * @param timer The timer instance to fill. + */ + void FillAddonData(PVR_TIMER& timer) const; + + // ISerializable implementation + void Serialize(CVariant& value) const override; + + static constexpr int DEFAULT_PVRRECORD_INSTANTRECORDTIME = -1; + + /*! + * @brief create a tag for an instant timer for a given channel + * @param channel is the channel the instant timer is to be created for + * @param iDuration is the duration for the instant timer, DEFAULT_PVRRECORD_INSTANTRECORDTIME + * denotes system default (setting value) + * @return the timer or null if timer could not be created + */ + static std::shared_ptr<CPVRTimerInfoTag> CreateInstantTimerTag( + const std::shared_ptr<CPVRChannel>& channel, + int iDuration = DEFAULT_PVRRECORD_INSTANTRECORDTIME); + + /*! + * @brief create a tag for a timer for a given channel at given time for given duration + * @param channel is the channel the timer is to be created for + * @param start is the start time for the recording + * @param iDuration is the duration of the recording + * @return the timer or null if timer could not be created + */ + static std::shared_ptr<CPVRTimerInfoTag> CreateTimerTag( + const std::shared_ptr<CPVRChannel>& channel, const CDateTime& start, int iDuration); + + /*! + * @brief create a recording or reminder timer or timer rule for the given epg info tag. + * @param tag the epg info tag + * @param bCreateRule if true, create a timer rule, create a one shot timer otherwise + * @param bCreateReminder if true, create a reminder timer or rule, create a recording timer + * or rule otherwise + * @param bReadOnly whether the timer/rule is read only + * @return the timer or null if timer could not be created + */ + static std::shared_ptr<CPVRTimerInfoTag> CreateFromEpg(const std::shared_ptr<CPVREpgInfoTag>& tag, + bool bCreateRule, + bool bCreateReminder, + bool bReadOnly = false); + + /*! + * @brief create a timer or timer rule for the given epg info tag. + * @param tag the epg info tag + * @param bCreateRule if true, create a timer rule, create a one shot timer otherwise + * @return the timer or null if timer could not be created + */ + static std::shared_ptr<CPVRTimerInfoTag> CreateFromEpg(const std::shared_ptr<CPVREpgInfoTag>& tag, + bool bCreateRule = false); + + /*! + * @brief create a reminder timer for the given start date. + * @param start the start date + * @param iDuration the duration the reminder is valid + * @param parent If non-zero, the new timer will be made a child of the given timer rule + * @return the timer or null if timer could not be created + */ + static std::shared_ptr<CPVRTimerInfoTag> CreateReminderFromDate( + const CDateTime& start, + int iDuration, + const std::shared_ptr<CPVRTimerInfoTag>& parent = std::shared_ptr<CPVRTimerInfoTag>()); + + /*! + * @brief create a reminder timer for the given epg info tag. + * @param tag the epg info tag + * @param parent If non-zero, the new timer will be made a child of the given timer rule + * @return the timer or null if timer could not be created + */ + static std::shared_ptr<CPVRTimerInfoTag> CreateReminderFromEpg( + const std::shared_ptr<CPVREpgInfoTag>& tag, + const std::shared_ptr<CPVRTimerInfoTag>& parent = std::shared_ptr<CPVRTimerInfoTag>()); + + /*! + * @brief Associate the given epg tag with this timer. + * @param tag The epg tag to assign. + */ + void SetEpgInfoTag(const std::shared_ptr<CPVREpgInfoTag>& tag); + + /*! + * @brief get the epg info tag associated with this timer, if any + * @param bCreate if true, try to find the epg tag if not yet set (lazy evaluation) + * @return the epg info tag associated with this timer or null if there is no tag + */ + std::shared_ptr<CPVREpgInfoTag> GetEpgInfoTag(bool bCreate = true) const; + + /*! + * @brief updates this timer excluding the state of any children. + * @param tag A timer containing the data that shall be merged into this timer's data. + * @return true if the timer was updated successfully + */ + bool UpdateEntry(const std::shared_ptr<CPVRTimerInfoTag>& tag); + + /*! + * @brief merge in the state of this child timer. + * @param childTimer The child timer + * @param bAdd If true, add child's data to parent's state, otherwise subtract. + * @return true if the child timer's state was merged successfully + */ + bool UpdateChildState(const std::shared_ptr<CPVRTimerInfoTag>& childTimer, bool bAdd); + + /*! + * @brief reset the state of children related to this timer. + */ + void ResetChildState(); + + /*! + * @brief Whether this timer is active. + * @return True if this timer is active, false otherwise. + */ + bool IsActive() const + { + return m_state == PVR_TIMER_STATE_SCHEDULED || m_state == PVR_TIMER_STATE_RECORDING || + m_state == PVR_TIMER_STATE_CONFLICT_OK || m_state == PVR_TIMER_STATE_CONFLICT_NOK || + m_state == PVR_TIMER_STATE_ERROR; + } + + /*! + * @brief Whether this timer is broken. + * @return True if this timer won't result in a recording because it is broken for some reason, + * false otherwise + */ + bool IsBroken() const + { + return m_state == PVR_TIMER_STATE_CONFLICT_NOK || m_state == PVR_TIMER_STATE_ERROR; + } + + /*! + * @brief Whether this timer has a conflict. + * @return True if this timer won't result in a recording because it is in conflict with another + * timer or live stream, false otherwise. + */ + bool HasConflict() const { return m_state == PVR_TIMER_STATE_CONFLICT_NOK; } + + /*! + * @brief Whether this timer is currently recording. + * @return True if recording, false otherwise. + */ + bool IsRecording() const { return m_state == PVR_TIMER_STATE_RECORDING; } + + /*! + * @brief Whether this timer is disabled, for example by the user. + * @return True if disabled, false otherwise. + */ + bool IsDisabled() const { return m_state == PVR_TIMER_STATE_DISABLED; } + + /*! + * @brief Gets the type of this timer. + * @return the timer type or NULL if this tag has no timer type. + */ + const std::shared_ptr<CPVRTimerType> GetTimerType() const { return m_timerType; } + + /*! + * @brief Sets the type of this timer. + * @param the new timer type. + */ + void SetTimerType(const std::shared_ptr<CPVRTimerType>& type); + + /*! + * @brief Checks whether this is a timer rule (vs. one time timer). + * @return True if this is a timer rule, false otherwise. + */ + bool IsTimerRule() const { return m_timerType && m_timerType->IsTimerRule(); } + + /*! + * @brief Checks whether this is a reminder timer (vs. recording timer). + * @return True if this is a reminder timer, false otherwise. + */ + bool IsReminder() const { return m_timerType && m_timerType->IsReminder(); } + + /*! + * @brief Checks whether this is a manual (vs. epg-based) timer. + * @return True if this is a manual timer, false otherwise. + */ + bool IsManual() const { return m_timerType && m_timerType->IsManual(); } + + /*! + * @brief Checks whether this is an epg-based (vs. manual) timer. + * @return True if this is an epg-Based timer, false otherwise. + */ + bool IsEpgBased() const { return !IsManual(); } + + /*! + * @brief The ID of the client for this timer. + * @return The client ID or -1 if this is a local timer. + */ + int ClientID() const { return m_iClientId; } + + /*! + * @brief Check, whether this timer is owned by a pvr client or by Kodi. + * @return True, if owned by a pvr client, false otherwise. + */ + bool IsOwnedByClient() const; + + /*! + * @brief Whether this timer is for Radio or TV. + * @return True if Radio, false otherwise. + */ + bool IsRadio() const { return m_bIsRadio; } + + /*! + * @brief The path that identifies this timer. + * @return The path. + */ + const std::string& Path() const; + + /*! + * @brief The index for this timer, as given by the client. + * @return The client index or PVR_TIMER_NO_CLIENT_INDEX if the timer was just created locally + * by Kodi and was not yet added by the client. + */ + int ClientIndex() const { return m_iClientIndex; } + + /*! + * @brief The index for the parent of this timer, as given by the client. Timers scheduled by a + * timer rule will have a parant index != PVR_TIMER_NO_PARENT. + * @return The client index or PVR_TIMER_NO_PARENT if the timer has no parent. + */ + int ParentClientIndex() const { return m_iParentClientIndex; } + + /*! + * @brief Whether this timer has a parent. + * @return True if timer has a parent, false otherwise. + */ + bool HasParent() const { return m_iParentClientIndex != PVR_TIMER_NO_PARENT; } + + /*! + * @brief The local ID for this timer, as given by Kodi. + * @return The ID or 0 if not yet set. + */ + unsigned int TimerID() const { return m_iTimerId; } + + /*! + * @brief Set the local ID for this timer. + * @param id The ID to set. + */ + void SetTimerID(unsigned int id) { m_iTimerId = id; } + + /*! + * @brief The UID of the channel for this timer. + * @return The channel UID or PVR_CHANNEL_INVALID_UID if not available. + */ + int ClientChannelUID() const { return m_iClientChannelUid; } + + /*! + * @brief The state for this timer. + * @return The state. + */ + PVR_TIMER_STATE State() const { return m_state; } + + /*! + * @brief Set the state for this timer. + * @param state The state to set. + */ + void SetState(PVR_TIMER_STATE state) { m_state = state; } + + /*! + * @brief The title for this timer. + * @return The title. + */ + const std::string& Title() const; + + /*! + * @brief Check whether this timer has an associated channel. + * @return True if this timer has a channel set, false otherwise. + */ + bool HasChannel() const; + + /*! + * @brief Get the channel associated with this timer, if any. + * @return the channel or null if non is associated with this timer. + */ + std::shared_ptr<CPVRChannel> Channel() const; + + /*! + * @brief Update the channel associated with this timer, based on current client ID and + * channel UID. + */ + void UpdateChannel(); + + /*! + * @brief The name of the channel associated with this timer, if any. + * @return The channel name. + */ + std::string ChannelName() const; + + /*! + * @brief The path for the channel icon associated with this timer, if any. + * @return The channel icon path. + */ + std::string ChannelIcon() const; + + /*! + * @brief The start date and time for this timer, as UTC. + * @return The start date and time. + */ + CDateTime StartAsUTC() const; + + /*! + * @brief The start date and time for this timer, as local time. + * @return The start date and time. + */ + CDateTime StartAsLocalTime() const; + + /*! + * @brief Set the start date and time from a CDateTime instance carrying the data as UTC. + * @param start The start date and time as UTC. + */ + void SetStartFromUTC(const CDateTime& start); + + /*! + * @brief Set the start date and time from a CDateTime instance carrying the data as local time. + * @param start The start date and time as local time. + */ + void SetStartFromLocalTime(const CDateTime& start); + + /*! + * @brief The end date and time for this timer, as UTC. + * @return The start date and time. + */ + CDateTime EndAsUTC() const; + + /*! + * @brief The end date and time for this timer, as local time. + * @return The start date and time. + */ + CDateTime EndAsLocalTime() const; + + /*! + * @brief Set the end date and time from a CDateTime instance carrying the data as UTC. + * @param start The end date and time as UTC. + */ + void SetEndFromUTC(const CDateTime& end); + + /*! + * @brief Set the end date and time from a CDateTime instance carrying the data as local time. + * @param start The end date and time as local time. + */ + void SetEndFromLocalTime(const CDateTime& end); + + /*! + * @brief The first day for this timer, as UTC. + * @return The first day. + */ + CDateTime FirstDayAsUTC() const; + + /*! + * @brief The first day for this timer, as local time. + * @return The first day. + */ + CDateTime FirstDayAsLocalTime() const; + + /*! + * @brief Set the first dday from a CDateTime instance carrying the data as UTC. + * @param start The first day as UTC. + */ + void SetFirstDayFromUTC(const CDateTime& firstDay); + + /*! + * @brief Set the first dday from a CDateTime instance carrying the data as local time. + * @param start The first day as local time. + */ + void SetFirstDayFromLocalTime(const CDateTime& firstDay); + + /*! + * @brief Helper function to convert a given CDateTime containing data as UTC to local time. + * @param utc A CDateTime instance carrying data as UTC. + * @return A CDateTime instance carrying data as local time. + */ + static CDateTime ConvertUTCToLocalTime(const CDateTime& utc); + + /*! + * @brief Helper function to convert a given CDateTime containing data as local time to UTC. + * @param local A CDateTime instance carrying data as local time. + * @return A CDateTime instance carrying data as UTC. + */ + static CDateTime ConvertLocalTimeToUTC(const CDateTime& local); + + /*! + * @brief Get the duration of this timer in seconds, excluding padding times. + * @return The duration. + */ + int GetDuration() const; + + /*! + * @brief Get time in minutes to start the recording before the start time of the programme. + * @return The start padding time. + */ + unsigned int MarginStart() const { return m_iMarginStart; } + + /*! + * @brief Get time in minutes to end the recording after the end time of the programme. + * @return The end padding time. + */ + unsigned int MarginEnd() const { return m_iMarginEnd; } + + /*! + * @brief For timer rules, the days of week this timer rule is scheduled for. + * @return The days of week. + */ + unsigned int WeekDays() const { return m_iWeekdays; } + + /*! + * @brief For timer rules, whether start time is "any time", not a particular time. + * @return True, if timer start is "any time", false otherwise. + */ + bool IsStartAnyTime() const { return m_bStartAnyTime; } + + /*! + * @brief For timer rules, whether end time is "any time", not a particular time. + * @return True, if timer end is "any time", false otherwise. + */ + bool IsEndAnyTime() const { return m_bEndAnyTime; } + + /*! + * @brief For timer rules, whether only the EPG programme title shall be searched or also other + * data like the programme's plot, if available. + * @return True, if not only the programme's title shall be included in EPG search, + * false otherwise. + */ + bool IsFullTextEpgSearch() const { return m_bFullTextEpgSearch; } + + /*! + * @brief For timer rules, the epg data match string for searches. Format is backend-dependent, + * for example regexp. + * @return The search string + */ + const std::string& EpgSearchString() const { return m_strEpgSearchString; } + + /*! + * @brief The series link for this timer. + * @return The series link or empty string, if not available. + */ + const std::string& SeriesLink() const; + + /*! + * @brief Get the UID of the epg event associated with this timer tag, if any. + * @return The UID or EPG_TAG_INVALID_UID. + */ + unsigned int UniqueBroadcastID() const { return m_iEpgUid; } + + /*! + * @brief Add this timer to the backend, transferring all local data of this timer to the backend. + * @return True on success, false otherwise. + */ + bool AddToClient() const; + + /*! + * @brief Delete this timer on the backend. + * @param bForce Control what to do in case the timer is currently recording. + * True to force to delete the timer, false to return TimerDeleteResult::RECORDING. + * @return The result. + */ + TimerOperationResult DeleteFromClient(bool bForce = false) const; + + /*! + * @brief Update this timer on the backend, transferring all local data of this timer to + * the backend. + * @return True on success, false otherwise. + */ + bool UpdateOnClient(); + + /*! + * @brief Persist this timer in the local database. + * @return True on success, false otherwise. + */ + bool Persist(); + + /*! + * @brief Delete this timer from the local database. + * @return True on success, false otherwise. + */ + bool DeleteFromDatabase(); + + /*! + * @brief GUI support: Get the text for the timer GUI notification. + * @return The notification text. + */ + std::string GetNotificationText() const; + + /*! + * @brief GUI support: Get the text for the timer GUI notification when a timer has been deleted. + * @return The notification text. + */ + std::string GetDeletedNotificationText() const; + + /*! + * @brief GUI support: Get the summary text for this timer, reflecting the timer schedule in a + * human readable form. + * @return The summary string. + */ + const std::string& Summary() const; + + /*! + * @brief GUI support: Update the summary text for this timer. + */ + void UpdateSummary(); + + /*! + * @brief GUI support: Get the status text for this timer, reflecting its current state in a + * human readable form. + * @return The status string. + */ + std::string GetStatus(bool bRadio) const; + + /*! + * @brief GUI support: Get the timer string in a human readable form. + * @return The type string. + */ + std::string GetTypeAsString() const; + + /*! + * @brief GUI support: Return string representation for any possible combination of weekdays. + * @param iWeekdays weekdays as bit mask (0x01 = Mo, 0x02 = Tu, ...) + * @param bEpgBased context is an epg-based timer + * @param bLongMultiDaysFormat use long format. ("Mo-__-We-__-Fr-Sa-__" vs. "Mo-We-Fr-Sa") + * @return The weekdays string representation. + */ + static std::string GetWeekdaysString(unsigned int iWeekdays, + bool bEpgBased, + bool bLongMultiDaysFormat); + +private: + CPVRTimerInfoTag(const CPVRTimerInfoTag& tag) = delete; + CPVRTimerInfoTag& operator=(const CPVRTimerInfoTag& orig) = delete; + + std::string GetWeekdaysString() const; + void UpdateEpgInfoTag(); + + static std::shared_ptr<CPVRTimerInfoTag> CreateFromDate( + const std::shared_ptr<CPVRChannel>& channel, + const CDateTime& start, + int iDuration, + bool bCreateReminder, + bool bReadOnly); + + mutable CCriticalSection m_critSection; + + std::string m_strTitle; /*!< @brief name of this timer */ + std::string + m_strEpgSearchString; /*!< @brief a epg data match string for epg-based timer rules. Format is backend-dependent, for example regexp */ + bool m_bFullTextEpgSearch = + false; /*!< @brief indicates whether only epg episode title can be matched by the pvr backend or "more" (backend-dependent") data. */ + std::string m_strDirectory; /*!< @brief directory where the recording must be stored */ + std::string m_strSummary; /*!< @brief summary string with the time to show inside a GUI list */ + PVR_TIMER_STATE m_state = PVR_TIMER_STATE_SCHEDULED; /*!< @brief the state of this timer */ + int m_iClientId; /*!< @brief ID of the backend */ + int m_iClientIndex; /*!< @brief index number of the tag, given by the backend, PVR_TIMER_NO_CLIENT_INDEX for new */ + int m_iParentClientIndex; /*!< @brief for timers scheduled by a timer rule, the index number of the parent, given by the backend, PVR_TIMER_NO_PARENT for no parent */ + int m_iClientChannelUid; /*!< @brief channel uid */ + bool m_bStartAnyTime = + false; /*!< @brief Ignore start date and time clock. Record at 'Any Time' */ + bool m_bEndAnyTime = false; /*!< @brief Ignore end date and time clock. Record at 'Any Time' */ + int m_iPriority; /*!< @brief priority of the timer */ + int m_iLifetime; /*!< @brief lifetime of the timer in days */ + int m_iMaxRecordings = + 0; /*!< @brief (optional) backend setting for maximum number of recordings to keep*/ + unsigned int m_iWeekdays; /*!< @brief bit based store of weekdays for timer rules */ + unsigned int + m_iPreventDupEpisodes; /*!< @brief only record new episodes for epg-based timer rules */ + unsigned int m_iRecordingGroup = + 0; /*!< @brief (optional) if set, the addon/backend stores the recording to a group (sub-folder) */ + std::string m_strFileNameAndPath; /*!< @brief file name is only for reference */ + bool m_bIsRadio; /*!< @brief is radio channel if set */ + unsigned int m_iTimerId = 0; /*!< @brief id that won't change as long as Kodi is running */ + unsigned int + m_iMarginStart; /*!< @brief (optional) if set, the backend starts the recording iMarginStart minutes before startTime. */ + unsigned int + m_iMarginEnd; /*!< @brief (optional) if set, the backend ends the recording iMarginEnd minutes after endTime. */ + mutable unsigned int + m_iEpgUid; /*!< id of epg event associated with this timer, EPG_TAG_INVALID_UID if none. */ + std::string m_strSeriesLink; /*!< series link */ + + CDateTime m_StartTime; /*!< start time */ + CDateTime m_StopTime; /*!< stop time */ + CDateTime m_FirstDay; /*!< if it is a manual timer rule the first date it starts */ + std::shared_ptr<CPVRTimerType> m_timerType; /*!< the type of this timer */ + + unsigned int m_iTVChildTimersActive = 0; + unsigned int m_iTVChildTimersConflictNOK = 0; + unsigned int m_iTVChildTimersRecording = 0; + unsigned int m_iTVChildTimersErrors = 0; + unsigned int m_iRadioChildTimersActive = 0; + unsigned int m_iRadioChildTimersConflictNOK = 0; + unsigned int m_iRadioChildTimersRecording = 0; + unsigned int m_iRadioChildTimersErrors = 0; + + mutable std::shared_ptr<CPVREpgInfoTag> m_epgTag; /*!< epg info tag matching m_iEpgUid. */ + mutable std::shared_ptr<CPVRChannel> m_channel; + + mutable bool m_bProbedEpgTag = false; +}; +} // namespace PVR diff --git a/xbmc/pvr/timers/PVRTimerRuleMatcher.cpp b/xbmc/pvr/timers/PVRTimerRuleMatcher.cpp new file mode 100644 index 0000000..9178e85 --- /dev/null +++ b/xbmc/pvr/timers/PVRTimerRuleMatcher.cpp @@ -0,0 +1,193 @@ +/* + * 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 "PVRTimerRuleMatcher.h" + +#include "XBDateTime.h" +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h" // PVR_CHANNEL_INVALID_UID +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "utils/RegExp.h" + +using namespace PVR; + +CPVRTimerRuleMatcher::CPVRTimerRuleMatcher(const std::shared_ptr<CPVRTimerInfoTag>& timerRule, + const CDateTime& start) + : m_timerRule(timerRule), m_start(CPVRTimerInfoTag::ConvertUTCToLocalTime(start)) +{ +} + +CPVRTimerRuleMatcher::~CPVRTimerRuleMatcher() = default; + +std::shared_ptr<CPVRChannel> CPVRTimerRuleMatcher::GetChannel() const +{ + if (m_timerRule->GetTimerType()->SupportsChannels()) + return m_timerRule->Channel(); + + return {}; +} + +CDateTime CPVRTimerRuleMatcher::GetNextTimerStart() const +{ + if (!m_timerRule->GetTimerType()->SupportsStartTime()) + return CDateTime(); // invalid datetime + + const CDateTime startDateLocal = m_timerRule->GetTimerType()->SupportsFirstDay() + ? m_timerRule->FirstDayAsLocalTime() + : m_start; + const CDateTime startTimeLocal = m_timerRule->StartAsLocalTime(); + CDateTime nextStart(startDateLocal.GetYear(), startDateLocal.GetMonth(), startDateLocal.GetDay(), + startTimeLocal.GetHour(), startTimeLocal.GetMinute(), 0); + + const CDateTimeSpan oneDay(1, 0, 0, 0); + while (nextStart < m_start) + { + nextStart += oneDay; + } + + if (m_timerRule->GetTimerType()->SupportsWeekdays() && + m_timerRule->WeekDays() != PVR_WEEKDAY_ALLDAYS) + { + bool bMatch = false; + while (!bMatch) + { + int startWeekday = nextStart.GetDayOfWeek(); + if (startWeekday == 0) + startWeekday = 7; + + bMatch = ((1 << (startWeekday - 1)) & m_timerRule->WeekDays()); + if (!bMatch) + nextStart += oneDay; + } + } + + return nextStart.GetAsUTCDateTime(); +} + +bool CPVRTimerRuleMatcher::Matches(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + return epgTag && CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->EndAsUTC()) > m_start && + MatchSeriesLink(epgTag) && MatchChannel(epgTag) && MatchStart(epgTag) && + MatchEnd(epgTag) && MatchDayOfWeek(epgTag) && MatchSearchText(epgTag); +} + +bool CPVRTimerRuleMatcher::MatchSeriesLink(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + if (m_timerRule->GetTimerType()->RequiresEpgSeriesLinkOnCreate()) + return epgTag->SeriesLink() == m_timerRule->SeriesLink(); + else + return true; +} + +bool CPVRTimerRuleMatcher::MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + if (m_timerRule->GetTimerType()->SupportsAnyChannel() && + m_timerRule->ClientChannelUID() == PVR_CHANNEL_INVALID_UID) + return true; // matches any channel + + if (m_timerRule->GetTimerType()->SupportsChannels()) + return m_timerRule->ClientChannelUID() != PVR_CHANNEL_INVALID_UID && + epgTag->ClientID() == m_timerRule->ClientID() && + epgTag->UniqueChannelID() == m_timerRule->ClientChannelUID(); + else + return true; +} + +bool CPVRTimerRuleMatcher::MatchStart(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + if (m_timerRule->GetTimerType()->SupportsFirstDay()) + { + // only year, month and day do matter here... + const CDateTime startEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->StartAsUTC()); + const CDateTime startEpg(startEpgLocal.GetYear(), startEpgLocal.GetMonth(), + startEpgLocal.GetDay(), 0, 0, 0); + const CDateTime firstDayLocal = m_timerRule->FirstDayAsLocalTime(); + const CDateTime startTimer(firstDayLocal.GetYear(), firstDayLocal.GetMonth(), + firstDayLocal.GetDay(), 0, 0, 0); + if (startEpg < startTimer) + return false; + } + + if (m_timerRule->GetTimerType()->SupportsStartAnyTime() && m_timerRule->IsStartAnyTime()) + return true; // matches any start time + + if (m_timerRule->GetTimerType()->SupportsStartTime()) + { + // only hours and minutes do matter here... + const CDateTime startEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->StartAsUTC()); + const CDateTime startEpg(2000, 1, 1, startEpgLocal.GetHour(), startEpgLocal.GetMinute(), 0); + const CDateTime startTimerLocal = m_timerRule->StartAsLocalTime(); + const CDateTime startTimer(2000, 1, 1, startTimerLocal.GetHour(), startTimerLocal.GetMinute(), + 0); + return startEpg >= startTimer; + } + else + return true; +} + +bool CPVRTimerRuleMatcher::MatchEnd(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + if (m_timerRule->GetTimerType()->SupportsEndAnyTime() && m_timerRule->IsEndAnyTime()) + return true; // matches any end time + + if (m_timerRule->GetTimerType()->SupportsEndTime()) + { + // only hours and minutes do matter here... + const CDateTime endEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->EndAsUTC()); + const CDateTime endEpg(2000, 1, 1, endEpgLocal.GetHour(), endEpgLocal.GetMinute(), 0); + const CDateTime endTimerLocal = m_timerRule->EndAsLocalTime(); + const CDateTime endTimer(2000, 1, 1, endTimerLocal.GetHour(), endTimerLocal.GetMinute(), 0); + return endEpg <= endTimer; + } + else + return true; +} + +bool CPVRTimerRuleMatcher::MatchDayOfWeek(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + if (m_timerRule->GetTimerType()->SupportsWeekdays()) + { + if (m_timerRule->WeekDays() != PVR_WEEKDAY_ALLDAYS) + { + const CDateTime startEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->StartAsUTC()); + int startWeekday = startEpgLocal.GetDayOfWeek(); + if (startWeekday == 0) + startWeekday = 7; + + return ((1 << (startWeekday - 1)) & m_timerRule->WeekDays()); + } + } + return true; +} + +bool CPVRTimerRuleMatcher::MatchSearchText(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + if (m_timerRule->GetTimerType()->SupportsEpgFulltextMatch() && m_timerRule->IsFullTextEpgSearch()) + { + if (!m_textSearch) + { + m_textSearch.reset(new CRegExp(true /* case insensitive */)); + m_textSearch->RegComp(m_timerRule->EpgSearchString()); + } + return m_textSearch->RegFind(epgTag->Title()) >= 0 || + m_textSearch->RegFind(epgTag->EpisodeName()) >= 0 || + m_textSearch->RegFind(epgTag->PlotOutline()) >= 0 || + m_textSearch->RegFind(epgTag->Plot()) >= 0; + } + else if (m_timerRule->GetTimerType()->SupportsEpgTitleMatch()) + { + if (!m_textSearch) + { + m_textSearch.reset(new CRegExp(true /* case insensitive */)); + m_textSearch->RegComp(m_timerRule->EpgSearchString()); + } + return m_textSearch->RegFind(epgTag->Title()) >= 0; + } + else + return true; +} diff --git a/xbmc/pvr/timers/PVRTimerRuleMatcher.h b/xbmc/pvr/timers/PVRTimerRuleMatcher.h new file mode 100644 index 0000000..9d502af --- /dev/null +++ b/xbmc/pvr/timers/PVRTimerRuleMatcher.h @@ -0,0 +1,47 @@ +/* + * 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 <memory> + +class CRegExp; + +namespace PVR +{ +class CPVRChannel; +class CPVRTimerInfoTag; +class CPVREpgInfoTag; + +class CPVRTimerRuleMatcher +{ +public: + CPVRTimerRuleMatcher(const std::shared_ptr<CPVRTimerInfoTag>& timerRule, const CDateTime& start); + virtual ~CPVRTimerRuleMatcher(); + + std::shared_ptr<CPVRTimerInfoTag> GetTimerRule() const { return m_timerRule; } + + std::shared_ptr<CPVRChannel> GetChannel() const; + CDateTime GetNextTimerStart() const; + bool Matches(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + +private: + bool MatchSeriesLink(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + bool MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + bool MatchStart(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + bool MatchEnd(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + bool MatchDayOfWeek(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + bool MatchSearchText(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + + const std::shared_ptr<CPVRTimerInfoTag> m_timerRule; + CDateTime m_start; + mutable std::unique_ptr<CRegExp> m_textSearch; +}; +} // namespace PVR diff --git a/xbmc/pvr/timers/PVRTimerType.cpp b/xbmc/pvr/timers/PVRTimerType.cpp new file mode 100644 index 0000000..db20fa1 --- /dev/null +++ b/xbmc/pvr/timers/PVRTimerType.cpp @@ -0,0 +1,418 @@ +/* + * 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 "PVRTimerType.h" + +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <algorithm> +#include <iterator> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +using namespace PVR; + +const std::vector<std::shared_ptr<CPVRTimerType>> CPVRTimerType::GetAllTypes() +{ + std::vector<std::shared_ptr<CPVRTimerType>> allTypes; + CServiceBroker::GetPVRManager().Clients()->GetTimerTypes(allTypes); + + // Add local reminder timer types. Local reminders are always available. + int iTypeId = PVR_TIMER_TYPE_NONE; + + // one time time-based reminder + allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId, + PVR_TIMER_TYPE_IS_MANUAL | + PVR_TIMER_TYPE_IS_REMINDER | + PVR_TIMER_TYPE_SUPPORTS_CHANNELS | + PVR_TIMER_TYPE_SUPPORTS_START_TIME | + PVR_TIMER_TYPE_SUPPORTS_END_TIME)); + + // one time epg-based reminder + allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId, + PVR_TIMER_TYPE_IS_REMINDER | + PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE | + PVR_TIMER_TYPE_SUPPORTS_CHANNELS | + PVR_TIMER_TYPE_SUPPORTS_START_TIME | + PVR_TIMER_TYPE_SUPPORTS_START_MARGIN)); + + // time-based reminder rule + allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId, + PVR_TIMER_TYPE_IS_REPEATING | + PVR_TIMER_TYPE_IS_MANUAL | + PVR_TIMER_TYPE_IS_REMINDER | + PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE | + PVR_TIMER_TYPE_SUPPORTS_CHANNELS | + PVR_TIMER_TYPE_SUPPORTS_START_TIME | + PVR_TIMER_TYPE_SUPPORTS_END_TIME | + PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY | + PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS)); + + // one time read-only time-based reminder (created by timer rule) + allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId, + PVR_TIMER_TYPE_IS_MANUAL | + PVR_TIMER_TYPE_IS_REMINDER | + PVR_TIMER_TYPE_IS_READONLY | + PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE | + PVR_TIMER_TYPE_SUPPORTS_CHANNELS | + PVR_TIMER_TYPE_SUPPORTS_START_TIME | + PVR_TIMER_TYPE_SUPPORTS_END_TIME, + g_localizeStrings.Get(819))); // One time (Scheduled by timer rule) + + // epg-based reminder rule + allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId, + PVR_TIMER_TYPE_IS_REPEATING | + PVR_TIMER_TYPE_IS_REMINDER | + PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE | + PVR_TIMER_TYPE_SUPPORTS_TITLE_EPG_MATCH | + PVR_TIMER_TYPE_SUPPORTS_FULLTEXT_EPG_MATCH | + PVR_TIMER_TYPE_SUPPORTS_CHANNELS | + PVR_TIMER_TYPE_SUPPORTS_ANY_CHANNEL | + PVR_TIMER_TYPE_SUPPORTS_START_TIME | + PVR_TIMER_TYPE_SUPPORTS_START_ANYTIME | + PVR_TIMER_TYPE_SUPPORTS_END_TIME | + PVR_TIMER_TYPE_SUPPORTS_END_ANYTIME | + PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY | + PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS | + PVR_TIMER_TYPE_SUPPORTS_START_END_MARGIN)); + + // one time read-only epg-based reminder (created by timer rule) + allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId, + PVR_TIMER_TYPE_IS_REMINDER | + PVR_TIMER_TYPE_IS_READONLY | + PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE | + PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE | + PVR_TIMER_TYPE_SUPPORTS_CHANNELS | + PVR_TIMER_TYPE_SUPPORTS_START_TIME | + PVR_TIMER_TYPE_SUPPORTS_START_MARGIN, + g_localizeStrings.Get(819))); // One time (Scheduled by timer rule) + + return allTypes; +} + +const std::shared_ptr<CPVRTimerType> CPVRTimerType::GetFirstAvailableType(const std::shared_ptr<CPVRClient>& client) +{ + if (client) + { + std::vector<std::shared_ptr<CPVRTimerType>> types; + if (client->GetTimerTypes(types) == PVR_ERROR_NO_ERROR && !types.empty()) + { + return *(types.begin()); + } + } + return {}; +} + +std::shared_ptr<CPVRTimerType> CPVRTimerType::CreateFromIds(unsigned int iTypeId, int iClientId) +{ + const std::vector<std::shared_ptr<CPVRTimerType>> types = GetAllTypes(); + const auto it = + std::find_if(types.cbegin(), types.cend(), [iClientId, iTypeId](const auto& type) { + return type->GetClientId() == iClientId && type->GetTypeId() == iTypeId; + }); + if (it != types.cend()) + return (*it); + + if (iClientId != -1) + { + // fallback. try to obtain local timer type. + std::shared_ptr<CPVRTimerType> type = CreateFromIds(iTypeId, -1); + if (type) + return type; + } + + CLog::LogF(LOGERROR, "Unable to resolve numeric timer type ({}, {})", iTypeId, iClientId); + return {}; +} + +std::shared_ptr<CPVRTimerType> CPVRTimerType::CreateFromAttributes(uint64_t iMustHaveAttr, + uint64_t iMustNotHaveAttr, + int iClientId) +{ + const std::vector<std::shared_ptr<CPVRTimerType>> types = GetAllTypes(); + const auto it = std::find_if(types.cbegin(), types.cend(), + [iClientId, iMustHaveAttr, iMustNotHaveAttr](const auto& type) { + return type->GetClientId() == iClientId && + (type->GetAttributes() & iMustHaveAttr) == iMustHaveAttr && + (type->GetAttributes() & iMustNotHaveAttr) == 0; + }); + if (it != types.cend()) + return (*it); + + if (iClientId != -1) + { + // fallback. try to obtain local timer type. + std::shared_ptr<CPVRTimerType> type = CreateFromAttributes(iMustHaveAttr, iMustNotHaveAttr, -1); + if (type) + return type; + } + + CLog::LogF(LOGERROR, "Unable to resolve timer type (0x{:x}, 0x{:x}, {})", iMustHaveAttr, + iMustNotHaveAttr, iClientId); + return {}; +} + +CPVRTimerType::CPVRTimerType() : + m_iTypeId(PVR_TIMER_TYPE_NONE), + m_iAttributes(PVR_TIMER_TYPE_ATTRIBUTE_NONE) +{ +} + +CPVRTimerType::CPVRTimerType(const PVR_TIMER_TYPE& type, int iClientId) : + m_iClientId(iClientId), + m_iTypeId(type.iId), + m_iAttributes(type.iAttributes), + m_strDescription(type.strDescription) +{ + InitDescription(); + InitAttributeValues(type); +} + +CPVRTimerType::CPVRTimerType(unsigned int iTypeId, + uint64_t iAttributes, + const std::string& strDescription) + : m_iTypeId(iTypeId), m_iAttributes(iAttributes), m_strDescription(strDescription) +{ + InitDescription(); +} + +CPVRTimerType::~CPVRTimerType() = default; + +bool CPVRTimerType::operator ==(const CPVRTimerType& right) const +{ + return (m_iClientId == right.m_iClientId && + m_iTypeId == right.m_iTypeId && + m_iAttributes == right.m_iAttributes && + m_strDescription == right.m_strDescription && + m_priorityValues == right.m_priorityValues && + m_iPriorityDefault == right.m_iPriorityDefault && + m_lifetimeValues == right.m_lifetimeValues && + m_iLifetimeDefault == right.m_iLifetimeDefault && + m_maxRecordingsValues == right.m_maxRecordingsValues && + m_iMaxRecordingsDefault == right.m_iMaxRecordingsDefault && + m_preventDupEpisodesValues == right.m_preventDupEpisodesValues && + m_iPreventDupEpisodesDefault == right.m_iPreventDupEpisodesDefault && + m_recordingGroupValues == right.m_recordingGroupValues && + m_iRecordingGroupDefault == right.m_iRecordingGroupDefault); +} + +bool CPVRTimerType::operator !=(const CPVRTimerType& right) const +{ + return !(*this == right); +} + +void CPVRTimerType::InitDescription() +{ + // if no description was given, compile it + if (m_strDescription.empty()) + { + int id; + if (m_iAttributes & PVR_TIMER_TYPE_IS_REPEATING) + { + id = (m_iAttributes & PVR_TIMER_TYPE_IS_MANUAL) + ? 822 // "Timer rule" + : 823; // "Timer rule (guide-based)" + } + else + { + id = (m_iAttributes & PVR_TIMER_TYPE_IS_MANUAL) + ? 820 // "One time" + : 821; // "One time (guide-based) + } + m_strDescription = g_localizeStrings.Get(id); + } + + // add reminder/recording prefix + int prefixId = (m_iAttributes & PVR_TIMER_TYPE_IS_REMINDER) + ? 824 // Reminder: ... + : 825; // Recording: ... + + m_strDescription = StringUtils::Format(g_localizeStrings.Get(prefixId), m_strDescription); +} + +void CPVRTimerType::InitAttributeValues(const PVR_TIMER_TYPE& type) +{ + InitPriorityValues(type); + InitLifetimeValues(type); + InitMaxRecordingsValues(type); + InitPreventDuplicateEpisodesValues(type); + InitRecordingGroupValues(type); +} + +void CPVRTimerType::InitPriorityValues(const PVR_TIMER_TYPE& type) +{ + if (type.iPrioritiesSize > 0) + { + for (unsigned int i = 0; i < type.iPrioritiesSize; ++i) + { + std::string strDescr(type.priorities[i].strDescription); + if (strDescr.empty()) + { + // No description given by addon. Create one from value. + strDescr = std::to_string(type.priorities[i].iValue); + } + m_priorityValues.emplace_back(strDescr, type.priorities[i].iValue); + } + + m_iPriorityDefault = type.iPrioritiesDefault; + } + else if (SupportsPriority()) + { + // No values given by addon, but priority supported. Use default values 1..100 + for (int i = 1; i < 101; ++i) + m_priorityValues.emplace_back(std::to_string(i), i); + + m_iPriorityDefault = DEFAULT_RECORDING_PRIORITY; + } + else + { + // No priority supported. + m_iPriorityDefault = DEFAULT_RECORDING_PRIORITY; + } +} + +void CPVRTimerType::GetPriorityValues(std::vector<std::pair<std::string, int>>& list) const +{ + std::copy(m_priorityValues.cbegin(), m_priorityValues.cend(), std::back_inserter(list)); +} + +void CPVRTimerType::InitLifetimeValues(const PVR_TIMER_TYPE& type) +{ + if (type.iLifetimesSize > 0) + { + for (unsigned int i = 0; i < type.iLifetimesSize; ++i) + { + int iValue = type.lifetimes[i].iValue; + std::string strDescr(type.lifetimes[i].strDescription); + if (strDescr.empty()) + { + // No description given by addon. Create one from value. + strDescr = std::to_string(iValue); + } + m_lifetimeValues.emplace_back(strDescr, iValue); + } + + m_iLifetimeDefault = type.iLifetimesDefault; + } + else if (SupportsLifetime()) + { + // No values given by addon, but lifetime supported. Use default values 1..365 + for (int i = 1; i < 366; ++i) + { + m_lifetimeValues.emplace_back(StringUtils::Format(g_localizeStrings.Get(17999), i), + i); // "{} days" + } + m_iLifetimeDefault = DEFAULT_RECORDING_LIFETIME; + } + else + { + // No lifetime supported. + m_iLifetimeDefault = DEFAULT_RECORDING_LIFETIME; + } +} + +void CPVRTimerType::GetLifetimeValues(std::vector<std::pair<std::string, int>>& list) const +{ + std::copy(m_lifetimeValues.cbegin(), m_lifetimeValues.cend(), std::back_inserter(list)); +} + +void CPVRTimerType::InitMaxRecordingsValues(const PVR_TIMER_TYPE& type) +{ + if (type.iMaxRecordingsSize > 0) + { + for (unsigned int i = 0; i < type.iMaxRecordingsSize; ++i) + { + std::string strDescr(type.maxRecordings[i].strDescription); + if (strDescr.empty()) + { + // No description given by addon. Create one from value. + strDescr = std::to_string(type.maxRecordings[i].iValue); + } + m_maxRecordingsValues.emplace_back(strDescr, type.maxRecordings[i].iValue); + } + + m_iMaxRecordingsDefault = type.iMaxRecordingsDefault; + } +} + +void CPVRTimerType::GetMaxRecordingsValues(std::vector<std::pair<std::string, int>>& list) const +{ + std::copy(m_maxRecordingsValues.cbegin(), m_maxRecordingsValues.cend(), std::back_inserter(list)); +} + +void CPVRTimerType::InitPreventDuplicateEpisodesValues(const PVR_TIMER_TYPE& type) +{ + if (type.iPreventDuplicateEpisodesSize > 0) + { + for (unsigned int i = 0; i < type.iPreventDuplicateEpisodesSize; ++i) + { + std::string strDescr(type.preventDuplicateEpisodes[i].strDescription); + if (strDescr.empty()) + { + // No description given by addon. Create one from value. + strDescr = std::to_string(type.preventDuplicateEpisodes[i].iValue); + } + m_preventDupEpisodesValues.emplace_back(strDescr, type.preventDuplicateEpisodes[i].iValue); + } + + m_iPreventDupEpisodesDefault = type.iPreventDuplicateEpisodesDefault; + } + else if (SupportsRecordOnlyNewEpisodes()) + { + // No values given by addon, but prevent duplicate episodes supported. Use default values 0..1 + m_preventDupEpisodesValues.emplace_back(g_localizeStrings.Get(815), 0); // "Record all episodes" + m_preventDupEpisodesValues.emplace_back(g_localizeStrings.Get(816), 1); // "Record only new episodes" + m_iPreventDupEpisodesDefault = DEFAULT_RECORDING_DUPLICATEHANDLING; + } + else + { + // No prevent duplicate episodes supported. + m_iPreventDupEpisodesDefault = DEFAULT_RECORDING_DUPLICATEHANDLING; + } +} + +void CPVRTimerType::GetPreventDuplicateEpisodesValues(std::vector<std::pair<std::string, int>>& list) const +{ + std::copy(m_preventDupEpisodesValues.cbegin(), m_preventDupEpisodesValues.cend(), + std::back_inserter(list)); +} + +void CPVRTimerType::InitRecordingGroupValues(const PVR_TIMER_TYPE& type) +{ + if (type.iRecordingGroupSize > 0) + { + for (unsigned int i = 0; i < type.iRecordingGroupSize; ++i) + { + std::string strDescr(type.recordingGroup[i].strDescription); + if (strDescr.empty()) + { + // No description given by addon. Create one from value. + strDescr = StringUtils::Format("{} {}", + g_localizeStrings.Get(811), // Recording group + type.recordingGroup[i].iValue); + } + m_recordingGroupValues.emplace_back(strDescr, type.recordingGroup[i].iValue); + } + + m_iRecordingGroupDefault = type.iRecordingGroupDefault; + } +} + +void CPVRTimerType::GetRecordingGroupValues(std::vector< std::pair<std::string, int>>& list) const +{ + std::copy(m_recordingGroupValues.cbegin(), m_recordingGroupValues.cend(), + std::back_inserter(list)); +} diff --git a/xbmc/pvr/timers/PVRTimerType.h b/xbmc/pvr/timers/PVRTimerType.h new file mode 100644 index 0000000..8ee9e22 --- /dev/null +++ b/xbmc/pvr/timers/PVRTimerType.h @@ -0,0 +1,411 @@ +/* + * 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_timers.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +struct PVR_TIMER_TYPE; + +namespace PVR +{ + class CPVRClient; + + static const int DEFAULT_RECORDING_PRIORITY = 50; + static const int DEFAULT_RECORDING_LIFETIME = 99; // days + static const unsigned int DEFAULT_RECORDING_DUPLICATEHANDLING = 0; + + class CPVRTimerType + { + public: + /*! + * @brief Return a list with all known timer types. + * @return A list of timer types or an empty list if no types available. + */ + static const std::vector<std::shared_ptr<CPVRTimerType>> GetAllTypes(); + + /*! + * @brief Return the first available timer type from given client. + * @param client the PVR client. + * @return A timer type or NULL if none available. + */ + static const std::shared_ptr<CPVRTimerType> GetFirstAvailableType(const std::shared_ptr<CPVRClient>& client); + + /*! + * @brief Create a timer type from given timer type id and client id. + * @param iTimerType the timer type id. + * @param iClientId the PVR client id. + * @return A timer type instance. + */ + static std::shared_ptr<CPVRTimerType> CreateFromIds(unsigned int iTypeId, int iClientId); + + /*! + * @brief Create a timer type from given timer type attributes and client id. + * @param iMustHaveAttr a combination of PVR_TIMER_TYPE_* attributes the type to create must have. + * @param iMustNotHaveAttr a combination of PVR_TIMER_TYPE_* attributes the type to create must not have. + * @param iClientId the PVR client id. + * @return A timer type instance. + */ + static std::shared_ptr<CPVRTimerType> CreateFromAttributes(uint64_t iMustHaveAttr, + uint64_t iMustNotHaveAttr, + int iClientId); + + CPVRTimerType(); + CPVRTimerType(const PVR_TIMER_TYPE& type, int iClientId); + CPVRTimerType(unsigned int iTypeId, + uint64_t iAttributes, + const std::string& strDescription = ""); + + virtual ~CPVRTimerType(); + + CPVRTimerType(const CPVRTimerType& type) = delete; + CPVRTimerType& operator=(const CPVRTimerType& orig) = delete; + + bool operator ==(const CPVRTimerType& right) const; + bool operator !=(const CPVRTimerType& right) const; + + /*! + * @brief Get the PVR client id for this type. + * @return The PVR client id. + */ + int GetClientId() const { return m_iClientId; } + + /*! + * @brief Get the numeric type id of this type. + * @return The type id. + */ + unsigned int GetTypeId() const { return m_iTypeId; } + + /*! + * @brief Get the plain text (UI) description of this type. + * @return The description. + */ + const std::string& GetDescription() const { return m_strDescription; } + + /*! + * @brief Get the attributes of this type. + * @return The attributes. + */ + uint64_t GetAttributes() const { return m_iAttributes; } + + /*! + * @brief Check whether this type is for timer rules or one time timers. + * @return True if type represents a timer rule, false otherwise. + */ + bool IsTimerRule() const { return (m_iAttributes & PVR_TIMER_TYPE_IS_REPEATING) > 0; } + + /*! + * @brief Check whether this type is for reminder timers or recording timers. + * @return True if type represents a reminder timer, false otherwise. + */ + bool IsReminder() const { return (m_iAttributes & PVR_TIMER_TYPE_IS_REMINDER) > 0; } + + /*! + * @brief Check whether this type is for timer rules or one time timers. + * @return True if type represents a one time timer, false otherwise. + */ + bool IsOnetime() const { return !IsTimerRule(); } + + /*! + * @brief Check whether this type is for epg-based or manual timers. + * @return True if manual, false otherwise. + */ + bool IsManual() const { return (m_iAttributes & PVR_TIMER_TYPE_IS_MANUAL) > 0; } + + /*! + * @brief Check whether this type is for epg-based or manual timers. + * @return True if epg-based, false otherwise. + */ + bool IsEpgBased() const { return !IsManual(); } + + /*! + * @brief Check whether this type is for epg-based timer rules. + * @return True if epg-based timer rule, false otherwise. + */ + bool IsEpgBasedTimerRule() const { return IsEpgBased() && IsTimerRule(); } + + /*! + * @brief Check whether this type is for one time epg-based timers. + * @return True if one time epg-based, false otherwise. + */ + bool IsEpgBasedOnetime() const { return IsEpgBased() && IsOnetime(); } + + /*! + * @brief Check whether this type is for manual timer rules. + * @return True if manual timer rule, false otherwise. + */ + bool IsManualTimerRule() const { return IsManual() && IsTimerRule(); } + + /*! + * @brief Check whether this type is for one time manual timers. + * @return True if one time manual, false otherwise. + */ + bool IsManualOnetime() const { return IsManual() && IsOnetime(); } + + /*! + * @brief Check whether this type is readonly (must not be modified after initial creation). + * @return True if readonly, false otherwise. + */ + bool IsReadOnly() const { return (m_iAttributes & PVR_TIMER_TYPE_IS_READONLY) > 0; } + + /*! + * @brief Check whether this type allows deletion. + * @return True if type allows deletion, false otherwise. + */ + bool AllowsDelete() const { return !IsReadOnly() || SupportsReadOnlyDelete(); } + + /*! + * @brief Check whether this type forbids creation of new timers of this type. + * @return True if new instances are forbidden, false otherwise. + */ + bool ForbidsNewInstances() const { return (m_iAttributes & PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES) > 0; } + + /*! + * @brief Check whether this timer type is forbidden when epg tag info is present. + * @return True if new instances are forbidden when epg info is present, false otherwise. + */ + bool ForbidsEpgTagOnCreate() const { return (m_iAttributes & PVR_TIMER_TYPE_FORBIDS_EPG_TAG_ON_CREATE) > 0; } + + /*! + * @brief Check whether this timer type requires epg tag info to be present. + * @return True if new instances require EPG info, false otherwise. + */ + bool RequiresEpgTagOnCreate() const { return (m_iAttributes & (PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE | + PVR_TIMER_TYPE_REQUIRES_EPG_SERIES_ON_CREATE | + PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE)) > 0; } + + /*! + * @brief Check whether this timer type requires epg tag info including series attributes to be present. + * @return True if new instances require an EPG tag with series attributes, false otherwise. + */ + bool RequiresEpgSeriesOnCreate() const { return (m_iAttributes & PVR_TIMER_TYPE_REQUIRES_EPG_SERIES_ON_CREATE) > 0; } + + /*! + * @brief Check whether this timer type requires epg tag info including a series link to be present. + * @return True if new instances require an EPG tag with a series link, false otherwise. + */ + bool RequiresEpgSeriesLinkOnCreate() const { return (m_iAttributes & PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE) > 0; } + + /*! + * @brief Check whether this type supports the "enabling/disabling" of timers of its type. + * @return True if "enabling/disabling" feature is supported, false otherwise. + */ + bool SupportsEnableDisable() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE) > 0; } + + /*! + * @brief Check whether this type supports channels. + * @return True if channels are supported, false otherwise. + */ + bool SupportsChannels() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_CHANNELS) > 0; } + + /*! + * @brief Check whether this type supports start time. + * @return True if start time values are supported, false otherwise. + */ + bool SupportsStartTime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_TIME) > 0; } + + /*! + * @brief Check whether this type supports end time. + * @return True if end time values are supported, false otherwise. + */ + bool SupportsEndTime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_END_TIME) > 0; } + /*! + * @brief Check whether this type supports start any time. + * @return True if start any time is supported, false otherwise. + */ + bool SupportsStartAnyTime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_ANYTIME) > 0; } + + /*! + * @brief Check whether this type supports end any time. + * @return True if end any time is supported, false otherwise. + */ + bool SupportsEndAnyTime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_END_ANYTIME) > 0; } + + /*! + * @brief Check whether this type supports matching a search string against epg episode title. + * @return True if title matching is supported, false otherwise. + */ + bool SupportsEpgTitleMatch() const { return (m_iAttributes & (PVR_TIMER_TYPE_SUPPORTS_TITLE_EPG_MATCH | PVR_TIMER_TYPE_SUPPORTS_FULLTEXT_EPG_MATCH)) > 0; } + + /*! + * @brief Check whether this type supports matching a search string against extended (fulltext) epg data. This + includes title matching. + * @return True if fulltext matching is supported, false otherwise. + */ + bool SupportsEpgFulltextMatch() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_FULLTEXT_EPG_MATCH) > 0; } + + /*! + * @brief Check whether this type supports a first day the timer is active. + * @return True if first day is supported, false otherwise. + */ + bool SupportsFirstDay() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY) > 0; } + + /*! + * @brief Check whether this type supports weekdays for timer schedules. + * @return True if weekdays are supported, false otherwise. + */ + bool SupportsWeekdays() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS) > 0; } + + /*! + * @brief Check whether this type supports the "record only new episodes" feature. + * @return True if the "record only new episodes" feature is supported, false otherwise. + */ + bool SupportsRecordOnlyNewEpisodes() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_RECORD_ONLY_NEW_EPISODES) > 0; } + + /*! + * @brief Check whether this type supports pre record time. + * @return True if pre record time is supported, false otherwise. + */ + bool SupportsStartMargin() const + { + return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_MARGIN) > 0 || + (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_END_MARGIN) > 0; + } + + /*! + * @brief Check whether this type supports post record time. + * @return True if post record time is supported, false otherwise. + */ + bool SupportsEndMargin() const + { + return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_END_MARGIN) > 0 || + (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_END_MARGIN) > 0; + } + + /*! + * @brief Check whether this type supports recording priorities. + * @return True if recording priority is supported, false otherwise. + */ + bool SupportsPriority() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_PRIORITY) > 0; } + + /*! + * @brief Check whether this type supports lifetime for recordings. + * @return True if recording lifetime is supported, false otherwise. + */ + bool SupportsLifetime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_LIFETIME) > 0; } + + /*! + * @brief Check whether this type supports MaxRecordings for recordings. + * @return True if MaxRecordings is supported, false otherwise. + */ + bool SupportsMaxRecordings() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_MAX_RECORDINGS) > 0; } + + /*! + * @brief Check whether this type supports user specified recording folders. + * @return True if recording folders are supported, false otherwise. + */ + bool SupportsRecordingFolders() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS) > 0; } + + /*! + * @brief Check whether this type supports recording groups. + * @return True if recording groups are supported, false otherwise. + */ + bool SupportsRecordingGroup() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_RECORDING_GROUP) > 0; } + + /*! + * @brief Check whether this type supports 'any channel', for example for defining a timer rule that should match any channel instead of a particular channel. + * @return True if any channel is supported, false otherwise. + */ + bool SupportsAnyChannel() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_ANY_CHANNEL) > 0; } + + /*! + * @brief Check whether this type supports deletion of an otherwise read-only timer. + * @return True if read-only deletion is supported, false otherwise. + */ + bool SupportsReadOnlyDelete() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_READONLY_DELETE) > 0; } + + /*! + * @brief Obtain a list with all possible values for the priority attribute. + * @param list out, the list with the values or an empty list, if priority is not supported by this type. + */ + void GetPriorityValues(std::vector<std::pair<std::string, int>>& list) const; + + /*! + * @brief Obtain the default value for the priority attribute. + * @return the default value. + */ + int GetPriorityDefault() const { return m_iPriorityDefault; } + + /*! + * @brief Obtain a list with all possible values for the lifetime attribute. + * @param list out, the list with the values or an empty list, if lifetime is not supported by this type. + */ + void GetLifetimeValues(std::vector<std::pair<std::string, int>>& list) const; + + /*! + * @brief Obtain the default value for the lifetime attribute. + * @return the default value. + */ + int GetLifetimeDefault() const { return m_iLifetimeDefault; } + + /*! + * @brief Obtain a list with all possible values for the MaxRecordings attribute. + * @param list out, the list with the values or an empty list, if MaxRecordings is not supported by this type. + */ + void GetMaxRecordingsValues(std::vector<std::pair<std::string, int>>& list) const; + + /*! + * @brief Obtain the default value for the MaxRecordings attribute. + * @return the default value. + */ + int GetMaxRecordingsDefault() const { return m_iMaxRecordingsDefault; } + + /*! + * @brief Obtain a list with all possible values for the duplicate episode prevention attribute. + * @param list out, the list with the values or an empty list, if duplicate episode prevention is not supported by this type. + */ + void GetPreventDuplicateEpisodesValues(std::vector<std::pair<std::string, int>>& list) const; + + /*! + * @brief Obtain the default value for the duplicate episode prevention attribute. + * @return the default value. + */ + int GetPreventDuplicateEpisodesDefault() const { return m_iPreventDupEpisodesDefault; } + + /*! + * @brief Obtain a list with all possible values for the recording group attribute. + * @param list out, the list with the values or an empty list, if recording group is not supported by this type. + */ + void GetRecordingGroupValues(std::vector<std::pair<std::string, int>>& list) const; + + /*! + * @brief Obtain the default value for the Recording Group attribute. + * @return the default value. + */ + int GetRecordingGroupDefault() const { return m_iRecordingGroupDefault; } + + private: + void InitDescription(); + void InitAttributeValues(const PVR_TIMER_TYPE& type); + void InitPriorityValues(const PVR_TIMER_TYPE& type); + void InitLifetimeValues(const PVR_TIMER_TYPE& type); + void InitMaxRecordingsValues(const PVR_TIMER_TYPE& type); + void InitPreventDuplicateEpisodesValues(const PVR_TIMER_TYPE& type); + void InitRecordingGroupValues(const PVR_TIMER_TYPE& type); + + int m_iClientId = -1; + unsigned int m_iTypeId; + uint64_t m_iAttributes; + std::string m_strDescription; + std::vector< std::pair<std::string, int> > m_priorityValues; + int m_iPriorityDefault = DEFAULT_RECORDING_PRIORITY; + std::vector< std::pair<std::string, int> > m_lifetimeValues; + int m_iLifetimeDefault = DEFAULT_RECORDING_LIFETIME; + std::vector< std::pair<std::string, int> > m_maxRecordingsValues; + int m_iMaxRecordingsDefault = 0; + std::vector< std::pair<std::string, int> > m_preventDupEpisodesValues; + unsigned int m_iPreventDupEpisodesDefault = DEFAULT_RECORDING_DUPLICATEHANDLING; + std::vector< std::pair<std::string, int> > m_recordingGroupValues; + unsigned int m_iRecordingGroupDefault = 0; + }; +} diff --git a/xbmc/pvr/timers/PVRTimers.cpp b/xbmc/pvr/timers/PVRTimers.cpp new file mode 100644 index 0000000..760bb19 --- /dev/null +++ b/xbmc/pvr/timers/PVRTimers.cpp @@ -0,0 +1,1338 @@ +/* + * 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 "PVRTimers.h" + +#include "ServiceBroker.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVREventLogJob.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/epg/Epg.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimerRuleMatcher.h" +#include "settings/Settings.h" +#include "utils/log.h" + +#include <algorithm> +#include <iterator> +#include <map> +#include <memory> +#include <mutex> +#include <numeric> +#include <string> +#include <utility> +#include <vector> + +using namespace PVR; +using namespace std::chrono_literals; + +namespace +{ +constexpr auto MAX_NOTIFICATION_DELAY = 10s; +} + +bool CPVRTimersContainer::UpdateFromClient(const std::shared_ptr<CPVRTimerInfoTag>& timer) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + std::shared_ptr<CPVRTimerInfoTag> tag = GetByClient(timer->ClientID(), timer->ClientIndex()); + if (tag) + { + return tag->UpdateEntry(timer); + } + else + { + timer->SetTimerID(++m_iLastId); + InsertEntry(timer); + } + + return true; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimersContainer::GetByClient(int iClientId, + int iClientIndex) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& startDates : m_tags) + { + const auto it = std::find_if(startDates.second.cbegin(), startDates.second.cend(), + [iClientId, iClientIndex](const auto& timer) { + return timer->ClientID() == iClientId && + timer->ClientIndex() == iClientIndex; + }); + if (it != startDates.second.cend()) + return (*it); + } + + return {}; +} + +void CPVRTimersContainer::InsertEntry(const std::shared_ptr<CPVRTimerInfoTag>& newTimer) +{ + auto it = m_tags.find(newTimer->IsStartAnyTime() ? CDateTime() : newTimer->StartAsUTC()); + if (it == m_tags.end()) + { + VecTimerInfoTag addEntry({newTimer}); + m_tags.insert(std::make_pair(newTimer->IsStartAnyTime() ? CDateTime() : newTimer->StartAsUTC(), + addEntry)); + } + else + { + it->second.emplace_back(newTimer); + } +} + +CPVRTimers::CPVRTimers() + : CThread("PVRTimers"), + m_settings({CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUP, + CSettings::SETTING_PVRPOWERMANAGEMENT_PREWAKEUP, + CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME, + CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME, + CSettings::SETTING_PVRRECORD_TIMERNOTIFICATIONS}) +{ +} + +bool CPVRTimers::Update(const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + return LoadFromDatabase(clients) && UpdateFromClients(clients); +} + +bool CPVRTimers::LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + // load local timers from database + const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase(); + if (database) + { + const std::vector<std::shared_ptr<CPVRTimerInfoTag>> timers = + database->GetTimers(*this, clients); + + if (std::accumulate(timers.cbegin(), timers.cend(), false, + [this](bool changed, const auto& timer) { + return (UpdateEntry(timer) != nullptr) ? true : changed; + })) + NotifyTimersEvent(); + } + + // ensure that every timer has its channel set + UpdateChannels(); + return true; +} + +void CPVRTimers::Unload() +{ + // remove all tags + std::unique_lock<CCriticalSection> lock(m_critSection); + m_tags.clear(); +} + +void CPVRTimers::Start() +{ + Stop(); + + CServiceBroker::GetPVRManager().Events().Subscribe(this, &CPVRTimers::Notify); + Create(); +} + +void CPVRTimers::Stop() +{ + StopThread(); + CServiceBroker::GetPVRManager().Events().Unsubscribe(this); +} + +bool CPVRTimers::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) +{ + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_bIsUpdating) + return false; + m_bIsUpdating = true; + } + + CLog::LogFC(LOGDEBUG, LOGPVR, "Updating timers"); + CPVRTimersContainer newTimerList; + std::vector<int> failedClients; + CServiceBroker::GetPVRManager().Clients()->GetTimers(clients, &newTimerList, failedClients); + return UpdateEntries(newTimerList, failedClients); +} + +void CPVRTimers::Process() +{ + while (!m_bStop) + { + // update all timers not owned by a client (e.g. reminders) + UpdateEntries(MAX_NOTIFICATION_DELAY.count()); + + CThread::Sleep(MAX_NOTIFICATION_DELAY); + } +} + +bool CPVRTimers::IsRecording() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (const auto& tagsEntry : m_tags) + { + if (std::any_of(tagsEntry.second.cbegin(), tagsEntry.second.cend(), + [](const auto& timersEntry) { return timersEntry->IsRecording(); })) + return true; + } + + return false; +} + +void CPVRTimers::RemoveEntry(const std::shared_ptr<CPVRTimerInfoTag>& tag) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + auto it = m_tags.find(tag->IsStartAnyTime() ? CDateTime() : tag->StartAsUTC()); + if (it != m_tags.end()) + { + it->second.erase(std::remove_if(it->second.begin(), it->second.end(), + [&tag](const std::shared_ptr<CPVRTimerInfoTag>& timer) { + return tag->ClientID() == timer->ClientID() && + tag->ClientIndex() == timer->ClientIndex(); + }), + it->second.end()); + + if (it->second.empty()) + m_tags.erase(it); + } +} + +bool CPVRTimers::CheckAndAppendTimerNotification( + std::vector<std::pair<int, std::string>>& timerNotifications, + const std::shared_ptr<CPVRTimerInfoTag>& tag, + bool bDeleted) const +{ + // no notification on first update or if previous update failed for tag's client. + if (!m_bFirstUpdate && std::find(m_failedClients.cbegin(), m_failedClients.cend(), + tag->ClientID()) == m_failedClients.cend()) + { + const std::string strMessage = + bDeleted ? tag->GetDeletedNotificationText() : tag->GetNotificationText(); + timerNotifications.emplace_back(std::make_pair(tag->ClientID(), strMessage)); + return true; + } + return false; +} + +bool CPVRTimers::UpdateEntries(const CPVRTimersContainer& timers, + const std::vector<int>& failedClients) +{ + bool bChanged(false); + bool bAddedOrDeleted(false); + std::vector<std::pair<int, std::string>> timerNotifications; + + std::unique_lock<CCriticalSection> lock(m_critSection); + + /* go through the timer list and check for updated or new timers */ + for (const auto& tagsEntry : timers.GetTags()) + { + for (const auto& timersEntry : tagsEntry.second) + { + /* check if this timer is present in this container */ + const std::shared_ptr<CPVRTimerInfoTag> existingTimer = + GetByClient(timersEntry->ClientID(), timersEntry->ClientIndex()); + if (existingTimer) + { + /* if it's present, update the current tag */ + bool bStateChanged(existingTimer->State() != timersEntry->State()); + if (existingTimer->UpdateEntry(timersEntry)) + { + bChanged = true; + existingTimer->ResetChildState(); + + if (bStateChanged) + CheckAndAppendTimerNotification(timerNotifications, existingTimer, false); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Updated timer {} on client {}", timersEntry->ClientIndex(), + timersEntry->ClientID()); + } + } + else + { + /* new timer */ + std::shared_ptr<CPVRTimerInfoTag> newTimer = + std::shared_ptr<CPVRTimerInfoTag>(new CPVRTimerInfoTag); + newTimer->UpdateEntry(timersEntry); + newTimer->SetTimerID(++m_iLastId); + InsertEntry(newTimer); + + bChanged = true; + bAddedOrDeleted = true; + + CheckAndAppendTimerNotification(timerNotifications, newTimer, false); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Added timer {} on client {}", timersEntry->ClientIndex(), + timersEntry->ClientID()); + } + } + } + + /* to collect timer with changed starting time */ + VecTimerInfoTag timersToMove; + + /* check for deleted timers */ + for (auto it = m_tags.begin(); it != m_tags.end();) + { + for (auto it2 = it->second.begin(); it2 != it->second.end();) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = *it2; + if (!timers.GetByClient(timer->ClientID(), timer->ClientIndex())) + { + /* timer was not found */ + bool bIgnoreTimer = !timer->IsOwnedByClient(); + if (!bIgnoreTimer) + { + bIgnoreTimer = std::any_of( + failedClients.cbegin(), failedClients.cend(), + [&timer](const auto& failedClient) { return failedClient == timer->ClientID(); }); + } + + if (bIgnoreTimer) + { + ++it2; + continue; + } + + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleted timer {} on client {}", timer->ClientIndex(), + timer->ClientID()); + + CheckAndAppendTimerNotification(timerNotifications, timer, true); + + it2 = it->second.erase(it2); + + bChanged = true; + bAddedOrDeleted = true; + } + else if ((timer->IsStartAnyTime() && it->first != CDateTime()) || + (!timer->IsStartAnyTime() && timer->StartAsUTC() != it->first)) + { + /* timer start has changed */ + CLog::LogFC(LOGDEBUG, LOGPVR, "Changed start time timer {} on client {}", + timer->ClientIndex(), timer->ClientID()); + + /* remember timer */ + timersToMove.push_back(timer); + + /* remove timer for now, reinsert later */ + it2 = it->second.erase(it2); + + bChanged = true; + bAddedOrDeleted = true; + } + else + { + ++it2; + } + } + if (it->second.empty()) + it = m_tags.erase(it); + else + ++it; + } + + /* reinsert timers with changed timer start */ + for (const auto& timer : timersToMove) + { + InsertEntry(timer); + } + + /* update child information for all parent timers */ + for (const auto& tagsEntry : m_tags) + { + for (const auto& timersEntry : tagsEntry.second) + { + if (timersEntry->IsTimerRule()) + timersEntry->ResetChildState(); + } + + for (const auto& timersEntry : tagsEntry.second) + { + const std::shared_ptr<CPVRTimerInfoTag> parentTimer = GetTimerRule(timersEntry); + if (parentTimer) + parentTimer->UpdateChildState(timersEntry, true); + } + } + + m_failedClients = failedClients; + m_bFirstUpdate = false; + m_bIsUpdating = false; + + if (bChanged) + { + UpdateChannels(); + lock.unlock(); + + NotifyTimersEvent(bAddedOrDeleted); + + if (!timerNotifications.empty()) + { + CPVREventLogJob* job = new CPVREventLogJob; + + /* queue notifications / fill eventlog */ + for (const auto& entry : timerNotifications) + { + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(entry.first); + if (client) + { + job->AddEvent(m_settings.GetBoolValue(CSettings::SETTING_PVRRECORD_TIMERNOTIFICATIONS), + EventLevel::Information, // info, no error + client->GetFriendlyName(), entry.second, client->Icon()); + } + } + + CServiceBroker::GetJobManager()->AddJob(job, nullptr); + } + } + + return true; +} + +namespace +{ +std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTagsForTimerRule( + const CPVRTimerRuleMatcher& matcher) +{ + std::vector<std::shared_ptr<CPVREpgInfoTag>> matches; + + const std::shared_ptr<CPVRChannel> channel = matcher.GetChannel(); + if (channel) + { + // match single channel + const std::shared_ptr<CPVREpg> epg = channel->GetEPG(); + if (epg) + { + const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags = epg->GetTags(); + std::copy_if(tags.cbegin(), tags.cend(), std::back_inserter(matches), + [&matcher](const auto& tag) { return matcher.Matches(tag); }); + } + } + else + { + // match any channel + const std::vector<std::shared_ptr<CPVREpg>> epgs = + CServiceBroker::GetPVRManager().EpgContainer().GetAllEpgs(); + + for (const auto& epg : epgs) + { + const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags = epg->GetTags(); + std::copy_if(tags.cbegin(), tags.cend(), std::back_inserter(matches), + [&matcher](const auto& tag) { return matcher.Matches(tag); }); + } + } + + return matches; +} + +void AddTimerRuleToEpgMap( + const std::shared_ptr<CPVRTimerInfoTag>& timer, + const CDateTime& now, + std::map<std::shared_ptr<CPVREpg>, std::vector<std::shared_ptr<CPVRTimerRuleMatcher>>>& epgMap, + bool& bFetchedAllEpgs) +{ + const std::shared_ptr<CPVRChannel> channel = timer->Channel(); + if (channel) + { + const std::shared_ptr<CPVREpg> epg = channel->GetEPG(); + if (epg) + { + const std::shared_ptr<CPVRTimerRuleMatcher> matcher = + std::make_shared<CPVRTimerRuleMatcher>(timer, now); + auto it = epgMap.find(epg); + if (it == epgMap.end()) + epgMap.insert({epg, {matcher}}); + else + it->second.emplace_back(matcher); + } + } + else + { + // rule matches "any channel" => we need to check all channels + if (!bFetchedAllEpgs) + { + const std::vector<std::shared_ptr<CPVREpg>> epgs = + CServiceBroker::GetPVRManager().EpgContainer().GetAllEpgs(); + for (const auto& epg : epgs) + { + const std::shared_ptr<CPVRTimerRuleMatcher> matcher = + std::make_shared<CPVRTimerRuleMatcher>(timer, now); + auto it = epgMap.find(epg); + if (it == epgMap.end()) + epgMap.insert({epg, {matcher}}); + else + it->second.emplace_back(matcher); + } + bFetchedAllEpgs = true; + } + else + { + for (auto& epgMapEntry : epgMap) + { + const std::shared_ptr<CPVRTimerRuleMatcher> matcher = + std::make_shared<CPVRTimerRuleMatcher>(timer, now); + epgMapEntry.second.emplace_back(matcher); + } + } + } +} +} // unnamed namespace + +bool CPVRTimers::UpdateEntries(int iMaxNotificationDelay) +{ + std::vector<std::shared_ptr<CPVRTimerInfoTag>> timersToReinsert; + std::vector<std::pair<std::shared_ptr<CPVRTimerInfoTag>, std::shared_ptr<CPVRTimerInfoTag>>> + childTimersToInsert; + bool bChanged = false; + const CDateTime now = CDateTime::GetUTCDateTime(); + bool bFetchedAllEpgs = false; + std::map<std::shared_ptr<CPVREpg>, std::vector<std::shared_ptr<CPVRTimerRuleMatcher>>> epgMap; + + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (auto it = m_tags.begin(); it != m_tags.end();) + { + for (auto it2 = it->second.begin(); it2 != it->second.end();) + { + std::shared_ptr<CPVRTimerInfoTag> timer = *it2; + bool bDeleteTimer = false; + if (!timer->IsOwnedByClient()) + { + if (timer->IsEpgBased()) + { + // update epg tag + const std::shared_ptr<CPVREpg> epg = + CServiceBroker::GetPVRManager().EpgContainer().GetByChannelUid( + timer->Channel()->ClientID(), timer->Channel()->UniqueID()); + if (epg) + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + epg->GetTagByBroadcastId(timer->UniqueBroadcastID()); + if (epgTag) + { + timer->SetEpgInfoTag(epgTag); + + bool bStartChanged = + !timer->IsStartAnyTime() && epgTag->StartAsUTC() != timer->StartAsUTC(); + bool bEndChanged = !timer->IsEndAnyTime() && epgTag->EndAsUTC() != timer->EndAsUTC(); + if (bStartChanged || bEndChanged) + { + if (bStartChanged) + timer->SetStartFromUTC(epgTag->StartAsUTC()); + if (bEndChanged) + timer->SetEndFromUTC(epgTag->EndAsUTC()); + + timer->UpdateSummary(); + bChanged = true; + + if (bStartChanged) + { + // start time changed. timer must be reinserted in timer map + bDeleteTimer = true; + timersToReinsert.emplace_back(timer); // remember and reinsert/save later + } + else + { + // save changes to database + timer->Persist(); + } + } + } + } + } + + // check for due timers and announce/delete them + int iMarginStart = timer->GetTimerType()->SupportsStartMargin() ? timer->MarginStart() : 0; + if (!timer->IsTimerRule() && + (timer->StartAsUTC() - CDateTimeSpan(0, 0, iMarginStart, iMaxNotificationDelay)) < now) + { + if (timer->IsReminder() && !timer->IsDisabled()) + { + // reminder is due / over due. announce it. + m_remindersToAnnounce.push(timer); + } + + if (timer->EndAsUTC() >= now) + { + // disable timer until timer's end time is due + if (!timer->IsDisabled()) + { + timer->SetState(PVR_TIMER_STATE_DISABLED); + bChanged = true; + } + } + else + { + // end time due. delete completed timer + bChanged = true; + bDeleteTimer = true; + timer->DeleteFromDatabase(); + } + } + + if (timer->IsTimerRule() && timer->IsReminder() && timer->IsActive()) + { + if (timer->IsEpgBased()) + { + if (m_bReminderRulesUpdatePending) + AddTimerRuleToEpgMap(timer, now, epgMap, bFetchedAllEpgs); + } + else + { + // create new children of time-based reminder timer rules + const CPVRTimerRuleMatcher matcher(timer, now); + const CDateTime nextStart = matcher.GetNextTimerStart(); + if (nextStart.IsValid()) + { + bool bCreate = false; + const auto it1 = m_tags.find(nextStart); + if (it1 == m_tags.end()) + bCreate = true; + else + bCreate = std::none_of(it1->second.cbegin(), it1->second.cend(), + [&timer](const std::shared_ptr<CPVRTimerInfoTag>& tmr) { + return tmr->ParentClientIndex() == timer->ClientIndex(); + }); + if (bCreate) + { + const CDateTimeSpan duration = timer->EndAsUTC() - timer->StartAsUTC(); + const std::shared_ptr<CPVRTimerInfoTag> childTimer = + CPVRTimerInfoTag::CreateReminderFromDate( + nextStart, duration.GetSecondsTotal() / 60, timer); + if (childTimer) + { + bChanged = true; + childTimersToInsert.emplace_back( + std::make_pair(timer, childTimer)); // remember and insert/save later + } + } + } + } + } + } + + if (bDeleteTimer) + { + const std::shared_ptr<CPVRTimerInfoTag> parent = GetTimerRule(timer); + if (parent) + parent->UpdateChildState(timer, false); + + it2 = it->second.erase(it2); + } + else + { + ++it2; + } + } + + if (it->second.empty()) + it = m_tags.erase(it); + else + ++it; + } + + // create new children of local epg-based reminder timer rules + for (const auto& epgMapEntry : epgMap) + { + const auto epgTags = epgMapEntry.first->GetTags(); + for (const auto& epgTag : epgTags) + { + if (GetTimerForEpgTag(epgTag)) + continue; + + for (const auto& matcher : epgMapEntry.second) + { + if (!matcher->Matches(epgTag)) + continue; + + const std::shared_ptr<CPVRTimerInfoTag> childTimer = + CPVRTimerInfoTag::CreateReminderFromEpg(epgTag, matcher->GetTimerRule()); + if (childTimer) + { + bChanged = true; + childTimersToInsert.emplace_back(std::make_pair( + matcher->GetTimerRule(), childTimer)); // remember and insert/save later + } + } + } + } + + // reinsert timers with changed timer start + for (const auto& timer : timersToReinsert) + { + InsertEntry(timer); + timer->Persist(); + } + + // insert new children of time-based local timer rules + for (const auto& timerPair : childTimersToInsert) + { + PersistAndUpdateLocalTimer(timerPair.second, timerPair.first); + } + + m_bReminderRulesUpdatePending = false; + + // announce changes + if (bChanged) + NotifyTimersEvent(); + + if (!m_remindersToAnnounce.empty()) + CServiceBroker::GetPVRManager().PublishEvent(PVREvent::AnnounceReminder); + + return bChanged; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextReminderToAnnnounce() +{ + std::shared_ptr<CPVRTimerInfoTag> ret; + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_remindersToAnnounce.empty()) + { + ret = m_remindersToAnnounce.front(); + m_remindersToAnnounce.pop(); + } + return ret; +} + +bool CPVRTimers::KindMatchesTag(const TimerKind& eKind, + const std::shared_ptr<CPVRTimerInfoTag>& tag) const +{ + return (eKind == TimerKindAny) || (eKind == TimerKindTV && !tag->IsRadio()) || + (eKind == TimerKindRadio && tag->IsRadio()); +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextActiveTimer(const TimerKind& eKind, + bool bIgnoreReminders) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (const auto& tagsEntry : m_tags) + { + for (const auto& timersEntry : tagsEntry.second) + { + if (bIgnoreReminders && timersEntry->IsReminder()) + continue; + + if (KindMatchesTag(eKind, timersEntry) && timersEntry->IsActive() && + !timersEntry->IsRecording() && !timersEntry->IsTimerRule() && !timersEntry->IsBroken()) + return timersEntry; + } + } + + return {}; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextActiveTimer( + bool bIgnoreReminders /* = true */) const +{ + return GetNextActiveTimer(TimerKindAny, bIgnoreReminders); +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextActiveTVTimer() const +{ + return GetNextActiveTimer(TimerKindTV, true); +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextActiveRadioTimer() const +{ + return GetNextActiveTimer(TimerKindRadio, true); +} + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveTimers() const +{ + std::vector<std::shared_ptr<CPVRTimerInfoTag>> tags; + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (const auto& tagsEntry : m_tags) + { + std::copy_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(), std::back_inserter(tags), + [](const auto& timersEntry) { + return timersEntry->IsActive() && !timersEntry->IsBroken() && + !timersEntry->IsReminder() && !timersEntry->IsTimerRule(); + }); + } + + return tags; +} + +int CPVRTimers::AmountActiveTimers(const TimerKind& eKind) const +{ + int iReturn = 0; + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (const auto& tagsEntry : m_tags) + { + iReturn += std::count_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(), + [this, &eKind](const auto& timersEntry) { + return KindMatchesTag(eKind, timersEntry) && + timersEntry->IsActive() && !timersEntry->IsBroken() && + !timersEntry->IsReminder() && !timersEntry->IsTimerRule(); + }); + } + + return iReturn; +} + +int CPVRTimers::AmountActiveTimers() const +{ + return AmountActiveTimers(TimerKindAny); +} + +int CPVRTimers::AmountActiveTVTimers() const +{ + return AmountActiveTimers(TimerKindTV); +} + +int CPVRTimers::AmountActiveRadioTimers() const +{ + return AmountActiveTimers(TimerKindRadio); +} + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveRecordings( + const TimerKind& eKind) const +{ + std::vector<std::shared_ptr<CPVRTimerInfoTag>> tags; + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (const auto& tagsEntry : m_tags) + { + std::copy_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(), std::back_inserter(tags), + [this, &eKind](const auto& timersEntry) { + return KindMatchesTag(eKind, timersEntry) && timersEntry->IsRecording() && + !timersEntry->IsTimerRule() && !timersEntry->IsBroken() && + !timersEntry->IsReminder(); + }); + } + + return tags; +} + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveRecordings() const +{ + return GetActiveRecordings(TimerKindAny); +} + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveTVRecordings() const +{ + return GetActiveRecordings(TimerKindTV); +} + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveRadioRecordings() const +{ + return GetActiveRecordings(TimerKindRadio); +} + +int CPVRTimers::AmountActiveRecordings(const TimerKind& eKind) const +{ + int iReturn = 0; + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (const auto& tagsEntry : m_tags) + { + iReturn += std::count_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(), + [this, &eKind](const auto& timersEntry) { + return KindMatchesTag(eKind, timersEntry) && + timersEntry->IsRecording() && !timersEntry->IsTimerRule() && + !timersEntry->IsBroken() && !timersEntry->IsReminder(); + }); + } + + return iReturn; +} + +int CPVRTimers::AmountActiveRecordings() const +{ + return AmountActiveRecordings(TimerKindAny); +} + +int CPVRTimers::AmountActiveTVRecordings() const +{ + return AmountActiveRecordings(TimerKindTV); +} + +int CPVRTimers::AmountActiveRadioRecordings() const +{ + return AmountActiveRecordings(TimerKindRadio); +} + +/********** channel methods **********/ + +bool CPVRTimers::DeleteTimersOnChannel(const std::shared_ptr<CPVRChannel>& channel, + bool bDeleteTimerRules /* = true */, + bool bCurrentlyActiveOnly /* = false */) +{ + bool bReturn = false; + bool bChanged = false; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (MapTags::reverse_iterator it = m_tags.rbegin(); it != m_tags.rend(); ++it) + { + for (const auto& timersEntry : (*it).second) + { + bool bDeleteActiveItem = !bCurrentlyActiveOnly || timersEntry->IsRecording(); + bool bDeleteTimerRuleItem = bDeleteTimerRules || !timersEntry->IsTimerRule(); + bool bChannelsMatch = timersEntry->HasChannel() && timersEntry->Channel() == channel; + + if (bDeleteActiveItem && bDeleteTimerRuleItem && bChannelsMatch) + { + CLog::LogFC(LOGDEBUG, LOGPVR, "Deleted timer {} on client {}", timersEntry->ClientIndex(), + timersEntry->ClientID()); + bReturn = (timersEntry->DeleteFromClient(true) == TimerOperationResult::OK) || bReturn; + bChanged = true; + } + } + } + } + + if (bChanged) + NotifyTimersEvent(); + + return bReturn; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::UpdateEntry( + const std::shared_ptr<CPVRTimerInfoTag>& timer) +{ + bool bChanged = false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + std::shared_ptr<CPVRTimerInfoTag> tag = GetByClient(timer->ClientID(), timer->ClientIndex()); + if (tag) + { + bool bReinsert = tag->StartAsUTC() != timer->StartAsUTC(); + if (bReinsert) + { + RemoveEntry(tag); + } + + bChanged = tag->UpdateEntry(timer); + + if (bReinsert) + { + InsertEntry(tag); + } + } + else + { + tag.reset(new CPVRTimerInfoTag()); + if (tag->UpdateEntry(timer)) + { + tag->SetTimerID(++m_iLastId); + InsertEntry(tag); + bChanged = true; + } + } + + return bChanged ? tag : std::shared_ptr<CPVRTimerInfoTag>(); +} + +bool CPVRTimers::AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag) +{ + bool bReturn = false; + if (tag->IsOwnedByClient()) + { + bReturn = tag->AddToClient(); + } + else + { + bReturn = AddLocalTimer(tag, true); + } + return bReturn; +} + +TimerOperationResult CPVRTimers::DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, + bool bForce /* = false */, + bool bDeleteRule /* = false */) +{ + TimerOperationResult ret = TimerOperationResult::FAILED; + if (!tag) + return ret; + + std::shared_ptr<CPVRTimerInfoTag> tagToDelete = tag; + + if (bDeleteRule) + { + /* delete the timer rule that scheduled this timer. */ + const std::shared_ptr<CPVRTimerInfoTag> ruleTag = GetTimerRule(tagToDelete); + if (!ruleTag) + { + CLog::LogF(LOGERROR, "Unable to obtain timer rule for given timer"); + return ret; + } + + tagToDelete = ruleTag; + } + + if (tagToDelete->IsOwnedByClient()) + { + ret = tagToDelete->DeleteFromClient(bForce); + } + else + { + if (DeleteLocalTimer(tagToDelete, true)) + ret = TimerOperationResult::OK; + } + + return ret; +} + +bool CPVRTimers::UpdateTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag) +{ + bool bReturn = false; + if (tag->IsOwnedByClient()) + { + bReturn = tag->UpdateOnClient(); + } + else + { + bReturn = UpdateLocalTimer(tag); + } + return bReturn; +} + +bool CPVRTimers::AddLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, bool bNotify) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + const std::shared_ptr<CPVRTimerInfoTag> persistedTimer = PersistAndUpdateLocalTimer(tag, nullptr); + bool bReturn = !!persistedTimer; + + if (bReturn && persistedTimer->IsTimerRule() && persistedTimer->IsActive()) + { + if (persistedTimer->IsEpgBased()) + { + // create and persist children of local epg-based timer rule + const std::vector<std::shared_ptr<CPVREpgInfoTag>> epgTags = + GetEpgTagsForTimerRule(CPVRTimerRuleMatcher(persistedTimer, CDateTime::GetUTCDateTime())); + for (const auto& epgTag : epgTags) + { + const std::shared_ptr<CPVRTimerInfoTag> childTimer = + CPVRTimerInfoTag::CreateReminderFromEpg(epgTag, persistedTimer); + if (childTimer) + { + PersistAndUpdateLocalTimer(childTimer, persistedTimer); + } + } + } + else + { + // create and persist children of local time-based timer rule + const CDateTime nextStart = + CPVRTimerRuleMatcher(persistedTimer, CDateTime::GetUTCDateTime()).GetNextTimerStart(); + if (nextStart.IsValid()) + { + const CDateTimeSpan duration = persistedTimer->EndAsUTC() - persistedTimer->StartAsUTC(); + const std::shared_ptr<CPVRTimerInfoTag> childTimer = + CPVRTimerInfoTag::CreateReminderFromDate(nextStart, duration.GetSecondsTotal() / 60, + persistedTimer); + if (childTimer) + { + PersistAndUpdateLocalTimer(childTimer, persistedTimer); + } + } + } + } + + if (bNotify && bReturn) + { + lock.unlock(); + NotifyTimersEvent(); + } + + return bReturn; +} + +bool CPVRTimers::DeleteLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, bool bNotify) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + RemoveEntry(tag); + + bool bReturn = tag->DeleteFromDatabase(); + + if (bReturn && tag->IsTimerRule()) + { + // delete children of local timer rule + for (auto it = m_tags.begin(); it != m_tags.end();) + { + for (auto it2 = it->second.begin(); it2 != it->second.end();) + { + std::shared_ptr<CPVRTimerInfoTag> timer = *it2; + if (timer->ParentClientIndex() == tag->ClientIndex()) + { + tag->UpdateChildState(timer, false); + it2 = it->second.erase(it2); + timer->DeleteFromDatabase(); + } + else + { + ++it2; + } + } + + if (it->second.empty()) + it = m_tags.erase(it); + else + ++it; + } + } + + if (bNotify && bReturn) + { + lock.unlock(); + NotifyTimersEvent(); + } + + return bReturn; +} + +bool CPVRTimers::UpdateLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag) +{ + // delete and re-create timer and children, if any. + bool bReturn = DeleteLocalTimer(tag, false); + + if (bReturn) + bReturn = AddLocalTimer(tag, false); + + if (bReturn) + NotifyTimersEvent(); + + return bReturn; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::PersistAndUpdateLocalTimer( + const std::shared_ptr<CPVRTimerInfoTag>& timer, + const std::shared_ptr<CPVRTimerInfoTag>& parentTimer) +{ + std::shared_ptr<CPVRTimerInfoTag> tag; + bool bReturn = timer->Persist(); + if (bReturn) + { + tag = UpdateEntry(timer); + if (tag && parentTimer) + parentTimer->UpdateChildState(timer, true); + } + return bReturn ? tag : std::shared_ptr<CPVRTimerInfoTag>(); +} + +bool CPVRTimers::IsRecordingOnChannel(const CPVRChannel& channel) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (const auto& tagsEntry : m_tags) + { + if (std::any_of(tagsEntry.second.cbegin(), tagsEntry.second.cend(), + [&channel](const auto& timersEntry) { + return timersEntry->IsRecording() && + timersEntry->ClientChannelUID() == channel.UniqueID() && + timersEntry->ClientID() == channel.ClientID(); + })) + return true; + } + + return false; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetActiveTimerForChannel( + const std::shared_ptr<CPVRChannel>& channel) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& tagsEntry : m_tags) + { + const auto it = std::find_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(), + [&channel](const auto& timersEntry) { + return timersEntry->IsRecording() && + timersEntry->ClientChannelUID() == channel->UniqueID() && + timersEntry->ClientID() == channel->ClientID(); + }); + if (it != tagsEntry.second.cend()) + return (*it); + } + + return {}; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetTimerForEpgTag( + const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + if (epgTag) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (const auto& tagsEntry : m_tags) + { + for (const auto& timersEntry : tagsEntry.second) + { + if (timersEntry->IsTimerRule()) + continue; + + if (timersEntry->GetEpgInfoTag(false) == epgTag) + return timersEntry; + + if (timersEntry->ClientChannelUID() != PVR_CHANNEL_INVALID_UID && + timersEntry->ClientChannelUID() == epgTag->UniqueChannelID() && + timersEntry->ClientID() == epgTag->ClientID()) + { + if (timersEntry->UniqueBroadcastID() != EPG_TAG_INVALID_UID && + timersEntry->UniqueBroadcastID() == epgTag->UniqueBroadcastID()) + return timersEntry; + + if (timersEntry->IsRadio() == epgTag->IsRadio() && + timersEntry->StartAsUTC() <= epgTag->StartAsUTC() && + timersEntry->EndAsUTC() >= epgTag->EndAsUTC()) + return timersEntry; + } + } + } + } + + return {}; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetTimerRule( + const std::shared_ptr<CPVRTimerInfoTag>& timer) const +{ + if (timer) + { + const int iParentClientIndex = timer->ParentClientIndex(); + if (iParentClientIndex != PVR_TIMER_NO_PARENT) + { + int iClientId = timer->ClientID(); + + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& tagsEntry : m_tags) + { + const auto it = std::find_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(), + [iClientId, iParentClientIndex](const auto& timersEntry) { + return timersEntry->ClientID() == iClientId && + timersEntry->ClientIndex() == iParentClientIndex; + }); + if (it != tagsEntry.second.cend()) + return (*it); + } + } + } + + return {}; +} + +void CPVRTimers::Notify(const PVREvent& event) +{ + switch (static_cast<PVREvent>(event)) + { + case PVREvent::EpgContainer: + CServiceBroker::GetPVRManager().TriggerTimersUpdate(); + break; + case PVREvent::Epg: + case PVREvent::EpgItemUpdate: + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bReminderRulesUpdatePending = true; + break; + } + default: + break; + } +} + +CDateTime CPVRTimers::GetNextEventTime() const +{ + const bool dailywakup = + m_settings.GetBoolValue(CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUP); + const CDateTime now = CDateTime::GetUTCDateTime(); + const CDateTimeSpan prewakeup( + 0, 0, m_settings.GetIntValue(CSettings::SETTING_PVRPOWERMANAGEMENT_PREWAKEUP), 0); + const CDateTimeSpan idle( + 0, 0, m_settings.GetIntValue(CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME), 0); + + CDateTime wakeuptime; + + /* Check next active time */ + const std::shared_ptr<CPVRTimerInfoTag> timer = GetNextActiveTimer(false); + if (timer) + { + const CDateTimeSpan prestart(0, 0, timer->MarginStart(), 0); + const CDateTime start = timer->StartAsUTC(); + wakeuptime = + ((start - prestart - prewakeup - idle) > now) ? start - prestart - prewakeup : now + idle; + } + + /* check daily wake up */ + if (dailywakup) + { + CDateTime dailywakeuptime; + dailywakeuptime.SetFromDBTime( + m_settings.GetStringValue(CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME)); + dailywakeuptime = dailywakeuptime.GetAsUTCDateTime(); + + dailywakeuptime.SetDateTime(now.GetYear(), now.GetMonth(), now.GetDay(), + dailywakeuptime.GetHour(), dailywakeuptime.GetMinute(), + dailywakeuptime.GetSecond()); + + if ((dailywakeuptime - idle) < now) + { + const CDateTimeSpan oneDay(1, 0, 0, 0); + dailywakeuptime += oneDay; + } + if (!wakeuptime.IsValid() || dailywakeuptime < wakeuptime) + wakeuptime = dailywakeuptime; + } + + const CDateTime retVal(wakeuptime); + return retVal; +} + +void CPVRTimers::UpdateChannels() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& tagsEntry : m_tags) + { + for (const auto& timersEntry : tagsEntry.second) + timersEntry->UpdateChannel(); + } +} + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetAll() const +{ + std::vector<std::shared_ptr<CPVRTimerInfoTag>> timers; + + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& tagsEntry : m_tags) + { + std::copy(tagsEntry.second.cbegin(), tagsEntry.second.cend(), std::back_inserter(timers)); + } + + return timers; +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetById(unsigned int iTimerId) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + for (const auto& tagsEntry : m_tags) + { + const auto it = std::find_if( + tagsEntry.second.cbegin(), tagsEntry.second.cend(), + [iTimerId](const auto& timersEntry) { return timersEntry->TimerID() == iTimerId; }); + if (it != tagsEntry.second.cend()) + return (*it); + } + + return {}; +} + +void CPVRTimers::NotifyTimersEvent(bool bAddedOrDeleted /* = true */) +{ + CServiceBroker::GetPVRManager().PublishEvent(bAddedOrDeleted ? PVREvent::TimersInvalidated + : PVREvent::Timers); +} diff --git a/xbmc/pvr/timers/PVRTimers.h b/xbmc/pvr/timers/PVRTimers.h new file mode 100644 index 0000000..6b0e4e7 --- /dev/null +++ b/xbmc/pvr/timers/PVRTimers.h @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/settings/PVRSettings.h" +#include "threads/Thread.h" + +#include <map> +#include <memory> +#include <queue> +#include <vector> + +class CDateTime; + +namespace PVR +{ +enum class TimerOperationResult; +enum class PVREvent; + +class CPVRChannel; +class CPVRClient; +class CPVREpgInfoTag; +class CPVRTimerInfoTag; +class CPVRTimersPath; + +class CPVRTimersContainer +{ +public: + /*! + * @brief Add a timer tag to this container or update the tag if already present in this + * container. + * @param The timer tag + * @return True, if the update was successful. False, otherwise. + */ + bool UpdateFromClient(const std::shared_ptr<CPVRTimerInfoTag>& timer); + + /*! + * @brief Get the timer tag denoted by given client id and timer id. + * @param iClientId The client id. + * @param iClientIndex The timer id. + * @return the timer tag if found, null otherwise. + */ + std::shared_ptr<CPVRTimerInfoTag> GetByClient(int iClientId, int iClientIndex) const; + + typedef std::vector<std::shared_ptr<CPVRTimerInfoTag>> VecTimerInfoTag; + typedef std::map<CDateTime, VecTimerInfoTag> MapTags; + + /*! + * @brief Get the timertags map. + * @return The map. + */ + const MapTags& GetTags() const { return m_tags; } + +protected: + void InsertEntry(const std::shared_ptr<CPVRTimerInfoTag>& newTimer); + + mutable CCriticalSection m_critSection; + unsigned int m_iLastId = 0; + MapTags m_tags; +}; + +class CPVRTimers : public CPVRTimersContainer, private CThread +{ +public: + CPVRTimers(); + ~CPVRTimers() override = default; + + /*! + * @brief start the timer update thread. + */ + void Start(); + + /*! + * @brief stop the timer update thread. + */ + void Stop(); + + /*! + * @brief Update all timers from PVR database and from given clients. + * @param clients The PVR clients data should be loaded for. Leave empty for all clients. + * @return True on success, false otherwise. + */ + bool Update(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + /*! + * @brief unload all timers. + */ + void Unload(); + + /*! + * @brief Update data with recordings from the given clients, sync with local data. + * @param clients The clients to fetch data from. Leave empty to fetch data from all created + * clients. + * @return True on success, false otherwise. + */ + bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + /*! + * @param bIgnoreReminders include or ignore reminders + * @return The tv or radio timer that will be active next (state scheduled), or nullptr if none. + */ + std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer(bool bIgnoreReminders = true) const; + + /*! + * @return The tv timer that will be active next (state scheduled), or nullptr if none. + */ + std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTVTimer() const; + + /*! + * @return The radio timer that will be active next (state scheduled), or nullptr if none. + */ + std::shared_ptr<CPVRTimerInfoTag> GetNextActiveRadioTimer() const; + + /*! + * @return All timers that are active (states scheduled or recording) + */ + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveTimers() const; + + /*! + * @return Next due reminder, if any. Removes it from the queue of due reminders. + */ + std::shared_ptr<CPVRTimerInfoTag> GetNextReminderToAnnnounce(); + + /*! + * Get all timers + * @return The list of all timers + */ + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetAll() const; + + /*! + * @return The amount of tv and radio timers that are active (states scheduled or recording) + */ + int AmountActiveTimers() const; + + /*! + * @return The amount of tv timers that are active (states scheduled or recording) + */ + int AmountActiveTVTimers() const; + + /*! + * @return The amount of radio timers that are active (states scheduled or recording) + */ + int AmountActiveRadioTimers() const; + + /*! + * @return All tv and radio timers that are recording + */ + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() const; + + /*! + * @return All tv timers that are recording + */ + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveTVRecordings() const; + + /*! + * @return All radio timers that are recording + */ + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRadioRecordings() const; + + /*! + * @return True when recording, false otherwise. + */ + bool IsRecording() const; + + /*! + * @brief Check if a recording is running on the given channel. + * @param channel The channel to check. + * @return True when recording, false otherwise. + */ + bool IsRecordingOnChannel(const CPVRChannel& channel) const; + + /*! + * @brief Obtain the active timer for a given channel. + * @param channel The channel to check. + * @return the timer, null otherwise. + */ + std::shared_ptr<CPVRTimerInfoTag> GetActiveTimerForChannel( + const std::shared_ptr<CPVRChannel>& channel) const; + + /*! + * @return The amount of tv and radio timers that are currently recording + */ + int AmountActiveRecordings() const; + + /*! + * @return The amount of tv timers that are currently recording + */ + int AmountActiveTVRecordings() const; + + /*! + * @return The amount of radio timers that are currently recording + */ + int AmountActiveRadioRecordings() const; + + /*! + * @brief Delete all timers on a channel. + * @param channel The channel to delete the timers for. + * @param bDeleteTimerRules True to delete timer rules too, false otherwise. + * @param bCurrentlyActiveOnly True to delete timers that are currently running only. + * @return True if timers any were deleted, false otherwise. + */ + bool DeleteTimersOnChannel(const std::shared_ptr<CPVRChannel>& channel, + bool bDeleteTimerRules = true, + bool bCurrentlyActiveOnly = false); + + /*! + * @return Next event time (timer or daily wake up) + */ + CDateTime GetNextEventTime() const; + + /*! + * @brief Add a timer to the client. Doesn't add the timer to the container. The backend will do + * this. + * @param tag The timer to add. + * @return True if timer add request was sent correctly, false if not. + */ + bool AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag); + + /*! + * @brief Delete a timer on the client. Doesn't delete the timer from the container. The backend + * will do this. + * @param tag The timer to delete. + * @param bForce Control what to do in case the timer is currently recording. + * True to force to delete the timer, false to return TimerDeleteResult::RECORDING. + * @param bDeleteRule Also delete the timer rule that scheduled the timer instead of single timer + * only. + * @return The result. + */ + TimerOperationResult DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, + bool bForce = false, + bool bDeleteRule = false); + + /*! + * @brief Update the timer on the client. Doesn't update the timer in the container. The backend + * will do this. + * @param tag The timer to update. + * @return True if timer update request was sent correctly, false if not. + */ + bool UpdateTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag); + + /*! + * @brief Get the timer tag that matches the given epg tag. + * @param epgTag The epg tag. + * @return The requested timer tag, or nullptr if none was found. + */ + std::shared_ptr<CPVRTimerInfoTag> GetTimerForEpgTag( + const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + + /*! + * @brief Get the timer rule for a given timer tag + * @param timer The timer to query the timer rule for + * @return The timer rule, or nullptr if none was found. + */ + std::shared_ptr<CPVRTimerInfoTag> GetTimerRule( + const std::shared_ptr<CPVRTimerInfoTag>& timer) const; + + /*! + * @brief Update the channel pointers. + */ + void UpdateChannels(); + + /*! + * @brief CEventStream callback for PVR events. + * @param event The event. + */ + void Notify(const PVREvent& event); + + /*! + * @brief Get a timer tag given it's unique ID + * @param iTimerId The ID to find + * @return The tag, or an empty one when not found + */ + std::shared_ptr<CPVRTimerInfoTag> GetById(unsigned int iTimerId) const; + +private: + void Process() override; + + /*! + * @brief Load all timers from PVR database. + * @param clients The PVR clients data should be loaded for. Leave empty for all clients. + * @return True on success, false otherwise. + */ + bool LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients); + + void RemoveEntry(const std::shared_ptr<CPVRTimerInfoTag>& tag); + bool UpdateEntries(const CPVRTimersContainer& timers, const std::vector<int>& failedClients); + bool UpdateEntries(int iMaxNotificationDelay); + std::shared_ptr<CPVRTimerInfoTag> UpdateEntry(const std::shared_ptr<CPVRTimerInfoTag>& timer); + + bool AddLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, bool bNotify); + bool DeleteLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, bool bNotify); + bool UpdateLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag); + std::shared_ptr<CPVRTimerInfoTag> PersistAndUpdateLocalTimer( + const std::shared_ptr<CPVRTimerInfoTag>& timer, + const std::shared_ptr<CPVRTimerInfoTag>& parentTimer); + void NotifyTimersEvent(bool bAddedOrDeleted = true); + + enum TimerKind + { + TimerKindAny = 0, + TimerKindTV, + TimerKindRadio + }; + + bool KindMatchesTag(const TimerKind& eKind, const std::shared_ptr<CPVRTimerInfoTag>& tag) const; + + std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer(const TimerKind& eKind, + bool bIgnoreReminders) const; + int AmountActiveTimers(const TimerKind& eKind) const; + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings(const TimerKind& eKind) const; + int AmountActiveRecordings(const TimerKind& eKind) const; + + bool CheckAndAppendTimerNotification(std::vector<std::pair<int, std::string>>& timerNotifications, + const std::shared_ptr<CPVRTimerInfoTag>& tag, + bool bDeleted) const; + + bool m_bIsUpdating = false; + CPVRSettings m_settings; + std::queue<std::shared_ptr<CPVRTimerInfoTag>> m_remindersToAnnounce; + bool m_bReminderRulesUpdatePending = false; + + bool m_bFirstUpdate = true; + std::vector<int> m_failedClients; +}; +} // namespace PVR diff --git a/xbmc/pvr/timers/PVRTimersPath.cpp b/xbmc/pvr/timers/PVRTimersPath.cpp new file mode 100644 index 0000000..ea2265c --- /dev/null +++ b/xbmc/pvr/timers/PVRTimersPath.cpp @@ -0,0 +1,82 @@ +/* + * 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 "PVRTimersPath.h" + +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +#include <cstdlib> +#include <string> +#include <vector> + +using namespace PVR; + +const std::string CPVRTimersPath::PATH_ADDTIMER = "pvr://timers/addtimer/"; +const std::string CPVRTimersPath::PATH_NEW = "pvr://timers/new/"; +const std::string CPVRTimersPath::PATH_TV_TIMERS = "pvr://timers/tv/timers/"; +const std::string CPVRTimersPath::PATH_RADIO_TIMERS = "pvr://timers/radio/timers/"; +const std::string CPVRTimersPath::PATH_TV_TIMER_RULES = "pvr://timers/tv/rules/"; +const std::string CPVRTimersPath::PATH_RADIO_TIMER_RULES = "pvr://timers/radio/rules/"; + +CPVRTimersPath::CPVRTimersPath(const std::string& strPath) +{ + Init(strPath); +} + +CPVRTimersPath::CPVRTimersPath(const std::string& strPath, int iClientId, int iParentId) +{ + if (Init(strPath)) + { + // set/replace client and parent id. + m_path = StringUtils::Format("pvr://timers/{}/{}/{}/{}", m_bRadio ? "radio" : "tv", + m_bTimerRules ? "rules" : "timers", iClientId, iParentId); + m_iClientId = iClientId; + m_iParentId = iParentId; + m_bRoot = false; + } +} + +CPVRTimersPath::CPVRTimersPath(bool bRadio, bool bTimerRules) + : m_path(StringUtils::Format( + "pvr://timers/{}/{}", bRadio ? "radio" : "tv", bTimerRules ? "rules" : "timers")), + m_bValid(true), + m_bRoot(true), + m_bRadio(bRadio), + m_bTimerRules(bTimerRules) +{ +} + +bool CPVRTimersPath::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() == 4) || (segments.size() == 6)) && (segments.at(1) == "timers") && + ((segments.at(2) == "radio") || (segments.at(2) == "tv")) && + ((segments.at(3) == "rules") || (segments.at(3) == "timers"))); + m_bRoot = (m_bValid && (segments.size() == 4)); + m_bRadio = (m_bValid && (segments.at(2) == "radio")); + m_bTimerRules = (m_bValid && (segments.at(3) == "rules")); + + if (!m_bValid || m_bRoot) + { + m_iClientId = -1; + m_iParentId = 0; + } + else + { + m_iClientId = std::stoi(segments.at(4)); + m_iParentId = std::stoi(segments.at(5)); + } + + return m_bValid; +} diff --git a/xbmc/pvr/timers/PVRTimersPath.h b/xbmc/pvr/timers/PVRTimersPath.h new file mode 100644 index 0000000..01fd5f0 --- /dev/null +++ b/xbmc/pvr/timers/PVRTimersPath.h @@ -0,0 +1,50 @@ +/* + * 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 <string> + +namespace PVR +{ +class CPVRTimersPath +{ +public: + static const std::string PATH_ADDTIMER; + static const std::string PATH_NEW; + static const std::string PATH_TV_TIMERS; + static const std::string PATH_TV_TIMER_RULES; + static const std::string PATH_RADIO_TIMERS; + static const std::string PATH_RADIO_TIMER_RULES; + + explicit CPVRTimersPath(const std::string& strPath); + CPVRTimersPath(const std::string& strPath, int iClientId, int iParentId); + CPVRTimersPath(bool bRadio, bool bTimerRules); + + bool IsValid() const { return m_bValid; } + + const std::string& GetPath() const { return m_path; } + bool IsTimersRoot() const { return m_bRoot; } + bool IsTimerRule() const { return !IsTimersRoot(); } + bool IsRadio() const { return m_bRadio; } + bool IsRules() const { return m_bTimerRules; } + int GetClientId() const { return m_iClientId; } + int GetParentId() const { return m_iParentId; } + +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_bTimerRules = false; + int m_iClientId = -1; + int m_iParentId = 0; +}; +} // namespace PVR diff --git a/xbmc/pvr/windows/CMakeLists.txt b/xbmc/pvr/windows/CMakeLists.txt new file mode 100644 index 0000000..9f5097e --- /dev/null +++ b/xbmc/pvr/windows/CMakeLists.txt @@ -0,0 +1,21 @@ +set(SOURCES GUIViewStatePVR.cpp + GUIWindowPVRBase.cpp + GUIWindowPVRChannels.cpp + GUIWindowPVRGuide.cpp + GUIWindowPVRRecordings.cpp + GUIWindowPVRSearch.cpp + GUIWindowPVRTimers.cpp + GUIWindowPVRTimersBase.cpp + GUIWindowPVRTimerRules.cpp) + +set(HEADERS GUIViewStatePVR.h + GUIWindowPVRBase.h + GUIWindowPVRChannels.h + GUIWindowPVRGuide.h + GUIWindowPVRRecordings.h + GUIWindowPVRSearch.h + GUIWindowPVRTimerRules.h + GUIWindowPVRTimers.h + GUIWindowPVRTimersBase.h) + +core_add_library(pvr_windows) diff --git a/xbmc/pvr/windows/GUIViewStatePVR.cpp b/xbmc/pvr/windows/GUIViewStatePVR.cpp new file mode 100644 index 0000000..d86f6fc --- /dev/null +++ b/xbmc/pvr/windows/GUIViewStatePVR.cpp @@ -0,0 +1,182 @@ +/* + * 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 "GUIViewStatePVR.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/epg/EpgSearchPath.h" +#include "pvr/recordings/PVRRecordingsPath.h" +#include "pvr/timers/PVRTimersPath.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "view/ViewStateSettings.h" + +using namespace PVR; + +CGUIViewStateWindowPVRChannels::CGUIViewStateWindowPVRChannels(const int windowId, + const CFileItemList& items) + : CGUIViewStatePVR(windowId, items) +{ + AddSortMethod(SortByChannelNumber, 549, // "Number" + LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty + AddSortMethod(SortByChannel, 551, // "Name" + LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty + AddSortMethod( + SortByLastPlayed, 568, // "Last played" + LABEL_MASKS("%L", "%p", "%L", "%p")); // Filename, LastPlayed | Foldername, LastPlayed + AddSortMethod(SortByClientChannelOrder, 19315, // "Backend number" + LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty + AddSortMethod(SortByProvider, 19348, // "Provider" + LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty + + // Default sorting + SetSortMethod(SortByChannelNumber); + + LoadViewState("pvr://channels/", m_windowId); +} + +void CGUIViewStateWindowPVRChannels::SaveViewState() +{ + SaveViewToDb("pvr://channels/", m_windowId, CViewStateSettings::GetInstance().Get("pvrchannels")); +} + +CGUIViewStateWindowPVRRecordings::CGUIViewStateWindowPVRRecordings(const int windowId, + const CFileItemList& items) + : CGUIViewStatePVR(windowId, items) +{ + AddSortMethod(SortByLabel, 551, // "Name" + LABEL_MASKS("%L", "%d", "%L", ""), // Filename, DateTime | Foldername, empty + CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) + ? SortAttributeIgnoreArticle + : SortAttributeNone); + AddSortMethod(SortByDate, 552, // "Date" + LABEL_MASKS("%L", "%d", "%L", "%d")); // Filename, DateTime | Foldername, DateTime + AddSortMethod(SortByTime, 180, // "Duration" + LABEL_MASKS("%L", "%D", "%L", "")); // Filename, Duration | Foldername, empty + AddSortMethod(SortByFile, 561, // "File" + LABEL_MASKS("%L", "%d", "%L", "")); // Filename, DateTime | Foldername, empty + + if (CServiceBroker::GetPVRManager().Clients()->AnyClientSupportingRecordingsSize()) + { + // "Size" : Filename, Size | Foldername, Size + AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I")); + } + + AddSortMethod(SortByEpisodeNumber, 20359, // "Episode" + LABEL_MASKS("%L", "%d", "%L", "")); // Filename, DateTime | Foldername, empty + AddSortMethod(SortByProvider, 19348, // "Provider" + LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty + + SetSortMethod( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_PVRDefaultSortOrder); + + LoadViewState(items.GetPath(), m_windowId); +} + +void CGUIViewStateWindowPVRRecordings::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), m_windowId, + CViewStateSettings::GetInstance().Get("pvrrecordings")); +} + +bool CGUIViewStateWindowPVRRecordings::HideParentDirItems() +{ + return (CGUIViewState::HideParentDirItems() || + CPVRRecordingsPath(m_items.GetPath()).IsRecordingsRoot()); +} + +CGUIViewStateWindowPVRGuide::CGUIViewStateWindowPVRGuide(const int windowId, + const CFileItemList& items) + : CGUIViewStatePVR(windowId, items) +{ + AddSortMethod(SortByChannelNumber, 549, // "Number" + LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty + AddSortMethod(SortByChannel, 551, // "Name" + LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty + AddSortMethod( + SortByLastPlayed, SortAttributeIgnoreLabel, 568, // "Last played" + LABEL_MASKS("%L", "%p", "%L", "%p")); // Filename, LastPlayed | Foldername, LastPlayed + AddSortMethod(SortByClientChannelOrder, 19315, // "Backend number" + LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty + AddSortMethod(SortByProvider, 19348, // "Provider" + LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty + + // Default sorting + SetSortMethod(SortByChannelNumber); + + LoadViewState("pvr://guide/", m_windowId); +} + +void CGUIViewStateWindowPVRGuide::SaveViewState() +{ + SaveViewToDb("pvr://guide/", m_windowId, CViewStateSettings::GetInstance().Get("pvrguide")); +} + +CGUIViewStateWindowPVRTimers::CGUIViewStateWindowPVRTimers(const int windowId, + const CFileItemList& items) + : CGUIViewStatePVR(windowId, items) +{ + int sortAttributes(CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) + ? SortAttributeIgnoreArticle + : SortAttributeNone); + sortAttributes |= SortAttributeIgnoreFolders; + AddSortMethod(SortByLabel, static_cast<SortAttribute>(sortAttributes), 551, // "Name" + LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty + AddSortMethod(SortByDate, static_cast<SortAttribute>(sortAttributes), 552, // "Date" + LABEL_MASKS("%L", "%d", "%L", "%d")); // Filename, DateTime | Foldername, DateTime + + // Default sorting + SetSortMethod(SortByDate); + + LoadViewState("pvr://timers/", m_windowId); +} + +void CGUIViewStateWindowPVRTimers::SaveViewState() +{ + SaveViewToDb("pvr://timers/", m_windowId, CViewStateSettings::GetInstance().Get("pvrtimers")); +} + +bool CGUIViewStateWindowPVRTimers::HideParentDirItems() +{ + return (CGUIViewState::HideParentDirItems() || CPVRTimersPath(m_items.GetPath()).IsTimersRoot()); +} + +CGUIViewStateWindowPVRSearch::CGUIViewStateWindowPVRSearch(const int windowId, + const CFileItemList& items) + : CGUIViewStatePVR(windowId, items) +{ + AddSortMethod(SortByLabel, 551, // "Name" + LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty + AddSortMethod(SortByDate, 552, // "Date" + LABEL_MASKS("%L", "%d", "%L", "%d")); // Filename, DateTime | Foldername, DateTime + + // Default sorting + if (CPVREpgSearchPath(m_items.GetPath()).IsSavedSearchesRoot()) + SetSortMethod(SortByDate, SortOrderDescending); + else + SetSortMethod(SortByDate, SortOrderAscending); + + LoadViewState(m_items.GetPath(), m_windowId); +} + +void CGUIViewStateWindowPVRSearch::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), m_windowId, CViewStateSettings::GetInstance().Get("pvrsearch")); +} + +bool CGUIViewStateWindowPVRSearch::HideParentDirItems() +{ + return (CGUIViewState::HideParentDirItems() || + CPVREpgSearchPath(m_items.GetPath()).IsSearchRoot()); +} diff --git a/xbmc/pvr/windows/GUIViewStatePVR.h b/xbmc/pvr/windows/GUIViewStatePVR.h new file mode 100644 index 0000000..ce39dd7 --- /dev/null +++ b/xbmc/pvr/windows/GUIViewStatePVR.h @@ -0,0 +1,78 @@ +/* + * 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 "view/GUIViewState.h" + +class CFileItemList; + +namespace PVR +{ +class CGUIViewStatePVR : public CGUIViewState +{ +public: + CGUIViewStatePVR(const int windowId, const CFileItemList& items) : CGUIViewState(items) + { + m_windowId = windowId; + } + +protected: + bool HideParentDirItems() override { return true; } + + int m_windowId; +}; + +class CGUIViewStateWindowPVRChannels : public CGUIViewStatePVR +{ +public: + CGUIViewStateWindowPVRChannels(const int windowId, const CFileItemList& items); + +protected: + void SaveViewState() override; +}; + +class CGUIViewStateWindowPVRRecordings : public CGUIViewStatePVR +{ +public: + CGUIViewStateWindowPVRRecordings(const int windowId, const CFileItemList& items); + +protected: + void SaveViewState() override; + bool HideParentDirItems() override; +}; + +class CGUIViewStateWindowPVRGuide : public CGUIViewStatePVR +{ +public: + CGUIViewStateWindowPVRGuide(const int windowId, const CFileItemList& items); + +protected: + void SaveViewState() override; +}; + +class CGUIViewStateWindowPVRTimers : public CGUIViewStatePVR +{ +public: + CGUIViewStateWindowPVRTimers(const int windowId, const CFileItemList& items); + +protected: + void SaveViewState() override; + bool HideParentDirItems() override; +}; + +class CGUIViewStateWindowPVRSearch : public CGUIViewStatePVR +{ +public: + CGUIViewStateWindowPVRSearch(const int windowId, const CFileItemList& items); + +protected: + void SaveViewState() override; + bool HideParentDirItems() override; +}; +} // namespace PVR diff --git a/xbmc/pvr/windows/GUIWindowPVRBase.cpp b/xbmc/pvr/windows/GUIWindowPVRBase.cpp new file mode 100644 index 0000000..96a14b5 --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRBase.cpp @@ -0,0 +1,563 @@ +/* + * 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 "GUIWindowPVRBase.h" + +#include "FileItem.h" +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "addons/AddonManager.h" +#include "addons/addoninfo/AddonType.h" +#include "dialogs/GUIDialogExtendedProgressBar.h" +#include "dialogs/GUIDialogSelect.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIMessage.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/filesystem/PVRGUIDirectory.h" +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <iterator> +#include <memory> +#include <mutex> +#include <string> +#include <utility> +#include <vector> + +using namespace std::chrono_literals; + +#define MAX_INVALIDATION_FREQUENCY 2000ms // limit to one invalidation per X milliseconds + +using namespace PVR; +using namespace KODI::MESSAGING; + +namespace PVR +{ + +class CGUIPVRChannelGroupsSelector +{ +public: + virtual ~CGUIPVRChannelGroupsSelector() = default; + + bool Initialize(CGUIWindow* parent, bool bRadio); + + bool HasFocus() const; + std::shared_ptr<CPVRChannelGroup> GetSelectedChannelGroup() const; + bool SelectChannelGroup(const std::shared_ptr<CPVRChannelGroup>& newGroup); + +private: + CGUIControl* m_control = nullptr; + std::vector<std::shared_ptr<CPVRChannelGroup>> m_channelGroups; +}; + +} // namespace PVR + +bool CGUIPVRChannelGroupsSelector::Initialize(CGUIWindow* parent, bool bRadio) +{ + CGUIControl* control = parent->GetControl(CONTROL_LSTCHANNELGROUPS); + if (control && control->IsContainer()) + { + m_control = control; + m_channelGroups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio)->GetMembers(true); + + CFileItemList channelGroupItems; + CPVRGUIDirectory::GetChannelGroupsDirectory(bRadio, true, channelGroupItems); + + CGUIMessage msg(GUI_MSG_LABEL_BIND, m_control->GetID(), CONTROL_LSTCHANNELGROUPS, 0, 0, &channelGroupItems); + m_control->OnMessage(msg); + return true; + } + return false; +} + +bool CGUIPVRChannelGroupsSelector::HasFocus() const +{ + return m_control && m_control->HasFocus(); +} + +std::shared_ptr<CPVRChannelGroup> CGUIPVRChannelGroupsSelector::GetSelectedChannelGroup() const +{ + if (m_control) + { + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, m_control->GetID(), CONTROL_LSTCHANNELGROUPS); + m_control->OnMessage(msg); + + const auto it = std::next(m_channelGroups.begin(), msg.GetParam1()); + if (it != m_channelGroups.end()) + { + return *it; + } + } + return std::shared_ptr<CPVRChannelGroup>(); +} + +bool CGUIPVRChannelGroupsSelector::SelectChannelGroup(const std::shared_ptr<CPVRChannelGroup>& newGroup) +{ + if (m_control && newGroup) + { + int iIndex = 0; + for (const auto& group : m_channelGroups) + { + if (*newGroup == *group) + { + CGUIMessage msg(GUI_MSG_ITEM_SELECT, m_control->GetID(), CONTROL_LSTCHANNELGROUPS, iIndex); + m_control->OnMessage(msg); + return true; + } + ++iIndex; + } + } + return false; +} + +CGUIWindowPVRBase::CGUIWindowPVRBase(bool bRadio, int id, const std::string& xmlFile) : + CGUIMediaWindow(id, xmlFile.c_str()), + m_bRadio(bRadio), + m_channelGroupsSelector(new CGUIPVRChannelGroupsSelector), + m_progressHandle(nullptr) +{ + // prevent removable drives to appear in directory listing (base class default behavior). + m_rootDir.AllowNonLocalSources(false); + + CServiceBroker::GetPVRManager().Events().Subscribe(this, &CGUIWindowPVRBase::Notify); +} + +CGUIWindowPVRBase::~CGUIWindowPVRBase() +{ + if (m_channelGroup) + m_channelGroup->Events().Unsubscribe(this); + + CServiceBroker::GetPVRManager().Events().Unsubscribe(this); +} + +void CGUIWindowPVRBase::UpdateSelectedItemPath() +{ + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().SetSelectedChannelPath( + m_bRadio, m_viewControl.GetSelectedItemPath()); +} + +void CGUIWindowPVRBase::Notify(const PVREvent& event) +{ + // call virtual event handler function + NotifyEvent(event); +} + +void CGUIWindowPVRBase::NotifyEvent(const PVREvent& event) +{ + if (event == PVREvent::ManagerStopped) + { + ClearData(); + } + else if (m_active) + { + if (event == PVREvent::SystemSleep) + { + CGUIMessage m(GUI_MSG_SYSTEM_SLEEP, GetID(), 0, static_cast<int>(event)); + CServiceBroker::GetAppMessenger()->SendGUIMessage(m); + } + else if (event == PVREvent::SystemWake) + { + CGUIMessage m(GUI_MSG_SYSTEM_WAKE, GetID(), 0, static_cast<int>(event)); + CServiceBroker::GetAppMessenger()->SendGUIMessage(m); + } + else + { + CGUIMessage m(GUI_MSG_REFRESH_LIST, GetID(), 0, static_cast<int>(event)); + CServiceBroker::GetAppMessenger()->SendGUIMessage(m); + } + } +} + +bool CGUIWindowPVRBase::OnAction(const CAction& action) +{ + switch (action.GetID()) + { + case ACTION_PREVIOUS_CHANNELGROUP: + ActivatePreviousChannelGroup(); + return true; + + case ACTION_NEXT_CHANNELGROUP: + ActivateNextChannelGroup(); + return true; + + case ACTION_MOVE_RIGHT: + case ACTION_MOVE_LEFT: + { + if (m_channelGroupsSelector->HasFocus() && CGUIMediaWindow::OnAction(action)) + { + SetChannelGroup(m_channelGroupsSelector->GetSelectedChannelGroup()); + return true; + } + } + } + + return CGUIMediaWindow::OnAction(action); +} + +bool CGUIWindowPVRBase::ActivatePreviousChannelGroup() +{ + const std::shared_ptr<CPVRChannelGroup> channelGroup = GetChannelGroup(); + if (channelGroup) + { + const CPVRChannelGroups* groups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(channelGroup->IsRadio()); + if (groups) + { + SetChannelGroup(groups->GetPreviousGroup(*channelGroup)); + return true; + } + } + return false; +} + +bool CGUIWindowPVRBase::ActivateNextChannelGroup() +{ + const std::shared_ptr<CPVRChannelGroup> channelGroup = GetChannelGroup(); + if (channelGroup) + { + const CPVRChannelGroups* groups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(channelGroup->IsRadio()); + if (groups) + { + SetChannelGroup(groups->GetNextGroup(*channelGroup)); + return true; + } + } + return false; +} + +void CGUIWindowPVRBase::ClearData() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_channelGroup.reset(); + m_channelGroupsSelector.reset(new CGUIPVRChannelGroupsSelector); +} + +void CGUIWindowPVRBase::OnInitWindow() +{ + SetProperty("IsRadio", m_bRadio ? "true" : ""); + + if (InitChannelGroup()) + { + m_channelGroupsSelector->Initialize(this, m_bRadio); + + CGUIMediaWindow::OnInitWindow(); + + // mark item as selected by channel path + m_viewControl.SetSelectedItem( + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetSelectedChannelPath(m_bRadio)); + + // This has to be done after base class OnInitWindow to restore correct selection + m_channelGroupsSelector->SelectChannelGroup(GetChannelGroup()); + } + else + { + CGUIWindow::OnInitWindow(); // do not call CGUIMediaWindow as it will do a Refresh which in no case works in this state (no channelgroup!) + ShowProgressDialog(g_localizeStrings.Get(19235), 0); // PVR manager is starting up + } +} + +void CGUIWindowPVRBase::OnDeinitWindow(int nextWindowID) +{ + HideProgressDialog(); + UpdateSelectedItemPath(); + CGUIMediaWindow::OnDeinitWindow(nextWindowID); +} + +bool CGUIWindowPVRBase::OnMessage(CGUIMessage& message) +{ + bool bReturn = false; + switch (message.GetMessage()) + { + case GUI_MSG_CLICKED: + { + switch (message.GetSenderId()) + { + case CONTROL_BTNCHANNELGROUPS: + return OpenChannelGroupSelectionDialog(); + + case CONTROL_LSTCHANNELGROUPS: + { + switch (message.GetParam1()) + { + case ACTION_SELECT_ITEM: + case ACTION_MOUSE_LEFT_CLICK: + { + SetChannelGroup(m_channelGroupsSelector->GetSelectedChannelGroup()); + bReturn = true; + break; + } + } + } + } + break; + } + + case GUI_MSG_REFRESH_LIST: + { + switch (static_cast<PVREvent>(message.GetParam1())) + { + case PVREvent::ManagerStarted: + case PVREvent::ClientsInvalidated: + case PVREvent::ChannelGroupsInvalidated: + { + if (InitChannelGroup()) + { + m_channelGroupsSelector->Initialize(this, m_bRadio); + m_channelGroupsSelector->SelectChannelGroup(GetChannelGroup()); + HideProgressDialog(); + Refresh(true); + m_viewControl.SetFocused(); + } + break; + } + default: + break; + } + + if (IsActive()) + { + // Only the active window must set the selected item path which is shared + // between all PVR windows, not the last notified window (observer). + UpdateSelectedItemPath(); + } + bReturn = true; + break; + } + + case GUI_MSG_NOTIFY_ALL: + { + switch (message.GetParam1()) + { + case GUI_MSG_UPDATE_SOURCES: + { + // removable drive connected/disconnected. base class triggers a window + // content refresh, which makes no sense for pvr windows. + bReturn = true; + break; + } + } + break; + } + } + + return bReturn || CGUIMediaWindow::OnMessage(message); +} + +void CGUIWindowPVRBase::SetInvalid() +{ + if (m_refreshTimeout.IsTimePast()) + { + for (const auto& item : *m_vecItems) + item->SetInvalid(); + + CGUIMediaWindow::SetInvalid(); + m_refreshTimeout.Set(MAX_INVALIDATION_FREQUENCY); + } +} + +bool CGUIWindowPVRBase::CanBeActivated() const +{ + // check if there is at least one enabled PVR add-on + if (!CServiceBroker::GetAddonMgr().HasAddons(ADDON::AddonType::PVRDLL)) + { + HELPERS::ShowOKDialogText(CVariant{19296}, CVariant{19272}); // No PVR add-on enabled, You need a tuner, backend software... + return false; + } + + return true; +} + +bool CGUIWindowPVRBase::OpenChannelGroupSelectionDialog() +{ + CGUIDialogSelect* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (!dialog) + return false; + + CFileItemList options; + CPVRGUIDirectory::GetChannelGroupsDirectory(m_bRadio, true, options); + + dialog->Reset(); + dialog->SetHeading(CVariant{g_localizeStrings.Get(19146)}); + dialog->SetItems(options); + dialog->SetMultiSelection(false); + if (const std::shared_ptr<CPVRChannelGroup> channelGroup = GetChannelGroup()) + { + dialog->SetSelected(channelGroup->GroupName()); + } + dialog->Open(); + + if (!dialog->IsConfirmed()) + return false; + + const CFileItemPtr item = dialog->GetSelectedFileItem(); + if (!item) + return false; + + SetChannelGroup(CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bRadio)->GetByName(item->m_strTitle)); + + return true; +} + +bool CGUIWindowPVRBase::InitChannelGroup() +{ + if (!CServiceBroker::GetPVRManager().IsStarted()) + return false; + + std::shared_ptr<CPVRChannelGroup> group; + if (m_channelGroupPath.empty()) + { + group = CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(m_bRadio); + } + else + { + group = CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bRadio)->GetGroupByPath(m_channelGroupPath); + if (group) + CServiceBroker::GetPVRManager().PlaybackState()->SetActiveChannelGroup(group); + else + CLog::LogF(LOGERROR, "Found no {} channel group with path '{}'!", m_bRadio ? "radio" : "TV", + m_channelGroupPath); + + m_channelGroupPath.clear(); + } + + if (group) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_channelGroup != group) + { + m_viewControl.SetSelectedItem(0); + SetChannelGroup(std::move(group), false); + } + // Path might have changed since last init. Set it always, not just on group change. + m_vecItems->SetPath(GetDirectoryPath()); + return true; + } + return false; +} + +std::shared_ptr<CPVRChannelGroup> CGUIWindowPVRBase::GetChannelGroup() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_channelGroup; +} + +void CGUIWindowPVRBase::SetChannelGroup(std::shared_ptr<CPVRChannelGroup> &&group, bool bUpdate /* = true */) +{ + if (!group) + return; + + std::shared_ptr<CPVRChannelGroup> updateChannelGroup; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_channelGroup != group) + { + if (m_channelGroup) + m_channelGroup->Events().Unsubscribe(this); + m_channelGroup = std::move(group); + // we need to register the window to receive changes from the new group + m_channelGroup->Events().Subscribe(this, &CGUIWindowPVRBase::Notify); + if (bUpdate) + updateChannelGroup = m_channelGroup; + } + } + + if (updateChannelGroup) + { + CServiceBroker::GetPVRManager().PlaybackState()->SetActiveChannelGroup(updateChannelGroup); + Update(GetDirectoryPath()); + } +} + +bool CGUIWindowPVRBase::Update(const std::string& strDirectory, bool updateFilterPath /*= true*/) +{ + if (m_bUpdating) + { + // no concurrent updates + return false; + } + + CUpdateGuard guard(m_bUpdating); + + if (!GetChannelGroup()) + { + // no updates before fully initialized + return false; + } + + int iOldCount = m_vecItems->Size(); + int iSelectedItem = m_viewControl.GetSelectedItem(); + const std::string oldPath = m_vecItems->GetPath(); + + bool bReturn = CGUIMediaWindow::Update(strDirectory, updateFilterPath); + + if (bReturn && + iSelectedItem != -1) // something must have been selected + { + int iNewCount = m_vecItems->Size(); + if (iOldCount > iNewCount && // at least one item removed by Update() + oldPath == m_vecItems->GetPath()) // update not due changing into another folder + { + // restore selected item if we just deleted one or more items. + if (iSelectedItem >= iNewCount) + iSelectedItem = iNewCount - 1; + + m_viewControl.SetSelectedItem(iSelectedItem); + } + } + + return bReturn; +} + +void CGUIWindowPVRBase::UpdateButtons() +{ + CGUIMediaWindow::UpdateButtons(); + + const std::shared_ptr<CPVRChannelGroup> channelGroup = GetChannelGroup(); + if (channelGroup) + { + SET_CONTROL_LABEL(CONTROL_BTNCHANNELGROUPS, g_localizeStrings.Get(19141) + ": " + channelGroup->GroupName()); + } + + m_channelGroupsSelector->SelectChannelGroup(channelGroup); +} + +void CGUIWindowPVRBase::ShowProgressDialog(const std::string& strText, int iProgress) +{ + if (!m_progressHandle) + { + CGUIDialogExtendedProgressBar* loadingProgressDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS); + if (!loadingProgressDialog) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_EXT_PROGRESS!"); + return; + } + m_progressHandle = loadingProgressDialog->GetHandle(g_localizeStrings.Get(19235)); // PVR manager is starting up + } + + m_progressHandle->SetPercentage(static_cast<float>(iProgress)); + m_progressHandle->SetText(strText); +} + +void CGUIWindowPVRBase::HideProgressDialog() +{ + if (m_progressHandle) + { + m_progressHandle->MarkFinished(); + m_progressHandle = nullptr; + } +} diff --git a/xbmc/pvr/windows/GUIWindowPVRBase.h b/xbmc/pvr/windows/GUIWindowPVRBase.h new file mode 100644 index 0000000..1fe27bb --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRBase.h @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "threads/SystemClock.h" +#include "windows/GUIMediaWindow.h" + +#include <atomic> +#include <memory> +#include <string> + +#define CONTROL_BTNVIEWASICONS 2 +#define CONTROL_BTNSORTBY 3 +#define CONTROL_BTNSORTASC 4 +#define CONTROL_BTNGROUPITEMS 5 +#define CONTROL_BTNSHOWHIDDEN 6 +#define CONTROL_BTNSHOWDELETED 7 +#define CONTROL_BTNHIDEDISABLEDTIMERS 8 +#define CONTROL_BTNSHOWMODE 10 +#define CONTROL_LSTCHANNELGROUPS 11 + +#define CONTROL_BTNCHANNELGROUPS 28 +#define CONTROL_BTNFILTERCHANNELS 31 + +#define CONTROL_LABEL_HEADER1 29 +#define CONTROL_LABEL_HEADER2 30 + +class CGUIDialogProgressBarHandle; + +namespace PVR +{ + enum class PVREvent; + + enum EPGSelectAction + { + EPG_SELECT_ACTION_CONTEXT_MENU = 0, + EPG_SELECT_ACTION_SWITCH = 1, + EPG_SELECT_ACTION_INFO = 2, + EPG_SELECT_ACTION_RECORD = 3, + EPG_SELECT_ACTION_PLAY_RECORDING = 4, + EPG_SELECT_ACTION_SMART_SELECT = 5 + }; + + class CPVRChannelGroup; + class CGUIPVRChannelGroupsSelector; + + class CGUIWindowPVRBase : public CGUIMediaWindow + { + public: + ~CGUIWindowPVRBase() override; + + void OnInitWindow() override; + void OnDeinitWindow(int nextWindowID) override; + bool OnMessage(CGUIMessage& message) override; + bool Update(const std::string& strDirectory, bool updateFilterPath = true) override; + void UpdateButtons() override; + bool OnAction(const CAction& action) override; + void SetInvalid() override; + bool CanBeActivated() const override; + + bool UseFileDirectories() override { return false; } + + /*! + * @brief CEventStream callback for PVR events. + * @param event The event. + */ + void Notify(const PVREvent& event); + virtual void NotifyEvent(const PVREvent& event); + + /*! + * @brief Refresh window content. + * @return true, if refresh succeeded, false otherwise. + */ + bool DoRefresh() { return Refresh(true); } + + bool ActivatePreviousChannelGroup(); + bool ActivateNextChannelGroup(); + bool OpenChannelGroupSelectionDialog(); + + protected: + CGUIWindowPVRBase(bool bRadio, int id, const std::string& xmlFile); + + virtual std::string GetDirectoryPath() = 0; + + virtual void ClearData(); + + /*! + * @brief Init this window's channel group with the currently active (the "playing") channel group. + * @return true if group could be set, false otherwise. + */ + bool InitChannelGroup(); + + /*! + * @brief Get the channel group for this window. + * @return the group or null, if no group set. + */ + std::shared_ptr<CPVRChannelGroup> GetChannelGroup(); + + /*! + * @brief Set a new channel group, start listening to this group, optionally update window content. + * @param group The new group. + * @param bUpdate if true, window content will be updated. + */ + void SetChannelGroup(std::shared_ptr<CPVRChannelGroup> &&group, bool bUpdate = true); + + virtual void UpdateSelectedItemPath(); + + CCriticalSection m_critSection; + std::string m_channelGroupPath; + bool m_bRadio; + std::atomic_bool m_bUpdating = {false}; + + private: + /*! + * @brief Show or update the progress dialog. + * @param strText The current status. + * @param iProgress The current progress in %. + */ + void ShowProgressDialog(const std::string& strText, int iProgress); + + /*! + * @brief Hide the progress dialog if it's visible. + */ + void HideProgressDialog(); + + std::unique_ptr<CGUIPVRChannelGroupsSelector> m_channelGroupsSelector; + std::shared_ptr<CPVRChannelGroup> m_channelGroup; + XbmcThreads::EndTime<> m_refreshTimeout; + CGUIDialogProgressBarHandle* m_progressHandle; /*!< progress dialog that is displayed while the pvr manager is loading */ + }; +} diff --git a/xbmc/pvr/windows/GUIWindowPVRChannels.cpp b/xbmc/pvr/windows/GUIWindowPVRChannels.cpp new file mode 100644 index 0000000..2b476f7 --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRChannels.cpp @@ -0,0 +1,414 @@ +/* + * 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 "GUIWindowPVRChannels.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogContextMenu.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIMessage.h" +#include "guilib/GUIRadioButtonControl.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/Key.h" +#include "input/actions/Action.h" +#include "pvr/PVRManager.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/channels/PVRChannelsPath.h" +#include "pvr/dialogs/GUIDialogPVRChannelManager.h" +#include "pvr/dialogs/GUIDialogPVRGroupManager.h" +#include "pvr/epg/Epg.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "pvr/guilib/PVRGUIActionsEPG.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +#include <mutex> +#include <string> + +using namespace PVR; + +CGUIWindowPVRChannelsBase::CGUIWindowPVRChannelsBase(bool bRadio, + int id, + const std::string& xmlFile) + : CGUIWindowPVRBase(bRadio, id, xmlFile), m_bShowHiddenChannels(false) +{ + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().RegisterChannelNumberInputHandler(this); +} + +CGUIWindowPVRChannelsBase::~CGUIWindowPVRChannelsBase() +{ + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().DeregisterChannelNumberInputHandler( + this); +} + +std::string CGUIWindowPVRChannelsBase::GetRootPath() const +{ + //! @todo Would it make sense to change GetRootPath() declaration in CGUIMediaWindow + //! to be non-const to get rid of the const_cast's here? + + CGUIWindowPVRChannelsBase* pThis = const_cast<CGUIWindowPVRChannelsBase*>(this); + if (pThis->InitChannelGroup()) + return pThis->GetDirectoryPath(); + + return CGUIWindowPVRBase::GetRootPath(); +} + +void CGUIWindowPVRChannelsBase::GetContextButtons(int itemNumber, CContextButtons& buttons) +{ + // Add parent buttons before the Manage button + CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons); + + buttons.Add(CONTEXT_BUTTON_EDIT, 16106); /* Manage... */ +} + +bool CGUIWindowPVRChannelsBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + if (itemNumber < 0 || itemNumber >= m_vecItems->Size()) + return false; + + return OnContextButtonManage(m_vecItems->Get(itemNumber), button) || + CGUIMediaWindow::OnContextButton(itemNumber, button); +} + +bool CGUIWindowPVRChannelsBase::Update(const std::string& strDirectory, + bool updateFilterPath /* = true */) +{ + bool bReturn = CGUIWindowPVRBase::Update(strDirectory); + + if (bReturn) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + /* empty list for hidden channels */ + if (m_vecItems->GetObjectCount() == 0 && m_bShowHiddenChannels) + { + /* show the visible channels instead */ + m_bShowHiddenChannels = false; + lock.unlock(); + Update(GetDirectoryPath()); + } + } + return bReturn; +} + +void CGUIWindowPVRChannelsBase::UpdateButtons() +{ + CGUIRadioButtonControl* btnShowHidden = + static_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_BTNSHOWHIDDEN)); + if (btnShowHidden) + { + btnShowHidden->SetVisible(CServiceBroker::GetPVRManager() + .ChannelGroups() + ->GetGroupAll(m_bRadio) + ->GetNumHiddenChannels() > 0); + btnShowHidden->SetSelected(m_bShowHiddenChannels); + } + + CGUIWindowPVRBase::UpdateButtons(); + SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, m_bShowHiddenChannels ? g_localizeStrings.Get(19022) + : GetChannelGroup()->GroupName()); +} + +bool CGUIWindowPVRChannelsBase::OnAction(const CAction& action) +{ + switch (action.GetID()) + { + case REMOTE_0: + case REMOTE_1: + case REMOTE_2: + case REMOTE_3: + case REMOTE_4: + case REMOTE_5: + case REMOTE_6: + case REMOTE_7: + case REMOTE_8: + case REMOTE_9: + AppendChannelNumberCharacter(static_cast<char>(action.GetID() - REMOTE_0) + '0'); + return true; + + case ACTION_CHANNEL_NUMBER_SEP: + AppendChannelNumberCharacter(CPVRChannelNumber::SEPARATOR); + return true; + } + + return CGUIWindowPVRBase::OnAction(action); +} + +bool CGUIWindowPVRChannelsBase::OnMessage(CGUIMessage& message) +{ + bool bReturn = false; + switch (message.GetMessage()) + { + case GUI_MSG_WINDOW_INIT: + { + const CPVRChannelsPath path(message.GetStringParam(0)); + if (path.IsValid() && path.IsChannelGroup()) + { + // if a path to a channel group is given we must init + // that group instead of last played/selected group + m_channelGroupPath = message.GetStringParam(0); + } + break; + } + + case GUI_MSG_CLICKED: + if (message.GetSenderId() == m_viewControl.GetCurrentControl()) + { + if (message.GetParam1() == ACTION_SELECT_ITEM || + message.GetParam1() == ACTION_MOUSE_LEFT_CLICK) + { + // If direct channel number input is active, select the entered channel. + if (CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNumberInputHandler() + .CheckInputAndExecuteAction()) + { + bReturn = true; + break; + } + } + + int iItem = m_viewControl.GetSelectedItem(); + if (iItem >= 0 && iItem < m_vecItems->Size()) + { + bReturn = true; + switch (message.GetParam1()) + { + case ACTION_SELECT_ITEM: + case ACTION_MOUSE_LEFT_CLICK: + case ACTION_PLAYER_PLAY: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + *(m_vecItems->Get(iItem)), true); + break; + case ACTION_SHOW_INFO: + CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo( + *(m_vecItems->Get(iItem))); + break; + case ACTION_DELETE_ITEM: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().HideChannel( + *(m_vecItems->Get(iItem))); + break; + case ACTION_CONTEXT_MENU: + case ACTION_MOUSE_RIGHT_CLICK: + OnPopupMenu(iItem); + break; + default: + bReturn = false; + break; + } + } + } + else if (message.GetSenderId() == CONTROL_BTNSHOWHIDDEN) + { + CGUIRadioButtonControl* radioButton = + static_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_BTNSHOWHIDDEN)); + if (radioButton) + { + m_bShowHiddenChannels = radioButton->IsSelected(); + Update(GetDirectoryPath()); + } + + bReturn = true; + } + else if (message.GetSenderId() == CONTROL_BTNFILTERCHANNELS) + { + std::string filter = GetProperty("filter").asString(); + CGUIKeyboardFactory::ShowAndGetFilter(filter, false); + OnFilterItems(filter); + UpdateButtons(); + + bReturn = true; + } + break; + + case GUI_MSG_REFRESH_LIST: + { + switch (static_cast<PVREvent>(message.GetParam1())) + { + case PVREvent::ChannelGroup: + case PVREvent::CurrentItem: + case PVREvent::Epg: + case PVREvent::EpgActiveItem: + case PVREvent::EpgContainer: + case PVREvent::RecordingsInvalidated: + case PVREvent::Timers: + SetInvalid(); + break; + + case PVREvent::ChannelGroupInvalidated: + Refresh(true); + break; + + default: + break; + } + break; + } + } + + return bReturn || CGUIWindowPVRBase::OnMessage(message); +} + +bool CGUIWindowPVRChannelsBase::OnContextButtonManage(const CFileItemPtr& item, + CONTEXT_BUTTON button) +{ + bool bReturn = false; + + if (button == CONTEXT_BUTTON_EDIT) + { + // Create context sub menu + CContextButtons buttons; + buttons.Add(CONTEXT_BUTTON_GROUP_MANAGER, 19048); + buttons.Add(CONTEXT_BUTTON_CHANNEL_MANAGER, 19199); + buttons.Add(CONTEXT_BUTTON_UPDATE_EPG, 19251); + + // Get the response + int choice = CGUIDialogContextMenu::ShowAndGetChoice(buttons); + if (choice >= 0) + { + switch (static_cast<CONTEXT_BUTTON>(choice)) + { + case CONTEXT_BUTTON_GROUP_MANAGER: + ShowGroupManager(); + break; + case CONTEXT_BUTTON_CHANNEL_MANAGER: + ShowChannelManager(); + break; + case CONTEXT_BUTTON_UPDATE_EPG: + UpdateEpg(item); + break; + default: + break; + } + + // Refresh the list in case anything was changed + Refresh(true); + } + + bReturn = true; + } + + return bReturn; +} + +void CGUIWindowPVRChannelsBase::UpdateEpg(const CFileItemPtr& item) +{ + const std::shared_ptr<CPVRChannel> channel(item->GetPVRChannelInfoTag()); + + if (!CGUIDialogYesNo::ShowAndGetInput( + CVariant{19251}, // "Update guide information" + CVariant{19252}, // "Schedule guide update for this channel?" + CVariant{""}, CVariant{channel->ChannelName()})) + return; + + const std::shared_ptr<CPVREpg> epg = channel->GetEPG(); + if (epg) + { + epg->ForceUpdate(); + + const std::string strMessage = + StringUtils::Format("{}: '{}'", + g_localizeStrings.Get(19253), // "Guide update scheduled for channel" + channel->ChannelName()); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, + g_localizeStrings.Get(19166), // "PVR information" + strMessage); + } + else + { + const std::string strMessage = + StringUtils::Format("{}: '{}'", + g_localizeStrings.Get(19254), // "Guide update failed for channel" + channel->ChannelName()); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, + g_localizeStrings.Get(19166), // "PVR information" + strMessage); + } +} + +void CGUIWindowPVRChannelsBase::ShowChannelManager() +{ + CGUIDialogPVRChannelManager* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRChannelManager>( + WINDOW_DIALOG_PVR_CHANNEL_MANAGER); + if (!dialog) + return; + + dialog->SetRadio(m_bRadio); + + const int iItem = m_viewControl.GetSelectedItem(); + dialog->Open(iItem >= 0 && iItem < m_vecItems->Size() ? m_vecItems->Get(iItem) : nullptr); +} + +void CGUIWindowPVRChannelsBase::ShowGroupManager() +{ + /* Load group manager dialog */ + CGUIDialogPVRGroupManager* pDlgInfo = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGroupManager>( + WINDOW_DIALOG_PVR_GROUP_MANAGER); + if (!pDlgInfo) + return; + + pDlgInfo->SetRadio(m_bRadio); + pDlgInfo->Open(); +} + +void CGUIWindowPVRChannelsBase::OnInputDone() +{ + const CPVRChannelNumber channelNumber = GetChannelNumber(); + if (channelNumber.IsValid()) + { + int itemIndex = 0; + for (const CFileItemPtr& channel : *m_vecItems) + { + if (channel->GetPVRChannelGroupMemberInfoTag()->ChannelNumber() == channelNumber) + { + m_viewControl.SetSelectedItem(itemIndex); + return; + } + ++itemIndex; + } + } +} + +void CGUIWindowPVRChannelsBase::GetChannelNumbers(std::vector<std::string>& channelNumbers) +{ + const std::shared_ptr<CPVRChannelGroup> group = GetChannelGroup(); + if (group) + group->GetChannelNumbers(channelNumbers); +} + +CGUIWindowPVRTVChannels::CGUIWindowPVRTVChannels() + : CGUIWindowPVRChannelsBase(false, WINDOW_TV_CHANNELS, "MyPVRChannels.xml") +{ +} + +std::string CGUIWindowPVRTVChannels::GetDirectoryPath() +{ + return CPVRChannelsPath(false, m_bShowHiddenChannels, GetChannelGroup()->GroupName()); +} + +CGUIWindowPVRRadioChannels::CGUIWindowPVRRadioChannels() + : CGUIWindowPVRChannelsBase(true, WINDOW_RADIO_CHANNELS, "MyPVRChannels.xml") +{ +} + +std::string CGUIWindowPVRRadioChannels::GetDirectoryPath() +{ + return CPVRChannelsPath(true, m_bShowHiddenChannels, GetChannelGroup()->GroupName()); +} diff --git a/xbmc/pvr/windows/GUIWindowPVRChannels.h b/xbmc/pvr/windows/GUIWindowPVRChannels.h new file mode 100644 index 0000000..ef8e848 --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRChannels.h @@ -0,0 +1,65 @@ +/* + * 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 "dialogs/GUIDialogContextMenu.h" +#include "pvr/PVRChannelNumberInputHandler.h" +#include "pvr/windows/GUIWindowPVRBase.h" + +#include <string> + +namespace PVR +{ +class CGUIWindowPVRChannelsBase : public CGUIWindowPVRBase, public CPVRChannelNumberInputHandler +{ +public: + CGUIWindowPVRChannelsBase(bool bRadio, int id, const std::string& xmlFile); + ~CGUIWindowPVRChannelsBase() override; + + std::string GetRootPath() const override; + bool OnMessage(CGUIMessage& message) override; + void GetContextButtons(int itemNumber, CContextButtons& buttons) override; + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override; + bool Update(const std::string& strDirectory, bool updateFilterPath = true) override; + void UpdateButtons() override; + bool OnAction(const CAction& action) override; + + // CPVRChannelNumberInputHandler implementation + void GetChannelNumbers(std::vector<std::string>& channelNumbers) override; + void OnInputDone() override; + +private: + bool OnContextButtonManage(const CFileItemPtr& item, CONTEXT_BUTTON button); + + void ShowChannelManager(); + void ShowGroupManager(); + void UpdateEpg(const CFileItemPtr& item); + +protected: + bool m_bShowHiddenChannels; +}; + +class CGUIWindowPVRTVChannels : public CGUIWindowPVRChannelsBase +{ +public: + CGUIWindowPVRTVChannels(); + +protected: + std::string GetDirectoryPath() override; +}; + +class CGUIWindowPVRRadioChannels : public CGUIWindowPVRChannelsBase +{ +public: + CGUIWindowPVRRadioChannels(); + +protected: + std::string GetDirectoryPath() override; +}; +} // namespace PVR diff --git a/xbmc/pvr/windows/GUIWindowPVRGuide.cpp b/xbmc/pvr/windows/GUIWindowPVRGuide.cpp new file mode 100644 index 0000000..f4c247f --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRGuide.cpp @@ -0,0 +1,973 @@ +/* + * 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 "GUIWindowPVRGuide.h" + +#include "FileItem.h" +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "addons/Skin.h" +#include "dialogs/GUIDialogBusy.h" +#include "dialogs/GUIDialogContextMenu.h" +#include "dialogs/GUIDialogNumeric.h" +#include "guilib/GUIMessage.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "messaging/helpers/DialogHelper.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/channels/PVRChannelsPath.h" +#include "pvr/epg/EpgChannelData.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/guilib/GUIEPGGridContainer.h" +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "pvr/guilib/PVRGUIActionsEPG.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "pvr/guilib/PVRGUIActionsTimers.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "view/GUIViewState.h" + +#include <functional> +#include <memory> +#include <mutex> +#include <utility> +#include <vector> + +using namespace KODI::MESSAGING; +using namespace PVR; +using namespace std::chrono_literals; + +CGUIWindowPVRGuideBase::CGUIWindowPVRGuideBase(bool bRadio, int id, const std::string& xmlFile) + : CGUIWindowPVRBase(bRadio, id, xmlFile) +{ + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().RegisterChannelNumberInputHandler(this); +} + +CGUIWindowPVRGuideBase::~CGUIWindowPVRGuideBase() +{ + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().DeregisterChannelNumberInputHandler( + this); + + m_bRefreshTimelineItems = false; + m_bSyncRefreshTimelineItems = false; + StopRefreshTimelineItemsThread(); +} + +CGUIEPGGridContainer* CGUIWindowPVRGuideBase::GetGridControl() +{ + return dynamic_cast<CGUIEPGGridContainer*>(GetControl(m_viewControl.GetCurrentControl())); +} + +void CGUIWindowPVRGuideBase::InitEpgGridControl() +{ + CGUIEPGGridContainer* epgGridContainer = GetGridControl(); + if (epgGridContainer) + { + CPVRManager& mgr = CServiceBroker::GetPVRManager(); + + const std::shared_ptr<CPVRChannel> channel = mgr.ChannelGroups()->GetByPath( + mgr.Get<PVR::GUI::Channels>().GetSelectedChannelPath(m_bRadio)); + + if (channel) + { + m_bChannelSelectionRestored = epgGridContainer->SetChannel(channel); + epgGridContainer->JumpToDate( + mgr.PlaybackState()->GetPlaybackTime(channel->ClientID(), channel->UniqueID())); + } + else + { + m_bChannelSelectionRestored = false; + epgGridContainer->JumpToNow(); + } + + if (!epgGridContainer->HasData()) + m_bSyncRefreshTimelineItems = true; // force data update on first window open + } + + StartRefreshTimelineItemsThread(); +} + +void CGUIWindowPVRGuideBase::ClearData() +{ + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_cachedChannelGroup.reset(); + } + + CGUIWindowPVRBase::ClearData(); +} + +void CGUIWindowPVRGuideBase::OnInitWindow() +{ + if (m_guiState) + m_viewControl.SetCurrentView(m_guiState->GetViewAsControl(), false); + + if (InitChannelGroup()) // no channels -> lazy init + InitEpgGridControl(); + + CGUIWindowPVRBase::OnInitWindow(); +} + +void CGUIWindowPVRGuideBase::OnDeinitWindow(int nextWindowID) +{ + StopRefreshTimelineItemsThread(); + + m_bChannelSelectionRestored = false; + + CGUIDialog* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_GUIDE_CONTROLS); + if (dialog && dialog->IsDialogRunning()) + { + dialog->Close(); + } + + CGUIWindowPVRBase::OnDeinitWindow(nextWindowID); +} + +void CGUIWindowPVRGuideBase::StartRefreshTimelineItemsThread() +{ + StopRefreshTimelineItemsThread(); + m_refreshTimelineItemsThread.reset(new CPVRRefreshTimelineItemsThread(this)); + m_refreshTimelineItemsThread->Create(); +} + +void CGUIWindowPVRGuideBase::StopRefreshTimelineItemsThread() +{ + if (m_refreshTimelineItemsThread) + m_refreshTimelineItemsThread->Stop(); +} + +void CGUIWindowPVRGuideBase::NotifyEvent(const PVREvent& event) +{ + if (event == PVREvent::Epg || event == PVREvent::EpgContainer || + event == PVREvent::ChannelGroupInvalidated || event == PVREvent::ChannelGroup) + { + m_bRefreshTimelineItems = true; + // no base class call => do async refresh + return; + } + else if (event == PVREvent::ChannelPlaybackStopped) + { + if (m_guiState && m_guiState->GetSortMethod().sortBy == SortByLastPlayed) + { + // set dirty to force sync refresh + m_bSyncRefreshTimelineItems = true; + } + } + + // do sync refresh if dirty + CGUIWindowPVRBase::NotifyEvent(event); +} + +void CGUIWindowPVRGuideBase::SetInvalid() +{ + CGUIEPGGridContainer* epgGridContainer = GetGridControl(); + if (epgGridContainer) + epgGridContainer->SetInvalid(); + + CGUIWindowPVRBase::SetInvalid(); +} + +void CGUIWindowPVRGuideBase::GetContextButtons(int itemNumber, CContextButtons& buttons) +{ + CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons); + + buttons.Add(CONTEXT_BUTTON_NAVIGATE, 19326); // Navigate... +} + +void CGUIWindowPVRGuideBase::UpdateSelectedItemPath() +{ + CGUIEPGGridContainer* epgGridContainer = GetGridControl(); + if (epgGridContainer) + { + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + epgGridContainer->GetSelectedChannelGroupMember(); + if (groupMember) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().SetSelectedChannelPath( + m_bRadio, groupMember->Path()); + } +} + +void CGUIWindowPVRGuideBase::UpdateButtons() +{ + CGUIWindowPVRBase::UpdateButtons(); + + SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, g_localizeStrings.Get(19032)); + + const std::shared_ptr<CPVRChannelGroup> group = GetChannelGroup(); + SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, group ? group->GroupName() : ""); +} + +bool CGUIWindowPVRGuideBase::Update(const std::string& strDirectory, + bool updateFilterPath /* = true */) +{ + if (m_bUpdating) + { + // Prevent concurrent updates. Instead, let the timeline items refresh thread pick it up later. + m_bRefreshTimelineItems = true; + return true; + } + + bool bReturn = CGUIWindowPVRBase::Update(strDirectory, updateFilterPath); + + if (bReturn && !m_bChannelSelectionRestored) + { + CGUIEPGGridContainer* epgGridContainer = GetGridControl(); + if (epgGridContainer) + m_bChannelSelectionRestored = epgGridContainer->SetChannel( + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetSelectedChannelPath( + m_bRadio)); + } + + return bReturn; +} + +bool CGUIWindowPVRGuideBase::GetDirectory(const std::string& strDirectory, CFileItemList& items) +{ + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_cachedChannelGroup && *m_cachedChannelGroup != *GetChannelGroup()) + { + // channel group change and not very first open of this window. force immediate update. + m_bSyncRefreshTimelineItems = true; + } + } + + if (m_bSyncRefreshTimelineItems) + m_refreshTimelineItemsThread->DoRefresh(true); + + const CGUIEPGGridContainer* epgGridContainer = GetGridControl(); + if (epgGridContainer) + { + const std::unique_ptr<CFileItemList> newTimeline = GetGridControl()->GetCurrentTimeLineItems(); + items.RemoveDiscCache(GetID()); + items.Assign(*newTimeline, false); + } + + return true; +} + +void CGUIWindowPVRGuideBase::FormatAndSort(CFileItemList& items) +{ + // Speedup: Nothing to do here as sorting was already done in RefreshTimelineItems + return; +} + +CFileItemPtr CGUIWindowPVRGuideBase::GetCurrentListItem(int offset /*= 0*/) +{ + const CGUIEPGGridContainer* epgGridContainer = GetGridControl(); + if (epgGridContainer) + return epgGridContainer->GetSelectedGridItem(offset); + + return {}; +} + +int CGUIWindowPVRGuideBase::GetCurrentListItemIndex(const std::shared_ptr<CFileItem>& item) +{ + return item ? item->GetProperty("TimelineIndex").asInteger() : -1; +} + +bool CGUIWindowPVRGuideBase::ShouldNavigateToGridContainer(int iAction) +{ + CGUIEPGGridContainer* epgGridContainer = GetGridControl(); + CGUIControl* control = GetControl(CONTROL_LSTCHANNELGROUPS); + if (epgGridContainer && control && GetFocusedControlID() == control->GetID()) + { + int iNavigationId = control->GetAction(iAction).GetNavigation(); + if (iNavigationId > 0) + { + control = epgGridContainer; + while (control != + this) // navigation target could be the grid control or one of its parent controls. + { + if (iNavigationId == control->GetID()) + { + // channel group selector control's target for the action is the grid control + return true; + } + control = control->GetParentControl(); + } + } + } + return false; +} + +bool CGUIWindowPVRGuideBase::OnAction(const CAction& action) +{ + switch (action.GetID()) + { + case ACTION_MOVE_UP: + case ACTION_MOVE_DOWN: + case ACTION_MOVE_LEFT: + case ACTION_MOVE_RIGHT: + { + // Check whether grid container is configured as channel group selector's navigation target for the given action. + if (ShouldNavigateToGridContainer(action.GetID())) + { + CGUIEPGGridContainer* epgGridContainer = GetGridControl(); + if (epgGridContainer) + { + CGUIWindowPVRBase::OnAction(action); + + switch (action.GetID()) + { + case ACTION_MOVE_UP: + epgGridContainer->GoToBottom(); + return true; + case ACTION_MOVE_DOWN: + epgGridContainer->GoToTop(); + return true; + case ACTION_MOVE_LEFT: + epgGridContainer->GoToMostRight(); + return true; + case ACTION_MOVE_RIGHT: + epgGridContainer->GoToMostLeft(); + return true; + default: + break; + } + } + } + break; + } + case REMOTE_0: + if (GetCurrentDigitCount() == 0) + { + // single zero input is handled by epg grid container + break; + } + // fall-thru is intended + [[fallthrough]]; + case REMOTE_1: + case REMOTE_2: + case REMOTE_3: + case REMOTE_4: + case REMOTE_5: + case REMOTE_6: + case REMOTE_7: + case REMOTE_8: + case REMOTE_9: + AppendChannelNumberCharacter(static_cast<char>(action.GetID() - REMOTE_0) + '0'); + return true; + + case ACTION_CHANNEL_NUMBER_SEP: + AppendChannelNumberCharacter(CPVRChannelNumber::SEPARATOR); + return true; + } + + return CGUIWindowPVRBase::OnAction(action); +} + +void CGUIWindowPVRGuideBase::RefreshView(CGUIMessage& message, bool bInitGridControl) +{ + CGUIWindowPVRBase::OnMessage(message); + + // force grid data update + m_bSyncRefreshTimelineItems = true; + + if (bInitGridControl) + InitEpgGridControl(); + + Refresh(true); +} + +bool CGUIWindowPVRGuideBase::OnMessage(CGUIMessage& message) +{ + bool bReturn = false; + switch (message.GetMessage()) + { + case GUI_MSG_WINDOW_INIT: + { + const CPVRChannelsPath path(message.GetStringParam(0)); + if (path.IsValid() && path.IsChannelGroup()) + { + // if a path to a channel group is given we must init + // that group instead of last played/selected group + m_channelGroupPath = message.GetStringParam(0); + } + break; + } + + case GUI_MSG_ITEM_SELECTED: + message.SetParam1(GetCurrentListItemIndex(GetCurrentListItem())); + bReturn = true; + break; + + case GUI_MSG_CLICKED: + { + if (message.GetSenderId() == m_viewControl.GetCurrentControl()) + { + if (message.GetParam1() == ACTION_SELECT_ITEM || + message.GetParam1() == ACTION_MOUSE_LEFT_CLICK) + { + // If direct channel number input is active, select the entered channel. + if (CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNumberInputHandler() + .CheckInputAndExecuteAction()) + { + bReturn = true; + break; + } + } + + const std::shared_ptr<CFileItem> pItem = GetCurrentListItem(); + if (pItem) + { + switch (message.GetParam1()) + { + case ACTION_SELECT_ITEM: + case ACTION_MOUSE_LEFT_CLICK: + switch (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_EPG_SELECTACTION)) + { + case EPG_SELECT_ACTION_CONTEXT_MENU: + OnPopupMenu(GetCurrentListItemIndex(pItem)); + bReturn = true; + break; + case EPG_SELECT_ACTION_SWITCH: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(*pItem, + true); + bReturn = true; + break; + case EPG_SELECT_ACTION_PLAY_RECORDING: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(*pItem, + true); + bReturn = true; + break; + case EPG_SELECT_ACTION_INFO: + CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*pItem); + bReturn = true; + break; + case EPG_SELECT_ACTION_RECORD: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleTimer(*pItem); + bReturn = true; + break; + case EPG_SELECT_ACTION_SMART_SELECT: + { + const std::shared_ptr<CPVREpgInfoTag> tag(pItem->GetEPGInfoTag()); + if (tag) + { + const CDateTime start(tag->StartAsUTC()); + const CDateTime end(tag->EndAsUTC()); + const CDateTime now(CDateTime::GetUTCDateTime()); + + if (start <= now && now <= end) + { + // current event + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + *pItem, true); + } + else if (now < start) + { + // future event + if (CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(tag)) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().EditTimer(*pItem); + else + { + bool bCanRecord = true; + const std::shared_ptr<CPVRChannel> channel = CPVRItem(pItem).GetChannel(); + if (channel) + bCanRecord = channel->CanRecord(); + + const int iTextID = + bCanRecord + ? 19302 // "Do you want to record the selected programme or to switch to the current programme?" + : 19344; // "Do you want to set a reminder for the selected programme or to switch to the current programme?" + const int iNoButtonID = bCanRecord ? 264 // No => "Record" + : 826; // "Set reminder" + + HELPERS::DialogResponse ret = + HELPERS::ShowYesNoDialogText(CVariant{19096}, // "Smart select" + CVariant{iTextID}, CVariant{iNoButtonID}, + CVariant{19165}); // Yes => "Switch" + if (ret == HELPERS::DialogResponse::CHOICE_NO) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimer(*pItem, + false); + else if (ret == HELPERS::DialogResponse::CHOICE_YES) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + *pItem, true); + } + } + else + { + // past event + if (CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(tag)) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording( + *pItem, true); + else if (tag->IsPlayable()) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayEpgTag( + *pItem); + else + CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*pItem); + } + bReturn = true; + } + break; + } + } + break; + case ACTION_SHOW_INFO: + CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*pItem); + bReturn = true; + break; + case ACTION_PLAYER_PLAY: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(*pItem, + true); + bReturn = true; + break; + case ACTION_RECORD: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleTimer(*pItem); + bReturn = true; + break; + case ACTION_PVR_SHOW_TIMER_RULE: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimerRule(*pItem, true, + false); + bReturn = true; + break; + case ACTION_CONTEXT_MENU: + case ACTION_MOUSE_RIGHT_CLICK: + OnPopupMenu(GetCurrentListItemIndex(pItem)); + bReturn = true; + break; + } + } + } + else if (message.GetSenderId() == CONTROL_BTNVIEWASICONS || + message.GetSenderId() == CONTROL_BTNSORTBY || + message.GetSenderId() == CONTROL_BTNSORTASC) + { + RefreshView(message, false); + bReturn = true; + } + break; + } + case GUI_MSG_CHANGE_SORT_DIRECTION: + case GUI_MSG_CHANGE_SORT_METHOD: + case GUI_MSG_CHANGE_VIEW_MODE: + { + RefreshView(message, message.GetMessage() == GUI_MSG_CHANGE_VIEW_MODE); + bReturn = true; + break; + } + case GUI_MSG_REFRESH_LIST: + { + switch (static_cast<PVREvent>(message.GetParam1())) + { + case PVREvent::ManagerStarted: + if (InitChannelGroup()) + InitEpgGridControl(); + break; + + case PVREvent::ChannelGroup: + case PVREvent::ChannelGroupInvalidated: + case PVREvent::ClientsInvalidated: + case PVREvent::ChannelPlaybackStopped: + case PVREvent::Epg: + case PVREvent::EpgContainer: + if (InitChannelGroup()) + Refresh(true); + break; + + case PVREvent::Timers: + case PVREvent::TimersInvalidated: + SetInvalid(); + break; + + default: + break; + } + break; + } + case GUI_MSG_SYSTEM_WAKE: + GotoCurrentProgramme(); + bReturn = true; + break; + } + + return bReturn || CGUIWindowPVRBase::OnMessage(message); +} + +bool CGUIWindowPVRGuideBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + if (OnContextButtonNavigate(button)) + return true; + + if (itemNumber < 0 || itemNumber >= m_vecItems->Size()) + return false; + + return CGUIMediaWindow::OnContextButton(itemNumber, button); +} + +namespace +{ + +template<typename A> +class CContextMenuFunctions : public CContextButtons +{ +public: + explicit CContextMenuFunctions(A* instance) : m_instance(instance) {} + + void Add(bool (A::*function)(), unsigned int resId) + { + CContextButtons::Add(size(), resId); + m_functions.emplace_back(std::bind(function, m_instance)); + } + + bool Call(int idx) + { + if (idx < 0 || idx >= static_cast<int>(m_functions.size())) + return false; + + return m_functions[idx](); + } + +private: + A* m_instance = nullptr; + std::vector<std::function<bool()>> m_functions; +}; + +} // unnamed namespace + +bool CGUIWindowPVRGuideBase::OnContextButtonNavigate(CONTEXT_BUTTON button) +{ + bool bReturn = false; + + if (button == CONTEXT_BUTTON_NAVIGATE) + { + if (g_SkinInfo->HasSkinFile("DialogPVRGuideControls.xml")) + { + // use controls dialog + CGUIDialog* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_GUIDE_CONTROLS); + if (dialog && !dialog->IsDialogRunning()) + { + dialog->Open(); + } + } + else + { + // use context menu + CContextMenuFunctions<CGUIWindowPVRGuideBase> buttons(this); + buttons.Add(&CGUIWindowPVRGuideBase::GotoBegin, 19063); // First programme + buttons.Add(&CGUIWindowPVRGuideBase::Go12HoursBack, 19317); // 12 hours back + buttons.Add(&CGUIWindowPVRGuideBase::GotoCurrentProgramme, 19070); // Current programme + buttons.Add(&CGUIWindowPVRGuideBase::Go12HoursForward, 19318); // 12 hours forward + buttons.Add(&CGUIWindowPVRGuideBase::GotoEnd, 19064); // Last programme + buttons.Add(&CGUIWindowPVRGuideBase::OpenDateSelectionDialog, 19288); // Date selector + buttons.Add(&CGUIWindowPVRGuideBase::GotoFirstChannel, 19322); // First channel + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV() || + CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio() || + CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingEpgTag()) + buttons.Add(&CGUIWindowPVRGuideBase::GotoPlayingChannel, 19323); // Playing channel + buttons.Add(&CGUIWindowPVRGuideBase::GotoLastChannel, 19324); // Last channel + buttons.Add(&CGUIWindowPVRBase::ActivatePreviousChannelGroup, 19319); // Previous group + buttons.Add(&CGUIWindowPVRBase::ActivateNextChannelGroup, 19320); // Next group + buttons.Add(&CGUIWindowPVRBase::OpenChannelGroupSelectionDialog, 19321); // Group selector + + int buttonIdx = 0; + int lastButtonIdx = 2; // initially select "Current programme" + + // loop until canceled + while (buttonIdx >= 0) + { + buttonIdx = CGUIDialogContextMenu::Show(buttons, lastButtonIdx); + lastButtonIdx = buttonIdx; + buttons.Call(buttonIdx); + } + } + bReturn = true; + } + + return bReturn; +} + +bool CGUIWindowPVRGuideBase::RefreshTimelineItems() +{ + if (m_bRefreshTimelineItems || m_bSyncRefreshTimelineItems) + { + m_bRefreshTimelineItems = false; + m_bSyncRefreshTimelineItems = false; + + CGUIEPGGridContainer* epgGridContainer = GetGridControl(); + if (epgGridContainer) + { + const std::shared_ptr<CPVRChannelGroup> group(GetChannelGroup()); + if (!group) + return false; + + CPVREpgContainer& epgContainer = CServiceBroker::GetPVRManager().EpgContainer(); + + const std::pair<CDateTime, CDateTime> dates = epgContainer.GetFirstAndLastEPGDate(); + CDateTime startDate = dates.first; + CDateTime endDate = dates.second; + const CDateTime currentDate = CDateTime::GetUTCDateTime(); + + if (!startDate.IsValid()) + startDate = currentDate; + + if (!endDate.IsValid() || endDate < startDate) + endDate = startDate; + + // limit start to past days to display + const int iPastDays = epgContainer.GetPastDaysToDisplay(); + const CDateTime maxPastDate(currentDate - CDateTimeSpan(iPastDays, 0, 0, 0)); + if (startDate < maxPastDate) + startDate = maxPastDate; + + // limit end to future days to display + const int iFutureDays = epgContainer.GetFutureDaysToDisplay(); + const CDateTime maxFutureDate(currentDate + CDateTimeSpan(iFutureDays, 0, 0, 0)); + if (endDate > maxFutureDate) + endDate = maxFutureDate; + + std::unique_ptr<CFileItemList> channels(new CFileItemList); + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers = + group->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE); + + for (const auto& groupMember : groupMembers) + { + channels->Add(std::make_shared<CFileItem>(groupMember)); + } + + if (m_guiState) + channels->Sort(m_guiState->GetSortMethod()); + + epgGridContainer->SetTimelineItems(channels, startDate, endDate); + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_cachedChannelGroup = group; + } + return true; + } + } + return false; +} + +bool CGUIWindowPVRGuideBase::GotoBegin() +{ + GetGridControl()->GoToBegin(); + return true; +} + +bool CGUIWindowPVRGuideBase::GotoEnd() +{ + GetGridControl()->GoToEnd(); + return true; +} + +bool CGUIWindowPVRGuideBase::GotoCurrentProgramme() +{ + const CPVRManager& mgr = CServiceBroker::GetPVRManager(); + std::shared_ptr<CPVRChannel> channel = mgr.PlaybackState()->GetPlayingChannel(); + + if (!channel) + { + const std::shared_ptr<CPVREpgInfoTag> playingTag = mgr.PlaybackState()->GetPlayingEpgTag(); + if (playingTag) + channel = mgr.ChannelGroups()->GetChannelForEpgTag(playingTag); + } + + if (channel) + GetGridControl()->GoToDate( + mgr.PlaybackState()->GetPlaybackTime(channel->ClientID(), channel->UniqueID())); + else + GetGridControl()->GoToNow(); + + return true; +} + +bool CGUIWindowPVRGuideBase::OpenDateSelectionDialog() +{ + bool bReturn = false; + + KODI::TIME::SystemTime date; + CGUIEPGGridContainer* epgGridContainer = GetGridControl(); + epgGridContainer->GetSelectedDate().GetAsSystemTime(date); + + if (CGUIDialogNumeric::ShowAndGetDate(date, g_localizeStrings.Get(19288))) /* Go to date */ + { + epgGridContainer->GoToDate(CDateTime(date)); + bReturn = true; + } + + return bReturn; +} + +bool CGUIWindowPVRGuideBase::Go12HoursBack() +{ + return GotoDate(-12); +} + +bool CGUIWindowPVRGuideBase::Go12HoursForward() +{ + return GotoDate(+12); +} + +bool CGUIWindowPVRGuideBase::GotoDate(int deltaHours) +{ + CGUIEPGGridContainer* epgGridContainer = GetGridControl(); + epgGridContainer->GoToDate(epgGridContainer->GetSelectedDate() + + CDateTimeSpan(0, deltaHours, 0, 0)); + return true; +} + +bool CGUIWindowPVRGuideBase::GotoFirstChannel() +{ + GetGridControl()->GoToFirstChannel(); + return true; +} + +bool CGUIWindowPVRGuideBase::GotoLastChannel() +{ + GetGridControl()->GoToLastChannel(); + return true; +} + +bool CGUIWindowPVRGuideBase::GotoPlayingChannel() +{ + const CPVRManager& mgr = CServiceBroker::GetPVRManager(); + std::shared_ptr<CPVRChannel> channel = mgr.PlaybackState()->GetPlayingChannel(); + + if (!channel) + { + const std::shared_ptr<CPVREpgInfoTag> playingTag = mgr.PlaybackState()->GetPlayingEpgTag(); + if (playingTag) + channel = mgr.ChannelGroups()->GetChannelForEpgTag(playingTag); + } + + if (channel) + { + GetGridControl()->SetChannel(channel); + return true; + } + return false; +} + +void CGUIWindowPVRGuideBase::OnInputDone() +{ + const CPVRChannelNumber channelNumber = GetChannelNumber(); + if (channelNumber.IsValid()) + { + GetGridControl()->SetChannel(channelNumber); + } +} + +void CGUIWindowPVRGuideBase::GetChannelNumbers(std::vector<std::string>& channelNumbers) +{ + const std::shared_ptr<CPVRChannelGroup> group = GetChannelGroup(); + if (group) + group->GetChannelNumbers(channelNumbers); +} + +CPVRRefreshTimelineItemsThread::CPVRRefreshTimelineItemsThread(CGUIWindowPVRGuideBase* pGuideWindow) + : CThread("epg-grid-refresh-timeline-items"), + m_pGuideWindow(pGuideWindow), + m_ready(true), + m_done(false) +{ +} + +CPVRRefreshTimelineItemsThread::~CPVRRefreshTimelineItemsThread() +{ + // Note: CThread dtor will also call StopThread(true), but if thread worker function exits that + // late, it might access member variables of this which are already destroyed. Thus, stop + // the thread worker here and synchronously, while all members of this are still alive. + StopThread(true); +} + +void CPVRRefreshTimelineItemsThread::Stop() +{ + StopThread(false); + m_ready.Set(); // wake up the worker thread to let it exit +} + +void CPVRRefreshTimelineItemsThread::DoRefresh(bool bWait) +{ + m_ready.Set(); // wake up the worker thread + + if (bWait) + { + m_done.Reset(); + CGUIDialogBusy::WaitOnEvent(m_done, 100, false); + } +} + +void CPVRRefreshTimelineItemsThread::Process() +{ + static const int BOOSTED_SLEEPS_THRESHOLD = 4; + + int iLastEpgItemsCount = 0; + int iUpdatesWithoutChange = 0; + + while (!m_bStop) + { + m_done.Reset(); + + if (m_pGuideWindow->RefreshTimelineItems() && !m_bStop) + { + CGUIMessage m(GUI_MSG_REFRESH_LIST, m_pGuideWindow->GetID(), 0, + static_cast<int>(PVREvent::Epg)); + CServiceBroker::GetAppMessenger()->SendGUIMessage(m); + } + + if (m_bStop) + break; + + m_done.Set(); + + // in order to fill the guide window asap, use a short update interval until we the + // same amount of epg events for BOOSTED_SLEEPS_THRESHOLD + 1 times in a row . + if (iUpdatesWithoutChange < BOOSTED_SLEEPS_THRESHOLD) + { + int iCurrentEpgItemsCount = m_pGuideWindow->CurrentDirectory().Size(); + + if (iCurrentEpgItemsCount == iLastEpgItemsCount) + iUpdatesWithoutChange++; + else + iUpdatesWithoutChange = 0; // reset + + iLastEpgItemsCount = iCurrentEpgItemsCount; + + m_ready.Wait(1000ms); // boosted update cycle + } + else + { + m_ready.Wait(5000ms); // normal update cycle + } + + m_ready.Reset(); + } + + m_ready.Reset(); + m_done.Set(); +} + +std::string CGUIWindowPVRTVGuide::GetRootPath() const +{ + return "pvr://guide/tv/"; +} + +std::string CGUIWindowPVRRadioGuide::GetRootPath() const +{ + return "pvr://guide/radio/"; +} diff --git a/xbmc/pvr/windows/GUIWindowPVRGuide.h b/xbmc/pvr/windows/GUIWindowPVRGuide.h new file mode 100644 index 0000000..87477ca --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRGuide.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/PVRChannelNumberInputHandler.h" +#include "pvr/windows/GUIWindowPVRBase.h" +#include "threads/Event.h" +#include "threads/Thread.h" + +#include <atomic> +#include <memory> +#include <string> + +class CFileItemList; +class CGUIMessage; + +namespace PVR +{ +enum class PVREvent; + +class CPVRChannelGroup; +class CGUIEPGGridContainer; +class CPVRRefreshTimelineItemsThread; + +class CGUIWindowPVRGuideBase : public CGUIWindowPVRBase, public CPVRChannelNumberInputHandler +{ +public: + CGUIWindowPVRGuideBase(bool bRadio, int id, const std::string& xmlFile); + ~CGUIWindowPVRGuideBase() override; + + void OnInitWindow() override; + void OnDeinitWindow(int nextWindowID) override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction& action) override; + void GetContextButtons(int itemNumber, CContextButtons& buttons) override; + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override; + void UpdateButtons() override; + void SetInvalid() override; + bool Update(const std::string& strDirectory, bool updateFilterPath = true) override; + + void NotifyEvent(const PVREvent& event) override; + + bool RefreshTimelineItems(); + + // CPVRChannelNumberInputHandler implementation + void GetChannelNumbers(std::vector<std::string>& channelNumbers) override; + void OnInputDone() override; + + bool GotoBegin(); + bool GotoEnd(); + bool GotoCurrentProgramme(); + bool GotoDate(int deltaHours); + bool OpenDateSelectionDialog(); + bool Go12HoursBack(); + bool Go12HoursForward(); + bool GotoFirstChannel(); + bool GotoLastChannel(); + bool GotoPlayingChannel(); + +protected: + void UpdateSelectedItemPath() override; + std::string GetDirectoryPath() override { return ""; } + bool GetDirectory(const std::string& strDirectory, CFileItemList& items) override; + void FormatAndSort(CFileItemList& items) override; + CFileItemPtr GetCurrentListItem(int offset = 0) override; + + void ClearData() override; + +private: + CGUIEPGGridContainer* GetGridControl(); + void InitEpgGridControl(); + + bool OnContextButtonNavigate(CONTEXT_BUTTON button); + + bool ShouldNavigateToGridContainer(int iAction); + + void StartRefreshTimelineItemsThread(); + void StopRefreshTimelineItemsThread(); + + void RefreshView(CGUIMessage& message, bool bInitGridControl); + + int GetCurrentListItemIndex(const std::shared_ptr<CFileItem>& item); + + std::unique_ptr<CPVRRefreshTimelineItemsThread> m_refreshTimelineItemsThread; + std::atomic_bool m_bRefreshTimelineItems{false}; + std::atomic_bool m_bSyncRefreshTimelineItems{false}; + + std::shared_ptr<CPVRChannelGroup> m_cachedChannelGroup; + + bool m_bChannelSelectionRestored{false}; +}; + +class CGUIWindowPVRTVGuide : public CGUIWindowPVRGuideBase +{ +public: + CGUIWindowPVRTVGuide() : CGUIWindowPVRGuideBase(false, WINDOW_TV_GUIDE, "MyPVRGuide.xml") {} + std::string GetRootPath() const override; +}; + +class CGUIWindowPVRRadioGuide : public CGUIWindowPVRGuideBase +{ +public: + CGUIWindowPVRRadioGuide() : CGUIWindowPVRGuideBase(true, WINDOW_RADIO_GUIDE, "MyPVRGuide.xml") {} + std::string GetRootPath() const override; +}; + +class CPVRRefreshTimelineItemsThread : public CThread +{ +public: + explicit CPVRRefreshTimelineItemsThread(CGUIWindowPVRGuideBase* pGuideWindow); + ~CPVRRefreshTimelineItemsThread() override; + + void Process() override; + + void DoRefresh(bool bWait); + void Stop(); + +private: + CGUIWindowPVRGuideBase* m_pGuideWindow; + CEvent m_ready; + CEvent m_done; +}; +} // namespace PVR diff --git a/xbmc/pvr/windows/GUIWindowPVRRecordings.cpp b/xbmc/pvr/windows/GUIWindowPVRRecordings.cpp new file mode 100644 index 0000000..d127217 --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRRecordings.cpp @@ -0,0 +1,440 @@ +/* + * 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 "GUIWindowPVRRecordings.h" + +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIMessage.h" +#include "guilib/GUIRadioButtonControl.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "pvr/PVRManager.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "pvr/guilib/PVRGUIActionsRecordings.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/recordings/PVRRecordingsPath.h" +#include "settings/MediaSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/URIUtils.h" +#include "video/VideoLibraryQueue.h" +#include "video/VideoUtils.h" +#include "video/windows/GUIWindowVideoBase.h" +#include "video/windows/GUIWindowVideoNav.h" + +#include <memory> +#include <mutex> +#include <string> + +using namespace PVR; + +CGUIWindowPVRRecordingsBase::CGUIWindowPVRRecordingsBase(bool bRadio, + int id, + const std::string& xmlFile) + : CGUIWindowPVRBase(bRadio, id, xmlFile), + m_settings( + {CSettings::SETTING_PVRRECORD_GROUPRECORDINGS, CSettings::SETTING_MYVIDEOS_SELECTACTION}) +{ +} + +CGUIWindowPVRRecordingsBase::~CGUIWindowPVRRecordingsBase() = default; + +void CGUIWindowPVRRecordingsBase::OnWindowLoaded() +{ + CONTROL_SELECT(CONTROL_BTNGROUPITEMS); +} + +std::string CGUIWindowPVRRecordingsBase::GetDirectoryPath() +{ + const std::string basePath = CPVRRecordingsPath(m_bShowDeletedRecordings, m_bRadio); + return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() + : basePath; +} + +void CGUIWindowPVRRecordingsBase::GetContextButtons(int itemNumber, CContextButtons& buttons) +{ + if (itemNumber < 0 || itemNumber >= m_vecItems->Size()) + return; + CFileItemPtr pItem = m_vecItems->Get(itemNumber); + + if (pItem->IsParentFolder()) + { + // No context menu for ".." items + return; + } + + bool isDeletedRecording = false; + + std::shared_ptr<CPVRRecording> recording(pItem->GetPVRRecordingInfoTag()); + if (recording) + { + isDeletedRecording = recording->IsDeleted(); + + if (isDeletedRecording) + { + if (m_vecItems->GetObjectCount() > 1) + buttons.Add(CONTEXT_BUTTON_DELETE_ALL, 19292); /* Delete all permanently */ + } + } + + if (!isDeletedRecording) + CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons); +} + +bool CGUIWindowPVRRecordingsBase::OnAction(const CAction& action) +{ + if (action.GetID() == ACTION_PARENT_DIR || action.GetID() == ACTION_NAV_BACK) + { + CPVRRecordingsPath path(m_vecItems->GetPath()); + if (path.IsValid() && !path.IsRecordingsRoot()) + { + GoParentFolder(); + return true; + } + } + else if (action.GetID() == ACTION_TOGGLE_WATCHED) + { + const std::shared_ptr<CFileItem> pItem = m_vecItems->Get(m_viewControl.GetSelectedItem()); + if (!pItem || pItem->IsParentFolder()) + return false; + + bool bUnWatched = false; + if (pItem->HasPVRRecordingInfoTag()) + bUnWatched = pItem->GetPVRRecordingInfoTag()->GetPlayCount() == 0; + else if (pItem->m_bIsFolder) + bUnWatched = pItem->GetProperty("unwatchedepisodes").asInteger() > 0; + else + return false; + + CVideoLibraryQueue::GetInstance().MarkAsWatched(pItem, bUnWatched); + return true; + } + + return CGUIWindowPVRBase::OnAction(action); +} + +bool CGUIWindowPVRRecordingsBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + if (itemNumber < 0 || itemNumber >= m_vecItems->Size()) + return false; + CFileItemPtr pItem = m_vecItems->Get(itemNumber); + + return OnContextButtonDeleteAll(pItem.get(), button) || + CGUIMediaWindow::OnContextButton(itemNumber, button); +} + +bool CGUIWindowPVRRecordingsBase::Update(const std::string& strDirectory, + bool updateFilterPath /* = true */) +{ + m_thumbLoader.StopThread(); + + int iOldCount = m_vecItems->GetObjectCount(); + const std::string oldPath = m_vecItems->GetPath(); + + bool bReturn = CGUIWindowPVRBase::Update(strDirectory); + + if (bReturn) + { + // TODO: does it make sense to show the non-deleted recordings, although user wants + // to see the deleted recordings? Or is this just another hack to avoid misbehavior + // of CGUIMediaWindow if it has no content? + + std::unique_lock<CCriticalSection> lock(m_critSection); + + /* empty list for deleted recordings */ + if (m_vecItems->GetObjectCount() == 0 && m_bShowDeletedRecordings) + { + /* show the normal recordings instead */ + m_bShowDeletedRecordings = false; + lock.unlock(); + Update(GetDirectoryPath()); + return bReturn; + } + } + + if (bReturn && iOldCount > 0 && m_vecItems->GetObjectCount() == 0 && + oldPath == m_vecItems->GetPath()) + { + /* go to the parent folder if we're in a subdirectory and for instance just deleted the last item */ + const CPVRRecordingsPath path(m_vecItems->GetPath()); + if (path.IsValid() && !path.IsRecordingsRoot()) + GoParentFolder(); + } + return bReturn; +} + +void CGUIWindowPVRRecordingsBase::UpdateButtons() +{ + int iWatchMode = CMediaSettings::GetInstance().GetWatchedMode("recordings"); + int iStringId = 257; // "Error" + + if (iWatchMode == WatchedModeAll) + iStringId = 22015; // "All recordings" + else if (iWatchMode == WatchedModeUnwatched) + iStringId = 16101; // "Unwatched" + else if (iWatchMode == WatchedModeWatched) + iStringId = 16102; // "Watched" + + SET_CONTROL_LABEL(CONTROL_BTNSHOWMODE, g_localizeStrings.Get(iStringId)); + + bool bGroupRecordings = m_settings.GetBoolValue(CSettings::SETTING_PVRRECORD_GROUPRECORDINGS); + SET_CONTROL_SELECTED(GetID(), CONTROL_BTNGROUPITEMS, bGroupRecordings); + + CGUIRadioButtonControl* btnShowDeleted = + static_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_BTNSHOWDELETED)); + if (btnShowDeleted) + { + btnShowDeleted->SetVisible( + m_bRadio ? CServiceBroker::GetPVRManager().Recordings()->HasDeletedRadioRecordings() + : CServiceBroker::GetPVRManager().Recordings()->HasDeletedTVRecordings()); + btnShowDeleted->SetSelected(m_bShowDeletedRecordings); + } + + CGUIWindowPVRBase::UpdateButtons(); + SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, m_bShowDeletedRecordings + ? g_localizeStrings.Get(19179) + : ""); /* Deleted recordings trash */ + + const CPVRRecordingsPath path(m_vecItems->GetPath()); + SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, + bGroupRecordings && path.IsValid() ? path.GetUnescapedDirectoryPath() : ""); +} + +bool CGUIWindowPVRRecordingsBase::OnMessage(CGUIMessage& message) +{ + bool bReturn = false; + switch (message.GetMessage()) + { + case GUI_MSG_CLICKED: + if (message.GetSenderId() == m_viewControl.GetCurrentControl()) + { + int iItem = m_viewControl.GetSelectedItem(); + if (iItem >= 0 && iItem < m_vecItems->Size()) + { + const CFileItemPtr item(m_vecItems->Get(iItem)); + switch (message.GetParam1()) + { + case ACTION_SELECT_ITEM: + case ACTION_MOUSE_LEFT_CLICK: + case ACTION_PLAYER_PLAY: + { + const CPVRRecordingsPath path(m_vecItems->GetPath()); + if (path.IsValid() && path.IsRecordingsRoot() && item->IsParentFolder()) + { + // handle special 'go home' item. + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_HOME); + bReturn = true; + break; + } + + if (!item->IsParentFolder() && message.GetParam1() == ACTION_PLAYER_PLAY) + { + if (item->m_bIsFolder) + { + if (CGUIWindowVideoNav::ShowResumeMenu(*item)) + VIDEO_UTILS::PlayItem(item); + } + else + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording( + *item, true /* check resume */); + + bReturn = true; + } + else if (item->m_bIsFolder) + { + // recording folders and ".." folders in subfolders are handled by base class. + bReturn = false; + } + else + { + switch (m_settings.GetIntValue(CSettings::SETTING_MYVIDEOS_SELECTACTION)) + { + case SELECT_ACTION_CHOOSE: + OnPopupMenu(iItem); + bReturn = true; + break; + case SELECT_ACTION_PLAY_OR_RESUME: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording( + *item, true /* check resume */); + bReturn = true; + break; + case SELECT_ACTION_RESUME: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().ResumePlayRecording( + *item, true /* fall back to play if no resume possible */); + bReturn = true; + break; + case SELECT_ACTION_INFO: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo( + *item); + bReturn = true; + break; + default: + bReturn = false; + break; + } + } + break; + } + case ACTION_CONTEXT_MENU: + case ACTION_MOUSE_RIGHT_CLICK: + OnPopupMenu(iItem); + bReturn = true; + break; + case ACTION_SHOW_INFO: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo(*item); + bReturn = true; + break; + case ACTION_DELETE_ITEM: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().DeleteRecording(*item); + bReturn = true; + break; + default: + bReturn = false; + break; + } + } + } + else if (message.GetSenderId() == CONTROL_BTNGROUPITEMS) + { + const std::shared_ptr<CSettings> settings = + CServiceBroker::GetSettingsComponent()->GetSettings(); + settings->ToggleBool(CSettings::SETTING_PVRRECORD_GROUPRECORDINGS); + settings->Save(); + Refresh(true); + } + else if (message.GetSenderId() == CONTROL_BTNSHOWDELETED) + { + CGUIRadioButtonControl* radioButton = + static_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_BTNSHOWDELETED)); + if (radioButton) + { + m_bShowDeletedRecordings = radioButton->IsSelected(); + Update(GetDirectoryPath()); + } + bReturn = true; + } + else if (message.GetSenderId() == CONTROL_BTNSHOWMODE) + { + CMediaSettings::GetInstance().CycleWatchedMode("recordings"); + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + OnFilterItems(GetProperty("filter").asString()); + UpdateButtons(); + return true; + } + break; + case GUI_MSG_REFRESH_LIST: + { + switch (static_cast<PVREvent>(message.GetParam1())) + { + case PVREvent::CurrentItem: + case PVREvent::Epg: + case PVREvent::EpgActiveItem: + case PVREvent::EpgContainer: + case PVREvent::Timers: + SetInvalid(); + break; + + case PVREvent::RecordingsInvalidated: + case PVREvent::TimersInvalidated: + Refresh(true); + break; + + default: + break; + } + break; + } + } + + return bReturn || CGUIWindowPVRBase::OnMessage(message); +} + +bool CGUIWindowPVRRecordingsBase::OnContextButtonDeleteAll(CFileItem* item, CONTEXT_BUTTON button) +{ + if (button == CONTEXT_BUTTON_DELETE_ALL) + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().DeleteAllRecordingsFromTrash(); + return true; + } + + return false; +} + +void CGUIWindowPVRRecordingsBase::OnPrepareFileItems(CFileItemList& items) +{ + if (items.IsEmpty()) + return; + + CFileItemList files; + for (const auto& item : items) + { + if (!item->m_bIsFolder) + files.Add(item); + } + + if (!files.IsEmpty()) + { + if (m_database.Open()) + { + CGUIWindowVideoBase::LoadVideoInfo(files, m_database, false); + m_database.Close(); + } + m_thumbLoader.Load(files); + } + + CGUIWindowPVRBase::OnPrepareFileItems(items); +} + +bool CGUIWindowPVRRecordingsBase::GetFilteredItems(const std::string& filter, CFileItemList& items) +{ + bool listchanged = CGUIWindowPVRBase::GetFilteredItems(filter, items); + + int watchMode = CMediaSettings::GetInstance().GetWatchedMode("recordings"); + + CFileItemPtr item; + for (int i = 0; i < items.Size(); i++) + { + item = items.Get(i); + + if (item->IsParentFolder()) // Don't delete the go to parent folder + continue; + + if (!item->HasPVRRecordingInfoTag()) + continue; + + int iPlayCount = item->GetPVRRecordingInfoTag()->GetPlayCount(); + if ((watchMode == WatchedModeWatched && iPlayCount == 0) || + (watchMode == WatchedModeUnwatched && iPlayCount > 0)) + { + items.Remove(i); + i--; + listchanged = true; + } + } + + // Remove the parent folder item, if it's the only item in the folder. + if (items.GetObjectCount() == 0 && items.GetFileCount() > 0 && items.Get(0)->IsParentFolder()) + items.Remove(0); + + return listchanged; +} + +std::string CGUIWindowPVRTVRecordings::GetRootPath() const +{ + return CPVRRecordingsPath(m_bShowDeletedRecordings, false); +} + +std::string CGUIWindowPVRRadioRecordings::GetRootPath() const +{ + return CPVRRecordingsPath(m_bShowDeletedRecordings, true); +} diff --git a/xbmc/pvr/windows/GUIWindowPVRRecordings.h b/xbmc/pvr/windows/GUIWindowPVRRecordings.h new file mode 100644 index 0000000..5091fde --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRRecordings.h @@ -0,0 +1,71 @@ +/* + * 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 "dialogs/GUIDialogContextMenu.h" +#include "pvr/settings/PVRSettings.h" +#include "pvr/windows/GUIWindowPVRBase.h" +#include "video/VideoDatabase.h" +#include "video/VideoThumbLoader.h" + +#include <string> + +class CFileItem; + +namespace PVR +{ +class CGUIWindowPVRRecordingsBase : public CGUIWindowPVRBase +{ +public: + CGUIWindowPVRRecordingsBase(bool bRadio, int id, const std::string& xmlFile); + ~CGUIWindowPVRRecordingsBase() override; + + void OnWindowLoaded() override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction& action) override; + void GetContextButtons(int itemNumber, CContextButtons& buttons) override; + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override; + bool Update(const std::string& strDirectory, bool updateFilterPath = true) override; + void UpdateButtons() override; + +protected: + std::string GetDirectoryPath() override; + void OnPrepareFileItems(CFileItemList& items) override; + bool GetFilteredItems(const std::string& filter, CFileItemList& items) override; + + bool m_bShowDeletedRecordings{false}; + +private: + bool OnContextButtonDeleteAll(CFileItem* item, CONTEXT_BUTTON button); + + CVideoThumbLoader m_thumbLoader; + CVideoDatabase m_database; + CPVRSettings m_settings; +}; + +class CGUIWindowPVRTVRecordings : public CGUIWindowPVRRecordingsBase +{ +public: + CGUIWindowPVRTVRecordings() + : CGUIWindowPVRRecordingsBase(false, WINDOW_TV_RECORDINGS, "MyPVRRecordings.xml") + { + } + std::string GetRootPath() const override; +}; + +class CGUIWindowPVRRadioRecordings : public CGUIWindowPVRRecordingsBase +{ +public: + CGUIWindowPVRRadioRecordings() + : CGUIWindowPVRRecordingsBase(true, WINDOW_RADIO_RECORDINGS, "MyPVRRecordings.xml") + { + } + std::string GetRootPath() const override; +}; +} // namespace PVR diff --git a/xbmc/pvr/windows/GUIWindowPVRSearch.cpp b/xbmc/pvr/windows/GUIWindowPVRSearch.cpp new file mode 100644 index 0000000..f0cd3fe --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRSearch.cpp @@ -0,0 +1,544 @@ +/* + * 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 "GUIWindowPVRSearch.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogBusy.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIMessage.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/dialogs/GUIDialogPVRGuideSearch.h" +#include "pvr/epg/Epg.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/epg/EpgSearchFilter.h" +#include "pvr/epg/EpgSearchPath.h" +#include "pvr/guilib/PVRGUIActionsEPG.h" +#include "pvr/guilib/PVRGUIActionsTimers.h" +#include "pvr/recordings/PVRRecording.h" +#include "threads/IRunnable.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" + +#include <algorithm> +#include <memory> +#include <vector> + +using namespace PVR; +using namespace KODI::MESSAGING; + +namespace +{ +class AsyncSearchAction : private IRunnable +{ +public: + AsyncSearchAction() = delete; + AsyncSearchAction(CFileItemList* items, CPVREpgSearchFilter* filter) + : m_items(items), m_filter(filter) + { + } + bool Execute(); + +private: + // IRunnable implementation + void Run() override; + + CFileItemList* m_items; + CPVREpgSearchFilter* m_filter; +}; + +bool AsyncSearchAction::Execute() +{ + CGUIDialogBusy::Wait(this, 100, false); + return true; +} + +void AsyncSearchAction::Run() +{ + std::vector<std::shared_ptr<CPVREpgInfoTag>> results = + CServiceBroker::GetPVRManager().EpgContainer().GetTags(m_filter->GetEpgSearchData()); + m_filter->SetEpgSearchDataFiltered(); + + // Tags can still contain false positives, for search criteria that cannot be handled via + // database. So, run extended search filters on what we got from the database. + for (auto it = results.begin(); it != results.end();) + { + it = results.erase(std::remove_if(results.begin(), results.end(), + [this](const std::shared_ptr<CPVREpgInfoTag>& entry) { + return !m_filter->FilterEntry(entry); + }), + results.end()); + } + + if (m_filter->ShouldRemoveDuplicates()) + m_filter->RemoveDuplicates(results); + + m_filter->SetLastExecutedDateTime(CDateTime::GetUTCDateTime()); + + for (const auto& tag : results) + { + m_items->Add(std::make_shared<CFileItem>(tag)); + } +} +} // unnamed namespace + +CGUIWindowPVRSearchBase::CGUIWindowPVRSearchBase(bool bRadio, int id, const std::string& xmlFile) + : CGUIWindowPVRBase(bRadio, id, xmlFile), m_bSearchConfirmed(false) +{ +} + +CGUIWindowPVRSearchBase::~CGUIWindowPVRSearchBase() +{ +} + +void CGUIWindowPVRSearchBase::GetContextButtons(int itemNumber, CContextButtons& buttons) +{ + if (itemNumber < 0 || itemNumber >= m_vecItems->Size()) + return; + + const CPVREpgSearchPath path(m_vecItems->GetPath()); + const bool bIsSavedSearchesRoot = (path.IsValid() && path.IsSavedSearchesRoot()); + if (!bIsSavedSearchesRoot) + buttons.Add(CONTEXT_BUTTON_CLEAR, 19232); // "Clear search results" + + CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons); +} + +bool CGUIWindowPVRSearchBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + if (itemNumber < 0 || itemNumber >= m_vecItems->Size()) + return false; + CFileItemPtr pItem = m_vecItems->Get(itemNumber); + + return OnContextButtonClear(pItem.get(), button) || + CGUIMediaWindow::OnContextButton(itemNumber, button); +} + +void CGUIWindowPVRSearchBase::SetItemToSearch(const CFileItem& item) +{ + if (item.HasEPGSearchFilter()) + { + SetSearchFilter(item.GetEPGSearchFilter()); + } + else if (item.IsUsablePVRRecording()) + { + SetSearchFilter(std::make_shared<CPVREpgSearchFilter>(m_bRadio)); + m_searchfilter->SetSearchPhrase(item.GetPVRRecordingInfoTag()->m_strTitle); + } + else + { + SetSearchFilter(std::make_shared<CPVREpgSearchFilter>(m_bRadio)); + + const std::shared_ptr<CPVREpgInfoTag> epgTag(CPVRItem(item).GetEpgInfoTag()); + if (epgTag && !CServiceBroker::GetPVRManager().IsParentalLocked(epgTag)) + m_searchfilter->SetSearchPhrase(epgTag->Title()); + } + + ExecuteSearch(); +} + +void CGUIWindowPVRSearchBase::OnPrepareFileItems(CFileItemList& items) +{ + if (m_bSearchConfirmed) + { + items.Clear(); + } + + if (items.IsEmpty()) + { + auto item = std::make_shared<CFileItem>(CPVREpgSearchPath::PATH_SEARCH_DIALOG, false); + item->SetLabel(g_localizeStrings.Get( + m_searchfilter == nullptr ? 19335 : 19336)); // "New search..." / "Edit search..." + item->SetLabelPreformatted(true); + item->SetSpecialSort(SortSpecialOnTop); + item->SetArt("icon", "DefaultPVRSearch.png"); + items.Add(item); + + item = std::make_shared<CFileItem>(m_bRadio ? CPVREpgSearchPath::PATH_RADIO_SAVEDSEARCHES + : CPVREpgSearchPath::PATH_TV_SAVEDSEARCHES, + true); + item->SetLabel(g_localizeStrings.Get(19337)); // "Saved searches" + item->SetLabelPreformatted(true); + item->SetSpecialSort(SortSpecialOnTop); + item->SetArt("icon", "DefaultFolder.png"); + items.Add(item); + } + + if (m_bSearchConfirmed) + { + const int itemCount = items.GetObjectCount(); + + AsyncSearchAction(&items, m_searchfilter.get()).Execute(); + + if (items.GetObjectCount() == itemCount) + { + HELPERS::ShowOKDialogText(CVariant{284}, // "No results found" + m_searchfilter->GetSearchTerm()); + } + } +} + +bool CGUIWindowPVRSearchBase::OnAction(const CAction& action) +{ + if (action.GetID() == ACTION_PARENT_DIR || action.GetID() == ACTION_NAV_BACK) + { + const CPVREpgSearchPath path(m_vecItems->GetPath()); + if (path.IsValid() && path.IsSavedSearchesRoot()) + { + // Go to root dir and show previous search results if any + m_bSearchConfirmed = (m_searchfilter != nullptr); + GoParentFolder(); + return true; + } + } + return CGUIWindowPVRBase::OnAction(action); +} + +bool CGUIWindowPVRSearchBase::OnMessage(CGUIMessage& message) +{ + if (message.GetMessage() == GUI_MSG_CLICKED) + { + if (message.GetSenderId() == m_viewControl.GetCurrentControl()) + { + int iItem = m_viewControl.GetSelectedItem(); + if (iItem >= 0 && iItem < m_vecItems->Size()) + { + CFileItemPtr pItem = m_vecItems->Get(iItem); + + /* process actions */ + switch (message.GetParam1()) + { + case ACTION_SHOW_INFO: + case ACTION_SELECT_ITEM: + case ACTION_MOUSE_LEFT_CLICK: + { + const CPVREpgSearchPath path(pItem->GetPath()); + const bool bIsSavedSearch = (path.IsValid() && path.IsSavedSearch()); + const bool bIsSavedSearchesRoot = (path.IsValid() && path.IsSavedSearchesRoot()); + + if (message.GetParam1() != ACTION_SHOW_INFO) + { + if (pItem->IsParentFolder()) + { + // Go to root dir and show previous search results if any + m_bSearchConfirmed = (m_searchfilter != nullptr); + break; // handled by base class + } + + if (bIsSavedSearchesRoot) + { + // List saved searches + m_bSearchConfirmed = false; + break; // handled by base class + } + + if (bIsSavedSearch) + { + // Execute selected saved search + SetSearchFilter(pItem->GetEPGSearchFilter()); + ExecuteSearch(); + return true; + } + } + + if (bIsSavedSearch) + { + OpenDialogSearch(*pItem); + } + else if (pItem->GetPath() == CPVREpgSearchPath::PATH_SEARCH_DIALOG) + { + OpenDialogSearch(m_searchfilter); + } + else + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*pItem); + } + return true; + } + + case ACTION_CONTEXT_MENU: + case ACTION_MOUSE_RIGHT_CLICK: + OnPopupMenu(iItem); + return true; + + case ACTION_RECORD: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleTimer(*pItem); + return true; + } + } + } + } + else if (message.GetMessage() == GUI_MSG_REFRESH_LIST) + { + if (static_cast<PVREvent>(message.GetParam1()) == PVREvent::SavedSearchesInvalidated) + { + Refresh(true); + + // Refresh triggered by deleted saved search? + if (m_searchfilter) + { + const CPVREpgSearchPath path(m_vecItems->GetPath()); + const bool bIsSavedSearchesRoot = (path.IsValid() && path.IsSavedSearchesRoot()); + if (bIsSavedSearchesRoot) + { + const std::string filterPath = m_searchfilter->GetPath(); + bool bFound = false; + for (const auto& item : *m_vecItems) + { + const auto filter = item->GetEPGSearchFilter(); + if (filter && filter->GetPath() == filterPath) + { + bFound = true; + break; + } + } + if (!bFound) + SetSearchFilter(nullptr); + } + } + } + } + else if (message.GetMessage() == GUI_MSG_WINDOW_INIT) + { + const CPVREpgSearchPath path(message.GetStringParam(0)); + if (path.IsValid() && path.IsSavedSearch()) + { + const std::shared_ptr<CPVREpgSearchFilter> filter = + CServiceBroker::GetPVRManager().EpgContainer().GetSavedSearchById(path.IsRadio(), + path.GetId()); + if (filter) + { + SetSearchFilter(filter); + m_bSearchConfirmed = true; + } + } + } + + return CGUIWindowPVRBase::OnMessage(message); +} + +bool CGUIWindowPVRSearchBase::Update(const std::string& strDirectory, + bool updateFilterPath /* = true */) +{ + if (m_vecItems->GetObjectCount() > 0) + { + const CPVREpgSearchPath path(m_vecItems->GetPath()); + if (path.IsValid() && path.IsSavedSearchesRoot()) + { + const std::string oldPath = m_vecItems->GetPath(); + + const bool bReturn = CGUIWindowPVRBase::Update(strDirectory); + + if (bReturn && oldPath == m_vecItems->GetPath() && m_vecItems->GetObjectCount() == 0) + { + // Go to parent folder if we're in a subdir and for instance just deleted the last item + GoParentFolder(); + } + return bReturn; + } + } + return CGUIWindowPVRBase::Update(strDirectory); +} + +void CGUIWindowPVRSearchBase::UpdateButtons() +{ + CGUIWindowPVRBase::UpdateButtons(); + + bool bSavedSearchesRoot = false; + std::string header; + const CPVREpgSearchPath path(m_vecItems->GetPath()); + if (path.IsValid() && path.IsSavedSearchesRoot()) + { + bSavedSearchesRoot = true; + header = g_localizeStrings.Get(19337); // "Saved searches" + } + + if (header.empty() && m_searchfilter) + { + header = m_searchfilter->GetTitle(); + if (header.empty()) + { + header = m_searchfilter->GetSearchTerm(); + StringUtils::Trim(header, "\""); + } + } + SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, header); + + if (!bSavedSearchesRoot && m_searchfilter && m_searchfilter->IsChanged()) + SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, g_localizeStrings.Get(19342)); // "[not saved]" + else if (!bSavedSearchesRoot && m_searchfilter) + SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, g_localizeStrings.Get(19343)); // "[saved]" + else + SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, ""); +} + +bool CGUIWindowPVRSearchBase::OnContextButtonClear(CFileItem* item, CONTEXT_BUTTON button) +{ + bool bReturn = false; + + if (button == CONTEXT_BUTTON_CLEAR) + { + bReturn = true; + + m_bSearchConfirmed = false; + SetSearchFilter(nullptr); + + Refresh(true); + } + + return bReturn; +} + +CGUIDialogPVRGuideSearch::Result CGUIWindowPVRSearchBase::OpenDialogSearch(const CFileItem& item) +{ + const auto searchFilter = item.GetEPGSearchFilter(); + if (!searchFilter) + return CGUIDialogPVRGuideSearch::Result::CANCEL; + + return OpenDialogSearch(searchFilter); +} + +CGUIDialogPVRGuideSearch::Result CGUIWindowPVRSearchBase::OpenDialogSearch( + const std::shared_ptr<CPVREpgSearchFilter>& searchFilter) +{ + CGUIDialogPVRGuideSearch* dlgSearch = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGuideSearch>( + WINDOW_DIALOG_PVR_GUIDE_SEARCH); + + if (!dlgSearch) + return CGUIDialogPVRGuideSearch::Result::CANCEL; + + const std::shared_ptr<CPVREpgSearchFilter> tmpSearchFilter = + searchFilter != nullptr ? std::make_shared<CPVREpgSearchFilter>(*searchFilter) + : std::make_shared<CPVREpgSearchFilter>(m_bRadio); + + dlgSearch->SetFilterData(tmpSearchFilter); + + /* Open dialog window */ + dlgSearch->Open(); + + const CGUIDialogPVRGuideSearch::Result result = dlgSearch->GetResult(); + if (result == CGUIDialogPVRGuideSearch::Result::SEARCH) + { + SetSearchFilter(tmpSearchFilter); + ExecuteSearch(); + } + else if (result == CGUIDialogPVRGuideSearch::Result::SAVE) + { + CServiceBroker::GetPVRManager().EpgContainer().PersistSavedSearch(*tmpSearchFilter); + if (searchFilter) + searchFilter->SetDatabaseId(tmpSearchFilter->GetDatabaseId()); + + const CPVREpgSearchPath path(m_vecItems->GetPath()); + if (path.IsValid() && path.IsSearchRoot()) + { + SetSearchFilter(tmpSearchFilter); + ExecuteSearch(); + } + } + + return result; +} + +void CGUIWindowPVRSearchBase::ExecuteSearch() +{ + m_bSearchConfirmed = true; + + const CPVREpgSearchPath path(m_vecItems->GetPath()); + if (path.IsValid() && path.IsSavedSearchesRoot()) + { + GoParentFolder(); + } + else if (IsActive()) + { + Refresh(true); + } + + // Save if not a transient search + if (m_searchfilter->GetDatabaseId() != -1) + CServiceBroker::GetPVRManager().EpgContainer().UpdateSavedSearchLastExecuted(*m_searchfilter); +} + +void CGUIWindowPVRSearchBase::SetSearchFilter( + const std::shared_ptr<CPVREpgSearchFilter>& searchFilter) +{ + if (m_searchfilter && m_searchfilter->IsChanged() && + (!searchFilter || m_searchfilter->GetPath() != searchFilter->GetPath())) + { + bool bCanceled = false; + if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{14117}, // "Warning" + CVariant{19341}, // "Save the current search?" + CVariant{}, + CVariant{!m_searchfilter->GetTitle().empty() + ? m_searchfilter->GetTitle() + : m_searchfilter->GetSearchTerm()}, + bCanceled, CVariant{107}, // "Yes" + CVariant{106}, // "No" + CGUIDialogYesNo::NO_TIMEOUT) && + !bCanceled) + { + std::string title = m_searchfilter->GetTitle(); + if (title.empty()) + { + title = m_searchfilter->GetSearchTerm(); + if (title.empty()) + title = g_localizeStrings.Get(137); // "Search" + else + StringUtils::Trim(title, "\""); + + m_searchfilter->SetTitle(title); + } + CServiceBroker::GetPVRManager().EpgContainer().PersistSavedSearch(*m_searchfilter); + } + } + m_searchfilter = searchFilter; +} + +std::string CGUIWindowPVRTVSearch::GetRootPath() const +{ + return CPVREpgSearchPath::PATH_TV_SEARCH; +} + +std::string CGUIWindowPVRTVSearch::GetStartFolder(const std::string& dir) +{ + return CPVREpgSearchPath::PATH_TV_SEARCH; +} + +std::string CGUIWindowPVRTVSearch::GetDirectoryPath() +{ + return URIUtils::PathHasParent(m_vecItems->GetPath(), CPVREpgSearchPath::PATH_TV_SEARCH) + ? m_vecItems->GetPath() + : CPVREpgSearchPath::PATH_TV_SEARCH; +} + +std::string CGUIWindowPVRRadioSearch::GetRootPath() const +{ + return CPVREpgSearchPath::PATH_RADIO_SEARCH; +} + +std::string CGUIWindowPVRRadioSearch::GetStartFolder(const std::string& dir) +{ + return CPVREpgSearchPath::PATH_RADIO_SEARCH; +} + +std::string CGUIWindowPVRRadioSearch::GetDirectoryPath() +{ + return URIUtils::PathHasParent(m_vecItems->GetPath(), CPVREpgSearchPath::PATH_RADIO_SEARCH) + ? m_vecItems->GetPath() + : CPVREpgSearchPath::PATH_RADIO_SEARCH; +} diff --git a/xbmc/pvr/windows/GUIWindowPVRSearch.h b/xbmc/pvr/windows/GUIWindowPVRSearch.h new file mode 100644 index 0000000..d6f6a35 --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRSearch.h @@ -0,0 +1,90 @@ +/* + * 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 "dialogs/GUIDialogContextMenu.h" +#include "pvr/dialogs/GUIDialogPVRGuideSearch.h" +#include "pvr/windows/GUIWindowPVRBase.h" + +#include <memory> +#include <string> + +class CFileItem; + +namespace PVR +{ +class CPVREpgSearchFilter; + +class CGUIWindowPVRSearchBase : public CGUIWindowPVRBase +{ +public: + CGUIWindowPVRSearchBase(bool bRadio, int id, const std::string& xmlFile); + ~CGUIWindowPVRSearchBase() override; + + bool OnAction(const CAction& action) override; + bool OnMessage(CGUIMessage& message) override; + void GetContextButtons(int itemNumber, CContextButtons& buttons) override; + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override; + bool Update(const std::string& strDirectory, bool updateFilterPath = true) override; + void UpdateButtons() override; + + /*! + * @brief set the item to search similar events for. + * @param item the epg event to search similar events for. + */ + void SetItemToSearch(const CFileItem& item); + + /*! + * @brief Open the search dialog for the given search filter item. + * @param item the epg search filter. + * @return The result of the dialog + */ + CGUIDialogPVRGuideSearch::Result OpenDialogSearch(const CFileItem& item); + +protected: + void OnPrepareFileItems(CFileItemList& items) override; + +private: + bool OnContextButtonClear(CFileItem* item, CONTEXT_BUTTON button); + + CGUIDialogPVRGuideSearch::Result OpenDialogSearch( + const std::shared_ptr<CPVREpgSearchFilter>& searchFilter); + + void ExecuteSearch(); + + void SetSearchFilter(const std::shared_ptr<CPVREpgSearchFilter>& searchFilter); + + bool m_bSearchConfirmed; + std::shared_ptr<CPVREpgSearchFilter> m_searchfilter; +}; + +class CGUIWindowPVRTVSearch : public CGUIWindowPVRSearchBase +{ +public: + CGUIWindowPVRTVSearch() : CGUIWindowPVRSearchBase(false, WINDOW_TV_SEARCH, "MyPVRSearch.xml") {} + +protected: + std::string GetRootPath() const override; + std::string GetStartFolder(const std::string& dir) override; + std::string GetDirectoryPath() override; +}; + +class CGUIWindowPVRRadioSearch : public CGUIWindowPVRSearchBase +{ +public: + CGUIWindowPVRRadioSearch() : CGUIWindowPVRSearchBase(true, WINDOW_RADIO_SEARCH, "MyPVRSearch.xml") + { + } + +protected: + std::string GetRootPath() const override; + std::string GetStartFolder(const std::string& dir) override; + std::string GetDirectoryPath() override; +}; +} // namespace PVR diff --git a/xbmc/pvr/windows/GUIWindowPVRTimerRules.cpp b/xbmc/pvr/windows/GUIWindowPVRTimerRules.cpp new file mode 100644 index 0000000..1b203d4 --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRTimerRules.cpp @@ -0,0 +1,47 @@ +/* + * 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 "GUIWindowPVRTimerRules.h" + +#include "FileItem.h" +#include "pvr/timers/PVRTimersPath.h" +#include "utils/URIUtils.h" + +using namespace PVR; + +CGUIWindowPVRTVTimerRules::CGUIWindowPVRTVTimerRules() +: CGUIWindowPVRTimersBase(false, WINDOW_TV_TIMER_RULES, "MyPVRTimers.xml") +{ +} + +std::string CGUIWindowPVRTVTimerRules::GetRootPath() const +{ + return CPVRTimersPath::PATH_TV_TIMER_RULES; +} + +std::string CGUIWindowPVRTVTimerRules::GetDirectoryPath() +{ + const std::string basePath(CPVRTimersPath(false, true).GetPath()); + return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() : basePath; +} + +CGUIWindowPVRRadioTimerRules::CGUIWindowPVRRadioTimerRules() +: CGUIWindowPVRTimersBase(true, WINDOW_RADIO_TIMER_RULES, "MyPVRTimers.xml") +{ +} + +std::string CGUIWindowPVRRadioTimerRules::GetRootPath() const +{ + return CPVRTimersPath::PATH_RADIO_TIMER_RULES; +} + +std::string CGUIWindowPVRRadioTimerRules::GetDirectoryPath() +{ + const std::string basePath(CPVRTimersPath(true, true).GetPath()); + return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() : basePath; +} diff --git a/xbmc/pvr/windows/GUIWindowPVRTimerRules.h b/xbmc/pvr/windows/GUIWindowPVRTimerRules.h new file mode 100644 index 0000000..dc3f050 --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRTimerRules.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/windows/GUIWindowPVRTimersBase.h" + +#include <string> + +namespace PVR +{ + class CGUIWindowPVRTVTimerRules : public CGUIWindowPVRTimersBase + { + public: + CGUIWindowPVRTVTimerRules(); + ~CGUIWindowPVRTVTimerRules() override = default; + + protected: + std::string GetRootPath() const override; + std::string GetDirectoryPath() override; + }; + + class CGUIWindowPVRRadioTimerRules : public CGUIWindowPVRTimersBase + { + public: + CGUIWindowPVRRadioTimerRules(); + ~CGUIWindowPVRRadioTimerRules() override = default; + + protected: + std::string GetRootPath() const override; + std::string GetDirectoryPath() override; + }; +} diff --git a/xbmc/pvr/windows/GUIWindowPVRTimers.cpp b/xbmc/pvr/windows/GUIWindowPVRTimers.cpp new file mode 100644 index 0000000..610571b --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRTimers.cpp @@ -0,0 +1,47 @@ +/* + * 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 "GUIWindowPVRTimers.h" + +#include "FileItem.h" +#include "pvr/timers/PVRTimersPath.h" +#include "utils/URIUtils.h" + +using namespace PVR; + +CGUIWindowPVRTVTimers::CGUIWindowPVRTVTimers() +: CGUIWindowPVRTimersBase(false, WINDOW_TV_TIMERS, "MyPVRTimers.xml") +{ +} + +std::string CGUIWindowPVRTVTimers::GetRootPath() const +{ + return CPVRTimersPath::PATH_TV_TIMERS; +} + +std::string CGUIWindowPVRTVTimers::GetDirectoryPath() +{ + const std::string basePath(CPVRTimersPath(false, false).GetPath()); + return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() : basePath; +} + +CGUIWindowPVRRadioTimers::CGUIWindowPVRRadioTimers() +: CGUIWindowPVRTimersBase(true, WINDOW_RADIO_TIMERS, "MyPVRTimers.xml") +{ +} + +std::string CGUIWindowPVRRadioTimers::GetRootPath() const +{ + return CPVRTimersPath::PATH_RADIO_TIMERS; +} + +std::string CGUIWindowPVRRadioTimers::GetDirectoryPath() +{ + const std::string basePath(CPVRTimersPath(true, false).GetPath()); + return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() : basePath; +} diff --git a/xbmc/pvr/windows/GUIWindowPVRTimers.h b/xbmc/pvr/windows/GUIWindowPVRTimers.h new file mode 100644 index 0000000..2193ac1 --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRTimers.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/windows/GUIWindowPVRTimersBase.h" + +#include <string> + +namespace PVR +{ + class CGUIWindowPVRTVTimers : public CGUIWindowPVRTimersBase + { + public: + CGUIWindowPVRTVTimers(); + + protected: + std::string GetRootPath() const override; + std::string GetDirectoryPath() override; + }; + + class CGUIWindowPVRRadioTimers : public CGUIWindowPVRTimersBase + { + public: + CGUIWindowPVRRadioTimers(); + + protected: + std::string GetRootPath() const override; + std::string GetDirectoryPath() override; + }; +} diff --git a/xbmc/pvr/windows/GUIWindowPVRTimersBase.cpp b/xbmc/pvr/windows/GUIWindowPVRTimersBase.cpp new file mode 100644 index 0000000..41677eb --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRTimersBase.cpp @@ -0,0 +1,204 @@ +/* + * 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 "GUIWindowPVRTimersBase.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIMessage.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "pvr/PVRManager.h" +#include "pvr/guilib/PVRGUIActionsTimers.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimersPath.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/URIUtils.h" + +#include <memory> +#include <string> + +using namespace PVR; + +CGUIWindowPVRTimersBase::CGUIWindowPVRTimersBase(bool bRadio, int id, const std::string& xmlFile) + : CGUIWindowPVRBase(bRadio, id, xmlFile) +{ +} + +CGUIWindowPVRTimersBase::~CGUIWindowPVRTimersBase() = default; + +bool CGUIWindowPVRTimersBase::OnAction(const CAction& action) +{ + if (action.GetID() == ACTION_PARENT_DIR || action.GetID() == ACTION_NAV_BACK) + { + CPVRTimersPath path(m_vecItems->GetPath()); + if (path.IsValid() && path.IsTimerRule()) + { + m_currentFileItem.reset(); + GoParentFolder(); + return true; + } + } + return CGUIWindowPVRBase::OnAction(action); +} + +void CGUIWindowPVRTimersBase::OnPrepareFileItems(CFileItemList& items) +{ + const CPVRTimersPath path(m_vecItems->GetPath()); + if (path.IsValid() && path.IsTimersRoot()) + { + const auto item = std::make_shared<CFileItem>(CPVRTimersPath::PATH_ADDTIMER, false); + item->SetLabel(g_localizeStrings.Get(19026)); // "Add timer..." + item->SetLabelPreformatted(true); + item->SetSpecialSort(SortSpecialOnTop); + item->SetArt("icon", "DefaultTVShows.png"); + + items.AddFront(item, 0); + } +} + +bool CGUIWindowPVRTimersBase::Update(const std::string& strDirectory, + bool updateFilterPath /* = true */) +{ + int iOldCount = m_vecItems->GetObjectCount(); + const std::string oldPath = m_vecItems->GetPath(); + + bool bReturn = CGUIWindowPVRBase::Update(strDirectory); + + if (bReturn && iOldCount > 0 && m_vecItems->GetObjectCount() == 0 && + oldPath == m_vecItems->GetPath()) + { + /* go to the parent folder if we're in a subdirectory and for instance just deleted the last item */ + const CPVRTimersPath path(m_vecItems->GetPath()); + if (path.IsValid() && path.IsTimerRule()) + { + m_currentFileItem.reset(); + GoParentFolder(); + } + } + + return bReturn; +} + +void CGUIWindowPVRTimersBase::UpdateButtons() +{ + SET_CONTROL_SELECTED(GetID(), CONTROL_BTNHIDEDISABLEDTIMERS, + CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_PVRTIMERS_HIDEDISABLEDTIMERS)); + + CGUIWindowPVRBase::UpdateButtons(); + + std::string strHeaderTitle; + if (m_currentFileItem && m_currentFileItem->HasPVRTimerInfoTag()) + { + std::shared_ptr<CPVRTimerInfoTag> timer = m_currentFileItem->GetPVRTimerInfoTag(); + strHeaderTitle = timer->Title(); + } + + SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, strHeaderTitle); +} + +bool CGUIWindowPVRTimersBase::OnMessage(CGUIMessage& message) +{ + bool bReturn = false; + switch (message.GetMessage()) + { + case GUI_MSG_CLICKED: + if (message.GetSenderId() == m_viewControl.GetCurrentControl()) + { + int iItem = m_viewControl.GetSelectedItem(); + if (iItem >= 0 && iItem < m_vecItems->Size()) + { + bReturn = true; + switch (message.GetParam1()) + { + case ACTION_SHOW_INFO: + case ACTION_SELECT_ITEM: + case ACTION_MOUSE_LEFT_CLICK: + { + CFileItemPtr item(m_vecItems->Get(iItem)); + if (item->m_bIsFolder && (message.GetParam1() != ACTION_SHOW_INFO)) + { + m_currentFileItem = item; + bReturn = false; // folders are handled by base class + } + else + { + m_currentFileItem.reset(); + ActionShowTimer(*item); + } + break; + } + case ACTION_CONTEXT_MENU: + case ACTION_MOUSE_RIGHT_CLICK: + OnPopupMenu(iItem); + break; + case ACTION_DELETE_ITEM: + CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().DeleteTimer( + *(m_vecItems->Get(iItem))); + break; + default: + bReturn = false; + break; + } + } + } + else if (message.GetSenderId() == CONTROL_BTNHIDEDISABLEDTIMERS) + { + const std::shared_ptr<CSettings> settings = + CServiceBroker::GetSettingsComponent()->GetSettings(); + settings->ToggleBool(CSettings::SETTING_PVRTIMERS_HIDEDISABLEDTIMERS); + settings->Save(); + Update(GetDirectoryPath()); + bReturn = true; + } + break; + case GUI_MSG_REFRESH_LIST: + { + switch (static_cast<PVREvent>(message.GetParam1())) + { + case PVREvent::CurrentItem: + case PVREvent::Epg: + case PVREvent::EpgActiveItem: + case PVREvent::EpgContainer: + case PVREvent::Timers: + SetInvalid(); + break; + + case PVREvent::TimersInvalidated: + Refresh(true); + break; + + default: + break; + } + break; + } + } + + return bReturn || CGUIWindowPVRBase::OnMessage(message); +} + +bool CGUIWindowPVRTimersBase::ActionShowTimer(const CFileItem& item) +{ + bool bReturn = false; + + /* Check if "Add timer..." entry is selected, if yes + create a new timer and open settings dialog, otherwise + open settings for selected timer entry */ + if (URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER)) + bReturn = CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimer(m_bRadio); + else + bReturn = CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().EditTimer(item); + + return bReturn; +} diff --git a/xbmc/pvr/windows/GUIWindowPVRTimersBase.h b/xbmc/pvr/windows/GUIWindowPVRTimersBase.h new file mode 100644 index 0000000..2f4232d --- /dev/null +++ b/xbmc/pvr/windows/GUIWindowPVRTimersBase.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/windows/GUIWindowPVRBase.h" + +#include <memory> + +class CFileItem; + +namespace PVR +{ +class CGUIWindowPVRTimersBase : public CGUIWindowPVRBase +{ +public: + CGUIWindowPVRTimersBase(bool bRadio, int id, const std::string& xmlFile); + ~CGUIWindowPVRTimersBase() override; + + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction& action) override; + void OnPrepareFileItems(CFileItemList& items) override; + bool Update(const std::string& strDirectory, bool updateFilterPath = true) override; + void UpdateButtons() override; + +private: + bool ActionShowTimer(const CFileItem& item); + + std::shared_ptr<CFileItem> m_currentFileItem; +}; +} // namespace PVR |