Skip to content

Commit

Permalink
Adapt to XMLTV Electronic Program Guide (EPG) schema changes
Browse files Browse the repository at this point in the history
  • Loading branch information
djp952 committed Mar 19, 2023
1 parent b2715b1 commit ad64049
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 86 deletions.
3 changes: 3 additions & 0 deletions pvr.hdhomerundvr/changelog.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
62 changes: 16 additions & 46 deletions src/addon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1069,9 +1069,6 @@ void addon::push_listings(scalar_condition<bool> 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);
Expand All @@ -1094,8 +1091,8 @@ void addon::push_listings(scalar_condition<bool> 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);
Expand All @@ -1108,14 +1105,9 @@ void addon::push_listings(scalar_condition<bool> 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);
Expand All @@ -1130,20 +1122,14 @@ void addon::push_listings(scalar_condition<bool> 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
Expand Down Expand Up @@ -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);

Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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
Expand Down
27 changes: 20 additions & 7 deletions src/database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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, "
Expand All @@ -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 "
Expand Down Expand Up @@ -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<char const*>(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
Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -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<char const*>(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
Expand Down Expand Up @@ -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
Expand Down
63 changes: 32 additions & 31 deletions src/dbextension.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand Down Expand Up @@ -1570,27 +1572,27 @@ int xmltv_column(sqlite3_vtab_cursor* cursor, sqlite3_context* context, int ordi

// Special case: concatenate all of the <category> 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 <category> element
{
std::string collector; // Collector for the disparate strings
bool progtype = false; // Flag to skip progType <category> element

xmlTextReaderForEachChildElement(xmltvcursor->reader, BAD_CAST("category"), [&](xmlNodePtr node) -> void {
xmlTextReaderForEachChildElement(xmltvcursor->reader, BAD_CAST("category"), [&](xmlNodePtr node) -> void {

// The first <category> element is the progType, which we don't want to use for anything
if(!progtype) { progtype = true; return; }
// The first <category> 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<char*>(value));
xmlFree(value);
}
});
if(!collector.empty()) collector.append(",");
collector.append(reinterpret_cast<char*>(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"));
Expand All @@ -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<char*>(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<char*>(progid)) >= 2) sqlite3_result_text(context, reinterpret_cast<char*>(progid), 2, SQLITE_TRANSIENT);
xmlFree(progid);
}
}
}
break;
node = xmlTextReaderGetChildElement(xmltvcursor->reader, BAD_CAST("category"));
if(node != nullptr) sqlite3_result_text(context, reinterpret_cast<char*>(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"));
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/dbtypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -140,6 +140,8 @@ struct listing {
int episodenumber;
char const* episodename;
bool isnew;
bool isrepeat;
bool islive;
int starrating;
};

Expand Down
2 changes: 1 addition & 1 deletion src/version.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
Company=Michael G. Brehm
Copyright=
Product=zuki.pvr.hdhomerundvr
Version=4.9.0
Version=4.9.1

0 comments on commit ad64049

Please sign in to comment.