diff --git a/Makefile b/Makefile index 70ad0d35..632fdf00 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/docs/html/config_access.html b/docs/html/config_access.html index 0e6d73bc..48cc42d9 100644 --- a/docs/html/config_access.html +++ b/docs/html/config_access.html @@ -68,13 +68,11 @@ The columns have the following functions:
Enables access to all video recording functions. This also include administration of the auto recordings. -
All Configs (VR) +
DVR Config Profile
- Allow to use all DVR configuration profiles. If not set, a DVR - configuration profile matched to the authorized user by name is always used - (the configuration profile must have same name as the user). If the DVR - configuration profile does not exists, the default profile is used. - The user is also not allowed to select another profile. + If set, the user will only be able to use the DVR config profile + equal to this value. + Note that this field is unset when the DVR Config Profile is removed.
Web interface
@@ -84,13 +82,6 @@ The columns have the following functions:
Enables access to the Configuration tab. -
Username Channel Tag Match -
- If enabled, the user will only be able to access channels with a tag the - same name as the username. - - This provides a very rudimentary way of limiting access to certain channels. -
Min Channel Num
If nonzero, the user will only be able to access channels with @@ -103,11 +94,9 @@ The columns have the following functions:
Channel Tag
- If set, the user will only be able to access channels with - a channel tag equal to this value. Note that this field stores - the tag name (not identified). It means that this field would not be - updated when the tag name is changed. A manual re-set of this field - is required (for security reasons). + If set, the user will only be able to access channels containing + this channel tag. + Note that this field is unset when the channel tag is removed.
Comment
diff --git a/docs/html/config_dvrauto.html b/docs/html/config_dvrauto.html new file mode 100644 index 00000000..e23b70cf --- /dev/null +++ b/docs/html/config_dvrauto.html @@ -0,0 +1,10 @@ +
+ +

+ This tab is used to manipulate with the Digital Video Recorder entries - + the automatic recording. + +

+ A volunteer required to fill this... + +

diff --git a/docs/html/config_dvrlog.html b/docs/html/config_dvrlog.html new file mode 100644 index 00000000..b7d77006 --- /dev/null +++ b/docs/html/config_dvrlog.html @@ -0,0 +1,9 @@ +
+ +

+ This tab is used to manipulate with the Digital Video Recorder entries. + +

+ A volunteer required to fill this... + +

diff --git a/docs/html/config_muxes.html b/docs/html/config_muxes.html index 6948cb04..209e2213 100644 --- a/docs/html/config_muxes.html +++ b/docs/html/config_muxes.html @@ -17,7 +17,40 @@
Whether or not the mux is enabled and thus available.
EPG -
Whether or not to retrieve EPG information from the mux. +
EPG scan setup + +
+
Disable +
Disable the EPG scan + +
Enable (auto) +
Enable the EPG scan (when some services from this mux are assigned to channels) + +
Force (auto) +
Force the EPG scan (everytime when the EPG scan is triggered) + +
Only EIT +
Do only EIT EPG scan (when some services from this mux are assigned to channels) + +
Only UK Freesat +
Do only UK Freesat EPG scan (when some services from this mux are assigned to channels) + +
Only UK Freeview +
Do only UK Freeview EPG scan (when some services from this mux are assigned to channels) + +
Only Viasat Baltic +
Do only Viasat Baltic EPG scan (when some services from this mux are assigned to channels) + +
Only OpenTV Sky UK +
Do only OpenTV Sky UK EPG scan (the TSID must match in the skyuk configuration file) + +
Only OpenTV Sky Italia +
Do only OpenTV Sky Italia EPG scan (the TSID must match in the skyit configuration file) + +
Only OpenTV Sky Ausat +
Do only OpenTV Sky Ausat EPG scan (the TSID must match in the ausat configuration file) + +
Network
The name of the network to which the mux belongs. Networks are defined in Configuration -> DVB Inputs -> Networks - DVB-S or ATSC, for example. @@ -45,6 +78,11 @@
Character Set
The character encoding for this mux (e.g. UTF-8). + +
PMT Descriptor 0x06 = AC-3 +
Whether or not the empty PMT descriptor 0x06 defaults to + the AC-3 stream. Some Chinese cable providers are using + this. If unsure, keep this off.
Interface
IPTV : the network interface/card on which the IPTV source can be found. diff --git a/src/access.c b/src/access.c index 5876b534..2859b2cd 100644 --- a/src/access.c +++ b/src/access.c @@ -37,6 +37,8 @@ #include "access.h" #include "settings.h" #include "channels.h" +#include "dvr/dvr.h" +#include "tcp.h" struct access_entry_queue access_entries; struct access_ticket_queue access_tickets; @@ -158,6 +160,11 @@ access_ticket_verify(const char *id, const char *resource) void access_destroy(access_t *a) { + if (a == NULL) + return; + free(a->aa_username); + free(a->aa_representative); + htsmsg_destroy(a->aa_dvrcfgs); htsmsg_destroy(a->aa_chtags); free(a); } @@ -292,10 +299,16 @@ access_update(access_t *a, access_entry_t *ae) } } - if(ae->ae_chtag && ae->ae_chtag[0] != '\0') { + if(ae->ae_dvr_config && ae->ae_dvr_config->dvr_config_name[0] != '\0') { + if (a->aa_dvrcfgs == NULL) + a->aa_dvrcfgs = htsmsg_create_list(); + htsmsg_add_str(a->aa_dvrcfgs, NULL, idnode_uuid_as_str(&ae->ae_dvr_config->dvr_id)); + } + + if(ae->ae_chtag && ae->ae_chtag->ct_name[0] != '\0') { if (a->aa_chtags == NULL) a->aa_chtags = htsmsg_create_list(); - htsmsg_add_str(a->aa_chtags, NULL, ae->ae_chtag); + htsmsg_add_str(a->aa_chtags, NULL, idnode_uuid_as_str(&ae->ae_chtag->ct_id)); } a->aa_rights |= ae->ae_rights; @@ -310,6 +323,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; @@ -546,12 +567,8 @@ access_entry_update_rights(access_entry_t *ae) r |= ACCESS_ADVANCED_STREAMING; if (ae->ae_dvr) r |= ACCESS_RECORDER; - if (ae->ae_dvrallcfg) - r |= ACCESS_RECORDER_ALL; if (ae->ae_webui) r |= ACCESS_WEB_INTERFACE; - if (ae->ae_tag_only) - r |= ACCESS_TAG_ONLY; if (ae->ae_admin) r |= ACCESS_ADMIN; ae->ae_rights = r; @@ -569,6 +586,7 @@ access_entry_create(const char *uuid, htsmsg_t *conf) { access_ipmask_t *ai; access_entry_t *ae, *ae2; + const char *s; lock_assert(&global_lock); @@ -585,6 +603,9 @@ access_entry_create(const char *uuid, htsmsg_t *conf) if (conf) { idnode_load(&ae->ae_id, conf); + /* note password has PO_NOSAVE, thus it must be set manually */ + if ((s = htsmsg_get_str(conf, "password")) != NULL) + access_entry_class_password_set(ae, s); access_entry_update_rights(ae); TAILQ_FOREACH(ae2, &access_entries, ae_link) if (ae->ae_index < ae2->ae_index) @@ -628,6 +649,11 @@ access_entry_destroy(access_entry_t *ae) TAILQ_REMOVE(&access_entries, ae, ae_link); idnode_unlink(&ae->ae_id); + if (ae->ae_dvr_config) + LIST_REMOVE(ae, ae_dvr_config_link); + if (ae->ae_chtag) + LIST_REMOVE(ae, ae_channel_tag_link); + while((ai = TAILQ_FIRST(&ae->ae_ipmasks)) != NULL) { TAILQ_REMOVE(&ae->ae_ipmasks, ai, ai_link); @@ -642,10 +668,42 @@ access_entry_destroy(access_entry_t *ae) free(ae); } +/* + * + */ +void +access_destroy_by_dvr_config(dvr_config_t *cfg, int delconf) +{ + access_entry_t *ae; + + while ((ae = LIST_FIRST(&cfg->dvr_accesses)) != NULL) { + LIST_REMOVE(ae, ae_dvr_config_link); + ae->ae_dvr_config = NULL; + if (delconf) + access_entry_save(ae); + } +} + +/* + * + */ +void +access_destroy_by_channel_tag(channel_tag_t *ct, int delconf) +{ + access_entry_t *ae; + + while ((ae = LIST_FIRST(&ct->ct_accesses)) != NULL) { + LIST_REMOVE(ae, ae_channel_tag_link); + ae->ae_chtag = NULL; + if (delconf) + access_entry_save(ae); + } +} + /** * */ -static void +void access_entry_save(access_entry_t *ae) { htsmsg_t *c = htsmsg_create_map(); @@ -795,19 +853,72 @@ access_entry_class_password2_set(void *o, const void *v) return 0; } -static htsmsg_t * -access_entry_chtag_list ( void *o ) +static int +access_entry_chtag_set(void *o, const void *v) { - channel_tag_t *ct; - htsmsg_t *m = htsmsg_create_list(); - TAILQ_FOREACH(ct, &channel_tags, ct_link) - htsmsg_add_str(m, NULL, ct->ct_name); - return m; + access_entry_t *ae = (access_entry_t *)o; + channel_tag_t *tag = v ? channel_tag_find_by_uuid(v) : NULL; + if (tag == NULL && ae->ae_chtag) { + LIST_REMOVE(ae, ae_channel_tag_link); + ae->ae_chtag = NULL; + return 1; + } else if (ae->ae_chtag != tag) { + if (ae->ae_chtag) + LIST_REMOVE(ae, ae_channel_tag_link); + ae->ae_chtag = tag; + LIST_INSERT_HEAD(&tag->ct_accesses, ae, ae_channel_tag_link); + return 1; + } + return 0; +} + +static const void * +access_entry_chtag_get(void *o) +{ + static const char *ret; + access_entry_t *ae = (access_entry_t *)o; + if (ae->ae_chtag) + ret = idnode_uuid_as_str(&ae->ae_chtag->ct_id); + else + ret = ""; + return &ret; +} + +static int +access_entry_dvr_config_set(void *o, const void *v) +{ + access_entry_t *ae = (access_entry_t *)o; + dvr_config_t *cfg = v ? dvr_config_find_by_uuid(v) : NULL; + if (cfg == NULL && ae->ae_dvr_config) { + LIST_REMOVE(ae, ae_dvr_config_link); + ae->ae_dvr_config = NULL; + return 1; + } else if (ae->ae_dvr_config != cfg) { + if (ae->ae_dvr_config) + LIST_REMOVE(ae, ae_dvr_config_link); + ae->ae_dvr_config = cfg; + LIST_INSERT_HEAD(&cfg->dvr_accesses, ae, ae_dvr_config_link); + return 1; + } + return 0; +} + +static const void * +access_entry_dvr_config_get(void *o) +{ + static const char *ret; + access_entry_t *ae = (access_entry_t *)o; + if (ae->ae_dvr_config) + ret = idnode_uuid_as_str(&ae->ae_dvr_config->dvr_id); + else + ret = ""; + return &ret; } const idclass_t access_entry_class = { .ic_class = "access", .ic_caption = "Access", + .ic_event = "access", .ic_save = access_entry_class_save, .ic_get_title = access_entry_class_get_title, .ic_delete = access_entry_class_delete, @@ -875,10 +986,13 @@ const idclass_t access_entry_class = { .off = offsetof(access_entry_t, ae_dvr), }, { - .type = PT_BOOL, - .id = "dvrallcfg", - .name = "All Configs (VR)", - .off = offsetof(access_entry_t, ae_dvrallcfg), + .type = PT_STR, + .id = "dvr_config", + .name = "DVR Config Profile", + .set = access_entry_dvr_config_set, + .get = access_entry_dvr_config_get, + .list = dvr_entry_class_config_name_list, + .off = offsetof(access_entry_t, ae_dvr_config), }, { .type = PT_BOOL, @@ -892,12 +1006,6 @@ const idclass_t access_entry_class = { .name = "Admin", .off = offsetof(access_entry_t, ae_admin), }, - { - .type = PT_BOOL, - .id = "tag_only", - .name = "Username Channel Tag Match", - .off = offsetof(access_entry_t, ae_tag_only), - }, { .type = PT_U32, .id = "channel_min", @@ -915,7 +1023,9 @@ const idclass_t access_entry_class = { .id = "channel_tag", .name = "Channel Tag", .off = offsetof(access_entry_t, ae_chtag), - .list = access_entry_chtag_list, + .set = access_entry_chtag_set, + .get = access_entry_chtag_get, + .list = channel_tag_class_get_list, }, { .type = PT_STR, @@ -956,7 +1066,7 @@ access_init(int createdefault, int noacl) TAILQ_INIT(&access_tickets); /* Load */ - if ((c = hts_settings_load_r(1, "accesscontrol")) != NULL) { + if ((c = hts_settings_load("accesscontrol")) != NULL) { HTSMSG_FOREACH(f, c) { if (!(m = htsmsg_field_get_map(f))) continue; (void)access_entry_create(f->hmf_name, m); diff --git a/src/access.h b/src/access.h index 1cc74a2a..b6adad0d 100644 --- a/src/access.h +++ b/src/access.h @@ -22,6 +22,9 @@ #include "idnode.h" #include "htsmsg.h" +struct dvr_config; +struct channel_tag; + typedef struct access_ipmask { TAILQ_ENTRY(access_ipmask) ai_link; @@ -48,18 +51,25 @@ typedef struct access_entry { char *ae_password; char *ae_password2; char *ae_comment; + int ae_index; int ae_enabled; + int ae_streaming; int ae_adv_streaming; + int ae_dvr; - int ae_dvrallcfg; + struct dvr_config *ae_dvr_config; + LIST_ENTRY(access_entry) ae_dvr_config_link; + int ae_webui; int ae_admin; - int ae_tag_only; + uint32_t ae_chmin; uint32_t ae_chmax; - char *ae_chtag; + + struct channel_tag *ae_chtag; + LIST_ENTRY(access_entry) ae_channel_tag_link; uint32_t ae_rights; @@ -82,7 +92,10 @@ typedef struct access_ticket { } access_ticket_t; typedef struct access { + char *aa_username; + char *aa_representative; uint32_t aa_rights; + htsmsg_t *aa_dvrcfgs; uint32_t aa_chmin; uint32_t aa_chmax; htsmsg_t *aa_chtags; @@ -94,14 +107,11 @@ typedef struct access { #define ACCESS_ADVANCED_STREAMING (1<<1) #define ACCESS_WEB_INTERFACE (1<<2) #define ACCESS_RECORDER (1<<3) -#define ACCESS_RECORDER_ALL (1<<4) -#define ACCESS_TAG_ONLY (1<<5) -#define ACCESS_ADMIN (1<<6) +#define ACCESS_ADMIN (1<<4) #define ACCESS_FULL \ (ACCESS_STREAMING | ACCESS_ADVANCED_STREAMING | \ - ACCESS_WEB_INTERFACE | ACCESS_RECORDER | \ - ACCESS_RECORDER_ALL | ACCESS_ADMIN) + ACCESS_WEB_INTERFACE | ACCESS_RECORDER | ACCESS_ADMIN) /** * Create a new ticket for the requested resource and generate a id for it @@ -129,6 +139,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 +167,20 @@ 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); + +/** + * + */ +void +access_destroy_by_dvr_config(struct dvr_config *cfg, int delconf); +void +access_destroy_by_channel_tag(struct channel_tag *ct, int delconf); + /** * */ diff --git a/src/api.c b/src/api.c index 3aa932d9..4eadf940 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; @@ -83,6 +84,9 @@ api_exec ( const char *subsystem, htsmsg_t *args, htsmsg_t **resp ) return ENOSYS; // TODO: is this really the right error code? } + if (access_verify2(perm, ah->hook->ah_access)) + return EPERM; + /* Extract method */ op = htsmsg_get_str(args, "method"); if (!op) @@ -90,12 +94,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 +132,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 e6caef59..0655865b 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 f9ad7708..15d87214 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 99f73c9f..aaf87b45 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 00000000..46da6482 --- /dev/null +++ b/src/api/api_dvr.c @@ -0,0 +1,358 @@ +/* + * 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 = NULL; + htsmsg_field_t *f; + const char *uuid; + + lock_assert(&global_lock); + + if (perm->aa_dvrcfgs == NULL) + return config_uuid; /* no change */ + + config_uuid = config_uuid ?: ""; + HTSMSG_FOREACH(f, perm->aa_dvrcfgs) { + uuid = htsmsg_field_get_str(f) ?: ""; + if (strcmp(uuid, config_uuid) == 0) + return config_uuid; + if (!cfg) + cfg = dvr_config_find_by_uuid(uuid); + } + + if (!cfg && perm->aa_username) + tvhlog(LOG_INFO, "dvr", "User '%s' has no valid dvr config in ACL, using default...", perm->aa_username); + + return cfg ? idnode_uuid_as_str(&cfg->dvr_id) : NULL; +} + +/* + * + */ + +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; + + 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; + const char *s1, *s2; + + if (!(conf = htsmsg_get_map(args, "conf"))) + return EINVAL; + + pthread_mutex_lock(&global_lock); + s1 = htsmsg_get_str(conf, "config_name"); + s2 = api_dvr_config_name(perm, s1); + if (strcmp(s1 ?: "", s2 ?: "")) + htsmsg_set_str(conf, "config_name", s2 ?: ""); + + if (perm->aa_representative) + htsmsg_set_str(conf, "creator", perm->aa_representative); + + 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; + + if (perm->aa_representative) + htsmsg_set_str(conf, "creator", perm->aa_representative); + + pthread_mutex_lock(&global_lock); + dae = dvr_autorec_create(NULL, conf); + if (dae) { + dvr_autorec_save(dae); + dvr_autorec_changed(dae, 1); + } + 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); + dvr_autorec_changed(dae, 1); + } + } + 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 9bd44903..d1ac6ef6 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 f0d38cfa..e984ff57 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 2769f8c3..bc8c761b 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 689a51b5..997b8742 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 }, @@ -33,7 +57,7 @@ static void api_idnode_grid_conf ( htsmsg_t *args, api_idnode_grid_conf_t *conf ) { - htsmsg_field_t *f; + htsmsg_field_t *f, *f2; htsmsg_t *filter, *e; const char *str; @@ -60,11 +84,20 @@ api_idnode_grid_conf if ((v = htsmsg_get_str(e, "value"))) idnode_filter_add_str(&conf->filter, k, v, IC_RE); } else if (!strcmp(t, "numeric")) { - uint32_t v; - if (!htsmsg_get_u32(e, "value", &v)) { - int t = str2val(htsmsg_get_str(e, "comparison") ?: "", - filtcmptab); - idnode_filter_add_num(&conf->filter, k, v, t == -1 ? IC_EQ : t); + f2 = htsmsg_field_find(e, "value"); + if (f2) { + int t = str2val(htsmsg_get_str(e, "comparison") ?: "", filtcmptab); + if (f2->hmf_type == HMF_DBL) { + double dbl; + if (!htsmsg_field_get_dbl(f2, &dbl)) + idnode_filter_add_dbl(&conf->filter, k, dbl, t == -1 ? IC_EQ : t); + } else { + int64_t v; + int64_t intsplit = 0; + htsmsg_get_s64(e, "intsplit", &intsplit); + if (!htsmsg_field_get_s64(f2, &v)) + idnode_filter_add_num(&conf->filter, k, v, t == -1 ? IC_EQ : t, intsplit); + } } } else if (!strcmp(t, "boolean")) { uint32_t v; @@ -87,10 +120,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 +134,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 +145,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 +160,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 +189,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 +199,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 +221,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 +239,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 +248,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 +260,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 +298,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 +328,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 +344,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 +365,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 +405,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 +424,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 +445,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 +480,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 +489,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 +497,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(args, resp, idnode_delete); + return api_idnode_handler(perm, args, resp, api_idnode_delete_); +} + +static void +api_idnode_moveup_ (access_t *perm, idnode_t *in) +{ + 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(args, resp, idnode_moveup); + return api_idnode_handler(perm, args, resp, api_idnode_moveup_); +} + +static void +api_idnode_movedown_ (access_t *perm, idnode_t *in) +{ + 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 c82aef8b..e60512df 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 dfe0c426..632b96ed 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 3e6ffcee..618ae683 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 b0d3ec64..3cc822ea 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 3104850b..6d19e843 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/channels.c b/src/channels.c index 7dbbbfa1..4c5eb837 100644 --- a/src/channels.c +++ b/src/channels.c @@ -163,19 +163,6 @@ channel_class_tags_set ( void *obj, const void *p ) return channel_set_tags_by_list(obj, (htsmsg_t*)p); } -static htsmsg_t * -channel_class_tags_enum ( void *obj ) -{ - 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_bool(e, "enum", 1); - htsmsg_add_msg(m, "params", e); - return m; -} - static void channel_class_icon_notify ( void *obj ) { @@ -221,7 +208,7 @@ channel_class_get_name ( void *p ) static const void * channel_class_get_number ( void *p ) { - static int i; + static int64_t i; i = channel_get_number(p); return &i; } @@ -288,6 +275,7 @@ channel_class_epggrab_list ( void *o ) const idclass_t channel_class = { .ic_class = "channel", .ic_caption = "Channel", + .ic_event = "channel", .ic_save = channel_class_save, .ic_get_title = channel_class_get_title, .ic_delete = channel_class_delete, @@ -308,7 +296,8 @@ const idclass_t channel_class = { .get = channel_class_get_name, }, { - .type = PT_INT, + .type = PT_S64, + .intsplit = CHANNEL_SPLIT, .id = "number", .name = "Number", .off = offsetof(channel_t, ch_number), @@ -369,7 +358,7 @@ const idclass_t channel_class = { .name = "Tags", .get = channel_class_tags_get, .set = channel_class_tags_set, - .list = channel_class_tags_enum, + .list = channel_tag_class_get_list, .rend = channel_class_tags_rend }, {} @@ -430,7 +419,8 @@ channel_access(channel_t *ch, access_t *a, const char *username) htsmsg_field_t *f; HTSMSG_FOREACH(f, a->aa_chtags) { LIST_FOREACH(ctm, &ch->ch_ctms, ctm_channel_link) { - if (!strcmp(htsmsg_field_get_str(f) ?: "", ctm->ctm_tag->ct_name)) + if (!strcmp(htsmsg_field_get_str(f) ?: "", + idnode_uuid_as_str(&ctm->ctm_tag->ct_id))) goto chtags_ok; } } @@ -438,17 +428,6 @@ channel_access(channel_t *ch, access_t *a, const char *username) } chtags_ok: - /* Channel tag <-> user name match */ - if (ch && (a->aa_rights & ACCESS_TAG_ONLY) != 0) { - channel_tag_mapping_t *ctm; - LIST_FOREACH(ctm, &ch->ch_ctms, ctm_channel_link) { - if (!strcmp(username ?: "", ctm->ctm_tag->ct_name)) - goto tagonly_ok; - } - return 0; - } -tagonly_ok: - return 1; } @@ -529,7 +508,7 @@ channel_get_name ( channel_t *ch ) return blank; } -int +int64_t channel_get_number ( channel_t *ch ) { int n; @@ -620,7 +599,7 @@ channel_delete ( channel_t *ch, int delconf ) /* Settings */ if (delconf) - hts_settings_remove("channel/%s", idnode_uuid_as_str(&ch->ch_id)); + hts_settings_remove("channel/config/%s", idnode_uuid_as_str(&ch->ch_id)); /* Free memory */ RB_REMOVE(&channels, ch, ch_link); @@ -638,7 +617,7 @@ channel_save ( channel_t *ch ) { htsmsg_t *c = htsmsg_create_map(); idnode_save(&ch->ch_id, c); - hts_settings_save(c, "channel/%s", idnode_uuid_as_str(&ch->ch_id)); + hts_settings_save(c, "channel/config/%s", idnode_uuid_as_str(&ch->ch_id)); htsmsg_destroy(c); } @@ -656,7 +635,7 @@ channel_init ( void ) channel_tag_init(); /* Channels */ - if (!(c = hts_settings_load_r(1, "channel"))) + if (!(c = hts_settings_load("channel/config"))) return; HTSMSG_FOREACH(f, c) { @@ -761,6 +740,9 @@ channel_tag_create(const char *uuid, htsmsg_t *conf) channel_tag_t *ct; ct = calloc(1, sizeof(channel_tag_t)); + LIST_INIT(&ct->ct_ctms); + LIST_INIT(&ct->ct_autorecs); + LIST_INIT(&ct->ct_accesses); if (idnode_insert(&ct->ct_id, uuid, &channel_tag_class, IDNODE_SHORT_UUID)) { if (uuid) @@ -798,7 +780,7 @@ channel_tag_destroy(channel_tag_t *ct, int delconf) channel_tag_mapping_destroy(ctm, CTM_DESTROY_UPDATE_CHANNEL); channel_save(ch); } - hts_settings_remove("channeltags/%s", idnode_uuid_as_str(&ct->ct_id)); + hts_settings_remove("channel/tag/%s", idnode_uuid_as_str(&ct->ct_id)); } if(ct->ct_enabled && !ct->ct_internal) @@ -807,6 +789,9 @@ channel_tag_destroy(channel_tag_t *ct, int delconf) TAILQ_REMOVE(&channel_tags, ct, ct_link); idnode_unlink(&ct->ct_id); + autorec_destroy_by_channel_tag(ct, delconf); + access_destroy_by_channel_tag(ct, delconf); + free(ct->ct_name); free(ct->ct_comment); free(ct->ct_icon); @@ -821,7 +806,7 @@ channel_tag_save(channel_tag_t *ct) { htsmsg_t *c = htsmsg_create_map(); idnode_save(&ct->ct_id, c); - hts_settings_save(c, "channeltags/%s", idnode_uuid_as_str(&ct->ct_id)); + hts_settings_save(c, "channel/tag/%s", idnode_uuid_as_str(&ct->ct_id)); htsmsg_destroy(c); } @@ -849,9 +834,21 @@ channel_tag_class_get_title (idnode_t *self) return ct->ct_name ?: ""; } +/* exported for others */ +htsmsg_t * +channel_tag_class_get_list(void *o) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_str(m, "type", "api"); + htsmsg_add_str(m, "uri", "channeltag/list"); + htsmsg_add_str(m, "event", "channeltag"); + return m; +} + const idclass_t channel_tag_class = { .ic_class = "channeltag", .ic_caption = "Channel Tag", + .ic_event = "channeltag", .ic_save = channel_tag_class_save, .ic_get_title = channel_tag_class_get_title, .ic_delete = channel_tag_class_delete, @@ -946,7 +943,7 @@ channel_tag_init ( void ) htsmsg_field_t *f; TAILQ_INIT(&channel_tags); - if ((c = hts_settings_load_r(1, "channeltags")) != NULL) { + if ((c = hts_settings_load("channel/tag")) != NULL) { HTSMSG_FOREACH(f, c) { if (!(m = htsmsg_field_get_map(f))) continue; (void)channel_tag_create(f->hmf_name, m); diff --git a/src/channels.h b/src/channels.h index 227caffb..32742d33 100644 --- a/src/channels.h +++ b/src/channels.h @@ -47,10 +47,10 @@ typedef struct channel int ch_zombie; /* Channel info */ - char *ch_name; // Note: do not access directly! - int ch_number; - char *ch_icon; - struct channel_tag_mapping_list ch_ctms; + char *ch_name; // Note: do not access directly! + int64_t ch_number; + char *ch_icon; + struct channel_tag_mapping_list ch_ctms; /* Service/subscriptions */ LIST_HEAD(, channel_service_mapping) ch_services; @@ -95,6 +95,8 @@ typedef struct channel_tag { struct dvr_autorec_entry_list ct_autorecs; + struct access_entry_list ct_accesses; + int ct_htsp_id; } channel_tag_t; @@ -164,6 +166,8 @@ static inline channel_tag_t *channel_tag_find_by_uuid(const char *uuid) void channel_tag_save(channel_tag_t *ct); +htsmsg_t * channel_tag_class_get_list(void *o); + int channel_access(channel_t *ch, struct access *a, const char *username); int channel_tag_map(channel_t *ch, channel_tag_t *ct); @@ -173,7 +177,12 @@ void channel_save(channel_t *ch); const char *channel_get_name ( channel_t *ch ); int channel_set_name ( channel_t *ch, const char *s ); -int channel_get_number ( channel_t *ch ); +#define CHANNEL_SPLIT 1000000 + +static inline uint32_t channel_get_major ( int64_t chnum ) { return chnum / CHANNEL_SPLIT; } +static inline uint32_t channel_get_minor ( int64_t chnum ) { return chnum % CHANNEL_SPLIT; } + +int64_t channel_get_number ( channel_t *ch ); const char *channel_get_icon ( channel_t *ch ); int channel_set_icon ( channel_t *ch, const char *icon ); diff --git a/src/config.c b/src/config.c index 5eee7b6a..d4cf9a34 100644 --- a/src/config.c +++ b/src/config.c @@ -16,13 +16,15 @@ * along with this program. If not, see . */ +#include +#include + #include "tvheadend.h" #include "settings.h" #include "config.h" #include "uuid.h" - -#include -#include +#include "htsbuf.h" +#include "spawn.h" /* ************************************************************************* * Global data @@ -620,7 +622,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 +634,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 +739,313 @@ 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)) { + htsmsg_delete_field(c, "approx_time"); + if (u32 != 0) + htsmsg_add_u32(c, "start", u32); + else + htsmsg_add_str(c, "start", ""); + } + 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); + } + + if ((c = hts_settings_load("autorec")) != NULL) { + HTSMSG_FOREACH(f, c) { + if (!(e = htsmsg_field_get_map(f))) continue; + hts_settings_remove("autorec/%s", f->hmf_name); + hts_settings_save(e, "dvr/autorec/%s", f->hmf_name); + } + } +} + +static void +config_migrate_move ( const char *dir, + const char *newdir ) +{ + htsmsg_t *c, *e; + htsmsg_field_t *f; + + if (!(c = hts_settings_load(dir))) + return; + + HTSMSG_FOREACH(f, c) { + if (!(e = htsmsg_field_get_map(f))) continue; + hts_settings_save(e, "%s/%s", newdir, f->hmf_name); + hts_settings_remove("%s/%s", dir, f->hmf_name); + } + + htsmsg_destroy(c); +} + +static void +config_migrate_v10 ( void ) +{ + config_migrate_move("channel", "channel/config"); + config_migrate_move("channeltags", "channel/tag"); +} + +static const char * +config_find_uuid( htsmsg_t *map, const char *name, const char *value ) +{ + htsmsg_t *e; + htsmsg_field_t *f; + const char *s; + + HTSMSG_FOREACH(f, map) { + if (!(e = htsmsg_field_get_map(f))) continue; + if ((s = htsmsg_get_str(e, name)) != NULL) { + if (!strcmp(s, value)) + return f->hmf_name; + } + } + return NULL; +} + +static void +config_modify_acl_dvallcfg( htsmsg_t *c, htsmsg_t *dvr_config ) +{ + uint32_t a; + const char *username, *uuid; + + username = htsmsg_get_str(c, "username"); + if (!htsmsg_get_u32(c, "dvallcfg", &a)) + if (a == 0) { + uuid = username ? config_find_uuid(dvr_config, "name", username) : NULL; + if (uuid) + htsmsg_add_str(c, "dvr_config", uuid); + } + htsmsg_delete_field(c, "dvallcfg"); +} + +static void +config_modify_acl_tag_only( htsmsg_t *c, htsmsg_t *channel_tag ) +{ + uint32_t a; + const char *username, *tag, *uuid; + + username = htsmsg_get_str(c, "username"); + tag = htsmsg_get_str(c, "channel_tag"); + if (!tag || tag[0] == '\0') + tag = NULL; + if (tag == NULL && !htsmsg_get_u32(c, "tag_only", &a)) { + if (a) { + uuid = username ? config_find_uuid(channel_tag, "name", username) : NULL; + if (uuid) + htsmsg_add_str(c, "channel_tag", uuid); + } + } else if (tag) { + uuid = config_find_uuid(channel_tag, "name", tag); + if (uuid) { + htsmsg_delete_field(c, "channel_tag"); + htsmsg_add_str(c, "channel_tag", uuid); + } + } + htsmsg_delete_field(c, "tag_only"); +} + +static void +config_modify_dvr_config_name( htsmsg_t *c, htsmsg_t *dvr_config ) +{ + const char *config_name, *uuid; + + config_name = htsmsg_get_str(c, "config_name"); + uuid = config_name ? config_find_uuid(dvr_config, "name", config_name) : NULL; + htsmsg_delete_field(c, "config_name"); + htsmsg_add_str(c, "config_name", uuid ?: ""); +} + + +static void +config_migrate_v11 ( void ) +{ + htsmsg_t *dvr_config; + htsmsg_t *channel_tag; + htsmsg_t *c, *e; + htsmsg_field_t *f; + + dvr_config = hts_settings_load("dvr/config"); + channel_tag = hts_settings_load("channel/tag"); + + if ((c = hts_settings_load("accesscontrol")) != NULL) { + HTSMSG_FOREACH(f, c) { + if (!(e = htsmsg_field_get_map(f))) continue; + config_modify_acl_dvallcfg(e, dvr_config); + config_modify_acl_tag_only(e, channel_tag); + } + htsmsg_destroy(c); + } + + if ((c = hts_settings_load("dvr/log")) != NULL) { + HTSMSG_FOREACH(f, c) { + if (!(e = htsmsg_field_get_map(f))) continue; + config_modify_dvr_config_name(e, dvr_config); + } + htsmsg_destroy(c); + } + + htsmsg_destroy(channel_tag); + htsmsg_destroy(dvr_config); +} + +/* + * Perform backup + */ +static void +dobackup(const char *oldver) +{ + char outfile[PATH_MAX], cwd[PATH_MAX]; + const char *argv[] = { + "/usr/bin/tar", "cjf", outfile, "--exclude", "backup", ".", NULL + }; + const char *root = hts_settings_get_root(); + char errtxt[128]; + const char **arg; + int code; + + tvhinfo("config", "backup: migrating config from %s (running %s)", + oldver, tvheadend_version); + + if (getcwd(cwd, sizeof(cwd)) == NULL) { + tvherror("config", "unable to get the current working directory"); + goto fatal; + } + + if (!access("/bin/tar", X_OK)) + argv[0] = "/bin/tar"; + else if (!access("/usr/bin/tar", X_OK)) + argv[0] = "/usr/bin/tar"; + else if (!access("/usr/local/bin/tar", X_OK)) + argv[0] = "/usr/local/bin/tar"; + else { + tvherror("config", "unable to find tar program"); + goto fatal; + } + + snprintf(outfile, sizeof(outfile), "%s/backup", root); + if (makedirs(outfile, 0700)) + goto fatal; + if (chdir(root)) { + tvherror("config", "unable to find directory '%s'", root); + goto fatal; + } + + snprintf(outfile, sizeof(outfile), "%s/backup/%s.tar.bz2", + root, oldver); + tvhinfo("config", "backup: running, output file %s", outfile); + + spawnv(argv[0], (void *)argv); + + while ((code = spawn_reap(errtxt, sizeof(errtxt))) == -EAGAIN) + usleep(20000); + + if (code) { + htsbuf_queue_t q; + char *s; + htsbuf_queue_init(&q, 0); + for (arg = argv; *arg; arg++) { + htsbuf_append(&q, *arg, strlen(*arg)); + if (arg[1]) + htsbuf_append(&q, " ", 1); + } + s = htsbuf_to_string(&q); + tvherror("config", "command '%s' returned error code %d", s, code); + tvherror("config", "executed in directory '%s'", root); + free(s); + htsbuf_queue_flush(&q); + goto fatal; + } + + if (chdir(cwd)) { + tvherror("config", "unable to change directory to '%s'", cwd); + goto fatal; + } + return; + +fatal: + tvherror("config", "backup: fatal error"); + exit(EXIT_FAILURE); +} + /* * Migration table */ @@ -743,18 +1058,28 @@ static const config_migrate_t config_migrate_table[] = { config_migrate_v6, config_migrate_v7, config_migrate_v8, + config_migrate_v9, + config_migrate_v10, + config_migrate_v11 }; /* * Perform migrations (if required) */ -static void -config_migrate ( void ) +static int +config_migrate ( int backup ) { uint32_t v; + const char *s; /* Get the current version */ v = htsmsg_get_u32_or_default(config, "version", 0); + s = htsmsg_get_str(config, "fullversion") ?: "unknown"; + + if (backup && strcmp(s, tvheadend_version)) + dobackup(s); + else + backup = 0; /* Attempt to auto-detect versions prior to v2 */ if (!v) { @@ -765,8 +1090,11 @@ config_migrate ( void ) } /* No changes required */ - if (v == ARRAY_SIZE(config_migrate_table)) - return; + if (v == ARRAY_SIZE(config_migrate_table)) { + if (backup) + goto update; + return 0; + } /* Run migrations */ for ( ; v < ARRAY_SIZE(config_migrate_table); v++) { @@ -775,8 +1103,48 @@ config_migrate ( void ) } /* Update */ +update: htsmsg_set_u32(config, "version", v); + htsmsg_set_str(config, "fullversion", tvheadend_version); config_save(); + return 1; +} + +/* + * + */ +static void +config_check_one ( const char *dir ) +{ + htsmsg_t *c, *e; + htsmsg_field_t *f; + + if (!(c = hts_settings_load(dir))) + return; + + HTSMSG_FOREACH(f, c) { + if (!(e = htsmsg_field_get_map(f))) continue; + if (strlen(f->hmf_name) != UUID_HEX_SIZE - 1) { + tvherror("START", "filename %s/%s/%s is invalid", hts_settings_get_root(), dir, f->hmf_name); + exit(1); + } + } + htsmsg_destroy(c); +} + +/* + * Perform a simple check for UUID files + */ +static void +config_check ( void ) +{ + config_check_one("accesscontrol"); + config_check_one("channel/config"); + config_check_one("channel/tag"); + config_check_one("dvr/config"); + config_check_one("dvr/log"); + config_check_one("dvr/autorec"); + config_check_one("esfilter"); } /* ************************************************************************** @@ -784,7 +1152,7 @@ config_migrate ( void ) * *************************************************************************/ void -config_init ( const char *path ) +config_init ( const char *path, int backup ) { struct stat st; char buf[1024]; @@ -829,11 +1197,13 @@ config_init ( const char *path ) /* Store version number */ if (new) { htsmsg_set_u32(config, "version", ARRAY_SIZE(config_migrate_table)); + htsmsg_set_str(config, "fullversion", tvheadend_version); config_save(); /* Perform migrations */ } else { - config_migrate(); + if (config_migrate(backup)) + config_check(); } } diff --git a/src/config.h b/src/config.h index 4473947d..9f08a672 100644 --- a/src/config.h +++ b/src/config.h @@ -23,7 +23,7 @@ #include "htsmsg.h" -void config_init ( const char *path ); +void config_init ( const char *path, int backup ); void config_done ( void ); void config_save ( void ); diff --git a/src/cron.c b/src/cron.c index 41850dd4..366139a2 100644 --- a/src/cron.c +++ b/src/cron.c @@ -73,7 +73,7 @@ cron_parse_field if ((sn - off) >= bits || (en - off) >= bits || mn > bits) return 1; if (en < 0) en = sn; - if (mn < 0) mn = 1; + if (mn <= 0) mn = 1; while (sn <= en) { if ( (sn % mn) == 0 ) val |= (0x1ULL << (sn - off)); @@ -176,7 +176,7 @@ cron_multi_set ( const char *str ) if (line[0] != '#') if (!cron_set(&cron, line)) { count++; - cm2 = realloc(cm, sizeof(cm) + sizeof(cron) * count); + cm2 = realloc(cm, sizeof(*cm) + sizeof(cron) * count); if (cm2 == NULL) { free(cm); return NULL; diff --git a/src/dvr/dvr.h b/src/dvr/dvr.h index 4cf13149..e0412ef7 100644 --- a/src/dvr/dvr.h +++ b/src/dvr/dvr.h @@ -27,18 +27,39 @@ #include "lang_str.h" typedef struct dvr_config { + idnode_t dvr_id; + + LIST_ENTRY(dvr_config) config_link; + + int dvr_enabled; + int dvr_valid; char *dvr_config_name; char *dvr_storage; uint32_t dvr_retention_days; - int dvr_flags; char *dvr_charset; char *dvr_charset_id; char *dvr_postproc; - int dvr_extra_time_pre; - int dvr_extra_time_post; + uint32_t dvr_extra_time_pre; + uint32_t dvr_extra_time_post; - muxer_container_type_t dvr_mc; - muxer_config_t dvr_muxcnf; + int dvr_mc; + muxer_config_t dvr_muxcnf; + + int dvr_dir_per_day; + int dvr_channel_dir; + int dvr_channel_in_title; + int dvr_omit_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; /* Series link support */ int dvr_sl_brand_lock; @@ -51,28 +72,16 @@ typedef struct dvr_config { /* Duplicate detect */ int dvr_dup_detect_episode; - LIST_ENTRY(dvr_config) config_link; + struct dvr_entry_list dvr_entries; + + struct access_entry_list dvr_accesses; + } dvr_config_t; extern struct dvr_config_list dvrconfigs; extern struct dvr_entry_list dvrentries; -#define DVR_DIR_PER_DAY 0x1 -#define DVR_DIR_PER_CHANNEL 0x2 -#define DVR_CHANNEL_IN_TITLE 0x4 -#define DVR_DATE_IN_TITLE 0x8 -#define DVR_TIME_IN_TITLE 0x10 -#define DVR_WHITESPACE_IN_TITLE 0x20 -#define DVR_DIR_PER_TITLE 0x40 -#define DVR_EPISODE_IN_TITLE 0x80 -#define DVR_CLEAN_TITLE 0x100 -#define DVR_TAG_FILES 0x200 -#define DVR_SKIP_COMMERCIALS 0x400 -#define DVR_SUBTITLE_IN_TITLE 0x800 -#define DVR_EPISODE_BEFORE_DATE 0x1000 -#define DVR_EPISODE_DUPLICATE_DETECTION 0x2000 - typedef enum { DVR_PRIO_IMPORTANT, DVR_PRIO_HIGH, @@ -106,6 +115,8 @@ typedef enum { typedef struct dvr_entry { + idnode_t de_id; + int de_refcnt; /* Modification is protected under global_lock */ @@ -115,7 +126,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,7 +138,8 @@ typedef struct dvr_entry { * These meta fields will stay valid as long as reference count > 0 */ - char *de_config_name; + dvr_config_t *de_config; + LIST_ENTRY(dvr_entry) de_config_link; time_t de_start; time_t de_stop; @@ -141,15 +152,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 +223,11 @@ typedef struct dvr_entry { * Autorec entry */ typedef struct dvr_autorec_entry { - TAILQ_ENTRY(dvr_autorec_entry) dae_link; - char *dae_id; + idnode_t dae_id; + TAILQ_ENTRY(dvr_autorec_entry) dae_link; + + char *dae_name; char *dae_config_name; int dae_enabled; @@ -226,11 +237,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 +262,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 @@ -258,15 +280,32 @@ typedef struct dvr_autorec_entry { void dvr_make_title(char *output, size_t outlen, dvr_entry_t *de); +static inline int dvr_config_is_valid(dvr_config_t *cfg) + { return cfg->dvr_valid; } + +static inline int dvr_config_is_default(dvr_config_t *cfg) + { return cfg->dvr_config_name == NULL || cfg->dvr_config_name[0] == '\0'; } + 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_entry_notify(dvr_entry_t *de); +void dvr_config_save(dvr_config_t *cfg); + +static inline int dvr_entry_is_editable(dvr_entry_t *de) + { return de->de_sched_state == DVR_SCHEDULED; } + +static inline int dvr_entry_is_valid(dvr_entry_t *de) + { return de->de_refcnt > 0; } + +int dvr_entry_get_mc(dvr_entry_t *de); void dvr_entry_save(dvr_entry_t *de); @@ -276,30 +315,37 @@ 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); +void dvr_entry_created(dvr_entry_t *de); -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_create ( const char *uuid, htsmsg_t *conf ); -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 ); + +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); +void dvr_config_init(void); void dvr_done(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,14 @@ 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); +htsmsg_t *dvr_entry_class_duration_list(void *o, const char *not_set, int max); + /** * Query interface */ @@ -386,15 +415,23 @@ 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); + +void dvr_autorec_changed(dvr_autorec_entry_t *dae, int purge); + +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,7 +441,7 @@ 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); +void autorec_destroy_by_channel_tag(channel_tag_t *ct, int delconf); /** * diff --git a/src/dvr/dvr_autorec.c b/src/dvr/dvr_autorec.c index f477d396..19ea6793 100644 --- a/src/dvr/dvr_autorec.c +++ b/src/dvr/dvr_autorec.c @@ -31,20 +31,13 @@ #include "tvheadend.h" #include "settings.h" #include "dvr.h" -#include "notify.h" #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; -static void dvr_autorec_changed(dvr_autorec_entry_t *dae, int purge); - /** * Unlink - and remove any unstarted */ @@ -80,13 +73,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_minduration <= 0 && + (dae->dae_maxduration <= 0 || dae->dae_maxduration > 24 * 3600) && dae->dae_serieslink == NULL) return 0; // Avoid super wildcard match @@ -100,7 +93,6 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e) if (!e->episode->season || dae->dae_season != e->episode->season) return 0; if(dae->dae_brand) if (!e->episode->brand || dae->dae_brand != e->episode->brand) return 0; - if(dae->dae_title != NULL && dae->dae_title[0] != '\0') { lang_str_ele_t *ls; if(!e->episode->title) return 0; @@ -115,7 +107,7 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e) if(dae->dae_channel != NULL && dae->dae_channel != e->channel) return 0; - + if(dae->dae_channel_tag != NULL) { LIST_FOREACH(ctm, &dae->dae_channel_tag->ct_ctms, ctm_tag_link) if(ctm->ctm_channel == e->channel) @@ -124,29 +116,32 @@ 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; } duration = difftime(e->stop,e->start); - if(dae->dae_minduration) { + if(dae->dae_minduration > 0) { if(duration < dae->dae_minduration) return 0; } - if(dae->dae_maxduration) { + if(dae->dae_maxduration > 0) { if(duration > dae->dae_maxduration) return 0; } @@ -159,54 +154,78 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e) return 1; } +/** + * + */ +dvr_autorec_entry_t * +dvr_autorec_create(const char *uuid, htsmsg_t *conf) +{ + dvr_autorec_entry_t *dae; + + dae = calloc(1, sizeof(*dae)); + + 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->dae_weekdays = 0x7f; + dae->dae_pri = DVR_PRIO_NORMAL; + dae->dae_start = -1; + + TAILQ_INSERT_TAIL(&autorec_entries, dae, dae_link); + + idnode_load(&dae->dae_id, conf); + + return dae; +} /** * */ dvr_autorec_entry_t * -autorec_entry_find(const char *id, int create) +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; - char buf[20]; - static int tally; - - if(id != NULL) { - TAILQ_FOREACH(dae, &autorec_entries, dae_link) - if(!strcmp(dae->dae_id, id)) - return dae; - } - - if(create == 0) + htsmsg_t *conf; + char *title; + if (!event || !event->episode) 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_id = strdup(id); - TAILQ_INSERT_TAIL(&autorec_entries, dae, dae_link); + conf = htsmsg_create_map(); + title = regexp_escape(epg_broadcast_get_title(event, NULL)); + htsmsg_add_u32(conf, "enabled", 1); + 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)); + if (event->serieslink) + 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 +247,584 @@ 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); } /** * */ +void +dvr_autorec_save(dvr_autorec_entry_t *dae) +{ + htsmsg_t *m = htsmsg_create_map(); + + 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 void -build_weekday_tags(htsmsg_t *l, int mask) +dvr_autorec_entry_class_save(idnode_t *self) { - int i; - for(i = 0; i < 7; i++) { - if(mask & (1 << i)) - htsmsg_add_u32(l, NULL, i+1); - } + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)self; + dvr_autorec_save(dae); + dvr_autorec_changed(dae, 1); +} + +static void +dvr_autorec_entry_class_delete(idnode_t *self) +{ + autorec_entry_destroy((dvr_autorec_entry_t *)self, 1); +} + +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; } -/** - * - */ static int -build_weekday_mask(htsmsg_t *l) +dvr_autorec_entry_class_channel_set(void *o, const void *v) { - 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; -} - - -/** - * - */ -static htsmsg_t * -autorec_record_build(dvr_autorec_entry_t *dae) -{ - 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)); - - if(dae->dae_channel_tag != NULL) - htsmsg_add_str(e, "tag", dae->dae_channel_tag->ct_name); - - htsmsg_add_u32(e, "contenttype",dae->dae_content_type.code); - - htsmsg_add_str(e, "title", dae->dae_title ?: ""); - - htsmsg_add_u32(e, "approx_time", dae->dae_approx_time); - - build_weekday_tags(l, dae->dae_weekdays); - htsmsg_add_msg(e, "weekdays", l); - - if (dae->dae_minduration) - htsmsg_add_u32(e, "minduration", dae->dae_minduration); - if (dae->dae_maxduration) - htsmsg_add_u32(e, "maxduration", dae->dae_maxduration); - - 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); - - return e; -} - -/** - * - */ -static htsmsg_t * -autorec_record_get_all(void *opaque) -{ - 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)); - - return r; -} - -/** - * - */ -static htsmsg_t * -autorec_record_get(void *opaque, const char *id) -{ - dvr_autorec_entry_t *ae; - - if((ae = autorec_entry_find(id, 0)) == NULL) - return NULL; - return autorec_record_build(ae); -} - - -/** - * - */ -static htsmsg_t * -autorec_record_create(void *opaque) -{ - return autorec_record_build(autorec_entry_find(NULL, 1)); -} - - -/** - * - */ -static htsmsg_t * -autorec_record_update(void *opaque, const char *id, htsmsg_t *values, - int maycreate) -{ - 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) { + 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; } - 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; - } + } 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; } - - 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); - } - - if(!regcomp(&dae->dae_title_preg, s, - REG_ICASE | REG_EXTENDED | REG_NOSUB)) { - dae->dae_title = strdup(s); - } - } - - 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; - } - } - - 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); - } - } - - if(!htsmsg_get_u32(values, "minduration", &u32)) - dae->dae_minduration = u32; - - if(!htsmsg_get_u32(values, "maxduration", &u32)) - dae->dae_maxduration = u32; - - if((l = htsmsg_get_list(values, "weekdays")) != NULL) - dae->dae_weekdays = build_weekday_mask(l); - - if(!htsmsg_get_u32(values, "enabled", &u32)) - dae->dae_enabled = u32; - - if((s = htsmsg_get_str(values, "pri")) != NULL) - dae->dae_pri = dvr_pri2val(s); - - if((s = htsmsg_get_str(values, "brand")) != NULL) { - dae->dae_brand = epg_brand_find_by_uri(s, 1, &save); - if (dae->dae_brand) - dae->dae_brand->getref((epg_object_t*)dae->dae_brand); - } - if((s = htsmsg_get_str(values, "season")) != NULL) { - dae->dae_season = epg_season_find_by_uri(s, 1, &save); - 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); - } - if (!dvr_autorec_in_init) - dvr_autorec_changed(dae, 1); - - return autorec_record_build(dae); -} - - -/** - * - */ -static int -autorec_record_delete(void *opaque, const char *id) -{ - dvr_autorec_entry_t *dae; - - if((dae = autorec_entry_find(id, 0)) == NULL) - return -1; - autorec_entry_destroy(dae); return 0; } +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; +} -/** - * - */ -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_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; +} + +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; +} + +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; +} + +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 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' || !isdigit(s[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 >= 24 * 60) + t = -1; + if (t != *tm) { + *tm = t; + return 1; + } + return 0; +} + +static int +dvr_autorec_entry_class_start_set(void *o, const void *v) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + return dvr_autorec_entry_class_time_set(o, v, &dae->dae_start); +} + +#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, "Any"); + ret = buf; + return &ret; +} + +static const void * +dvr_autorec_entry_class_start_get(void *o) +{ + 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) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + return dvr_autorec_entry_class_time_get(o, v, &dae->dae_stop); +} +#endif + +static htsmsg_t * +dvr_autorec_entry_class_time_list(void *o) +{ + int i; + htsmsg_t *l = htsmsg_create_list(); + char buf[16]; + htsmsg_add_str(l, NULL, "Any"); + for (i = 0; i < 24*60; i += 10) { + snprintf(buf, sizeof(buf), "%02d:%02d", i / 60, (i % 60)); + htsmsg_add_str(l, NULL, buf); + } + return l; +} + +static htsmsg_t * +dvr_autorec_entry_class_minduration_list(void *o) +{ + return dvr_entry_class_duration_list(o, "Any", 24*60); +} + +static htsmsg_t * +dvr_autorec_entry_class_maxduration_list(void *o) +{ + return dvr_entry_class_duration_list(o, "Any", 24*60); +} + +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; +} + +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; + + HTSMSG_FOREACH(f, (htsmsg_t *)v) + if (!htsmsg_field_get_u32(f, &u32) && u32 > 0 && u32 < 8) + bits |= (1 << (u32 - 1)); + + if (bits != dae->dae_weekdays) { + dae->dae_weekdays = bits; + return 1; + } + return 0; +} + +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; +} + +static const struct strtab dvr_autorec_entry_class_weekdays_tab[] = { + { "Mon", 1 }, + { "Tue", 2 }, + { "Wed", 3 }, + { "Thu", 4 }, + { "Fri", 5 }, + { "Sat", 6 }, + { "Sun", 7 }, +}; + +static htsmsg_t * +dvr_autorec_entry_class_weekdays_list ( void *o ) +{ + return strtab2htsmsg(dvr_autorec_entry_class_weekdays_tab); +} + +static char * +dvr_autorec_entry_class_weekdays_rend(void *o) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + 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 strdup(buf + 1); +} + +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; + + if (v && *(char *)v == '\0') + v = NULL; + brand = v ? epg_brand_find_by_uri(v, 1, &save) : NULL; + if (brand && dae->dae_brand != brand) { + if (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; + } + 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; + + if (v && *(char *)v == '\0') + v = NULL; + season = v ? epg_season_find_by_uri(v, 1, &save) : NULL; + if (season && dae->dae_season != season) { + if (dae->dae_season) + 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; + } + 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 +dvr_autorec_entry_class_series_link_set(void *o, const void *v) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + int save; + epg_serieslink_t *sl; + + if (v && *(char *)v == '\0') + v = NULL; + sl = v ? epg_serieslink_find_by_uri(v, 1, &save) : NULL; + 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 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_event = "dvrautorec", + .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 = channel_tag_class_get_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_INT, + .id = "minduration", + .name = "Minimal Duration", + .list = dvr_autorec_entry_class_minduration_list, + .off = offsetof(dvr_autorec_entry_t, dae_minduration), + }, + { + .type = PT_INT, + .id = "maxduration", + .name = "Maximal Duration", + .list = dvr_autorec_entry_class_maxduration_list, + .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 +833,19 @@ 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); + } + htsmsg_destroy(l); + } dvr_autorec_in_init = 0; } @@ -511,12 +855,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 +869,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); -} - - /** * */ @@ -665,7 +905,7 @@ void dvr_autorec_check_serieslink(epg_serieslink_t *s) /** * */ -static void +void dvr_autorec_changed(dvr_autorec_entry_t *dae, int purge) { channel_t *ch; @@ -677,7 +917,7 @@ dvr_autorec_changed(dvr_autorec_entry_t *dae, int purge) CHANNEL_FOREACH(ch) { RB_FOREACH(e, &ch->ch_epg_schedule, sched_link) { if(autorec_cmp(dae, e)) - dvr_entry_create_by_autorec(e, dae); + dvr_entry_create_by_autorec(e, dae); } } } @@ -690,16 +930,24 @@ void 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); - } - - /* Notify web clients that we have messed with the tables */ - m = htsmsg_create_map(); - htsmsg_add_u32(m, "reload", 1); - notify_by_msg("autorec", m); + while((dae = LIST_FIRST(&ch->ch_autorecs)) != NULL) + autorec_entry_destroy(dae, delconf); +} + +/* + * + */ +void +autorec_destroy_by_channel_tag(channel_tag_t *ct, int delconf) +{ + dvr_autorec_entry_t *dae; + + while((dae = LIST_FIRST(&ct->ct_autorecs)) != NULL) { + LIST_REMOVE(dae, dae_channel_tag_link); + dae->dae_channel_tag = NULL; + idnode_notify_simple(&dae->dae_id); + if (delconf) + dvr_autorec_save(dae); + } } diff --git a/src/dvr/dvr_db.c b/src/dvr/dvr_db.c index 7f75dc63..b564cc45 100644 --- a/src/dvr/dvr_db.c +++ b/src/dvr/dvr_db.c @@ -26,13 +26,12 @@ #include "tvheadend.h" #include "dvr.h" -#include "notify.h" #include "htsp_server.h" #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 +42,56 @@ struct dvr_entry_list dvrentries; static gtimer_t dvr_dbus_timer; #endif +static void dvr_entry_destroy(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 inline int extra_valid(time_t extra) +{ + return extra != 0 && extra != (time_t)-1; +} + +static int +dvr_entry_get_start_time( dvr_entry_t *de ) +{ + time_t extra = de->de_start_extra; + + if (!extra_valid(extra)) { + if (de->de_channel) + extra = de->de_channel->ch_dvr_extra_time_pre; + if (!extra_valid(extra)) + 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_valid(extra)) { + if (de->de_channel) + extra = de->de_channel->ch_dvr_extra_time_post; + if (!extra_valid(extra)) + extra = de->de_config->dvr_extra_time_post; + } + 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 +101,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 +110,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,53 +213,14 @@ 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); -} - - -/** - * - */ -void -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, "status", dvr_entry_status(de)); - htsmsg_add_str(m, "schedstate", dvr_entry_schedstatus(de)); - notify_by_msg("dvrdb", m); -} - - -/** - * - */ -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 +228,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,18 +239,19 @@ 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) + if(cfg->dvr_channel_in_title) snprintf(output, outlen, "%s-", DVR_CH_NAME(de)); else output[0] = 0; - snprintf(output + strlen(output), outlen - strlen(output), - "%s", lang_str_get(de->de_title, NULL)); + if (cfg->dvr_omit_title == 0) + snprintf(output + strlen(output), outlen - strlen(output), + "%s", lang_str_get(de->de_title, NULL)); - if(cfg->dvr_flags & DVR_EPISODE_BEFORE_DATE) { - if(cfg->dvr_flags & DVR_EPISODE_IN_TITLE) { + if(cfg->dvr_episode_before_date) { + if(cfg->dvr_episode_in_title) { if(de->de_bcast && de->de_bcast->episode) epg_episode_number_format(de->de_bcast->episode, output + strlen(output), @@ -251,7 +260,7 @@ dvr_make_title(char *output, size_t outlen, dvr_entry_t *de) } } - if(cfg->dvr_flags & DVR_SUBTITLE_IN_TITLE) { + if(cfg->dvr_subtitle_in_title) { if(de->de_bcast && de->de_bcast->episode && de->de_bcast->episode->subtitle) snprintf(output + strlen(output), outlen - strlen(output), ".%s", lang_str_get(de->de_bcast->episode->subtitle, NULL)); @@ -259,18 +268,18 @@ dvr_make_title(char *output, size_t outlen, dvr_entry_t *de) localtime_r(&de->de_start, &tm); - if(cfg->dvr_flags & DVR_DATE_IN_TITLE) { + if(cfg->dvr_date_in_title) { strftime(buf, sizeof(buf), "%F", &tm); snprintf(output + strlen(output), outlen - strlen(output), ".%s", buf); } - if(cfg->dvr_flags & DVR_TIME_IN_TITLE) { + if(cfg->dvr_time_in_title) { strftime(buf, sizeof(buf), "%H-%M", &tm); snprintf(output + strlen(output), outlen - strlen(output), ".%s", buf); } - if(!(cfg->dvr_flags & DVR_EPISODE_BEFORE_DATE)) { - if(cfg->dvr_flags & DVR_EPISODE_IN_TITLE) { + if(!cfg->dvr_episode_before_date) { + if(cfg->dvr_episode_in_title) { if(de->de_bcast && de->de_bcast->episode) epg_episode_number_format(de->de_bcast->episode, output + strlen(output), @@ -283,52 +292,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 @@ -363,116 +364,159 @@ dvr_entry_fuzzy_match(dvr_entry_t *de, epg_broadcast_t *e) return strcmp(title1, title2) == 0; } +/** + * Create the event from config + */ +dvr_entry_t * +dvr_entry_create(const char *uuid, htsmsg_t *conf) +{ + dvr_entry_t *de, *de2; + int64_t start, stop; + const char *s; + + 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)); + + 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; + } + + de->de_mc = -1; + idnode_load(&de->de_id, conf); + + /* 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_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_destroy(de, 0); + return NULL; + } + } + + dvr_entry_set_timer(de); + htsp_dvr_entry_add(de); + + return de; +} + /** * Create the event */ -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) +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; - dvr_config_t *cfg = dvr_config_find_by_name_default(config_name); + lang_str_t *l; + htsmsg_t *conf; - LIST_FOREACH(de, &ch->ch_dvrs, de_channel_link) - if(de->de_start == start && de->de_sched_state != DVR_COMPLETED) - return NULL; - - de = calloc(1, sizeof(dvr_entry_t)); - de->de_id = ++de_tally; - - ch = de->de_channel = ch; - LIST_INSERT_HEAD(&de->de_channel->ch_dvrs, de, de_channel_link); - - de->de_mc = cfg->dvr_mc; - - 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_desc = NULL; - // TODO: this really needs updating + 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)); - dvr_entry_link(de); + de = dvr_entry_create(NULL, conf); - t = de->de_start - de->de_start_extra * 60; + htsmsg_destroy(conf); + + if (de == NULL) + return NULL; + + 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) +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_name, NULL, + dvr_config_t *cfg = dvr_config_find_by_uuid(config_uuid); + if (!cfg) + cfg = dvr_config_find_by_name(config_uuid); + return _dvr_entry_create(cfg ? idnode_uuid_as_str(&cfg->dvr_id) : NULL, + NULL, ch, start, stop, start_extra, stop_extra, title, description, lang, content_type, creator, dae, pri); @@ -482,7 +526,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 +535,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 +562,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_episode_duplicate; if (ep_dup_det) { const char* de_title = lang_str_get(de->de_bcast->episode->title, NULL); @@ -556,7 +599,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,30 +612,31 @@ 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); + if(de->de_config != NULL) + LIST_REMOVE(de, de_config_link); + free(de->de_filename); - free(de->de_config_name); 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); } - - - /** * */ static void -dvr_entry_remove(dvr_entry_t *de, int delconf) +dvr_entry_destroy(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 +656,30 @@ 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_destroy_by_config(dvr_config_t *cfg, int delconf) { 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; + dvr_config_t *def = NULL; - 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); + while ((de = LIST_FIRST(&cfg->dvr_entries)) != NULL) { + LIST_REMOVE(de, de_config_link); + if (!def) + def = dvr_config_find_by_name_default(""); + de->de_config = def; + if (def) + LIST_INSERT_HEAD(&def->dvr_entries, de, de_config_link); + if (delconf) + dvr_entry_save(de); } - - 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 +693,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 +709,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); } @@ -818,7 +722,7 @@ static void dvr_timer_expire(void *aux) { dvr_entry_t *de = aux; - dvr_entry_remove(de, 1); + dvr_entry_destroy(de, 1); } @@ -829,6 +733,9 @@ static dvr_entry_t *_dvr_entry_update { int save = 0; + if (!dvr_entry_is_editable(de)) + return de; + /* Start/Stop */ if (e) { start = e->start; @@ -873,8 +780,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; } } @@ -890,9 +797,8 @@ static dvr_entry_t *_dvr_entry_update /* Save changes */ if (save) { - dvr_entry_save(de); + idnode_changed(&de->de_id); htsp_dvr_entry_update(de); - dvr_entry_notify(de); tvhlog(LOG_INFO, "dvr", "\"%s\" on \"%s\": Updated Timer", lang_str_get(de->de_title, NULL), DVR_CH_NAME(de)); } @@ -930,9 +836,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); @@ -946,7 +853,7 @@ dvr_event_replaced(epg_broadcast_t *e, epg_broadcast_t *new_e) /* If this was craeted by autorec - just remove it, it'll get recreated */ if (de->de_autorec) { - dvr_entry_remove(de, 1); + dvr_entry_destroy(de, 1); /* Find match */ } else { @@ -981,9 +888,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); @@ -999,11 +907,13 @@ void dvr_event_updated ( epg_broadcast_t *e ) * */ static void -dvr_stop_recording(dvr_entry_t *de, int stopcode, int delconf) +dvr_stop_recording(dvr_entry_t *de, int stopcode, int saveconf) { - 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) + if (de->de_rec_state == DVR_RS_PENDING || + de->de_rec_state == DVR_RS_WAIT_PROGRAM_START || + de->de_filename == NULL) de->de_sched_state = DVR_MISSED_TIME; else _dvr_entry_completed(de); @@ -1015,10 +925,10 @@ dvr_stop_recording(dvr_entry_t *de, int stopcode, int delconf) lang_str_get(de->de_title, NULL), DVR_CH_NAME(de), dvr_entry_status(de)); - if (delconf) + if (saveconf) dvr_entry_save(de); + idnode_notify_simple(&de->de_id); htsp_dvr_entry_update(de); - dvr_entry_notify(de); gtimer_arm_abs(&de->de_timer, dvr_timer_expire, de, de->de_stop + cfg->dvr_retention_days * 86400); @@ -1050,7 +960,7 @@ dvr_timer_start_recording(void *aux) tvhlog(LOG_INFO, "dvr", "\"%s\" on \"%s\" recorder starting", lang_str_get(de->de_title, NULL), DVR_CH_NAME(de)); - dvr_entry_notify(de); + idnode_changed(&de->de_id); htsp_dvr_entry_update(de); dvr_rec_subscribe(de); @@ -1067,7 +977,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; } @@ -1115,7 +1025,7 @@ dvr_entry_cancel(dvr_entry_t *de) { switch(de->de_sched_state) { case DVR_SCHEDULED: - dvr_entry_remove(de, 1); + dvr_entry_destroy(de, 1); return NULL; case DVR_RECORDING: @@ -1124,11 +1034,11 @@ dvr_entry_cancel(dvr_entry_t *de) return de; case DVR_COMPLETED: - dvr_entry_remove(de, 1); + dvr_entry_destroy(de, 1); return NULL; case DVR_MISSED_TIME: - dvr_entry_remove(de, 1); + dvr_entry_destroy(de, 1); return NULL; default: @@ -1147,6 +1057,825 @@ dvr_entry_purge(dvr_entry_t *de, int delconf) dvr_stop_recording(de, SM_CODE_SOURCE_DELETED, delconf); } + +/* ************************************************************************** + * DVR Entry Class definition + * **************************************************************************/ + +static void +dvr_entry_class_save(idnode_t *self) +{ + dvr_entry_t *de = (dvr_entry_t *)self; + dvr_entry_save(de); + if (dvr_entry_is_valid(de)) + dvr_entry_set_timer(de); +} + +static void +dvr_entry_class_delete(idnode_t *self) +{ + dvr_entry_cancel_delete((dvr_entry_t *)self); +} + +static const char * +dvr_entry_class_get_title (idnode_t *self) +{ + dvr_entry_t *de = (dvr_entry_t *)self; + const char *s; + s = lang_str_get(de->de_title, NULL); + if (s == NULL || s[0] == '\0') + s = lang_str_get(de->de_desc, NULL); + return s; +} + +static int +dvr_entry_class_time_set(dvr_entry_t *de, time_t *v, time_t nv) +{ + if (!dvr_entry_is_editable(de)) + return 0; + if (nv != *v) { + *v = nv; + return 1; + } + return 0; +} + +static int +dvr_entry_class_int_set(dvr_entry_t *de, int *v, int nv) +{ + if (!dvr_entry_is_editable(de)) + return 0; + if (nv != *v) { + *v = nv; + return 1; + } + return 0; +} + +static int +dvr_entry_class_start_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + return dvr_entry_class_time_set(de, &de->de_start, *(time_t *)v); +} + +static uint32_t +dvr_entry_class_start_opts(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + if (de && !dvr_entry_is_editable(de)) + return PO_RDONLY; + return 0; +} + +static uint32_t +dvr_entry_class_start_extra_opts(void *o) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + if (de && !dvr_entry_is_editable(de)) + return PO_RDONLY | PO_DURATION; + return PO_DURATION; +} + +static int +dvr_entry_class_start_extra_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + return dvr_entry_class_time_set(de, &de->de_start_extra, *(time_t *)v); +} + +static int +dvr_entry_class_stop_set(void *o, const void *_v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + time_t v = *(time_t *)_v; + + if (!dvr_entry_is_editable(de)) { + if (v < dispatch_clock) + v = dispatch_clock; + } + if (v < de->de_start) + v = de->de_start; + return dvr_entry_class_time_set(de, &de->de_stop, v); +} + +static int +dvr_entry_class_config_name_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + dvr_config_t *cfg; + + if (!dvr_entry_is_editable(de)) + return 0; + cfg = v ? dvr_config_find_by_uuid(v) : NULL; + if (!cfg) + cfg = dvr_config_find_by_name_default(v); + if (cfg == NULL) { + if (de->de_config) { + LIST_REMOVE(de, de_config_link); + de->de_config = NULL; + return 1; + } + } else if (de->de_config != cfg) { + if (de->de_config) + LIST_REMOVE(de, de_config_link); + de->de_config = cfg; + LIST_INSERT_HEAD(&cfg->dvr_entries, de, de_config_link); + return 1; + } + return 0; +} + +static const void * +dvr_entry_class_config_name_get(void *o) +{ + static const char *ret; + dvr_entry_t *de = (dvr_entry_t *)o; + if (de->de_config) + ret = idnode_uuid_as_str(&de->de_config->dvr_id); + else + ret = ""; + return &ret; +} + +static int +dvr_entry_class_channel_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + channel_t *ch; + + if (!dvr_entry_is_editable(de)) + return 0; + ch = v ? channel_find_by_uuid(v) : NULL; + if (!de->de_config) { + de->de_config = dvr_config_find_by_name_default(""); + if (de->de_config) + LIST_INSERT_HEAD(&de->de_config->dvr_entries, de, de_config_link); + } + if (ch == NULL) { + if (de->de_channel) { + LIST_REMOVE(de, de_channel_link); + free(de->de_channel_name); + de->de_channel_name = NULL; + 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; +} + +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; +} + +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; +} + +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 (!dvr_entry_is_editable(de)) + return 0; + if (!de->de_config) { + de->de_config = dvr_config_find_by_name_default(""); + if (de->de_config) + LIST_INSERT_HEAD(&de->de_config->dvr_entries, de, de_config_link); + } + 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; + } +} + +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; +} + +static int +dvr_entry_class_pri_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + return dvr_entry_class_int_set(de, &de->de_pri, *(int *)v); +} + +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 int +dvr_entry_class_mc_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + return dvr_entry_class_int_set(de, &de->de_mc, *(int *)v); +} + +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); +} + +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; +} + + +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; + if (!dvr_entry_is_editable(de)) + return 0; + 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; +} + +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 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; + if (!dvr_entry_is_editable(de)) + return 0; + 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; +} + +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; +} + +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 (v == NULL || *((char *)v) == '\0') + v = "UnknownTitle"; + 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; +} + +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; +} + +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; +} + +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; +} + +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; +} + +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; +} + +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'; + } + s = buf; + return &s; +} + +htsmsg_t * +dvr_entry_class_duration_list(void *o, const char *not_set, int max) +{ + 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); + 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 <= max; 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_extra_list(void *o) +{ + return dvr_entry_class_duration_list(o, "Not set (use channel or DVR config)", 4*60); +} + +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_event = "dvrentry", + .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", + .set = dvr_entry_class_start_set, + .off = offsetof(dvr_entry_t, de_start), + .get_opts = dvr_entry_class_start_opts, + }, + { + .type = PT_TIME, + .id = "start_extra", + .name = "Extra Start Time", + .off = offsetof(dvr_entry_t, de_start_extra), + .set = dvr_entry_class_start_extra_set, + .list = dvr_entry_class_extra_list, + .get_opts = dvr_entry_class_start_extra_opts, + .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", + .set = dvr_entry_class_stop_set, + .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, + .get_opts = dvr_entry_class_start_opts, + }, + { + .type = PT_STR, + .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, + .set = dvr_entry_class_pri_set, + .list = dvr_entry_class_pri_list, + }, + { + .type = PT_INT, + .id = "container", + .name = "Container", + .off = offsetof(dvr_entry_t, de_mc), + .def.i = MC_MATROSKA, + .set = dvr_entry_class_mc_set, + .list = dvr_entry_class_mc_list, + }, + { + .type = PT_STR, + .id = "config_name", + .name = "DVR Configuration", + .set = dvr_entry_class_config_name_set, + .get = dvr_entry_class_config_name_get, + .list = dvr_entry_class_config_name_list, + .get_opts = dvr_entry_class_start_opts, + }, + { + .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, + .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, + .opts = PO_RDONLY, + }, + { + .type = PT_STR, + .id = "episode", + .name = "Episode", + .get = dvr_entry_class_episode_get, + .opts = PO_RDONLY | PO_NOSAVE | PO_HIDDEN, + }, + { + .type = PT_STR, + .id = "url", + .name = "URL", + .get = dvr_entry_class_url_get, + .opts = PO_RDONLY | PO_NOSAVE | PO_HIDDEN, + }, + { + .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, + }, + {} + } +}; + /** * */ @@ -1163,186 +1892,6 @@ dvr_destroy_by_channel(channel_t *ch, int delconf) } } -/** - * - */ -void -dvr_init(void) -{ - htsmsg_t *m, *l; - htsmsg_field_t *f; - 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 */ - - LIST_INIT(&dvrconfigs); - cfg = dvr_config_create(""); - - /* Override settings with config */ - - 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; - } - - 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")); - -/* - * 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 = 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; - - if(!htsmsg_get_u32(m, "episode-in-title", &u32) && u32) - cfg->dvr_flags |= DVR_EPISODE_IN_TITLE; - - 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; - - if(!htsmsg_get_u32(m, "skip-commercials", &u32) && !u32) - cfg->dvr_flags &= ~DVR_SKIP_COMMERCIALS; - - if(!htsmsg_get_u32(m, "subtitle-in-title", &u32) && u32) - cfg->dvr_flags |= DVR_SUBTITLE_IN_TITLE; - - if(!htsmsg_get_u32(m, "episode-before-date", &u32) && u32) - cfg->dvr_flags |= DVR_EPISODE_BEFORE_DATE; - - if(!htsmsg_get_u32(m, "episode-duplicate-detection", &u32) && u32) - cfg->dvr_flags |= DVR_EPISODE_DUPLICATE_DETECTION; - - dvr_charset_update(cfg, htsmsg_get_str(m, "charset")); - - tvh_str_set(&cfg->dvr_postproc, htsmsg_get_str(m, "postproc")); - } - - htsmsg_destroy(l); - } - - 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) { - 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(&dvrentries)) != NULL) - dvr_entry_remove(de, 0); - pthread_mutex_unlock(&global_lock); - dvr_autorec_done(); -} - /** * find a dvr config by name, return NULL if not found */ @@ -1372,12 +1921,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,20 +1942,31 @@ 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); + LIST_INIT(&cfg->dvr_entries); + LIST_INIT(&cfg->dvr_accesses); + + 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; + cfg->dvr_tag_files = 1; + cfg->dvr_skip_commercials = 1; dvr_charset_update(cfg, intlconv_filesystem_charset()); /* series link support */ @@ -1414,7 +1979,7 @@ dvr_config_create(const char *name) /* Muxer config */ cfg->dvr_muxcnf.m_cache = MC_CACHE_DONTKEEP; - cfg->dvr_muxcnf.m_flags |= MC_REWRITE_PAT; + cfg->dvr_muxcnf.m_rewrite_pat = 1; /* dup detect */ cfg->dvr_dup_detect_episode = 1; // detect dup episodes @@ -1423,16 +1988,49 @@ 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); + if (dvr_config_is_default(cfg)) + cfg->dvr_enabled = 1; + cfg->dvr_valid = 1; + } + 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 + */ +static void +dvr_config_destroy(dvr_config_t *cfg, int delconf) +{ + if (delconf) { + tvhlog(LOG_INFO, "dvr", "Deleting configuration '%s'", + cfg->dvr_config_name); + hts_settings_remove("dvr/config/%s", idnode_uuid_as_str(&cfg->dvr_id)); + } + LIST_REMOVE(cfg, config_link); + idnode_unlink(&cfg->dvr_id); + + dvr_entry_destroy_by_config(cfg, delconf); + access_destroy_by_dvr_config(cfg, delconf); + + free(cfg->dvr_charset_id); + free(cfg->dvr_charset); + free(cfg->dvr_storage); + free(cfg->dvr_config_name); + free(cfg); } /** * */ -void +void dvr_config_delete(const char *name) { dvr_config_t *cfg; @@ -1443,256 +2041,409 @@ dvr_config_delete(const char *name) } cfg = dvr_config_find_by_name(name); - if (cfg != NULL) { - 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(); - } + if (cfg != NULL) + dvr_config_destroy(cfg, 1); } -/** +/* * */ -static void -dvr_save(dvr_config_t *cfg) +void +dvr_config_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 */ + lock_assert(&global_lock); - 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); + idnode_save(&cfg->dvr_id, m); + hts_settings_save(m, "dvr/config/%s", idnode_uuid_as_str(&cfg->dvr_id)); htsmsg_destroy(m); - - dvrconfig_changed(); } -/** - * - */ -void -dvr_storage_set(dvr_config_t *cfg, const char *storage) +/* ************************************************************************** + * DVR Config Class definition + * **************************************************************************/ + +static void +dvr_config_class_save(idnode_t *self) { - if(cfg->dvr_storage != NULL && !strcmp(cfg->dvr_storage, storage)) - return; - - tvh_str_set(&cfg->dvr_storage, storage); - dvr_save(cfg); + dvr_config_t *cfg = (dvr_config_t *)self; + if (dvr_config_is_default(cfg)) + cfg->dvr_enabled = 1; + cfg->dvr_valid = 1; + dvr_config_save(cfg); } -/** - * - */ -void -dvr_charset_set(dvr_config_t *cfg, const char *charset) +static void +dvr_config_class_delete(idnode_t *self) { - if(cfg->dvr_charset != NULL && !strcmp(cfg->dvr_charset, charset)) - return; - - dvr_charset_update(cfg, charset); - dvr_save(cfg); + dvr_config_t *cfg = (dvr_config_t *)self; + if (!dvr_config_is_default(cfg)) + dvr_config_destroy(cfg, 1); } -/** - * - */ -void -dvr_file_permissions_set(dvr_config_t *cfg, int permissions) +static int +dvr_config_class_perm(idnode_t *self, access_t *a, htsmsg_t *msg_to_write) { - if(cfg->dvr_muxcnf.m_file_permissions == permissions) - return; + dvr_config_t *cfg = (dvr_config_t *)self; + htsmsg_field_t *f; + const char *uuid, *my_uuid; - cfg->dvr_muxcnf.m_file_permissions = permissions; - dvr_save(cfg); + if (access_verify2(a, ACCESS_RECORDER)) + return -1; + if (!access_verify2(a, ACCESS_ADMIN)) + return 0; + if (a->aa_dvrcfgs) { + my_uuid = idnode_uuid_as_str(&cfg->dvr_id); + HTSMSG_FOREACH(f, a->aa_dvrcfgs) { + uuid = htsmsg_field_get_str(f) ?: ""; + if (!strcmp(uuid, my_uuid)) + goto fine; + } + return -1; + } +fine: + if (strcmp(cfg->dvr_config_name ?: "", a->aa_username ?: "")) + return -1; + return 0; } -/** - * - */ -void -dvr_directory_permissions_set(dvr_config_t *cfg, int permissions) +static int +dvr_config_class_enabled_set(void *o, const void *v) { - if(cfg->dvr_muxcnf.m_directory_permissions == permissions) - return; - - cfg->dvr_muxcnf.m_directory_permissions = permissions; - dvr_save(cfg); + dvr_config_t *cfg = (dvr_config_t *)o; + if (dvr_config_is_default(cfg) && dvr_config_is_valid(cfg)) + return 0; + if (cfg->dvr_enabled != *(int *)v) { + cfg->dvr_enabled = *(int *)v; + return 1; + } + return 0; } -/** - * - */ -void -dvr_container_set(dvr_config_t *cfg, const char *container) +static uint32_t +dvr_config_class_enabled_opts(void *o) { - 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); + dvr_config_t *cfg = (dvr_config_t *)o; + if (cfg && dvr_config_is_default(cfg) && dvr_config_is_valid(cfg)) + return PO_RDONLY; + return 0; } -/** - * - */ -void -dvr_mux_cache_set(dvr_config_t *cfg, int mcache) +static int +dvr_config_class_name_set(void *o, const void *v) { - if (mcache < MC_CACHE_UNKNOWN || mcache > MC_CACHE_LAST) - mcache = MC_CACHE_UNKNOWN; - - if(cfg->dvr_muxcnf.m_cache == mcache) - return; - - cfg->dvr_muxcnf.m_cache = mcache; - - dvr_save(cfg); + dvr_config_t *cfg = (dvr_config_t *)o; + if (dvr_config_is_default(cfg) && dvr_config_is_valid(cfg)) + return 0; + if (strcmp(cfg->dvr_config_name ?: "", v ?: "")) { + if (dvr_config_is_valid(cfg) && (v == NULL || *(char *)v == '\0')) + return 0; + free(cfg->dvr_config_name); + cfg->dvr_config_name = strdup(v); + return 1; + } + return 0; } - -/** - * - */ -void -dvr_postproc_set(dvr_config_t *cfg, const char *postproc) +static const char * +dvr_config_class_get_title (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_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_retention_set(dvr_config_t *cfg, int days) +static int +dvr_config_class_charset_set(void *o, const void *v) { - 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_t *cfg = (dvr_config_t *)o; + return dvr_charset_update(cfg, v); } - -/** - * - */ -void -dvr_flags_set(dvr_config_t *cfg, int flags) +static htsmsg_t * +dvr_config_class_charset_list(void *o) { - if(cfg->dvr_flags == flags) - return; - - cfg->dvr_flags = flags; - dvr_save(cfg); + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_str(m, "type", "api"); + htsmsg_add_str(m, "uri", "intlconv/charsets"); + return m; } -/** - * - */ -void -dvr_mux_flags_set(dvr_config_t *cfg, int flags) +static htsmsg_t * +dvr_config_class_cache_list(void *o) { - if(cfg->dvr_muxcnf.m_flags == flags) - return; - - cfg->dvr_muxcnf.m_flags = flags; - dvr_save(cfg); -} - - -/** - * - */ -void -dvr_extra_time_pre_set(dvr_config_t *cfg, int d) -{ - if(cfg->dvr_extra_time_pre == d) - return; - - cfg->dvr_extra_time_pre = d; - dvr_save(cfg); -} - - -/** - * - */ -void -dvr_extra_time_post_set(dvr_config_t *cfg, int d) -{ - if(cfg->dvr_extra_time_post == d) - return; - - cfg->dvr_extra_time_post = d; - dvr_save(cfg); + 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_event = "dvrconfig", + .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", + .set = dvr_config_class_enabled_set, + .off = offsetof(dvr_config_t, dvr_enabled), + .def.i = 1, + .group = 1, + .get_opts = dvr_config_class_enabled_opts, + }, + { + .type = PT_STR, + .id = "name", + .name = "Config Name", + .set = dvr_config_class_name_set, + .off = offsetof(dvr_config_t, dvr_config_name), + .def.s = "! New config", + .group = 1, + .get_opts = dvr_config_class_enabled_opts, + }, + { + .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_muxcnf.m_rewrite_pat), + .def.i = 1, + .group = 2, + }, + { + .type = PT_BOOL, + .id = "rewrite-pmt", + .name = "Rewrite PMT", + .off = offsetof(dvr_config_t, dvr_muxcnf.m_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_episode_in_title), + .group = 4, + }, + { + .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 = 4, + }, + { + .type = PT_BOOL, + .id = "subtitle-in-title", + .name = "Include Subtitle In Filename", + .off = offsetof(dvr_config_t, dvr_subtitle_in_title), + .group = 5, + }, + { + .type = PT_BOOL, + .id = "omit-title", + .name = "Do Not Include Title To Filename", + .off = offsetof(dvr_config_t, dvr_omit_title), + .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,12 +2591,12 @@ 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); - if(cfg->dvr_flags & DVR_DIR_PER_TITLE || cfg->dvr_flags & DVR_DIR_PER_CHANNEL || cfg->dvr_flags & DVR_DIR_PER_DAY) { + if(cfg->dvr_title_dir || cfg->dvr_channel_dir || cfg->dvr_dir_per_day) { char *p; int l; @@ -1866,7 +2617,7 @@ dvr_entry_delete(dvr_entry_t *de) } } - dvr_entry_remove(de, 1); + dvr_entry_destroy(de, 1); } /** @@ -1877,7 +2628,7 @@ dvr_entry_cancel_delete(dvr_entry_t *de) { switch(de->de_sched_state) { case DVR_SCHEDULED: - dvr_entry_remove(de, 1); + dvr_entry_destroy(de, 1); break; case DVR_RECORDING: @@ -1888,10 +2639,101 @@ dvr_entry_cancel_delete(dvr_entry_t *de) break; case DVR_MISSED_TIME: - dvr_entry_remove(de, 1); + dvr_entry_destroy(de, 1); break; default: abort(); } } + +/** + * + */ +void +dvr_config_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); + } + } +} + +void +dvr_init(void) +{ +#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 ((de = LIST_FIRST(&dvrentries)) != NULL) + dvr_entry_destroy(de, 0); + while ((cfg = LIST_FIRST(&dvrconfigs)) != NULL) + dvr_config_destroy(cfg, 0); + pthread_mutex_unlock(&global_lock); + dvr_autorec_done(); +} diff --git a/src/dvr/dvr_inotify.c b/src/dvr/dvr_inotify.c index ba439f96..e9aa6441 100644 --- a/src/dvr/dvr_inotify.c +++ b/src/dvr/dvr_inotify.c @@ -115,17 +115,16 @@ void dvr_inotify_add ( dvr_entry_t *de ) SKEL_USED(dvr_inotify_entry_skel); e->path = strdup(e->path); e->fd = inotify_add_watch(_inot_fd, e->path, EVENT_MASK); - if (e->fd == -1) { - tvhlog(LOG_ERR, "dvr", "failed to add inotify watch to %s (err=%s)", - e->path, strerror(errno)); - free(path); - dvr_inotify_del(de); - return; - } } LIST_INSERT_HEAD(&e->entries, de, de_inotify_link); + if (e->fd < 0) { + tvhlog(LOG_ERR, "dvr", "failed to add inotify watch to %s (err=%s)", + e->path, strerror(errno)); + dvr_inotify_del(de); + } + free(path); } @@ -146,7 +145,8 @@ void dvr_inotify_del ( dvr_entry_t *de ) LIST_REMOVE(det, de_inotify_link); if (LIST_FIRST(&e->entries) == NULL) { RB_REMOVE(&_inot_tree, e, link); - inotify_rm_watch(_inot_fd, e->fd); + if (e->fd >= 0) + inotify_rm_watch(_inot_fd, e->fd); free(e->path); free(e); } @@ -180,7 +180,7 @@ _dvr_inotify_find2 snprintf(path, sizeof(path), "%s/%s", die->path, name); LIST_FOREACH(de, &die->entries, de_inotify_link) - if (!strcmp(path, de->de_filename)) + if (de->de_filename && !strcmp(path, de->de_filename)) break; return de; @@ -211,7 +211,7 @@ _dvr_inotify_moved dvr_inotify_del(de); htsp_dvr_entry_update(de); - dvr_entry_notify(de); + idnode_notify_simple(&de->de_id); } /* @@ -239,7 +239,7 @@ _dvr_inotify_moved_all while ((de = LIST_FIRST(&die->entries))) { htsp_dvr_entry_update(de); - dvr_entry_notify(de); + idnode_notify_simple(&de->de_id); dvr_inotify_del(de); } } diff --git a/src/dvr/dvr_rec.c b/src/dvr/dvr_rec.c index 66bd2e24..dbade701 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; @@ -126,7 +126,7 @@ dvr_rec_unsubscribe(dvr_entry_t *de, int stopcode) static char * cleanup_filename(char *s, dvr_config_t *cfg) { - int i, len = strlen(s), dvr_flags = cfg->dvr_flags; + int i, len = strlen(s); char *s1; s1 = intlconv_utf8safestr(cfg->dvr_charset_id, s, len * 2); @@ -148,11 +148,11 @@ cleanup_filename(char *s, dvr_config_t *cfg) if(s[i] == '/') s[i] = '-'; - else if((dvr_flags & DVR_WHITESPACE_IN_TITLE) && + else if(cfg->dvr_whitespace_in_title && (s[i] == ' ' || s[i] == '\t')) s[i] = '-'; - else if((dvr_flags & DVR_CLEAN_TITLE) && + else if(cfg->dvr_clean_title && ((s[i] < 32) || (s[i] > 122) || (strchr("/:\\<>|*?'\"", s[i]) != NULL))) s[i] = '_'; @@ -177,7 +177,10 @@ pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss) struct stat st; char *filename, *s; struct tm tm; - dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); + dvr_config_t *cfg = de->de_config; + + if (de == NULL) + return -1; strncpy(path, cfg->dvr_storage, sizeof(path)); path[sizeof(path)-1] = '\0'; @@ -187,7 +190,7 @@ pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss) path[strlen(path)-1] = '\0'; /* Append per-day directory */ - if (cfg->dvr_flags & DVR_DIR_PER_DAY) { + if (cfg->dvr_dir_per_day) { localtime_r(&de->de_start, &tm); strftime(fullname, sizeof(fullname), "%F", &tm); s = cleanup_filename(fullname, cfg); @@ -198,7 +201,7 @@ pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss) } /* Append per-channel directory */ - if (cfg->dvr_flags & DVR_DIR_PER_CHANNEL) { + if (cfg->dvr_channel_dir) { char *chname = strdup(DVR_CH_NAME(de)); s = cleanup_filename(chname, cfg); free(chname); @@ -211,7 +214,7 @@ pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss) // TODO: per-brand, per-season /* Append per-title directory */ - if (cfg->dvr_flags & DVR_DIR_PER_TITLE) { + if (cfg->dvr_title_dir) { char *title = strdup(lang_str_get(de->de_title, NULL)); s = cleanup_filename(title, cfg); free(title); @@ -292,7 +295,7 @@ dvr_rec_set_state(dvr_entry_t *de, dvr_rs_state_t newstate, int error) de->de_errors++; } if (notify) - dvr_entry_notify(de); + idnode_notify_simple(&de->de_id); } /** @@ -304,10 +307,15 @@ dvr_rec_start(dvr_entry_t *de, const streaming_start_t *ss) const source_info_t *si = &ss->ss_si; const streaming_start_component_t *ssc; int i; - dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); + dvr_config_t *cfg = de->de_config; muxer_container_type_t mc; - mc = de->de_mc; + if (!cfg) { + dvr_rec_fatal_error(de, "Unable to determine config profile"); + return -1; + } + + mc = dvr_entry_get_mc(de); de->de_mux = muxer_create(mc, &cfg->dvr_muxcnf); if(!de->de_mux) { @@ -330,7 +338,7 @@ dvr_rec_start(dvr_entry_t *de, const streaming_start_t *ss) return -1; } - if(cfg->dvr_flags & DVR_TAG_FILES && de->de_bcast) { + if(cfg->dvr_tag_files && de->de_bcast) { if(muxer_write_meta(de->de_mux, de->de_bcast)) { dvr_rec_fatal_error(de, "Unable to write meta data"); return -1; @@ -427,13 +435,13 @@ static void * dvr_thread(void *aux) { dvr_entry_t *de = aux; - dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); + dvr_config_t *cfg = de->de_config; streaming_queue_t *sq = &de->de_sq; streaming_message_t *sm; th_pkt_t *pkt; int run = 1; int started = 0; - int comm_skip = (cfg->dvr_flags & DVR_SKIP_COMMERCIALS); + int comm_skip = cfg->dvr_skip_commercials; int commercial = COMMERCIAL_UNKNOWN; pthread_mutex_lock(&sq->sq_mutex); @@ -508,9 +516,8 @@ dvr_thread(void *aux) dvr_rec_set_state(de, DVR_RS_WAIT_PROGRAM_START, 0); if(dvr_rec_start(de, sm->sm_data) == 0) { started = 1; - dvr_entry_notify(de); + idnode_changed(&de->de_id); htsp_dvr_entry_update(de); - dvr_entry_save(de); } pthread_mutex_unlock(&global_lock); } @@ -666,7 +673,7 @@ dvr_thread_epilog(dvr_entry_t *de) muxer_destroy(de->de_mux); de->de_mux = NULL; - dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); - if(cfg->dvr_postproc && de->de_filename) + dvr_config_t *cfg = de->de_config; + if(cfg && cfg->dvr_postproc && de->de_filename) dvr_spawn_postproc(de,cfg->dvr_postproc); } diff --git a/src/epg.c b/src/epg.c index 02e01bcc..74264595 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/epggrab.c b/src/epggrab.c index a35b1c8a..97d6d947 100644 --- a/src/epggrab.c +++ b/src/epggrab.c @@ -417,4 +417,5 @@ void epggrab_done ( void ) epggrab_cron = NULL; free(epggrab_cron_multi); epggrab_cron_multi = NULL; + epggrab_channel_done(); } diff --git a/src/epggrab/channel.c b/src/epggrab/channel.c index 8cdff425..7f02f1da 100644 --- a/src/epggrab/channel.c +++ b/src/epggrab/channel.c @@ -27,6 +27,8 @@ #include #include +SKEL_DECLARE(epggrab_channel_skel, epggrab_channel_t); + /* ************************************************************************** * EPG Grab Channel functions * *************************************************************************/ @@ -189,13 +191,12 @@ epggrab_channel_t *epggrab_channel_find { char *s; epggrab_channel_t *ec; - static epggrab_channel_t *skel = NULL; - if (!skel) skel = calloc(1, sizeof(epggrab_channel_t)); - skel->id = tvh_strdupa(id); + + SKEL_ALLOC(epggrab_channel_skel); + s = epggrab_channel_skel->id = tvh_strdupa(id); /* Replace / with # */ // Note: this is a bit of a nasty fix for #1774, but will do for now - s = skel->id; while (*s) { if (*s == '/') *s = '#'; s++; @@ -203,18 +204,19 @@ epggrab_channel_t *epggrab_channel_find /* Find */ if (!create) { - ec = RB_FIND(tree, skel, link, _ch_id_cmp); + ec = RB_FIND(tree, epggrab_channel_skel, link, _ch_id_cmp); /* Find/Create */ } else { - ec = RB_INSERT_SORTED(tree, skel, link, _ch_id_cmp); + ec = RB_INSERT_SORTED(tree, epggrab_channel_skel, link, _ch_id_cmp); if (!ec) { assert(owner); - ec = skel; - ec->id = strdup(skel->id); + ec = epggrab_channel_skel; + SKEL_USED(epggrab_channel_skel); + ec->id = strdup(ec->id); ec->mod = owner; - skel = NULL; *save = 1; + return ec; } } return ec; @@ -299,3 +301,9 @@ epggrab_channel_is_ota ( epggrab_channel_t *ec ) { return ec->mod->type == EPGGRAB_OTA; } + +void +epggrab_channel_done( void ) +{ + SKEL_FREE(epggrab_channel_skel); +} diff --git a/src/epggrab/module/eit.c b/src/epggrab/module/eit.c index 12faaf9d..13f6b63e 100644 --- a/src/epggrab/module/eit.c +++ b/src/epggrab/module/eit.c @@ -713,7 +713,7 @@ static int _eit_tune // consider changeing it? for (osl = RB_FIRST(&map->om_svcs); osl != NULL; osl = nxt) { nxt = RB_NEXT(osl, link); - /* rule: if 5 mux scans fails for this service, remove it */ + /* rule: if 5 mux scans fail for this service, remove it */ if (osl->last_tune_count + 5 <= map->om_tune_count || !(s = mpegts_service_find_by_uuid(osl->uuid))) { epggrab_ota_service_del(map, om, osl, 1); diff --git a/src/epggrab/otamux.c b/src/epggrab/otamux.c index e6ff17b4..aa6836cc 100644 --- a/src/epggrab/otamux.c +++ b/src/epggrab/otamux.c @@ -416,13 +416,13 @@ epggrab_ota_kick_cb ( void *p ) [MM_EPG_DISABLE] = NULL, [MM_EPG_ENABLE] = NULL, [MM_EPG_FORCE] = NULL, - [MM_EPG_FORCE_EIT] = "eit", - [MM_EPG_FORCE_UK_FREESAT] = "uk_freesat", - [MM_EPG_FORCE_UK_FREEVIEW] = "uk_freeview", - [MM_EPG_FORCE_VIASAT_BALTIC] = "viasat_baltic", - [MM_EPG_FORCE_OPENTV_SKY_UK] = "opentv-skyuk", - [MM_EPG_FORCE_OPENTV_SKY_ITALIA] = "opentv-skyit", - [MM_EPG_FORCE_OPENTV_SKY_AUSAT] = "opentv-ausat", + [MM_EPG_ONLY_EIT] = "eit", + [MM_EPG_ONLY_UK_FREESAT] = "uk_freesat", + [MM_EPG_ONLY_UK_FREEVIEW] = "uk_freeview", + [MM_EPG_ONLY_VIASAT_BALTIC] = "viasat_baltic", + [MM_EPG_ONLY_OPENTV_SKY_UK] = "opentv-skyuk", + [MM_EPG_ONLY_OPENTV_SKY_ITALIA] = "opentv-skyit", + [MM_EPG_ONLY_OPENTV_SKY_AUSAT] = "opentv-ausat", }; lock_assert(&global_lock); @@ -476,26 +476,25 @@ next_one: goto done; } - if (epg_flag != MM_EPG_FORCE) { - /* Check we have modules attached and enabled */ - LIST_FOREACH(map, &om->om_modules, om_link) { - if (map->om_module->tune(map, om, mm)) - break; - } - if (!map) { - char name[256]; - mpegts_mux_nice_name(mm, name, sizeof(name)); - tvhdebug("epggrab", "no OTA modules active for %s, check again next time", name); - goto done; + /* Check we have modules attached and enabled */ + i = r = 0; + LIST_FOREACH(map, &om->om_modules, om_link) { + if (map->om_module->tune(map, om, mm)) { + i++; + if (modname && !strcmp(modname, map->om_module->id)) + r = 1; } } + if ((i == 0 || (r == 0 && modname)) && epg_flag != MM_EPG_FORCE) { + char name[256]; + mpegts_mux_nice_name(mm, name, sizeof(name)); + tvhdebug("epggrab", "no OTA modules active for %s, check again next time", name); + goto done; + } /* Some init stuff */ free(om->om_force_modname); - if (modname) - om->om_force_modname = strdup(modname); - else - om->om_force_modname = NULL; + om->om_force_modname = modname ? strdup(modname) : NULL; /* Subscribe to the mux */ if ((r = mpegts_mux_subscribe(mm, "epggrab", SUBSCRIPTION_PRIO_EPG))) { @@ -555,6 +554,8 @@ epggrab_ota_start_cb ( void *p ) pthread_mutex_lock(&epggrab_ota_mutex); if (!cron_multi_next(epggrab_ota_cron_multi, dispatch_clock, &next)) epggrab_ota_next_arm(next); + else + tvhwarn("epggrab", "ota cron config invalid or unset"); pthread_mutex_unlock(&epggrab_ota_mutex); } @@ -570,6 +571,8 @@ epggrab_ota_arm ( time_t last ) if (last != (time_t)-1 && last + 1800 > next) next = last + 1800; epggrab_ota_next_arm(next); + } else { + tvhwarn("epggrab", "ota cron config invalid or unset"); } pthread_mutex_unlock(&epggrab_ota_mutex); @@ -615,6 +618,7 @@ epggrab_ota_service_add ( epggrab_ota_map_t *map, epggrab_ota_mux_t *ota, ota->om_save = 1; epggrab_ota_service_trace(ota, svcl, "add new"); } + svcl->last_tune_count = map->om_tune_count; } void @@ -724,8 +728,8 @@ epggrab_ota_init ( void ) epggrab_ota_initial = 1; epggrab_ota_timeout = 600; - epggrab_ota_cron = strdup("# Default config (02:04 and 14:04 everyday)\n4 2 * * *\n4 14 * * *");; - epggrab_ota_cron_multi = NULL; + epggrab_ota_cron = strdup("# Default config (02:04 and 14:04 everyday)\n4 2 * * *\n4 14 * * *"); + epggrab_ota_cron_multi = cron_multi_set(epggrab_ota_cron); epggrab_ota_pending_flag = 0; RB_INIT(&epggrab_ota_all); diff --git a/src/epggrab/private.h b/src/epggrab/private.h index ac16afc7..36adf650 100644 --- a/src/epggrab/private.h +++ b/src/epggrab/private.h @@ -54,6 +54,8 @@ epggrab_channel_t *epggrab_channel_find ( epggrab_channel_tree_t *chs, const char *id, int create, int *save, epggrab_module_t *owner ); +void epggrab_channel_done(void); + /* ************************************************************************** * Internal module routines * *************************************************************************/ diff --git a/src/esfilter.c b/src/esfilter.c index d09e1cae..2a5f1f55 100644 --- a/src/esfilter.c +++ b/src/esfilter.c @@ -587,6 +587,7 @@ esfilter_class_action_enum(void *o) const idclass_t esfilter_class = { .ic_class = "esfilter", .ic_caption = "Elementary Stream Filter", + .ic_event = "esfilter", .ic_save = esfilter_class_save, .ic_get_title = esfilter_class_get_title, .ic_delete = esfilter_class_delete, @@ -1034,7 +1035,7 @@ esfilter_init(void) for (i = 0; i <= ESF_CLASS_LAST; i++) TAILQ_INIT(&esfilters[i]); - if (!(c = hts_settings_load_r(1, "esfilter"))) + if (!(c = hts_settings_load("esfilter"))) return; HTSMSG_FOREACH(f, c) { if (!(e = htsmsg_field_get_map(f))) diff --git a/src/htsmsg.c b/src/htsmsg.c index 6ef0259a..1f9744f9 100644 --- a/src/htsmsg.c +++ b/src/htsmsg.c @@ -261,6 +261,25 @@ htsmsg_add_str(htsmsg_t *msg, const char *name, const char *str) f->hmf_str = strdup(str); } +/* + * + */ +int +htsmsg_set_str(htsmsg_t *msg, const char *name, const char *str) +{ + htsmsg_field_t *f = htsmsg_field_find(msg, name); + if (!f) + f = htsmsg_field_add(msg, name, HMF_STR, HMF_ALLOCED | HMF_NAME_ALLOCED); + else { + if (f->hmf_type != HMF_STR) + return 1; + if(f->hmf_flags & HMF_ALLOCED) + free((void *)f->hmf_str); + } + f->hmf_str = strdup(str); + return 0; +} + /* * */ diff --git a/src/htsmsg.h b/src/htsmsg.h index fb0e0d60..479a39a3 100644 --- a/src/htsmsg.h +++ b/src/htsmsg.h @@ -135,6 +135,11 @@ void htsmsg_add_s64(htsmsg_t *msg, const char *name, int64_t s64); */ void htsmsg_add_str(htsmsg_t *msg, const char *name, const char *str); +/** + * Add/update a string field + */ +int htsmsg_set_str(htsmsg_t *msg, const char *name, const char *str); + /** * Add an field where source is a list or map message. */ diff --git a/src/htsp_server.c b/src/htsp_server.c index 93a445f2..05f958d8 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -550,13 +550,16 @@ htsp_build_channel(channel_t *ch, const char *method, htsp_connection_t *htsp) channel_tag_t *ct; service_t *t; epg_broadcast_t *now, *next = NULL; + int64_t chnum = channel_get_number(ch); htsmsg_t *out = htsmsg_create_map(); htsmsg_t *tags = htsmsg_create_list(); htsmsg_t *services = htsmsg_create_list(); htsmsg_add_u32(out, "channelId", channel_get_id(ch)); - htsmsg_add_u32(out, "channelNumber", channel_get_number(ch)); + htsmsg_add_u32(out, "channelNumber", channel_get_major(chnum)); + if (channel_get_minor(chnum)) + htsmsg_add_u32(out, "channelNumberMinor", channel_get_minor(chnum)); htsmsg_add_str(out, "channelName", channel_get_name(ch)); if(ch->ch_icon != NULL) { @@ -652,9 +655,8 @@ htsp_build_dvrentry(dvr_entry_t *de, const char *method) htsmsg_t *out = htsmsg_create_map(); const char *s = NULL, *error = NULL; 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)); @@ -666,11 +668,9 @@ htsp_build_dvrentry(dvr_entry_t *de, const char *method) if( de->de_desc && (s = lang_str_get(de->de_desc, NULL))) htsmsg_add_str(out, "description", s); - if( de->de_filename && de->de_config_name ) { - if ((cfg = dvr_config_find_by_name_default(de->de_config_name))) { - if ((p = tvh_strbegins(de->de_filename, cfg->dvr_storage))) - htsmsg_add_str(out, "path", p); - } + if( de->de_filename && de->de_config ) { + if ((p = tvh_strbegins(de->de_filename, de->de_config->dvr_storage))) + htsmsg_add_str(out, "path", p); } switch(de->de_sched_state) { @@ -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 eba23517..15297e69 100644 --- a/src/http.c +++ b/src/http.c @@ -324,9 +324,13 @@ http_error(http_connection_t *hc, int error) "\r\n" "%d %s\r\n" "\r\n" - "

%d %s

\r\n" - "\r\n", - error, errtxt, error, errtxt); + "

%d %s

\r\n", + error, errtxt, error, errtxt); + + if (error == HTTP_STATUS_UNAUTHORIZED) + htsbuf_qprintf(&hc->hc_reply, "

Default Login

"); + + htsbuf_qprintf(&hc->hc_reply, "\r\n"); http_send_reply(hc, error, "text/html", NULL, NULL, 0); } @@ -403,11 +407,14 @@ static int http_access_verify_ticket(http_connection_t *hc) { const char *ticket_id = http_arg_get(&hc->hc_req_args, "ticket"); + if (hc->hc_ticket) + return 0; if(!access_ticket_verify(ticket_id, hc->hc_url)) { char addrstr[50]; tcp_get_ip_str((struct sockaddr*)hc->hc_peer, addrstr, 50); tvhlog(LOG_INFO, "HTTP", "%s: using ticket %s for %s", addrstr, ticket_id, hc->hc_url); + hc->hc_ticket = 1; return 0; } return -1; @@ -422,8 +429,14 @@ http_access_verify(http_connection_t *hc, int mask) if (!http_access_verify_ticket(hc)) return 0; - return access_verify(hc->hc_username, hc->hc_password, - (struct sockaddr *)hc->hc_peer, mask); + if (hc->hc_access == NULL) { + hc->hc_access = access_get(hc->hc_username, hc->hc_password, + (struct sockaddr *)hc->hc_peer); + if (hc->hc_access == NULL) + return -1; + } + + return access_verify2(hc->hc_access, mask); } /** @@ -431,21 +444,24 @@ http_access_verify(http_connection_t *hc, int mask) */ int http_access_verify_channel(http_connection_t *hc, int mask, - struct channel *ch) + struct channel *ch, int ticket) { - access_t *a; int res = -1; assert(ch); - if (!http_access_verify_ticket(hc)) + if (ticket && !http_access_verify_ticket(hc)) return 0; - a = access_get(hc->hc_username, hc->hc_password, - (struct sockaddr *)hc->hc_peer); - if (channel_access(ch, a, hc->hc_username)) + if (hc->hc_access == NULL) { + hc->hc_access = access_get(hc->hc_username, hc->hc_password, + (struct sockaddr *)hc->hc_peer); + if (hc->hc_access == NULL) + return -1; + } + + if (channel_access(ch, hc->hc_access, hc->hc_username)) res = 0; - access_destroy(a); return res; } @@ -464,6 +480,9 @@ http_exec(http_connection_t *hc, http_path_t *hp, char *remain) err = HTTP_STATUS_UNAUTHORIZED; else err = hp->hp_callback(hc, remain, hp->hp_opaque); + access_destroy(hc->hc_access); + hc->hc_access = NULL; + hc->hc_ticket = 0; if(err == -1) return 1; diff --git a/src/http.h b/src/http.h index cf35f76d..7a4baf61 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,8 @@ typedef struct http_connection { char *hc_username; char *hc_password; + access_t *hc_access; + int hc_ticket; struct config_head *hc_user_config; @@ -206,7 +209,8 @@ void http_server_register(void); void http_server_done(void); int http_access_verify(http_connection_t *hc, int mask); -int http_access_verify_channel(http_connection_t *hc, int mask, struct channel *ch); +int http_access_verify_channel(http_connection_t *hc, int mask, + struct channel *ch, int ticket); void http_deescape(char *s); diff --git a/src/idnode.c b/src/idnode.c index 9436e276..6764d589 100644 --- a/src/idnode.c +++ b/src/idnode.c @@ -155,7 +155,7 @@ idnode_insert(idnode_t *in, const char *uuid, const idclass_t *class, int flags) idclass_register(class); // Note: we never actually unregister /* Fire event */ - idnode_notify(in, NULL, 0, 1); + idnode_notify_simple(in); return 0; } @@ -169,7 +169,7 @@ idnode_unlink(idnode_t *in) lock_assert(&global_lock); RB_REMOVE(&idnodes, in, in_link); tvhtrace("idnode", "unlink node %s", idnode_uuid_as_str(in)); - idnode_notify(in, NULL, 0, 1); + idnode_notify_simple(in); } /** @@ -227,7 +227,7 @@ idnode_get_short_uuid (const idnode_t *in) const char * idnode_uuid_as_str(const idnode_t *in) { - static tvh_uuid_t ret[16]; + static tvh_uuid_t __thread ret[16]; static uint8_t p = 0; bin2hex(ret[p].hex, sizeof(ret[p].hex), in->in_uuid, sizeof(in->in_uuid)); const char *s = ret[p].hex; @@ -366,10 +366,94 @@ idnode_get_u32 *u32 = *(int*)ptr; return 0; case PT_U16: - *u32 = *(uint32_t*)ptr; + *u32 = *(uint16_t*)ptr; return 0; case PT_U32: - *u32 = *(uint16_t*)ptr; + *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: + *s64 = *(uint32_t*)ptr; + return 0; + case PT_S64: + *s64 = *(int64_t*)ptr; + return 0; + case PT_DBL: + *s64 = *(double*)ptr; + return 0; + case PT_TIME: + *s64 = *(time_t*)ptr; + return 0; + default: + break; + } + } + return 1; +} + +/* + * Get field as double + */ +int +idnode_get_dbl + ( idnode_t *self, const char *key, double *dbl ) +{ + 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: + *dbl = *(int*)ptr; + return 0; + case PT_U16: + *dbl = *(uint16_t*)ptr; + return 0; + case PT_U32: + *dbl = *(uint32_t*)ptr; + return 0; + case PT_S64: + *dbl = *(int64_t*)ptr; + return 0; + case PT_DBL: + *dbl = *(double *)ptr; + return 0; + case PT_TIME: + *dbl = *(time_t*)ptr; return 0; default: break; @@ -388,8 +472,11 @@ idnode_get_bool const property_t *p = idnode_find_prop(self, key); if (p->islist) return 1; if (p) { - void *ptr = self; - ptr += p->off; + const void *ptr; + if (p->get) + ptr = p->get(self); + else + ptr = ((void*)self) + p->off; switch (p->type) { case PT_BOOL: *b = *(int*)ptr; @@ -401,6 +488,32 @@ 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) { + const void *ptr; + if (p->get) + ptr = p->get(self); + else + ptr = ((void*)self) + p->off; + switch (p->type) { + case PT_TIME: + *tm = *(time_t*)ptr; + return 0; + default: + break; + } + } + return 1; +} + /* ************************************************************************** * Lookup * *************************************************************************/ @@ -414,6 +527,8 @@ idnode_find(const char *uuid, const idclass_t *idc) idnode_t skel, *r; tvhtrace("idnode", "find node %s class %s", uuid, idc ? idc->ic_class : NULL); + if(uuid == NULL || strlen(uuid) != UUID_HEX_SIZE - 1) + return NULL; if(hex2bin(skel.in_uuid, sizeof(skel.in_uuid), uuid)) return NULL; r = RB_FIND(&idnodes, &skel, in_link, in_cmp); @@ -464,6 +579,8 @@ idnode_cmp_title return strcmp(sa ?: "", sb ?: ""); } +#define safecmp(a, b) ((a) > (b) ? 1 : ((a) < (b) ? -1 : 0)) + static int idnode_cmp_sort ( const void *a, const void *b, void *s ) @@ -493,36 +610,105 @@ 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; case PT_INT: case PT_U16: - case PT_U32: case PT_BOOL: + case PT_PERM: + { + int32_t i32a = 0, i32b = 0; + idnode_get_u32(ina, sort->key, (uint32_t *)&i32a); + idnode_get_u32(inb, sort->key, (uint32_t *)&i32b); + if (sort->dir == IS_ASC) + return safecmp(i32a, i32b); + else + return safecmp(i32b, i32a); + } + break; + case PT_U32: { uint32_t u32a = 0, u32b = 0; idnode_get_u32(ina, sort->key, &u32a); idnode_get_u32(inb, sort->key, &u32b); if (sort->dir == IS_ASC) - return u32a - u32b; + return safecmp(u32a, u32b); else - return u32b - u32a; + return safecmp(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 safecmp(s64a, s64b); + else + return safecmp(s64b, s64a); } break; case PT_DBL: - // TODO + { + double dbla = 0, dblb = 0; + idnode_get_dbl(ina, sort->key, &dbla); + idnode_get_dbl(inb, sort->key, &dblb); + if (sort->dir == IS_ASC) + return safecmp(dbla, dblb); + else + return safecmp(dblb, dbla); + } + break; + 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 safecmp(ta, tb); + else + return safecmp(tb, ta); + } + break; + case PT_LANGSTR: + // TODO? case PT_NONE: break; } return 0; } +static void +idnode_filter_init + ( idnode_t *in, idnode_filter_t *filter ) +{ + idnode_filter_ele_t *f; + const property_t *p; + + LIST_FOREACH(f, filter, link) { + if (f->type == IF_NUM) { + p = idnode_find_prop(in, f->key); + if (p) { + if (p->type == PT_U32 || p->type == PT_S64 || + p->type == PT_TIME) { + int64_t v = f->u.n.n; + if (p->intsplit != f->u.n.intsplit) { + v = (v / (f->u.n.intsplit <= 0 ? 1 : 0)) * p->intsplit; + f->u.n.n = v; + } + } + } + } + f->checked = 1; + } +} + int idnode_filter ( idnode_t *in, idnode_filter_t *filter ) @@ -530,6 +716,8 @@ idnode_filter idnode_filter_ele_t *f; LIST_FOREACH(f, filter, link) { + if (!f->checked) + idnode_filter_init(in, filter); if (f->type == IF_STR) { const char *str; str = idnode_get_display(in, idnode_find_prop(in, f->key)); @@ -558,12 +746,32 @@ idnode_filter break; } } else if (f->type == IF_NUM || f->type == IF_BOOL) { - uint32_t u32; int64_t a, b; - if (idnode_get_u32(in, f->key, &u32)) + if (idnode_get_s64(in, f->key, &a)) return 1; - a = u32; - b = (f->type == IF_NUM) ? f->u.n : f->u.b; + b = (f->type == IF_NUM) ? f->u.n.n : f->u.b; + switch (f->comp) { + case IC_IN: + case IC_RE: + break; // Note: invalid + case IC_EQ: + if (a != b) + return 1; + break; + case IC_LT: + if (a > b) + return 1; + break; + case IC_GT: + if (a < b) + return 1; + break; + } + } else if (f->type == IF_DBL) { + double a, b; + if (idnode_get_dbl(in, f->key, &a)) + return 1; + b = f->u.dbl; switch (f->comp) { case IC_IN: case IC_RE: @@ -607,13 +815,26 @@ idnode_filter_add_str void idnode_filter_add_num - ( idnode_filter_t *filt, const char *key, int64_t val, int comp ) + ( idnode_filter_t *filt, const char *key, int64_t val, int comp, int64_t intsplit ) { idnode_filter_ele_t *ele = calloc(1, sizeof(idnode_filter_ele_t)); ele->key = strdup(key); ele->type = IF_NUM; ele->comp = comp; - ele->u.n = val; + ele->u.n.n = val; + ele->u.n.intsplit = intsplit; + LIST_INSERT_HEAD(filt, ele, link); +} + +void +idnode_filter_add_dbl + ( idnode_filter_t *filt, const char *key, double dbl, int comp ) +{ + idnode_filter_ele_t *ele = calloc(1, sizeof(idnode_filter_ele_t)); + ele->key = strdup(key); + ele->type = IF_DBL; + ele->comp = comp; + ele->u.dbl = dbl; LIST_INSERT_HEAD(filt, ele, link); } @@ -742,7 +963,7 @@ idnode_write0 ( idnode_t *self, htsmsg_t *c, int optmask, int dosave ) if (save && dosave) idnode_savefn(self); if (dosave) - idnode_notify(self, NULL, 0, 0); + idnode_notify_simple(self); // Note: always output event if "dosave", reason is that UI updates on // these, but there are some subtle cases where it will expect // an update and not get one. This include fields being set for @@ -751,19 +972,23 @@ idnode_write0 ( idnode_t *self, htsmsg_t *c, int optmask, int dosave ) return save; } +void +idnode_changed( idnode_t *self ) +{ + idnode_notify_simple(self); + idnode_savefn(self); +} + /* ************************************************************************** * 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 +996,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 +1013,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; } @@ -821,17 +1046,59 @@ idclass_get_class (const idclass_t *idc) return NULL; } +static const char * +idclass_get_event (const idclass_t *idc) +{ + while (idc) { + if (idc->ic_event) + return idc->ic_event; + idc = idc->ic_super; + } + return NULL; +} + 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 +1137,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(); @@ -880,11 +1147,15 @@ idclass_serialize0(const idclass_t *idc, int optmask) htsmsg_add_str(m, "caption", s); if ((s = idclass_get_class(idc))) htsmsg_add_str(m, "class", s); + if ((s = idclass_get_event(idc))) + htsmsg_add_str(m, "event", 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 +1165,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; @@ -908,16 +1179,32 @@ idnode_serialize0(idnode_t *self, int optmask) htsmsg_add_str(m, "caption", s); if ((s = idclass_get_class(idc))) htsmsg_add_str(m, "class", s); + if ((s = idclass_get_event(idc))) + htsmsg_add_str(m, "event", s); - htsmsg_add_msg(m, "params", idnode_params(idc, self, optmask)); + htsmsg_add_msg(m, "params", idnode_params(idc, self, list, optmask)); return m; } /* ************************************************************************** - * Notifcation + * Notification * *************************************************************************/ +/** + * Delayed notification + */ +static void +idnode_notify_delayed ( idnode_t *in, const char *uuid, const char *event ) +{ + pthread_mutex_lock(&idnode_mutex); + if (!idnode_queue) + idnode_queue = htsmsg_create_map(); + htsmsg_set_str(idnode_queue, uuid, event); + pthread_cond_signal(&idnode_cond); + pthread_mutex_unlock(&idnode_mutex); +} + /** * Update internal event pipes */ @@ -927,11 +1214,8 @@ idnode_notify_event ( idnode_t *in ) const idclass_t *ic = in->in_class; const char *uuid = idnode_uuid_as_str(in); while (ic) { - if (ic->ic_event) { - htsmsg_t *m = htsmsg_create_map(); - htsmsg_add_str(m, "uuid", uuid); - notify_by_msg(ic->ic_event, m); - } + if (ic->ic_event) + idnode_notify_delayed(in, uuid, ic->ic_event); ic = ic->ic_super; } } @@ -941,38 +1225,37 @@ idnode_notify_event ( idnode_t *in ) */ void idnode_notify - (idnode_t *in, const char *chn, int force, int event) + (idnode_t *in, int event) { const char *uuid = idnode_uuid_as_str(in); if (!tvheadend_running) return; - /* Forced */ - if (chn || force) { - htsmsg_t *m = htsmsg_create_map(); - htsmsg_add_str(m, "uuid", uuid); - notify_by_msg(chn ?: "idnodeUpdated", m); + /* Immediate */ + if (!event) { + + const idclass_t *ic = in->in_class; + + while (ic) { + if (ic->ic_event) { + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_str(m, "uuid", uuid); + notify_by_msg(ic->ic_event, m); + } + ic = ic->ic_super; + } /* Rate-limited */ } else { - pthread_mutex_lock(&idnode_mutex); - if (!idnode_queue) - idnode_queue = htsmsg_create_map(); - htsmsg_set_u32(idnode_queue, uuid, 1); - pthread_cond_signal(&idnode_cond); - pthread_mutex_unlock(&idnode_mutex); - } - - /* Send event */ - if (event) idnode_notify_event(in); + } } void idnode_notify_simple (void *in) { - idnode_notify(in, NULL, 0, 0); + idnode_notify(in, 1); } void @@ -981,7 +1264,7 @@ idnode_notify_title_changed (void *in) htsmsg_t *m = htsmsg_create_map(); htsmsg_add_str(m, "uuid", idnode_uuid_as_str(in)); htsmsg_add_str(m, "text", idnode_get_title(in)); - notify_by_msg("idnodeUpdated", m); + notify_by_msg("title", m); idnode_notify_event(in); } @@ -994,6 +1277,7 @@ idnode_thread ( void *p ) idnode_t *node; htsmsg_t *m, *q = NULL; htsmsg_field_t *f; + const char *event; pthread_mutex_lock(&idnode_mutex); @@ -1012,13 +1296,13 @@ idnode_thread ( void *p ) pthread_mutex_lock(&global_lock); HTSMSG_FOREACH(f, q) { - node = idnode_find(f->hmf_name, NULL); - m = htsmsg_create_map(); + node = idnode_find(f->hmf_name, NULL); + event = htsmsg_field_get_str(f); + m = htsmsg_create_map(); htsmsg_add_str(m, "uuid", f->hmf_name); - if (node) - notify_by_msg("idnodeUpdated", m); - else - notify_by_msg("idnodeDeleted", m); + if (!node) + htsmsg_add_u32(m, "removed", 1); + notify_by_msg(event, m); } /* Finished */ diff --git a/src/idnode.h b/src/idnode.h index 43ce3328..f3d03387 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); }; /* @@ -87,16 +100,22 @@ typedef struct idnode_filter_ele { LIST_ENTRY(idnode_filter_ele) link; ///< List link + int checked; char *key; ///< Filter key enum { IF_STR, IF_NUM, + IF_DBL, IF_BOOL } type; ///< Filter type union { int b; char *s; - int64_t n; + struct { + int64_t n; + int64_t intsplit; + } n; + double dbl; regex_t re; } u; ///< Filter data enum { @@ -128,36 +147,50 @@ void idnode_delete (idnode_t *in); void idnode_moveup (idnode_t *in); void idnode_movedown (idnode_t *in); +void idnode_changed (idnode_t *in); + void *idnode_find (const char *uuid, const idclass_t *idc); idnode_set_t *idnode_find_all(const idclass_t *idc); -#define idnode_updated(in) idnode_notify(in, NULL, 0, 0) -void idnode_notify - (idnode_t *in, const char *chn, int force, int event); + +void idnode_notify (idnode_t *in, int event); void idnode_notify_simple (void *in); 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_dbl (idnode_t *self, const char *key, double *dbl); 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); void idnode_filter_add_num - (idnode_filter_t *f, const char *k, int64_t s64, int t); + (idnode_filter_t *f, const char *k, int64_t s64, int t, int64_t intsplit); +void idnode_filter_add_dbl + (idnode_filter_t *f, const char *k, double dbl, int t); void idnode_filter_add_bool (idnode_filter_t *f, const char *k, int b, int t); void idnode_filter_clear diff --git a/src/imagecache.c b/src/imagecache.c index b28a25cb..1cc5ed7c 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 c5cdc4e6..7b410877 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/input.c b/src/input.c index b6f1eceb..7952cf2f 100644 --- a/src/input.c +++ b/src/input.c @@ -43,9 +43,6 @@ tvh_hardware_create0 /* Load config */ if (conf) idnode_load(&th->th_id, conf); - - /* Update */ - notify_reload("hardware"); return o; } @@ -60,7 +57,6 @@ tvh_hardware_delete ( tvh_hardware_t *th ) // TODO LIST_REMOVE(th, th_link); idnode_unlink(&th->th_id); - notify_reload("hardware"); } /* diff --git a/src/input/mpegts.h b/src/input/mpegts.h index e0be22c0..285d3683 100644 --- a/src/input/mpegts.h +++ b/src/input/mpegts.h @@ -312,15 +312,15 @@ enum mpegts_mux_epg_flag MM_EPG_DISABLE, MM_EPG_ENABLE, MM_EPG_FORCE, - MM_EPG_FORCE_EIT, - MM_EPG_FORCE_UK_FREESAT, - MM_EPG_FORCE_UK_FREEVIEW, - MM_EPG_FORCE_VIASAT_BALTIC, - MM_EPG_FORCE_OPENTV_SKY_UK, - MM_EPG_FORCE_OPENTV_SKY_ITALIA, - MM_EPG_FORCE_OPENTV_SKY_AUSAT, + MM_EPG_ONLY_EIT, + MM_EPG_ONLY_UK_FREESAT, + MM_EPG_ONLY_UK_FREEVIEW, + MM_EPG_ONLY_VIASAT_BALTIC, + MM_EPG_ONLY_OPENTV_SKY_UK, + MM_EPG_ONLY_OPENTV_SKY_ITALIA, + MM_EPG_ONLY_OPENTV_SKY_AUSAT, }; -#define MM_EPG_LAST MM_EPG_FORCE_OPENTV_SKY_AUSAT +#define MM_EPG_LAST MM_EPG_ONLY_OPENTV_SKY_AUSAT /* Multiplex */ struct mpegts_mux @@ -411,6 +411,7 @@ struct mpegts_mux int mm_enabled; int mm_epg; char *mm_charset; + int mm_pmt_06_ac3; }; /* Service */ @@ -424,6 +425,7 @@ struct mpegts_service uint16_t s_dvb_service_id; uint16_t s_dvb_channel_num; + uint16_t s_dvb_channel_minor; char *s_dvb_svcname; char *s_dvb_provider; char *s_dvb_cridauth; @@ -437,7 +439,7 @@ struct mpegts_service */ int s_dvb_eit_enable; - uint16_t s_dvb_opentv_chnum; + uint64_t s_dvb_opentv_chnum; /* * Link to carrying multiplex and active adapter @@ -562,6 +564,9 @@ struct mpegts_input /* Active sources */ LIST_HEAD(,mpegts_mux_instance) mi_mux_active; LIST_HEAD(,service) mi_transports; + + mpegts_mux_t **mi_destroyed_muxes; + int mi_destroyed_muxes_count; /* Table processing */ pthread_t mi_table_tid; diff --git a/src/input/mpegts/dvb_psi.c b/src/input/mpegts/dvb_psi.c index 79204e17..67d35c92 100644 --- a/src/input/mpegts/dvb_psi.c +++ b/src/input/mpegts/dvb_psi.c @@ -34,7 +34,7 @@ SKEL_DECLARE(mpegts_table_state_skel, struct mpegts_table_state); static int -psi_parse_pmt(mpegts_service_t *t, const uint8_t *ptr, int len); +psi_parse_pmt(mpegts_mux_t *mux, mpegts_service_t *t, const uint8_t *ptr, int len); /* ************************************************************************** * Lookup tables @@ -695,7 +695,7 @@ dvb_pmt_callback tvhdebug("pmt", "sid %04X (%d)", sid, sid); pthread_mutex_lock(&s->s_stream_mutex); had_components = !!TAILQ_FIRST(&s->s_components); - r = psi_parse_pmt(s, ptr, len); + r = psi_parse_pmt(mt->mt_mux, s, ptr, len); pthread_mutex_unlock(&s->s_stream_mutex); if (r) service_restart((service_t*)s, had_components); @@ -989,8 +989,7 @@ dvb_sdt_callback /* Save details */ if (save) { - idnode_updated(&s->s_id); - s->s_config_save((service_t*)s); + idnode_changed(&s->s_id); service_refresh_channel((service_t*)s); } } @@ -1071,11 +1070,9 @@ atsc_vct_callback tvh_str_set(&s->s_dvb_svcname, chname); save = 1; } - if (s->s_dvb_channel_num != maj) { - // TODO: ATSC channel numbering is plain weird! - // could shift the major (*100 or something) and append - // minor, but that'll probably confuse people, as will this! + if (s->s_dvb_channel_num != maj || s->s_dvb_channel_minor != min) { s->s_dvb_channel_num = maj; + s->s_dvb_channel_minor = min; save = 1; } @@ -1280,7 +1277,7 @@ psi_desc_teletext(mpegts_service_t *t, const uint8_t *ptr, int size, */ static int psi_parse_pmt - (mpegts_service_t *t, const uint8_t *ptr, int len) + (mpegts_mux_t *mux, mpegts_service_t *t, const uint8_t *ptr, int len) { int ret = 0; uint16_t pcr_pid, pid; @@ -1377,7 +1374,7 @@ psi_parse_pmt case 0x06: /* 0x06 is Chinese Cable TV AC-3 audio track */ /* but mark it so only when no more descriptors exist */ - if (dllen > 1) + if (dllen > 1 || !mux || !mux->mm_pmt_06_ac3) break; /* fall through to SCT_AC3 */ case 0x81: diff --git a/src/input/mpegts/linuxdvb/linuxdvb_adapter.c b/src/input/mpegts/linuxdvb/linuxdvb_adapter.c index 1f5d8e70..5c610fbf 100644 --- a/src/input/mpegts/linuxdvb/linuxdvb_adapter.c +++ b/src/input/mpegts/linuxdvb/linuxdvb_adapter.c @@ -72,6 +72,7 @@ const idclass_t linuxdvb_adapter_class = { .ic_class = "linuxdvb_adapter", .ic_caption = "LinuxDVB Adapter", + .ic_event = "linuxdvb_adapter", .ic_save = linuxdvb_adapter_class_save, .ic_get_childs = linuxdvb_adapter_class_get_childs, .ic_get_title = linuxdvb_adapter_class_get_title, diff --git a/src/input/mpegts/linuxdvb/linuxdvb_en50494.c b/src/input/mpegts/linuxdvb/linuxdvb_en50494.c index 4b1219d7..3a888de6 100644 --- a/src/input/mpegts/linuxdvb/linuxdvb_en50494.c +++ b/src/input/mpegts/linuxdvb/linuxdvb_en50494.c @@ -18,9 +18,11 @@ * along with this program. If not, see . * * Open things: - * - TODO: collision dectection - * when a en50494-command wasn't executed succesful, retry. - * delay time is easly random, but in standard is special (complicated) way described (cap. 8). + * - TODO: collision detection + * * compare transport-stream-id from stream with id in config + * * check continuity of the pcr-counter + * * when one point is given -> retry + * * delay time is easily random, but in standard is special (complicated) way described (cap. 8). */ #include "tvheadend.h" @@ -39,7 +41,7 @@ #define LINUXDVB_EN50494_NOPIN 256 #define LINUXDVB_EN50494_FRAME 0xE0 -/* adresses 0x00, 0x10 and 0x11 are possible */ +/* addresses 0x00, 0x10 and 0x11 are possible */ #define LINUXDVB_EN50494_ADDRESS 0x10 #define LINUXDVB_EN50494_CMD_NORMAL 0x5A @@ -162,7 +164,7 @@ linuxdvb_en50494_tune linuxdvb_en50494_t *le = (linuxdvb_en50494_t*) ld; linuxdvb_lnb_t *lnb = sc->lse_lnb; - /* band & polarisation */ + /* band & polarization */ uint8_t pol = lnb->lnb_pol(lnb, lm); uint8_t band = lnb->lnb_band(lnb, lm); uint32_t freq = lnb->lnb_freq(lnb, lm); @@ -180,21 +182,29 @@ linuxdvb_en50494_tune /* 2 data fields (16bit) */ uint8_t data1, data2; data1 = (le->le_id & 7) << 5; /* 3bit user-band */ - data1 |= (le->le_position & 1) << 4; /* 1bit position (satelitte A(0)/B(1)) */ - data1 |= (pol & 1) << 3; /* 1bit polarisation v(0)/h(1) */ + data1 |= (le->le_position & 1) << 4; /* 1bit position (satellite A(0)/B(1)) */ + data1 |= (pol & 1) << 3; /* 1bit polarization v(0)/h(1) */ data1 |= (band & 1) << 2; /* 1bit band lower(0)/upper(1) */ data1 |= (t >> 8) & 3; /* 2bit transponder value bit 1-2 */ data2 = t & 0xFF; /* 8bit transponder value bit 3-10 */ - tvhdebug("en50494", - "lnb=%i id=%i freq=%i pin=%i v/h=%i l/u=%i f=%i, data=0x%02X%02X", - le->le_position, le->le_id, le->le_frequency, le->le_pin, pol, - band, freq, data1, data2); - pthread_mutex_lock(&linuxdvb_en50494_lock); + /* wait until no other thread is setting up switch. + * when an other thread was blocking, waiting 20ms. + */ + if (pthread_mutex_trylock(&linuxdvb_en50494_lock) != 0) { + if (pthread_mutex_lock(&linuxdvb_en50494_lock) != 0) { + tvherror("en50494","failed to lock for tuning"); + return -1; + } + usleep(20000); + } + + /* setup en50494 switch */ for (i = 0; i <= sc->lse_parent->ls_diseqc_repeats; i++) { - /* to avoid repeated collision, wait a random time (5-25ms) */ + /* to avoid repeated collision, wait a random time 68-118 + * 67,5 is the typical diseqc-time */ if (i != 0) { - int ms = rand()%20 + 5; + int ms = rand()%50 + 68; usleep(ms*1000); } @@ -207,6 +217,10 @@ linuxdvb_en50494_tune usleep(15000); /* standard: 4ms < x < 22ms */ /* send tune command (with/without pin) */ + tvhdebug("en50494", + "lnb=%i id=%i freq=%i pin=%i v/h=%i l/u=%i f=%i, data=0x%02X%02X", + le->le_position, le->le_id, le->le_frequency, le->le_pin, pol, + band, freq, data1, data2); if (le->le_pin != LINUXDVB_EN50494_NOPIN) { ret = linuxdvb_diseqc_send(fd, LINUXDVB_EN50494_FRAME, @@ -273,7 +287,7 @@ linuxdvb_en50494_create0 return NULL; if (port > 1) { - tvherror("en50494", "only 2 ports/positions are posible. given %i", port); + tvherror("en50494", "only 2 ports/positions are possible. given %i", port); port = 0; } diff --git a/src/input/mpegts/linuxdvb/linuxdvb_frontend.c b/src/input/mpegts/linuxdvb/linuxdvb_frontend.c index 7129c64b..be3cb32c 100644 --- a/src/input/mpegts/linuxdvb/linuxdvb_frontend.c +++ b/src/input/mpegts/linuxdvb/linuxdvb_frontend.c @@ -289,13 +289,14 @@ linuxdvb_frontend_stop_mux lfe->lfe_ready = 0; lfe->lfe_locked = 0; lfe->lfe_status = 0; - assert(lfe->lfe_in_setup == 0); /* Ensure it won't happen immediately */ gtimer_arm(&lfe->lfe_monitor_timer, linuxdvb_frontend_monitor, lfe, 2); if (lfe->lfe_satconf) linuxdvb_satconf_post_stop_mux(lfe->lfe_satconf); + + lfe->lfe_in_setup = 0; } static int diff --git a/src/input/mpegts/linuxdvb/linuxdvb_lnb.c b/src/input/mpegts/linuxdvb/linuxdvb_lnb.c index 27a4c78f..f7607f42 100644 --- a/src/input/mpegts/linuxdvb/linuxdvb_lnb.c +++ b/src/input/mpegts/linuxdvb/linuxdvb_lnb.c @@ -105,6 +105,10 @@ static int linuxdvb_lnb_standard_tune ( linuxdvb_diseqc_t *ld, dvb_mux_t *lm, linuxdvb_satconf_ele_t *ls, int fd ) { + /* en50494 does not use the voltage tune. this is happend in the switch */ + if (ls->lse_en50494) + return 0; + int pol = linuxdvb_lnb_standard_pol((linuxdvb_lnb_t*)ld, lm); return linuxdvb_diseqc_set_volt(fd, pol); } diff --git a/src/input/mpegts/linuxdvb/linuxdvb_satconf.c b/src/input/mpegts/linuxdvb/linuxdvb_satconf.c index 39fa40c9..635288ed 100644 --- a/src/input/mpegts/linuxdvb/linuxdvb_satconf.c +++ b/src/input/mpegts/linuxdvb/linuxdvb_satconf.c @@ -211,6 +211,7 @@ const idclass_t linuxdvb_satconf_class = { .ic_class = "linuxdvb_satconf", .ic_caption = "DVB-S Satconf", + .ic_event = "linuxdvb_satconf", .ic_get_title = linuxdvb_satconf_class_get_title, .ic_save = linuxdvb_satconf_class_save, .ic_properties = (const property_t[]) { @@ -632,10 +633,12 @@ linuxdvb_satconf_ele_tune ( linuxdvb_satconf_ele_t *lse ) // TODO: really need to understand whether or not we need to pre configure // and/or re-affirm the switch - /* Disable tone */ - if (ioctl(lfe->lfe_fe_fd, FE_SET_TONE, SEC_TONE_OFF)) { - tvherror("diseqc", "failed to disable tone"); - return -1; + /* Disable tone (en50494 don't use tone) */ + if (!lse->lse_en50494) { + if (ioctl(lfe->lfe_fe_fd, FE_SET_TONE, SEC_TONE_OFF)) { + tvherror("diseqc", "failed to disable tone"); + return -1; + } } /* Diseqc */ @@ -659,14 +662,16 @@ linuxdvb_satconf_ele_tune ( linuxdvb_satconf_ele_t *lse ) &lse->lse_parent->ls_orbital_pos, &lse->lse_parent->ls_orbital_dir); - /* Set the tone */ - b = lse->lse_lnb->lnb_band(lse->lse_lnb, lm); - tvhtrace("disqec", "set diseqc tone %s", b ? "on" : "off"); - if (ioctl(lfe->lfe_fe_fd, FE_SET_TONE, b ? SEC_TONE_ON : SEC_TONE_OFF)) { - tvherror("diseqc", "failed to set diseqc tone (e=%s)", strerror(errno)); - return -1; + /* Set the tone (en50494 don't use tone) */ + if (!lse->lse_en50494) { + b = lse->lse_lnb->lnb_band(lse->lse_lnb, lm); + tvhtrace("disqec", "set diseqc tone %s", b ? "on" : "off"); + if (ioctl(lfe->lfe_fe_fd, FE_SET_TONE, b ? SEC_TONE_ON : SEC_TONE_OFF)) { + tvherror("diseqc", "failed to set diseqc tone (e=%s)", strerror(errno)); + return -1; + } + usleep(20000); // Allow LNB to settle before tuning } - usleep(20000); // Allow LNB to settle before tuning /* Frontend */ /* use en50494 tuning frequency, if needed (not channel frequency) */ @@ -700,13 +705,16 @@ linuxdvb_satconf_start_mux // Note: basically this ensures the tuning params are acceptable // for the FE, so that if they're not we don't have to wait // for things like rotors and switches + // the en50494 have to skip this test if (!lse->lse_lnb) return SM_CODE_TUNING_FAILED; f = lse->lse_lnb->lnb_freq(lse->lse_lnb, lm); if (f == (uint32_t)-1) return SM_CODE_TUNING_FAILED; - r = linuxdvb_frontend_tune0(lfe, mmi, f); - if (r) return r; + if (!lse->lse_en50494) { + r = linuxdvb_frontend_tune0(lfe, mmi, f); + if (r) return r; + } /* Diseqc */ ls->ls_mmi = mmi; @@ -1020,6 +1028,7 @@ const idclass_t linuxdvb_satconf_ele_class = { .ic_class = "linuxdvb_satconf_ele", .ic_caption = "Satconf", + .ic_event = "linuxdvb_satconf_ele", .ic_get_title = linuxdvb_satconf_ele_class_get_title, .ic_get_childs = linuxdvb_satconf_ele_class_get_childs, .ic_save = linuxdvb_satconf_ele_class_save, @@ -1205,6 +1214,7 @@ const idclass_t linuxdvb_diseqc_class = { .ic_class = "linuxdvb_diseqc", .ic_caption = "DiseqC", + .ic_event = "linuxdvb_diseqc", .ic_get_title = linuxdvb_diseqc_class_get_title, .ic_save = linuxdvb_diseqc_class_save, }; diff --git a/src/input/mpegts/mpegts_input.c b/src/input/mpegts/mpegts_input.c index 96b2c176..c1a859b1 100644 --- a/src/input/mpegts/mpegts_input.c +++ b/src/input/mpegts/mpegts_input.c @@ -142,6 +142,7 @@ const idclass_t mpegts_input_class = { .ic_class = "mpegts_input", .ic_caption = "MPEGTS Input", + .ic_event = "mpegts_input", .ic_get_title = mpegts_input_class_get_title, .ic_properties = (const property_t[]){ { @@ -851,6 +852,7 @@ mpegts_input_table_thread ( void *aux ) { mpegts_table_feed_t *mtf; mpegts_input_t *mi = aux; + int i; pthread_mutex_lock(&mi->mi_output_lock); while (mi->mi_running) { @@ -866,7 +868,18 @@ mpegts_input_table_thread ( void *aux ) /* Process */ if (mtf->mtf_mux) { pthread_mutex_lock(&global_lock); - mpegts_input_table_dispatch(mtf->mtf_mux, mtf->mtf_tsb); + if (mi->mi_destroyed_muxes) { + for (i = 0; i < mi->mi_destroyed_muxes_count; i++) + if (mtf->mtf_mux == mi->mi_destroyed_muxes[i]) + goto clean; + mpegts_input_table_dispatch(mtf->mtf_mux, mtf->mtf_tsb); +clean: + free(mi->mi_destroyed_muxes); + mi->mi_destroyed_muxes = NULL; + mi->mi_destroyed_muxes_count = 0; + } else { + mpegts_input_table_dispatch(mtf->mtf_mux, mtf->mtf_tsb); + } pthread_mutex_unlock(&global_lock); } @@ -892,6 +905,8 @@ mpegts_input_flush_mux mpegts_table_feed_t *mtf; mpegts_packet_t *mp; + lock_assert(&global_lock); + // Note: to avoid long delays in here, rather than actually // remove things from the Q, we simply invalidate by clearing // the mux pointer and allow the threads to deal with the deletion @@ -910,6 +925,10 @@ mpegts_input_flush_mux if (mtf->mtf_mux == mm) mtf->mtf_mux = NULL; } + mi->mi_destroyed_muxes = realloc(mi->mi_destroyed_muxes, + (mi->mi_destroyed_muxes_count + 1) * + sizeof(mpegts_mux_t *)); + mi->mi_destroyed_muxes[mi->mi_destroyed_muxes_count++] = mm; pthread_mutex_unlock(&mi->mi_output_lock); } @@ -1136,6 +1155,7 @@ mpegts_input_delete ( mpegts_input_t *mi, int delconf ) pthread_mutex_destroy(&mi->mi_output_lock); pthread_cond_destroy(&mi->mi_table_cond); free(mi->mi_name); + free(mi->mi_destroyed_muxes); free(mi); } diff --git a/src/input/mpegts/mpegts_mux.c b/src/input/mpegts/mpegts_mux.c index 0e894435..2add0bfd 100644 --- a/src/input/mpegts/mpegts_mux.c +++ b/src/input/mpegts/mpegts_mux.c @@ -303,13 +303,13 @@ mpegts_mux_epg_list ( void *o ) { "Disable", MM_EPG_DISABLE }, { "Enable (auto)", MM_EPG_ENABLE }, { "Force (auto)", MM_EPG_FORCE }, - { "Force EIT", MM_EPG_FORCE_EIT }, - { "Force UK Freesat", MM_EPG_FORCE_UK_FREESAT }, - { "Force UK Freeview", MM_EPG_FORCE_UK_FREEVIEW }, - { "Force Viasat Baltic", MM_EPG_FORCE_VIASAT_BALTIC }, - { "Force OpenTV Sky UK", MM_EPG_FORCE_OPENTV_SKY_UK }, - { "Force OpenTV Sky Italia", MM_EPG_FORCE_OPENTV_SKY_ITALIA }, - { "Force OpenTV Sky Ausat", MM_EPG_FORCE_OPENTV_SKY_AUSAT }, + { "Only EIT", MM_EPG_ONLY_EIT }, + { "Only UK Freesat", MM_EPG_ONLY_UK_FREESAT }, + { "Only UK Freeview", MM_EPG_ONLY_UK_FREEVIEW }, + { "Only Viasat Baltic", MM_EPG_ONLY_VIASAT_BALTIC }, + { "Only OpenTV Sky UK", MM_EPG_ONLY_OPENTV_SKY_UK }, + { "Only OpenTV Sky Italia", MM_EPG_ONLY_OPENTV_SKY_ITALIA }, + { "Only OpenTV Sky Ausat", MM_EPG_ONLY_OPENTV_SKY_AUSAT }, }; return strtab2htsmsg(tab); } @@ -406,6 +406,13 @@ const idclass_t mpegts_mux_class = .opts = PO_RDONLY | PO_NOSAVE, .get = mpegts_mux_class_get_num_svc, }, + { + .type = PT_BOOL, + .id = "pmt_06_ac3", + .name = "PMT Descriptor 0x06 = AC-3", + .off = offsetof(mpegts_mux_t, mm_pmt_06_ac3), + .opts = PO_ADVANCED, + }, {} } }; diff --git a/src/input/mpegts/mpegts_network_scan.c b/src/input/mpegts/mpegts_network_scan.c index 664db417..94646ecd 100644 --- a/src/input/mpegts/mpegts_network_scan.c +++ b/src/input/mpegts/mpegts_network_scan.c @@ -27,8 +27,8 @@ static void mpegts_network_scan_notify ( mpegts_mux_t *mm ) { - idnode_updated(&mm->mm_id); - idnode_updated(&mm->mm_network->mn_id); + idnode_notify_simple(&mm->mm_id); + idnode_notify_simple(&mm->mm_network->mn_id); } static int diff --git a/src/input/mpegts/mpegts_service.c b/src/input/mpegts/mpegts_service.c index 57c882e9..e0959257 100644 --- a/src/input/mpegts/mpegts_service.c +++ b/src/input/mpegts/mpegts_service.c @@ -20,6 +20,7 @@ #include #include "service.h" +#include "channels.h" #include "input.h" #include "settings.h" #include "dvb_charset.h" @@ -101,6 +102,13 @@ const idclass_t mpegts_service_class = .opts = PO_RDONLY, .off = offsetof(mpegts_service_t, s_dvb_channel_num), }, + { + .type = PT_U16, + .id = "lcn_minor", + .name = "Local Channel Minor", + .opts = PO_RDONLY, + .off = offsetof(mpegts_service_t, s_dvb_channel_minor), + }, { .type = PT_U16, .id = "lcn2", @@ -364,12 +372,13 @@ mpegts_service_grace_period(service_t *t) /* * Channel number */ -static int +static int64_t mpegts_service_channel_number ( service_t *s ) { - int r = ((mpegts_service_t*)s)->s_dvb_channel_num; + int r = ((mpegts_service_t*)s)->s_dvb_channel_num * CHANNEL_SPLIT + + ((mpegts_service_t*)s)->s_dvb_channel_minor; if (r <= 0) - r = ((mpegts_service_t*)s)->s_dvb_opentv_chnum; + r = ((mpegts_service_t*)s)->s_dvb_opentv_chnum * CHANNEL_SPLIT; return r; } @@ -423,7 +432,9 @@ mpegts_service_create0 { int r; char buf[256]; - service_create0((service_t*)s, class, uuid, S_MPEG_TS, conf); + + if (service_create0((service_t*)s, class, uuid, S_MPEG_TS, conf) == NULL) + return NULL; /* Create */ sbuf_init(&s->s_tsbuf); @@ -457,8 +468,8 @@ mpegts_service_create0 tvhlog(LOG_DEBUG, "mpegts", "%s - add service %04X %s", buf, s->s_dvb_service_id, s->s_dvb_svcname); /* Notification */ - idnode_updated(&mm->mm_id); - idnode_updated(&mm->mm_network->mn_id); + idnode_notify_simple(&mm->mm_id); + idnode_notify_simple(&mm->mm_network->mn_id); return s; } diff --git a/src/input/mpegts/satip/satip_satconf.c b/src/input/mpegts/satip/satip_satconf.c index 0271a297..ea1ece1c 100644 --- a/src/input/mpegts/satip/satip_satconf.c +++ b/src/input/mpegts/satip/satip_satconf.c @@ -169,6 +169,7 @@ const idclass_t satip_satconf_class = { .ic_class = "satip_satconf", .ic_caption = "Satconf", + .ic_event = "satip_satconf", .ic_get_title = satip_satconf_class_get_title, .ic_save = satip_satconf_class_save, .ic_properties = (const property_t[]) { diff --git a/src/lang_str.c b/src/lang_str.c index 4fe4aea8..ed9ee089 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 ecd22413..ff901b8b 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/main.c b/src/main.c index 719ba4aa..9bc86856 100644 --- a/src/main.c +++ b/src/main.c @@ -484,7 +484,8 @@ main(int argc, char **argv) opt_dump = 0, opt_xspf = 0, opt_dbus = 0, - opt_dbus_session = 0; + opt_dbus_session = 0, + opt_nobackup = 0; const char *opt_config = NULL, *opt_user = NULL, *opt_group = NULL, @@ -507,6 +508,7 @@ main(int argc, char **argv) { 0, NULL, "Service Configuration", OPT_BOOL, NULL }, { 'c', "config", "Alternate config path", OPT_STR, &opt_config }, + { 'B', "nobackup", "Do not backup config tree at upgrade", OPT_BOOL, &opt_nobackup }, { 'f', "fork", "Fork and run as daemon", OPT_BOOL, &opt_fork }, { 'u', "user", "Run as user", OPT_STR, &opt_user }, { 'g', "group", "Run as group", OPT_STR, &opt_group }, @@ -787,7 +789,7 @@ main(int argc, char **argv) /* Initialise configuration */ uuid_init(); idnode_init(); - config_init(opt_config); + config_init(opt_config, opt_nobackup == 0); /** * Initialize subsystems @@ -821,6 +823,8 @@ main(int argc, char **argv) subscription_init(); + dvr_config_init(); + access_init(opt_firstrun, opt_noacl); #if ENABLE_TIMESHIFT diff --git a/src/muxer.h b/src/muxer.h index ec72ccda..2c6b9651 100644 --- a/src/muxer.h +++ b/src/muxer.h @@ -21,9 +21,6 @@ #include "htsmsg.h" -#define MC_REWRITE_PAT 0x0001 -#define MC_REWRITE_PMT 0x0002 - #define MC_IS_EOS_ERROR(e) ((e) == EPIPE || (e) == ECONNRESET) typedef enum { @@ -47,8 +44,9 @@ typedef enum { /* Muxer configuration used when creating a muxer. */ typedef struct muxer_config { - int m_flags; - muxer_cache_type_t m_cache; + int m_rewrite_pat; + int m_rewrite_pmt; + 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/muxer_pass.c b/src/muxer/muxer_pass.c index cf28e84d..18b767a1 100644 --- a/src/muxer/muxer_pass.c +++ b/src/muxer/muxer_pass.c @@ -316,7 +316,7 @@ pass_muxer_reconfigure(muxer_t* m, const struct streaming_start *ss) pm->pm_pmt_pid = ss->ss_pmt_pid; pm->pm_service_id = ss->ss_service_id; - if (pm->m_config.m_flags & MC_REWRITE_PMT) { + if (pm->m_config.m_rewrite_pmt) { pm->pm_pmt = realloc(pm->pm_pmt, 188); memset(pm->pm_pmt, 0xff, 188); pm->pm_pmt[0] = 0x47; @@ -433,16 +433,15 @@ pass_muxer_write_ts(muxer_t *m, pktbuf_t *pb) size_t len = pb->pb_size; /* Rewrite PAT/PMT in operation */ - if (pm->m_config.m_flags & (MC_REWRITE_PAT | MC_REWRITE_PMT)) { + if (pm->m_config.m_rewrite_pat || pm->m_config.m_rewrite_pmt) { tsb = pb->pb_data; len = 0; while (tsb < pb->pb_data + pb->pb_size) { int pid = (tsb[1] & 0x1f) << 8 | tsb[2]; /* Process */ - if ( ((pm->m_config.m_flags & MC_REWRITE_PAT) && (pid == 0)) || - ((pm->m_config.m_flags & MC_REWRITE_PMT) && - (pid == pm->pm_pmt_pid)) ) { + if ( (pm->m_config.m_rewrite_pat && (pid == 0)) || + (pm->m_config.m_rewrite_pmt && (pid == pm->pm_pmt_pid)) ) { /* Flush */ if (len) @@ -458,7 +457,7 @@ pass_muxer_write_ts(muxer_t *m, pktbuf_t *pb) e = pass_muxer_rewrite_pat(pm, tmp); if (e < 0) { tvherror("pass", "PAT rewrite failed, disabling"); - pm->m_config.m_flags &= ~MC_REWRITE_PAT; + pm->m_config.m_rewrite_pat = 0; } if (e) pass_muxer_write(m, tmp, 188); diff --git a/src/muxer/tvh/mkmux.c b/src/muxer/tvh/mkmux.c index 5ccd51da..3e2a11ea 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 944eb1e8..f73a4568 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,13 +87,18 @@ 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); if (!f) continue; /* Ignore */ - if(p->opts & optmask) continue; + u32 = p->get_opts ? p->get_opts(obj) : p->opts; + if(u32 & optmask) continue; + + /* Sanity check */ + assert(p->set || p->off); /* Write */ save = 0; @@ -122,11 +133,35 @@ prop_write_values break; } case PT_U32: { - if (htsmsg_field_get_u32(f, &u32)) - continue; + if (p->intsplit) { + char *s; + if (!(new = htsmsg_field_get_str(f))) + continue; + u32 = atol(new) * p->intsplit; + if ((s = strchr(new, '.')) != NULL) + u32 += (atol(s + 1) % p->intsplit); + } else { + if (htsmsg_field_get_u32(f, &u32)) + continue; + } PROP_UPDATE(u32, uint32_t); break; } + case PT_S64: { + if (p->intsplit) { + char *s; + if (!(new = htsmsg_field_get_str(f))) + continue; + s64 = (int64_t)atol(new) * p->intsplit; + if ((s = strchr(new, '.')) != NULL) + s64 += (atol(s + 1) % p->intsplit); + } else { + if (htsmsg_field_get_s64(f, &s64)) + continue; + } + PROP_UPDATE(s64, int64_t); + break; + } case PT_DBL: { if (htsmsg_field_get_dbl(f, &dbl)) continue; @@ -144,6 +179,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 +242,20 @@ 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; + uint32_t u32; + char buf[24]; /* Ignore */ - if (p->opts & optmask) return; + u32 = p->get_opts ? p->get_opts(obj) : p->opts; + if (u32 & 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,12 +275,33 @@ prop_read_value case PT_INT: htsmsg_add_s64(m, name, *(int *)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); break; + case PT_U32: + if (p->intsplit) { + uint32_t maj = *(int64_t *)val / p->intsplit; + uint32_t min = *(int64_t *)val % p->intsplit; + if (min) { + snprintf(buf, sizeof(buf), "%u.%u", (unsigned int)maj, (unsigned int)min); + htsmsg_add_str(m, name, buf); + } else + htsmsg_add_s64(m, name, maj); + } else + htsmsg_add_u32(m, name, *(uint32_t *)val); + break; + case PT_S64: + if (p->intsplit) { + int64_t maj = *(int64_t *)val / p->intsplit; + int64_t min = *(int64_t *)val % p->intsplit; + if (min) { + snprintf(buf, sizeof(buf), "%lu.%lu", (unsigned long)maj, (unsigned long)min); + htsmsg_add_str(m, name, buf); + } else + htsmsg_add_s64(m, name, maj); + } else + htsmsg_add_s64(m, name, *(int64_t *)val); + break; case PT_STR: if ((s = *(const char **)val)) htsmsg_add_str(m, name, s); @@ -220,6 +309,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,12 +330,141 @@ 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); + } + } + } +} + +/** + * + */ +static void +prop_serialize_value + (void *obj, const property_t *pl, htsmsg_t *msg, int optmask) +{ + htsmsg_field_t *f; + char buf[16]; + uint32_t opts; + + /* 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; + } + } + } + + 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"); + + /* Skip - special blocker */ + if (pl->type == PT_NONE) { + htsmsg_add_msg(msg, NULL, m); + return; + } + + /* 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_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; + } + + /* Options */ + opts = pl->get_opts ? pl->get_opts(obj) : pl->opts; + if (opts & PO_RDONLY) + htsmsg_add_bool(m, "rdonly", 1); + if (opts & PO_NOSAVE) + htsmsg_add_bool(m, "nosave", 1); + if (opts & PO_WRONCE) + htsmsg_add_bool(m, "wronce", 1); + if (opts & PO_ADVANCED) + htsmsg_add_bool(m, "advanced", 1); + if (opts & PO_HIDDEN) + htsmsg_add_bool(m, "hidden", 1); + if (opts & PO_PASSWORD) + htsmsg_add_bool(m, "password", 1); + if (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); + + /* Split integer value */ + if (pl->intsplit) + htsmsg_add_u32(m, "intsplit", pl->intsplit); + + /* Data */ + if (obj) + prop_read_value(obj, pl, m, "value", optmask); + + htsmsg_add_msg(msg, NULL, m); } /** @@ -244,97 +472,25 @@ prop_read_values */ void prop_serialize - (void *obj, const property_t *pl, htsmsg_t *msg, int optmask, htsmsg_t *inc) + (void *obj, const property_t *pl, htsmsg_t *msg, htsmsg_t *list, 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; - } + 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); } } - - /* Ignore */ - if (inc && !htsmsg_get_u32_or_default(inc, pl->id, 0)) - continue; - - 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"); - - /* 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; - } - - /* 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); - - htsmsg_add_msg(msg, NULL, m); } } diff --git a/src/prop.h b/src/prop.h index 67fa2b1a..42ee871d 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 @@ -57,9 +62,11 @@ typedef struct property { const char *id; ///< Property Key const char *name; ///< Textual description prop_type_t type; ///< Type - int islist; ///< Is a list + uint8_t islist; ///< Is a list + uint8_t group; ///< Visual group ID (like ExtJS FieldSet) size_t off; ///< Offset into object - int opts; ///< Options + uint32_t opts; ///< Options + uint32_t intsplit; ///< integer/remainder boundary /* String based processing */ const void *(*get) (void *ptr); @@ -72,12 +79,17 @@ 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; + /* Extended options */ + uint32_t (*get_opts) (void *ptr); + /* Notification callback */ void (*notify) (void *ptr); @@ -89,10 +101,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 a95df8a8..973c8199 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; } @@ -171,6 +168,7 @@ service_class_caid_get ( void *obj ) const idclass_t service_class = { .ic_class = "service", .ic_caption = "Service", + .ic_event = "service", .ic_save = service_class_save, .ic_get_title = service_class_get_title, .ic_properties = (const property_t[]){ @@ -805,7 +803,7 @@ service_destroy(service_t *t, int delconf) service_unref(t); } -static int +static int64_t service_channel_number ( service_t *s ) { return 0; @@ -1488,7 +1486,6 @@ service_instance_add(service_instance_list_t *sil, si->si_s = s; service_ref(s); si->si_instance = instance; - si->si_weight = weight; } else { si->si_mark = 0; if(si->si_prio == prio && si->si_weight == weight) @@ -1570,7 +1567,7 @@ service_get_full_channel_name ( service_t *s ) /* * Get number for service */ -int +int64_t service_get_channel_number ( service_t *s ) { if (s->s_channel_number) return s->s_channel_number(s); diff --git a/src/service.h b/src/service.h index d1419803..6707daa9 100644 --- a/src/service.h +++ b/src/service.h @@ -292,7 +292,7 @@ typedef struct service { /** * Channel info */ - int (*s_channel_number) (struct service *); + int64_t (*s_channel_number) (struct service *); const char *(*s_channel_name) (struct service *); const char *(*s_provider_name) (struct service *); @@ -554,6 +554,6 @@ void sort_elementary_streams(service_t *t); const char *service_get_channel_name (service_t *s); const char *service_get_full_channel_name (service_t *s); -int service_get_channel_number (service_t *s); +int64_t service_get_channel_number (service_t *s); #endif // SERVICE_H__ diff --git a/src/settings.c b/src/settings.c index 5fea51c5..cf31d893 100644 --- a/src/settings.c +++ b/src/settings.c @@ -184,21 +184,22 @@ hts_settings_save(htsmsg_t *record, const char *pathfmt, ...) static htsmsg_t * hts_settings_load_one(const char *filename) { - ssize_t n; + ssize_t n, size; char *mem; fb_file *fp; htsmsg_t *r = NULL; /* Open */ if (!(fp = fb_open(filename, 1, 0))) return NULL; + size = fb_size(fp); /* Load data */ - mem = malloc(fb_size(fp)+1); - n = fb_read(fp, mem, fb_size(fp)); + mem = malloc(size+1); + n = fb_read(fp, mem, size); if (n >= 0) mem[n] = 0; /* Decode */ - if(n == fb_size(fp)) + if(n == size) r = htsmsg_json_deserialize(mem); /* Close */ diff --git a/src/spawn.c b/src/spawn.c index b1bdacbf..b7d8bacc 100644 --- a/src/spawn.c +++ b/src/spawn.c @@ -84,50 +84,56 @@ find_exec ( const char *name, char *out, size_t len ) } /** - * The reaper is called once a second to finish of any pending spawns + * Reap one child */ -void -spawn_reaper(void) +int +spawn_reap(char *stxt, size_t stxtlen) { pid_t pid; - int status; - char txt[100]; + int status, res; spawn_t *s; - while(1) { - pid = waitpid(-1, &status, WNOHANG); - if(pid < 1) + pid = waitpid(-1, &status, WNOHANG); + if(pid < 1) + return -EAGAIN; + + pthread_mutex_lock(&spawn_mutex); + LIST_FOREACH(s, &spawns, link) + if(s->pid == pid) break; - pthread_mutex_lock(&spawn_mutex); - LIST_FOREACH(s, &spawns, link) - if(s->pid == pid) - break; - - if (WIFEXITED(status)) { - snprintf(txt, sizeof(txt), - "exited, status=%d", WEXITSTATUS(status)); - } else if (WIFSIGNALED(status)) { - snprintf(txt, sizeof(txt), - "killed by signal %d", WTERMSIG(status)); - } else if (WIFSTOPPED(status)) { - snprintf(txt, sizeof(txt), - "stopped by signal %d", WSTOPSIG(status)); - } else if (WIFCONTINUED(status)) { - snprintf(txt, sizeof(txt), - "continued"); - } else { - snprintf(txt, sizeof(txt), - "unknown status"); - } - - if(s != NULL) { - LIST_REMOVE(s, link); - free((void *)s->name); - free(s); - } - pthread_mutex_unlock(&spawn_mutex); + res = -EIO; + if (WIFEXITED(status)) { + res = WEXITSTATUS(status); + if (stxt) + snprintf(stxt, stxtlen, "exited, status=%d", WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + snprintf(stxt, stxtlen, "killed by signal %d, " + "stopped by signal %d", + WTERMSIG(status), + WSTOPSIG(status)); + } else if (WIFCONTINUED(status)) { + snprintf(stxt, stxtlen, "continued"); + } else { + snprintf(stxt, stxtlen, "unknown status"); } + + if(s != NULL) { + LIST_REMOVE(s, link); + free((void *)s->name); + free(s); + } + pthread_mutex_unlock(&spawn_mutex); + return res; +} + +/** + * The reaper is called once a second to finish of any pending spawns + */ +void +spawn_reaper(void) +{ + while (spawn_reap(NULL, 0) != -EAGAIN) ; } diff --git a/src/spawn.h b/src/spawn.h index 5f89426b..64e734bf 100644 --- a/src/spawn.h +++ b/src/spawn.h @@ -25,6 +25,8 @@ int spawn_and_store_stdout(const char *prog, char *argv[], char **outp); int spawnv(const char *prog, char *argv[]); +int spawn_reap(char *stxt, size_t stxtlen); + void spawn_reaper(void); #endif /* SPAWN_H */ diff --git a/src/subscriptions.c b/src/subscriptions.c index 193dd213..56e5f5b5 100644 --- a/src/subscriptions.c +++ b/src/subscriptions.c @@ -90,6 +90,7 @@ subscription_link_service(th_subscription_t *s, service_t *t) sm = streaming_msg_create_code(SMT_GRACE, s->ths_postpone + t->s_grace_delay); streaming_pad_deliver(&t->s_streaming_pad, sm); + streaming_msg_free(sm); if(s->ths_start_message != NULL && t->s_streaming_status & TSS_PACKETS) { diff --git a/src/tvheadend.h b/src/tvheadend.h index f32731dc..cf928a4c 100644 --- a/src/tvheadend.h +++ b/src/tvheadend.h @@ -177,6 +177,7 @@ void gtimer_disarm(gtimer_t *gti); /* * List / Queue header declarations */ +LIST_HEAD(access_entry_list, access_entry); LIST_HEAD(th_subscription_list, th_subscription); LIST_HEAD(dvr_config_list, dvr_config); LIST_HEAD(dvr_entry_list, dvr_entry); diff --git a/src/webui/comet.c b/src/webui/comet.c index 574a9fa2..f8a2231a 100644 --- a/src/webui/comet.c +++ b/src/webui/comet.c @@ -139,11 +139,13 @@ static void comet_access_update(http_connection_t *hc, comet_mailbox_t *cmb) { htsmsg_t *m = htsmsg_create_map(); + const char *username = hc->hc_access ? (hc->hc_access->aa_username ?: "") : ""; htsmsg_add_str(m, "notificationClass", "accessUpdate"); - htsmsg_add_u32(m, "dvr", !http_access_verify(hc, ACCESS_RECORDER)); - htsmsg_add_u32(m, "admin", !http_access_verify(hc, ACCESS_ADMIN)); + htsmsg_add_str(m, "username", username); + htsmsg_add_u32(m, "dvr", !http_access_verify(hc, ACCESS_RECORDER)); + htsmsg_add_u32(m, "admin", !http_access_verify(hc, ACCESS_ADMIN)); if(cmb->cmb_messages == NULL) cmb->cmb_messages = htsmsg_create_list(); diff --git a/src/webui/extjs.c b/src/webui/extjs.c index b6f92ce0..edadc6e6 100755 --- a/src/webui/extjs.c +++ b/src/webui/extjs.c @@ -153,10 +153,6 @@ extjs_root(http_connection_t *hc, const char *remain, void *opaque) extjs_load(hq, "static/app/esfilter.js"); #if ENABLE_MPEGTS extjs_load(hq, "static/app/mpegts.js"); -#endif - extjs_load(hq, "static/app/iptv.js"); -#if ENABLE_V4L - extjs_load(hq, "static/app/v4l.js"); #endif #if ENABLE_TIMESHIFT extjs_load(hq, "static/app/timeshift.js"); @@ -256,7 +252,7 @@ page_about(http_connection_t *hc, const char *remain, void *opaque) "
" "HTS Tvheadend %s" "

" - "© 2006 - 2013 Andreas \303\226man, et al.

" + "© 2006 - 2014 Andreas \303\226man, et al.

" "
" "" "https://tvheadend.org

" @@ -362,24 +358,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 +480,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 +585,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 +645,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 +787,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 +1062,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 9bd698d9..b4db578f 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/statedump.c b/src/webui/statedump.c index 79dfc0fd..db74cd55 100644 --- a/src/webui/statedump.c +++ b/src/webui/statedump.c @@ -54,18 +54,27 @@ dumpchannels(htsbuf_queue_t *hq) { channel_t *ch; outputtitle(hq, 0, "Channels"); + int64_t chnum; + char chbuf[32]; CHANNEL_FOREACH(ch) { htsbuf_qprintf(hq, "%s (%d)\n", channel_get_name(ch), channel_get_id(ch)); + chnum = channel_get_number(ch); + if (channel_get_minor(chnum)) + snprintf(chbuf, sizeof(chbuf), "%u.%u", + channel_get_major(chnum), + channel_get_minor(chnum)); + else + snprintf(chbuf, sizeof(chbuf), "%u", channel_get_major(chnum)); htsbuf_qprintf(hq, " refcount = %d\n" " zombie = %d\n" - " number = %d\n" + " number = %s\n" " icon = %s\n\n", ch->ch_refcount, ch->ch_zombie, - channel_get_number(ch), + chbuf, ch->ch_icon ?: ""); } } diff --git a/src/webui/static/app/acleditor.js b/src/webui/static/app/acleditor.js index eb71cf1e..2f1426ad 100644 --- a/src/webui/static/app/acleditor.js +++ b/src/webui/static/app/acleditor.js @@ -2,32 +2,47 @@ * Access Control */ -tvheadend.acleditor = function(panel) +tvheadend.acleditor = function(panel, index) { - panel = new Ext.TabPanel({ - activeTab: 0, - autoScroll: true, - title: 'Access Control', - iconCls: 'group', - items: [] - }); + var list = 'enabled,username,password,prefix,streaming,adv_streaming,' + + 'dvr,dvr_config,webui,admin,channel_min,channel_max,channel_tag,' + + 'comment'; tvheadend.idnode_grid(panel, { url: 'api/access/entry', - comet: 'acl_entries', titleS: 'Access Entry', titleP: 'Access Entries', - tabIndex: 0, + iconCls: 'group', + columns: { + username: { width: 250 }, + password: { width: 250 }, + prefix: { width: 350 }, + streaming: { width: 100 }, + adv_streaming: { width: 100 }, + dvr: { width: 100 }, + webui: { width: 100 }, + admin: { width: 100 }, + channel_min: { width: 100 }, + channel_max: { width: 100 }, + }, + tabIndex: index, + edit: { + params: { + list: list, + }, + }, add: { url: 'api/access/entry', + params: { + list: list, + }, create: { } }, del: true, move: true, + list: list, help: function() { new tvheadend.help('Access Control Entries', 'config_access.html'); }, }); - - return panel; }; diff --git a/src/webui/static/app/capmteditor.js b/src/webui/static/app/capmteditor.js index 90141b5a..96599795 100644 --- a/src/webui/static/app/capmteditor.js +++ b/src/webui/static/app/capmteditor.js @@ -1,4 +1,5 @@ -tvheadend.capmteditor = function() { +tvheadend.capmteditor = function(panel, index) { + var fm = Ext.form; function setMetaAttr(meta, record) { @@ -104,6 +105,8 @@ tvheadend.capmteditor = function() { } }); - return new tvheadend.tableEditor('Capmt Connections', 'capmt', cm, rec, + var p = new tvheadend.tableEditor('Capmt Connections', 'capmt', cm, rec, [], store, 'config_capmt.html', 'key'); + + tvheadend.paneladd(panel, p, index); }; diff --git a/src/webui/static/app/chconf.js b/src/webui/static/app/chconf.js index f25baa91..09e65a59 100644 --- a/src/webui/static/app/chconf.js +++ b/src/webui/static/app/chconf.js @@ -6,16 +6,9 @@ insertChannelTagsClearOption = function( scope, records, options ){ scope.insert(0,new placeholder({key: '-1', val: '(Clear filter)'})); }; -tvheadend.channelTags = new Ext.data.JsonStore({ +tvheadend.channelTags = tvheadend.idnode_get_enum({ url: 'api/channeltag/list', - root: 'entries', - fields: ['key', 'val'], - id: 'key', - autoLoad: true, - sortInfo: { - field: 'val', - direction: 'ASC', - }, + event: 'channeltag', listeners: { 'load': insertChannelTagsClearOption } @@ -60,11 +53,7 @@ tvheadend.comet.on('channels', function(m) { tvheadend.channel_tab = function(panel, index) { - function assign_low_number() { - var tab = panel.getActiveTab(); - var sm = tab.getSelectionModel(); - var store = tab.getStore(); - + function assign_low_number(ctx, e, store, sm) { if (sm.getCount() !== 1) return; @@ -107,20 +96,14 @@ tvheadend.channel_tab = function(panel, index) sm.selectNext(); } - function move_number_up() { - var tab = panel.getActiveTab(); - var sm = tab.getSelectionModel(); - + function move_number_up(ctx, e, store, sm) { Ext.each(sm.getSelections(), function(channel) { var number = channel.data.number; channel.set('number', number + 1); }); } - function move_number_down() { - var tab = panel.getActiveTab(); - var sm = tab.getSelectionModel(); - + function move_number_down(ctx, e, store, sm) { Ext.each(sm.getSelections(), function(channel) { var number = channel.data.number; @@ -130,11 +113,7 @@ tvheadend.channel_tab = function(panel, index) }); } - function swap_numbers() { - var tab = panel.getActiveTab(); - var sm = tab.getSelectionModel(); - var store = tab.getStore(); //store is unused - + function swap_numbers(ctx, e, store, sm) { if (sm.getCount() !== 2) return; @@ -145,45 +124,70 @@ tvheadend.channel_tab = function(panel, index) sel[1].set('number', tmp); } - var mapButton = new Ext.Toolbar.Button({ - tooltip: 'Map services to channels', - iconCls: 'clone', - text: 'Map Services', - handler: tvheadend.service_mapper, - disabled: false - }); + var mapButton = { + name: 'map', + builder: function() { + return new Ext.Toolbar.Button({ + tooltip: 'Map services to channels', + iconCls: 'clone', + text: 'Map Services', + disabled: false + }); + }, + callback: tvheadend.service_mapper + }; - var lowNoButton = new Ext.Toolbar.Button({ - tooltip: 'Assign lowest free channel number', - iconCls: 'bullet_add', - text: 'Assign Number', - handler: assign_low_number, - disabled: false - }); + var lowNoButton = { + name: 'lowno', + builder: function() { + return new Ext.Toolbar.Button({ + tooltip: 'Assign lowest free channel number', + iconCls: 'bullet_add', + text: 'Assign Number', + disabled: false + }); + }, + callback: assign_low_number + }; - var noUpButton = new Ext.Toolbar.Button({ - tooltip: 'Move channel one number up', - iconCls: 'arrow_up', - text: 'Number Up', - handler: move_number_up, - disabled: false - }); + var noUpButton = { + name: 'noup', + builder: function() { + return new Ext.Toolbar.Button({ + tooltip: 'Move channel one number up', + iconCls: 'arrow_up', + text: 'Number Up', + disabled: false + }); + }, + callback: move_number_up + }; - var noDownButton = new Ext.Toolbar.Button({ - tooltip: 'Move channel one number down', - iconCls: 'arrow_down', - text: 'Number Down', - handler: move_number_down, - disabled: false - }); + var noDownButton = { + name: 'nodown', + builder: function() { + return new Ext.Toolbar.Button({ + tooltip: 'Move channel one number down', + iconCls: 'arrow_down', + text: 'Number Down', + disabled: false + }); + }, + callback: move_number_down + }; - var noSwapButton = new Ext.Toolbar.Button({ - tooltip: 'Swap the two selected channels numbers', - iconCls: 'arrow_switch', - text: 'Swap Numbers', - handler: swap_numbers, - disabled: false - }); + var noSwapButton = { + name: 'swap', + builder: function() { + return new Ext.Toolbar.Button({ + tooltip: 'Swap the two selected channels numbers', + iconCls: 'arrow_switch', + text: 'Swap Numbers', + disabled: false + }); + }, + callback: swap_numbers + }; tvheadend.idnode_grid(panel, { url: 'api/channel', diff --git a/src/webui/static/app/comet.js b/src/webui/static/app/comet.js index 3b9493e6..aff24087 100644 --- a/src/webui/static/app/comet.js +++ b/src/webui/static/app/comet.js @@ -2,19 +2,7 @@ * Comet interfaces */ Ext.extend(tvheadend.Comet = function() { - this.addEvents({ - accessUpdate: true, - tvAdapter: true, - dvbMux: true, - dvbStore: true, - dvbSatConf: true, - logmessage: true, - channeltags: true, - autorec: true, - dvrdb: true, - dvrconfig: true, - channels: true - }); + this.addEvents({ }); }, Ext.util.Observable); tvheadend.comet = new tvheadend.Comet(); diff --git a/src/webui/static/app/config.js b/src/webui/static/app/config.js index cd92fc58..04daff73 100644 --- a/src/webui/static/app/config.js +++ b/src/webui/static/app/config.js @@ -31,7 +31,7 @@ tvheadend.comet.on('config', function(m) { } }); -tvheadend.miscconf = function() { +tvheadend.miscconf = function(panel, index) { /* * Basic Config @@ -240,7 +240,7 @@ tvheadend.miscconf = function() { if (imagecache_form) _items.push(imagecache_form); - var panel = new Ext.Panel({ + var mpanel = new Ext.Panel({ title: 'General', iconCls: 'wrench', border: false, @@ -251,6 +251,8 @@ tvheadend.miscconf = function() { tbar: [saveButton, '->', helpButton] }); + tvheadend.paneladd(panel, mpanel, index); + /* **************************************************************** * Load/Save * ***************************************************************/ @@ -297,6 +299,4 @@ tvheadend.miscconf = function() { } }); } - - return panel; }; diff --git a/src/webui/static/app/cwceditor.js b/src/webui/static/app/cwceditor.js index 62b88749..662b84a9 100644 --- a/src/webui/static/app/cwceditor.js +++ b/src/webui/static/app/cwceditor.js @@ -1,4 +1,4 @@ -tvheadend.cwceditor = function() { +tvheadend.cwceditor = function(panel, index) { var fm = Ext.form; function setMetaAttr(meta, record) { @@ -125,5 +125,5 @@ tvheadend.cwceditor = function() { } }); - return grid; + tvheadend.paneladd(panel, grid, index); }; diff --git a/src/webui/static/app/dvr.js b/src/webui/static/app/dvr.js index 35a23e74..496bdff7 100644 --- a/src/webui/static/app/dvr.js +++ b/src/webui/static/app/dvr.js @@ -1,586 +1,170 @@ -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) { +tvheadend.dvrDetails = function(uuid) { - var content = ''; - var but; + 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 (entry.chicon != null && entry.chicon.length > 0) - content += ''; + if (chicon != null && chicon.length > 0) + content += ''; - content += '
' + entry.title + '
'; - content += '
' + entry.description + '
'; - content += '
'; - content += '
Status: ' + entry.status + '
'; + content += '
' + title + '
'; + content += '
' + desc + '
'; + content += '
'; + content += '
Status: ' + status + '
'; - var win = new Ext.Window({ - title: entry.title, - layout: 'fit', - width: 400, - height: 300, - constrainHeader: true, - buttonAlign: 'center', - html: content + var win = new Ext.Window({ + title: title, + layout: 'fit', + width: 400, + height: 300, + constrainHeader: true, + buttonAlign: 'center', + html: content + }); + + win.show(); + } + + 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'; + ], + destroy: function() { } - else { - return parseInt(value) + ' min'; - } - } + }); +} - function renderSize(value) - { - if (value == null) - return ''; - return parseInt(value / 1000000) + ' MB'; - } +/** + * + */ +tvheadend.dvr_upcoming = function(panel, index) { - function renderPri(value) { - return tvheadend.dvrprio.getById(value).data.name; - } + var actions = tvheadend.dvrRowActions(); + var list = 'disp_title,start,start_extra,stop,stop_extra,' + + 'channel,config_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 abortButton = { + name: 'abort', + builder: function() { + return new Ext.Toolbar.Button({ + tooltip: 'Abort the selected recording', + iconCls: 'cancel', + text: 'Abort', + disabled: true, }); - } - - 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 - }, - 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, - plugins: [actions], - viewConfig: { - forceFit: true }, - tbar: panelButtons, - bbar: bbar + callback: function(conf, e, store, select) { + var r = select.getSelections(); + if (r && r.length > 0) { + var uuids = []; + for (var i = 0; i < r.length; i++) + uuids.push(r[i].id); + tvheadend.AjaxConfirm({ + url: 'api/dvr/entry/cancel', + params: { + uuid: Ext.encode(uuids) + }, + success: function(d) { + store.reload(); + }, + question: 'Do you really want to abort/unschedule the selection?' + }); + } + } + }; + + function selected(s, abuttons) { + var recording = 0; + s.each(function(s) { + if (s.data.sched_status == 'recording') + recording++; + }); + abuttons.abort.setDisabled(recording < 1); + } + + function beforeedit(e, grid) { + if (e.record.data.sched_status == 'recording') + return false; + } + + tvheadend.idnode_grid(panel, { + url: 'api/dvr/entry', + gridURL: 'api/dvr/entry/grid_upcoming', + titleS: 'Upcoming Recording', + titleP: 'Upcoming Recordings', + iconCls: 'clock', + tabIndex: index, + add: { + url: 'api/dvr/entry', + params: { + list: list, + }, + create: { } + }, + edit: { + params: { + list: list, + } + }, + del: true, + list: 'disp_title,episode,pri,start_real,stop_real,' + + 'duration,channelname,creator,config_name,' + + 'sched_status', + sort: { + field: 'start_real', + direction: 'ASC' + }, + plugins: [actions], + lcol: [actions], + tbar: [abortButton], + selected: selected, + beforeedit: beforeedit, + help: function() { + new tvheadend.help('DVR', 'config_dvrlog.html'); + }, }); return panel; @@ -589,710 +173,238 @@ tvheadend.dvrschedule = function(title, iconCls, dvrStore) { /** * */ -tvheadend.autoreceditor = function() { - var fm = Ext.form; +tvheadend.dvr_finished = function(panel, index) { - 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; + var actions = tvheadend.dvrRowActions(); - if (value === 0) - return ''; + var downloadButton = { + name: 'download', + builder: function() { + return new Ext.Toolbar.Button({ + tooltip: 'Download the selected recording', + iconCls: 'save', + text: 'Download', + disabled: true + }); + }, + callback: function(conf, e, store, select) { + var r = select.getSelections(); + if (r.length > 0) { + var url = r[0].data.url; + window.location = url; + } + } + }; - 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 - }) - }]}); + function selected(s, abuttons) { + var count = s.getCount(); + abuttons.download.setDisabled(count < 1); + } - 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.idnode_grid(panel, { + url: 'api/dvr/entry', + gridURL: 'api/dvr/entry/grid_finished', + readonly: true, + 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,url', + columns: { + filesize: { + renderer: function() { + return function(v) { + if (v == null) + return ''; + return parseInt(v / 1000000) + ' MB'; + } } } - - 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',""); - } + }, + sort: { + field: 'start_real', + direction: 'ASC' + }, + plugins: [actions], + lcol: [ + actions, + { + width: 40, + header: "Play", + renderer: function(v, o, r) { + var title = r.data['disp_title']; + if (r.data['episode']) + title += ' / ' + r.data['episode']; + return 'Play'; + } + }], + tbar: [downloadButton], + selected: selected, + help: function() { + new tvheadend.help('DVR', 'config_dvrlog.html'); + }, }); - return new tvheadend.tableEditor('Automatic Recorder', 'autorec', cm, - tvheadend.autorecRecord, [], tvheadend.autorecStore, - 'autorec.html', 'wand'); + return panel; }; /** * */ -tvheadend.dvr = function() { +tvheadend.dvr_failed = function(panel, index) { - 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]; + var actions = tvheadend.dvrRowActions(); - - function reloadStores() { - for (var i = 0; i < tvheadend.dvrStores.length; i++) { - tvheadend.dvrStores[i].reload(); - } - } - - tvheadend.comet.on('dvrdb', function(m) { - reloadStores(); + tvheadend.idnode_grid(panel, { + url: 'api/dvr/entry', + gridURL: 'api/dvr/entry/grid_failed', + comet: 'dvrentry', + readonly: true, + 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_real', + direction: 'ASC' + }, + plugins: [actions], + lcol: [actions], + help: function() { + new tvheadend.help('DVR', 'config_dvrlog.html'); + }, }); - tvheadend.autorecRecord = Ext.data.Record.create(['enabled', 'title', - 'serieslink', 'channel', 'tag', 'creator', 'contenttype', 'comment', - 'minduration', 'maxduration', - 'weekdays', 'pri', 'approx_time', 'config_name']); + return panel; +}; - tvheadend.autorecStore = new Ext.data.JsonStore({ - root: 'entries', - fields: tvheadend.autorecRecord, - url: "tablemgr", - autoLoad: true, - id: 'id', - baseParams: { - table: "autorec", - op: "get" - } +/** + * + */ +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, + help: function() { + new tvheadend.help('DVR', 'config_dvr.html'); + }, }); - tvheadend.comet.on('autorec', function(m) { - if (m.reload != null) - tvheadend.autorecStore.reload(); + return panel; + +} + +/** + * + */ +tvheadend.autorec_editor = function(panel, index) { + + tvheadend.idnode_grid(panel, { + url: 'api/dvr/autorec', + titleS: 'DVR AutoRec Entry', + titleP: 'DVR AutoRec Entries', + iconCls: 'wand', + tabIndex: index, + columns: { + enabled: { width: 50 }, + title: { width: 300 }, + channel: { width: 200 }, + tag: { width: 200 }, + content_type: { width: 100 }, + minduration: { width: 80 }, + maxduration: { width: 80 }, + weekdays: { width: 160 }, + start: { width: 100 }, + pri: { width: 80 }, + config_name: { width: 120 }, + creator: { width: 200 }, + comment: { width: 200 }, + }, + add: { + url: 'api/dvr/autorec', + params: { + list: 'enabled,title,channel,tag,content_type,minduration,' + + 'maxduration,weekdays,start,pri,config_name,comment', + }, + create: { } + }, + del: true, + list: 'enabled,title,channel,tag,content_type,minduration,' + + 'maxduration,weekdays,start,pri,config_name,creator,comment', + columns: { + weekdays: { + renderer: function(st) { + return function(v) { + var t = []; + var d = v.push ? v : [v]; + if (d.length == 7) { + v = "All days"; + } else if (d.length == 0) { + v = "No days"; + } else { + for (var i = 0; i < d.length; i++) { + var r = st.find('key', d[i]); + if (r !== -1) { + var nv = st.getAt(r).get('val'); + if (nv) + t.push(nv); + } else { + t.push(d[i]); + } + } + v = t.join(','); + } + return v; + } + } + } + }, + sort: { + field: 'title', + direction: 'ASC' + }, + help: function() { + new tvheadend.help('DVR', 'config_dvrauto.html'); + }, }); - var panel = new Ext.TabPanel({ + return panel; + +}; + +/** + * + */ +tvheadend.dvr = function(panel, index) { + var p = 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 - ] + items: [], }); - 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] - }); - - /* Sub-Panel - File operations - Break into two 4-item panels */ - - 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] - }); - - 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] - }] - }); - - /* Main (form) panel */ - - var confpanel = new Ext.FormPanel({ - 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(); - }); - - 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(p, 0); + tvheadend.dvr_finished(p, 1); + tvheadend.dvr_failed(p, 2); + tvheadend.autorec_editor(p, 3); + return p; +} diff --git a/src/webui/static/app/epg.js b/src/webui/static/app/epg.js index add7723f..08021939 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,17 +118,56 @@ tvheadend.epgDetails = function(event) { '?title=' + encodeURIComponent(title) + '">Play'; } - var confcombo = new Ext.form.ComboBox({ - store: tvheadend.configNames, - triggerAction: 'all', - mode: 'local', - valueField: 'identifier', - displayField: 'name', - name: 'config_name', - emptyText: '(default)', - value: '', - editable: false - }); + var buttons = []; + + if (tvheadend.accessUpdate.dvr) { + + 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: store, + triggerAction: 'all', + mode: 'local', + valueField: 'key', + displayField: 'val', + name: 'config_name', + emptyText: '(default)', + value: '', + editable: false + }); + + buttons.push(confcombo); + buttons.push(new Ext.Button({ + handler: recordEvent, + text: "Record program" + })); + buttons.push(new Ext.Button({ + handler: recordSeries, + text: event.serieslink ? "Record series" : "Autorec" + })); + + } else { + + buttons.push(new Ext.Button({ + handler: function() { win.close(); }, + text: "Close" + })); + } var win = new Ext.Window({ title: event.title, @@ -145,33 +175,26 @@ tvheadend.epgDetails = function(event) { width: 500, height: 300, constrainHeader: true, - buttons: [confcombo, new Ext.Button({ - handler: recordEvent, - text: "Record program" - }), new Ext.Button({ - handler: recordSeries, - text: event.serieslink ? "Record series" : "Autorec" - })], + buttons: buttons, buttonAlign: 'center', html: content }); win.show(); function recordEvent() { - record('recordEvent'); + record('api/dvr/entry/create_by_event'); } function recordSeries() { - record('recordSeries'); + record('api/dvr/autorec/create_by_series'); } - function record(op) { + function record(url) { Ext.Ajax.request({ - url: 'dvr', + url: url, 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 +255,7 @@ tvheadend.epg = function() { }, { name: 'agerating' }, { - name: 'contenttype' + name: 'content_type' }, { name: 'schedstate' }, { @@ -244,7 +267,7 @@ tvheadend.epg = function() { var now = new Date; var start = record.get('start'); - if (now.getTime() > start.getTime()) { + if (now.getTime() >= start.getTime()) { meta.attr = 'style="font-weight:bold;"'; } } @@ -302,7 +325,7 @@ tvheadend.epg = function() { var now = new Date(); // Only render a progress bar for currently running programmes - if (now <= end && now >= start) + if (now >= start) return (now - start) / 1000 / duration * 100; else return ""; @@ -364,9 +387,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 +513,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 +555,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(); }); @@ -566,6 +589,44 @@ tvheadend.epg = function() { } }); + tvheadend.autorecButton = new Ext.Button({ + text: 'Create AutoRec', + iconCls: 'wand', + tooltip: 'Create an automatic recording entry that will ' + + 'record all future programmes that matches ' + + 'the current query.', + handler: createAutoRec + }); + + var tbar = [ + epgFilterTitle, '-', + epgFilterChannels, '-', + epgFilterChannelTags, '-', + epgFilterContentGroup, '-', + epgFilterDuration, '-', + { + text: 'Reset All', + handler: epgQueryClear + }, + '->', + { + text: 'Watch TV', + iconCls: 'eye', + handler: function() { + new tvheadend.VideoPlayer(); + } + }, + '-', + tvheadend.autorecButton, + '-', + { + text: 'Help', + handler: function() { + new tvheadend.help('Electronic Program Guide', 'epg.html'); + } + } + ]; + var panel = new Ext.ux.grid.livegrid.GridPanel({ stateful: true, stateId: 'epggrid', @@ -577,43 +638,7 @@ tvheadend.epg = function() { store: epgStore, selModel: new Ext.ux.grid.livegrid.RowSelectionModel(), view: epgView, - tbar: [ - epgFilterTitle, - '-', - epgFilterChannels, - '-', - epgFilterChannelTags, - '-', - epgFilterContentGroup, - '-', - epgFilterDuration, - '-', - { - text: 'Reset All', - handler: epgQueryClear - }, - '->', - { - text: 'Watch TV', - iconCls: 'eye', - handler: function() { - new tvheadend.VideoPlayer(); - } - }, - '-', - { - text: 'Create AutoRec', - iconCls: 'wand', - tooltip: 'Create an automatic recording entry that will ' - + 'record all future programmes that matches ' - + 'the current query.', - handler: createAutoRec - }, '-', { - text: 'Help', - handler: function() { - new tvheadend.help('Electronic Program Guide', 'epg.html'); - } - }], + tbar: tbar, bbar: new Ext.ux.grid.livegrid.Toolbar({ view: epgView, displayInfo: true @@ -643,6 +668,9 @@ tvheadend.epg = function() { } function createAutoRec() { + + if (!tvheadend.accessUpdate.dvr) + return; var title = epgStore.baseParams.title ? epgStore.baseParams.title : "Don't care"; @@ -650,19 +678,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,10 +702,19 @@ tvheadend.epg = function() { function createAutoRec2(params) { /* Really do it */ - params.op = 'createAutoRec'; + var conf = { + enabled: 1, + comment: 'Created from EPG query', + }; + if (params.title) conf.title = params.title; + if (params.channel) conf.channel = params.channel; + if (params.tag) conf.tag = params.tag; + if (params.content_type) conf.content_type = params.content_type; + if (params.minduration) conf.minduration = params.minduration; + if (params.maxduration) conf.maxduration = params.maxduration; Ext.Ajax.request({ - url: 'dvr', - params: params + url: 'api/dvr/autorec/create', + params: { conf: Ext.encode(conf) } }); } diff --git a/src/webui/static/app/epggrab.js b/src/webui/static/app/epggrab.js index 92d6699b..7720891c 100644 --- a/src/webui/static/app/epggrab.js +++ b/src/webui/static/app/epggrab.js @@ -8,7 +8,7 @@ tvheadend.epggrabChannels = new Ext.data.JsonStore({ 'mod-name'] }); -tvheadend.epggrab = function() { +tvheadend.epggrab = function(panel, index) { /* **************************************************************** * Data @@ -394,5 +394,5 @@ tvheadend.epggrab = function() { }); } - return confpanel; + tvheadend.paneladd(panel, confpanel, index); }; diff --git a/src/webui/static/app/esfilter.js b/src/webui/static/app/esfilter.js index a7ed3873..84e97626 100644 --- a/src/webui/static/app/esfilter.js +++ b/src/webui/static/app/esfilter.js @@ -6,7 +6,6 @@ tvheadend.esfilter_tab = function(panel) { tvheadend.idnode_grid(panel, { url: 'api/esfilter/video', - comet: 'esfilter_video', titleS: 'Video Stream Filter', titleP: 'Video Stream Filters', tabIndex: 0, @@ -23,7 +22,6 @@ tvheadend.esfilter_tab = function(panel) tvheadend.idnode_grid(panel, { url: 'api/esfilter/audio', - comet: 'esfilter_audio', titleS: 'Audio Stream Filter', titleP: 'Audio Stream Filters', tabIndex: 1, @@ -40,7 +38,6 @@ tvheadend.esfilter_tab = function(panel) tvheadend.idnode_grid(panel, { url: 'api/esfilter/teletext', - comet: 'esfilter_teletext', titleS: 'Teletext Stream Filter', titleP: 'Teletext Stream Filters', tabIndex: 2, @@ -57,7 +54,6 @@ tvheadend.esfilter_tab = function(panel) tvheadend.idnode_grid(panel, { url: 'api/esfilter/subtit', - comet: 'esfilter_subtit', titleS: 'Subtitle Stream Filter', titleP: 'Subtitle Stream Filters', tabIndex: 3, @@ -74,7 +70,6 @@ tvheadend.esfilter_tab = function(panel) tvheadend.idnode_grid(panel, { url: 'api/esfilter/ca', - comet: 'esfilter_ca', titleS: 'CA Stream Filter', titleP: 'CA Stream Filters', tabIndex: 4, @@ -91,7 +86,6 @@ tvheadend.esfilter_tab = function(panel) tvheadend.idnode_grid(panel, { url: 'api/esfilter/other', - comet: 'esfilter_other', titleS: 'Other Stream Filter', titleP: 'Other Stream Filters', tabIndex: 5, diff --git a/src/webui/static/app/ext.css b/src/webui/static/app/ext.css index 6eb6767f..d9f08263 100644 --- a/src/webui/static/app/ext.css +++ b/src/webui/static/app/ext.css @@ -24,6 +24,24 @@ text-shadow: 0 -2px rgba(0, 0, 0, 0.2); } + +.x-tab-strip li.x-tab-login { + margin-left: 16px; +} + +.x-tab-strip span.x-tab-strip-login { + vertical-align: middle; + white-space: nowrap; + padding:4px 0px; +} + +.x-tab-strip span.x-tab-strip-login-cmd { + vertical-align: middle; + cursor:pointer; + padding:4px 0; + float: right; +} + .x-tree-col { float: left; overflow: hidden; @@ -148,6 +166,10 @@ background-image: url(../icons/delete.png) !important; } +.cancel { + background-image: url(../icons/cancel.png) !important; +} + .moveup { background-image: url(../icons/arrow_up.png) !important; } @@ -294,7 +316,6 @@ .arrow_switch { background-image: url(../icons/arrow_switch.png) !important; - } .stream_config { diff --git a/src/webui/static/app/extensions.js b/src/webui/static/app/extensions.js index abd485ed..a8df6cb8 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 0827f5c6..9608274f 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; @@ -104,6 +107,44 @@ tvheadend.idnode_enum_store = function(f) return store; }; +Ext.ux.grid.filter.IntsplitFilter = Ext.extend(Ext.ux.grid.filter.NumericFilter, { + + fieldCls : Ext.form.TextField, + + constructor: function(conf) { + this.intsplit = conf.intsplit; + if (!conf.fields) + conf.fields = { + gt: { maskRe: /[0-9\.]/ }, + lt: { maskRe: /[0-9\.]/ }, + eq: { maskRe: /[0-9\.]/ } + }; + Ext.ux.grid.filter.IntsplitFilter.superclass.constructor.call(this, conf); + }, + + getSerialArgs: function () { + var key, + args = [], + values = this.menu.getValue(); + for (key in values) { + var s = values[key].toString().split('.'); + var v = 0; + if (s.length > 0) + v = parseInt(s[0]) * this.intsplit; + if (s.length > 1) + v += parseInt(s[1]); + args.push({ + type: 'numeric', + comparison: key, + value: v, + intsplit: this.intsplit + }); + } + return args; + } + +}); + tvheadend.IdNodeField = function(conf) { /* @@ -118,6 +159,9 @@ tvheadend.IdNodeField = function(conf) this.wronce = conf.wronce; this.hidden = conf.hidden || conf.advanced; this.password = conf.password; + this.duration = conf.duration; + this.intsplit = conf.intsplit; + this.group = conf.group; this.enum = conf.enum; this.store = null; if (this.enum) @@ -128,14 +172,38 @@ tvheadend.IdNodeField = function(conf) * Methods */ - this.column = function() + this.onrefresh = function(callback) { + var st = this.store; + if (st && st instanceof Ext.data.JsonStore) + st.on('load', callback); + } + + this.unrefresh = function(callback) { + var st = this.store; + if (st && st instanceof Ext.data.JsonStore) + st.un('load', callback); + } + + this.column = function(conf) { + var cfg = conf && this.id in conf ? conf[this.id] : {}; var w = 300; var ftype = 'string'; - if (this.type === 'int' || this.type === 'u32' || - this.type === 'u16' || this.type === 'dbl') { + if (this.intsplit) { + ftype = 'intsplit'; + w = 80; + } else if (this.type === 'int' || this.type === 'u32' || + this.type === 'u16' || this.type === 's64' || + this.type === 'dbl') { ftype = 'numeric'; w = 80; + } else if (this.type === 'time') { + w = 120; + ftype = 'date'; + if (this.durations) { + ftype = 'numeric'; + w = 80; + } } else if (this.type === 'bool') { ftype = 'boolean'; w = 60; @@ -144,16 +212,17 @@ tvheadend.IdNodeField = function(conf) w = 300; var props = { - width: w, + width: cfg.width || w, dataIndex: this.id, header: this.text, editor: this.editor({create: false}), - renderer: this.renderer(), + renderer: cfg.renderer ? cfg.renderer(this.store) : this.renderer(this.store), editable: !this.rdonly, hidden: this.hidden, filter: { type: ftype, - dataIndex: this.id + dataIndex: this.id, + intsplit: this.intsplit } }; @@ -167,25 +236,43 @@ tvheadend.IdNodeField = function(conf) return props; }; - this.renderer = function() + this.renderer = function(st) { if (this.password) return function(v) { return '********'; } - - if (!this.store) + + if (this.type === 'time') { + if (this.duration) + return function(v) { + if (v < 0 || v === '') + return "Not set"; + var i = parseInt(v / 60); /* Nevermind the seconds */ + if (i === 0) + return "0"; + var hours = parseInt(i / 60); + var min = parseInt(i % 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 (!st) return null; - var st = this.store; 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) { @@ -206,6 +293,7 @@ tvheadend.IdNodeField = function(conf) this.editor = function(conf) { var cons = null; + var combo = false; /* Editable? */ var d = this.rdonly; @@ -220,7 +308,7 @@ tvheadend.IdNodeField = function(conf) disabled: d, width: 300 }; - + /* ComboBox */ if (this.enum) { cons = Ext.form.ComboBox; @@ -234,11 +322,22 @@ 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 + ' ...'; + + combo = true; /* 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,17 +346,27 @@ tvheadend.IdNodeField = function(conf) case 'int': case 'u32': case 'u16': + case 's32': case 'dbl': - cons = Ext.form.NumberField; + case 'time': + if (this.intsplit) { + c['maskRe'] = /[0-9\.]/; + cons = Ext.form.TextField; + } else + cons = Ext.form.NumberField; break; + /* 'str' and 'perm' */ default: cons = Ext.form.TextField; break; } } - return new cons(c); + var r = new cons(c); + if (combo) + r.doQuery = tvheadend.doQueryAnyMatch; + return r; }; }; @@ -271,8 +380,10 @@ tvheadend.IdNode = function(conf) */ this.clazz = conf.class; this.text = conf.caption || this.clazz; + this.event = conf.event; 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])); @@ -363,6 +474,9 @@ tvheadend.idnode_editor_field = function(f, create) } } }); + + r.doQuery = tvheadend.doQueryAnyMatch; + if (st.on) { var fn = function() { st.un('load', fn); @@ -370,6 +484,7 @@ tvheadend.idnode_editor_field = function(f, create) }; st.on('load', fn); } + return r; /* TODO: listeners for regexp? listeners : { @@ -387,16 +502,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,12 +509,45 @@ 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': + if (f.intsplit) { + /* this should be improved */ + return new Ext.form.TextField({ + fieldLabel: f.caption, + name: f.id, + value: value, + disabled: d, + width: 300, + maskRe: /[0-9\.]/, + }); + } return new Ext.form.NumberField({ fieldLabel: f.caption, name: f.id, @@ -417,61 +555,143 @@ 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 (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 (df.length) { - panel.add(new Ext.form.FieldSet({ - title: 'Basic Settings', - autoHeight: true, - autoWidth: true, - collapsible: true, - collapsed: false, - items: df - })); + + 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 (af.length) { - panel.add(new Ext.form.FieldSet({ - title: 'Advanced Settings', - autoHeight: true, - autoWidth: true, - collapsible: true, - collapsed: false, //true, - items: af - })); + + 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, style: 'border-width: 0px', bodyStyle: ' ' }); + 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, style: 'border-width: 0px', bodyStyle: ' ' }); + 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 (rf.length) { - panel.add(new Ext.form.FieldSet({ - title: 'Read-only Info', - autoHeight: true, - autoWidth: true, - collapsible: true, - collapsed: false, //true, - items: rf - })); + if (df.length && !af.length && !rf.length) { + var f = newFieldSet({ nocollapse: true, items: df }); + panel.add(f); + } else { + 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 +705,52 @@ 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(); - } - }); - } - }); - buttons.push(saveBtn); - - if (conf.help) { - var helpBtn = new Ext.Button({ - text: 'Help', - handler: conf.help + 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(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, - autoWidth: true, + labelWidth: conf.labelWidth || 200, + autoWidth: conf.noautoWidth ? false : true, autoHeight: !conf.fixedHeight, - width: 600, - //defaults: {width: 330}, + width: conf.nowidth ? null : (conf.width || 600), 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 +759,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 +771,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 +841,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 +850,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 +882,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(); } }); } @@ -679,36 +905,58 @@ tvheadend.idnode_create = function(conf) */ tvheadend.idnode_grid = function(panel, conf) { + var store = null; + var grid = null; + var event = null; + var auto = null; + var idnode = null; + + var update = function(o) { + if (auto.getValue()) + store.reload(); + }; + + var update2 = function(o) { + grid.getView().refresh(); + }; + function build(d) { - var columns = conf.lcol || []; + if (conf.builder) + conf.builder(conf); + + var columns = []; var filters = []; var fields = []; var buttons = []; + var abuttons = {}; var plugins = conf.plugins || []; - var saveBtn = null; - var undoBtn = null; - var addBtn = null; - var delBtn = null; - var upBtn = null; - var downBtn = null; - var editBtn = null; + + /* Some copies */ + if (conf.add && !conf.add.titleS && conf.titleS) + conf.add.titleS = conf.titleS; + + /* Left-hand columns (do copy, no reference!) */ + if (conf.lcol) + for (i = 0; i < conf.lcol.length; i++) + columns.push(conf.lcol[i]); /* Model */ - var idnode = new tvheadend.IdNode(d); + idnode = new tvheadend.IdNode(d); for (var i = 0; i < idnode.length(); i++) { var f = idnode.field(i); - var c = f.column(); + var c = f.column(conf.columns); fields.push(f.id); columns.push(c); if (c.filter) filters.push(c.filter); + f.onrefresh(update2); } /* Right-hand columns */ if (conf.rcol) for (i = 0; i < conf.rcol.length; i++) - columns.push(conf.rcol[i]); + columns.push(conf.rcol[i]); /* Filters */ var filter = new Ext.ux.grid.GridFilters({ @@ -717,21 +965,17 @@ tvheadend.idnode_grid = function(panel, conf) filters: filters }); - var sort = null; - if (conf.sort) - sort = conf.sort; - /* Store */ - var store = new Ext.data.JsonStore({ + store = new Ext.data.JsonStore({ root: 'entries', - url: conf.url + '/grid', + url: conf.gridURL || (conf.url + '/grid'), autoLoad: true, id: 'uuid', totalProperty: 'total', fields: fields, remoteSort: true, pruneModifiedRecords: true, - sortInfo: sort + sortInfo: conf.sort ? conf.sort : null, }); /* Model */ @@ -751,62 +995,69 @@ tvheadend.idnode_grid = function(panel, conf) /* Event handlers */ store.on('update', function(s, r, o) { var d = (s.getModifiedRecords().length === 0); - undoBtn.setDisabled(d); - saveBtn.setDisabled(d); + if (abuttons.undo) + abuttons.undo.setDisabled(d); + if (abuttons.save) + abuttons.save.setDisabled(d); }); select.on('selectionchange', function(s) { - if (delBtn) - delBtn.setDisabled(s.getCount() === 0); - if (upBtn) { - upBtn.setDisabled(s.getCount() === 0); - downBtn.setDisabled(s.getCount() === 0); + var count = s.getCount(); + if (abuttons.del) + abuttons.del.setDisabled(count === 0); + if (abuttons.up) { + abuttons.up.setDisabled(count === 0); + abuttons.down.setDisabled(count === 0); } - editBtn.setDisabled(s.getCount() !== 1); + if (abuttons.edit) + abuttons.edit.setDisabled(count !== 1); if (conf.selected) - conf.selected(s); + conf.selected(s, abuttons); }); /* 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) { + abuttons.save = 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(abuttons.save); + abuttons.undo = new Ext.Toolbar.Button({ + tooltip: 'Revert pending changes (marked with red border)', + iconCls: 'undo', + text: 'Undo', + disabled: true, + handler: function() { + store.rejectChanges(); + } + }); + buttons.push(abuttons.undo); + } if (conf.add) { - addBtn = new Ext.Toolbar.Button({ + if (buttons.length > 0) + buttons.push('-'); + abuttons.add = new Ext.Toolbar.Button({ tooltip: 'Add a new entry', iconCls: 'add', text: 'Add', @@ -815,10 +1066,12 @@ tvheadend.idnode_grid = function(panel, conf) tvheadend.idnode_create(conf.add); } }); - buttons.push(addBtn); + buttons.push(abuttons.add); } if (conf.del) { - delBtn = new Ext.Toolbar.Button({ + if (!conf.add && buttons.length > 0) + buttons.push('-'); + abuttons.del = new Ext.Toolbar.Button({ tooltip: 'Delete selected entries', iconCls: 'remove', text: 'Delete', @@ -829,7 +1082,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.AjaxConfirm({ url: 'api/idnode/delete', params: { uuid: Ext.encode(uuids) @@ -843,10 +1096,10 @@ tvheadend.idnode_grid = function(panel, conf) } } }); - buttons.push(delBtn); + buttons.push(abuttons.del); } if (conf.move) { - upBtn = new Ext.Toolbar.Button({ + abuttons.up = new Ext.Toolbar.Button({ tooltip: 'Move selected entries up', iconCls: 'moveup', text: 'Move Up', @@ -857,7 +1110,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) @@ -870,8 +1123,8 @@ tvheadend.idnode_grid = function(panel, conf) } } }); - buttons.push(upBtn); - downBtn = new Ext.Toolbar.Button({ + buttons.push(abuttons.up); + abuttons.down = new Ext.Toolbar.Button({ tooltip: 'Move selected entries down', iconCls: 'movedown', text: 'Move Down', @@ -882,7 +1135,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) @@ -895,64 +1148,68 @@ tvheadend.idnode_grid = function(panel, conf) } } }); - buttons.push(downBtn); + buttons.push(abuttons.down); } - 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('-'); + abuttons.edit = 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 = {}; + if (conf.edit && conf.edit.params) + params = conf.edit.params; + params['uuid'] = r.id; + params['meta'] = 1; + 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(abuttons.edit); + } /* Hide Mode */ if (conf.hidemode) { @@ -995,12 +1252,19 @@ tvheadend.idnode_grid = function(panel, conf) 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]); + var t = conf.tbar[i]; + if (t.name && t.builder) { + var b = t.builder(); + if (t.callback) { + b.callback = t.callback; + b.handler = function(b, e) { + this.callback(this, e, store, select); + } + } + abuttons[t.name] = b; + buttons.push(b); + } else if (t.name) + buttons.push(t.name); } } @@ -1014,7 +1278,7 @@ tvheadend.idnode_grid = function(panel, conf) } /* Grid Panel */ - var auto = new Ext.form.Checkbox({ + auto = new Ext.form.Checkbox({ checked: true, listeners: { check: function(s, c) { @@ -1059,11 +1323,10 @@ 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, store: store, cm: model, selModel: select, @@ -1073,87 +1336,398 @@ tvheadend.idnode_grid = function(panel, conf) }, tbar: buttons, bbar: page - }); + }; + grid = conf.readonly ? new Ext.grid.GridPanel(gconf) : + new Ext.grid.EditorGridPanel(gconf); grid.on('filterupdate', function() { page.changePage(0); }); + if (conf.beforeedit) + grid.on('beforeedit', conf.beforeedit); - if (conf.tabIndex != null) - panel.insert(conf.tabIndex, grid); - else - panel.add(grid); + dpanel.add(grid); + dpanel.doLayout(false, true); /* Add comet listeners */ - var update = function(o) { - if (auto.getValue()) - store.reload(); - }; if (conf.comet) tvheadend.comet.on(conf.comet, update); - tvheadend.comet.on('idnodeUpdated', update); - tvheadend.comet.on('idnodeDeleted', update); + if (idnode.event && idnode.event != conf.comet) { + event = idnode.event; + tvheadend.comet.on(idnode.event, update); + } } - /* Request data */ - if (!conf.fields) { - Ext.Ajax.request({ - url: conf.url + '/class', - success: function(d) - { - var d = json_decode(d); - build(d); - } - }); - } else { - build(conf.fields); + function builder() { + if (grid) + return; + + /* Request data */ + if (!conf.fields) { + 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); + build(d); + } + }); + } else { + build(conf.fields); + } } + + function destroyer() { + if (grid === null || !tvheadend.dynamic) + return; + if (conf.comet) + tvheadend.comet.un(conf.comet, update); + if (event) + tvheadend.comet.un(event, update); + for (var i = 0; i < idnode.length(); i++) { + var f = idnode.field(i); + f.unrefresh(); + } + dpanel.removeAll(true); + store.destroy(); + grid = null; + store = null; + auto = null; + event = null; + idnode = null; + if (conf.destroyer) + conf.destroyer(conf); + } + + var dpanel = new Ext.Panel({ + border: false, + header: false, + layout: 'fit', + title: conf.titleP || '', + iconCls: conf.iconCls || '' + }); + + tvheadend.paneladd(panel, dpanel, conf.tabIndex); + tvheadend.panelreg(panel, dpanel, builder, destroyer); }; -tvheadend.idnode_tree = function(conf) +/* + * IDnode form grid + */ +tvheadend.idnode_form_grid = function(panel, conf) { - var current = null; - var params = conf.params || {}; - var loader = new Ext.tree.TreeLoader({ - dataUrl: conf.url, - baseParams: params, - preloadChildren: conf.preload, - nodeParameter: 'uuid' - }); + var mpanel = null; + var store = null; - var tree = new Ext.tree.TreePanel({ - loader: loader, - flex: 1, - autoScroll: true, - border: false, - animate: false, - root: new Ext.tree.AsyncTreeNode({ - id: conf.root || 'root', - text: conf.title || '' - }), - listeners: { - click: function(n) { + var update = function(o) { + if (store) + store.reload(); + }; + + function builder() { + if (mpanel) + return; + + if (conf.builder) + conf.builder(conf); + + var buttons = []; + var abuttons = {}; + var plugins = conf.plugins || []; + var current = null; + var selectuuid = null; + + /* Store */ + 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' + } + }); + + store.on('load', function(records) { + var s = false; + if (selectuuid) { + records.each(function(r) { + if (r.id === selectuuid) { + select.selectRecords([r]); + s = true; + } + }); + selectuuid = null; + } else if (!current && !select.getSelected()) + select.selectFirstRow(); + }); + + /* 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, abuttons); + }); + + /* Top bar */ + abuttons.save = 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() { + selectuuid = current.uuid; + roweditor_destroy(); + store.reload(); + } + }); + } + }); + buttons.push(abuttons.save); + abuttons.undo = new Ext.Toolbar.Button({ + tooltip: 'Revert pending changes (marked with red border)', + iconCls: 'undo', + text: 'Undo', + disabled: true, + handler: function() { if (current) - panel.remove(current); - if (!n.isRoot) - current = panel.add(new tvheadend.idnode_editor(n.attributes, { - title: 'Parameters', - fixedHeight: true, - help: conf.help || null - })); - panel.doLayout(); + current.editor.getForm().reset(); + } + }); + buttons.push(abuttons.undo); + buttons.push('-'); + if (conf.add) { + abuttons.add = 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(abuttons.add); + } + if (conf.del) { + abuttons.del = new Ext.Toolbar.Button({ + tooltip: 'Delete selected entries', + iconCls: 'remove', + text: 'Delete', + disabled: true, + handler: function() { + if (current) { + tvheadend.AjaxConfirm({ + url: 'api/idnode/delete', + params: { + uuid: current.uuid + }, + success: function(d) { + roweditor_destroy(); + store.reload(); + } + }); + } + } + }); + buttons.push(abuttons.del); + } + if (conf.add || conf.del) + buttons.push('-'); + + /* Extra buttons */ + if (conf.tbar) { + buttons.push('-'); + for (i = 0; i < conf.tbar.length; i++) { + var t = conf.tbar[i]; + if (t.name && t.builder) { + var b = t.builder(); + if (t.callback) { + b.callback = t.callback; + b.handler = function(b, e) { + this.callback(this, e, store, select); + } + } + abuttons[t.name] = b; + buttons.push(b); + } else if (t.name) + buttons.push(t.name); } } - }); - if (conf.comet) { - tvheadend.comet.on(conf.comet, function(o) { - if (o.reload) - tree.getRootNode().reload(); + /* Help */ + if (conf.help) { + buttons.push('->'); + buttons.push({ + text: 'Help', + handler: conf.help + }); + } + + function roweditor_destroy() { + if (current) + mpanel.remove(current.editor); + current = null; + } + + function roweditor(r) { + if (!r || !r.id) + return; + if (current && current.uuid == r.id) + return; + tvheadend.Ajax({ + url: 'api/idnode/load', + params: { + uuid: r.id, + meta: 1 + }, + success: function(d) { + d = json_decode(d); + roweditor_destroy(); + var editor = new tvheadend.idnode_editor(d[0], { + title: 'Parameters', + labelWidth: 300, + fixedHeight: true, + help: conf.help || null, + inTabPanel: true, + noButtons: true, + width: 730, + noautoWidth: true + }); + current = { + uuid: d[0].id, + editor: editor + } + abuttons.save.setDisabled(false); + abuttons.undo.setDisabled(false); + abuttons.del.setDisabled(false); + mpanel.add(editor); + mpanel.doLayout(); + } + }); + } + + /* Grid Panel (Selector) */ + var grid = new Ext.grid.GridPanel({ + flex: 1, + autoWidth: true, + autoScroll: true, + stripeRows: true, + store: store, + cm: model, + selModel: select, + plugins: plugins, + border: false, + viewConfig: { + forceFit: true + }, + listeners : { + render : { + fn : function() { + if (!current) + select.selectFirstRow(); + } + } + } }); + + var mpanel = new Ext.Panel({ + tbar: buttons, + layout: 'hbox', + padding: 5, + border: false, + layoutConfig: { + align: 'stretch' + }, + items: [grid] + }); + + dpanel.add(mpanel); + dpanel.doLayout(false, true); + + if (conf.comet) + tvheadend.comet.on(conf.comet, update); } - // TODO: top-level reload - tvheadend.comet.on('idnodeUpdated', function(o) { + function destroyer() { + if (mpanel === null || !tvheadend.dynamic) + return; + if (conf.comet) + tvheadend.comet.un(conf.comet, update); + dpanel.removeAll(true); + store.destroy(); + mpanel = null; + store = null; + if (conf.destroyer) + conf.destroyer(conf); + } + + var dpanel = new Ext.Panel({ + border: false, + header: false, + layout: 'fit', + title: conf.titleP || '', + iconCls: conf.iconCls || '' + }); + + tvheadend.paneladd(panel, dpanel, conf.tabIndex); + tvheadend.panelreg(panel, dpanel, builder, destroyer); +}; + +/* + * IDNode Tree + */ +tvheadend.idnode_tree = function(panel, conf) +{ + var tree = null; + var events = {}; + + function update(o) { + if (tree && o.reload) + tree.getRootNode().reload(); + } + + function updatenode(o) { + if (o.uuid) + tree.getRootNode().reload(); + } + + function updatetitle(o) { var n = tree.getNodeById(o.uuid); if (n) { if (o.text) @@ -1161,25 +1735,115 @@ tvheadend.idnode_tree = function(conf) tree.getRootNode().reload(); // cannot get this to properly reload children and maintain state } - }); + } - var panel = new Ext.Panel({ - title: conf.title || '', - layout: 'hbox', - flex: 1, - padding: 5, + function builder() { + if (tree) + return; + if (conf.builder) + conf.builder(conf); + + var current = null; + var first = true; + var params = conf.params || {}; + var loader = new Ext.tree.TreeLoader({ + dataUrl: conf.url, + baseParams: params, + preloadChildren: conf.preload, + nodeParameter: 'uuid' + }); + + loader.on('load', function(l, n, r) { + var event = n.attributes.event; + if (n.attributes.uuid && event && !(event in events)) { + events[event] = 1; + tvheadend.comet.on(event, updatenode); + } + if (first) { /* hack */ + dpanel.doLayout(); + first = false; + } + }); + + tree = new Ext.tree.TreePanel({ + loader: loader, + autoWidth: true, + flex: 1, + autoScroll: true, + border: false, + animate: false, + root: new Ext.tree.AsyncTreeNode({ + id: conf.root || 'root', + text: conf.title || '' + }), + listeners: { + click: function(n) { + if (current) { + mpanel.remove(current); + current = null; + } + if (!n.isRoot) { + current = new tvheadend.idnode_editor(n.attributes, { + title: 'Parameters', + width: 550, + noautoWidth: true, + fixedHeight: true, + help: conf.help || null + }); + mpanel.add(current); + mpanel.doLayout(); + } + } + } + }); + + var mpanel = new Ext.Panel({ + layout: 'hbox', + padding: 5, + border: false, + layoutConfig: { + align: 'stretch' + }, + items: [tree] + }); + + tree.on('beforerender', function() { + // To be honest this isn't quite right, but it'll do + tree.expandAll(); + }); + + dpanel.add(mpanel); + dpanel.doLayout(false, true); + + if (conf.comet) + tvheadend.comet.on(conf.comet, update); + + tvheadend.comet.on('title', updatetitle); + } + + function destroyer() { + if (tree === null || !tvheadend.dynamic) + return; + for (var event in events) + tvheadend.comet.un(event, updatenode); + delete events; + events = {}; + tvheadend.comet.un('title', updatetitle); + if (conf.comet) + tvheadend.comet.un(conf.comet, update); + dpanel.removeAll(true); + tree = null; + if (conf.destroyer) + conf.destroyer(conf); + } + + var dpanel = new Ext.Panel({ border: false, - layoutConfig: { - align: 'stretch' - }, - items: [tree] + header: false, + layout: 'fit', + title: conf.title || '', }); - - tree.on('beforerender', function() { - // To be honest this isn't quite right, but it'll do - tree.expandAll(); - }); - - return panel; + tvheadend.paneladd(panel, dpanel, conf.tabIndex); + tvheadend.panelreg(panel, dpanel, builder, destroyer); }; diff --git a/src/webui/static/app/iptv.js b/src/webui/static/app/iptv.js deleted file mode 100644 index 83609b35..00000000 --- a/src/webui/static/app/iptv.js +++ /dev/null @@ -1,318 +0,0 @@ -/** - * IPTV service grid - */ -tvheadend.iptv = function(adapterId) { - - var servicetypeStore = new Ext.data.JsonStore({ - root: 'entries', - id: 'val', - url: '/iptv/services', - baseParams: { - op: 'servicetypeList' - }, - fields: ['val', 'str'], - autoLoad: false, - sortInfo: { - field: 'channelname', - direction: 'ASC' - } - }); - - var fm = Ext.form; - - - var actions = new Ext.ux.grid.RowActions({ - header: '', - dataIndex: 'actions', - width: 45, - actions: [{ - iconCls: 'info', - qtip: 'Detailed information about service', - cb: function(grid, record, action, row, col) { - Ext.Ajax.request({ - url: "servicedetails/" + record.id, - success: function(response, options) { - r = Ext.util.JSON.decode(response.responseText); - tvheadend.showTransportDetails(r); - } - }); - } - }] - }); - - var cm = new Ext.grid.ColumnModel({ - defaultSortable: true, - columns: [ - { - xtype: 'checkcolumn', - header: "Enabled", - dataIndex: 'enabled', - width: 45 - }, - { - header: "Channel name", - dataIndex: 'channelname', - width: 150, - renderer: function(value, metadata, record, row, col, store) { - return value ? value - : 'Unmapped'; - }, - editor: new fm.ComboBox({ - store: tvheadend.channels, - allowBlank: true, - typeAhead: true, - minChars: 2, - lazyRender: true, - triggerAction: 'all', - mode: 'local', - displayField: 'name' - }) - }, - { - header: "Interface", - dataIndex: 'interface', - width: 100, - renderer: function(value, metadata, record, row, col, store) { - return value ? value : 'Unset'; - }, - editor: new fm.TextField({ - allowBlank: false - }) - }, - { - header: "Group", - dataIndex: 'group', - width: 100, - renderer: function(value, metadata, record, row, col, store) { - return value ? value : 'Unset'; - }, - editor: new fm.TextField({ - allowBlank: false - }) - }, - { - header: "UDP Port", - dataIndex: 'port', - width: 60, - editor: new fm.NumberField({ - minValue: 1, - maxValue: 65535 - }) - }, - { - header: "Service ID", - dataIndex: 'sid', - width: 50, - hidden: true - }, - { - header: 'Service Type', - width: 100, - dataIndex: 'stype', - hidden: true, - editor: new fm.ComboBox({ - valueField: 'val', - displayField: 'str', - forceSelection: false, - editable: false, - mode: 'local', - triggerAction: 'all', - store: servicetypeStore - }), - renderer: function(value, metadata, record, row, col, store) { - var val = value ? servicetypeStore.getById(value) : null; - return val ? val.get('str') - : 'Unset'; - } - }, { - header: "PMT PID", - dataIndex: 'pmt', - width: 50, - hidden: true - }, { - header: "PCR PID", - dataIndex: 'pcr', - width: 50, - hidden: true - }, actions]}); - - var rec = Ext.data.Record.create(['id', 'enabled', 'channelname', - 'interface', 'group', 'port', 'sid', 'pmt', 'pcr', 'stype']); - - var store = new Ext.data.JsonStore({ - root: 'entries', - fields: rec, - url: "iptv/services", - autoLoad: true, - id: 'id', - baseParams: { - op: "get" - }, - listeners: { - 'update': function(s, r, o) { - d = s.getModifiedRecords().length === 0 - saveBtn.setDisabled(d); - rejectBtn.setDisabled(d); - } - } - }); - - /* - var storeReloader = new Ext.util.DelayedTask(function() { - store.reload() - }); - - tvheadend.comet.on('dvbService', function(m) { - storeReloader.delay(500); - }); - */ - - function addRecord() { - Ext.Ajax.request({ - url: "iptv/services", - params: { - op: "create" - }, - failure: function(response, options) { - Ext.MessageBox.alert('Server Error', - 'Unable to generate new record'); - }, - success: function(response, options) { - var responseData = Ext.util.JSON.decode(response.responseText); - var p = new rec(responseData, responseData.id); - grid.stopEditing(); - store.insert(0, p); - grid.startEditing(0, 0); - } - }); - } - ; - - function delSelected() { - var selectedKeys = grid.selModel.selections.keys; - if (selectedKeys.length > 0) { - Ext.MessageBox.confirm('Message', - 'Do you really want to delete selection?', deleteRecord); - } - else { - Ext.MessageBox.alert('Message', - 'Please select at least one item to delete'); - } - } - ; - - function deleteRecord(btn) { - if (btn === 'yes') { - var selectedKeys = grid.selModel.selections.keys; - - Ext.Ajax.request({ - url: "iptv/services", - params: { - op: "delete", - entries: Ext.encode(selectedKeys) - }, - failure: function(response, options) { - Ext.MessageBox.alert('Server Error', 'Unable to delete'); - }, - success: function(response, options) { - store.reload(); - } - }); - } - } - - function saveChanges() { - 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].id = mr[x].id; - } - - Ext.Ajax.request({ - url: "iptv/services", - params: { - op: "update", - entries: Ext.encode(out) - }, - success: function(response, options) { - store.commitChanges(); - }, - failure: function(response, options) { - Ext.MessageBox.alert('Message', response.statusText); - } - }); - } - - var delButton = new Ext.Toolbar.Button({ - tooltip: 'Delete one or more selected rows', - iconCls: 'remove', - text: 'Delete selected services', - handler: delSelected, - disabled: true - }); - - var saveBtn = new Ext.Toolbar.Button({ - tooltip: 'Save any changes made (Changed cells have red borders).', - iconCls: 'save', - text: "Save changes", - handler: saveChanges, - disabled: true - }); - - var rejectBtn = new Ext.Toolbar.Button({ - tooltip: 'Revert any changes made (Changed cells have red borders).', - iconCls: 'undo', - text: "Revert changes", - handler: function() { - store.rejectChanges(); - }, - disabled: true - }); - - var selModel = new Ext.grid.RowSelectionModel({ - singleSelect: false - }); - - var grid = new Ext.grid.EditorGridPanel({ - stripeRows: true, - title: 'IPTV', - iconCls: 'iptv', - plugins: [actions], - store: store, - clicksToEdit: 2, - cm: cm, - viewConfig: { - forceFit: true - }, - selModel: selModel, - tbar: [ - { - tooltip: 'Create a new entry on the server. ' - + 'The new entry is initially disabled so it must be enabled ' - + 'before it start taking effect.', - iconCls: 'add', - text: 'Add service', - handler: addRecord - }, '-', delButton, '-', saveBtn, rejectBtn, '->', - { - text: 'Help', - handler: function() { - new tvheadend.help('IPTV', 'config_iptv.html'); - } - }] - }); - - store.on('update', function(s, r, o) { - d = s.getModifiedRecords().length === 0; - saveBtn.setDisabled(d); - rejectBtn.setDisabled(d); - }); - - selModel.on('selectionchange', function(self) { - delButton.setDisabled(self.getCount() === 0); - }); - - return grid; -}; diff --git a/src/webui/static/app/mpegts.js b/src/webui/static/app/mpegts.js index a091b4eb..bb5c193d 100644 --- a/src/webui/static/app/mpegts.js +++ b/src/webui/static/app/mpegts.js @@ -25,14 +25,13 @@ tvheadend.comet.on('mpegts_network', function() { tvheadend.network_list.reload(); }); -tvheadend.networks = function(panel) +tvheadend.networks = function(panel, index) { tvheadend.idnode_grid(panel, { url: 'api/mpegts/network', - comet: 'mpegts_network', titleS: 'Network', titleP: 'Networks', - tabIndex: 1, + tabIndex: index, help: function() { new tvheadend.help('Networks', 'config_networks.html'); }, @@ -57,14 +56,13 @@ tvheadend.networks = function(panel) }); }; -tvheadend.muxes = function(panel) +tvheadend.muxes = function(panel, index) { tvheadend.idnode_grid(panel, { url: 'api/mpegts/mux', - comet: 'mpegts_mux', titleS: 'Mux', titleP: 'Muxes', - tabIndex: 2, + tabIndex: index, hidemode: true, help: function() { new tvheadend.help('Muxes', 'config_muxes.html'); @@ -195,53 +193,71 @@ tvheadend.show_service_streams = function(data) { win.show(); }; -tvheadend.services = function(panel) +tvheadend.services = function(panel, index) { - var mapButton = new Ext.Toolbar.Button({ - tooltip: 'Map services to channels', - iconCls: 'clone', - text: 'Map All', - callback: tvheadend.service_mapper, - disabled: false - }); - var selected = function(s) - { - if (s.getCount() > 0) - mapButton.setText('Map Selected'); - else - mapButton.setText('Map All'); - }; - var actions = new Ext.ux.grid.RowActions({ - header: 'Details', - width: 10, - actions: [{ - iconCls: 'info', - qtip: 'Detailed stream info', - cb: function(grid, rec, act, row, col) { - Ext.Ajax.request({ - url: 'api/service/streams', - params: { - uuid: rec.id - }, - success: function(r, o) { - var d = Ext.util.JSON.decode(r.responseText); - tvheadend.show_service_streams(d); - } - }); - } - }] - }); + function builder(conf) { + var mapButton = { + name: 'map', + builder: function() { + return new Ext.Toolbar.Button({ + tooltip: 'Map services to channels', + iconCls: 'clone', + text: 'Map All', + disabled: false + }); + }, + callback: tvheadend.service_mapper + }; + + var selected = function(s, abuttons) + { + if (s.getCount() > 0) + abuttons.map.setText('Map Selected'); + else + abuttons.map.setText('Map All'); + }; + + var actions = new Ext.ux.grid.RowActions({ + header: 'Details', + width: 10, + actions: [{ + iconCls: 'info', + qtip: 'Detailed stream info', + cb: function(grid, rec, act, row, col) { + Ext.Ajax.request({ + url: 'api/service/streams', + params: { + uuid: rec.id + }, + success: function(r, o) { + var d = Ext.util.JSON.decode(r.responseText); + tvheadend.show_service_streams(d); + } + }); + } + }], + destroy: function() { + } + }); + conf.tbar = [mapButton]; + conf.selected = selected; + conf.lcol[1] = actions; + conf.plugins = [actions]; + } + function destroyer(conf) { + delete conf.tbar; + delete conf.plugins; + conf.lcol[1] = {}; + conf.selected = null; + } tvheadend.idnode_grid(panel, { url: 'api/mpegts/service', - comet: 'service', titleS: 'Service', titleP: 'Services', - tabIndex: 3, + tabIndex: index, hidemode: true, add: false, del: false, - selected: selected, - tbar: [mapButton], help: function() { new tvheadend.help('Services', 'config_services.html'); }, @@ -255,24 +271,26 @@ tvheadend.services = function(panel) "?title=" + encodeURIComponent(title) + "'>Play"; } }, - actions + { + /* placeholder for actions */ + } ], - plugins: [actions], sort: { field: 'svcname', direction: 'ASC' - } + }, + builder: builder, + destroyer: destroyer }); }; -tvheadend.mux_sched = function(panel) +tvheadend.mux_sched = function(panel, index) { tvheadend.idnode_grid(panel, { url: 'api/mpegts/mux_sched', - comet: 'mpegts_mux_sched', titleS: 'Mux Scheduler', titleP: 'Mux Schedulers', - tabIndex: 4, + tabIndex: index, help: function() { new tvheadend.help('Mux Schedulers', 'config_muxsched.html'); }, diff --git a/src/webui/static/app/servicemapper.js b/src/webui/static/app/servicemapper.js index f4e8873d..26b2857e 100644 --- a/src/webui/static/app/servicemapper.js +++ b/src/webui/static/app/servicemapper.js @@ -4,10 +4,8 @@ tvheadend.service_mapper_status_panel = null; -tvheadend.service_mapper_status = function() +tvheadend.service_mapper_status = function(panel, index) { - var panel; - /* Fields */ var ok = new Ext.form.Label({ fieldLabel: 'Mapped', @@ -31,7 +29,7 @@ tvheadend.service_mapper_status = function() }); /* Panel */ - panel = new Ext.FormPanel({ + var mpanel = new Ext.FormPanel({ method: 'get', title: 'Service Mapper', frame: true, @@ -72,9 +70,9 @@ tvheadend.service_mapper_status = function() } }); - tvheadend.service_mapper_status_panel = panel; - return panel; -}; + tvheadend.service_mapper_status_panel = mpanel; + tvheadend.paneladd(panel, mpanel, index); +} /* * Start mapping @@ -182,4 +180,4 @@ tvheadend.service_mapper = function(t, e, store, select) }); win.show(); -}; +} diff --git a/src/webui/static/app/status.js b/src/webui/static/app/status.js index 5375d124..13caa2b9 100644 --- a/src/webui/static/app/status.js +++ b/src/webui/static/app/status.js @@ -1,52 +1,19 @@ /** * */ -tvheadend.status_subs = function() { - - tvheadend.subsStore = new Ext.data.JsonStore({ - root: 'entries', - totalProperty: 'totalCount', - fields: [{ - name: 'id' - }, { - name: 'hostname' - }, { - name: 'username' - }, { - name: 'title' - }, { - name: 'channel' - }, { - name: 'service' - }, { - name: 'state' - }, { - name: 'errors' - }, { - name: 'in' - }, { - name: 'out' - }, { - name: 'start', - type: 'date', - dateFormat: 'U' /* unix time */ - }], - url: 'api/status/subscriptions', - autoLoad: true, - id: 'id' - }); - - - - tvheadend.comet.on('subscriptions', function(m) { +tvheadend.status_subs = function(panel, index) +{ + var subs = null; + var store = null; + function update(m) { if (m.reload != null) - tvheadend.subsStore.reload(); + store.reload(); if (m.updateEntry != null) { - r = tvheadend.subsStore.getById(m.id); + r = store.getById(m.id); if (typeof r === 'undefined') { - tvheadend.subsStore.reload(); + store.reload(); return; } @@ -57,378 +24,495 @@ tvheadend.status_subs = function() { r.data.in = m.in; r.data.out = m.out; - tvheadend.subsStore.afterEdit(r); - tvheadend.subsStore.fireEvent('updated', tvheadend.subsStore, r, - Ext.data.Record.COMMIT); + store.afterEdit(r); + store.fireEvent('updated', store, r, Ext.data.Record.COMMIT); } - }); - - function renderDate(value) { - var dt = new Date(value); - return dt.format('D j M H:i'); } - function renderBw(value, item, store) { - var txt = parseInt(value / 125); - var href = 'javascript:tvheadend.subscription_bw_monitor(' + store.id + ');'; - return '' + txt + ''; + function builder() { + if (subs) + return; + + store = new Ext.data.JsonStore({ + root: 'entries', + totalProperty: 'totalCount', + fields: [ + { name: 'id' }, + { name: 'hostname' }, + { name: 'username' }, + { name: 'title' }, + { name: 'channel' }, + { name: 'service' }, + { name: 'state' }, + { name: 'errors' }, + { name: 'in' }, + { name: 'out' }, + { + name: 'start', + type: 'date', + dateFormat: 'U' /* unix time */ + } + ], + url: 'api/status/subscriptions', + autoLoad: true, + id: 'id' + }); + tvheadend.subsStore = store; + + tvheadend.comet.on('subscriptions', update); + + function renderBw(value, item, record) { + var txt = parseInt(value / 125); + var href = 'javascript:tvheadend.subscription_bw_monitor(' + record.id + ');'; + return '' + txt + ''; + } + + var subsCm = new Ext.grid.ColumnModel([ + { + width: 50, + id: 'hostname', + header: "Hostname", + dataIndex: 'hostname' + }, + { + width: 50, + id: 'username', + header: "Username", + dataIndex: 'username' + }, + { + width: 80, + id: 'title', + header: "Title", + dataIndex: 'title' + }, + { + width: 50, + id: 'channel', + header: "Channel", + dataIndex: 'channel' + }, + { + width: 200, + id: 'service', + header: "Service", + dataIndex: 'service' + }, + { + width: 50, + id: 'start', + header: "Start", + dataIndex: 'start', + renderer: function(v) { + var dt = new Date(v); + return dt.format('D j M H:i'); + } + }, + { + width: 50, + id: 'state', + header: "State", + dataIndex: 'state' + }, + { + width: 50, + id: 'errors', + header: "Errors", + dataIndex: 'errors' + }, + { + width: 50, + id: 'in', + header: "Input (kb/s)", + dataIndex: 'in', + renderer: renderBw, + }, + { + width: 50, + id: 'out', + header: "Output (kb/s)", + dataIndex: 'out', + renderer: renderBw + } + ]); + + subs = new Ext.grid.GridPanel({ + border: false, + loadMask: true, + stripeRows: true, + disableSelection: true, + store: store, + cm: subsCm, + flex: 1, + viewConfig: { + forceFit: true + } + }); + + dpanel.add(subs); + dpanel.doLayout(false, true); + } + + function destroyer() { + if (subs === null || !tvheadend.dynamic) + return; + dpanel.removeAll() + tvheadend.subsStore = null; + store.destroy(); + store = null; + subs = null; } - var subsCm = new Ext.grid.ColumnModel([{ - width: 50, - id: 'hostname', - header: "Hostname", - dataIndex: 'hostname' - }, { - width: 50, - id: 'username', - header: "Username", - dataIndex: 'username' - }, { - width: 80, - id: 'title', - header: "Title", - dataIndex: 'title' - }, { - width: 50, - id: 'channel', - header: "Channel", - dataIndex: 'channel' - }, { - width: 200, - id: 'service', - header: "Service", - dataIndex: 'service' - }, { - width: 50, - id: 'start', - header: "Start", - dataIndex: 'start', - renderer: renderDate - }, { - width: 50, - id: 'state', - header: "State", - dataIndex: 'state' - }, { - width: 50, - id: 'errors', - header: "Errors", - dataIndex: 'errors' - }, { - width: 50, - id: 'in', - header: "Input (kb/s)", - dataIndex: 'in', - renderer: renderBw - }, { - width: 50, - id: 'out', - header: "Output (kb/s)", - dataIndex: 'out', - renderer: renderBw - }]); - - var subs = new Ext.grid.GridPanel({ + var dpanel = new Ext.Panel({ border: false, - loadMask: true, - stripeRows: true, - disableSelection: true, + header: false, + layout: 'fit', title: 'Subscriptions', - iconCls: 'eye', - store: tvheadend.subsStore, - cm: subsCm, - flex: 1, - viewConfig: { - forceFit: true - } + iconCls: 'eye' }); - return subs; + + tvheadend.paneladd(panel, dpanel, index); + tvheadend.panelreg(panel, dpanel, builder, destroyer); }; /** * Streams */ -tvheadend.status_streams = function() { +tvheadend.status_streams = function(panel, index) +{ + var grid = null; + var store = null; - tvheadend.streamStatusStore = new Ext.data.JsonStore({ - root: 'entries', - totalProperty: 'totalCount', - fields: [{ - name: 'uuid' - }, { - name: 'input' - }, { - name: 'username' - }, { - name: 'stream' - }, { - name: 'subs' - }, { - name: 'weight' - }, { - name: 'signal' - }, { - name: 'ber' - }, { - name: 'unc' - }, { - name: 'snr' - }, { - name: 'bps' - }, { - name: 'cc' - }, { - name: 'te' - }, { - name: 'signal_scale' - }, { - name: 'snr_scale' - }, { - name: 'ec_bit' - }, { - name: 'tc_bit' - }, { - name: 'ec_block' - }, { - name: 'tc_block' - } - ], - url: 'api/status/inputs', - autoLoad: true, - id: 'uuid' - }); + function update(m) { + if (m.reload != null) { + store.reload(); + return; + } + if (m.update == null) + return; + var r = store.getById(m.uuid); + if (!r) { + store.reload(); + return; + } + r.data.subs = m.subs; + r.data.weight = m.weight; + r.data.signal = m.signal; + r.data.ber = m.ber; + r.data.unc = m.unc; + r.data.snr = m.snr; + r.data.bps = m.bps; + r.data.cc = m.cc; + r.data.te = m.te; + r.data.signal_scale = m.signal_scale; + r.data.snr_scale = m.snr_scale; + r.data.ec_bit = m.ec_bit; + r.data.tc_bit = m.tc_bit; + r.data.ec_block = m.ec_block; + r.data.tc_block = m.tc_block; - function renderBw(value, item, store) { - var txt = parseInt(value / 1024); - var href = "javascript:tvheadend.stream_bw_monitor('" + store.id + "');"; - return '' + txt + ''; + store.afterEdit(r); + store.fireEvent('updated', store, Ext.data.Record.COMMIT); } - function renderBer(value, item, store) { - if (store.data.tc_bit == 0) - return value; // fallback (driver/vendor dependent ber) + function builder() { + if (grid) + return; - // ber = error_bit_count / total_bit_count - var ber = store.data.ec_bit / store.data.tc_bit; - return ber; + store = new Ext.data.JsonStore({ + root: 'entries', + totalProperty: 'totalCount', + fields: [ + { name: 'uuid' }, + { name: 'input' }, + { name: 'username' }, + { name: 'stream' }, + { name: 'subs' }, + { name: 'weight' }, + { name: 'signal' }, + { name: 'ber' }, + { name: 'unc' }, + { name: 'snr' }, + { name: 'bps' }, + { name: 'cc' }, + { name: 'te' }, + { name: 'signal_scale' }, + { name: 'snr_scale' }, + { name: 'ec_bit' }, + { name: 'tc_bit' }, + { name: 'ec_block' }, + { name: 'tc_block' } + ], + url: 'api/status/inputs', + autoLoad: true, + id: 'uuid' + }); + tvheadend.streamStatusStore = store; + + tvheadend.comet.on('input_status', update); + + function renderBw(value, item, record) { + var txt = parseInt(value / 1024); + var href = "javascript:tvheadend.stream_bw_monitor('" + record.id + "');"; + return '' + txt + ''; + } + + function renderBer(value, item, store) { + if (store.data.tc_bit == 0) + return value; // fallback (driver/vendor dependent ber) + + // ber = error_bit_count / total_bit_count + var ber = store.data.ec_bit / store.data.tc_bit; + return ber; + } + + function renderPer(value, item, store) { + if (value == 0) // value: total_block_count + return 'Unknown'; + + // per = error_block_count / total_block_count + var per = store.data.ec_block / value; + return per; + } + + var cm = new Ext.grid.ColumnModel([ + { + width: 120, + header: "Input", + dataIndex: 'input' + }, + { + width: 100, + header: "Stream", + dataIndex: 'stream' + }, + { + width: 50, + header: "Subs #", + dataIndex: 'subs' + }, + { + width: 50, + header: "Weight", + dataIndex: 'weight' + }, + { + width: 50, + header: "Bandwidth (kb/s)", + dataIndex: 'bps', + renderer: renderBw + }, + { + width: 50, + header: "BER", + dataIndex: 'ber', + renderer: renderBer + }, + { + width: 50, + header: "PER", + dataIndex: 'tc_block', + renderer: renderPer + }, + { + width: 50, + header: "Uncorrected Blocks", + dataIndex: 'unc' + }, + { + width: 50, + header: "Transport Errors", + dataIndex: 'te' + }, + { + width: 50, + header: "Continuity Errors", + dataIndex: 'cc' + } + ]); + + cm.config.push(new Ext.ux.grid.ProgressColumn({ + header: "SNR", + dataIndex: 'snr', + width: 85, + colored: true, + ceiling: 65535, + tvh_renderer: function(v, p, record) { + var scale = record.get('snr_scale'); + if (scale == 1) + return v; + if (scale == 2 && v > 0) { + var snr = v * 0.0001; + return snr.toFixed(1) + " dB"; + } + return 'Unknown'; + }, + destroy: function() { + } + })); + + cm.config.push(new Ext.ux.grid.ProgressColumn({ + header: "Signal Strength", + dataIndex: 'signal', + width: 85, + colored: true, + ceiling: 65535, + tvh_renderer: function(v, p, record) { + var scale = record.get('snr_scale'); + if (scale == 1) + return v; + if (scale == 2 && v > 0) { + var snr = v * 0.0001; + return snr.toFixed(1) + " dBm"; + } + return 'Unknown'; + }, + destroy: function() { + } + })); + + grid = new Ext.grid.GridPanel({ + border: false, + loadMask: true, + stripeRows: true, + disableSelection: true, + store: store, + cm: cm, + flex: 1, + viewConfig: { + forceFit: true + } + }); + + dpanel.add(grid); + dpanel.doLayout(false, true); } - function renderPer(value, item, store) { - if (value == 0) // value: total_block_count - return 'Unknown'; - - // per = error_block_count / total_block_count - var per = store.data.ec_block / value; - return per; + function destroyer() { + if (grid === null || !tvheadend.dynamic) + return; + dpanel.removeAll() + tvheadend.streamStatusStore = null; + store.destroy(); + store = null; + grid = null; } - var cm = new Ext.grid.ColumnModel([{ - width: 120, - header: "Input", - dataIndex: 'input' - }, { - width: 100, - header: "Stream", - dataIndex: 'stream' - }, { - width: 50, - header: "Subs #", - dataIndex: 'subs' - }, { - width: 50, - header: "Weight", - dataIndex: 'weight' - }, { - width: 50, - header: "Bandwidth (kb/s)", - dataIndex: 'bps', - renderer: renderBw - }, { - width: 50, - header: "BER", - dataIndex: 'ber', - renderer: renderBer - }, { - width: 50, - header: "PER", - dataIndex: 'tc_block', - renderer: renderPer - }, { - width: 50, - header: "Uncorrected Blocks", - dataIndex: 'unc' - }, { - width: 50, - header: "Transport Errors", - dataIndex: 'te' - }, { - width: 50, - header: "Continuity Errors", - dataIndex: 'cc' - }]); - - cm.config.push(new Ext.ux.grid.ProgressColumn({ - header: "SNR", - dataIndex: 'snr', - width: 85, - colored: true, - ceiling: 65535, - tvh_renderer: function(v, p, record) { - var scale = record.get('snr_scale'); - if (scale == 1) - return v; - if (scale == 2 && v > 0) { - var snr = v * 0.0001; - return snr.toFixed(1) + " dB"; - } - return 'Unknown'; - } - })); - - cm.config.push(new Ext.ux.grid.ProgressColumn({ - header: "Signal Strength", - dataIndex: 'signal', - width: 85, - colored: true, - ceiling: 65535, - tvh_renderer: function(v, p, record) { - var scale = record.get('snr_scale'); - if (scale == 1) - return v; - if (scale == 2 && v > 0) { - var snr = v * 0.0001; - return snr.toFixed(1) + " dBm"; - } - return 'Unknown'; - } - })); - - tvheadend.comet.on('input_status', function(m) { - if (m.reload != null) - tvheadend.streamStatusStore.reload(); - if (m.update != null) { - var r = tvheadend.streamStatusStore.getById(m.uuid); - if (r) { - r.data.subs = m.subs; - r.data.weight = m.weight; - r.data.signal = m.signal; - r.data.ber = m.ber; - r.data.unc = m.unc; - r.data.snr = m.snr; - r.data.bps = m.bps; - r.data.cc = m.cc; - r.data.te = m.te; - r.data.signal_scale = m.signal_scale; - r.data.snr_scale = m.snr_scale; - r.data.ec_bit = m.ec_bit; - r.data.tc_bit = m.tc_bit; - r.data.ec_block = m.ec_block; - r.data.tc_block = m.tc_block; - - tvheadend.streamStatusStore.afterEdit(r); - tvheadend.streamStatusStore.fireEvent('updated', - tvheadend.streamStatusStore, - r, - Ext.data.Record.COMMIT); - } else { - tvheadend.streamStatusStore.reload(); - } - } - }); - - var panel = new Ext.grid.GridPanel({ + var dpanel = new Ext.Panel({ border: false, - loadMask: true, - stripeRows: true, - disableSelection: true, + header: false, + layout: 'fit', title: 'Stream', - iconCls: 'hardware', - store: tvheadend.streamStatusStore, - cm: cm, - flex: 1, - viewConfig: { - forceFit: true - } + iconCls: 'hardware' }); - return panel; + + tvheadend.paneladd(panel, dpanel, index); + tvheadend.panelreg(panel, dpanel, builder, destroyer); }; /** * */ -tvheadend.status_conns = function() { +tvheadend.status_conns = function(panel, index) { - var store = new Ext.data.JsonStore({ - root: 'entries', - totalProperty: 'totalCount', - fields: [{ - name: 'id' - }, { - name: 'type' - }, { - name: 'peer' - }, { - name: 'user' - }, { - name: 'started', - type: 'date', - dateFormat: 'U' /* unix time */ - }], - url: 'api/status/connections', - autoLoad: true, - id: 'id' - }); + var grid = null; + var store = null; - tvheadend.comet.on('connections', function(m) { + function update(m) { if (m.reload != null) store.reload(); - }); - - function renderDate(value) { - var dt = new Date(value); - return dt.format('Y-m-d H:i:s'); } - var cm = new Ext.grid.ColumnModel([{ - width: 50, - id: 'type', - header: "Type", - dataIndex: 'type' - }, { - width: 50, - id: 'peer', - header: "IP Address", - dataIndex: 'peer' - }, { - width: 50, - id: 'user', - header: "Username", - dataIndex: 'user' - }, { - width: 50, - id: 'started', - header: "Started", - dataIndex: 'started', - renderer: renderDate - }]); + function builder() { + if (grid) + return; - var panel = new Ext.grid.GridPanel({ - border: false, - loadMask: true, - stripeRows: true, - disableSelection: true, - title: 'Connections', - iconCls: 'eye', - store: store, - cm: cm, - flex: 1, - viewConfig: { - forceFit: true + store = new Ext.data.JsonStore({ + root: 'entries', + totalProperty: 'totalCount', + fields: [ + { name: 'id' }, + { name: 'type' }, + { name: 'peer' }, + { name: 'user' }, + { + name: 'started', + type: 'date', + dateFormat: 'U' /* unix time */ + } + ], + url: 'api/status/connections', + autoLoad: true, + id: 'id' + }); + + tvheadend.comet.on('connections', update); + + function renderDate(value) { + var dt = new Date(value); + return dt.format('Y-m-d H:i:s'); } + + var cm = new Ext.grid.ColumnModel([{ + width: 50, + id: 'type', + header: "Type", + dataIndex: 'type' + }, { + width: 50, + id: 'peer', + header: "IP Address", + dataIndex: 'peer' + }, { + width: 50, + id: 'user', + header: "Username", + dataIndex: 'user' + }, { + width: 50, + id: 'started', + header: "Started", + dataIndex: 'started', + renderer: renderDate + }]); + + grid = new Ext.grid.GridPanel({ + border: false, + loadMask: true, + stripeRows: true, + disableSelection: true, + store: store, + cm: cm, + flex: 1, + viewConfig: { + forceFit: true + } + }); + + dpanel.add(grid); + dpanel.doLayout(false, true); + } + + function destroyer() { + if (grid === null || !tvheadend.dynamic) + return; + dpanel.removeAll() + store.destroy(); + store = null; + grid = null; + } + + var dpanel = new Ext.Panel({ + border: false, + header: false, + layout: 'fit', + title: 'Connections', + iconCls: 'eye' }); - return panel; + + tvheadend.paneladd(panel, dpanel, index); + tvheadend.panelreg(panel, dpanel, builder, destroyer); }; tvheadend.status = function() { @@ -437,13 +521,12 @@ tvheadend.status = function() { autoScroll: true, activeTab: 0, iconCls: 'eye', - items: [ - new tvheadend.status_streams, - new tvheadend.status_subs, - new tvheadend.status_conns, - new tvheadend.service_mapper_status - ] + items: [], }); + tvheadend.status_streams(panel); + tvheadend.status_subs(panel); + tvheadend.status_conns(panel); + tvheadend.service_mapper_status(panel); return panel; }; @@ -517,8 +600,9 @@ tvheadend.subscription_bw_monitor = function(id) { var task = { interval: 1000, run: function() { - r = tvheadend.subsStore.getById(id); - if (typeof r === 'undefined') { + var store = tvheadend.subsStore; + var r = store ? store.getById(id) : null; + if (!store || typeof r === 'undefined') { chart.stop(); Ext.TaskMgr.stop(task); return; @@ -607,10 +691,11 @@ tvheadend.stream_bw_monitor = function(id) { }); var task = { - interval: 1000, + interval: 10000, run: function() { - r = tvheadend.streamStatusStore.getById(id); - if (typeof r === 'undefined') { + var store = tvheadend.streamStatusStore; + var r = store ? store.getById(id) : null; + if (!store || typeof r === 'undefined') { chart.stop(); Ext.TaskMgr.stop(task); return; diff --git a/src/webui/static/app/timeshift.js b/src/webui/static/app/timeshift.js index 3e4ed45a..8653b330 100644 --- a/src/webui/static/app/timeshift.js +++ b/src/webui/static/app/timeshift.js @@ -1,4 +1,4 @@ -tvheadend.timeshift = function() { +tvheadend.timeshift = function(panel, index) { /* **************************************************************** * Data @@ -177,5 +177,5 @@ tvheadend.timeshift = function() { }); } - return confpanel; + tvheadend.paneladd(panel, confpanel, index); }; diff --git a/src/webui/static/app/tvadapters.js b/src/webui/static/app/tvadapters.js index 50e32480..1f0aaf50 100644 --- a/src/webui/static/app/tvadapters.js +++ b/src/webui/static/app/tvadapters.js @@ -1,10 +1,13 @@ -tvheadend.tvadapters = function() { - return tvheadend.idnode_tree({ +tvheadend.tvadapters = function(panel, index) { + + tvheadend.idnode_tree(panel, { url: 'api/hardware/tree', title: 'TV adapters', - comet: 'hardware', + tabIndex: index, help: function() { new tvheadend.help('TV adapters', 'config_tvadapters.html'); } }); + + return panel; }; diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js index d89c88aa..2a9e40be 100644 --- a/src/webui/static/app/tvheadend.js +++ b/src/webui/static/app/tvheadend.js @@ -1,9 +1,8 @@ +tvheadend.dynamic = true; tvheadend.accessupdate = null; tvheadend.capabilties = null; -tvheadend.conf_chepg = null; -tvheadend.conf_dvbin = null; -tvheadend.conf_tsdvr = null; -tvheadend.conf_csa = null; +tvheadend.dvrpanel = null; +tvheadend.confpanel = null; /* State Provider */ Ext.state.Manager.setProvider(new Ext.state.CookieProvider({ @@ -40,6 +39,100 @@ tvheadend.help = function(title, pagename) { }); }; +tvheadend.paneladd = function(dst, add, idx) { + if (idx != null) + dst.insert(idx, add); + else + dst.add(add); +}; + +tvheadend.panelreg = function(tabpanel, panel, builder, destroyer) { + /* the 'activate' event does not work in ExtJS 3.4 */ + tabpanel.on('beforetabchange', function(tp, p) { + if (p == panel) + builder(); + }); + panel.on('deactivate', destroyer); +} + +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.AjaxConfirm = function(conf) { + Ext.MessageBox.confirm( + conf.title || 'Message', + conf.question || 'Do you really want to delete the selection?', + function (btn) { + if (btn == 'yes') + tvheadend.Ajax(conf); + } + ); +}; + +tvheadend.loading = function(on) { + if (on) + Ext.getBody().mask('Loading... Please, wait...', 'loading'); + else + Ext.getBody().unmask(); +}; + +/* + * Any Match option in ComboBox queries + * This query is identical as in extjs-all.js + * except one + */ +tvheadend.doQueryAnyMatch = function(q, forceAll) { + q = Ext.isEmpty(q) ? '' : q; + var qe = { + query: q, + forceAll: forceAll, + combo: this, + cancel:false + }; + + if (this.fireEvent('beforequery', qe) === false || qe.cancel) + return false; + + q = qe.query; + forceAll = qe.forceAll; + if (forceAll === true || (q.length >= this.minChars)) { + if (this.lastQuery !== q) { + this.lastQuery = q; + if (this.mode == 'local') { + this.selectedIndex = -1; + if (forceAll) { + this.store.clearFilter(); + } else { + /* supply the anyMatch option (last param) */ + this.store.filter(this.displayField, q, true); + } + this.onLoad(); + } else { + this.store.baseParams[this.queryParam] = q; + this.store.load({ params: this.getParams(q) }); + this.expand(); + } + } else { + this.selectedIndex = -1; + this.onLoad(); + } + } +} + /* * General capabilities */ @@ -218,109 +311,119 @@ function accessUpdate(o) { if (!tvheadend.capabilities) return; + tvheadend.rootTabPanel.setLogin(o.username); + + if (tvheadend.autorecButton) + tvheadend.autorecButton.setDisabled(o.dvr != true); + if (o.dvr == true && tvheadend.dvrpanel == null) { - tvheadend.dvrpanel = new tvheadend.dvr; + tvheadend.dvrpanel = tvheadend.dvr(); tvheadend.rootTabPanel.add(tvheadend.dvrpanel); } if (o.admin == true && tvheadend.confpanel == null) { - var tabs1 = [ - new tvheadend.miscconf, - new tvheadend.acleditor - ]; - var tabs2; - /* DVB inputs */ - tabs2 = []; - if (tvheadend.capabilities.indexOf('linuxdvb') !== -1 || - tvheadend.capabilities.indexOf('satip_client') !== -1 || - tvheadend.capabilities.indexOf('v4l') !== -1) { - tabs2.push(new tvheadend.tvadapters); - } - /* - tabs2.push(new tvheadend.iptv); - */ - tvheadend.conf_dvbin = new Ext.TabPanel({ + var cp = new Ext.TabPanel({ + activeTab: 0, + autoScroll: true, + title: 'Configuration', + iconCls: 'wrench', + items: [] + }); + + tvheadend.miscconf(cp); + + tvheadend.acleditor(cp); + + /* DVB inputs, networks, muxes, services */ + var dvbin = new Ext.TabPanel({ activeTab: 0, autoScroll: true, title: 'DVB Inputs', iconCls: 'hardware', - items: tabs2 + items: [] }); - tvheadend.networks(tvheadend.conf_dvbin); - tvheadend.muxes(tvheadend.conf_dvbin); - tvheadend.services(tvheadend.conf_dvbin); - tvheadend.mux_sched(tvheadend.conf_dvbin); - tabs1.push(tvheadend.conf_dvbin); + + var idx = 0; + + if (tvheadend.capabilities.indexOf('linuxdvb') !== -1 || + tvheadend.capabilities.indexOf('satip_client') !== -1 || + tvheadend.capabilities.indexOf('v4l') !== -1) + tvheadend.tvadapters(dvbin); + tvheadend.networks(dvbin); + tvheadend.muxes(dvbin); + tvheadend.services(dvbin); + tvheadend.mux_sched(dvbin); + + cp.add(dvbin); /* Channel / EPG */ - tvheadend.conf_chepg = new Ext.TabPanel({ + var chepg = new Ext.TabPanel({ activeTab: 0, autoScroll: true, title: 'Channel / EPG', iconCls: 'television', items: [] }); - tvheadend.channel_tab(tvheadend.conf_chepg, 0); - tvheadend.cteditor(tvheadend.conf_chepg, 1); - tvheadend.conf_chepg.insert(2, new tvheadend.epggrab); - tabs1.push(tvheadend.conf_chepg); + tvheadend.channel_tab(chepg); + tvheadend.cteditor(chepg); + tvheadend.epggrab(chepg); + + cp.add(chepg); /* DVR / Timeshift */ - tabs2 = [new tvheadend.dvrsettings]; - if (tvheadend.capabilities.indexOf('timeshift') !== -1) { - tabs2.push(new tvheadend.timeshift); - } - tvheadend.conf_tsdvr = new Ext.TabPanel({ + var tsdvr = new Ext.TabPanel({ activeTab: 0, autoScroll: true, title: 'Recording', iconCls: 'drive', - items: tabs2 + items: [] }); - tabs1.push(tvheadend.conf_tsdvr); + tvheadend.dvr_settings(tsdvr); + if (tvheadend.capabilities.indexOf('timeshift') !== -1) + tvheadend.timeshift(tsdvr); + + cp.add(tsdvr); /* CSA */ - tabs2 = []; - if (tvheadend.capabilities.indexOf('cwc') !== -1) - tabs2.push(new tvheadend.cwceditor); - if (tvheadend.capabilities.indexOf('capmt') !== -1) - tabs2.push(new tvheadend.capmteditor); - if (tabs2.length > 0) { - tvheadend.conf_csa = new Ext.TabPanel({ + if (tvheadend.capabilities.indexOf('cwc') !== -1 || + tvheadend.capabilities.indexOf('capmt') !== -1) { + + var csa = new Ext.TabPanel({ activeTab: 0, autoScroll: true, title: 'CSA', iconCls: 'key', - items: tabs2 + items: [] }); - tabs1.push(tvheadend.conf_csa); + + if (tvheadend.capabilities.indexOf('cwc') !== -1) + tvheadend.cwceditor(csa); + if (tvheadend.capabilities.indexOf('capmt') !== -1) + tvheadend.capmteditor(csa); + + cp.add(csa); } /* Stream Config */ - tvheadend.conf_stream = new Ext.TabPanel({ + var stream = new Ext.TabPanel({ activeTab: 0, autoScroll: true, title: 'Stream', iconCls: 'stream_config', items: [] }); - tvheadend.esfilter_tab(tvheadend.conf_stream); - tabs1.push(tvheadend.conf_stream); + tvheadend.esfilter_tab(stream); + + cp.add(stream); /* Debug */ - tabs1.push(new tvheadend.tvhlog); + tvheadend.tvhlog(cp); - tvheadend.confpanel = new Ext.TabPanel({ - activeTab: 0, - autoScroll: true, - title: 'Configuration', - iconCls: 'wrench', - items: tabs1 - }); - - tvheadend.rootTabPanel.add(tvheadend.confpanel); - tvheadend.confpanel.doLayout(); + /* Finish */ + tvheadend.rootTabPanel.add(cp); + tvheadend.confpanel = cp; + cp.doLayout(); } if (o.admin == true && tvheadend.statuspanel == null) { @@ -365,6 +468,86 @@ tvheadend.log = function(msg, style) { e.scrollIntoView('systemlog'); }; +/** + * + */ +tvheadend.RootTabPanel = Ext.extend(Ext.TabPanel, { + + onRender: function(ct, position) { + tvheadend.RootTabPanel.superclass.onRender.call(this, ct, position); + + /* Create login components */ + var before = this.strip.dom.childNodes[this.strip.dom.childNodes.length-1]; + + if (!this.loginTpl) { + var tt = new Ext.Template( + '' + ); + tt.disableFormats = true; + tt.compile(); + tvheadend.RootTabPanel.prototype.loginTpl = tt; + } + var item = new Ext.Component(); + var p = this.getTemplateArgs(item); + var before = this.strip.dom.childNodes[this.strip.dom.childNodes.length-1]; + item.tabEl = this.loginTpl.insertBefore(before, p); + this.loginItem = item; + + if (!this.loginCmdTpl) { + var tt = new Ext.Template( + '' + ); + tt.disableFormats = true; + tt.compile(); + tvheadend.RootTabPanel.prototype.loginCmdTpl = tt; + } + var item = new Ext.Component(); + var p = this.getTemplateArgs(item); + var el = this.loginCmdTpl.insertBefore(before, p); + item.tabEl = Ext.get(el); + item.tabEl.select('a').on('click', this.onLoginCmdClicked, this, {preventDefault: true}); + this.loginCmdItem = item; + + this.on('beforetabchange', function(tp, p) { + if (p == this.loginItem || p == this.loginCmdItem) + return false; + }); + + this.setLogin(''); + }, + + getComponent: function(comp) { + if (comp === this.loginItem.id || comp == this.loginItem) + return this.loginItem; + if (comp === this.loginCmdItem.id || comp == this.loginCmdItem) + return this.loginCmdItem; + return tvheadend.RootTabPanel.superclass.getComponent.call(this, comp); + }, + + setLogin: function(login) { + this.login = login; + if (login) { + text = 'Logged in as ' + login + ''; + cmd = '(logout)'; + } else { + text = 'No verified access'; + cmd = '(login)'; + } + var el = this.loginItem.tabEl; + var fly = Ext.fly(this.loginItem.tabEl); + var t = fly.child('span.x-tab-strip-login', true); + Ext.fly(this.loginItem.tabEl).child('span.x-tab-strip-login', true).innerHTML = text; + Ext.fly(this.loginCmdItem.tabEl).child('span.x-tab-strip-login-cmd', true).innerHTML = cmd; + }, + + onLoginCmdClicked: function(e) { + window.location.href = this.login ? 'logout' : 'login'; + } + +}); + /** * */ @@ -386,10 +569,10 @@ tvheadend.app = function() { html: '' }); - tvheadend.rootTabPanel = new Ext.TabPanel({ + tvheadend.rootTabPanel = new tvheadend.RootTabPanel({ region: 'center', activeTab: 0, - items: [new tvheadend.epg] + items: [tvheadend.epg()] }); var viewport = new Ext.Viewport({ @@ -439,4 +622,3 @@ tvheadend.app = function() { }; }(); // end of app - diff --git a/src/webui/static/app/tvhlog.js b/src/webui/static/app/tvhlog.js index 8062b7ac..665cd710 100644 --- a/src/webui/static/app/tvhlog.js +++ b/src/webui/static/app/tvhlog.js @@ -1,4 +1,4 @@ -tvheadend.tvhlog = function() { +tvheadend.tvhlog = function(panel, index) { /* * Basic Config */ @@ -115,5 +115,5 @@ tvheadend.tvhlog = function() { }); } - return confpanel; + tvheadend.paneladd(panel, confpanel, index); }; diff --git a/src/webui/static/app/v4l.js b/src/webui/static/app/v4l.js deleted file mode 100644 index ab355e10..00000000 --- a/src/webui/static/app/v4l.js +++ /dev/null @@ -1,321 +0,0 @@ -/** - * V4L adapter details - */ -tvheadend.v4l_adapter_general = function(adapterData) { - - adapterId = adapterData.identifier; - - /* Conf panel */ - - var confreader = new Ext.data.JsonReader({ - root: 'v4ladapters' - }, ['name', 'logging']); - - function saveConfForm() { - confform.getForm().submit({ - url: 'v4l/adapter/' + adapterId, - params: { - 'op': 'save' - }, - waitMsg: 'Saving Data...' - }); - } - - var items = [{ - fieldLabel: 'Adapter name', - name: 'name', - width: 250 - }, new Ext.form.Checkbox({ - fieldLabel: 'Detailed logging', - name: 'logging' - })]; - - var confform = new Ext.FormPanel({ - title: 'Adapter configuration', - columnWidth: .40, - frame: true, - border: true, - disabled: true, - style: 'margin:10px', - bodyStyle: 'padding:5px', - labelAlign: 'right', - labelWidth: 110, - waitMsgTarget: true, - reader: confreader, - defaultType: 'textfield', - items: items, - buttons: [{ - text: 'Save', - handler: saveConfForm - }] - }); - - confform.getForm().load({ - url: 'v4l/adapter/' + adapterId, - params: { - 'op': 'load' - }, - success: function(form, action) { - confform.enable(); - } - }); - - /** - * Information / capabilities panel - */ - - var infoTemplate = new Ext.XTemplate( - '

Hardware

' - + '

Device path:

{path}' + '

Device name:

{devicename}' - + '

Status

' - + '

Currently tuned to:

{currentMux} '); - - var infoPanel = new Ext.Panel({ - title: 'Information and capabilities', - columnWidth: .35, - frame: true, - border: true, - style: 'margin:10px', - bodyStyle: 'padding:5px', - html: infoTemplate.applyTemplate(adapterData) - }); - - /** - * Main adapter panel - */ - var panel = new Ext.Panel({ - title: 'General', - layout: 'column', - items: [confform, infoPanel] - }); - - /** - * Subscribe and react on updates for this adapter - */ - tvheadend.tvAdapterStore.on('update', function(s, r, o) { - if (r.data.identifier !== adapterId) - return; - infoTemplate.overwrite(infoPanel.body, r.data); - }); - - return panel; -}; - -/** - * V4L service grid - */ -tvheadend.v4l_services = function(adapterId) { - - var fm = Ext.form; - - var enabledColumn = new Ext.grid.CheckColumn({ - header: "Enabled", - dataIndex: 'enabled', - width: 45 - }); - - var cm = new Ext.grid.ColumnModel({ - defaultSortable: true, - columns: [ - enabledColumn, { - header: "Channel name", - dataIndex: 'channelname', - width: 150, - renderer: function(value, metadata, record, row, col, store) { - return value ? value : 'Unmapped'; - }, - editor: new fm.ComboBox({ - store: tvheadend.channels, - allowBlank: true, - typeAhead: true, - minChars: 2, - lazyRender: true, - triggerAction: 'all', - mode: 'local', - displayField: 'name' - }) - }, { - header: "Frequency", - dataIndex: 'frequency', - width: 60, - editor: new fm.NumberField({ - minValue: 10000, - maxValue: 1000000000 - }) - }]}); - - var rec = Ext.data.Record.create(['id', 'enabled', 'channelname', - 'frequency']); - - var store = new Ext.data.JsonStore({ - root: 'entries', - fields: rec, - url: "v4l/services/" + adapterId, - autoLoad: true, - id: 'id', - baseParams: { - op: "get" - }, - listeners: { - 'update': function(s, r, o) { - d = s.getModifiedRecords().length === 0; - saveBtn.setDisabled(d); - rejectBtn.setDisabled(d); - } - } - }); - - function addRecord() { - Ext.Ajax.request({ - url: "v4l/services/" + adapterId, - params: { - op: "create" - }, - failure: function(response, options) { - Ext.MessageBox.alert('Server Error', - 'Unable to generate new record'); - }, - success: function(response, options) { - var responseData = Ext.util.JSON.decode(response.responseText); - var p = new rec(responseData, responseData.id); - grid.stopEditing(); - store.insert(0, p); - grid.startEditing(0, 0); - } - }); - }; - - function delSelected() { - var selectedKeys = grid.selModel.selections.keys; - if (selectedKeys.length > 0) { - Ext.MessageBox.confirm('Message', - 'Do you really want to delete selection?', deleteRecord); - } - else { - Ext.MessageBox.alert('Message', - 'Please select at least one item to delete'); - } - }; - - function deleteRecord(btn) { - if (btn === 'yes') { - var selectedKeys = grid.selModel.selections.keys; - - Ext.Ajax.request({ - url: "v4l/services/" + adapterId, - params: { - op: "delete", - entries: Ext.encode(selectedKeys) - }, - failure: function(response, options) { - Ext.MessageBox.alert('Server Error', 'Unable to delete'); - }, - success: function(response, options) { - store.reload(); - } - }); - } - } - - function saveChanges() { - 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].id = mr[x].id; - } - - Ext.Ajax.request({ - url: "v4l/services/" + adapterId, - params: { - op: "update", - entries: Ext.encode(out) - }, - success: function(response, options) { - store.commitChanges(); - }, - failure: function(response, options) { - Ext.MessageBox.alert('Message', response.statusText); - } - }); - } - - var delButton = new Ext.Toolbar.Button({ - tooltip: 'Delete one or more selected rows', - iconCls: 'remove', - text: 'Delete selected services', - handler: delSelected, - disabled: true - }); - - var saveBtn = new Ext.Toolbar.Button({ - tooltip: 'Save any changes made (Changed cells have red borders).', - iconCls: 'save', - text: "Save changes", - handler: saveChanges, - disabled: true - }); - - var rejectBtn = new Ext.Toolbar.Button({ - tooltip: 'Revert any changes made (Changed cells have red borders).', - iconCls: 'undo', - text: "Revert changes", - handler: function() { - store.rejectChanges(); - }, - disabled: true - }); - - var selModel = new Ext.grid.RowSelectionModel({ - singleSelect: false - }); - - var grid = new Ext.grid.EditorGridPanel({ - stripeRows: true, - title: 'Services', - plugins: [enabledColumn], - store: store, - clicksToEdit: 2, - cm: cm, - viewConfig: { - forceFit: true - }, - selModel: selModel, - tbar: [ - { - tooltip: 'Create a new entry on the server. ' - + 'The new entry is initially disabled so it must be enabled ' - + 'before it start taking effect.', - iconCls: 'add', - text: 'Add service', - handler: addRecord - }, '-', delButton, '-', saveBtn, rejectBtn] - }); - - store.on('update', function(s, r, o) { - d = s.getModifiedRecords().length === 0; - saveBtn.setDisabled(d); - rejectBtn.setDisabled(d); - }); - - selModel.on('selectionchange', function(self) { - delButton.setDisabled(self.getCount() === 0); - }); - - return grid; -}; - -/** - * - */ -tvheadend.v4l_adapter = function(data) { - var panel = new Ext.TabPanel({ - border: false, - activeTab: 0, - autoScroll: true, - items: [new tvheadend.v4l_adapter_general(data), - new tvheadend.v4l_services(data.identifier)] - }); - return panel; -}; diff --git a/src/webui/static/icons/cancel.png b/src/webui/static/icons/cancel.png new file mode 120000 index 00000000..79f09a35 --- /dev/null +++ b/src/webui/static/icons/cancel.png @@ -0,0 +1 @@ +../../../../vendor/famfamsilk/cancel.png \ No newline at end of file diff --git a/src/webui/webui.c b/src/webui/webui.c index 9c96cac8..e6c2c0b5 100644 --- a/src/webui/webui.c +++ b/src/webui/webui.c @@ -163,6 +163,32 @@ page_root2(http_connection_t *hc, const char *remain, void *opaque) return 0; } +static int +page_login(http_connection_t *hc, const char *remain, void *opaque) +{ + if (hc->hc_access != NULL && + hc->hc_access->aa_username != NULL && + hc->hc_access->aa_username != '\0') { + http_redirect(hc, "/", &hc->hc_req_args); + return 0; + } else { + return HTTP_STATUS_UNAUTHORIZED; + } +} + +static int +page_logout(http_connection_t *hc, const char *remain, void *opaque) +{ + if (hc->hc_access == NULL || + hc->hc_access->aa_username == NULL || + hc->hc_access->aa_username == '\0') { + http_redirect(hc, "/", &hc->hc_req_args); + return 0; + } else { + return HTTP_STATUS_UNAUTHORIZED; + } +} + /** * Static download of a file from the filesystem */ @@ -372,7 +398,7 @@ http_channel_playlist(http_connection_t *hc, channel_t *channel) const char *host; muxer_container_type_t mc; - if (http_access_verify_channel(hc, ACCESS_STREAMING, channel)) + if (http_access_verify_channel(hc, ACCESS_STREAMING, channel, 1)) return HTTP_STATUS_UNAUTHORIZED; mc = muxer_container_txt2type(http_arg_get(&hc->hc_req_args, "mux")); @@ -438,7 +464,7 @@ http_tag_playlist(http_connection_t *hc, channel_tag_t *tag) htsbuf_qprintf(hq, "#EXTM3U\n"); LIST_FOREACH(ctm, &tag->ct_ctms, ctm_tag_link) { - if (http_access_verify_channel(hc, ACCESS_STREAMING, ctm->ctm_channel)) + if (http_access_verify_channel(hc, ACCESS_STREAMING, ctm->ctm_channel, 0)) continue; snprintf(buf, sizeof(buf), "/stream/channelid/%d", channel_get_id(ctm->ctm_channel)); htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", channel_get_name(ctm->ctm_channel)); @@ -537,7 +563,7 @@ http_channel_list_playlist(http_connection_t *hc) for (idx = 0; idx < count; idx++) { ch = chlist[idx]; - if (http_access_verify_channel(hc, ACCESS_STREAMING, ch)) + if (http_access_verify_channel(hc, ACCESS_STREAMING, ch, 0)) continue; snprintf(buf, sizeof(buf), "/stream/channelid/%d", channel_get_id(ch)); @@ -566,7 +592,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; @@ -582,9 +608,10 @@ http_dvr_list_playlist(http_connection_t *hc) continue; if (de->de_channel && - http_access_verify_channel(hc, ACCESS_RECORDER, de->de_channel)) + http_access_verify_channel(hc, ACCESS_RECORDER, de->de_channel, 0)) 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 +620,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 +642,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 +662,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); @@ -856,7 +885,7 @@ http_stream_channel(http_connection_t *hc, channel_t *ch, int weight) const char *name; char addrbuf[50]; - if (http_access_verify_channel(hc, ACCESS_STREAMING, ch)) + if (http_access_verify_channel(hc, ACCESS_STREAMING, ch, 1)) return HTTP_STATUS_UNAUTHORIZED; cfg = dvr_config_find_by_name_default(""); @@ -1122,7 +1151,9 @@ page_dvrfile(http_connection_t *hc, const char *remain, void *opaque) pthread_mutex_lock(&global_lock); - de = dvr_entry_find_by_id(atoi(remain)); + de = dvr_entry_find_by_uuid(remain); + if (de == NULL) + de = dvr_entry_find_by_id(atoi(remain)); if(de == NULL || de->de_filename == NULL) { pthread_mutex_unlock(&global_lock); return 404; @@ -1298,6 +1329,8 @@ webui_init(int xspf) http_path_add("", NULL, page_root2, ACCESS_WEB_INTERFACE); http_path_add("/", NULL, page_root, ACCESS_WEB_INTERFACE); + http_path_add("/login", NULL, page_login, ACCESS_WEB_INTERFACE); + http_path_add("/logout", NULL, page_logout, ACCESS_WEB_INTERFACE); http_path_add_modify("/play", NULL, page_play, ACCESS_WEB_INTERFACE, page_play_path_modify); http_path_add("/dvrfile", NULL, page_dvrfile, ACCESS_WEB_INTERFACE); diff --git a/src/webui/webui.h b/src/webui/webui.h index a1bee1cd..819bb6a6 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 6db06194..89521443 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;