diff options
Diffstat (limited to 'xbmc/pvr/recordings')
-rw-r--r-- | xbmc/pvr/recordings/CMakeLists.txt | 9 | ||||
-rw-r--r-- | xbmc/pvr/recordings/PVRRecording.cpp | 730 | ||||
-rw-r--r-- | xbmc/pvr/recordings/PVRRecording.h | 541 | ||||
-rw-r--r-- | xbmc/pvr/recordings/PVRRecordings.cpp | 361 | ||||
-rw-r--r-- | xbmc/pvr/recordings/PVRRecordings.h | 154 | ||||
-rw-r--r-- | xbmc/pvr/recordings/PVRRecordingsPath.cpp | 258 | ||||
-rw-r--r-- | xbmc/pvr/recordings/PVRRecordingsPath.h | 68 |
7 files changed, 2121 insertions, 0 deletions
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 |