Skip to content

Commit

Permalink
Trigger channel list updates
Browse files Browse the repository at this point in the history
Store a hash of the XML channel list and if the hash changes after an EPG update trigger Kodi to reload the channel list and channel groups.  Use cache to remove obsolete channel icons. Add error checking to channel cache logic introduced in Piers.
  • Loading branch information
emveepee committed Dec 24, 2024
1 parent cb8e126 commit 5bbac98
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 58 deletions.
196 changes: 143 additions & 53 deletions src/Channels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "utilities/XMLUtils.h"
#include "pvrclient-nextpvr.h"

#include <kodi/General.h>
#include <kodi/tools/StringUtils.h>
#include "zlib.h"

Expand All @@ -19,18 +20,20 @@ using namespace NextPVR::utilities;

Channels::Channels(const std::shared_ptr<InstanceSettings>& settings, Request& request) :
m_settings(settings),
m_request(request)
m_request(request),
m_channelCacheFile(kodi::vfs::TranslateSpecialProtocol(kodi::tools::StringUtils::Format("%s%s", settings->m_instanceDirectory.c_str(), "channel.cache")))
{
}

int Channels::GetNumChannels()
{
// Kodi polls this while recordings are open avoid calls to backend
std::lock_guard<std::recursive_mutex> lock(m_channelMutex);
int channelCount = m_channelDetails.size();
if (channelCount == 0)
{
tinyxml2::XMLDocument doc;
if (ReadCachedChannelList(doc) == tinyxml2::XML_SUCCESS)
if (GetChannelList(doc) == tinyxml2::XML_SUCCESS)
{
tinyxml2::XMLNode* channelsNode = doc.RootElement()->FirstChildElement("channels");
tinyxml2::XMLNode* pChannelNode;
Expand Down Expand Up @@ -91,17 +94,9 @@ PVR_ERROR Channels::GetChannels(bool radio, kodi::addon::PVRChannelsResultSet& r
return PVR_ERROR_NO_ERROR;
PVR_ERROR returnValue = PVR_ERROR_NO_ERROR;
std::string stream;
std::map<int, std::pair<bool, bool>>::iterator itr = m_channelDetails.begin();
while (itr != m_channelDetails.end())
{
if (itr->second.second == (radio == true))
itr = m_channelDetails.erase(itr);
else
++itr;
}

tinyxml2::XMLDocument doc;
if (ReadCachedChannelList(doc) == tinyxml2::XML_SUCCESS)
if (GetChannelList(doc) == tinyxml2::XML_SUCCESS)
{
tinyxml2::XMLNode* channelsNode = doc.RootElement()->FirstChildElement("channels");
tinyxml2::XMLNode* pChannelNode;
Expand Down Expand Up @@ -149,14 +144,6 @@ PVR_ERROR Channels::GetChannels(bool radio, kodi::addon::PVRChannelsResultSet& r
if (iconFile.length() > 0)
tag.SetIconPath(iconFile);
}

// V5 has the EPG source type info.
std::string epg;
if (XMLUtils::GetString(pChannelNode, "epg", epg))
m_channelDetails[tag.GetUniqueId()] = std::make_pair(epg == "None", tag.GetIsRadio());
else
m_channelDetails[tag.GetUniqueId()] = std::make_pair(false, tag.GetIsRadio());

// transfer channel to XBMC
results.Add(tag);
}
Expand All @@ -168,7 +155,6 @@ PVR_ERROR Channels::GetChannels(bool radio, kodi::addon::PVRChannelsResultSet& r
return returnValue;
}


/************************************************************/
/** Channel group handling **/

Expand All @@ -182,6 +168,7 @@ PVR_ERROR Channels::GetChannelGroupsAmount(int& amount)
PVR_RECORDING_CHANNEL_TYPE Channels::GetChannelType(unsigned int uid)
{
// when uid is invalid we assume TV because Kodi will
std::lock_guard<std::recursive_mutex> lock(m_channelMutex);
if (m_channelDetails.count(uid) > 0 && m_channelDetails[uid].second == true)
return PVR_RECORDING_CHANNEL_TYPE_RADIO;

Expand All @@ -192,7 +179,7 @@ PVR_ERROR Channels::GetChannelGroups(bool radio, kodi::addon::PVRChannelGroupsRe
{
if (radio && !m_settings->m_showRadio)
return PVR_ERROR_NO_ERROR;

std::lock_guard<std::recursive_mutex> lock(m_channelMutex);
PVR_ERROR returnValue = PVR_ERROR_NO_ERROR;
int priority = 1;

Expand All @@ -201,7 +188,7 @@ PVR_ERROR Channels::GetChannelGroups(bool radio, kodi::addon::PVRChannelGroupsRe
selectedGroups.clear();
bool hasAllChannels = false;
tinyxml2::XMLDocument doc;
if (ReadCachedChannelList(doc) == tinyxml2::XML_SUCCESS)
if (GetChannelList(doc) == tinyxml2::XML_SUCCESS)
{
tinyxml2::XMLNode* channelsNode = doc.RootElement()->FirstChildElement("channels");
tinyxml2::XMLNode* pChannelNode;
Expand Down Expand Up @@ -289,7 +276,7 @@ PVR_ERROR Channels::GetChannelGroupMembers(const kodi::addon::PVRChannelGroup& g
tinyxml2::XMLError retCode;
if (group.GetGroupName() == GetAllChannelsGroupName(group.GetIsRadio()))
{
retCode = ReadCachedChannelList(doc);
retCode = GetChannelList(doc);
}
else
{
Expand All @@ -299,6 +286,7 @@ PVR_ERROR Channels::GetChannelGroupMembers(const kodi::addon::PVRChannelGroup& g

if (retCode == tinyxml2::XML_SUCCESS)
{
std::lock_guard<std::recursive_mutex> lock(m_channelMutex);
tinyxml2::XMLNode* channelsNode = doc.RootElement()->FirstChildElement("channels");
tinyxml2::XMLNode* pChannelNode;
for (pChannelNode = channelsNode->FirstChildElement("channel"); pChannelNode; pChannelNode = pChannelNode->NextSiblingElement())
Expand Down Expand Up @@ -389,50 +377,152 @@ void Channels::LoadLiveStreams()
}
}
}
bool Channels::CacheAllChannels(time_t updateTime)
bool Channels::ChannelCacheChanged(time_t updateTime)
{
std::string checksum = m_checksumChannelList;
std::string response;
const std::string filename = kodi::tools::StringUtils::Format("%s%s", m_settings->m_instanceDirectory.c_str(), "channel.cache");
gzFile gz_file;
struct { time_t update; unsigned long size; } header{0,0};
if (kodi::vfs::FileExists(filename))
const time_t cacheTime = ReadChannelListCache(response);
// on first load need to cache details
if (cacheTime != 0 && m_channelDetails.empty())
LoadChannelDetails();

if (updateTime == cacheTime)
return false;
else
{
gz_file = gzopen(kodi::vfs::TranslateSpecialProtocol(filename).c_str(), "rb");
gzread(gz_file, (void*)&header, sizeof(header));
// change EPG updateTime
response.clear();
// Get new channel list but return on error
if (!ReloadChannelListCache(response, updateTime))
return false;
}
// checksum will be empty on first call
return checksum != m_checksumChannelList;
}

time_t Channels::ReadChannelListCache(std::string& response)
{
time_t updateTime = 0;
if (kodi::vfs::FileExists(m_channelCacheFile))
{
gzFile gz_file = gzopen(m_channelCacheFile.c_str(), "rb");
if (gz_file != NULL)
{
CacheHeader header{ 0,0 };
if (gzread(gz_file, (void*)&header, sizeof(CacheHeader)) == sizeof(CacheHeader))
{
response.resize(header.size);
if (gzread(gz_file, (void*)response.data(), header.size) == header.size)
{
m_checksumChannelList = kodi::GetMD5(response);
updateTime = header.updateTime;
}
}
}
gzclose(gz_file);
if (updateTime == header.update)
if (updateTime == 0)
{
return true;
kodi::Log(ADDON_LOG_WARNING, "Remove invalid cache file.");
kodi::vfs::DeleteFile(m_channelCacheFile);
}
}
return updateTime;
}

bool Channels::ReloadChannelListCache(std::string& response, time_t updateTime)
{
bool rc = false;
gzFile gz_file;
m_checksumChannelList.clear();
if (m_request.DoRequest("/service?method=channel.list&extras=true", response) == HTTP_OK)
{
gz_file = gzopen(kodi::vfs::TranslateSpecialProtocol(filename).c_str(), "wb");
header.size = sizeof(char) * response.size();
header.update = updateTime - m_settings->m_serverTimeOffset;
gzwrite(gz_file, (void*)&header, sizeof(header));
gzwrite(gz_file, (void*)(response.c_str()), header.size);
gz_file = gzopen(m_channelCacheFile.c_str(), "wb");
if (gz_file != NULL)
{
CacheHeader header{ 0,0 };
header.size = response.size();
header.updateTime = updateTime;
if (gzwrite(gz_file, (void*)&header, sizeof(CacheHeader)) == sizeof(CacheHeader))
{
if (gzwrite(gz_file, (void*)(response.c_str()), header.size) == header.size)
{
m_checksumChannelList = kodi::GetMD5(response);
rc = true;
}
}
}
gzclose(gz_file);
return true;
}
return false;
if (!rc)
kodi::Log(ADDON_LOG_ERROR, "Could not write channel cache");

return rc;
}

tinyxml2::XMLError Channels::ReadCachedChannelList(tinyxml2::XMLDocument& doc)
tinyxml2::XMLError Channels::GetChannelList(tinyxml2::XMLDocument& doc)
{
auto start = std::chrono::steady_clock::now();
std::string response;
const std::string filename = kodi::tools::StringUtils::Format("%s%s", m_settings->m_instanceDirectory.c_str(), "channel.cache");
struct { time_t update; unsigned long size; } header{0,0};
gzFile gz_file = gzopen(kodi::vfs::TranslateSpecialProtocol(filename).c_str(), "rb");
gzread(gz_file, (void*)&header, sizeof(header));
response.resize(header.size / sizeof(char));
gzread(gz_file, (void*)response.data(), header.size);
gzclose(gz_file);
tinyxml2::XMLError xmlCheck = doc.Parse(response.c_str());
if (doc.Parse(response.c_str()) != tinyxml2::XML_SUCCESS)
return m_request.DoMethodRequest("channel.list&extras=true", doc);
int milliseconds = static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count());
kodi::Log(ADDON_LOG_DEBUG, "ReadCachedChannelList %d %d %d %d", m_settings->m_instanceNumber, xmlCheck, response.length(), milliseconds);
return xmlCheck;
if (ReadChannelListCache(response) != 0)
{
tinyxml2::XMLError xmlCheck = doc.Parse(response.c_str());
if (xmlCheck == tinyxml2::XML_SUCCESS)
{
int milliseconds = static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count());
kodi::Log(ADDON_LOG_DEBUG, "GetChannelList %d %d %d %d", m_settings->m_instanceNumber, xmlCheck, response.length(), milliseconds);
return xmlCheck;
}
}
kodi::Log(ADDON_LOG_ERROR, "Cannot read channel cache");
return m_request.DoMethodRequest("channel.list&extras=true", doc);
}

bool Channels::ResetChannelCache(time_t updateTime)
{
if (ChannelCacheChanged(updateTime) && !m_checksumChannelList.empty())
{
// m_checksumChannelList will be empty on error
std::lock_guard<std::recursive_mutex> lock(m_channelMutex);
auto oldDetails = m_channelDetails;
m_channelDetails.clear();
LoadChannelDetails();
for( const auto &details : oldDetails)
{
if (m_channelDetails.find(details.first) == m_channelDetails.end())
{
DeleteChannelIcon(details.first);
}
}
return true;
}
return false;
}

bool Channels::LoadChannelDetails()
{
tinyxml2::XMLDocument doc;
if (GetChannelList(doc) == tinyxml2::XML_SUCCESS)
{
tinyxml2::XMLNode* channelsNode = doc.RootElement()->FirstChildElement("channels");
tinyxml2::XMLNode* pChannelNode;
for (pChannelNode = channelsNode->FirstChildElement("channel"); pChannelNode; pChannelNode = pChannelNode->NextSiblingElement())
{
std::string buffer;
bool isRadio = false;
XMLUtils::GetString(pChannelNode, "type", buffer);
if (buffer == "0xa")
{
if (!m_settings->m_showRadio)
continue;
isRadio = true;
}
std::string epg;
if (XMLUtils::GetString(pChannelNode, "epg", epg))
m_channelDetails[XMLUtils::GetUIntValue(pChannelNode, "id")] = std::make_pair(epg == "None", isRadio);
else
m_channelDetails[XMLUtils::GetUIntValue(pChannelNode, "id")] = std::make_pair(false, isRadio);
}
return true;
}
return false;
}
12 changes: 10 additions & 2 deletions src/Channels.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

namespace NextPVR
{
typedef struct { time_t updateTime; size_t size; } CacheHeader;

class ATTR_DLL_LOCAL Channels
{
Expand All @@ -25,9 +26,10 @@ namespace NextPVR
/* Channel handling */
int GetNumChannels();

bool CacheAllChannels(time_t updateTime);
bool ChannelCacheChanged(time_t updateTime);

PVR_ERROR GetChannels(bool radio, kodi::addon::PVRChannelsResultSet& results);
bool ResetChannelCache(time_t updateTime);
/* Channel group handling */
PVR_ERROR GetChannelGroupsAmount(int& amount);
PVR_ERROR GetChannelGroups(bool radio, kodi::addon::PVRChannelGroupsResultSet& results);
Expand All @@ -43,6 +45,7 @@ namespace NextPVR
std::map<int, std::pair<bool, bool>> m_channelDetails;
std::unordered_set<std::string> m_tvGroups;
std::unordered_set<std::string> m_radioGroups;
mutable std::recursive_mutex m_channelMutex;

private:
Channels() = default;
Expand All @@ -53,6 +56,11 @@ namespace NextPVR
std::string GetChannelIcon(int channelID);
const std::shared_ptr<InstanceSettings> m_settings;
Request& m_request;
tinyxml2::XMLError ReadCachedChannelList(tinyxml2::XMLDocument& doc);
tinyxml2::XMLError GetChannelList(tinyxml2::XMLDocument& doc);
time_t ReadChannelListCache(std::string& response);
bool ReloadChannelListCache(std::string& response, time_t updateTime);
bool LoadChannelDetails();
std::string m_checksumChannelList;
const std::string m_channelCacheFile;
};
} // namespace NextPVR
2 changes: 2 additions & 0 deletions src/EPG.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ EPG::EPG(const std::shared_ptr<InstanceSettings>& settings, Request& request, Re
PVR_ERROR EPG::GetEPGForChannel(int channelUid, time_t start, time_t end, kodi::addon::PVREPGTagsResultSet& results)
{
std::pair<bool, bool> channelDetail;
std::lock_guard<std::recursive_mutex> lock(m_channels.m_channelMutex);
channelDetail = m_channels.m_channelDetails[channelUid];
std::lock_guard<std::recursive_mutex> unlock(m_channels.m_channelMutex);
if (channelDetail.first == true)
{
kodi::Log(ADDON_LOG_DEBUG, "Skipping %d", channelUid);
Expand Down
4 changes: 2 additions & 2 deletions src/Recordings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ using namespace NextPVR::utilities;
/************************************************************/
/** Record handling **/

Recordings::Recordings(const std::shared_ptr<InstanceSettings>& settings, Request& request, Timers& timers, Channels& channels,
Recordings::Recordings(const std::shared_ptr<InstanceSettings>& settings, Request& request, Timers& timers, Channels& channels,
GenreMapper& genreMapper, cPVRClientNextPVR& pvrclient) :
m_settings(settings),
m_request(request),
Expand Down Expand Up @@ -521,7 +521,7 @@ bool Recordings::ParseNextPVRSubtitle(const tinyxml2::XMLNode *pRecordingNode, k
}
}
const std::string plot = tag.GetPlot();
if (tag.GetEpisodeNumber() == PVR_RECORDING_INVALID_SERIES_EPISODE && !plot.empty());
if (tag.GetEpisodeNumber() == PVR_RECORDING_INVALID_SERIES_EPISODE && !plot.empty())
{
static std::regex base_regex("^.*\\([eE][pP](\\d+)(?:/?(\\d+))?\\)");
std::smatch base_match;
Expand Down
1 change: 1 addition & 0 deletions src/Timers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ PVR_ERROR Timers::GetTimers(kodi::addon::PVRTimersResultSet& results)
int timerCount = 0;
// first add the recurring recordings
tinyxml2::XMLDocument doc;
std::lock_guard<std::recursive_mutex> lock(m_channels.m_channelMutex);
if (m_request.DoMethodRequest("recording.recurring.list", doc) == tinyxml2::XML_SUCCESS)
{
tinyxml2::XMLNode* recurringsNode = doc.RootElement()->FirstChildElement("recurrings");
Expand Down
Loading

0 comments on commit 5bbac98

Please sign in to comment.