summaryrefslogtreecommitdiffstats
path: root/xbmc/pvr/recordings
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/pvr/recordings')
-rw-r--r--xbmc/pvr/recordings/CMakeLists.txt9
-rw-r--r--xbmc/pvr/recordings/PVRRecording.cpp730
-rw-r--r--xbmc/pvr/recordings/PVRRecording.h541
-rw-r--r--xbmc/pvr/recordings/PVRRecordings.cpp361
-rw-r--r--xbmc/pvr/recordings/PVRRecordings.h154
-rw-r--r--xbmc/pvr/recordings/PVRRecordingsPath.cpp258
-rw-r--r--xbmc/pvr/recordings/PVRRecordingsPath.h68
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