diff --git a/Makefile b/Makefile index 632fdf00..b75b9936 100644 --- a/Makefile +++ b/Makefile @@ -178,6 +178,7 @@ SRCS += src/plumbing/tsfix.c \ SRCS += src/dvr/dvr_db.c \ src/dvr/dvr_rec.c \ src/dvr/dvr_autorec.c \ + src/dvr/dvr_timerec.c \ src/dvr/dvr_cutpoints.c \ SRCS += src/webui/webui.c \ diff --git a/docs/html/config_dvrtime.html b/docs/html/config_dvrtime.html new file mode 100644 index 00000000..6dc9af74 --- /dev/null +++ b/docs/html/config_dvrtime.html @@ -0,0 +1,10 @@ +
+ +

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

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

diff --git a/src/api/api_dvr.c b/src/api/api_dvr.c index 34b6ee36..e1e781bf 100644 --- a/src/api/api_dvr.c +++ b/src/api/api_dvr.c @@ -330,6 +330,40 @@ api_dvr_autorec_create_by_series return !count ? EINVAL : 0; } +static void +api_dvr_timerec_grid + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) +{ + dvr_timerec_entry_t *dte; + + TAILQ_FOREACH(dte, &timerec_entries, dte_link) + idnode_set_add(ins, (idnode_t*)dte, &conf->filter); +} + +static int +api_dvr_timerec_create + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + htsmsg_t *conf; + dvr_timerec_entry_t *dte; + + 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); + dte = dvr_timerec_create(NULL, conf); + if (dte) { + dvr_timerec_save(dte); + dvr_timerec_check(dte); + } + pthread_mutex_unlock(&global_lock); + + return 0; +} + void api_dvr_init ( void ) { static api_hook_t ah[] = { @@ -351,6 +385,10 @@ void api_dvr_init ( void ) { "dvr/autorec/create", ACCESS_RECORDER, api_dvr_autorec_create, NULL }, { "dvr/autorec/create_by_series", ACCESS_RECORDER, api_dvr_autorec_create_by_series, NULL }, + { "dvr/timerec/class", ACCESS_RECORDER, api_idnode_class, (void*)&dvr_timerec_entry_class }, + { "dvr/timerec/grid", ACCESS_RECORDER, api_idnode_grid, api_dvr_timerec_grid }, + { "dvr/timerec/create", ACCESS_RECORDER, api_dvr_timerec_create, NULL }, + { NULL }, }; diff --git a/src/channels.c b/src/channels.c index 4c5eb837..886b26c0 100644 --- a/src/channels.c +++ b/src/channels.c @@ -197,6 +197,17 @@ channel_class_get_title ( idnode_t *self ) return channel_get_name((channel_t*)self); } +/* exported for others */ +htsmsg_t * +channel_class_get_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 const void * channel_class_get_name ( void *p ) { @@ -531,6 +542,12 @@ channel_create0 { lock_assert(&global_lock); + LIST_INIT(&ch->ch_services); + LIST_INIT(&ch->ch_subscriptions); + LIST_INIT(&ch->ch_epggrab); + LIST_INIT(&ch->ch_autorecs); + LIST_INIT(&ch->ch_timerecs); + if (idnode_insert(&ch->ch_id, uuid, idc, IDNODE_SHORT_UUID)) { if (uuid) tvherror("channel", "invalid uuid '%s'", uuid); @@ -578,6 +595,7 @@ channel_delete ( channel_t *ch, int delconf ) /* DVR */ autorec_destroy_by_channel(ch, delconf); + timerec_destroy_by_channel(ch, delconf); dvr_destroy_by_channel(ch, delconf); /* Services */ diff --git a/src/channels.h b/src/channels.h index 32742d33..23755eb6 100644 --- a/src/channels.h +++ b/src/channels.h @@ -71,6 +71,7 @@ typedef struct channel int ch_dvr_extra_time_post; struct dvr_entry_list ch_dvrs; struct dvr_autorec_entry_list ch_autorecs; + struct dvr_timerec_entry_list ch_timerecs; } channel_t; @@ -152,6 +153,8 @@ channel_t *channel_find_by_number(int no); #define channel_find channel_find_by_uuid +htsmsg_t * channel_class_get_list(void *o); + int channel_set_tags_by_list ( channel_t *ch, htsmsg_t *tags ); int channel_set_services_by_list ( channel_t *ch, htsmsg_t *svcs ); diff --git a/src/dvr/dvr.h b/src/dvr/dvr.h index f4dba1a7..b004e136 100644 --- a/src/dvr/dvr.h +++ b/src/dvr/dvr.h @@ -194,6 +194,11 @@ typedef struct dvr_entry { LIST_ENTRY(dvr_entry) de_autorec_link; struct dvr_autorec_entry *de_autorec; + /** + * Timerec linkage + */ + struct dvr_timerec_entry *de_timerec; + /** * Fields for recording */ @@ -251,7 +256,7 @@ typedef struct dvr_autorec_entry { channel_tag_t *dae_channel_tag; LIST_ENTRY(dvr_autorec_entry) dae_channel_tag_link; - dvr_prio_t dae_pri; + int dae_pri; struct dvr_entry_list dae_spawns; @@ -272,6 +277,42 @@ TAILQ_HEAD(dvr_autorec_entry_queue, dvr_autorec_entry); extern struct dvr_autorec_entry_queue autorec_entries; +/** + * Timerec entry + */ +typedef struct dvr_timerec_entry { + idnode_t dte_id; + + TAILQ_ENTRY(dvr_timerec_entry) dte_link; + + char *dte_name; + char *dte_config_name; + + int dte_enabled; + char *dte_creator; + char *dte_comment; + + char *dte_title; + + int dte_start; /* Minutes from midnight */ + int dte_stop; /* Minutes from midnight */ + + uint32_t dte_weekdays; + + channel_t *dte_channel; + LIST_ENTRY(dvr_timerec_entry) dte_channel_link; + + int dte_pri; + + dvr_entry_t *dte_spawn; + + int dte_retention; +} dvr_timerec_entry_t; + +TAILQ_HEAD(dvr_timerec_entry_queue, dvr_timerec_entry); + +extern struct dvr_timerec_entry_queue timerec_entries; + /** * */ @@ -279,6 +320,7 @@ 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; +extern const idclass_t dvr_timerec_entry_class; /** * Prototypes @@ -366,12 +408,6 @@ void dvr_config_init(void); void dvr_done(void); -void dvr_autorec_init(void); - -void dvr_autorec_done(void); - -void dvr_autorec_update(void); - void dvr_destroy_by_channel(channel_t *ch, int delconf); void dvr_rec_subscribe(dvr_entry_t *de); @@ -436,6 +472,16 @@ int dvr_sort_start_ascending(const void *A, const void *B); dvr_autorec_entry_t * dvr_autorec_create(const char *uuid, htsmsg_t *conf); +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_timerec_entry_t *tae, + dvr_prio_t pri, int retention); + dvr_autorec_entry_t* dvr_autorec_create_htsp(const char *dvr_config_name, const char *title, channel_t *ch, uint32_t aroundTime, uint32_t days, @@ -458,18 +504,53 @@ dvr_autorec_find_by_uuid(const char *uuid) { return (dvr_autorec_entry_t*)idnode_find(uuid, &dvr_autorec_entry_class); } +htsmsg_t * dvr_autorec_entry_class_time_list(void *o, const char *null); +htsmsg_t * dvr_autorec_entry_class_weekdays_list ( void *o ); +char * dvr_autorec_entry_class_weekdays_rend(uint32_t weekdays); + void dvr_autorec_check_event(epg_broadcast_t *e); void dvr_autorec_check_brand(epg_brand_t *b); void dvr_autorec_check_season(epg_season_t *s); void dvr_autorec_check_serieslink(epg_serieslink_t *s); - void autorec_destroy_by_channel(channel_t *ch, int delconf); void autorec_destroy_by_channel_tag(channel_tag_t *ct, int delconf); void autorec_destroy_by_id(const char *id, int delconf); +void dvr_autorec_init(void); + +void dvr_autorec_done(void); + +void dvr_autorec_update(void); + +/** + * + */ + +dvr_timerec_entry_t * +dvr_timerec_create(const char *uuid, htsmsg_t *conf); + +static inline dvr_timerec_entry_t * +dvr_timerec_find_by_uuid(const char *uuid) + { return (dvr_timerec_entry_t*)idnode_find(uuid, &dvr_timerec_entry_class); } + + +void dvr_timerec_save(dvr_timerec_entry_t *dae); + +void dvr_timerec_check(dvr_timerec_entry_t *dae); + +void timerec_destroy_by_channel(channel_t *ch, int delconf); + +void timerec_destroy_by_id(const char *id, int delconf); + +void dvr_timerec_init(void); + +void dvr_timerec_done(void); + +void dvr_timerec_update(void); + /** * */ diff --git a/src/dvr/dvr_autorec.c b/src/dvr/dvr_autorec.c index d2f7078a..7c7a9ed3 100644 --- a/src/dvr/dvr_autorec.c +++ b/src/dvr/dvr_autorec.c @@ -385,16 +385,6 @@ dvr_autorec_entry_class_channel_get(void *o) return &ret; } -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) { @@ -477,14 +467,6 @@ dvr_autorec_entry_class_start_set(void *o, const void *v) 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) { @@ -505,22 +487,13 @@ dvr_autorec_entry_class_start_get(void *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) +htsmsg_t * +dvr_autorec_entry_class_time_list(void *o, const char *null) { int i; htsmsg_t *l = htsmsg_create_list(); char buf[16]; - htsmsg_add_str(l, NULL, "Any"); + htsmsg_add_str(l, NULL, null); for (i = 0; i < 24*60; i += 10) { snprintf(buf, sizeof(buf), "%02d:%02d", i / 60, (i % 60)); htsmsg_add_str(l, NULL, buf); @@ -528,6 +501,12 @@ dvr_autorec_entry_class_time_list(void *o) return l; } +static htsmsg_t * +dvr_autorec_entry_class_time_list_(void *o) +{ + return dvr_autorec_entry_class_time_list(o, "Any"); +} + static htsmsg_t * dvr_autorec_entry_class_minduration_list(void *o) { @@ -597,27 +576,26 @@ static const struct strtab dvr_autorec_entry_class_weekdays_tab[] = { { "Sun", 7 }, }; -static htsmsg_t * +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) +char * +dvr_autorec_entry_class_weekdays_rend(uint32_t weekdays) { - dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; char buf[32]; size_t l; int i; - if (dae->dae_weekdays == 0x7f) + if (weekdays == 0x7f) strcpy(buf + 1, "All days"); - else if (dae->dae_weekdays == 0) + else if (weekdays == 0) strcpy(buf + 1, "No days"); else { buf[0] = '\0'; for (i = 0; i < 7; i++) - if (dae->dae_weekdays & (1 << i)) { + if (weekdays & (1 << i)) { l = strlen(buf); snprintf(buf + l, sizeof(buf) - l, ",%s", val2str(i + 1, dvr_autorec_entry_class_weekdays_tab)); @@ -626,6 +604,13 @@ dvr_autorec_entry_class_weekdays_rend(void *o) return strdup(buf + 1); } +static char * +dvr_autorec_entry_class_weekdays_rend_(void *o) +{ + dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o; + return dvr_autorec_entry_class_weekdays_rend(dae->dae_weekdays); +} + static int dvr_autorec_entry_class_brand_set(void *o, const void *v) { @@ -776,7 +761,7 @@ const idclass_t dvr_autorec_entry_class = { .name = "Channel", .set = dvr_autorec_entry_class_channel_set, .get = dvr_autorec_entry_class_channel_get, - .list = dvr_autorec_entry_class_channel_list, + .list = channel_class_get_list, }, { .type = PT_STR, @@ -792,7 +777,7 @@ const idclass_t dvr_autorec_entry_class = { .name = "Starting Around", .set = dvr_autorec_entry_class_start_set, .get = dvr_autorec_entry_class_start_get, - .list = dvr_autorec_entry_class_time_list, + .list = dvr_autorec_entry_class_time_list_, }, { .type = PT_TIME, @@ -816,7 +801,8 @@ const idclass_t dvr_autorec_entry_class = { .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, + .rend = dvr_autorec_entry_class_weekdays_rend_, + .def.u32 = 0x7f }, { .type = PT_INT, diff --git a/src/dvr/dvr_db.c b/src/dvr/dvr_db.c index 17ec5b31..972992cf 100644 --- a/src/dvr/dvr_db.c +++ b/src/dvr/dvr_db.c @@ -74,6 +74,8 @@ dvr_entry_get_extra_time_pre( dvr_entry_t *de ) { time_t extra = de->de_start_extra; + if (de->de_timerec) + return 0; if (!extra_valid(extra)) { if (de->de_channel) extra = de->de_channel->ch_dvr_extra_time_pre; @@ -88,6 +90,8 @@ dvr_entry_get_extra_time_post( dvr_entry_t *de ) { time_t extra = de->de_stop_extra; + if (de->de_timerec) + return 0; if (!extra_valid(extra)) { if (de->de_channel) extra = de->de_channel->ch_dvr_extra_time_post; @@ -443,13 +447,14 @@ dvr_entry_create(const char *uuid, htsmsg_t *conf) /** * Create the event */ -static dvr_entry_t * -_dvr_entry_create(const char *config_uuid, epg_broadcast_t *e, +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_timerec_entry_t *dte, dvr_prio_t pri, int retention) { dvr_entry_t *de; @@ -497,6 +502,8 @@ _dvr_entry_create(const char *config_uuid, epg_broadcast_t *e, htsmsg_add_u32(conf, "broadcast", e->id); if (dae) htsmsg_add_str(conf, "autorec", idnode_uuid_as_str(&dae->dae_id)); + if (dte) + htsmsg_add_str(conf, "timerec", idnode_uuid_as_str(&dte->dte_id)); de = dvr_entry_create(NULL, conf); @@ -535,11 +542,11 @@ dvr_entry_create_htsp(const char *config_uuid, 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, + 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, retention); + creator, dae, NULL, pri, retention); } /** @@ -555,12 +562,12 @@ dvr_entry_create_by_event(const char *config_uuid, if(!e->channel || !e->episode || !e->episode->title) return NULL; - return _dvr_entry_create(config_uuid, e, + return dvr_entry_create_(config_uuid, e, e->channel, e->start, e->stop, start_extra, stop_extra, NULL, NULL, NULL, LIST_FIRST(&e->episode->genre), - creator, dae, pri, retention); + creator, dae, NULL, pri, retention); } /** @@ -637,6 +644,11 @@ dvr_entry_dec_ref(dvr_entry_t *de) if(de->de_autorec != NULL) LIST_REMOVE(de, de_autorec_link); + if (de->de_timerec) { + de->de_timerec->dte_spawn = NULL; + de->de_timerec = NULL; + } + if(de->de_config != NULL) LIST_REMOVE(de, de_config_link); @@ -1272,16 +1284,6 @@ dvr_entry_class_channel_get(void *o) 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) { @@ -1417,6 +1419,40 @@ dvr_entry_class_autorec_get(void *o) return &ret; } +static int +dvr_entry_class_timerec_set(void *o, const void *v) +{ + dvr_entry_t *de = (dvr_entry_t *)o; + dvr_timerec_entry_t *dte; + if (!dvr_entry_is_editable(de)) + return 0; + dte = v ? dvr_timerec_find_by_uuid(v) : NULL; + if (dte == NULL) { + if (de->de_timerec) { + de->de_timerec->dte_spawn = NULL; + de->de_timerec = NULL; + return 1; + } + } else if (de->de_timerec != dte) { + de->de_timerec = dte; + dte->dte_spawn = de; + return 1; + } + return 0; +} + +static const void * +dvr_entry_class_timerec_get(void *o) +{ + static const char *ret; + dvr_entry_t *de = (dvr_entry_t *)o; + if (de->de_timerec) + ret = idnode_uuid_as_str(&de->de_timerec->dte_id); + else + ret = ""; + return &ret; +} + static int dvr_entry_class_broadcast_set(void *o, const void *v) { @@ -1732,7 +1768,7 @@ const idclass_t dvr_entry_class = { .name = "Channel", .set = dvr_entry_class_channel_set, .get = dvr_entry_class_channel_get, - .list = dvr_entry_class_channel_list, + .list = channel_class_get_list, .get_opts = dvr_entry_class_start_opts, }, { @@ -1863,6 +1899,14 @@ const idclass_t dvr_entry_class = { .get = dvr_entry_class_autorec_get, .opts = PO_RDONLY, }, + { + .type = PT_STR, + .id = "timerec", + .name = "Auto Time Record", + .set = dvr_entry_class_timerec_set, + .get = dvr_entry_class_timerec_get, + .opts = PO_RDONLY, + }, { .type = PT_U32, .id = "content_type", @@ -2755,8 +2799,10 @@ dvr_init(void) dvr_inotify_init(); #endif dvr_autorec_init(); + dvr_timerec_init(); dvr_db_load(); dvr_autorec_update(); + dvr_timerec_update(); } /** @@ -2778,4 +2824,5 @@ dvr_done(void) dvr_config_destroy(cfg, 0); pthread_mutex_unlock(&global_lock); dvr_autorec_done(); + dvr_timerec_done(); } diff --git a/src/dvr/dvr_timerec.c b/src/dvr/dvr_timerec.c new file mode 100644 index 00000000..7dbfc1d4 --- /dev/null +++ b/src/dvr/dvr_timerec.c @@ -0,0 +1,606 @@ +/* + * tvheadend, Automatic time-based recording + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tvheadend.h" +#include "settings.h" +#include "dvr.h" +#include "dtable.h" +#include "epg.h" + +static int dvr_timerec_in_init = 0; + +struct dvr_timerec_entry_queue timerec_entries; + +static gtimer_t dvr_timerec_timer; + +/** + * + */ +static time_t +dvr_timerec_timecorrection(time_t clk, int hm, struct tm *tm) +{ + time_t r; + int isdst; + + localtime_r(&clk, tm); + tm->tm_min = hm % 60; + tm->tm_hour = hm / 60; + isdst = tm->tm_isdst; + r = mktime(tm); + if (tm->tm_isdst != isdst) { + tm->tm_min = hm % 60; + tm->tm_hour = hm / 60; + r = mktime(tm); + } + return r; +} + +/** + * Unlink - and remove any unstarted + */ +static void +dvr_timerec_purge_spawn(dvr_timerec_entry_t *dte) +{ + dvr_entry_t *de = dte->dte_spawn; + + if (de && de->de_timerec) { + dte->dte_spawn = NULL; + de->de_timerec = NULL; + if (de->de_sched_state == DVR_SCHEDULED) + dvr_entry_cancel(de); + else + dvr_entry_save(de); + } +} + +/** + * Title + */ +static const char * +dvr_timerec_title(dvr_timerec_entry_t *dte, struct tm *start) +{ + static char buf[256]; + size_t len; + + if (dte->dte_title == NULL) + return "Unknown"; + len = strftime(buf, sizeof(buf) - 1, dte->dte_title, start); + buf[len] = '\0'; + return buf; +} + +/** + * handle the timerec entry + */ +void +dvr_timerec_check(dvr_timerec_entry_t *dte) +{ + dvr_entry_t *de; + time_t start, stop; + struct tm tm_start, tm_stop; + const char *title; + char buf[200]; + + if(dte->dte_enabled == 0 || dte->dte_weekdays == 0) + goto fail; + if(dte->dte_start < 0 || dte->dte_start >= 24*60 || + dte->dte_stop < 0 || dte->dte_stop >= 24*60) + goto fail; + if(dte->dte_start >= dte->dte_stop) + goto fail; + if(dte->dte_channel == NULL) + goto fail; + + if(dte->dte_weekdays != 0x7f) { + localtime_r(&dispatch_clock, &tm_start); + if(!((1 << ((tm_start.tm_wday ?: 7) - 1)) & dte->dte_weekdays)) + goto fail; + } + + start = dvr_timerec_timecorrection(dispatch_clock, dte->dte_start, &tm_start); + stop = dvr_timerec_timecorrection(dispatch_clock, dte->dte_stop, &tm_stop); + + /* day boundary correction */ + if (start > stop) + stop += 24 * 60 * 60; + assert(start < stop); + + /* if it's really in past, don't queue */ + if (stop < dispatch_clock - 3600) + goto fail; + + /* purge the old entry */ + de = dte->dte_spawn; + if (de) { + if (de->de_start == start && de->de_stop == stop) + return; + dvr_timerec_purge_spawn(dte); + } + + title = dvr_timerec_title(dte, &tm_start); + snprintf(buf, sizeof(buf), "Time recording%s%s", + dte->dte_creator ? " by: " : "", + dte->dte_creator ?: ""); + de = dvr_entry_create_(dte->dte_config_name, NULL, dte->dte_channel, + start, stop, 0, 0, title, + NULL, NULL, NULL, buf, + NULL, dte, dte->dte_pri, dte->dte_retention); + + return; + +fail: + dvr_timerec_purge_spawn(dte); +} + +/** + * + */ +dvr_timerec_entry_t * +dvr_timerec_create(const char *uuid, htsmsg_t *conf) +{ + dvr_timerec_entry_t *dte; + + dte = calloc(1, sizeof(*dte)); + + if (idnode_insert(&dte->dte_id, uuid, &dvr_timerec_entry_class, 0)) { + if (uuid) + tvhwarn("dvr", "invalid timerec entry uuid '%s'", uuid); + free(dte); + return NULL; + } + + dte->dte_title = strdup("Time-%x-%R"); + dte->dte_weekdays = 0x7f; + dte->dte_pri = DVR_PRIO_NORMAL; + dte->dte_start = -1; + dte->dte_stop = -1; + + TAILQ_INSERT_TAIL(&timerec_entries, dte, dte_link); + + idnode_load(&dte->dte_id, conf); + + return dte; +} + +/** + * + */ +static void +timerec_entry_destroy(dvr_timerec_entry_t *dte, int delconf) +{ + dvr_timerec_purge_spawn(dte); + + if (delconf) + hts_settings_remove("dvr/timerec/%s", idnode_uuid_as_str(&dte->dte_id)); + + TAILQ_REMOVE(&timerec_entries, dte, dte_link); + idnode_unlink(&dte->dte_id); + + free(dte->dte_name); + free(dte->dte_config_name); + free(dte->dte_creator); + free(dte->dte_comment); + + if(dte->dte_channel != NULL) + LIST_REMOVE(dte, dte_channel_link); + + free(dte); +} + +/** + * + */ +void +dvr_timerec_save(dvr_timerec_entry_t *dte) +{ + htsmsg_t *m = htsmsg_create_map(); + + lock_assert(&global_lock); + + idnode_save(&dte->dte_id, m); + hts_settings_save(m, "dvr/timerec/%s", idnode_uuid_as_str(&dte->dte_id)); + htsmsg_destroy(m); +} + +/* ************************************************************************** + * DVR Autorec Entry Class definition + * **************************************************************************/ + +static void +dvr_timerec_entry_class_save(idnode_t *self) +{ + dvr_timerec_entry_t *dte = (dvr_timerec_entry_t *)self; + dvr_timerec_save(dte); + dvr_timerec_check(dte); +} + +static void +dvr_timerec_entry_class_delete(idnode_t *self) +{ + timerec_entry_destroy((dvr_timerec_entry_t *)self, 1); +} + +static const char * +dvr_timerec_entry_class_get_title (idnode_t *self) +{ + dvr_timerec_entry_t *dte = (dvr_timerec_entry_t *)self; + const char *s = ""; + if (dte->dte_name && dte->dte_name[0] != '\0') + s = dte->dte_name; + else if (dte->dte_comment && dte->dte_comment[0] != '\0') + s = dte->dte_comment; + return s; +} + +static int +dvr_timerec_entry_class_channel_set(void *o, const void *v) +{ + dvr_timerec_entry_t *dte = (dvr_timerec_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 (dte->dte_channel) { + LIST_REMOVE(dte, dte_channel_link); + dte->dte_channel = NULL; + return 1; + } + } else if (dte->dte_channel != ch) { + if (dte->dte_channel) + LIST_REMOVE(dte, dte_channel_link); + dte->dte_channel = ch; + LIST_INSERT_HEAD(&ch->ch_timerecs, dte, dte_channel_link); + return 1; + } + return 0; +} + +static const void * +dvr_timerec_entry_class_channel_get(void *o) +{ + static const char *ret; + dvr_timerec_entry_t *dte = (dvr_timerec_entry_t *)o; + if (dte->dte_channel) + ret = idnode_uuid_as_str(&dte->dte_channel->ch_id); + else + ret = ""; + return &ret; +} + +static int +dvr_timerec_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_timerec_entry_class_start_set(void *o, const void *v) +{ + dvr_timerec_entry_t *dte = (dvr_timerec_entry_t *)o; + return dvr_timerec_entry_class_time_set(o, v, &dte->dte_start); +} + +static int +dvr_timerec_entry_class_stop_set(void *o, const void *v) +{ + dvr_timerec_entry_t *dte = (dvr_timerec_entry_t *)o; + return dvr_timerec_entry_class_time_set(o, v, &dte->dte_stop); +} + +static const void * +dvr_timerec_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_timerec_entry_class_start_get(void *o) +{ + dvr_timerec_entry_t *dte = (dvr_timerec_entry_t *)o; + return dvr_timerec_entry_class_time_get(o, dte->dte_start); +} + +static const void * +dvr_timerec_entry_class_stop_get(void *o) +{ + dvr_timerec_entry_t *dte = (dvr_timerec_entry_t *)o; + return dvr_timerec_entry_class_time_get(o, dte->dte_stop); +} + +static htsmsg_t * +dvr_timerec_entry_class_time_list(void *o) +{ + return dvr_autorec_entry_class_time_list(o, "Invalid"); +} + +static int +dvr_timerec_entry_class_config_name_set(void *o, const void *v) +{ + dvr_timerec_entry_t *dte = (dvr_timerec_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 && dte->dte_config_name) { + free(dte->dte_config_name); + return 1; + } else if (strcmp(dte->dte_config_name ?: "", cfg ? cfg->dvr_config_name : "")) { + free(dte->dte_config_name); + dte->dte_config_name = strdup(cfg->dvr_config_name); + return 1; + } + return 0; +} + +static int +dvr_timerec_entry_class_weekdays_set(void *o, const void *v) +{ + dvr_timerec_entry_t *dte = (dvr_timerec_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 != dte->dte_weekdays) { + dte->dte_weekdays = bits; + return 1; + } + return 0; +} + +static const void * +dvr_timerec_entry_class_weekdays_get(void *o) +{ + dvr_timerec_entry_t *dte = (dvr_timerec_entry_t *)o; + htsmsg_t *m = htsmsg_create_list(); + int i; + for (i = 0; i < 7; i++) + if (dte->dte_weekdays & (1 << i)) + htsmsg_add_u32(m, NULL, i + 1); + return m; +} + +static char * +dvr_timerec_entry_class_weekdays_rend(void *o) +{ + dvr_timerec_entry_t *dte = (dvr_timerec_entry_t *)o; + return dvr_autorec_entry_class_weekdays_rend(dte->dte_weekdays); +} + +const idclass_t dvr_timerec_entry_class = { + .ic_class = "dvrtimerec", + .ic_caption = "DVR Time-Record Entry", + .ic_event = "dvrtimerec", + .ic_save = dvr_timerec_entry_class_save, + .ic_get_title = dvr_timerec_entry_class_get_title, + .ic_delete = dvr_timerec_entry_class_delete, + .ic_properties = (const property_t[]) { + { + .type = PT_BOOL, + .id = "enabled", + .name = "Enabled", + .off = offsetof(dvr_timerec_entry_t, dte_enabled), + }, + { + .type = PT_STR, + .id = "name", + .name = "Name", + .off = offsetof(dvr_timerec_entry_t, dte_name), + }, + { + .type = PT_STR, + .id = "title", + .name = "Title", + .off = offsetof(dvr_timerec_entry_t, dte_title), + .def.s = "Time-%x-%R", + }, + { + .type = PT_STR, + .id = "channel", + .name = "Channel", + .set = dvr_timerec_entry_class_channel_set, + .get = dvr_timerec_entry_class_channel_get, + .list = channel_class_get_list, + }, + { + .type = PT_STR, + .id = "start", + .name = "Start", + .set = dvr_timerec_entry_class_start_set, + .get = dvr_timerec_entry_class_start_get, + .list = dvr_timerec_entry_class_time_list, + .def.s = "12:00", + }, + { + .type = PT_STR, + .id = "stop", + .name = "Stop", + .set = dvr_timerec_entry_class_stop_set, + .get = dvr_timerec_entry_class_stop_get, + .list = dvr_timerec_entry_class_time_list, + .def.s = "12:00", + }, + { + .type = PT_U32, + .islist = 1, + .id = "weekdays", + .name = "Week Days", + .set = dvr_timerec_entry_class_weekdays_set, + .get = dvr_timerec_entry_class_weekdays_get, + .list = dvr_autorec_entry_class_weekdays_list, + .rend = dvr_timerec_entry_class_weekdays_rend, + .def.u32 = 0x7f + }, + { + .type = PT_U32, + .id = "pri", + .name = "Priority", + .list = dvr_entry_class_pri_list, + .def.i = DVR_PRIO_NORMAL, + .off = offsetof(dvr_timerec_entry_t, dte_pri), + }, + { + .type = PT_INT, + .id = "retention", + .name = "Retention", + .off = offsetof(dvr_timerec_entry_t, dte_retention), + }, + { + .type = PT_STR, + .id = "config_name", + .name = "DVR Configuration", + .set = dvr_timerec_entry_class_config_name_set, + .list = dvr_entry_class_config_name_list, + .off = offsetof(dvr_timerec_entry_t, dte_config_name), + }, + { + .type = PT_STR, + .id = "creator", + .name = "Creator", + .off = offsetof(dvr_timerec_entry_t, dte_creator), + .opts = PO_RDONLY, + }, + { + .type = PT_STR, + .id = "comment", + .name = "Comment", + .off = offsetof(dvr_timerec_entry_t, dte_comment), + }, + {} + } +}; + +/** + * + */ +void +dvr_timerec_init(void) +{ + htsmsg_t *l, *c; + htsmsg_field_t *f; + + TAILQ_INIT(&timerec_entries); + dvr_timerec_in_init = 1; + if((l = hts_settings_load("dvr/timerec")) != NULL) { + HTSMSG_FOREACH(f, l) { + if((c = htsmsg_get_map_by_field(f)) == NULL) + continue; + (void)dvr_timerec_create(f->hmf_name, c); + } + htsmsg_destroy(l); + } + dvr_timerec_in_init = 0; +} + +void +dvr_timerec_done(void) +{ + dvr_timerec_entry_t *dte; + + pthread_mutex_lock(&global_lock); + while ((dte = TAILQ_FIRST(&timerec_entries)) != NULL) + timerec_entry_destroy(dte, 0); + pthread_mutex_unlock(&global_lock); +} + +static void +dvr_timerec_timer_cb(void *aux) +{ + dvr_timerec_entry_t *dte; + time_t next; + struct tm tm; + + tvhtrace("dvr", "timerec update"); + + /* check all entries */ + TAILQ_FOREACH(dte, &timerec_entries, dte_link) + dvr_timerec_check(dte); + + /* load the timer */ + next = dispatch_clock + 60 * 60 * 24; /* next day */ + next = dvr_timerec_timecorrection(next, 0 /* midnight */, &tm); + tvhtrace("dvr", "next timerec check scheduled in %li seconds", (long)(next - dispatch_clock)); + gtimer_arm(&dvr_timerec_timer, dvr_timerec_timer_cb, NULL, next); +} + +void +dvr_timerec_update(void) +{ + /* check all timerec entries and load the timer */ + dvr_timerec_timer_cb(NULL); +} + +/** + * + */ +void +timerec_destroy_by_channel(channel_t *ch, int delconf) +{ + dvr_timerec_entry_t *dte; + + while((dte = LIST_FIRST(&ch->ch_timerecs)) != NULL) + timerec_entry_destroy(dte, delconf); +} + +/* + * + */ +void +timerec_destroy_by_id(const char *id, int delconf) +{ + dvr_timerec_entry_t *dte; + dte = dvr_timerec_find_by_uuid(id); + + if (dte) + timerec_entry_destroy(dte, delconf); +} diff --git a/src/tvheadend.h b/src/tvheadend.h index cf928a4c..d32d8c16 100644 --- a/src/tvheadend.h +++ b/src/tvheadend.h @@ -193,6 +193,7 @@ LIST_HEAD(th_descrambler_list, th_descrambler); TAILQ_HEAD(th_refpkt_queue, th_refpkt); TAILQ_HEAD(th_muxpkt_queue, th_muxpkt); LIST_HEAD(dvr_autorec_entry_list, dvr_autorec_entry); +LIST_HEAD(dvr_timerec_entry_list, dvr_timerec_entry); TAILQ_HEAD(th_pktref_queue, th_pktref); LIST_HEAD(streaming_target_list, streaming_target); diff --git a/src/webui/static/app/dvr.js b/src/webui/static/app/dvr.js index 496bdff7..2248f880 100644 --- a/src/webui/static/app/dvr.js +++ b/src/webui/static/app/dvr.js @@ -77,6 +77,30 @@ tvheadend.dvrRowActions = function() { }); } +tvheadend.weekdaysRenderer = 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; +} + + /** * */ @@ -326,6 +350,7 @@ tvheadend.autorec_editor = function(panel, index) { tabIndex: index, columns: { enabled: { width: 50 }, + name: { width: 200 }, title: { width: 300 }, channel: { width: 200 }, tag: { width: 200 }, @@ -342,44 +367,21 @@ tvheadend.autorec_editor = function(panel, index) { add: { url: 'api/dvr/autorec', params: { - list: 'enabled,title,channel,tag,content_type,minduration,' + + list: 'enabled,name,title,channel,tag,content_type,minduration,' + 'maxduration,weekdays,start,pri,config_name,comment', }, create: { } }, del: true, - list: 'enabled,title,channel,tag,content_type,minduration,' + + list: 'enabled,name,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; - } - } + renderer: function(st) { return tvheadend.weekdaysRenderer; } } }, sort: { - field: 'title', + field: 'name', direction: 'ASC' }, help: function() { @@ -391,6 +393,57 @@ tvheadend.autorec_editor = function(panel, index) { }; +/** + * + */ +tvheadend.timerec_editor = function(panel, index) { + + tvheadend.idnode_grid(panel, { + url: 'api/dvr/timerec', + titleS: 'DVR TimeRec Entry', + titleP: 'DVR TimeRec Entries', + iconCls: 'clock', + tabIndex: index, + columns: { + enabled: { width: 50 }, + name: { width: 200 }, + title: { width: 300 }, + channel: { width: 200 }, + weekdays: { width: 160 }, + start: { width: 100 }, + stop: { width: 100 }, + pri: { width: 80 }, + config_name: { width: 120 }, + creator: { width: 200 }, + comment: { width: 200 }, + }, + add: { + url: 'api/dvr/timerec', + params: { + list: 'enabled,name,title,channel,weekdays,start,stop,pri,config_name,comment', + }, + create: { } + }, + del: true, + list: 'enabled,name,title,channel,weekdays,start,stop,pri,config_name,comment', + columns: { + weekdays: { + renderer: function(st) { return tvheadend.weekdaysRenderer; } + } + }, + sort: { + field: 'name', + direction: 'ASC' + }, + help: function() { + new tvheadend.help('DVR', 'config_dvrtime.html'); + }, + }); + + return panel; + +}; + /** * */ @@ -406,5 +459,6 @@ tvheadend.dvr = function(panel, index) { tvheadend.dvr_finished(p, 1); tvheadend.dvr_failed(p, 2); tvheadend.autorec_editor(p, 3); + tvheadend.timerec_editor(p, 4); return p; }