tvheadend/src/dvr/dvr_config.c
2014-10-16 16:36:34 +02:00

837 lines
21 KiB
C

/*
* Digital Video Recorder
* Copyright (C) 2008 Andreas Öman
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <pthread.h>
#include <sys/stat.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include "settings.h"
#include "tvheadend.h"
#include "dvr.h"
#include "htsp_server.h"
#include "streaming.h"
#include "intlconv.h"
#include "dbus.h"
#include "imagecache.h"
#include "access.h"
int dvr_iov_max;
struct dvr_config_list dvrconfigs;
static dvr_config_t *dvrdefaultconfig = NULL;
static void dvr_config_destroy(dvr_config_t *cfg, int delconf);
/**
* find a dvr config by name, return NULL if not found
*/
dvr_config_t *
dvr_config_find_by_name(const char *name)
{
dvr_config_t *cfg;
if (name == NULL)
name = "";
LIST_FOREACH(cfg, &dvrconfigs, config_link)
if (cfg->dvr_enabled && !strcmp(name, cfg->dvr_config_name))
return cfg;
return NULL;
}
/**
* find a dvr config by name, return the default config if not found
*/
dvr_config_t *
dvr_config_find_by_name_default(const char *name)
{
dvr_config_t *cfg;
if (dvrdefaultconfig == NULL)
dvrdefaultconfig = dvr_config_find_by_name(NULL);
if (dvrdefaultconfig == NULL) {
cfg = dvr_config_create("", NULL, NULL);
assert(cfg);
dvr_config_save(cfg);
dvrdefaultconfig = cfg;
}
if (name == NULL || *name == '\0')
return dvrdefaultconfig;
cfg = dvr_config_find_by_name(name);
if (cfg == NULL) {
if (name && *name)
tvhlog(LOG_WARNING, "dvr", "Configuration '%s' not found, using default", name);
cfg = dvrdefaultconfig;
} else if (!cfg->dvr_enabled) {
tvhlog(LOG_WARNING, "dvr", "Configuration '%s' not enabled, using default", name);
cfg = dvrdefaultconfig;
}
return cfg;
}
/*
* find a dvr config by name using a filter list,
* return the first config from list if name is not valid
* return the default config if not found
*/
dvr_config_t *
dvr_config_find_by_list(htsmsg_t *uuids, const char *name)
{
dvr_config_t *cfg, *res = NULL;
htsmsg_field_t *f;
const char *uuid, *uuid2;
cfg = dvr_config_find_by_name(name);
uuid = idnode_uuid_as_str(&cfg->dvr_id);
if (uuids) {
HTSMSG_FOREACH(f, uuids) {
uuid2 = htsmsg_field_get_str(f) ?: "";
if (strcmp(uuid, uuid2) == 0)
return cfg;
if (!res) {
res = dvr_config_find_by_uuid(uuid2);
if (!res->dvr_enabled)
res = NULL;
}
}
} else {
res = cfg;
}
if (!res)
res = dvr_config_find_by_name_default(NULL);
return res;
}
/**
*
*/
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);
s = charset ? charset : intlconv_filesystem_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;
}
/**
* create a new named dvr config; the caller is responsible
* to avoid duplicates
*/
dvr_config_t *
dvr_config_create(const char *name, const char *uuid, htsmsg_t *conf)
{
dvr_config_t *cfg;
if (name == NULL)
name = "";
cfg = calloc(1, sizeof(dvr_config_t));
LIST_INIT(&cfg->dvr_entries);
LIST_INIT(&cfg->dvr_autorec_entries);
LIST_INIT(&cfg->dvr_timerec_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;
cfg->dvr_config_name = strdup(name);
cfg->dvr_retention_days = 31;
cfg->dvr_tag_files = 1;
cfg->dvr_skip_commercials = 1;
dvr_charset_update(cfg, intlconv_filesystem_charset());
cfg->dvr_update_window = 24 * 3600;
/* series link support */
cfg->dvr_sl_brand_lock = 1; // use brand linking
cfg->dvr_sl_season_lock = 0; // ignore season (except if no brand)
cfg->dvr_sl_channel_lock = 1; // channel locked
cfg->dvr_sl_time_lock = 0; // time slot (approx) locked
cfg->dvr_sl_more_recent = 1; // Only record more reason episodes
cfg->dvr_sl_quality_lock = 1; // Don't attempt to ajust quality
/* Muxer config */
cfg->dvr_muxcnf.m_cache = MC_CACHE_DONTKEEP;
/* dup detect */
cfg->dvr_dup_detect_episode = 1; // detect dup episodes
/* Default recording file and directory permissions */
cfg->dvr_muxcnf.m_file_permissions = 0664;
cfg->dvr_muxcnf.m_directory_permissions = 0775;
if (conf) {
idnode_load(&cfg->dvr_id, conf);
cfg->dvr_valid = 1;
}
tvhinfo("dvr", "Creating new configuration '%s'", cfg->dvr_config_name);
if (cfg->dvr_profile == NULL) {
cfg->dvr_profile = profile_find_by_name("dvr", NULL);
assert(cfg->dvr_profile);
LIST_INSERT_HEAD(&cfg->dvr_profile->pro_dvr_configs, cfg, profile_link);
}
if (dvr_config_is_default(cfg) && dvr_config_find_by_name(NULL)) {
tvherror("dvr", "Unable to create second default config, removing");
LIST_INSERT_HEAD(&dvrconfigs, cfg, config_link);
dvr_config_destroy(cfg, 0);
cfg = NULL;
}
if (cfg) {
LIST_INSERT_HEAD(&dvrconfigs, cfg, config_link);
if (conf && dvr_config_is_default(cfg))
cfg->dvr_enabled = 1;
}
return cfg;
}
/**
* destroy a dvr config
*/
static void
dvr_config_destroy(dvr_config_t *cfg, int delconf)
{
if (delconf) {
tvhinfo("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);
if (cfg->dvr_profile) {
LIST_REMOVE(cfg, profile_link);
cfg->dvr_profile = NULL;
}
dvr_entry_destroy_by_config(cfg, delconf);
access_destroy_by_dvr_config(cfg, delconf);
autorec_destroy_by_config(cfg, delconf);
timerec_destroy_by_config(cfg, delconf);
free(cfg->dvr_charset_id);
free(cfg->dvr_charset);
free(cfg->dvr_storage);
free(cfg->dvr_config_name);
free(cfg);
}
/**
*
*/
void
dvr_config_delete(const char *name)
{
dvr_config_t *cfg;
cfg = dvr_config_find_by_name(name);
if (!dvr_config_is_default(cfg))
dvr_config_destroy(cfg, 1);
else
tvhwarn("dvr", "Attempt to delete default config ignored");
}
/*
*
*/
void
dvr_config_save(dvr_config_t *cfg)
{
htsmsg_t *m = htsmsg_create_map();
lock_assert(&global_lock);
idnode_save(&cfg->dvr_id, m);
hts_settings_save(m, "dvr/config/%s", idnode_uuid_as_str(&cfg->dvr_id));
htsmsg_destroy(m);
}
/* **************************************************************************
* DVR Config Class definition
* **************************************************************************/
static void
dvr_config_class_save(idnode_t *self)
{
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);
}
static void
dvr_config_class_delete(idnode_t *self)
{
dvr_config_t *cfg = (dvr_config_t *)self;
if (!dvr_config_is_default(cfg))
dvr_config_destroy(cfg, 1);
}
static int
dvr_config_class_perm(idnode_t *self, access_t *a, htsmsg_t *msg_to_write)
{
dvr_config_t *cfg = (dvr_config_t *)self;
htsmsg_field_t *f;
const char *uuid, *my_uuid;
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:
return 0;
}
static int
dvr_config_class_enabled_set(void *o, const void *v)
{
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;
}
static uint32_t
dvr_config_class_enabled_opts(void *o)
{
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;
}
static int
dvr_config_class_name_set(void *o, const void *v)
{
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;
}
static int
dvr_config_class_profile_set(void *o, const void *v)
{
dvr_config_t *cfg = (dvr_config_t *)o;
profile_t *pro;
pro = v ? profile_find_by_uuid(v) : NULL;
pro = pro ?: profile_find_by_name(v, "dvr");
if (pro == NULL) {
if (cfg->dvr_profile) {
LIST_REMOVE(cfg, profile_link);
cfg->dvr_profile = NULL;
return 1;
}
} else if (cfg->dvr_profile != pro) {
if (cfg->dvr_profile)
LIST_REMOVE(cfg, profile_link);
cfg->dvr_profile = pro;
LIST_INSERT_HEAD(&pro->pro_dvr_configs, cfg, profile_link);
return 1;
}
return 0;
}
static const void *
dvr_config_class_profile_get(void *o)
{
static const char *ret;
dvr_config_t *cfg = (dvr_config_t *)o;
if (cfg->dvr_profile)
ret = idnode_uuid_as_str(&cfg->dvr_profile->pro_id);
else
ret = "";
return &ret;
}
static char *
dvr_config_class_profile_rend(void *o)
{
dvr_config_t *cfg = (dvr_config_t *)o;
if (cfg->dvr_profile)
return strdup(profile_get_name(cfg->dvr_profile));
return NULL;
}
static const char *
dvr_config_class_get_title (idnode_t *self)
{
dvr_config_t *cfg = (dvr_config_t *)self;
if (!dvr_config_is_default(cfg))
return cfg->dvr_config_name;
return "(Default Profile)";
}
static int
dvr_config_class_charset_set(void *o, const void *v)
{
dvr_config_t *cfg = (dvr_config_t *)o;
return dvr_charset_update(cfg, v);
}
static htsmsg_t *
dvr_config_class_charset_list(void *o)
{
htsmsg_t *m = htsmsg_create_map();
htsmsg_add_str(m, "type", "api");
htsmsg_add_str(m, "uri", "intlconv/charsets");
return m;
}
static htsmsg_t *
dvr_config_class_cache_list(void *o)
{
static struct strtab tab[] = {
{ "Unknown", MC_CACHE_UNKNOWN },
{ "System", MC_CACHE_SYSTEM },
{ "Do not keep", MC_CACHE_DONTKEEP },
{ "Sync", MC_CACHE_SYNC },
{ "Sync + Do not keep", MC_CACHE_SYNCDONTKEEP }
};
return strtab2htsmsg(tab);
}
static htsmsg_t *
dvr_config_class_extra_list(void *o)
{
return dvr_entry_class_duration_list(o, "Not set (none or channel config)", 4*60, 1);
}
static htsmsg_t *
dvr_config_entry_class_update_window_list(void *o)
{
return dvr_entry_class_duration_list(o, "Update Disabled", 24*3600, 60);
}
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_STR,
.id = "comment",
.name = "Comment",
.off = offsetof(dvr_config_t, dvr_comment),
.group = 1,
},
{
.type = PT_STR,
.id = "profile",
.name = "Stream Profile",
.off = offsetof(dvr_config_t, dvr_profile),
.set = dvr_config_class_profile_set,
.get = dvr_config_class_profile_get,
.rend = dvr_config_class_profile_rend,
.list = profile_class_get_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),
.list = dvr_config_class_extra_list,
.group = 1,
},
{
.type = PT_U32,
.id = "post-extra-time",
.name = "Extra Time After Recordings (minutes)",
.off = offsetof(dvr_config_t, dvr_extra_time_post),
.list = dvr_config_class_extra_list,
.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_U32,
.id = "epg-update-window",
.name = "EPG Update Window",
.off = offsetof(dvr_config_t, dvr_update_window),
.list = dvr_config_entry_class_update_window_list,
.def.u32 = 24*3600,
.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 = "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,
},
{}
},
};
/**
*
*/
void
dvr_config_destroy_by_profile(profile_t *pro, int delconf)
{
dvr_config_t *cfg;
while((cfg = LIST_FIRST(&pro->pro_dvr_configs)) != NULL) {
LIST_REMOVE(cfg, profile_link);
cfg->dvr_profile = profile_find_by_name(NULL, "dvr");
}
}
/**
*
*/
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(NULL);
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_timerec_init();
dvr_entry_init();
dvr_autorec_update();
dvr_timerec_update();
}
/**
*
*/
void
dvr_done(void)
{
dvr_config_t *cfg;
#if ENABLE_INOTIFY
dvr_inotify_done();
#endif
pthread_mutex_lock(&global_lock);
dvr_entry_done();
while ((cfg = LIST_FIRST(&dvrconfigs)) != NULL)
dvr_config_destroy(cfg, 0);
pthread_mutex_unlock(&global_lock);
dvr_autorec_done();
dvr_timerec_done();
}