From: Jaroslav Kysela Date: Thu, 21 Aug 2014 19:36:49 +0000 (+0200) Subject: dvr: rewrite to use the idnode system X-Git-Tag: v4.1~1471 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=220833201fdd56dc25bfbdedbf7633ceb19b63b6;p=thirdparty%2Ftvheadend.git dvr: rewrite to use the idnode system --- diff --git a/Makefile b/Makefile index 70ad0d359..632fdf00f 100644 --- a/Makefile +++ b/Makefile @@ -156,7 +156,8 @@ SRCS += \ src/api/api_imagecache.c \ src/api/api_esfilter.c \ src/api/api_intlconv.c \ - src/api/api_access.c + src/api/api_access.c \ + src/api/api_dvr.c SRCS += \ src/parsers/parsers.c \ diff --git a/src/access.c b/src/access.c index 5876b5349..3a175b113 100644 --- a/src/access.c +++ b/src/access.c @@ -37,6 +37,7 @@ #include "access.h" #include "settings.h" #include "channels.h" +#include "tcp.h" struct access_entry_queue access_entries; struct access_ticket_queue access_tickets; @@ -158,6 +159,8 @@ access_ticket_verify(const char *id, const char *resource) void access_destroy(access_t *a) { + free(a->aa_username); + free(a->aa_representative); htsmsg_destroy(a->aa_chtags); free(a); } @@ -310,6 +313,14 @@ access_get(const char *username, const char *password, struct sockaddr *src) access_t *a = calloc(1, sizeof(*a)); access_entry_t *ae; + if (username) { + a->aa_username = strdup(username); + a->aa_representative = strdup(username); + } else { + a->aa_representative = malloc(50); + tcp_get_ip_str((struct sockaddr*)src, a->aa_representative, 50); + } + if (access_noacl) { a->aa_rights = ACCESS_FULL; return a; @@ -645,7 +656,7 @@ access_entry_destroy(access_entry_t *ae) /** * */ -static void +void access_entry_save(access_entry_t *ae) { htsmsg_t *c = htsmsg_create_map(); diff --git a/src/access.h b/src/access.h index 1cc74a2a8..9315c50ee 100644 --- a/src/access.h +++ b/src/access.h @@ -82,6 +82,8 @@ typedef struct access_ticket { } access_ticket_t; typedef struct access { + char *aa_username; + char *aa_representative; uint32_t aa_rights; uint32_t aa_chmin; uint32_t aa_chmax; @@ -129,6 +131,9 @@ void access_destroy(access_t *a); int access_verify(const char *username, const char *password, struct sockaddr *src, uint32_t mask); +static inline int access_verify2(access_t *a, uint32_t mask) + { return (a->aa_rights & mask) == mask ? 0 : -1; } + /** * Get the access structure */ @@ -154,6 +159,12 @@ access_get_by_addr(struct sockaddr *src); access_entry_t * access_entry_create(const char *uuid, htsmsg_t *conf); +/** + * + */ +void +access_entry_save(access_entry_t *ae); + /** * */ diff --git a/src/api.c b/src/api.c index 3aa932d92..382955739 100644 --- a/src/api.c +++ b/src/api.c @@ -61,7 +61,8 @@ api_register_all ( const api_hook_t *hooks ) } int -api_exec ( const char *subsystem, htsmsg_t *args, htsmsg_t **resp ) +api_exec ( access_t *perm, const char *subsystem, + htsmsg_t *args, htsmsg_t **resp ) { api_hook_t h; api_link_t *ah, skel; @@ -90,12 +91,12 @@ api_exec ( const char *subsystem, htsmsg_t *args, htsmsg_t **resp ) // Note: this is not required (so no final validation) /* Execute */ - return ah->hook->ah_callback(ah->hook->ah_opaque, op, args, resp); + return ah->hook->ah_callback(perm, ah->hook->ah_opaque, op, args, resp); } static int api_serverinfo - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { *resp = htsmsg_create_map(); htsmsg_add_str(*resp, "sw_version", tvheadend_version); @@ -128,6 +129,7 @@ void api_init ( void ) api_esfilter_init(); api_intlconv_init(); api_access_init(); + api_dvr_init(); } void api_done ( void ) diff --git a/src/api.h b/src/api.h index e6caef597..0655865b9 100644 --- a/src/api.h +++ b/src/api.h @@ -23,15 +23,17 @@ #include "htsmsg.h" #include "idnode.h" #include "redblack.h" +#include "access.h" -#define TVH_API_VERSION 12 +#define TVH_API_VERSION 14 /* * Command hook */ typedef int (*api_callback_t) - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ); + ( access_t *perm, void *opaque, const char *op, + htsmsg_t *args, htsmsg_t **resp ); typedef struct api_hook { @@ -50,7 +52,8 @@ void api_register_all ( const api_hook_t *hooks ); /* * Execute */ -int api_exec ( const char *subsystem, htsmsg_t *args, htsmsg_t **resp ); +int api_exec ( access_t *perm, const char *subsystem, + htsmsg_t *args, htsmsg_t **resp ); /* * Initialise @@ -70,6 +73,7 @@ void api_imagecache_init ( void ); void api_esfilter_init ( void ); void api_intlconv_init ( void ); void api_access_init ( void ); +void api_dvr_init ( void ); /* * IDnode @@ -84,21 +88,24 @@ typedef struct api_idnode_grid_conf } api_idnode_grid_conf_t; typedef void (*api_idnode_grid_callback_t) - (idnode_set_t*, api_idnode_grid_conf_t*, htsmsg_t *args); + (access_t *perm, idnode_set_t*, api_idnode_grid_conf_t*, htsmsg_t *args); typedef idnode_set_t *(*api_idnode_tree_callback_t) - (void); + (access_t *perm); int api_idnode_grid - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ); + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ); int api_idnode_class - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ); + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ); int api_idnode_tree - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ); + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ); int api_idnode_load_by_class - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ); + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ); + +int api_idnode_handler + ( access_t *perm, htsmsg_t *args, htsmsg_t **resp, void (*handler)(access_t *perm, idnode_t *in) ); /* * Service mapper diff --git a/src/api/api_access.c b/src/api/api_access.c index f9ad77084..15d87214a 100644 --- a/src/api/api_access.c +++ b/src/api/api_access.c @@ -23,7 +23,7 @@ static void api_access_entry_grid - ( idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) { access_entry_t *ae; @@ -33,15 +33,17 @@ api_access_entry_grid static int api_access_entry_create - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { htsmsg_t *conf; + access_entry_t *ae; if (!(conf = htsmsg_get_map(args, "conf"))) return EINVAL; pthread_mutex_lock(&global_lock); - access_entry_create(NULL, conf); + if ((ae = access_entry_create(NULL, conf)) != NULL) + access_entry_save(ae); pthread_mutex_unlock(&global_lock); return 0; diff --git a/src/api/api_channel.c b/src/api/api_channel.c index 99f73c9fe..aaf87b451 100644 --- a/src/api/api_channel.c +++ b/src/api/api_channel.c @@ -28,7 +28,7 @@ // TODO: this will need converting to an idnode system static int api_channel_list - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { channel_t *ch; htsmsg_t *l, *e; @@ -50,7 +50,7 @@ api_channel_list static void api_channel_grid - ( idnode_set_t *ins, api_idnode_grid_conf_t *conf ) + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf ) { channel_t *ch; @@ -60,7 +60,7 @@ api_channel_grid static int api_channel_create - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { htsmsg_t *conf; channel_t *ch; @@ -79,7 +79,7 @@ api_channel_create static int api_channel_tag_list - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { channel_tag_t *ct; htsmsg_t *l, *e; @@ -98,7 +98,7 @@ api_channel_tag_list static void api_channel_tag_grid - ( idnode_set_t *ins, api_idnode_grid_conf_t *conf ) + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf ) { channel_tag_t *ct; @@ -108,7 +108,7 @@ api_channel_tag_grid static int api_channel_tag_create - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { htsmsg_t *conf; channel_tag_t *ct; diff --git a/src/api/api_dvr.c b/src/api/api_dvr.c new file mode 100644 index 000000000..f342e4195 --- /dev/null +++ b/src/api/api_dvr.c @@ -0,0 +1,346 @@ +/* + * API - DVR + * + * Copyright (C) 2014 Jaroslav Kysela + * + * 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 "dvr/dvr.h" +#include "epg.h" +#include "api.h" + +static const char * +api_dvr_config_name( access_t *perm, const char *config_uuid ) +{ + dvr_config_t *cfg; + + lock_assert(&global_lock); + + if (access_verify2(perm, ACCESS_RECORDER_ALL)) + return config_uuid; /* no change */ + + cfg = dvr_config_find_by_name(perm->aa_username); + if (cfg) + return cfg->dvr_config_name; + + if (perm->aa_username) + tvhlog(LOG_INFO, "dvr", "User '%s' has no dvr config with identical name, using default...", perm->aa_username); + + return NULL; /* default */ +} + +/* + * + */ + +static void +api_dvr_config_grid + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) +{ + dvr_config_t *cfg; + + LIST_FOREACH(cfg, &dvrconfigs, config_link) + if (!idnode_perm((idnode_t *)cfg, perm, NULL)) + idnode_set_add(ins, (idnode_t*)cfg, &conf->filter); +} + +static int +api_dvr_config_create + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + dvr_config_t *cfg; + htsmsg_t *conf; + const char *s; + + if (!(conf = htsmsg_get_map(args, "conf"))) + return EINVAL; + if (!(s = htsmsg_get_str(conf, "name"))) + return EINVAL; + if (s[0] == '\0') + return EINVAL; + if (access_verify2(perm, ACCESS_RECORDER_ALL | ACCESS_RECORDER)) + return EACCES; + + pthread_mutex_lock(&global_lock); + if ((cfg = dvr_config_create(NULL, NULL, conf))) + dvr_config_save(cfg); + pthread_mutex_unlock(&global_lock); + + return 0; +} + +static int is_dvr_entry_finished(dvr_entry_t *entry) +{ + dvr_entry_sched_state_t state = entry->de_sched_state; + return state == DVR_COMPLETED && !entry->de_last_error && dvr_get_filesize(entry) != -1; +} + +static int is_dvr_entry_upcoming(dvr_entry_t *entry) +{ + dvr_entry_sched_state_t state = entry->de_sched_state; + return state == DVR_RECORDING || state == DVR_SCHEDULED; +} + +static int is_dvr_entry_failed(dvr_entry_t *entry) +{ + if (is_dvr_entry_finished(entry)) + return 0; + if (is_dvr_entry_upcoming(entry)) + return 0; + return 1; +} + +static void +api_dvr_entry_grid + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) +{ + dvr_entry_t *de; + + LIST_FOREACH(de, &dvrentries, de_global_link) + idnode_set_add(ins, (idnode_t*)de, &conf->filter); +} + +static void +api_dvr_entry_grid_upcoming + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) +{ + dvr_entry_t *de; + + LIST_FOREACH(de, &dvrentries, de_global_link) + if (is_dvr_entry_upcoming(de)) + idnode_set_add(ins, (idnode_t*)de, &conf->filter); +} + +static void +api_dvr_entry_grid_finished + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) +{ + dvr_entry_t *de; + + LIST_FOREACH(de, &dvrentries, de_global_link) + if (is_dvr_entry_finished(de)) + idnode_set_add(ins, (idnode_t*)de, &conf->filter); +} + +static void +api_dvr_entry_grid_failed + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) +{ + dvr_entry_t *de; + + LIST_FOREACH(de, &dvrentries, de_global_link) + if (is_dvr_entry_failed(de)) + idnode_set_add(ins, (idnode_t*)de, &conf->filter); +} + +static int +api_dvr_entry_create + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + dvr_entry_t *de; + htsmsg_t *conf; + + if (!(conf = htsmsg_get_map(args, "conf"))) + return EINVAL; + + if (access_verify2(perm, ACCESS_RECORDER_ALL)) { + htsmsg_delete_field(conf, "config_name"); + htsmsg_add_str(conf, "config_name", perm->aa_username ?: ""); + } + + pthread_mutex_lock(&global_lock); + if ((de = dvr_entry_create(NULL, conf))) + dvr_entry_save(de); + pthread_mutex_unlock(&global_lock); + + return 0; +} + +static htsmsg_t * +api_dvr_entry_create_from_single(htsmsg_t *args) +{ + htsmsg_t *entries, *m; + const char *s1, *s2; + + if (!(s1 = htsmsg_get_str(args, "config_uuid"))) + return NULL; + if (!(s2 = htsmsg_get_str(args, "event_id"))) + return NULL; + entries = htsmsg_create_list(); + m = htsmsg_create_map(); + htsmsg_add_str(m, "config_uuid", s1); + htsmsg_add_str(m, "event_id", s2); + htsmsg_add_msg(entries, NULL, m); + return entries; +} + +static int +api_dvr_entry_create_by_event + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + dvr_entry_t *de; + const char *config_uuid; + epg_broadcast_t *e; + htsmsg_t *entries, *entries2 = NULL, *m; + htsmsg_field_t *f; + const char *s; + int count = 0; + + if (!(entries = htsmsg_get_list(args, "entries"))) { + entries = entries2 = api_dvr_entry_create_from_single(args); + if (!entries) + return EINVAL; + } + + HTSMSG_FOREACH(f, entries) { + if (!(m = htsmsg_get_map_by_field(f))) continue; + + if (!(config_uuid = htsmsg_get_str(m, "config_uuid"))) + continue; + if (!(s = htsmsg_get_str(m, "event_id"))) + continue; + + pthread_mutex_lock(&global_lock); + if ((e = epg_broadcast_find_by_id(atoi(s), NULL))) { + de = dvr_entry_create_by_event(api_dvr_config_name(perm, config_uuid), + e, 0, 0, perm->aa_representative, + NULL, DVR_PRIO_NORMAL); + if (de) + dvr_entry_save(de); + } + pthread_mutex_unlock(&global_lock); + count++; + } + + htsmsg_destroy(entries2); + + return !count ? EINVAL : 0; +} + +static void +api_dvr_cancel(access_t *perm, idnode_t *self) +{ + dvr_entry_cancel((dvr_entry_t *)self); +} + +static int +api_dvr_entry_cancel + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + return api_idnode_handler(perm, args, resp, api_dvr_cancel); +} + +static void +api_dvr_autorec_grid + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) +{ + dvr_autorec_entry_t *dae; + + TAILQ_FOREACH(dae, &autorec_entries, dae_link) + idnode_set_add(ins, (idnode_t*)dae, &conf->filter); +} + +static int +api_dvr_autorec_create + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + htsmsg_t *conf; + dvr_autorec_entry_t *dae; + + if (!(conf = htsmsg_get_map(args, "conf"))) + return EINVAL; + + htsmsg_delete_field(conf, "creator"); + if (perm->aa_representative) + htsmsg_add_str(conf, "creator", perm->aa_representative); + + pthread_mutex_lock(&global_lock); + dae = dvr_autorec_create(NULL, conf); + if (dae) + dvr_autorec_save(dae); + pthread_mutex_unlock(&global_lock); + + return 0; +} + +static int +api_dvr_autorec_create_by_series + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + dvr_autorec_entry_t *dae; + epg_broadcast_t *e; + htsmsg_t *entries, *entries2 = NULL, *m; + htsmsg_field_t *f; + const char *config_uuid, *s; + int count = 0; + + if (!(entries = htsmsg_get_list(args, "entries"))) { + entries = entries2 = api_dvr_entry_create_from_single(args); + if (!entries) + return EINVAL; + } + + HTSMSG_FOREACH(f, entries) { + if (!(m = htsmsg_get_map_by_field(f))) continue; + + if (!(config_uuid = htsmsg_get_str(m, "config_uuid"))) + continue; + if (!(s = htsmsg_get_str(m, "event_id"))) + continue; + + pthread_mutex_lock(&global_lock); + if ((e = epg_broadcast_find_by_id(atoi(s), NULL))) { + dae = dvr_autorec_add_series_link(api_dvr_config_name(perm, config_uuid), + e, perm->aa_representative, + "Created from EPG query"); + if (dae) + dvr_autorec_save(dae); + } + pthread_mutex_unlock(&global_lock); + count++; + } + + htsmsg_destroy(entries2); + + return !count ? EINVAL : 0; +} + +void api_dvr_init ( void ) +{ + static api_hook_t ah[] = { + { "dvr/config/class", ACCESS_RECORDER, api_idnode_class, (void*)&dvr_config_class }, + { "dvr/config/grid", ACCESS_RECORDER, api_idnode_grid, api_dvr_config_grid }, + { "dvr/config/create", ACCESS_ADMIN, api_dvr_config_create, NULL }, + + { "dvr/entry/class", ACCESS_RECORDER, api_idnode_class, (void*)&dvr_entry_class }, + { "dvr/entry/grid", ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid }, + { "dvr/entry/grid_upcoming", ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid_upcoming }, + { "dvr/entry/grid_finished", ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid_finished }, + { "dvr/entry/grid_failed", ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid_failed }, + { "dvr/entry/create", ACCESS_RECORDER, api_dvr_entry_create, NULL }, + { "dvr/entry/create_by_event", ACCESS_RECORDER, api_dvr_entry_create_by_event, NULL }, + { "dvr/entry/cancel", ACCESS_RECORDER, api_dvr_entry_cancel, NULL }, + + { "dvr/autorec/class", ACCESS_RECORDER, api_idnode_class, (void*)&dvr_autorec_entry_class }, + { "dvr/autorec/grid", ACCESS_RECORDER, api_idnode_grid, api_dvr_autorec_grid }, + { "dvr/autorec/create", ACCESS_RECORDER, api_dvr_autorec_create, NULL }, + { "dvr/autorec/create_by_series", ACCESS_RECORDER, api_dvr_autorec_create_by_series, NULL }, + + { NULL }, + }; + + api_register_all(ah); +} diff --git a/src/api/api_epg.c b/src/api/api_epg.c index 9bd44903a..d1ac6ef63 100644 --- a/src/api/api_epg.c +++ b/src/api/api_epg.c @@ -112,7 +112,7 @@ api_epg_entry ( epg_broadcast_t *eb, const char *lang ) /* Recording */ if ((de = dvr_entry_find_by_event(eb))) - htsmsg_add_u32(m, "dvrId", de->de_id); + htsmsg_add_str(m, "dvrId", idnode_uuid_as_str(&de->de_id)); /* Next event */ if ((eb = epg_broadcast_get_next(eb))) @@ -123,7 +123,7 @@ api_epg_entry ( epg_broadcast_t *eb, const char *lang ) static int api_epg_grid - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int i; epg_query_result_t eqr; @@ -176,10 +176,25 @@ api_epg_grid 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; + + *resp = htsmsg_create_map(); + array = epg_genres_list_all(1, 0); + htsmsg_add_msg(*resp, "entries", array); + return 0; +} + + void api_epg_init ( void ) { static api_hook_t ah[] = { - { "epg/grid", ACCESS_ANONYMOUS, api_epg_grid, NULL }, + { "epg/data/grid", ACCESS_ANONYMOUS, api_epg_grid, NULL }, + { "epg/content_type/list", ACCESS_ANONYMOUS, api_epg_content_type_list, NULL }, + { NULL }, }; diff --git a/src/api/api_epggrab.c b/src/api/api_epggrab.c index f0d38cfa2..e984ff573 100644 --- a/src/api/api_epggrab.c +++ b/src/api/api_epggrab.c @@ -24,7 +24,7 @@ static int api_epggrab_channel_list - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { htsmsg_t *m; pthread_mutex_lock(&global_lock); diff --git a/src/api/api_esfilter.c b/src/api/api_esfilter.c index 2769f8c3c..bc8c761b4 100644 --- a/src/api/api_esfilter.c +++ b/src/api/api_esfilter.c @@ -25,7 +25,7 @@ static void api_esfilter_grid - ( idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args, + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args, esfilter_class_t cls ) { esfilter_t *esf; @@ -37,7 +37,7 @@ api_esfilter_grid static int api_esfilter_create - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp, + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp, esfilter_class_t cls ) { htsmsg_t *conf; @@ -54,11 +54,11 @@ api_esfilter_create #define ESFILTER(func, t) \ static void api_esfilter_grid_##func \ - ( idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) \ -{ return api_esfilter_grid(ins, conf, args, (t)); } \ + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) \ +{ return api_esfilter_grid(perm, ins, conf, args, (t)); } \ static int api_esfilter_create_##func \ - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) \ -{ return api_esfilter_create(opaque, op, args, resp, (t)); } + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) \ +{ return api_esfilter_create(perm, opaque, op, args, resp, (t)); } ESFILTER(video, ESF_CLASS_VIDEO); ESFILTER(audio, ESF_CLASS_AUDIO); diff --git a/src/api/api_idnode.c b/src/api/api_idnode.c index 689a51b5c..b7d56d510 100644 --- a/src/api/api_idnode.c +++ b/src/api/api_idnode.c @@ -23,6 +23,30 @@ #include "htsmsg.h" #include "api.h" +static htsmsg_t * +api_idnode_flist_conf( htsmsg_t *args, const char *name ) +{ + htsmsg_t *m = NULL; + const char *s = htsmsg_get_str(args, name); + char *r, *saveptr; + 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_map(); + htsmsg_add_bool(m, r, 1); + } + r = strtok_r(NULL, ",;:", &saveptr); + } + free((char *)s); + } + return m; +} + static struct strtab filtcmptab[] = { { "gt", IC_GT }, { "lt", IC_LT }, @@ -87,10 +111,11 @@ api_idnode_grid_conf int api_idnode_grid - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int i; htsmsg_t *list, *e; + htsmsg_t *flist = api_idnode_flist_conf(args, "list"); api_idnode_grid_conf_t conf = { 0 }; idnode_set_t ins = { 0 }; api_idnode_grid_callback_t cb = opaque; @@ -100,7 +125,7 @@ api_idnode_grid /* Create list */ pthread_mutex_lock(&global_lock); - cb(&ins, &conf, args); + cb(perm, &ins, &conf, args); /* Sort */ if (conf.sort.key) @@ -111,7 +136,7 @@ api_idnode_grid for (i = conf.start; i < ins.is_count && conf.limit != 0; i++) { e = htsmsg_create_map(); htsmsg_add_str(e, "uuid", idnode_uuid_as_str(ins.is_array[i])); - idnode_read0(ins.is_array[i], e, 0); + idnode_read0(ins.is_array[i], e, flist, 0); htsmsg_add_msg(list, NULL, e); if (conf.limit > 0) conf.limit--; } @@ -126,13 +151,14 @@ api_idnode_grid /* Cleanup */ free(ins.is_array); idnode_filter_clear(&conf.filter); + htsmsg_destroy(flist); return 0; } int api_idnode_load_by_class - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int i, _enum; const idclass_t *idc; @@ -154,6 +180,9 @@ api_idnode_load_by_class for (i = 0; i < is->is_count; i++) { in = is->is_array[i]; + if (idnode_perm(in, perm, NULL)) + continue; + /* Name/UUID only */ if (_enum) { e = htsmsg_create_map(); @@ -161,8 +190,11 @@ api_idnode_load_by_class htsmsg_add_str(e, "val", idnode_get_title(in)); /* Full record */ - } else - e = idnode_serialize(in); + } else { + htsmsg_t *flist = api_idnode_flist_conf(args, "list"); + e = idnode_serialize0(in, flist, 0); + htsmsg_destroy(flist); + } if (e) htsmsg_add_msg(l, NULL, e); @@ -180,11 +212,12 @@ api_idnode_load_by_class static int api_idnode_load - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { - int err = 0; + int err = 0, meta = 0, count = 0; idnode_t *in; - htsmsg_t *uuids, *l = NULL; + htsmsg_t *uuids, *l = NULL, *m; + htsmsg_t *flist; htsmsg_field_t *f; const char *uuid, *class; @@ -197,7 +230,7 @@ api_idnode_load if (!idc) return EINVAL; // TODO: bit naff that 2 locks are required here - return api_idnode_load_by_class((void*)idc, NULL, args, resp); + return api_idnode_load_by_class(perm, (void*)idc, NULL, args, resp); } /* UUIDs */ @@ -206,6 +239,9 @@ api_idnode_load if (!(uuids = htsmsg_field_get_list(f))) if (!(uuid = htsmsg_field_get_str(f))) return EINVAL; + htsmsg_get_s32(args, "meta", &meta); + + flist = api_idnode_flist_conf(args, "list"); pthread_mutex_lock(&global_lock); @@ -215,16 +251,34 @@ api_idnode_load HTSMSG_FOREACH(f, uuids) { if (!(uuid = htsmsg_field_get_str(f))) continue; if (!(in = idnode_find(uuid, NULL))) continue; - htsmsg_add_msg(l, NULL, idnode_serialize(in)); + if (idnode_perm(in, perm, NULL)) { + err = EPERM; + continue; + } + m = idnode_serialize0(in, flist, 0); + if (meta > 0) + htsmsg_add_msg(m, "meta", idclass_serialize0(in->in_class, flist, 0)); + htsmsg_add_msg(l, NULL, m); + count++; } + if (count) + err = 0; + /* Single */ } else { if (!(in = idnode_find(uuid, NULL))) err = ENOENT; else { - l = htsmsg_create_list(); - htsmsg_add_msg(l, NULL, idnode_serialize(in)); + if (idnode_perm(in, perm, NULL)) { + err = EPERM; + } else { + l = htsmsg_create_list(); + m = idnode_serialize0(in, flist, 0); + if (meta > 0) + htsmsg_add_msg(m, "meta", idclass_serialize0(in->in_class, flist, 0)); + htsmsg_add_msg(l, NULL, m); + } } } @@ -235,18 +289,21 @@ api_idnode_load pthread_mutex_unlock(&global_lock); + htsmsg_destroy(flist); + return err; } static int api_idnode_save - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int err = EINVAL; idnode_t *in; htsmsg_t *msg, *conf; htsmsg_field_t *f; const char *uuid; + int count = 0; if (!(f = htsmsg_field_find(args, "node"))) return EINVAL; @@ -262,6 +319,10 @@ api_idnode_save goto exit; if (!(in = idnode_find(uuid, NULL))) goto exit; + if (idnode_perm(in, perm, msg)) { + err = EPERM; + goto exit; + } idnode_update(in, msg); err = 0; @@ -274,9 +335,15 @@ api_idnode_save continue; if (!(in = idnode_find(uuid, NULL))) continue; + if (idnode_perm(in, perm, conf)) { + err = EPERM; + continue; + } + count++; idnode_update(in, conf); } - err = 0; + if (count) + err = 0; } // TODO: return updated UUIDs? @@ -289,7 +356,7 @@ exit: int api_idnode_tree - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { const char *uuid; const char *root = NULL; @@ -329,7 +396,7 @@ api_idnode_tree /* Children */ } else { - idnode_set_t *v = node ? idnode_get_childs(node) : rootfn(); + idnode_set_t *v = node ? idnode_get_childs(node) : rootfn(perm); if (v) { int i; idnode_set_sort_by_title(v); @@ -348,11 +415,12 @@ api_idnode_tree int api_idnode_class - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int err = EINVAL; const char *name; const idclass_t *idc; + htsmsg_t *flist = api_idnode_flist_conf(args, "list"); pthread_mutex_lock(&global_lock); @@ -368,17 +436,20 @@ api_idnode_class } err = 0; - *resp = idclass_serialize(idc); + *resp = idclass_serialize0(idc, flist, 0); exit: pthread_mutex_unlock(&global_lock); + htsmsg_destroy(flist); + return err; } -static int +int api_idnode_handler - ( htsmsg_t *args, htsmsg_t **resp, void (*handler)(idnode_t *in) ) + ( access_t *perm, htsmsg_t *args, htsmsg_t **resp, + void (*handler)(access_t *perm, idnode_t *in) ) { int err = 0; idnode_t *in; @@ -400,7 +471,7 @@ api_idnode_handler HTSMSG_FOREACH(f, uuids) { if (!(uuid = htsmsg_field_get_string(f))) continue; if (!(in = idnode_find(uuid, NULL))) continue; - handler(in); + handler(perm, in); } /* Single */ @@ -409,7 +480,7 @@ api_idnode_handler if (!(in = idnode_find(uuid, NULL))) err = ENOENT; else - handler(in); + handler(perm, in); } pthread_mutex_unlock(&global_lock); @@ -417,25 +488,43 @@ api_idnode_handler return err; } +static void +api_idnode_delete_ (access_t *perm, idnode_t *in) +{ + return idnode_delete(in); +} + static int api_idnode_delete - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + return api_idnode_handler(perm, args, resp, api_idnode_delete_); +} + +static void +api_idnode_moveup_ (access_t *perm, idnode_t *in) { - return api_idnode_handler(args, resp, idnode_delete); + return idnode_moveup(in); } static int api_idnode_moveup - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + return api_idnode_handler(perm, args, resp, api_idnode_moveup_); +} + +static void +api_idnode_movedown_ (access_t *perm, idnode_t *in) { - return api_idnode_handler(args, resp, idnode_moveup); + return idnode_movedown(in); } static int api_idnode_movedown - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { - return api_idnode_handler(args, resp, idnode_movedown); + return api_idnode_handler(perm, args, resp, api_idnode_movedown_); } void api_idnode_init ( void ) diff --git a/src/api/api_imagecache.c b/src/api/api_imagecache.c index c82aef8be..e60512df8 100644 --- a/src/api/api_imagecache.c +++ b/src/api/api_imagecache.c @@ -27,7 +27,7 @@ static int api_imagecache_load - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { htsmsg_t *l; pthread_mutex_lock(&global_lock); @@ -41,7 +41,7 @@ api_imagecache_load static int api_imagecache_save - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { pthread_mutex_lock(&global_lock); if (imagecache_set_config(args)) diff --git a/src/api/api_intlconv.c b/src/api/api_intlconv.c index dfe0c4266..632b96ed6 100644 --- a/src/api/api_intlconv.c +++ b/src/api/api_intlconv.c @@ -27,28 +27,22 @@ static int api_intlconv_charset_enum - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { const char **chrst; htsmsg_t *l, *e; - int _enum = htsmsg_get_bool_or_default(args, "enum", 0); - - if (_enum) { - l = htsmsg_create_list(); - chrst = intlconv_charsets; - while (*chrst) { - e = htsmsg_create_map(); - htsmsg_add_str(e, "key", *chrst); - htsmsg_add_str(e, "val", *chrst); - htsmsg_add_msg(l, NULL, e); - chrst++; - } - *resp = htsmsg_create_map(); - htsmsg_add_msg(*resp, "entries", l); - } else { - // TODO: support full listing v enum + l = htsmsg_create_list(); + chrst = intlconv_charsets; + while (*chrst) { + e = htsmsg_create_map(); + htsmsg_add_str(e, "key", *chrst); + htsmsg_add_str(e, "val", *chrst); + htsmsg_add_msg(l, NULL, e); + chrst++; } + *resp = htsmsg_create_map(); + htsmsg_add_msg(*resp, "entries", l); return 0; } diff --git a/src/api/api_mpegts.c b/src/api/api_mpegts.c index 3e6ffcee6..618ae6832 100644 --- a/src/api/api_mpegts.c +++ b/src/api/api_mpegts.c @@ -31,7 +31,7 @@ */ static int api_mpegts_input_network_list - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int i, err = EINVAL; const char *uuid; @@ -77,7 +77,7 @@ exit: */ static void api_mpegts_network_grid - ( idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) { mpegts_network_t *mn; @@ -88,7 +88,7 @@ api_mpegts_network_grid static int api_mpegts_network_builders - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { mpegts_network_builder_t *mnb; htsmsg_t *l, *e; @@ -108,7 +108,7 @@ api_mpegts_network_builders static int api_mpegts_network_create - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int err; const char *class; @@ -136,7 +136,7 @@ api_mpegts_network_create static int api_mpegts_network_muxclass - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int err = EINVAL; const idclass_t *idc; @@ -164,7 +164,7 @@ exit: static int api_mpegts_network_muxcreate - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int err = EINVAL; mpegts_network_t *mn; @@ -198,7 +198,7 @@ exit: */ static void api_mpegts_mux_grid - ( idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) { mpegts_network_t *mn; mpegts_mux_t *mm; @@ -225,7 +225,7 @@ api_mpegts_mux_grid */ static void api_mpegts_service_grid - ( idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) { mpegts_network_t *mn; mpegts_mux_t *mm; @@ -256,7 +256,7 @@ api_mpegts_service_grid */ static void api_mpegts_mux_sched_grid - ( idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) { mpegts_mux_sched_t *mms; LIST_FOREACH(mms, &mpegts_mux_sched_all, mms_link) @@ -265,7 +265,7 @@ api_mpegts_mux_sched_grid static int api_mpegts_mux_sched_create - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int err; htsmsg_t *conf; @@ -291,7 +291,7 @@ api_mpegts_mux_sched_create #if ENABLE_MPEGTS_DVB static int api_dvb_scanfile_list - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { char buf[512]; const char *type = htsmsg_get_str(args, "type"); diff --git a/src/api/api_service.c b/src/api/api_service.c index b0d3ec64c..3cc822eaa 100644 --- a/src/api/api_service.c +++ b/src/api/api_service.c @@ -29,7 +29,7 @@ static int api_mapper_start - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { service_mapper_conf_t conf = { 0 }; htsmsg_t *uuids; @@ -52,7 +52,7 @@ api_mapper_start static int api_mapper_stop - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { pthread_mutex_lock(&global_lock); service_mapper_stop(); @@ -78,7 +78,7 @@ api_mapper_status_msg ( void ) static int api_mapper_status - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { pthread_mutex_lock(&global_lock); *resp = api_mapper_status_msg(); @@ -129,7 +129,7 @@ api_service_streams_get_one ( elementary_stream_t *es, int use_filter ) static int api_service_streams - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { const char *uuid; htsmsg_t *e, *st, *stf; diff --git a/src/api/api_status.c b/src/api/api_status.c index 3104850bc..6d19e8434 100644 --- a/src/api/api_status.c +++ b/src/api/api_status.c @@ -29,7 +29,7 @@ static int api_status_inputs - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int c = 0; htsmsg_t *l, *e; @@ -59,7 +59,7 @@ api_status_inputs static int api_status_subscriptions - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { int c; htsmsg_t *l, *e; @@ -82,7 +82,7 @@ api_status_subscriptions static int api_status_connections - ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { pthread_mutex_lock(&global_lock); *resp = tcp_server_connections(); diff --git a/src/config.c b/src/config.c index 5eee7b6ab..1337caf0d 100644 --- a/src/config.c +++ b/src/config.c @@ -620,7 +620,7 @@ config_migrate_v6 ( void ) * v6 -> v7 : acesscontrol changes */ static void -config_migrate_simple ( const char *dir, htsmsg_t **orig, +config_migrate_simple ( const char *dir, htsmsg_t *list, void (*modify)(htsmsg_t *record, uint32_t id, const char *uuid, @@ -632,25 +632,31 @@ config_migrate_simple ( const char *dir, htsmsg_t **orig, tvh_uuid_t u; uint32_t index = 1, id; - if (!(c = hts_settings_load_r(1, dir))) + if (!(c = hts_settings_load(dir))) return; HTSMSG_FOREACH(f, c) { if (!(e = htsmsg_field_get_map(f))) continue; + uuid_init_hex(&u, NULL); if (htsmsg_get_u32(e, "id", &id)) id = 0; + else if (list) { + htsmsg_t *m = htsmsg_create_map(); + char buf[16]; + snprintf(buf, sizeof(buf), "%d", id); + htsmsg_add_str(m, "id", buf); + htsmsg_add_str(m, "uuid", u.hex); + htsmsg_add_msg(list, NULL, m); + } htsmsg_delete_field(e, "id"); htsmsg_add_u32(e, "index", index++); - uuid_init_hex(&u, NULL); - modify(e, id, u.hex, aux); + if (modify) + modify(e, id, u.hex, aux); hts_settings_save(e, "%s/%s", dir, u.hex); hts_settings_remove("%s/%s", dir, f->hmf_name); } - if (orig) - *orig = c; - else - htsmsg_destroy(c); + htsmsg_destroy(c); } static void @@ -731,6 +737,96 @@ config_migrate_v8 ( void ) htsmsg_destroy(ch); } +static void +config_modify_autorec( htsmsg_t *c, uint32_t id, const char *uuid, void *aux ) +{ + uint32_t u32; + htsmsg_delete_field(c, "index"); + if (!htsmsg_get_u32(c, "approx_time", &u32)) { + if (u32 == 0) + u32 = -1; + htsmsg_delete_field(c, "approx_time"); + htsmsg_add_u32(c, "start", u32); + } + if (!htsmsg_get_u32(c, "contenttype", &u32)) { + htsmsg_delete_field(c, "contenttype"); + htsmsg_add_u32(c, "content_type", u32 / 16); + } +} + +static void +config_modify_dvr_log( htsmsg_t *c, uint32_t id, const char *uuid, void *aux ) +{ + htsmsg_t *list = aux; + const char *chname = htsmsg_get_str(c, "channelname"); + const char *chuuid = htsmsg_get_str(c, "channel"); + htsmsg_t *e; + htsmsg_field_t *f; + tvh_uuid_t uuid0; + const char *s1; + uint32_t u32; + + htsmsg_delete_field(c, "index"); + if (chname == NULL || (chuuid != NULL && uuid_init_bin(&uuid0, chuuid))) { + chname = strdup(chuuid); + htsmsg_delete_field(c, "channelname"); + htsmsg_delete_field(c, "channel"); + htsmsg_add_str(c, "channelname", chname); + free((char *)chname); + if (!htsmsg_get_u32(c, "contenttype", &u32)) { + htsmsg_delete_field(c, "contenttype"); + htsmsg_add_u32(c, "content_type", u32 / 16); + } + } + if ((s1 = htsmsg_get_str(c, "autorec")) != NULL) { + s1 = strdup(s1); + htsmsg_delete_field(c, "autorec"); + HTSMSG_FOREACH(f, list) { + if (!(e = htsmsg_field_get_map(f))) continue; + if (strcmp(s1, htsmsg_get_str(e, "id")) == 0) { + htsmsg_add_str(c, "autorec", htsmsg_get_str(e, "uuid")); + break; + } + } + } +} + +static void +config_migrate_v9 ( void ) +{ + htsmsg_t *list = htsmsg_create_list(); + htsmsg_t *c, *e; + htsmsg_field_t *f; + tvh_uuid_t u; + + config_migrate_simple("autorec", list, config_modify_autorec, NULL); + config_migrate_simple("dvr/log", NULL, config_modify_dvr_log, list); + htsmsg_destroy(list); + + if ((c = hts_settings_load("dvr")) != NULL) { + /* step 1: only "config" */ + HTSMSG_FOREACH(f, c) { + if (!(e = htsmsg_field_get_map(f))) continue; + if (strcmp(f->hmf_name, "config")) continue; + htsmsg_add_str(e, "name", f->hmf_name + 6); + uuid_init_hex(&u, NULL); + hts_settings_remove("dvr/%s", f->hmf_name); + hts_settings_save(e, "dvr/config/%s", u.hex); + } + /* step 2: reset (without "config") */ + HTSMSG_FOREACH(f, c) { + if (!(e = htsmsg_field_get_map(f))) continue; + if (strcmp(f->hmf_name, "config") == 0) continue; + if (strncmp(f->hmf_name, "config", 6)) continue; + htsmsg_add_str(e, "name", f->hmf_name + 6); + uuid_init_hex(&u, NULL); + hts_settings_remove("dvr/%s", f->hmf_name); + hts_settings_save(e, "dvr/config/%s", u.hex); + } + htsmsg_destroy(c); + } +} + /* * Migration table */ @@ -743,6 +839,7 @@ static const config_migrate_t config_migrate_table[] = { config_migrate_v6, config_migrate_v7, config_migrate_v8, + config_migrate_v9, }; /* diff --git a/src/dvr/dvr.h b/src/dvr/dvr.h index 4cf131495..015b5b8a9 100644 --- a/src/dvr/dvr.h +++ b/src/dvr/dvr.h @@ -27,6 +27,10 @@ #include "lang_str.h" typedef struct dvr_config { + idnode_t dvr_id; + LIST_ENTRY(dvr_config) config_link; + + int dvr_enabled; char *dvr_config_name; char *dvr_storage; uint32_t dvr_retention_days; @@ -34,11 +38,28 @@ typedef struct dvr_config { char *dvr_charset; char *dvr_charset_id; char *dvr_postproc; - int dvr_extra_time_pre; - int dvr_extra_time_post; - - muxer_container_type_t dvr_mc; - muxer_config_t dvr_muxcnf; + uint32_t dvr_extra_time_pre; + uint32_t dvr_extra_time_post; + + int dvr_mc; + muxer_config_t dvr_muxcnf; + + int dvr_dir_per_day; + int dvr_channel_dir; + int dvr_channel_in_title; + int dvr_date_in_title; + int dvr_time_in_title; + int dvr_whitespace_in_title; + int dvr_title_dir; + int dvr_episode_in_title; + int dvr_clean_title; + int dvr_tag_files; + int dvr_skip_commercials; + int dvr_subtitle_in_title; + int dvr_episode_before_date; + int dvr_episode_duplicate; + int dvr_rewrite_pat; + int dvr_rewrite_pmt; /* Series link support */ int dvr_sl_brand_lock; @@ -51,7 +72,6 @@ typedef struct dvr_config { /* Duplicate detect */ int dvr_dup_detect_episode; - LIST_ENTRY(dvr_config) config_link; } dvr_config_t; extern struct dvr_config_list dvrconfigs; @@ -106,6 +126,8 @@ typedef enum { typedef struct dvr_entry { + idnode_t de_id; + int de_refcnt; /* Modification is protected under global_lock */ @@ -115,7 +137,6 @@ typedef struct dvr_entry { */ LIST_ENTRY(dvr_entry) de_global_link; - int de_id; channel_t *de_channel; LIST_ENTRY(dvr_entry) de_channel_link; @@ -128,6 +149,7 @@ typedef struct dvr_entry { * These meta fields will stay valid as long as reference count > 0 */ + dvr_config_t *de_config; char *de_config_name; time_t de_start; @@ -141,15 +163,13 @@ typedef struct dvr_entry { generated yet */ lang_str_t *de_title; /* Title in UTF-8 (from EPG) */ lang_str_t *de_desc; /* Description in UTF-8 (from EPG) */ - epg_genre_t de_content_type; /* Content type (from EPG) */ + uint32_t de_content_type; /* Content type (from EPG) (only code) */ uint16_t de_dvb_eid; - dvr_prio_t de_pri; - - uint32_t de_dont_reschedule; - - muxer_container_type_t de_mc; + int de_pri; + int de_dont_reschedule; + int de_mc; /** * EPG information / links @@ -214,9 +234,11 @@ typedef struct dvr_entry { * Autorec entry */ typedef struct dvr_autorec_entry { + idnode_t dae_id; + TAILQ_ENTRY(dvr_autorec_entry) dae_link; - char *dae_id; + char *dae_name; char *dae_config_name; int dae_enabled; @@ -226,11 +248,11 @@ typedef struct dvr_autorec_entry { char *dae_title; regex_t dae_title_preg; - epg_genre_t dae_content_type; + uint32_t dae_content_type; - int dae_approx_time; /* Minutes from midnight */ + int dae_start; /* Minutes from midnight */ - int dae_weekdays; + uint32_t dae_weekdays; channel_t *dae_channel; LIST_ENTRY(dvr_autorec_entry) dae_channel_link; @@ -251,6 +273,17 @@ typedef struct dvr_autorec_entry { int dae_maxduration; } dvr_autorec_entry_t; +TAILQ_HEAD(dvr_autorec_entry_queue, dvr_autorec_entry); + +extern struct dvr_autorec_entry_queue autorec_entries; + +/** + * + */ + +extern const idclass_t dvr_config_class; +extern const idclass_t dvr_entry_class; +extern const idclass_t dvr_autorec_entry_class; /** * Prototypes @@ -262,10 +295,17 @@ dvr_config_t *dvr_config_find_by_name(const char *name); dvr_config_t *dvr_config_find_by_name_default(const char *name); -dvr_config_t *dvr_config_create(const char *name); +dvr_config_t *dvr_config_create(const char *name, const char *uuid, htsmsg_t *conf); + +static inline dvr_config_t *dvr_config_find_by_uuid(const char *uuid) + { return (dvr_config_t*)idnode_find(uuid, &dvr_config_class); } void dvr_config_delete(const char *name); +void dvr_config_save(dvr_config_t *cfg); + +int dvr_entry_get_mc( dvr_entry_t *de); + void dvr_entry_notify(dvr_entry_t *de); void dvr_entry_save(dvr_entry_t *de); @@ -276,28 +316,34 @@ const char *dvr_entry_schedstatus(dvr_entry_t *de); void dvr_entry_create_by_autorec(epg_broadcast_t *e, dvr_autorec_entry_t *dae); -dvr_entry_t *dvr_entry_create_by_event - (const char *dvr_config_name, - epg_broadcast_t *e, - time_t start_extra, time_t stop_extra, - const char *creator, - dvr_autorec_entry_t *dae, - dvr_prio_t pri); - -dvr_entry_t *dvr_entry_create - (const char *dvr_config_name, - channel_t *ch, time_t start, time_t stop, - time_t start_extra, time_t stop_extra, - const char *title, const char *description, const char *lang, - epg_genre_t *content_type, - const char *creator, dvr_autorec_entry_t *dae, - dvr_prio_t pri); - -dvr_entry_t *dvr_entry_update - (dvr_entry_t *de, - const char* de_title, const char *de_desc, const char *lang, - time_t de_start, time_t de_stop, - time_t de_start_extra, time_t de_stop_extra ); +void dvr_entry_created(dvr_entry_t *de); + +dvr_entry_t * +dvr_entry_create ( const char *uuid, htsmsg_t *conf ); + + +dvr_entry_t * +dvr_entry_create_by_event( const char *dvr_config_uuid, + epg_broadcast_t *e, + time_t start_extra, time_t stop_extra, + const char *creator, + dvr_autorec_entry_t *dae, + dvr_prio_t pri ); + +dvr_entry_t * +dvr_entry_create_htsp( const char *dvr_config_uuid, + channel_t *ch, time_t start, time_t stop, + time_t start_extra, time_t stop_extra, + const char *title, const char *description, + const char *lang, epg_genre_t *content_type, + const char *creator, dvr_autorec_entry_t *dae, + dvr_prio_t pri ); + +dvr_entry_t * +dvr_entry_update( dvr_entry_t *de, + const char* de_title, const char *de_desc, const char *lang, + time_t de_start, time_t de_stop, + time_t de_start_extra, time_t de_stop_extra ); void dvr_init(void); @@ -321,6 +367,9 @@ void dvr_event_updated(epg_broadcast_t *e); dvr_entry_t *dvr_entry_find_by_id(int id); +static inline dvr_entry_t *dvr_entry_find_by_uuid(const char *uuid) + { return (dvr_entry_t*)idnode_find(uuid, &dvr_entry_class); } + dvr_entry_t *dvr_entry_find_by_event(epg_broadcast_t *e); dvr_entry_t *dvr_entry_find_by_event_fuzzy(epg_broadcast_t *e); @@ -333,34 +382,13 @@ dvr_entry_t *dvr_entry_cancel(dvr_entry_t *de); void dvr_entry_dec_ref(dvr_entry_t *de); -void dvr_storage_set(dvr_config_t *cfg, const char *storage); - -void dvr_charset_set(dvr_config_t *cfg, const char *charset); - -void dvr_container_set(dvr_config_t *cfg, const char *container); - -void dvr_file_permissions_set(dvr_config_t *cfg, int permissions); - -void dvr_directory_permissions_set(dvr_config_t *cfg, int permissions); - -void dvr_mux_cache_set(dvr_config_t *cfg, int mcache); - -void dvr_postproc_set(dvr_config_t *cfg, const char *postproc); - -void dvr_retention_set(dvr_config_t *cfg, int days); - -void dvr_flags_set(dvr_config_t *cfg, int flags); - -void dvr_mux_flags_set(dvr_config_t *cfg, int flags); - -void dvr_extra_time_pre_set(dvr_config_t *cfg, int d); - -void dvr_extra_time_post_set(dvr_config_t *cfg, int d); - void dvr_entry_delete(dvr_entry_t *de); void dvr_entry_cancel_delete(dvr_entry_t *de); +htsmsg_t *dvr_entry_class_pri_list(void *o); +htsmsg_t *dvr_entry_class_config_name_list(void *o); + /** * Query interface */ @@ -386,15 +414,21 @@ int dvr_sort_start_ascending(const void *A, const void *B); /** * */ -void dvr_autorec_add(const char *dvr_config_name, - const char *title, const char *channel, - const char *tag, epg_genre_t *content_type, - const int min_duration, const int max_duration, - const char *creator, const char *comment); -void dvr_autorec_add_series_link(const char *dvr_config_name, - epg_broadcast_t *event, - const char *creator, const char *comment); +dvr_autorec_entry_t * +dvr_autorec_create(const char *uuid, htsmsg_t *conf); + +dvr_autorec_entry_t * +dvr_autorec_add_series_link(const char *dvr_config_name, + epg_broadcast_t *event, + const char *creator, const char *comment); + +void dvr_autorec_save(dvr_autorec_entry_t *dae); + +static inline dvr_autorec_entry_t * +dvr_autorec_find_by_uuid(const char *uuid) + { return (dvr_autorec_entry_t*)idnode_find(uuid, &dvr_autorec_entry_class); } + void dvr_autorec_check_event(epg_broadcast_t *e); void dvr_autorec_check_brand(epg_brand_t *b); @@ -404,8 +438,6 @@ void dvr_autorec_check_serieslink(epg_serieslink_t *s); void autorec_destroy_by_channel(channel_t *ch, int delconf); -dvr_autorec_entry_t *autorec_entry_find(const char *id, int create); - /** * */ diff --git a/src/dvr/dvr_autorec.c b/src/dvr/dvr_autorec.c index f477d3967..75ba7234a 100644 --- a/src/dvr/dvr_autorec.c +++ b/src/dvr/dvr_autorec.c @@ -35,10 +35,6 @@ #include "dtable.h" #include "epg.h" -dtable_t *autorec_dt; - -TAILQ_HEAD(dvr_autorec_entry_queue, dvr_autorec_entry); - static int dvr_autorec_in_init = 0; struct dvr_autorec_entry_queue autorec_entries; @@ -80,13 +76,13 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e) if(dae->dae_channel == NULL && dae->dae_channel_tag == NULL && - dae->dae_content_type.code == 0 && + dae->dae_content_type == 0 && (dae->dae_title == NULL || dae->dae_title[0] == '\0') && dae->dae_brand == NULL && dae->dae_season == NULL && dae->dae_minduration == 0 && - dae->dae_maxduration == 0 && + (dae->dae_maxduration == 0 || dae->dae_maxduration > 24 * 3600) && dae->dae_serieslink == NULL) return 0; // Avoid super wildcard match @@ -124,18 +120,21 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e) return 0; } - if(dae->dae_content_type.code != 0) { - if (!epg_genre_list_contains(&e->episode->genre, &dae->dae_content_type, 1)) + if(dae->dae_content_type != 0) { + epg_genre_t ct; + memset(&ct, 0, sizeof(ct)); + ct.code = dae->dae_content_type; + if (!epg_genre_list_contains(&e->episode->genre, &ct, 1)) return 0; } - if(dae->dae_approx_time != 0) { + if(dae->dae_start >= 0) { struct tm a_time; struct tm ev_time; localtime_r(&e->start, &a_time); localtime_r(&e->start, &ev_time); - a_time.tm_min = dae->dae_approx_time % 60; - a_time.tm_hour = dae->dae_approx_time / 60; + a_time.tm_min = dae->dae_start % 60; + a_time.tm_hour = dae->dae_start / 60; if(abs(mktime(&a_time) - mktime(&ev_time)) > 900) return 0; } @@ -159,54 +158,76 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e) return 1; } - /** * */ dvr_autorec_entry_t * -autorec_entry_find(const char *id, int create) +dvr_autorec_create(const char *uuid, htsmsg_t *conf) { dvr_autorec_entry_t *dae; - char buf[20]; - static int tally; - if(id != NULL) { - TAILQ_FOREACH(dae, &autorec_entries, dae_link) - if(!strcmp(dae->dae_id, id)) - return dae; - } + dae = calloc(1, sizeof(*dae)); - if(create == 0) + if (idnode_insert(&dae->dae_id, uuid, &dvr_autorec_entry_class, 0)) { + if (uuid) + tvhwarn("dvr", "invalid autorec entry uuid '%s'", uuid); + free(dae); return NULL; - - dae = calloc(1, sizeof(dvr_autorec_entry_t)); - if(id == NULL) { - tally++; - snprintf(buf, sizeof(buf), "%d", tally); - id = buf; - } else { - tally = MAX(atoi(id), tally); } + dae->dae_weekdays = 0x7f; dae->dae_pri = DVR_PRIO_NORMAL; + dae->dae_start = -1; - dae->dae_id = strdup(id); TAILQ_INSERT_TAIL(&autorec_entries, dae, dae_link); + + idnode_load(&dae->dae_id, conf); + return dae; } - +/** + * + */ +dvr_autorec_entry_t * +dvr_autorec_add_series_link(const char *dvr_config_name, + epg_broadcast_t *event, + const char *creator, const char *comment) +{ + dvr_autorec_entry_t *dae; + htsmsg_t *conf; + char *title; + if (!event || !event->episode || !event->serieslink) + return NULL; + conf = htsmsg_create_map(); + title = regexp_escape(epg_broadcast_get_title(event, NULL)); + htsmsg_add_str(conf, "title", title); + free(title); + htsmsg_add_str(conf, "config_name", dvr_config_name ?: ""); + htsmsg_add_str(conf, "channel", channel_get_name(event->channel)); + htsmsg_add_str(conf, "serieslink", event->serieslink->uri); + htsmsg_add_str(conf, "creator", creator ?: ""); + htsmsg_add_str(conf, "comment", comment ?: ""); + dae = dvr_autorec_create(NULL, conf); + htsmsg_destroy(conf); + return dae; +} /** * */ static void -autorec_entry_destroy(dvr_autorec_entry_t *dae) +autorec_entry_destroy(dvr_autorec_entry_t *dae, int delconf) { dvr_autorec_purge_spawns(dae); - free(dae->dae_id); + if (delconf) + hts_settings_remove("dvr/autorec/%s", idnode_uuid_as_str(&dae->dae_id)); + TAILQ_REMOVE(&autorec_entries, dae, dae_link); + idnode_unlink(&dae->dae_id); + + free(dae->dae_name); free(dae->dae_config_name); free(dae->dae_creator); free(dae->dae_comment); @@ -228,268 +249,578 @@ autorec_entry_destroy(dvr_autorec_entry_t *dae) dae->dae_season->putref(dae->dae_season); if(dae->dae_serieslink) dae->dae_serieslink->putref(dae->dae_serieslink); - - TAILQ_REMOVE(&autorec_entries, dae, dae_link); free(dae); } /** * */ -static void -build_weekday_tags(htsmsg_t *l, int mask) +void +dvr_autorec_save(dvr_autorec_entry_t *dae) { - int i; - for(i = 0; i < 7; i++) { - if(mask & (1 << i)) - htsmsg_add_u32(l, NULL, i+1); - } -} + htsmsg_t *m = htsmsg_create_map(); -/** - * - */ -static int -build_weekday_mask(htsmsg_t *l) -{ - int r = 0; - uint32_t u32; - htsmsg_field_t *f; - HTSMSG_FOREACH(f, l) - if (!htsmsg_field_get_u32(f, &u32)) - r |= 1 << (u32 - 1); - return r; + lock_assert(&global_lock); + + idnode_save(&dae->dae_id, m); + hts_settings_save(m, "dvr/autorec/%s", idnode_uuid_as_str(&dae->dae_id)); + htsmsg_destroy(m); } +/* ************************************************************************** + * DVR Autorec Entry Class definition + * **************************************************************************/ -/** - * - */ -static htsmsg_t * -autorec_record_build(dvr_autorec_entry_t *dae) +static void +dvr_autorec_entry_class_save(idnode_t *self) { - htsmsg_t *e = htsmsg_create_map(); - htsmsg_t *l = htsmsg_create_list(); - - htsmsg_add_str(e, "id", dae->dae_id); - htsmsg_add_u32(e, "enabled", !!dae->dae_enabled); - - if (dae->dae_config_name != NULL) - htsmsg_add_str(e, "config_name", dae->dae_config_name); - if(dae->dae_creator != NULL) - htsmsg_add_str(e, "creator", dae->dae_creator); - if(dae->dae_comment != NULL) - htsmsg_add_str(e, "comment", dae->dae_comment); - - if(dae->dae_channel != NULL) - htsmsg_add_str(e, "channel", channel_get_uuid(dae->dae_channel)); + dvr_autorec_save((dvr_autorec_entry_t *)self); +} - if(dae->dae_channel_tag != NULL) - htsmsg_add_str(e, "tag", dae->dae_channel_tag->ct_name); +static void +dvr_autorec_entry_class_delete(idnode_t *self) +{ + autorec_entry_destroy((dvr_autorec_entry_t *)self, 1); +} - htsmsg_add_u32(e, "contenttype",dae->dae_content_type.code); +static const char * +dvr_autorec_entry_class_get_title (idnode_t *self) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)self; + const char *s = ""; + if (dae->dae_name && dae->dae_name[0] != '\0') + s = dae->dae_name; + else if (dae->dae_comment && dae->dae_comment[0] != '\0') + s = dae->dae_comment; + return s; +} - htsmsg_add_str(e, "title", dae->dae_title ?: ""); +static int +dvr_autorec_entry_class_channel_set(void *o, const void *v) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + channel_t *ch = v ? channel_find_by_uuid(v) : NULL; + if (ch == NULL) ch = v ? channel_find_by_name(v) : NULL; + if (ch == NULL) { + if (dae->dae_channel) { + LIST_REMOVE(dae, dae_channel_link); + dae->dae_channel = NULL; + return 1; + } + } else if (dae->dae_channel != ch) { + if (dae->dae_channel) + LIST_REMOVE(dae, dae_channel_link); + dae->dae_channel = ch; + LIST_INSERT_HEAD(&ch->ch_autorecs, dae, dae_channel_link); + return 1; + } + return 0; +} - htsmsg_add_u32(e, "approx_time", dae->dae_approx_time); +static const void * +dvr_autorec_entry_class_channel_get(void *o) +{ + static const char *ret; + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + if (dae->dae_channel) + ret = idnode_uuid_as_str(&dae->dae_channel->ch_id); + else + ret = ""; + return &ret; +} - build_weekday_tags(l, dae->dae_weekdays); - htsmsg_add_msg(e, "weekdays", l); +static htsmsg_t * +dvr_autorec_entry_class_channel_list(void *o) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_str(m, "type", "api"); + htsmsg_add_str(m, "uri", "channel/list"); + htsmsg_add_str(m, "event", "channel"); + return m; +} - if (dae->dae_minduration) - htsmsg_add_u32(e, "minduration", dae->dae_minduration); - if (dae->dae_maxduration) - htsmsg_add_u32(e, "maxduration", dae->dae_maxduration); +static int +dvr_autorec_entry_class_title_set(void *o, const void *v) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + const char *title = v ?: ""; + if (strcmp(title, dae->dae_title ?: "")) { + if (dae->dae_title) { + regfree(&dae->dae_title_preg); + free(dae->dae_title); + dae->dae_title = NULL; + } + if (title != NULL && title[0] != '\0' && + !regcomp(&dae->dae_title_preg, title, + REG_ICASE | REG_EXTENDED | REG_NOSUB)) + dae->dae_title = strdup(title); + return 1; + } + return 0; +} - htsmsg_add_str(e, "pri", dvr_val2pri(dae->dae_pri)); - - if (dae->dae_brand) - htsmsg_add_str(e, "brand", dae->dae_brand->uri); - if (dae->dae_season) - htsmsg_add_str(e, "season", dae->dae_season->uri); - if (dae->dae_serieslink) - htsmsg_add_str(e, "serieslink", dae->dae_serieslink->uri); +static int +dvr_autorec_entry_class_tag_set(void *o, const void *v) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + channel_tag_t *tag = v ? channel_tag_find_by_uuid(v) : NULL; + if (tag == NULL) tag = v ? channel_tag_find_by_name(v, 0) : NULL; + if (tag == NULL && dae->dae_channel_tag) { + LIST_REMOVE(dae, dae_channel_tag_link); + dae->dae_channel_tag = NULL; + return 1; + } else if (dae->dae_channel_tag != tag) { + if (dae->dae_channel_tag) + LIST_REMOVE(dae, dae_channel_tag_link); + dae->dae_channel_tag = tag; + LIST_INSERT_HEAD(&tag->ct_autorecs, dae, dae_channel_tag_link); + return 1; + } + return 0; +} - return e; +static const void * +dvr_autorec_entry_class_tag_get(void *o) +{ + static const char *ret; + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + if (dae->dae_channel_tag) + ret = idnode_uuid_as_str(&dae->dae_channel_tag->ct_id); + else + ret = ""; + return &ret; } -/** - * - */ static htsmsg_t * -autorec_record_get_all(void *opaque) +dvr_autorec_entry_class_tag_list(void *o) { - htsmsg_t *r = htsmsg_create_list(); - dvr_autorec_entry_t *dae; - - TAILQ_FOREACH(dae, &autorec_entries, dae_link) - htsmsg_add_msg(r, NULL, autorec_record_build(dae)); + htsmsg_t *e, *m = htsmsg_create_map(); + htsmsg_add_str(m, "type", "api"); + htsmsg_add_str(m, "uri", "channeltag/list"); + htsmsg_add_str(m, "event", "channeltag"); + e = htsmsg_create_map(); + htsmsg_add_msg(m, "params", e); + return m; +} - return r; +static int +dvr_autorec_entry_class_time_set(void *o, const void *v, int *tm) +{ + const char *s = v; + int t; + + if(s == NULL || s[0] == '\0') + t = -1; + else if(strchr(s, ':') != NULL) + // formatted time string - convert + t = (atoi(s) * 60) + atoi(s + 3); + else { + t = atoi(s); + } + if (t != *tm) { + *tm = t; + return 1; + } + return 0; } -/** - * - */ -static htsmsg_t * -autorec_record_get(void *opaque, const char *id) +static int +dvr_autorec_entry_class_start_set(void *o, const void *v) { - dvr_autorec_entry_t *ae; + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + return dvr_autorec_entry_class_time_set(o, v, &dae->dae_start); +} - if((ae = autorec_entry_find(id, 0)) == NULL) - return NULL; - return autorec_record_build(ae); +#if 0 +static int +dvr_autorec_entry_class_stop_set(void *o, const void *v) +{ + return dvr_autorec_entry_class_time_set(o, v, &dae->dae_stop); } +#endif +static const void * +dvr_autorec_entry_class_time_get(void *o, int tm) +{ + static const char *ret; + static char buf[16]; + if (tm >= 0) + snprintf(buf, sizeof(buf), "%02d:%02d", tm / 60, tm % 60); + else + strcpy(buf, "Not set"); + ret = buf; + return &ret; +} -/** - * - */ -static htsmsg_t * -autorec_record_create(void *opaque) +static const void * +dvr_autorec_entry_class_start_get(void *o) { - return autorec_record_build(autorec_entry_find(NULL, 1)); + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + return dvr_autorec_entry_class_time_get(o, dae->dae_start); } +#if 0 +static int +dvr_autorec_entry_class_stop_get(void *o) +{ + return dvr_autorec_entry_class_time_get(o, v, &dae->dae_stop); +} +#endif -/** - * - */ static htsmsg_t * -autorec_record_update(void *opaque, const char *id, htsmsg_t *values, - int maycreate) +dvr_autorec_entry_class_time_list(void *o) { - int save; - dvr_autorec_entry_t *dae; - const char *s; - channel_t *ch; - channel_tag_t *ct; - uint32_t u32; - htsmsg_t *l; - - if((dae = autorec_entry_find(id, maycreate)) == NULL) - return NULL; - - tvh_str_update(&dae->dae_config_name, htsmsg_get_str(values, "config_name")); - tvh_str_update(&dae->dae_creator, htsmsg_get_str(values, "creator")); - tvh_str_update(&dae->dae_comment, htsmsg_get_str(values, "comment")); - - if((s = htsmsg_get_str(values, "channel")) != NULL) { - if(dae->dae_channel != NULL) { - LIST_REMOVE(dae, dae_channel_link); - dae->dae_channel = NULL; - } - ch = channel_find(s); - if (!ch) ch = channel_find_by_name(s); - if (ch) { - LIST_INSERT_HEAD(&ch->ch_autorecs, dae, dae_channel_link); - dae->dae_channel = ch; - } + int i; + htsmsg_t *e, *l = htsmsg_create_list(); + char buf[16]; + e = htsmsg_create_map(); + htsmsg_add_str(e, "key", ""); + htsmsg_add_str(e, "val", "Not set"); + htsmsg_add_msg(l, NULL, e); + for (i = 0; i < 24*60; i += 10) { + snprintf(buf, sizeof(buf), "%02d:%02d", i / 60, (i % 60)); + e = htsmsg_create_map(); + htsmsg_add_str(e, "key", buf); + htsmsg_add_str(e, "val", buf); + htsmsg_add_msg(l, NULL, e); } + return l; +} - if((s = htsmsg_get_str(values, "title")) != NULL) { - if(dae->dae_title != NULL) { - free(dae->dae_title); - dae->dae_title = NULL; - regfree(&dae->dae_title_preg); - } +static htsmsg_t * +dvr_autorec_entry_class_minduration_list(void *o) +{ + /* reuse */ + return dvr_autorec_entry_class_time_list(o); +} - if(!regcomp(&dae->dae_title_preg, s, - REG_ICASE | REG_EXTENDED | REG_NOSUB)) { - dae->dae_title = strdup(s); - } +static int +dvr_autorec_entry_class_config_name_set(void *o, const void *v) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + dvr_config_t *cfg = v ? dvr_config_find_by_uuid(v) : NULL; + if (cfg == NULL) cfg = v ? dvr_config_find_by_name_default(v): NULL; + if (cfg == NULL && dae->dae_config_name) { + free(dae->dae_config_name); + return 1; + } else if (strcmp(dae->dae_config_name ?: "", cfg ? cfg->dvr_config_name : "")) { + free(dae->dae_config_name); + dae->dae_config_name = strdup(cfg->dvr_config_name); + return 1; } + return 0; +} - if((s = htsmsg_get_str(values, "tag")) != NULL) { - if(dae->dae_channel_tag != NULL) { - LIST_REMOVE(dae, dae_channel_tag_link); - dae->dae_channel_tag = NULL; - } - if((ct = channel_tag_find_by_name(s, 0)) != NULL) { - LIST_INSERT_HEAD(&ct->ct_autorecs, dae, dae_channel_tag_link); - dae->dae_channel_tag = ct; - } - } +static int +dvr_autorec_entry_class_weekdays_set(void *o, const void *v) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + htsmsg_field_t *f; + uint32_t u32, bits = 0; - if (!htsmsg_get_u32(values, "contenttype", &u32)) - dae->dae_content_type.code = u32; - - if((s = htsmsg_get_str(values, "approx_time")) != NULL) { - if(strchr(s, ':') != NULL) { - // formatted time string - convert - dae->dae_approx_time = (atoi(s) * 60) + atoi(s + 3); - } else if(strlen(s) == 0) { - dae->dae_approx_time = 0; - } else { - dae->dae_approx_time = atoi(s); - } - } + HTSMSG_FOREACH(f, (htsmsg_t *)v) + if (!htsmsg_field_get_u32(f, &u32) && u32 > 0 && u32 < 8) + bits |= (1 << (u32 - 1)); - if(!htsmsg_get_u32(values, "minduration", &u32)) - dae->dae_minduration = u32; + if (bits != dae->dae_weekdays) { + dae->dae_weekdays = bits; + return 1; + } + return 0; +} - if(!htsmsg_get_u32(values, "maxduration", &u32)) - dae->dae_maxduration = u32; +static const void * +dvr_autorec_entry_class_weekdays_get(void *o) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + htsmsg_t *m = htsmsg_create_list(); + int i; + for (i = 0; i < 7; i++) + if (dae->dae_weekdays & (1 << i)) + htsmsg_add_u32(m, NULL, i + 1); + return m; +} - if((l = htsmsg_get_list(values, "weekdays")) != NULL) - dae->dae_weekdays = build_weekday_mask(l); +static const struct strtab dvr_autorec_entry_class_weekdays_tab[] = { + { "Mon", 1 }, + { "Tue", 2 }, + { "Wed", 3 }, + { "Thu", 4 }, + { "Fri", 5 }, + { "Sat", 6 }, + { "Sun", 7 }, +}; - if(!htsmsg_get_u32(values, "enabled", &u32)) - dae->dae_enabled = u32; +static htsmsg_t * +dvr_autorec_entry_class_weekdays_list ( void *o ) +{ + return strtab2htsmsg(dvr_autorec_entry_class_weekdays_tab); +} - if((s = htsmsg_get_str(values, "pri")) != NULL) - dae->dae_pri = dvr_pri2val(s); +static char * +dvr_autorec_entry_class_weekdays_rend(void *o) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + static char buf[32]; + size_t l; + int i; + if (dae->dae_weekdays == 0x7f) + strcpy(buf + 1, "All days"); + else if (dae->dae_weekdays == 0) + strcpy(buf + 1, "No days"); + else { + buf[0] = '\0'; + for (i = 0; i < 7; i++) + if (dae->dae_weekdays & (1 << i)) { + l = strlen(buf); + snprintf(buf + l, sizeof(buf) - l, ",%s", + val2str(i + 1, dvr_autorec_entry_class_weekdays_tab)); + } + } + return buf + 1; +} - if((s = htsmsg_get_str(values, "brand")) != NULL) { - dae->dae_brand = epg_brand_find_by_uri(s, 1, &save); +static int +dvr_autorec_entry_class_brand_set(void *o, const void *v) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + int save; + epg_brand_t *brand = epg_brand_find_by_uri(v, 1, &save); + if (brand && dae->dae_brand != brand) { if (dae->dae_brand) - dae->dae_brand->getref((epg_object_t*)dae->dae_brand); + dae->dae_brand->putref((epg_object_t*)dae->dae_brand); + brand->getref((epg_object_t*)brand); + dae->dae_brand = brand; + return 1; + } else if (brand == NULL && dae->dae_brand) { + dae->dae_brand->putref((epg_object_t*)dae->dae_brand); + dae->dae_brand = NULL; + return 1; } - if((s = htsmsg_get_str(values, "season")) != NULL) { - dae->dae_season = epg_season_find_by_uri(s, 1, &save); + return 0; +} + +static const void * +dvr_autorec_entry_class_brand_get(void *o) +{ + static const char *ret; + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + if (dae->dae_brand) + ret = dae->dae_brand->uri; + else + ret = ""; + return &ret; +} + +static int +dvr_autorec_entry_class_season_set(void *o, const void *v) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + int save; + epg_season_t *season = epg_season_find_by_uri(v, 1, &save); + if (season && dae->dae_season != season) { if (dae->dae_season) - dae->dae_season->getref((epg_object_t*)dae->dae_season); - } - if((s = htsmsg_get_str(values, "serieslink")) != NULL) { - dae->dae_serieslink = epg_serieslink_find_by_uri(s, 1, &save); - if (dae->dae_serieslink) - dae->dae_serieslink->getref(dae->dae_serieslink); + dae->dae_season->putref((epg_object_t*)dae->dae_season); + season->getref((epg_object_t*)season); + dae->dae_season = season; + return 1; + } else if (season == NULL && dae->dae_season) { + dae->dae_season->putref((epg_object_t*)dae->dae_season); + dae->dae_season = NULL; + return 1; } - if (!dvr_autorec_in_init) - dvr_autorec_changed(dae, 1); - - return autorec_record_build(dae); + return 0; } +static const void * +dvr_autorec_entry_class_season_get(void *o) +{ + static const char *ret; + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + if (dae->dae_season) + ret = dae->dae_season->uri; + else + ret = ""; + return &ret; +} -/** - * - */ static int -autorec_record_delete(void *opaque, const char *id) +dvr_autorec_entry_class_series_link_set(void *o, const void *v) { - dvr_autorec_entry_t *dae; - - if((dae = autorec_entry_find(id, 0)) == NULL) - return -1; - autorec_entry_destroy(dae); + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + int save; + epg_serieslink_t *sl = epg_serieslink_find_by_uri(v, 1, &save); + if (sl && dae->dae_serieslink != sl) { + if (dae->dae_serieslink) + dae->dae_serieslink->putref((epg_object_t*)dae->dae_season); + sl->getref((epg_object_t*)sl); + dae->dae_serieslink = sl; + return 1; + } else if (sl == NULL && dae->dae_serieslink) { + dae->dae_season->putref((epg_object_t*)dae->dae_season); + dae->dae_season = NULL; + return 1; + } return 0; } +static const void * +dvr_autorec_entry_class_series_link_get(void *o) +{ + static const char *ret; + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + if (dae->dae_serieslink) + ret = dae->dae_serieslink->uri; + else + ret = ""; + return &ret; +} -/** - * - */ -static const dtable_class_t autorec_dtc = { - .dtc_record_get = autorec_record_get, - .dtc_record_get_all = autorec_record_get_all, - .dtc_record_create = autorec_record_create, - .dtc_record_update = autorec_record_update, - .dtc_record_delete = autorec_record_delete, - .dtc_read_access = ACCESS_RECORDER, - .dtc_write_access = ACCESS_RECORDER, - .dtc_mutex = &global_lock, +static htsmsg_t * +dvr_autorec_entry_class_content_type_list(void *o) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_str(m, "type", "api"); + htsmsg_add_str(m, "uri", "epg/content_type/list"); + return m; +} + +const idclass_t dvr_autorec_entry_class = { + .ic_class = "dvrautorec", + .ic_caption = "DVR Auto-Record Entry", + .ic_save = dvr_autorec_entry_class_save, + .ic_get_title = dvr_autorec_entry_class_get_title, + .ic_delete = dvr_autorec_entry_class_delete, + .ic_properties = (const property_t[]) { + { + .type = PT_BOOL, + .id = "enabled", + .name = "Enabled", + .off = offsetof(dvr_autorec_entry_t, dae_enabled), + }, + { + .type = PT_STR, + .id = "name", + .name = "Name", + .off = offsetof(dvr_autorec_entry_t, dae_name), + }, + { + .type = PT_STR, + .id = "title", + .name = "Title (Regexp)", + .set = dvr_autorec_entry_class_title_set, + .off = offsetof(dvr_autorec_entry_t, dae_title), + }, + { + .type = PT_STR, + .id = "channel", + .name = "Channel", + .set = dvr_autorec_entry_class_channel_set, + .get = dvr_autorec_entry_class_channel_get, + .list = dvr_autorec_entry_class_channel_list, + }, + { + .type = PT_STR, + .id = "tag", + .name = "Channel Tag", + .set = dvr_autorec_entry_class_tag_set, + .get = dvr_autorec_entry_class_tag_get, + .list = dvr_autorec_entry_class_tag_list, + }, + { + .type = PT_STR, + .id = "start", + .name = "Starting Around", + .set = dvr_autorec_entry_class_start_set, + .get = dvr_autorec_entry_class_start_get, + .list = dvr_autorec_entry_class_time_list, + }, + { + .type = PT_U32, + .islist = 1, + .id = "weekdays", + .name = "Week Days", + .set = dvr_autorec_entry_class_weekdays_set, + .get = dvr_autorec_entry_class_weekdays_get, + .list = dvr_autorec_entry_class_weekdays_list, + .rend = dvr_autorec_entry_class_weekdays_rend, + }, + { + .type = PT_U32, + .id = "minduration", + .name = "Minimal Duration", + .list = dvr_autorec_entry_class_minduration_list, + .off = offsetof(dvr_autorec_entry_t, dae_minduration), + }, + { + .type = PT_U32, + .id = "maxduration", + .name = "Maximal Duration", + .off = offsetof(dvr_autorec_entry_t, dae_maxduration), + }, + { + .type = PT_U32, + .id = "content_type", + .name = "Content Type", + .list = dvr_autorec_entry_class_content_type_list, + .off = offsetof(dvr_autorec_entry_t, dae_content_type), + }, + { + .type = PT_U32, + .id = "pri", + .name = "Priority", + .list = dvr_entry_class_pri_list, + .def.i = DVR_PRIO_NORMAL, + .off = offsetof(dvr_autorec_entry_t, dae_pri), + }, + { + .type = PT_STR, + .id = "config_name", + .name = "DVR Configuration", + .set = dvr_autorec_entry_class_config_name_set, + .list = dvr_entry_class_config_name_list, + .off = offsetof(dvr_autorec_entry_t, dae_config_name), + }, + { + .type = PT_STR, + .id = "brand", + .name = "Brand", + .set = dvr_autorec_entry_class_brand_set, + .get = dvr_autorec_entry_class_brand_get, + .opts = PO_RDONLY, + }, + { + .type = PT_STR, + .id = "season", + .name = "Season", + .set = dvr_autorec_entry_class_season_set, + .get = dvr_autorec_entry_class_season_get, + .opts = PO_RDONLY, + }, + { + .type = PT_STR, + .id = "serieslink", + .name = "Series Link", + .set = dvr_autorec_entry_class_series_link_set, + .get = dvr_autorec_entry_class_series_link_get, + .opts = PO_RDONLY, + }, + { + .type = PT_STR, + .id = "creator", + .name = "Creator", + .off = offsetof(dvr_autorec_entry_t, dae_creator), + .opts = PO_RDONLY, + }, + { + .type = PT_STR, + .id = "comment", + .name = "Comment", + .off = offsetof(dvr_autorec_entry_t, dae_comment), + }, + {} + } }; /** @@ -498,10 +829,18 @@ static const dtable_class_t autorec_dtc = { void dvr_autorec_init(void) { + htsmsg_t *l, *c; + htsmsg_field_t *f; + TAILQ_INIT(&autorec_entries); - autorec_dt = dtable_create(&autorec_dtc, "autorec", NULL); dvr_autorec_in_init = 1; - dtable_load(autorec_dt); + if((l = hts_settings_load("dvr/autorec")) != NULL) { + HTSMSG_FOREACH(f, l) { + if((c = htsmsg_get_map_by_field(f)) == NULL) + continue; + (void *)dvr_autorec_create(f->hmf_name, c); + } + } dvr_autorec_in_init = 0; } @@ -511,12 +850,9 @@ dvr_autorec_done(void) dvr_autorec_entry_t *dae; pthread_mutex_lock(&global_lock); - while ((dae = TAILQ_FIRST(&autorec_entries)) != NULL) { - TAILQ_REMOVE(&autorec_entries, dae, dae_link); - free(dae); - } + while ((dae = TAILQ_FIRST(&autorec_entries)) != NULL) + autorec_entry_destroy(dae, 0); pthread_mutex_unlock(&global_lock); - dtable_delete("autorec"); } void @@ -528,107 +864,6 @@ dvr_autorec_update(void) } } -static void -_dvr_autorec_add(const char *config_name, - const char *title, channel_t *ch, - const char *tag, epg_genre_t *content_type, - const int min_duration, const int max_duration, - epg_brand_t *brand, epg_season_t *season, - epg_serieslink_t *serieslink, - int approx_time, epg_episode_num_t *epnum, - const char *creator, const char *comment) -{ - dvr_autorec_entry_t *dae; - htsmsg_t *m; - channel_tag_t *ct; - - if((dae = autorec_entry_find(NULL, 1)) == NULL) - return; - - tvh_str_set(&dae->dae_config_name, config_name); - tvh_str_set(&dae->dae_creator, creator); - tvh_str_set(&dae->dae_comment, comment); - - if(ch) { - LIST_INSERT_HEAD(&ch->ch_autorecs, dae, dae_channel_link); - dae->dae_channel = ch; - } - - if(title != NULL && - !regcomp(&dae->dae_title_preg, title, - REG_ICASE | REG_EXTENDED | REG_NOSUB)) { - dae->dae_title = strdup(title); - } - - if(tag != NULL && (ct = channel_tag_find_by_uuid(tag)) != NULL) { - LIST_INSERT_HEAD(&ct->ct_autorecs, dae, dae_channel_tag_link); - dae->dae_channel_tag = ct; - } - - dae->dae_enabled = 1; - if (content_type) - dae->dae_content_type.code = content_type->code; - - if (min_duration) - dae->dae_minduration = min_duration; - - if (max_duration) - dae->dae_maxduration = max_duration; - - if(serieslink) { - serieslink->getref(serieslink); - dae->dae_serieslink = serieslink; - } - - dae->dae_approx_time = approx_time; - - m = autorec_record_build(dae); - hts_settings_save(m, "%s/%s", "autorec", dae->dae_id); - htsmsg_destroy(m); - - /* Notify web clients that we have messed with the tables */ - - notify_reload("autorec"); - - dvr_autorec_changed(dae, 1); -} - -void -dvr_autorec_add(const char *config_name, - const char *title, const char *channel, - const char *tag, epg_genre_t *content_type, - const int min_duration, const int max_duration, - const char *creator, const char *comment) -{ - channel_t *ch = NULL; - if(channel != NULL) ch = channel_find(channel); - _dvr_autorec_add(config_name, title, ch, tag, content_type, - min_duration, max_duration, - NULL, NULL, NULL, 0, NULL, creator, comment); -} - -void dvr_autorec_add_series_link - ( const char *dvr_config_name, epg_broadcast_t *event, - const char *creator, const char *comment ) -{ - char *title; - if (!event || !event->episode) return; - title = regexp_escape(epg_broadcast_get_title(event, NULL)); - _dvr_autorec_add(dvr_config_name, - title, - event->channel, - NULL, 0, // tag/content type - 0,INT_MAX, - NULL, - NULL, - event->serieslink, - 0, NULL, - creator, comment); - if (title) - free(title); -} - - /** * */ @@ -692,11 +927,8 @@ autorec_destroy_by_channel(channel_t *ch, int delconf) dvr_autorec_entry_t *dae; htsmsg_t *m; - while((dae = LIST_FIRST(&ch->ch_autorecs)) != NULL) { - if (delconf) - dtable_record_erase(autorec_dt, dae->dae_id); - autorec_entry_destroy(dae); - } + while((dae = LIST_FIRST(&ch->ch_autorecs)) != NULL) + autorec_entry_destroy(dae, delconf); /* Notify web clients that we have messed with the tables */ m = htsmsg_create_map(); diff --git a/src/dvr/dvr_db.c b/src/dvr/dvr_db.c index 7f75dc630..5b1b80ed4 100644 --- a/src/dvr/dvr_db.c +++ b/src/dvr/dvr_db.c @@ -31,8 +31,8 @@ #include "streaming.h" #include "intlconv.h" #include "dbus.h" - -static int de_tally; +#include "imagecache.h" +#include "access.h" int dvr_iov_max; @@ -43,9 +43,51 @@ struct dvr_entry_list dvrentries; static gtimer_t dvr_dbus_timer; #endif +static void dvr_entry_remove(dvr_entry_t *de, int delconf); static void dvr_timer_expire(void *aux); static void dvr_timer_start_recording(void *aux); static void dvr_timer_stop_recording(void *aux); +static int dvr_entry_class_disp_title_set(void *o, const void *v); + +/* + * Start / stop time calculators + */ +static int +dvr_entry_get_start_time( dvr_entry_t *de ) +{ + time_t extra = de->de_start_extra; + + if (extra == 0) { + if (de->de_channel) + extra = de->de_channel->ch_dvr_extra_time_pre; + else + extra = de->de_config->dvr_extra_time_pre; + } + /* Note 30 seconds might not be enough (rotors) */ + return de->de_start - (60 * extra) - 30; +} + +static int +dvr_entry_get_stop_time( dvr_entry_t *de ) +{ + time_t extra = de->de_stop_extra; + + if (extra == 0) { + if (de->de_channel) + extra = de->de_channel->ch_dvr_extra_time_post; + else + extra = de->de_config->dvr_extra_time_pre; + } + return de->de_stop + (60 * extra); +} + +int +dvr_entry_get_mc( dvr_entry_t *de ) +{ + if (de->de_mc >= 0) + return de->de_mc; + return de->de_config->dvr_mc; +} /* * DBUS next dvr start notifications @@ -55,7 +97,7 @@ static void dvr_dbus_timer_cb( void *aux ) { dvr_entry_t *de; - time_t result, preamble, max = 0; + time_t result, start, max = 0; static time_t last_result = 0; lock_assert(&global_lock); @@ -64,18 +106,18 @@ dvr_dbus_timer_cb( void *aux ) LIST_FOREACH(de, &dvrentries, de_global_link) { if (de->de_sched_state != DVR_SCHEDULED) continue; - preamble = de->de_start - (60 * de->de_start_extra) - 30; - if (dispatch_clock < preamble && preamble > max) - max = preamble; + start = dvr_entry_get_start_time(de); + if (dispatch_clock < start && start > max) + max = start; } /* lower the maximum value */ result = max; LIST_FOREACH(de, &dvrentries, de_global_link) { if (de->de_sched_state != DVR_SCHEDULED) continue; - preamble = de->de_start - (60 * de->de_start_extra) - 30; - if (dispatch_clock < preamble && preamble < result) - result = preamble; + start = dvr_entry_get_start_time(de); + if (dispatch_clock < start && start < result) + result = start; } /* different? send it.... */ if (result && result != last_result) { @@ -167,30 +209,6 @@ dvr_entry_schedstatus(dvr_entry_t *de) } } - -/** - * - */ -static void -dvrdb_changed(void) -{ - htsmsg_t *m = htsmsg_create_map(); - htsmsg_add_u32(m, "reload", 1); - notify_by_msg("dvrdb", m); -} - -/** - * - */ -static void -dvrconfig_changed(void) -{ - htsmsg_t *m = htsmsg_create_map(); - htsmsg_add_u32(m, "reload", 1); - notify_by_msg("dvrconfig", m); -} - - /** * */ @@ -200,7 +218,7 @@ dvr_entry_notify(dvr_entry_t *de) htsmsg_t *m = htsmsg_create_map(); htsmsg_add_u32(m, "updateEntry", 1); - htsmsg_add_u32(m, "id", de->de_id); + htsmsg_add_str(m, "uuid", idnode_uuid_as_str(&de->de_id)); htsmsg_add_str(m, "status", dvr_entry_status(de)); htsmsg_add_str(m, "schedstate", dvr_entry_schedstatus(de)); notify_by_msg("dvrdb", m); @@ -210,10 +228,11 @@ dvr_entry_notify(dvr_entry_t *de) /** * */ -static void +static int dvr_charset_update(dvr_config_t *cfg, const char *charset) { const char *s, *id; + int change = strcmp(cfg->dvr_charset ?: "", charset ?: ""); free(cfg->dvr_charset); free(cfg->dvr_charset_id); @@ -221,6 +240,7 @@ dvr_charset_update(dvr_config_t *cfg, const char *charset) id = intlconv_charset_id(s, 1, 1); cfg->dvr_charset = s ? strdup(s) : NULL; cfg->dvr_charset_id = id ? strdup(id) : NULL; + return change; } /** @@ -231,7 +251,7 @@ dvr_make_title(char *output, size_t outlen, dvr_entry_t *de) { struct tm tm; char buf[40]; - dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); + dvr_config_t *cfg = de->de_config; if(cfg->dvr_flags & DVR_CHANNEL_IN_TITLE) snprintf(output, outlen, "%s-", DVR_CH_NAME(de)); @@ -283,52 +303,44 @@ dvr_make_title(char *output, size_t outlen, dvr_entry_t *de) static void dvr_entry_set_timer(dvr_entry_t *de) { - time_t now, preamble; - dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); + time_t now, start, stop; + dvr_config_t *cfg = de->de_config; time(&now); - preamble = de->de_start - (60 * de->de_start_extra) - 30; + start = dvr_entry_get_start_time(de); + stop = dvr_entry_get_stop_time(de); + + if(now >= stop || de->de_dont_reschedule) { - if(now >= de->de_stop || de->de_dont_reschedule) { if(de->de_filename == NULL) de->de_sched_state = DVR_MISSED_TIME; else _dvr_entry_completed(de); gtimer_arm_abs(&de->de_timer, dvr_timer_expire, de, - de->de_stop + cfg->dvr_retention_days * 86400); + de->de_stop + cfg->dvr_retention_days * 86400); } else if (de->de_sched_state == DVR_RECORDING) { - gtimer_arm_abs(&de->de_timer, dvr_timer_stop_recording, de, - de->de_stop + (60 * de->de_stop_extra)); + + gtimer_arm_abs(&de->de_timer, dvr_timer_stop_recording, de, stop); } else if (de->de_channel) { + de->de_sched_state = DVR_SCHEDULED; - tvhtrace("dvr", "entry timer scheduled for %"PRItime_t, preamble); - gtimer_arm_abs(&de->de_timer, dvr_timer_start_recording, de, preamble); + tvhtrace("dvr", "entry timer scheduled for %"PRItime_t, start); + gtimer_arm_abs(&de->de_timer, dvr_timer_start_recording, de, start); #if ENABLE_DBUS_1 gtimer_arm(&dvr_dbus_timer, dvr_dbus_timer_cb, NULL, 5); #endif + } else { + de->de_sched_state = DVR_NOSTATE; + } } -/** - * - */ -static void -dvr_entry_link(dvr_entry_t *de) -{ - de->de_refcnt = 1; - - LIST_INSERT_HEAD(&dvrentries, de, de_global_link); - - dvr_entry_set_timer(de); - - htsp_dvr_entry_add(de); -} /** * Find dvr entry using 'fuzzy' search @@ -364,115 +376,155 @@ dvr_entry_fuzzy_match(dvr_entry_t *de, epg_broadcast_t *e) } /** - * Create the event + * Create the event from config */ -static dvr_entry_t *_dvr_entry_create ( - const char *config_name, epg_broadcast_t *e, - channel_t *ch, time_t start, time_t stop, - time_t start_extra, time_t stop_extra, - const char *title, const char *description, const char *lang, - epg_genre_t *content_type, - const char *creator, dvr_autorec_entry_t *dae, - dvr_prio_t pri) +dvr_entry_t * +dvr_entry_create(const char *uuid, htsmsg_t *conf) { - dvr_entry_t *de; - char tbuf[64]; - struct tm tm; - time_t t; - dvr_config_t *cfg = dvr_config_find_by_name_default(config_name); + dvr_entry_t *de, *de2; + int64_t start, stop; + const char *s; - LIST_FOREACH(de, &ch->ch_dvrs, de_channel_link) - if(de->de_start == start && de->de_sched_state != DVR_COMPLETED) + if (conf) { + if (htsmsg_get_s64(conf, "start", &start)) return NULL; + if (htsmsg_get_s64(conf, "stop", &stop)) + return NULL; + if ((htsmsg_get_str(conf, "channel")) == NULL && + (htsmsg_get_str(conf, "channelname")) == NULL) + return NULL; + } + + de = calloc(1, sizeof(*de)); - de = calloc(1, sizeof(dvr_entry_t)); - de->de_id = ++de_tally; + if (idnode_insert(&de->de_id, uuid, &dvr_entry_class, IDNODE_SHORT_UUID)) { + if (uuid) + tvhwarn("dvr", "invalid entry uuid '%s'", uuid); + free(de); + return NULL; + } - ch = de->de_channel = ch; - LIST_INSERT_HEAD(&de->de_channel->ch_dvrs, de, de_channel_link); + de->de_mc = -1; + idnode_load(&de->de_id, conf); - de->de_mc = cfg->dvr_mc; + /* special case, becaous PO_NOSAVE, load ignores it */ + if (de->de_title == NULL && + (s = htsmsg_get_str(conf, "disp_title")) != NULL) + dvr_entry_class_disp_title_set(de, s); - de->de_start = start; - de->de_stop = stop; - de->de_pri = pri; - if (start_extra) - de->de_start_extra = start_extra; - else if (ch->ch_dvr_extra_time_pre) - de->de_start_extra = ch->ch_dvr_extra_time_pre; - else - de->de_start_extra = cfg->dvr_extra_time_pre; - if (stop_extra) - de->de_stop_extra = stop_extra; - else if (ch->ch_dvr_extra_time_post) - de->de_stop_extra = ch->ch_dvr_extra_time_post; - else - de->de_stop_extra = cfg->dvr_extra_time_post; - de->de_config_name = strdup(cfg->dvr_config_name); - de->de_creator = strdup(creator); + de->de_refcnt = 1; + + LIST_INSERT_HEAD(&dvrentries, de, de_global_link); + + if (de->de_channel) { + LIST_FOREACH(de2, &de->de_channel->ch_dvrs, de_channel_link) + if(de2 != de && + de2->de_start == de->de_start && + de2->de_sched_state != DVR_COMPLETED) { + dvr_entry_remove(de, 0); + return NULL; + } + } + + + dvr_entry_set_timer(de); + htsp_dvr_entry_add(de); - de->de_desc = NULL; - // TODO: this really needs updating + return de; +} + +/** + * Create the event + */ +static dvr_entry_t * +_dvr_entry_create(const char *config_uuid, epg_broadcast_t *e, + channel_t *ch, time_t start, time_t stop, + time_t start_extra, time_t stop_extra, + const char *title, const char *description, + const char *lang, epg_genre_t *content_type, + const char *creator, dvr_autorec_entry_t *dae, + dvr_prio_t pri) +{ + dvr_entry_t *de; + char tbuf[64]; + struct tm tm; + time_t t; + lang_str_t *l; + htsmsg_t *conf; + + conf = htsmsg_create_map(); + htsmsg_add_s64(conf, "start", start); + htsmsg_add_s64(conf, "stop", stop); + htsmsg_add_str(conf, "channel", idnode_uuid_as_str(&ch->ch_id)); + htsmsg_add_u32(conf, "pri", pri); + htsmsg_add_str(conf, "config_name", config_uuid ?: ""); + htsmsg_add_s64(conf, "start_extra", start_extra); + htsmsg_add_s64(conf, "stop_extra", stop_extra); + htsmsg_add_str(conf, "creator", creator ?: ""); if (e) { - de->de_dvb_eid = e->dvb_eid; + htsmsg_add_u32(conf, "dvb_eid", e->dvb_eid); if (e->episode && e->episode->title) - de->de_title = lang_str_copy(e->episode->title); + lang_str_serialize(e->episode->title, conf, "title"); if (e->description) - de->de_desc = lang_str_copy(e->description); + lang_str_serialize(e->description, conf, "description"); else if (e->episode && e->episode->description) - de->de_desc = lang_str_copy(e->episode->description); + lang_str_serialize(e->episode->description, conf, "description"); else if (e->summary) - de->de_desc = lang_str_copy(e->summary); + lang_str_serialize(e->summary, conf, "description"); else if (e->episode && e->episode->summary) - de->de_desc = lang_str_copy(e->episode->summary); + lang_str_serialize(e->episode->summary, conf, "description"); } else if (title) { - de->de_title = lang_str_create(); - lang_str_add(de->de_title, title, lang, 0); + l = lang_str_create(); + lang_str_add(l, title, lang, 0); + lang_str_serialize(l, conf, "title"); if (description) { - de->de_desc = lang_str_create(); - lang_str_add(de->de_desc, description, lang, 0); + l = lang_str_create(); + lang_str_add(l, description, lang, 0); + lang_str_serialize(l, conf, "description"); } } - if (content_type) de->de_content_type = *content_type; - de->de_bcast = e; - if (e) e->getref((epg_object_t*)e); + if (content_type) + htsmsg_add_u32(conf, "content_type", content_type->code / 16); + if (e) + htsmsg_add_u32(conf, "broadcast", e->id); + if (dae) + htsmsg_add_str(conf, "autorec", idnode_uuid_as_str(&dae->dae_id)); + + de = dvr_entry_create(NULL, conf); - dvr_entry_link(de); + htsmsg_destroy(conf); + + if (de == NULL) + return NULL; - t = de->de_start - de->de_start_extra * 60; + t = dvr_entry_get_start_time(de); localtime_r(&t, &tm); if (strftime(tbuf, sizeof(tbuf), "%F %T", &tm) <= 0) *tbuf = 0; - if(dae != NULL) { - de->de_autorec = dae; - LIST_INSERT_HEAD(&dae->dae_spawns, de, de_autorec_link); - } - - tvhlog(LOG_INFO, "dvr", "entry %d \"%s\" on \"%s\" starting at %s, " + tvhlog(LOG_INFO, "dvr", "entry %s \"%s\" on \"%s\" starting at %s, " "scheduled for recording by \"%s\"", - de->de_id, + idnode_uuid_as_str(&de->de_id), lang_str_get(de->de_title, NULL), DVR_CH_NAME(de), tbuf, creator); - - dvrdb_changed(); + dvr_entry_save(de); return de; } - /** * */ dvr_entry_t * -dvr_entry_create(const char *config_name, - channel_t *ch, time_t start, time_t stop, - time_t start_extra, time_t stop_extra, - const char *title, const char *description, const char *lang, - epg_genre_t *content_type, - const char *creator, dvr_autorec_entry_t *dae, - dvr_prio_t pri) -{ - return _dvr_entry_create(config_name, NULL, +dvr_entry_create_htsp(const char *config_uuid, + channel_t *ch, time_t start, time_t stop, + time_t start_extra, time_t stop_extra, + const char *title, + const char *description, const char *lang, + epg_genre_t *content_type, + const char *creator, dvr_autorec_entry_t *dae, + dvr_prio_t pri) +{ + return _dvr_entry_create(config_uuid, NULL, ch, start, stop, start_extra, stop_extra, title, description, lang, content_type, creator, dae, pri); @@ -482,7 +534,7 @@ dvr_entry_create(const char *config_name, * */ dvr_entry_t * -dvr_entry_create_by_event(const char *config_name, +dvr_entry_create_by_event(const char *config_uuid, epg_broadcast_t *e, time_t start_extra, time_t stop_extra, const char *creator, @@ -491,7 +543,7 @@ dvr_entry_create_by_event(const char *config_name, if(!e->channel || !e->episode || !e->episode->title) return NULL; - return _dvr_entry_create(config_name, e, + return _dvr_entry_create(config_uuid, e, e->channel, e->start, e->stop, start_extra, stop_extra, NULL, NULL, NULL, @@ -518,8 +570,7 @@ static int _dvr_duplicate_event ( epg_broadcast_t *e ) if (de->de_bcast->episode == e->episode) return 1; if (has_epnum) { - dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); - int ep_dup_det = (cfg->dvr_flags & DVR_EPISODE_DUPLICATE_DETECTION); + int ep_dup_det = (de->de_config->dvr_flags & DVR_EPISODE_DUPLICATE_DETECTION); if (ep_dup_det) { const char* de_title = lang_str_get(de->de_bcast->episode->title, NULL); @@ -556,7 +607,6 @@ dvr_entry_create_by_autorec(epg_broadcast_t *e, dvr_autorec_entry_t *dae) dvr_entry_create_by_event(dae->dae_config_name, e, 0, 0, buf, dae, dae->dae_pri); } - /** * */ @@ -570,6 +620,8 @@ dvr_entry_dec_ref(dvr_entry_t *de) return; } + idnode_unlink(&de->de_id); + if(de->de_autorec != NULL) LIST_REMOVE(de, de_autorec_link); @@ -578,14 +630,11 @@ dvr_entry_dec_ref(dvr_entry_t *de) free(de->de_creator); if (de->de_title) lang_str_destroy(de->de_title); if (de->de_desc) lang_str_destroy(de->de_desc); - if(de->de_bcast) de->de_bcast->putref((epg_object_t*)de->de_bcast); + if (de->de_bcast) de->de_bcast->putref((epg_object_t*)de->de_bcast); free(de); } - - - /** * */ @@ -593,7 +642,7 @@ static void dvr_entry_remove(dvr_entry_t *de, int delconf) { if (delconf) - hts_settings_remove("dvr/log/%d", de->de_id); + hts_settings_remove("dvr/log/%s", idnode_uuid_as_str(&de->de_id)); htsp_dvr_entry_delete(de); @@ -613,123 +662,9 @@ dvr_entry_remove(dvr_entry_t *de, int delconf) free(de->de_channel_name); de->de_channel_name = NULL; - dvrdb_changed(); - dvr_entry_dec_ref(de); } - -/** - * - */ -static void -dvr_db_load_one(htsmsg_t *c, int id) -{ - dvr_entry_t *de; - const char *chuuid, *chname, *s, *creator; - channel_t *ch; - uint32_t start, stop, bcid, u32; - int d; - dvr_config_t *cfg; - lang_str_t *title, *ls; - - if(htsmsg_get_u32(c, "start", &start)) - return; - if(htsmsg_get_u32(c, "stop", &stop)) - return; - - chname = htsmsg_get_str(c, "channelname"); - chuuid = htsmsg_get_str(c, "channel"); - ch = chuuid ? channel_find(chuuid) : NULL; - - /* Backwards compat */ - if (!ch && !chname) { - chname = chuuid; - ch = channel_find_by_name(chname); - } - - s = htsmsg_get_str(c, "config_name"); - cfg = dvr_config_find_by_name_default(s); - - if(!(title = lang_str_deserialize(c, "title"))) - return; - - if((creator = htsmsg_get_str(c, "creator")) == NULL) - return; - - de = calloc(1, sizeof(dvr_entry_t)); - de->de_id = id; - - de_tally = MAX(id, de_tally); - - if (ch) { - de->de_channel = ch; - LIST_INSERT_HEAD(&de->de_channel->ch_dvrs, de, de_channel_link); - } else { - de->de_channel_name = strdup(chname); - } - - de->de_start = start; - de->de_stop = stop; - de->de_config_name = strdup(cfg->dvr_config_name); - de->de_creator = strdup(creator); - de->de_title = title; - de->de_pri = dvr_pri2val(htsmsg_get_str(c, "pri")); - if (!htsmsg_get_u32(c, "dvb_eid", &u32)) - de->de_dvb_eid = (uint16_t)u32; - - if(htsmsg_get_s32(c, "start_extra", &d)) - if (ch && ch->ch_dvr_extra_time_pre) - de->de_start_extra = ch->ch_dvr_extra_time_pre; - else - de->de_start_extra = cfg->dvr_extra_time_pre; - else - de->de_start_extra = d; - - if(htsmsg_get_s32(c, "stop_extra", &d)) - if (ch && ch->ch_dvr_extra_time_post) - de->de_stop_extra = ch->ch_dvr_extra_time_post; - else - de->de_stop_extra = cfg->dvr_extra_time_post; - else - de->de_stop_extra = d; - - - if ((ls = lang_str_deserialize(c, "description"))) - de->de_desc = ls; - tvh_str_set(&de->de_filename, htsmsg_get_str(c, "filename")); - - htsmsg_get_u32(c, "errorcode", &de->de_last_error); - htsmsg_get_u32(c, "errors", &de->de_errors); - - htsmsg_get_u32(c, "noresched", &de->de_dont_reschedule); - - s = htsmsg_get_str(c, "autorec"); - if(s != NULL) { - dvr_autorec_entry_t *dae = autorec_entry_find(s, 0); - - if(dae != NULL) { - de->de_autorec = dae; - LIST_INSERT_HEAD(&dae->dae_spawns, de, de_autorec_link); - } - } - - - de->de_content_type.code = htsmsg_get_u32_or_default(c, "contenttype", 0); - - if (!htsmsg_get_u32(c, "broadcast", &bcid)) { - de->de_bcast = epg_broadcast_find_by_id(bcid, ch); - if (de->de_bcast) { - de->de_bcast->getref((epg_object_t*)de->de_bcast); - } - } - - de->de_mc = htsmsg_get_u32_or_default(c, "container", MC_MATROSKA); - - dvr_entry_link(de); -} - - /** * */ @@ -743,14 +678,12 @@ dvr_db_load(void) HTSMSG_FOREACH(f, l) { if((c = htsmsg_get_map_by_field(f)) == NULL) continue; - dvr_db_load_one(c, atoi(f->hmf_name)); + (void)dvr_entry_create(f->hmf_name, c); } htsmsg_destroy(l); } } - - /** * */ @@ -761,52 +694,8 @@ dvr_entry_save(dvr_entry_t *de) lock_assert(&global_lock); - if (de->de_channel) - htsmsg_add_str(m, "channel", channel_get_uuid(de->de_channel)); - htsmsg_add_str(m, "channelname", DVR_CH_NAME(de)); - htsmsg_add_u32(m, "start", de->de_start); - htsmsg_add_u32(m, "stop", de->de_stop); - - htsmsg_add_s32(m, "start_extra", de->de_start_extra); - htsmsg_add_s32(m, "stop_extra", de->de_stop_extra); - - htsmsg_add_str(m, "config_name", de->de_config_name); - - htsmsg_add_str(m, "creator", de->de_creator); - - if(de->de_filename != NULL) - htsmsg_add_str(m, "filename", de->de_filename); - - lang_str_serialize(de->de_title, m, "title"); - - if(de->de_dvb_eid) - htsmsg_add_u32(m, "dvb_eid", de->de_dvb_eid); - - if(de->de_desc != NULL) - lang_str_serialize(de->de_desc, m, "description"); - - htsmsg_add_str(m, "pri", dvr_val2pri(de->de_pri)); - - if(de->de_last_error) - htsmsg_add_u32(m, "errorcode", de->de_last_error); - - if(de->de_errors) - htsmsg_add_u32(m, "errors", de->de_errors); - - htsmsg_add_u32(m, "noresched", de->de_dont_reschedule); - - if(de->de_autorec != NULL) - htsmsg_add_str(m, "autorec", de->de_autorec->dae_id); - - if(de->de_content_type.code) - htsmsg_add_u32(m, "contenttype", de->de_content_type.code); - - if(de->de_bcast) - htsmsg_add_u32(m, "broadcast", de->de_bcast->id); - - htsmsg_add_u32(m, "container", de->de_mc); - - hts_settings_save(m, "dvr/log/%d", de->de_id); + idnode_save(&de->de_id, m); + hts_settings_save(m, "dvr/log/%s", idnode_uuid_as_str(&de->de_id)); htsmsg_destroy(m); } @@ -873,8 +762,8 @@ static dvr_entry_t *_dvr_entry_update /* Genre */ if (e && e->episode) { epg_genre_t *g = LIST_FIRST(&e->episode->genre); - if (g && (g->code != de->de_content_type.code)) { - de->de_content_type.code = g->code; + if (g && (g->code / 16) != de->de_content_type) { + de->de_content_type = g->code / 16; save = 1; } } @@ -930,9 +819,10 @@ dvr_event_replaced(epg_broadcast_t *e, epg_broadcast_t *new_e) /* Existing entry */ if ((de = dvr_entry_find_by_event(e))) { tvhtrace("dvr", - "dvr entry %d event replaced %s on %s @ %"PRItime_t + "dvr entry %s event replaced %s on %s @ %"PRItime_t " to %"PRItime_t, - de->de_id, epg_broadcast_get_title(e, NULL), + idnode_uuid_as_str(&de->de_id), + epg_broadcast_get_title(e, NULL), channel_get_name(e->channel), e->start, e->stop); @@ -981,9 +871,10 @@ void dvr_event_updated ( epg_broadcast_t *e ) if (de->de_channel != e->channel) continue; if (dvr_entry_fuzzy_match(de, e)) { tvhtrace("dvr", - "dvr entry %d link to event %s on %s @ %"PRItime_t + "dvr entry %s link to event %s on %s @ %"PRItime_t " to %"PRItime_t, - de->de_id, epg_broadcast_get_title(e, NULL), + idnode_uuid_as_str(&de->de_id), + epg_broadcast_get_title(e, NULL), channel_get_name(e->channel), e->start, e->stop); e->getref(e); @@ -1001,7 +892,7 @@ void dvr_event_updated ( epg_broadcast_t *e ) static void dvr_stop_recording(dvr_entry_t *de, int stopcode, int delconf) { - dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); + dvr_config_t *cfg = de->de_config; if (de->de_rec_state == DVR_RS_PENDING || de->de_rec_state == DVR_RS_WAIT_PROGRAM_START) de->de_sched_state = DVR_MISSED_TIME; @@ -1067,7 +958,7 @@ dvr_entry_find_by_id(int id) { dvr_entry_t *de; LIST_FOREACH(de, &dvrentries, de_global_link) - if(de->de_id == id) + if(idnode_get_short_uuid(&de->de_id) == id) break; return de; } @@ -1147,200 +1038,721 @@ dvr_entry_purge(dvr_entry_t *de, int delconf) dvr_stop_recording(de, SM_CODE_SOURCE_DELETED, delconf); } -/** - * - */ -void -dvr_destroy_by_channel(channel_t *ch, int delconf) + +/* ************************************************************************** + * DVR Entry Class definition + * **************************************************************************/ + +static void +dvr_entry_class_save(idnode_t *self) { - dvr_entry_t *de; + dvr_entry_save((dvr_entry_t *)self); +} - while((de = LIST_FIRST(&ch->ch_dvrs)) != NULL) { - LIST_REMOVE(de, de_channel_link); - de->de_channel = NULL; - de->de_channel_name = strdup(channel_get_name(ch)); - dvr_entry_purge(de, delconf); - } +static void +dvr_entry_class_delete(idnode_t *self) +{ + dvr_entry_remove((dvr_entry_t *)self, 1); } -/** - * - */ -void -dvr_init(void) +static const char * +dvr_entry_class_get_title (idnode_t *self) { - htsmsg_t *m, *l; - htsmsg_field_t *f; + dvr_entry_t *de = (dvr_entry_t *)self; const char *s; - char buf[500]; - const char *homedir; - struct stat st; - uint32_t u32; - dvr_config_t *cfg; - - dvr_iov_max = sysconf(_SC_IOV_MAX); - - /* Default settings */ + s = lang_str_get(de->de_title, NULL); + if (s == NULL || s[0] == '\0') + s = lang_str_get(de->de_desc, NULL); + return s; +} - LIST_INIT(&dvrconfigs); - cfg = dvr_config_create(""); +static int +dvr_entry_class_config_name_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + const char *s = v ?: ""; + if (strcmp(s, de->de_config_name ?: "")) { + free(de->de_config_name); + de->de_config_name = strdup(v); + de->de_config = dvr_config_find_by_name_default(de->de_config_name); + return 1; + } + /* for sure */ + de->de_config = dvr_config_find_by_name_default(de->de_config_name); + return 0; +} - /* Override settings with config */ +static int +dvr_entry_class_channel_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + channel_t *ch = v ? channel_find_by_uuid(v) : NULL; + if (!de->de_config_name) + dvr_entry_class_config_name_set(o, ""); + if (ch == NULL) { + if (de->de_channel) { + LIST_REMOVE(de, de_channel_link); + de->de_channel = NULL; + return 1; + } + } else if (de->de_channel != ch) { + if (de->de_channel) + LIST_REMOVE(de, de_channel_link); + free(de->de_channel_name); + de->de_channel_name = strdup(channel_get_name(ch)); + de->de_channel = ch; + LIST_INSERT_HEAD(&ch->ch_dvrs, de, de_channel_link); + return 1; + } + return 0; +} - l = hts_settings_load("dvr"); - if(l != NULL) { - HTSMSG_FOREACH(f, l) { - m = htsmsg_get_map_by_field(f); - if(m == NULL) - continue; - - s = htsmsg_get_str(m, "config_name"); - cfg = dvr_config_find_by_name(s); - if(cfg == NULL) - cfg = dvr_config_create(s); - - cfg->dvr_mc = htsmsg_get_u32_or_default(m, "container", MC_MATROSKA); - cfg->dvr_muxcnf.m_cache - = htsmsg_get_u32_or_default(m, "cache", MC_CACHE_DONTKEEP); - - if(!htsmsg_get_u32(m, "rewrite-pat", &u32)) { - if (u32) - cfg->dvr_muxcnf.m_flags |= MC_REWRITE_PAT; - else - cfg->dvr_muxcnf.m_flags &= ~MC_REWRITE_PAT; - } - if(!htsmsg_get_u32(m, "rewrite-pmt", &u32)) { - if (u32) - cfg->dvr_muxcnf.m_flags |= MC_REWRITE_PMT; - else - cfg->dvr_muxcnf.m_flags &= ~MC_REWRITE_PMT; - } +static const void * +dvr_entry_class_channel_get(void *o) +{ + static const char *ret; + dvr_entry_t *de = (dvr_entry_t *)o; + if (de->de_channel) + ret = idnode_uuid_as_str(&de->de_channel->ch_id); + else + ret = ""; + return &ret; +} - htsmsg_get_s32(m, "pre-extra-time", &cfg->dvr_extra_time_pre); - htsmsg_get_s32(m, "post-extra-time", &cfg->dvr_extra_time_post); - htsmsg_get_u32(m, "retention-days", &cfg->dvr_retention_days); - tvh_str_set(&cfg->dvr_storage, htsmsg_get_str(m, "storage")); +static htsmsg_t * +dvr_entry_class_channel_list(void *o) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_str(m, "type", "api"); + htsmsg_add_str(m, "uri", "channel/list"); + htsmsg_add_str(m, "event", "channel"); + return m; +} -/* - * Convert 0xxx format permission strings to integer for internal use - * Note no checking that strtol won't overflow int - this should never happen with three-digit numbers - */ +static int +dvr_entry_class_channel_name_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + channel_t *ch; + if (!de->de_config_name) + dvr_entry_class_config_name_set(o, ""); + if (!strcmp(de->de_channel_name ?: "", v ?: "")) + return 0; + ch = v ? channel_find_by_name(v) : NULL; + if (ch) { + return dvr_entry_class_channel_set(o, idnode_uuid_as_str(&ch->ch_id)); + } else { + free(de->de_channel_name); + de->de_channel_name = strdup(v); + return 1; + } +} - if ((s = htsmsg_get_str(m, "file-permissions"))) - cfg->dvr_muxcnf.m_file_permissions = (int)strtol(s,NULL,0); - - if ((s = htsmsg_get_str(m, "directory-permissions"))) - cfg->dvr_muxcnf.m_directory_permissions = (int)strtol(s,NULL,0); - - if(!htsmsg_get_u32(m, "day-dir", &u32) && u32) - cfg->dvr_flags |= DVR_DIR_PER_DAY; - - if(!htsmsg_get_u32(m, "channel-dir", &u32) && u32) - cfg->dvr_flags |= DVR_DIR_PER_CHANNEL; - - if(!htsmsg_get_u32(m, "channel-in-title", &u32) && u32) - cfg->dvr_flags |= DVR_CHANNEL_IN_TITLE; - - if(!htsmsg_get_u32(m, "date-in-title", &u32) && u32) - cfg->dvr_flags |= DVR_DATE_IN_TITLE; - - if(!htsmsg_get_u32(m, "time-in-title", &u32) && u32) - cfg->dvr_flags |= DVR_TIME_IN_TITLE; - - if(!htsmsg_get_u32(m, "whitespace-in-title", &u32) && u32) - cfg->dvr_flags |= DVR_WHITESPACE_IN_TITLE; - - if(!htsmsg_get_u32(m, "title-dir", &u32) && u32) - cfg->dvr_flags |= DVR_DIR_PER_TITLE; +static const void * +dvr_entry_class_channel_name_get(void *o) +{ + static const char *ret; + dvr_entry_t *de = (dvr_entry_t *)o; + if (de->de_channel) + ret = channel_get_name(de->de_channel); + else + ret = de->de_channel_name; + return &ret; +} + +htsmsg_t * +dvr_entry_class_pri_list ( void *o ) +{ + static const struct strtab tab[] = { + { "Not set", -1 }, + { "Important", DVR_PRIO_IMPORTANT }, + { "High", DVR_PRIO_HIGH, }, + { "Normal", DVR_PRIO_NORMAL }, + { "Low", DVR_PRIO_LOW }, + { "Unimportant", DVR_PRIO_UNIMPORTANT }, + }; + return strtab2htsmsg(tab); +} + +static htsmsg_t * +dvr_entry_class_mc_list ( void *o ) +{ + static const struct strtab tab[] = { + { "Not set", -1 }, + { "Matroska (mkv)", MC_MATROSKA, }, + { "Same as source (pass through)", MC_PASS, }, +#if ENABLE_LIBAV + { "MPEG-TS", MC_MPEGTS }, + { "MPEG-PS (DVD)", MC_MPEGPS }, +#endif + }; + return strtab2htsmsg(tab); +} - if(!htsmsg_get_u32(m, "episode-in-title", &u32) && u32) - cfg->dvr_flags |= DVR_EPISODE_IN_TITLE; +htsmsg_t * +dvr_entry_class_config_name_list(void *o) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_t *p = htsmsg_create_map(); + htsmsg_add_str(m, "type", "api"); + htsmsg_add_str(m, "uri", "idnode/load"); + htsmsg_add_str(m, "event", "dvrconfig"); + htsmsg_add_u32(p, "enum", 1); + htsmsg_add_str(p, "class", dvr_config_class.ic_class); + htsmsg_add_msg(m, "params", p); + return m; +} - if(!htsmsg_get_u32(m, "clean-title", &u32) && u32) - cfg->dvr_flags |= DVR_CLEAN_TITLE; - if(!htsmsg_get_u32(m, "tag-files", &u32) && !u32) - cfg->dvr_flags &= ~DVR_TAG_FILES; +static int +dvr_entry_class_autorec_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + dvr_autorec_entry_t *dae = v ? dvr_autorec_find_by_uuid(v) : NULL; + if (dae == NULL) { + if (de->de_autorec) { + LIST_REMOVE(de, de_autorec_link); + de->de_autorec = NULL; + return 1; + } + } else if (de->de_autorec != dae) { + de->de_autorec = dae; + LIST_INSERT_HEAD(&dae->dae_spawns, de, de_autorec_link); + return 1; + } + return 0; +} - if(!htsmsg_get_u32(m, "skip-commercials", &u32) && !u32) - cfg->dvr_flags &= ~DVR_SKIP_COMMERCIALS; +static const void * +dvr_entry_class_autorec_get(void *o) +{ + static const char *ret; + dvr_entry_t *de = (dvr_entry_t *)o; + if (de->de_autorec) + ret = idnode_uuid_as_str(&de->de_autorec->dae_id); + else + ret = ""; + return &ret; +} + +static char * +dvr_entry_class_autorec_rend(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + const char *s = ""; + if (de->de_autorec) { + if (de->de_autorec->dae_name != NULL && + de->de_autorec->dae_name[0] != '\0') + s = de->de_autorec->dae_name; + else if (de->de_autorec->dae_comment != NULL && + de->de_autorec->dae_comment[0] != '\0') + s = de->de_autorec->dae_comment; + else + s = de->de_autorec->dae_title; + } + return strdup(s); +} - if(!htsmsg_get_u32(m, "subtitle-in-title", &u32) && u32) - cfg->dvr_flags |= DVR_SUBTITLE_IN_TITLE; +static int +dvr_entry_class_broadcast_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + uint32_t id = *(uint32_t *)v; + epg_broadcast_t *bcast = epg_broadcast_find_by_id(id, de->de_channel); + if (bcast == NULL) { + if (de->de_bcast) { + de->de_bcast->putref((epg_object_t*)de->de_bcast); + de->de_bcast = NULL; + return 1; + } + } else if (de->de_bcast != bcast) { + if (de->de_bcast) + de->de_bcast->putref((epg_object_t*)de->de_bcast); + de->de_bcast = bcast; + de->de_bcast->getref((epg_object_t*)bcast); + return 1; + } + return 0; +} - if(!htsmsg_get_u32(m, "episode-before-date", &u32) && u32) - cfg->dvr_flags |= DVR_EPISODE_BEFORE_DATE; +static const void * +dvr_entry_class_broadcast_get(void *o) +{ + static uint32_t id; + dvr_entry_t *de = (dvr_entry_t *)o; + if (de->de_bcast) + id = de->de_bcast->id; + else + id = 0; + return &id; +} - if(!htsmsg_get_u32(m, "episode-duplicate-detection", &u32) && u32) - cfg->dvr_flags |= DVR_EPISODE_DUPLICATE_DETECTION; +static char * +dvr_entry_class_broadcast_rend(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + const char *s = ""; + if (de->de_bcast) + s = lang_str_get(de->de_bcast->summary, NULL); + return strdup(s); +} - dvr_charset_update(cfg, htsmsg_get_str(m, "charset")); +static int +dvr_entry_class_disp_title_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + const char *s = ""; + if (de->de_title) + s = lang_str_get(de->de_title, NULL); + if (strcmp(s, v ?: "")) { + lang_str_destroy(de->de_title); + de->de_title = lang_str_create(); + if (v) + lang_str_add(de->de_title, v, NULL, 0); + return 1; + } + return 0; +} - tvh_str_set(&cfg->dvr_postproc, htsmsg_get_str(m, "postproc")); - } +static const void * +dvr_entry_class_disp_title_get(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + static const char *s; + s = ""; + if (de->de_title) { + s = lang_str_get(de->de_title, NULL); + if (s == NULL) + s = ""; + } + return &s; +} - htsmsg_destroy(l); +static const void * +dvr_entry_class_disp_description_get(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + static const char *s; + s = ""; + if (de->de_title) { + s = lang_str_get(de->de_desc, NULL); + if (s == NULL) + s = ""; + } + return &s; +} + +static const void * +dvr_entry_class_episode_get(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + static const char *s; + static char buf[100]; + s = ""; + if (de->de_bcast && de->de_bcast->episode) + if (epg_episode_number_format(de->de_bcast->episode, + buf, sizeof(buf), NULL, + "Season %d", ".", "Episode %d", "/%d")) + s = buf; + return &s; +} + +static const void * +dvr_entry_class_url_get(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + static const char *s; + static char buf[100]; + s = ""; + if (de->de_sched_state == DVR_COMPLETED) { + snprintf(buf, sizeof(buf), "dvrfile/%s", idnode_uuid_as_str(&de->de_id)); + s = buf; } + return &s; +} - LIST_FOREACH(cfg, &dvrconfigs, config_link) { - if(cfg->dvr_storage == NULL || !strlen(cfg->dvr_storage)) { - /* Try to figure out a good place to put them videos */ +static const void * +dvr_entry_class_filesize_get(void *o) +{ + static int64_t size; + dvr_entry_t *de = (dvr_entry_t *)o; + if (de->de_sched_state == DVR_COMPLETED) + size = dvr_get_filesize(de); + else + size = 0; + return &size; +} - homedir = getenv("HOME"); +static const void * +dvr_entry_class_start_real_get(void *o) +{ + static time_t tm; + dvr_entry_t *de = (dvr_entry_t *)o; + tm = dvr_entry_get_start_time(de); + return &tm; +} - if(homedir != NULL) { - snprintf(buf, sizeof(buf), "%s/Videos", homedir); - if(stat(buf, &st) == 0 && S_ISDIR(st.st_mode)) - cfg->dvr_storage = strdup(buf); - - else if(stat(homedir, &st) == 0 && S_ISDIR(st.st_mode)) - cfg->dvr_storage = strdup(homedir); - else - cfg->dvr_storage = strdup(getcwd(buf, sizeof(buf))); - } +static const void * +dvr_entry_class_stop_real_get(void *o) +{ + static time_t tm; + dvr_entry_t *de = (dvr_entry_t *)o; + tm = dvr_entry_get_stop_time(de); + return &tm; +} - tvhlog(LOG_WARNING, "dvr", - "Output directory for video recording is not yet configured " - "for DVR configuration \"%s\". " - "Defaulting to to \"%s\". " - "This can be changed from the web user interface.", - cfg->dvr_config_name, cfg->dvr_storage); - } +static const void * +dvr_entry_class_duration_get(void *o) +{ + static time_t tm; + time_t start, stop; + dvr_entry_t *de = (dvr_entry_t *)o; + start = dvr_entry_get_start_time(de); + stop = dvr_entry_get_stop_time(de); + if (stop > start) + tm = stop - start; + else + tm = 0; + return &tm; +} + +static const void * +dvr_entry_class_status_get(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + static const char *s; + static char buf[100]; + strncpy(buf, dvr_entry_status(de), sizeof(buf)); + buf[sizeof(buf)-1] = '\0'; + s = buf; + return &s; +} + +static const void * +dvr_entry_class_sched_status_get(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + static const char *s; + static char buf[100]; + strncpy(buf, dvr_entry_schedstatus(de), sizeof(buf)); + buf[sizeof(buf)-1] = '\0'; + s = buf; + return &s; +} + +static const void * +dvr_entry_class_channel_icon_url_get(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + channel_t *ch = de->de_channel; + static const char *s; + static char buf[256]; + uint32_t id; + if (ch == NULL) { + s = ""; + } else if ((id = imagecache_get_id(ch->ch_icon)) != 0) { + snprintf(buf, sizeof(buf), "imagecache/%d", id); + } else { + strncpy(buf, ch->ch_icon ?: "", sizeof(buf)); + buf[strlen(buf)-1] = '\0'; } - -#if ENABLE_INOTIFY - dvr_inotify_init(); -#endif - dvr_autorec_init(); - dvr_db_load(); - dvr_autorec_update(); + s = buf; + return &s; +} + +static htsmsg_t * +dvr_entry_class_extra_list(void *o) +{ + int i; + htsmsg_t *e, *l = htsmsg_create_list(); + char buf[32]; + e = htsmsg_create_map(); + htsmsg_add_u32(e, "key", 0); + htsmsg_add_str(e, "val", "Not set (use channel or DVR config)"); + htsmsg_add_msg(l, NULL, e); + for (i = 1; i <= 120; i++) { + snprintf(buf, sizeof(buf), "%d min%s", i, i > 1 ? "s" : ""); + e = htsmsg_create_map(); + htsmsg_add_u32(e, "key", i); + htsmsg_add_str(e, "val", buf); + htsmsg_add_msg(l, NULL, e); + } + for (i = 120; i <= 240; i += 30) { + if ((i % 60) == 0) + snprintf(buf, sizeof(buf), "%d hrs", i / 60); + else + snprintf(buf, sizeof(buf), "%d hrs %d min%s", i / 60, i % 60, (i % 60) > 0 ? "s" : ""); + e = htsmsg_create_map(); + htsmsg_add_u32(e, "key", i); + htsmsg_add_str(e, "val", buf); + htsmsg_add_msg(l, NULL, e); + } + return l; } + + +static htsmsg_t * +dvr_entry_class_content_type_list(void *o) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_str(m, "type", "api"); + htsmsg_add_str(m, "uri", "epg/content_type/list"); + return m; +} + +const idclass_t dvr_entry_class = { + .ic_class = "dvrentry", + .ic_caption = "DVR Entry", + .ic_save = dvr_entry_class_save, + .ic_get_title = dvr_entry_class_get_title, + .ic_delete = dvr_entry_class_delete, + .ic_properties = (const property_t[]) { + { + .type = PT_TIME, + .id = "start", + .name = "Start Time", + .off = offsetof(dvr_entry_t, de_start), + }, + { + .type = PT_TIME, + .id = "start_extra", + .name = "Extra Start Time", + .off = offsetof(dvr_entry_t, de_start_extra), + .list = dvr_entry_class_extra_list, + .opts = PO_DURATION, + }, + { + .type = PT_TIME, + .id = "start_real", + .name = "Scheduled Start Time", + .get = dvr_entry_class_start_real_get, + .opts = PO_RDONLY | PO_NOSAVE, + }, + { + .type = PT_TIME, + .id = "stop", + .name = "Stop Time", + .off = offsetof(dvr_entry_t, de_stop), + }, + { + .type = PT_TIME, + .id = "stop_extra", + .name = "Extra Stop Time", + .off = offsetof(dvr_entry_t, de_stop_extra), + .list = dvr_entry_class_extra_list, + .opts = PO_DURATION, + }, + { + .type = PT_TIME, + .id = "stop_real", + .name = "Scheduled Stop Time", + .get = dvr_entry_class_stop_real_get, + .opts = PO_RDONLY | PO_NOSAVE, + }, + { + .type = PT_TIME, + .id = "duration", + .name = "Duration", + .get = dvr_entry_class_duration_get, + .opts = PO_RDONLY | PO_NOSAVE | PO_DURATION, + }, + { + .type = PT_STR, + .id = "channel", + .name = "Channel", + .set = dvr_entry_class_channel_set, + .get = dvr_entry_class_channel_get, + .list = dvr_entry_class_channel_list, + }, + { + .type = PT_TIME, + .id = "channel_icon", + .name = "Channel Icon", + .get = dvr_entry_class_channel_icon_url_get, + .opts = PO_HIDDEN | PO_RDONLY | PO_NOSAVE, + }, + { + .type = PT_STR, + .id = "channelname", + .name = "Channel Name", + .get = dvr_entry_class_channel_name_get, + .set = dvr_entry_class_channel_name_set, + .off = offsetof(dvr_entry_t, de_channel_name), + }, + { + .type = PT_LANGSTR, + .id = "title", + .name = "Title", + .off = offsetof(dvr_entry_t, de_title), + .opts = PO_RDONLY, + }, + { + .type = PT_STR, + .id = "disp_title", + .name = "Title", + .get = dvr_entry_class_disp_title_get, + .set = dvr_entry_class_disp_title_set, + .opts = PO_NOSAVE, + }, + { + .type = PT_LANGSTR, + .id = "description", + .name = "Description", + .off = offsetof(dvr_entry_t, de_desc), + .opts = PO_RDONLY, + }, + { + .type = PT_STR, + .id = "disp_description", + .name = "Description", + .get = dvr_entry_class_disp_description_get, + .opts = PO_RDONLY | PO_NOSAVE | PO_HIDDEN, + }, + { + .type = PT_INT, + .id = "pri", + .name = "Priority", + .off = offsetof(dvr_entry_t, de_pri), + .def.i = DVR_PRIO_NORMAL, + .list = dvr_entry_class_pri_list, + }, + { + .type = PT_INT, + .id = "container", + .name = "Container", + .off = offsetof(dvr_entry_t, de_mc), + .def.i = MC_MATROSKA, + .list = dvr_entry_class_mc_list, + }, + { + .type = PT_STR, + .id = "config_name", + .name = "DVR Configuration", + .set = dvr_entry_class_config_name_set, + .list = dvr_entry_class_config_name_list, + .off = offsetof(dvr_entry_t, de_config_name), + }, + { + .type = PT_STR, + .id = "creator", + .name = "Creator", + .off = offsetof(dvr_entry_t, de_creator), + .opts = PO_RDONLY, + }, + { + .type = PT_STR, + .id = "filename", + .name = "Filename", + .off = offsetof(dvr_entry_t, de_filename), + .opts = PO_RDONLY, + }, + { + .type = PT_U32, + .id = "errorcode", + .name = "Error Code", + .off = offsetof(dvr_entry_t, de_last_error), + .opts = PO_RDONLY, + }, + { + .type = PT_U32, + .id = "errors", + .name = "Errors", + .off = offsetof(dvr_entry_t, de_errors), + .opts = PO_RDONLY, + }, + { + .type = PT_U16, + .id = "dvb_eid", + .name = "DVB EPG ID", + .off = offsetof(dvr_entry_t, de_dvb_eid), + .opts = PO_RDONLY, + }, + { + .type = PT_BOOL, + .id = "noresched", + .name = "Do Not Reschedule", + .off = offsetof(dvr_entry_t, de_dvb_eid), + .opts = PO_RDONLY, + }, + { + .type = PT_STR, + .id = "autorec", + .name = "Auto Record", + .set = dvr_entry_class_autorec_set, + .get = dvr_entry_class_autorec_get, + .rend = dvr_entry_class_autorec_rend, + .opts = PO_RDONLY, + }, + { + .type = PT_U32, + .id = "content_type", + .name = "Content Type", + .list = dvr_entry_class_content_type_list, + .off = offsetof(dvr_entry_t, de_content_type), + .opts = PO_RDONLY, + }, + { + .type = PT_U32, + .id = "broadcast", + .name = "Broadcast Type", + .set = dvr_entry_class_broadcast_set, + .get = dvr_entry_class_broadcast_get, + .rend = dvr_entry_class_broadcast_rend, + .opts = PO_RDONLY, + }, + { + .type = PT_STR, + .id = "episode", + .name = "Episode", + .get = dvr_entry_class_episode_get, + .opts = PO_RDONLY | PO_NOSAVE, + }, + { + .type = PT_STR, + .id = "url", + .name = "URL", + .get = dvr_entry_class_url_get, + .opts = PO_RDONLY | PO_NOSAVE, + }, + { + .type = PT_S64, + .id = "filesize", + .name = "File Size", + .get = dvr_entry_class_filesize_get, + .opts = PO_RDONLY | PO_NOSAVE, + }, + { + .type = PT_STR, + .id = "status", + .name = "Status", + .get = dvr_entry_class_status_get, + .opts = PO_RDONLY | PO_NOSAVE, + }, + { + .type = PT_STR, + .id = "sched_status", + .name = "Schedule Status", + .get = dvr_entry_class_sched_status_get, + .opts = PO_RDONLY | PO_NOSAVE | PO_HIDDEN, + }, + {} + } +}; /** * */ void -dvr_done(void) +dvr_destroy_by_channel(channel_t *ch, int delconf) { - dvr_config_t *cfg; dvr_entry_t *de; -#if ENABLE_INOTIFY - dvr_inotify_done(); -#endif - pthread_mutex_lock(&global_lock); - while ((cfg = LIST_FIRST(&dvrconfigs)) != NULL) { - LIST_REMOVE(cfg, config_link); - free(cfg->dvr_charset_id); - free(cfg->dvr_charset); - free(cfg->dvr_storage); - free(cfg->dvr_config_name); - free(cfg); + while((de = LIST_FIRST(&ch->ch_dvrs)) != NULL) { + LIST_REMOVE(de, de_channel_link); + de->de_channel = NULL; + de->de_channel_name = strdup(channel_get_name(ch)); + dvr_entry_purge(de, delconf); } - while ((de = LIST_FIRST(&dvrentries)) != NULL) - dvr_entry_remove(de, 0); - pthread_mutex_unlock(&global_lock); - dvr_autorec_done(); } /** @@ -1372,12 +1784,17 @@ dvr_config_find_by_name_default(const char *name) cfg = dvr_config_find_by_name(name); if (cfg == NULL) { - tvhlog(LOG_WARNING, "dvr", "Configuration '%s' not found", name); + if (name && name[0]) + tvhlog(LOG_WARNING, "dvr", "Configuration '%s' not found", name); + cfg = dvr_config_find_by_name(""); + } else if (!cfg->dvr_enabled) { + tvhlog(LOG_WARNING, "dvr", "Configuration '%s' not enabled", name); cfg = dvr_config_find_by_name(""); } if (cfg == NULL) { - cfg = dvr_config_create(""); + cfg = dvr_config_create("", NULL, NULL); + dvr_config_save(cfg); } return cfg; @@ -1388,17 +1805,25 @@ dvr_config_find_by_name_default(const char *name) * to avoid duplicates */ dvr_config_t * -dvr_config_create(const char *name) +dvr_config_create(const char *name, const char *uuid, htsmsg_t *conf) { dvr_config_t *cfg; if (name == NULL) name = ""; - tvhlog(LOG_INFO, "dvr", "Creating new configuration '%s'", name); - cfg = calloc(1, sizeof(dvr_config_t)); - cfg->dvr_config_name = strdup(name); + + if (idnode_insert(&cfg->dvr_id, uuid, &dvr_config_class, 0)) { + if (uuid) + tvherror("dvr", "invalid config uuid '%s'", uuid); + free(cfg); + return NULL; + } + + cfg->dvr_enabled = 1; + if (name) + cfg->dvr_config_name = strdup(name); cfg->dvr_retention_days = 31; cfg->dvr_mc = MC_MATROSKA; cfg->dvr_flags = DVR_TAG_FILES | DVR_SKIP_COMMERCIALS; @@ -1423,276 +1848,416 @@ dvr_config_create(const char *name) cfg->dvr_muxcnf.m_file_permissions = 0664; cfg->dvr_muxcnf.m_directory_permissions = 0775; + + if (conf) + idnode_load(&cfg->dvr_id, conf); + tvhlog(LOG_INFO, "dvr", "Creating new configuration '%s'", cfg->dvr_config_name); + LIST_INSERT_HEAD(&dvrconfigs, cfg, config_link); - return LIST_FIRST(&dvrconfigs); + return cfg; } /** - * + * destroy a dvr config */ -void -dvr_config_delete(const char *name) +static void +dvr_config_destroy(dvr_config_t *cfg, int delconf) { - dvr_config_t *cfg; - - if (name == NULL || strlen(name) == 0) { - tvhlog(LOG_WARNING,"dvr","Attempt to delete default config ignored"); - return; - } - - cfg = dvr_config_find_by_name(name); - if (cfg != NULL) { + if (delconf) { tvhlog(LOG_INFO, "dvr", "Deleting configuration '%s'", - cfg->dvr_config_name); - hts_settings_remove("dvr/config%s", cfg->dvr_config_name); - LIST_REMOVE(cfg, config_link); - free(cfg->dvr_charset_id); - free(cfg->dvr_charset); - free(cfg->dvr_storage); - free(cfg->dvr_config_name); - free(cfg); - - dvrconfig_changed(); + cfg->dvr_config_name); + hts_settings_remove("dvr/config/%s", idnode_uuid_as_str(&cfg->dvr_id)); } -} - -/** - * - */ -static void -dvr_save(dvr_config_t *cfg) -{ - htsmsg_t *m = htsmsg_create_map(); - char buffer[5]; // Permissions buffer: leading zero, three octal digits plus terminating null - - if (cfg->dvr_config_name != NULL && strlen(cfg->dvr_config_name) != 0) - htsmsg_add_str(m, "config_name", cfg->dvr_config_name); - htsmsg_add_str(m, "storage", cfg->dvr_storage); - -/* Convert permissions to 0xxx octal format and output */ - - snprintf(buffer,sizeof(buffer),"%04o",cfg->dvr_muxcnf.m_file_permissions); - htsmsg_add_str(m, "file-permissions", buffer); - - snprintf(buffer,sizeof(buffer),"%04o",cfg->dvr_muxcnf.m_directory_permissions); - htsmsg_add_str(m, "directory-permissions", buffer); - - htsmsg_add_u32(m, "container", cfg->dvr_mc); - htsmsg_add_u32(m, "cache", cfg->dvr_muxcnf.m_cache); - htsmsg_add_u32(m, "rewrite-pat", - !!(cfg->dvr_muxcnf.m_flags & MC_REWRITE_PAT)); - htsmsg_add_u32(m, "rewrite-pmt", - !!(cfg->dvr_muxcnf.m_flags & MC_REWRITE_PMT)); - htsmsg_add_u32(m, "retention-days", cfg->dvr_retention_days); - htsmsg_add_u32(m, "pre-extra-time", cfg->dvr_extra_time_pre); - htsmsg_add_u32(m, "post-extra-time", cfg->dvr_extra_time_post); - htsmsg_add_u32(m, "day-dir", !!(cfg->dvr_flags & DVR_DIR_PER_DAY)); - htsmsg_add_u32(m, "channel-dir", !!(cfg->dvr_flags & DVR_DIR_PER_CHANNEL)); - htsmsg_add_u32(m, "channel-in-title", !!(cfg->dvr_flags & DVR_CHANNEL_IN_TITLE)); - htsmsg_add_u32(m, "date-in-title", !!(cfg->dvr_flags & DVR_DATE_IN_TITLE)); - htsmsg_add_u32(m, "time-in-title", !!(cfg->dvr_flags & DVR_TIME_IN_TITLE)); - htsmsg_add_u32(m, "whitespace-in-title", !!(cfg->dvr_flags & DVR_WHITESPACE_IN_TITLE)); - htsmsg_add_u32(m, "title-dir", !!(cfg->dvr_flags & DVR_DIR_PER_TITLE)); - htsmsg_add_u32(m, "episode-in-title", !!(cfg->dvr_flags & DVR_EPISODE_IN_TITLE)); - htsmsg_add_u32(m, "clean-title", !!(cfg->dvr_flags & DVR_CLEAN_TITLE)); - htsmsg_add_u32(m, "tag-files", !!(cfg->dvr_flags & DVR_TAG_FILES)); - htsmsg_add_u32(m, "skip-commercials", !!(cfg->dvr_flags & DVR_SKIP_COMMERCIALS)); - htsmsg_add_u32(m, "subtitle-in-title", !!(cfg->dvr_flags & DVR_SUBTITLE_IN_TITLE)); - htsmsg_add_u32(m, "episode-before-date", !!(cfg->dvr_flags & DVR_EPISODE_BEFORE_DATE)); - htsmsg_add_u32(m, "episode-duplicate-detection", !!(cfg->dvr_flags & DVR_EPISODE_DUPLICATE_DETECTION)); - if (cfg->dvr_charset != NULL) - htsmsg_add_str(m, "charset", cfg->dvr_charset); - if (cfg->dvr_postproc != NULL) - htsmsg_add_str(m, "postproc", cfg->dvr_postproc); - - hts_settings_save(m, "dvr/config%s", cfg->dvr_config_name); - htsmsg_destroy(m); - - dvrconfig_changed(); -} - -/** - * - */ -void -dvr_storage_set(dvr_config_t *cfg, const char *storage) -{ - if(cfg->dvr_storage != NULL && !strcmp(cfg->dvr_storage, storage)) - return; - - tvh_str_set(&cfg->dvr_storage, storage); - dvr_save(cfg); + LIST_REMOVE(cfg, config_link); + idnode_unlink(&cfg->dvr_id); + free(cfg->dvr_charset_id); + free(cfg->dvr_charset); + free(cfg->dvr_storage); + free(cfg->dvr_config_name); + free(cfg); } /** * */ void -dvr_charset_set(dvr_config_t *cfg, const char *charset) +dvr_config_delete(const char *name) { - if(cfg->dvr_charset != NULL && !strcmp(cfg->dvr_charset, charset)) - return; - - dvr_charset_update(cfg, charset); - dvr_save(cfg); -} + dvr_config_t *cfg; -/** - * - */ -void -dvr_file_permissions_set(dvr_config_t *cfg, int permissions) -{ - if(cfg->dvr_muxcnf.m_file_permissions == permissions) + if (name == NULL || strlen(name) == 0) { + tvhlog(LOG_WARNING,"dvr","Attempt to delete default config ignored"); return; + } - cfg->dvr_muxcnf.m_file_permissions = permissions; - dvr_save(cfg); + cfg = dvr_config_find_by_name(name); + if (cfg != NULL) + dvr_config_destroy(cfg, 1); } -/** +/* * */ -void -dvr_directory_permissions_set(dvr_config_t *cfg, int permissions) -{ - if(cfg->dvr_muxcnf.m_directory_permissions == permissions) - return; - - cfg->dvr_muxcnf.m_directory_permissions = permissions; - dvr_save(cfg); +static void +dvr_config_update_flags(dvr_config_t *cfg) +{ + int r = 0; + if (cfg->dvr_dir_per_day) r |= DVR_DIR_PER_DAY; + if (cfg->dvr_channel_dir) r |= DVR_DIR_PER_CHANNEL; + if (cfg->dvr_channel_in_title) r |= DVR_CHANNEL_IN_TITLE; + if (cfg->dvr_date_in_title) r |= DVR_DATE_IN_TITLE; + if (cfg->dvr_time_in_title) r |= DVR_TIME_IN_TITLE; + if (cfg->dvr_whitespace_in_title) r |= DVR_WHITESPACE_IN_TITLE; + if (cfg->dvr_title_dir) r |= DVR_DIR_PER_TITLE; + if (cfg->dvr_episode_in_title) r |= DVR_EPISODE_IN_TITLE; + if (cfg->dvr_clean_title) r |= DVR_CLEAN_TITLE; + if (cfg->dvr_tag_files) r |= DVR_TAG_FILES; + if (cfg->dvr_skip_commercials) r |= DVR_SKIP_COMMERCIALS; + if (cfg->dvr_subtitle_in_title) r |= DVR_SUBTITLE_IN_TITLE; + if (cfg->dvr_episode_before_date) r |= DVR_EPISODE_BEFORE_DATE; + if (cfg->dvr_episode_duplicate) r |= DVR_EPISODE_DUPLICATE_DETECTION; + cfg->dvr_flags = r; + r = 0; + if (cfg->dvr_rewrite_pat) r |= MC_REWRITE_PAT; + if (cfg->dvr_rewrite_pmt) r |= MC_REWRITE_PMT; + cfg->dvr_muxcnf.m_flags = r; } -/** - * - */ -void -dvr_container_set(dvr_config_t *cfg, const char *container) -{ - muxer_container_type_t mc; - - mc = muxer_container_txt2type(container); - if(mc == MC_UNKNOWN) - mc = MC_MATROSKA; - - if(cfg->dvr_mc == mc) - return; - - cfg->dvr_mc = mc; - dvr_save(cfg); -} - -/** +/* * */ void -dvr_mux_cache_set(dvr_config_t *cfg, int mcache) +dvr_config_save(dvr_config_t *cfg) { - if (mcache < MC_CACHE_UNKNOWN || mcache > MC_CACHE_LAST) - mcache = MC_CACHE_UNKNOWN; - - if(cfg->dvr_muxcnf.m_cache == mcache) - return; + htsmsg_t *m = htsmsg_create_map(); - cfg->dvr_muxcnf.m_cache = mcache; + lock_assert(&global_lock); - dvr_save(cfg); + idnode_save(&cfg->dvr_id, m); + hts_settings_save(m, "dvr/config/%s", idnode_uuid_as_str(&cfg->dvr_id)); + htsmsg_destroy(m); } +/* ************************************************************************** + * DVR Config Class definition + * **************************************************************************/ -/** - * - */ -void -dvr_postproc_set(dvr_config_t *cfg, const char *postproc) +static void +dvr_config_class_save(idnode_t *self) { - if(cfg->dvr_postproc != NULL && !strcmp(cfg->dvr_postproc, postproc)) - return; - - tvh_str_set(&cfg->dvr_postproc, !strcmp(postproc, "") ? NULL : postproc); - dvr_save(cfg); + dvr_config_update_flags((dvr_config_t *)self); + dvr_config_save((dvr_config_t *)self); } - -/** - * - */ -void -dvr_retention_set(dvr_config_t *cfg, int days) +static void +dvr_config_class_delete(idnode_t *self) { - dvr_entry_t *de; - if(days < 1 || cfg->dvr_retention_days == days) - return; - - cfg->dvr_retention_days = days; - - /* Also, rearm all timers */ - - LIST_FOREACH(de, &dvrentries, de_global_link) - if(de->de_sched_state == DVR_COMPLETED) - gtimer_arm_abs(&de->de_timer, dvr_timer_expire, de, - de->de_stop + cfg->dvr_retention_days * 86400); - dvr_save(cfg); + dvr_config_destroy((dvr_config_t *)self, 1); } - -/** - * - */ -void -dvr_flags_set(dvr_config_t *cfg, int flags) +static int +dvr_config_class_perm(idnode_t *self, access_t *a, htsmsg_t *msg_to_write) { - if(cfg->dvr_flags == flags) - return; - - cfg->dvr_flags = flags; - dvr_save(cfg); + dvr_config_t *cfg = (dvr_config_t *)self; + if (access_verify2(a, ACCESS_RECORDER)) + return -1; + if (access_verify2(a, ACCESS_ADMIN) && access_verify2(a, ACCESS_RECORDER_ALL)) + return 0; + if (strcmp(cfg->dvr_config_name ?: "", a->aa_username ?: "")) + return -1; + return 0; } -/** - * - */ -void -dvr_mux_flags_set(dvr_config_t *cfg, int flags) +static const char * +dvr_config_class_get_title (idnode_t *self) { - if(cfg->dvr_muxcnf.m_flags == flags) - return; - - cfg->dvr_muxcnf.m_flags = flags; - dvr_save(cfg); + dvr_config_t *cfg = (dvr_config_t *)self; + if (cfg->dvr_config_name && cfg->dvr_config_name[0] != '\0') + return cfg->dvr_config_name; + return "(Default Profile)"; } - -/** - * - */ -void -dvr_extra_time_pre_set(dvr_config_t *cfg, int d) +static int +dvr_config_class_charset_set(void *o, const void *v) { - if(cfg->dvr_extra_time_pre == d) - return; - - cfg->dvr_extra_time_pre = d; - dvr_save(cfg); + dvr_config_t *cfg = (dvr_config_t *)o; + return dvr_charset_update(cfg, v); } - -/** - * - */ -void -dvr_extra_time_post_set(dvr_config_t *cfg, int d) +static htsmsg_t * +dvr_config_class_charset_list(void *o) { - if(cfg->dvr_extra_time_post == d) - return; - - cfg->dvr_extra_time_post = d; - dvr_save(cfg); -} - + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_str(m, "type", "api"); + htsmsg_add_str(m, "uri", "intlconv/charsets"); + htsmsg_add_str(m, "event", "charsets"); + return m; +} + +static htsmsg_t * +dvr_config_class_cache_list(void *o) +{ + static struct strtab tab[] = { + { "Unknown", MC_CACHE_UNKNOWN }, + { "System", MC_CACHE_SYSTEM }, + { "Do not keep", MC_CACHE_DONTKEEP }, + { "Sync", MC_CACHE_SYNC }, + { "Sync + Do not keep", MC_CACHE_SYNCDONTKEEP } + }; + return strtab2htsmsg(tab); +} + +const idclass_t dvr_config_class = { + .ic_class = "dvrconfig", + .ic_caption = "DVR Configuration Profile", + .ic_save = dvr_config_class_save, + .ic_get_title = dvr_config_class_get_title, + .ic_delete = dvr_config_class_delete, + .ic_perm = dvr_config_class_perm, + .ic_groups = (const property_group_t[]) { + { + .name = "DVR Behaviour", + .number = 1, + }, + { + .name = "Recording File Options", + .number = 2, + }, + { + .name = "Subdirectory Options", + .number = 3, + }, + { + .name = "Filename Options", + .number = 4, + .column = 1, + }, + { + .name = "", + .number = 5, + .parent = 4, + .column = 2, + }, + {} + }, + .ic_properties = (const property_t[]){ + { + .type = PT_BOOL, + .id = "enabled", + .name = "Enabled", + .off = offsetof(dvr_config_t, dvr_enabled), + .def.i = 1, + .group = 1, + }, + { + .type = PT_STR, + .id = "name", + .name = "Config Name", + .off = offsetof(dvr_config_t, dvr_config_name), + .def.s = "! New config", + .group = 1, + }, + { + .type = PT_INT, + .id = "container", + .name = "Container", + .off = offsetof(dvr_config_t, dvr_mc), + .def.i = MC_MATROSKA, + .list = dvr_entry_class_mc_list, + .group = 1, + }, + { + .type = PT_INT, + .id = "cache", + .name = "Cache Scheme", + .off = offsetof(dvr_config_t, dvr_muxcnf.m_cache), + .def.i = MC_CACHE_DONTKEEP, + .list = dvr_config_class_cache_list, + .group = 1, + }, + { + .type = PT_U32, + .id = "retention-days", + .name = "DVR Log Retention Time (days)", + .off = offsetof(dvr_config_t, dvr_retention_days), + .def.u32 = 31, + .group = 1, + }, + { + .type = PT_U32, + .id = "pre-extra-time", + .name = "Extra Time Before Recordings (minutes)", + .off = offsetof(dvr_config_t, dvr_extra_time_pre), + .group = 1, + }, + { + .type = PT_U32, + .id = "post-extra-time", + .name = "Extra Time After Recordings (minutes)", + .off = offsetof(dvr_config_t, dvr_extra_time_post), + .group = 1, + }, + { + .type = PT_BOOL, + .id = "episode-duplicate-detection", + .name = "Episode Duplicate Detect", + .off = offsetof(dvr_config_t, dvr_episode_duplicate), + .group = 1, + }, + { + .type = PT_STR, + .id = "postproc", + .name = "Post-Processor Command", + .off = offsetof(dvr_config_t, dvr_postproc), + .group = 1, + }, + { + .type = PT_STR, + .id = "storage", + .name = "Recording System Path", + .off = offsetof(dvr_config_t, dvr_storage), + .group = 2, + }, + { + .type = PT_PERM, + .id = "file-permissions", + .name = "File Permissions (octal, e.g. 0664)", + .off = offsetof(dvr_config_t, dvr_muxcnf.m_file_permissions), + .def.u32 = 0664, + .group = 2, + }, + { + .type = PT_STR, + .id = "charset", + .name = "Filename Charset", + .off = offsetof(dvr_config_t, dvr_charset), + .set = dvr_config_class_charset_set, + .list = dvr_config_class_charset_list, + .def.s = "UTF-8", + .group = 2, + }, + { + .type = PT_BOOL, + .id = "rewrite-pat", + .name = "Rewrite PAT", + .off = offsetof(dvr_config_t, dvr_rewrite_pat), + .def.i = 1, + .group = 2, + }, + { + .type = PT_BOOL, + .id = "rewrite-pmt", + .name = "Rewrite PMT", + .off = offsetof(dvr_config_t, dvr_rewrite_pmt), + .group = 2, + }, + { + .type = PT_BOOL, + .id = "tag-files", + .name = "Tag Files With Metadata", + .off = offsetof(dvr_config_t, dvr_tag_files), + .def.i = 1, + .group = 2, + }, + { + .type = PT_BOOL, + .id = "skip-commercials", + .name = "Skip Commercials", + .off = offsetof(dvr_config_t, dvr_skip_commercials), + .def.i = 1, + .group = 2, + }, + { + .type = PT_PERM, + .id = "directory-permissions", + .name = "Directory Permissions (octal, e.g. 0775)", + .off = offsetof(dvr_config_t, dvr_muxcnf.m_directory_permissions), + .def.u32 = 0775, + .group = 3, + }, + { + .type = PT_BOOL, + .id = "day-dir", + .name = "Make Subdirectories Per Day", + .off = offsetof(dvr_config_t, dvr_dir_per_day), + .group = 3, + }, + { + .type = PT_BOOL, + .id = "channel-dir", + .name = "Make Subdirectories Per Channel", + .off = offsetof(dvr_config_t, dvr_channel_dir), + .group = 3, + }, + { + .type = PT_BOOL, + .id = "title-dir", + .name = "Make Subdirectories Per Title", + .off = offsetof(dvr_config_t, dvr_title_dir), + .group = 3, + }, + { + .type = PT_BOOL, + .id = "channel-in-title", + .name = "Include Channel Name In Filename", + .off = offsetof(dvr_config_t, dvr_channel_in_title), + .group = 4, + }, + { + .type = PT_BOOL, + .id = "date-in-title", + .name = "Include Date In Filename", + .off = offsetof(dvr_config_t, dvr_date_in_title), + .group = 4, + }, + { + .type = PT_BOOL, + .id = "time-in-title", + .name = "Include Time In Filename", + .off = offsetof(dvr_config_t, dvr_time_in_title), + .group = 4, + }, + { + .type = PT_BOOL, + .id = "episode-in-title", + .name = "Include Episode In Filename", + .off = offsetof(dvr_config_t, dvr_time_in_title), + .group = 4, + }, + { + .type = PT_BOOL, + .id = "subtitle-in-title", + .name = "Include Subtitle In Filename", + .off = offsetof(dvr_config_t, dvr_time_in_title), + .group = 5, + }, + { + .type = PT_BOOL, + .id = "episode-before-date", + .name = "Put Episode In Filename Before Date And Time", + .off = offsetof(dvr_config_t, dvr_episode_before_date), + .group = 5, + }, + { + .type = PT_BOOL, + .id = "clean-title", + .name = "Remove All Unsafe Characters From Filename", + .off = offsetof(dvr_config_t, dvr_clean_title), + .group = 5, + }, + { + .type = PT_BOOL, + .id = "whitespace-in-title", + .name = "Replace Whitespace In Title with '-'", + .off = offsetof(dvr_config_t, dvr_whitespace_in_title), + .group = 5, + }, + {} + }, +}; /** * @@ -1840,7 +2405,7 @@ dvr_entry_delete(dvr_entry_t *de) /* Also delete directories, if they were created for the recording and if they are empty */ - dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); + dvr_config_t *cfg = de->de_config; char path[500]; snprintf(path, sizeof(path), "%s", cfg->dvr_storage); @@ -1895,3 +2460,90 @@ dvr_entry_cancel_delete(dvr_entry_t *de) abort(); } } + +/** + * + */ +void +dvr_init(void) +{ + htsmsg_t *m, *l; + htsmsg_field_t *f; + char buf[500]; + const char *homedir; + struct stat st; + dvr_config_t *cfg; + + dvr_iov_max = sysconf(_SC_IOV_MAX); + + /* Default settings */ + + LIST_INIT(&dvrconfigs); + + if ((l = hts_settings_load("dvr/config")) != NULL) { + HTSMSG_FOREACH(f, l) { + if ((m = htsmsg_get_map_by_field(f)) == NULL) continue; + (void)dvr_config_create(NULL, f->hmf_name, m); + } + htsmsg_destroy(l); + } + + /* Create the default entry */ + + cfg = dvr_config_find_by_name_default(""); + assert(cfg); + + LIST_FOREACH(cfg, &dvrconfigs, config_link) { + if(cfg->dvr_storage == NULL || !strlen(cfg->dvr_storage)) { + /* Try to figure out a good place to put them videos */ + + homedir = getenv("HOME"); + + if(homedir != NULL) { + snprintf(buf, sizeof(buf), "%s/Videos", homedir); + if(stat(buf, &st) == 0 && S_ISDIR(st.st_mode)) + cfg->dvr_storage = strdup(buf); + + else if(stat(homedir, &st) == 0 && S_ISDIR(st.st_mode)) + cfg->dvr_storage = strdup(homedir); + else + cfg->dvr_storage = strdup(getcwd(buf, sizeof(buf))); + } + + tvhlog(LOG_WARNING, "dvr", + "Output directory for video recording is not yet configured " + "for DVR configuration \"%s\". " + "Defaulting to to \"%s\". " + "This can be changed from the web user interface.", + cfg->dvr_config_name, cfg->dvr_storage); + } + } + +#if ENABLE_INOTIFY + dvr_inotify_init(); +#endif + dvr_autorec_init(); + dvr_db_load(); + dvr_autorec_update(); +} + +/** + * + */ +void +dvr_done(void) +{ + dvr_config_t *cfg; + dvr_entry_t *de; + +#if ENABLE_INOTIFY + dvr_inotify_done(); +#endif + pthread_mutex_lock(&global_lock); + while ((cfg = LIST_FIRST(&dvrconfigs)) != NULL) + dvr_config_destroy(cfg, 0); + while ((de = LIST_FIRST(&dvrentries)) != NULL) + dvr_entry_remove(de, 0); + pthread_mutex_unlock(&global_lock); + dvr_autorec_done(); +} diff --git a/src/dvr/dvr_rec.c b/src/dvr/dvr_rec.c index 66bd2e240..472177b55 100644 --- a/src/dvr/dvr_rec.c +++ b/src/dvr/dvr_rec.c @@ -74,7 +74,7 @@ dvr_rec_subscribe(dvr_entry_t *de) snprintf(buf, sizeof(buf), "DVR: %s", lang_str_get(de->de_title, NULL)); - if(de->de_mc == MC_PASS) { + if(dvr_entry_get_mc(de) == MC_PASS) { streaming_queue_init(&de->de_sq, SMT_PACKET); de->de_gh = NULL; de->de_tsfix = NULL; @@ -307,7 +307,7 @@ dvr_rec_start(dvr_entry_t *de, const streaming_start_t *ss) dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); muxer_container_type_t mc; - mc = de->de_mc; + mc = dvr_entry_get_mc(de); de->de_mux = muxer_create(mc, &cfg->dvr_muxcnf); if(!de->de_mux) { diff --git a/src/epg.c b/src/epg.c index 02e01bcc0..742645951 100644 --- a/src/epg.c +++ b/src/epg.c @@ -2190,8 +2190,8 @@ htsmsg_t *epg_genres_list_all ( int major_only, int major_prefix ) for (j = 0; j < (major_only ? 1 : 16); j++) { if (_epg_genre_names[i][j]) { e = htsmsg_create_map(); - htsmsg_add_u32(e, "code", i << 4 | j); - htsmsg_add_str(e, "name", _epg_genre_names[i][j]); + htsmsg_add_u32(e, "key", major_only ? i : (i << 4 | j)); + htsmsg_add_str(e, "val", _epg_genre_names[i][j]); // TODO: use major_prefix htsmsg_add_msg(m, NULL, e); } diff --git a/src/htsp_server.c b/src/htsp_server.c index 93a445f21..eced3aba1 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -654,7 +654,7 @@ htsp_build_dvrentry(dvr_entry_t *de, const char *method) const char *p; dvr_config_t *cfg; - htsmsg_add_u32(out, "id", de->de_id); + htsmsg_add_u32(out, "id", idnode_get_short_uuid(&de->de_id)); if (de->de_channel) htsmsg_add_u32(out, "channel", channel_get_id(de->de_channel)); @@ -796,7 +796,7 @@ htsp_build_event } if((de = dvr_entry_find_by_event(e)) != NULL) { - htsmsg_add_u32(out, "dvrId", de->de_id); + htsmsg_add_u32(out, "dvrId", idnode_get_short_uuid(&de->de_id)); } if ((n = epg_broadcast_get_next(e))) @@ -1217,9 +1217,9 @@ htsp_method_addDvrEntry(htsp_connection_t *htsp, htsmsg_t *in) desc = ""; // create the dvr entry - de = dvr_entry_create(dvr_config_name, ch, start, stop, - start_extra, stop_extra, - title, desc, lang, 0, creator, NULL, priority); + de = dvr_entry_create_htsp(dvr_config_name, ch, start, stop, + start_extra, stop_extra, + title, desc, lang, 0, creator, NULL, priority); /* Event timer */ } else { @@ -1238,7 +1238,7 @@ htsp_method_addDvrEntry(htsp_connection_t *htsp, htsmsg_t *in) case DVR_RECORDING: case DVR_MISSED_TIME: case DVR_COMPLETED: - htsmsg_add_u32(out, "id", de->de_id); + htsmsg_add_u32(out, "id", idnode_get_short_uuid(&de->de_id)); htsmsg_add_u32(out, "success", 1); break; case DVR_NOSTATE: @@ -2531,7 +2531,7 @@ void htsp_dvr_entry_delete(dvr_entry_t *de) { htsmsg_t *m = htsmsg_create_map(); - htsmsg_add_u32(m, "id", de->de_id); + htsmsg_add_u32(m, "id", idnode_get_short_uuid(&de->de_id)); htsmsg_add_str(m, "method", "dvrEntryDelete"); htsp_async_send(m, HTSP_ASYNC_ON); } diff --git a/src/http.c b/src/http.c index eba235177..14f94b030 100644 --- a/src/http.c +++ b/src/http.c @@ -462,8 +462,14 @@ http_exec(http_connection_t *hc, http_path_t *hp, char *remain) if(http_access_verify(hc, hp->hp_accessmask)) err = HTTP_STATUS_UNAUTHORIZED; - else + else { + /* FIXME: Recode to obtain access only once */ + hc->hc_access = access_get(hc->hc_username, hc->hc_password, + (struct sockaddr *)hc->hc_peer); err = hp->hp_callback(hc, remain, hp->hp_opaque); + access_destroy(hc->hc_access); + hc->hc_access = NULL; + } if(err == -1) return 1; diff --git a/src/http.h b/src/http.h index cf35f76d6..2b0d405cd 100644 --- a/src/http.h +++ b/src/http.h @@ -22,6 +22,7 @@ #include "htsbuf.h" #include "url.h" #include "tvhpoll.h" +#include "access.h" struct channel; @@ -130,6 +131,7 @@ typedef struct http_connection { char *hc_username; char *hc_password; + access_t *hc_access; struct config_head *hc_user_config; diff --git a/src/idnode.c b/src/idnode.c index 9436e2768..1d8a0294a 100644 --- a/src/idnode.c +++ b/src/idnode.c @@ -366,10 +366,46 @@ idnode_get_u32 *u32 = *(int*)ptr; return 0; case PT_U16: + *u32 = *(uint16_t*)ptr; + return 0; + case PT_U32: *u32 = *(uint32_t*)ptr; return 0; + default: + break; + } + } + return 1; +} + +/* + * Get field as signed 64-bit int + */ +int +idnode_get_s64 + ( idnode_t *self, const char *key, int64_t *s64 ) +{ + const property_t *p = idnode_find_prop(self, key); + if (p->islist) return 1; + if (p) { + const void *ptr; + if (p->get) + ptr = p->get(self); + else + ptr = ((void*)self) + p->off; + switch (p->type) { + case PT_INT: + case PT_BOOL: + *s64 = *(int*)ptr; + return 0; + case PT_U16: + *s64 = *(uint16_t*)ptr; + return 0; case PT_U32: - *u32 = *(uint16_t*)ptr; + *s64 = *(uint32_t*)ptr; + return 0; + case PT_S64: + *s64 = *(int64_t*)ptr; return 0; default: break; @@ -401,6 +437,29 @@ idnode_get_bool return 1; } +/* + * Get field as time + */ +int +idnode_get_time + ( idnode_t *self, const char *key, time_t *tm ) +{ + const property_t *p = idnode_find_prop(self, key); + if (p->islist) return 1; + if (p) { + void *ptr = self; + ptr += p->off; + switch (p->type) { + case PT_TIME: + *tm = *(time_t*)ptr; + return 0; + default: + break; + } + } + return 1; +} + /* ************************************************************************** * Lookup * *************************************************************************/ @@ -493,11 +552,11 @@ idnode_cmp_sort { int r; const char *stra = tvh_strdupa(idnode_get_str(ina, sort->key) ?: ""); - const char *strb = idnode_get_str(inb, sort->key); + const char *strb = idnode_get_str(inb, sort->key) ?: ""; if (sort->dir == IS_ASC) - r = strcmp(stra ?: "", strb ?: ""); + r = strcmp(stra, strb); else - r = strcmp(strb ?: "", stra ?: ""); + r = strcmp(strb, stra); return r; } break; @@ -505,6 +564,7 @@ idnode_cmp_sort case PT_U16: case PT_U32: case PT_BOOL: + case PT_PERM: { uint32_t u32a = 0, u32b = 0; idnode_get_u32(ina, sort->key, &u32a); @@ -515,8 +575,32 @@ idnode_cmp_sort return u32b - u32a; } break; + case PT_S64: + { + int64_t s64a = 0, s64b = 0; + idnode_get_s64(ina, sort->key, &s64a); + idnode_get_s64(inb, sort->key, &s64b); + if (sort->dir == IS_ASC) + return s64a - s64b; + else + return s64b - s64a; + } + break; case PT_DBL: // TODO + case PT_TIME: + { + time_t ta = 0, tb = 0; + idnode_get_time(ina, sort->key, &ta); + idnode_get_time(inb, sort->key, &tb); + if (sort->dir == IS_ASC) + return ta - tb; + else + return tb - ta; + } + break; + case PT_LANGSTR: + // TODO? case PT_NONE: break; } @@ -755,15 +839,12 @@ idnode_write0 ( idnode_t *self, htsmsg_t *c, int optmask, int dosave ) * Read * *************************************************************************/ -/* - * Save - */ void -idnode_read0 ( idnode_t *self, htsmsg_t *c, int optmask ) +idnode_read0 ( idnode_t *self, htsmsg_t *c, htsmsg_t *list, int optmask ) { const idclass_t *idc = self->in_class; for (; idc; idc = idc->ic_super) - prop_read_values(self, idc->ic_properties, c, optmask, NULL); + prop_read_values(self, idc->ic_properties, c, list, optmask); } /** @@ -771,11 +852,11 @@ idnode_read0 ( idnode_t *self, htsmsg_t *c, int optmask ) */ static void add_params - (struct idnode *self, const idclass_t *ic, htsmsg_t *p, int optmask, htsmsg_t *inc) + (struct idnode *self, const idclass_t *ic, htsmsg_t *p, htsmsg_t *list, int optmask) { /* Parent first */ if(ic->ic_super != NULL) - add_params(self, ic->ic_super, p, optmask, inc); + add_params(self, ic->ic_super, p, list, optmask); /* Seperator (if not empty) */ #if 0 @@ -788,14 +869,14 @@ add_params #endif /* Properties */ - prop_serialize(self, ic->ic_properties, p, optmask, inc); + prop_serialize(self, ic->ic_properties, p, list, optmask); } static htsmsg_t * -idnode_params (const idclass_t *idc, idnode_t *self, int optmask) +idnode_params (const idclass_t *idc, idnode_t *self, htsmsg_t *list, int optmask) { htsmsg_t *p = htsmsg_create_list(); - add_params(self, idc, p, optmask, NULL); + add_params(self, idc, p, list, optmask); return p; } @@ -825,13 +906,44 @@ static const char * idclass_get_order (const idclass_t *idc) { while (idc) { - if (idc->ic_class) + if (idc->ic_order) return idc->ic_order; idc = idc->ic_super; } return NULL; } +static htsmsg_t * +idclass_get_property_groups (const idclass_t *idc) +{ + const property_group_t *g; + htsmsg_t *e, *m; + int count; + while (idc) { + if (idc->ic_groups) { + m = htsmsg_create_list(); + count = 0; + for (g = idc->ic_groups; g->number && g->name; g++) { + e = htsmsg_create_map(); + htsmsg_add_u32(e, "number", g->number); + htsmsg_add_str(e, "name", g->name); + if (g->parent) + htsmsg_add_u32(e, "parent", g->parent); + if (g->column) + htsmsg_add_u32(e, "column", g->column); + htsmsg_add_msg(m, NULL, e); + count++; + } + if (count) + return m; + htsmsg_destroy(m); + break; + } + idc = idc->ic_super; + } + return NULL; +} + static int ic_cmp ( const idclass_link_t *a, const idclass_link_t *b ) { @@ -870,7 +982,7 @@ idclass_find ( const char *class ) * Just get the class definition */ htsmsg_t * -idclass_serialize0(const idclass_t *idc, int optmask) +idclass_serialize0(const idclass_t *idc, htsmsg_t *list, int optmask) { const char *s; htsmsg_t *p, *m = htsmsg_create_map(); @@ -882,9 +994,11 @@ idclass_serialize0(const idclass_t *idc, int optmask) htsmsg_add_str(m, "class", s); if ((s = idclass_get_order(idc))) htsmsg_add_str(m, "order", s); + if ((p = idclass_get_property_groups(idc))) + htsmsg_add_msg(m, "groups", p); /* Props */ - if ((p = idnode_params(idc, NULL, optmask))) + if ((p = idnode_params(idc, NULL, list, optmask))) htsmsg_add_msg(m, "props", p); return m; @@ -894,7 +1008,7 @@ idclass_serialize0(const idclass_t *idc, int optmask) * */ htsmsg_t * -idnode_serialize0(idnode_t *self, int optmask) +idnode_serialize0(idnode_t *self, htsmsg_t *list, int optmask) { const idclass_t *idc = self->in_class; const char *uuid, *s; @@ -909,7 +1023,7 @@ idnode_serialize0(idnode_t *self, int optmask) if ((s = idclass_get_class(idc))) htsmsg_add_str(m, "class", s); - htsmsg_add_msg(m, "params", idnode_params(idc, self, optmask)); + htsmsg_add_msg(m, "params", idnode_params(idc, self, list, optmask)); return m; } diff --git a/src/idnode.h b/src/idnode.h index 43ce33280..4a016d39b 100644 --- a/src/idnode.h +++ b/src/idnode.h @@ -26,7 +26,7 @@ #include -struct htsmsg; +struct access; typedef struct idnode idnode_t; /* @@ -39,17 +39,29 @@ typedef struct idnode_set size_t is_count; ///< Current usage of is_array } idnode_set_t; +/* + * Property groups + */ +typedef struct property_group +{ + const char *name; + uint32_t number; + uint32_t parent; + uint32_t column; +} property_group_t; + /* * Class definition */ typedef struct idclass idclass_t; struct idclass { - const struct idclass *ic_super; /// Parent class - const char *ic_class; /// Class name - const char *ic_caption; /// Class description - const char *ic_order; /// Property order (comma separated) - const property_t *ic_properties; /// Property list - const char *ic_event; /// Events to fire on add/delete/title + const struct idclass *ic_super; ///< Parent class + const char *ic_class; ///< Class name + const char *ic_caption; ///< Class description + const char *ic_order; ///< Property order (comma separated) + const property_group_t *ic_groups; ///< Groups for visual representation + const property_t *ic_properties; ///< Property list + const char *ic_event; ///< Events to fire on add/delete/title /* Callbacks */ idnode_set_t *(*ic_get_childs) (idnode_t *self); @@ -58,6 +70,7 @@ struct idclass { void (*ic_delete) (idnode_t *self); void (*ic_moveup) (idnode_t *self); void (*ic_movedown) (idnode_t *self); + int (*ic_perm) (idnode_t *self, struct access *a, htsmsg_t *msg_to_write); }; /* @@ -139,20 +152,30 @@ void idnode_notify_title_changed (void *in); void idclass_register ( const idclass_t *idc ); const idclass_t *idclass_find ( const char *name ); -htsmsg_t *idclass_serialize0 (const idclass_t *idc, int optmask); -htsmsg_t *idnode_serialize0 (idnode_t *self, int optmask); -void idnode_read0 (idnode_t *self, htsmsg_t *m, int optmask); +htsmsg_t *idclass_serialize0 (const idclass_t *idc, htsmsg_t *list, int optmask); +htsmsg_t *idnode_serialize0 (idnode_t *self, htsmsg_t *list, int optmask); +void idnode_read0 (idnode_t *self, htsmsg_t *m, htsmsg_t *list, int optmask); int idnode_write0 (idnode_t *self, htsmsg_t *m, int optmask, int dosave); -#define idclass_serialize(idc) idclass_serialize0(idc, 0) -#define idnode_serialize(in) idnode_serialize0(in, 0) +#define idclass_serialize(idc) idclass_serialize0(idc, NULL, 0) +#define idnode_serialize(in) idnode_serialize0(in, NULL, 0) #define idnode_load(in, m) idnode_write0(in, m, PO_NOSAVE, 0) -#define idnode_save(in, m) idnode_read0(in, m, PO_NOSAVE | PO_USERAW) +#define idnode_save(in, m) idnode_read0(in, m, NULL, PO_NOSAVE | PO_USERAW) #define idnode_update(in, m) idnode_write0(in, m, PO_RDONLY | PO_WRONCE, 1) +static inline int +idnode_perm(idnode_t *self, struct access *a, htsmsg_t *msg_to_write) +{ + if (self->in_class->ic_perm) + return self->in_class->ic_perm(self, a, msg_to_write); + return 0; +} + const char *idnode_get_str (idnode_t *self, const char *key ); int idnode_get_u32 (idnode_t *self, const char *key, uint32_t *u32); +int idnode_get_s64 (idnode_t *self, const char *key, int64_t *s64); int idnode_get_bool(idnode_t *self, const char *key, int *b); +int idnode_get_time(idnode_t *self, const char *key, time_t *tm); void idnode_filter_add_str (idnode_filter_t *f, const char *k, const char *v, int t); diff --git a/src/imagecache.c b/src/imagecache.c index b28a25cb9..1cc5ed7c2 100644 --- a/src/imagecache.c +++ b/src/imagecache.c @@ -388,7 +388,7 @@ htsmsg_t * imagecache_get_config ( void ) { htsmsg_t *m = htsmsg_create_map(); - prop_read_values(&imagecache_conf, imagecache_props, m, 0, NULL); + prop_read_values(&imagecache_conf, imagecache_props, m, NULL, 0); return m; } diff --git a/src/imagecache.h b/src/imagecache.h index c5cdc4e6a..7b4108774 100644 --- a/src/imagecache.h +++ b/src/imagecache.h @@ -22,6 +22,7 @@ #include struct imagecache_config { + int __unused__; // to avoid assert in prop.c (first member should be idnode_t) int enabled; int ignore_sslcert; uint32_t ok_period; diff --git a/src/lang_str.c b/src/lang_str.c index 4fe4aea81..ed9ee089b 100644 --- a/src/lang_str.c +++ b/src/lang_str.c @@ -50,6 +50,8 @@ lang_str_t *lang_str_create ( void ) void lang_str_destroy ( lang_str_t *ls ) { lang_str_ele_t *e; + if (ls == NULL) + return; while ((e = RB_FIRST(ls))) { if (e->str) free(e->str); RB_REMOVE(ls, e, link); @@ -158,38 +160,95 @@ int lang_str_append return _lang_str_add(ls, str, lang, 0, 1); } -/* Serialize */ -void lang_str_serialize ( lang_str_t *ls, htsmsg_t *m, const char *f ) +/* Serialize map */ +htsmsg_t *lang_str_serialize_map ( lang_str_t *ls ) { lang_str_ele_t *e; - if (!ls) return; + if (!ls) return NULL; htsmsg_t *a = htsmsg_create_map(); RB_FOREACH(e, ls, link) { htsmsg_add_str(a, e->lang, e->str); } - htsmsg_add_msg(m, f, a); + return a; +} + +/* Serialize */ +void lang_str_serialize ( lang_str_t *ls, htsmsg_t *m, const char *f ) +{ + if (!ls) return; + htsmsg_add_msg(m, f, lang_str_serialize_map(ls)); +} + +/* De-serialize map */ +lang_str_t *lang_str_deserialize_map ( htsmsg_t *map ) +{ + lang_str_t *ret = lang_str_create(); + htsmsg_field_t *f; + const char *str; + + HTSMSG_FOREACH(f, map) { + if ((str = htsmsg_field_get_string(f))) { + lang_str_add(ret, str, f->hmf_name, 0); + } + } + return ret; } /* De-serialize */ lang_str_t *lang_str_deserialize ( htsmsg_t *m, const char *n ) { - lang_str_t *ret = NULL; htsmsg_t *a; - htsmsg_field_t *f; const char *str; if ((a = htsmsg_get_map(m, n))) { - ret = lang_str_create(); - HTSMSG_FOREACH(f, a) { - if ((str = htsmsg_field_get_string(f))) { - lang_str_add(ret, str, f->hmf_name, 0); - } - } + return lang_str_deserialize_map(a); } else if ((str = htsmsg_get_str(m, n))) { - ret = lang_str_create(); + lang_str_t *ret = lang_str_create(); lang_str_add(ret, str, NULL, 0); + return ret; } - return ret; + return NULL; +} + +/* Compare */ +int lang_str_compare( lang_str_t *ls1, lang_str_t *ls2 ) +{ + lang_str_ele_t *e; + const char *s1, *s2; + int r; + + if (ls1 == NULL && ls2) + return -1; + if (ls2 == NULL && ls1) + return 1; + if (ls1 == ls2) + return 0; + /* Note: may be optimized to not check languages twice */ + RB_FOREACH(e, ls1, link) { + s1 = lang_str_get(ls1, e->lang); + s2 = lang_str_get(ls2, e->lang); + if (s1 == NULL && s2 != NULL) + return -1; + if (s2 == NULL && s1 != NULL) + return 1; + if (s1 == NULL || s2 == NULL) + continue; + r = strcmp(s1, s2); + if (r) return r; + } + RB_FOREACH(e, ls2, link) { + s1 = lang_str_get(ls1, e->lang); + s2 = lang_str_get(ls2, e->lang); + if (s1 == NULL && s2 != NULL) + return -1; + if (s2 == NULL && s1 != NULL) + return 1; + if (s1 == NULL || s2 == NULL) + continue; + r = strcmp(s1, s2); + if (r) return r; + } + return 0; } void lang_str_done( void ) diff --git a/src/lang_str.h b/src/lang_str.h index ecd22413c..ff901b8ba 100644 --- a/src/lang_str.h +++ b/src/lang_str.h @@ -47,11 +47,18 @@ int lang_str_append ( lang_str_t *ls, const char *str, const char *lang ); /* Serialize/Deserialize */ +htsmsg_t *lang_str_serialize_map + ( lang_str_t *ls ); void lang_str_serialize ( lang_str_t *ls, htsmsg_t *msg, const char *f ); +lang_str_t *lang_str_deserialize_map + ( htsmsg_t *map ); lang_str_t *lang_str_deserialize ( htsmsg_t *m, const char *f ); +/* Compare */ +int lang_str_compare ( lang_str_t *ls1, lang_str_t *ls2 ); + /* Init/Done */ void lang_str_done( void ); diff --git a/src/muxer.h b/src/muxer.h index ec72ccda4..8576c1b27 100644 --- a/src/muxer.h +++ b/src/muxer.h @@ -48,7 +48,7 @@ typedef enum { /* Muxer configuration used when creating a muxer. */ typedef struct muxer_config { int m_flags; - muxer_cache_type_t m_cache; + int m_cache; /* * directory_permissions should really be in dvr.h as it's not really needed for the muxer diff --git a/src/muxer/tvh/mkmux.c b/src/muxer/tvh/mkmux.c index 5ccd51dac..3e2a11eac 100644 --- a/src/muxer/tvh/mkmux.c +++ b/src/muxer/tvh/mkmux.c @@ -652,6 +652,7 @@ _mk_build_metadata(const dvr_entry_t *de, const epg_broadcast_t *ebc) htsbuf_queue_t *q = htsbuf_queue_alloc(0); char datestr[64], ctype[100]; const epg_genre_t *eg = NULL; + epg_genre_t eg0; struct tm tm; localtime_r(de ? &de->de_start : &ebc->start, &tm); epg_episode_t *ee = NULL; @@ -677,8 +678,10 @@ _mk_build_metadata(const dvr_entry_t *de, const epg_broadcast_t *ebc) addtag(q, build_tag_string("ORIGINAL_MEDIA_TYPE", "TV", NULL, 0, NULL)); - if(de && de->de_content_type.code) { - eg = &de->de_content_type; + if(de && de->de_content_type) { + memset(&eg0, 0, sizeof(eg0)); + eg0.code = de->de_content_type; + eg = &eg0; } else if (ee) { eg = LIST_FIRST(&ee->genre); } diff --git a/src/prop.c b/src/prop.c index 944eb1e8a..05066adf0 100644 --- a/src/prop.c +++ b/src/prop.c @@ -22,6 +22,7 @@ #include "tvheadend.h" #include "prop.h" +#include "lang_str.h" /* ************************************************************************** * Utilities @@ -31,12 +32,16 @@ * */ const static struct strtab typetab[] = { - { "bool", PT_BOOL }, - { "int", PT_INT }, - { "str", PT_STR }, - { "u16", PT_U16 }, - { "u32", PT_U32 }, - { "dbl", PT_DBL }, + { "bool", PT_BOOL }, + { "int", PT_INT }, + { "str", PT_STR }, + { "u16", PT_U16 }, + { "u32", PT_U32 }, + { "s64", PT_S64 }, + { "dbl", PT_DBL }, + { "time", PT_TIME }, + { "langstr", PT_LANGSTR }, + { "perm", PT_PERM }, }; @@ -71,6 +76,7 @@ prop_write_values int64_t s64; uint32_t u32; uint16_t u16; + time_t tm; #define PROP_UPDATE(v, t)\ new = &v;\ if (!p->set && (*((t*)cur) != *((t*)new))) {\ @@ -81,6 +87,7 @@ prop_write_values if (!pl) return 0; for (p = pl; p->id; p++) { + if (p->type == PT_NONE) continue; f = htsmsg_field_find(m, p->id); @@ -89,6 +96,9 @@ prop_write_values /* Ignore */ if(p->opts & optmask) continue; + /* Sanity check */ + assert(p->set || p->off); + /* Write */ save = 0; cur = obj + p->off; @@ -127,6 +137,13 @@ prop_write_values PROP_UPDATE(u32, uint32_t); break; } + case PT_S64: { + if (htsmsg_field_get_s64(f, &s64)) + continue; + i = s64; + PROP_UPDATE(i, int64_t); + break; + } case PT_DBL: { if (htsmsg_field_get_dbl(f, &dbl)) continue; @@ -144,6 +161,38 @@ prop_write_values } break; } + case PT_TIME: { + if (htsmsg_field_get_s64(f, &s64)) + continue; + tm = s64; + PROP_UPDATE(tm, time_t); + break; + } + case PT_LANGSTR: { + lang_str_t **lstr1 = cur; + lang_str_t *lstr2; + new = htsmsg_field_get_map(f); + if (!new) + continue; + if (!p->set) { + lstr2 = lang_str_deserialize_map((htsmsg_t *)new); + if (lang_str_compare(*lstr1, lstr2)) { + lang_str_destroy(*lstr1); + *lstr1 = lstr2; + save = 1; + } else { + lang_str_destroy(lstr2); + } + } + break; + } + case PT_PERM: { + if (!(new = htsmsg_field_get_str(f))) + continue; + u32 = (int)strtol(new,NULL,0); + PROP_UPDATE(u32, uint32_t); + break; + } case PT_NONE: break; } @@ -175,19 +224,18 @@ prop_write_values */ static void prop_read_value - (void *obj, const property_t *p, htsmsg_t *m, const char *name, - int optmask, htsmsg_t *inc) + (void *obj, const property_t *p, htsmsg_t *m, const char *name, int optmask) { const char *s; const void *val = obj + p->off; + char buf[16]; /* Ignore */ if (p->opts & optmask) return; if (p->type == PT_NONE) return; - /* Ignore */ - if (inc && !htsmsg_get_u32_or_default(inc, p->id, 0)) - return; + /* Sanity check */ + assert(p->get || p->off); /* Get method */ if (!(optmask & PO_USERAW) || !p->off) @@ -207,11 +255,14 @@ prop_read_value case PT_INT: htsmsg_add_s64(m, name, *(int *)val); break; + case PT_U16: + htsmsg_add_u32(m, name, *(uint16_t *)val); + break; case PT_U32: htsmsg_add_u32(m, name, *(uint32_t *)val); break; - case PT_U16: - htsmsg_add_u32(m, name, *(uint16_t *)val); + case PT_S64: + htsmsg_add_s64(m, name, *(int64_t *)val); break; case PT_STR: if ((s = *(const char **)val)) @@ -220,6 +271,16 @@ prop_read_value case PT_DBL: htsmsg_add_dbl(m, name, *(double*)val); break; + case PT_TIME: + htsmsg_add_s64(m, name, *(time_t *)val); + break; + case PT_LANGSTR: + lang_str_serialize(*(lang_str_t **)val, m, name); + break; + case PT_PERM: + snprintf(buf, sizeof(buf), "%04o", *(uint32_t *)val); + htsmsg_add_str(m, name, buf); + break; case PT_NONE: break; } @@ -231,110 +292,161 @@ prop_read_value */ void prop_read_values - (void *obj, const property_t *pl, htsmsg_t *m, int optmask, htsmsg_t *inc) + (void *obj, const property_t *pl, htsmsg_t *m, htsmsg_t *list, int optmask) { if(pl == NULL) return; - for (; pl->id; pl++) - prop_read_value(obj, pl, m, pl->id, optmask, inc); + + if(list == NULL) { + for (; pl->id; pl++) + prop_read_value(obj, pl, m, pl->id, optmask); + } else { + const property_t *p; + htsmsg_field_t *f; + int b; + HTSMSG_FOREACH(f, list) { + if (!htsmsg_field_get_bool(f, &b) && b > 0) { + p = prop_find(pl, f->hmf_name); + if (p) + prop_read_value(obj, p, m, p->id, optmask); + } + } + } } /** * */ -void -prop_serialize - (void *obj, const property_t *pl, htsmsg_t *msg, int optmask, htsmsg_t *inc) +static void +prop_serialize_value + (void *obj, const property_t *pl, htsmsg_t *msg, int optmask) { htsmsg_field_t *f; - - if(pl == NULL) - return; - - for(; pl->id; pl++) { - - /* Remove parent */ - // TODO: this is really horrible and inefficient! - HTSMSG_FOREACH(f, msg) { - htsmsg_t *t = htsmsg_field_get_map(f); - const char *str; - if (t && (str = htsmsg_get_str(t, "id"))) { - if (!strcmp(str, pl->id)) { - htsmsg_field_destroy(msg, f); - break; - } + char buf[16]; + + /* Remove parent */ + // TODO: this is really horrible and inefficient! + HTSMSG_FOREACH(f, msg) { + htsmsg_t *t = htsmsg_field_get_map(f); + const char *str; + if (t && (str = htsmsg_get_str(t, "id"))) { + if (!strcmp(str, pl->id)) { + htsmsg_field_destroy(msg, f); + break; } } + } - /* Ignore */ - if (inc && !htsmsg_get_u32_or_default(inc, pl->id, 0)) - continue; + htsmsg_t *m = htsmsg_create_map(); - htsmsg_t *m = htsmsg_create_map(); + /* ID / type */ + htsmsg_add_str(m, "id", pl->id); + htsmsg_add_str(m, "type", val2str(pl->type, typetab) ?: "none"); - /* ID / type */ - htsmsg_add_str(m, "id", pl->id); - htsmsg_add_str(m, "type", val2str(pl->type, typetab) ?: "none"); + /* Skip - special blocker */ + if (pl->type == PT_NONE) { + htsmsg_add_msg(msg, NULL, m); + return; + } - /* Skip - special blocker */ - if (pl->type == PT_NONE) { - htsmsg_add_msg(msg, NULL, m); - continue; - } - - /* Metadata */ - htsmsg_add_str(m, "caption", pl->name); - if (pl->islist) - htsmsg_add_u32(m, "list", 1); - - /* Default */ - // TODO: currently no support for list defaults - switch (pl->type) { - case PT_BOOL: - htsmsg_add_bool(m, "default", pl->def.i); - break; - case PT_INT: - htsmsg_add_s32(m, "default", pl->def.i); - break; - case PT_U16: - htsmsg_add_u32(m, "default", pl->def.u16); - break; - case PT_U32: - htsmsg_add_u32(m, "default", pl->def.u32); - break; - case PT_DBL: - htsmsg_add_dbl(m, "default", pl->def.d); - break; - case PT_STR: - htsmsg_add_str(m, "default", pl->def.s ?: ""); - break; - case PT_NONE: - break; - } + /* Metadata */ + htsmsg_add_str(m, "caption", pl->name); + if (pl->islist) + htsmsg_add_u32(m, "list", 1); - /* Options */ - if (pl->opts & PO_RDONLY) - htsmsg_add_bool(m, "rdonly", 1); - if (pl->opts & PO_NOSAVE) - htsmsg_add_bool(m, "nosave", 1); - if (pl->opts & PO_WRONCE) - htsmsg_add_bool(m, "wronce", 1); - if (pl->opts & PO_ADVANCED) - htsmsg_add_bool(m, "advanced", 1); - if (pl->opts & PO_HIDDEN) - htsmsg_add_bool(m, "hidden", 1); - if (pl->opts & PO_PASSWORD) - htsmsg_add_bool(m, "password", 1); - - /* Enum list */ - if (pl->list) - htsmsg_add_msg(m, "enum", pl->list(obj)); - - /* Data */ - if (obj) - prop_read_value(obj, pl, m, "value", optmask, NULL); + /* Default */ + // TODO: currently no support for list defaults + switch (pl->type) { + case PT_BOOL: + htsmsg_add_bool(m, "default", pl->def.i); + break; + case PT_INT: + htsmsg_add_s32(m, "default", pl->def.i); + break; + case PT_U16: + htsmsg_add_u32(m, "default", pl->def.u16); + break; + case PT_U32: + htsmsg_add_u32(m, "default", pl->def.u32); + break; + case PT_S64: + htsmsg_add_s64(m, "default", pl->def.s64); + break; + case PT_DBL: + htsmsg_add_dbl(m, "default", pl->def.d); + break; + case PT_STR: + htsmsg_add_str(m, "default", pl->def.s ?: ""); + break; + case PT_TIME: + htsmsg_add_s64(m, "default", pl->def.tm); + break; + case PT_LANGSTR: + /* TODO? */ + break; + case PT_PERM: + snprintf(buf, sizeof(buf), "%04o", pl->def.u32); + htsmsg_add_str(m, "default", buf); + break; + case PT_NONE: + break; + } - htsmsg_add_msg(msg, NULL, m); + /* Options */ + if (pl->opts & PO_RDONLY) + htsmsg_add_bool(m, "rdonly", 1); + if (pl->opts & PO_NOSAVE) + htsmsg_add_bool(m, "nosave", 1); + if (pl->opts & PO_WRONCE) + htsmsg_add_bool(m, "wronce", 1); + if (pl->opts & PO_ADVANCED) + htsmsg_add_bool(m, "advanced", 1); + if (pl->opts & PO_HIDDEN) + htsmsg_add_bool(m, "hidden", 1); + if (pl->opts & PO_PASSWORD) + htsmsg_add_bool(m, "password", 1); + if (pl->opts & PO_DURATION) + htsmsg_add_bool(m, "duration", 1); + + /* Enum list */ + if (pl->list) + htsmsg_add_msg(m, "enum", pl->list(obj)); + + /* Visual group */ + if (pl->group) + htsmsg_add_u32(m, "group", pl->group); + + /* Data */ + if (obj) + prop_read_value(obj, pl, m, "value", optmask); + + htsmsg_add_msg(msg, NULL, m); +} + +/** + * + */ +void +prop_serialize + (void *obj, const property_t *pl, htsmsg_t *msg, htsmsg_t *list, int optmask) +{ + if(pl == NULL) + return; + + if(list == NULL) { + for (; pl->id; pl++) + prop_serialize_value(obj, pl, msg, optmask); + } else { + const property_t *p; + htsmsg_field_t *f; + int b; + HTSMSG_FOREACH(f, list) { + if (!htsmsg_field_get_bool(f, &b) && b > 0) { + p = prop_find(pl, f->hmf_name); + if (p) + prop_serialize_value(obj, p, msg, optmask); + } + } } } diff --git a/src/prop.h b/src/prop.h index 67fa2b1a2..803543a9a 100644 --- a/src/prop.h +++ b/src/prop.h @@ -34,21 +34,26 @@ typedef enum { PT_INT, PT_U16, PT_U32, + PT_S64, PT_DBL, + PT_TIME, + PT_LANGSTR, + PT_PERM, // like PT_U32 but with the special save } prop_type_t; /* * Property options */ -#define PO_NONE 0x00 -#define PO_RDONLY 0x01 // Property is read-only -#define PO_NOSAVE 0x02 // Property is transient (not saved) -#define PO_WRONCE 0x04 // Property is write-once (i.e. on creation) -#define PO_ADVANCED 0x08 // Property is advanced -#define PO_HIDDEN 0x10 // Property is hidden (by default) -#define PO_USERAW 0x20 // Only save the RAW (off) value if it exists -#define PO_SORTKEY 0x40 // Sort using key (not display value) -#define PO_PASSWORD 0x80 // String is a password +#define PO_NONE 0x0000 +#define PO_RDONLY 0x0001 // Property is read-only +#define PO_NOSAVE 0x0002 // Property is transient (not saved) +#define PO_WRONCE 0x0004 // Property is write-once (i.e. on creation) +#define PO_ADVANCED 0x0008 // Property is advanced +#define PO_HIDDEN 0x0010 // Property is hidden (by default) +#define PO_USERAW 0x0020 // Only save the RAW (off) value if it exists +#define PO_SORTKEY 0x0040 // Sort using key (not display value) +#define PO_PASSWORD 0x0080 // String is a password +#define PO_DURATION 0x0100 // For PT_TIME - differentiate between duration and datetime /* * Property definition @@ -59,7 +64,8 @@ typedef struct property { prop_type_t type; ///< Type int islist; ///< Is a list size_t off; ///< Offset into object - int opts; ///< Options + uint32_t opts; ///< Options + uint32_t group; ///< Visual group ID (like ExtJS FieldSet) /* String based processing */ const void *(*get) (void *ptr); @@ -72,10 +78,12 @@ typedef struct property { /* Default (for UI) */ union { int i; // PT_BOOL/PT_INT - const char *s; // PR_STR + const char *s; // PT_STR uint16_t u16; // PT_U16 - uint32_t u32; // PR_U32 + uint32_t u32; // PT_U32 + int64_t s64; // PT_S64 double d; // PT_DBL + time_t tm; // PT_TIME } def; /* Notification callback */ @@ -89,10 +97,10 @@ int prop_write_values (void *obj, const property_t *pl, htsmsg_t *m, int optmask, htsmsg_t *updated); void prop_read_values - (void *obj, const property_t *pl, htsmsg_t *m, int optmask, htsmsg_t *inc); + (void *obj, const property_t *pl, htsmsg_t *m, htsmsg_t *list, int optmask); void prop_serialize - (void *obj, const property_t *pl, htsmsg_t *m, int optmask, htsmsg_t *inc); + (void *obj, const property_t *pl, htsmsg_t *m, htsmsg_t *list, int optmask); #endif /* __TVH_PROP_H__ */ diff --git a/src/service.c b/src/service.c index a95df8a87..e7c91b423 100644 --- a/src/service.c +++ b/src/service.c @@ -115,13 +115,10 @@ static htsmsg_t * service_class_channel_enum ( void *obj ) { - htsmsg_t *p, *m = htsmsg_create_map(); + htsmsg_t *m = htsmsg_create_map(); htsmsg_add_str(m, "type", "api"); htsmsg_add_str(m, "uri", "channel/list"); htsmsg_add_str(m, "event", "channel"); - p = htsmsg_create_map(); - htsmsg_add_u32(p, "enum", 1); - htsmsg_add_msg(m, "params", p); return m; } diff --git a/src/webui/extjs.c b/src/webui/extjs.c index b6f92ce01..077131447 100755 --- a/src/webui/extjs.c +++ b/src/webui/extjs.c @@ -362,24 +362,6 @@ extjs_tablemgr(http_connection_t *hc, const char *remain, void *opaque) return 0; } -/** - * EPG Content Groups - */ -static int -extjs_ecglist(http_connection_t *hc, const char *remain, void *opaque) -{ - htsbuf_queue_t *hq = &hc->hc_reply; - htsmsg_t *out, *array; - - out = htsmsg_create_map(); - array = epg_genres_list_all(1, 0); - htsmsg_add_msg(out, "entries", array); - htsmsg_json_serialize(out, hq, 0); - htsmsg_destroy(out); - http_output_content(hc, "text/x-json; charset=UTF-8"); - return 0; -} - /** * */ @@ -502,127 +484,6 @@ extjs_epggrab(http_connection_t *hc, const char *remain, void *opaque) return 0; } -/** - * - */ -static int -extjs_confignames(http_connection_t *hc, const char *remain, void *opaque) -{ - htsbuf_queue_t *hq = &hc->hc_reply; - const char *op = http_arg_get(&hc->hc_req_args, "op"); - htsmsg_t *out, *array, *e; - dvr_config_t *cfg; - - pthread_mutex_lock(&global_lock); - - if(op != NULL && !strcmp(op, "list")) { - - out = htsmsg_create_map(); - array = htsmsg_create_list(); - - if (http_access_verify(hc, ACCESS_RECORDER_ALL)) - goto skip; - - LIST_FOREACH(cfg, &dvrconfigs, config_link) { - e = htsmsg_create_map(); - htsmsg_add_str(e, "identifier", cfg->dvr_config_name); - if (strlen(cfg->dvr_config_name) == 0) - htsmsg_add_str(e, "name", "(default)"); - else - htsmsg_add_str(e, "name", cfg->dvr_config_name); - htsmsg_add_msg(array, NULL, e); - } - -skip: - htsmsg_add_msg(out, "entries", array); - - } else { - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_BAD_REQUEST; - } - - pthread_mutex_unlock(&global_lock); - - htsmsg_json_serialize(out, hq, 0); - htsmsg_destroy(out); - http_output_content(hc, "text/x-json; charset=UTF-8"); - return 0; - -} - - -/** - * - */ -static int -extjs_dvr_containers(http_connection_t *hc, const char *remain, void *opaque) -{ - htsbuf_queue_t *hq = &hc->hc_reply; - const char *op = http_arg_get(&hc->hc_req_args, "op"); - htsmsg_t *out, *array; - - pthread_mutex_lock(&global_lock); - - if(op != NULL && !strcmp(op, "list")) { - - out = htsmsg_create_map(); - array = htsmsg_create_list(); - - muxer_container_list(array); - - htsmsg_add_msg(out, "entries", array); - - } else { - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_BAD_REQUEST; - } - - pthread_mutex_unlock(&global_lock); - - htsmsg_json_serialize(out, hq, 0); - htsmsg_destroy(out); - http_output_content(hc, "text/x-json; charset=UTF-8"); - return 0; - -} - - -/** - * - */ -static int -extjs_dvr_caches(http_connection_t *hc, const char *remain, void *opaque) -{ - htsbuf_queue_t *hq = &hc->hc_reply; - const char *op = http_arg_get(&hc->hc_req_args, "op"); - htsmsg_t *out, *array; - - pthread_mutex_lock(&global_lock); - - if(op != NULL && !strcmp(op, "list")) { - - out = htsmsg_create_map(); - array = htsmsg_create_list(); - - muxer_cache_list(array); - - htsmsg_add_msg(out, "entries", array); - - } else { - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_BAD_REQUEST; - } - - pthread_mutex_unlock(&global_lock); - - htsmsg_json_serialize(out, hq, 0); - htsmsg_destroy(out); - http_output_content(hc, "text/x-json; charset=UTF-8"); - return 0; - -} - - /** * */ @@ -728,8 +589,8 @@ extjs_epg(http_connection_t *hc, const char *remain, void *opaque) else limit = 20; /* XXX */ - if ((s = http_arg_get(&hc->hc_req_args, "contenttype"))) { - genre.code = atoi(s); + if ((s = http_arg_get(&hc->hc_req_args, "content_type"))) { + genre.code = atoi(s) * 16; eg = &genre; } @@ -788,7 +649,7 @@ extjs_epg(http_connection_t *hc, const char *remain, void *opaque) htsmsg_add_str(m, "serieslink", e->serieslink->uri); if((eg = LIST_FIRST(&ee->genre))) { - htsmsg_add_u32(m, "contenttype", eg->code); + htsmsg_add_u32(m, "content_type", eg->code / 16); } dvr_entry_t *de; @@ -930,501 +791,6 @@ extjs_epgobject(http_connection_t *hc, const char *remain, void *opaque) return 0; } -/** - * - */ -static int -extjs_dvr(http_connection_t *hc, const char *remain, void *opaque) -{ - htsbuf_queue_t *hq = &hc->hc_reply; - const char *op = http_arg_get(&hc->hc_req_args, "op"); - htsmsg_t *out, *r; - dvr_entry_t *de; - const char *s; - int flags = 0; - dvr_config_t *cfg; - epg_broadcast_t *e; - char buffer[5]; // Permissions buffer: leading zero, three octal digits plus terminating null - - if(op == NULL) - op = "loadSettings"; - - pthread_mutex_lock(&global_lock); - - if(http_access_verify(hc, ACCESS_RECORDER)) { - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_UNAUTHORIZED; - } - - if(!strcmp(op, "recordEvent") || !strcmp(op, "recordSeries")) { - - const char *config_name = http_arg_get(&hc->hc_req_args, "config_name"); - - s = http_arg_get(&hc->hc_req_args, "eventId"); - if((e = epg_broadcast_find_by_id(atoi(s), NULL)) == NULL) { - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_BAD_REQUEST; - } - - if (http_access_verify(hc, ACCESS_RECORDER_ALL)) { - config_name = NULL; - LIST_FOREACH(cfg, &dvrconfigs, config_link) { - if (cfg->dvr_config_name && hc->hc_username && - strcmp(cfg->dvr_config_name, hc->hc_username) == 0) { - config_name = cfg->dvr_config_name; - break; - } - } - if (config_name == NULL && hc->hc_username) - tvhlog(LOG_INFO,"dvr","User '%s' has no dvr config with identical name, using default...", hc->hc_username); - } - - if (!strcmp(op, "recordEvent")) - dvr_entry_create_by_event(config_name, - e, 0, 0, - hc->hc_representative, NULL, DVR_PRIO_NORMAL); - else - dvr_autorec_add_series_link(config_name, e, hc->hc_representative, "Created from EPG query"); - - out = htsmsg_create_map(); - htsmsg_add_u32(out, "success", 1); - } else if(!strcmp(op, "cancelEntry")) { - s = http_arg_get(&hc->hc_req_args, "entryId"); - - if((de = dvr_entry_find_by_id(atoi(s))) == NULL) { - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_BAD_REQUEST; - } - - dvr_entry_cancel(de); - - out = htsmsg_create_map(); - htsmsg_add_u32(out, "success", 1); - - } else if(!strcmp(op, "deleteEntry")) { - s = http_arg_get(&hc->hc_req_args, "entryId"); - - if((de = dvr_entry_find_by_id(atoi(s))) == NULL) { - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_BAD_REQUEST; - } - - dvr_entry_delete(de); - - out = htsmsg_create_map(); - htsmsg_add_u32(out, "success", 1); - - } else if(!strcmp(op, "createEntry")) { - - const char *config_name = http_arg_get(&hc->hc_req_args, "config_name"); - const char *title = http_arg_get(&hc->hc_req_args, "title"); - const char *datestr = http_arg_get(&hc->hc_req_args, "date"); - const char *startstr = http_arg_get(&hc->hc_req_args, "starttime"); - const char *stopstr = http_arg_get(&hc->hc_req_args, "stoptime"); - const char *channel = http_arg_get(&hc->hc_req_args, "channelid"); - const char *pri = http_arg_get(&hc->hc_req_args, "pri"); - - channel_t *ch = channel ? channel_find(channel) : NULL; - - if(ch == NULL || title == NULL || - datestr == NULL || strlen(datestr) != 10 || - startstr == NULL || strlen(startstr) != 5 || - stopstr == NULL || strlen(stopstr) != 5) { - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_BAD_REQUEST; - } - - struct tm t = {0}; - t.tm_year = atoi(datestr + 6) - 1900; - t.tm_mon = atoi(datestr) - 1; - t.tm_mday = atoi(datestr + 3); - t.tm_isdst = -1; - - t.tm_hour = atoi(startstr); - t.tm_min = atoi(startstr + 3); - - time_t start = mktime(&t); - - t.tm_hour = atoi(stopstr); - t.tm_min = atoi(stopstr + 3); - - time_t stop = mktime(&t); - - if(stop < start) - stop += 86400; - - if (http_access_verify(hc, ACCESS_RECORDER_ALL)) { - config_name = NULL; - LIST_FOREACH(cfg, &dvrconfigs, config_link) { - if (cfg->dvr_config_name && hc->hc_username && - strcmp(cfg->dvr_config_name, hc->hc_username) == 0) { - config_name = cfg->dvr_config_name; - break; - } - } - if (config_name == NULL && hc->hc_username) - tvhlog(LOG_INFO,"dvr","User '%s' has no dvr config with identical name, using default...", hc->hc_username); - } - - dvr_entry_create(config_name, - ch, start, stop, 0, 0, title, NULL, NULL, - 0, hc->hc_representative, - NULL, dvr_pri2val(pri)); - - out = htsmsg_create_map(); - htsmsg_add_u32(out, "success", 1); - - } else if(!strcmp(op, "createAutoRec")) { - int min_duration; - int max_duration; - epg_genre_t genre, *eg = NULL; - - if ((s = http_arg_get(&hc->hc_req_args, "contenttype"))) { - genre.code = atoi(s); - eg = &genre; - } - - if((s = http_arg_get(&hc->hc_req_args, "minduration")) != NULL) - min_duration = atoi(s); - else - min_duration = 0; - - if((s = http_arg_get(&hc->hc_req_args, "maxduration")) != NULL) - max_duration = atoi(s); - else - max_duration = INT_MAX; - - dvr_autorec_add(http_arg_get(&hc->hc_req_args, "config_name"), - http_arg_get(&hc->hc_req_args, "title"), - http_arg_get(&hc->hc_req_args, "channel"), - http_arg_get(&hc->hc_req_args, "tag"), - eg, min_duration,max_duration, - hc->hc_representative, "Created from EPG query"); - - out = htsmsg_create_map(); - htsmsg_add_u32(out, "success", 1); - - } else if(!strcmp(op, "loadSettings")) { - - s = http_arg_get(&hc->hc_req_args, "config_name"); - if (s == NULL) - s = ""; - cfg = dvr_config_find_by_name_default(s); - - r = htsmsg_create_map(); - htsmsg_add_str(r, "storage", cfg->dvr_storage); - htsmsg_add_str(r, "charset", cfg->dvr_charset ? cfg->dvr_charset : "UTF-8"); - htsmsg_add_str(r, "container", muxer_container_type2txt(cfg->dvr_mc)); - -/* Convert integer permissions to an octal-format 0xxx string and store it in the config file */ - - snprintf(buffer,sizeof(buffer),"%04o",cfg->dvr_muxcnf.m_file_permissions); - htsmsg_add_str(r, "filePermissions", buffer); - snprintf(buffer,sizeof(buffer),"%04o",cfg->dvr_muxcnf.m_directory_permissions); - htsmsg_add_str(r, "dirPermissions", buffer); - - htsmsg_add_u32(r, "cache", cfg->dvr_muxcnf.m_cache); - htsmsg_add_u32(r, "rewritePAT", - !!(cfg->dvr_muxcnf.m_flags & MC_REWRITE_PAT)); - htsmsg_add_u32(r, "rewritePMT", - !!(cfg->dvr_muxcnf.m_flags & MC_REWRITE_PMT)); - if(cfg->dvr_postproc != NULL) - htsmsg_add_str(r, "postproc", cfg->dvr_postproc); - htsmsg_add_u32(r, "retention", cfg->dvr_retention_days); - htsmsg_add_u32(r, "preExtraTime", cfg->dvr_extra_time_pre); - htsmsg_add_u32(r, "postExtraTime", cfg->dvr_extra_time_post); - htsmsg_add_u32(r, "dayDirs", !!(cfg->dvr_flags & DVR_DIR_PER_DAY)); - htsmsg_add_u32(r, "channelDirs", !!(cfg->dvr_flags & DVR_DIR_PER_CHANNEL)); - htsmsg_add_u32(r, "channelInTitle", !!(cfg->dvr_flags & DVR_CHANNEL_IN_TITLE)); - htsmsg_add_u32(r, "dateInTitle", !!(cfg->dvr_flags & DVR_DATE_IN_TITLE)); - htsmsg_add_u32(r, "timeInTitle", !!(cfg->dvr_flags & DVR_TIME_IN_TITLE)); - htsmsg_add_u32(r, "whitespaceInTitle", !!(cfg->dvr_flags & DVR_WHITESPACE_IN_TITLE)); - htsmsg_add_u32(r, "titleDirs", !!(cfg->dvr_flags & DVR_DIR_PER_TITLE)); - htsmsg_add_u32(r, "episodeInTitle", !!(cfg->dvr_flags & DVR_EPISODE_IN_TITLE)); - htsmsg_add_u32(r, "cleanTitle", !!(cfg->dvr_flags & DVR_CLEAN_TITLE)); - htsmsg_add_u32(r, "tagFiles", !!(cfg->dvr_flags & DVR_TAG_FILES)); - htsmsg_add_u32(r, "commSkip", !!(cfg->dvr_flags & DVR_SKIP_COMMERCIALS)); - htsmsg_add_u32(r, "subtitleInTitle", !!(cfg->dvr_flags & DVR_SUBTITLE_IN_TITLE)); - htsmsg_add_u32(r, "episodeBeforeDate", !!(cfg->dvr_flags & DVR_EPISODE_BEFORE_DATE)); - htsmsg_add_u32(r, "episodeDuplicateDetection", !!(cfg->dvr_flags & DVR_EPISODE_DUPLICATE_DETECTION)); - - out = json_single_record(r, "dvrSettings"); - - } else if(!strcmp(op, "saveSettings")) { - - s = http_arg_get(&hc->hc_req_args, "config_name"); - cfg = dvr_config_find_by_name(s); - if (cfg == NULL) - cfg = dvr_config_create(s); - - tvhlog(LOG_INFO,"dvr","Saving configuration '%s'", cfg->dvr_config_name); - - if((s = http_arg_get(&hc->hc_req_args, "storage")) != NULL) - dvr_storage_set(cfg,s); - - if((s = http_arg_get(&hc->hc_req_args, "charset")) != NULL) - dvr_charset_set(cfg,s); - - if((s = http_arg_get(&hc->hc_req_args, "container")) != NULL) - dvr_container_set(cfg,s); - -/* - * Convert 0xxx format permission strings to integer for internal use - * Note no checking that strtol won't overflow int - this should never happen with three-digit numbers - */ - - if((s = http_arg_get(&hc->hc_req_args, "filePermissions")) != NULL) - dvr_file_permissions_set(cfg,(int)strtol(s,NULL,0)); - - if((s = http_arg_get(&hc->hc_req_args, "dirPermissions")) != NULL) - dvr_directory_permissions_set(cfg,(int)strtol(s,NULL,0)); - - if((s = http_arg_get(&hc->hc_req_args, "cache")) != NULL) - dvr_mux_cache_set(cfg,atoi(s)); - - if((s = http_arg_get(&hc->hc_req_args, "postproc")) != NULL) - dvr_postproc_set(cfg,s); - - if((s = http_arg_get(&hc->hc_req_args, "retention")) != NULL) - dvr_retention_set(cfg,atoi(s)); - - if((s = http_arg_get(&hc->hc_req_args, "preExtraTime")) != NULL) - dvr_extra_time_pre_set(cfg,atoi(s)); - - if((s = http_arg_get(&hc->hc_req_args, "postExtraTime")) != NULL) - dvr_extra_time_post_set(cfg,atoi(s)); - - if(http_arg_get(&hc->hc_req_args, "dayDirs") != NULL) - flags |= DVR_DIR_PER_DAY; - if(http_arg_get(&hc->hc_req_args, "channelDirs") != NULL) - flags |= DVR_DIR_PER_CHANNEL; - if(http_arg_get(&hc->hc_req_args, "channelInTitle") != NULL) - flags |= DVR_CHANNEL_IN_TITLE; - if(http_arg_get(&hc->hc_req_args, "cleanTitle") != NULL) - flags |= DVR_CLEAN_TITLE; - if(http_arg_get(&hc->hc_req_args, "dateInTitle") != NULL) - flags |= DVR_DATE_IN_TITLE; - if(http_arg_get(&hc->hc_req_args, "timeInTitle") != NULL) - flags |= DVR_TIME_IN_TITLE; - if(http_arg_get(&hc->hc_req_args, "whitespaceInTitle") != NULL) - flags |= DVR_WHITESPACE_IN_TITLE; - if(http_arg_get(&hc->hc_req_args, "titleDirs") != NULL) - flags |= DVR_DIR_PER_TITLE; - if(http_arg_get(&hc->hc_req_args, "episodeInTitle") != NULL) - flags |= DVR_EPISODE_IN_TITLE; - if(http_arg_get(&hc->hc_req_args, "tagFiles") != NULL) - flags |= DVR_TAG_FILES; - if(http_arg_get(&hc->hc_req_args, "commSkip") != NULL) - flags |= DVR_SKIP_COMMERCIALS; - if(http_arg_get(&hc->hc_req_args, "subtitleInTitle") != NULL) - flags |= DVR_SUBTITLE_IN_TITLE; - if(http_arg_get(&hc->hc_req_args, "episodeBeforeDate") != NULL) - flags |= DVR_EPISODE_BEFORE_DATE; - if(http_arg_get(&hc->hc_req_args, "episodeDuplicateDetection") != NULL) - flags |= DVR_EPISODE_DUPLICATE_DETECTION; - - - dvr_flags_set(cfg,flags); - - /* Muxer flags */ - flags = 0; - if(http_arg_get(&hc->hc_req_args, "rewritePAT") != NULL) - flags |= MC_REWRITE_PAT; - if(http_arg_get(&hc->hc_req_args, "rewritePMT") != NULL) - flags |= MC_REWRITE_PMT; - - dvr_mux_flags_set(cfg, flags); - - out = htsmsg_create_map(); - htsmsg_add_u32(out, "success", 1); - - } else if(!strcmp(op, "deleteSettings")) { - - s = http_arg_get(&hc->hc_req_args, "config_name"); - dvr_config_delete(s); - - out = htsmsg_create_map(); - htsmsg_add_u32(out, "success", 1); - - } else { - - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_BAD_REQUEST; - } - - pthread_mutex_unlock(&global_lock); - - htsmsg_json_serialize(out, hq, 0); - htsmsg_destroy(out); - http_output_content(hc, "text/x-json; charset=UTF-8"); - return 0; - -} - -/** - * - */ -static int -extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque, - dvr_entry_filter filter, dvr_entry_comparator cmp) -{ - htsbuf_queue_t *hq = &hc->hc_reply; - htsmsg_t *out, *array, *m; - dvr_query_result_t dqr; - dvr_entry_t *de; - int start = 0, end, limit, i; - const char *s; - int64_t fsize = 0; - char buf[100]; - - if((s = http_arg_get(&hc->hc_req_args, "start")) != NULL) - start = atoi(s); - - if((s = http_arg_get(&hc->hc_req_args, "limit")) != NULL) - limit = atoi(s); - else - limit = 20; /* XXX */ - - pthread_mutex_lock(&global_lock); - - if(http_access_verify(hc, ACCESS_RECORDER)) { - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_UNAUTHORIZED; - } - - out = htsmsg_create_map(); - array = htsmsg_create_list(); - - - dvr_query_filter(&dqr, filter); - - dvr_query_sort_cmp(&dqr, cmp); - - htsmsg_add_u32(out, "totalCount", dqr.dqr_entries); - - start = MIN(start, dqr.dqr_entries); - end = MIN(start + limit, dqr.dqr_entries); - - for(i = start; i < end; i++) { - de = dqr.dqr_array[i]; - - m = htsmsg_create_map(); - - htsmsg_add_str(m, "channel", DVR_CH_NAME(de)); - if(de->de_channel != NULL) { - htsmsg_add_str(m, "channelid", channel_get_uuid(de->de_channel)); - if (de->de_channel->ch_icon) - htsmsg_add_imageurl(m, "chicon", "imagecache/%d", - de->de_channel->ch_icon); - } - - htsmsg_add_str(m, "config_name", de->de_config_name); - - if(de->de_title != NULL) - htsmsg_add_str(m, "title", lang_str_get(de->de_title, NULL)); - - if(de->de_desc != NULL) - htsmsg_add_str(m, "description", lang_str_get(de->de_desc, NULL)); - - if (de->de_bcast && de->de_bcast->episode) - if (epg_episode_number_format(de->de_bcast->episode, buf, 100, NULL, "Season %d", ".", "Episode %d", "/%d")) - htsmsg_add_str(m, "episode", buf); - - htsmsg_add_u32(m, "id", de->de_id); - htsmsg_add_u32(m, "start", de->de_start); - htsmsg_add_u32(m, "end", de->de_stop); - htsmsg_add_u32(m, "duration", de->de_stop - de->de_start); - - htsmsg_add_str(m, "creator", de->de_creator); - - htsmsg_add_str(m, "pri", dvr_val2pri(de->de_pri)); - - htsmsg_add_str(m, "status", dvr_entry_status(de)); - htsmsg_add_str(m, "schedstate", dvr_entry_schedstatus(de)); - - - if(de->de_sched_state == DVR_COMPLETED) { - fsize = dvr_get_filesize(de); - if (fsize > 0) { - char url[100]; - htsmsg_add_s64(m, "filesize", fsize); - snprintf(url, sizeof(url), "dvrfile/%d", de->de_id); - htsmsg_add_str(m, "url", url); - } - } - - htsmsg_add_msg(array, NULL, m); - } - - dvr_query_free(&dqr); - - pthread_mutex_unlock(&global_lock); - - htsmsg_add_msg(out, "entries", array); - - htsmsg_json_serialize(out, hq, 0); - htsmsg_destroy(out); - http_output_content(hc, "text/x-json; charset=UTF-8"); - return 0; -} - -static int is_dvr_entry_finished(dvr_entry_t *entry) -{ - dvr_entry_sched_state_t state = entry->de_sched_state; - return state == DVR_COMPLETED && !entry->de_last_error && dvr_get_filesize(entry) != -1; -} - -static int is_dvr_entry_upcoming(dvr_entry_t *entry) -{ - dvr_entry_sched_state_t state = entry->de_sched_state; - return state == DVR_RECORDING || state == DVR_SCHEDULED; -} - - -static int is_dvr_entry_failed(dvr_entry_t *entry) -{ - if (is_dvr_entry_finished(entry)) - return 0; - if (is_dvr_entry_upcoming(entry)) - return 0; - return 1; -} - -static int -extjs_dvrlist_finished(http_connection_t *hc, const char *remain, void *opaque) -{ - return extjs_dvrlist(hc, remain, opaque, is_dvr_entry_finished, dvr_sort_start_descending); -} - -static int -extjs_dvrlist_upcoming(http_connection_t *hc, const char *remain, void *opaque) -{ - return extjs_dvrlist(hc, remain, opaque, is_dvr_entry_upcoming, dvr_sort_start_ascending); -} - -static int -extjs_dvrlist_failed(http_connection_t *hc, const char *remain, void *opaque) -{ - return extjs_dvrlist(hc, remain, opaque, is_dvr_entry_failed, dvr_sort_start_descending); -} - -/** - * - */ -void -extjs_service_delete(htsmsg_t *in) -{ - htsmsg_field_t *f; - service_t *t; - const char *id; - - TAILQ_FOREACH(f, &in->hm_fields, hmf_link) { - if((id = htsmsg_field_get_string(f)) != NULL && - (t = service_find_by_identifier(id)) != NULL) - service_destroy(t, 1); - } -} - /** * */ @@ -1700,17 +1066,9 @@ extjs_start(void) http_path_add("/capabilities", NULL, extjs_capabilities, ACCESS_WEB_INTERFACE); http_path_add("/tablemgr", NULL, extjs_tablemgr, ACCESS_WEB_INTERFACE); http_path_add("/epggrab", NULL, extjs_epggrab, ACCESS_WEB_INTERFACE); - http_path_add("/confignames", NULL, extjs_confignames, ACCESS_WEB_INTERFACE); http_path_add("/epg", NULL, extjs_epg, ACCESS_WEB_INTERFACE); http_path_add("/epgrelated", NULL, extjs_epgrelated, ACCESS_WEB_INTERFACE); http_path_add("/epgobject", NULL, extjs_epgobject, ACCESS_WEB_INTERFACE); - http_path_add("/dvr", NULL, extjs_dvr, ACCESS_WEB_INTERFACE); - http_path_add("/dvrlist_upcoming", NULL, extjs_dvrlist_upcoming, ACCESS_WEB_INTERFACE); - http_path_add("/dvrlist_finished", NULL, extjs_dvrlist_finished, ACCESS_WEB_INTERFACE); - http_path_add("/dvrlist_failed", NULL, extjs_dvrlist_failed, ACCESS_WEB_INTERFACE); - http_path_add("/dvr_containers", NULL, extjs_dvr_containers, ACCESS_WEB_INTERFACE); - http_path_add("/dvr_caches", NULL, extjs_dvr_caches, ACCESS_WEB_INTERFACE); - http_path_add("/ecglist", NULL, extjs_ecglist, ACCESS_WEB_INTERFACE); http_path_add("/config", NULL, extjs_config, ACCESS_WEB_INTERFACE); http_path_add("/languages", NULL, extjs_languages, ACCESS_WEB_INTERFACE); #if ENABLE_TIMESHIFT diff --git a/src/webui/simpleui.c b/src/webui/simpleui.c index 9bd698d96..b4db578f0 100644 --- a/src/webui/simpleui.c +++ b/src/webui/simpleui.c @@ -171,7 +171,7 @@ page_simple(http_connection_t *hc, rstatus = val2str(de->de_sched_state, recstatustxt); - htsbuf_qprintf(hq, "", de->de_id); + htsbuf_qprintf(hq, "", idnode_uuid_as_str(&de->de_id)); htsbuf_qprintf(hq, "%02d:%02d-%02d:%02d  %s", @@ -216,7 +216,7 @@ page_einfo(http_connection_t *hc, const char *remain, void *opaque) de = dvr_entry_find_by_event(e); if((http_arg_get(&hc->hc_req_args, "rec")) != NULL) { - de = dvr_entry_create_by_event("", e, 0, 0, hc->hc_username ?: "anonymous", NULL, + de = dvr_entry_create_by_event(NULL, e, 0, 0, hc->hc_username ?: "anonymous", NULL, DVR_PRIO_NORMAL); } else if(de != NULL && (http_arg_get(&hc->hc_req_args, "cancel")) != NULL) { de = dvr_entry_cancel(de); @@ -328,8 +328,8 @@ page_pvrinfo(http_connection_t *hc, const char *remain, void *opaque) if((rstatus = val2str(de->de_sched_state, recstatustxt)) != NULL) htsbuf_qprintf(hq, "Recording status: %s
", rstatus); - htsbuf_qprintf(hq, "
", - de->de_id); + htsbuf_qprintf(hq, "", + idnode_uuid_as_str(&de->de_id)); switch(de->de_sched_state) { case DVR_SCHEDULED: diff --git a/src/webui/static/app/dvr.js b/src/webui/static/app/dvr.js index 35a23e748..79569f5d6 100644 --- a/src/webui/static/app/dvr.js +++ b/src/webui/static/app/dvr.js @@ -1,586 +1,116 @@ -tvheadend.weekdays = new Ext.data.SimpleStore({ - fields: ['identifier', 'name'], - id: 0, - data: [['1', 'Mon'], ['2', 'Tue'], ['3', 'Wed'], ['4', 'Thu'], - ['5', 'Fri'], ['6', 'Sat'], ['7', 'Sun']] -}); - -//This should be loaded from tvheadend -tvheadend.dvrprio = new Ext.data.SimpleStore({ - fields: ['identifier', 'name'], - id: 0, - data: [['important', 'Important'], ['high', 'High'], - ['normal', 'Normal'], ['low', 'Low'], - ['unimportant', 'Unimportant']] -}); - -//For the container configuration -tvheadend.containers = new Ext.data.JsonStore({ - autoLoad: true, - root: 'entries', - fields: ['name', 'description'], - id: 'name', - url: 'dvr_containers', - baseParams: { - op: 'list' - } -}); - -//For the cache configuration -tvheadend.charsets = new Ext.data.JsonStore({ - autoLoad: true, - root: 'entries', - fields: ['key', 'val'], - id: 'key', - url: 'api/intlconv/charsets', - baseParams: { - enum: 1 - } -}); - -//For the charset configuration -tvheadend.caches = new Ext.data.JsonStore({ - autoLoad: true, - root: 'entries', - fields: ['index', 'description'], - id: 'name', - url: 'dvr_caches', - baseParams: { - op: 'list' - } -}); - -/** - * Configuration names +/* + * DVR Config / Schedule / Log editor and viewer */ -tvheadend.configNames = new Ext.data.JsonStore({ - autoLoad: true, - root: 'entries', - fields: ['identifier', 'name'], - id: 'identifier', - url: 'confignames', - baseParams: { - op: 'list' - } -}); - -tvheadend.configNames.setDefaultSort('name', 'ASC'); - -tvheadend.comet.on('dvrconfig', function(m) { - if (m.reload != null) - tvheadend.configNames.reload(); -}); /** * */ -tvheadend.dvrDetails = function(entry) { - - var content = ''; - var but; - - if (entry.chicon != null && entry.chicon.length > 0) - content += ''; +tvheadend.dvrDetails = function(uuid) { + + function showit(d) { + var params = d[0].params; + var chicon = params[0].value; + var title = params[1].value; + var desc = params[2].value; + var status = params[3].value; + var content = ''; + var but; + + if (chicon != null && chicon.length > 0) + content += ''; + + content += '
' + title + '
'; + content += '
' + desc + '
'; + content += '
'; + content += '
Status: ' + status + '
'; + + var win = new Ext.Window({ + title: title, + layout: 'fit', + width: 400, + height: 300, + constrainHeader: true, + buttonAlign: 'center', + html: content + }); - content += '
' + entry.title + '
'; - content += '
' + entry.description + '
'; - content += '
'; - content += '
Status: ' + entry.status + '
'; + win.show(); + } - var win = new Ext.Window({ - title: entry.title, - layout: 'fit', - width: 400, - height: 300, - constrainHeader: true, - buttonAlign: 'center', - html: content + tvheadend.loading(1); + Ext.Ajax.request({ + url: 'api/idnode/load', + params: { + uuid: uuid, + list: 'channel_icon,disp_title,disp_description,status', + }, + success: function(d) { + d = json_decode(d); + tvheadend.loading(0), + showit(d); + }, + failure: function(d) { + tvheadend.loading(0); + } }); - - win.show(); }; -/** - * - */ -tvheadend.dvrschedule = function(title, iconCls, dvrStore) { - - var actions = new Ext.ux.grid.RowActions({ +tvheadend.dvrRowActions = function() { + return new Ext.ux.grid.RowActions({ + id: 'details', header: 'Details', - dataIndex: 'actions', width: 45, actions: [ { - iconIndex: 'schedstate' + iconIndex: 'sched_status' }, { iconCls: 'info', qtip: 'Recording details', cb: function(grid, rec, act, row) { - new tvheadend.dvrDetails(grid.getStore().getAt(row).data); + new tvheadend.dvrDetails(grid.getStore().getAt(row).id); } } ] }); +} - function renderDate(value) { - var dt = new Date(value); - return dt.format('D j M H:i'); - } - - function renderDuration(value) { - value = value / 60; /* Nevermind the seconds */ - - if (value >= 60) { - var min = parseInt(value % 60); - var hours = parseInt(value / 60); - - if (min === 0) { - return hours + ' hrs'; - } - return hours + ' hrs, ' + min + ' min'; - } - else { - return parseInt(value) + ' min'; - } - } - - function renderSize(value) - { - if (value == null) - return ''; - return parseInt(value / 1000000) + ' MB'; - } - - function renderPri(value) { - return tvheadend.dvrprio.getById(value).data.name; - } - - var cols = [actions]; - if (iconCls === 'television') - cols.push({ - width: 40, - header: "Play", - renderer: function(v, o, r) { - var title = r.data['title']; - if (r.data['episode']) - title += ' / ' + r.data['episode']; - return '
Play'; - } - }); - cols.push({ - width: 250, - id: 'title', - header: "Title", - sortable: true, - dataIndex: 'title' - }); - cols.push({ - width: 100, - id: 'episode', - header: "Episode", - sortable: true, - dataIndex: 'episode' - }); - if (iconCls === 'clock') - cols.push({ - width: 100, - id: 'pri', - header: "Priority", - sortable: true, - dataIndex: 'pri', - renderer: renderPri, - hidden: iconCls !== 'clock' - }); - cols.push({ - width: 100, - id: 'start', - header: iconCls === 'clock' ? "Start" : "Date/Time", - sortable: true, - dataIndex: 'start', - renderer: renderDate - }); - cols.push({ - width: 100, - hidden: true, - id: 'end', - header: "End", - sortable: true, - dataIndex: 'end', - renderer: renderDate - }); - cols.push({ - width: 100, - id: 'duration', - header: "Duration", - sortable: true, - dataIndex: 'duration', - renderer: renderDuration - }); - if (iconCls === 'television') - cols.push({ - width: 100, - id: 'filesize', - header: "Filesize", - sortable: true, - dataIndex: 'filesize', - renderer: renderSize, - hidden: iconCls !== 'television' - }); - cols.push({ - width: 250, - id: 'channel', - header: "Channel", - sortable: true, - dataIndex: 'channel' - }); - cols.push({ - width: 200, - id: 'creator', - header: "Created by", - sortable: true, - hidden: true, - dataIndex: 'creator' - }); - if (iconCls === 'clock') - cols.push({ - width: 200, - id: 'config_name', - header: "DVR Configuration", - sortable: true, - renderer: function(value, metadata, record, row, col, store) { - if (!value) { - return '(default)'; - } - else { - return value; - } - }, - dataIndex: 'config_name', - hidden: iconCls !== 'clock' - }); - if (iconCls === 'exclamation') - cols.push({ - width: 200, - id: 'status', - header: "Status", - sortable: true, - dataIndex: 'status', - hidden: iconCls !== 'exclamation' - }); - - var dvrCm = new Ext.grid.ColumnModel({columns: cols}); - - function addEntry() { - - function createRecording() { - panel.getForm().submit({ - params: { - 'op': 'createEntry' - }, - url: 'dvr/addentry', - waitMsg: 'Creating entry...', - failure: function(response, options) { - Ext.MessageBox.alert('Server Error', 'Unable to create entry'); - }, - success: function() { - win.close(); - } - }); - } - - var panel = new Ext.FormPanel({ - frame: true, - border: true, - bodyStyle: 'padding:5px', - labelAlign: 'right', - labelWidth: 110, - defaultType: 'textfield', - items: [new Ext.form.ComboBox({ - fieldLabel: 'Channel', - name: 'channel', - hiddenName: 'channelid', - editable: false, - allowBlank: false, - displayField: 'val', - valueField: 'key', - mode: 'remote', - triggerAction: 'all', - store: tvheadend.channels - }), new Ext.form.DateField({ - allowBlank: false, - fieldLabel: 'Date', - name: 'date' - }), new Ext.form.TimeField({ - allowBlank: false, - fieldLabel: 'Start time', - name: 'starttime', - increment: 10, - format: 'H:i' - }), new Ext.form.TimeField({ - allowBlank: false, - fieldLabel: 'Stop time', - name: 'stoptime', - increment: 10, - format: 'H:i' - }), new Ext.form.ComboBox({ - store: tvheadend.dvrprio, - value: "normal", - triggerAction: 'all', - mode: 'local', - fieldLabel: 'Priority', - valueField: 'identifier', - displayField: 'name', - name: 'pri' - }), { - allowBlank: false, - fieldLabel: 'Title', - name: 'title' - }, new Ext.form.ComboBox({ - store: tvheadend.configNames, - triggerAction: 'all', - mode: 'local', - fieldLabel: 'DVR Configuration', - valueField: 'identifier', - displayField: 'name', - name: 'config_name', - emptyText: '(default)', - value: '', - editable: false - })], - buttons: [{ - text: 'Create', - handler: createRecording - }] - }); - - win = new Ext.Window({ - title: 'Add single recording', - layout: 'fit', - width: 500, - height: 300, - plain: true, - items: panel - }); - win.show(); - new Ext.form.ComboBox({ - store: tvheadend.configNames, - triggerAction: 'all', - mode: 'local', - fieldLabel: 'DVR Configuration', - valueField: 'identifier', - displayField: 'name', - name: 'config_name', - emptyText: '(default)', - value: '', - editable: false - }); - } - ; - - /* Create combobox to allow user to select page size for upcoming/completed/failed recordings */ - - var itemPageCombo = new Ext.form.ComboBox({ - name : 'itemsperpage', - width: 50, - mode : 'local', - store: new Ext.data.ArrayStore({ - fields: ['perpage','value'], - data : [['10',10],['20',20],['30',30],['40',40],['50',50],['75',75],['100',100],['All',9999999999]] - }), - value : '20', - listWidth : 40, - triggerAction : 'all', - displayField : 'perpage', - valueField : 'value', - editable : true, - forceSelection : true, - listeners : { - scope: this, - 'select' : function(combo, record) { - bbar.pageSize = parseInt(record.get('value'), 10); - bbar.doLoad(bbar.cursor); - } - } - }); - - /* Bottom toolbar to include default previous/goto-page/next and refresh buttons, also number-of-items combobox */ - - var bbar = new Ext.PagingToolbar({ - store : dvrStore, - displayInfo : true, - items : ['-','Recordings per page: ',itemPageCombo], - displayMsg : 'Programs {0} - {1} of {2}', - emptyMsg : "No programs to display" - }); - - function abortEntry(btn) { - if (btn !== 'yes') - return; - - var selectedKeys = panel.selModel.selections.keys; - - // Delete each entry one by one since the API doesn't support deleting - // multiple - for (var i = 0; i < selectedKeys.length; i++) { - var recordingId = selectedKeys[i]; - - Ext.Ajax.request({ - url: 'dvr', - params: { - entryId: recordingId, - op: 'cancelEntry' - }, - failure: function(response, options) { - Ext.MessageBox.alert('Server Error', 'Unable to cancel recording'); - } - }); - } - }; - - function deleteEntry(btn) { - if (btn !== 'yes') - return; - - var selectedKeys = panel.selModel.selections.keys; - - // Delete each entry one by one since the API doesn't support deleting - // multiple - for (var i = 0; i < selectedKeys.length; i++) { - var recordingId = selectedKeys[i]; - - Ext.Ajax.request({ - url: 'dvr', - params: { - entryId: recordingId, - op: 'deleteEntry' - }, - success: function(response, options) { - - }, - failure: function(response, options) { - Ext.MessageBox.alert('Server Error', 'Unable to delete recording'); - } - }); - } - }; - - function downloadSelected() { - var selectedKey = panel.selModel.selections.keys[0]; - var entry = dvrStore.getById(selectedKey); - - window.location = entry.data.url; - } - - function abortSelected() { - Ext.MessageBox.confirm('Message', - 'Do you really want to abort/unschedule the selection?', abortEntry); - }; - - function deleteSelected() { - Ext.MessageBox.confirm('Message', - 'Do you really want to delete the selection?', deleteEntry); - }; - - var abortButton = new Ext.Toolbar.Button({ - tooltip: 'Abort or unschedule one or more selected rows', - iconCls: 'remove', - text: 'Abort/unschedule selected', - handler: abortSelected, - disabled: true - }); - - var downloadButton = new Ext.Toolbar.Button({ - tooltip: 'Download the selected recording', - iconCls: 'save', - text: 'Download', - handler: downloadSelected, - disabled: true - }); - - var deleteButton = new Ext.Toolbar.Button({ - tooltip: 'Delete one or more selected rows', - iconCls: 'remove', - text: 'Delete selected', - handler: deleteSelected, - disabled: true - }); - - // Make multiple rows selectable - var selModel = new Ext.grid.RowSelectionModel({ - singleSelect: false - }); - - // Enable/disable some buttons when nothing is selected - selModel.on('selectionchange', function(self) { - if (self.getCount() > 0) { - deleteButton.enable(); - abortButton.enable(); - - // It only makes sense to download one item at a time - if (self.getCount() === 1) - downloadButton.enable(); - else - downloadButton.disable(); - } - else { - downloadButton.disable(); - deleteButton.disable(); - abortButton.disable(); - } - }); - - // Define which panel buttons should be visible - var panelButtons = []; - - // Add the "Add entry" and "Abort" buttons only to "Upcoming recordings" - if (iconCls === 'clock') { - panelButtons.push([ - { - tooltip: 'Schedule a new recording session on the server.', - iconCls: 'add', - text: 'Add entry', - handler: addEntry +/** + * + */ +tvheadend.dvr_upcoming = function(panel, index) { + + var actions = tvheadend.dvrRowActions(); + + tvheadend.idnode_grid(panel, { + url: 'api/dvr/entry', + gridURL: 'api/dvr/entry/grid_upcoming', + comet: 'dvrentry', + titleS: 'Upcoming Recording', + titleP: 'Upcoming Recordings', + iconCls: 'clock', + tabIndex: index, + add: { + url: 'api/dvr/entry', + params: { + list: 'disp_title,start,start_extra,stop,stop_extra,' + + 'channel,config_name', }, - abortButton - ]); - } - // Add the "Download" and "Delete" button to the others - else { - panelButtons.push(downloadButton); - panelButtons.push(deleteButton); - } - - // Add the help button to all panels - panelButtons.push([ - '->', - { - text: 'Help', - handler: function() { - new tvheadend.help('Digital Video Recorder', 'dvrlog.html'); - } - } - ]); - - var panel = new Ext.grid.GridPanel({ - stateful: true, - stateId: dvrStore.url, - loadMask: true, - stripeRows: true, - disableSelection: false, - title: title, - iconCls: iconCls, - store: dvrStore, - selModel: selModel, - cm: dvrCm, + create: { } + }, + del: true, + list: 'disp_title,episode,pri,start_real,stop_real,' + + 'duration,channelname,creator,config_name,' + + 'sched_status', + sort: { + field: 'start', + direction: 'DESC' + }, plugins: [actions], - viewConfig: { - forceFit: true + lcol: [actions], + help: function() { + new tvheadend.help('DVR', 'config_dvr.html'); }, - tbar: panelButtons, - bbar: bbar }); return panel; @@ -589,710 +119,164 @@ tvheadend.dvrschedule = function(title, iconCls, dvrStore) { /** * */ -tvheadend.autoreceditor = function() { - var fm = Ext.form; - - var cm = new Ext.grid.ColumnModel({ - defaultSortable: true, - columns: - [ - { - header: 'Enabled', - dataIndex: 'enabled', - width: 30, - xtype: 'checkcolumn' - }, - { - header: "Title (Regexp)", - dataIndex: 'title', - editor: new fm.TextField({ - allowBlank: true - }) - }, - { - header: "Channel", - dataIndex: 'channel', - editor: new Ext.form.ComboBox({ - loadingText: 'Loading...', - displayField: 'val', - valueField: 'key', - store: tvheadend.channels, - mode: 'local', - editable: true, - forceSelection: true, - typeAhead: true, - triggerAction: 'all', - emptyText: 'Only include channel...' - }), - renderer: function(v) { - return tvheadend.channelLookupName(v); - }, - }, - { - header: "SeriesLink", - dataIndex: 'serieslink', - renderer: function(v) { - return v ? 'yes' : 'no'; - } - }, - { - header: "Channel tag", - dataIndex: 'tag', - editor: new Ext.form.ComboBox({ - displayField: 'val', - store: tvheadend.channelTags, - mode: 'local', - editable: true, - forceSelection: true, - typeAhead: true, - triggerAction: 'all', - emptyText: 'Only include tag...' - }) - }, - { - header: "Genre", - dataIndex: 'contenttype', - renderer: function(v) { - return tvheadend.contentGroupLookupName(v); - }, - editor: new Ext.form.ComboBox({ - valueField: 'code', - displayField: 'name', - store: tvheadend.ContentGroupStore, - mode: 'local', - editable: true, - forceSelection: true, - typeAhead: true, - triggerAction: 'all', - emptyText: 'Only include content...' - }) - }, - { - header: "Duration", - dataIndex: 'minduration', - renderer: function(v) { - return tvheadend.durationLookupRange(v); - }, - editor: durationCombo = new Ext.form.ComboBox({ - store: tvheadend.DurationStore, - mode: 'local', - valueField: 'minvalue', - displayField: 'label', - editable: true, - forceSelection: true, - typeAhead: true, - triggerAction: 'all', - id: 'minfield' - }) - }, - { - header: "Weekdays", - dataIndex: 'weekdays', - renderer: function(value, metadata, record, row, col, store) { - if (value.split) - value = value.split(','); - if (value.length === 7) - return 'All days'; - if (value.length === 0 || value[0] === "") - return 'No days'; - ret = []; - tags = value; - for (var i = 0; i < tags.length; i++) { - var tag = tvheadend.weekdays.getById(tags[i]); - if (typeof tag !== 'undefined') - ret.push(tag.data.name); - } - return ret.join(', '); - }, - editor: new Ext.ux.form.LovCombo({ - store: tvheadend.weekdays, - mode: 'local', - valueField: 'identifier', - displayField: 'name' - }) - }, { - header: "Starting Around", - dataIndex: 'approx_time', - renderer: function(value, metadata, record, row, col, store) { - if (typeof value === 'string') - return value; - - if (value === 0) - return ''; - - var hours = Math.floor(value / 60); - var mins = value % 60; - var dt = new Date(); - dt.setHours(hours); - dt.setMinutes(mins); - return dt.format('H:i'); - }, - editor: new Ext.form.TimeField({ - allowBlank: true, - increment: 10, - format: 'H:i' - }) - }, { - header: "Priority", - dataIndex: 'pri', - width: 100, - renderer: function(value, metadata, record, row, col, store) { - return tvheadend.dvrprio.getById(value).data.name; - }, - editor: new fm.ComboBox({ - store: tvheadend.dvrprio, - triggerAction: 'all', - mode: 'local', - valueField: 'identifier', - displayField: 'name' - }) - }, { - header: "DVR Configuration", - dataIndex: 'config_name', - renderer: function(value, metadata, record, row, col, store) { - if (!value) { - return '(default)'; - } - else { - return value; - } - }, - editor: new Ext.form.ComboBox({ - store: tvheadend.configNames, - triggerAction: 'all', - mode: 'local', - valueField: 'identifier', - displayField: 'name', - name: 'config_name', - emptyText: '(default)', - editable: false - }) - }, { - header: "Created by", - dataIndex: 'creator', - editor: new fm.TextField({ - allowBlank: false - }) - }, { - header: "Comment", - dataIndex: 'comment', - editor: new fm.TextField({ - allowBlank: false - }) - }]}); - - tvheadend.autorecStore.on('update', function (store, record, operation) { - if (operation == 'edit') { - if (record.isModified('minduration')) { - if (record.data.minduration == "") - record.set('maxduration',""); - else { - var index = tvheadend.DurationStore.find('minvalue', record.data.minduration); - - if (index !== -1) - record.set('maxduration', tvheadend.DurationStore.getById(index).data.maxvalue); +tvheadend.dvr_finished = function(panel, index) { + + var actions = tvheadend.dvrRowActions(); + + tvheadend.idnode_grid(panel, { + url: 'api/dvr/entry', + gridURL: 'api/dvr/entry/grid_finished', + readonly: true, + comet: 'dvrentry', + titleS: 'Finished Recording', + titleP: 'Finished Recordings', + iconCls: 'television', + tabIndex: index, + del: true, + list: 'disp_title,episode,start_real,stop_real,' + + 'duration,filesize,channelname,creator,' + + 'sched_status', + sort: { + field: 'start', + direction: 'DESC' + }, + plugins: [actions], + lcol: [ + actions, + { + width: 40, + header: "Play", + renderer: function(v, o, r) { + var title = r.data['title']; + if (r.data['episode']) + title += ' / ' + r.data['episode']; + return 'Play'; } - } - - if (record.isModified('channel') && record.data.channel == -1) - record.set('channel',""); - - if (record.isModified('tag') && record.data.tag == '(Clear filter)') - record.set('tag',""); - - if (record.isModified('contenttype') && record.data.contenttype == -1) - record.set('contenttype',""); - } + }], + help: function() { + new tvheadend.help('DVR', 'config_dvr.html'); + }, }); - return new tvheadend.tableEditor('Automatic Recorder', 'autorec', cm, - tvheadend.autorecRecord, [], tvheadend.autorecStore, - 'autorec.html', 'wand'); + return panel; }; /** * */ -tvheadend.dvr = function() { - - function datastoreBuilder(url) { - return new Ext.data.JsonStore({ - root: 'entries', - totalProperty: 'totalCount', - fields: [{ - name: 'id' - }, { - name: 'channel' - }, { - name: 'title' - }, { - name: 'episode' - }, { - name: 'pri' - }, { - name: 'description' - }, { - name: 'chicon' - }, { - name: 'start', - type: 'date', - dateFormat: 'U' /* unix time */ - }, { - name: 'end', - type: 'date', - dateFormat: 'U' /* unix time */ - }, { - name: 'config_name' - }, { - name: 'status' - }, { - name: 'schedstate' - }, { - name: 'error' - }, { - name: 'creator' - }, { - name: 'duration' - }, { - name: 'filesize' - }, { - name: 'url' - }], - url: url, - autoLoad: true, - id: 'id' - }); - } - tvheadend.dvrStoreUpcoming = datastoreBuilder('dvrlist_upcoming'); - tvheadend.dvrStoreFinished = datastoreBuilder('dvrlist_finished'); - tvheadend.dvrStoreFailed = datastoreBuilder('dvrlist_failed'); - tvheadend.dvrStores = [tvheadend.dvrStoreUpcoming, - tvheadend.dvrStoreFinished, - tvheadend.dvrStoreFailed]; - - - function reloadStores() { - for (var i = 0; i < tvheadend.dvrStores.length; i++) { - tvheadend.dvrStores[i].reload(); - } - } - - tvheadend.comet.on('dvrdb', function(m) { - reloadStores(); - }); - - tvheadend.autorecRecord = Ext.data.Record.create(['enabled', 'title', - 'serieslink', 'channel', 'tag', 'creator', 'contenttype', 'comment', - 'minduration', 'maxduration', - 'weekdays', 'pri', 'approx_time', 'config_name']); - - tvheadend.autorecStore = new Ext.data.JsonStore({ - root: 'entries', - fields: tvheadend.autorecRecord, - url: "tablemgr", - autoLoad: true, - id: 'id', - baseParams: { - table: "autorec", - op: "get" - } - }); - - tvheadend.comet.on('autorec', function(m) { - if (m.reload != null) - tvheadend.autorecStore.reload(); +tvheadend.dvr_failed = function(panel, index) { + + var actions = tvheadend.dvrRowActions(); + + tvheadend.idnode_grid(panel, { + url: 'api/dvr/entry', + gridURL: 'api/dvr/entry/grid_failed', + readonly: true, + comet: 'dvrentry', + titleS: 'Failed Recording', + titleP: 'Failed Recordings', + iconCls: 'exclamation', + tabIndex: index, + del: true, + list: 'disp_title,episode,start_real,stop_real,' + + 'duration,channelname,creator,' + + 'status,sched_status', + sort: { + field: 'start', + direction: 'DESC' + }, + plugins: [actions], + lcol: [actions], + help: function() { + new tvheadend.help('DVR', 'config_dvr.html'); + }, }); - var panel = new Ext.TabPanel({ - activeTab: 0, - autoScroll: true, - title: 'Digital Video Recorder', - iconCls: 'drive', - items: [ - new tvheadend.dvrschedule('Upcoming recordings', 'clock', tvheadend.dvrStoreUpcoming), - new tvheadend.dvrschedule('Finished recordings', 'television', tvheadend.dvrStoreFinished), - new tvheadend.dvrschedule('Failed recordings', 'exclamation', tvheadend.dvrStoreFailed), - new tvheadend.autoreceditor - ] - }); return panel; }; /** - * Configuration panel (located under configuration) + * */ -tvheadend.dvrsettings = function() { - - var confreader = new Ext.data.JsonReader({ - root: 'dvrSettings' - }, ['storage', 'filePermissions', 'dirPermissions', 'postproc', 'retention', 'dayDirs', 'channelDirs', - 'channelInTitle', 'container', 'cache', 'charset', 'dateInTitle', 'timeInTitle', - 'preExtraTime', 'postExtraTime', 'whitespaceInTitle', 'titleDirs', - 'episodeInTitle', 'cleanTitle', 'tagFiles', 'commSkip', 'subtitleInTitle', - 'episodeBeforeDate', 'rewritePAT', 'rewritePMT', 'episodeDuplicateDetection']); - - var confcombo = new Ext.form.ComboBox({ - store: tvheadend.configNames, - triggerAction: 'all', - mode: 'local', - displayField: 'name', - name: 'config_name', - emptyText: '(default)', - value: '', - editable: true - }); - - var delButton = new Ext.Toolbar.Button({ - tooltip: 'Delete named configuration', - iconCls: 'remove', - text: "Delete configuration", - handler: deleteConfiguration, - disabled: true - }); - - /* Config panel variables */ - - /* DVR Behaviour */ - - var recordingContainer = new Ext.form.ComboBox({ - store: tvheadend.containers, - fieldLabel: 'Media container', - triggerAction: 'all', - displayField: 'description', - valueField: 'name', - editable: false, - width: 350, - hiddenName: 'container' - }); - - var cacheScheme = new Ext.form.ComboBox({ - store: tvheadend.caches, - fieldLabel: 'Cache scheme', - triggerAction: 'all', - displayField: 'description', - valueField: 'index', - editable: false, - width: 350, - hiddenName: 'cache' - }); - - var logRetention = new Ext.form.NumberField({ - allowNegative: false, - allowDecimals: false, - minValue: 1, - fieldLabel: 'DVR Log retention time (days)', - name: 'retention' - }); - - var timeBefore = new Ext.form.NumberField({ - allowDecimals: false, - fieldLabel: 'Extra time before recordings (minutes)', - name: 'preExtraTime' - }); - - var timeAfter = new Ext.form.NumberField({ - allowDecimals: false, - fieldLabel: 'Extra time after recordings (minutes)', - name: 'postExtraTime' - }); - - var postProcessing = new Ext.form.TextField({ - width: 350, - fieldLabel: 'Post-processor command', - name: 'postproc' - }); - - /* Recording File Options */ - - var recordingPath = new Ext.form.TextField({ - width: 350, - fieldLabel: 'Recording system path', - name: 'storage' - }); - - /* NB: recordingPermissions is defined as a TextField for validation purposes (leading zeros), but is ultimately a number */ - - var recordingPermissions = new Ext.form.TextField({ - regex: /^[0][0-7]{3}$/, - maskRe: /[0-7]/, - width: 125, - allowBlank: false, - blankText: 'You must provide a value - use octal chmod notation, e.g. 0664', - fieldLabel: 'File permissions (octal, e.g. 0664)', - name: 'filePermissions' - }); - - var charset = new Ext.form.ComboBox({ - store: tvheadend.charsets, - fieldLabel: 'Filename charset', - triggerAction: 'all', - displayField: 'val', - valueField: 'key', - editable: false, - width: 200, - hiddenName: 'charset' - }); - - /* TO DO - Add 'override user umask?' option, then trigger fchmod in mkmux.c, muxer_pass.c after file created */ - - var PATrewrite = new Ext.form.Checkbox({ - fieldLabel: 'Rewrite PAT in passthrough mode', - name: 'rewritePAT' - }); - - var PMTrewrite = new Ext.form.Checkbox({ - fieldLabel: 'Rewrite PMT in passthrough mode', - name: 'rewritePMT' - }); - - var tagMetadata = new Ext.form.Checkbox({ - fieldLabel: 'Tag files with metadata', - name: 'tagFiles' - }); - - var skipCommercials = new Ext.form.Checkbox({ - fieldLabel: 'Skip commercials', - name: 'commSkip' - }); - - var episodeDuplicateDetection = new Ext.form.Checkbox({ - fieldLabel: 'Episode Duplicate Detect', - name: 'episodeDuplicateDetection' - }); - - /* Subdirectories and filename handling */ - - /* NB: directoryPermissions is defined as a TextField for validation purposes (leading zeros), but is ultimately a number */ - - var directoryPermissions = new Ext.form.TextField({ - regex: /^[0][0-7]{3}$/, - maskRe: /[0-7]/, - width: 125, - allowBlank: false, - blankText: 'You must provide a value - use octal chmod notation, e.g. 0775', - fieldLabel: 'Directory permissions (octal, e.g. 0775)', - name: 'dirPermissions' - }); - - /* TO DO - Add 'override user umask?' option, then trigger fchmod in utils.c after directory created */ - - var dirsPerDay = new Ext.form.Checkbox({ - fieldLabel: 'Make subdirectories per day', - name: 'dayDirs' - }); - - var dirsPerChannel = new Ext.form.Checkbox({ - fieldLabel: 'Make subdirectories per channel', - name: 'channelDirs' - }); - - var dirsPerTitle = new Ext.form.Checkbox({ - fieldLabel: 'Make subdirectories per title', - name: 'titleDirs' - }); - - var incChannelInTitle = new Ext.form.Checkbox({ - fieldLabel: 'Include channel name in filename', - name: 'channelInTitle' - }); - - var incDateInTitle = new Ext.form.Checkbox({ - fieldLabel: 'Include date in filename', - name: 'dateInTitle' - }); - - var incTimeInTitle = new Ext.form.Checkbox({ - fieldLabel: 'Include time in filename', - name: 'timeInTitle' - }); - - var incEpisodeInTitle = new Ext.form.Checkbox({ - fieldLabel: 'Include episode in filename', - name: 'episodeInTitle' - }); - - var incSubtitleInTitle = new Ext.form.Checkbox({ - fieldLabel: 'Include subtitle in filename', - name: 'subtitleInTitle' - }); - - var episodeFirst = new Ext.form.Checkbox({ - fieldLabel: 'Put episode in filename before date and time', - name: 'episodeBeforeDate' - }); - - var stripUnsafeChars = new Ext.form.Checkbox({ - fieldLabel: 'Remove all unsafe characters from filename', - name: 'cleanTitle' - }); - - var stripWhitespace = new Ext.form.Checkbox({ - fieldLabel: 'Replace whitespace in title with \'-\'', - name: 'whitespaceInTitle' - }); - - /* Sub-Panel - DVR behaviour */ - - var DVRBehaviour = new Ext.form.FieldSet({ - title: 'DVR Behaviour', - width: 700, - autoHeight: true, - collapsible: true, - animCollapse: true, - items: [recordingContainer, cacheScheme, logRetention, timeBefore, timeAfter, postProcessing] - }); - - /* Sub-Panel - File Output */ - - var FileOutputPanel = new Ext.form.FieldSet({ - title: 'Recording File Options', - width: 700, - autoHeight: true, - collapsible: true, - animCollapse: true, - items: [recordingPath, recordingPermissions, charset, PATrewrite, PMTrewrite, tagMetadata, skipCommercials, episodeDuplicateDetection] - }); - - /* Sub-Panel - Directory operations */ - - var DirHandlingPanel = new Ext.form.FieldSet({ - title: 'Subdirectory Options', - width: 700, - autoHeight: true, - collapsible: true, - animCollapse: true, - items: [directoryPermissions, dirsPerDay, dirsPerChannel, dirsPerTitle] +tvheadend.dvr_settings = function(panel, index) { + tvheadend.idnode_form_grid(panel, { + url: 'api/dvr/config', + clazz: 'dvrconfig', + comet: 'dvrconfig', + titleS: 'Digital Video Recorder Profile', + titleP: 'Digital Video Recorder Profiles', + titleC: 'Profile Name', + iconCls: 'drive', + tabIndex: index, + add: { + url: 'api/dvr/config', + create: { } + }, + del: true, + sort: { + field: 'name', + direction: 'ASC' + }, + help: function() { + new tvheadend.help('DVR', 'config_dvr.html'); + }, }); - /* Sub-Panel - File operations - Break into two 4-item panels */ + return panel; - var FileHandlingPanelA = new Ext.form.FieldSet({ - width: 350, - border: false, - autoHeight: true, - items : [incChannelInTitle, incDateInTitle, incTimeInTitle, incEpisodeInTitle] - }); +} - var FileHandlingPanelB = new Ext.form.FieldSet({ - width: 350, - border: false, - autoHeight: true, - items : [incSubtitleInTitle, episodeFirst, stripUnsafeChars, stripWhitespace] +/** + * + */ +tvheadend.autorec_editor = function(panel, index) { + + tvheadend.idnode_grid(panel, { + url: 'api/dvr/autorec', + comet: 'dvrautorec', + titleS: 'DVR AutoRec Entry', + titleP: 'DVR AutoRec Entries', + iconCls: 'wand', + tabIndex: index, + add: { + url: 'api/dvr/autorec', + params: { + list: 'enable,title,channel,tag,content_type,minduration,' + + 'weekdays,approx_time,pri,config_name,comment', + }, + create: { } + }, + del: true, + list: 'enable,title,channel,tag,content_type,minduration,' + + 'weekdays,approx_time,pri,config_name,creator,comment', + sort: { + field: 'name', + direction: 'ASC' + }, + help: function() { + new tvheadend.help('DVR', 'config_dvr.html'); + }, }); - var FileHandlingPanel = new Ext.form.FieldSet({ - title: 'Filename Options', - width: 700, - autoHeight: true, - collapsible: true, - animCollapse : true, - items : [{ - layout: 'column', - border: false, - items : [FileHandlingPanelA, FileHandlingPanelB] - }] - }); + return panel; - /* Main (form) panel */ +}; - var confpanel = new Ext.FormPanel({ +/** + * + */ +tvheadend.dvr = function() { + var panel = new Ext.TabPanel({ + activeTab: 0, + autoScroll: true, title: 'Digital Video Recorder', iconCls: 'drive', - border: false, - bodyStyle: 'padding:15px', - anchor: '100% 50%', - labelAlign: 'right', - labelWidth: 300, - autoScroll: true, - waitMsgTarget: true, - reader: confreader, - defaultType: 'textfield', - layout: 'form', - items: [DVRBehaviour, FileOutputPanel, DirHandlingPanel, FileHandlingPanel], - tbar: [confcombo, { - tooltip: 'Save changes made to dvr configuration below', - iconCls: 'save', - text: "Save configuration", - handler: saveChanges - }, delButton, '->', { - text: 'Help', - handler: function() { - new tvheadend.help('DVR configuration', 'config_dvr.html'); - } - }] - }); - - function loadConfig() { - confpanel.getForm().load({ - url: 'dvr', - params: { - 'op': 'loadSettings', - 'config_name': confcombo.getValue() - }, - success: function(form, action) { - confpanel.enable(); - } - }); - } - - confcombo.on('select', function() { - if (confcombo.getValue() === '') - delButton.disable(); - else - delButton.enable(); - loadConfig(); - }); - - confpanel.on('render', function() { - loadConfig(); + items: [], }); - - function saveChanges() { - var config_name = confcombo.getValue(); - confpanel.getForm().submit({ - url: 'dvr', - params: { - 'op': 'saveSettings', - 'config_name': config_name - }, - waitMsg: 'Saving Data...', - success: function(form, action) { - confcombo.setValue(config_name); - confcombo.fireEvent('select'); - }, - failure: function(form, action) { - Ext.Msg.alert('Save failed', action.result.errormsg); - } - }); - } - - function deleteConfiguration() { - if (confcombo.getValue() !== "") { - Ext.MessageBox.confirm('Message', - 'Do you really want to delete DVR configuration \'' - + confcombo.getValue() + '\'?', deleteAction); - } - } - - function deleteAction(btn) { - if (btn === 'yes') { - confpanel.getForm().submit({ - url: 'dvr', - params: { - 'op': 'deleteSettings', - 'config_name': confcombo.getValue() - }, - waitMsg: 'Deleting Data...', - success: function(form, action) { - confcombo.setValue(''); - confcombo.fireEvent('select'); - }, - failure: function(form, action) { - Ext.Msg.alert('Delete failed', action.result.errormsg); - } - }); - } - } - - return confpanel; -}; + tvheadend.dvr_upcoming(panel, 0); + tvheadend.dvr_finished(panel, 1); + tvheadend.dvr_failed(panel, 2); + tvheadend.autorec_editor(panel, 3); + return panel; +} diff --git a/src/webui/static/app/epg.js b/src/webui/static/app/epg.js index add7723fa..3446877fb 100644 --- a/src/webui/static/app/epg.js +++ b/src/webui/static/app/epg.js @@ -13,31 +13,22 @@ insertContentGroupClearOption = function( scope, records, options ){ scope.insert(0,new placeholder({name: '(Clear filter)', code: '-1'})); }; -//WIBNI: might want this store to periodically update - -tvheadend.ContentGroupStore = new Ext.data.JsonStore({ - root: 'entries', - fields: ['name', 'code'], - autoLoad: true, - url: 'ecglist', +tvheadend.ContentGroupStore = tvheadend.idnode_get_enum({ + url: 'api/epg/content_type/list', listeners: { - 'load': insertContentGroupClearOption + load: insertContentGroupClearOption } }); tvheadend.contentGroupLookupName = function(code) { ret = ""; tvheadend.ContentGroupStore.each(function(r) { - if (r.data.code === code) - ret = r.data.name; - else if (ret === "" && r.data.code === (code & 0xF0)) - ret = r.data.name; + if (r.data.key === code) + ret = r.data.val; }); return ret; }; -tvheadend.ContentGroupStore.setDefaultSort('code', 'ASC'); - tvheadend.channelLookupName = function(key) { channelString = ""; @@ -103,7 +94,7 @@ tvheadend.epgDetails = function(event) { content += '
' + event.description + '
'; content += '
' + event.starrating + '
'; content += '
' + event.agerating + '
'; - content += '
' + tvheadend.contentGroupLookupName(event.contenttype) + '
'; + content += '
' + tvheadend.contentGroupLookupName(event.content_type) + '
'; if (event.ext_desc != null) content += '
' + event.ext_desc + '
'; @@ -127,12 +118,29 @@ tvheadend.epgDetails = function(event) { '?title=' + encodeURIComponent(title) + '">Play'; } + var store = new Ext.data.JsonStore({ + autoload: true, + root: 'entries', + fields: ['key','val'], + id: 'key', + url: 'api/idnode/load', + baseParams: { + enum: 1, + 'class': 'dvrconfig' + }, + sortInfo: { + field: 'val', + direction: 'ASC' + } + }); + store.load(); + var confcombo = new Ext.form.ComboBox({ - store: tvheadend.configNames, + store: store, triggerAction: 'all', mode: 'local', - valueField: 'identifier', - displayField: 'name', + valueField: 'key', + displayField: 'val', name: 'config_name', emptyText: '(default)', value: '', @@ -158,20 +166,19 @@ tvheadend.epgDetails = function(event) { win.show(); function recordEvent() { - record('recordEvent'); + record('event') } function recordSeries() { - record('recordSeries'); + record('series'); } function record(op) { Ext.Ajax.request({ - url: 'dvr', + url: 'api/dvr/entry/create_by_' + op, params: { - eventId: event.id, - op: op, - config_name: confcombo.getValue() + event_id: event.id, + config_uuid: confcombo.getValue() }, success: function(response, options) { win.close(); @@ -232,7 +239,7 @@ tvheadend.epg = function() { }, { name: 'agerating' }, { - name: 'contenttype' + name: 'content_type' }, { name: 'schedstate' }, { @@ -364,9 +371,9 @@ tvheadend.epg = function() { renderer: renderInt }, { width: 250, - id: 'contenttype', + id: 'content_type', header: "Content Type", - dataIndex: 'contenttype', + dataIndex: 'content_type', renderer: function(v) { return tvheadend.contentGroupLookupName(v); } @@ -490,12 +497,12 @@ tvheadend.epg = function() { }; clearContentGroupFilter = function() { - delete epgStore.baseParams.contenttype; + delete epgStore.baseParams.content_type; epgFilterContentGroup.setValue(""); }; clearDurationFilter = function() { - delete epgStore.baseParams.minduration; + delete epgStore.baseParams.minduration; delete epgStore.baseParams.maxduration; epgFilterDuration.setValue(""); }; @@ -532,8 +539,8 @@ tvheadend.epg = function() { epgFilterContentGroup.on('select', function(c, r) { if (r.data.code == -1) clearContentGroupFilter(); - else if (epgStore.baseParams.contenttype !== r.data.code) - epgStore.baseParams.contenttype = r.data.code; + else if (epgStore.baseParams.content_type !== r.data.code) + epgStore.baseParams.content_type = r.data.code; epgStore.reload(); }); @@ -650,19 +657,18 @@ tvheadend.epg = function() { : "Don't care"; var tag = epgStore.baseParams.tag ? tvheadend.tagLookupName(epgStore.baseParams.tag) : "Don't care"; - var contenttype = epgStore.baseParams.contenttype ? tvheadend.contentGroupLookupName(epgStore.baseParams.contenttype) + var content_type = epgStore.baseParams.content_type ? tvheadend.contentGroupLookupName(epgStore.baseParams.content_type) : "Don't care"; var duration = epgStore.baseParams.minduration ? tvheadend.durationLookupRange(epgStore.baseParams.minduration) : "Don't care"; - Ext.MessageBox.confirm('Auto Recorder', 'This will create an automatic rule that ' + 'continuously scans the EPG for programmes ' + 'to record that match this query: ' + '

' + '
Title:
' + title + '
' + '
Channel:
' + channel + '
' + '
Tag:
' + tag + '
' - + '
Genre:
' + contenttype + '
' + + '
Genre:
' + content_type + '
' + '
Duration:
' + duration + '
' + '

' + 'Currently this will match (and record) ' + epgStore.getTotalCount() + ' events. ' + 'Are you sure?', @@ -675,9 +681,18 @@ tvheadend.epg = function() { function createAutoRec2(params) { /* Really do it */ - params.op = 'createAutoRec'; + params.conf = { + enabled: 1, + comment: 'Created from EPG query', + }; + if (params.title) params['title'] = params.title; + if (params.channel) params['channel'] = params.channel; + if (params.tag) params['tag'] = params.tag; + if (params.content_type) params['content_type'] = params.content_type; + if (params.minduration) params['minduration'] = params.minduration; + if (params.maxduration) params['maxduration'] = params.maxduration; Ext.Ajax.request({ - url: 'dvr', + url: 'api/dvr/autorec/create', params: params }); } diff --git a/src/webui/static/app/extensions.js b/src/webui/static/app/extensions.js index abd485edb..a8df6cb8f 100644 --- a/src/webui/static/app/extensions.js +++ b/src/webui/static/app/extensions.js @@ -7,8 +7,6 @@ */ - - /* * Ext JS Library 2.2 * Copyright(c) 2006-2008, Ext JS, LLC. @@ -1237,3 +1235,782 @@ Ext.ux.form.LovCombo = Ext.extend(Ext.form.ComboBox, { // register xtype Ext.reg('lovcombo', Ext.ux.form.LovCombo); + +/** + * @class Ext.ux.form.TwinDateTimeField + * @extends Ext.form.Field + * + * DateTime field, combination of DateField and TimeField + * + * @author Ing. Jozef Sakáloš + * @copyright (c) 2008, Ing. Jozef Sakáloš + * @version 2.0 + * @revision $Id: Ext.ux.form.TwinDateTimeField.js 813 2010-01-29 23:32:36Z jozo $ + * + * @license Ext.ux.form.TwinDateTimeField is licensed under the terms of the Open Source + * LGPL 3.0 license. Commercial use is permitted to the extent that the + * code/component(s) do NOT become part of another Open Source or + * Commercially licensed development library or toolkit without + * explicit permission. + * + *

+ * License details: http://www.gnu.org/licenses/lgpl.html + *

+ */ + +Ext.ns('Ext.ux.form'); + +// register xtype +//Ext.reg('twindatetime', Ext.ux.form.TwinDateTimeField); +//Ext.namespace('Ext.ux.form'); +Ext.ux.form.TwinDateField = Ext.extend(Ext.form.DateField, { + getTrigger : Ext.form.TwinTriggerField.prototype.getTrigger, + initTrigger : Ext.form.TwinTriggerField.prototype.initTrigger, + initComponent : Ext.form.TwinTriggerField.prototype.initComponent, + trigger2Class : 'x-form-date-trigger', + trigger1Class : 'x-form-clear-trigger', + hideTrigger1 : true, + submitOnSelect : true, + submitOnClear : true, + allowClear : true, + defaultValue : null, + + onSelect : Ext.form.DateField.prototype.onSelect.createSequence(function(v) { + if (this.value && this.ownerCt && this.ownerCt.buttons && this.submitOnSelect) { + this.ownerCt.buttons[0].handler.call(this.ownerCt); + } + }), + + onRender : Ext.form.DateField.prototype.onRender.createSequence(function(v) { + this.getTrigger(0).hide(); + }), + + setValue : Ext.form.DateField.prototype.setValue.createSequence(function(v) { + if (v !== null && v != '') { + if (this.allowClear) + this.getTrigger(0).show(); + } else { + this.getTrigger(0).hide(); + } + }), + + reset : Ext.form.DateField.prototype.reset.createSequence(function() { + this.originalValue = this.defaultValue; + this.setValue(this.defaultValue); + if (this.allowClear) + this.getTrigger(0).hide(); + }), + + onTrigger2Click : function() { + if (!this.readOnly) + this.onTriggerClick(); + }, + + onTrigger1Click : function() { + if (!this.disabled && !this.readOnly) { + this.clearValue(); + this.getTrigger(0).hide(); + if (this.ownerCt && this.ownerCt.buttons && this.submitOnClear) { + this.ownerCt.buttons[0].handler.call(this.ownerCt); + } + this.fireEvent('clear', this); + this.onFocus(); + } + }, + + /** + * Clears any text/value currently set in the field + */ + clearValue : function() { + if (this.hiddenField) { + this.hiddenField.value = ''; + } + this.setRawValue(''); + this.lastSelectionText = ''; + this.applyEmptyText(); + this.value = ''; + } +}); +//Ext.ComponentMgr.registerType('twindatefield', Ext.ux.form.TwinDateField); + + +/** + * Creates new DateTime + * + * @constructor + * @param {Object} + * config A config object + */ +Ext.ux.form.TwinDateTimeField = Ext.extend(Ext.form.Field, { + /** + * @cfg {Function} dateValidator A custom validation function to be called + * during date field validation (defaults to null) + */ + dateValidator : null, + + /** + * @cfg {String/Object} defaultAutoCreate DomHelper element spec Let + * superclass to create hidden field instead of textbox. Hidden will be + * submittend to server + */ + defaultAutoCreate : { + tag : 'input', + type : 'hidden' + }, + + /** + * @cfg {String} dtSeparator Date - Time separator. Used to split date and + * time (defaults to ' ' (space)) + */ + dtSeparator : ' ', + + /** + * @cfg {String} hiddenFormat Format of datetime used to store value in hidden + * field and submitted to server (defaults to 'Y-m-d H:i:s' that is mysql + * format) + */ + hiddenFormat : 'Y-m-d H:i:s', + + /** + * @cfg {Boolean} otherToNow Set other field to now() if not explicly filled + * in (defaults to true) + */ + otherToNow : true, + + /** + * @cfg {Boolean} emptyToNow Set field value to now on attempt to set empty + * value. If it is true then setValue() sets value of field to current + * date and time (defaults to false) + */ + /** + * @cfg {String} timePosition Where the time field should be rendered. 'right' + * is suitable for forms and 'below' is suitable if the field is used as + * the grid editor (defaults to 'right') + */ + timePosition : 'right', // valid values:'below', 'right' + + /** + * @cfg {Function} timeValidator A custom validation function to be called + * during time field validation (defaults to null) + */ + timeValidator : null, + + /** + * @cfg {Number} timeWidth Width of time field in pixels (defaults to 100) + */ + timeWidth : 100, + + /** + * @cfg {String} dateFormat Format of DateField. Can be localized. (defaults + * to 'm/y/d') + */ + dateFormat : 'm/d/Y', + + /** + * @cfg {String} timeFormat Format of TimeField. Can be localized. (defaults + * to 'g:i A') + */ + timeFormat : 'g:i A', + + /** + * @cfg {Object} dateConfig Config for DateField constructor. + */ + /** + * @cfg {Object} timeConfig Config for TimeField constructor. + */ + + /** + * @private creates DateField and TimeField and installs the necessary event + * handlers + */ + initComponent : function() { + // call parent initComponent + Ext.ux.form.TwinDateTimeField.superclass.initComponent.call(this); + + if (this.value) this.value = this.value * 1000; + + // create DateField + var dateConfig = Ext.apply({}, { + id : this.id + '-date', + format : this.dateFormat || Ext.ux.form.TwinDateField.prototype.format, + width : this.timeWidth, + selectOnFocus : this.selectOnFocus, + validator : this.dateValidator, + listeners : { + blur : { + scope : this, + fn : this.onBlur + }, + focus : { + scope : this, + fn : this.onFocus + } + } + }, this.dateConfig); + this.df = new Ext.ux.form.TwinDateField(dateConfig); + this.df.ownerCt = this; + delete(this.dateFormat); + + // create TimeField + var timeConfig = Ext.apply({}, { + id : this.id + '-time', + format : this.timeFormat || Ext.form.TimeField.prototype.format, + width : this.timeWidth, + selectOnFocus : this.selectOnFocus, + validator : this.timeValidator, + listeners : { + blur : { + scope : this, + fn : this.onBlur + }, + focus : { + scope : this, + fn : this.onFocus + } + } + }, this.timeConfig); + this.tf = new Ext.form.TimeField(timeConfig); + this.tf.ownerCt = this; + delete(this.timeFormat); + + // relay events + this.relayEvents(this.df, ['focus', 'specialkey', 'invalid', 'valid']); + this.relayEvents(this.tf, ['focus', 'specialkey', 'invalid', 'valid']); + + this.on('specialkey', this.onSpecialKey, this); + }, + + /** + * @private Renders underlying DateField and TimeField and provides a + * workaround for side error icon bug + */ + onRender : function(ct, position) { + // don't run more than once + if (this.isRendered) { + return; + } + + // render underlying hidden field + Ext.ux.form.TwinDateTimeField.superclass.onRender.call(this, ct, position); + + // render DateField and TimeField + // create bounding table + var t; + if ('below' === this.timePosition || 'bellow' === this.timePosition) { + t = Ext.DomHelper.append(ct, { + tag : 'table', + style : 'border-collapse:collapse', + children : [{ + tag : 'tr', + children : [{ + tag : 'td', + style : 'padding-bottom:1px', + cls : 'ux-datetime-date' + }] + }, { + tag : 'tr', + children : [{ + tag : 'td', + cls : 'ux-datetime-time' + }] + }] + }, true); + } else { + t = Ext.DomHelper.append(ct, { + tag : 'table', + style : 'border-collapse:collapse', + children : [{ + tag : 'tr', + children : [{ + tag : 'td', + style : 'padding-right:4px', + cls : 'ux-datetime-date' + }, { + tag : 'td', + cls : 'ux-datetime-time' + }] + }] + }, true); + } + + this.tableEl = t; + this.wrap = t.wrap({ + cls : 'x-form-field-wrap' + }); + // this.wrap = t.wrap(); + this.wrap.on("mousedown", this.onMouseDown, this, { + delay : 10 + }); + + // render DateField & TimeField + this.df.render(t.child('td.ux-datetime-date')); + this.tf.render(t.child('td.ux-datetime-time')); + + // workaround for IE trigger misalignment bug + // see http://extjs.com/forum/showthread.php?p=341075#post341075 + // if(Ext.isIE && Ext.isStrict) { + // t.select('input').applyStyles({top:0}); + // } + + this.df.el.swallowEvent(['keydown', 'keypress']); + this.tf.el.swallowEvent(['keydown', 'keypress']); + + // create icon for side invalid errorIcon + if ('side' === this.msgTarget) { + var elp = this.el.findParent('.x-form-element', 10, true); + if (elp) { + this.errorIcon = elp.createChild({ + cls : 'x-form-invalid-icon' + }); + } + + var o = { + errorIcon : this.errorIcon, + msgTarget : 'side', + alignErrorIcon : this.alignErrorIcon.createDelegate(this) + }; + Ext.apply(this.df, o); + Ext.apply(this.tf, o); + // this.df.errorIcon = this.errorIcon; + // this.tf.errorIcon = this.errorIcon; + } + + // setup name for submit + this.el.dom.name = this.hiddenName || this.name || this.id; + + // prevent helper fields from being submitted + this.df.el.dom.removeAttribute("name"); + this.tf.el.dom.removeAttribute("name"); + + // we're rendered flag + this.isRendered = true; + + // update hidden field + this.updateHidden(); + + }, + + /** + * @private + */ + adjustSize : Ext.BoxComponent.prototype.adjustSize, + + /** + * @private + */ + alignErrorIcon : function() { + this.errorIcon.alignTo(this.tableEl, 'tl-tr', [2, 0]); + }, + + /** + * @private initializes internal dateValue + */ + initDateValue : function() { + this.dateValue = this.otherToNow ? new Date() : new Date(1970, 0, 1, 0, 0, 0); + }, + + /** + * Calls clearInvalid on the DateField and TimeField + */ + clearInvalid : function() { + this.df.clearInvalid(); + this.tf.clearInvalid(); + }, + + /** + * Calls markInvalid on both DateField and TimeField + * + * @param {String} + * msg Invalid message to display + */ + markInvalid : function(msg) { + this.df.markInvalid(msg); + this.tf.markInvalid(msg); + }, + + /** + * @private called from Component::destroy. Destroys all elements and removes + * all listeners we've created. + */ + beforeDestroy : function() { + if (this.isRendered) { + // this.removeAllListeners(); + this.wrap.removeAllListeners(); + this.wrap.remove(); + this.tableEl.remove(); + this.df.destroy(); + this.tf.destroy(); + } + }, + + /** + * Disable this component. + * + * @return {Ext.Component} this + */ + disable : function() { + if (this.isRendered) { + this.df.disabled = this.disabled; + this.df.onDisable(); + this.tf.onDisable(); + } + this.disabled = true; + this.df.disabled = true; + this.tf.disabled = true; + this.fireEvent("disable", this); + return this; + }, + + /** + * Enable this component. + * + * @return {Ext.Component} this + */ + enable : function() { + if (this.rendered) { + this.df.onEnable(); + this.tf.onEnable(); + } + this.disabled = false; + this.df.disabled = false; + this.tf.disabled = false; + this.fireEvent("enable", this); + return this; + }, + + /** + * @private Focus date filed + */ + focus : function() { + this.df.focus(); + }, + + /** + * @private + */ + getPositionEl : function() { + return this.wrap; + }, + + /** + * @private + */ + getResizeEl : function() { + return this.wrap; + }, + + /** + * @return {Date/String} Returns value of this field + */ + getValue : function() { + // create new instance of date + return this.dateValue ? parseInt(this.dateValue.getTime() / 1000) : ''; + }, + + /** + * @return {Boolean} true = valid, false = invalid + * @private Calls isValid methods of underlying DateField and TimeField and + * returns the result + */ + isValid : function() { + return this.df.isValid() && this.tf.isValid(); + }, + + /** + * Returns true if this component is visible + * + * @return {boolean} + */ + isVisible : function() { + return this.df.rendered && this.df.getActionEl().isVisible(); + }, + + /** + * @private Handles blur event + */ + onBlur : function(f) { + // called by both DateField and TimeField blur events + + // revert focus to previous field if clicked in between + if (this.wrapClick) { + f.focus(); + this.wrapClick = false; + } + + // update underlying value + if (f === this.df) { + this.updateDate(); + } else { + this.updateTime(); + } + this.updateHidden(); + + this.validate(); + +// fire events later + (function() { + if (!this.df.hasFocus && !this.tf.hasFocus) { + var v = this.getValue(); + if (String(v) !== String(this.startValue)) { + this.fireEvent("change", this, v, this.startValue); + } + this.hasFocus = false; + this.fireEvent('blur', this); + } + }).defer(100, this); + }, + + /** + * @private Handles focus event + */ + onFocus : function() { + if (!this.hasFocus) { + this.hasFocus = true; + this.startValue = this.getValue(); + this.fireEvent("focus", this); + } + }, + + /** + * @private Just to prevent blur event when clicked in the middle of fields + */ + onMouseDown : function(e) { + if (!this.disabled) { + this.wrapClick = 'td' === e.target.nodeName.toLowerCase(); + } + }, + + /** + * @private Handles Tab and Shift-Tab events + */ + onSpecialKey : function(t, e) { + var key = e.getKey(); + if (key === e.TAB) { + if (t === this.df && !e.shiftKey) { + e.stopEvent(); + this.tf.focus(); + } + if (t === this.tf && e.shiftKey) { + e.stopEvent(); + this.df.focus(); + } + this.updateValue(); + } + // otherwise it misbehaves in editor grid + if (key === e.ENTER) { + this.updateValue(); + } + }, + + + /** + * Resets the current field value to the originally loaded value and clears + * any validation messages. See Ext.form.BasicForm.trackResetOnLoad + */ + reset : function() { + this.df.setValue(this.originalValue); + this.tf.setValue(this.originalValue); + }, + + /** + * @private Sets the value of DateField + */ + setDate : function(date) { + this.df.setValue(date); + }, + + /** + * @private Sets the value of TimeField + */ + setTime : function(date) { + this.tf.setValue(date); + }, + + /** + * @private Sets correct sizes of underlying DateField and TimeField With + * workarounds for IE bugs + */ + setSize : function(w, h) { + if (!w) { + return; + } + if ('below' === this.timePosition) { + this.df.setSize(w, h); + this.tf.setSize(w, h); + if (Ext.isIE) { + this.df.el.up('td').setWidth(w); + this.tf.el.up('td').setWidth(w); + } + } else { + this.df.setSize(w - this.timeWidth - 4, h); + this.tf.setSize(this.timeWidth, h); + + if (Ext.isIE) { + this.df.el.up('td').setWidth(w - this.timeWidth - 4); + this.tf.el.up('td').setWidth(this.timeWidth); + } + } + }, + + /** + * @param {Mixed} + * val Value to set Sets the value of this field + */ + setValue : function(val) { + if (!val && true === this.emptyToNow) { + this.setValue(new Date()); + return; + } else if (!val) { + this.setDate(''); + this.setTime(''); + this.updateValue(); + return; + } + if ('number' === typeof val) { + val = new Date(val); + } else if ('string' === typeof val && this.hiddenFormat) { + val = Date.parseDate(val, this.hiddenFormat); + } + val = val ? val : new Date(1970, 0, 1, 0, 0, 0); + var da; + if (val instanceof Date) { + this.setDate(val); + this.setTime(val); + this.dateValue = new Date(Ext.isIE ? val.getTime() : val); + } else { + da = val.split(this.dtSeparator); + this.setDate(da[0]); + if (da[1]) { + if (da[2]) { + // add am/pm part back to time + da[1] += da[2]; + } + this.setTime(da[1]); + } + } + this.updateValue(); + }, + + /** + * Hide or show this component by boolean + * + * @return {Ext.Component} this + */ + setVisible : function(visible) { + if (visible) { + this.df.show(); + this.tf.show(); + } else { + this.df.hide(); + this.tf.hide(); + } + return this; + }, + + show : function() { + return this.setVisible(true); + }, + + hide : function() { + return this.setVisible(false); + }, + + /** + * @private Updates the date part + */ + updateDate : function() { + var d = this.df.getValue(); + if (d) { + if (!(this.dateValue instanceof Date)) { + this.initDateValue(); + if (!this.tf.getValue()) { + this.setTime(this.dateValue); + } + } + this.dateValue.setMonth(0); // because of leap years + this.dateValue.setFullYear(d.getFullYear()); + this.dateValue.setMonth(d.getMonth(), d.getDate()); + // this.dateValue.setDate(d.getDate()); + } else { + this.dateValue = ''; + this.setTime(''); + } + }, + + + /** + * @private Updates the time part + */ + updateTime : function() { + var t = this.tf.getValue(); + if (t && !(t instanceof Date)) { + t = Date.parseDate(t, this.tf.format); + } + if (t && !this.df.getValue()) { + this.initDateValue(); + this.setDate(this.dateValue); + } + if (this.dateValue instanceof Date) { + if (t) { + this.dateValue.setHours(t.getHours()); + this.dateValue.setMinutes(t.getMinutes()); + this.dateValue.setSeconds(t.getSeconds()); + } else { + this.dateValue.setHours(0); + this.dateValue.setMinutes(0); + this.dateValue.setSeconds(0); + } + } + }, + + + /** + * @private Updates the underlying hidden field value + */ + updateHidden : function() { + if (this.isRendered) { + var value = this.dateValue instanceof Date ? this.dateValue.format(this.hiddenFormat) : ''; + this.el.dom.value = value; + } + }, + + + /** + * @private Updates all of Date, Time and Hidden + */ + updateValue : function() { + + this.updateDate(); + this.updateTime(); + this.updateHidden(); + + return; + }, + + /** + * @return {Boolean} true = valid, false = invalid calls validate methods of + * DateField and TimeField + */ + validate : function() { + return this.df.validate() && this.tf.validate(); + }, + + + /** + * Returns renderer suitable to render this field + * + * @param {Object} + * Column model config + */ + renderer : function(field) { + var format = field.editor.dateFormat || Ext.ux.form.TwinDateTimeField.prototype.dateFormat; + format += ' ' + (field.editor.timeFormat || Ext.ux.form.TwinDateTimeField.prototype.timeFormat); + var renderer = function(val) { + var retval = Ext.util.Format.date(val, format); + return retval; + }; + return renderer; + } +}); + diff --git a/src/webui/static/app/idnode.js b/src/webui/static/app/idnode.js index 0827f5c67..497de939c 100644 --- a/src/webui/static/app/idnode.js +++ b/src/webui/static/app/idnode.js @@ -16,7 +16,7 @@ tvheadend.idnode_get_enum = function(conf) if (key in tvheadend.idnode_enum_stores) return tvheadend.idnode_enum_stores[key]; - /* Build combobox */ + /* Build store */ var st = new Ext.data.JsonStore({ root: conf.root || 'entries', url: conf.url, @@ -24,7 +24,8 @@ tvheadend.idnode_get_enum = function(conf) fields: conf.fields || ['key', 'val'], id: conf.id || 'key', autoLoad: true, - sortInfo: { + listeners: conf.listeners || {}, + sortInfo: conf.sort || { field: 'val', direction: 'ASC' } @@ -85,7 +86,9 @@ tvheadend.idnode_enum_store = function(f) case 'int': case 'u32': case 'u16': + case 's64': case 'dbl': + case 'time': var data = null; if (f.enum.length > 0 && f.enum[0] instanceof Object) { data = f.enum; @@ -118,6 +121,8 @@ tvheadend.IdNodeField = function(conf) this.wronce = conf.wronce; this.hidden = conf.hidden || conf.advanced; this.password = conf.password; + this.duration = conf.duration; + this.group = conf.group; this.enum = conf.enum; this.store = null; if (this.enum) @@ -133,9 +138,16 @@ tvheadend.IdNodeField = function(conf) var w = 300; var ftype = 'string'; if (this.type === 'int' || this.type === 'u32' || - this.type === 'u16' || this.type === 'dbl') { + this.type === 'u16' || this.type === 's64' || + this.type === 'dbl') { ftype = 'numeric'; w = 80; + } else if (this.type === 'time') { + w = 120; + if (this.durations) { + ftype = 'numeric'; + w = 80; + } } else if (this.type === 'bool') { ftype = 'boolean'; w = 60; @@ -173,7 +185,28 @@ tvheadend.IdNodeField = function(conf) return function(v) { return '********'; } - + + if (this.type === 'time') { + if (this.duration) + return function(v) { + v = parseInt(v / 60); /* Nevermind the seconds */ + if (v === 0 && v !== '0') + return "Not set"; + var hours = parseInt(v / 60); + var min = parseInt(v % 60); + if (hours) { + if (min === 0) + return hours + ' hrs'; + return hours + ' hrs, ' + min + ' min'; + } + return min + ' min'; + } + return function(v) { + var dt = new Date(v * 1000); + return dt.format('D j M H:i'); + } + } + if (!this.store) return null; @@ -181,11 +214,7 @@ tvheadend.IdNodeField = function(conf) return function(v) { if (st && st instanceof Ext.data.JsonStore) { var t = []; - var d; - if (v.push) - d = v; - else - d = [v]; + var d = v.push ? v : [v]; for (var i = 0; i < d.length; i++) { var r = st.find('key', d[i]); if (r !== -1) { @@ -220,7 +249,7 @@ tvheadend.IdNodeField = function(conf) disabled: d, width: 300 }; - + /* ComboBox */ if (this.enum) { cons = Ext.form.ComboBox; @@ -234,11 +263,20 @@ tvheadend.IdNodeField = function(conf) c['store'] = this.store; c['typeAhead'] = true; c['forceSelection'] = false; - c['triggerAction'] = 'all', - c['emptyText'] = 'Select ' + this.text + ' ...'; + c['triggerAction'] = 'all'; + c['emptyText'] = 'Select ' + this.text + ' ...'; /* Single */ } else { + + if (this.type == 'perm') { + c['regex'] = /^[0][0-7]{3}$/; + c['maskRe'] = /[0-7]/; + c['allowBlank'] = false; + c['blankText'] = 'You must provide a value - use octal chmod notation, e.g. 0664'; + c['width'] = 125; + } + switch (this.type) { case 'bool': cons = Ext.form.Checkbox; @@ -247,10 +285,13 @@ tvheadend.IdNodeField = function(conf) case 'int': case 'u32': case 'u16': + case 's32': case 'dbl': + case 'time': cons = Ext.form.NumberField; break; + /* 'str' and 'perm' */ default: cons = Ext.form.TextField; break; @@ -273,6 +314,7 @@ tvheadend.IdNode = function(conf) this.text = conf.caption || this.clazz; this.props = conf.props; this.order = []; + this.groups = conf.groups; this.fields = []; for (var i = 0; i < this.props.length; i++) { this.fields.push(new tvheadend.IdNodeField(this.props[i])); @@ -387,16 +429,6 @@ tvheadend.idnode_editor_field = function(f, create) /* Singular */ switch (f.type) { - case 'str': - return new Ext.form.TextField({ - fieldLabel: f.caption, - name: f.id, - value: value, - disabled: d, - width: 300 - }); - break; - case 'bool': return new Ext.ux.form.XCheckbox({ fieldLabel: f.caption, @@ -404,11 +436,33 @@ tvheadend.idnode_editor_field = function(f, create) checked: value, disabled: d }); - break; + + case 'time': + if (!f.duration) + return new Ext.ux.form.TwinDateTimeField({ + fieldLabel: f.caption, + name: f.id, + value: value, + disabled: d, + width: 300, + timeFormat: 'H:i:s', + timeConfig: { + altFormats: 'H:i:s', + allowBlank: true, + increment: 10, + }, + dateFormat:'d.n.Y', + dateConfig: { + altFormats: 'Y-m-d|Y-n-d', + allowBlank: true, + } + }); + /* fall thru!!! */ case 'int': case 'u32': case 'u16': + case 's64': case 'dbl': return new Ext.form.NumberField({ fieldLabel: f.caption, @@ -417,62 +471,139 @@ tvheadend.idnode_editor_field = function(f, create) disabled: d, width: 300 }); - break; + + case 'perm': + return new Ext.form.TextField({ + fieldLabel: f.caption, + name: f.id, + value: value, + disabled: d, + width: 125, + regex: /^[0][0-7]{3}$/, + maskRe: /[0-7]/, + allowBlank: false, + blankText: 'You must provide a value - use octal chmod notation, e.g. 0664' + }); + + + default: + return new Ext.form.TextField({ + fieldLabel: f.caption, + name: f.id, + value: value, + disabled: d, + width: 300 + }); + } - return null; }; /* * ID node editor form fields */ -tvheadend.idnode_editor_form = function(d, panel) +tvheadend.idnode_editor_form = function(d, meta, panel, create) { var af = []; var rf = []; var df = []; + var groups = null; /* Fields */ for (var i = 0; i < d.length; i++) { - var f = tvheadend.idnode_editor_field(d[i]); + var p = d[i]; + var f = tvheadend.idnode_editor_field(p, create); if (!f) continue; - if (d[i].rdonly) - rf.push(f); - else if (d[i].advanced) - af.push(f); - else - df.push(f); - } - if (df.length) { - panel.add(new Ext.form.FieldSet({ - title: 'Basic Settings', - autoHeight: true, - autoWidth: true, - collapsible: true, - collapsed: false, - items: df - })); + if (p.group && meta.groups) { + if (!groups) + groups = {}; + if (!(p.group in groups)) + groups[p.group] = [f]; + else + groups[p.group].push(f); + } else { + if (p.rdonly) + rf.push(f); + else if (p.advanced) + af.push(f); + else + df.push(f); + } } - if (af.length) { - panel.add(new Ext.form.FieldSet({ - title: 'Advanced Settings', - autoHeight: true, - autoWidth: true, - collapsible: true, - collapsed: false, //true, - items: af - })); + + function newFieldSet(conf) { + return new Ext.form.FieldSet({ + title: conf.title || '', + layout: conf.layout || 'form', + border: conf.border || true, + style: conf.style || 'padding: 0 5px 5px 10px', + bodyStyle: conf.bodyStyle || 'padding-top: ' + (Ext.isIE ? '0' : '10px'), + autoHeight: true, + autoWidth: true, + collapsible: conf.nocollapse ? false : true, + collapsed: false, + items: conf.items + }); } - if (rf.length) { - panel.add(new Ext.form.FieldSet({ - title: 'Read-only Info', - autoHeight: true, - autoWidth: true, - collapsible: true, - collapsed: false, //true, - items: rf - })); + + if (groups) { + var met = {}; + for (var i = 0; i < meta.groups.length; i++) + met[meta.groups[i].number] = meta.groups[i]; + var fs = {}; + var cfs = {}; + var mfs = {}; + var rest = []; + for (var number in groups) { + if (!(number in met)) + met[number] = null; + var m = met[number]; + var columns = 0; + for (var k in met) + if (met[k].parent == m.number) + if (columns < met[k].column) + columns = met[k].column; + met[number].columns = columns; + if (columns) { + var p = newFieldSet({ title: m.name || "Settings", layout: 'column', border: false }); + cfs[number] = newFieldSet({ nocollapse: true }); + p.add(cfs[number]); + fs[number] = p; + mfs[number] = p; + } + } + for (var number in groups) { + var m = met[number]; + if (number in fs) continue; + var parent = m.parent; + var p = null; + if (parent && !met[parent].columns) + parent = null; + if (!m.columns) { + if (parent) { + p = newFieldSet({ nocollapse: true }); + fs[parent].add(p); + } else { + p = newFieldSet({ title: m.name }); + mfs[number] = p; + } + cfs[number] = p; + } + } + for (var number in groups) { + var g = groups[number]; + for (var i = 0; i < g.length; i++) + cfs[number].add(g[i]); + if (number in mfs) + panel.add(mfs[number]); + } } + if (df.length) + panel.add(newFieldSet({ title: "Basic Settings", items: df })); + if (af.length) + panel.add(newFieldSet({ title: "Advanced Settings", items: af })); + if (rf.length) + panel.add(newFieldSet({ title: "Read-only Info", items: rf })); panel.doLayout(); }; @@ -485,50 +616,53 @@ tvheadend.idnode_editor = function(item, conf) var buttons = []; /* Buttons */ - var saveBtn = new Ext.Button({ - text: 'Save', - handler: function() { - var node = panel.getForm().getFieldValues(); - node.uuid = item.uuid; - Ext.Ajax.request({ - url: 'api/idnode/save', - params: { - node: Ext.encode(node) - }, - success: function(d) { - if (conf.win) - conf.win.hide(); - } + if (!conf.noButtons) { + var saveBtn = new Ext.Button({ + text: 'Save', + handler: function() { + var node = panel.getForm().getFieldValues(); + node.uuid = item.uuid; + tvheadend.Ajax({ + url: 'api/idnode/save', + params: { + node: Ext.encode(node) + }, + success: function(d) { + if (conf.win) + conf.win.hide(); + } + }); + } + }); + buttons.push(saveBtn); + + if (conf.help) { + var helpBtn = new Ext.Button({ + text: 'Help', + handler: conf.help }); + buttons.push(helpBtn); } - }); - buttons.push(saveBtn); - - if (conf.help) { - var helpBtn = new Ext.Button({ - text: 'Help', - handler: conf.help - }); - buttons.push(helpBtn); } - panel = new Ext.FormPanel({ + panel = new Ext.form.FormPanel({ title: conf.title || null, frame: true, - border: true, + border: conf.inTabPanel ? false : true, bodyStyle: 'padding: 5px', labelAlign: 'left', - labelWidth: 200, + labelWidth: conf.labelWidth || 200, autoWidth: true, autoHeight: !conf.fixedHeight, width: 600, //defaults: {width: 330}, defaultType: 'textfield', buttonAlign: 'left', - buttons: buttons + autoScroll: true, + buttons: buttons, }); - tvheadend.idnode_editor_form(item.props || item.params, panel); + tvheadend.idnode_editor_form(item.props || item.params, item.meta, panel, false); return panel; }; @@ -537,7 +671,7 @@ tvheadend.idnode_editor = function(item, conf) /* * IDnode creation dialog */ -tvheadend.idnode_create = function(conf) +tvheadend.idnode_create = function(conf, onlyDefault) { var puuid = null; var panel = null; @@ -549,13 +683,13 @@ tvheadend.idnode_create = function(conf) text: 'Create', hidden: true, handler: function() { - params = conf.create.params || {}; + var params = conf.create.params || {}; if (puuid) params['uuid'] = puuid; if (pclass) params['class'] = pclass; params['conf'] = Ext.encode(panel.getForm().getFieldValues()); - Ext.Ajax.request({ + tvheadend.Ajax({ url: conf.create.url || conf.url + '/create', params: params, success: function(d) { @@ -619,7 +753,7 @@ tvheadend.idnode_create = function(conf) pclass = r.get(conf.select.valueField); win.setTitle('Add ' + s.lastSelectionText); panel.remove(s); - tvheadend.idnode_editor_form(d, panel); + tvheadend.idnode_editor_form(d, null, panel, true); saveBtn.setVisible(true); } } @@ -628,15 +762,15 @@ tvheadend.idnode_create = function(conf) select = function(s, n, o) { params = conf.select.clazz.params || {}; params['uuid'] = puuid = n.id; - Ext.Ajax.request({ + tvheadend.Ajax({ url: conf.select.clazz.url || conf.select.url || conf.url, + params: params, success: function(d) { panel.remove(s); d = json_decode(d); - tvheadend.idnode_editor_form(d.props, panel); + tvheadend.idnode_editor_form(d.props, d, panel, true); saveBtn.setVisible(true); - }, - params: params + } }); }; } @@ -660,14 +794,18 @@ tvheadend.idnode_create = function(conf) panel.add(combo); win.show(); } else { - Ext.Ajax.request({ + tvheadend.Ajax({ url: conf.url + '/class', params: conf.params, success: function(d) { d = json_decode(d); - tvheadend.idnode_editor_form(d.props, panel); + tvheadend.idnode_editor_form(d.props, d, panel, true); saveBtn.setVisible(true); - win.show(); + if (onlyDefault) { + saveBtn.handler(); + panel.destroy(); + } else + win.show(); } }); } @@ -694,6 +832,10 @@ tvheadend.idnode_grid = function(panel, conf) var downBtn = null; var editBtn = null; + /* Some copies */ + if (conf.add && !conf.add.titleS && conf.titleS) + conf.add.titleS = conf.titleS; + /* Model */ var idnode = new tvheadend.IdNode(d); for (var i = 0; i < idnode.length(); i++) { @@ -724,7 +866,7 @@ tvheadend.idnode_grid = function(panel, conf) /* Store */ var store = new Ext.data.JsonStore({ root: 'entries', - url: conf.url + '/grid', + url: conf.gridURL || (conf.url + '/grid'), autoLoad: true, id: 'uuid', totalProperty: 'total', @@ -755,57 +897,62 @@ tvheadend.idnode_grid = function(panel, conf) saveBtn.setDisabled(d); }); select.on('selectionchange', function(s) { + var count = s.getCount(); if (delBtn) - delBtn.setDisabled(s.getCount() === 0); + delBtn.setDisabled(count === 0); if (upBtn) { - upBtn.setDisabled(s.getCount() === 0); - downBtn.setDisabled(s.getCount() === 0); + upBtn.setDisabled(count === 0); + downBtn.setDisabled(count === 0); } - editBtn.setDisabled(s.getCount() !== 1); + if (editBtn) + editBtn.setDisabled(count !== 1); if (conf.selected) conf.selected(s); }); /* Top bar */ - saveBtn = new Ext.Toolbar.Button({ - tooltip: 'Save pending changes (marked with red border)', - iconCls: 'save', - text: 'Save', - disabled: true, - handler: function() { - var mr = store.getModifiedRecords(); - var out = new Array(); - for (var x = 0; x < mr.length; x++) { - v = mr[x].getChanges(); - out[x] = v; - out[x].uuid = mr[x].id; - } - Ext.Ajax.request({ - url: 'api/idnode/save', - params: { - node: Ext.encode(out) - }, - success: function(d) - { - if (!auto.getValue()) - store.reload(); + if (!conf.readonly) { + saveBtn = new Ext.Toolbar.Button({ + tooltip: 'Save pending changes (marked with red border)', + iconCls: 'save', + text: 'Save', + disabled: true, + handler: function() { + var mr = store.getModifiedRecords(); + var out = new Array(); + for (var x = 0; x < mr.length; x++) { + v = mr[x].getChanges(); + out[x] = v; + out[x].uuid = mr[x].id; } - }); - } - }); - buttons.push(saveBtn); - undoBtn = new Ext.Toolbar.Button({ - tooltip: 'Revert pending changes (marked with red border)', - iconCls: 'undo', - text: 'Undo', - disabled: true, - handler: function() { - store.rejectChanges(); - } - }); - buttons.push(undoBtn); - buttons.push('-'); + tvheadend.Ajax({ + url: 'api/idnode/save', + params: { + node: Ext.encode(out) + }, + success: function(d) + { + if (!auto.getValue()) + store.reload(); + } + }); + } + }); + buttons.push(saveBtn); + undoBtn = new Ext.Toolbar.Button({ + tooltip: 'Revert pending changes (marked with red border)', + iconCls: 'undo', + text: 'Undo', + disabled: true, + handler: function() { + store.rejectChanges(); + } + }); + buttons.push(undoBtn); + } if (conf.add) { + if (buttons.length > 0) + buttons.push('-'); addBtn = new Ext.Toolbar.Button({ tooltip: 'Add a new entry', iconCls: 'add', @@ -818,6 +965,8 @@ tvheadend.idnode_grid = function(panel, conf) buttons.push(addBtn); } if (conf.del) { + if (!conf.add && buttons.length > 0) + buttons.push('-'); delBtn = new Ext.Toolbar.Button({ tooltip: 'Delete selected entries', iconCls: 'remove', @@ -829,7 +978,7 @@ tvheadend.idnode_grid = function(panel, conf) var uuids = []; for (var i = 0; i < r.length; i++) uuids.push(r[i].id); - Ext.Ajax.request({ + tvheadend.Ajax({ url: 'api/idnode/delete', params: { uuid: Ext.encode(uuids) @@ -857,7 +1006,7 @@ tvheadend.idnode_grid = function(panel, conf) var uuids = []; for (var i = 0; i < r.length; i++) uuids.push(r[i].id); - Ext.Ajax.request({ + tvheadend.Ajax({ url: 'api/idnode/moveup', params: { uuid: Ext.encode(uuids) @@ -882,7 +1031,7 @@ tvheadend.idnode_grid = function(panel, conf) var uuids = []; for (var i = 0; i < r.length; i++) uuids.push(r[i].id); - Ext.Ajax.request({ + tvheadend.Ajax({ url: 'api/idnode/movedown', params: { uuid: Ext.encode(uuids) @@ -897,62 +1046,67 @@ tvheadend.idnode_grid = function(panel, conf) }); buttons.push(downBtn); } - if (conf.add || conf.del || conf.move) - buttons.push('-'); - editBtn = new Ext.Toolbar.Button({ - tooltip: 'Edit selected entry', - iconCls: 'edit', - text: 'Edit', - disabled: true, - handler: function() { - var r = select.getSelected(); - if (r) { - if (conf.edittree) { - var p = tvheadend.idnode_tree({ - url: 'api/idnode/tree', - params: { - root: r.id - } - }); - p.setSize(800, 600); - var w = new Ext.Window({ - title: 'Edit ' + conf.titleS, - layout: 'fit', - autoWidth: true, - autoHeight: true, - plain: true, - items: p - }); - w.show(); - } else { - Ext.Ajax.request({ - url: 'api/idnode/load', - params: { - uuid: r.id - }, - success: function(d) - { - d = json_decode(d); - var w = null; - var c = {win: w}; - var p = tvheadend.idnode_editor(d[0], c); - w = new Ext.Window({ - title: 'Edit ' + conf.titleS, - layout: 'fit', - autoWidth: true, - autoHeight: true, - plain: true, - items: p - }); - c.win = w; - w.show(); - } - }); + if (!conf.readonly) { + if (buttons.length > 0) + buttons.push('-'); + editBtn = new Ext.Toolbar.Button({ + tooltip: 'Edit selected entry', + iconCls: 'edit', + text: 'Edit', + disabled: true, + handler: function() { + var r = select.getSelected(); + if (r) { + if (conf.edittree) { + var p = tvheadend.idnode_tree({ + url: 'api/idnode/tree', + params: { + root: r.id + } + }); + p.setSize(800, 600); + var w = new Ext.Window({ + title: 'Edit ' + conf.titleS, + layout: 'fit', + autoWidth: true, + autoHeight: true, + plain: true, + items: p + }); + w.show(); + } else { + var params = { + uuid: r.id, + meta: 1 + }; + if (conf.listEdit) + params['list'] = conf.listEdit; + tvheadend.Ajax({ + url: 'api/idnode/load', + params: params, + success: function(d) { + d = json_decode(d); + var w = null; + var c = {win: w}; + var p = tvheadend.idnode_editor(d[0], c); + w = new Ext.Window({ + title: 'Edit ' + conf.titleS, + layout: 'fit', + autoWidth: true, + autoHeight: true, + plain: true, + items: p + }); + c.win = w; + w.show(); + } + }); + } } } - } - }); - buttons.push(editBtn); + }); + buttons.push(editBtn); + } /* Hide Mode */ if (conf.hidemode) { @@ -1059,11 +1213,12 @@ tvheadend.idnode_grid = function(panel, conf) '->', '-', 'Per page', count] }); plugins.push(filter); - var grid = new Ext.grid.EditorGridPanel({ + var gconf = { stateful: true, - stateId: conf.url, + stateId: conf.gridURL || conf.url, stripeRows: true, title: conf.titleP, + iconCls: conf.iconCls || '', store: store, cm: model, selModel: select, @@ -1073,7 +1228,9 @@ tvheadend.idnode_grid = function(panel, conf) }, tbar: buttons, bbar: page - }); + }; + var grid = conf.readonly ? new Ext.grid.GridPanel(gconf) : + new Ext.grid.EditorGridPanel(gconf); grid.on('filterupdate', function() { page.changePage(0); }); @@ -1096,8 +1253,11 @@ tvheadend.idnode_grid = function(panel, conf) /* Request data */ if (!conf.fields) { - Ext.Ajax.request({ + var p = {}; + if (conf.list) p['list'] = conf.list; + tvheadend.Ajax({ url: conf.url + '/class', + params: p, success: function(d) { var d = json_decode(d); @@ -1109,6 +1269,246 @@ tvheadend.idnode_grid = function(panel, conf) } }; +/* + * IDnode form grid + */ +tvheadend.idnode_form_grid = function(panel, conf) +{ + var buttons = []; + var plugins = conf.plugins || []; + var saveBtn = null; + var undoBtn = null; + var addBtn = null; + var delBtn = null; + var current = null; + var grid = null; + var mpanel = null; + + /* Store */ + var store = new Ext.data.JsonStore({ + root: 'entries', + url: 'api/idnode/load', + baseParams: { + enum: 1, + 'class': conf.clazz + }, + autoLoad: true, + id: 'key', + totalProperty: 'total', + fields: ['key','val'], + remoteSort: false, + pruneModifiedRecords: true, + sortInfo: { + field: 'val', + direction: 'ASC' + }, + }); + + /* Model */ + var model = new Ext.grid.ColumnModel({ + defaultSortable: true, + columns: [{ + width: 300, + id: 'val', + header: conf.titleC, + sortable: true, + dataIndex: 'val' + }] + }); + + /* Selection */ + var select = new Ext.grid.RowSelectionModel({ + singleSelect: true + }); + + /* Event handlers */ + select.on('selectionchange', function(s) { + roweditor(s.getSelected()); + if (conf.selected) + conf.selected(s); + }); + + /* Top bar */ + saveBtn = new Ext.Toolbar.Button({ + tooltip: 'Save pending changes (marked with red border)', + iconCls: 'save', + text: 'Save', + disabled: true, + handler: function() { + var node = current.editor.getForm().getFieldValues(); + node.uuid = current.uuid; + tvheadend.Ajax({ + url: 'api/idnode/save', + params: { + node: Ext.encode(node) + }, + success: function() { + store.reload() + } + }); + } + }); + buttons.push(saveBtn); + undoBtn = new Ext.Toolbar.Button({ + tooltip: 'Revert pending changes (marked with red border)', + iconCls: 'undo', + text: 'Undo', + disabled: true, + handler: function() { + if (current) + current.editor.getForm().reset(); + } + }); + buttons.push(undoBtn); + buttons.push('-'); + if (conf.add) { + addBtn = new Ext.Toolbar.Button({ + tooltip: 'Add a new entry', + iconCls: 'add', + text: 'Add', + disabled: false, + handler: function() { + tvheadend.idnode_create(conf.add, true); + } + }); + buttons.push(addBtn); + } + if (conf.del) { + delBtn = new Ext.Toolbar.Button({ + tooltip: 'Delete selected entries', + iconCls: 'remove', + text: 'Delete', + disabled: true, + handler: function() { + if (current) { + tvheadend.Ajax({ + url: 'api/idnode/delete', + params: { + uuid: current.uuid + }, + success: function(d) + { + store.reload(); + grid.getSelectionModel().selectFirstRow(); + } + }); + } + } + }); + buttons.push(delBtn); + } + if (conf.add || conf.del) + buttons.push('-'); + + /* Extra buttons */ + if (conf.tbar) { + buttons.push('-'); + for (i = 0; i < conf.tbar.length; i++) { + if (conf.tbar[i].callback) { + conf.tbar[i].handler = function(b, e) { + this.callback(this, e, store, select); + }; + } + buttons.push(conf.tbar[i]); + } + } + + /* Help */ + if (conf.help) { + buttons.push('->'); + buttons.push({ + text: 'Help', + handler: conf.help + }); + } + + function roweditor(r) { + if (!r || !r.id) + return; + tvheadend.Ajax({ + url: 'api/idnode/load', + params: { + uuid: r.id, + meta: 1 + }, + success: function(d) { + d = json_decode(d); + if (current) + mpanel.remove(current.editor); + var editor = new tvheadend.idnode_editor(d[0], { + title: 'Parameters', + labelWidth: 300, + fixedHeight: true, + help: conf.help || null, + inTabPanel: true, + noButtons: true + }); + current = { + uuid: d[0].id, + editor: editor + } + saveBtn.setDisabled(false); + undoBtn.setDisabled(false); + delBtn.setDisabled(false); + mpanel.add(editor); + mpanel.doLayout(); + } + }); + } + + /* Grid Panel (Selector) */ + grid = new Ext.grid.GridPanel({ + width: 200, + stripeRows: true, + store: store, + cm: model, + selModel: select, + plugins: plugins, + border: false, + viewConfig: { + forceFit: true + }, + listeners : { + render : { + fn : function() { + if (!current) + grid.getSelectionModel().selectFirstRow(); + } + } + } + }); + + mpanel = new Ext.Panel({ + tbar: buttons, + title: conf.titleP || '', + iconCls: conf.iconCls || '', + layout: 'hbox', + padding: 5, + border: false, + layoutConfig: { + align: 'stretch' + }, + items: [grid] + }); + + if (conf.tabIndex != null) + panel.insert(conf.tabIndex, mpanel); + else + panel.add(mpanel); + + /* Add comet listeners */ + var update = function(o) { + store.reload(); + }; + if (conf.comet) + tvheadend.comet.on(conf.comet, update); + tvheadend.comet.on('idnodeUpdated', update); + tvheadend.comet.on('idnodeDeleted', update); +}; + +/* + * IDNode Tree + */ tvheadend.idnode_tree = function(conf) { var current = null; diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js index d89c88aac..3e8d93dfc 100644 --- a/src/webui/static/app/tvheadend.js +++ b/src/webui/static/app/tvheadend.js @@ -40,6 +40,30 @@ tvheadend.help = function(title, pagename) { }); }; +tvheadend.Ajax = function(conf) { + var orig_success = conf.success; + var orig_failure = conf.failure; + conf.success = function(d) { + tvheadend.loading(0); + if (orig_success) + orig_success(d); + } + conf.failure = function(d) { + tvheadend.loading(0); + if (orig_failure) + orig_failure(d); + } + tvheadend.loading(1); + Ext.Ajax.request(conf); +}; + +tvheadend.loading = function(on) { + if (on) + Ext.getBody().mask('Loading... Please, wait...', 'loading'); + else + Ext.getBody().unmask(); +}; + /* * General capabilities */ @@ -219,7 +243,7 @@ function accessUpdate(o) { return; if (o.dvr == true && tvheadend.dvrpanel == null) { - tvheadend.dvrpanel = new tvheadend.dvr; + tvheadend.dvrpanel = tvheadend.dvr(); tvheadend.rootTabPanel.add(tvheadend.dvrpanel); } @@ -267,17 +291,16 @@ function accessUpdate(o) { tabs1.push(tvheadend.conf_chepg); /* DVR / Timeshift */ - tabs2 = [new tvheadend.dvrsettings]; - if (tvheadend.capabilities.indexOf('timeshift') !== -1) { - tabs2.push(new tvheadend.timeshift); - } tvheadend.conf_tsdvr = new Ext.TabPanel({ activeTab: 0, autoScroll: true, title: 'Recording', iconCls: 'drive', - items: tabs2 + items: [] }); + tvheadend.dvr_settings(tvheadend.conf_tsdvr, 0); + if (tvheadend.capabilities.indexOf('timeshift') !== -1) + tvheadend.conf_tsdvr.add(new tvheadend.timeshift); tabs1.push(tvheadend.conf_tsdvr); /* CSA */ diff --git a/src/webui/webui.c b/src/webui/webui.c index 9c96cac80..d9cbba378 100644 --- a/src/webui/webui.c +++ b/src/webui/webui.c @@ -566,7 +566,7 @@ http_dvr_list_playlist(http_connection_t *hc) htsbuf_queue_t *hq; char buf[255]; dvr_entry_t *de; - const char *host; + const char *host, *uuid; off_t fsize; time_t durration; struct tm tm; @@ -585,6 +585,7 @@ http_dvr_list_playlist(http_connection_t *hc) http_access_verify_channel(hc, ACCESS_RECORDER, de->de_channel)) continue; + durration = de->de_stop - de->de_start; durration += (de->de_stop_extra + de->de_start_extra)*60; bandwidth = ((8*fsize) / (durration*1024.0)); @@ -593,10 +594,11 @@ http_dvr_list_playlist(http_connection_t *hc) htsbuf_qprintf(hq, "#EXTINF:%"PRItime_t",%s\n", durration, lang_str_get(de->de_title, NULL)); htsbuf_qprintf(hq, "#EXT-X-TARGETDURATION:%"PRItime_t"\n", durration); - htsbuf_qprintf(hq, "#EXT-X-STREAM-INF:PROGRAM-ID=%d,BANDWIDTH=%d\n", de->de_id, bandwidth); + uuid = idnode_uuid_as_str(&de->de_id); + htsbuf_qprintf(hq, "#EXT-X-STREAM-INF:PROGRAM-ID=%s,BANDWIDTH=%d\n", uuid, bandwidth); htsbuf_qprintf(hq, "#EXT-X-PROGRAM-DATE-TIME:%s\n", buf); - snprintf(buf, sizeof(buf), "/dvrfile/%d", de->de_id); + snprintf(buf, sizeof(buf), "/dvrfile/%s", uuid); htsbuf_qprintf(hq, "http://%s%s?ticket=%s\n", host, buf, access_ticket_create(buf)); } @@ -614,7 +616,7 @@ http_dvr_playlist(http_connection_t *hc, dvr_entry_t *de) { htsbuf_queue_t *hq = &hc->hc_reply; char buf[255]; - const char *ticket_id = NULL; + const char *ticket_id = NULL, *uuid; time_t durration = 0; off_t fsize = 0; int bandwidth = 0; @@ -634,10 +636,11 @@ http_dvr_playlist(http_connection_t *hc, dvr_entry_t *de) htsbuf_qprintf(hq, "#EXTINF:%"PRItime_t",%s\n", durration, lang_str_get(de->de_title, NULL)); htsbuf_qprintf(hq, "#EXT-X-TARGETDURATION:%"PRItime_t"\n", durration); - htsbuf_qprintf(hq, "#EXT-X-STREAM-INF:PROGRAM-ID=%d,BANDWIDTH=%d\n", de->de_id, bandwidth); + uuid = idnode_uuid_as_str(&de->de_id); + htsbuf_qprintf(hq, "#EXT-X-STREAM-INF:PROGRAM-ID=%s,BANDWIDTH=%d\n", uuid, bandwidth); htsbuf_qprintf(hq, "#EXT-X-PROGRAM-DATE-TIME:%s\n", buf); - snprintf(buf, sizeof(buf), "/dvrfile/%d", de->de_id); + snprintf(buf, sizeof(buf), "/dvrfile/%s", uuid); ticket_id = access_ticket_create(buf); htsbuf_qprintf(hq, "http://%s%s?ticket=%s\n", host, buf, ticket_id); diff --git a/src/webui/webui.h b/src/webui/webui.h index a1bee1cd5..819bb6a6f 100644 --- a/src/webui/webui.h +++ b/src/webui/webui.h @@ -43,10 +43,6 @@ void extjs_start_dvb(void); void extjs_start_v4l(void); #endif -void extjs_service_update(htsmsg_t *in); - -void extjs_service_delete(htsmsg_t *in); - void webui_api_init ( void ); diff --git a/src/webui/webui_api.c b/src/webui/webui_api.c index 6db06194c..895214431 100644 --- a/src/webui/webui_api.c +++ b/src/webui/webui_api.c @@ -40,12 +40,13 @@ webui_api_handler } /* Call */ - r = api_exec(remain, args, &resp); + r = api_exec(hc->hc_access, remain, args, &resp); htsmsg_destroy(args); /* Convert error */ if (r) { switch (r) { + case EPERM: case EACCES: r = HTTP_STATUS_UNAUTHORIZED; break;