/* * API - EPG related calls * * Copyright (C) 2013 Adam Sutton * * 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; withm 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 "channels.h" #include "access.h" #include "api.h" #include "epg.h" #include "imagecache.h" #include "dvr/dvr.h" static htsmsg_t * api_epg_get_list ( const char *s ) { htsmsg_t *m = NULL; char *r, *saveptr = NULL; if (s && s[0] != '\0') { s = r = strdup(s); r = strtok_r(r, ";", &saveptr); while (r) { while (*r != '\0' && *r <= ' ') r++; if (*r != '\0') { if (m == NULL) m = htsmsg_create_list(); htsmsg_add_str(m, NULL, r); } r = strtok_r(NULL, ";", &saveptr); } free((char *)s); } return m; } static void api_epg_add_channel ( htsmsg_t *m, channel_t *ch ) { int64_t chnum; char buf[32]; const char *s; htsmsg_add_str(m, "channelName", channel_get_name(ch)); htsmsg_add_str(m, "channelUuid", channel_get_uuid(ch)); if ((chnum = channel_get_number(ch)) >= 0) { uint32_t maj = chnum / CHANNEL_SPLIT; uint32_t min = chnum % CHANNEL_SPLIT; if (min) snprintf(buf, sizeof(buf), "%u.%u", maj, min); else snprintf(buf, sizeof(buf), "%u", maj); htsmsg_add_str(m, "channelNumber", buf); } if ((s = channel_get_icon(ch)) != NULL) htsmsg_add_str(m, "channelIcon", s); } static htsmsg_t * api_epg_entry ( epg_broadcast_t *eb, const char *lang ) { const char *s; char buf[64]; epg_episode_t *ee = eb->episode; channel_t *ch = eb->channel; htsmsg_t *m, *m2; epg_episode_num_t epnum; epg_genre_t *eg; dvr_entry_t *de; if (!ee || !ch) return NULL; m = htsmsg_create_map(); /* EPG IDs */ htsmsg_add_u32(m, "eventId", eb->id); if (ee) { htsmsg_add_u32(m, "episodeId", ee->id); if (ee->uri && strncasecmp(ee->uri, "tvh://", 6)) htsmsg_add_str(m, "episodeUri", ee->uri); } if (eb->serieslink) { htsmsg_add_u32(m, "serieslinkId", eb->serieslink->id); if (eb->serieslink->uri) htsmsg_add_str(m, "serieslinkUri", eb->serieslink->uri); } /* Channel Info */ api_epg_add_channel(m, ch); /* Time */ htsmsg_add_s64(m, "start", eb->start); htsmsg_add_s64(m, "stop", eb->stop); /* Title/description */ if ((s = epg_broadcast_get_title(eb, lang))) htsmsg_add_str(m, "title", s); if ((s = epg_broadcast_get_subtitle(eb, lang))) htsmsg_add_str(m, "subtitle", s); if ((s = epg_broadcast_get_summary(eb, lang))) htsmsg_add_str(m, "summary", s); if ((s = epg_broadcast_get_description(eb, lang))) htsmsg_add_str(m, "description", s); /* Episode info */ if (ee) { /* Number */ epg_episode_get_epnum(ee, &epnum); if (epnum.s_num) { htsmsg_add_u32(m, "seasonNumber", epnum.s_num); if (epnum.s_cnt) htsmsg_add_u32(m, "seasonCount", epnum.s_cnt); } if (epnum.e_num) { htsmsg_add_u32(m, "episodeNumber", epnum.e_num); if (epnum.s_cnt) htsmsg_add_u32(m, "episodeCount", epnum.e_cnt); } if (epnum.p_num) { htsmsg_add_u32(m, "partNumber", epnum.p_num); if (epnum.p_cnt) htsmsg_add_u32(m, "partCount", epnum.p_cnt); } if (epnum.text) htsmsg_add_str(m, "episodeOnscreen", epnum.text); else if (epg_episode_number_format(ee, buf, sizeof(buf), NULL, "s%02d", ".", "e%02d", "")) htsmsg_add_str(m, "episodeOnscreen", buf); /* Image */ if (ee->image) htsmsg_add_str(m, "image", ee->image); /* Rating */ if (ee->star_rating) htsmsg_add_u32(m, "starRating", ee->star_rating); if (ee->age_rating) htsmsg_add_u32(m, "ageRating", ee->age_rating); /* Content Type */ m2 = NULL; LIST_FOREACH(eg, &ee->genre, link) { if (m2 == NULL) m2 = htsmsg_create_list(); htsmsg_add_u32(m2, NULL, eg->code); } if (m2) htsmsg_add_msg(m, "genre", m2); } /* Recording */ if ((de = dvr_entry_find_by_event(eb))) { htsmsg_add_str(m, "dvrUuid", idnode_uuid_as_str(&de->de_id)); htsmsg_add_str(m, "dvrState", dvr_entry_schedstatus(de)); } /* Next event */ if ((eb = epg_broadcast_get_next(eb))) htsmsg_add_u32(m, "nextEventId", eb->id); return m; } static void api_epg_filter_set_str ( epg_filter_str_t *f, const char *str, int comp ) { f->str = strdup(str); f->comp = comp; } static void api_epg_filter_add_str ( epg_query_t *eq, const char *k, const char *v, int comp ) { if (!strcmp(k, "channelName")) api_epg_filter_set_str(&eq->channel_name, v, comp); else if (!strcmp(k, "title")) api_epg_filter_set_str(&eq->title, v, comp); else if (!strcmp(k, "subtitle")) api_epg_filter_set_str(&eq->subtitle, v, comp); else if (!strcmp(k, "summary")) api_epg_filter_set_str(&eq->summary, v, comp); else if (!strcmp(k, "description")) api_epg_filter_set_str(&eq->description, v, comp); } static void api_epg_filter_set_num ( epg_filter_num_t *f, int64_t v1, int64_t v2, int comp ) { /* Range? */ if (f->comp == EC_LT && comp == EC_GT) { f->val2 = f->val1; f->val1 = v1; f->comp = EC_RG; return; } if (f->comp == EC_GT && comp == EC_LT) { f->val2 = v1; f->comp = EC_RG; return; } f->val1 = v1; f->val2 = v2; f->comp = comp; } static void api_epg_filter_add_num ( epg_query_t *eq, const char *k, int64_t v1, int64_t v2, int comp ) { if (!strcmp(k, "start")) api_epg_filter_set_num(&eq->start, v1, v2, comp); else if (!strcmp(k, "stop")) api_epg_filter_set_num(&eq->stop, v1, v2, comp); else if (!strcmp(k, "duration")) api_epg_filter_set_num(&eq->duration, v1, v2, comp); else if (!strcmp(k, "episode")) api_epg_filter_set_num(&eq->episode, v1, v2, comp); else if (!strcmp(k, "stars")) api_epg_filter_set_num(&eq->episode, v1, v2, comp); else if (!strcmp(k, "age")) api_epg_filter_set_num(&eq->age, v1, v2, comp); } static struct strtab sortcmptab[] = { { "start", ESK_START }, { "stop", ESK_STOP }, { "duration", ESK_DURATION }, { "title", ESK_TITLE }, { "subtitle", ESK_SUBTITLE }, { "summary", ESK_SUMMARY }, { "description", ESK_DESCRIPTION }, { "channelName", ESK_CHANNEL }, { "channelNumber", ESK_CHANNEL_NUM }, { "starRating", ESK_STARS }, { "ageRating", ESK_AGE }, { "genre", ESK_GENRE } }; static struct strtab filtcmptab[] = { { "gt", EC_GT }, { "lt", EC_LT }, { "eq", EC_EQ }, { "regex", EC_RE }, { "range", EC_RG } }; static int64_t api_epg_decode_channel_num ( const char *s ) { int64_t v = atol(s); const char *s1 = strchr(s, '.'); if (s1) v += atol(s1 + 1) % CHANNEL_SPLIT; return v; } static int api_epg_grid ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int i; epg_query_t eq; const char *lang, *str; uint32_t start, limit, end, genre; int64_t duration_min, duration_max; htsmsg_field_t *f, *f2; htsmsg_t *l = NULL, *e, *filter; memset(&eq, 0, sizeof(eq)); lang = htsmsg_get_str(args, "lang"); eq.lang = lang ? strdup(lang) : NULL; str = htsmsg_get_str(args, "title"); if (str) eq.stitle = strdup(str); str = htsmsg_get_str(args, "channel"); if (str) eq.channel = strdup(str); str = htsmsg_get_str(args, "channelTag"); if (str) eq.channel_tag = strdup(str); duration_min = -1; duration_max = -1; htsmsg_get_s64(args, "durationMin", &duration_min); htsmsg_get_s64(args, "durationMax", &duration_max); if (duration_min > 0 || duration_max > 0) { eq.duration.comp = EC_RG; eq.duration.val1 = duration_min < 0 ? 0 : duration_min; eq.duration.val2 = duration_max < 0 ? 0 : duration_max; } if (!htsmsg_get_u32(args, "contentType", &genre)) { eq.genre = eq.genre_static; eq.genre[0] = genre; eq.genre_count = 1; } /* Filter params */ if ((filter = htsmsg_get_list(args, "filter"))) { HTSMSG_FOREACH(f, filter) { const char *k, *t, *v; int comp; if (!(e = htsmsg_get_map_by_field(f))) continue; if (!(k = htsmsg_get_str(e, "field"))) continue; if (!(t = htsmsg_get_str(e, "type"))) continue; comp = str2val(htsmsg_get_str(e, "comparison") ?: "", filtcmptab); if (comp == -1) comp = EC_EQ; if (!strcmp(k, "channelNumber")) { if (!strcmp(t, "numeric")) { f2 = htsmsg_field_find(e, "value"); if (f2) { int64_t v1, v2 = 0; if (f2->hmf_type == HMF_STR) { const char *s = htsmsg_field_get_str(f2); const char *z = s ? strchr(s, ';') : NULL; if (s) { v1 = api_epg_decode_channel_num(s); if (z) v2 = api_epg_decode_channel_num(z); api_epg_filter_set_num(&eq.channel_num, v1, v2, comp); } } else { if (!htsmsg_field_get_s64(f2, &v1)) { if (v1 < CHANNEL_SPLIT) v1 *= CHANNEL_SPLIT; api_epg_filter_set_num(&eq.channel_num, v1, 0, comp); } } } } } else if (!strcmp(k, "genre")) { if (!strcmp(t, "numeric")) { f2 = htsmsg_field_find(e, "value"); if (f2) { int64_t v; if (f2->hmf_type == HMF_STR) { htsmsg_t *z = api_epg_get_list(htsmsg_field_get_str(f2)); if (z) { htsmsg_field_t *f3; uint32_t count = 0; HTSMSG_FOREACH(f3, z) count++; if (ARRAY_SIZE(eq.genre_static) > count) eq.genre = malloc(sizeof(eq.genre[0]) * count); else eq.genre = eq.genre_static; HTSMSG_FOREACH(f3, z) if (!htsmsg_field_get_s64(f3, &v)) eq.genre[eq.genre_count++] = v; } } else { if (!htsmsg_field_get_s64(f2, &v)) { eq.genre_count = 1; eq.genre = eq.genre_static; eq.genre[0] = v; } } } } } else if (!strcmp(t, "string")) { if ((v = htsmsg_get_str(e, "value"))) api_epg_filter_add_str(&eq, k, v, EC_RE); } else if (!strcmp(t, "numeric")) { f2 = htsmsg_field_find(e, "value"); if (f2) { int64_t v1, v2 = 0; if (f2->hmf_type == HMF_STR) { const char *z = htsmsg_field_get_str(f2); if (z) { const char *z2 = strchr(z, ';'); if (z2) v2 = strtoll(z2 + 1, NULL, 0); } v1 = strtoll(z, NULL, 0); api_epg_filter_add_num(&eq, k, v1, v2, comp); } else { if (!htsmsg_field_get_s64(f2, &v1)) api_epg_filter_add_num(&eq, k, v1, v2, comp); } } } } } /* Sort */ if ((str = htsmsg_get_str(args, "sort"))) { int skey = str2val(str, sortcmptab); if (skey >= 0) { eq.sort_key = skey; if ((str = htsmsg_get_str(args, "dir")) && !strcasecmp(str, "DESC")) eq.sort_dir = IS_DSC; else eq.sort_dir = IS_ASC; } } /* else.. keep default start time ascending sorting */ /* Pagination settings */ start = htsmsg_get_u32_or_default(args, "start", 0); limit = htsmsg_get_u32_or_default(args, "limit", 50); /* Query the EPG */ pthread_mutex_lock(&global_lock); epg_query(&eq); /* Build response */ start = MIN(eq.entries, start); end = MIN(eq.entries, start + limit); l = htsmsg_create_list(); for (i = start; i < end; i++) { if (!(e = api_epg_entry(eq.result[i], lang))) continue; htsmsg_add_msg(l, NULL, e); } pthread_mutex_unlock(&global_lock); epg_query_free(&eq); /* Build response */ *resp = htsmsg_create_map(); htsmsg_add_u32(*resp, "totalCount", eq.entries); htsmsg_add_msg(*resp, "entries", l); return 0; } static void api_epg_episode_broadcasts ( htsmsg_t *l, const char *lang, epg_episode_t *ep, uint32_t *entries, epg_broadcast_t *ebc_skip ) { epg_broadcast_t *ebc; channel_t *ch; htsmsg_t *m; LIST_FOREACH(ebc, &ep->broadcasts, ep_link) { ch = ebc->channel; if (ch == NULL) continue; if (ebc == ebc_skip) continue; m = api_epg_entry(ebc, lang); htsmsg_add_msg(l, NULL, m); (*entries)++; } } static int api_epg_alternative ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { uint32_t id, entries = 0; htsmsg_t *l = htsmsg_create_list(); epg_broadcast_t *e; const char *lang = htsmsg_get_str(args, "lang"); if (!htsmsg_get_u32(args, "eventId", &id)) return -EINVAL; /* Main Job */ pthread_mutex_lock(&global_lock); e = epg_broadcast_find_by_id(id, NULL); if (e && e->episode) api_epg_episode_broadcasts(l, lang, e->episode, &entries, e); pthread_mutex_unlock(&global_lock); /* Build response */ htsmsg_add_u32(*resp, "totalCount", entries); htsmsg_add_msg(*resp, "entries", l); return 0; } static int api_epg_related ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { uint32_t id, entries = 0; htsmsg_t *l = htsmsg_create_list(); epg_broadcast_t *e; epg_episode_t *ep, *ep2; const char *lang = htsmsg_get_str(args, "lang"); if (!htsmsg_get_u32(args, "eventId", &id)) return -EINVAL; /* Main Job */ pthread_mutex_lock(&global_lock); e = epg_broadcast_find_by_id(id, NULL); ep = e ? e->episode : NULL; if (ep && ep->brand) { LIST_FOREACH(ep2, &ep->brand->episodes, blink) { if (ep2 == ep) continue; if (!ep2->title) continue; api_epg_episode_broadcasts(l, lang, ep2, &entries, e); entries++; } } else if (ep && ep->season) { LIST_FOREACH(ep2, &ep->season->episodes, slink) { if (ep2 == ep) continue; if (!ep2->title) continue; api_epg_episode_broadcasts(l, lang, ep2, &entries, e); } } pthread_mutex_unlock(&global_lock); /* Build response */ htsmsg_add_u32(*resp, "totalCount", entries); htsmsg_add_msg(*resp, "entries", l); return 0; } static int api_epg_brand_list(access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp) { htsmsg_t *array; *resp = htsmsg_create_map(); pthread_mutex_lock(&global_lock); array = epg_brand_list(); pthread_mutex_unlock(&global_lock); htsmsg_add_msg(*resp, "entries", array); return 0; } static int api_epg_content_type_list(access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp) { htsmsg_t *array; int full = 0; htsmsg_get_bool(args, "full", &full); *resp = htsmsg_create_map(); array = epg_genres_list_all(full ? 0 : 1, 0); htsmsg_add_msg(*resp, "entries", array); return 0; } void api_epg_init ( void ) { static api_hook_t ah[] = { { "epg/events/grid", ACCESS_ANONYMOUS, api_epg_grid, NULL }, { "epg/events/alternative", ACCESS_ANONYMOUS, api_epg_alternative, NULL }, { "epg/events/related", ACCESS_ANONYMOUS, api_epg_related, NULL }, { "epg/brand/list", ACCESS_ANONYMOUS, api_epg_brand_list, NULL }, { "epg/content_type/list", ACCESS_ANONYMOUS, api_epg_content_type_list, NULL }, { NULL }, }; api_register_all(ah); }