Skip to content

Commit 5a40a48

Browse files
authored
Trigger channel list updates (#280)
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.
1 parent cb8e126 commit 5a40a48

6 files changed

+170
-59
lines changed

src/Channels.cpp

+143-53
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "utilities/XMLUtils.h"
1010
#include "pvrclient-nextpvr.h"
1111

12+
#include <kodi/General.h>
1213
#include <kodi/tools/StringUtils.h>
1314
#include "zlib.h"
1415

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

2021
Channels::Channels(const std::shared_ptr<InstanceSettings>& settings, Request& request) :
2122
m_settings(settings),
22-
m_request(request)
23+
m_request(request),
24+
m_channelCacheFile(kodi::vfs::TranslateSpecialProtocol(kodi::tools::StringUtils::Format("%s%s", settings->m_instanceDirectory.c_str(), "channel.cache")))
2325
{
2426
}
2527

2628
int Channels::GetNumChannels()
2729
{
2830
// Kodi polls this while recordings are open avoid calls to backend
31+
std::lock_guard<std::recursive_mutex> lock(m_channelMutex);
2932
int channelCount = m_channelDetails.size();
3033
if (channelCount == 0)
3134
{
3235
tinyxml2::XMLDocument doc;
33-
if (ReadCachedChannelList(doc) == tinyxml2::XML_SUCCESS)
36+
if (GetChannelList(doc) == tinyxml2::XML_SUCCESS)
3437
{
3538
tinyxml2::XMLNode* channelsNode = doc.RootElement()->FirstChildElement("channels");
3639
tinyxml2::XMLNode* pChannelNode;
@@ -91,17 +94,9 @@ PVR_ERROR Channels::GetChannels(bool radio, kodi::addon::PVRChannelsResultSet& r
9194
return PVR_ERROR_NO_ERROR;
9295
PVR_ERROR returnValue = PVR_ERROR_NO_ERROR;
9396
std::string stream;
94-
std::map<int, std::pair<bool, bool>>::iterator itr = m_channelDetails.begin();
95-
while (itr != m_channelDetails.end())
96-
{
97-
if (itr->second.second == (radio == true))
98-
itr = m_channelDetails.erase(itr);
99-
else
100-
++itr;
101-
}
10297

10398
tinyxml2::XMLDocument doc;
104-
if (ReadCachedChannelList(doc) == tinyxml2::XML_SUCCESS)
99+
if (GetChannelList(doc) == tinyxml2::XML_SUCCESS)
105100
{
106101
tinyxml2::XMLNode* channelsNode = doc.RootElement()->FirstChildElement("channels");
107102
tinyxml2::XMLNode* pChannelNode;
@@ -149,14 +144,6 @@ PVR_ERROR Channels::GetChannels(bool radio, kodi::addon::PVRChannelsResultSet& r
149144
if (iconFile.length() > 0)
150145
tag.SetIconPath(iconFile);
151146
}
152-
153-
// V5 has the EPG source type info.
154-
std::string epg;
155-
if (XMLUtils::GetString(pChannelNode, "epg", epg))
156-
m_channelDetails[tag.GetUniqueId()] = std::make_pair(epg == "None", tag.GetIsRadio());
157-
else
158-
m_channelDetails[tag.GetUniqueId()] = std::make_pair(false, tag.GetIsRadio());
159-
160147
// transfer channel to XBMC
161148
results.Add(tag);
162149
}
@@ -168,7 +155,6 @@ PVR_ERROR Channels::GetChannels(bool radio, kodi::addon::PVRChannelsResultSet& r
168155
return returnValue;
169156
}
170157

171-
172158
/************************************************************/
173159
/** Channel group handling **/
174160

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

@@ -192,7 +179,7 @@ PVR_ERROR Channels::GetChannelGroups(bool radio, kodi::addon::PVRChannelGroupsRe
192179
{
193180
if (radio && !m_settings->m_showRadio)
194181
return PVR_ERROR_NO_ERROR;
195-
182+
std::lock_guard<std::recursive_mutex> lock(m_channelMutex);
196183
PVR_ERROR returnValue = PVR_ERROR_NO_ERROR;
197184
int priority = 1;
198185

@@ -201,7 +188,7 @@ PVR_ERROR Channels::GetChannelGroups(bool radio, kodi::addon::PVRChannelGroupsRe
201188
selectedGroups.clear();
202189
bool hasAllChannels = false;
203190
tinyxml2::XMLDocument doc;
204-
if (ReadCachedChannelList(doc) == tinyxml2::XML_SUCCESS)
191+
if (GetChannelList(doc) == tinyxml2::XML_SUCCESS)
205192
{
206193
tinyxml2::XMLNode* channelsNode = doc.RootElement()->FirstChildElement("channels");
207194
tinyxml2::XMLNode* pChannelNode;
@@ -289,7 +276,7 @@ PVR_ERROR Channels::GetChannelGroupMembers(const kodi::addon::PVRChannelGroup& g
289276
tinyxml2::XMLError retCode;
290277
if (group.GetGroupName() == GetAllChannelsGroupName(group.GetIsRadio()))
291278
{
292-
retCode = ReadCachedChannelList(doc);
279+
retCode = GetChannelList(doc);
293280
}
294281
else
295282
{
@@ -299,6 +286,7 @@ PVR_ERROR Channels::GetChannelGroupMembers(const kodi::addon::PVRChannelGroup& g
299286

300287
if (retCode == tinyxml2::XML_SUCCESS)
301288
{
289+
std::lock_guard<std::recursive_mutex> lock(m_channelMutex);
302290
tinyxml2::XMLNode* channelsNode = doc.RootElement()->FirstChildElement("channels");
303291
tinyxml2::XMLNode* pChannelNode;
304292
for (pChannelNode = channelsNode->FirstChildElement("channel"); pChannelNode; pChannelNode = pChannelNode->NextSiblingElement())
@@ -389,50 +377,152 @@ void Channels::LoadLiveStreams()
389377
}
390378
}
391379
}
392-
bool Channels::CacheAllChannels(time_t updateTime)
380+
bool Channels::ChannelCacheChanged(time_t updateTime)
393381
{
382+
std::string checksum = m_checksumChannelList;
394383
std::string response;
395-
const std::string filename = kodi::tools::StringUtils::Format("%s%s", m_settings->m_instanceDirectory.c_str(), "channel.cache");
396-
gzFile gz_file;
397-
struct { time_t update; unsigned long size; } header{0,0};
398-
if (kodi::vfs::FileExists(filename))
384+
const time_t cacheTime = ReadChannelListCache(response);
385+
// on first load need to cache details
386+
if (cacheTime != 0 && m_channelDetails.empty())
387+
LoadChannelDetails();
388+
389+
if (updateTime == cacheTime)
390+
return false;
391+
else
399392
{
400-
gz_file = gzopen(kodi::vfs::TranslateSpecialProtocol(filename).c_str(), "rb");
401-
gzread(gz_file, (void*)&header, sizeof(header));
393+
// change EPG updateTime
394+
response.clear();
395+
// Get new channel list but return on error
396+
if (!ReloadChannelListCache(response, updateTime))
397+
return false;
398+
}
399+
// checksum will be empty on first call
400+
return checksum != m_checksumChannelList;
401+
}
402+
403+
time_t Channels::ReadChannelListCache(std::string& response)
404+
{
405+
time_t updateTime = 0;
406+
if (kodi::vfs::FileExists(m_channelCacheFile))
407+
{
408+
gzFile gz_file = gzopen(m_channelCacheFile.c_str(), "rb");
409+
if (gz_file != NULL)
410+
{
411+
CacheHeader header{ 0,0 };
412+
if (gzread(gz_file, (void*)&header, sizeof(CacheHeader)) == sizeof(CacheHeader))
413+
{
414+
response.resize(header.size);
415+
if (gzread(gz_file, (void*)response.data(), header.size) == header.size)
416+
{
417+
m_checksumChannelList = kodi::GetMD5(response);
418+
updateTime = header.updateTime;
419+
}
420+
}
421+
}
402422
gzclose(gz_file);
403-
if (updateTime == header.update)
423+
if (updateTime == 0)
404424
{
405-
return true;
425+
kodi::Log(ADDON_LOG_WARNING, "Remove invalid cache file.");
426+
kodi::vfs::DeleteFile(m_channelCacheFile);
406427
}
407428
}
429+
return updateTime;
430+
}
431+
432+
bool Channels::ReloadChannelListCache(std::string& response, time_t updateTime)
433+
{
434+
bool rc = false;
435+
gzFile gz_file;
436+
m_checksumChannelList.clear();
408437
if (m_request.DoRequest("/service?method=channel.list&extras=true", response) == HTTP_OK)
409438
{
410-
gz_file = gzopen(kodi::vfs::TranslateSpecialProtocol(filename).c_str(), "wb");
411-
header.size = sizeof(char) * response.size();
412-
header.update = updateTime - m_settings->m_serverTimeOffset;
413-
gzwrite(gz_file, (void*)&header, sizeof(header));
414-
gzwrite(gz_file, (void*)(response.c_str()), header.size);
439+
gz_file = gzopen(m_channelCacheFile.c_str(), "wb");
440+
if (gz_file != NULL)
441+
{
442+
CacheHeader header{ 0,0 };
443+
header.size = response.size();
444+
header.updateTime = updateTime;
445+
if (gzwrite(gz_file, (void*)&header, sizeof(CacheHeader)) == sizeof(CacheHeader))
446+
{
447+
if (gzwrite(gz_file, (void*)(response.c_str()), header.size) == header.size)
448+
{
449+
m_checksumChannelList = kodi::GetMD5(response);
450+
rc = true;
451+
}
452+
}
453+
}
415454
gzclose(gz_file);
416-
return true;
417455
}
418-
return false;
456+
if (!rc)
457+
kodi::Log(ADDON_LOG_ERROR, "Could not write channel cache");
458+
459+
return rc;
419460
}
420461

421-
tinyxml2::XMLError Channels::ReadCachedChannelList(tinyxml2::XMLDocument& doc)
462+
tinyxml2::XMLError Channels::GetChannelList(tinyxml2::XMLDocument& doc)
422463
{
423464
auto start = std::chrono::steady_clock::now();
424465
std::string response;
425-
const std::string filename = kodi::tools::StringUtils::Format("%s%s", m_settings->m_instanceDirectory.c_str(), "channel.cache");
426-
struct { time_t update; unsigned long size; } header{0,0};
427-
gzFile gz_file = gzopen(kodi::vfs::TranslateSpecialProtocol(filename).c_str(), "rb");
428-
gzread(gz_file, (void*)&header, sizeof(header));
429-
response.resize(header.size / sizeof(char));
430-
gzread(gz_file, (void*)response.data(), header.size);
431-
gzclose(gz_file);
432-
tinyxml2::XMLError xmlCheck = doc.Parse(response.c_str());
433-
if (doc.Parse(response.c_str()) != tinyxml2::XML_SUCCESS)
434-
return m_request.DoMethodRequest("channel.list&extras=true", doc);
435-
int milliseconds = static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count());
436-
kodi::Log(ADDON_LOG_DEBUG, "ReadCachedChannelList %d %d %d %d", m_settings->m_instanceNumber, xmlCheck, response.length(), milliseconds);
437-
return xmlCheck;
466+
if (ReadChannelListCache(response) != 0)
467+
{
468+
tinyxml2::XMLError xmlCheck = doc.Parse(response.c_str());
469+
if (xmlCheck == tinyxml2::XML_SUCCESS)
470+
{
471+
int milliseconds = static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count());
472+
kodi::Log(ADDON_LOG_DEBUG, "GetChannelList %d %d %d %d", m_settings->m_instanceNumber, xmlCheck, response.length(), milliseconds);
473+
return xmlCheck;
474+
}
475+
}
476+
kodi::Log(ADDON_LOG_ERROR, "Cannot read channel cache");
477+
return m_request.DoMethodRequest("channel.list&extras=true", doc);
478+
}
479+
480+
bool Channels::ResetChannelCache(time_t updateTime)
481+
{
482+
if (ChannelCacheChanged(updateTime) && !m_checksumChannelList.empty())
483+
{
484+
// m_checksumChannelList will be empty on error
485+
std::lock_guard<std::recursive_mutex> lock(m_channelMutex);
486+
auto oldDetails = m_channelDetails;
487+
m_channelDetails.clear();
488+
LoadChannelDetails();
489+
for( const auto &details : oldDetails)
490+
{
491+
if (m_channelDetails.find(details.first) == m_channelDetails.end())
492+
{
493+
DeleteChannelIcon(details.first);
494+
}
495+
}
496+
return true;
497+
}
498+
return false;
499+
}
500+
501+
bool Channels::LoadChannelDetails()
502+
{
503+
tinyxml2::XMLDocument doc;
504+
if (GetChannelList(doc) == tinyxml2::XML_SUCCESS)
505+
{
506+
tinyxml2::XMLNode* channelsNode = doc.RootElement()->FirstChildElement("channels");
507+
tinyxml2::XMLNode* pChannelNode;
508+
for (pChannelNode = channelsNode->FirstChildElement("channel"); pChannelNode; pChannelNode = pChannelNode->NextSiblingElement())
509+
{
510+
std::string buffer;
511+
bool isRadio = false;
512+
XMLUtils::GetString(pChannelNode, "type", buffer);
513+
if (buffer == "0xa")
514+
{
515+
if (!m_settings->m_showRadio)
516+
continue;
517+
isRadio = true;
518+
}
519+
std::string epg;
520+
if (XMLUtils::GetString(pChannelNode, "epg", epg))
521+
m_channelDetails[XMLUtils::GetUIntValue(pChannelNode, "id")] = std::make_pair(epg == "None", isRadio);
522+
else
523+
m_channelDetails[XMLUtils::GetUIntValue(pChannelNode, "id")] = std::make_pair(false, isRadio);
524+
}
525+
return true;
526+
}
527+
return false;
438528
}

src/Channels.h

+10-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
namespace NextPVR
1616
{
17+
typedef struct { time_t updateTime; size_t size; } CacheHeader;
1718

1819
class ATTR_DLL_LOCAL Channels
1920
{
@@ -25,9 +26,10 @@ namespace NextPVR
2526
/* Channel handling */
2627
int GetNumChannels();
2728

28-
bool CacheAllChannels(time_t updateTime);
29+
bool ChannelCacheChanged(time_t updateTime);
2930

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

4750
private:
4851
Channels() = default;
@@ -53,6 +56,11 @@ namespace NextPVR
5356
std::string GetChannelIcon(int channelID);
5457
const std::shared_ptr<InstanceSettings> m_settings;
5558
Request& m_request;
56-
tinyxml2::XMLError ReadCachedChannelList(tinyxml2::XMLDocument& doc);
59+
tinyxml2::XMLError GetChannelList(tinyxml2::XMLDocument& doc);
60+
time_t ReadChannelListCache(std::string& response);
61+
bool ReloadChannelListCache(std::string& response, time_t updateTime);
62+
bool LoadChannelDetails();
63+
std::string m_checksumChannelList;
64+
const std::string m_channelCacheFile;
5765
};
5866
} // namespace NextPVR

src/EPG.cpp

+4-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ EPG::EPG(const std::shared_ptr<InstanceSettings>& settings, Request& request, Re
3131
PVR_ERROR EPG::GetEPGForChannel(int channelUid, time_t start, time_t end, kodi::addon::PVREPGTagsResultSet& results)
3232
{
3333
std::pair<bool, bool> channelDetail;
34-
channelDetail = m_channels.m_channelDetails[channelUid];
34+
{
35+
std::lock_guard<std::recursive_mutex> lock(m_channels.m_channelMutex);
36+
channelDetail = m_channels.m_channelDetails[channelUid];
37+
}
3538
if (channelDetail.first == true)
3639
{
3740
kodi::Log(ADDON_LOG_DEBUG, "Skipping %d", channelUid);

src/Recordings.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ using namespace NextPVR::utilities;
2222
/************************************************************/
2323
/** Record handling **/
2424

25-
Recordings::Recordings(const std::shared_ptr<InstanceSettings>& settings, Request& request, Timers& timers, Channels& channels,
25+
Recordings::Recordings(const std::shared_ptr<InstanceSettings>& settings, Request& request, Timers& timers, Channels& channels,
2626
GenreMapper& genreMapper, cPVRClientNextPVR& pvrclient) :
2727
m_settings(settings),
2828
m_request(request),
@@ -521,7 +521,7 @@ bool Recordings::ParseNextPVRSubtitle(const tinyxml2::XMLNode *pRecordingNode, k
521521
}
522522
}
523523
const std::string plot = tag.GetPlot();
524-
if (tag.GetEpisodeNumber() == PVR_RECORDING_INVALID_SERIES_EPISODE && !plot.empty());
524+
if (tag.GetEpisodeNumber() == PVR_RECORDING_INVALID_SERIES_EPISODE && !plot.empty())
525525
{
526526
static std::regex base_regex("^.*\\([eE][pP](\\d+)(?:/?(\\d+))?\\)");
527527
std::smatch base_match;

src/Timers.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ PVR_ERROR Timers::GetTimers(kodi::addon::PVRTimersResultSet& results)
7878
int timerCount = 0;
7979
// first add the recurring recordings
8080
tinyxml2::XMLDocument doc;
81+
std::lock_guard<std::recursive_mutex> lock(m_channels.m_channelMutex);
8182
if (m_request.DoMethodRequest("recording.recurring.list", doc) == tinyxml2::XML_SUCCESS)
8283
{
8384
tinyxml2::XMLNode* recurringsNode = doc.RootElement()->FirstChildElement("recurrings");

0 commit comments

Comments
 (0)