/* * 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 "epg.h" 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; tm->tm_sec = 0; isdst = tm->tm_isdst; r = mktime(tm); if (tm->tm_isdst != isdst) { tm->tm_min = hm % 60; tm->tm_hour = hm / 60; tm->tm_sec = 0; r = mktime(tm); } return r; } /** * Unlink - and remove any unstarted */ static void dvr_timerec_purge_spawn(dvr_timerec_entry_t *dte, int delconf) { dvr_entry_t *de = dte->dte_spawn; if (de && de->de_timerec) { dte->dte_spawn = NULL; de->de_timerec = NULL; if (delconf) { 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, limit; 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_channel == NULL) goto fail; limit = dispatch_clock - 600; start = dvr_timerec_timecorrection(dispatch_clock, dte->dte_start, &tm_start); stop = dvr_timerec_timecorrection(dispatch_clock, dte->dte_stop, &tm_stop); if (start < limit && stop < limit) { /* next day */ start = dvr_timerec_timecorrection(dispatch_clock + 24*60*60, dte->dte_start, &tm_start); stop = dvr_timerec_timecorrection(dispatch_clock + 24*60*60, dte->dte_stop, &tm_stop); } /* day boundary correction */ if (start > stop) stop += 24 * 60 * 60; assert(start < stop); if(dte->dte_weekdays != 0x7f) { localtime_r(&start, &tm_start); if(!((1 << ((tm_start.tm_wday ?: 7) - 1)) & dte->dte_weekdays)) 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, 1); } 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_(idnode_uuid_as_str(&dte->dte_config->dvr_id), 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, 1); } /** * */ 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; dte->dte_config = dvr_config_find_by_name_default(NULL); LIST_INSERT_HEAD(&dte->dte_config->dvr_timerec_entries, dte, dte_config_link); 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, delconf); 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); if(dte->dte_config != NULL) LIST_REMOVE(dte, dte_config_link); free(dte->dte_name); free(dte->dte_title); 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 char * dvr_timerec_entry_class_channel_rend(void *o) { dvr_timerec_entry_t *dte = (dvr_timerec_entry_t *)o; if (dte->dte_channel) return strdup(channel_get_name(dte->dte_channel)); return NULL; } 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) { dte->dte_config = NULL; LIST_REMOVE(dte, dte_config_link); return 1; } else if (cfg != dte->dte_config) { if (dte->dte_config) LIST_REMOVE(dte, dte_config_link); LIST_INSERT_HEAD(&cfg->dvr_timerec_entries, dte, dte_config_link); dte->dte_config = cfg; return 1; } return 0; } static const void * dvr_timerec_entry_class_config_name_get(void *o) { static const char *buf; dvr_timerec_entry_t *dte = (dvr_timerec_entry_t *)o; if (dte->dte_config) buf = idnode_uuid_as_str(&dte->dte_config->dvr_id); else buf = ""; return &buf; } static char * dvr_timerec_entry_class_config_name_rend(void *o) { dvr_timerec_entry_t *dte = (dvr_timerec_entry_t *)o; if (dte->dte_config) return strdup(dte->dte_config->dvr_config_name); return NULL; } 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; return dvr_autorec_entry_class_weekdays_get(dte->dte_weekdays); } static htsmsg_t * dvr_timerec_entry_class_weekdays_default(void) { return dvr_autorec_entry_class_weekdays_get(0x7f); } 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, .rend = dvr_timerec_entry_class_channel_rend, .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", .opts = PO_SORTKEY, }, { .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", .opts = PO_SORTKEY, }, { .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.list = dvr_timerec_entry_class_weekdays_default }, { .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), .opts = PO_SORTKEY, }, { .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, .get = dvr_timerec_entry_class_config_name_get, .rend = dvr_timerec_entry_class_config_name_rend, .list = dvr_entry_class_config_name_list, }, { .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); 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); } } 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; tvhtrace("dvr", "timerec update"); /* check all entries */ TAILQ_FOREACH(dte, &timerec_entries, dte_link) dvr_timerec_check(dte); /* load the timer */ gtimer_arm(&dvr_timerec_timer, dvr_timerec_timer_cb, NULL, 3550); } 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); } /** * */ void timerec_destroy_by_config(dvr_config_t *kcfg, int delconf) { dvr_timerec_entry_t *dte; dvr_config_t *cfg = NULL; while((dte = LIST_FIRST(&kcfg->dvr_timerec_entries)) != NULL) { LIST_REMOVE(dte, dte_config_link); if (cfg == NULL && delconf) cfg = dvr_config_find_by_name_default(NULL); if (cfg) LIST_INSERT_HEAD(&cfg->dvr_timerec_entries, dte, dte_config_link); dte->dte_config = cfg; if (delconf) dvr_timerec_save(dte); } }