From ad640494164387c5706bb53518b30b4b15c2ed25 Mon Sep 17 00:00:00 2001 From: djp952 Date: Sat, 18 Mar 2023 22:38:31 -0400 Subject: [PATCH] Adapt to XMLTV Electronic Program Guide (EPG) schema changes --- pvr.hdhomerundvr/changelog.txt | 3 ++ src/addon.cpp | 62 +++++++++------------------------ src/database.cpp | 27 +++++++++++---- src/dbextension.cpp | 63 +++++++++++++++++----------------- src/dbtypes.h | 4 ++- src/version.ini | 2 +- 6 files changed, 75 insertions(+), 86 deletions(-) diff --git a/pvr.hdhomerundvr/changelog.txt b/pvr.hdhomerundvr/changelog.txt index 4746f88..3823bae 100644 --- a/pvr.hdhomerundvr/changelog.txt +++ b/pvr.hdhomerundvr/changelog.txt @@ -1,3 +1,6 @@ +v4.9.1 (2023.03.18) + - Adapt to XMLTV Electronic Program Guide (EPG) schema changes + v4.9.0 (2023.01.08) - Remove dependency on OpenSSL library - Add dependency on wolfSSL library version 5.5.4 diff --git a/src/addon.cpp b/src/addon.cpp index 6f9dd27..e02b058 100644 --- a/src/addon.cpp +++ b/src/addon.cpp @@ -1069,9 +1069,6 @@ void addon::push_listings(scalar_condition const& cancel) // Abort the enumeration if the cancellation scalar_condition has been set if(cancel.test(true) == true) { cancelenum = true; return; } - // Determine if the episode is a repeat. If the program type is "EP" or "SH" and isnew is *not* set, flag it as a repeat - bool isrepeat = ((item.programtype != nullptr) && ((strcasecmp(item.programtype, "EP") == 0) || (strcasecmp(item.programtype, "SH") == 0)) && (item.isnew == false)); - // UniqueBroadcastId (required) assert(item.broadcastid > EPG_TAG_INVALID_UID); epgtag.SetUniqueBroadcastId(item.broadcastid); @@ -1094,8 +1091,8 @@ void addon::push_listings(scalar_condition const& cancel) // Year // - // Only report for program type "MV" (Movies) - if((item.programtype != nullptr) && (strcasecmp(item.programtype, "MV") == 0)) epgtag.SetYear(item.year); + // Only report for program type MOVIE + if((item.programtype != nullptr) && (strcasecmp(item.programtype, "MOVIE") == 0)) epgtag.SetYear(item.year); // IconPath if(item.iconurl != nullptr) epgtag.SetIconPath(item.iconurl); @@ -1108,14 +1105,9 @@ void addon::push_listings(scalar_condition const& cancel) // FirstAired // - // Only report for program types "EP" (Series Episode) and "SH" (Show) - if((item.programtype != nullptr) && ((strcasecmp(item.programtype, "EP") == 0) || (strcasecmp(item.programtype, "SH") == 0))) { - - // Special case: don't report original air date for listings of type EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS - // unless series/episode information is available - if((item.genretype != EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS) || ((item.seriesnumber >= 1) || (item.episodenumber >= 1))) - if(item.originalairdate != nullptr) epgtag.SetFirstAired(item.originalairdate); - } + // Only report for program types other than MOVIE + if((item.programtype != nullptr) && (item.originalairdate != nullptr) && (strcasecmp(item.programtype, "MOVIE") != 0)) + epgtag.SetFirstAired(item.originalairdate); // SeriesNumber epgtag.SetSeriesNumber(item.seriesnumber); @@ -1130,20 +1122,14 @@ void addon::push_listings(scalar_condition const& cancel) if(item.episodename != nullptr) { // If the setting to generate repeat indicators is set, append to the episode name as appropriate - std::string episodename = std::string(item.episodename) + std::string(((isrepeat) && (settings.generate_epg_repeat_indicators)) ? " [R]" : ""); + std::string episodename = std::string(item.episodename) + std::string(((item.isrepeat) && (settings.generate_epg_repeat_indicators)) ? " [R]" : ""); epgtag.SetEpisodeName(episodename); } // Flags - // - // Only report EPG_TAG_FLAG_IS_NEW for program types "EP" (Series Episode) and "SH" (Show) unsigned int flags = EPG_TAG_FLAG_IS_SERIES; - if((item.isnew) && (item.programtype != nullptr) && ((strcasecmp(item.programtype, "EP") == 0) || (strcasecmp(item.programtype, "SH") == 0))) { - - // Special case: don't report EPG_TAG_FLAG_IS_NEW for listings of type EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS - // unless series/episode information is available - if((item.genretype != EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS) || ((item.seriesnumber >= 1) || (item.episodenumber >= 1))) flags |= EPG_TAG_FLAG_IS_NEW; - } + if(item.isnew) flags |= EPG_TAG_FLAG_IS_NEW; + if(item.islive) flags |= EPG_TAG_FLAG_IS_LIVE; epgtag.SetFlags(flags); // SeriesLink @@ -3490,9 +3476,6 @@ PVR_ERROR addon::GetEPGForChannel(int channelUid, time_t start, time_t end, kodi // Don't send EPG entries with start/end times outside the requested range if((item.starttime > end) || (item.endtime < start)) return; - // Determine if the episode is a repeat. If the program type is "EP" or "SH" and isnew is *not* set, flag it as a repeat - bool isrepeat = ((item.programtype != nullptr) && ((strcasecmp(item.programtype, "EP") == 0) || (strcasecmp(item.programtype, "SH") == 0)) && (item.isnew == false)); - // UniqueBroadcastId (required) epgtag.SetUniqueBroadcastId(item.broadcastid); @@ -3514,8 +3497,8 @@ PVR_ERROR addon::GetEPGForChannel(int channelUid, time_t start, time_t end, kodi // Year // - // Only report for program type "MV" (Movies) - if((item.programtype != nullptr) && (strcasecmp(item.programtype, "MV") == 0)) epgtag.SetYear(item.year); + // Only report for program type MOVIE + if((item.programtype != nullptr) && (strcasecmp(item.programtype, "MOVIE") == 0)) epgtag.SetYear(item.year); // IconPath if(item.iconurl != nullptr) epgtag.SetIconPath(item.iconurl); @@ -3528,16 +3511,9 @@ PVR_ERROR addon::GetEPGForChannel(int channelUid, time_t start, time_t end, kodi // FirstAired // - // Only report for program types "EP" (Series Episode) and "SH" (Show) - if((item.programtype != nullptr) && ((strcasecmp(item.programtype, "EP") == 0) || (strcasecmp(item.programtype, "SH") == 0))) { - - // Special case: don't report original air date for listings of type EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS - // unless series/episode information is available - if((item.genretype != EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS) || ((item.seriesnumber >= 1) || (item.episodenumber >= 1))) { - - if(item.originalairdate != nullptr) epgtag.SetFirstAired(item.originalairdate); - } - } + // Only report for program types other than MOVIE + if((item.programtype != nullptr) && (item.originalairdate != nullptr) && (strcasecmp(item.programtype, "MOVIE") != 0)) + epgtag.SetFirstAired(item.originalairdate); // SeriesNumber epgtag.SetSeriesNumber(item.seriesnumber); @@ -3552,20 +3528,14 @@ PVR_ERROR addon::GetEPGForChannel(int channelUid, time_t start, time_t end, kodi if(item.episodename != nullptr) { // If the setting to generate repeat indicators is set, append to the episode name as appropriate - std::string episodename = std::string(item.episodename) + std::string(((isrepeat) && (settings.generate_epg_repeat_indicators)) ? " [R]" : ""); + std::string episodename = std::string(item.episodename) + std::string(((item.isrepeat) && (settings.generate_epg_repeat_indicators)) ? " [R]" : ""); epgtag.SetEpisodeName(episodename); } // Flags - // - // Only report EPG_TAG_FLAG_IS_NEW for program types "EP" (Series Episode) and "SH" (Show) unsigned int flags = EPG_TAG_FLAG_IS_SERIES; - if((item.isnew) && (item.programtype != nullptr) && ((strcasecmp(item.programtype, "EP") == 0) || (strcasecmp(item.programtype, "SH") == 0))) { - - // Special case: don't report EPG_TAG_FLAG_IS_NEW for listings of type EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS - // unless series/episode information is available - if((item.genretype != EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS) || ((item.seriesnumber >= 1) || (item.episodenumber >= 1))) flags |= EPG_TAG_FLAG_IS_NEW; - } + if(item.isnew) flags |= EPG_TAG_FLAG_IS_NEW; + if(item.islive) flags |= EPG_TAG_FLAG_IS_LIVE; epgtag.SetFlags(flags); // SeriesLink diff --git a/src/database.cpp b/src/database.cpp index e738050..ce45292 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -894,6 +894,8 @@ void discover_listings(sqlite3* instance, char const* deviceauth, bool& changed) "xmltv.categories as genres, " "xmltv.episodenum as episodenumber, " "cast(coalesce(xmltv.isnew, 0) as integer) as isnew, " + "cast(coalesce(xmltv.isrepeat, 0) as integer) as isrepeat, " + "cast(coalesce(xmltv.islive, 0) as integer) as islive, " "xmltv.starrating as starrating " "from xmltv where xmltv.uri = 'https://api.hdhomerun.com/api/xmltv?DeviceAuth=' || ?1 and onchannel = ?2"; @@ -1622,7 +1624,7 @@ void enumerate_listings(sqlite3* instance, bool showdrm, int maxdays, enumerate_ // If the maximum number of days wasn't provided, use a month as the boundary if(maxdays < 0) maxdays = 31; - // seriesid | title | broadcastid | channelid | starttime | endtime | synopsis | year | iconurl | programtype | genretype | genres | originalairdate | seriesnumber | episodenumber | episodename | isnew | starrating + // seriesid | title | broadcastid | channelid | starttime | endtime | synopsis | year | iconurl | programtype | genretype | genres | originalairdate | seriesnumber | episodenumber | episodename | isnew | isrepeat | islive | starrating auto sql = "with allchannels(number) as " "(select distinct(json_extract(entry.value, '$.GuideNumber')) as number from lineup, json_each(lineup.data) as entry where nullif(json_extract(entry.value, '$.DRM'), ?1) is null) " "select listing.seriesid as seriesid, " @@ -1635,13 +1637,16 @@ void enumerate_listings(sqlite3* instance, bool showdrm, int maxdays, enumerate_ "coalesce(listing.year, 0) as year, " "listing.iconurl as iconurl, " "listing.programtype as programtype, " - "case upper(listing.programtype) when 'MV' then 0x10 when 'SP' then 0x40 else (select case when genremap.genretype is not null then genremap.genretype else 0x30 end) end as genretype, " + "case upper(listing.programtype) when 'MOVIE' then 0x10 when 'NEWS' then 0x20 when 'SPORT' then 0x40 when 'SHOP' then 0xA0 " + " else (select case when genremap.genretype is not null then genremap.genretype else 0x30 end) end as genretype, " "listing.genres as genres, " "listing.originalairdate as originalairdate, " "get_season_number(listing.episodenumber) as seriesnumber, " "get_episode_number(listing.episodenumber) as episodenumber, " "listing.episodename as episodename, " "listing.isnew as isnew, " + "listing.isrepeat as isrepeat, " + "listing.islive as islive, " "decode_star_rating(listing.starrating) as starrating " "from listing inner join guide on listing.channelid = guide.channelid " "inner join allchannels on guide.number = allchannels.number " @@ -1684,7 +1689,9 @@ void enumerate_listings(sqlite3* instance, bool showdrm, int maxdays, enumerate_ item.episodenumber = sqlite3_column_int(statement, 14); item.episodename = reinterpret_cast(sqlite3_column_text(statement, 15)); item.isnew = (sqlite3_column_int(statement, 16) != 0); - item.starrating = sqlite3_column_int(statement, 17); + item.isrepeat = (sqlite3_column_int(statement, 17) != 0); + item.islive = (sqlite3_column_int(statement, 18) != 0); + item.starrating = sqlite3_column_int(statement, 19); callback(item, cancel); // Invoke caller-supplied callback result = sqlite3_step(statement); // Move to the next row of data @@ -1730,13 +1737,16 @@ void enumerate_listings(sqlite3* instance, bool showdrm, union channelid channel "coalesce(listing.year, 0) as year, " "listing.iconurl as iconurl, " "listing.programtype as programtype, " - "case upper(listing.programtype) when 'MV' then 0x10 when 'SP' then 0x40 else (select case when genremap.genretype is not null then genremap.genretype else 0x30 end) end as genretype, " + "case upper(listing.programtype) when 'MOVIE' then 0x10 when 'NEWS' then 0x20 when 'SPORT' then 0x40 when 'SHOP' then 0xA0 " + " else (select case when genremap.genretype is not null then genremap.genretype else 0x30 end) end as genretype, " "listing.genres as genres, " "listing.originalairdate as originalairdate, " "get_season_number(listing.episodenumber) as seriesnumber, " "get_episode_number(listing.episodenumber) as episodenumber, " "listing.episodename as episodename, " "listing.isnew as isnew, " + "listing.isrepeat as isrepeat, " + "listing.islive as islive, " "decode_star_rating(listing.starrating) as starrating " "from listing inner join guide on listing.channelid = guide.channelid " "inner join allchannels on guide.number = allchannels.number " @@ -1781,7 +1791,9 @@ void enumerate_listings(sqlite3* instance, bool showdrm, union channelid channel item.episodenumber = sqlite3_column_int(statement, 13); item.episodename = reinterpret_cast(sqlite3_column_text(statement, 14)); item.isnew = (sqlite3_column_int(statement, 15) != 0); - item.starrating = sqlite3_column_int(statement, 16); + item.isrepeat = (sqlite3_column_int(statement, 16) != 0); + item.islive = (sqlite3_column_int(statement, 17) != 0); + item.starrating = sqlite3_column_int(statement, 18); callback(item, cancel); // Invoke caller-supplied callback result = sqlite3_step(statement); // Move to the next row of data @@ -3229,9 +3241,10 @@ sqlite3* open_database(char const* connstring, int flags, bool initialize) // table: listing // - // channelid | starttime | endtime | seriesid | title | episodename | synopsis | year | originalairdate | iconurl | programtype | primarygenre | genres | episodenumber | isnew | starrating + // channelid | starttime | endtime | seriesid | title | episodename | synopsis | year | originalairdate | iconurl | programtype | primarygenre | genres | episodenumber | isnew | isrepeat | islive | starrating execute_non_query(instance, "create table if not exists listing(channelid text not null, starttime integer not null, endtime integer not null, seriesid text, title text, " - "episodename text, synopsis text, year integer, originalairdate text, iconurl text, programtype text, primarygenre text, genres text, episodenumber text, isnew integer, starrating text)"); + "episodename text, synopsis text, year integer, originalairdate text, iconurl text, programtype text, primarygenre text, genres text, episodenumber text, isnew integer, " + "isrepeat integer, islive integer, starrating text)"); execute_non_query(instance, "create index if not exists listing_channelid_starttime_endtime_index on listing(channelid, starttime, endtime)"); // table: recording diff --git a/src/dbextension.cpp b/src/dbextension.cpp index 7432329..bbc40ca 100644 --- a/src/dbextension.cpp +++ b/src/dbextension.cpp @@ -124,6 +124,8 @@ enum class xmltv_vtab_columns { episodenum, // episodenum text programtype, // programtype text isnew, // isnew integer + isrepeat, // isrepeat integer + islive, // islive integer starrating, // starrating text }; @@ -1570,27 +1572,27 @@ int xmltv_column(sqlite3_vtab_cursor* cursor, sqlite3_context* context, int ordi // Special case: concatenate all of the element values into a comma-delimted string case xmltv_vtab_columns::categories: - { - std::string collector; // Collector for the disparate strings - bool progtype = false; // Flag to skip progType element + { + std::string collector; // Collector for the disparate strings + bool progtype = false; // Flag to skip progType element - xmlTextReaderForEachChildElement(xmltvcursor->reader, BAD_CAST("category"), [&](xmlNodePtr node) -> void { + xmlTextReaderForEachChildElement(xmltvcursor->reader, BAD_CAST("category"), [&](xmlNodePtr node) -> void { - // The first element is the progType, which we don't want to use for anything - if(!progtype) { progtype = true; return; } + // The first element is the progType, which we don't want to use for anything + if(!progtype) { progtype = true; return; } - xmlChar* value = xmlNodeGetContent(node); - if(value != nullptr) { + xmlChar* value = xmlNodeGetContent(node); + if(value != nullptr) { - if(!collector.empty()) collector.append(","); - collector.append(reinterpret_cast(value)); - xmlFree(value); - } - }); + if(!collector.empty()) collector.append(","); + collector.append(reinterpret_cast(value)); + xmlFree(value); + } + }); - if(!collector.empty()) sqlite3_result_text(context, collector.c_str(), -1, SQLITE_TRANSIENT); - } - break; + if(!collector.empty()) sqlite3_result_text(context, collector.c_str(), -1, SQLITE_TRANSIENT); + } + break; case xmltv_vtab_columns::language: node = xmlTextReaderGetChildElement(xmltvcursor->reader, BAD_CAST("language")); @@ -1615,27 +1617,26 @@ int xmltv_column(sqlite3_vtab_cursor* cursor, sqlite3_context* context, int ordi if(node != nullptr) sqlite3_result_text(context, reinterpret_cast(xmlNodeGetContent(node)), -1, xmlFree); break; - // Special case: extract the program type from the alphanumeric identifer at the start of the dd_progid case xmltv_vtab_columns::programtype: - { - node = xmlTextReaderGetChildElementWithAttribute(xmltvcursor->reader, BAD_CAST("episode-num"), BAD_CAST("system"), BAD_CAST("dd_progid")); - if(node != nullptr) { - - xmlChar* progid = xmlNodeGetContent(node); - if(progid != nullptr) { - - if(strlen(reinterpret_cast(progid)) >= 2) sqlite3_result_text(context, reinterpret_cast(progid), 2, SQLITE_TRANSIENT); - xmlFree(progid); - } - } - } - break; + node = xmlTextReaderGetChildElement(xmltvcursor->reader, BAD_CAST("category")); + if(node != nullptr) sqlite3_result_text(context, reinterpret_cast(xmlNodeGetContent(node)), -1, xmlFree); + break; case xmltv_vtab_columns::isnew: node = xmlTextReaderGetChildElement(xmltvcursor->reader, BAD_CAST("new")); if(node != nullptr) sqlite3_result_int(context, 1); break; + case xmltv_vtab_columns::isrepeat: + node = xmlTextReaderGetChildElement(xmltvcursor->reader, BAD_CAST("previously-shown")); + if(node != nullptr) sqlite3_result_int(context, 1); + break; + + case xmltv_vtab_columns::islive: + node = xmlTextReaderGetChildElement(xmltvcursor->reader, BAD_CAST("live")); + if(node != nullptr) sqlite3_result_int(context, 1); + break; + case xmltv_vtab_columns::starrating: node = xmlTextReaderGetChildElement(xmltvcursor->reader, BAD_CAST("star-rating")); if(node != nullptr) node = xmlNodeGetChildElement(node, BAD_CAST("value")); @@ -1665,7 +1666,7 @@ int xmltv_connect(sqlite3* instance, void* /*aux*/, int /*argc*/, const char* co // Declare the schema for the virtual table, use hidden columns for all of the filter criteria int result = sqlite3_declare_vtab(instance, "create table xmltv(uri text hidden, onchannel pointer hidden, channel text, start text, " "stop text, title text, subtitle text, desc text, date text, categories text, language text, iconsrc text, seriesid text, " - "episodenum text, programtype text, isnew integer, starrating text)"); + "episodenum text, programtype text, isnew integer, isrepeat integer, islive integer, starrating text)"); if(result != SQLITE_OK) return result; // Allocate and initialize the custom virtual table class diff --git a/src/dbtypes.h b/src/dbtypes.h index 99ac128..d112304 100644 --- a/src/dbtypes.h +++ b/src/dbtypes.h @@ -41,7 +41,7 @@ static size_t const DATABASE_CONNECTIONPOOL_SIZE = 5; // DATABASE_SCHEMA_VERSION // // This value needs to be incremented with any database schema change -static char const DATABASE_SCHEMA_VERSION[] = "13"; +static char const DATABASE_SCHEMA_VERSION[] = "14"; //--------------------------------------------------------------------------- // DATA TYPES @@ -140,6 +140,8 @@ struct listing { int episodenumber; char const* episodename; bool isnew; + bool isrepeat; + bool islive; int starrating; }; diff --git a/src/version.ini b/src/version.ini index 954547f..7c80d28 100644 --- a/src/version.ini +++ b/src/version.ini @@ -8,4 +8,4 @@ Company=Michael G. Brehm Copyright= Product=zuki.pvr.hdhomerundvr -Version=4.9.0 +Version=4.9.1