EPG_OBJECT_SET_FN(_epg_object_set_lang_str, lang_str_t, lang_str_destroy, lang_str_compare, lang_str_copy)
+EPG_OBJECT_SET_FN(_epg_object_set_string_list, string_list_t, string_list_destroy, string_list_cmp, string_list_copy)
+EPG_OBJECT_SET_FN(_epg_object_set_htsmsg, htsmsg_t, htsmsg_destroy, htsmsg_cmp, htsmsg_copy)
#undef EPG_OBJECT_SET_FN
static int _epg_object_set_u8
if (ebc->serieslink) _epg_serieslink_rem_broadcast(ebc->serieslink, ebc);
if (ebc->summary) lang_str_destroy(ebc->summary);
if (ebc->description) lang_str_destroy(ebc->description);
+ if (ebc->credits) htsmsg_destroy(ebc->credits);
+ if (ebc->credits_cached) lang_str_destroy(ebc->credits_cached);
+ if (ebc->category) string_list_destroy(ebc->category);
+ if (ebc->keyword) string_list_destroy(ebc->keyword);
+ if (ebc->keyword_cached) lang_str_destroy(ebc->keyword_cached);
_epg_object_destroy(eo, NULL);
free(ebc);
}
save |= epg_broadcast_set_summary(broadcast, NULL, NULL);
if (!(changes & EPG_CHANGED_DESCRIPTION))
save |= epg_broadcast_set_description(broadcast, NULL, NULL);
+ if (!(changes & EPG_CHANGED_CREDITS))
+ save |= epg_broadcast_set_credits(broadcast, NULL, NULL);
+ if (!(changes & EPG_CHANGED_CATEGORY))
+ save |= epg_broadcast_set_category(broadcast, NULL, NULL);
+ if (!(changes & EPG_CHANGED_KEYWORD))
+ save |= epg_broadcast_set_keyword(broadcast, NULL, NULL);
return save;
}
*save |= epg_broadcast_set_is_new(ebc, src->is_new, &changes);
*save |= epg_broadcast_set_is_repeat(ebc, src->is_repeat, &changes);
*save |= epg_broadcast_set_summary(ebc, src->summary, &changes);
+ *save |= epg_broadcast_set_credits(ebc, src->credits, &changes);
+ *save |= epg_broadcast_set_category(ebc, src->category, &changes);
+ *save |= epg_broadcast_set_keyword(ebc, src->keyword, &changes);
*save |= epg_broadcast_set_description(ebc, src->description, &changes);
*save |= epg_broadcast_set_serieslink(ebc, src->serieslink, &changes);
*save |= epg_broadcast_set_episode(ebc, src->episode, &changes);
changed, EPG_CHANGED_DESCRIPTION);
}
+int epg_broadcast_set_credits
+( epg_broadcast_t *b, htsmsg_t *credits, uint32_t *changed )
+{
+ if (!b) return 0;
+ const int mod = _epg_object_set_htsmsg(b, &b->credits, credits, changed, EPG_CHANGED_CREDITS);
+ if (mod) {
+ /* Copy in to cached csv for regex searching in autorec/GUI.
+ * We use just one string (rather than regex across each entry
+ * separately) so you could do a regex of "Douglas.*Stallone"
+ * to match the movies with the two actors.
+ */
+ if (!b->credits_cached) {
+ b->credits_cached = lang_str_create();
+ }
+ lang_str_set(&b->credits_cached, "", NULL);
+
+ if (b->credits) {
+ int add_sep = 0;
+ htsmsg_field_t *f;
+ HTSMSG_FOREACH(f, b->credits) {
+ if (add_sep) {
+ lang_str_append(b->credits_cached, ", ", NULL);
+ } else {
+ add_sep = 1;
+ }
+ lang_str_append(b->credits_cached, f->hmf_name, NULL);
+ }
+ } else {
+ if (b->credits_cached) {
+ lang_str_destroy(b->credits_cached);
+ b->credits_cached = NULL;
+ }
+ }
+ }
+ return mod;
+}
+
+int epg_broadcast_set_category
+( epg_broadcast_t *b, string_list_t *msg, uint32_t *changed )
+{
+ if (!b) return 0;
+ return _epg_object_set_string_list(b, &b->category, msg, changed, EPG_CHANGED_CATEGORY);
+}
+
+int epg_broadcast_set_keyword
+( epg_broadcast_t *b, string_list_t *msg, uint32_t *changed )
+{
+ if (!b) return 0;
+ const int mod = _epg_object_set_string_list(b, &b->keyword, msg, changed, EPG_CHANGED_KEYWORD);
+ if (mod) {
+ /* Copy in to cached csv for regex searching in autorec/GUI. */
+ if (msg) {
+ /* 1==>human readable */
+ char *str = string_list_2_csv(msg, ',', 1);
+ lang_str_set(&b->keyword_cached, str, NULL);
+ free(str);
+ } else {
+ if (b->keyword_cached) {
+ lang_str_destroy(b->keyword_cached);
+ b->keyword_cached = NULL;
+ }
+ }
+ }
+ return mod;
+}
+
epg_broadcast_t *epg_broadcast_get_next ( epg_broadcast_t *broadcast )
{
if ( !broadcast ) return NULL;
return lang_str_get(b->summary, lang);
}
+const char *epg_broadcast_get_credits_cached ( epg_broadcast_t *b, const char *lang)
+{
+ if (!b || !b->credits_cached) return NULL;
+ return lang_str_get(b->credits_cached, lang);
+}
+
+const char *epg_broadcast_get_keyword_cached ( epg_broadcast_t *b, const char *lang)
+{
+ if (!b || !b->keyword_cached) return NULL;
+ return lang_str_get(b->keyword_cached, lang);
+}
+
const char *epg_broadcast_get_description ( epg_broadcast_t *b, const char *lang )
{
if (!b || !b->description) return NULL;
lang_str_serialize(broadcast->summary, m, "summary");
if (broadcast->description)
lang_str_serialize(broadcast->description, m, "description");
+ if (broadcast->credits)
+ htsmsg_add_msg(m, "credits", htsmsg_copy(broadcast->credits));
+ /* No need to serialize credits_cached since it is rebuilt from credits. */
+ if (broadcast->category)
+ string_list_serialize(broadcast->category, m, "category");
+ if (broadcast->keyword)
+ string_list_serialize(broadcast->keyword, m, "keyword");
+ /* No need to serialize keyword_cached since it is rebuilt from keyword */
+
if (broadcast->serieslink)
htsmsg_add_str(m, "serieslink", broadcast->serieslink->uri);
epg_episode_t *ee;
epg_serieslink_t *esl;
lang_str_t *ls;
+ htsmsg_t *hm;
+ string_list_t *sl;
const char *str;
uint32_t eid, u32, changes = 0, changes2 = 0;
int64_t start, stop;
lang_str_destroy(ls);
}
+ if ((hm = htsmsg_get_map(m, "credits"))) {
+ *save |= epg_broadcast_set_credits(ebc, hm, &changes);
+ }
+
+ if ((sl = string_list_deserialize(m, "keyword"))) {
+ *save |= epg_broadcast_set_keyword(ebc, sl, &changes);
+ }
+
+ if ((sl = string_list_deserialize(m, "category"))) {
+ *save |= epg_broadcast_set_category(ebc, sl, &changes);
+ }
+
/* Series link */
if ((str = htsmsg_get_str(m, "serieslink")))
if ((esl = epg_serieslink_find_by_uri(str, ebc->grabber, 1, save, &changes2))) {
regex_match(&eq->stitle_re, s)) {
if ((s = epg_broadcast_get_description(e, lang)) == NULL ||
regex_match(&eq->stitle_re, s)) {
- return;
+ if ((s = epg_broadcast_get_credits_cached(e, lang)) == NULL ||
+ regex_match(&eq->stitle_re, s)) {
+ if ((s = epg_broadcast_get_keyword_cached(e, lang)) == NULL ||
+ regex_match(&eq->stitle_re, s)) {
+ return;
+ }
+ }
}
}
}
struct channel;
struct channel_tag;
struct epggrab_module;
+struct string_list;
/*
* Map/List types
#define EPG_CHANGED_SUMMARY (1<<3)
#define EPG_CHANGED_DESCRIPTION (1<<4)
#define EPG_CHANGED_IMAGE (1<<5)
+#define EPG_CHANGED_CREDITS (1<<6)
+#define EPG_CHANGED_CATEGORY (1<<7)
+#define EPG_CHANGED_KEYWORD (1<<8)
#define EPG_CHANGED_SLAST 2
typedef struct epg_object_ops {
/* Broadcast level text */
lang_str_t *summary; ///< Summary
lang_str_t *description; ///< Description
-
+ htsmsg_t *credits; ///< Cast/Credits map of name -> role type (actor, presenter, director, etc).
+ lang_str_t *credits_cached; ///< Comma separated cast (for regex searching in GUI/autorec). Kept in sync with cast_map
+ struct string_list *category; ///< Extra categories (typically from xmltv) such as "Western" or "Sumo Wrestling".
+ ///< These extra categories are often a superset of our EN 300 468 DVB genre.
+ ///< Currently not explicitly searchable in GUI.
+ struct string_list *keyword; ///< Extra keywords (typically from xmltv) such as "Wild West" or "Unicorn".
+ lang_str_t *keyword_cached; ///< Cached CSV version for regex searches.
RB_ENTRY(epg_broadcast) sched_link; ///< Schedule link
LIST_ENTRY(epg_broadcast) ep_link; ///< Episode link
epg_episode_t *episode; ///< Episode shown
int epg_broadcast_set_description
( epg_broadcast_t *b, const lang_str_t *str, uint32_t *changed )
__attribute__((warn_unused_result));
+int epg_broadcast_set_credits
+( epg_broadcast_t *b, htsmsg_t* msg, uint32_t *changed )
+ __attribute__((warn_unused_result));
+int epg_broadcast_set_category
+( epg_broadcast_t *b, struct string_list* msg, uint32_t *changed )
+ __attribute__((warn_unused_result));
+int epg_broadcast_set_keyword
+( epg_broadcast_t *b, struct string_list* msg, uint32_t *changed )
+ __attribute__((warn_unused_result));
int epg_broadcast_set_serieslink
( epg_broadcast_t *b, epg_serieslink_t *sl, uint32_t *changed )
__attribute__((warn_unused_result));
( epg_broadcast_t *b, const char *lang );
const char *epg_broadcast_get_description
( epg_broadcast_t *b, const char *lang );
+/* Get the cached (csv) version for regex searching */
+const char *epg_broadcast_get_credits_cached
+ ( epg_broadcast_t *b, const char *lang );
+const char *epg_broadcast_get_keyword_cached
+ ( epg_broadcast_t *b, const char *lang );
/* Serialization */
htsmsg_t *epg_broadcast_serialize ( epg_broadcast_t *b );
const char *args; ///< Extra arguments
int xmltv_chnum;
+ int xmltv_scrape_extra; ///< Scrape actors and extra details
+ int xmltv_scrape_onto_desc; ///< Include scraped actors
+ ///< and extra details on to programme description for viewing by legacy clients.
/* Handle data */
char* (*grab) ( void *mod );
#include "spawn.h"
#include "file.h"
#include "htsstr.h"
+#include "string_list.h"
#include "lang_str.h"
#include "epg.h"
/*
* Tries to get age ratingform <rating> element.
* Expects integer representing minimal age of watcher.
- * Other rating types (non-integer, for example MPAA or VCHIP) are ignored.
+ * Other rating types (non-integer, for example MPAA or VCHIP) are
+ * mostly ignored, but we have some basic mappings for common
+ * ratings such as TV-MA which may only be the only ratings for
+ * some movies.
+ *
+ * We use the first rating that we find that returns a usable age. Of
+ * course that means some programmes might not have the rating you
+ * expect for your country. For example one episode of a cooking
+ * programme has BBFC 18 but VCHIP TV-G.
*
* Attribute system is ignored.
*
* <rating system="pl"><value>16</value></rating>
*
* Currently non-working example:
- * <rating system="MPAA">
- * <value>PG</value>
- * <icon src="pg_symbol.png" />
+ * <rating system="CSA">
+ * <value>-12</value>
* </rating>
*
* TODO - support for other rating systems:
const char *s1;
if (!ee || !body) return 0;
- if (!(rating = htsmsg_get_map(body, "rating"))) return 0;
- if (!(tags = htsmsg_get_map(rating, "tags"))) return 0;
- if (!(s1 = htsmsg_xml_get_cdata_str(tags, "value"))) return 0;
- age = atoi(s1);
-
- return epg_episode_set_age_rating(ee, age, changes);
+ htsmsg_field_t *f;
+ HTSMSG_FOREACH(f, body) {
+ if (!strcmp(f->hmf_name, "rating") && (rating = htsmsg_get_map_by_field(f))) {
+ if ((tags = htsmsg_get_map(rating, "tags"))) {
+ if ((s1 = htsmsg_xml_get_cdata_str(tags, "value"))) {
+ /* We map some common ratings since some movies only
+ * have one of these flags rather than an age rating.
+ */
+ if (!strcmp(s1, "TV-G") || !strcmp(s1, "U"))
+ age = 3;
+ else if (!strcmp(s1, "TV-Y7") || !strcmp(s1, "PG"))
+ age = 7;
+ else if (!strcmp(s1, "TV-14"))
+ age = 14;
+ else if (!strcmp(s1, "TV-MA"))
+ age = 17;
+ else
+ age = atoi(s1);
+ /* Since age is uint8_t it means some rating systems can
+ * underflow and become very large, for example CSA has age
+ * rating of -10.
+ */
+ if (age > 0 && age < 22)
+ return epg_episode_set_age_rating(ee, age, changes);
+ }
+ }
+ }
+ }
+ return 0;
}
/*
}
}
+/// Make a string list from the contents of all tags in the message
+/// that have tagname.
+__attribute__((warn_unused_result))
+static string_list_t *
+ _xmltv_make_str_list_from_matching(htsmsg_t *tags, const char *tagname)
+{
+ htsmsg_t *e;
+ htsmsg_field_t *f;
+ string_list_t *tag_list = NULL;
+
+ HTSMSG_FOREACH(f, tags) {
+ if (!strcmp(f->hmf_name, tagname) && (e = htsmsg_get_map_by_field(f))) {
+ const char *str = htsmsg_get_str(e, "cdata");
+ if (str && *str) {
+ if (!tag_list) tag_list = string_list_create();
+ string_list_insert(tag_list, str);
+ }
+ }
+ }
+
+ return tag_list;
+}
+
+
+/// Parse credits from the message tags and store the name/type (such
+/// as actor, director) in to out_credits (created if necessary).
+/// Also return a string list of the names only.
+///
+/// Sample input:
+/// <credits>
+/// <actor role="Bob">Fred Foo</actor>
+/// <actor role="Walt">Vic Vicson</actor>
+/// <director>Simon Scott</director>
+/// </credits>
+///
+/// Returns string list of {"Fred Foo", "Simon Scott", "Vic Vicson"} and
+/// out_credits containing the names and actor/director.
+__attribute__((warn_unused_result))
+static string_list_t *
+_xmltv_parse_credits(htsmsg_t **out_credits, htsmsg_t *tags)
+{
+ htsmsg_t *credits = htsmsg_get_map(tags, "credits");
+ if (!credits)
+ return NULL;
+ htsmsg_t *credits_tags;
+ if (!(credits_tags = htsmsg_get_map(credits, "tags")))
+ return NULL;
+
+ string_list_t *credits_names = NULL;
+ htsmsg_t *e;
+ htsmsg_field_t *f;
+
+ HTSMSG_FOREACH(f, credits_tags) {
+ if ((!strcmp(f->hmf_name, "actor") ||
+ !strcmp(f->hmf_name, "director") ||
+ !strcmp(f->hmf_name, "guest") ||
+ !strcmp(f->hmf_name, "presenter") ||
+ !strcmp(f->hmf_name, "writer")
+ ) &&
+ (e = htsmsg_get_map_by_field(f))) {
+ const char* str = htsmsg_get_str(e, "cdata");
+ if (str) {
+ if (!credits_names) credits_names = string_list_create();
+ string_list_insert(credits_names, str);
+
+ if (!*out_credits) *out_credits = htsmsg_create_map();
+ htsmsg_add_str(*out_credits, str, f->hmf_name);
+ }
+ }
+ }
+
+ return credits_names;
+}
+
/**
* Parse tags inside of a programme
*/
time_t start, time_t stop, const char *icon,
epggrab_stats_t *stats)
{
+ const int scrape_extra = ((epggrab_module_ext_t *)mod)->xmltv_scrape_extra;
+ const int scrape_onto_desc = ((epggrab_module_ext_t *)mod)->xmltv_scrape_onto_desc;
int save = 0, save2 = 0, save3 = 0;
epg_episode_t *ee = NULL;
epg_serieslink_t *es = NULL;
/* Description (wait for episode first) */
_xmltv_parse_lang_str(&desc, tags, "desc");
- if (desc)
+
+ /* If user has requested it then retrieve additional information
+ * from programme such as credits and keywords.
+ */
+ if (scrape_extra || scrape_onto_desc) {
+ htsmsg_t *credits = NULL;
+ string_list_t *credits_names = _xmltv_parse_credits(&credits, tags);
+ string_list_t *category = _xmltv_make_str_list_from_matching(tags, "category");
+ string_list_t *keyword = _xmltv_make_str_list_from_matching(tags, "keyword");
+
+ if (scrape_extra && credits) {
+ save3 |= epg_broadcast_set_credits(ebc, credits, &changes);
+ }
+
+ if (scrape_extra && category) {
+ save3 |= epg_broadcast_set_category(ebc, category, &changes);
+ }
+
+ if (scrape_extra && keyword) {
+ save3 |= epg_broadcast_set_keyword(ebc, keyword, &changes);
+ }
+
+ /* Convert the string list VAR to a human-readable csv and append
+ * it to the desc with a prefix of NAME.
+ */
+#define APPENDIT(VAR,NAME) \
+ if (VAR) { \
+ char *str = string_list_2_csv(VAR, ',', 1); \
+ if (str) { \
+ lang_str_append(desc, "\n\n", NULL); \
+ lang_str_append(desc, NAME, NULL); \
+ lang_str_append(desc, str, NULL); \
+ free(str); \
+ } \
+ }
+
+ /* Append the details on to the description, mainly for legacy
+ * clients. This allow you to view the details in the description
+ * on old boxes/tablets that don't parse the newer fields or
+ * don't display them.
+ */
+ if (desc && scrape_onto_desc) {
+ APPENDIT(credits_names, N_("Credits: "));
+ APPENDIT(category, N_("Categories: "));
+ APPENDIT(keyword, N_("Keywords: "));
+ }
+
+ if (credits) htsmsg_destroy(credits);
+ if (credits_names) string_list_destroy(credits_names);
+ if (category) string_list_destroy(category);
+ if (keyword) string_list_destroy(keyword);
+
+#undef APPENDIT
+ } /* desc */
+
+ if (desc) {
save3 |= epg_broadcast_set_description(ebc, desc, &changes);
+ } /* desc */
/* summary */
_xmltv_parse_lang_str(&summary, tags, "summary");
if (subtitle) lang_str_destroy(subtitle);
if (desc) lang_str_destroy(desc);
if (summary) lang_str_destroy(summary);
-
return save | save2 | save3;
}
N_("Try to obtain channel numbers from the display-name xml tag. " \
"If the first word is number, it is used as the channel number.")
+#define SCRAPE_EXTRA_NAME N_("Scrape credits and extra information")
+#define SCRAPE_EXTRA_DESC \
+ N_("Obtain list of credits (actors, etc.), keywords and extra information from the xml tags (if available). " \
+ "Some xmltv providers supply a list of actors and additional keywords to " \
+ "describe programmes. This option will retrieve this additional information. " \
+ "This can be very detailed (20+ actors per movie) " \
+ "and will take a lot of memory and resources on this box, and will " \
+ "pass this information to your client machines and GUI too, using " \
+ "memory and resources on those boxes too. " \
+ "Do not enable on low-spec machines.")
+
+#define SCRAPE_ONTO_DESC_NAME N_("Alter programme description to include detailed information")
+#define SCRAPE_ONTO_DESC_DESC \
+ N_("If enabled then this will alter the programme descriptions to " \
+ "include information about actors, keywords and categories (if available from the xmltv file). " \
+ "This is useful for legacy clients that can not parse newer Tvheadend messages " \
+ "containing this information or do not display the information. "\
+ "For example the modified description might include 'Starring: Lorem Ipsum'. " \
+ "The description is altered for all clients, both legacy, modern, and GUI. "\
+ "Enabling scraping of detailed information can use significant resources (memory and CPU). "\
+ "You should not enable this if you use 'duplicate detect if different description' " \
+ "since the descriptions will change due to added information.")
+
static htsmsg_t *
xmltv_dn_chnum_list ( void *o, const char *lang )
{
.opts = PO_DOC_NLIST,
.group = 1
},
+ {
+ .type = PT_BOOL,
+ .id = "scrape_extra",
+ .name = SCRAPE_EXTRA_NAME,
+ .desc = SCRAPE_EXTRA_DESC,
+ .off = offsetof(epggrab_module_int_t, xmltv_scrape_extra),
+ .group = 1
+ },
+ {
+ .type = PT_BOOL,
+ .id = "scrape_onto_desc",
+ .name = SCRAPE_ONTO_DESC_NAME,
+ .desc = SCRAPE_ONTO_DESC_DESC,
+ .off = offsetof(epggrab_module_int_t, xmltv_scrape_onto_desc),
+ .group = 1
+ },
{}
}
};
.opts = PO_DOC_NLIST,
.group = 1
},
+ {
+ .type = PT_BOOL,
+ .id = "scrape_extra",
+ .name = SCRAPE_EXTRA_NAME,
+ .desc = SCRAPE_EXTRA_DESC,
+ .off = offsetof(epggrab_module_int_t, xmltv_scrape_extra),
+ .group = 1
+ },
+ {
+ .type = PT_BOOL,
+ .id = "scrape_onto_desc",
+ .name = SCRAPE_ONTO_DESC_NAME,
+ .desc = SCRAPE_ONTO_DESC_DESC,
+ .off = offsetof(epggrab_module_int_t, xmltv_scrape_onto_desc),
+ .group = 1
+ },
{}
}
};
#include "descrambler/caid.h"
#include "notify.h"
#include "htsmsg_json.h"
+#include "string_list.h"
#include "lang_codes.h"
#if ENABLE_TIMESHIFT
#include "timeshift.h"
htsmsg_add_str(out, "summary", str);
} else if((str = epg_broadcast_get_summary(e, lang)))
htsmsg_add_str(out, "description", str);
+
+ if (e->credits) {
+ htsmsg_add_msg(out, "credits", htsmsg_copy(e->credits));
+ }
+ if (e->category) {
+ htsmsg_add_msg(out, "category", string_list_to_htsmsg(e->category));
+ }
+ if (e->keyword) {
+ htsmsg_add_msg(out, "keyword", string_list_to_htsmsg(e->keyword));
+ }
+
if (e->serieslink) {
htsmsg_add_u32(out, "serieslinkId", e->serieslink->id);
if (e->serieslink->uri)
/* Compare */
int lang_str_compare ( const lang_str_t *ls1, const lang_str_t *ls2 );
-/* Empty */
-int strempty(const char* c);
-int lang_str_empty(lang_str_t* str);
+/* Is string empty? */
+int strempty(const char* c)
+ __attribute__((warn_unused_result));
+int lang_str_empty(lang_str_t* str)
+ __attribute__((warn_unused_result));
/* Size in bytes */
size_t lang_str_size ( const lang_str_t *ls );