From: DeltaMikeCharlie <127641886+DeltaMikeCharlie@users.noreply.github.com> Date: Tue, 28 Nov 2023 02:38:39 +0000 (+1100) Subject: Add Parental Rating Labels X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b061e641bc4f863d4c91340b691672bedd46b035;p=thirdparty%2Ftvheadend.git Add Parental Rating Labels --- diff --git a/Makefile b/Makefile index 3a7e16557..5e9688fbb 100644 --- a/Makefile +++ b/Makefile @@ -275,6 +275,7 @@ SRCS-1 = \ src/intlconv.c \ src/profile.c \ src/bouquet.c \ + src/ratinglabels.c \ src/lock.c \ src/string_list.c \ src/wizard.c \ @@ -320,6 +321,7 @@ SRCS-2 = \ src/api/api_caclient.c \ src/api/api_profile.c \ src/api/api_bouquet.c \ + src/api/api_ratinglabel.c \ src/api/api_language.c \ src/api/api_satip.c \ src/api/api_timeshift.c \ diff --git a/Makefile.webui b/Makefile.webui index c0465fe0e..e6bc0dace 100644 --- a/Makefile.webui +++ b/Makefile.webui @@ -159,6 +159,7 @@ JAVASCRIPT += $(ROOTPATH)/app/status.js JAVASCRIPT += $(ROOTPATH)/app/wizard.js JAVASCRIPT += $(ROOTPATH)/tv.js JAVASCRIPT += $(ROOTPATH)/app/servicemapper.js +JAVASCRIPT += $(ROOTPATH)/app/ratinglabels.js JAVASCRIPT += $(ROOTPATH)/app/tvheadend.js diff --git a/docs/class/ratinglabel.md b/docs/class/ratinglabel.md new file mode 100644 index 000000000..69d79ea35 --- /dev/null +++ b/docs/class/ratinglabel.md @@ -0,0 +1,59 @@ +inc/ratinglabel_contents + +--- + +## Overview + +This tab lists all defined parental rating labels. + +!['Complete rating labels list'](static/img/doc/ratinglabel/rating_labels_complete.png) + +A 'rating label' is a text code like 'PG', 'PG-13' or 'FSK 12' used to identify the parental rating classification of a TV programme. + +Rating labels can be sourced from the OTA EPG grabber or from the XMLTV grabber. + +**NOTE:** Rating labels are not enabled by default and must be enabled in the [EPG Grabber](class/epggrab) module under 'General Settings'. + + +# DVB OTA + +Ratings from the OTA EPG do not contain rating text like 'PG', instead, a combination of country code and age is transmitted, eg: AUS + 8. It is the responsibility of the receiver unit to decode this combination and determine the rating text to display. + +When the rating labels module encounters a new country and age combination, it will create a placeholder entry in the rating labels table as follows: + +!['Newly learned rating labels list'](static/img/doc/ratinglabel/rating_labels_learned.png) + +When a placeholder label is in use, the programme details in the EPG will show this placeholder entry rather than the expected value. + +!['EPG with placeholder rating'](static/img/doc/ratinglabel/epg_placeholder.png) + +You are required to manually edit this placeholder entry in order to provide the appropriate rating text to display. The correct text can be found by searching for the specific programme in another EPG source or by obtaining the classification guidelines in the location (country) in question. This only needs to be done once for each label, all other programmes with that label will be automatically adjusted. + +!['Updated rating label details'](static/img/doc/ratinglabel/updated_label.png) + +**NOTE:** In the example, the age provided by DVB is '10', whereas the age displayed is '13'. This is because the DVB standard subtracts 3 from some recommended ages before transmission meaning that the receiver must add 3 to the number received. When creating a placeholder label, this module will automatically add 3 where appropriate. + + +# XMLTV + +Ratings from XMLTV contain the rating label text, but not the recommended age. +``` + + MA + +``` + +When a new rating is encountered from an XMLTV EPG source, a placeholder label similar to the DVB ones is created and you will need to add the country code and the ages. + +!['Rating label learned from xmltv'](static/img/doc/ratinglabel/xmltv_learned.png) + +# Combined DVB OTA and XMLTV + +If you have multiple EPG sources for different groups of channels, it is possible to map the ratings from those multiple sources to produce a single unified rating system. This can be done by adjusting the 'display age' and 'display label' of the various sources until they are matched to your requirements. + +The same rating label can be used for both DVB OTA and XMLTV EPG sources. Because DVB OTA is matched on Country+Age and XMLTV is matched on Authority+Label, a rating label that contains all 4 of these values will be selected and the 'display age' and 'display label' will be used for both EPG sources. + +Sources can also be kept seperated by ensuring that a DVB OTA rating does not have an 'authority' that matches any XMLTV sources and that an XMLTV rating does not have an 'age' or 'country' that matches a DVB OTA source, only a 'display age'. + + +--- diff --git a/docs/markdown/inc/channels_contents.md b/docs/markdown/inc/channels_contents.md index baf1fd383..3c496cf6f 100644 --- a/docs/markdown/inc/channels_contents.md +++ b/docs/markdown/inc/channels_contents.md @@ -8,7 +8,7 @@ Contents | Description [EPG Grabber Channels](class/epggrab_channel) | EPG data sources used by channels [EPG Grabber](class/epggrab) | EPG grabber configuration [EPG Grabber Modules](class/epggrab_mod) | EPG grabber module management - +[Rating Labels Module](class/ratinglabel) | Rating Labels management diff --git a/docs/markdown/inc/ratinglabel_contents.md b/docs/markdown/inc/ratinglabel_contents.md new file mode 100644 index 000000000..3de35c2eb --- /dev/null +++ b/docs/markdown/inc/ratinglabel_contents.md @@ -0,0 +1,8 @@ +Contents | Description +---------------------------------------|------------------------ +[Overview](#overview) | Tab overview +[Items/Properties](#items) | Items and Properties +[EPG Grabber](class/epggrab) | EPG grabber configuration + + + diff --git a/src/api.c b/src/api.c index b224a30c5..7e853aa95 100644 --- a/src/api.c +++ b/src/api.c @@ -146,6 +146,7 @@ void api_init ( void ) api_service_init(); api_channel_init(); api_bouquet_init(); + api_ratinglabel_init(); api_epg_init(); api_epggrab_init(); api_status_init(); diff --git a/src/api.h b/src/api.h index a412776b9..7a815b426 100644 --- a/src/api.h +++ b/src/api.h @@ -67,6 +67,7 @@ void api_input_init ( void ); void api_service_init ( void ); void api_channel_init ( void ); void api_bouquet_init ( void ); +void api_ratinglabel_init ( void ); void api_mpegts_init ( void ); void api_epg_init ( void ); void api_epggrab_init ( void ); diff --git a/src/api/api_epg.c b/src/api/api_epg.c index 468475cc3..35718cc27 100644 --- a/src/api/api_epg.c +++ b/src/api/api_epg.c @@ -26,6 +26,7 @@ #include "dvr/dvr.h" #include "lang_codes.h" #include "string_list.h" +#include "epggrab.h" //Needed to be able to test for epggrab_conf.epgdb_processparentallabels static htsmsg_t * api_epg_get_list ( const char *s ) @@ -79,7 +80,7 @@ static htsmsg_t * api_epg_entry ( epg_broadcast_t *eb, const char *lang, const access_t *perm, const char **blank ) { const char *s, *blank2 = NULL; - char buf[32]; + char buf[128]; channel_t *ch = eb->channel; htsmsg_t *m, *m2; epg_episode_num_t epnum; @@ -102,10 +103,10 @@ api_epg_entry ( epg_broadcast_t *eb, const char *lang, const access_t *perm, con htsmsg_add_str(m, "episodeUri", eb->episodelink->uri); if (eb->serieslink) htsmsg_add_str(m, "serieslinkUri", eb->serieslink->uri); - + /* Channel Info */ api_epg_add_channel(m, ch, *blank); - + /* Time */ htsmsg_add_s64(m, "start", eb->start); htsmsg_add_s64(m, "stop", eb->stop); @@ -187,6 +188,24 @@ api_epg_entry ( epg_broadcast_t *eb, const char *lang, const access_t *perm, con if (eb->age_rating) htsmsg_add_u32(m, "ageRating", eb->age_rating); + if(epggrab_conf.epgdb_processparentallabels) + { + if (eb->rating_label) + { + if(eb->rating_label->rl_display_label){ + htsmsg_add_str(m, "ratingLabel", eb->rating_label->rl_display_label); + } + if(eb->rating_label->rl_icon){ + s = eb->rating_label->rl_icon; + if (!strempty(s)) { + s = imagecache_get_propstr(s, buf, sizeof(buf)); + if (s) + htsmsg_add_str(m, "ratingLabelIcon", s); + }//END we got an imagecache + }//END rating label icon is not null + }//END rating label is not null + }//END parental labels enabled. + if (eb->first_aired) htsmsg_add_s64(m, "first_aired", eb->first_aired); if (eb->copyright_year) @@ -220,7 +239,7 @@ api_epg_entry ( epg_broadcast_t *eb, const char *lang, const access_t *perm, con /* Next event */ if ((eb = epg_broadcast_get_next(eb))) htsmsg_add_u32(m, "nextEventId", eb->id); - + return m; } @@ -635,7 +654,7 @@ api_epg_related char *lang, *title_esc, *title_anchor; epg_set_t *serieslink = NULL; const char *title = NULL; - + if (htsmsg_get_u32(args, "eventId", &id)) return EINVAL; diff --git a/src/api/api_ratinglabel.c b/src/api/api_ratinglabel.c new file mode 100644 index 000000000..58e673be5 --- /dev/null +++ b/src/api/api_ratinglabel.c @@ -0,0 +1,92 @@ +/* + * API - ratinglabel calls + * + * Copyright (C) 2014 Jaroslav Kysela (Original Bouquets) + * Copyright (C) 2023 DeltaMikeCharlie (Updated for Rating Labels) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef __TVH_API_RATINGLABEL_H__ +#define __TVH_API_RATINGLABEL_H__ + +#include "tvheadend.h" +#include "ratinglabels.h" +#include "access.h" +#include "api.h" + +static int +api_ratinglabel_list + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + ratinglabel_t *rl; + htsmsg_t *l; + char ubuf[UUID_HEX_SIZE]; + + l = htsmsg_create_list(); + tvh_mutex_lock(&global_lock); + RB_FOREACH(rl, &ratinglabels, rl_link) + { + htsmsg_add_msg(l, NULL, htsmsg_create_key_val(idnode_uuid_as_str(&rl->rl_id, ubuf), rl->rl_country ?: "")); + } + tvh_mutex_unlock(&global_lock); + *resp = htsmsg_create_map(); + htsmsg_add_msg(*resp, "entries", l); + + return 0; +} + +static void +api_ratinglabel_grid + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf ) +{ + ratinglabel_t *rl; + + RB_FOREACH(rl, &ratinglabels, rl_link) + idnode_set_add(ins, (idnode_t*)rl, &conf->filter, perm->aa_lang_ui); +} + +static int +api_ratinglabel_create + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + htsmsg_t *conf; + ratinglabel_t *rl; + + if (!(conf = htsmsg_get_map(args, "conf"))) + return EINVAL; + + tvh_mutex_lock(&global_lock); + rl = ratinglabel_create(NULL, conf, NULL, NULL); + if (rl) + api_idnode_create(resp, &rl->rl_id); + tvh_mutex_unlock(&global_lock); + + return 0; +} + +void api_ratinglabel_init ( void ) +{ + static api_hook_t ah[] = { + { "ratinglabel/list", ACCESS_ADMIN, api_ratinglabel_list, NULL }, + { "ratinglabel/class", ACCESS_ADMIN, api_idnode_class, (void*)&ratinglabel_class }, + { "ratinglabel/grid", ACCESS_ADMIN, api_idnode_grid, api_ratinglabel_grid }, + { "ratinglabel/create", ACCESS_ADMIN, api_ratinglabel_create, NULL }, + { NULL }, + }; + + api_register_all(ah); +} + +#endif /* __TVH_API_RATINGLABEL_H__ */ diff --git a/src/dvr/dvr.h b/src/dvr/dvr.h index 3244b93b9..00c9d70cd 100644 --- a/src/dvr/dvr.h +++ b/src/dvr/dvr.h @@ -244,7 +244,14 @@ typedef struct dvr_entry { uint32_t de_content_type; /* Content type (from EPG) (only code) */ uint16_t de_copyright_year; /* Copyright year (from EPG) */ uint16_t de_dvb_eid; - uint16_t de_age_rating; /* Age rating (from EPG) */ + uint16_t de_age_rating; /* Age rating (from EPG) */ + //Depending how old the recording is, the current rating label system + //may have changed, so keep an absolute copy of the values at + //the time of recording rather than pointing to a rating label + //object that may no longer exist many years later. + char *de_rating_label_saved; /* Saved rating label for once the recording has been completed*/ + char *de_rating_icon_saved; /* Saved rating icon full path (not image cache) for once the recording has been completed*/ + ratinglabel_t *de_rating_label; /* 'Live' rating label object */ int de_pri; int de_dont_reschedule; @@ -592,7 +599,8 @@ dvr_entry_update( dvr_entry_t *de, int enabled, time_t start, time_t stop, time_t start_extra, time_t stop_extra, dvr_prio_t pri, int retention, int removal, - int playcount, int playposition, int age_rating); + int playcount, int playposition, int age_rating, + ratinglabel_t *rating_label); void dvr_destroy_by_channel(channel_t *ch, int delconf); diff --git a/src/dvr/dvr_db.c b/src/dvr/dvr_db.c index a7554a375..318b7f30e 100644 --- a/src/dvr/dvr_db.c +++ b/src/dvr/dvr_db.c @@ -31,6 +31,7 @@ #include "notify.h" #include "compat.h" #include "string_list.h" +#include "epggrab.h" //Needed to get the epggrab_conf.epgdb_processparentallabels flag. struct dvr_entry_list dvrentries; static int dvr_in_init; @@ -58,6 +59,8 @@ static void dvr_entry_watched_timer_disarm(dvr_entry_t* de); static dvr_entry_t *_dvr_duplicate_event(dvr_entry_t *de); +static const void *dvr_entry_class_rating_icon_url_get(void *o); + /* * */ @@ -1209,6 +1212,18 @@ dvr_entry_create_from_htsmsg(htsmsg_t *conf, epg_broadcast_t *e) htsmsg_add_u32(conf, "content_type", genre->code / 16); if(e->age_rating) htsmsg_add_u32(conf, "age_rating", e->age_rating); + + //Only process these fields if rating labels are enabled. + if(epggrab_conf.epgdb_processparentallabels){ + if(e->rating_label){ + htsmsg_set_uuid(conf, "rating_label_uuid", &e->rating_label->rl_id.in_uuid); + + if(e->rating_label->rl_icon){ + htsmsg_add_str(conf, "rating_icon_saved", imagecache_get_propstr(e->rating_label->rl_icon, tbuf, sizeof(tbuf))); + } + } + }//END rating labels enabled. + } de = dvr_entry_create(NULL, conf, 0); @@ -2416,7 +2431,8 @@ static dvr_entry_t *_dvr_entry_update const char *lang, time_t start, time_t stop, time_t start_extra, time_t stop_extra, dvr_prio_t pri, int retention, int removal, - int playcount, int playposition, int age_rating) + int playcount, int playposition, int age_rating, + ratinglabel_t *rating_label) { char buf[40]; int save = 0, updated = 0; @@ -2654,13 +2670,13 @@ dvr_entry_update time_t start, time_t stop, time_t start_extra, time_t stop_extra, dvr_prio_t pri, int retention, int removal, int playcount, int playposition, - int age_rating ) + int age_rating, ratinglabel_t *rating_label ) { return _dvr_entry_update(de, enabled, dvr_config_uuid, NULL, ch, title, subtitle, summary, desc, lang, start, stop, start_extra, stop_extra, pri, retention, removal, playcount, playposition, - age_rating); + age_rating, rating_label); } /** @@ -2714,7 +2730,7 @@ dvr_event_replaced(epg_broadcast_t *e, epg_broadcast_t *new_e) gmtime2local(e2->start, t1buf, sizeof(t1buf)), gmtime2local(e2->stop, t2buf, sizeof(t2buf))); _dvr_entry_update(de, -1, NULL, e2, NULL, NULL, NULL, NULL, NULL, - NULL, 0, 0, 0, 0, DVR_PRIO_NOTSET, 0, 0, -1, -1, 0); + NULL, 0, 0, 0, 0, DVR_PRIO_NOTSET, 0, 0, -1, -1, 0, NULL); return; } } @@ -2755,7 +2771,7 @@ void dvr_event_updated(epg_broadcast_t *e) assert(de->de_bcast == e); if (de->de_sched_state != DVR_SCHEDULED) continue; _dvr_entry_update(de, -1, NULL, e, NULL, NULL, NULL, NULL, NULL, - NULL, 0, 0, 0, 0, DVR_PRIO_NOTSET, 0, 0, -1, -1, 0); + NULL, 0, 0, 0, 0, DVR_PRIO_NOTSET, 0, 0, -1, -1, 0, NULL); } LIST_FOREACH(de, &e->channel->ch_dvrs, de_channel_link) { if (de->de_sched_state != DVR_SCHEDULED) continue; @@ -2767,7 +2783,7 @@ void dvr_event_updated(epg_broadcast_t *e) epg_broadcast_get_title(e, NULL), channel_get_name(e->channel, channel_blank_name)); _dvr_entry_update(de, -1, NULL, e, NULL, NULL, NULL, NULL, NULL, - NULL, 0, 0, 0, 0, DVR_PRIO_NOTSET, 0, 0, -1, -1, 0); + NULL, 0, 0, 0, 0, DVR_PRIO_NOTSET, 0, 0, -1, -1, 0, NULL); } } } @@ -3400,6 +3416,71 @@ dvr_entry_class_channel_name_get(void *o) return &prop_ptr; } +static int +dvr_entry_class_rating_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + ratinglabel_t *rl = NULL; + + //If RL processing is not enabled, return a null and exit. + if(!epggrab_conf.epgdb_processparentallabels){ + de->de_rating_label = NULL; + return 0; + } + + if (!dvr_entry_is_editable(de)) + return 0; + + //If the entry is in the past, don't link to the RL object. + if (de->de_stop < gclk()){ + de->de_rating_label = NULL; + return 0; + } + + rl = v ? ratinglabel_find_from_uuid(v) : NULL; + + //If the rating label is found. + if(rl){ + //Set the rating label pointer in the DVR entry object + de->de_rating_label = rl; + + //Save the label and icon values + if(de->de_rating_label_saved){ + free(de->de_rating_label_saved); + } + + if(rl->rl_display_label){ + de->de_rating_label_saved = strdup(rl->rl_display_label); + } + + if(de->de_rating_icon_saved){ + free(de->de_rating_icon_saved); + } + + if(rl->rl_icon){ + de->de_rating_icon_saved = strdup(rl->rl_icon); + } + + return 1; + }//END we got an RL object. + + return 0; +} + +//Return the UUID string for this rating label of this entry. +//If RL is not enabled, this function must return an empty string, +//returning NULL will cause a crash. +static const void * +dvr_entry_class_rating_get(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + if (de->de_rating_label) + idnode_uuid_as_str(&de->de_rating_label->rl_id, prop_sbuf); + else + prop_sbuf[0] = '\0'; + return &prop_sbuf_ptr; +} + static int dvr_entry_class_pri_set(void *o, const void *v) { @@ -3947,6 +4028,48 @@ dvr_entry_class_fanart_image_notify(void *o, const char *lang) (void)imagecache_get_id(dvr_entry_get_fanart_image(o)); } +static const void * +dvr_entry_class_rating_icon_url_get(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + ratinglabel_t *rl = de->de_rating_label; + if ((rl == NULL) || (de->de_sched_state > DVR_SCHEDULED)) { + //See if there is a saved icon and if so return the imagecache path for that icon. + prop_ptr = ""; + if(de->de_rating_icon_saved){ + prop_ptr = de->de_rating_icon_saved; + prop_ptr = imagecache_get_propstr(prop_ptr, prop_sbuf, PROP_SBUF_LEN); + } + } else { + //Get the icon from the live RL object. + return ratinglabel_class_get_icon (rl); + } + return &prop_ptr; +} + +static const void * +dvr_entry_class_rating_label_get(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + ratinglabel_t *rl = de->de_rating_label; + if (rl == NULL) { + prop_ptr = ""; + if(de->de_rating_label_saved){ + prop_ptr = de->de_rating_label_saved; + } + } else { + if(de->de_sched_state == DVR_SCHEDULED){ + prop_ptr = rl->rl_display_label; + } + else + { + prop_ptr = de->de_rating_label_saved; + } + + } + return &prop_ptr; +} + static const void * dvr_entry_class_duplicate_get(void *o) { @@ -4648,6 +4771,51 @@ const idclass_t dvr_entry_class = { .off = offsetof(dvr_entry_t, de_age_rating), .opts = PO_RDONLY | PO_EXPERT, }, + { + .type = PT_STR, + .id = "rating_label_saved", + .name = N_("Saved Rating Label"), + .desc = N_("Saved parental rating for once recording is complete."), + .off = offsetof(dvr_entry_t, de_rating_label_saved), + .opts = PO_RDONLY | PO_NOUI, + }, + { + .type = PT_STR, + .id = "rating_icon_saved", + .name = N_("Saved Rating Icon Path"), + .desc = N_("Saved parental rating icon for once recording is complete."), + .off = offsetof(dvr_entry_t, de_rating_icon_saved), + .opts = PO_RDONLY | PO_NOUI, + }, + //This needs to go after the 'saved' properties because loading the RL object + //can refresh the 'saved' objects for scheduled entries. + { + .type = PT_STR, + .id = "rating_label_uuid", + .name = N_("Rating Label UUID"), + .desc = N_("Parental rating label UUID."), + .set = dvr_entry_class_rating_set, + .get = dvr_entry_class_rating_get, + .opts = PO_RDONLY | PO_NOUI, + }, + //This needs to go after the RL object is loaded because the + //getter needs the object in order to get the imagecache icon path. + { + .type = PT_STR, + .id = "rating_icon", + .name = N_("Rating Icon"), + .desc = N_("Rating Icon URL."), + .get = dvr_entry_class_rating_icon_url_get, + .opts = PO_HIDDEN | PO_RDONLY | PO_NOSAVE | PO_NOUI, + }, + { + .type = PT_STR, + .id = "rating_label", + .name = N_("Rating Label"), + .desc = N_("Rating Label."), + .get = dvr_entry_class_rating_label_get, + .opts = PO_HIDDEN | PO_RDONLY | PO_NOSAVE, + }, {} } }; diff --git a/src/dvr/dvr_rec.c b/src/dvr/dvr_rec.c index eec60585d..972185803 100644 --- a/src/dvr/dvr_rec.c +++ b/src/dvr/dvr_rec.c @@ -1567,6 +1567,7 @@ dvr_thread_rec_start(dvr_entry_t **_de, streaming_start_t *ss, /* Persist entry so we save the filename details to avoid orphan * files if we crash before the programme completes recording. */ + de->de_rating_label = NULL; //Forget the rating label pointer and only rely on the saved values from here on. dvr_entry_changed(de); htsp_dvr_entry_update(de); if(code == 0) { diff --git a/src/epg.c b/src/epg.c index 865c90841..c8f160233 100644 --- a/src/epg.c +++ b/src/epg.c @@ -109,7 +109,7 @@ void epg_updated ( void ) * Object (Generic routines) * *************************************************************************/ -static void _epg_object_destroy +static void _epg_object_destroy ( epg_object_t *eo, epg_object_tree_t *tree ) { assert(eo->refcount == 0); @@ -475,7 +475,7 @@ int epg_channel_ignore_broadcast(channel_t *ch, time_t start) return 0; } -static void _epg_channel_rem_broadcast +static void _epg_channel_rem_broadcast ( channel_t *ch, epg_broadcast_t *ebc, epg_broadcast_t *ebc_new ) { RB_REMOVE(&ch->ch_epg_schedule, ebc, sched_link); @@ -533,7 +533,7 @@ static void _epg_channel_timer_callback ( void *p ) } break; } - + /* Change (update HTSP) */ if (cur != ch->ch_epg_now || nxt != ch->ch_epg_next) { tvhdebug(LS_EPG, "now/next %u/%u set on %s", @@ -557,7 +557,7 @@ static void _epg_channel_timer_callback ( void *p ) if (nxt) nxt->ops->putref(nxt); } -static epg_broadcast_t *_epg_channel_add_broadcast +static epg_broadcast_t *_epg_channel_add_broadcast ( channel_t *ch, epg_broadcast_t **bcast, epggrab_module_t *src, int create, int *save, epg_changes_t *changed ) { @@ -624,7 +624,7 @@ static epg_broadcast_t *_epg_channel_add_broadcast } } } - + /* Changed */ *save |= 1; @@ -988,6 +988,8 @@ int epg_broadcast_change_finish save |= epg_broadcast_set_star_rating(broadcast, 0, NULL); if (!(changes & EPG_CHANGED_AGE_RATING)) save |= epg_broadcast_set_age_rating(broadcast, 0, NULL); + if (!(changes & EPG_CHANGED_RATING_LABEL)) + save |= epg_broadcast_set_rating_label(broadcast, 0, NULL); if (!(changes & EPG_CHANGED_IMAGE)) save |= epg_broadcast_set_image(broadcast, NULL, NULL); if (!(changes & EPG_CHANGED_GENRE)) @@ -1051,6 +1053,7 @@ epg_broadcast_t *epg_broadcast_clone *save |= epg_broadcast_set_is_repeat(ebc, src->is_repeat, &changes); *save |= epg_broadcast_set_star_rating(ebc, src->star_rating, &changes); *save |= epg_broadcast_set_age_rating(ebc, src->age_rating, &changes); + *save |= epg_broadcast_set_rating_label(ebc, src->rating_label, &changes); *save |= epg_broadcast_set_image(ebc, src->image, &changes); *save |= epg_broadcast_set_genre(ebc, &src->genre, &changes); *save |= epg_broadcast_set_title(ebc, src->title, &changes); @@ -1414,7 +1417,7 @@ int epg_broadcast_set_genre } g1 = g2; } - + /* Insert all entries */ if (genre) { LIST_FOREACH(g1, genre, link) @@ -1440,6 +1443,20 @@ int epg_broadcast_set_age_rating changed, EPG_CHANGED_AGE_RATING); } +int epg_broadcast_set_rating_label + ( epg_broadcast_t *b, ratinglabel_t *rating_label, epg_changes_t *changed ) +{ + if (!b || !rating_label) return 0; + + if(rating_label != b->rating_label){ + b->rating_label = rating_label; + if (changed) *changed |= EPG_CHANGED_RATING_LABEL; + return 1; + } + + return 0; +} + int epg_broadcast_set_first_aired ( epg_broadcast_t *b, time_t aired, epg_changes_t *changed ) { @@ -1476,6 +1493,11 @@ const char *epg_broadcast_get_subtitle ( epg_broadcast_t *b, const char *lang ) if (!b || !b->subtitle) return NULL; return lang_str_get(b->subtitle, lang); } +const ratinglabel_t *epg_broadcast_get_rating_label ( epg_broadcast_t *b ) +{ + if (!b || !b->rating_label) return NULL; + return b->rating_label; +} const char *epg_broadcast_get_summary ( epg_broadcast_t *b, const char *lang ) { @@ -1561,6 +1583,10 @@ htsmsg_t *epg_broadcast_serialize ( epg_broadcast_t *broadcast ) htsmsg_add_u32(m, "star", broadcast->star_rating); if (broadcast->age_rating) htsmsg_add_u32(m, "age", broadcast->age_rating); + if (broadcast->rating_label) + { + htsmsg_add_str(m, "ratlab", idnode_uuid_as_str((idnode_t *)(broadcast->rating_label), ubuf)); + } if (broadcast->image) htsmsg_add_str(m, "img", broadcast->image); if (broadcast->title) @@ -1661,6 +1687,12 @@ epg_broadcast_t *epg_broadcast_deserialize *save |= epg_broadcast_set_star_rating(ebc, u32, &changes); if (!htsmsg_get_u32(m, "age", &u32)) *save |= epg_broadcast_set_age_rating(ebc, u32, &changes); + if ((str = htsmsg_get_str(m, "ratlab"))) + { + //Convert the UUID string saved on disk into an idnode ID + //and then fetch the ratinglabel object. + *save |= epg_broadcast_set_rating_label(ebc, ratinglabel_find_from_uuid(str), &changes); + } if ((str = htsmsg_get_str(m, "img"))) *save |= epg_broadcast_set_image(ebc, str, &changes); @@ -2010,7 +2042,7 @@ int epg_genre_list_add ( epg_genre_list_t *list, epg_genre_t *genre ) LIST_INSERT_HEAD(list, g2, link); } else { while (g1) { - + /* Already exists */ if (g1->code == genre->code) return 0; @@ -2059,7 +2091,7 @@ int epg_genre_list_add_by_str ( epg_genre_list_t *list, const char *str, const c // Note: if partial=1 and genre is a major only category then all minor // entries will also match -int epg_genre_list_contains +int epg_genre_list_contains ( epg_genre_list_t *list, epg_genre_t *genre, int partial ) { uint8_t mask = 0xFF; @@ -2487,7 +2519,7 @@ epg_query ( epg_query_t *eq, access_t *perm ) if (channel && tag == NULL) { if (channel_access(channel, perm, 0)) _eq_add_channel(eq, channel); - + /* Tag based */ } else if (tag) { idnode_list_mapping_t *ilm; diff --git a/src/epg.h b/src/epg.h index b81589c86..da595fd39 100644 --- a/src/epg.h +++ b/src/epg.h @@ -24,6 +24,7 @@ #include "lang_str.h" #include "string_list.h" #include "access.h" +#include "ratinglabels.h" //Needed for the ratinglabel_t struct. /* * External forward decls @@ -149,6 +150,7 @@ typedef uint64_t epg_changes_t; #define EPG_CHANGED_AGE_RATING (1ULL<<31) #define EPG_CHANGED_FIRST_AIRED (1ULL<<32) #define EPG_CHANGED_COPYRIGHT_YEAR (1ULL<<33) +#define EPG_CHANGED_RATING_LABEL (1ULL<<34) typedef struct epg_object_ops { void (*getref) ( void *o ); ///< Get a reference @@ -163,7 +165,7 @@ struct epg_object RB_ENTRY(epg_object) id_link; ///< Global (ID) link LIST_ENTRY(epg_object) un_link; ///< Global unref'd link LIST_ENTRY(epg_object) up_link; ///< Global updated link - + epg_object_type_t type; ///< Specific object type uint32_t id; ///< Internal ID time_t updated; ///< Last time object was changed @@ -203,7 +205,7 @@ void epg_episode_epnum_deserialize( htsmsg_t *m, epg_episode_num_t *num ); /* EpNum format helper */ // output string will be: -// if (episode_num) +// if (episode_num) // ret = pre // if (season_num) ret += sprintf(sfmt, season_num) // if (season_cnt && cnt) ret += sprintf(cnt, season_cnt) @@ -255,7 +257,7 @@ struct epg_broadcast struct channel *channel; ///< Channel being broadcast on RB_ENTRY(epg_broadcast) sched_link; ///< Schedule link LIST_HEAD(, dvr_entry) dvr_entries; ///< Associated DVR entries - + /* */ uint16_t dvb_eid; ///< DVB Event ID time_t start; ///< Start time @@ -276,6 +278,7 @@ struct epg_broadcast /* Misc flags */ uint8_t star_rating; ///< Star rating uint8_t age_rating; ///< Age certificate + ratinglabel_t *rating_label; ///< Age certificate label (eg: 'PG') uint8_t is_new; ///< New series / file premiere uint8_t is_repeat; ///< Repeat screening uint8_t running; ///< EPG running flag @@ -310,7 +313,7 @@ struct epg_broadcast }; /* Lookup */ -epg_broadcast_t *epg_broadcast_find_by_time +epg_broadcast_t *epg_broadcast_find_by_time ( struct channel *ch, struct epggrab_module *src, time_t start, time_t stop, int create, int *save, epg_changes_t *changes ); epg_broadcast_t *epg_broadcast_find_by_eid ( struct channel *ch, uint16_t eid ); @@ -337,7 +340,7 @@ int epg_broadcast_set_is_widescreen int epg_broadcast_set_is_hd ( epg_broadcast_t *b, uint8_t hd, epg_changes_t *changed ) __attribute__((warn_unused_result)); -int epg_broadcast_set_lines +int epg_broadcast_set_lines ( epg_broadcast_t *b, uint16_t lines, epg_changes_t *changed ) __attribute__((warn_unused_result)); int epg_broadcast_set_aspect @@ -415,11 +418,14 @@ int epg_broadcast_set_copyright_year int epg_broadcast_set_age_rating ( epg_broadcast_t *b, uint8_t age, epg_changes_t *changed ) __attribute__((warn_unused_result)); +int epg_broadcast_set_rating_label + ( epg_broadcast_t *b, ratinglabel_t *rating_label, epg_changes_t *changed ) + __attribute__((warn_unused_result)); /* Accessors */ epg_broadcast_t *epg_broadcast_get_prev( epg_broadcast_t *b ); epg_broadcast_t *epg_broadcast_get_next( epg_broadcast_t *b ); -const char *epg_broadcast_get_title +const char *epg_broadcast_get_title ( const epg_broadcast_t *b, const char *lang ); const char *epg_broadcast_get_subtitle ( epg_broadcast_t *b, const char *lang ); @@ -432,12 +438,14 @@ 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 ); +const ratinglabel_t *epg_broadcast_get_rating_label + ( epg_broadcast_t *b ); /* Episode number heplers */ // Note: this does NOT strdup the text field void epg_broadcast_get_epnum ( const epg_broadcast_t *b, epg_episode_num_t *epnum ); -size_t epg_broadcast_epnumber_format +size_t epg_broadcast_epnumber_format ( epg_broadcast_t *b, char *buf, size_t len, const char *pre, const char *sfmt, const char *sep, const char *efmt, @@ -452,7 +460,7 @@ static inline int epg_episode_match(epg_broadcast_t *a, epg_broadcast_t *b) /* Serialization */ htsmsg_t *epg_broadcast_serialize ( epg_broadcast_t *b ); -epg_broadcast_t *epg_broadcast_deserialize +epg_broadcast_t *epg_broadcast_deserialize ( htsmsg_t *m, int create, int *save ); /* ************************************************************************ diff --git a/src/epggrab.c b/src/epggrab.c index 415250061..369651be7 100644 --- a/src/epggrab.c +++ b/src/epggrab.c @@ -271,7 +271,7 @@ static void _epggrab_load ( void ) epggrab_conf.epgdb_periodicsave * 3600); idnode_notify_changed(&epggrab_conf.idnode); - + /* Load module config (channels) */ eit_load(); opentv_load(); @@ -426,6 +426,15 @@ const idclass_t epggrab_class = { .off = offsetof(epggrab_conf_t, epgdb_saveafterimport), .group = 1, }, + { + .type = PT_BOOL, + .id = "epgdb_processparentallabels", + .name = N_("Process Parental Rating Labels"), + .desc = N_("Convert broadcast ratings codes into " + "human-readable labels like 'PG' or 'FSK 16'."), + .off = offsetof(epggrab_conf_t, epgdb_processparentallabels), + .group = 1, + }, { .type = PT_STR, .id = "cron", @@ -536,6 +545,7 @@ void epggrab_init ( void ) epggrab_conf.channel_reicon = 0; epggrab_conf.epgdb_periodicsave = 0; epggrab_conf.epgdb_saveafterimport = 0; + epggrab_conf.epgdb_processparentallabels = 0; epggrab_cron_multi = NULL; @@ -566,7 +576,7 @@ void epggrab_init ( void ) /* Initialise the OTA subsystem */ epggrab_ota_init(); - + /* Load config */ _epggrab_load(); diff --git a/src/epggrab.h b/src/epggrab.h index 5b33696e7..62020a49a 100644 --- a/src/epggrab.h +++ b/src/epggrab.h @@ -318,6 +318,7 @@ typedef struct epggrab_conf { uint32_t channel_reicon; uint32_t epgdb_periodicsave; uint32_t epgdb_saveafterimport; + uint32_t epgdb_processparentallabels; char *ota_cron; char *ota_genre_translation; uint32_t ota_timeout; diff --git a/src/epggrab/module/eit.c b/src/epggrab/module/eit.c index 63df1b154..1b40b1dc3 100644 --- a/src/epggrab/module/eit.c +++ b/src/epggrab/module/eit.c @@ -28,6 +28,7 @@ #include "input.h" #include "input/mpegts/dvb_charset.h" #include "dvr/dvr.h" +#include "ratinglabels.h" /* ************************************************************************ * Opaque @@ -134,6 +135,7 @@ typedef struct eit_event uint8_t bw; uint8_t parental; + ratinglabel_t *rating_label; uint8_t is_new; time_t first_aired; @@ -424,14 +426,62 @@ static int _eit_desc_parental ( epggrab_module_t *mod, const uint8_t *ptr, int len, eit_event_t *ev ) { int cnt = 0, sum = 0, i = 3; + + char tmpCountry[4]; + int tmpAge = 0; + ratinglabel_t *rl = NULL; + while (len > 3) { + + //If we are processing parental rating labels. + if(epggrab_conf.epgdb_processparentallabels) + { + //Get the recommended age for this rating. + //0x00 undefined + //0x01 to 0x0F minimum age = rating + 3 years + //0x10 to 0xFF defined by the broadcaster + if(ptr[i] == 0) + { + tmpAge = 0; + } + else + { + tmpAge = ptr[i]; //Do not add 3 here, do that with the 'display age'. + } + + //Get the country code for this rating. + tmpCountry[0] = ptr[0]; + tmpCountry[1] = ptr[1]; + tmpCountry[2] = ptr[2]; + tmpCountry[3] = 0; + + tvhtrace(LS_TBL_EIT, "Country '%s', age '%d'", tmpCountry, tmpAge); + + //Look for a matching rating label + rl = ratinglabel_find_from_eit(tmpCountry, tmpAge); + + //If we have found a rating label, save the details and exit. + //ie, ony use the first parental rating found. + //TODO: In future, if (eg in Europe) the rating codes from multiple + //countries are present, select the one that user prefers. + //A new config option will be needed for this. + //HOWEVER: A sampling of EIT data from European users + //suggests that this will not be necessary. + if(rl){ + ev->parental = rl->rl_display_age; + ev->rating_label = rl; + return 0; + } + }//END rating labels are being processed. + //If rating labels are not processed, do the original TVH process. + if ( ptr[i] && ptr[i] < 0x10 ) { cnt++; sum += (ptr[i] + 3); } len -= 4; i += 4; - } + }//END loop through descriptors // Note: we ignore the country code and average the lot! if (cnt) ev->parental = (uint8_t)(sum / cnt); @@ -734,6 +784,13 @@ static int _eit_process_event_one *save |= epg_broadcast_set_genre(ebc, ev->genre, &changes); if (ev->parental) *save |= epg_broadcast_set_age_rating(ebc, ev->parental, &changes); + + if (ev->rating_label) + { + tvhtrace(mod->subsys, "About to save rating label '%p'", ev->rating_label); + *save |= epg_broadcast_set_rating_label(ebc, ev->rating_label, &changes); + } + if (ev->subtitle) *save |= epg_broadcast_set_subtitle(ebc, ev->subtitle, &changes); else if ((short_target == 0 || short_target == 2) && ev->summary) @@ -826,6 +883,14 @@ static int _eit_process_event break; case DVB_DESC_PARENTAL_RAT: r = _eit_desc_parental(mod, ptr, dlen, &ev); + if(epggrab_conf.epgdb_processparentallabels){ + if(ev.rating_label){ + tvhtrace(mod->subsys, "RATINGLABEL '%d' '%s'", ev.parental, ev.rating_label->rl_display_label); + } else { + tvhtrace(mod->subsys, "RATINGLABEL '%d' ''", ev.parental); + } + } + break; case DVB_DESC_CRID: r = _eit_desc_crid(mod, ptr, dlen, &ev, ed); diff --git a/src/epggrab/module/xmltv.c b/src/epggrab/module/xmltv.c index 6f09f3e18..f21133470 100644 --- a/src/epggrab/module/xmltv.c +++ b/src/epggrab/module/xmltv.c @@ -448,40 +448,86 @@ static int _xmltv_parse_star_rating static int _xmltv_parse_age_rating ( epg_broadcast_t *ebc, htsmsg_t *body, epg_changes_t *changes ) { - uint8_t age; - htsmsg_t *rating, *tags; + uint8_t age = 0; + htsmsg_t *rating, *tags, *attrib; const char *s1; if (!ebc || !body) return 0; htsmsg_field_t *f; - HTSMSG_FOREACH(f, body) { - if (!strcmp(htsmsg_field_name(f), "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_broadcast_set_age_rating(ebc, age, changes); + + const char *rating_system; //System attribute from the 'rating' tag : + ratinglabel_t *rl = NULL; + + //Clear the rating label. + //If the event is already in the EPG DB with another + //rating label, this will clear the existing rating lable + //prior to setting the new one -if- the new one + //just happens to be null. + ebc->rating_label = rl; + + //Only look for rating labels if enabled. + if(epggrab_conf.epgdb_processparentallabels){ + HTSMSG_FOREACH(f, body) { + if (!strcmp(htsmsg_field_name(f), "rating") && (rating = htsmsg_get_map_by_field(f))) { + //Look for a 'system' attribute of the 'rating' tag + rating_system = NULL; + if ((attrib = htsmsg_get_map(rating, "attrib"))){ + rating_system = htsmsg_get_str(attrib, "system"); + }//END get the atrtibutes for the rating tag. + //Look for sub-tags of the 'rating' tag + if ((tags = htsmsg_get_map(rating, "tags"))) { + //Look the the 'value' tag containing the actual rating text + if ((s1 = htsmsg_xml_get_cdata_str(tags, "value"))) { + //tvherror(LS_RATINGLABELS, "XMLTV got system: '%s' / '%s'", rating_system, s1); + + rl = ratinglabel_find_from_xmltv(rating_system, s1); + + if(rl){ + tvhtrace(LS_RATINGLABELS, "Found label: '%s' / '%s' / '%s' / '%d'", rl->rl_authority, rl->rl_label, rl->rl_country, rl->rl_age); + ebc->rating_label = rl; + if (rl->rl_display_age >= 0 && rl->rl_display_age < 22){ + return epg_broadcast_set_age_rating(ebc, rl->rl_display_age, changes); + }//END age sanity test + }//END we matched a rating label + }//END we got a value to inspect + }//END get sub-tags of the rating tag. + }//END got the 'rating' tag. + }//END loop through each XML tag + }//END rating labels enabled + else + //Perform the existing XMLTV lookup + { + HTSMSG_FOREACH(f, body) { + if (!strcmp(htsmsg_field_name(f), "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_broadcast_set_age_rating(ebc, age, changes); + } } } } } + + return 0; } @@ -589,13 +635,13 @@ _xmltv_parse_credits(htsmsg_t **out_credits, htsmsg_t *tags) char *s, *str2 = NULL, *saveptr = NULL; if (str == NULL) continue; if (strstr(str, "|") == 0) { - + if (strlen(str) > 255) { str2 = strdup(str); str2[255] = '\0'; str = str2; } - + if (!credits_names) credits_names = string_list_create(); string_list_insert(credits_names, str); diff --git a/src/htsp_server.c b/src/htsp_server.c index 352e40c27..7102a9a87 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -43,6 +43,7 @@ #endif #include "settings.h" +#include "epggrab.h" //Needed to be able to test for epggrab_conf.epgdb_processparentallabels /* ************************************************************************** * Datatypes and variables @@ -50,7 +51,7 @@ static void *htsp_server, *htsp_server_2; -#define HTSP_PROTO_VERSION 36 +#define HTSP_PROTO_VERSION 37 #define HTSP_ASYNC_OFF 0x00 #define HTSP_ASYNC_ON 0x01 @@ -960,6 +961,7 @@ htsp_build_dvrentry(htsp_connection_t *htsp, dvr_entry_t *de, const char *method uint32_t u32; char buf[512]; char ubuf[UUID_HEX_SIZE]; + const char *str; htsmsg_add_u32(out, "id", idnode_get_short_uuid(&de->de_id)); @@ -1001,10 +1003,52 @@ htsp_build_dvrentry(htsp_connection_t *htsp, dvr_entry_t *de, const char *method //To not risk breaking older clients, only //provide the 'age rating' via HTSP if the requested - //API version if greater than 35. - if (htsp->htsp_version > 35) + //API version if greater than 36. + if (htsp->htsp_version > 36) { + //Having the age in the DVR entry is new, that is why + //it is processed inside the version test. htsmsg_add_u32(out, "ageRating", de->de_age_rating); + + //Only go on to add the rating label stuff if + //rating labels are enabled. + if(epggrab_conf.epgdb_processparentallabels){ + //If this is still scheduled (in the future) then send the current values, + //if not, send the 'saved' values. + + if(de->de_sched_state == DVR_SCHEDULED){ + if(de->de_rating_label){ + if(de->de_rating_label->rl_display_label){ + htsmsg_add_str(out, "ratingLabel", de->de_rating_label->rl_display_label); + } + //If the rating icon is not null. + if(de->de_rating_label->rl_icon){ + str = de->de_rating_label->rl_icon; + if (!strempty(str)) { + str = imagecache_get_propstr(str, buf, sizeof(buf)); + if (str) + htsmsg_add_str(out, "ratingIcon", str); + }//END got an imagecache location + }//END icon not null + } + } + else + { + if(de->de_rating_label_saved){ + if(de->de_rating_label_saved){ + htsmsg_add_str(out, "ratingLabel", de->de_rating_label_saved); + } + if(de->de_rating_icon_saved){ + str = de->de_rating_icon_saved; + if (!strempty(str)) { + str = imagecache_get_propstr(str, buf, sizeof(buf)); + if (str) + htsmsg_add_str(out, "ratingIcon", str); + }//END got an imagecache location + }//END icon not null + } + } + }//END processing rating labels is enabled } @@ -1313,8 +1357,41 @@ htsp_build_event if (htsp->htsp_version < 6) code = (code >> 4) & 0xF; htsmsg_add_u32(out, "contentType", code); } - if (e->age_rating) + if (e->age_rating){ htsmsg_add_u32(out, "ageRating", e->age_rating); + } + //To not risk breaking older clients, only + //provide the 'rating label' & 'rating icon' via HTSP if the requested + //version if greater than 36. + //Because this is the EPG, do not restrict the ageRating based on version, + //that field was added in a very early version. + if (htsp->htsp_version > 36) + { + //If we are processing parental labels + if(epggrab_conf.epgdb_processparentallabels) + { + //If this event had a label pointer that is not null + if (e->rating_label) + { + //If there is a 'display label' + //Do not fall-back to the 'label' because the 'display label' + //may be intentionally null. + if(e->rating_label->rl_display_label){ + htsmsg_add_str(out, "ratingLabel", e->rating_label->rl_display_label); + } + //If the rating icon is not null. + if(e->rating_label->rl_icon){ + str = e->rating_label->rl_icon; + if (!strempty(str)) { + str = imagecache_get_propstr(str, buf, sizeof(buf)); + if (str) + htsmsg_add_str(out, "ratingIcon", str); + }//END got an imagecache location + }//END icon not null + }//END rating label not null + }//END parental labels enabled. + }//END HTSP version check + if (e->star_rating) htsmsg_add_u32(out, "starRating", e->star_rating); if (e->copyright_year) @@ -2056,6 +2133,7 @@ htsp_method_updateDvrEntry(htsp_connection_t *htsp, htsmsg_t *in) channel_t *channel = NULL; int enabled, retention, removal, playcount = -1, playposition = -1; int age_rating; + ratinglabel_t *rating_label; de = htsp_findDvrEntry(htsp, in, &out, 0); if (de == NULL) @@ -2080,6 +2158,7 @@ htsp_method_updateDvrEntry(htsp_connection_t *htsp, htsmsg_t *in) removal = htsmsg_get_u32_or_default(in, "removal", DVR_RET_REM_DVRCONFIG); priority = htsmsg_get_u32_or_default(in, "priority", DVR_PRIO_NOTSET); age_rating = htsmsg_get_u32_or_default(in, "ageRating", 0); + rating_label = NULL; //Rating labels not supported for manually created DVR entries title = htsmsg_get_str(in, "title"); subtitle = htsmsg_get_str(in, "subtitle"); summary = htsmsg_get_str(in, "summary"); @@ -2113,7 +2192,7 @@ htsp_method_updateDvrEntry(htsp_connection_t *htsp, htsmsg_t *in) de = dvr_entry_update(de, enabled, dvr_config_name, channel, title, subtitle, summary, desc, lang, start, stop, start_extra, stop_extra, priority, retention, removal, playcount, playposition, - age_rating); + age_rating, rating_label); return htsp_success(); } diff --git a/src/imagecache.c b/src/imagecache.c index 67d0a937e..a88ef6253 100644 --- a/src/imagecache.c +++ b/src/imagecache.c @@ -681,7 +681,7 @@ imagecache_get_id ( const char *url ) return 0; tvh_mutex_lock(&imagecache_lock); - + /* Skeleton */ SKEL_ALLOC(imagecache_skel); imagecache_skel->url = url; diff --git a/src/main.c b/src/main.c index 04c5434ce..f02b4f8cc 100644 --- a/src/main.c +++ b/src/main.c @@ -71,6 +71,7 @@ #include "transcoding/codec.h" #include "profile.h" #include "bouquet.h" +#include "ratinglabels.h" #include "tvhtime.h" #include "packet.h" #include "streaming.h" @@ -696,7 +697,7 @@ mtimer_thread(void *aux) cb = mti->mti_callback; LIST_REMOVE(mti, mti_link); mti->mti_callback = NULL; - + mtimer_running = mti; tvh_mutex_unlock(&mtimer_lock); @@ -1293,6 +1294,7 @@ main(int argc, char **argv) tvhftrace(LS_MAIN, http_client_init); tvhftrace(LS_MAIN, esfilter_init); tvhftrace(LS_MAIN, bouquet_init); + tvhftrace(LS_MAIN, ratinglabel_init); tvhftrace(LS_MAIN, service_init); tvhftrace(LS_MAIN, descrambler_init); tvhftrace(LS_MAIN, dvb_init); @@ -1401,6 +1403,7 @@ main(int argc, char **argv) tvhftrace(LS_MAIN, service_done); tvhftrace(LS_MAIN, channel_done); tvhftrace(LS_MAIN, bouquet_done); + tvhftrace(LS_MAIN, ratinglabel_done); tvhftrace(LS_MAIN, subscription_done); tvhftrace(LS_MAIN, access_done); tvhftrace(LS_MAIN, epg_done); diff --git a/src/prop.c b/src/prop.c index 57612c742..e91f75b31 100644 --- a/src/prop.c +++ b/src/prop.c @@ -241,7 +241,7 @@ prop_write_values break; } } - + /* Setter */ if (p->set && snew) save = p->set(obj, snew); @@ -298,7 +298,7 @@ prop_read_value assert(p->get); /* requirement */ if (val) htsmsg_add_msg(m, name, (htsmsg_t*)val); - + /* Single */ } else { switch(p->type) { @@ -380,7 +380,7 @@ prop_read_values const property_t *p; htsmsg_field_t *f; int b, total = 0, count = 0; - + HTSMSG_FOREACH(f, list) { total++; if (!htsmsg_field_get_bool(f, &b)) { diff --git a/src/ratinglabels.c b/src/ratinglabels.c new file mode 100644 index 000000000..52ea4f1fa --- /dev/null +++ b/src/ratinglabels.c @@ -0,0 +1,711 @@ +/* + * tvheadend, Rating Labels + * Copyright (C) 2014 Jaroslav Kysela (Original Bouquets) + * Copyright (C) 2023 DeltaMikeCharlie (Updated for Rating Labels) + * + * 'Rating labels' are text codes like 'PG', 'PG-13', 'FSK 12', etc, + * and are related to the parental classification code values + * that are broadcast via DVB as numbers. + * Each country/region has their own ratings. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "tvheadend.h" +#include "settings.h" +#include "access.h" +#include "imagecache.h" +#include "ratinglabels.h" +#include "input.h" + +#include "channels.h" //Needed to loop through channels when deleting RL pointers from EPG +#include "dvr/dvr.h" //Needed to check recordings for RL pointers. + +ratinglabel_tree_t ratinglabels; + +void ratinglabel_init(void); +void ratinglabel_done(void); +ratinglabel_t *ratinglabel_find_from_eit(char *country, int age); +ratinglabel_t *ratinglabel_find_from_xmltv(const char *authority, const char *label); +ratinglabel_t *ratinglabel_find_from_uuid(const char *string_uuid); +const char *ratinglabel_get_icon(ratinglabel_t *rl); + +static htsmsg_t *ratinglabel_class_save(idnode_t *self, char *filename, size_t fsize); +const void *ratinglabel_class_get_icon (void *obj); +ratinglabel_t *ratinglabel_create_placeholder(int enabled, const char *country, int age, + int display_age, const char *display_label, + const char *label, const char *authority); + +/** + * + */ +static int +_rl_cmp(const void *a, const void *b) +{ + return 1; +} + +/* + Used at EPG load time to get the parentalrating object from the + UUID string that is stored in the EPG database. +*/ +ratinglabel_t * +ratinglabel_find_from_uuid(const char *string_uuid) +{ + + if(string_uuid[0] == 0){ + tvhtrace(LS_RATINGLABELS, "Empty UUID when matching rating label, exiting."); + return NULL; + } + + tvh_uuid_t binary_uuid; + ratinglabel_t *rl = NULL; + ratinglabel_t *temp_rl = NULL; + + if (!uuid_set(&binary_uuid, string_uuid)) { + + RB_FOREACH(temp_rl, &ratinglabels, rl_link) + { + if(!memcmp((const void *)&binary_uuid, (const void *)(temp_rl->rl_id).in_uuid.bin, UUID_BIN_SIZE)){ + rl = temp_rl; + if(rl->rl_enabled){ + tvhdebug(LS_RATINGLABELS, "Matched UUID '%s' with rating label '%s' / '%d'.", string_uuid, rl->rl_display_label, rl->rl_display_age); + return rl; + } + } + }//END FOR loop + }//END the binary conversion worked + + //To get here, either the UUID was invalid or there was no matching ENABLED ratinglabel. + return NULL; + +} + +/* + Used by the XMLTV EPG load process when a tag + is encountered. Match the Authority+Label to a rating label and + return a pointer to that object if the label in enabled, null if not. +*/ +ratinglabel_t * +ratinglabel_find_from_xmltv(const char *authority, const char *label){ + + ratinglabel_t *rl = NULL; + ratinglabel_t *temp_rl = NULL; + + RB_FOREACH(temp_rl, &ratinglabels, rl_link) + { + //If both authorities are null OR both authorities match + if((!authority && !temp_rl->rl_authority) || !strcasecmp(temp_rl->rl_authority, authority)){ + + //Match 2 null labels + if(!temp_rl->rl_label && !label){ + tvherror(LS_RATINGLABELS, " matched 2 nulls."); + rl = temp_rl; + break; + } + + //if only 1 of the labels is null, then no match. + //This test protects strcasecmp from a null pointer + if(!temp_rl->rl_label || !label){ + //No match found + } + else + { + if(!strcasecmp(temp_rl->rl_label, label)){ + rl = temp_rl; + break; + } + } + } + } + + //Did we get a match? + if(rl) + { + if(!rl->rl_enabled) + { + tvhtrace(LS_RATINGLABELS, "Not enabled, returning NULL."); + return NULL; + } + } + else + { + char tmpLabel[129]; //Authority+label + + snprintf(tmpLabel, 128, "XMLTV:%s:%s", authority, label); + + //The XMLTV module is holding a lock. Unlock and then relock. + tvh_mutex_unlock(&global_lock); + rl = ratinglabel_create_placeholder(1, "???", 0, 0, tmpLabel, label, authority); + tvh_mutex_lock(&global_lock); + + } + + return rl; + +} + +/* + Used by the EIT EPG load process when a parental rating tag + is encountered. Match the Country+Age to a rating label and + return a pointer to that object if the label in enabled, null if not. +*/ +ratinglabel_t * +ratinglabel_find_from_eit(char *country, int age) +{ + tvhdebug(LS_RATINGLABELS, "Looking for '%s', '%d'. Count: '%d'.", country, age, ratinglabels.entries); + + ratinglabel_t *rl = NULL; + ratinglabel_t *temp_rl = NULL; + int tmpAge = 0; + + //RB_FIND was tried but it gave some false negatives + //so I decided to do things the hard way. + RB_FOREACH(temp_rl, &ratinglabels, rl_link) + { + if(temp_rl->rl_age == age){ + if(!strcasecmp(temp_rl->rl_country, country)){ + rl = temp_rl; + break; + } + } + } + + //Did we get a match? + if(rl) + { + if(!rl->rl_enabled) + { + tvhtrace(LS_RATINGLABELS, "Not enabled, returning NULL."); + return NULL; + } + } + else + { + tvhtrace(LS_RATINGLABELS, "Not found, creating placeholder '%s' / '%d'. Count: '%d' ", country, age, ratinglabels.entries); + + char tmpLabel[17]; //DBV+Country+Age+null + + snprintf(tmpLabel, 16, "DVB:%s:%d", country, age); + + //Do the DVB age adjustment + //0x00 undefined + //0x01 to 0x0F minimum age = rating + 3 years + //0x10 to 0xFF defined by the broadcaster + tmpAge = age; + if((age < 0x10) && (age != 0x00)){ + tmpAge = age + 3; + } + + rl = ratinglabel_create_placeholder(1, country, age, tmpAge, tmpLabel, tmpLabel, "NONE"); + + } + + return rl; +} + +/* + Create a placeholder label for newly encountered ratings. + The user needs to manually provide the appropriate fields. +*/ +ratinglabel_t * +ratinglabel_create_placeholder(int enabled, const char *country, int age, + int display_age, const char *display_label, + const char *label, const char *authority){ + + ratinglabel_t *rl_new = NULL; + htsmsg_t *msg_new = htsmsg_create_map(); + + htsmsg_add_bool(msg_new, "enabled", enabled); + htsmsg_add_s64(msg_new, "age", age); + htsmsg_add_s64(msg_new, "display_age", display_age); + htsmsg_add_str(msg_new, "country", country); + htsmsg_add_str(msg_new, "label", label); + htsmsg_add_str(msg_new, "display_label", display_label); + htsmsg_add_str(msg_new, "authority", authority); + + tvh_mutex_lock(&global_lock); + rl_new = ratinglabel_create(NULL, msg_new, NULL, NULL); + if (rl_new) + { + tvhtrace(LS_RATINGLABELS, "Success: Created placeholder '%s' / '%d' : '%s / '%s'. Count: '%d'", country, age, label, authority, ratinglabels.entries); + idnode_changed((idnode_t *)rl_new); + } + + tvh_mutex_unlock(&global_lock); + + return rl_new; + +} + + +/** + * Free up the memory and safely dispose of the ratinglabel object + */ +static void +ratinglabel_free(ratinglabel_t *rl) +{ + idnode_save_check(&rl->rl_id, 1); + idnode_unlink(&rl->rl_id); + + free(rl->rl_country); + free(rl->rl_display_label); + free(rl->rl_label); + free(rl->rl_authority); + free(rl->rl_icon); + free(rl); +} + +/** + * Create a reating label object + */ +ratinglabel_t * +ratinglabel_create(const char *uuid, htsmsg_t *conf, + const char *name, const char *src) +{ + //Minimum fields: (country+age) or (authority+label) + //If the XMLTV has no 'system' then that function needs + //send something like 'xmltvraw' as the authority. + //'remapping' could see multiple country+age+label conbinations + + tvhtrace(LS_RATINGLABELS, "Creating rating label '%s'.", uuid); + ratinglabel_t *rl, *rl2 = NULL; + int i; + + lock_assert(&global_lock); + + rl = calloc(1, sizeof(ratinglabel_t)); + + if (idnode_insert(&rl->rl_id, uuid, &ratinglabel_class, 0)) { + if (uuid) + tvherror(LS_RATINGLABELS, "Invalid rating label UUID '%s'", uuid); + ratinglabel_free(rl); + return NULL; + } + + if (conf) { + rl->rl_in_load = 1; + idnode_load(&rl->rl_id, conf); + rl->rl_in_load = 0; + if (!htsmsg_get_bool(conf, "shield", &i) && i) + rl->rl_shield = 1; + } + + rl2 = RB_INSERT_SORTED(&ratinglabels, rl, rl_link, _rl_cmp); + if (rl2) { + ratinglabel_free(rl); + return NULL; + } + + //Load the rating icon into the image cache if it is not already there. + if(rl){ + if(rl->rl_icon){ + (void)imagecache_get_id(rl->rl_icon); + } + } + + rl->rl_saveflag = 1; + + return rl; +} + +/** + * This deletes an individual ratinglabel object + */ +static void +ratinglabel_destroy(ratinglabel_t *rl) +{ + + if (!rl){ + return; + } + + tvhtrace(LS_RATINGLABELS, "Deleting rating label '%s' '%d'.", rl->rl_country, rl->rl_age); + RB_REMOVE(&ratinglabels, rl, rl_link); + + ratinglabel_free(rl); +} + +/* + * + */ +void +ratinglabel_completed(ratinglabel_t *rl, uint32_t seen) +{ + idnode_set_t *remove; + //size_t z; + + //z=0; //DUMMY + //z++; //DUMMY + + if (!rl) + return; + + if (!rl->rl_enabled) + goto save; + + remove = idnode_set_create(0); + idnode_set_free(remove); + +save: + if (rl->rl_saveflag) { + rl->rl_saveflag = 0; + idnode_changed(&rl->rl_id); + } +} + +/** + * Delete a rating label object from memory and disk. + * Cleanup EPG entries that use that RL object. + */ +void +ratinglabel_delete(ratinglabel_t *rl) +{ + char ubuf[UUID_HEX_SIZE]; + if (rl == NULL) return; + rl->rl_enabled = 0; + + channel_t *ch; + epg_broadcast_t *ebc; + dvr_entry_t *de; + int foundCount = 0; + epg_changes_t *changes = NULL; + int retVal = 0; + + if (!rl->rl_shield) { + hts_settings_remove("epggrab/ratinglabel/%s", idnode_uuid_as_str(&rl->rl_id, ubuf)); + + tvhtrace(LS_RATINGLABELS, "Deleting rating label '%s' / '%d'.", rl->rl_display_label, rl->rl_display_age); + + //Read through all of the EPG entries and set the rating label pointer to NULL for matching labels. + //If this is not done, if an EPG entry is called that has the deleted RL, then the pointer + //will point to rubbish and TVH will most likely crash. + //Note: In order to delete the RL object, the mutex is already locked. + + CHANNEL_FOREACH(ch) { + if (ch->ch_epg_parent) continue; + RB_FOREACH(ebc, &ch->ch_epg_schedule, sched_link) { + if(ebc->rating_label == rl) + { + //Cause the entry to be flagged as changed + ebc->rating_label = NULL; //Clear the pointer to the RL object + retVal = epg_broadcast_set_age_rating(ebc, 99, changes); + retVal = epg_broadcast_set_age_rating(ebc, 0, changes); + ebc->age_rating = 0; //Clear the age rating field. + foundCount++; + }//END matching RL + }//END loop through EPG entries + }//END loop through channels + + retVal = 0; + + if (foundCount != 0 && (retVal == 0)){ + epg_updated(); + } + + tvhtrace(LS_RATINGLABELS, "Found '%d' EPG entries when deleting rating label.", foundCount); + + //Now check for any upcomming recordings that use this RL and remove their RL UUID. + foundCount = 0; + + LIST_FOREACH(de, &dvrentries, de_global_link){ + if (dvr_entry_is_upcoming(de)){ + if(de->de_rating_label == rl){ + tvhtrace(LS_RATINGLABELS, "Removing rating label for scheduled recording '%s'.", de->de_title->first->str); + de->de_rating_label = NULL; //Set the rating label UUID for this recording to null + dvr_entry_changed(de); //Save this recording. + foundCount++; + } + } + } + + tvhtrace(LS_RATINGLABELS, "Found '%d' upcomming recordings when deleting rating label.", foundCount); + + ratinglabel_destroy(rl); + } else { + idnode_changed(&rl->rl_id); + } +} + +/* ************************************************************************** + * Class definition + * **************************************************************************/ + +static htsmsg_t * +ratinglabel_class_save(idnode_t *self, char *filename, size_t fsize) +{ + ratinglabel_t *rl = (ratinglabel_t *)self; + dvr_entry_t *de; + channel_t *ch; + epg_broadcast_t *ebc; + int foundCount = 0; + epg_changes_t *changes = NULL; + int retVal = 0; + + tvhtrace(LS_RATINGLABELS, "Saving rating label '%s' / '%d'.", rl->rl_display_label, rl->rl_display_age); + + htsmsg_t *c = htsmsg_create_map(); + char ubuf[UUID_HEX_SIZE]; + idnode_save(&rl->rl_id, c); + if (filename) + snprintf(filename, fsize, "epggrab/ratinglabel/%s", idnode_uuid_as_str(&rl->rl_id, ubuf)); + if (rl->rl_shield) + htsmsg_add_bool(c, "shield", 1); + rl->rl_saveflag = 0; + + //Check for EPG entries that use this RL and update the age field. + CHANNEL_FOREACH(ch) { + if (ch->ch_epg_parent) continue; + RB_FOREACH(ebc, &ch->ch_epg_schedule, sched_link) { + if(ebc->rating_label == rl) + { + //This could either be a change to the display_label or the display_age + //or perhaps event the icon, regardless, whatever the change, + //for the EPG update to be pushed out to any attached client + //if required we need to have made a change. + retVal = epg_broadcast_set_age_rating(ebc, 99, changes); + retVal = epg_broadcast_set_age_rating(ebc, rl->rl_display_age, changes); + foundCount++; + }//END match the RL + }//END loop through EPG entries for that channel + }//END loop through channels + + //This is a workaround so that I can evaluate retVal and not have the compiler warning kill my buzz. + //If the RL has changed, the EPG entry will be updated, I just need a variable to hold the return + //code from epg_broadcast_set_age_rating even though I'm not going to use it for anything. + retVal = 0; + + if (foundCount != 0 && (retVal == 0)){ + epg_updated(); + } + + tvhtrace(LS_RATINGLABELS, "Found '%d' EPG entries when updating rating label.", foundCount); + + foundCount = 0; + + //Check for upcomming recordings that need their saved RL details to be updated. + LIST_FOREACH(de, &dvrentries, de_global_link){ + if (dvr_entry_is_upcoming(de)){ + if(de->de_rating_label == rl){ + tvhtrace(LS_RATINGLABELS, "Updating rating label for scheduled recording '%s', from '%s' / '%d' to '%s' / '%d'.", de->de_title->first->str, de->de_rating_label_saved, de->de_age_rating, rl->rl_display_label, rl->rl_display_age); + + //Update the recording's RL details. + de->de_rating_label_saved = strdup(rl->rl_display_label); + de->de_age_rating = rl->rl_display_age; + + //If this RL has an icon, save that, else, ensure that the recording's RL icon is empty. + if(rl->rl_icon){ + de->de_rating_icon_saved = strdup(rl->rl_icon); + } + else { + de->de_rating_icon_saved = NULL; + } + + dvr_entry_changed(de); //Save this recording and push it out to clients. + foundCount++; + }//END we matched the RL + }//END we got an upcomminf + }//END loop through recordings + + tvhtrace(LS_RATINGLABELS, "Found '%d' upcomming recordings when updating rating label.", foundCount); + + return c; +} + +static void +ratinglabel_class_delete(idnode_t *self) +{ + ratinglabel_delete((ratinglabel_t *)self); +} + +//For compatability, return the 'display label' if the 'title' is requested +//because RLs don't have a title. +static void +ratinglabel_class_get_title + (idnode_t *self, const char *lang, char *dst, size_t dstsize) +{ + ratinglabel_t *rl = (ratinglabel_t *)self; + snprintf(dst, dstsize, "%s", rl->rl_display_label); +} + +/* exported for others */ +htsmsg_t * +ratinglabel_class_get_list(void *o, const char *lang) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_str(m, "type", "api"); + htsmsg_add_str(m, "uri", "ratinglabel/list"); + htsmsg_add_str(m, "event", "ratinglabel"); + return m; +} + +//Get the icon from the rating label object +const char * +ratinglabel_get_icon ( ratinglabel_t *rl ) +{ + if(rl){ + return rl->rl_icon; + } + return NULL; +} + +//This is defined as a property getter and is delivered +//via the JSON API. +const void * +ratinglabel_class_get_icon ( void *obj ) +{ + prop_ptr = ratinglabel_get_icon(obj); + if (!strempty(prop_ptr)){ + prop_ptr = imagecache_get_propstr(prop_ptr, prop_sbuf, PROP_SBUF_LEN); + } + return &prop_ptr; +} + +static void +ratinglabel_class_enabled_notify ( void *obj, const char *lang ) +{ + int a=1; + ratinglabel_t *rl = obj; + + if (rl->rl_enabled) + a++; +} + +CLASS_DOC(ratinglabel) + +const idclass_t ratinglabel_class = { + .ic_class = "ratinglabel", + .ic_caption = N_("EPG Parental Rating Labels"), + .ic_doc = tvh_doc_ratinglabel_class, + .ic_event = "ratinglabel", + .ic_perm_def = ACCESS_ADMIN, + .ic_save = ratinglabel_class_save, + .ic_get_title = ratinglabel_class_get_title, + .ic_delete = ratinglabel_class_delete, + .ic_properties = (const property_t[]){ + { + .type = PT_BOOL, + .id = "enabled", + .name = N_("Enabled"), + .desc = N_("Enable/disable the rating label."), + .def.i = 1, + .off = offsetof(ratinglabel_t, rl_enabled), + .notify = ratinglabel_class_enabled_notify, + }, + { + .type = PT_STR, + .id = "country", + .name = N_("Country"), + .desc = N_("Country recieved via OTA EPG."), + .off = offsetof(ratinglabel_t, rl_country), + }, + { + .type = PT_INT, + .id = "age", + .name = N_("Age"), + .desc = N_("Unprocessed rating 'age' received via DVB OTA EPG."), + .off = offsetof(ratinglabel_t, rl_age), + //.doc = prop_doc_ratinglabel_mapping_options, + }, + { + .type = PT_INT, + .id = "display_age", + .name = N_("Display Age"), + .desc = N_("Age to use in the EPG parental rating field."), + .off = offsetof(ratinglabel_t, rl_display_age), + //.doc = prop_doc_ratinglabel_mapping_options, + }, + { + .type = PT_STR, + .id = "display_label", + .name = N_("Display Label"), + .desc = N_("Rating label to be displayed."), + .off = offsetof(ratinglabel_t, rl_display_label), + }, + { + .type = PT_STR, + .id = "label", + .name = N_("Label"), + .desc = N_("XML 'rating' tag value to match events received via XMLTV."), + .off = offsetof(ratinglabel_t, rl_label), + }, + { + .type = PT_STR, + .id = "authority", + .name = N_("Authority"), + .desc = N_("XMLTV 'system' attribute to match events received via XMLTV."), + .off = offsetof(ratinglabel_t, rl_authority), + }, + { + .type = PT_STR, + .id = "icon", + .name = N_("Icon"), + .desc = N_("File name for this rating's icon."), + .off = offsetof(ratinglabel_t, rl_icon), + }, + { + .type = PT_STR, + .id = "icon_public_url", + .name = N_("Icon URL"), + .desc = N_("The imagecache path to the icon to use/used " + "for the rating label."), + .get = ratinglabel_class_get_icon, + .opts = PO_RDONLY | PO_NOSAVE | PO_HIDDEN, + }, + {} + } +}; + +/** + * + */ +void +ratinglabel_init(void) +{ + tvhtrace(LS_RATINGLABELS, "Initialising Rating Labels"); + htsmsg_t *c, *m; + htsmsg_field_t *f; + ratinglabel_t *rl; + + RB_INIT(&ratinglabels); + idclass_register(&ratinglabel_class); + + /* Load */ + if ((c = hts_settings_load("epggrab/ratinglabel")) != NULL) { + HTSMSG_FOREACH(f, c) { + if (!(m = htsmsg_field_get_map(f))) continue; + rl = ratinglabel_create(htsmsg_field_name(f), m, NULL, NULL); + if (rl) + { + tvhtrace(LS_RATINGLABELS, "Loaded label: '%s' / '%d', enabled: '%d', label count: '%d'", rl->rl_display_label, rl->rl_display_age, rl->rl_enabled, ratinglabels.entries); + rl->rl_saveflag = 0; + } + } + htsmsg_destroy(c); + } + +} + +//Delete all of the ratinglabel objects. +//This appears to be called from main.c. +void +ratinglabel_done(void) +{ + ratinglabel_t *rl; + + tvh_mutex_lock(&global_lock); + while ((rl = RB_FIRST(&ratinglabels)) != NULL) + ratinglabel_destroy(rl); + tvh_mutex_unlock(&global_lock); +} diff --git a/src/ratinglabels.h b/src/ratinglabels.h new file mode 100644 index 000000000..50415ed73 --- /dev/null +++ b/src/ratinglabels.h @@ -0,0 +1,102 @@ +/* + * TV headend - Rating Labels + * Copyright (C) 2014 Jaroslav Kysela (Original Bouquets) + * Copyright (C) 2023 DeltaMikeCharlie (Updated for Rating Labels) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef RATINGLABEL_H_ +#define RATINGLABEL_H_ + +#include "idnode.h" +#include "htsmsg.h" + +typedef struct ratinglabel { + idnode_t rl_id; + RB_ENTRY(ratinglabel) rl_link; + + int rl_saveflag; //} These were kept because + int rl_in_load; //} they were in the module that I copied + int rl_shield; //} and I was not sure if I could delete them + + int rl_enabled; //Can this label be matched to? + char *rl_country; //The 3 byte country code from the EIT + int rl_age; //The age value from the EIT + int rl_display_age; //The age value to actually use + char *rl_display_label; //The label to actually use + char *rl_label; //The rating label from XMLTV + char *rl_authority; //The 'system' from XMLTV + char *rl_icon; //The pretty picture + +} ratinglabel_t; +/* +*** EIT Documentation + +parental_rating_descriptor(){ + descriptor_tag 8 uimsbf + descriptor_length 8 uimsbf + for (i=0;i + TV-G + +*/ + +typedef RB_HEAD(,ratinglabel) ratinglabel_tree_t; + +extern ratinglabel_tree_t ratinglabels; + +extern const idclass_t ratinglabel_class; + +htsmsg_t * ratinglabel_class_get_list(void *o, const char *lang); +const void *ratinglabel_class_get_icon (void *obj); + +ratinglabel_t * ratinglabel_create(const char *uuid, htsmsg_t *conf, + const char *name, const char *src); + +void ratinglabel_delete(ratinglabel_t *rl); + +void ratinglabel_completed(ratinglabel_t *rl, uint32_t seen); +void ratinglabel_change_comment(ratinglabel_t *rl, const char *comment, int replace); + +extern void ratinglabel_init(void); +extern void ratinglabel_done(void); + +extern ratinglabel_t *ratinglabel_find_from_eit(char *country, int age); +extern ratinglabel_t *ratinglabel_find_from_xmltv(const char *authority, const char *label); +extern ratinglabel_t *ratinglabel_find_from_uuid(const char *string_uuid); + +#endif /* RATINGLABEL_H_ */ diff --git a/src/tvhlog.c b/src/tvhlog.c index 82f565e2d..d92a959c8 100644 --- a/src/tvhlog.c +++ b/src/tvhlog.c @@ -184,7 +184,8 @@ tvhlog_subsys_t tvhlog_subsystems[] = { #if ENABLE_DDCI [LS_DDCI] = { "ddci", N_("DD-CI") }, #endif - [LS_UDP] = { "udp", N_("UDP Streamer") }, + [LS_UDP] = { "udp", N_("UDP Streamer") }, + [LS_RATINGLABELS] = { "ratinglabels", N_("Rating Labels") }, }; diff --git a/src/tvhlog.h b/src/tvhlog.h index 3b7a76fb1..c69cd8fc9 100644 --- a/src/tvhlog.h +++ b/src/tvhlog.h @@ -198,7 +198,8 @@ enum { #if ENABLE_DDCI LS_DDCI, #endif - LS_UDP, + LS_UDP, + LS_RATINGLABELS, LS_LAST /* keep this last */ }; diff --git a/src/webui/static/app/dvr.js b/src/webui/static/app/dvr.js index 212f464d9..5d540f564 100644 --- a/src/webui/static/app/dvr.js +++ b/src/webui/static/app/dvr.js @@ -73,7 +73,10 @@ tvheadend.dvrDetails = function(grid, index) { var genre = params[21].value; /* channelname is unused param 22 */ var fanart_image = params[23].value; + /* broadcast is unused param 24 */ var age_rating = params[25].value; + var rating_label = params[26].value; + var rating_icon = params[27].value; var content = '
' + '
' + '
'; @@ -138,18 +141,24 @@ tvheadend.dvrDetails = function(grid, index) { content += tvheadend.sortAndAddArray(keyword, _('Keywords')); if (category) content += tvheadend.sortAndAddArray(category, _('Categories')); + + if (rating_icon) + content += ''; + if (age_rating) content += '
' + _('Age Rating') + ':' + age_rating + '
'; + if (rating_label) + content += '
' + _('Parental Rating') + ':' + rating_label + '
'; if (status) - content += '
' + _('Status') + ':' + status + '
'; + content += '
' + _('Status') + ':' + status + '
'; if (filesize) - content += '
' + _('File size') + ':' + parseInt(filesize / 1000000) + ' MB
'; + content += '
' + _('File size') + ':' + parseInt(filesize / 1000000) + ' MB
'; if (comment) - content += '
' + _('Comment') + ':' + comment + '
'; + content += '
' + _('Comment') + ':' + comment + '
'; if (autorec_caption) - content += '
' + _('Autorec') + ':' + autorec_caption + '
'; + content += '
' + _('Autorec') + ':' + autorec_caption + '
'; if (timerec_caption) - content += '
' + _('Time Scheduler') + ':' + timerec_caption + '
'; + content += '
' + _('Time Scheduler') + ':' + timerec_caption + '
'; if (chicon) content += '
'; /* x-epg-bottom */ content += '
'; //dialog content @@ -308,7 +317,7 @@ tvheadend.dvrDetails = function(grid, index) { list: 'channel_icon,disp_title,disp_subtitle,disp_summary,episode_disp,start_real,stop_real,' + 'duration,disp_description,status,filesize,comment,duplicate,' + 'autorec_caption,timerec_caption,image,copyright_year,credits,keyword,category,' + - 'first_aired,genre,channelname,fanart_image,broadcast,age_rating', + 'first_aired,genre,channelname,fanart_image,broadcast,age_rating,rating_label,rating_icon', }, success: function(d) { d = json_decode(d); @@ -615,7 +624,7 @@ tvheadend.dvr_upcoming = function(panel, index) { del: true, list: 'category,enabled,duplicate,disp_title,disp_extratext,episode_disp,' + 'channel,image,copyright_year,start_real,stop_real,duration,pri,filesize,' + - 'sched_status,errors,data_errors,config_name,owner,creator,comment,genre,broadcast,age_rating', + 'sched_status,errors,data_errors,config_name,owner,creator,comment,genre,broadcast,age_rating,rating_label', columns: { disp_title: { renderer: tvheadend.displayWithYearAndDuplicateRenderer(), @@ -805,7 +814,7 @@ tvheadend.dvr_finished = function(panel, index) { del: false, list: 'disp_title,disp_extratext,episode_disp,channel,channelname,' + 'start_real,stop_real,duration,filesize,copyright_year,' + - 'sched_status,errors,data_errors,playcount,url,config_name,owner,creator,comment,age_rating', + 'sched_status,errors,data_errors,playcount,url,config_name,owner,creator,comment,age_rating,rating_label', columns: { disp_title: { renderer: tvheadend.displayWithYearRenderer(), @@ -925,7 +934,7 @@ tvheadend.dvr_failed = function(panel, index) { _('The associated file will be removed from storage.'), list: 'disp_title,disp_extratext,episode_disp,channel,channelname,' + 'image,copyright_year,start_real,stop_real,duration,filesize,status,' + - 'sched_status,errors,data_errors,playcount,url,config_name,owner,creator,comment,age_rating', + 'sched_status,errors,data_errors,playcount,url,config_name,owner,creator,comment,age_rating,rating_label', columns: { disp_title: { renderer: tvheadend.displayWithYearRenderer(), @@ -1004,7 +1013,7 @@ tvheadend.dvr_removed = function(panel, index) { del: true, list: 'disp_title,disp_extratext,episode_disp,channel,channelname,image,' + 'copyright_year,start_real,stop_real,duration,status,' + - 'sched_status,errors,data_errors,url,config_name,owner,creator,comment,age_rating', + 'sched_status,errors,data_errors,url,config_name,owner,creator,comment,age_rating,rating_label', columns: { disp_title: { renderer: tvheadend.displayWithYearRenderer(), diff --git a/src/webui/static/app/epg.js b/src/webui/static/app/epg.js index 04da8bd56..636e5a8e3 100644 --- a/src/webui/static/app/epg.js +++ b/src/webui/static/app/epg.js @@ -267,8 +267,15 @@ tvheadend.epgDetails = function(grid, index) { content += tvheadend.sortAndAddArray(event.category, _('Categories')); if (event.starRating) content += '
' + _('Star Rating') + ':' + event.starRating + '
'; + + if (event.ratingLabelIcon) + content += ''; + if (event.ageRating) content += '
' + _('Age Rating') + ':' + event.ageRating + '
'; + if (event.ratingLabel) + content += '
' + _('Parental Rating') + ':' + event.ratingLabel + '
'; + if (event.genre) { var genre = []; Ext.each(event.genre, function(g) { @@ -651,6 +658,8 @@ tvheadend.epg = function() { { name: 'category' }, { name: 'keyword' }, { name: 'ageRating' }, + { name: 'ratingLabel' }, + { name: 'ratingLabelIcon' }, { name: 'copyright_year' }, { name: 'new' }, { name: 'genre' }, @@ -849,6 +858,14 @@ tvheadend.epg = function() { dataIndex: 'starRating', renderer: renderInt }, + { + width: 50, + id: 'ratingLabel', + header: _("Rating"), + tooltip: _("Parental Rating"), + dataIndex: 'ratingLabel', + renderer: renderInt + }, { width: 50, id: 'ageRating', @@ -892,6 +909,7 @@ tvheadend.epg = function() { { type: 'string', dataIndex: 'episodeOnscreen' }, { type: 'intsplit', dataIndex: 'channelNumber', intsplit: 1000000 }, { type: 'string', dataIndex: 'channelName' }, + { type: 'string', dataIndex: 'ratingLabel' }, { type: 'numeric', dataIndex: 'starRating' }, { type: 'numeric', dataIndex: 'ageRating' } ] diff --git a/src/webui/static/app/ext.css b/src/webui/static/app/ext.css index f165d3c66..eb6d3adae 100644 --- a/src/webui/static/app/ext.css +++ b/src/webui/static/app/ext.css @@ -789,6 +789,13 @@ margin: 5px; width: 20%; max-height: 99px; + max-width: 99px; +} + +.x-epg-rlicon { + float: right; + margin: 5px; + max-height: 50px; } .x-epg-time { @@ -828,7 +835,7 @@ } .x-epg-genre { - margin-left: 5px; + margin-left: 10px; } .x-epg-duplicate { diff --git a/src/webui/static/app/ratinglabels.js b/src/webui/static/app/ratinglabels.js new file mode 100644 index 000000000..64413df4f --- /dev/null +++ b/src/webui/static/app/ratinglabels.js @@ -0,0 +1,35 @@ +tvheadend.ratinglabel = function(panel, index) { + + tvheadend.idnode_grid(panel, { + url: 'api/ratinglabel', + all: 1, + titleS: _('Rating Label'), + titleP: _('Rating Labels'), + iconCls: 'baseconf', + tabIndex: index, + columns: { + enabled: { width: 70 }, + country: { width: 80 }, + age: { width: 50 }, + display_age: { width: 100 }, + display_label: { width: 100 }, + label: { width: 100 }, + authority: { width: 100 }, + icon: { width: 200 } + }, + add: { + url: 'api/ratinglabel', + create: { } + }, + del: true, + uilevel: 'expert', + del: true, + sort: { + field: 'country', + direction: 'ASC' + } + }); + + return panel; + +} diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js index 79269f8c3..192d15fd0 100644 --- a/src/webui/static/app/tvheadend.js +++ b/src/webui/static/app/tvheadend.js @@ -1131,6 +1131,7 @@ function accessUpdate(o) { tvheadend.epggrab_map(chepg); tvheadend.epggrab_base(chepg); tvheadend.epggrab_mod(chepg); + tvheadend.ratinglabel(chepg); cp.add(chepg); diff --git a/src/webui/static/img/doc/channel/epgconf_tab.png b/src/webui/static/img/doc/channel/epgconf_tab.png old mode 100755 new mode 100644 index be4db94b4..9ffa00840 Binary files a/src/webui/static/img/doc/channel/epgconf_tab.png and b/src/webui/static/img/doc/channel/epgconf_tab.png differ diff --git a/src/webui/static/img/doc/ratinglabel/epg_placeholder.png b/src/webui/static/img/doc/ratinglabel/epg_placeholder.png new file mode 100644 index 000000000..27c46bab7 Binary files /dev/null and b/src/webui/static/img/doc/ratinglabel/epg_placeholder.png differ diff --git a/src/webui/static/img/doc/ratinglabel/rating_labels_complete.png b/src/webui/static/img/doc/ratinglabel/rating_labels_complete.png new file mode 100644 index 000000000..a82d0c3d5 Binary files /dev/null and b/src/webui/static/img/doc/ratinglabel/rating_labels_complete.png differ diff --git a/src/webui/static/img/doc/ratinglabel/rating_labels_learned.png b/src/webui/static/img/doc/ratinglabel/rating_labels_learned.png new file mode 100644 index 000000000..5a32d7882 Binary files /dev/null and b/src/webui/static/img/doc/ratinglabel/rating_labels_learned.png differ diff --git a/src/webui/static/img/doc/ratinglabel/updated_label.png b/src/webui/static/img/doc/ratinglabel/updated_label.png new file mode 100644 index 000000000..517ba50b4 Binary files /dev/null and b/src/webui/static/img/doc/ratinglabel/updated_label.png differ diff --git a/src/webui/static/img/doc/ratinglabel/xmltv_learned.png b/src/webui/static/img/doc/ratinglabel/xmltv_learned.png new file mode 100644 index 000000000..10333db15 Binary files /dev/null and b/src/webui/static/img/doc/ratinglabel/xmltv_learned.png differ