diff --git a/CMakeLists.txt b/CMakeLists.txt index 0cc65450..6dd5abc3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ set(NEXTPVR_SOURCES src/addon.cpp src/buffers/ClientTimeshift.cpp src/buffers/RecordingBuffer.cpp src/buffers/CircularBuffer.cpp + src/utilities/GenreMapper.cpp src/utilities/SettingsMigration.cpp src/buffers/Seeker.cpp) @@ -50,6 +51,7 @@ set(NEXTPVR_HEADERS src/addon.h src/buffers/RecordingBuffer.h src/buffers/CircularBuffer.h src/buffers/Seeker.h + src/utilities/GenreMapper.h src/utilities/SettingsMigration.h src/utilities/XMLUtils.h) diff --git a/pvr.nextpvr/resources/genre-mapping.xml b/pvr.nextpvr/resources/genre-mapping.xml new file mode 100644 index 00000000..57f7841f --- /dev/null +++ b/pvr.nextpvr/resources/genre-mapping.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/EPG.cpp b/src/EPG.cpp index 499b2940..a56b8040 100644 --- a/src/EPG.cpp +++ b/src/EPG.cpp @@ -19,10 +19,11 @@ using namespace NextPVR::utilities; /************************************************************/ /** EPG handling */ -EPG::EPG(const std::shared_ptr& settings, Request& request, Recordings& recordings, Channels& channels) : +EPG::EPG(const std::shared_ptr& settings, Request& request, Recordings& recordings, Channels& channels,GenreMapper& genreMapper) : m_settings(settings), m_request(request), m_recordings(recordings), + m_genreMapper(genreMapper), m_channels(channels) { } @@ -51,7 +52,7 @@ PVR_ERROR EPG::GetEPGForChannel(int channelUid, time_t start, time_t end, kodi:: if (m_request.DoMethodRequest(request, doc) == tinyxml2::XML_SUCCESS) { tinyxml2::XMLNode* listingsNode = doc.RootElement()->FirstChildElement("listings"); - for (tinyxml2::XMLNode* pListingNode = listingsNode->FirstChildElement("l"); pListingNode; pListingNode = pListingNode->NextSiblingElement()) + for (const tinyxml2::XMLNode* pListingNode = listingsNode->FirstChildElement("l"); pListingNode; pListingNode = pListingNode->NextSiblingElement()) { kodi::addon::PVREPGTag broadcast; std::string title; @@ -82,6 +83,14 @@ PVR_ERROR EPG::GetEPGForChannel(int channelUid, time_t start, time_t end, kodi:: broadcast.SetStartTime(stol(startTime)); broadcast.SetUniqueBroadcastId(stoi(endTime)); broadcast.SetEndTime(stol(endTime)); + + static std::regex yearRegex("^(.+[12]\\d{3})\\n"); + std::smatch base_match; + if (std::regex_search(description, base_match, yearRegex)) + { + kodi::tools::StringUtils::Replace(description, base_match[0].str(), base_match[1].str() + " "); + } + broadcast.SetPlot(description); std::string artworkPath; @@ -111,23 +120,13 @@ PVR_ERROR EPG::GetEPGForChannel(int channelUid, time_t start, time_t end, kodi:: broadcast.SetGenreSubType(XMLUtils::GetIntValue(pListingNode, "genre_subtype")); } - std::string allGenres; - if (XMLUtils::GetAdditiveString(pListingNode->FirstChildElement("genres"), "genre", EPG_STRING_TOKEN_SEPARATOR, allGenres, true)) - { - if (allGenres.find(EPG_STRING_TOKEN_SEPARATOR) != std::string::npos) - { - if (broadcast.GetGenreType() != EPG_GENRE_USE_STRING) - { - broadcast.SetGenreSubType(EPG_GENRE_USE_STRING); - } - broadcast.SetGenreDescription(allGenres); - } - else if (m_settings->m_genreString && broadcast.GetGenreSubType() != EPG_GENRE_USE_STRING) - { - broadcast.SetGenreDescription(allGenres); - broadcast.SetGenreSubType(EPG_GENRE_USE_STRING); - } + NextPVR::GenreBlock genreBlock = { sGenre, broadcast.GetGenreType(), EPG_EVENT_CONTENTMASK_UNDEFINED }; + if (m_genreMapper.ParseAllGenres(pListingNode, genreBlock)) + { + broadcast.SetGenreDescription(genreBlock.description); + broadcast.SetGenreType(genreBlock.genreType); + broadcast.SetGenreSubType(genreBlock.genreSubType); } int season{EPG_TAG_INVALID_SERIES_EPISODE}; diff --git a/src/EPG.h b/src/EPG.h index 3c38ec23..2b1d6215 100644 --- a/src/EPG.h +++ b/src/EPG.h @@ -12,6 +12,7 @@ #include #include "Channels.h" #include "Recordings.h" +#include "utilities/GenreMapper.h" namespace NextPVR { @@ -19,7 +20,7 @@ namespace NextPVR class ATTR_DLL_LOCAL EPG { public: - EPG(const std::shared_ptr& settings, Request& request, Recordings& recordings, Channels& channels); + EPG(const std::shared_ptr& settings, Request& request, Recordings& recordings, Channels& channels, GenreMapper& genreMapper); PVR_ERROR GetEPGForChannel(int channelUid, time_t start, time_t end, kodi::addon::PVREPGTagsResultSet& results); private: @@ -31,5 +32,6 @@ namespace NextPVR Request& m_request; Recordings& m_recordings; Channels& m_channels; + GenreMapper& m_genreMapper; }; } // namespace NextPVR diff --git a/src/Recordings.cpp b/src/Recordings.cpp index d7a27da3..5e5ff164 100644 --- a/src/Recordings.cpp +++ b/src/Recordings.cpp @@ -22,11 +22,13 @@ using namespace NextPVR::utilities; /************************************************************/ /** Record handling **/ -Recordings::Recordings(const std::shared_ptr& settings, Request& request, Timers& timers, Channels& channels, cPVRClientNextPVR& pvrclient) : +Recordings::Recordings(const std::shared_ptr& settings, Request& request, Timers& timers, Channels& channels, + GenreMapper& genreMapper, cPVRClientNextPVR& pvrclient) : m_settings(settings), m_request(request), m_timers(timers), m_channels(channels), + m_genreMapper(genreMapper), m_pvrclient(pvrclient) { @@ -441,11 +443,13 @@ bool Recordings::UpdatePvrRecording(const tinyxml2::XMLNode* pRecordingNode, kod tag.SetFanartPath(artworkPath + "&prefer=fanart"); tag.SetThumbnailPath(artworkPath + "&prefer=poster"); } - if (XMLUtils::GetAdditiveString(pRecordingNode->FirstChildElement("genres"), "genre", EPG_STRING_TOKEN_SEPARATOR, buffer, true)) + + NextPVR::GenreBlock genreBlock = { "", EPG_EVENT_CONTENTMASK_UNDEFINED, EPG_EVENT_CONTENTMASK_UNDEFINED }; + if (m_genreMapper.ParseAllGenres(pRecordingNode, genreBlock)) { - tag.SetGenreType(EPG_GENRE_USE_STRING); - tag.SetGenreSubType(0); - tag.SetGenreDescription(buffer); + tag.SetGenreDescription(genreBlock.description); + tag.SetGenreType(genreBlock.genreType); + tag.SetGenreSubType(genreBlock.genreSubType); } std::string significance; diff --git a/src/Recordings.h b/src/Recordings.h index ce558c75..1bc8e771 100644 --- a/src/Recordings.h +++ b/src/Recordings.h @@ -10,6 +10,7 @@ #include "BackendRequest.h" #include "Timers.h" +#include "utilities/GenreMapper.h" #include @@ -21,7 +22,8 @@ namespace NextPVR { public: - Recordings(const std::shared_ptr& settings, Request& request, Timers& timers, Channels& channels, cPVRClientNextPVR& pvrclent); + Recordings(const std::shared_ptr& settings, Request& request, Timers& timers, Channels& channels, + GenreMapper& genreMapper, cPVRClientNextPVR& pvrclent); /* Recording handling **/ PVR_ERROR GetRecordingsAmount(bool deleted, int& amount); PVR_ERROR GetDriveSpace(uint64_t& total, uint64_t& used); @@ -48,6 +50,7 @@ namespace NextPVR Request& m_request; Timers& m_timers; Channels& m_channels; + GenreMapper& m_genreMapper; cPVRClientNextPVR& m_pvrclient; // update these at end of counting loop can be called during action diff --git a/src/pvrclient-nextpvr.cpp b/src/pvrclient-nextpvr.cpp index d5542294..788365db 100644 --- a/src/pvrclient-nextpvr.cpp +++ b/src/pvrclient-nextpvr.cpp @@ -91,9 +91,10 @@ cPVRClientNextPVR::cPVRClientNextPVR(const CNextPVRAddon& base, const kodi::addo m_request(m_settings), m_channels(m_settings, m_request), m_timers(m_settings, m_request, m_channels, *this), - m_recordings(m_settings, m_request, m_timers, m_channels, *this), + m_recordings(m_settings, m_request, m_timers, m_channels,m_genreMapper, *this), m_menuhook(m_settings, m_recordings, m_channels, *this), - m_epg(m_settings, m_request, m_recordings, m_channels) + m_genreMapper(m_settings), + m_epg(m_settings, m_request, m_recordings, m_channels, m_genreMapper) { if (!kodi::vfs::DirectoryExists(m_settings->m_instanceDirectory)) { diff --git a/src/pvrclient-nextpvr.h b/src/pvrclient-nextpvr.h index 02aa0284..6b140ca1 100644 --- a/src/pvrclient-nextpvr.h +++ b/src/pvrclient-nextpvr.h @@ -21,6 +21,7 @@ #include "Recordings.h" #include "InstanceSettings.h" #include "Timers.h" +#include "utilities/GenreMapper.h" #include "buffers/ClientTimeshift.h" #include "buffers/DummyBuffer.h" #include "buffers/RecordingBuffer.h" @@ -156,6 +157,7 @@ class ATTR_DLL_LOCAL cPVRClientNextPVR : public kodi::addon::CInstancePVRClient EPG m_epg; MenuHook m_menuhook; Recordings m_recordings; + GenreMapper m_genreMapper; Timers m_timers; void SetConnectionState(PVR_CONNECTION_STATE state, std::string displayMessage = ""); diff --git a/src/utilities/GenreMapper.cpp b/src/utilities/GenreMapper.cpp new file mode 100644 index 00000000..6a054ef0 --- /dev/null +++ b/src/utilities/GenreMapper.cpp @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2005-2024 Team Kodi (https://kodi.tv) + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSE.md for more information. + */ + + +#include +#include "GenreMapper.h" +#include "XMLUtils.h" +#include "tinyxml2.h" + +#include + +using namespace NextPVR; +using namespace NextPVR::utilities; + +GenreMapper::GenreMapper(const std::shared_ptr& settings) : m_settings(settings) +{ + LoadGenreTextMappingFiles(); +} + +GenreMapper::~GenreMapper() {} + + +bool GenreMapper::IsEnabled() +{ + return !m_settings->m_genreString; +} + +int GenreMapper::GetGenreTypeFromCombined(int combinedGenreType) +{ + return combinedGenreType & 0xF0; +} + +int GenreMapper::GetGenreSubTypeFromCombined(int combinedGenreType) +{ + return combinedGenreType & 0x0F; +} + + +int GenreMapper::LookupGenreValueInMaps(const std::string& genreText) +{ + int genreType = EPG_EVENT_CONTENTMASK_UNDEFINED; + + auto genreMapSearch = m_genreMap.find(genreText); + if (genreMapSearch != m_genreMap.end()) + { + genreType = genreMapSearch->second; + } + return genreType; +} + +void GenreMapper::LoadGenreTextMappingFiles() +{ + if (!LoadTextToIdGenreFile(GENRE_KODI_DVB_FILEPATH, m_genreMap)) + kodi::Log(ADDON_LOG_ERROR, "%s Could not load text to genre id file: %s", __func__, GENRE_KODI_DVB_FILEPATH.c_str()); + +} + +bool GenreMapper::ParseAllGenres(const tinyxml2::XMLNode* node, GenreBlock& genreBlock) +{ + std::string allGenres; + if (XMLUtils::GetAdditiveString(node->FirstChildElement("genres"), "genre", EPG_STRING_TOKEN_SEPARATOR, allGenres, true)) + { + if (allGenres.find(EPG_STRING_TOKEN_SEPARATOR) != std::string::npos) + { + if (IsEnabled()) + { + std::vector genreCodes = kodi::tools::StringUtils::Split(allGenres, EPG_STRING_TOKEN_SEPARATOR); + if (genreCodes.size() == 2) + { + if (genreBlock.genreType == EPG_EVENT_CONTENTMASK_UNDEFINED) + genreBlock.genreType = GetGenreType(genreCodes[0]); + + if (genreCodes[0] == "Show / Game show") + genreBlock.genreType = 48; + + if (genreBlock.genreType == GetGenreType(genreCodes[0])) + { + if (genreBlock.genreType == GetGenreType(genreCodes[1])) + genreBlock.genreSubType = GetGenreSubType(genreCodes[1]); + } + } + } + if (genreBlock.genreSubType == EPG_EVENT_CONTENTMASK_UNDEFINED) + { + if (genreBlock.genreType != EPG_GENRE_USE_STRING) + { + genreBlock.genreType = EPG_GENRE_USE_STRING; + } + genreBlock.description = allGenres; + } + } + else if (!IsEnabled() && genreBlock.genreSubType != EPG_GENRE_USE_STRING) + { + genreBlock.description = allGenres; + genreBlock.genreType = EPG_GENRE_USE_STRING; + } + + return true; + } + return false; +} + +bool GenreMapper::LoadTextToIdGenreFile(const std::string& xmlFile, std::map& map) +{ + map.clear(); + + if (!kodi::vfs::FileExists(xmlFile.c_str())) + { + kodi::Log(ADDON_LOG_ERROR, "%s No XML file found: %s", __func__, xmlFile.c_str()); + return false; + } + + kodi::Log(ADDON_LOG_DEBUG, "%s Loading XML File: %s", __func__, xmlFile.c_str()); + + std::string fileContents; + kodi::vfs::CFile loadXml; + if (loadXml.OpenFile(xmlFile, ADDON_READ_NO_CACHE)) + { + char buffer[1025] = { 0 }; + int count; + while ((count = loadXml.Read(buffer, 1024))) + { + fileContents.append(buffer, count); + } + } + loadXml.Close(); + + tinyxml2::XMLDocument xmlDoc; + + if (xmlDoc.Parse(fileContents.c_str()) != tinyxml2::XML_SUCCESS) + { + kodi::Log(ADDON_LOG_ERROR, "%s Unable to parse XML: %s at line %d", __func__, xmlDoc.ErrorStr(), xmlDoc.ErrorLineNum()); + return false; + } + + tinyxml2::XMLHandle hDoc(&xmlDoc); + + tinyxml2::XMLElement* pNode = hDoc.FirstChildElement("translations").ToElement(); + + if (!pNode) + { + kodi::Log(ADDON_LOG_ERROR, "%s Could not find element", __func__); + return false; + } + + pNode = pNode->FirstChildElement("genre"); + + if (!pNode) + { + kodi::Log(ADDON_LOG_ERROR, "%s Could not find element", __func__); + return false; + } + + for (; pNode != nullptr; pNode = pNode->NextSiblingElement("genre")) + { + std::string textMapping; + + textMapping = pNode->Attribute("name"); + int type = atoi(pNode->Attribute("type")); + int subtype = atoi(pNode->Attribute("subtype")); + if (!textMapping.empty()) + { + map.insert({ textMapping, type | subtype }); + kodi::Log(ADDON_LOG_DEBUG, "%s Read Text Mapping text=%s, targetId=%#02X", __func__, textMapping.c_str(), type|subtype); + } + } + return true; +} + +int GenreMapper::GetGenreType(std::string code) +{ + return GetGenreTypeFromCombined(LookupGenreValueInMaps(code)); +}; + +int GenreMapper::GetGenreSubType(std::string code) +{ + return GetGenreSubTypeFromCombined(LookupGenreValueInMaps(code)); +}; \ No newline at end of file diff --git a/src/utilities/GenreMapper.h b/src/utilities/GenreMapper.h new file mode 100644 index 00000000..1a70df98 --- /dev/null +++ b/src/utilities/GenreMapper.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005-2024 Team Kodi (https://kodi.tv) + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSE.md for more information. + */ + +#pragma once + +#include +#include +#include "../InstanceSettings.h" +#include +#include + + +namespace NextPVR +{ + struct GenreBlock { std::string description; int genreType; int genreSubType; }; + static const std::string GENRE_KODI_DVB_FILEPATH = "special://home/addons/pvr.nextpvr/resources/genre-mapping.xml"; + class ATTR_DLL_LOCAL GenreMapper + { + + public: + GenreMapper(const std::shared_ptr& settings); + ~GenreMapper(); + + int GetGenreType(std::string code); + int GetGenreSubType(std::string code); + bool ParseAllGenres(const tinyxml2::XMLNode* node, GenreBlock& genreBlock); + bool IsEnabled(); + + private: + GenreMapper() = default; + GenreMapper(GenreMapper const&) = delete; + void operator=(GenreMapper const&) = delete; + + int GetGenreTypeFromCombined(int combinedGenreType); + int GetGenreSubTypeFromCombined(int combinedGenreType); + int LookupGenreValueInMaps(const std::string& genreText); + + void LoadGenreTextMappingFiles(); + bool LoadTextToIdGenreFile(const std::string& xmlFile, std::map& map); + std::map m_genreMap; + const std::shared_ptr m_settings; + }; +} // namespace NextPVR