From d814e5af2c2ef4277cadbc2b430509488de81c4f 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/addon.xml.in | 2 +- pvr.nextpvr/changelog.txt | 4 + 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 ++++++ 12 files changed, 357 insertions(+), 28 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 8dd9170a..ca9d91e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,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) @@ -48,6 +49,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/addon.xml.in b/pvr.nextpvr/addon.xml.in index a259390c..1492065d 100644 --- a/pvr.nextpvr/addon.xml.in +++ b/pvr.nextpvr/addon.xml.in @@ -1,7 +1,7 @@ @ADDON_DEPENDS@ diff --git a/pvr.nextpvr/changelog.txt b/pvr.nextpvr/changelog.txt index b1e27594..29223fa6 100644 --- a/pvr.nextpvr/changelog.txt +++ b/pvr.nextpvr/changelog.txt @@ -1,3 +1,7 @@ +v21.3.0 +- Support EPG genre sub type for better genre display +- Add genre type and subtype to recordings + v21.2.0 - Start timeshift in realtime for radio playback - Add support for episode and episode part parsing 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 d02d43f4..b9f7420b 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_sub_type")); } - 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 e717e692..43f47b0b 100644 --- a/src/EPG.h +++ b/src/EPG.h @@ -12,13 +12,14 @@ #include #include "Channels.h" #include "Recordings.h" +#include "utilities/GenreMapper.h" 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: @@ -30,5 +31,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 dd764305..5977cab2 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) { @@ -443,11 +445,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 19dc4698..1792749d 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 97401db1..f45c66ab 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..e254fb7c --- /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)); +}; 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