From 30486d2bc6b493f2393b4de0293abf693d49aa35 Mon Sep 17 00:00:00 2001 From: Martin Vallevand Date: Thu, 19 Dec 2024 10:35:29 -0500 Subject: [PATCH] Genre and sub genre mapping Use the genre mapping xml file to map genre sub types from the English codes send by NextPVR. Extend genre type and sub type matching to recordings. --- CMakeLists.txt | 2 + pvr.nextpvr/resources/genre-mapping.xml | 83 +++++++++++ src/EPG.cpp | 35 +++-- src/EPG.h | 4 +- src/Recordings.cpp | 14 +- src/Recordings.h | 5 +- src/pvrclient-nextpvr.cpp | 5 +- src/pvrclient-nextpvr.h | 2 + src/utilities/GenreMapper.cpp | 182 ++++++++++++++++++++++++ src/utilities/GenreMapper.h | 47 ++++++ 10 files changed, 352 insertions(+), 27 deletions(-) create mode 100644 pvr.nextpvr/resources/genre-mapping.xml create mode 100644 src/utilities/GenreMapper.cpp create mode 100644 src/utilities/GenreMapper.h 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