]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
dvr: rewrite to use the idnode system
authorJaroslav Kysela <perex@perex.cz>
Thu, 21 Aug 2014 19:36:49 +0000 (21:36 +0200)
committerJaroslav Kysela <perex@perex.cz>
Mon, 8 Sep 2014 14:48:58 +0000 (16:48 +0200)
47 files changed:
Makefile
src/access.c
src/access.h
src/api.c
src/api.h
src/api/api_access.c
src/api/api_channel.c
src/api/api_dvr.c [new file with mode: 0644]
src/api/api_epg.c
src/api/api_epggrab.c
src/api/api_esfilter.c
src/api/api_idnode.c
src/api/api_imagecache.c
src/api/api_intlconv.c
src/api/api_mpegts.c
src/api/api_service.c
src/api/api_status.c
src/config.c
src/dvr/dvr.h
src/dvr/dvr_autorec.c
src/dvr/dvr_db.c
src/dvr/dvr_rec.c
src/epg.c
src/htsp_server.c
src/http.c
src/http.h
src/idnode.c
src/idnode.h
src/imagecache.c
src/imagecache.h
src/lang_str.c
src/lang_str.h
src/muxer.h
src/muxer/tvh/mkmux.c
src/prop.c
src/prop.h
src/service.c
src/webui/extjs.c
src/webui/simpleui.c
src/webui/static/app/dvr.js
src/webui/static/app/epg.js
src/webui/static/app/extensions.js
src/webui/static/app/idnode.js
src/webui/static/app/tvheadend.js
src/webui/webui.c
src/webui/webui.h
src/webui/webui_api.c

index 70ad0d3597966fbd0a05861acc3c767734b6e1e4..632fdf00fb591aa18ba6d1ddb67d79cba0abb0fc 100644 (file)
--- 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 \
index 5876b53495e125b440931de00c041f8f8b3da01f..3a175b11391558cb94197ec29e437f79055fb52c 100644 (file)
@@ -37,6 +37,7 @@
 #include "access.h"
 #include "settings.h"
 #include "channels.h"
+#include "tcp.h"
 
 struct access_entry_queue access_entries;
 struct access_ticket_queue access_tickets;
@@ -158,6 +159,8 @@ access_ticket_verify(const char *id, const char *resource)
 void
 access_destroy(access_t *a)
 {
+  free(a->aa_username);
+  free(a->aa_representative);
   htsmsg_destroy(a->aa_chtags);
   free(a);
 }
@@ -310,6 +313,14 @@ access_get(const char *username, const char *password, struct sockaddr *src)
   access_t *a = calloc(1, sizeof(*a));
   access_entry_t *ae;
 
+  if (username) {
+    a->aa_username = strdup(username);
+    a->aa_representative = strdup(username);
+  } else {
+    a->aa_representative = malloc(50);
+    tcp_get_ip_str((struct sockaddr*)src, a->aa_representative, 50);
+  }
+
   if (access_noacl) {
     a->aa_rights = ACCESS_FULL;
     return a;
@@ -645,7 +656,7 @@ access_entry_destroy(access_entry_t *ae)
 /**
  *
  */
-static void
+void
 access_entry_save(access_entry_t *ae)
 {
   htsmsg_t *c = htsmsg_create_map();
index 1cc74a2a867c7baa88bc476a500cf3584fea6e3c..9315c50eef3342716cdf01e4b78f99cd1e7a3042 100644 (file)
@@ -82,6 +82,8 @@ typedef struct access_ticket {
 } access_ticket_t;
 
 typedef struct access {
+  char     *aa_username;
+  char     *aa_representative;
   uint32_t  aa_rights;
   uint32_t  aa_chmin;
   uint32_t  aa_chmax;
@@ -129,6 +131,9 @@ void access_destroy(access_t *a);
 int access_verify(const char *username, const char *password,
                  struct sockaddr *src, uint32_t mask);
 
+static inline int access_verify2(access_t *a, uint32_t mask)
+  { return (a->aa_rights & mask) == mask ? 0 : -1; }
+
 /**
  * Get the access structure
  */
@@ -154,6 +159,12 @@ access_get_by_addr(struct sockaddr *src);
 access_entry_t *
 access_entry_create(const char *uuid, htsmsg_t *conf);
 
+/**
+ *
+ */
+void
+access_entry_save(access_entry_t *ae);
+
 /**
  *
  */
index 3aa932d92af370562422ca788759b12b471092d6..3829557398d0841946ebd479e3444186d1dd6970 100644 (file)
--- a/src/api.c
+++ b/src/api.c
@@ -61,7 +61,8 @@ api_register_all ( const api_hook_t *hooks )
 }
 
 int
-api_exec ( const char *subsystem, htsmsg_t *args, htsmsg_t **resp )
+api_exec ( access_t *perm, const char *subsystem,
+           htsmsg_t *args, htsmsg_t **resp )
 {
   api_hook_t h;
   api_link_t *ah, skel;
@@ -90,12 +91,12 @@ api_exec ( const char *subsystem, htsmsg_t *args, htsmsg_t **resp )
   // Note: this is not required (so no final validation)
 
   /* Execute */
-  return ah->hook->ah_callback(ah->hook->ah_opaque, op, args, resp);
+  return ah->hook->ah_callback(perm, ah->hook->ah_opaque, op, args, resp);
 }
 
 static int
 api_serverinfo
-  ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
 {
   *resp = htsmsg_create_map();
   htsmsg_add_str(*resp, "sw_version",   tvheadend_version);
@@ -128,6 +129,7 @@ void api_init ( void )
   api_esfilter_init();
   api_intlconv_init();
   api_access_init();
+  api_dvr_init();
 }
 
 void api_done ( void )
index e6caef59719a5533142e707ae3749c012cd4f360..0655865b9d36863951821f8b614b24452a0f6f9f 100644 (file)
--- a/src/api.h
+++ b/src/api.h
 #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
index f9ad7708439d07b0b4f741f7b0c25709f014d229..15d87214ab8f58537cb3286992bf3fe1952ed2b1 100644 (file)
@@ -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;
index 99f73c9fe4ba9e05294233f97942dab5a5a2bcc3..aaf87b451eb18a57f5934c24528d6f7e9088a418 100644 (file)
@@ -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 (file)
index 0000000..f342e41
--- /dev/null
@@ -0,0 +1,346 @@
+/*
+ *  API - DVR
+ *
+ *  Copyright (C) 2014 Jaroslav Kysela
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tvheadend.h"
+#include "dvr/dvr.h"
+#include "epg.h"
+#include "api.h"
+
+static const char *
+api_dvr_config_name( access_t *perm, const char *config_uuid )
+{
+  dvr_config_t *cfg;
+
+  lock_assert(&global_lock);
+
+  if (access_verify2(perm, ACCESS_RECORDER_ALL))
+    return config_uuid; /* no change */
+
+  cfg = dvr_config_find_by_name(perm->aa_username);
+  if (cfg)
+    return cfg->dvr_config_name;
+
+  if (perm->aa_username)
+    tvhlog(LOG_INFO, "dvr", "User '%s' has no dvr config with identical name, using default...", perm->aa_username);
+
+  return NULL; /* default */
+}
+
+/*
+ *
+ */
+
+static void
+api_dvr_config_grid
+  ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args )
+{
+  dvr_config_t *cfg;
+
+  LIST_FOREACH(cfg, &dvrconfigs, config_link)
+    if (!idnode_perm((idnode_t *)cfg, perm, NULL))
+      idnode_set_add(ins, (idnode_t*)cfg, &conf->filter);
+}
+
+static int
+api_dvr_config_create
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+{
+  dvr_config_t *cfg;
+  htsmsg_t *conf;
+  const char *s;
+
+  if (!(conf = htsmsg_get_map(args, "conf")))
+    return EINVAL;
+  if (!(s = htsmsg_get_str(conf, "name")))
+    return EINVAL;
+  if (s[0] == '\0')
+    return EINVAL;
+  if (access_verify2(perm, ACCESS_RECORDER_ALL | ACCESS_RECORDER))
+    return EACCES;
+
+  pthread_mutex_lock(&global_lock);
+  if ((cfg = dvr_config_create(NULL, NULL, conf)))
+    dvr_config_save(cfg);
+  pthread_mutex_unlock(&global_lock);
+
+  return 0;
+}
+
+static int is_dvr_entry_finished(dvr_entry_t *entry)
+{
+  dvr_entry_sched_state_t state = entry->de_sched_state;
+  return state == DVR_COMPLETED && !entry->de_last_error && dvr_get_filesize(entry) != -1;
+}
+
+static int is_dvr_entry_upcoming(dvr_entry_t *entry)
+{
+  dvr_entry_sched_state_t state = entry->de_sched_state;
+  return state == DVR_RECORDING || state == DVR_SCHEDULED;
+}
+
+static int is_dvr_entry_failed(dvr_entry_t *entry)
+{
+  if (is_dvr_entry_finished(entry))
+    return 0;
+  if (is_dvr_entry_upcoming(entry))
+    return 0;
+  return 1;
+}
+
+static void
+api_dvr_entry_grid
+  ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args )
+{
+  dvr_entry_t *de;
+
+  LIST_FOREACH(de, &dvrentries, de_global_link)
+    idnode_set_add(ins, (idnode_t*)de, &conf->filter);
+}
+
+static void
+api_dvr_entry_grid_upcoming
+  ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args )
+{
+  dvr_entry_t *de;
+
+  LIST_FOREACH(de, &dvrentries, de_global_link)
+    if (is_dvr_entry_upcoming(de))
+      idnode_set_add(ins, (idnode_t*)de, &conf->filter);
+}
+
+static void
+api_dvr_entry_grid_finished
+  ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args )
+{
+  dvr_entry_t *de;
+
+  LIST_FOREACH(de, &dvrentries, de_global_link)
+    if (is_dvr_entry_finished(de))
+      idnode_set_add(ins, (idnode_t*)de, &conf->filter);
+}
+
+static void
+api_dvr_entry_grid_failed
+  ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args )
+{
+  dvr_entry_t *de;
+
+  LIST_FOREACH(de, &dvrentries, de_global_link)
+    if (is_dvr_entry_failed(de))
+      idnode_set_add(ins, (idnode_t*)de, &conf->filter);
+}
+
+static int
+api_dvr_entry_create
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+{
+  dvr_entry_t *de;
+  htsmsg_t *conf;
+
+  if (!(conf = htsmsg_get_map(args, "conf")))
+    return EINVAL;
+
+  if (access_verify2(perm, ACCESS_RECORDER_ALL)) {
+    htsmsg_delete_field(conf, "config_name");
+    htsmsg_add_str(conf, "config_name", perm->aa_username ?: "");
+  }
+
+  pthread_mutex_lock(&global_lock);
+  if ((de = dvr_entry_create(NULL, conf)))
+    dvr_entry_save(de);
+  pthread_mutex_unlock(&global_lock);
+
+  return 0;
+}
+
+static htsmsg_t *
+api_dvr_entry_create_from_single(htsmsg_t *args)
+{
+  htsmsg_t *entries, *m;
+  const char *s1, *s2;
+
+  if (!(s1 = htsmsg_get_str(args, "config_uuid")))
+    return NULL;
+  if (!(s2 = htsmsg_get_str(args, "event_id")))
+    return NULL;
+  entries = htsmsg_create_list();
+  m = htsmsg_create_map();
+  htsmsg_add_str(m, "config_uuid", s1);
+  htsmsg_add_str(m, "event_id", s2);
+  htsmsg_add_msg(entries, NULL, m);
+  return entries;
+}
+
+static int
+api_dvr_entry_create_by_event
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+{
+  dvr_entry_t *de;
+  const char *config_uuid;
+  epg_broadcast_t *e;
+  htsmsg_t *entries, *entries2 = NULL, *m;
+  htsmsg_field_t *f;
+  const char *s;
+  int count = 0;
+
+  if (!(entries = htsmsg_get_list(args, "entries"))) {
+    entries = entries2 = api_dvr_entry_create_from_single(args);
+    if (!entries)
+      return EINVAL;
+  }
+
+  HTSMSG_FOREACH(f, entries) {
+    if (!(m = htsmsg_get_map_by_field(f))) continue;
+
+    if (!(config_uuid = htsmsg_get_str(m, "config_uuid")))
+      continue;
+    if (!(s = htsmsg_get_str(m, "event_id")))
+      continue;
+
+    pthread_mutex_lock(&global_lock);
+    if ((e = epg_broadcast_find_by_id(atoi(s), NULL))) {
+      de = dvr_entry_create_by_event(api_dvr_config_name(perm, config_uuid),
+                                     e, 0, 0, perm->aa_representative,
+                                     NULL, DVR_PRIO_NORMAL);
+      if (de)
+        dvr_entry_save(de);
+    }
+    pthread_mutex_unlock(&global_lock);
+    count++;
+  }
+
+  htsmsg_destroy(entries2);
+
+  return !count ? EINVAL : 0;
+}
+
+static void
+api_dvr_cancel(access_t *perm, idnode_t *self)
+{
+  dvr_entry_cancel((dvr_entry_t *)self);
+}
+
+static int
+api_dvr_entry_cancel
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+{
+  return api_idnode_handler(perm, args, resp, api_dvr_cancel);
+}
+
+static void
+api_dvr_autorec_grid
+  ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args )
+{
+  dvr_autorec_entry_t *dae;
+
+  TAILQ_FOREACH(dae, &autorec_entries, dae_link)
+    idnode_set_add(ins, (idnode_t*)dae, &conf->filter);
+}
+
+static int
+api_dvr_autorec_create
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+{
+  htsmsg_t *conf;
+  dvr_autorec_entry_t *dae;
+
+  if (!(conf  = htsmsg_get_map(args, "conf")))
+    return EINVAL;
+
+  htsmsg_delete_field(conf, "creator");
+  if (perm->aa_representative)
+    htsmsg_add_str(conf, "creator", perm->aa_representative);
+
+  pthread_mutex_lock(&global_lock);
+  dae = dvr_autorec_create(NULL, conf);
+  if (dae)
+    dvr_autorec_save(dae);
+  pthread_mutex_unlock(&global_lock);
+
+  return 0;
+}
+
+static int
+api_dvr_autorec_create_by_series
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+{
+  dvr_autorec_entry_t *dae;
+  epg_broadcast_t *e;
+  htsmsg_t *entries, *entries2 = NULL, *m;
+  htsmsg_field_t *f;
+  const char *config_uuid, *s;
+  int count = 0;
+
+  if (!(entries = htsmsg_get_list(args, "entries"))) {
+    entries = entries2 = api_dvr_entry_create_from_single(args);
+    if (!entries)
+      return EINVAL;
+  }
+
+  HTSMSG_FOREACH(f, entries) {
+    if (!(m = htsmsg_get_map_by_field(f))) continue;
+
+    if (!(config_uuid = htsmsg_get_str(m, "config_uuid")))
+      continue;
+    if (!(s = htsmsg_get_str(m, "event_id")))
+      continue;
+
+    pthread_mutex_lock(&global_lock);
+    if ((e = epg_broadcast_find_by_id(atoi(s), NULL))) {
+      dae = dvr_autorec_add_series_link(api_dvr_config_name(perm, config_uuid),
+                                        e, perm->aa_representative,
+                                        "Created from EPG query");
+      if (dae)
+        dvr_autorec_save(dae);
+    }
+    pthread_mutex_unlock(&global_lock);
+    count++;
+  }
+
+  htsmsg_destroy(entries2);
+
+  return !count ? EINVAL : 0;
+}
+
+void api_dvr_init ( void )
+{
+  static api_hook_t ah[] = {
+    { "dvr/config/class",          ACCESS_RECORDER, api_idnode_class, (void*)&dvr_config_class },
+    { "dvr/config/grid",           ACCESS_RECORDER, api_idnode_grid, api_dvr_config_grid },
+    { "dvr/config/create",         ACCESS_ADMIN, api_dvr_config_create, NULL },
+
+    { "dvr/entry/class",           ACCESS_RECORDER, api_idnode_class, (void*)&dvr_entry_class },
+    { "dvr/entry/grid",            ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid },
+    { "dvr/entry/grid_upcoming",   ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid_upcoming },
+    { "dvr/entry/grid_finished",   ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid_finished },
+    { "dvr/entry/grid_failed",     ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid_failed },
+    { "dvr/entry/create",          ACCESS_RECORDER, api_dvr_entry_create, NULL },
+    { "dvr/entry/create_by_event", ACCESS_RECORDER, api_dvr_entry_create_by_event, NULL },
+    { "dvr/entry/cancel",          ACCESS_RECORDER, api_dvr_entry_cancel, NULL },
+
+    { "dvr/autorec/class",         ACCESS_RECORDER, api_idnode_class, (void*)&dvr_autorec_entry_class },
+    { "dvr/autorec/grid",          ACCESS_RECORDER, api_idnode_grid,  api_dvr_autorec_grid },
+    { "dvr/autorec/create",        ACCESS_RECORDER, api_dvr_autorec_create, NULL },
+    { "dvr/autorec/create_by_series", ACCESS_RECORDER, api_dvr_autorec_create_by_series, NULL },
+
+    { NULL },
+  };
+
+  api_register_all(ah);
+}
index 9bd44903a688d0236ecee2de953eb3a266f22d8e..d1ac6ef631391575641ec8f73167b2d99c84d1ae 100644 (file)
@@ -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 },
   };
 
index f0d38cfa2cc98d27b05f9e8da9aa9e8b88d0d838..e984ff573da0038894eae9e1e9fe520c60be9d05 100644 (file)
@@ -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);
index 2769f8c3cbe5f191bbbd863116ae78c41b22a17b..bc8c761b4678d456f9b355fa8bad2bb18a3b99c3 100644 (file)
@@ -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);
index 689a51b5c695af6ef60d20749e24d6bd358d3629..b7d56d51076ffbaa110f381eb4bfa274c3e283f0 100644 (file)
 #include "htsmsg.h"
 #include "api.h"
 
+static htsmsg_t *
+api_idnode_flist_conf( htsmsg_t *args, const char *name )
+{
+  htsmsg_t *m = NULL;
+  const char *s = htsmsg_get_str(args, name);
+  char *r, *saveptr;
+  if (s && s[0] != '\0') {
+    s = r = strdup(s);
+    r = strtok_r(r, ",;:", &saveptr);
+    while (r) {
+      while (*r != '\0' && *r <= ' ')
+        r++;
+      if (*r != '\0') {
+        if (m == NULL)
+          m = htsmsg_create_map();
+        htsmsg_add_bool(m, r, 1);
+      }
+      r = strtok_r(NULL, ",;:", &saveptr);
+    }
+    free((char *)s);
+  }
+  return m;
+}
+
 static struct strtab filtcmptab[] = {
   { "gt", IC_GT },
   { "lt", IC_LT },
@@ -87,10 +111,11 @@ api_idnode_grid_conf
 
 int
 api_idnode_grid
-  ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
 {
   int i;
   htsmsg_t *list, *e;
+  htsmsg_t *flist = api_idnode_flist_conf(args, "list");
   api_idnode_grid_conf_t conf = { 0 };
   idnode_set_t ins = { 0 };
   api_idnode_grid_callback_t cb = opaque;
@@ -100,7 +125,7 @@ api_idnode_grid
 
   /* Create list */
   pthread_mutex_lock(&global_lock);
-  cb(&ins, &conf, args);
+  cb(perm, &ins, &conf, args);
 
   /* Sort */
   if (conf.sort.key)
@@ -111,7 +136,7 @@ api_idnode_grid
   for (i = conf.start; i < ins.is_count && conf.limit != 0; i++) {
     e = htsmsg_create_map();
     htsmsg_add_str(e, "uuid", idnode_uuid_as_str(ins.is_array[i]));
-    idnode_read0(ins.is_array[i], e, 0);
+    idnode_read0(ins.is_array[i], e, flist, 0);
     htsmsg_add_msg(list, NULL, e);
     if (conf.limit > 0) conf.limit--;
   }
@@ -126,13 +151,14 @@ api_idnode_grid
   /* Cleanup */
   free(ins.is_array);
   idnode_filter_clear(&conf.filter);
+  htsmsg_destroy(flist);
 
   return 0;
 }
 
 int
 api_idnode_load_by_class
-  ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
 {
   int i, _enum;
   const idclass_t *idc;
@@ -154,6 +180,9 @@ api_idnode_load_by_class
     for (i = 0; i < is->is_count; i++) {
       in = is->is_array[i];
 
+      if (idnode_perm(in, perm, NULL))
+        continue;
+
       /* Name/UUID only */
       if (_enum) {
         e = htsmsg_create_map();
@@ -161,8 +190,11 @@ api_idnode_load_by_class
         htsmsg_add_str(e, "val", idnode_get_title(in));
 
       /* Full record */
-      } else
-        e = idnode_serialize(in);
+      } else {
+        htsmsg_t *flist = api_idnode_flist_conf(args, "list");
+        e = idnode_serialize0(in, flist, 0);
+        htsmsg_destroy(flist);
+      }
         
       if (e)
         htsmsg_add_msg(l, NULL, e);
@@ -180,11 +212,12 @@ api_idnode_load_by_class
 
 static int
 api_idnode_load
-  ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
 {
-  int err = 0;
+  int err = 0, meta = 0, count = 0;
   idnode_t *in;
-  htsmsg_t *uuids, *l = NULL;
+  htsmsg_t *uuids, *l = NULL, *m;
+  htsmsg_t *flist;
   htsmsg_field_t *f;
   const char *uuid, *class;
 
@@ -197,7 +230,7 @@ api_idnode_load
     if (!idc)
       return EINVAL;
     // TODO: bit naff that 2 locks are required here
-    return api_idnode_load_by_class((void*)idc, NULL, args, resp);
+    return api_idnode_load_by_class(perm, (void*)idc, NULL, args, resp);
   }
   
   /* UUIDs */
@@ -206,6 +239,9 @@ api_idnode_load
   if (!(uuids = htsmsg_field_get_list(f)))
     if (!(uuid = htsmsg_field_get_str(f)))
       return EINVAL;
+  htsmsg_get_s32(args, "meta", &meta);
+
+  flist = api_idnode_flist_conf(args, "list");
 
   pthread_mutex_lock(&global_lock);
 
@@ -215,16 +251,34 @@ api_idnode_load
     HTSMSG_FOREACH(f, uuids) {
       if (!(uuid = htsmsg_field_get_str(f))) continue;
       if (!(in   = idnode_find(uuid, NULL))) continue;
-      htsmsg_add_msg(l, NULL, idnode_serialize(in));
+      if (idnode_perm(in, perm, NULL)) {
+        err = EPERM;
+        continue;
+      }
+      m = idnode_serialize0(in, flist, 0);
+      if (meta > 0)
+        htsmsg_add_msg(m, "meta", idclass_serialize0(in->in_class, flist, 0));
+      htsmsg_add_msg(l, NULL, m);
+      count++;
     }
 
+    if (count)
+      err = 0;
+
   /* Single */
   } else {
     if (!(in = idnode_find(uuid, NULL)))
       err = ENOENT;
     else {
-      l     = htsmsg_create_list();
-      htsmsg_add_msg(l, NULL, idnode_serialize(in));
+      if (idnode_perm(in, perm, NULL)) {
+        err = EPERM;
+      } else {
+        l = htsmsg_create_list();
+        m = idnode_serialize0(in, flist, 0);
+        if (meta > 0)
+          htsmsg_add_msg(m, "meta", idclass_serialize0(in->in_class, flist, 0));
+        htsmsg_add_msg(l, NULL, m);
+      }
     }
   }
 
@@ -235,18 +289,21 @@ api_idnode_load
 
   pthread_mutex_unlock(&global_lock);
 
+  htsmsg_destroy(flist);
+
   return err;
 }
 
 static int
 api_idnode_save
-  ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
 {
   int err = EINVAL;
   idnode_t *in;
   htsmsg_t *msg, *conf;
   htsmsg_field_t *f;
   const char *uuid;
+  int count = 0;
 
   if (!(f = htsmsg_field_find(args, "node")))
     return EINVAL;
@@ -262,6 +319,10 @@ api_idnode_save
       goto exit;
     if (!(in = idnode_find(uuid, NULL)))
       goto exit;
+    if (idnode_perm(in, perm, msg)) {
+      err = EPERM;
+      goto exit;
+    }
     idnode_update(in, msg);
     err = 0;
 
@@ -274,9 +335,15 @@ api_idnode_save
         continue;
       if (!(in = idnode_find(uuid, NULL)))
         continue;
+      if (idnode_perm(in, perm, conf)) {
+        err = EPERM;
+        continue;
+      }
+      count++;
       idnode_update(in, conf);
     }
-    err = 0;
+    if (count)
+      err = 0;
   }
 
   // TODO: return updated UUIDs?
@@ -289,7 +356,7 @@ exit:
 
 int
 api_idnode_tree
-  ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
 {
   const char *uuid;
   const char *root = NULL;
@@ -329,7 +396,7 @@ api_idnode_tree
 
   /* Children */
   } else {
-    idnode_set_t *v = node ? idnode_get_childs(node) : rootfn();
+    idnode_set_t *v = node ? idnode_get_childs(node) : rootfn(perm);
     if (v) {
       int i;
       idnode_set_sort_by_title(v);
@@ -348,11 +415,12 @@ api_idnode_tree
 
 int
 api_idnode_class
-  ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
 {
   int err = EINVAL;
   const char      *name;
   const idclass_t *idc;
+  htsmsg_t *flist = api_idnode_flist_conf(args, "list");
 
   pthread_mutex_lock(&global_lock);
 
@@ -368,17 +436,20 @@ api_idnode_class
   }
 
   err   = 0;
-  *resp = idclass_serialize(idc);
+  *resp = idclass_serialize0(idc, flist, 0);
 
 exit:
   pthread_mutex_unlock(&global_lock);
 
+  htsmsg_destroy(flist);
+
   return err;
 }
 
-static int
+int
 api_idnode_handler
-  ( htsmsg_t *args, htsmsg_t **resp, void (*handler)(idnode_t *in) )
+  ( access_t *perm, htsmsg_t *args, htsmsg_t **resp,
+    void (*handler)(access_t *perm, idnode_t *in) )
 {
   int err = 0;
   idnode_t *in;
@@ -400,7 +471,7 @@ api_idnode_handler
     HTSMSG_FOREACH(f, uuids) {
       if (!(uuid = htsmsg_field_get_string(f))) continue;
       if (!(in   = idnode_find(uuid, NULL))) continue;
-      handler(in);
+      handler(perm, in);
     }
   
   /* Single */
@@ -409,7 +480,7 @@ api_idnode_handler
     if (!(in   = idnode_find(uuid, NULL)))
       err = ENOENT;
     else
-      handler(in);
+      handler(perm, in);
   }
 
   pthread_mutex_unlock(&global_lock);
@@ -417,25 +488,43 @@ api_idnode_handler
   return err;
 }
 
+static void
+api_idnode_delete_ (access_t *perm, idnode_t *in)
+{
+  return idnode_delete(in);
+}
+
 static int
 api_idnode_delete
-  ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+{
+  return api_idnode_handler(perm, args, resp, api_idnode_delete_);
+}
+
+static void
+api_idnode_moveup_ (access_t *perm, idnode_t *in)
 {
-  return api_idnode_handler(args, resp, idnode_delete);
+  return idnode_moveup(in);
 }
 
 static int
 api_idnode_moveup
-  ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+{
+  return api_idnode_handler(perm, args, resp, api_idnode_moveup_);
+}
+
+static void
+api_idnode_movedown_ (access_t *perm, idnode_t *in)
 {
-  return api_idnode_handler(args, resp, idnode_moveup);
+  return idnode_movedown(in);
 }
 
 static int
 api_idnode_movedown
-  ( void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
 {
-  return api_idnode_handler(args, resp, idnode_movedown);
+  return api_idnode_handler(perm, args, resp, api_idnode_movedown_);
 }
 
 void api_idnode_init ( void )
index c82aef8befc9b24219ed079a2753ca472eb02075..e60512df8c7de079bf0b3151727792f056313fba 100644 (file)
@@ -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))
index dfe0c4266190fae7656a7e18a832d513816c6d04..632b96ed692dc5e8e5a17e76340a4f05d56775af 100644 (file)
 
 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;
 }
 
index 3e6ffcee653d6fbcab99527f5c9892bb7abd2cd1..618ae6832e653888bbf7bca590eafd9a4d5b9080 100644 (file)
@@ -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");
index b0d3ec64c0b88be3537693c8457b0109530ca698..3cc822eaabc46535413d19c8ba4a5ac9f972532f 100644 (file)
@@ -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;
index 3104850bc7388917d6975ab2adacbe840050e5e3..6d19e8434afed305df5c89db08458434f5b78e39 100644 (file)
@@ -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();
index 5eee7b6ab1be6ecb84d64655f77d295185f32237..1337caf0d7ff1d20c8dafd3bf7e5e4475b40a5e2 100644 (file)
@@ -620,7 +620,7 @@ config_migrate_v6 ( void )
  * v6 -> v7 : acesscontrol changes
  */
 static void
-config_migrate_simple ( const char *dir, htsmsg_t **orig,
+config_migrate_simple ( const char *dir, htsmsg_t *list,
                         void (*modify)(htsmsg_t *record,
                                        uint32_t id,
                                        const char *uuid,
@@ -632,25 +632,31 @@ config_migrate_simple ( const char *dir, htsmsg_t **orig,
   tvh_uuid_t u;
   uint32_t index = 1, id;
 
-  if (!(c = hts_settings_load_r(1, dir)))
+  if (!(c = hts_settings_load(dir)))
     return;
 
   HTSMSG_FOREACH(f, c) {
     if (!(e = htsmsg_field_get_map(f))) continue;
+    uuid_init_hex(&u, NULL);
     if (htsmsg_get_u32(e, "id", &id))
       id = 0;
+    else if (list) {
+      htsmsg_t *m = htsmsg_create_map();
+      char buf[16];
+      snprintf(buf, sizeof(buf), "%d", id);
+      htsmsg_add_str(m, "id", buf);
+      htsmsg_add_str(m, "uuid", u.hex);
+      htsmsg_add_msg(list, NULL, m);
+    }
     htsmsg_delete_field(e, "id");
     htsmsg_add_u32(e, "index", index++);
-    uuid_init_hex(&u, NULL);
-    modify(e, id, u.hex, aux);
+    if (modify)
+      modify(e, id, u.hex, aux);
     hts_settings_save(e, "%s/%s", dir, u.hex);
     hts_settings_remove("%s/%s", dir, f->hmf_name);
   }
 
-  if (orig)
-    *orig = c;
-  else
-    htsmsg_destroy(c);
+  htsmsg_destroy(c);
 }
 
 static void
@@ -731,6 +737,96 @@ config_migrate_v8 ( void )
   htsmsg_destroy(ch);
 }
 
+static void
+config_modify_autorec( htsmsg_t *c, uint32_t id, const char *uuid, void *aux )
+{
+  uint32_t u32;
+  htsmsg_delete_field(c, "index");
+  if (!htsmsg_get_u32(c, "approx_time", &u32)) {
+    if (u32 == 0)
+      u32 = -1;
+    htsmsg_delete_field(c, "approx_time");
+    htsmsg_add_u32(c, "start", u32);
+  }
+  if (!htsmsg_get_u32(c, "contenttype", &u32)) {
+    htsmsg_delete_field(c, "contenttype");
+    htsmsg_add_u32(c, "content_type", u32 / 16);
+  }
+}
+
+static void
+config_modify_dvr_log( htsmsg_t *c, uint32_t id, const char *uuid, void *aux )
+{
+  htsmsg_t *list = aux;
+  const char *chname = htsmsg_get_str(c, "channelname");
+  const char *chuuid = htsmsg_get_str(c, "channel");
+  htsmsg_t *e;
+  htsmsg_field_t *f;
+  tvh_uuid_t uuid0;
+  const char *s1;
+  uint32_t u32;
+
+  htsmsg_delete_field(c, "index");
+  if (chname == NULL || (chuuid != NULL && uuid_init_bin(&uuid0, chuuid))) {
+    chname = strdup(chuuid);
+    htsmsg_delete_field(c, "channelname");
+    htsmsg_delete_field(c, "channel");
+    htsmsg_add_str(c, "channelname", chname);
+    free((char *)chname);
+    if (!htsmsg_get_u32(c, "contenttype", &u32)) {
+      htsmsg_delete_field(c, "contenttype");
+      htsmsg_add_u32(c, "content_type", u32 / 16);
+    }
+  }
+  if ((s1 = htsmsg_get_str(c, "autorec")) != NULL) {
+    s1 = strdup(s1);
+    htsmsg_delete_field(c, "autorec");
+    HTSMSG_FOREACH(f, list) {
+      if (!(e = htsmsg_field_get_map(f))) continue;
+      if (strcmp(s1, htsmsg_get_str(e, "id")) == 0) {
+        htsmsg_add_str(c, "autorec", htsmsg_get_str(e, "uuid"));
+        break;
+      }
+    }
+  }
+}
+
+static void
+config_migrate_v9 ( void )
+{
+  htsmsg_t *list = htsmsg_create_list();
+  htsmsg_t *c, *e;
+  htsmsg_field_t *f;
+  tvh_uuid_t u;
+
+  config_migrate_simple("autorec", list, config_modify_autorec, NULL);
+  config_migrate_simple("dvr/log", NULL, config_modify_dvr_log, list);
+  htsmsg_destroy(list);
+
+  if ((c = hts_settings_load("dvr")) != NULL) {
+    /* step 1: only "config" */
+    HTSMSG_FOREACH(f, c) {
+      if (!(e = htsmsg_field_get_map(f))) continue;
+      if (strcmp(f->hmf_name, "config")) continue;
+      htsmsg_add_str(e, "name", f->hmf_name + 6);
+      uuid_init_hex(&u, NULL);
+      hts_settings_remove("dvr/%s", f->hmf_name);
+      hts_settings_save(e, "dvr/config/%s", u.hex);
+    }
+    /* step 2: reset (without "config") */
+    HTSMSG_FOREACH(f, c) {
+      if (!(e = htsmsg_field_get_map(f))) continue;
+      if (strcmp(f->hmf_name, "config") == 0) continue;
+      if (strncmp(f->hmf_name, "config", 6)) continue;
+      htsmsg_add_str(e, "name", f->hmf_name + 6);
+      uuid_init_hex(&u, NULL);
+      hts_settings_remove("dvr/%s", f->hmf_name);
+      hts_settings_save(e, "dvr/config/%s", u.hex);
+    }
+    htsmsg_destroy(c);
+  }
+}
+
 /*
  * Migration table
  */
@@ -743,6 +839,7 @@ static const config_migrate_t config_migrate_table[] = {
   config_migrate_v6,
   config_migrate_v7,
   config_migrate_v8,
+  config_migrate_v9,
 };
 
 /*
index 4cf131495abed542294943f50b63d778dbc88edd..015b5b8a949a51f4f545a63f19e9f5e2534ece78 100644 (file)
 #include "lang_str.h"
 
 typedef struct dvr_config {
+  idnode_t dvr_id;
+  LIST_ENTRY(dvr_config) config_link;
+
+  int dvr_enabled;
   char *dvr_config_name;
   char *dvr_storage;
   uint32_t dvr_retention_days;
@@ -34,11 +38,28 @@ typedef struct dvr_config {
   char *dvr_charset;
   char *dvr_charset_id;
   char *dvr_postproc;
-  int dvr_extra_time_pre;
-  int dvr_extra_time_post;
-
-  muxer_container_type_t dvr_mc;
-  muxer_config_t         dvr_muxcnf;
+  uint32_t dvr_extra_time_pre;
+  uint32_t dvr_extra_time_post;
+
+  int dvr_mc;
+  muxer_config_t dvr_muxcnf;
+
+  int dvr_dir_per_day;
+  int dvr_channel_dir;
+  int dvr_channel_in_title;
+  int dvr_date_in_title;
+  int dvr_time_in_title;
+  int dvr_whitespace_in_title;
+  int dvr_title_dir;
+  int dvr_episode_in_title;
+  int dvr_clean_title;
+  int dvr_tag_files;
+  int dvr_skip_commercials;
+  int dvr_subtitle_in_title;
+  int dvr_episode_before_date;
+  int dvr_episode_duplicate;
+  int dvr_rewrite_pat;
+  int dvr_rewrite_pmt;
 
   /* Series link support */
   int dvr_sl_brand_lock;
@@ -51,7 +72,6 @@ typedef struct dvr_config {
   /* Duplicate detect */
   int dvr_dup_detect_episode;
 
-  LIST_ENTRY(dvr_config) config_link;
 } dvr_config_t;
 
 extern struct dvr_config_list dvrconfigs;
@@ -106,6 +126,8 @@ typedef enum {
 
 typedef struct dvr_entry {
 
+  idnode_t de_id;
+
   int de_refcnt;   /* Modification is protected under global_lock */
 
 
@@ -115,7 +137,6 @@ typedef struct dvr_entry {
    */
 
   LIST_ENTRY(dvr_entry) de_global_link;
-  int de_id;
   
   channel_t *de_channel;
   LIST_ENTRY(dvr_entry) de_channel_link;
@@ -128,6 +149,7 @@ typedef struct dvr_entry {
    * These meta fields will stay valid as long as reference count > 0
    */
 
+  dvr_config_t *de_config;
   char *de_config_name;
 
   time_t de_start;
@@ -141,15 +163,13 @@ typedef struct dvr_entry {
                          generated yet */
   lang_str_t *de_title;      /* Title in UTF-8 (from EPG) */
   lang_str_t *de_desc;       /* Description in UTF-8 (from EPG) */
-  epg_genre_t de_content_type; /* Content type (from EPG) */
+  uint32_t de_content_type;  /* Content type (from EPG) (only code) */
 
   uint16_t de_dvb_eid;
 
-  dvr_prio_t de_pri;
-
-  uint32_t de_dont_reschedule;
-
-  muxer_container_type_t de_mc;
+  int de_pri;
+  int de_dont_reschedule;
+  int de_mc;
 
   /**
    * EPG information / links
@@ -214,9 +234,11 @@ typedef struct dvr_entry {
  * Autorec entry
  */
 typedef struct dvr_autorec_entry {
+  idnode_t dae_id;
+
   TAILQ_ENTRY(dvr_autorec_entry) dae_link;
-  char *dae_id;
 
+  char *dae_name;
   char *dae_config_name;
 
   int dae_enabled;
@@ -226,11 +248,11 @@ typedef struct dvr_autorec_entry {
   char *dae_title;
   regex_t dae_title_preg;
   
-  epg_genre_t dae_content_type;
+  uint32_t dae_content_type;
 
-  int dae_approx_time; /* Minutes from midnight */
+  int dae_start;  /* Minutes from midnight */
 
-  int dae_weekdays;
+  uint32_t dae_weekdays;
 
   channel_t *dae_channel;
   LIST_ENTRY(dvr_autorec_entry) dae_channel_link;
@@ -251,6 +273,17 @@ typedef struct dvr_autorec_entry {
   int dae_maxduration;
 } dvr_autorec_entry_t;
 
+TAILQ_HEAD(dvr_autorec_entry_queue, dvr_autorec_entry);
+
+extern struct dvr_autorec_entry_queue autorec_entries;
+
+/**
+ *
+ */
+
+extern const idclass_t dvr_config_class;
+extern const idclass_t dvr_entry_class;
+extern const idclass_t dvr_autorec_entry_class;
 
 /**
  * Prototypes
@@ -262,10 +295,17 @@ dvr_config_t *dvr_config_find_by_name(const char *name);
 
 dvr_config_t *dvr_config_find_by_name_default(const char *name);
 
-dvr_config_t *dvr_config_create(const char *name);
+dvr_config_t *dvr_config_create(const char *name, const char *uuid, htsmsg_t *conf);
+
+static inline dvr_config_t *dvr_config_find_by_uuid(const char *uuid)
+  { return (dvr_config_t*)idnode_find(uuid, &dvr_config_class); }
 
 void dvr_config_delete(const char *name);
 
+void dvr_config_save(dvr_config_t *cfg);
+
+int dvr_entry_get_mc( dvr_entry_t *de);
+
 void dvr_entry_notify(dvr_entry_t *de);
 
 void dvr_entry_save(dvr_entry_t *de);
@@ -276,28 +316,34 @@ const char *dvr_entry_schedstatus(dvr_entry_t *de);
 
 void dvr_entry_create_by_autorec(epg_broadcast_t *e, dvr_autorec_entry_t *dae);
 
-dvr_entry_t *dvr_entry_create_by_event
-  (const char *dvr_config_name,
-   epg_broadcast_t *e, 
-   time_t start_extra, time_t stop_extra,
-   const char *creator,
-   dvr_autorec_entry_t *dae,
-   dvr_prio_t pri);
-
-dvr_entry_t *dvr_entry_create
-  (const char *dvr_config_name,
-   channel_t *ch, time_t start, time_t stop, 
-   time_t start_extra, time_t stop_extra,
-        const char *title, const char *description, const char *lang,
-   epg_genre_t *content_type,
-        const char *creator, dvr_autorec_entry_t *dae,
-        dvr_prio_t pri);
-
-dvr_entry_t *dvr_entry_update
-  (dvr_entry_t *de,
-   const char* de_title, const char *de_desc, const char *lang, 
-   time_t de_start, time_t de_stop,
-   time_t de_start_extra, time_t de_stop_extra );
+void dvr_entry_created(dvr_entry_t *de);
+
+dvr_entry_t *
+dvr_entry_create ( const char *uuid, htsmsg_t *conf );
+
+
+dvr_entry_t *
+dvr_entry_create_by_event( const char *dvr_config_uuid,
+                           epg_broadcast_t *e,
+                           time_t start_extra, time_t stop_extra,
+                           const char *creator,
+                           dvr_autorec_entry_t *dae,
+                           dvr_prio_t pri );
+
+dvr_entry_t *
+dvr_entry_create_htsp( const char *dvr_config_uuid,
+                       channel_t *ch, time_t start, time_t stop,
+                       time_t start_extra, time_t stop_extra,
+                       const char *title, const char *description,
+                       const char *lang, epg_genre_t *content_type,
+                       const char *creator, dvr_autorec_entry_t *dae,
+                       dvr_prio_t pri );
+
+dvr_entry_t *
+dvr_entry_update( dvr_entry_t *de,
+                  const char* de_title, const char *de_desc, const char *lang,
+                  time_t de_start, time_t de_stop,
+                  time_t de_start_extra, time_t de_stop_extra );
 
 void dvr_init(void);
 
@@ -321,6 +367,9 @@ void dvr_event_updated(epg_broadcast_t *e);
 
 dvr_entry_t *dvr_entry_find_by_id(int id);
 
+static inline dvr_entry_t *dvr_entry_find_by_uuid(const char *uuid)
+  { return (dvr_entry_t*)idnode_find(uuid, &dvr_entry_class); }
+
 dvr_entry_t *dvr_entry_find_by_event(epg_broadcast_t *e);
 
 dvr_entry_t *dvr_entry_find_by_event_fuzzy(epg_broadcast_t *e);
@@ -333,34 +382,13 @@ dvr_entry_t *dvr_entry_cancel(dvr_entry_t *de);
 
 void dvr_entry_dec_ref(dvr_entry_t *de);
 
-void dvr_storage_set(dvr_config_t *cfg, const char *storage);
-
-void dvr_charset_set(dvr_config_t *cfg, const char *charset);
-
-void dvr_container_set(dvr_config_t *cfg, const char *container);
-
-void dvr_file_permissions_set(dvr_config_t *cfg, int permissions);
-
-void dvr_directory_permissions_set(dvr_config_t *cfg, int permissions);
-
-void dvr_mux_cache_set(dvr_config_t *cfg, int mcache);
-
-void dvr_postproc_set(dvr_config_t *cfg, const char *postproc);
-
-void dvr_retention_set(dvr_config_t *cfg, int days);
-
-void dvr_flags_set(dvr_config_t *cfg, int flags);
-
-void dvr_mux_flags_set(dvr_config_t *cfg, int flags);
-
-void dvr_extra_time_pre_set(dvr_config_t *cfg, int d);
-
-void dvr_extra_time_post_set(dvr_config_t *cfg, int d);
-
 void dvr_entry_delete(dvr_entry_t *de);
 
 void dvr_entry_cancel_delete(dvr_entry_t *de);
 
+htsmsg_t *dvr_entry_class_pri_list(void *o);
+htsmsg_t *dvr_entry_class_config_name_list(void *o);
+
 /**
  * Query interface
  */
@@ -386,15 +414,21 @@ int dvr_sort_start_ascending(const void *A, const void *B);
 /**
  *
  */
-void dvr_autorec_add(const char *dvr_config_name,
-                     const char *title, const char *channel,
-                     const char *tag, epg_genre_t *content_type,
-                     const int min_duration, const int max_duration,
-                     const char *creator, const char *comment);
 
-void dvr_autorec_add_series_link(const char *dvr_config_name,
-                                 epg_broadcast_t *event,
-                                 const char *creator, const char *comment);
+dvr_autorec_entry_t *
+dvr_autorec_create(const char *uuid, htsmsg_t *conf);
+
+dvr_autorec_entry_t *
+dvr_autorec_add_series_link(const char *dvr_config_name,
+                            epg_broadcast_t *event,
+                            const char *creator, const char *comment);
+
+void dvr_autorec_save(dvr_autorec_entry_t *dae);
+
+static inline dvr_autorec_entry_t *
+dvr_autorec_find_by_uuid(const char *uuid)
+  { return (dvr_autorec_entry_t*)idnode_find(uuid, &dvr_autorec_entry_class); }
+
 
 void dvr_autorec_check_event(epg_broadcast_t *e);
 void dvr_autorec_check_brand(epg_brand_t *b);
@@ -404,8 +438,6 @@ void dvr_autorec_check_serieslink(epg_serieslink_t *s);
 
 void autorec_destroy_by_channel(channel_t *ch, int delconf);
 
-dvr_autorec_entry_t *autorec_entry_find(const char *id, int create);
-
 /**
  *
  */
index f477d3967effc395523a5764165024361834c88b..75ba7234a394d1c3e5dfb38c25bcbbe9d7ad6893 100644 (file)
 #include "dtable.h"
 #include "epg.h"
 
-dtable_t *autorec_dt;
-
-TAILQ_HEAD(dvr_autorec_entry_queue, dvr_autorec_entry);
-
 static int dvr_autorec_in_init = 0;
 
 struct dvr_autorec_entry_queue autorec_entries;
@@ -80,13 +76,13 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e)
 
   if(dae->dae_channel == NULL &&
      dae->dae_channel_tag == NULL &&
-     dae->dae_content_type.code == 0 &&
+     dae->dae_content_type == 0 &&
      (dae->dae_title == NULL ||
      dae->dae_title[0] == '\0') &&
      dae->dae_brand == NULL &&
      dae->dae_season == NULL &&
      dae->dae_minduration == 0 &&
-     dae->dae_maxduration == 0 &&
+     (dae->dae_maxduration == 0 || dae->dae_maxduration > 24 * 3600) &&
      dae->dae_serieslink == NULL)
     return 0; // Avoid super wildcard match
 
@@ -124,18 +120,21 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e)
       return 0;
   }
 
-  if(dae->dae_content_type.code != 0) {
-    if (!epg_genre_list_contains(&e->episode->genre, &dae->dae_content_type, 1))
+  if(dae->dae_content_type != 0) {
+    epg_genre_t ct;
+    memset(&ct, 0, sizeof(ct));
+    ct.code = dae->dae_content_type;
+    if (!epg_genre_list_contains(&e->episode->genre, &ct, 1))
       return 0;
   }
 
-  if(dae->dae_approx_time != 0) {
+  if(dae->dae_start >= 0) {
     struct tm a_time;
     struct tm ev_time;
     localtime_r(&e->start, &a_time);
     localtime_r(&e->start, &ev_time);
-    a_time.tm_min = dae->dae_approx_time % 60;
-    a_time.tm_hour = dae->dae_approx_time / 60;
+    a_time.tm_min = dae->dae_start % 60;
+    a_time.tm_hour = dae->dae_start / 60;
     if(abs(mktime(&a_time) - mktime(&ev_time)) > 900)
       return 0;
   }
@@ -159,54 +158,76 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e)
   return 1;
 }
 
-
 /**
  *
  */
 dvr_autorec_entry_t *
-autorec_entry_find(const char *id, int create)
+dvr_autorec_create(const char *uuid, htsmsg_t *conf)
 {
   dvr_autorec_entry_t *dae;
-  char buf[20];
-  static int tally;
 
-  if(id != NULL) {
-    TAILQ_FOREACH(dae, &autorec_entries, dae_link)
-      if(!strcmp(dae->dae_id, id))
-       return dae;
-  }
+  dae = calloc(1, sizeof(*dae));
 
-  if(create == 0)
+  if (idnode_insert(&dae->dae_id, uuid, &dvr_autorec_entry_class, 0)) {
+    if (uuid)
+      tvhwarn("dvr", "invalid autorec entry uuid '%s'", uuid);
+    free(dae);
     return NULL;
-
-  dae = calloc(1, sizeof(dvr_autorec_entry_t));
-  if(id == NULL) {
-    tally++;
-    snprintf(buf, sizeof(buf), "%d", tally);
-    id = buf;
-  } else {
-    tally = MAX(atoi(id), tally);
   }
+
   dae->dae_weekdays = 0x7f;
   dae->dae_pri = DVR_PRIO_NORMAL;
+  dae->dae_start = -1;
 
-  dae->dae_id = strdup(id);
   TAILQ_INSERT_TAIL(&autorec_entries, dae, dae_link);
+
+  idnode_load(&dae->dae_id, conf);
+
   return dae;
 }
 
-
+/**
+ *
+ */
+dvr_autorec_entry_t *
+dvr_autorec_add_series_link(const char *dvr_config_name,
+                            epg_broadcast_t *event,
+                            const char *creator, const char *comment)
+{
+  dvr_autorec_entry_t *dae;
+  htsmsg_t *conf;
+  char *title;
+  if (!event || !event->episode || !event->serieslink)
+    return NULL;
+  conf = htsmsg_create_map();
+  title = regexp_escape(epg_broadcast_get_title(event, NULL));
+  htsmsg_add_str(conf, "title", title);
+  free(title);
+  htsmsg_add_str(conf, "config_name", dvr_config_name ?: "");
+  htsmsg_add_str(conf, "channel", channel_get_name(event->channel));
+  htsmsg_add_str(conf, "serieslink", event->serieslink->uri);
+  htsmsg_add_str(conf, "creator", creator ?: "");
+  htsmsg_add_str(conf, "comment", comment ?: "");
+  dae = dvr_autorec_create(NULL, conf);
+  htsmsg_destroy(conf);
+  return dae;
+}
 
 /**
  *
  */
 static void
-autorec_entry_destroy(dvr_autorec_entry_t *dae)
+autorec_entry_destroy(dvr_autorec_entry_t *dae, int delconf)
 {
   dvr_autorec_purge_spawns(dae);
 
-  free(dae->dae_id);
+  if (delconf)
+    hts_settings_remove("dvr/autorec/%s", idnode_uuid_as_str(&dae->dae_id));
 
+  TAILQ_REMOVE(&autorec_entries, dae, dae_link);
+  idnode_unlink(&dae->dae_id);
+
+  free(dae->dae_name);
   free(dae->dae_config_name);
   free(dae->dae_creator);
   free(dae->dae_comment);
@@ -228,268 +249,578 @@ autorec_entry_destroy(dvr_autorec_entry_t *dae)
     dae->dae_season->putref(dae->dae_season);
   if(dae->dae_serieslink)
     dae->dae_serieslink->putref(dae->dae_serieslink);
-  
 
-  TAILQ_REMOVE(&autorec_entries, dae, dae_link);
   free(dae);
 }
 
 /**
  *
  */
-static void
-build_weekday_tags(htsmsg_t *l, int mask)
+void
+dvr_autorec_save(dvr_autorec_entry_t *dae)
 {
-  int i;
-  for(i = 0; i < 7; i++) {
-    if(mask & (1 << i))
-      htsmsg_add_u32(l, NULL, i+1);
-  }
-}
+  htsmsg_t *m = htsmsg_create_map();
 
-/**
- *
- */
-static int
-build_weekday_mask(htsmsg_t *l)
-{
-  int r = 0;
-  uint32_t u32;
-  htsmsg_field_t *f;
-  HTSMSG_FOREACH(f, l)
-    if (!htsmsg_field_get_u32(f, &u32))
-      r |= 1 << (u32 - 1);
-  return r;
+  lock_assert(&global_lock);
+
+  idnode_save(&dae->dae_id, m);
+  hts_settings_save(m, "dvr/autorec/%s", idnode_uuid_as_str(&dae->dae_id));
+  htsmsg_destroy(m);
 }
 
+/* **************************************************************************
+ * DVR Autorec Entry Class definition
+ * **************************************************************************/
 
-/**
- *
- */
-static htsmsg_t *
-autorec_record_build(dvr_autorec_entry_t *dae)
+static void
+dvr_autorec_entry_class_save(idnode_t *self)
 {
-  htsmsg_t *e = htsmsg_create_map();
-  htsmsg_t *l = htsmsg_create_list();
-
-  htsmsg_add_str(e, "id", dae->dae_id);
-  htsmsg_add_u32(e, "enabled",  !!dae->dae_enabled);
-
-  if (dae->dae_config_name != NULL)
-    htsmsg_add_str(e, "config_name", dae->dae_config_name);
-  if(dae->dae_creator != NULL)
-    htsmsg_add_str(e, "creator", dae->dae_creator);
-  if(dae->dae_comment != NULL)
-    htsmsg_add_str(e, "comment", dae->dae_comment);
-
-  if(dae->dae_channel != NULL)
-    htsmsg_add_str(e, "channel", channel_get_uuid(dae->dae_channel));
+  dvr_autorec_save((dvr_autorec_entry_t *)self);
+}
 
-  if(dae->dae_channel_tag != NULL)
-    htsmsg_add_str(e, "tag", dae->dae_channel_tag->ct_name);
+static void
+dvr_autorec_entry_class_delete(idnode_t *self)
+{
+  autorec_entry_destroy((dvr_autorec_entry_t *)self, 1);
+}
 
-  htsmsg_add_u32(e, "contenttype",dae->dae_content_type.code);
+static const char *
+dvr_autorec_entry_class_get_title (idnode_t *self)
+{
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)self;
+  const char *s = "";
+  if (dae->dae_name && dae->dae_name[0] != '\0')
+    s = dae->dae_name;
+  else if (dae->dae_comment && dae->dae_comment[0] != '\0')
+    s = dae->dae_comment;
+  return s;
+}
 
-  htsmsg_add_str(e, "title", dae->dae_title ?: "");
+static int
+dvr_autorec_entry_class_channel_set(void *o, const void *v)
+{
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  channel_t *ch = v ? channel_find_by_uuid(v) : NULL;
+  if (ch == NULL) ch = v ? channel_find_by_name(v) : NULL;
+  if (ch == NULL) {
+    if (dae->dae_channel) {
+      LIST_REMOVE(dae, dae_channel_link);
+      dae->dae_channel = NULL;
+      return 1;
+    }
+  } else if (dae->dae_channel != ch) {
+    if (dae->dae_channel)
+      LIST_REMOVE(dae, dae_channel_link);
+    dae->dae_channel = ch;
+    LIST_INSERT_HEAD(&ch->ch_autorecs, dae, dae_channel_link);
+    return 1;
+  }
+  return 0;
+}
 
-  htsmsg_add_u32(e, "approx_time", dae->dae_approx_time);
+static const void *
+dvr_autorec_entry_class_channel_get(void *o)
+{
+  static const char *ret;
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  if (dae->dae_channel)
+    ret = idnode_uuid_as_str(&dae->dae_channel->ch_id);
+  else
+    ret = "";
+  return &ret;
+}
 
-  build_weekday_tags(l, dae->dae_weekdays);
-  htsmsg_add_msg(e, "weekdays", l);
+static htsmsg_t *
+dvr_autorec_entry_class_channel_list(void *o)
+{
+  htsmsg_t *m = htsmsg_create_map();
+  htsmsg_add_str(m, "type",  "api");
+  htsmsg_add_str(m, "uri",   "channel/list");
+  htsmsg_add_str(m, "event", "channel");
+  return m;
+}
 
-  if (dae->dae_minduration)
-    htsmsg_add_u32(e, "minduration", dae->dae_minduration);
-  if (dae->dae_maxduration)
-    htsmsg_add_u32(e, "maxduration", dae->dae_maxduration);
+static int
+dvr_autorec_entry_class_title_set(void *o, const void *v)
+{
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  const char *title = v ?: "";
+  if (strcmp(title, dae->dae_title ?: "")) {
+    if (dae->dae_title) {
+       regfree(&dae->dae_title_preg);
+       free(dae->dae_title);
+       dae->dae_title = NULL;
+    }
+    if (title != NULL && title[0] != '\0' &&
+        !regcomp(&dae->dae_title_preg, title,
+                 REG_ICASE | REG_EXTENDED | REG_NOSUB))
+      dae->dae_title = strdup(title);
+    return 1;
+  }
+  return 0;
+}
 
-  htsmsg_add_str(e, "pri", dvr_val2pri(dae->dae_pri));
-  
-  if (dae->dae_brand)
-    htsmsg_add_str(e, "brand", dae->dae_brand->uri);
-  if (dae->dae_season)
-    htsmsg_add_str(e, "season", dae->dae_season->uri);
-  if (dae->dae_serieslink)
-    htsmsg_add_str(e, "serieslink", dae->dae_serieslink->uri);
+static int
+dvr_autorec_entry_class_tag_set(void *o, const void *v)
+{
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  channel_tag_t *tag = v ? channel_tag_find_by_uuid(v) : NULL;
+  if (tag == NULL) tag = v ? channel_tag_find_by_name(v, 0) : NULL;
+  if (tag == NULL && dae->dae_channel_tag) {
+    LIST_REMOVE(dae, dae_channel_tag_link);
+    dae->dae_channel_tag = NULL;
+    return 1;
+  } else if (dae->dae_channel_tag != tag) {
+    if (dae->dae_channel_tag)
+      LIST_REMOVE(dae, dae_channel_tag_link);
+    dae->dae_channel_tag = tag;
+    LIST_INSERT_HEAD(&tag->ct_autorecs, dae, dae_channel_tag_link);
+    return 1;
+  }
+  return 0;
+}
 
-  return e;
+static const void *
+dvr_autorec_entry_class_tag_get(void *o)
+{
+  static const char *ret;
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  if (dae->dae_channel_tag)
+    ret = idnode_uuid_as_str(&dae->dae_channel_tag->ct_id);
+  else
+    ret = "";
+  return &ret;
 }
 
-/**
- *
- */
 static htsmsg_t *
-autorec_record_get_all(void *opaque)
+dvr_autorec_entry_class_tag_list(void *o)
 {
-  htsmsg_t *r = htsmsg_create_list();
-  dvr_autorec_entry_t *dae;
-
-  TAILQ_FOREACH(dae, &autorec_entries, dae_link)
-    htsmsg_add_msg(r, NULL, autorec_record_build(dae));
+  htsmsg_t *e, *m = htsmsg_create_map();
+  htsmsg_add_str(m, "type",  "api");
+  htsmsg_add_str(m, "uri",   "channeltag/list");
+  htsmsg_add_str(m, "event", "channeltag");
+  e = htsmsg_create_map();
+  htsmsg_add_msg(m, "params", e);
+  return m;
+}
 
-  return r;
+static int
+dvr_autorec_entry_class_time_set(void *o, const void *v, int *tm)
+{
+  const char *s = v;
+  int t;
+
+  if(s == NULL || s[0] == '\0')
+    t = -1;
+  else if(strchr(s, ':') != NULL)
+    // formatted time string - convert
+    t = (atoi(s) * 60) + atoi(s + 3);
+  else {
+    t = atoi(s);
+  }
+  if (t != *tm) {
+    *tm = t;
+    return 1;
+  }
+  return 0;
 }
 
-/**
- *
- */
-static htsmsg_t *
-autorec_record_get(void *opaque, const char *id)
+static int
+dvr_autorec_entry_class_start_set(void *o, const void *v)
 {
-  dvr_autorec_entry_t *ae;
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  return dvr_autorec_entry_class_time_set(o, v, &dae->dae_start);
+}
 
-  if((ae = autorec_entry_find(id, 0)) == NULL)
-    return NULL;
-  return autorec_record_build(ae);
+#if 0
+static int
+dvr_autorec_entry_class_stop_set(void *o, const void *v)
+{
+  return dvr_autorec_entry_class_time_set(o, v, &dae->dae_stop);
 }
+#endif
 
+static const void *
+dvr_autorec_entry_class_time_get(void *o, int tm)
+{
+  static const char *ret;
+  static char buf[16];
+  if (tm >= 0)
+    snprintf(buf, sizeof(buf), "%02d:%02d", tm / 60, tm % 60);
+  else
+    strcpy(buf, "Not set");
+  ret = buf;
+  return &ret;
+}
 
-/**
- *
- */
-static htsmsg_t *
-autorec_record_create(void *opaque)
+static const void *
+dvr_autorec_entry_class_start_get(void *o)
 {
-  return autorec_record_build(autorec_entry_find(NULL, 1));
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  return dvr_autorec_entry_class_time_get(o, dae->dae_start);
 }
 
+#if 0
+static int
+dvr_autorec_entry_class_stop_get(void *o)
+{
+  return dvr_autorec_entry_class_time_get(o, v, &dae->dae_stop);
+}
+#endif
 
-/**
- *
- */
 static htsmsg_t *
-autorec_record_update(void *opaque, const char *id, htsmsg_t *values, 
-                     int maycreate)
+dvr_autorec_entry_class_time_list(void *o)
 {
-  int save;
-  dvr_autorec_entry_t *dae;
-  const char *s;
-  channel_t *ch;
-  channel_tag_t *ct;
-  uint32_t u32;
-  htsmsg_t *l;
-
-  if((dae = autorec_entry_find(id, maycreate)) == NULL)
-    return NULL;
-
-  tvh_str_update(&dae->dae_config_name, htsmsg_get_str(values, "config_name"));
-  tvh_str_update(&dae->dae_creator, htsmsg_get_str(values, "creator"));
-  tvh_str_update(&dae->dae_comment, htsmsg_get_str(values, "comment"));
-
-  if((s = htsmsg_get_str(values, "channel")) != NULL) {
-    if(dae->dae_channel != NULL) {
-      LIST_REMOVE(dae, dae_channel_link);
-      dae->dae_channel = NULL;
-    }
-    ch = channel_find(s);
-    if (!ch) ch = channel_find_by_name(s);
-    if (ch) {
-      LIST_INSERT_HEAD(&ch->ch_autorecs, dae, dae_channel_link);
-      dae->dae_channel = ch;
-    }
+  int i;
+  htsmsg_t *e, *l = htsmsg_create_list();
+  char buf[16];
+  e = htsmsg_create_map();
+  htsmsg_add_str(e, "key", "");
+  htsmsg_add_str(e, "val", "Not set");
+  htsmsg_add_msg(l, NULL, e);
+  for (i = 0; i < 24*60;  i += 10) {
+    snprintf(buf, sizeof(buf), "%02d:%02d", i / 60, (i % 60));
+    e = htsmsg_create_map();
+    htsmsg_add_str(e, "key", buf);
+    htsmsg_add_str(e, "val", buf);
+    htsmsg_add_msg(l, NULL, e);
   }
+  return l;
+}
 
-  if((s = htsmsg_get_str(values, "title")) != NULL) {
-    if(dae->dae_title != NULL) {
-      free(dae->dae_title);
-      dae->dae_title = NULL;
-      regfree(&dae->dae_title_preg);
-    }
+static htsmsg_t *
+dvr_autorec_entry_class_minduration_list(void *o)
+{
+  /* reuse */
+  return dvr_autorec_entry_class_time_list(o);
+}
 
-    if(!regcomp(&dae->dae_title_preg, s,
-               REG_ICASE | REG_EXTENDED | REG_NOSUB)) {
-      dae->dae_title = strdup(s);
-    }
+static int
+dvr_autorec_entry_class_config_name_set(void *o, const void *v)
+{
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  dvr_config_t *cfg = v ? dvr_config_find_by_uuid(v) : NULL;
+  if (cfg == NULL) cfg = v ? dvr_config_find_by_name_default(v): NULL;
+  if (cfg == NULL && dae->dae_config_name) {
+    free(dae->dae_config_name);
+    return 1;
+  } else if (strcmp(dae->dae_config_name ?: "", cfg ? cfg->dvr_config_name : "")) {
+    free(dae->dae_config_name);
+    dae->dae_config_name = strdup(cfg->dvr_config_name);
+    return 1;
   }
+  return 0;
+}
 
-  if((s = htsmsg_get_str(values, "tag")) != NULL) {
-    if(dae->dae_channel_tag != NULL) {
-      LIST_REMOVE(dae, dae_channel_tag_link);
-      dae->dae_channel_tag = NULL;
-    }
-    if((ct = channel_tag_find_by_name(s, 0)) != NULL) {
-      LIST_INSERT_HEAD(&ct->ct_autorecs, dae, dae_channel_tag_link);
-      dae->dae_channel_tag = ct;
-    }
-  }
+static int
+dvr_autorec_entry_class_weekdays_set(void *o, const void *v)
+{
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  htsmsg_field_t *f;
+  uint32_t u32, bits = 0;
 
-  if (!htsmsg_get_u32(values, "contenttype", &u32))
-    dae->dae_content_type.code = u32;
-
-  if((s = htsmsg_get_str(values, "approx_time")) != NULL) {
-    if(strchr(s, ':') != NULL) {
-      // formatted time string - convert
-      dae->dae_approx_time = (atoi(s) * 60) + atoi(s + 3);
-    } else if(strlen(s) == 0) {
-      dae->dae_approx_time = 0;
-    } else {
-      dae->dae_approx_time = atoi(s);
-    }
-  }
+  HTSMSG_FOREACH(f, (htsmsg_t *)v)
+    if (!htsmsg_field_get_u32(f, &u32) && u32 > 0 && u32 < 8)
+      bits |= (1 << (u32 - 1));
 
-  if(!htsmsg_get_u32(values, "minduration", &u32))
-    dae->dae_minduration = u32;
+  if (bits != dae->dae_weekdays) {
+    dae->dae_weekdays = bits;
+    return 1;
+  }
+  return 0;
+}
 
-  if(!htsmsg_get_u32(values, "maxduration", &u32))
-    dae->dae_maxduration = u32;
+static const void *
+dvr_autorec_entry_class_weekdays_get(void *o)
+{
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  htsmsg_t *m = htsmsg_create_list();
+  int i;
+  for (i = 0; i < 7; i++)
+    if (dae->dae_weekdays & (1 << i))
+      htsmsg_add_u32(m, NULL, i + 1);
+  return m;
+}
 
-  if((l = htsmsg_get_list(values, "weekdays")) != NULL)
-    dae->dae_weekdays = build_weekday_mask(l);
+static const struct strtab dvr_autorec_entry_class_weekdays_tab[] = {
+  { "Mon", 1 },
+  { "Tue", 2 },
+  { "Wed", 3 },
+  { "Thu", 4 },
+  { "Fri", 5 },
+  { "Sat", 6 },
+  { "Sun", 7 },
+};
 
-  if(!htsmsg_get_u32(values, "enabled", &u32))
-    dae->dae_enabled = u32;
+static htsmsg_t *
+dvr_autorec_entry_class_weekdays_list ( void *o )
+{
+  return strtab2htsmsg(dvr_autorec_entry_class_weekdays_tab);
+}
 
-  if((s = htsmsg_get_str(values, "pri")) != NULL)
-    dae->dae_pri = dvr_pri2val(s);
+static char *
+dvr_autorec_entry_class_weekdays_rend(void *o)
+{
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  static char buf[32];
+  size_t l;
+  int i;
+  if (dae->dae_weekdays == 0x7f)
+    strcpy(buf + 1, "All days");
+  else if (dae->dae_weekdays == 0)
+    strcpy(buf + 1, "No days");
+  else {
+    buf[0] = '\0';
+    for (i = 0; i < 7; i++)
+      if (dae->dae_weekdays & (1 << i)) {
+        l = strlen(buf);
+        snprintf(buf + l, sizeof(buf) - l, ",%s",
+                 val2str(i + 1, dvr_autorec_entry_class_weekdays_tab));
+      }
+  }
+  return buf + 1;
+}
 
-  if((s = htsmsg_get_str(values, "brand")) != NULL) {
-    dae->dae_brand = epg_brand_find_by_uri(s, 1, &save);
+static int
+dvr_autorec_entry_class_brand_set(void *o, const void *v)
+{
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  int save;
+  epg_brand_t *brand = epg_brand_find_by_uri(v, 1, &save);
+  if (brand && dae->dae_brand != brand) {
     if (dae->dae_brand)
-      dae->dae_brand->getref((epg_object_t*)dae->dae_brand);
+      dae->dae_brand->putref((epg_object_t*)dae->dae_brand);
+    brand->getref((epg_object_t*)brand);
+    dae->dae_brand = brand;
+    return 1;
+  } else if (brand == NULL && dae->dae_brand) {
+    dae->dae_brand->putref((epg_object_t*)dae->dae_brand);
+    dae->dae_brand = NULL;
+    return 1;
   }
-  if((s = htsmsg_get_str(values, "season")) != NULL) {
-    dae->dae_season = epg_season_find_by_uri(s, 1, &save);
+  return 0;
+}
+
+static const void *
+dvr_autorec_entry_class_brand_get(void *o)
+{
+  static const char *ret;
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  if (dae->dae_brand)
+    ret = dae->dae_brand->uri;
+  else
+    ret = "";
+  return &ret;
+}
+
+static int
+dvr_autorec_entry_class_season_set(void *o, const void *v)
+{
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  int save;
+  epg_season_t *season = epg_season_find_by_uri(v, 1, &save);
+  if (season && dae->dae_season != season) {
     if (dae->dae_season)
-      dae->dae_season->getref((epg_object_t*)dae->dae_season);
-  }
-  if((s = htsmsg_get_str(values, "serieslink")) != NULL) {
-    dae->dae_serieslink = epg_serieslink_find_by_uri(s, 1, &save);
-    if (dae->dae_serieslink)
-      dae->dae_serieslink->getref(dae->dae_serieslink);
+      dae->dae_season->putref((epg_object_t*)dae->dae_season);
+    season->getref((epg_object_t*)season);
+    dae->dae_season = season;
+    return 1;
+  } else if (season == NULL && dae->dae_season) {
+    dae->dae_season->putref((epg_object_t*)dae->dae_season);
+    dae->dae_season = NULL;
+    return 1;
   }
-  if (!dvr_autorec_in_init)
-    dvr_autorec_changed(dae, 1);
-
-  return autorec_record_build(dae);
+  return 0;
 }
 
+static const void *
+dvr_autorec_entry_class_season_get(void *o)
+{
+  static const char *ret;
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  if (dae->dae_season)
+    ret = dae->dae_season->uri;
+  else
+    ret = "";
+  return &ret;
+}
 
-/**
- *
- */
 static int
-autorec_record_delete(void *opaque, const char *id)
+dvr_autorec_entry_class_series_link_set(void *o, const void *v)
 {
-  dvr_autorec_entry_t *dae;
-
-  if((dae = autorec_entry_find(id, 0)) == NULL)
-    return -1;
-  autorec_entry_destroy(dae);
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  int save;
+  epg_serieslink_t *sl = epg_serieslink_find_by_uri(v, 1, &save);
+  if (sl && dae->dae_serieslink != sl) {
+    if (dae->dae_serieslink)
+      dae->dae_serieslink->putref((epg_object_t*)dae->dae_season);
+    sl->getref((epg_object_t*)sl);
+    dae->dae_serieslink = sl;
+    return 1;
+  } else if (sl == NULL && dae->dae_serieslink) {
+    dae->dae_season->putref((epg_object_t*)dae->dae_season);
+    dae->dae_season = NULL;
+    return 1;
+  }
   return 0;
 }
 
+static const void *
+dvr_autorec_entry_class_series_link_get(void *o)
+{
+  static const char *ret;
+  dvr_autorec_entry_t *dae = (dvr_autorec_entry_t *)o;
+  if (dae->dae_serieslink)
+    ret = dae->dae_serieslink->uri;
+  else
+    ret = "";
+  return &ret;
+}
 
-/**
- *
- */
-static const dtable_class_t autorec_dtc = {
-  .dtc_record_get     = autorec_record_get,
-  .dtc_record_get_all = autorec_record_get_all,
-  .dtc_record_create  = autorec_record_create,
-  .dtc_record_update  = autorec_record_update,
-  .dtc_record_delete  = autorec_record_delete,
-  .dtc_read_access = ACCESS_RECORDER,
-  .dtc_write_access = ACCESS_RECORDER,
-  .dtc_mutex = &global_lock,
+static htsmsg_t *
+dvr_autorec_entry_class_content_type_list(void *o)
+{
+  htsmsg_t *m = htsmsg_create_map();
+  htsmsg_add_str(m, "type",  "api");
+  htsmsg_add_str(m, "uri",   "epg/content_type/list");
+  return m;
+}
+
+const idclass_t dvr_autorec_entry_class = {
+  .ic_class      = "dvrautorec",
+  .ic_caption    = "DVR Auto-Record Entry",
+  .ic_save       = dvr_autorec_entry_class_save,
+  .ic_get_title  = dvr_autorec_entry_class_get_title,
+  .ic_delete     = dvr_autorec_entry_class_delete,
+  .ic_properties = (const property_t[]) {
+    {
+      .type     = PT_BOOL,
+      .id       = "enabled",
+      .name     = "Enabled",
+      .off      = offsetof(dvr_autorec_entry_t, dae_enabled),
+    },
+    {
+      .type     = PT_STR,
+      .id       = "name",
+      .name     = "Name",
+      .off      = offsetof(dvr_autorec_entry_t, dae_name),
+    },
+    {
+      .type     = PT_STR,
+      .id       = "title",
+      .name     = "Title (Regexp)",
+      .set      = dvr_autorec_entry_class_title_set,
+      .off      = offsetof(dvr_autorec_entry_t, dae_title),
+    },
+    {
+      .type     = PT_STR,
+      .id       = "channel",
+      .name     = "Channel",
+      .set      = dvr_autorec_entry_class_channel_set,
+      .get      = dvr_autorec_entry_class_channel_get,
+      .list     = dvr_autorec_entry_class_channel_list,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "tag",
+      .name     = "Channel Tag",
+      .set      = dvr_autorec_entry_class_tag_set,
+      .get      = dvr_autorec_entry_class_tag_get,
+      .list     = dvr_autorec_entry_class_tag_list,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "start",
+      .name     = "Starting Around",
+      .set      = dvr_autorec_entry_class_start_set,
+      .get      = dvr_autorec_entry_class_start_get,
+      .list     = dvr_autorec_entry_class_time_list,
+    },
+    {
+      .type     = PT_U32,
+      .islist   = 1,
+      .id       = "weekdays",
+      .name     = "Week Days",
+      .set      = dvr_autorec_entry_class_weekdays_set,
+      .get      = dvr_autorec_entry_class_weekdays_get,
+      .list     = dvr_autorec_entry_class_weekdays_list,
+      .rend     = dvr_autorec_entry_class_weekdays_rend,
+    },
+    {
+      .type     = PT_U32,
+      .id       = "minduration",
+      .name     = "Minimal Duration",
+      .list     = dvr_autorec_entry_class_minduration_list,
+      .off      = offsetof(dvr_autorec_entry_t, dae_minduration),
+    },
+    {
+      .type     = PT_U32,
+      .id       = "maxduration",
+      .name     = "Maximal Duration",
+      .off      = offsetof(dvr_autorec_entry_t, dae_maxduration),
+    },
+    {
+      .type     = PT_U32,
+      .id       = "content_type",
+      .name     = "Content Type",
+      .list     = dvr_autorec_entry_class_content_type_list,
+      .off      = offsetof(dvr_autorec_entry_t, dae_content_type),
+    },
+    {
+      .type     = PT_U32,
+      .id       = "pri",
+      .name     = "Priority",
+      .list     = dvr_entry_class_pri_list,
+      .def.i    = DVR_PRIO_NORMAL,
+      .off      = offsetof(dvr_autorec_entry_t, dae_pri),
+    },
+    {
+      .type     = PT_STR,
+      .id       = "config_name",
+      .name     = "DVR Configuration",
+      .set      = dvr_autorec_entry_class_config_name_set,
+      .list     = dvr_entry_class_config_name_list,
+      .off      = offsetof(dvr_autorec_entry_t, dae_config_name),
+    },
+    {
+      .type     = PT_STR,
+      .id       = "brand",
+      .name     = "Brand",
+      .set      = dvr_autorec_entry_class_brand_set,
+      .get      = dvr_autorec_entry_class_brand_get,
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "season",
+      .name     = "Season",
+      .set      = dvr_autorec_entry_class_season_set,
+      .get      = dvr_autorec_entry_class_season_get,
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "serieslink",
+      .name     = "Series Link",
+      .set      = dvr_autorec_entry_class_series_link_set,
+      .get      = dvr_autorec_entry_class_series_link_get,
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "creator",
+      .name     = "Creator",
+      .off      = offsetof(dvr_autorec_entry_t, dae_creator),
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "comment",
+      .name     = "Comment",
+      .off      = offsetof(dvr_autorec_entry_t, dae_comment),
+    },
+    {}
+  }
 };
 
 /**
@@ -498,10 +829,18 @@ static const dtable_class_t autorec_dtc = {
 void
 dvr_autorec_init(void)
 {
+  htsmsg_t *l, *c;
+  htsmsg_field_t *f;
+
   TAILQ_INIT(&autorec_entries);
-  autorec_dt = dtable_create(&autorec_dtc, "autorec", NULL);
   dvr_autorec_in_init = 1;
-  dtable_load(autorec_dt);
+  if((l = hts_settings_load("dvr/autorec")) != NULL) {
+    HTSMSG_FOREACH(f, l) {
+      if((c = htsmsg_get_map_by_field(f)) == NULL)
+        continue;
+      (void *)dvr_autorec_create(f->hmf_name, c);
+    }
+  }
   dvr_autorec_in_init = 0;
 }
 
@@ -511,12 +850,9 @@ dvr_autorec_done(void)
   dvr_autorec_entry_t *dae;
 
   pthread_mutex_lock(&global_lock);
-  while ((dae = TAILQ_FIRST(&autorec_entries)) != NULL) {
-    TAILQ_REMOVE(&autorec_entries, dae, dae_link);
-    free(dae);
-  }
+  while ((dae = TAILQ_FIRST(&autorec_entries)) != NULL)
+    autorec_entry_destroy(dae, 0);
   pthread_mutex_unlock(&global_lock);
-  dtable_delete("autorec");
 }
 
 void
@@ -528,107 +864,6 @@ dvr_autorec_update(void)
   }
 }
 
-static void
-_dvr_autorec_add(const char *config_name,
-                const char *title, channel_t *ch,
-                const char *tag, epg_genre_t *content_type,
-                const int min_duration, const int max_duration,
-    epg_brand_t *brand, epg_season_t *season,
-    epg_serieslink_t *serieslink,
-    int approx_time, epg_episode_num_t *epnum,
-               const char *creator, const char *comment)
-{
-  dvr_autorec_entry_t *dae;
-  htsmsg_t *m;
-  channel_tag_t *ct;
-
-  if((dae = autorec_entry_find(NULL, 1)) == NULL)
-    return;
-
-  tvh_str_set(&dae->dae_config_name, config_name);
-  tvh_str_set(&dae->dae_creator, creator);
-  tvh_str_set(&dae->dae_comment, comment);
-
-  if(ch) {
-    LIST_INSERT_HEAD(&ch->ch_autorecs, dae, dae_channel_link);
-    dae->dae_channel = ch;
-  }
-
-  if(title != NULL &&
-     !regcomp(&dae->dae_title_preg, title,
-             REG_ICASE | REG_EXTENDED | REG_NOSUB)) {
-    dae->dae_title = strdup(title);
-  }
-
-  if(tag != NULL && (ct = channel_tag_find_by_uuid(tag)) != NULL) {
-    LIST_INSERT_HEAD(&ct->ct_autorecs, dae, dae_channel_tag_link);
-    dae->dae_channel_tag = ct;
-  }
-
-  dae->dae_enabled = 1;
-  if (content_type)
-    dae->dae_content_type.code = content_type->code;
-
-  if (min_duration)
-    dae->dae_minduration = min_duration;
-
-  if (max_duration)
-    dae->dae_maxduration = max_duration;
-
-  if(serieslink) {
-    serieslink->getref(serieslink);
-    dae->dae_serieslink = serieslink;
-  }
-
-  dae->dae_approx_time = approx_time;
-
-  m = autorec_record_build(dae);
-  hts_settings_save(m, "%s/%s", "autorec", dae->dae_id);
-  htsmsg_destroy(m);
-
-  /* Notify web clients that we have messed with the tables */
-
-  notify_reload("autorec");
-
-  dvr_autorec_changed(dae, 1);
-}
-
-void
-dvr_autorec_add(const char *config_name,
-                const char *title, const char *channel,
-                const char *tag, epg_genre_t *content_type,
-                const int min_duration, const int max_duration,
-                const char *creator, const char *comment)
-{
-  channel_t *ch = NULL;
-  if(channel != NULL) ch = channel_find(channel);
-  _dvr_autorec_add(config_name, title, ch, tag, content_type,
-                   min_duration, max_duration,
-                   NULL, NULL, NULL, 0, NULL, creator, comment);
-}
-
-void dvr_autorec_add_series_link 
-  ( const char *dvr_config_name, epg_broadcast_t *event,
-    const char *creator, const char *comment )
-{
-  char *title;
-  if (!event || !event->episode) return;
-  title = regexp_escape(epg_broadcast_get_title(event, NULL));
-  _dvr_autorec_add(dvr_config_name,
-                   title,
-                   event->channel,
-                   NULL, 0, // tag/content type
-                   0,INT_MAX,
-                   NULL,
-                   NULL,
-                   event->serieslink,
-                   0, NULL,
-                   creator, comment);
-  if (title)
-    free(title);
-}
-
-
 /**
  *
  */
@@ -692,11 +927,8 @@ autorec_destroy_by_channel(channel_t *ch, int delconf)
   dvr_autorec_entry_t *dae;
   htsmsg_t *m;
 
-  while((dae = LIST_FIRST(&ch->ch_autorecs)) != NULL) {
-    if (delconf)
-      dtable_record_erase(autorec_dt, dae->dae_id);
-    autorec_entry_destroy(dae);
-  }
+  while((dae = LIST_FIRST(&ch->ch_autorecs)) != NULL)
+    autorec_entry_destroy(dae, delconf);
 
   /* Notify web clients that we have messed with the tables */
   m = htsmsg_create_map();
index 7f75dc630ccb6bb8da2dd7a1931142887214b96a..5b1b80ed4e90734e925191eceaa61f61e1b917c0 100644 (file)
@@ -31,8 +31,8 @@
 #include "streaming.h"
 #include "intlconv.h"
 #include "dbus.h"
-
-static int de_tally;
+#include "imagecache.h"
+#include "access.h"
 
 int dvr_iov_max;
 
@@ -43,9 +43,51 @@ struct dvr_entry_list dvrentries;
 static gtimer_t dvr_dbus_timer;
 #endif
 
+static void dvr_entry_remove(dvr_entry_t *de, int delconf);
 static void dvr_timer_expire(void *aux);
 static void dvr_timer_start_recording(void *aux);
 static void dvr_timer_stop_recording(void *aux);
+static int dvr_entry_class_disp_title_set(void *o, const void *v);
+
+/*
+ * Start / stop time calculators
+ */
+static int
+dvr_entry_get_start_time( dvr_entry_t *de )
+{
+  time_t extra = de->de_start_extra;
+
+  if (extra == 0) {
+    if (de->de_channel)
+      extra = de->de_channel->ch_dvr_extra_time_pre;
+    else
+      extra = de->de_config->dvr_extra_time_pre;
+  }
+  /* Note 30 seconds might not be enough (rotors) */
+  return de->de_start - (60 * extra) - 30;
+}
+
+static int
+dvr_entry_get_stop_time( dvr_entry_t *de )
+{
+  time_t extra = de->de_stop_extra;
+
+  if (extra == 0) {
+    if (de->de_channel)
+      extra = de->de_channel->ch_dvr_extra_time_post;
+    else
+      extra = de->de_config->dvr_extra_time_pre;
+  }
+  return de->de_stop + (60 * extra);
+}
+
+int
+dvr_entry_get_mc( dvr_entry_t *de )
+{
+  if (de->de_mc >= 0)
+    return de->de_mc;
+  return de->de_config->dvr_mc;
+}
 
 /*
  * DBUS next dvr start notifications
@@ -55,7 +97,7 @@ static void
 dvr_dbus_timer_cb( void *aux )
 {
   dvr_entry_t *de;
-  time_t result, preamble, max = 0;
+  time_t result, start, max = 0;
   static time_t last_result = 0;
 
   lock_assert(&global_lock);
@@ -64,18 +106,18 @@ dvr_dbus_timer_cb( void *aux )
   LIST_FOREACH(de, &dvrentries, de_global_link) {
     if (de->de_sched_state != DVR_SCHEDULED)
       continue;
-    preamble = de->de_start - (60 * de->de_start_extra) - 30;
-    if (dispatch_clock < preamble && preamble > max)
-      max = preamble;
+    start = dvr_entry_get_start_time(de);
+    if (dispatch_clock < start && start > max)
+      max = start;
   }
   /* lower the maximum value */
   result = max;
   LIST_FOREACH(de, &dvrentries, de_global_link) {
     if (de->de_sched_state != DVR_SCHEDULED)
       continue;
-    preamble = de->de_start - (60 * de->de_start_extra) - 30;
-    if (dispatch_clock < preamble && preamble < result)
-      result = preamble;
+    start = dvr_entry_get_start_time(de);
+    if (dispatch_clock < start && start < result)
+      result = start;
   }
   /* different? send it.... */
   if (result && result != last_result) {
@@ -167,30 +209,6 @@ dvr_entry_schedstatus(dvr_entry_t *de)
   }
 }
 
-
-/**
- *
- */
-static void
-dvrdb_changed(void)
-{
-  htsmsg_t *m = htsmsg_create_map();
-  htsmsg_add_u32(m, "reload", 1);
-  notify_by_msg("dvrdb", m);
-}
-
-/**
- *
- */
-static void
-dvrconfig_changed(void)
-{
-  htsmsg_t *m = htsmsg_create_map();
-  htsmsg_add_u32(m, "reload", 1);
-  notify_by_msg("dvrconfig", m);
-}
-
-
 /**
  *
  */
@@ -200,7 +218,7 @@ dvr_entry_notify(dvr_entry_t *de)
   htsmsg_t *m = htsmsg_create_map();
 
   htsmsg_add_u32(m, "updateEntry", 1);
-  htsmsg_add_u32(m, "id", de->de_id);
+  htsmsg_add_str(m, "uuid", idnode_uuid_as_str(&de->de_id));
   htsmsg_add_str(m, "status", dvr_entry_status(de));
   htsmsg_add_str(m, "schedstate", dvr_entry_schedstatus(de));
   notify_by_msg("dvrdb", m);
@@ -210,10 +228,11 @@ dvr_entry_notify(dvr_entry_t *de)
 /**
  *
  */
-static void
+static int
 dvr_charset_update(dvr_config_t *cfg, const char *charset)
 {
   const char *s, *id;
+  int change = strcmp(cfg->dvr_charset ?: "", charset ?: "");
 
   free(cfg->dvr_charset);
   free(cfg->dvr_charset_id);
@@ -221,6 +240,7 @@ dvr_charset_update(dvr_config_t *cfg, const char *charset)
   id = intlconv_charset_id(s, 1, 1);
   cfg->dvr_charset = s ? strdup(s) : NULL;
   cfg->dvr_charset_id = id ? strdup(id) : NULL;
+  return change;
 }
 
 /**
@@ -231,7 +251,7 @@ dvr_make_title(char *output, size_t outlen, dvr_entry_t *de)
 {
   struct tm tm;
   char buf[40];
-  dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name);
+  dvr_config_t *cfg = de->de_config;
 
   if(cfg->dvr_flags & DVR_CHANNEL_IN_TITLE)
     snprintf(output, outlen, "%s-", DVR_CH_NAME(de));
@@ -283,52 +303,44 @@ dvr_make_title(char *output, size_t outlen, dvr_entry_t *de)
 static void
 dvr_entry_set_timer(dvr_entry_t *de)
 {
-  time_t now, preamble;
-  dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name);
+  time_t now, start, stop;
+  dvr_config_t *cfg = de->de_config;
 
   time(&now);
 
-  preamble = de->de_start - (60 * de->de_start_extra) - 30;
+  start = dvr_entry_get_start_time(de);
+  stop  = dvr_entry_get_stop_time(de);
+
+  if(now >= stop || de->de_dont_reschedule) {
 
-  if(now >= de->de_stop || de->de_dont_reschedule) {
     if(de->de_filename == NULL)
       de->de_sched_state = DVR_MISSED_TIME;
     else
       _dvr_entry_completed(de);
     gtimer_arm_abs(&de->de_timer, dvr_timer_expire, de, 
-              de->de_stop + cfg->dvr_retention_days * 86400);
+                   de->de_stop + cfg->dvr_retention_days * 86400);
 
   } else if (de->de_sched_state == DVR_RECORDING)  {
-    gtimer_arm_abs(&de->de_timer, dvr_timer_stop_recording, de,
-                  de->de_stop + (60 * de->de_stop_extra));
+
+    gtimer_arm_abs(&de->de_timer, dvr_timer_stop_recording, de, stop);
 
   } else if (de->de_channel) {
+
     de->de_sched_state = DVR_SCHEDULED;
 
-    tvhtrace("dvr", "entry timer scheduled for %"PRItime_t, preamble);
-    gtimer_arm_abs(&de->de_timer, dvr_timer_start_recording, de, preamble);
+    tvhtrace("dvr", "entry timer scheduled for %"PRItime_t, start);
+    gtimer_arm_abs(&de->de_timer, dvr_timer_start_recording, de, start);
 #if ENABLE_DBUS_1
     gtimer_arm(&dvr_dbus_timer, dvr_dbus_timer_cb, NULL, 5);
 #endif
+
   } else {
+
     de->de_sched_state = DVR_NOSTATE;
+
   }
 }
 
-/**
- *
- */
-static void
-dvr_entry_link(dvr_entry_t *de)
-{
-  de->de_refcnt = 1;
-
-  LIST_INSERT_HEAD(&dvrentries, de, de_global_link);
-
-  dvr_entry_set_timer(de);
-
-  htsp_dvr_entry_add(de);
-}
 
 /**
  * Find dvr entry using 'fuzzy' search
@@ -364,115 +376,155 @@ dvr_entry_fuzzy_match(dvr_entry_t *de, epg_broadcast_t *e)
 }
 
 /**
- * Create the event
+ * Create the event from config
  */
-static dvr_entry_t *_dvr_entry_create (
-  const char *config_name, epg_broadcast_t *e,
-  channel_t *ch, time_t start, time_t stop, 
-  time_t start_extra, time_t stop_extra,
-       const char *title, const char *description, const char *lang,
-  epg_genre_t *content_type,
-       const char *creator, dvr_autorec_entry_t *dae,
-       dvr_prio_t pri)
+dvr_entry_t *
+dvr_entry_create(const char *uuid, htsmsg_t *conf)
 {
-  dvr_entry_t *de;
-  char tbuf[64];
-  struct tm tm;
-  time_t t;
-  dvr_config_t *cfg = dvr_config_find_by_name_default(config_name);
+  dvr_entry_t *de, *de2;
+  int64_t start, stop;
+  const char *s;
 
-  LIST_FOREACH(de, &ch->ch_dvrs, de_channel_link)
-    if(de->de_start == start && de->de_sched_state != DVR_COMPLETED)
+  if (conf) {
+    if (htsmsg_get_s64(conf, "start", &start))
       return NULL;
+    if (htsmsg_get_s64(conf, "stop", &stop))
+      return NULL;
+    if ((htsmsg_get_str(conf, "channel")) == NULL &&
+        (htsmsg_get_str(conf, "channelname")) == NULL)
+      return NULL;
+  }
+
+  de = calloc(1, sizeof(*de));
 
-  de = calloc(1, sizeof(dvr_entry_t));
-  de->de_id = ++de_tally;
+  if (idnode_insert(&de->de_id, uuid, &dvr_entry_class, IDNODE_SHORT_UUID)) {
+    if (uuid)
+      tvhwarn("dvr", "invalid entry uuid '%s'", uuid);
+    free(de);
+    return NULL;
+  }
 
-  ch = de->de_channel = ch;
-  LIST_INSERT_HEAD(&de->de_channel->ch_dvrs, de, de_channel_link);
+  de->de_mc = -1;
+  idnode_load(&de->de_id, conf);
 
-  de->de_mc = cfg->dvr_mc;
+  /* special case, becaous PO_NOSAVE, load ignores it */
+  if (de->de_title == NULL &&
+      (s = htsmsg_get_str(conf, "disp_title")) != NULL)
+    dvr_entry_class_disp_title_set(de, s);
 
-  de->de_start   = start;
-  de->de_stop    = stop;
-  de->de_pri     = pri;
-  if (start_extra)
-    de->de_start_extra = start_extra;
-  else if (ch->ch_dvr_extra_time_pre)
-    de->de_start_extra = ch->ch_dvr_extra_time_pre;
-  else
-    de->de_start_extra = cfg->dvr_extra_time_pre;
-  if (stop_extra)
-    de->de_stop_extra = stop_extra;
-  else if (ch->ch_dvr_extra_time_post)
-    de->de_stop_extra  = ch->ch_dvr_extra_time_post;
-  else
-    de->de_stop_extra  = cfg->dvr_extra_time_post;
-  de->de_config_name = strdup(cfg->dvr_config_name);
-  de->de_creator = strdup(creator);
+  de->de_refcnt = 1;
+
+  LIST_INSERT_HEAD(&dvrentries, de, de_global_link);
+
+  if (de->de_channel) {
+    LIST_FOREACH(de2, &de->de_channel->ch_dvrs, de_channel_link)
+      if(de2 != de &&
+         de2->de_start == de->de_start &&
+         de2->de_sched_state != DVR_COMPLETED) {
+        dvr_entry_remove(de, 0);
+        return NULL;
+      }
+  }
+
+
+  dvr_entry_set_timer(de);
+  htsp_dvr_entry_add(de);
 
-  de->de_desc  = NULL;
-  // TODO: this really needs updating
+  return de;
+}
+
+/**
+ * Create the event
+ */
+static dvr_entry_t *
+_dvr_entry_create(const char *config_uuid, epg_broadcast_t *e,
+                  channel_t *ch, time_t start, time_t stop,
+                  time_t start_extra, time_t stop_extra,
+                  const char *title, const char *description,
+                  const char *lang, epg_genre_t *content_type,
+                  const char *creator, dvr_autorec_entry_t *dae,
+                  dvr_prio_t pri)
+{
+  dvr_entry_t *de;
+  char tbuf[64];
+  struct tm tm;
+  time_t t;
+  lang_str_t *l;
+  htsmsg_t *conf;
+
+  conf = htsmsg_create_map();
+  htsmsg_add_s64(conf, "start", start);
+  htsmsg_add_s64(conf, "stop", stop);
+  htsmsg_add_str(conf, "channel", idnode_uuid_as_str(&ch->ch_id));
+  htsmsg_add_u32(conf, "pri", pri);
+  htsmsg_add_str(conf, "config_name", config_uuid ?: "");
+  htsmsg_add_s64(conf, "start_extra", start_extra);
+  htsmsg_add_s64(conf, "stop_extra", stop_extra);
+  htsmsg_add_str(conf, "creator", creator ?: "");
   if (e) {
-    de->de_dvb_eid = e->dvb_eid;
+    htsmsg_add_u32(conf, "dvb_eid", e->dvb_eid);
     if (e->episode && e->episode->title)
-      de->de_title = lang_str_copy(e->episode->title);
+      lang_str_serialize(e->episode->title, conf, "title");
     if (e->description)
-      de->de_desc  = lang_str_copy(e->description);
+      lang_str_serialize(e->description, conf, "description");
     else if (e->episode && e->episode->description)
-      de->de_desc = lang_str_copy(e->episode->description);
+      lang_str_serialize(e->episode->description, conf, "description");
     else if (e->summary)
-      de->de_desc = lang_str_copy(e->summary);
+      lang_str_serialize(e->summary, conf, "description");
     else if (e->episode && e->episode->summary)
-      de->de_desc = lang_str_copy(e->episode->summary);
+      lang_str_serialize(e->episode->summary, conf, "description");
   } else if (title) {
-    de->de_title = lang_str_create();
-    lang_str_add(de->de_title, title, lang, 0);
+    l = lang_str_create();
+    lang_str_add(l, title, lang, 0);
+    lang_str_serialize(l, conf, "title");
     if (description) {
-      de->de_desc = lang_str_create();
-      lang_str_add(de->de_desc, description, lang, 0);
+      l = lang_str_create();
+      lang_str_add(l, description, lang, 0);
+      lang_str_serialize(l, conf, "description");
     }
   }
-  if (content_type) de->de_content_type = *content_type;
-  de->de_bcast   = e;
-  if (e) e->getref((epg_object_t*)e);
+  if (content_type)
+    htsmsg_add_u32(conf, "content_type", content_type->code / 16);
+  if (e)
+    htsmsg_add_u32(conf, "broadcast", e->id);
+  if (dae)
+    htsmsg_add_str(conf, "autorec", idnode_uuid_as_str(&dae->dae_id));
+
+  de = dvr_entry_create(NULL, conf);
 
-  dvr_entry_link(de);
+  htsmsg_destroy(conf);
+
+  if (de == NULL)
+    return NULL;
 
-  t = de->de_start - de->de_start_extra * 60;
+  t = dvr_entry_get_start_time(de);
   localtime_r(&t, &tm);
   if (strftime(tbuf, sizeof(tbuf), "%F %T", &tm) <= 0)
     *tbuf = 0;
 
-  if(dae != NULL) {
-    de->de_autorec = dae;
-    LIST_INSERT_HEAD(&dae->dae_spawns, de, de_autorec_link);
-  }
-
-  tvhlog(LOG_INFO, "dvr", "entry %d \"%s\" on \"%s\" starting at %s, "
+  tvhlog(LOG_INFO, "dvr", "entry %s \"%s\" on \"%s\" starting at %s, "
         "scheduled for recording by \"%s\"",
-   de->de_id,
+         idnode_uuid_as_str(&de->de_id),
         lang_str_get(de->de_title, NULL), DVR_CH_NAME(de), tbuf, creator);
-        
-  dvrdb_changed();
+
   dvr_entry_save(de);
   return de;
 }
 
-
 /**
  *
  */
 dvr_entry_t *
-dvr_entry_create(const char *config_name,
-                 channel_t *ch, time_t start, time_t stop, 
-                 time_t start_extra, time_t stop_extra,
-                            const char *title, const char *description, const char *lang,
-                 epg_genre_t *content_type,
-                            const char *creator, dvr_autorec_entry_t *dae,
-                 dvr_prio_t pri)
-{
-  return _dvr_entry_create(config_name, NULL,
+dvr_entry_create_htsp(const char *config_uuid,
+                      channel_t *ch, time_t start, time_t stop,
+                      time_t start_extra, time_t stop_extra,
+                      const char *title,
+                      const char *description, const char *lang,
+                      epg_genre_t *content_type,
+                      const char *creator, dvr_autorec_entry_t *dae,
+                      dvr_prio_t pri)
+{
+  return _dvr_entry_create(config_uuid, NULL,
                            ch, start, stop, start_extra, stop_extra,
                            title, description, lang, content_type,
                            creator, dae, pri);
@@ -482,7 +534,7 @@ dvr_entry_create(const char *config_name,
  *
  */
 dvr_entry_t *
-dvr_entry_create_by_event(const char *config_name,
+dvr_entry_create_by_event(const char *config_uuid,
                           epg_broadcast_t *e,
                           time_t start_extra, time_t stop_extra,
                           const char *creator, 
@@ -491,7 +543,7 @@ dvr_entry_create_by_event(const char *config_name,
   if(!e->channel || !e->episode || !e->episode->title)
     return NULL;
 
-  return _dvr_entry_create(config_name, e,
+  return _dvr_entry_create(config_uuid, e,
                            e->channel, e->start, e->stop,
                            start_extra, stop_extra,
                            NULL, NULL, NULL,
@@ -518,8 +570,7 @@ static int _dvr_duplicate_event ( epg_broadcast_t *e )
       if (de->de_bcast->episode == e->episode) return 1;
 
       if (has_epnum) {
-        dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name);
-        int ep_dup_det = (cfg->dvr_flags & DVR_EPISODE_DUPLICATE_DETECTION);
+        int ep_dup_det = (de->de_config->dvr_flags & DVR_EPISODE_DUPLICATE_DETECTION);
 
         if (ep_dup_det) {
           const char* de_title = lang_str_get(de->de_bcast->episode->title, NULL);
@@ -556,7 +607,6 @@ dvr_entry_create_by_autorec(epg_broadcast_t *e, dvr_autorec_entry_t *dae)
   dvr_entry_create_by_event(dae->dae_config_name, e, 0, 0, buf, dae, dae->dae_pri);
 }
 
-
 /**
  *
  */
@@ -570,6 +620,8 @@ dvr_entry_dec_ref(dvr_entry_t *de)
     return;
   }
 
+  idnode_unlink(&de->de_id);
+
   if(de->de_autorec != NULL)
     LIST_REMOVE(de, de_autorec_link);
 
@@ -578,14 +630,11 @@ dvr_entry_dec_ref(dvr_entry_t *de)
   free(de->de_creator);
   if (de->de_title) lang_str_destroy(de->de_title);
   if (de->de_desc)  lang_str_destroy(de->de_desc);
-  if(de->de_bcast) de->de_bcast->putref((epg_object_t*)de->de_bcast);
+  if (de->de_bcast) de->de_bcast->putref((epg_object_t*)de->de_bcast);
 
   free(de);
 }
 
-
-
-
 /**
  *
  */
@@ -593,7 +642,7 @@ static void
 dvr_entry_remove(dvr_entry_t *de, int delconf)
 {
   if (delconf)
-    hts_settings_remove("dvr/log/%d", de->de_id);
+    hts_settings_remove("dvr/log/%s", idnode_uuid_as_str(&de->de_id));
 
   htsp_dvr_entry_delete(de);
   
@@ -613,123 +662,9 @@ dvr_entry_remove(dvr_entry_t *de, int delconf)
   free(de->de_channel_name);
   de->de_channel_name = NULL;
 
-  dvrdb_changed();
-
   dvr_entry_dec_ref(de);
 }
 
-
-/**
- *
- */
-static void
-dvr_db_load_one(htsmsg_t *c, int id)
-{
-  dvr_entry_t *de;
-  const char *chuuid, *chname, *s, *creator;
-  channel_t *ch;
-  uint32_t start, stop, bcid, u32;
-  int d;
-  dvr_config_t *cfg;
-  lang_str_t *title, *ls;
-
-  if(htsmsg_get_u32(c, "start", &start))
-    return;
-  if(htsmsg_get_u32(c, "stop", &stop))
-    return;
-
-  chname = htsmsg_get_str(c, "channelname");
-  chuuid = htsmsg_get_str(c, "channel");
-  ch     = chuuid ? channel_find(chuuid) : NULL;
-
-  /* Backwards compat */
-  if (!ch && !chname) {
-    chname = chuuid;
-    ch     = channel_find_by_name(chname);
-  }
-    
-  s = htsmsg_get_str(c, "config_name");
-  cfg = dvr_config_find_by_name_default(s);
-
-  if(!(title = lang_str_deserialize(c, "title")))
-    return;
-
-  if((creator = htsmsg_get_str(c, "creator")) == NULL)
-    return;
-
-  de = calloc(1, sizeof(dvr_entry_t));
-  de->de_id = id;
-
-  de_tally = MAX(id, de_tally);
-
-  if (ch) {
-    de->de_channel = ch;
-    LIST_INSERT_HEAD(&de->de_channel->ch_dvrs, de, de_channel_link);
-  } else {
-    de->de_channel_name = strdup(chname);
-  }
-
-  de->de_start   = start;
-  de->de_stop    = stop;
-  de->de_config_name = strdup(cfg->dvr_config_name);
-  de->de_creator = strdup(creator);
-  de->de_title   = title;
-  de->de_pri     = dvr_pri2val(htsmsg_get_str(c, "pri"));
-  if (!htsmsg_get_u32(c, "dvb_eid", &u32))
-    de->de_dvb_eid = (uint16_t)u32;
-  
-  if(htsmsg_get_s32(c, "start_extra", &d))
-    if (ch && ch->ch_dvr_extra_time_pre)
-      de->de_start_extra = ch->ch_dvr_extra_time_pre;
-    else
-      de->de_start_extra = cfg->dvr_extra_time_pre;
-  else
-    de->de_start_extra = d;
-
-  if(htsmsg_get_s32(c, "stop_extra", &d))
-    if (ch && ch->ch_dvr_extra_time_post)
-      de->de_stop_extra = ch->ch_dvr_extra_time_post;
-    else
-      de->de_stop_extra = cfg->dvr_extra_time_post;
-  else
-    de->de_stop_extra = d;
-
-
-  if ((ls = lang_str_deserialize(c, "description")))
-    de->de_desc = ls;
-  tvh_str_set(&de->de_filename, htsmsg_get_str(c, "filename"));
-
-  htsmsg_get_u32(c, "errorcode", &de->de_last_error);
-  htsmsg_get_u32(c, "errors", &de->de_errors);
-
-  htsmsg_get_u32(c, "noresched", &de->de_dont_reschedule);
-
-  s = htsmsg_get_str(c, "autorec");
-  if(s != NULL) {
-    dvr_autorec_entry_t *dae = autorec_entry_find(s, 0);
-
-    if(dae != NULL) {
-      de->de_autorec = dae;
-      LIST_INSERT_HEAD(&dae->dae_spawns, de, de_autorec_link);
-    }
-  }
-
-
-  de->de_content_type.code = htsmsg_get_u32_or_default(c, "contenttype", 0);
-
-  if (!htsmsg_get_u32(c, "broadcast", &bcid)) {
-    de->de_bcast = epg_broadcast_find_by_id(bcid, ch);
-    if (de->de_bcast) {
-      de->de_bcast->getref((epg_object_t*)de->de_bcast);
-    }
-  }
-
-  de->de_mc = htsmsg_get_u32_or_default(c, "container", MC_MATROSKA);
-
-  dvr_entry_link(de);
-}
-
-
 /**
  *
  */
@@ -743,14 +678,12 @@ dvr_db_load(void)
     HTSMSG_FOREACH(f, l) {
       if((c = htsmsg_get_map_by_field(f)) == NULL)
        continue;
-      dvr_db_load_one(c, atoi(f->hmf_name));
+      (void)dvr_entry_create(f->hmf_name, c);
     }
     htsmsg_destroy(l);
   }
 }
 
-
-
 /**
  *
  */
@@ -761,52 +694,8 @@ dvr_entry_save(dvr_entry_t *de)
 
   lock_assert(&global_lock);
 
-  if (de->de_channel)
-    htsmsg_add_str(m, "channel", channel_get_uuid(de->de_channel));
-  htsmsg_add_str(m, "channelname", DVR_CH_NAME(de));
-  htsmsg_add_u32(m, "start", de->de_start);
-  htsmsg_add_u32(m, "stop", de->de_stop);
-  htsmsg_add_s32(m, "start_extra", de->de_start_extra);
-  htsmsg_add_s32(m, "stop_extra", de->de_stop_extra);
-  
-  htsmsg_add_str(m, "config_name", de->de_config_name);
-
-  htsmsg_add_str(m, "creator", de->de_creator);
-
-  if(de->de_filename != NULL)
-    htsmsg_add_str(m, "filename", de->de_filename);
-
-  lang_str_serialize(de->de_title, m, "title");
-
-  if(de->de_dvb_eid)
-    htsmsg_add_u32(m, "dvb_eid", de->de_dvb_eid);
-
-  if(de->de_desc != NULL)
-    lang_str_serialize(de->de_desc, m, "description");
-
-  htsmsg_add_str(m, "pri", dvr_val2pri(de->de_pri));
-
-  if(de->de_last_error)
-    htsmsg_add_u32(m, "errorcode", de->de_last_error);
-
-  if(de->de_errors)
-    htsmsg_add_u32(m, "errors", de->de_errors);
-
-  htsmsg_add_u32(m, "noresched", de->de_dont_reschedule);
-
-  if(de->de_autorec != NULL)
-    htsmsg_add_str(m, "autorec", de->de_autorec->dae_id);
-
-  if(de->de_content_type.code)
-    htsmsg_add_u32(m, "contenttype", de->de_content_type.code);
-
-  if(de->de_bcast)
-    htsmsg_add_u32(m, "broadcast", de->de_bcast->id);
-
-  htsmsg_add_u32(m, "container", de->de_mc);
-
-  hts_settings_save(m, "dvr/log/%d", de->de_id);
+  idnode_save(&de->de_id, m);
+  hts_settings_save(m, "dvr/log/%s", idnode_uuid_as_str(&de->de_id));
   htsmsg_destroy(m);
 }
 
@@ -873,8 +762,8 @@ static dvr_entry_t *_dvr_entry_update
   /* Genre */
   if (e && e->episode) {
     epg_genre_t *g = LIST_FIRST(&e->episode->genre);
-    if (g && (g->code != de->de_content_type.code)) {
-      de->de_content_type.code = g->code;
+    if (g && (g->code / 16) != de->de_content_type) {
+      de->de_content_type = g->code / 16;
       save = 1;
     }
   }
@@ -930,9 +819,10 @@ dvr_event_replaced(epg_broadcast_t *e, epg_broadcast_t *new_e)
   /* Existing entry */
   if ((de = dvr_entry_find_by_event(e))) {
     tvhtrace("dvr",
-             "dvr entry %d event replaced %s on %s @ %"PRItime_t
+             "dvr entry %s event replaced %s on %s @ %"PRItime_t
              " to %"PRItime_t,
-             de->de_id, epg_broadcast_get_title(e, NULL),
+             idnode_uuid_as_str(&de->de_id),
+             epg_broadcast_get_title(e, NULL),
              channel_get_name(e->channel),
              e->start, e->stop);
 
@@ -981,9 +871,10 @@ void dvr_event_updated ( epg_broadcast_t *e )
       if (de->de_channel != e->channel) continue;
       if (dvr_entry_fuzzy_match(de, e)) {
         tvhtrace("dvr",
-                 "dvr entry %d link to event %s on %s @ %"PRItime_t
+                 "dvr entry %s link to event %s on %s @ %"PRItime_t
                  " to %"PRItime_t,
-                 de->de_id, epg_broadcast_get_title(e, NULL),
+                 idnode_uuid_as_str(&de->de_id),
+                 epg_broadcast_get_title(e, NULL),
                  channel_get_name(e->channel),
                  e->start, e->stop);
         e->getref(e);
@@ -1001,7 +892,7 @@ void dvr_event_updated ( epg_broadcast_t *e )
 static void
 dvr_stop_recording(dvr_entry_t *de, int stopcode, int delconf)
 {
-  dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name);
+  dvr_config_t *cfg = de->de_config;
 
   if (de->de_rec_state == DVR_RS_PENDING || de->de_rec_state == DVR_RS_WAIT_PROGRAM_START)
     de->de_sched_state = DVR_MISSED_TIME;
@@ -1067,7 +958,7 @@ dvr_entry_find_by_id(int id)
 {
   dvr_entry_t *de;
   LIST_FOREACH(de, &dvrentries, de_global_link)
-    if(de->de_id == id)
+    if(idnode_get_short_uuid(&de->de_id) == id)
       break;
   return de;  
 }
@@ -1147,200 +1038,721 @@ dvr_entry_purge(dvr_entry_t *de, int delconf)
     dvr_stop_recording(de, SM_CODE_SOURCE_DELETED, delconf);
 }
 
-/**
- *
- */
-void
-dvr_destroy_by_channel(channel_t *ch, int delconf)
+
+/* **************************************************************************
+ * DVR Entry Class definition
+ * **************************************************************************/
+
+static void
+dvr_entry_class_save(idnode_t *self)
 {
-  dvr_entry_t *de;
+  dvr_entry_save((dvr_entry_t *)self);
+}
 
-  while((de = LIST_FIRST(&ch->ch_dvrs)) != NULL) {
-    LIST_REMOVE(de, de_channel_link);
-    de->de_channel = NULL;
-    de->de_channel_name = strdup(channel_get_name(ch));
-    dvr_entry_purge(de, delconf);
-  }
+static void
+dvr_entry_class_delete(idnode_t *self)
+{
+  dvr_entry_remove((dvr_entry_t *)self, 1);
 }
 
-/**
- *
- */
-void
-dvr_init(void)
+static const char *
+dvr_entry_class_get_title (idnode_t *self)
 {
-  htsmsg_t *m, *l;
-  htsmsg_field_t *f;
+  dvr_entry_t *de = (dvr_entry_t *)self;
   const char *s;
-  char buf[500];
-  const char *homedir;
-  struct stat st;
-  uint32_t u32;
-  dvr_config_t *cfg;
-
-  dvr_iov_max = sysconf(_SC_IOV_MAX);
-
-  /* Default settings */
+  s = lang_str_get(de->de_title, NULL);
+  if (s == NULL || s[0] == '\0')
+    s = lang_str_get(de->de_desc, NULL);
+  return s;
+}
 
-  LIST_INIT(&dvrconfigs);
-  cfg = dvr_config_create("");
+static int
+dvr_entry_class_config_name_set(void *o, const void *v)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  const char *s = v ?: "";
+  if (strcmp(s, de->de_config_name ?: "")) {
+    free(de->de_config_name);
+    de->de_config_name = strdup(v);
+    de->de_config = dvr_config_find_by_name_default(de->de_config_name);
+    return 1;
+  }
+  /* for sure */
+  de->de_config = dvr_config_find_by_name_default(de->de_config_name);
+  return 0;
+}
 
-  /* Override settings with config */
+static int
+dvr_entry_class_channel_set(void *o, const void *v)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  channel_t   *ch = v ? channel_find_by_uuid(v) : NULL;
+  if (!de->de_config_name)
+    dvr_entry_class_config_name_set(o, "");
+  if (ch == NULL) {
+    if (de->de_channel) {
+      LIST_REMOVE(de, de_channel_link);
+      de->de_channel = NULL;
+      return 1;
+    }
+  } else if (de->de_channel != ch) {
+    if (de->de_channel)
+      LIST_REMOVE(de, de_channel_link);
+    free(de->de_channel_name);
+    de->de_channel_name = strdup(channel_get_name(ch));
+    de->de_channel = ch;
+    LIST_INSERT_HEAD(&ch->ch_dvrs, de, de_channel_link);
+    return 1;
+  }
+  return 0;
+}
 
-  l = hts_settings_load("dvr");
-  if(l != NULL) {
-    HTSMSG_FOREACH(f, l) {
-      m = htsmsg_get_map_by_field(f);
-      if(m == NULL)
-        continue;
-
-      s = htsmsg_get_str(m, "config_name");
-      cfg = dvr_config_find_by_name(s);
-      if(cfg == NULL)
-        cfg = dvr_config_create(s);
-
-      cfg->dvr_mc = htsmsg_get_u32_or_default(m, "container", MC_MATROSKA);
-      cfg->dvr_muxcnf.m_cache
-        = htsmsg_get_u32_or_default(m, "cache", MC_CACHE_DONTKEEP);
-
-      if(!htsmsg_get_u32(m, "rewrite-pat", &u32)) {
-        if (u32)
-          cfg->dvr_muxcnf.m_flags |= MC_REWRITE_PAT;
-        else
-          cfg->dvr_muxcnf.m_flags &= ~MC_REWRITE_PAT;
-      }
-      if(!htsmsg_get_u32(m, "rewrite-pmt", &u32)) {
-        if (u32)
-          cfg->dvr_muxcnf.m_flags |= MC_REWRITE_PMT;
-        else
-          cfg->dvr_muxcnf.m_flags &= ~MC_REWRITE_PMT;
-      }
+static const void *
+dvr_entry_class_channel_get(void *o)
+{
+  static const char *ret;
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  if (de->de_channel)
+    ret = idnode_uuid_as_str(&de->de_channel->ch_id);
+  else
+    ret = "";
+  return &ret;
+}
 
-      htsmsg_get_s32(m, "pre-extra-time", &cfg->dvr_extra_time_pre);
-      htsmsg_get_s32(m, "post-extra-time", &cfg->dvr_extra_time_post);
-      htsmsg_get_u32(m, "retention-days", &cfg->dvr_retention_days);
-      tvh_str_set(&cfg->dvr_storage, htsmsg_get_str(m, "storage"));
+static htsmsg_t *
+dvr_entry_class_channel_list(void *o)
+{
+  htsmsg_t *m = htsmsg_create_map();
+  htsmsg_add_str(m, "type",  "api");
+  htsmsg_add_str(m, "uri",   "channel/list");
+  htsmsg_add_str(m, "event", "channel");
+  return m;
+}
 
-/* 
- * Convert 0xxx format permission strings to integer for internal use
- * Note no checking that strtol won't overflow int - this should never happen with three-digit numbers 
- */     
+static int
+dvr_entry_class_channel_name_set(void *o, const void *v)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  channel_t   *ch;
+  if (!de->de_config_name)
+    dvr_entry_class_config_name_set(o, "");
+  if (!strcmp(de->de_channel_name ?: "", v ?: ""))
+    return 0;
+  ch = v ? channel_find_by_name(v) : NULL;
+  if (ch) {
+    return dvr_entry_class_channel_set(o, idnode_uuid_as_str(&ch->ch_id));
+  } else {
+    free(de->de_channel_name);
+    de->de_channel_name = strdup(v);
+    return 1;
+  }
+}
 
-      if ((s = htsmsg_get_str(m, "file-permissions")))
-        cfg->dvr_muxcnf.m_file_permissions = (int)strtol(s,NULL,0);
-         
-      if ((s = htsmsg_get_str(m, "directory-permissions")))
-        cfg->dvr_muxcnf.m_directory_permissions = (int)strtol(s,NULL,0);
-
-      if(!htsmsg_get_u32(m, "day-dir", &u32) && u32)
-        cfg->dvr_flags |= DVR_DIR_PER_DAY;
-
-      if(!htsmsg_get_u32(m, "channel-dir", &u32) && u32)
-        cfg->dvr_flags |= DVR_DIR_PER_CHANNEL;
-
-      if(!htsmsg_get_u32(m, "channel-in-title", &u32) && u32)
-        cfg->dvr_flags |= DVR_CHANNEL_IN_TITLE;
-
-      if(!htsmsg_get_u32(m, "date-in-title", &u32) && u32)
-        cfg->dvr_flags |= DVR_DATE_IN_TITLE;
-
-      if(!htsmsg_get_u32(m, "time-in-title", &u32) && u32)
-        cfg->dvr_flags |= DVR_TIME_IN_TITLE;
-   
-      if(!htsmsg_get_u32(m, "whitespace-in-title", &u32) && u32)
-        cfg->dvr_flags |= DVR_WHITESPACE_IN_TITLE;
-
-      if(!htsmsg_get_u32(m, "title-dir", &u32) && u32)
-        cfg->dvr_flags |= DVR_DIR_PER_TITLE;
+static const void *
+dvr_entry_class_channel_name_get(void *o)
+{
+  static const char *ret;
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  if (de->de_channel)
+    ret = channel_get_name(de->de_channel);
+  else
+    ret = de->de_channel_name;
+  return &ret;
+}
+
+htsmsg_t *
+dvr_entry_class_pri_list ( void *o )
+{
+  static const struct strtab tab[] = {
+    { "Not set",                  -1 },
+    { "Important",                DVR_PRIO_IMPORTANT },
+    { "High",                     DVR_PRIO_HIGH, },
+    { "Normal",                   DVR_PRIO_NORMAL },
+    { "Low",                      DVR_PRIO_LOW },
+    { "Unimportant",              DVR_PRIO_UNIMPORTANT },
+  };
+  return strtab2htsmsg(tab);
+}
+
+static htsmsg_t *
+dvr_entry_class_mc_list ( void *o )
+{
+  static const struct strtab tab[] = {
+    { "Not set",                       -1 },
+    { "Matroska (mkv)",                MC_MATROSKA, },
+    { "Same as source (pass through)", MC_PASS, },
+#if ENABLE_LIBAV
+    { "MPEG-TS",                       MC_MPEGTS },
+    { "MPEG-PS (DVD)",                 MC_MPEGPS },
+#endif
+  };
+  return strtab2htsmsg(tab);
+}
 
-      if(!htsmsg_get_u32(m, "episode-in-title", &u32) && u32)
-        cfg->dvr_flags |= DVR_EPISODE_IN_TITLE;
+htsmsg_t *
+dvr_entry_class_config_name_list(void *o)
+{
+  htsmsg_t *m = htsmsg_create_map();
+  htsmsg_t *p = htsmsg_create_map();
+  htsmsg_add_str(m, "type",  "api");
+  htsmsg_add_str(m, "uri",   "idnode/load");
+  htsmsg_add_str(m, "event", "dvrconfig");
+  htsmsg_add_u32(p, "enum",  1);
+  htsmsg_add_str(p, "class", dvr_config_class.ic_class);
+  htsmsg_add_msg(m, "params", p);
+  return m;
+}
 
-      if(!htsmsg_get_u32(m, "clean-title", &u32) && u32)
-        cfg->dvr_flags |= DVR_CLEAN_TITLE;
 
-      if(!htsmsg_get_u32(m, "tag-files", &u32) && !u32)
-        cfg->dvr_flags &= ~DVR_TAG_FILES;
+static int
+dvr_entry_class_autorec_set(void *o, const void *v)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  dvr_autorec_entry_t *dae = v ? dvr_autorec_find_by_uuid(v) : NULL;
+  if (dae == NULL) {
+    if (de->de_autorec) {
+      LIST_REMOVE(de, de_autorec_link);
+      de->de_autorec = NULL;
+      return 1;
+    }
+  } else if (de->de_autorec != dae) {
+    de->de_autorec = dae;
+    LIST_INSERT_HEAD(&dae->dae_spawns, de, de_autorec_link);
+    return 1;
+  }
+  return 0;
+}
 
-      if(!htsmsg_get_u32(m, "skip-commercials", &u32) && !u32)
-        cfg->dvr_flags &= ~DVR_SKIP_COMMERCIALS;
+static const void *
+dvr_entry_class_autorec_get(void *o)
+{
+  static const char *ret;
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  if (de->de_autorec)
+    ret = idnode_uuid_as_str(&de->de_autorec->dae_id);
+  else
+    ret = "";
+  return &ret;
+}
+
+static char *
+dvr_entry_class_autorec_rend(void *o)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  const char *s = "";
+  if (de->de_autorec) {
+    if (de->de_autorec->dae_name != NULL &&
+        de->de_autorec->dae_name[0] != '\0')
+      s = de->de_autorec->dae_name;
+    else if (de->de_autorec->dae_comment != NULL &&
+        de->de_autorec->dae_comment[0] != '\0')
+      s = de->de_autorec->dae_comment;
+    else
+      s = de->de_autorec->dae_title;
+  }
+  return strdup(s);
+}
 
-      if(!htsmsg_get_u32(m, "subtitle-in-title", &u32) && u32)
-        cfg->dvr_flags |= DVR_SUBTITLE_IN_TITLE;
+static int
+dvr_entry_class_broadcast_set(void *o, const void *v)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  uint32_t id = *(uint32_t *)v;
+  epg_broadcast_t *bcast = epg_broadcast_find_by_id(id, de->de_channel);
+  if (bcast == NULL) {
+    if (de->de_bcast) {
+      de->de_bcast->putref((epg_object_t*)de->de_bcast);
+      de->de_bcast = NULL;
+      return 1;
+    }
+  } else if (de->de_bcast != bcast) {
+    if (de->de_bcast)
+      de->de_bcast->putref((epg_object_t*)de->de_bcast);
+    de->de_bcast = bcast;
+    de->de_bcast->getref((epg_object_t*)bcast);
+    return 1;
+  }
+  return 0;
+}
 
-      if(!htsmsg_get_u32(m, "episode-before-date", &u32) && u32)
-        cfg->dvr_flags |= DVR_EPISODE_BEFORE_DATE;
+static const void *
+dvr_entry_class_broadcast_get(void *o)
+{
+  static uint32_t id;
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  if (de->de_bcast)
+    id = de->de_bcast->id;
+  else
+    id = 0;
+  return &id;
+}
 
-      if(!htsmsg_get_u32(m, "episode-duplicate-detection", &u32) && u32)
-        cfg->dvr_flags |= DVR_EPISODE_DUPLICATE_DETECTION;
+static char *
+dvr_entry_class_broadcast_rend(void *o)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  const char *s = "";
+  if (de->de_bcast)
+    s = lang_str_get(de->de_bcast->summary, NULL);
+  return strdup(s);
+}
 
-      dvr_charset_update(cfg, htsmsg_get_str(m, "charset"));
+static int
+dvr_entry_class_disp_title_set(void *o, const void *v)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  const char *s = "";
+  if (de->de_title)
+    s = lang_str_get(de->de_title, NULL);
+  if (strcmp(s, v ?: "")) {
+    lang_str_destroy(de->de_title);
+    de->de_title = lang_str_create();
+    if (v)
+      lang_str_add(de->de_title, v, NULL, 0);
+    return 1;
+  }
+  return 0;
+}
 
-      tvh_str_set(&cfg->dvr_postproc, htsmsg_get_str(m, "postproc"));
-    }
+static const void *
+dvr_entry_class_disp_title_get(void *o)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  static const char *s;
+  s = "";
+  if (de->de_title) {
+    s = lang_str_get(de->de_title, NULL);
+    if (s == NULL)
+      s = "";
+  }
+  return &s;
+}
 
-    htsmsg_destroy(l);
+static const void *
+dvr_entry_class_disp_description_get(void *o)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  static const char *s;
+  s = "";
+  if (de->de_title) {
+    s = lang_str_get(de->de_desc, NULL);
+    if (s == NULL)
+      s = "";
+  }
+  return &s;
+}
+
+static const void *
+dvr_entry_class_episode_get(void *o)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  static const char *s;
+  static char buf[100];
+  s = "";
+  if (de->de_bcast && de->de_bcast->episode)
+    if (epg_episode_number_format(de->de_bcast->episode,
+                                  buf, sizeof(buf), NULL,
+                                  "Season %d", ".", "Episode %d", "/%d"))
+      s = buf;
+  return &s;
+}
+
+static const void *
+dvr_entry_class_url_get(void *o)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  static const char *s;
+  static char buf[100];
+  s = "";
+  if (de->de_sched_state == DVR_COMPLETED) {
+    snprintf(buf, sizeof(buf), "dvrfile/%s", idnode_uuid_as_str(&de->de_id));
+    s = buf;
   }
+  return &s;
+}
 
-  LIST_FOREACH(cfg, &dvrconfigs, config_link) {
-    if(cfg->dvr_storage == NULL || !strlen(cfg->dvr_storage)) {
-      /* Try to figure out a good place to put them videos */
+static const void *
+dvr_entry_class_filesize_get(void *o)
+{
+  static int64_t size;
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  if (de->de_sched_state == DVR_COMPLETED)
+    size = dvr_get_filesize(de);
+  else
+    size = 0;
+  return &size;
+}
 
-      homedir = getenv("HOME");
+static const void *
+dvr_entry_class_start_real_get(void *o)
+{
+  static time_t tm;
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  tm = dvr_entry_get_start_time(de);
+  return &tm;
+}
 
-      if(homedir != NULL) {
-        snprintf(buf, sizeof(buf), "%s/Videos", homedir);
-        if(stat(buf, &st) == 0 && S_ISDIR(st.st_mode))
-          cfg->dvr_storage = strdup(buf);
-        
-        else if(stat(homedir, &st) == 0 && S_ISDIR(st.st_mode))
-          cfg->dvr_storage = strdup(homedir);
-        else
-          cfg->dvr_storage = strdup(getcwd(buf, sizeof(buf)));
-      }
+static const void *
+dvr_entry_class_stop_real_get(void *o)
+{
+  static time_t tm;
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  tm  = dvr_entry_get_stop_time(de);
+  return &tm;
+}
 
-      tvhlog(LOG_WARNING, "dvr",
-             "Output directory for video recording is not yet configured "
-             "for DVR configuration \"%s\". "
-             "Defaulting to to \"%s\". "
-             "This can be changed from the web user interface.",
-             cfg->dvr_config_name, cfg->dvr_storage);
-    }
+static const void *
+dvr_entry_class_duration_get(void *o)
+{
+  static time_t tm;
+  time_t start, stop;
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  start = dvr_entry_get_start_time(de);
+  stop  = dvr_entry_get_stop_time(de);
+  if (stop > start)
+    tm = stop - start;
+  else
+    tm = 0;
+  return &tm;
+}
+
+static const void *
+dvr_entry_class_status_get(void *o)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  static const char *s;
+  static char buf[100];
+  strncpy(buf, dvr_entry_status(de), sizeof(buf));
+  buf[sizeof(buf)-1] = '\0';
+  s = buf;
+  return &s;
+}
+
+static const void *
+dvr_entry_class_sched_status_get(void *o)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  static const char *s;
+  static char buf[100];
+  strncpy(buf, dvr_entry_schedstatus(de), sizeof(buf));
+  buf[sizeof(buf)-1] = '\0';
+  s = buf;
+  return &s;
+}
+
+static const void *
+dvr_entry_class_channel_icon_url_get(void *o)
+{
+  dvr_entry_t *de = (dvr_entry_t *)o;
+  channel_t *ch = de->de_channel;
+  static const char *s;
+  static char buf[256];
+  uint32_t id;
+  if (ch == NULL) {
+    s = "";
+  } else if ((id = imagecache_get_id(ch->ch_icon)) != 0) {
+    snprintf(buf, sizeof(buf), "imagecache/%d", id);
+  } else {
+    strncpy(buf, ch->ch_icon ?: "", sizeof(buf));
+    buf[strlen(buf)-1] = '\0';
   }
-
-#if ENABLE_INOTIFY
-  dvr_inotify_init();
-#endif
-  dvr_autorec_init();
-  dvr_db_load();
-  dvr_autorec_update();
+  s = buf;
+  return &s;
+}
+
+static htsmsg_t *
+dvr_entry_class_extra_list(void *o)
+{
+  int i;
+  htsmsg_t *e, *l = htsmsg_create_list();
+  char buf[32];
+  e = htsmsg_create_map();
+  htsmsg_add_u32(e, "key", 0);
+  htsmsg_add_str(e, "val", "Not set (use channel or DVR config)");
+  htsmsg_add_msg(l, NULL, e);
+  for (i = 1; i <= 120;  i++) {
+    snprintf(buf, sizeof(buf), "%d min%s", i, i > 1 ? "s" : "");
+    e = htsmsg_create_map();
+    htsmsg_add_u32(e, "key", i);
+    htsmsg_add_str(e, "val", buf);
+    htsmsg_add_msg(l, NULL, e);
+  }
+  for (i = 120; i <= 240;  i += 30) {
+    if ((i % 60) == 0)
+      snprintf(buf, sizeof(buf), "%d hrs", i / 60);
+    else
+      snprintf(buf, sizeof(buf), "%d hrs %d min%s", i / 60, i % 60, (i % 60) > 0 ? "s" : "");
+    e = htsmsg_create_map();
+    htsmsg_add_u32(e, "key", i);
+    htsmsg_add_str(e, "val", buf);
+    htsmsg_add_msg(l, NULL, e);
+  }
+  return l;
 }
+                                        
+
+static htsmsg_t *
+dvr_entry_class_content_type_list(void *o)
+{
+  htsmsg_t *m = htsmsg_create_map();
+  htsmsg_add_str(m, "type",  "api");
+  htsmsg_add_str(m, "uri",   "epg/content_type/list");
+  return m;
+}
+
+const idclass_t dvr_entry_class = {
+  .ic_class     = "dvrentry",
+  .ic_caption   = "DVR Entry",
+  .ic_save      = dvr_entry_class_save,
+  .ic_get_title = dvr_entry_class_get_title,
+  .ic_delete    = dvr_entry_class_delete,
+  .ic_properties = (const property_t[]) {
+    {
+      .type     = PT_TIME,
+      .id       = "start",
+      .name     = "Start Time",
+      .off      = offsetof(dvr_entry_t, de_start),
+    },
+    {
+      .type     = PT_TIME,
+      .id       = "start_extra",
+      .name     = "Extra Start Time",
+      .off      = offsetof(dvr_entry_t, de_start_extra),
+      .list     = dvr_entry_class_extra_list,
+      .opts     = PO_DURATION,
+    },
+    {
+      .type     = PT_TIME,
+      .id       = "start_real",
+      .name     = "Scheduled Start Time",
+      .get      = dvr_entry_class_start_real_get,
+      .opts     = PO_RDONLY | PO_NOSAVE,
+    },
+    {
+      .type     = PT_TIME,
+      .id       = "stop",
+      .name     = "Stop Time",
+      .off      = offsetof(dvr_entry_t, de_stop),
+    },
+    {
+      .type     = PT_TIME,
+      .id       = "stop_extra",
+      .name     = "Extra Stop Time",
+      .off      = offsetof(dvr_entry_t, de_stop_extra),
+      .list     = dvr_entry_class_extra_list,
+      .opts     = PO_DURATION,
+    },
+    {
+      .type     = PT_TIME,
+      .id       = "stop_real",
+      .name     = "Scheduled Stop Time",
+      .get      = dvr_entry_class_stop_real_get,
+      .opts     = PO_RDONLY | PO_NOSAVE,
+    },
+    {
+      .type     = PT_TIME,
+      .id       = "duration",
+      .name     = "Duration",
+      .get      = dvr_entry_class_duration_get,
+      .opts     = PO_RDONLY | PO_NOSAVE | PO_DURATION,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "channel",
+      .name     = "Channel",
+      .set      = dvr_entry_class_channel_set,
+      .get      = dvr_entry_class_channel_get,
+      .list     = dvr_entry_class_channel_list,
+    },
+    {
+      .type     = PT_TIME,
+      .id       = "channel_icon",
+      .name     = "Channel Icon",
+      .get      = dvr_entry_class_channel_icon_url_get,
+      .opts     = PO_HIDDEN | PO_RDONLY | PO_NOSAVE,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "channelname",
+      .name     = "Channel Name",
+      .get      = dvr_entry_class_channel_name_get,
+      .set      = dvr_entry_class_channel_name_set,
+      .off      = offsetof(dvr_entry_t, de_channel_name),
+    },
+    {
+      .type     = PT_LANGSTR,
+      .id       = "title",
+      .name     = "Title",
+      .off      = offsetof(dvr_entry_t, de_title),
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "disp_title",
+      .name     = "Title",
+      .get      = dvr_entry_class_disp_title_get,
+      .set      = dvr_entry_class_disp_title_set,
+      .opts     = PO_NOSAVE,
+    },
+    {
+      .type     = PT_LANGSTR,
+      .id       = "description",
+      .name     = "Description",
+      .off      = offsetof(dvr_entry_t, de_desc),
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "disp_description",
+      .name     = "Description",
+      .get      = dvr_entry_class_disp_description_get,
+      .opts     = PO_RDONLY | PO_NOSAVE | PO_HIDDEN,
+    },
+    {
+      .type     = PT_INT,
+      .id       = "pri",
+      .name     = "Priority",
+      .off      = offsetof(dvr_entry_t, de_pri),
+      .def.i    = DVR_PRIO_NORMAL,
+      .list     = dvr_entry_class_pri_list,
+    },
+    {
+      .type     = PT_INT,
+      .id       = "container",
+      .name     = "Container",
+      .off      = offsetof(dvr_entry_t, de_mc),
+      .def.i    = MC_MATROSKA,
+      .list     = dvr_entry_class_mc_list,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "config_name",
+      .name     = "DVR Configuration",
+      .set      = dvr_entry_class_config_name_set,
+      .list     = dvr_entry_class_config_name_list,
+      .off      = offsetof(dvr_entry_t, de_config_name),
+    },
+    {
+      .type     = PT_STR,
+      .id       = "creator",
+      .name     = "Creator",
+      .off      = offsetof(dvr_entry_t, de_creator),
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "filename",
+      .name     = "Filename",
+      .off      = offsetof(dvr_entry_t, de_filename),
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_U32,
+      .id       = "errorcode",
+      .name     = "Error Code",
+      .off      = offsetof(dvr_entry_t, de_last_error),
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_U32,
+      .id       = "errors",
+      .name     = "Errors",
+      .off      = offsetof(dvr_entry_t, de_errors),
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_U16,
+      .id       = "dvb_eid",
+      .name     = "DVB EPG ID",
+      .off      = offsetof(dvr_entry_t, de_dvb_eid),
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "noresched",
+      .name     = "Do Not Reschedule",
+      .off      = offsetof(dvr_entry_t, de_dvb_eid),
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "autorec",
+      .name     = "Auto Record",
+      .set      = dvr_entry_class_autorec_set,
+      .get      = dvr_entry_class_autorec_get,
+      .rend     = dvr_entry_class_autorec_rend,
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_U32,
+      .id       = "content_type",
+      .name     = "Content Type",
+      .list     = dvr_entry_class_content_type_list,
+      .off      = offsetof(dvr_entry_t, de_content_type),
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_U32,
+      .id       = "broadcast",
+      .name     = "Broadcast Type",
+      .set      = dvr_entry_class_broadcast_set,
+      .get      = dvr_entry_class_broadcast_get,
+      .rend     = dvr_entry_class_broadcast_rend,
+      .opts     = PO_RDONLY,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "episode",
+      .name     = "Episode",
+      .get      = dvr_entry_class_episode_get,
+      .opts     = PO_RDONLY | PO_NOSAVE,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "url",
+      .name     = "URL",
+      .get      = dvr_entry_class_url_get,
+      .opts     = PO_RDONLY | PO_NOSAVE,
+    },
+    {
+      .type     = PT_S64,
+      .id       = "filesize",
+      .name     = "File Size",
+      .get      = dvr_entry_class_filesize_get,
+      .opts     = PO_RDONLY | PO_NOSAVE,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "status",
+      .name     = "Status",
+      .get      = dvr_entry_class_status_get,
+      .opts     = PO_RDONLY | PO_NOSAVE,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "sched_status",
+      .name     = "Schedule Status",
+      .get      = dvr_entry_class_sched_status_get,
+      .opts     = PO_RDONLY | PO_NOSAVE | PO_HIDDEN,
+    },
+    {}
+  }
+};
 
 /**
  *
  */
 void
-dvr_done(void)
+dvr_destroy_by_channel(channel_t *ch, int delconf)
 {
-  dvr_config_t *cfg;
   dvr_entry_t *de;
 
-#if ENABLE_INOTIFY
-  dvr_inotify_done();
-#endif
-  pthread_mutex_lock(&global_lock);
-  while ((cfg = LIST_FIRST(&dvrconfigs)) != NULL) {
-    LIST_REMOVE(cfg, config_link);
-    free(cfg->dvr_charset_id);
-    free(cfg->dvr_charset);
-    free(cfg->dvr_storage);
-    free(cfg->dvr_config_name);
-    free(cfg);
+  while((de = LIST_FIRST(&ch->ch_dvrs)) != NULL) {
+    LIST_REMOVE(de, de_channel_link);
+    de->de_channel = NULL;
+    de->de_channel_name = strdup(channel_get_name(ch));
+    dvr_entry_purge(de, delconf);
   }
-  while ((de = LIST_FIRST(&dvrentries)) != NULL)
-    dvr_entry_remove(de, 0);
-  pthread_mutex_unlock(&global_lock);
-  dvr_autorec_done();
 }
 
 /**
@@ -1372,12 +1784,17 @@ dvr_config_find_by_name_default(const char *name)
   cfg = dvr_config_find_by_name(name);
 
   if (cfg == NULL) {
-    tvhlog(LOG_WARNING, "dvr", "Configuration '%s' not found", name);
+    if (name && name[0])
+      tvhlog(LOG_WARNING, "dvr", "Configuration '%s' not found", name);
+    cfg = dvr_config_find_by_name("");
+  } else if (!cfg->dvr_enabled) {
+    tvhlog(LOG_WARNING, "dvr", "Configuration '%s' not enabled", name);
     cfg = dvr_config_find_by_name("");
   }
 
   if (cfg == NULL) {
-    cfg = dvr_config_create("");
+    cfg = dvr_config_create("", NULL, NULL);
+    dvr_config_save(cfg);
   }
 
   return cfg;
@@ -1388,17 +1805,25 @@ dvr_config_find_by_name_default(const char *name)
  * to avoid duplicates
  */
 dvr_config_t *
-dvr_config_create(const char *name)
+dvr_config_create(const char *name, const char *uuid, htsmsg_t *conf)
 {
   dvr_config_t *cfg;
 
   if (name == NULL)
     name = "";
 
-  tvhlog(LOG_INFO, "dvr", "Creating new configuration '%s'", name);
-
   cfg = calloc(1, sizeof(dvr_config_t));
-  cfg->dvr_config_name = strdup(name);
+
+  if (idnode_insert(&cfg->dvr_id, uuid, &dvr_config_class, 0)) {
+    if (uuid)
+      tvherror("dvr", "invalid config uuid '%s'", uuid);
+    free(cfg);
+    return NULL;
+  }
+
+  cfg->dvr_enabled = 1;
+  if (name)
+    cfg->dvr_config_name = strdup(name);
   cfg->dvr_retention_days = 31;
   cfg->dvr_mc = MC_MATROSKA;
   cfg->dvr_flags = DVR_TAG_FILES | DVR_SKIP_COMMERCIALS;
@@ -1423,276 +1848,416 @@ dvr_config_create(const char *name)
 
   cfg->dvr_muxcnf.m_file_permissions      = 0664;
   cfg->dvr_muxcnf.m_directory_permissions = 0775;
+
+  if (conf)
+    idnode_load(&cfg->dvr_id, conf);
   
+  tvhlog(LOG_INFO, "dvr", "Creating new configuration '%s'", cfg->dvr_config_name);
+
   LIST_INSERT_HEAD(&dvrconfigs, cfg, config_link);
 
-  return LIST_FIRST(&dvrconfigs);
+  return cfg;
 }
 
 /**
- *
+ * destroy a dvr config
  */
-void 
-dvr_config_delete(const char *name)
+static void
+dvr_config_destroy(dvr_config_t *cfg, int delconf)
 {
-  dvr_config_t *cfg;
-
-  if (name == NULL || strlen(name) == 0) {
-    tvhlog(LOG_WARNING,"dvr","Attempt to delete default config ignored");
-    return;
-  }
-
-  cfg = dvr_config_find_by_name(name);
-  if (cfg != NULL) {
+  if (delconf) {
     tvhlog(LOG_INFO, "dvr", "Deleting configuration '%s'", 
-        cfg->dvr_config_name);
-    hts_settings_remove("dvr/config%s", cfg->dvr_config_name);
-    LIST_REMOVE(cfg, config_link);
-    free(cfg->dvr_charset_id);
-    free(cfg->dvr_charset);
-    free(cfg->dvr_storage);
-    free(cfg->dvr_config_name);
-    free(cfg);
-
-    dvrconfig_changed();    
+           cfg->dvr_config_name);
+    hts_settings_remove("dvr/config/%s", idnode_uuid_as_str(&cfg->dvr_id));
   }
-}
-
-/**
- *
- */
-static void
-dvr_save(dvr_config_t *cfg)
-{
-  htsmsg_t *m = htsmsg_create_map();
-  char buffer[5]; // Permissions buffer: leading zero, three octal digits plus terminating null
-  
-  if (cfg->dvr_config_name != NULL && strlen(cfg->dvr_config_name) != 0)
-    htsmsg_add_str(m, "config_name", cfg->dvr_config_name);
-  htsmsg_add_str(m, "storage", cfg->dvr_storage);
-
-/* Convert permissions to 0xxx octal format and output */
-
-  snprintf(buffer,sizeof(buffer),"%04o",cfg->dvr_muxcnf.m_file_permissions);
-  htsmsg_add_str(m, "file-permissions", buffer);
-  
-  snprintf(buffer,sizeof(buffer),"%04o",cfg->dvr_muxcnf.m_directory_permissions);
-  htsmsg_add_str(m, "directory-permissions", buffer);
-
-  htsmsg_add_u32(m, "container", cfg->dvr_mc);
-  htsmsg_add_u32(m, "cache", cfg->dvr_muxcnf.m_cache);
-  htsmsg_add_u32(m, "rewrite-pat",
-                 !!(cfg->dvr_muxcnf.m_flags & MC_REWRITE_PAT));
-  htsmsg_add_u32(m, "rewrite-pmt",
-                 !!(cfg->dvr_muxcnf.m_flags & MC_REWRITE_PMT));
-  htsmsg_add_u32(m, "retention-days", cfg->dvr_retention_days);
-  htsmsg_add_u32(m, "pre-extra-time", cfg->dvr_extra_time_pre);
-  htsmsg_add_u32(m, "post-extra-time", cfg->dvr_extra_time_post);
-  htsmsg_add_u32(m, "day-dir",          !!(cfg->dvr_flags & DVR_DIR_PER_DAY));
-  htsmsg_add_u32(m, "channel-dir",      !!(cfg->dvr_flags & DVR_DIR_PER_CHANNEL));
-  htsmsg_add_u32(m, "channel-in-title", !!(cfg->dvr_flags & DVR_CHANNEL_IN_TITLE));
-  htsmsg_add_u32(m, "date-in-title",    !!(cfg->dvr_flags & DVR_DATE_IN_TITLE));
-  htsmsg_add_u32(m, "time-in-title",    !!(cfg->dvr_flags & DVR_TIME_IN_TITLE));
-  htsmsg_add_u32(m, "whitespace-in-title", !!(cfg->dvr_flags & DVR_WHITESPACE_IN_TITLE));
-  htsmsg_add_u32(m, "title-dir", !!(cfg->dvr_flags & DVR_DIR_PER_TITLE));
-  htsmsg_add_u32(m, "episode-in-title", !!(cfg->dvr_flags & DVR_EPISODE_IN_TITLE));
-  htsmsg_add_u32(m, "clean-title", !!(cfg->dvr_flags & DVR_CLEAN_TITLE));
-  htsmsg_add_u32(m, "tag-files", !!(cfg->dvr_flags & DVR_TAG_FILES));
-  htsmsg_add_u32(m, "skip-commercials", !!(cfg->dvr_flags & DVR_SKIP_COMMERCIALS));
-  htsmsg_add_u32(m, "subtitle-in-title", !!(cfg->dvr_flags & DVR_SUBTITLE_IN_TITLE));
-  htsmsg_add_u32(m, "episode-before-date", !!(cfg->dvr_flags & DVR_EPISODE_BEFORE_DATE));
-  htsmsg_add_u32(m, "episode-duplicate-detection", !!(cfg->dvr_flags & DVR_EPISODE_DUPLICATE_DETECTION));
-  if (cfg->dvr_charset != NULL)
-    htsmsg_add_str(m, "charset", cfg->dvr_charset);
-  if (cfg->dvr_postproc != NULL)
-    htsmsg_add_str(m, "postproc", cfg->dvr_postproc);
-
-  hts_settings_save(m, "dvr/config%s", cfg->dvr_config_name);
-  htsmsg_destroy(m);
-
-  dvrconfig_changed();
-}
-
-/**
- *
- */
-void
-dvr_storage_set(dvr_config_t *cfg, const char *storage)
-{
-  if(cfg->dvr_storage != NULL && !strcmp(cfg->dvr_storage, storage))
-    return;
-
-  tvh_str_set(&cfg->dvr_storage, storage);
-  dvr_save(cfg);
+  LIST_REMOVE(cfg, config_link);
+  idnode_unlink(&cfg->dvr_id);
+  free(cfg->dvr_charset_id);
+  free(cfg->dvr_charset);
+  free(cfg->dvr_storage);
+  free(cfg->dvr_config_name);
+  free(cfg);
 }
 
 /**
  *
  */
 void
-dvr_charset_set(dvr_config_t *cfg, const char *charset)
+dvr_config_delete(const char *name)
 {
-  if(cfg->dvr_charset != NULL && !strcmp(cfg->dvr_charset, charset))
-    return;
-
-  dvr_charset_update(cfg, charset);
-  dvr_save(cfg);
-}
+  dvr_config_t *cfg;
 
-/**
- *
- */
-void
-dvr_file_permissions_set(dvr_config_t *cfg, int permissions)
-{
-  if(cfg->dvr_muxcnf.m_file_permissions == permissions)
+  if (name == NULL || strlen(name) == 0) {
+    tvhlog(LOG_WARNING,"dvr","Attempt to delete default config ignored");
     return;
+  }
 
-  cfg->dvr_muxcnf.m_file_permissions = permissions;
-  dvr_save(cfg);
+  cfg = dvr_config_find_by_name(name);
+  if (cfg != NULL)
+    dvr_config_destroy(cfg, 1);
 }
 
-/**
+/*
  *
  */
-void
-dvr_directory_permissions_set(dvr_config_t *cfg, int permissions)
-{
-  if(cfg->dvr_muxcnf.m_directory_permissions == permissions)
-    return;
-
-  cfg->dvr_muxcnf.m_directory_permissions = permissions;
-  dvr_save(cfg);
+static void
+dvr_config_update_flags(dvr_config_t *cfg)
+{
+  int r = 0;
+  if (cfg->dvr_dir_per_day)          r |= DVR_DIR_PER_DAY;
+  if (cfg->dvr_channel_dir)          r |= DVR_DIR_PER_CHANNEL;
+  if (cfg->dvr_channel_in_title)     r |= DVR_CHANNEL_IN_TITLE;
+  if (cfg->dvr_date_in_title)        r |= DVR_DATE_IN_TITLE;
+  if (cfg->dvr_time_in_title)        r |= DVR_TIME_IN_TITLE;
+  if (cfg->dvr_whitespace_in_title)  r |= DVR_WHITESPACE_IN_TITLE;
+  if (cfg->dvr_title_dir)            r |= DVR_DIR_PER_TITLE;
+  if (cfg->dvr_episode_in_title)     r |= DVR_EPISODE_IN_TITLE;
+  if (cfg->dvr_clean_title)          r |= DVR_CLEAN_TITLE;
+  if (cfg->dvr_tag_files)            r |= DVR_TAG_FILES;
+  if (cfg->dvr_skip_commercials)     r |= DVR_SKIP_COMMERCIALS;
+  if (cfg->dvr_subtitle_in_title)    r |= DVR_SUBTITLE_IN_TITLE;
+  if (cfg->dvr_episode_before_date)  r |= DVR_EPISODE_BEFORE_DATE;
+  if (cfg->dvr_episode_duplicate)    r |= DVR_EPISODE_DUPLICATE_DETECTION;
+  cfg->dvr_flags = r;
+  r = 0;
+  if (cfg->dvr_rewrite_pat)          r |= MC_REWRITE_PAT;
+  if (cfg->dvr_rewrite_pmt)          r |= MC_REWRITE_PMT;
+  cfg->dvr_muxcnf.m_flags = r;
 }
 
-/**
- *
- */
-void
-dvr_container_set(dvr_config_t *cfg, const char *container)
-{
-  muxer_container_type_t mc;
-
-  mc = muxer_container_txt2type(container);
-  if(mc == MC_UNKNOWN)
-    mc = MC_MATROSKA;
-
-  if(cfg->dvr_mc == mc)
-    return;
-
-  cfg->dvr_mc = mc;
 
-  dvr_save(cfg);
-}
-
-/**
+/*
  *
  */
 void
-dvr_mux_cache_set(dvr_config_t *cfg, int mcache)
+dvr_config_save(dvr_config_t *cfg)
 {
-  if (mcache < MC_CACHE_UNKNOWN || mcache > MC_CACHE_LAST)
-    mcache = MC_CACHE_UNKNOWN;
-
-  if(cfg->dvr_muxcnf.m_cache == mcache)
-    return;
+  htsmsg_t *m = htsmsg_create_map();
 
-  cfg->dvr_muxcnf.m_cache = mcache;
+  lock_assert(&global_lock);
 
-  dvr_save(cfg);
+  idnode_save(&cfg->dvr_id, m);
+  hts_settings_save(m, "dvr/config/%s", idnode_uuid_as_str(&cfg->dvr_id));
+  htsmsg_destroy(m);
 }
 
+/* **************************************************************************
+ * DVR Config Class definition
+ * **************************************************************************/
 
-/**
- *
- */
-void
-dvr_postproc_set(dvr_config_t *cfg, const char *postproc)
+static void
+dvr_config_class_save(idnode_t *self)
 {
-  if(cfg->dvr_postproc != NULL && !strcmp(cfg->dvr_postproc, postproc))
-    return;
-
-  tvh_str_set(&cfg->dvr_postproc, !strcmp(postproc, "") ? NULL : postproc);
-  dvr_save(cfg);
+  dvr_config_update_flags((dvr_config_t *)self);
+  dvr_config_save((dvr_config_t *)self);
 }
 
-
-/**
- *
- */
-void
-dvr_retention_set(dvr_config_t *cfg, int days)
+static void
+dvr_config_class_delete(idnode_t *self)
 {
-  dvr_entry_t *de;
-  if(days < 1 || cfg->dvr_retention_days == days)
-    return;
-
-  cfg->dvr_retention_days = days;
-
-  /* Also, rearm all timers */
-
-  LIST_FOREACH(de, &dvrentries, de_global_link)
-    if(de->de_sched_state == DVR_COMPLETED)
-      gtimer_arm_abs(&de->de_timer, dvr_timer_expire, de, 
-                    de->de_stop + cfg->dvr_retention_days * 86400);
-  dvr_save(cfg);
+  dvr_config_destroy((dvr_config_t *)self, 1);
 }
 
-
-/**
- *
- */
-void
-dvr_flags_set(dvr_config_t *cfg, int flags)
+static int
+dvr_config_class_perm(idnode_t *self, access_t *a, htsmsg_t *msg_to_write)
 {
-  if(cfg->dvr_flags == flags)
-    return;
-
-  cfg->dvr_flags = flags;
-  dvr_save(cfg);
+  dvr_config_t *cfg = (dvr_config_t *)self;
+  if (access_verify2(a, ACCESS_RECORDER))
+    return -1;
+  if (access_verify2(a, ACCESS_ADMIN) && access_verify2(a, ACCESS_RECORDER_ALL))
+    return 0;
+  if (strcmp(cfg->dvr_config_name ?: "", a->aa_username ?: ""))
+    return -1;
+  return 0;
 }
 
-/**
- *
- */
-void
-dvr_mux_flags_set(dvr_config_t *cfg, int flags)
+static const char *
+dvr_config_class_get_title (idnode_t *self)
 {
-  if(cfg->dvr_muxcnf.m_flags == flags)
-    return;
-
-  cfg->dvr_muxcnf.m_flags = flags;
-  dvr_save(cfg);
+  dvr_config_t *cfg = (dvr_config_t *)self;
+  if (cfg->dvr_config_name && cfg->dvr_config_name[0] != '\0')
+    return cfg->dvr_config_name;
+  return "(Default Profile)";
 }
 
-
-/**
- *
- */
-void
-dvr_extra_time_pre_set(dvr_config_t *cfg, int d)
+static int
+dvr_config_class_charset_set(void *o, const void *v)
 {
-  if(cfg->dvr_extra_time_pre == d)
-    return;
-
-  cfg->dvr_extra_time_pre = d;
-  dvr_save(cfg);
+  dvr_config_t *cfg = (dvr_config_t *)o;
+  return dvr_charset_update(cfg, v);
 }
 
-
-/**
- *
- */
-void
-dvr_extra_time_post_set(dvr_config_t *cfg, int d)
+static htsmsg_t *
+dvr_config_class_charset_list(void *o)
 {
-  if(cfg->dvr_extra_time_post == d)
-    return;
-
-  cfg->dvr_extra_time_post = d;
-  dvr_save(cfg);
-}
-
+  htsmsg_t *m = htsmsg_create_map();
+  htsmsg_add_str(m, "type",  "api");
+  htsmsg_add_str(m, "uri",   "intlconv/charsets");
+  htsmsg_add_str(m, "event", "charsets");
+  return m;
+}
+
+static htsmsg_t *
+dvr_config_class_cache_list(void *o)
+{
+  static struct strtab tab[] = {
+    { "Unknown",            MC_CACHE_UNKNOWN },
+    { "System",             MC_CACHE_SYSTEM },
+    { "Do not keep",        MC_CACHE_DONTKEEP },
+    { "Sync",               MC_CACHE_SYNC },
+    { "Sync + Do not keep", MC_CACHE_SYNCDONTKEEP }
+  };
+  return strtab2htsmsg(tab);
+}
+
+const idclass_t dvr_config_class = {
+  .ic_class      = "dvrconfig",
+  .ic_caption    = "DVR Configuration Profile",
+  .ic_save       = dvr_config_class_save,
+  .ic_get_title  = dvr_config_class_get_title,
+  .ic_delete     = dvr_config_class_delete,
+  .ic_perm       = dvr_config_class_perm,
+  .ic_groups     = (const property_group_t[]) {
+      {
+         .name   = "DVR Behaviour",
+         .number = 1,
+      },
+      {
+         .name   = "Recording File Options",
+         .number = 2,
+      },
+      {
+         .name   = "Subdirectory Options",
+         .number = 3,
+      },
+      {
+         .name   = "Filename Options",
+         .number = 4,
+         .column = 1,
+      },
+      {
+         .name   = "",
+         .number = 5,
+         .parent = 4,
+         .column = 2,
+      },
+      {}
+  },
+  .ic_properties = (const property_t[]){
+    {
+      .type     = PT_BOOL,
+      .id       = "enabled",
+      .name     = "Enabled",
+      .off      = offsetof(dvr_config_t, dvr_enabled),
+      .def.i    = 1,
+      .group    = 1,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "name",
+      .name     = "Config Name",
+      .off      = offsetof(dvr_config_t, dvr_config_name),
+      .def.s    = "! New config",
+      .group    = 1,
+    },
+    {
+      .type     = PT_INT,
+      .id       = "container",
+      .name     = "Container",
+      .off      = offsetof(dvr_config_t, dvr_mc),
+      .def.i    = MC_MATROSKA,
+      .list     = dvr_entry_class_mc_list,
+      .group    = 1,
+    },
+    {
+      .type     = PT_INT,
+      .id       = "cache",
+      .name     = "Cache Scheme",
+      .off      = offsetof(dvr_config_t, dvr_muxcnf.m_cache),
+      .def.i    = MC_CACHE_DONTKEEP,
+      .list     = dvr_config_class_cache_list,
+      .group    = 1,
+    },
+    {
+      .type     = PT_U32,
+      .id       = "retention-days",
+      .name     = "DVR Log Retention Time (days)",
+      .off      = offsetof(dvr_config_t, dvr_retention_days),
+      .def.u32  = 31,
+      .group    = 1,
+    },
+    {
+      .type     = PT_U32,
+      .id       = "pre-extra-time",
+      .name     = "Extra Time Before Recordings (minutes)",
+      .off      = offsetof(dvr_config_t, dvr_extra_time_pre),
+      .group    = 1,
+    },
+    {
+      .type     = PT_U32,
+      .id       = "post-extra-time",
+      .name     = "Extra Time After Recordings (minutes)",
+      .off      = offsetof(dvr_config_t, dvr_extra_time_post),
+      .group    = 1,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "episode-duplicate-detection",
+      .name     = "Episode Duplicate Detect",
+      .off      = offsetof(dvr_config_t, dvr_episode_duplicate),
+      .group    = 1,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "postproc",
+      .name     = "Post-Processor Command",
+      .off      = offsetof(dvr_config_t, dvr_postproc),
+      .group    = 1,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "storage",
+      .name     = "Recording System Path",
+      .off      = offsetof(dvr_config_t, dvr_storage),
+      .group    = 2,
+    },
+    {
+      .type     = PT_PERM,
+      .id       = "file-permissions",
+      .name     = "File Permissions (octal, e.g. 0664)",
+      .off      = offsetof(dvr_config_t, dvr_muxcnf.m_file_permissions),
+      .def.u32  = 0664,
+      .group    = 2,
+    },
+    {
+      .type     = PT_STR,
+      .id       = "charset",
+      .name     = "Filename Charset",
+      .off      = offsetof(dvr_config_t, dvr_charset),
+      .set      = dvr_config_class_charset_set,
+      .list     = dvr_config_class_charset_list,
+      .def.s    = "UTF-8",
+      .group    = 2,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "rewrite-pat",
+      .name     = "Rewrite PAT",
+      .off      = offsetof(dvr_config_t, dvr_rewrite_pat),
+      .def.i    = 1,
+      .group    = 2,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "rewrite-pmt",
+      .name     = "Rewrite PMT",
+      .off      = offsetof(dvr_config_t, dvr_rewrite_pmt),
+      .group    = 2,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "tag-files",
+      .name     = "Tag Files With Metadata",
+      .off      = offsetof(dvr_config_t, dvr_tag_files),
+      .def.i    = 1,
+      .group    = 2,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "skip-commercials",
+      .name     = "Skip Commercials",
+      .off      = offsetof(dvr_config_t, dvr_skip_commercials),
+      .def.i    = 1,
+      .group    = 2,
+    },
+    {
+      .type     = PT_PERM,
+      .id       = "directory-permissions",
+      .name     = "Directory Permissions (octal, e.g. 0775)",
+      .off      = offsetof(dvr_config_t, dvr_muxcnf.m_directory_permissions),
+      .def.u32  = 0775,
+      .group    = 3,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "day-dir",
+      .name     = "Make Subdirectories Per Day",
+      .off      = offsetof(dvr_config_t, dvr_dir_per_day),
+      .group    = 3,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "channel-dir",
+      .name     = "Make Subdirectories Per Channel",
+      .off      = offsetof(dvr_config_t, dvr_channel_dir),
+      .group    = 3,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "title-dir",
+      .name     = "Make Subdirectories Per Title",
+      .off      = offsetof(dvr_config_t, dvr_title_dir),
+      .group    = 3,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "channel-in-title",
+      .name     = "Include Channel Name In Filename",
+      .off      = offsetof(dvr_config_t, dvr_channel_in_title),
+      .group    = 4,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "date-in-title",
+      .name     = "Include Date In Filename",
+      .off      = offsetof(dvr_config_t, dvr_date_in_title),
+      .group    = 4,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "time-in-title",
+      .name     = "Include Time In Filename",
+      .off      = offsetof(dvr_config_t, dvr_time_in_title),
+      .group    = 4,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "episode-in-title",
+      .name     = "Include Episode In Filename",
+      .off      = offsetof(dvr_config_t, dvr_time_in_title),
+      .group    = 4,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "subtitle-in-title",
+      .name     = "Include Subtitle In Filename",
+      .off      = offsetof(dvr_config_t, dvr_time_in_title),
+      .group    = 5,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "episode-before-date",
+      .name     = "Put Episode In Filename Before Date And Time",
+      .off      = offsetof(dvr_config_t, dvr_episode_before_date),
+      .group    = 5,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "clean-title",
+      .name     = "Remove All Unsafe Characters From Filename",
+      .off      = offsetof(dvr_config_t, dvr_clean_title),
+      .group    = 5,
+    },
+    {
+      .type     = PT_BOOL,
+      .id       = "whitespace-in-title",
+      .name     = "Replace Whitespace In Title with '-'",
+      .off      = offsetof(dvr_config_t, dvr_whitespace_in_title),
+      .group    = 5,
+    },
+    {}
+  },
+};
 
 /**
  *
@@ -1840,7 +2405,7 @@ dvr_entry_delete(dvr_entry_t *de)
 
     /* Also delete directories, if they were created for the recording and if they are empty */
 
-    dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name);
+    dvr_config_t *cfg = de->de_config;
     char path[500];
 
     snprintf(path, sizeof(path), "%s", cfg->dvr_storage);
@@ -1895,3 +2460,90 @@ dvr_entry_cancel_delete(dvr_entry_t *de)
     abort();
   }
 }
+
+/**
+ *
+ */
+void
+dvr_init(void)
+{
+  htsmsg_t *m, *l;
+  htsmsg_field_t *f;
+  char buf[500];
+  const char *homedir;
+  struct stat st;
+  dvr_config_t *cfg;
+
+  dvr_iov_max = sysconf(_SC_IOV_MAX);
+
+  /* Default settings */
+
+  LIST_INIT(&dvrconfigs);
+
+  if ((l = hts_settings_load("dvr/config")) != NULL) {
+    HTSMSG_FOREACH(f, l) {
+      if ((m = htsmsg_get_map_by_field(f)) == NULL) continue;
+      (void)dvr_config_create(NULL, f->hmf_name, m);
+    }
+    htsmsg_destroy(l);
+  }
+
+  /* Create the default entry */
+
+  cfg = dvr_config_find_by_name_default("");
+  assert(cfg);
+
+  LIST_FOREACH(cfg, &dvrconfigs, config_link) {
+    if(cfg->dvr_storage == NULL || !strlen(cfg->dvr_storage)) {
+      /* Try to figure out a good place to put them videos */
+
+      homedir = getenv("HOME");
+
+      if(homedir != NULL) {
+        snprintf(buf, sizeof(buf), "%s/Videos", homedir);
+        if(stat(buf, &st) == 0 && S_ISDIR(st.st_mode))
+          cfg->dvr_storage = strdup(buf);
+        
+        else if(stat(homedir, &st) == 0 && S_ISDIR(st.st_mode))
+          cfg->dvr_storage = strdup(homedir);
+        else
+          cfg->dvr_storage = strdup(getcwd(buf, sizeof(buf)));
+      }
+
+      tvhlog(LOG_WARNING, "dvr",
+             "Output directory for video recording is not yet configured "
+             "for DVR configuration \"%s\". "
+             "Defaulting to to \"%s\". "
+             "This can be changed from the web user interface.",
+             cfg->dvr_config_name, cfg->dvr_storage);
+    }
+  }
+
+#if ENABLE_INOTIFY
+  dvr_inotify_init();
+#endif
+  dvr_autorec_init();
+  dvr_db_load();
+  dvr_autorec_update();
+}
+
+/**
+ *
+ */
+void
+dvr_done(void)
+{
+  dvr_config_t *cfg;
+  dvr_entry_t *de;
+
+#if ENABLE_INOTIFY
+  dvr_inotify_done();
+#endif
+  pthread_mutex_lock(&global_lock);
+  while ((cfg = LIST_FIRST(&dvrconfigs)) != NULL)
+    dvr_config_destroy(cfg, 0);
+  while ((de = LIST_FIRST(&dvrentries)) != NULL)
+    dvr_entry_remove(de, 0);
+  pthread_mutex_unlock(&global_lock);
+  dvr_autorec_done();
+}
index 66bd2e240f91152070bbeac92eff59e0c200d346..472177b55e73bc80887f430deb80519499a5f9e1 100644 (file)
@@ -74,7 +74,7 @@ dvr_rec_subscribe(dvr_entry_t *de)
 
   snprintf(buf, sizeof(buf), "DVR: %s", lang_str_get(de->de_title, NULL));
 
-  if(de->de_mc == MC_PASS) {
+  if(dvr_entry_get_mc(de) == MC_PASS) {
     streaming_queue_init(&de->de_sq, SMT_PACKET);
     de->de_gh = NULL;
     de->de_tsfix = NULL;
@@ -307,7 +307,7 @@ dvr_rec_start(dvr_entry_t *de, const streaming_start_t *ss)
   dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name);
   muxer_container_type_t mc;
 
-  mc = de->de_mc;
+  mc = dvr_entry_get_mc(de);
 
   de->de_mux = muxer_create(mc, &cfg->dvr_muxcnf);
   if(!de->de_mux) {
index 02e01bcc02bd8a6bb69b31549d342b4cb9e29dfc..7426459517db717c91a5f17544464de80f5f5c53 100644 (file)
--- 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);
       }
index 93a445f21d49f57c0fa9f1fe73d730304ccd4182..eced3aba11bbd9cda61758ae1d68f1adb7dbec80 100644 (file)
@@ -654,7 +654,7 @@ htsp_build_dvrentry(dvr_entry_t *de, const char *method)
   const char *p;
   dvr_config_t *cfg;
 
-  htsmsg_add_u32(out, "id", de->de_id);
+  htsmsg_add_u32(out, "id", idnode_get_short_uuid(&de->de_id));
   if (de->de_channel)
     htsmsg_add_u32(out, "channel", channel_get_id(de->de_channel));
 
@@ -796,7 +796,7 @@ htsp_build_event
   }
 
   if((de = dvr_entry_find_by_event(e)) != NULL) {
-    htsmsg_add_u32(out, "dvrId", de->de_id);
+    htsmsg_add_u32(out, "dvrId", idnode_get_short_uuid(&de->de_id));
   }
 
   if ((n = epg_broadcast_get_next(e)))
@@ -1217,9 +1217,9 @@ htsp_method_addDvrEntry(htsp_connection_t *htsp, htsmsg_t *in)
       desc = "";
 
     // create the dvr entry
-    de = dvr_entry_create(dvr_config_name, ch, start, stop,
-                          start_extra, stop_extra,
-                          title, desc, lang, 0, creator, NULL, priority);
+    de = dvr_entry_create_htsp(dvr_config_name, ch, start, stop,
+                               start_extra, stop_extra,
+                               title, desc, lang, 0, creator, NULL, priority);
 
   /* Event timer */
   } else {
@@ -1238,7 +1238,7 @@ htsp_method_addDvrEntry(htsp_connection_t *htsp, htsmsg_t *in)
   case DVR_RECORDING:
   case DVR_MISSED_TIME:
   case DVR_COMPLETED:
-    htsmsg_add_u32(out, "id", de->de_id);
+    htsmsg_add_u32(out, "id", idnode_get_short_uuid(&de->de_id));
     htsmsg_add_u32(out, "success", 1);
     break;  
   case DVR_NOSTATE:
@@ -2531,7 +2531,7 @@ void
 htsp_dvr_entry_delete(dvr_entry_t *de)
 {
   htsmsg_t *m = htsmsg_create_map();
-  htsmsg_add_u32(m, "id", de->de_id);
+  htsmsg_add_u32(m, "id", idnode_get_short_uuid(&de->de_id));
   htsmsg_add_str(m, "method", "dvrEntryDelete");
   htsp_async_send(m, HTSP_ASYNC_ON);
 }
index eba2351772e9969d3a39fcf7aaf5db0f7a15eb8a..14f94b030da31ae609479ac03ea7aa08a99a370a 100644 (file)
@@ -462,8 +462,14 @@ http_exec(http_connection_t *hc, http_path_t *hp, char *remain)
 
   if(http_access_verify(hc, hp->hp_accessmask))
     err = HTTP_STATUS_UNAUTHORIZED;
-  else
+  else {
+    /* FIXME: Recode to obtain access only once */
+    hc->hc_access = access_get(hc->hc_username, hc->hc_password,
+                               (struct sockaddr *)hc->hc_peer);
     err = hp->hp_callback(hc, remain, hp->hp_opaque);
+    access_destroy(hc->hc_access);
+    hc->hc_access = NULL;
+  }
 
   if(err == -1)
      return 1;
index cf35f76d69889086244cd437f52df2a8bee3209c..2b0d405cdc05216dc9bac52ee025f94eade83bc4 100644 (file)
@@ -22,6 +22,7 @@
 #include "htsbuf.h"
 #include "url.h"
 #include "tvhpoll.h"
+#include "access.h"
 
 struct channel;
 
@@ -130,6 +131,7 @@ typedef struct http_connection {
 
   char *hc_username;
   char *hc_password;
+  access_t *hc_access;
 
   struct config_head *hc_user_config;
 
index 9436e276840ec40bf13b7689d39199e1d6de06ca..1d8a0294addc3e8a120a8f113966c2670e81343c 100644 (file)
@@ -366,10 +366,46 @@ idnode_get_u32
         *u32 = *(int*)ptr;
         return 0;
       case PT_U16:
+        *u32 = *(uint16_t*)ptr;
+        return 0;
+      case PT_U32:
         *u32 = *(uint32_t*)ptr;
         return 0;
+      default:
+        break;
+    }
+  }
+  return 1;
+}
+
+/*
+ * Get field as signed 64-bit int
+ */
+int
+idnode_get_s64
+  ( idnode_t *self, const char *key, int64_t *s64 )
+{
+  const property_t *p = idnode_find_prop(self, key);
+  if (p->islist) return 1;
+  if (p) {
+    const void *ptr;
+    if (p->get)
+      ptr = p->get(self);
+    else
+      ptr = ((void*)self) + p->off;
+    switch (p->type) {
+      case PT_INT:
+      case PT_BOOL:
+        *s64 = *(int*)ptr;
+        return 0;
+      case PT_U16:
+        *s64 = *(uint16_t*)ptr;
+        return 0;
       case PT_U32:
-        *u32 = *(uint16_t*)ptr;
+        *s64 = *(uint32_t*)ptr;
+        return 0;
+      case PT_S64:
+        *s64 = *(int64_t*)ptr;
         return 0;
       default:
         break;
@@ -401,6 +437,29 @@ idnode_get_bool
   return 1; 
 }
 
+/*
+ * Get field as time
+ */
+int
+idnode_get_time
+  ( idnode_t *self, const char *key, time_t *tm )
+{
+  const property_t *p = idnode_find_prop(self, key);
+  if (p->islist) return 1;
+  if (p) {
+    void *ptr = self;
+    ptr += p->off;
+    switch (p->type) {
+      case PT_TIME:
+        *tm = *(time_t*)ptr;
+        return 0;
+      default:
+        break;
+    }
+  }
+  return 1;
+}
+
 /* **************************************************************************
  * Lookup
  * *************************************************************************/
@@ -493,11 +552,11 @@ idnode_cmp_sort
       {
         int r;
         const char *stra = tvh_strdupa(idnode_get_str(ina, sort->key) ?: "");
-        const char *strb = idnode_get_str(inb, sort->key);
+        const char *strb = idnode_get_str(inb, sort->key) ?: "";
         if (sort->dir == IS_ASC)
-          r = strcmp(stra ?: "", strb ?: "");
+          r = strcmp(stra, strb);
         else
-          r = strcmp(strb ?: "", stra ?: "");
+          r = strcmp(strb, stra);
         return r;
       }
       break;
@@ -505,6 +564,7 @@ idnode_cmp_sort
     case PT_U16:
     case PT_U32:
     case PT_BOOL:
+    case PT_PERM:
       {
         uint32_t u32a = 0, u32b = 0;
         idnode_get_u32(ina, sort->key, &u32a);
@@ -515,8 +575,32 @@ idnode_cmp_sort
           return u32b - u32a;
       }
       break;
+    case PT_S64:
+      {
+        int64_t s64a = 0, s64b = 0;
+        idnode_get_s64(ina, sort->key, &s64a);
+        idnode_get_s64(inb, sort->key, &s64b);
+        if (sort->dir == IS_ASC)
+          return s64a - s64b;
+        else
+          return s64b - s64a;
+      }
+      break;
     case PT_DBL:
       // TODO
+    case PT_TIME:
+      {
+        time_t ta = 0, tb = 0;
+        idnode_get_time(ina, sort->key, &ta);
+        idnode_get_time(inb, sort->key, &tb);
+        if (sort->dir == IS_ASC)
+          return ta - tb;
+        else
+          return tb - ta;
+      }
+      break;
+    case PT_LANGSTR:
+      // TODO?
     case PT_NONE:
       break;
   }
@@ -755,15 +839,12 @@ idnode_write0 ( idnode_t *self, htsmsg_t *c, int optmask, int dosave )
  * Read
  * *************************************************************************/
 
-/*
- * Save
- */
 void
-idnode_read0 ( idnode_t *self, htsmsg_t *c, int optmask )
+idnode_read0 ( idnode_t *self, htsmsg_t *c, htsmsg_t *list, int optmask )
 {
   const idclass_t *idc = self->in_class;
   for (; idc; idc = idc->ic_super)
-    prop_read_values(self, idc->ic_properties, c, optmask, NULL);
+    prop_read_values(self, idc->ic_properties, c, list, optmask);
 }
 
 /**
@@ -771,11 +852,11 @@ idnode_read0 ( idnode_t *self, htsmsg_t *c, int optmask )
  */
 static void
 add_params
-  (struct idnode *self, const idclass_t *ic, htsmsg_t *p, int optmask, htsmsg_t *inc)
+  (struct idnode *self, const idclass_t *ic, htsmsg_t *p, htsmsg_t *list, int optmask)
 {
   /* Parent first */
   if(ic->ic_super != NULL)
-    add_params(self, ic->ic_super, p, optmask, inc);
+    add_params(self, ic->ic_super, p, list, optmask);
 
   /* Seperator (if not empty) */
 #if 0
@@ -788,14 +869,14 @@ add_params
 #endif
 
   /* Properties */
-  prop_serialize(self, ic->ic_properties, p, optmask, inc);
+  prop_serialize(self, ic->ic_properties, p, list, optmask);
 }
 
 static htsmsg_t *
-idnode_params (const idclass_t *idc, idnode_t *self, int optmask)
+idnode_params (const idclass_t *idc, idnode_t *self, htsmsg_t *list, int optmask)
 {
   htsmsg_t *p  = htsmsg_create_list();
-  add_params(self, idc, p, optmask, NULL);
+  add_params(self, idc, p, list, optmask);
   return p;
 }
 
@@ -825,13 +906,44 @@ static const char *
 idclass_get_order (const idclass_t *idc)
 {
   while (idc) {
-    if (idc->ic_class)
+    if (idc->ic_order)
       return idc->ic_order;
     idc = idc->ic_super;
   }
   return NULL;
 }
 
+static htsmsg_t *
+idclass_get_property_groups (const idclass_t *idc)
+{
+  const property_group_t *g;
+  htsmsg_t *e, *m;
+  int count;
+  while (idc) {
+    if (idc->ic_groups) {
+      m = htsmsg_create_list();
+      count = 0;
+      for (g = idc->ic_groups; g->number && g->name; g++) {
+        e = htsmsg_create_map();
+        htsmsg_add_u32(e, "number", g->number);
+        htsmsg_add_str(e, "name",   g->name);
+        if (g->parent)
+          htsmsg_add_u32(e, "parent", g->parent);
+        if (g->column)
+          htsmsg_add_u32(e, "column", g->column);
+        htsmsg_add_msg(m, NULL, e);
+        count++;
+      }
+      if (count)
+        return m;
+      htsmsg_destroy(m);
+      break;
+    }
+    idc = idc->ic_super;
+  }
+  return NULL;
+}
+
 static int
 ic_cmp ( const idclass_link_t *a, const idclass_link_t *b )
 {
@@ -870,7 +982,7 @@ idclass_find ( const char *class )
  * Just get the class definition
  */
 htsmsg_t *
-idclass_serialize0(const idclass_t *idc, int optmask)
+idclass_serialize0(const idclass_t *idc, htsmsg_t *list, int optmask)
 {
   const char *s;
   htsmsg_t *p, *m = htsmsg_create_map();
@@ -882,9 +994,11 @@ idclass_serialize0(const idclass_t *idc, int optmask)
     htsmsg_add_str(m, "class", s);
   if ((s = idclass_get_order(idc)))
     htsmsg_add_str(m, "order", s);
+  if ((p = idclass_get_property_groups(idc)))
+    htsmsg_add_msg(m, "groups", p);
 
   /* Props */
-  if ((p = idnode_params(idc, NULL, optmask)))
+  if ((p = idnode_params(idc, NULL, list, optmask)))
     htsmsg_add_msg(m, "props", p);
   
   return m;
@@ -894,7 +1008,7 @@ idclass_serialize0(const idclass_t *idc, int optmask)
  *
  */
 htsmsg_t *
-idnode_serialize0(idnode_t *self, int optmask)
+idnode_serialize0(idnode_t *self, htsmsg_t *list, int optmask)
 {
   const idclass_t *idc = self->in_class;
   const char *uuid, *s;
@@ -909,7 +1023,7 @@ idnode_serialize0(idnode_t *self, int optmask)
   if ((s = idclass_get_class(idc)))
     htsmsg_add_str(m, "class", s);
 
-  htsmsg_add_msg(m, "params", idnode_params(idc, self, optmask));
+  htsmsg_add_msg(m, "params", idnode_params(idc, self, list, optmask));
 
   return m;
 }
index 43ce33280fa50692fe273d26981743efc866fe18..4a016d39b8dcf24f3923c2c2ff1ea86259b735d5 100644 (file)
@@ -26,7 +26,7 @@
 
 #include <regex.h>
 
-struct htsmsg;
+struct access;
 typedef struct idnode idnode_t;
 
 /*
@@ -39,17 +39,29 @@ typedef struct idnode_set
   size_t     is_count;  ///< Current usage of is_array
 } idnode_set_t;
 
+/*
+ * Property groups
+ */
+typedef struct property_group
+{
+  const char *name;
+  uint32_t    number;
+  uint32_t    parent;
+  uint32_t    column;
+} property_group_t;
+
 /*
  * Class definition
  */
 typedef struct idclass idclass_t;
 struct idclass {
-  const struct idclass  *ic_super;      /// Parent class
-  const char            *ic_class;      /// Class name
-  const char            *ic_caption;    /// Class description
-  const char            *ic_order;      /// Property order (comma separated)
-  const property_t      *ic_properties; /// Property list
-  const char            *ic_event;      /// Events to fire on add/delete/title
+  const struct idclass   *ic_super;        ///< Parent class
+  const char             *ic_class;        ///< Class name
+  const char             *ic_caption;      ///< Class description
+  const char             *ic_order;        ///< Property order (comma separated)
+  const property_group_t *ic_groups;       ///< Groups for visual representation
+  const property_t       *ic_properties;   ///< Property list
+  const char             *ic_event;        ///< Events to fire on add/delete/title
 
   /* Callbacks */
   idnode_set_t   *(*ic_get_childs) (idnode_t *self);
@@ -58,6 +70,7 @@ struct idclass {
   void            (*ic_delete)     (idnode_t *self);
   void            (*ic_moveup)     (idnode_t *self);
   void            (*ic_movedown)   (idnode_t *self);
+  int             (*ic_perm)       (idnode_t *self, struct access *a, htsmsg_t *msg_to_write);
 };
 
 /*
@@ -139,20 +152,30 @@ void idnode_notify_title_changed (void *in);
 
 void idclass_register ( const idclass_t *idc );
 const idclass_t *idclass_find ( const char *name );
-htsmsg_t *idclass_serialize0 (const idclass_t *idc, int optmask);
-htsmsg_t *idnode_serialize0  (idnode_t *self, int optmask);
-void      idnode_read0  (idnode_t *self, htsmsg_t *m, int optmask);
+htsmsg_t *idclass_serialize0 (const idclass_t *idc, htsmsg_t *list, int optmask);
+htsmsg_t *idnode_serialize0  (idnode_t *self, htsmsg_t *list, int optmask);
+void      idnode_read0  (idnode_t *self, htsmsg_t *m, htsmsg_t *list, int optmask);
 int       idnode_write0 (idnode_t *self, htsmsg_t *m, int optmask, int dosave);
 
-#define idclass_serialize(idc) idclass_serialize0(idc, 0)
-#define idnode_serialize(in)   idnode_serialize0(in, 0)
+#define idclass_serialize(idc) idclass_serialize0(idc, NULL, 0)
+#define idnode_serialize(in)   idnode_serialize0(in, NULL, 0)
 #define idnode_load(in, m)     idnode_write0(in, m, PO_NOSAVE, 0)
-#define idnode_save(in, m)     idnode_read0(in, m, PO_NOSAVE | PO_USERAW)
+#define idnode_save(in, m)     idnode_read0(in, m, NULL, PO_NOSAVE | PO_USERAW)
 #define idnode_update(in, m)   idnode_write0(in, m, PO_RDONLY | PO_WRONCE, 1)
 
+static inline int
+idnode_perm(idnode_t *self, struct access *a, htsmsg_t *msg_to_write)
+{
+  if (self->in_class->ic_perm)
+    return self->in_class->ic_perm(self, a, msg_to_write);
+  return 0;
+}
+
 const char *idnode_get_str (idnode_t *self, const char *key );
 int         idnode_get_u32 (idnode_t *self, const char *key, uint32_t *u32);
+int         idnode_get_s64 (idnode_t *self, const char *key,  int64_t *s64);
 int         idnode_get_bool(idnode_t *self, const char *key, int *b);
+int         idnode_get_time(idnode_t *self, const char *key, time_t *tm);
 
 void idnode_filter_add_str
   (idnode_filter_t *f, const char *k, const char *v, int t);
index b28a25cb9b461f27e74854a748812f308c0951f1..1cc5ed7c22547825410b3af2fb7230c7c0832487 100644 (file)
@@ -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;
 }
 
index c5cdc4e6a6c4504ad49f956b940f32d8292b70dc..7b410877437ed05628cf38ff8d712f18fb992606 100644 (file)
@@ -22,6 +22,7 @@
 #include <pthread.h>
 
 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;
index 4fe4aea81d2d851552e3faa225c6733940651762..ed9ee089b8dc09da2d33164b1475a390a812ee0a 100644 (file)
@@ -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 )
index ecd22413cabf2804e75baf91bda2fbe4f868f64c..ff901b8ba94d1af2245c343fb6a91de13476f54d 100644 (file)
@@ -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 );
 
index ec72ccda465fcfabc09856bf575bcb0023c08629..8576c1b277fc4cec031fc2d6a55bb64793eaf4ad 100644 (file)
@@ -48,7 +48,7 @@ typedef enum {
 /* Muxer configuration used when creating a muxer. */
 typedef struct muxer_config {
   int                  m_flags;
-  muxer_cache_type_t m_cache;
+  int                  m_cache;
 
 /* 
  * directory_permissions should really be in dvr.h as it's not really needed for the muxer
index 5ccd51dac281eea27a42167b1538b080a3fe7562..3e2a11eacd5d995d648d60c432f1b7e63129c9cf 100644 (file)
@@ -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);
   }
index 944eb1e8a6e92f1143bbdb0add66e97fc406e41c..05066adf0f1bf1f099bbf24f5a4fcf09b6a6e5db 100644 (file)
@@ -22,6 +22,7 @@
 
 #include "tvheadend.h"
 #include "prop.h"
+#include "lang_str.h"
 
 /* **************************************************************************
  * Utilities
  *
  */
 const static struct strtab typetab[] = {
-  { "bool",  PT_BOOL },
-  { "int",   PT_INT },
-  { "str",   PT_STR },
-  { "u16",   PT_U16 },
-  { "u32",   PT_U32 },
-  { "dbl",   PT_DBL },
+  { "bool",    PT_BOOL },
+  { "int",     PT_INT },
+  { "str",     PT_STR },
+  { "u16",     PT_U16 },
+  { "u32",     PT_U32 },
+  { "s64",     PT_S64 },
+  { "dbl",     PT_DBL },
+  { "time",    PT_TIME },
+  { "langstr", PT_LANGSTR },
+  { "perm",    PT_PERM },
 };
 
 
@@ -71,6 +76,7 @@ prop_write_values
   int64_t s64;
   uint32_t u32;
   uint16_t u16;
+  time_t tm;
 #define PROP_UPDATE(v, t)\
   new = &v;\
   if (!p->set && (*((t*)cur) != *((t*)new))) {\
@@ -81,6 +87,7 @@ prop_write_values
   if (!pl) return 0;
 
   for (p = pl; p->id; p++) {
+
     if (p->type == PT_NONE) continue;
 
     f = htsmsg_field_find(m, p->id);
@@ -89,6 +96,9 @@ prop_write_values
     /* Ignore */
     if(p->opts & optmask) continue;
 
+    /* Sanity check */
+    assert(p->set || p->off);
+
     /* Write */
     save = 0;
     cur  = obj + p->off;
@@ -127,6 +137,13 @@ prop_write_values
         PROP_UPDATE(u32, uint32_t);
         break;
       }
+      case PT_S64: {
+        if (htsmsg_field_get_s64(f, &s64))
+          continue;
+        i = s64;
+        PROP_UPDATE(i, int64_t);
+        break;
+      }
       case PT_DBL: {
         if (htsmsg_field_get_dbl(f, &dbl))
           continue;
@@ -144,6 +161,38 @@ prop_write_values
         }
         break;
       }
+      case PT_TIME: {
+        if (htsmsg_field_get_s64(f, &s64))
+          continue;
+        tm = s64;
+        PROP_UPDATE(tm, time_t);
+        break;
+      }
+      case PT_LANGSTR: {
+        lang_str_t **lstr1 = cur;
+        lang_str_t  *lstr2;
+        new = htsmsg_field_get_map(f);
+        if (!new)
+          continue;
+        if (!p->set) {
+          lstr2 = lang_str_deserialize_map((htsmsg_t *)new);
+          if (lang_str_compare(*lstr1, lstr2)) {
+            lang_str_destroy(*lstr1);
+            *lstr1 = lstr2;
+            save = 1;
+          } else {
+            lang_str_destroy(lstr2);
+          }
+        }
+        break;
+      }
+      case PT_PERM: {
+        if (!(new = htsmsg_field_get_str(f)))
+          continue;
+        u32 = (int)strtol(new,NULL,0);
+        PROP_UPDATE(u32, uint32_t);
+        break;
+      }
       case PT_NONE:
         break;
       }
@@ -175,19 +224,18 @@ prop_write_values
  */
 static void
 prop_read_value
-  (void *obj, const property_t *p, htsmsg_t *m, const char *name,
-   int optmask, htsmsg_t *inc)
+  (void *obj, const property_t *p, htsmsg_t *m, const char *name, int optmask)
 {
   const char *s;
   const void *val = obj + p->off;
+  char buf[16];
 
   /* Ignore */
   if (p->opts & optmask) return;
   if (p->type == PT_NONE) return;
 
-  /* Ignore */
-  if (inc && !htsmsg_get_u32_or_default(inc, p->id, 0))
-    return;
+  /* Sanity check */
+  assert(p->get || p->off);
 
   /* Get method */
   if (!(optmask & PO_USERAW) || !p->off)
@@ -207,11 +255,14 @@ prop_read_value
     case PT_INT:
       htsmsg_add_s64(m, name, *(int *)val);
       break;
+    case PT_U16:
+      htsmsg_add_u32(m, name, *(uint16_t *)val);
+      break;
     case PT_U32:
       htsmsg_add_u32(m, name, *(uint32_t *)val);
       break;
-    case PT_U16:
-      htsmsg_add_u32(m, name, *(uint16_t *)val);
+    case PT_S64:
+      htsmsg_add_s64(m, name, *(int64_t *)val);
       break;
     case PT_STR:
       if ((s = *(const char **)val))
@@ -220,6 +271,16 @@ prop_read_value
     case PT_DBL:
       htsmsg_add_dbl(m, name, *(double*)val);
       break;
+    case PT_TIME:
+      htsmsg_add_s64(m, name, *(time_t *)val);
+      break;
+    case PT_LANGSTR:
+      lang_str_serialize(*(lang_str_t **)val, m, name);
+      break;
+    case PT_PERM:
+      snprintf(buf, sizeof(buf), "%04o", *(uint32_t *)val);
+      htsmsg_add_str(m, name, buf);
+      break;
     case PT_NONE:
       break;
     }
@@ -231,110 +292,161 @@ prop_read_value
  */
 void
 prop_read_values
-  (void *obj, const property_t *pl, htsmsg_t *m, int optmask, htsmsg_t *inc)
+  (void *obj, const property_t *pl, htsmsg_t *m, htsmsg_t *list, int optmask)
 {
   if(pl == NULL)
     return;
-  for (; pl->id; pl++)
-    prop_read_value(obj, pl, m, pl->id, optmask, inc);
+
+  if(list == NULL) {
+    for (; pl->id; pl++)
+      prop_read_value(obj, pl, m, pl->id, optmask);
+  } else {
+    const property_t *p;
+    htsmsg_field_t *f;
+    int b;
+    HTSMSG_FOREACH(f, list) {
+      if (!htsmsg_field_get_bool(f, &b) && b > 0) {
+        p = prop_find(pl, f->hmf_name);
+        if (p)
+          prop_read_value(obj, p, m, p->id, optmask);
+      }
+    }
+  }
 }
 
 /**
  *
  */
-void
-prop_serialize
-  (void *obj, const property_t *pl, htsmsg_t *msg, int optmask, htsmsg_t *inc)
+static void
+prop_serialize_value
+  (void *obj, const property_t *pl, htsmsg_t *msg, int optmask)
 {
   htsmsg_field_t *f;
-
-  if(pl == NULL)
-    return;
-
-  for(; pl->id; pl++) {
-
-    /* Remove parent */
-    // TODO: this is really horrible and inefficient!
-    HTSMSG_FOREACH(f, msg) {
-      htsmsg_t *t = htsmsg_field_get_map(f);
-      const char *str;
-      if (t && (str = htsmsg_get_str(t, "id"))) {
-        if (!strcmp(str, pl->id)) {
-          htsmsg_field_destroy(msg, f);
-          break;
-        }
+  char buf[16];
+
+  /* Remove parent */
+  // TODO: this is really horrible and inefficient!
+  HTSMSG_FOREACH(f, msg) {
+    htsmsg_t *t = htsmsg_field_get_map(f);
+    const char *str;
+    if (t && (str = htsmsg_get_str(t, "id"))) {
+      if (!strcmp(str, pl->id)) {
+        htsmsg_field_destroy(msg, f);
+        break;
       }
     }
+  }
 
-    /* Ignore */
-    if (inc && !htsmsg_get_u32_or_default(inc, pl->id, 0))
-      continue;
+  htsmsg_t *m = htsmsg_create_map();
 
-    htsmsg_t *m = htsmsg_create_map();
+  /* ID / type */
+  htsmsg_add_str(m, "id",       pl->id);
+  htsmsg_add_str(m, "type",     val2str(pl->type, typetab) ?: "none");
 
-    /* ID / type */
-    htsmsg_add_str(m, "id",       pl->id);
-    htsmsg_add_str(m, "type",     val2str(pl->type, typetab) ?: "none");
+  /* Skip - special blocker */
+  if (pl->type == PT_NONE) {
+    htsmsg_add_msg(msg, NULL, m);
+    return;
+  }
 
-    /* Skip - special blocker */
-    if (pl->type == PT_NONE) {
-      htsmsg_add_msg(msg, NULL, m);
-      continue;
-    }
-      
-    /* Metadata */
-    htsmsg_add_str(m, "caption",  pl->name);
-    if (pl->islist)
-      htsmsg_add_u32(m, "list", 1);
-
-    /* Default */
-    // TODO: currently no support for list defaults
-    switch (pl->type) {
-      case PT_BOOL:
-        htsmsg_add_bool(m, "default", pl->def.i);
-        break;
-      case PT_INT:
-        htsmsg_add_s32(m, "default", pl->def.i);
-        break;
-      case PT_U16:
-        htsmsg_add_u32(m, "default", pl->def.u16);
-        break;
-      case PT_U32:
-        htsmsg_add_u32(m, "default", pl->def.u32);
-        break;
-      case PT_DBL:
-        htsmsg_add_dbl(m, "default", pl->def.d);
-        break;
-      case PT_STR:
-        htsmsg_add_str(m, "default", pl->def.s ?: "");
-        break;
-      case PT_NONE:
-        break;
-    }
+  /* Metadata */
+  htsmsg_add_str(m, "caption",  pl->name);
+  if (pl->islist)
+    htsmsg_add_u32(m, "list", 1);
 
-    /* Options */
-    if (pl->opts & PO_RDONLY)
-      htsmsg_add_bool(m, "rdonly", 1);
-    if (pl->opts & PO_NOSAVE)
-      htsmsg_add_bool(m, "nosave", 1);
-    if (pl->opts & PO_WRONCE)
-      htsmsg_add_bool(m, "wronce", 1);
-    if (pl->opts & PO_ADVANCED)
-      htsmsg_add_bool(m, "advanced", 1);
-    if (pl->opts & PO_HIDDEN)
-      htsmsg_add_bool(m, "hidden", 1);
-    if (pl->opts & PO_PASSWORD)
-      htsmsg_add_bool(m, "password", 1);
-
-    /* Enum list */
-    if (pl->list)
-      htsmsg_add_msg(m, "enum", pl->list(obj));
-
-    /* Data */
-    if (obj)
-      prop_read_value(obj, pl, m, "value", optmask, NULL);
+  /* Default */
+  // TODO: currently no support for list defaults
+  switch (pl->type) {
+    case PT_BOOL:
+      htsmsg_add_bool(m, "default", pl->def.i);
+      break;
+    case PT_INT:
+      htsmsg_add_s32(m, "default", pl->def.i);
+      break;
+    case PT_U16:
+      htsmsg_add_u32(m, "default", pl->def.u16);
+      break;
+    case PT_U32:
+      htsmsg_add_u32(m, "default", pl->def.u32);
+      break;
+    case PT_S64:
+      htsmsg_add_s64(m, "default", pl->def.s64);
+      break;
+    case PT_DBL:
+      htsmsg_add_dbl(m, "default", pl->def.d);
+      break;
+    case PT_STR:
+      htsmsg_add_str(m, "default", pl->def.s ?: "");
+      break;
+    case PT_TIME:
+      htsmsg_add_s64(m, "default", pl->def.tm);
+      break;
+    case PT_LANGSTR:
+      /* TODO? */
+      break;
+    case PT_PERM:
+      snprintf(buf, sizeof(buf), "%04o", pl->def.u32);
+      htsmsg_add_str(m, "default", buf);
+      break;
+    case PT_NONE:
+      break;
+  }
 
-    htsmsg_add_msg(msg, NULL, m);
+  /* Options */
+  if (pl->opts & PO_RDONLY)
+    htsmsg_add_bool(m, "rdonly", 1);
+  if (pl->opts & PO_NOSAVE)
+    htsmsg_add_bool(m, "nosave", 1);
+  if (pl->opts & PO_WRONCE)
+    htsmsg_add_bool(m, "wronce", 1);
+  if (pl->opts & PO_ADVANCED)
+    htsmsg_add_bool(m, "advanced", 1);
+  if (pl->opts & PO_HIDDEN)
+    htsmsg_add_bool(m, "hidden", 1);
+  if (pl->opts & PO_PASSWORD)
+    htsmsg_add_bool(m, "password", 1);
+  if (pl->opts & PO_DURATION)
+    htsmsg_add_bool(m, "duration", 1);
+
+  /* Enum list */
+  if (pl->list)
+    htsmsg_add_msg(m, "enum", pl->list(obj));
+
+  /* Visual group */
+  if (pl->group)
+    htsmsg_add_u32(m, "group", pl->group);
+
+  /* Data */
+  if (obj)
+    prop_read_value(obj, pl, m, "value", optmask);
+
+  htsmsg_add_msg(msg, NULL, m);
+}
+
+/**
+ *
+ */
+void
+prop_serialize
+  (void *obj, const property_t *pl, htsmsg_t *msg, htsmsg_t *list, int optmask)
+{
+  if(pl == NULL)
+    return;
+
+  if(list == NULL) {
+    for (; pl->id; pl++)
+      prop_serialize_value(obj, pl, msg, optmask);
+  } else {
+    const property_t *p;
+    htsmsg_field_t *f;
+    int b;
+    HTSMSG_FOREACH(f, list) {
+      if (!htsmsg_field_get_bool(f, &b) && b > 0) {
+        p = prop_find(pl, f->hmf_name);
+        if (p)
+          prop_serialize_value(obj, p, msg, optmask);
+      }
+    }
   }
 }
 
index 67fa2b1a2a6ac52ed2b024fe3077a0a26a256900..803543a9a37ccc8020db7aa287e04e2d07694655 100644 (file)
@@ -34,21 +34,26 @@ typedef enum {
   PT_INT,
   PT_U16,
   PT_U32,
+  PT_S64,
   PT_DBL,
+  PT_TIME,
+  PT_LANGSTR,
+  PT_PERM,                // like PT_U32 but with the special save
 } prop_type_t;
 
 /*
  * Property options
  */
-#define PO_NONE     0x00
-#define PO_RDONLY   0x01  // Property is read-only 
-#define PO_NOSAVE   0x02  // Property is transient (not saved)
-#define PO_WRONCE   0x04  // Property is write-once (i.e. on creation)
-#define PO_ADVANCED 0x08  // Property is advanced
-#define PO_HIDDEN   0x10  // Property is hidden (by default)
-#define PO_USERAW   0x20  // Only save the RAW (off) value if it exists
-#define PO_SORTKEY  0x40  // Sort using key (not display value)
-#define PO_PASSWORD 0x80  // String is a password
+#define PO_NONE     0x0000
+#define PO_RDONLY   0x0001  // Property is read-only
+#define PO_NOSAVE   0x0002  // Property is transient (not saved)
+#define PO_WRONCE   0x0004  // Property is write-once (i.e. on creation)
+#define PO_ADVANCED 0x0008  // Property is advanced
+#define PO_HIDDEN   0x0010  // Property is hidden (by default)
+#define PO_USERAW   0x0020  // Only save the RAW (off) value if it exists
+#define PO_SORTKEY  0x0040  // Sort using key (not display value)
+#define PO_PASSWORD 0x0080  // String is a password
+#define PO_DURATION 0x0100  // For PT_TIME - differentiate between duration and datetime
 
 /*
  * Property definition
@@ -59,7 +64,8 @@ typedef struct property {
   prop_type_t type;       ///< Type
   int         islist;     ///< Is a list
   size_t      off;        ///< Offset into object
-  int         opts;       ///< Options
+  uint32_t    opts;       ///< Options
+  uint32_t    group;      ///< Visual group ID (like ExtJS FieldSet)
 
   /* String based processing */
   const void *(*get)  (void *ptr);
@@ -72,10 +78,12 @@ typedef struct property {
   /* Default (for UI) */
   union {
     int         i;   // PT_BOOL/PT_INT
-    const char *s;   // PR_STR
+    const char *s;   // PT_STR
     uint16_t    u16; // PT_U16
-    uint32_t    u32; // PR_U32
+    uint32_t    u32; // PT_U32
+    int64_t     s64; // PT_S64
     double      d;   // PT_DBL
+    time_t      tm;  // PT_TIME
   } def;
 
   /* Notification callback */
@@ -89,10 +97,10 @@ int prop_write_values
   (void *obj, const property_t *pl, htsmsg_t *m, int optmask, htsmsg_t *updated);
 
 void prop_read_values
-  (void *obj, const property_t *pl, htsmsg_t *m, int optmask, htsmsg_t *inc);
+  (void *obj, const property_t *pl, htsmsg_t *m, htsmsg_t *list, int optmask);
 
 void prop_serialize
-  (void *obj, const property_t *pl, htsmsg_t *m, int optmask, htsmsg_t *inc);
+  (void *obj, const property_t *pl, htsmsg_t *m, htsmsg_t *list, int optmask);
 
 #endif /* __TVH_PROP_H__ */
 
index a95df8a8778f9e6474799b3007697cebf15ea6ec..e7c91b423a7fe66076ccfb6b9ebb6c85d286c184 100644 (file)
@@ -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;
 }
 
index b6f92ce0193c16bc5f0be9315fec658e004881a5..0771314475a784681df946be670526ec1c357a68 100755 (executable)
@@ -362,24 +362,6 @@ extjs_tablemgr(http_connection_t *hc, const char *remain, void *opaque)
   return 0;
 }
 
-/**
- * EPG Content Groups
- */
-static int
-extjs_ecglist(http_connection_t *hc, const char *remain, void *opaque)
-{
-  htsbuf_queue_t *hq = &hc->hc_reply;
-  htsmsg_t *out, *array;
-
-  out   = htsmsg_create_map();
-  array = epg_genres_list_all(1, 0);
-  htsmsg_add_msg(out, "entries", array);
-  htsmsg_json_serialize(out, hq, 0);
-  htsmsg_destroy(out);
-  http_output_content(hc, "text/x-json; charset=UTF-8");
-  return 0;
-}
-
 /**
  *
  */
@@ -502,127 +484,6 @@ extjs_epggrab(http_connection_t *hc, const char *remain, void *opaque)
   return 0;
 }
 
-/**
- *
- */
-static int
-extjs_confignames(http_connection_t *hc, const char *remain, void *opaque)
-{
-  htsbuf_queue_t *hq = &hc->hc_reply;
-  const char *op = http_arg_get(&hc->hc_req_args, "op");
-  htsmsg_t *out, *array, *e;
-  dvr_config_t *cfg;
-
-  pthread_mutex_lock(&global_lock);
-
-  if(op != NULL && !strcmp(op, "list")) {
-
-    out = htsmsg_create_map();
-    array = htsmsg_create_list();
-
-    if (http_access_verify(hc, ACCESS_RECORDER_ALL))
-      goto skip;
-
-    LIST_FOREACH(cfg, &dvrconfigs, config_link) {
-      e = htsmsg_create_map();
-      htsmsg_add_str(e, "identifier", cfg->dvr_config_name);
-      if (strlen(cfg->dvr_config_name) == 0)
-        htsmsg_add_str(e, "name", "(default)");
-      else
-        htsmsg_add_str(e, "name", cfg->dvr_config_name);
-      htsmsg_add_msg(array, NULL, e);
-    }
-
-skip:
-    htsmsg_add_msg(out, "entries", array);
-
-  } else {
-    pthread_mutex_unlock(&global_lock);
-    return HTTP_STATUS_BAD_REQUEST;
-  }
-
-  pthread_mutex_unlock(&global_lock);
-
-  htsmsg_json_serialize(out, hq, 0);
-  htsmsg_destroy(out);
-  http_output_content(hc, "text/x-json; charset=UTF-8");
-  return 0;
-
-}
-
-
-/**
- *
- */
-static int
-extjs_dvr_containers(http_connection_t *hc, const char *remain, void *opaque)
-{
-  htsbuf_queue_t *hq = &hc->hc_reply;
-  const char *op = http_arg_get(&hc->hc_req_args, "op");
-  htsmsg_t *out, *array;
-
-  pthread_mutex_lock(&global_lock);
-
-  if(op != NULL && !strcmp(op, "list")) {
-
-    out = htsmsg_create_map();
-    array = htsmsg_create_list();
-
-    muxer_container_list(array);
-
-    htsmsg_add_msg(out, "entries", array);
-
-  } else {
-    pthread_mutex_unlock(&global_lock);
-    return HTTP_STATUS_BAD_REQUEST;
-  }
-
-  pthread_mutex_unlock(&global_lock);
-
-  htsmsg_json_serialize(out, hq, 0);
-  htsmsg_destroy(out);
-  http_output_content(hc, "text/x-json; charset=UTF-8");
-  return 0;
-
-}
-
-
-/**
- *
- */
-static int
-extjs_dvr_caches(http_connection_t *hc, const char *remain, void *opaque)
-{
-  htsbuf_queue_t *hq = &hc->hc_reply;
-  const char *op = http_arg_get(&hc->hc_req_args, "op");
-  htsmsg_t *out, *array;
-
-  pthread_mutex_lock(&global_lock);
-
-  if(op != NULL && !strcmp(op, "list")) {
-
-    out = htsmsg_create_map();
-    array = htsmsg_create_list();
-
-    muxer_cache_list(array);
-
-    htsmsg_add_msg(out, "entries", array);
-
-  } else {
-    pthread_mutex_unlock(&global_lock);
-    return HTTP_STATUS_BAD_REQUEST;
-  }
-
-  pthread_mutex_unlock(&global_lock);
-
-  htsmsg_json_serialize(out, hq, 0);
-  htsmsg_destroy(out);
-  http_output_content(hc, "text/x-json; charset=UTF-8");
-  return 0;
-
-}
-
-
 /**
  *
  */
@@ -728,8 +589,8 @@ extjs_epg(http_connection_t *hc, const char *remain, void *opaque)
   else
     limit = 20; /* XXX */
 
-  if ((s = http_arg_get(&hc->hc_req_args, "contenttype"))) {
-    genre.code = atoi(s);
+  if ((s = http_arg_get(&hc->hc_req_args, "content_type"))) {
+    genre.code = atoi(s) * 16;
     eg = &genre;
   }
 
@@ -788,7 +649,7 @@ extjs_epg(http_connection_t *hc, const char *remain, void *opaque)
       htsmsg_add_str(m, "serieslink", e->serieslink->uri);
     
     if((eg = LIST_FIRST(&ee->genre))) {
-      htsmsg_add_u32(m, "contenttype", eg->code);
+      htsmsg_add_u32(m, "content_type", eg->code / 16);
     }
 
     dvr_entry_t *de;
@@ -930,501 +791,6 @@ extjs_epgobject(http_connection_t *hc, const char *remain, void *opaque)
   return 0;
 }
 
-/**
- *
- */
-static int
-extjs_dvr(http_connection_t *hc, const char *remain, void *opaque)
-{
-  htsbuf_queue_t *hq = &hc->hc_reply;
-  const char *op = http_arg_get(&hc->hc_req_args, "op");
-  htsmsg_t *out, *r;
-  dvr_entry_t *de;
-  const char *s;
-  int flags = 0;
-  dvr_config_t *cfg;
-  epg_broadcast_t *e;
-  char buffer[5]; // Permissions buffer: leading zero, three octal digits plus terminating null
-
-  if(op == NULL)
-    op = "loadSettings";
-
-  pthread_mutex_lock(&global_lock);
-
-  if(http_access_verify(hc, ACCESS_RECORDER)) {
-    pthread_mutex_unlock(&global_lock);
-    return HTTP_STATUS_UNAUTHORIZED;
-  }
-
-  if(!strcmp(op, "recordEvent") || !strcmp(op, "recordSeries")) {
-
-    const char *config_name = http_arg_get(&hc->hc_req_args, "config_name");
-
-    s = http_arg_get(&hc->hc_req_args, "eventId");
-    if((e = epg_broadcast_find_by_id(atoi(s), NULL)) == NULL) {
-      pthread_mutex_unlock(&global_lock);
-      return HTTP_STATUS_BAD_REQUEST;
-    }
-
-    if (http_access_verify(hc, ACCESS_RECORDER_ALL)) {
-      config_name = NULL;
-      LIST_FOREACH(cfg, &dvrconfigs, config_link) {
-        if (cfg->dvr_config_name && hc->hc_username &&
-            strcmp(cfg->dvr_config_name, hc->hc_username) == 0) {
-          config_name = cfg->dvr_config_name;
-          break;
-        }
-      }
-      if (config_name == NULL && hc->hc_username)
-        tvhlog(LOG_INFO,"dvr","User '%s' has no dvr config with identical name, using default...", hc->hc_username);
-    }
-
-    if (!strcmp(op, "recordEvent"))
-      dvr_entry_create_by_event(config_name,
-                                e, 0, 0, 
-                                hc->hc_representative, NULL, DVR_PRIO_NORMAL);
-    else
-      dvr_autorec_add_series_link(config_name, e, hc->hc_representative, "Created from EPG query");
-
-    out = htsmsg_create_map();
-    htsmsg_add_u32(out, "success", 1);
-  } else if(!strcmp(op, "cancelEntry")) {
-    s = http_arg_get(&hc->hc_req_args, "entryId");
-
-    if((de = dvr_entry_find_by_id(atoi(s))) == NULL) {
-      pthread_mutex_unlock(&global_lock);
-      return HTTP_STATUS_BAD_REQUEST;
-    }
-
-    dvr_entry_cancel(de);
-
-    out = htsmsg_create_map();
-    htsmsg_add_u32(out, "success", 1);
-
-  } else if(!strcmp(op, "deleteEntry")) {
-    s = http_arg_get(&hc->hc_req_args, "entryId");
-
-    if((de = dvr_entry_find_by_id(atoi(s))) == NULL) {
-      pthread_mutex_unlock(&global_lock);
-      return HTTP_STATUS_BAD_REQUEST;
-    }
-
-    dvr_entry_delete(de);
-
-    out = htsmsg_create_map();
-    htsmsg_add_u32(out, "success", 1);
-
-  } else if(!strcmp(op, "createEntry")) {
-
-    const char *config_name = http_arg_get(&hc->hc_req_args, "config_name");
-    const char *title    = http_arg_get(&hc->hc_req_args, "title");
-    const char *datestr  = http_arg_get(&hc->hc_req_args, "date");
-    const char *startstr = http_arg_get(&hc->hc_req_args, "starttime");
-    const char *stopstr  = http_arg_get(&hc->hc_req_args, "stoptime");
-    const char *channel  = http_arg_get(&hc->hc_req_args, "channelid");
-    const char *pri      = http_arg_get(&hc->hc_req_args, "pri");
-
-    channel_t *ch = channel ? channel_find(channel) : NULL;
-
-    if(ch == NULL || title == NULL || 
-       datestr  == NULL || strlen(datestr)  != 10 ||
-       startstr == NULL || strlen(startstr) != 5  ||
-       stopstr  == NULL || strlen(stopstr)  != 5) {
-      pthread_mutex_unlock(&global_lock);
-      return HTTP_STATUS_BAD_REQUEST;
-    }
-
-    struct tm t = {0};
-    t.tm_year = atoi(datestr + 6) - 1900;
-    t.tm_mon = atoi(datestr) - 1;
-    t.tm_mday = atoi(datestr + 3);
-    t.tm_isdst = -1;
-
-    t.tm_hour = atoi(startstr);
-    t.tm_min = atoi(startstr + 3);
-    
-    time_t start = mktime(&t);
-
-    t.tm_hour = atoi(stopstr);
-    t.tm_min = atoi(stopstr + 3);
-    
-    time_t stop = mktime(&t);
-
-    if(stop < start)
-      stop += 86400;
-
-    if (http_access_verify(hc, ACCESS_RECORDER_ALL)) {
-      config_name = NULL;
-      LIST_FOREACH(cfg, &dvrconfigs, config_link) {
-        if (cfg->dvr_config_name && hc->hc_username &&
-            strcmp(cfg->dvr_config_name, hc->hc_username) == 0) {
-          config_name = cfg->dvr_config_name;
-          break;
-        }
-      }
-      if (config_name == NULL && hc->hc_username)
-        tvhlog(LOG_INFO,"dvr","User '%s' has no dvr config with identical name, using default...", hc->hc_username);
-    }
-
-    dvr_entry_create(config_name,
-                     ch, start, stop, 0, 0, title, NULL, NULL,
-                     0, hc->hc_representative, 
-                                NULL, dvr_pri2val(pri));
-
-    out = htsmsg_create_map();
-    htsmsg_add_u32(out, "success", 1);
-
-  } else if(!strcmp(op, "createAutoRec")) {
-       int min_duration;
-       int max_duration;
-    epg_genre_t genre, *eg = NULL;
-
-    if ((s = http_arg_get(&hc->hc_req_args, "contenttype"))) {
-      genre.code = atoi(s);
-      eg = &genre;
-    }
-
-    if((s = http_arg_get(&hc->hc_req_args, "minduration")) != NULL)
-      min_duration = atoi(s);
-    else
-      min_duration = 0;
-
-    if((s = http_arg_get(&hc->hc_req_args, "maxduration")) != NULL)
-      max_duration = atoi(s);
-    else
-      max_duration = INT_MAX;
-
-    dvr_autorec_add(http_arg_get(&hc->hc_req_args, "config_name"),
-                    http_arg_get(&hc->hc_req_args, "title"),
-                    http_arg_get(&hc->hc_req_args, "channel"),
-                    http_arg_get(&hc->hc_req_args, "tag"),
-                    eg, min_duration,max_duration,
-                    hc->hc_representative, "Created from EPG query");
-
-    out = htsmsg_create_map();
-    htsmsg_add_u32(out, "success", 1);
-
-  } else if(!strcmp(op, "loadSettings")) {
-
-    s = http_arg_get(&hc->hc_req_args, "config_name");
-    if (s == NULL)
-      s = "";
-    cfg = dvr_config_find_by_name_default(s);
-
-    r = htsmsg_create_map();
-    htsmsg_add_str(r, "storage", cfg->dvr_storage);
-    htsmsg_add_str(r, "charset", cfg->dvr_charset ? cfg->dvr_charset : "UTF-8");
-    htsmsg_add_str(r, "container", muxer_container_type2txt(cfg->dvr_mc));
-
-/* Convert integer permissions to an octal-format 0xxx string and store it in the config file */
-
-    snprintf(buffer,sizeof(buffer),"%04o",cfg->dvr_muxcnf.m_file_permissions);
-    htsmsg_add_str(r, "filePermissions", buffer);
-    snprintf(buffer,sizeof(buffer),"%04o",cfg->dvr_muxcnf.m_directory_permissions);
-    htsmsg_add_str(r, "dirPermissions", buffer);
-
-    htsmsg_add_u32(r, "cache",     cfg->dvr_muxcnf.m_cache);
-    htsmsg_add_u32(r, "rewritePAT",
-                   !!(cfg->dvr_muxcnf.m_flags & MC_REWRITE_PAT));
-    htsmsg_add_u32(r, "rewritePMT",
-                   !!(cfg->dvr_muxcnf.m_flags & MC_REWRITE_PMT));
-    if(cfg->dvr_postproc != NULL)
-      htsmsg_add_str(r, "postproc", cfg->dvr_postproc);
-    htsmsg_add_u32(r, "retention", cfg->dvr_retention_days);
-    htsmsg_add_u32(r, "preExtraTime", cfg->dvr_extra_time_pre);
-    htsmsg_add_u32(r, "postExtraTime", cfg->dvr_extra_time_post);
-    htsmsg_add_u32(r, "dayDirs",        !!(cfg->dvr_flags & DVR_DIR_PER_DAY));
-    htsmsg_add_u32(r, "channelDirs",    !!(cfg->dvr_flags & DVR_DIR_PER_CHANNEL));
-    htsmsg_add_u32(r, "channelInTitle", !!(cfg->dvr_flags & DVR_CHANNEL_IN_TITLE));
-    htsmsg_add_u32(r, "dateInTitle",    !!(cfg->dvr_flags & DVR_DATE_IN_TITLE));
-    htsmsg_add_u32(r, "timeInTitle",    !!(cfg->dvr_flags & DVR_TIME_IN_TITLE));
-    htsmsg_add_u32(r, "whitespaceInTitle", !!(cfg->dvr_flags & DVR_WHITESPACE_IN_TITLE));
-    htsmsg_add_u32(r, "titleDirs", !!(cfg->dvr_flags & DVR_DIR_PER_TITLE));
-    htsmsg_add_u32(r, "episodeInTitle", !!(cfg->dvr_flags & DVR_EPISODE_IN_TITLE));
-    htsmsg_add_u32(r, "cleanTitle", !!(cfg->dvr_flags & DVR_CLEAN_TITLE));
-    htsmsg_add_u32(r, "tagFiles", !!(cfg->dvr_flags & DVR_TAG_FILES));
-    htsmsg_add_u32(r, "commSkip", !!(cfg->dvr_flags & DVR_SKIP_COMMERCIALS));
-    htsmsg_add_u32(r, "subtitleInTitle", !!(cfg->dvr_flags & DVR_SUBTITLE_IN_TITLE));
-    htsmsg_add_u32(r, "episodeBeforeDate", !!(cfg->dvr_flags & DVR_EPISODE_BEFORE_DATE));
-    htsmsg_add_u32(r, "episodeDuplicateDetection", !!(cfg->dvr_flags & DVR_EPISODE_DUPLICATE_DETECTION));
-
-    out = json_single_record(r, "dvrSettings");
-
-  } else if(!strcmp(op, "saveSettings")) {
-
-    s = http_arg_get(&hc->hc_req_args, "config_name");
-    cfg = dvr_config_find_by_name(s);
-    if (cfg == NULL)
-      cfg = dvr_config_create(s);
-
-    tvhlog(LOG_INFO,"dvr","Saving configuration '%s'", cfg->dvr_config_name);
-
-    if((s = http_arg_get(&hc->hc_req_args, "storage")) != NULL)
-      dvr_storage_set(cfg,s);
-    
-    if((s = http_arg_get(&hc->hc_req_args, "charset")) != NULL)
-      dvr_charset_set(cfg,s);
-
-    if((s = http_arg_get(&hc->hc_req_args, "container")) != NULL)
-      dvr_container_set(cfg,s);
-
-/*
- * Convert 0xxx format permission strings to integer for internal use
- * Note no checking that strtol won't overflow int - this should never happen with three-digit numbers
- */
-
-    if((s = http_arg_get(&hc->hc_req_args, "filePermissions")) != NULL)
-      dvr_file_permissions_set(cfg,(int)strtol(s,NULL,0));
-
-    if((s = http_arg_get(&hc->hc_req_args, "dirPermissions")) != NULL)
-      dvr_directory_permissions_set(cfg,(int)strtol(s,NULL,0));
-   
-    if((s = http_arg_get(&hc->hc_req_args, "cache")) != NULL)
-      dvr_mux_cache_set(cfg,atoi(s));
-
-    if((s = http_arg_get(&hc->hc_req_args, "postproc")) != NULL)
-      dvr_postproc_set(cfg,s);
-
-    if((s = http_arg_get(&hc->hc_req_args, "retention")) != NULL)
-      dvr_retention_set(cfg,atoi(s));
-
-    if((s = http_arg_get(&hc->hc_req_args, "preExtraTime")) != NULL)
-      dvr_extra_time_pre_set(cfg,atoi(s));
-
-    if((s = http_arg_get(&hc->hc_req_args, "postExtraTime")) != NULL)
-      dvr_extra_time_post_set(cfg,atoi(s));
-
-    if(http_arg_get(&hc->hc_req_args, "dayDirs") != NULL)
-      flags |= DVR_DIR_PER_DAY;
-    if(http_arg_get(&hc->hc_req_args, "channelDirs") != NULL)
-      flags |= DVR_DIR_PER_CHANNEL;
-    if(http_arg_get(&hc->hc_req_args, "channelInTitle") != NULL)
-      flags |= DVR_CHANNEL_IN_TITLE;
-    if(http_arg_get(&hc->hc_req_args, "cleanTitle") != NULL)
-      flags |= DVR_CLEAN_TITLE;
-    if(http_arg_get(&hc->hc_req_args, "dateInTitle") != NULL)
-      flags |= DVR_DATE_IN_TITLE;
-    if(http_arg_get(&hc->hc_req_args, "timeInTitle") != NULL)
-      flags |= DVR_TIME_IN_TITLE;
-    if(http_arg_get(&hc->hc_req_args, "whitespaceInTitle") != NULL)
-      flags |= DVR_WHITESPACE_IN_TITLE;
-    if(http_arg_get(&hc->hc_req_args, "titleDirs") != NULL)
-      flags |= DVR_DIR_PER_TITLE;
-    if(http_arg_get(&hc->hc_req_args, "episodeInTitle") != NULL)
-      flags |= DVR_EPISODE_IN_TITLE;
-    if(http_arg_get(&hc->hc_req_args, "tagFiles") != NULL)
-      flags |= DVR_TAG_FILES;
-    if(http_arg_get(&hc->hc_req_args, "commSkip") != NULL)
-      flags |= DVR_SKIP_COMMERCIALS;
-    if(http_arg_get(&hc->hc_req_args, "subtitleInTitle") != NULL)
-      flags |= DVR_SUBTITLE_IN_TITLE;
-    if(http_arg_get(&hc->hc_req_args, "episodeBeforeDate") != NULL)
-      flags |= DVR_EPISODE_BEFORE_DATE;
-    if(http_arg_get(&hc->hc_req_args, "episodeDuplicateDetection") != NULL)
-      flags |= DVR_EPISODE_DUPLICATE_DETECTION;
-
-
-    dvr_flags_set(cfg,flags);
-
-    /* Muxer flags */
-    flags = 0;
-    if(http_arg_get(&hc->hc_req_args, "rewritePAT") != NULL)
-      flags |= MC_REWRITE_PAT;
-    if(http_arg_get(&hc->hc_req_args, "rewritePMT") != NULL)
-      flags |= MC_REWRITE_PMT;
-
-    dvr_mux_flags_set(cfg, flags);
-
-    out = htsmsg_create_map();
-    htsmsg_add_u32(out, "success", 1);
-
-  } else if(!strcmp(op, "deleteSettings")) {
-
-    s = http_arg_get(&hc->hc_req_args, "config_name");
-    dvr_config_delete(s);
-
-    out = htsmsg_create_map();
-    htsmsg_add_u32(out, "success", 1);
-
-  } else {
-
-    pthread_mutex_unlock(&global_lock);
-    return HTTP_STATUS_BAD_REQUEST;
-  }
-
-  pthread_mutex_unlock(&global_lock);
-
-  htsmsg_json_serialize(out, hq, 0);
-  htsmsg_destroy(out);
-  http_output_content(hc, "text/x-json; charset=UTF-8");
-  return 0;
-
-}
-
-/**
- *
- */
-static int
-extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque,
-              dvr_entry_filter filter, dvr_entry_comparator cmp)
-{
-  htsbuf_queue_t *hq = &hc->hc_reply;
-  htsmsg_t *out, *array, *m;
-  dvr_query_result_t dqr;
-  dvr_entry_t *de;
-  int start = 0, end, limit, i;
-  const char *s;
-  int64_t fsize = 0;
-  char buf[100];
-
-  if((s = http_arg_get(&hc->hc_req_args, "start")) != NULL)
-    start = atoi(s);
-
-  if((s = http_arg_get(&hc->hc_req_args, "limit")) != NULL)
-    limit = atoi(s);
-  else
-    limit = 20; /* XXX */
-
-  pthread_mutex_lock(&global_lock);
-
-  if(http_access_verify(hc, ACCESS_RECORDER)) {
-    pthread_mutex_unlock(&global_lock);
-    return HTTP_STATUS_UNAUTHORIZED;
-  }
-
-  out = htsmsg_create_map();
-  array = htsmsg_create_list();
-
-
-  dvr_query_filter(&dqr, filter);
-
-  dvr_query_sort_cmp(&dqr, cmp);
-
-  htsmsg_add_u32(out, "totalCount", dqr.dqr_entries);
-
-  start = MIN(start, dqr.dqr_entries);
-  end = MIN(start + limit, dqr.dqr_entries);
-
-  for(i = start; i < end; i++) {
-    de = dqr.dqr_array[i];
-
-    m = htsmsg_create_map();
-
-    htsmsg_add_str(m, "channel", DVR_CH_NAME(de));
-    if(de->de_channel != NULL) {
-      htsmsg_add_str(m, "channelid", channel_get_uuid(de->de_channel));
-      if (de->de_channel->ch_icon)
-        htsmsg_add_imageurl(m, "chicon", "imagecache/%d",
-                            de->de_channel->ch_icon);
-    }
-
-    htsmsg_add_str(m, "config_name", de->de_config_name);
-
-    if(de->de_title != NULL)
-      htsmsg_add_str(m, "title", lang_str_get(de->de_title, NULL));
-
-    if(de->de_desc != NULL)
-      htsmsg_add_str(m, "description", lang_str_get(de->de_desc, NULL));
-
-    if (de->de_bcast && de->de_bcast->episode)
-      if (epg_episode_number_format(de->de_bcast->episode, buf, 100, NULL, "Season %d", ".", "Episode %d", "/%d"))
-        htsmsg_add_str(m, "episode", buf);
-
-    htsmsg_add_u32(m, "id", de->de_id);
-    htsmsg_add_u32(m, "start", de->de_start);
-    htsmsg_add_u32(m, "end", de->de_stop);
-    htsmsg_add_u32(m, "duration", de->de_stop - de->de_start);
-    
-    htsmsg_add_str(m, "creator", de->de_creator);
-
-    htsmsg_add_str(m, "pri", dvr_val2pri(de->de_pri));
-
-    htsmsg_add_str(m, "status", dvr_entry_status(de));
-    htsmsg_add_str(m, "schedstate", dvr_entry_schedstatus(de));
-
-
-    if(de->de_sched_state == DVR_COMPLETED) {
-      fsize = dvr_get_filesize(de);
-      if (fsize > 0) {
-        char url[100];
-        htsmsg_add_s64(m, "filesize", fsize);
-        snprintf(url, sizeof(url), "dvrfile/%d", de->de_id);
-        htsmsg_add_str(m, "url", url);
-      }
-    }
-
-    htsmsg_add_msg(array, NULL, m);
-  }
-
-  dvr_query_free(&dqr);
-
-  pthread_mutex_unlock(&global_lock);
-
-  htsmsg_add_msg(out, "entries", array);
-
-  htsmsg_json_serialize(out, hq, 0);
-  htsmsg_destroy(out);
-  http_output_content(hc, "text/x-json; charset=UTF-8");
-  return 0;
-}
-
-static int is_dvr_entry_finished(dvr_entry_t *entry)
-{
-  dvr_entry_sched_state_t state = entry->de_sched_state;
-  return state == DVR_COMPLETED && !entry->de_last_error && dvr_get_filesize(entry) != -1;
-}
-
-static int is_dvr_entry_upcoming(dvr_entry_t *entry)
-{
-  dvr_entry_sched_state_t state = entry->de_sched_state;
-  return state == DVR_RECORDING || state == DVR_SCHEDULED;
-}
-
-
-static int is_dvr_entry_failed(dvr_entry_t *entry)
-{
-  if (is_dvr_entry_finished(entry))
-    return 0;
-  if (is_dvr_entry_upcoming(entry))
-    return 0;
-  return 1;
-}
-
-static int
-extjs_dvrlist_finished(http_connection_t *hc, const char *remain, void *opaque)
-{
-  return extjs_dvrlist(hc, remain, opaque, is_dvr_entry_finished, dvr_sort_start_descending);
-}
-
-static int
-extjs_dvrlist_upcoming(http_connection_t *hc, const char *remain, void *opaque)
-{
-  return extjs_dvrlist(hc, remain, opaque, is_dvr_entry_upcoming, dvr_sort_start_ascending);
-}
-
-static int
-extjs_dvrlist_failed(http_connection_t *hc, const char *remain, void *opaque)
-{
-  return extjs_dvrlist(hc, remain, opaque, is_dvr_entry_failed, dvr_sort_start_descending);
-}
-
-/**
- *
- */
-void
-extjs_service_delete(htsmsg_t *in)
-{
-  htsmsg_field_t *f;
-  service_t *t;
-  const char *id;
-
-  TAILQ_FOREACH(f, &in->hm_fields, hmf_link) {
-    if((id = htsmsg_field_get_string(f)) != NULL &&
-       (t = service_find_by_identifier(id)) != NULL)
-      service_destroy(t, 1);
-  }
-}
-
 /**
  *
  */
@@ -1700,17 +1066,9 @@ extjs_start(void)
   http_path_add("/capabilities",     NULL, extjs_capabilities,     ACCESS_WEB_INTERFACE);
   http_path_add("/tablemgr",         NULL, extjs_tablemgr,         ACCESS_WEB_INTERFACE);
   http_path_add("/epggrab",          NULL, extjs_epggrab,          ACCESS_WEB_INTERFACE);
-  http_path_add("/confignames",      NULL, extjs_confignames,      ACCESS_WEB_INTERFACE);
   http_path_add("/epg",              NULL, extjs_epg,              ACCESS_WEB_INTERFACE);
   http_path_add("/epgrelated",       NULL, extjs_epgrelated,       ACCESS_WEB_INTERFACE);
   http_path_add("/epgobject",        NULL, extjs_epgobject,        ACCESS_WEB_INTERFACE);
-  http_path_add("/dvr",              NULL, extjs_dvr,              ACCESS_WEB_INTERFACE);
-  http_path_add("/dvrlist_upcoming", NULL, extjs_dvrlist_upcoming, ACCESS_WEB_INTERFACE);
-  http_path_add("/dvrlist_finished", NULL, extjs_dvrlist_finished, ACCESS_WEB_INTERFACE);
-  http_path_add("/dvrlist_failed",   NULL, extjs_dvrlist_failed,   ACCESS_WEB_INTERFACE);
-  http_path_add("/dvr_containers",   NULL, extjs_dvr_containers,   ACCESS_WEB_INTERFACE);
-  http_path_add("/dvr_caches",       NULL, extjs_dvr_caches,       ACCESS_WEB_INTERFACE);
-  http_path_add("/ecglist",          NULL, extjs_ecglist,          ACCESS_WEB_INTERFACE);
   http_path_add("/config",           NULL, extjs_config,           ACCESS_WEB_INTERFACE);
   http_path_add("/languages",        NULL, extjs_languages,        ACCESS_WEB_INTERFACE);
 #if ENABLE_TIMESHIFT
index 9bd698d96cd79c792c2f1c6dac930c3a6890dde5..b4db578f072e96e4ce8cfc4e7d7df1662a354583 100644 (file)
@@ -171,7 +171,7 @@ page_simple(http_connection_t *hc,
     rstatus = val2str(de->de_sched_state, recstatustxt);
 
 
-    htsbuf_qprintf(hq, "<a href=\"/pvrinfo/%d\">", de->de_id);
+    htsbuf_qprintf(hq, "<a href=\"/pvrinfo/%s\">", idnode_uuid_as_str(&de->de_id));
     
     htsbuf_qprintf(hq, 
                "%02d:%02d-%02d:%02d&nbsp; %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<br>", rstatus);
 
-  htsbuf_qprintf(hq, "<form method=\"post\" action=\"/pvrinfo/%d\">", 
-             de->de_id);
+  htsbuf_qprintf(hq, "<form method=\"post\" action=\"/pvrinfo/%s\">",
+                idnode_uuid_as_str(&de->de_id));
 
   switch(de->de_sched_state) {
   case DVR_SCHEDULED:
index 35a23e748e94f17f908e7c4fcd1f52230924a5ad..79569f5d625c40153b878db004b23e03c302fea2 100644 (file)
-tvheadend.weekdays = new Ext.data.SimpleStore({
-    fields: ['identifier', 'name'],
-    id: 0,
-    data: [['1', 'Mon'], ['2', 'Tue'], ['3', 'Wed'], ['4', 'Thu'],
-        ['5', 'Fri'], ['6', 'Sat'], ['7', 'Sun']]
-});
-
-//This should be loaded from tvheadend
-tvheadend.dvrprio = new Ext.data.SimpleStore({
-    fields: ['identifier', 'name'],
-    id: 0,
-    data: [['important', 'Important'], ['high', 'High'],
-        ['normal', 'Normal'], ['low', 'Low'],
-        ['unimportant', 'Unimportant']]
-});
-
-//For the container configuration
-tvheadend.containers = new Ext.data.JsonStore({
-    autoLoad: true,
-    root: 'entries',
-    fields: ['name', 'description'],
-    id: 'name',
-    url: 'dvr_containers',
-    baseParams: {
-        op: 'list'
-    }
-});
-
-//For the cache configuration
-tvheadend.charsets = new Ext.data.JsonStore({
-    autoLoad: true,
-    root: 'entries',
-    fields: ['key', 'val'],
-    id: 'key',
-    url: 'api/intlconv/charsets',
-    baseParams: {
-        enum: 1
-    }
-});
-
-//For the charset configuration
-tvheadend.caches = new Ext.data.JsonStore({
-    autoLoad: true,
-    root: 'entries',
-    fields: ['index', 'description'],
-    id: 'name',
-    url: 'dvr_caches',
-    baseParams: {
-        op: 'list'
-    }
-});
-
-/**
- * Configuration names
+/*
+ * DVR Config / Schedule / Log editor and viewer
  */
-tvheadend.configNames = new Ext.data.JsonStore({
-    autoLoad: true,
-    root: 'entries',
-    fields: ['identifier', 'name'],
-    id: 'identifier',
-    url: 'confignames',
-    baseParams: {
-        op: 'list'
-    }
-});
-
-tvheadend.configNames.setDefaultSort('name', 'ASC');
-
-tvheadend.comet.on('dvrconfig', function(m) {
-    if (m.reload != null)
-        tvheadend.configNames.reload();
-});
 
 /**
  *
  */
-tvheadend.dvrDetails = function(entry) {
-
-    var content = '';
-    var but;
-
-    if (entry.chicon != null && entry.chicon.length > 0)
-        content += '<img class="x-epg-chicon" src="'
-                + entry.chicon + '">';
+tvheadend.dvrDetails = function(uuid) {
+
+    function showit(d) {
+        var params = d[0].params;
+        var chicon = params[0].value;
+        var title = params[1].value;
+        var desc = params[2].value;
+        var status = params[3].value;
+        var content = '';
+        var but;
+
+        if (chicon != null && chicon.length > 0)
+            content += '<img class="x-epg-chicon" src="' + chicon + '">';
+
+        content += '<div class="x-epg-title">' + title + '</div>';
+        content += '<div class="x-epg-desc">' + desc + '</div>';
+        content += '<hr>';
+        content += '<div class="x-epg-meta">Status: ' + status + '</div>';
+
+        var win = new Ext.Window({
+            title: title,
+            layout: 'fit',
+            width: 400,
+            height: 300,
+            constrainHeader: true,
+            buttonAlign: 'center',
+            html: content
+        });
 
-    content += '<div class="x-epg-title">' + entry.title + '</div>';
-    content += '<div class="x-epg-desc">' + entry.description + '</div>';
-    content += '<hr>';
-    content += '<div class="x-epg-meta">Status: ' + entry.status + '</div>';
+        win.show();
+    }
 
-    var win = new Ext.Window({
-        title: entry.title,
-        layout: 'fit',
-        width: 400,
-        height: 300,
-        constrainHeader: true,
-        buttonAlign: 'center',
-        html: content
+    tvheadend.loading(1);
+    Ext.Ajax.request({
+        url: 'api/idnode/load',
+        params: {
+            uuid: uuid,
+            list: 'channel_icon,disp_title,disp_description,status',
+        },
+        success: function(d) {
+            d = json_decode(d);
+            tvheadend.loading(0),
+            showit(d);
+        },
+        failure: function(d) {
+            tvheadend.loading(0);
+        }
     });
-
-    win.show();
 };
 
-/**
- *
- */
-tvheadend.dvrschedule = function(title, iconCls, dvrStore) {
-
-    var actions = new Ext.ux.grid.RowActions({
+tvheadend.dvrRowActions = function() {
+    return new Ext.ux.grid.RowActions({
+        id: 'details',
         header: 'Details',
-        dataIndex: 'actions',
         width: 45,
         actions: [
             {
-                iconIndex: 'schedstate'
+                iconIndex: 'sched_status'
             },
             {
                 iconCls: 'info',
                 qtip: 'Recording details',
                 cb: function(grid, rec, act, row) {
-                    new tvheadend.dvrDetails(grid.getStore().getAt(row).data);
+                    new tvheadend.dvrDetails(grid.getStore().getAt(row).id);
                 }
             }
         ]
     });
+}
 
-    function renderDate(value) {
-        var dt = new Date(value);
-        return dt.format('D j M H:i');
-    }
-
-    function renderDuration(value) {
-        value = value / 60; /* Nevermind the seconds */
-
-        if (value >= 60) {
-            var min = parseInt(value % 60);
-            var hours = parseInt(value / 60);
-
-            if (min === 0) {
-                return hours + ' hrs';
-            }
-            return hours + ' hrs, ' + min + ' min';
-        }
-        else {
-            return parseInt(value) + ' min';
-        }
-    }
-
-    function renderSize(value)
-    {
-        if (value == null)
-            return '';
-        return parseInt(value / 1000000) + ' MB';
-    }
-
-    function renderPri(value) {
-        return tvheadend.dvrprio.getById(value).data.name;
-    }
-
-    var cols = [actions];
-    if (iconCls === 'television')
-        cols.push({
-            width: 40,
-            header: "Play",
-            renderer: function(v, o, r) {
-                var title = r.data['title'];
-                if (r.data['episode'])
-                    title += ' / ' + r.data['episode'];
-                return '<a href="play/dvrfile/' + r.data['id'] +
-                       '?title=' + encodeURIComponent(title) + '">Play</a>';
-            }
-        });
-    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 '<span class="tvh-grid-unset">(default)</span>';
-                }
-                else {
-                    return value;
-                }
-            },
-            dataIndex: 'config_name',
-            hidden: iconCls !== 'clock'
-        });
-    if (iconCls === 'exclamation')
-        cols.push({
-            width: 200,
-            id: 'status',
-            header: "Status",
-            sortable: true,
-            dataIndex: 'status',
-            hidden: iconCls !== 'exclamation'
-        });
-
-    var dvrCm = new Ext.grid.ColumnModel({columns: cols});
-
-    function addEntry() {
-
-        function createRecording() {
-            panel.getForm().submit({
-                params: {
-                    'op': 'createEntry'
-                },
-                url: 'dvr/addentry',
-                waitMsg: 'Creating entry...',
-                failure: function(response, options) {
-                    Ext.MessageBox.alert('Server Error', 'Unable to create entry');
-                },
-                success: function() {
-                    win.close();
-                }
-            });
-        }
-
-        var panel = new Ext.FormPanel({
-            frame: true,
-            border: true,
-            bodyStyle: 'padding:5px',
-            labelAlign: 'right',
-            labelWidth: 110,
-            defaultType: 'textfield',
-            items: [new Ext.form.ComboBox({
-                    fieldLabel: 'Channel',
-                    name: 'channel',
-                    hiddenName: 'channelid',
-                    editable: false,
-                    allowBlank: false,
-                    displayField: 'val',
-                    valueField: 'key',
-                    mode: 'remote',
-                    triggerAction: 'all',
-                    store: tvheadend.channels
-                }), new Ext.form.DateField({
-                    allowBlank: false,
-                    fieldLabel: 'Date',
-                    name: 'date'
-                }), new Ext.form.TimeField({
-                    allowBlank: false,
-                    fieldLabel: 'Start time',
-                    name: 'starttime',
-                    increment: 10,
-                    format: 'H:i'
-                }), new Ext.form.TimeField({
-                    allowBlank: false,
-                    fieldLabel: 'Stop time',
-                    name: 'stoptime',
-                    increment: 10,
-                    format: 'H:i'
-                }), new Ext.form.ComboBox({
-                    store: tvheadend.dvrprio,
-                    value: "normal",
-                    triggerAction: 'all',
-                    mode: 'local',
-                    fieldLabel: 'Priority',
-                    valueField: 'identifier',
-                    displayField: 'name',
-                    name: 'pri'
-                }), {
-                    allowBlank: false,
-                    fieldLabel: 'Title',
-                    name: 'title'
-                }, new Ext.form.ComboBox({
-                    store: tvheadend.configNames,
-                    triggerAction: 'all',
-                    mode: 'local',
-                    fieldLabel: 'DVR Configuration',
-                    valueField: 'identifier',
-                    displayField: 'name',
-                    name: 'config_name',
-                    emptyText: '(default)',
-                    value: '',
-                    editable: false
-                })],
-            buttons: [{
-                    text: 'Create',
-                    handler: createRecording
-                }]
-        });
-
-        win = new Ext.Window({
-            title: 'Add single recording',
-            layout: 'fit',
-            width: 500,
-            height: 300,
-            plain: true,
-            items: panel
-        });
-        win.show();
-        new Ext.form.ComboBox({
-            store: tvheadend.configNames,
-            triggerAction: 'all',
-            mode: 'local',
-            fieldLabel: 'DVR Configuration',
-            valueField: 'identifier',
-            displayField: 'name',
-            name: 'config_name',
-            emptyText: '(default)',
-            value: '',
-            editable: false
-        });
-    }
-    ;
-
-    /* Create combobox to allow user to select page size for upcoming/completed/failed recordings */
-
-    var itemPageCombo = new Ext.form.ComboBox({
-        name : 'itemsperpage',
-        width: 50,
-        mode : 'local',
-        store: new Ext.data.ArrayStore({
-            fields: ['perpage','value'],
-            data  : [['10',10],['20',20],['30',30],['40',40],['50',50],['75',75],['100',100],['All',9999999999]]
-        }),
-        value : '20',
-        listWidth : 40,
-        triggerAction : 'all',
-        displayField : 'perpage',
-        valueField : 'value',
-        editable : true,
-        forceSelection : true,
-        listeners : {
-            scope: this,
-            'select' : function(combo, record) {
-                bbar.pageSize = parseInt(record.get('value'), 10);
-                bbar.doLoad(bbar.cursor);
-            }
-        }
-    });
-
-    /* Bottom toolbar to include default previous/goto-page/next and refresh buttons, also number-of-items combobox */
-
-    var bbar = new Ext.PagingToolbar({
-        store : dvrStore,
-        displayInfo : true,
-        items : ['-','Recordings per page: ',itemPageCombo],
-        displayMsg : 'Programs {0} - {1} of {2}',
-        emptyMsg : "No programs to display"
-    });
-
-    function abortEntry(btn) {
-        if (btn !== 'yes')
-            return;
-
-        var selectedKeys = panel.selModel.selections.keys;
-
-        // Delete each entry one by one since the API doesn't support deleting
-        // multiple
-        for (var i = 0; i < selectedKeys.length; i++) {
-            var recordingId = selectedKeys[i];
-            
-            Ext.Ajax.request({
-                url: 'dvr',
-                params: {
-                    entryId: recordingId,
-                    op: 'cancelEntry'
-                },
-                failure: function(response, options) {
-                    Ext.MessageBox.alert('Server Error', 'Unable to cancel recording');
-                }
-            });
-        }
-    };
-    
-    function deleteEntry(btn) {
-        if (btn !== 'yes')
-            return;
-
-        var selectedKeys = panel.selModel.selections.keys;
-            
-        // Delete each entry one by one since the API doesn't support deleting
-        // multiple
-        for (var i = 0; i < selectedKeys.length; i++) {
-            var recordingId = selectedKeys[i];
-
-            Ext.Ajax.request({
-                url: 'dvr',
-                params: {
-                    entryId: recordingId,
-                    op: 'deleteEntry'
-                },
-                success: function(response, options) {
-
-                },
-                failure: function(response, options) {
-                    Ext.MessageBox.alert('Server Error', 'Unable to delete recording');
-                }
-            });
-        }
-    };
-    
-    function downloadSelected() {
-        var selectedKey = panel.selModel.selections.keys[0];
-        var entry = dvrStore.getById(selectedKey);
-        
-        window.location = entry.data.url;
-    }
-
-    function abortSelected() {
-        Ext.MessageBox.confirm('Message',
-            'Do you really want to abort/unschedule the selection?', abortEntry);
-    };
-
-    function deleteSelected() {
-        Ext.MessageBox.confirm('Message',
-            'Do you really want to delete the selection?', deleteEntry);
-    };
-
-    var abortButton = new Ext.Toolbar.Button({
-        tooltip: 'Abort or unschedule one or more selected rows',
-        iconCls: 'remove',
-        text: 'Abort/unschedule selected',
-        handler: abortSelected,
-        disabled: true
-    });
-    
-    var downloadButton = new Ext.Toolbar.Button({
-        tooltip: 'Download the selected recording',
-        iconCls: 'save',
-        text: 'Download',
-        handler: downloadSelected,
-        disabled: true
-    });
-
-    var deleteButton = new Ext.Toolbar.Button({
-        tooltip: 'Delete one or more selected rows',
-        iconCls: 'remove',
-        text: 'Delete selected',
-        handler: deleteSelected,
-        disabled: true
-    });
-
-    // Make multiple rows selectable
-    var selModel = new Ext.grid.RowSelectionModel({
-        singleSelect: false
-    });
-
-    // Enable/disable some buttons when nothing is selected
-    selModel.on('selectionchange', function(self) {
-        if (self.getCount() > 0) {
-            deleteButton.enable();
-            abortButton.enable();
-            
-            // It only makes sense to download one item at a time
-            if (self.getCount() === 1)
-                downloadButton.enable();
-            else
-                downloadButton.disable();
-        }
-        else {
-            downloadButton.disable();
-            deleteButton.disable();
-            abortButton.disable();
-        }
-    });
-
-    // Define which panel buttons should be visible
-    var panelButtons = [];
-
-    // Add the "Add entry" and "Abort" buttons only to "Upcoming recordings"
-    if (iconCls === 'clock') {
-        panelButtons.push([
-            {
-                tooltip: 'Schedule a new recording session on the server.',
-                iconCls: 'add',
-                text: 'Add entry',
-                handler: addEntry
+/**
+ *
+ */
+tvheadend.dvr_upcoming = function(panel, index) {
+
+    var actions = tvheadend.dvrRowActions();
+
+    tvheadend.idnode_grid(panel, {
+        url: 'api/dvr/entry',
+        gridURL: 'api/dvr/entry/grid_upcoming',
+        comet: 'dvrentry',
+        titleS: 'Upcoming Recording',
+        titleP: 'Upcoming Recordings',
+        iconCls: 'clock',
+        tabIndex: index,
+        add: {
+            url: 'api/dvr/entry',
+            params: {
+               list: 'disp_title,start,start_extra,stop,stop_extra,' +
+                     'channel,config_name',
             },
-            abortButton
-        ]);
-    }
-    // Add the "Download" and "Delete" button to the others
-    else {
-        panelButtons.push(downloadButton);
-        panelButtons.push(deleteButton);
-    }
-
-    // Add the help button to all panels
-    panelButtons.push([
-        '->',
-        {
-            text: 'Help',
-            handler: function() {
-                new tvheadend.help('Digital Video Recorder', 'dvrlog.html');
-            }
-        }
-    ]);
-
-    var panel = new Ext.grid.GridPanel({
-        stateful: true,
-        stateId: dvrStore.url,
-        loadMask: true,
-        stripeRows: true,
-        disableSelection: false,
-        title: title,
-        iconCls: iconCls,
-        store: dvrStore,
-        selModel: selModel,
-        cm: dvrCm,
+            create: { }
+        },
+        del: true,
+        list: 'disp_title,episode,pri,start_real,stop_real,' +
+              'duration,channelname,creator,config_name,' +
+              'sched_status',
+        sort: {
+          field: 'start',
+          direction: 'DESC'
+        },
         plugins: [actions],
-        viewConfig: {
-            forceFit: true
+        lcol: [actions],
+        help: function() {
+            new tvheadend.help('DVR', 'config_dvr.html');
         },
-        tbar: panelButtons,
-        bbar: bbar
     });
 
     return panel;
@@ -589,710 +119,164 @@ tvheadend.dvrschedule = function(title, iconCls, dvrStore) {
 /**
  *
  */
-tvheadend.autoreceditor = function() {
-    var fm = Ext.form;
-
-    var cm = new Ext.grid.ColumnModel({
-        defaultSortable: true,
-        columns:
-                [
-                    {
-                        header: 'Enabled',
-                        dataIndex: 'enabled',
-                        width: 30,
-                        xtype: 'checkcolumn'
-                    },
-                    {
-                        header: "Title (Regexp)",
-                        dataIndex: 'title',
-                        editor: new fm.TextField({
-                            allowBlank: true
-                        })
-                    },
-                    {
-                        header: "Channel",
-                        dataIndex: 'channel',
-                        editor: new Ext.form.ComboBox({
-                            loadingText: 'Loading...',
-                            displayField: 'val',
-                            valueField: 'key',
-                            store: tvheadend.channels,
-                            mode: 'local',
-                            editable: true,
-                            forceSelection: true,
-                            typeAhead: true,
-                            triggerAction: 'all',
-                            emptyText: 'Only include channel...'
-                        }),
-                        renderer: function(v) {
-                            return tvheadend.channelLookupName(v);
-                        },
-                    },
-                    {
-                        header: "SeriesLink",
-                        dataIndex: 'serieslink',
-                        renderer: function(v) {
-                            return v ? 'yes' : 'no';
-                        }
-                    },
-                    {
-                        header: "Channel tag",
-                        dataIndex: 'tag',
-                        editor: new Ext.form.ComboBox({
-                            displayField: 'val',
-                            store: tvheadend.channelTags,
-                            mode: 'local',
-                            editable: true,
-                            forceSelection: true,
-                            typeAhead: true,
-                            triggerAction: 'all',
-                            emptyText: 'Only include tag...'
-                        })
-                    },
-                    {
-                        header: "Genre",
-                        dataIndex: 'contenttype',
-                        renderer: function(v) {
-                            return tvheadend.contentGroupLookupName(v);
-                        },
-                        editor: new Ext.form.ComboBox({
-                            valueField: 'code',
-                            displayField: 'name',
-                            store: tvheadend.ContentGroupStore,
-                            mode: 'local',
-                            editable: true,
-                            forceSelection: true,
-                            typeAhead: true,
-                            triggerAction: 'all',
-                            emptyText: 'Only include content...'
-                        })
-                    },
-                    {
-                        header: "Duration",
-                        dataIndex: 'minduration',
-                        renderer: function(v) {
-                            return tvheadend.durationLookupRange(v);
-                        },
-                        editor: durationCombo = new Ext.form.ComboBox({
-                            store: tvheadend.DurationStore,
-                            mode: 'local',
-                            valueField: 'minvalue',
-                            displayField: 'label',
-                            editable: true,
-                            forceSelection: true,
-                            typeAhead: true,
-                            triggerAction: 'all',
-                            id: 'minfield'
-                        })
-                    },
-                    {
-                        header: "Weekdays",
-                        dataIndex: 'weekdays',
-                        renderer: function(value, metadata, record, row, col, store) {
-                            if (value.split)
-                                value = value.split(',');
-                            if (value.length === 7)
-                                return 'All days';
-                            if (value.length === 0 || value[0] === "")
-                                return 'No days';
-                            ret = [];
-                            tags = value;
-                            for (var i = 0; i < tags.length; i++) {
-                                var tag = tvheadend.weekdays.getById(tags[i]);
-                                if (typeof tag !== 'undefined')
-                                    ret.push(tag.data.name);
-                            }
-                            return ret.join(', ');
-                        },
-                        editor: new Ext.ux.form.LovCombo({
-                            store: tvheadend.weekdays,
-                            mode: 'local',
-                            valueField: 'identifier',
-                            displayField: 'name'
-                        })
-                    }, {
-                        header: "Starting Around",
-                        dataIndex: 'approx_time',
-                        renderer: function(value, metadata, record, row, col, store) {
-                            if (typeof value === 'string')
-                                return value;
-
-                            if (value === 0)
-                                return '';
-
-                            var hours = Math.floor(value / 60);
-                            var mins = value % 60;
-                            var dt = new Date();
-                            dt.setHours(hours);
-                            dt.setMinutes(mins);
-                            return dt.format('H:i');
-                        },
-                        editor: new Ext.form.TimeField({
-                            allowBlank: true,
-                            increment: 10,
-                            format: 'H:i'
-                        })
-                    }, {
-                        header: "Priority",
-                        dataIndex: 'pri',
-                        width: 100,
-                        renderer: function(value, metadata, record, row, col, store) {
-                            return tvheadend.dvrprio.getById(value).data.name;
-                        },
-                        editor: new fm.ComboBox({
-                            store: tvheadend.dvrprio,
-                            triggerAction: 'all',
-                            mode: 'local',
-                            valueField: 'identifier',
-                            displayField: 'name'
-                        })
-                    }, {
-                        header: "DVR Configuration",
-                        dataIndex: 'config_name',
-                        renderer: function(value, metadata, record, row, col, store) {
-                            if (!value) {
-                                return '<span class="tvh-grid-unset">(default)</span>';
-                            }
-                            else {
-                                return value;
-                            }
-                        },
-                        editor: new Ext.form.ComboBox({
-                            store: tvheadend.configNames,
-                            triggerAction: 'all',
-                            mode: 'local',
-                            valueField: 'identifier',
-                            displayField: 'name',
-                            name: 'config_name',
-                            emptyText: '(default)',
-                            editable: false
-                        })
-                    }, {
-                        header: "Created by",
-                        dataIndex: 'creator',
-                        editor: new fm.TextField({
-                            allowBlank: false
-                        })
-                    }, {
-                        header: "Comment",
-                        dataIndex: 'comment',
-                        editor: new fm.TextField({
-                            allowBlank: false
-                        })
-                    }]});
-
-    tvheadend.autorecStore.on('update', function (store, record, operation) {
-        if (operation == 'edit') {
-            if (record.isModified('minduration')) {
-                if (record.data.minduration == "")
-                    record.set('maxduration',"");
-                else {
-                    var index = tvheadend.DurationStore.find('minvalue', record.data.minduration); 
-
-                    if (index !== -1)
-                        record.set('maxduration', tvheadend.DurationStore.getById(index).data.maxvalue);
+tvheadend.dvr_finished = function(panel, index) {
+
+    var actions = tvheadend.dvrRowActions();
+
+    tvheadend.idnode_grid(panel, {
+        url: 'api/dvr/entry',
+        gridURL: 'api/dvr/entry/grid_finished',
+        readonly: true,
+        comet: 'dvrentry',
+        titleS: 'Finished Recording',
+        titleP: 'Finished Recordings',
+        iconCls: 'television',
+        tabIndex: index,
+        del: true,
+        list: 'disp_title,episode,start_real,stop_real,' +
+              'duration,filesize,channelname,creator,' +
+              'sched_status',
+        sort: {
+          field: 'start',
+          direction: 'DESC'
+        },
+        plugins: [actions],
+        lcol: [
+            actions,
+            {
+                width: 40,
+                header: "Play",
+                renderer: function(v, o, r) {
+                    var title = r.data['title'];
+                    if (r.data['episode'])
+                        title += ' / ' + r.data['episode'];
+                    return '<a href="play/dvrfile/' + r.data['id'] +
+                           '?title=' + encodeURIComponent(title) + '">Play</a>';
                 }
-            }
-
-            if (record.isModified('channel') && record.data.channel == -1)
-                record.set('channel',"");
-
-            if (record.isModified('tag') && record.data.tag == '(Clear filter)')
-                record.set('tag',"");
-
-            if (record.isModified('contenttype') && record.data.contenttype == -1) 
-                record.set('contenttype',"");
-        }
+            }],
+        help: function() {
+            new tvheadend.help('DVR', 'config_dvr.html');
+        },
     });
 
-    return new tvheadend.tableEditor('Automatic Recorder', 'autorec', cm,
-            tvheadend.autorecRecord, [], tvheadend.autorecStore,
-            'autorec.html', 'wand');
+    return panel;
 };
 
 /**
  *
  */
-tvheadend.dvr = function() {
-
-    function datastoreBuilder(url) {
-        return new Ext.data.JsonStore({
-            root: 'entries',
-            totalProperty: 'totalCount',
-            fields: [{
-                    name: 'id'
-                }, {
-                    name: 'channel'
-                }, {
-                    name: 'title'
-                }, {
-                    name: 'episode'
-                }, {
-                    name: 'pri'
-                }, {
-                    name: 'description'
-                }, {
-                    name: 'chicon'
-                }, {
-                    name: 'start',
-                    type: 'date',
-                    dateFormat: 'U' /* unix time */
-                }, {
-                    name: 'end',
-                    type: 'date',
-                    dateFormat: 'U' /* unix time */
-                }, {
-                    name: 'config_name'
-                }, {
-                    name: 'status'
-                }, {
-                    name: 'schedstate'
-                }, {
-                    name: 'error'
-                }, {
-                    name: 'creator'
-                }, {
-                    name: 'duration'
-                }, {
-                    name: 'filesize'
-                }, {
-                    name: 'url'
-                }],
-            url: url,
-            autoLoad: true,
-            id: 'id'
-        });
-    }
-    tvheadend.dvrStoreUpcoming = datastoreBuilder('dvrlist_upcoming');
-    tvheadend.dvrStoreFinished = datastoreBuilder('dvrlist_finished');
-    tvheadend.dvrStoreFailed = datastoreBuilder('dvrlist_failed');
-    tvheadend.dvrStores = [tvheadend.dvrStoreUpcoming,
-        tvheadend.dvrStoreFinished,
-        tvheadend.dvrStoreFailed];
-
-
-    function reloadStores() {
-        for (var i = 0; i < tvheadend.dvrStores.length; i++) {
-            tvheadend.dvrStores[i].reload();
-        }
-    }
-
-    tvheadend.comet.on('dvrdb', function(m) {
-        reloadStores();
-    });
-
-    tvheadend.autorecRecord = Ext.data.Record.create(['enabled', 'title',
-        'serieslink', 'channel', 'tag', 'creator', 'contenttype', 'comment',
-        'minduration', 'maxduration',
-        'weekdays', 'pri', 'approx_time', 'config_name']);
-
-    tvheadend.autorecStore = new Ext.data.JsonStore({
-        root: 'entries',
-        fields: tvheadend.autorecRecord,
-        url: "tablemgr",
-        autoLoad: true,
-        id: 'id',
-        baseParams: {
-            table: "autorec",
-            op: "get"
-        }
-    });
-
-    tvheadend.comet.on('autorec', function(m) {
-        if (m.reload != null)
-            tvheadend.autorecStore.reload();
+tvheadend.dvr_failed = function(panel, index) {
+
+    var actions = tvheadend.dvrRowActions();
+
+    tvheadend.idnode_grid(panel, {
+        url: 'api/dvr/entry',
+        gridURL: 'api/dvr/entry/grid_failed',
+        readonly: true,
+        comet: 'dvrentry',
+        titleS: 'Failed Recording',
+        titleP: 'Failed Recordings',
+        iconCls: 'exclamation',
+        tabIndex: index,
+        del: true,
+        list: 'disp_title,episode,start_real,stop_real,' +
+              'duration,channelname,creator,' +
+              'status,sched_status',
+        sort: {
+          field: 'start',
+          direction: 'DESC'
+        },
+        plugins: [actions],
+        lcol: [actions],
+        help: function() {
+            new tvheadend.help('DVR', 'config_dvr.html');
+        },
     });
 
-    var panel = new Ext.TabPanel({
-        activeTab: 0,
-        autoScroll: true,
-        title: 'Digital Video Recorder',
-        iconCls: 'drive',
-        items: [
-            new tvheadend.dvrschedule('Upcoming recordings', 'clock', tvheadend.dvrStoreUpcoming),
-            new tvheadend.dvrschedule('Finished recordings', 'television', tvheadend.dvrStoreFinished),
-            new tvheadend.dvrschedule('Failed recordings', 'exclamation', tvheadend.dvrStoreFailed),
-            new tvheadend.autoreceditor
-        ]
-    });
     return panel;
 };
 
 /**
- * Configuration panel (located under configuration)
+ *
  */
-tvheadend.dvrsettings = function() {
-
-    var confreader = new Ext.data.JsonReader({
-        root: 'dvrSettings'
-    }, ['storage', 'filePermissions', 'dirPermissions', 'postproc', 'retention', 'dayDirs', 'channelDirs',
-        'channelInTitle', 'container', 'cache', 'charset', 'dateInTitle', 'timeInTitle',
-        'preExtraTime', 'postExtraTime', 'whitespaceInTitle', 'titleDirs',
-        'episodeInTitle', 'cleanTitle', 'tagFiles', 'commSkip', 'subtitleInTitle',
-        'episodeBeforeDate', 'rewritePAT', 'rewritePMT', 'episodeDuplicateDetection']);
-
-    var confcombo = new Ext.form.ComboBox({
-        store: tvheadend.configNames,
-        triggerAction: 'all',
-        mode: 'local',
-        displayField: 'name',
-        name: 'config_name',
-        emptyText: '(default)',
-        value: '',
-        editable: true
-    });
-
-    var delButton = new Ext.Toolbar.Button({
-        tooltip: 'Delete named configuration',
-        iconCls: 'remove',
-        text: "Delete configuration",
-        handler: deleteConfiguration,
-        disabled: true
-    });
-
-    /* Config panel variables */
-
-    /* DVR Behaviour */
-
-    var recordingContainer = new Ext.form.ComboBox({
-        store: tvheadend.containers,
-        fieldLabel: 'Media container',
-        triggerAction: 'all',
-        displayField: 'description',
-        valueField: 'name',
-        editable: false,
-        width: 350,
-        hiddenName: 'container'
-    });
-
-    var cacheScheme = new Ext.form.ComboBox({
-        store: tvheadend.caches,
-        fieldLabel: 'Cache scheme',
-        triggerAction: 'all',
-        displayField: 'description',
-        valueField: 'index',
-        editable: false,
-        width: 350,
-        hiddenName: 'cache'
-    });
-
-    var logRetention = new Ext.form.NumberField({
-        allowNegative: false,
-        allowDecimals: false,
-        minValue: 1,
-        fieldLabel: 'DVR Log retention time (days)',
-        name: 'retention'
-    });
-
-    var timeBefore = new Ext.form.NumberField({
-        allowDecimals: false,
-        fieldLabel: 'Extra time before recordings (minutes)',
-        name: 'preExtraTime'
-    });
-
-    var timeAfter = new Ext.form.NumberField({
-        allowDecimals: false,
-        fieldLabel: 'Extra time after recordings (minutes)',
-        name: 'postExtraTime'
-    });
-
-    var postProcessing = new Ext.form.TextField({
-        width: 350,
-        fieldLabel: 'Post-processor command',
-        name: 'postproc'
-    });
-
-    /* Recording File Options */
-
-    var recordingPath = new Ext.form.TextField({
-        width: 350,
-        fieldLabel: 'Recording system path',
-        name: 'storage'
-    });
-
-    /* NB: recordingPermissions is defined as a TextField for validation purposes (leading zeros), but is ultimately a number */
-
-    var recordingPermissions = new Ext.form.TextField({
-        regex: /^[0][0-7]{3}$/,
-        maskRe: /[0-7]/,
-        width: 125,
-        allowBlank: false,
-        blankText: 'You must provide a value - use octal chmod notation, e.g. 0664',
-        fieldLabel: 'File permissions (octal, e.g. 0664)',
-        name: 'filePermissions'
-    });
-
-    var charset = new Ext.form.ComboBox({
-        store: tvheadend.charsets,
-        fieldLabel: 'Filename charset',
-        triggerAction: 'all',
-        displayField: 'val',
-        valueField: 'key',
-        editable: false,
-        width: 200,
-        hiddenName: 'charset'
-    });
-
-    /* TO DO - Add 'override user umask?' option, then trigger fchmod in mkmux.c, muxer_pass.c after file created */
-
-    var PATrewrite = new Ext.form.Checkbox({
-        fieldLabel: 'Rewrite PAT in passthrough mode',
-        name: 'rewritePAT'
-    });
-
-    var PMTrewrite = new Ext.form.Checkbox({
-        fieldLabel: 'Rewrite PMT in passthrough mode',
-        name: 'rewritePMT'
-    });
-
-    var tagMetadata = new Ext.form.Checkbox({
-        fieldLabel: 'Tag files with metadata',
-        name: 'tagFiles'
-    });
-
-    var skipCommercials = new Ext.form.Checkbox({
-        fieldLabel: 'Skip commercials',
-        name: 'commSkip'
-    });
-
-    var episodeDuplicateDetection = new Ext.form.Checkbox({
-        fieldLabel: 'Episode Duplicate Detect',
-        name: 'episodeDuplicateDetection'
-    });
-
-    /* Subdirectories and filename handling */
-
-    /* NB: directoryPermissions is defined as a TextField for validation purposes (leading zeros), but is ultimately a number */
-
-    var directoryPermissions = new Ext.form.TextField({
-        regex: /^[0][0-7]{3}$/,
-        maskRe: /[0-7]/,
-        width: 125,
-        allowBlank: false,
-        blankText: 'You must provide a value - use octal chmod notation, e.g. 0775',
-        fieldLabel: 'Directory permissions (octal, e.g. 0775)',
-        name: 'dirPermissions'
-    });
-
-    /* TO DO - Add 'override user umask?' option, then trigger fchmod in utils.c after directory created */
-
-    var dirsPerDay = new Ext.form.Checkbox({
-        fieldLabel: 'Make subdirectories per day',
-        name: 'dayDirs'
-    });
-
-    var dirsPerChannel = new Ext.form.Checkbox({
-        fieldLabel: 'Make subdirectories per channel',
-        name: 'channelDirs'
-    });
-
-    var dirsPerTitle = new Ext.form.Checkbox({
-        fieldLabel: 'Make subdirectories per title',
-        name: 'titleDirs'
-    });
-
-    var incChannelInTitle = new Ext.form.Checkbox({
-        fieldLabel: 'Include channel name in filename',
-        name: 'channelInTitle'
-    });
-
-    var incDateInTitle = new Ext.form.Checkbox({
-        fieldLabel: 'Include date in filename',
-        name: 'dateInTitle'
-    });
-
-    var incTimeInTitle = new Ext.form.Checkbox({
-        fieldLabel: 'Include time in filename',
-        name: 'timeInTitle'
-    });
-
-    var incEpisodeInTitle = new Ext.form.Checkbox({
-        fieldLabel: 'Include episode in filename',
-        name: 'episodeInTitle'
-    });
-
-    var incSubtitleInTitle = new Ext.form.Checkbox({
-        fieldLabel: 'Include subtitle in filename',
-        name: 'subtitleInTitle'
-    });
-
-    var episodeFirst = new Ext.form.Checkbox({
-        fieldLabel: 'Put episode in filename before date and time',
-        name: 'episodeBeforeDate'
-    });
-
-    var stripUnsafeChars = new Ext.form.Checkbox({
-        fieldLabel: 'Remove all unsafe characters from filename',
-        name: 'cleanTitle'
-    });
-
-    var stripWhitespace = new Ext.form.Checkbox({
-        fieldLabel: 'Replace whitespace in title with \'-\'',
-        name: 'whitespaceInTitle'
-    });
-
-    /* Sub-Panel - DVR behaviour */
-
-    var DVRBehaviour = new Ext.form.FieldSet({
-        title: 'DVR Behaviour',
-        width: 700,
-        autoHeight: true,
-        collapsible: true,
-        animCollapse: true,
-        items: [recordingContainer, cacheScheme, logRetention, timeBefore, timeAfter, postProcessing]
-    });
-
-    /* Sub-Panel - File Output */
-
-    var FileOutputPanel = new Ext.form.FieldSet({
-        title: 'Recording File Options',
-        width: 700,
-        autoHeight: true,
-        collapsible: true,
-        animCollapse: true,
-        items: [recordingPath, recordingPermissions, charset, PATrewrite, PMTrewrite, tagMetadata, skipCommercials, episodeDuplicateDetection]
-    });
-
-    /* Sub-Panel - Directory operations */
-
-    var DirHandlingPanel = new Ext.form.FieldSet({
-        title: 'Subdirectory Options',
-        width: 700,
-        autoHeight: true,
-        collapsible: true,
-        animCollapse: true,
-        items: [directoryPermissions, dirsPerDay, dirsPerChannel, dirsPerTitle]
+tvheadend.dvr_settings = function(panel, index) {
+    tvheadend.idnode_form_grid(panel, {
+        url: 'api/dvr/config',
+        clazz: 'dvrconfig',
+        comet: 'dvrconfig',
+        titleS: 'Digital Video Recorder Profile',
+        titleP: 'Digital Video Recorder Profiles',
+        titleC: 'Profile Name',
+        iconCls: 'drive',
+        tabIndex: index,
+        add: {
+            url: 'api/dvr/config',
+            create: { }
+        },
+        del: true,
+        sort: {
+          field: 'name',
+          direction: 'ASC'
+        },
+        help: function() {
+            new tvheadend.help('DVR', 'config_dvr.html');
+        },
     });
 
-    /* Sub-Panel - File operations - Break into two 4-item panels */
+    return panel;
 
-    var FileHandlingPanelA = new Ext.form.FieldSet({
-        width: 350,
-        border: false,
-        autoHeight: true,
-        items : [incChannelInTitle, incDateInTitle, incTimeInTitle, incEpisodeInTitle]
-    });
+}
 
-    var FileHandlingPanelB = new Ext.form.FieldSet({
-        width: 350,
-        border: false,
-        autoHeight: true,
-        items : [incSubtitleInTitle, episodeFirst, stripUnsafeChars, stripWhitespace]
+/**
+ *
+ */
+tvheadend.autorec_editor = function(panel, index) {
+
+    tvheadend.idnode_grid(panel, {
+        url: 'api/dvr/autorec',
+        comet: 'dvrautorec',
+        titleS: 'DVR AutoRec Entry',
+        titleP: 'DVR AutoRec Entries',
+        iconCls: 'wand',
+        tabIndex: index,
+        add: {
+            url: 'api/dvr/autorec',
+            params: {
+               list: 'enable,title,channel,tag,content_type,minduration,' +
+                     'weekdays,approx_time,pri,config_name,comment',
+            },
+            create: { }
+        },
+        del: true,
+        list: 'enable,title,channel,tag,content_type,minduration,' +
+              'weekdays,approx_time,pri,config_name,creator,comment',
+        sort: {
+          field: 'name',
+          direction: 'ASC'
+        },
+        help: function() {
+            new tvheadend.help('DVR', 'config_dvr.html');
+        },
     });
 
-    var FileHandlingPanel = new Ext.form.FieldSet({
-        title: 'Filename Options',
-        width: 700,
-        autoHeight: true,
-        collapsible: true,
-        animCollapse : true,
-        items : [{
-            layout: 'column',
-            border: false,
-            items : [FileHandlingPanelA, FileHandlingPanelB] 
-        }]
-    });
+    return panel;
 
-    /* Main (form) panel */
+};
 
-    var confpanel = new Ext.FormPanel({
+/**
+ *
+ */
+tvheadend.dvr = function() {
+    var panel = new Ext.TabPanel({
+        activeTab: 0,
+        autoScroll: true,
         title: 'Digital Video Recorder',
         iconCls: 'drive',
-        border: false,
-        bodyStyle: 'padding:15px',
-        anchor: '100% 50%',
-        labelAlign: 'right',
-        labelWidth: 300,
-        autoScroll: true,
-        waitMsgTarget: true,
-        reader: confreader,
-        defaultType: 'textfield',
-        layout: 'form',
-        items: [DVRBehaviour, FileOutputPanel, DirHandlingPanel, FileHandlingPanel],
-        tbar: [confcombo, {
-                tooltip: 'Save changes made to dvr configuration below',
-                iconCls: 'save',
-                text: "Save configuration",
-                handler: saveChanges
-            }, delButton, '->', {
-                text: 'Help',
-                handler: function() {
-                    new tvheadend.help('DVR configuration', 'config_dvr.html');
-                }
-            }]
-    });
-
-    function loadConfig() {
-        confpanel.getForm().load({
-            url: 'dvr',
-            params: {
-                'op': 'loadSettings',
-                'config_name': confcombo.getValue()
-            },
-            success: function(form, action) {
-                confpanel.enable();
-            }
-        });
-    }
-
-    confcombo.on('select', function() {
-        if (confcombo.getValue() === '')
-            delButton.disable();
-        else
-            delButton.enable();
-        loadConfig();
-    });
-
-    confpanel.on('render', function() {
-        loadConfig();
+        items: [],
     });
-
-    function saveChanges() {
-        var config_name = confcombo.getValue();
-        confpanel.getForm().submit({
-            url: 'dvr',
-            params: {
-                'op': 'saveSettings',
-                'config_name': config_name
-            },
-            waitMsg: 'Saving Data...',
-            success: function(form, action) {
-                confcombo.setValue(config_name);
-                confcombo.fireEvent('select');
-            },
-            failure: function(form, action) {
-                Ext.Msg.alert('Save failed', action.result.errormsg);
-            }
-        });
-    }
-
-    function deleteConfiguration() {
-        if (confcombo.getValue() !== "") {
-            Ext.MessageBox.confirm('Message',
-                    'Do you really want to delete DVR configuration \''
-                    + confcombo.getValue() + '\'?', deleteAction);
-        }
-    }
-
-    function deleteAction(btn) {
-        if (btn === 'yes') {
-            confpanel.getForm().submit({
-                url: 'dvr',
-                params: {
-                    'op': 'deleteSettings',
-                    'config_name': confcombo.getValue()
-                },
-                waitMsg: 'Deleting Data...',
-                success: function(form, action) {
-                    confcombo.setValue('');
-                    confcombo.fireEvent('select');
-                },
-                failure: function(form, action) {
-                    Ext.Msg.alert('Delete failed', action.result.errormsg);
-                }
-            });
-        }
-    }
-
-    return confpanel;
-};
+    tvheadend.dvr_upcoming(panel, 0);
+    tvheadend.dvr_finished(panel, 1);
+    tvheadend.dvr_failed(panel, 2);
+    tvheadend.autorec_editor(panel, 3);
+    return panel;
+}
index add7723faaa3fa6362b08a119a6b35e03b4c26d7..3446877fbe8eac4b26d9bae080338b9b2bd3f1d4 100644 (file)
@@ -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 += '<div class="x-epg-desc">' + event.description + '</div>';
     content += '<div class="x-epg-meta">' + event.starrating + '</div>';
     content += '<div class="x-epg-meta">' + event.agerating + '</div>';
-    content += '<div class="x-epg-meta">' + tvheadend.contentGroupLookupName(event.contenttype) + '</div>';
+    content += '<div class="x-epg-meta">' + tvheadend.contentGroupLookupName(event.content_type) + '</div>';
 
     if (event.ext_desc != null)
         content += '<div class="x-epg-meta">' + event.ext_desc + '</div>';
@@ -127,12 +118,29 @@ tvheadend.epgDetails = function(event) {
                    '?title=' + encodeURIComponent(title) + '">Play</a></div>';
     }
 
+    var store = new Ext.data.JsonStore({
+        autoload: true,
+        root: 'entries',
+        fields: ['key','val'],
+        id: 'key',
+        url: 'api/idnode/load',
+        baseParams: {
+            enum: 1,
+            'class': 'dvrconfig'
+        },
+        sortInfo: {
+            field: 'val',
+            direction: 'ASC'
+        }
+    });
+    store.load();
+
     var confcombo = new Ext.form.ComboBox({
-        store: tvheadend.configNames,
+        store: store,
         triggerAction: 'all',
         mode: 'local',
-        valueField: 'identifier',
-        displayField: 'name',
+        valueField: 'key',
+        displayField: 'val',
         name: 'config_name',
         emptyText: '(default)',
         value: '',
@@ -158,20 +166,19 @@ tvheadend.epgDetails = function(event) {
     win.show();
 
     function recordEvent() {
-        record('recordEvent');
+        record('event')
     }
 
     function recordSeries() {
-        record('recordSeries');
+        record('series');
     }
 
     function record(op) {
         Ext.Ajax.request({
-            url: 'dvr',
+            url: 'api/dvr/entry/create_by_' + op,
             params: {
-                eventId: event.id,
-                op: op,
-                config_name: confcombo.getValue()
+                event_id: event.id,
+                config_uuid: confcombo.getValue()
             },
             success: function(response, options) {
                 win.close();
@@ -232,7 +239,7 @@ tvheadend.epg = function() {
             }, {
                 name: 'agerating'
             }, {
-                name: 'contenttype'
+                name: 'content_type'
             }, {
                 name: 'schedstate'
             }, {
@@ -364,9 +371,9 @@ tvheadend.epg = function() {
             renderer: renderInt
         }, {
             width: 250,
-            id: 'contenttype',
+            id: 'content_type',
             header: "Content Type",
-            dataIndex: 'contenttype',
+            dataIndex: 'content_type',
             renderer: function(v) {
                 return tvheadend.contentGroupLookupName(v);
             }
@@ -490,12 +497,12 @@ tvheadend.epg = function() {
     };
 
     clearContentGroupFilter = function() {
-        delete epgStore.baseParams.contenttype;
+        delete epgStore.baseParams.content_type;
         epgFilterContentGroup.setValue("");
     };
 
     clearDurationFilter = function() {
-           delete epgStore.baseParams.minduration;
+        delete epgStore.baseParams.minduration;
         delete epgStore.baseParams.maxduration;
         epgFilterDuration.setValue("");
     };
@@ -532,8 +539,8 @@ tvheadend.epg = function() {
     epgFilterContentGroup.on('select', function(c, r) {
         if (r.data.code == -1)
             clearContentGroupFilter();
-        else if (epgStore.baseParams.contenttype !== r.data.code)
-            epgStore.baseParams.contenttype = r.data.code;
+        else if (epgStore.baseParams.content_type !== r.data.code)
+            epgStore.baseParams.content_type = r.data.code;
         epgStore.reload();
     });
 
@@ -650,19 +657,18 @@ tvheadend.epg = function() {
                 : "<i>Don't care</i>";
         var tag = epgStore.baseParams.tag ? tvheadend.tagLookupName(epgStore.baseParams.tag)
                 : "<i>Don't care</i>";
-        var contenttype = epgStore.baseParams.contenttype ? tvheadend.contentGroupLookupName(epgStore.baseParams.contenttype)
+        var content_type = epgStore.baseParams.content_type ? tvheadend.contentGroupLookupName(epgStore.baseParams.content_type)
                 : "<i>Don't care</i>";
         var duration = epgStore.baseParams.minduration ? tvheadend.durationLookupRange(epgStore.baseParams.minduration)
                 : "<i>Don't care</i>";
 
-
         Ext.MessageBox.confirm('Auto Recorder', 'This will create an automatic rule that '
                 + 'continuously scans the EPG for programmes '
                 + 'to record that match this query: ' + '<br><br>'
                 + '<div class="x-smallhdr">Title:</div>' + title + '<br>'
                 + '<div class="x-smallhdr">Channel:</div>' + channel + '<br>'
                 + '<div class="x-smallhdr">Tag:</div>' + tag + '<br>'
-                + '<div class="x-smallhdr">Genre:</div>' + contenttype + '<br>'
+                + '<div class="x-smallhdr">Genre:</div>' + content_type + '<br>'
                 + '<div class="x-smallhdr">Duration:</div>' + duration + '<br>'
                 + '<br><br>' + 'Currently this will match (and record) '
                 + epgStore.getTotalCount() + ' events. ' + 'Are you sure?',
@@ -675,9 +681,18 @@ tvheadend.epg = function() {
 
     function createAutoRec2(params) {
         /* Really do it */
-        params.op = 'createAutoRec';
+        params.conf = {
+          enabled: 1,
+          comment: 'Created from EPG query',
+        };
+        if (params.title) params['title'] = params.title;
+        if (params.channel) params['channel'] = params.channel;
+        if (params.tag) params['tag'] = params.tag;
+        if (params.content_type) params['content_type'] = params.content_type;
+        if (params.minduration) params['minduration'] = params.minduration;
+        if (params.maxduration) params['maxduration'] = params.maxduration;
         Ext.Ajax.request({
-            url: 'dvr',
+            url: 'api/dvr/autorec/create',
             params: params
         });
     }
index abd485edb35a9fe1da0d78a1c07e734abda8a07e..a8df6cb8fa5f4a51728ecc116ef2c8577074490d 100644 (file)
@@ -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.
+ *
+ * <p>
+ * License details: <a href="http://www.gnu.org/licenses/lgpl.html"
+ * target="_blank">http://www.gnu.org/licenses/lgpl.html</a>
+ * </p>
+ */
+
+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;
+  }
+});
+
index 0827f5c67ce98704f9fe7a35b01b0ede1a1d692e..497de939ce052454cff1f9d69be37027f5293835 100644 (file)
@@ -16,7 +16,7 @@ tvheadend.idnode_get_enum = function(conf)
     if (key in tvheadend.idnode_enum_stores)
         return tvheadend.idnode_enum_stores[key];
 
-    /* Build combobox */
+    /* Build store */
     var st = new Ext.data.JsonStore({
         root: conf.root || 'entries',
         url: conf.url,
@@ -24,7 +24,8 @@ tvheadend.idnode_get_enum = function(conf)
         fields: conf.fields || ['key', 'val'],
         id: conf.id || 'key',
         autoLoad: true,
-        sortInfo: {
+        listeners: conf.listeners || {},
+        sortInfo: conf.sort || {
             field: 'val',
             direction: 'ASC'
         }
@@ -85,7 +86,9 @@ tvheadend.idnode_enum_store = function(f)
         case 'int':
         case 'u32':
         case 'u16':
+        case 's64':
         case 'dbl':
+        case 'time':
             var data = null;
             if (f.enum.length > 0 && f.enum[0] instanceof Object) {
                 data = f.enum;
@@ -118,6 +121,8 @@ tvheadend.IdNodeField = function(conf)
     this.wronce = conf.wronce;
     this.hidden = conf.hidden || conf.advanced;
     this.password = conf.password;
+    this.duration = conf.duration;
+    this.group = conf.group;
     this.enum = conf.enum;
     this.store = null;
     if (this.enum)
@@ -133,9 +138,16 @@ tvheadend.IdNodeField = function(conf)
         var w = 300;
         var ftype = 'string';
         if (this.type === 'int' || this.type === 'u32' ||
-                this.type === 'u16' || this.type === 'dbl') {
+            this.type === 'u16' || this.type === 's64' ||
+            this.type === 'dbl') {
             ftype = 'numeric';
             w = 80;
+        } else if (this.type === 'time') {
+            w = 120;
+            if (this.durations) {
+              ftype = 'numeric';
+              w = 80;
+            }
         } else if (this.type === 'bool') {
             ftype = 'boolean';
             w = 60;
@@ -173,7 +185,28 @@ tvheadend.IdNodeField = function(conf)
             return function(v) {
                 return '<span class="tvh-grid-unset">********</span>';
             }
-    
+            
+        if (this.type === 'time') {
+            if (this.duration)
+                return function(v) {
+                    v = parseInt(v / 60); /* Nevermind the seconds */
+                    if (v === 0 && v !== '0')
+                       return "Not set";
+                    var hours = parseInt(v / 60);
+                    var min = parseInt(v % 60);
+                    if (hours) {
+                        if (min === 0)
+                            return hours + ' hrs';
+                        return hours + ' hrs, ' + min + ' min';
+                    }
+                    return min + ' min';
+                }
+            return function(v) {
+                var dt = new Date(v * 1000);
+                return dt.format('D j M H:i');
+            }
+        }
+
         if (!this.store)
             return null;
 
@@ -181,11 +214,7 @@ tvheadend.IdNodeField = function(conf)
         return function(v) {
             if (st && st instanceof Ext.data.JsonStore) {
                 var t = [];
-                var d;
-                if (v.push)
-                    d = v;
-                else
-                    d = [v];
+                var d = v.push ? v : [v];
                 for (var i = 0; i < d.length; i++) {
                     var r = st.find('key', d[i]);
                     if (r !== -1) {
@@ -220,7 +249,7 @@ tvheadend.IdNodeField = function(conf)
             disabled: d,
             width: 300
         };
-
+        
         /* ComboBox */
         if (this.enum) {
             cons = Ext.form.ComboBox;
@@ -234,11 +263,20 @@ tvheadend.IdNodeField = function(conf)
             c['store'] = this.store;
             c['typeAhead'] = true;
             c['forceSelection'] = false;
-            c['triggerAction'] = 'all',
-                    c['emptyText'] = 'Select ' + this.text + ' ...';
+            c['triggerAction'] = 'all';
+            c['emptyText'] = 'Select ' + this.text + ' ...';
 
             /* Single */
         } else {
+
+            if (this.type == 'perm') {
+                c['regex'] = /^[0][0-7]{3}$/;
+                c['maskRe'] = /[0-7]/;
+                c['allowBlank'] = false;
+                c['blankText'] = 'You must provide a value - use octal chmod notation, e.g. 0664';
+                c['width'] = 125;
+            }
+
             switch (this.type) {
                 case 'bool':
                     cons = Ext.form.Checkbox;
@@ -247,10 +285,13 @@ tvheadend.IdNodeField = function(conf)
                 case 'int':
                 case 'u32':
                 case 'u16':
+                case 's32':
                 case 'dbl':
+                case 'time':
                     cons = Ext.form.NumberField;
                     break;
 
+                /* 'str' and 'perm' */
                 default:
                     cons = Ext.form.TextField;
                     break;
@@ -273,6 +314,7 @@ tvheadend.IdNode = function(conf)
     this.text = conf.caption || this.clazz;
     this.props = conf.props;
     this.order = [];
+    this.groups = conf.groups;
     this.fields = [];
     for (var i = 0; i < this.props.length; i++) {
         this.fields.push(new tvheadend.IdNodeField(this.props[i]));
@@ -387,16 +429,6 @@ tvheadend.idnode_editor_field = function(f, create)
 
     /* Singular */
     switch (f.type) {
-        case 'str':
-            return new Ext.form.TextField({
-                fieldLabel: f.caption,
-                name: f.id,
-                value: value,
-                disabled: d,
-                width: 300
-            });
-            break;
-
         case 'bool':
             return new Ext.ux.form.XCheckbox({
                 fieldLabel: f.caption,
@@ -404,11 +436,33 @@ tvheadend.idnode_editor_field = function(f, create)
                 checked: value,
                 disabled: d
             });
-            break;
+
+        case 'time':
+            if (!f.duration)
+                return new Ext.ux.form.TwinDateTimeField({
+                    fieldLabel: f.caption,
+                    name: f.id,
+                    value: value,
+                    disabled: d,
+                    width: 300,
+                    timeFormat: 'H:i:s',
+                    timeConfig: {
+                        altFormats: 'H:i:s',
+                        allowBlank: true,
+                        increment: 10,
+                    },
+                    dateFormat:'d.n.Y',
+                    dateConfig: {
+                        altFormats: 'Y-m-d|Y-n-d',
+                        allowBlank: true,
+                    }
+                });
+            /* fall thru!!! */
 
         case 'int':
         case 'u32':
         case 'u16':
+        case 's64':
         case 'dbl':
             return new Ext.form.NumberField({
                 fieldLabel: f.caption,
@@ -417,62 +471,139 @@ tvheadend.idnode_editor_field = function(f, create)
                 disabled: d,
                 width: 300
             });
-            break;
+
+        case 'perm':
+            return new Ext.form.TextField({
+                fieldLabel: f.caption,
+                name: f.id,
+                value: value,
+                disabled: d,
+                width: 125,
+                regex: /^[0][0-7]{3}$/,
+                maskRe: /[0-7]/,
+                allowBlank: false,
+                blankText: 'You must provide a value - use octal chmod notation, e.g. 0664'
+            });
+
+
+        default:
+            return new Ext.form.TextField({
+                fieldLabel: f.caption,
+                name: f.id,
+                value: value,
+                disabled: d,
+                width: 300
+            });
+
     }
-    return null;
 };
 
 /*
  * ID node editor form fields
  */
-tvheadend.idnode_editor_form = function(d, panel)
+tvheadend.idnode_editor_form = function(d, meta, panel, create)
 {
     var af = [];
     var rf = [];
     var df = [];
+    var groups = null;
 
     /* Fields */
     for (var i = 0; i < d.length; i++) {
-        var f = tvheadend.idnode_editor_field(d[i]);
+        var p = d[i];
+        var f = tvheadend.idnode_editor_field(p, create);
         if (!f)
             continue;
-        if (d[i].rdonly)
-            rf.push(f);
-        else if (d[i].advanced)
-            af.push(f);
-        else
-            df.push(f);
-    }
-    if (df.length) {
-        panel.add(new Ext.form.FieldSet({
-            title: 'Basic Settings',
-            autoHeight: true,
-            autoWidth: true,
-            collapsible: true,
-            collapsed: false,
-            items: df
-        }));
+        if (p.group && meta.groups) {
+            if (!groups)
+                groups = {};
+            if (!(p.group in groups))
+                groups[p.group] = [f];
+            else
+                groups[p.group].push(f);
+        } else {
+            if (p.rdonly)
+                rf.push(f);
+            else if (p.advanced)
+                af.push(f);
+            else
+                df.push(f);
+        }
     }
-    if (af.length) {
-        panel.add(new Ext.form.FieldSet({
-            title: 'Advanced Settings',
-            autoHeight: true,
-            autoWidth: true,
-            collapsible: true,
-            collapsed: false, //true,
-            items: af
-        }));
+
+    function newFieldSet(conf) {
+        return new Ext.form.FieldSet({
+                       title: conf.title || '',
+                       layout: conf.layout || 'form',
+                       border: conf.border || true,
+                       style: conf.style || 'padding: 0 5px 5px 10px',
+                       bodyStyle: conf.bodyStyle || 'padding-top: ' + (Ext.isIE ? '0' : '10px'),
+                       autoHeight: true,
+                       autoWidth: true,
+                       collapsible: conf.nocollapse ? false : true,
+                       collapsed: false,
+                       items: conf.items
+                   });
     }
-    if (rf.length) {
-        panel.add(new Ext.form.FieldSet({
-            title: 'Read-only Info',
-            autoHeight: true,
-            autoWidth: true,
-            collapsible: true,
-            collapsed: false, //true,
-            items: rf
-        }));
+
+    if (groups) {
+        var met = {};
+        for (var i = 0; i < meta.groups.length; i++)
+            met[meta.groups[i].number] = meta.groups[i];
+        var fs = {};
+        var cfs = {};
+        var mfs = {};
+        var rest = [];
+        for (var number in groups) {
+            if (!(number in met))
+                met[number] = null;
+            var m = met[number];
+            var columns = 0;
+            for (var k in met)
+                if (met[k].parent == m.number)
+                    if (columns < met[k].column)
+                        columns = met[k].column;
+            met[number].columns = columns;
+            if (columns) {
+                var p = newFieldSet({ title: m.name || "Settings", layout: 'column', border: false });
+                cfs[number] = newFieldSet({ nocollapse: true });
+                p.add(cfs[number]);
+                fs[number] = p;
+                mfs[number] = p;
+            }
+        }
+        for (var number in groups) {
+            var m = met[number];
+            if (number in fs) continue;
+            var parent = m.parent;
+            var p = null;
+            if (parent && !met[parent].columns)
+                parent = null;
+            if (!m.columns) {
+                if (parent) {
+                    p = newFieldSet({ nocollapse: true });
+                    fs[parent].add(p);
+                } else {
+                    p = newFieldSet({ title: m.name });
+                    mfs[number] = p;
+                }
+                cfs[number] = p;                    
+            }
+        }
+        for (var number in groups) {
+            var g = groups[number];
+            for (var i = 0; i < g.length; i++)
+                cfs[number].add(g[i]);
+            if (number in mfs)
+                panel.add(mfs[number]);
+        }
     }
+    if (df.length)
+        panel.add(newFieldSet({ title: "Basic Settings", items: df }));
+    if (af.length)
+        panel.add(newFieldSet({ title: "Advanced Settings", items: af }));
+    if (rf.length)
+        panel.add(newFieldSet({ title: "Read-only Info", items: rf }));
     panel.doLayout();
 };
 
@@ -485,50 +616,53 @@ tvheadend.idnode_editor = function(item, conf)
     var buttons = [];
 
     /* Buttons */
-    var saveBtn = new Ext.Button({
-        text: 'Save',
-        handler: function() {
-            var node = panel.getForm().getFieldValues();
-            node.uuid = item.uuid;
-            Ext.Ajax.request({
-                url: 'api/idnode/save',
-                params: {
-                    node: Ext.encode(node)
-                },
-                success: function(d) {
-                    if (conf.win)
-                        conf.win.hide();
-                }
+    if (!conf.noButtons) {
+        var saveBtn = new Ext.Button({
+            text: 'Save',
+            handler: function() {
+                var node = panel.getForm().getFieldValues();
+                node.uuid = item.uuid;
+                tvheadend.Ajax({
+                    url: 'api/idnode/save',
+                    params: {
+                        node: Ext.encode(node)
+                    },
+                    success: function(d) {
+                        if (conf.win)
+                            conf.win.hide();
+                    }
+                });
+            }
+        });
+        buttons.push(saveBtn);
+
+        if (conf.help) {
+            var helpBtn = new Ext.Button({
+                text: 'Help',
+                handler: conf.help
             });
+            buttons.push(helpBtn);
         }
-    });
-    buttons.push(saveBtn);
-
-    if (conf.help) {
-        var helpBtn = new Ext.Button({
-            text: 'Help',
-            handler: conf.help
-        });
-        buttons.push(helpBtn);
     }
 
-    panel = new Ext.FormPanel({
+    panel = new Ext.form.FormPanel({
         title: conf.title || null,
         frame: true,
-        border: true,
+        border: conf.inTabPanel ? false : true,
         bodyStyle: 'padding: 5px',
         labelAlign: 'left',
-        labelWidth: 200,
+        labelWidth: conf.labelWidth || 200,
         autoWidth: true,
         autoHeight: !conf.fixedHeight,
         width: 600,
         //defaults: {width: 330},
         defaultType: 'textfield',
         buttonAlign: 'left',
-        buttons: buttons
+        autoScroll: true,
+        buttons: buttons,
     });
 
-    tvheadend.idnode_editor_form(item.props || item.params, panel);
+    tvheadend.idnode_editor_form(item.props || item.params, item.meta, panel, false);
 
     return panel;
 };
@@ -537,7 +671,7 @@ tvheadend.idnode_editor = function(item, conf)
 /*
  * IDnode creation dialog
  */
-tvheadend.idnode_create = function(conf)
+tvheadend.idnode_create = function(conf, onlyDefault)
 {
     var puuid = null;
     var panel = null;
@@ -549,13 +683,13 @@ tvheadend.idnode_create = function(conf)
         text: 'Create',
         hidden: true,
         handler: function() {
-            params = conf.create.params || {};
+            var params = conf.create.params || {};
             if (puuid)
                 params['uuid'] = puuid;
             if (pclass)
                 params['class'] = pclass;
             params['conf'] = Ext.encode(panel.getForm().getFieldValues());
-            Ext.Ajax.request({
+            tvheadend.Ajax({
                 url: conf.create.url || conf.url + '/create',
                 params: params,
                 success: function(d) {
@@ -619,7 +753,7 @@ tvheadend.idnode_create = function(conf)
                         pclass = r.get(conf.select.valueField);
                         win.setTitle('Add ' + s.lastSelectionText);
                         panel.remove(s);
-                        tvheadend.idnode_editor_form(d, panel);
+                        tvheadend.idnode_editor_form(d, null, panel, true);
                         saveBtn.setVisible(true);
                     }
                 }
@@ -628,15 +762,15 @@ tvheadend.idnode_create = function(conf)
             select = function(s, n, o) {
                 params = conf.select.clazz.params || {};
                 params['uuid'] = puuid = n.id;
-                Ext.Ajax.request({
+                tvheadend.Ajax({
                     url: conf.select.clazz.url || conf.select.url || conf.url,
+                    params: params,
                     success: function(d) {
                         panel.remove(s);
                         d = json_decode(d);
-                        tvheadend.idnode_editor_form(d.props, panel);
+                        tvheadend.idnode_editor_form(d.props, d, panel, true);
                         saveBtn.setVisible(true);
-                    },
-                    params: params
+                    }
                 });
             };
         }
@@ -660,14 +794,18 @@ tvheadend.idnode_create = function(conf)
         panel.add(combo);
         win.show();
     } else {
-        Ext.Ajax.request({
+        tvheadend.Ajax({
             url: conf.url + '/class',
             params: conf.params,
             success: function(d) {
                 d = json_decode(d);
-                tvheadend.idnode_editor_form(d.props, panel);
+                tvheadend.idnode_editor_form(d.props, d, panel, true);
                 saveBtn.setVisible(true);
-                win.show();
+                if (onlyDefault) {
+                    saveBtn.handler();
+                    panel.destroy();
+                } else
+                    win.show();
             }
         });
     }
@@ -694,6 +832,10 @@ tvheadend.idnode_grid = function(panel, conf)
         var downBtn = null;
         var editBtn = null;
 
+        /* Some copies */
+        if (conf.add && !conf.add.titleS && conf.titleS)
+            conf.add.titleS = conf.titleS;
+
         /* Model */
         var idnode = new tvheadend.IdNode(d);
         for (var i = 0; i < idnode.length(); i++) {
@@ -724,7 +866,7 @@ tvheadend.idnode_grid = function(panel, conf)
         /* Store */
         var store = new Ext.data.JsonStore({
             root: 'entries',
-            url: conf.url + '/grid',
+            url: conf.gridURL || (conf.url + '/grid'),
             autoLoad: true,
             id: 'uuid',
             totalProperty: 'total',
@@ -755,57 +897,62 @@ tvheadend.idnode_grid = function(panel, conf)
             saveBtn.setDisabled(d);
         });
         select.on('selectionchange', function(s) {
+            var count = s.getCount();
             if (delBtn)
-                delBtn.setDisabled(s.getCount() === 0);
+                delBtn.setDisabled(count === 0);
             if (upBtn) {
-                upBtn.setDisabled(s.getCount() === 0);
-                downBtn.setDisabled(s.getCount() === 0);
+                upBtn.setDisabled(count === 0);
+                downBtn.setDisabled(count === 0);
             }
-            editBtn.setDisabled(s.getCount() !== 1);
+            if (editBtn)
+                editBtn.setDisabled(count !== 1);
             if (conf.selected)
                 conf.selected(s);
         });
 
         /* Top bar */
-        saveBtn = new Ext.Toolbar.Button({
-            tooltip: 'Save pending changes (marked with red border)',
-            iconCls: 'save',
-            text: 'Save',
-            disabled: true,
-            handler: function() {
-                var mr = store.getModifiedRecords();
-                var out = new Array();
-                for (var x = 0; x < mr.length; x++) {
-                    v = mr[x].getChanges();
-                    out[x] = v;
-                    out[x].uuid = mr[x].id;
-                }
-                Ext.Ajax.request({
-                    url: 'api/idnode/save',
-                    params: {
-                        node: Ext.encode(out)
-                    },
-                    success: function(d)
-                    {
-                        if (!auto.getValue())
-                            store.reload();
+        if (!conf.readonly) {
+            saveBtn = new Ext.Toolbar.Button({
+                tooltip: 'Save pending changes (marked with red border)',
+                iconCls: 'save',
+                text: 'Save',
+                disabled: true,
+                handler: function() {
+                    var mr = store.getModifiedRecords();
+                    var out = new Array();
+                    for (var x = 0; x < mr.length; x++) {
+                        v = mr[x].getChanges();
+                        out[x] = v;
+                        out[x].uuid = mr[x].id;
                     }
-                });
-            }
-        });
-        buttons.push(saveBtn);
-        undoBtn = new Ext.Toolbar.Button({
-            tooltip: 'Revert pending changes (marked with red border)',
-            iconCls: 'undo',
-            text: 'Undo',
-            disabled: true,
-            handler: function() {
-                store.rejectChanges();
-            }
-        });
-        buttons.push(undoBtn);
-        buttons.push('-');
+                    tvheadend.Ajax({
+                        url: 'api/idnode/save',
+                        params: {
+                            node: Ext.encode(out)
+                        },
+                        success: function(d)
+                        {
+                            if (!auto.getValue())
+                                store.reload();
+                        }
+                    });
+                }
+            });
+            buttons.push(saveBtn);
+            undoBtn = new Ext.Toolbar.Button({
+                tooltip: 'Revert pending changes (marked with red border)',
+                iconCls: 'undo',
+                text: 'Undo',
+                disabled: true,
+                handler: function() {
+                    store.rejectChanges();
+                }
+            });
+            buttons.push(undoBtn);
+        }
         if (conf.add) {
+            if (buttons.length > 0)
+                buttons.push('-');
             addBtn = new Ext.Toolbar.Button({
                 tooltip: 'Add a new entry',
                 iconCls: 'add',
@@ -818,6 +965,8 @@ tvheadend.idnode_grid = function(panel, conf)
             buttons.push(addBtn);
         }
         if (conf.del) {
+            if (!conf.add && buttons.length > 0)
+                buttons.push('-');
             delBtn = new Ext.Toolbar.Button({
                 tooltip: 'Delete selected entries',
                 iconCls: 'remove',
@@ -829,7 +978,7 @@ tvheadend.idnode_grid = function(panel, conf)
                         var uuids = [];
                         for (var i = 0; i < r.length; i++)
                             uuids.push(r[i].id);
-                        Ext.Ajax.request({
+                        tvheadend.Ajax({
                             url: 'api/idnode/delete',
                             params: {
                                 uuid: Ext.encode(uuids)
@@ -857,7 +1006,7 @@ tvheadend.idnode_grid = function(panel, conf)
                         var uuids = [];
                         for (var i = 0; i < r.length; i++)
                             uuids.push(r[i].id);
-                        Ext.Ajax.request({
+                        tvheadend.Ajax({
                             url: 'api/idnode/moveup',
                             params: {
                                 uuid: Ext.encode(uuids)
@@ -882,7 +1031,7 @@ tvheadend.idnode_grid = function(panel, conf)
                         var uuids = [];
                         for (var i = 0; i < r.length; i++)
                             uuids.push(r[i].id);
-                        Ext.Ajax.request({
+                        tvheadend.Ajax({
                             url: 'api/idnode/movedown',
                             params: {
                                 uuid: Ext.encode(uuids)
@@ -897,62 +1046,67 @@ tvheadend.idnode_grid = function(panel, conf)
             });
             buttons.push(downBtn);
         }
-        if (conf.add || conf.del || conf.move)
-            buttons.push('-');
-        editBtn = new Ext.Toolbar.Button({
-            tooltip: 'Edit selected entry',
-            iconCls: 'edit',
-            text: 'Edit',
-            disabled: true,
-            handler: function() {
-                var r = select.getSelected();
-                if (r) {
-                    if (conf.edittree) {
-                        var p = tvheadend.idnode_tree({
-                            url: 'api/idnode/tree',
-                            params: {
-                                root: r.id
-                            }
-                        });
-                        p.setSize(800, 600);
-                        var w = new Ext.Window({
-                            title: 'Edit ' + conf.titleS,
-                            layout: 'fit',
-                            autoWidth: true,
-                            autoHeight: true,
-                            plain: true,
-                            items: p
-                        });
-                        w.show();
-                    } else {
-                        Ext.Ajax.request({
-                            url: 'api/idnode/load',
-                            params: {
-                                uuid: r.id
-                            },
-                            success: function(d)
-                            {
-                                d = json_decode(d);
-                                var w = null;
-                                var c = {win: w};
-                                var p = tvheadend.idnode_editor(d[0], c);
-                                w = new Ext.Window({
-                                    title: 'Edit ' + conf.titleS,
-                                    layout: 'fit',
-                                    autoWidth: true,
-                                    autoHeight: true,
-                                    plain: true,
-                                    items: p
-                                });
-                                c.win = w;
-                                w.show();
-                            }
-                        });
+        if (!conf.readonly) {
+            if (buttons.length > 0)
+                buttons.push('-');
+            editBtn = new Ext.Toolbar.Button({
+                tooltip: 'Edit selected entry',
+                iconCls: 'edit',
+                text: 'Edit',
+                disabled: true,
+                handler: function() {
+                    var r = select.getSelected();
+                    if (r) {
+                        if (conf.edittree) {
+                            var p = tvheadend.idnode_tree({
+                                url: 'api/idnode/tree',
+                                params: {
+                                    root: r.id
+                                }
+                            });
+                            p.setSize(800, 600);
+                            var w = new Ext.Window({
+                                title: 'Edit ' + conf.titleS,
+                                layout: 'fit',
+                                autoWidth: true,
+                                autoHeight: true,
+                                plain: true,
+                                items: p
+                            });
+                            w.show();
+                        } else {
+                            var params = {
+                                uuid: r.id,
+                                meta: 1
+                            };
+                            if (conf.listEdit)
+                                params['list'] = conf.listEdit;
+                            tvheadend.Ajax({
+                                url: 'api/idnode/load',
+                                params: params,
+                                success: function(d) {
+                                    d = json_decode(d);
+                                    var w = null;
+                                    var c = {win: w};
+                                    var p = tvheadend.idnode_editor(d[0], c);
+                                    w = new Ext.Window({
+                                        title: 'Edit ' + conf.titleS,
+                                        layout: 'fit',
+                                        autoWidth: true,
+                                        autoHeight: true,
+                                        plain: true,
+                                        items: p
+                                    });
+                                    c.win = w;
+                                    w.show();
+                                }
+                            });
+                        }
                     }
                 }
-            }
-        });
-        buttons.push(editBtn);
+            });
+            buttons.push(editBtn);
+        }
 
         /* Hide Mode */
         if (conf.hidemode) {
@@ -1059,11 +1213,12 @@ tvheadend.idnode_grid = function(panel, conf)
                 '->', '-', 'Per page', count]
         });
         plugins.push(filter);
-        var grid = new Ext.grid.EditorGridPanel({
+        var gconf = {
             stateful: true,
-            stateId: conf.url,
+            stateId: conf.gridURL || conf.url,
             stripeRows: true,
             title: conf.titleP,
+            iconCls: conf.iconCls || '',
             store: store,
             cm: model,
             selModel: select,
@@ -1073,7 +1228,9 @@ tvheadend.idnode_grid = function(panel, conf)
             },
             tbar: buttons,
             bbar: page
-        });
+        };
+        var grid = conf.readonly ? new Ext.grid.GridPanel(gconf) :
+                                   new Ext.grid.EditorGridPanel(gconf);
         grid.on('filterupdate', function() {
             page.changePage(0);
         });
@@ -1096,8 +1253,11 @@ tvheadend.idnode_grid = function(panel, conf)
 
     /* Request data */
     if (!conf.fields) {
-        Ext.Ajax.request({
+        var p = {};
+        if (conf.list) p['list'] = conf.list;
+        tvheadend.Ajax({
             url: conf.url + '/class',
+            params: p,
             success: function(d)
             {
                 var d = json_decode(d);
@@ -1109,6 +1269,246 @@ tvheadend.idnode_grid = function(panel, conf)
     }
 };
 
+/*
+ * IDnode form grid
+ */
+tvheadend.idnode_form_grid = function(panel, conf)
+{
+    var buttons = [];
+    var plugins = conf.plugins || [];
+    var saveBtn = null;
+    var undoBtn = null;
+    var addBtn = null;
+    var delBtn = null;
+    var current = null;
+    var grid = null;
+    var mpanel = null;
+
+    /* Store */
+    var store = new Ext.data.JsonStore({
+        root: 'entries',
+        url: 'api/idnode/load',
+        baseParams: {
+            enum: 1,
+            'class': conf.clazz
+        },
+        autoLoad: true,
+        id: 'key',
+        totalProperty: 'total',
+        fields: ['key','val'],
+        remoteSort: false,
+        pruneModifiedRecords: true,
+        sortInfo: {
+            field: 'val',
+            direction: 'ASC'
+        },
+    });
+
+    /* Model */
+    var model = new Ext.grid.ColumnModel({
+        defaultSortable: true,
+        columns: [{
+            width: 300,
+            id: 'val',
+            header: conf.titleC,
+            sortable: true,
+            dataIndex: 'val'
+        }]
+    });
+
+    /* Selection */
+    var select = new Ext.grid.RowSelectionModel({
+        singleSelect: true
+    });
+
+    /* Event handlers */
+    select.on('selectionchange', function(s) {
+        roweditor(s.getSelected());
+        if (conf.selected)
+            conf.selected(s);
+    });
+
+    /* Top bar */
+    saveBtn = new Ext.Toolbar.Button({
+        tooltip: 'Save pending changes (marked with red border)',
+        iconCls: 'save',
+        text: 'Save',
+        disabled: true,
+        handler: function() {
+            var node = current.editor.getForm().getFieldValues();
+            node.uuid = current.uuid;
+            tvheadend.Ajax({
+                url: 'api/idnode/save',
+                params: {
+                    node: Ext.encode(node)
+                },
+                success: function() {
+                    store.reload()
+                }
+            });
+        }
+    });
+    buttons.push(saveBtn);
+    undoBtn = new Ext.Toolbar.Button({
+        tooltip: 'Revert pending changes (marked with red border)',
+        iconCls: 'undo',
+        text: 'Undo',
+        disabled: true,
+        handler: function() {
+            if (current)
+                current.editor.getForm().reset();
+        }
+    });
+    buttons.push(undoBtn);
+    buttons.push('-');
+    if (conf.add) {
+        addBtn = new Ext.Toolbar.Button({
+            tooltip: 'Add a new entry',
+            iconCls: 'add',
+            text: 'Add',
+            disabled: false,
+            handler: function() {
+                tvheadend.idnode_create(conf.add, true);
+            }
+        });
+        buttons.push(addBtn);
+    }
+    if (conf.del) {
+        delBtn = new Ext.Toolbar.Button({
+            tooltip: 'Delete selected entries',
+            iconCls: 'remove',
+            text: 'Delete',
+            disabled: true,
+            handler: function() {
+                if (current) {
+                    tvheadend.Ajax({
+                        url: 'api/idnode/delete',
+                        params: {
+                            uuid: current.uuid
+                        },
+                        success: function(d)
+                        {
+                            store.reload();
+                            grid.getSelectionModel().selectFirstRow();
+                        }
+                    });
+                }
+            }
+        });
+        buttons.push(delBtn);
+    }
+    if (conf.add || conf.del)
+        buttons.push('-');
+
+    /* Extra buttons */
+    if (conf.tbar) {
+        buttons.push('-');
+        for (i = 0; i < conf.tbar.length; i++) {
+            if (conf.tbar[i].callback) {
+                conf.tbar[i].handler = function(b, e) {
+                    this.callback(this, e, store, select);
+                };
+            }
+            buttons.push(conf.tbar[i]);
+        }
+    }
+
+    /* Help */
+    if (conf.help) {
+        buttons.push('->');
+        buttons.push({
+            text: 'Help',
+            handler: conf.help
+        });
+    }
+
+    function roweditor(r) {
+        if (!r || !r.id)
+            return;
+        tvheadend.Ajax({
+            url: 'api/idnode/load',
+            params: {
+                uuid: r.id,
+                meta: 1
+            },
+            success: function(d) {
+                d = json_decode(d);
+                if (current)
+                    mpanel.remove(current.editor);
+                var editor = new tvheadend.idnode_editor(d[0], {
+                                title: 'Parameters',
+                                labelWidth: 300,
+                                fixedHeight: true,
+                                help: conf.help || null,
+                                inTabPanel: true,
+                                noButtons: true
+                            });
+                current = {
+                    uuid: d[0].id,
+                    editor: editor
+                }
+                saveBtn.setDisabled(false);
+                undoBtn.setDisabled(false);
+                delBtn.setDisabled(false);
+                mpanel.add(editor);
+                mpanel.doLayout();
+            }
+        });
+    }
+
+    /* Grid Panel (Selector) */
+    grid = new Ext.grid.GridPanel({
+        width: 200,
+        stripeRows: true,
+        store: store,
+        cm: model,
+        selModel: select,
+        plugins: plugins,
+        border: false,
+        viewConfig: {
+            forceFit: true
+        },
+        listeners : {
+            render : {
+                fn :  function() {
+                    if (!current)
+                        grid.getSelectionModel().selectFirstRow();
+                }
+            }
+        }
+    });
+
+    mpanel = new Ext.Panel({
+       tbar: buttons,
+       title: conf.titleP || '',
+       iconCls: conf.iconCls || '',
+       layout: 'hbox',
+       padding: 5,
+       border: false,
+       layoutConfig: {
+          align: 'stretch'
+       },
+       items: [grid]
+    });
+
+    if (conf.tabIndex != null)
+        panel.insert(conf.tabIndex, mpanel);
+    else
+        panel.add(mpanel);
+
+    /* Add comet listeners */
+    var update = function(o) {
+        store.reload();
+    };
+    if (conf.comet)
+        tvheadend.comet.on(conf.comet, update);
+    tvheadend.comet.on('idnodeUpdated', update);
+    tvheadend.comet.on('idnodeDeleted', update);
+};
+
+/*
+ * IDNode Tree
+ */
 tvheadend.idnode_tree = function(conf)
 {
     var current = null;
index d89c88aacf23f3cf2a4fc0f3bd29d7ee255e9a06..3e8d93dfce191f9ccd4734372765b4a50cc6dbe1 100644 (file)
@@ -40,6 +40,30 @@ tvheadend.help = function(title, pagename) {
     });
 };
 
+tvheadend.Ajax = function(conf) {
+  var orig_success = conf.success;
+  var orig_failure = conf.failure;
+  conf.success = function(d) {
+    tvheadend.loading(0);
+    if (orig_success)
+      orig_success(d);
+  }
+  conf.failure = function(d) {
+    tvheadend.loading(0);
+    if (orig_failure)
+      orig_failure(d);
+  }
+  tvheadend.loading(1);
+  Ext.Ajax.request(conf);
+};
+
+tvheadend.loading = function(on) {
+    if (on)
+      Ext.getBody().mask('Loading... Please, wait...', 'loading');
+    else
+      Ext.getBody().unmask();
+};
+
 /*
  * General capabilities
  */
@@ -219,7 +243,7 @@ function accessUpdate(o) {
         return;
 
     if (o.dvr == true && tvheadend.dvrpanel == null) {
-        tvheadend.dvrpanel = new tvheadend.dvr;
+        tvheadend.dvrpanel = tvheadend.dvr();
         tvheadend.rootTabPanel.add(tvheadend.dvrpanel);
     }
 
@@ -267,17 +291,16 @@ function accessUpdate(o) {
         tabs1.push(tvheadend.conf_chepg);
 
         /* DVR / Timeshift */
-        tabs2 = [new tvheadend.dvrsettings];
-        if (tvheadend.capabilities.indexOf('timeshift') !== -1) {
-            tabs2.push(new tvheadend.timeshift);
-        }
         tvheadend.conf_tsdvr = new Ext.TabPanel({
             activeTab: 0,
             autoScroll: true,
             title: 'Recording',
             iconCls: 'drive',
-            items: tabs2
+            items: []
         });
+        tvheadend.dvr_settings(tvheadend.conf_tsdvr, 0);
+        if (tvheadend.capabilities.indexOf('timeshift') !== -1)
+          tvheadend.conf_tsdvr.add(new tvheadend.timeshift);
         tabs1.push(tvheadend.conf_tsdvr);
 
         /* CSA */
index 9c96cac80c6279d98364e456c1f6f3018639879f..d9cbba3789ca7512ec5080a4042b68e9035f15c0 100644 (file)
@@ -566,7 +566,7 @@ http_dvr_list_playlist(http_connection_t *hc)
   htsbuf_queue_t *hq;
   char buf[255];
   dvr_entry_t *de;
-  const char *host;
+  const char *host, *uuid;
   off_t fsize;
   time_t durration;
   struct tm tm;
@@ -585,6 +585,7 @@ http_dvr_list_playlist(http_connection_t *hc)
         http_access_verify_channel(hc, ACCESS_RECORDER, de->de_channel))
       continue;
 
+
     durration  = de->de_stop - de->de_start;
     durration += (de->de_stop_extra + de->de_start_extra)*60;
     bandwidth = ((8*fsize) / (durration*1024.0));
@@ -593,10 +594,11 @@ http_dvr_list_playlist(http_connection_t *hc)
     htsbuf_qprintf(hq, "#EXTINF:%"PRItime_t",%s\n", durration, lang_str_get(de->de_title, NULL));
     
     htsbuf_qprintf(hq, "#EXT-X-TARGETDURATION:%"PRItime_t"\n", durration);
-    htsbuf_qprintf(hq, "#EXT-X-STREAM-INF:PROGRAM-ID=%d,BANDWIDTH=%d\n", de->de_id, bandwidth);
+    uuid = idnode_uuid_as_str(&de->de_id);
+    htsbuf_qprintf(hq, "#EXT-X-STREAM-INF:PROGRAM-ID=%s,BANDWIDTH=%d\n", uuid, bandwidth);
     htsbuf_qprintf(hq, "#EXT-X-PROGRAM-DATE-TIME:%s\n", buf);
 
-    snprintf(buf, sizeof(buf), "/dvrfile/%d", de->de_id);
+    snprintf(buf, sizeof(buf), "/dvrfile/%s", uuid);
     htsbuf_qprintf(hq, "http://%s%s?ticket=%s\n", host, buf,
        access_ticket_create(buf));
   }
@@ -614,7 +616,7 @@ http_dvr_playlist(http_connection_t *hc, dvr_entry_t *de)
 {
   htsbuf_queue_t *hq = &hc->hc_reply;
   char buf[255];
-  const char *ticket_id = NULL;
+  const char *ticket_id = NULL, *uuid;
   time_t durration = 0;
   off_t fsize = 0;
   int bandwidth = 0;
@@ -634,10 +636,11 @@ http_dvr_playlist(http_connection_t *hc, dvr_entry_t *de)
     htsbuf_qprintf(hq, "#EXTINF:%"PRItime_t",%s\n", durration, lang_str_get(de->de_title, NULL));
     
     htsbuf_qprintf(hq, "#EXT-X-TARGETDURATION:%"PRItime_t"\n", durration);
-    htsbuf_qprintf(hq, "#EXT-X-STREAM-INF:PROGRAM-ID=%d,BANDWIDTH=%d\n", de->de_id, bandwidth);
+    uuid = idnode_uuid_as_str(&de->de_id);
+    htsbuf_qprintf(hq, "#EXT-X-STREAM-INF:PROGRAM-ID=%s,BANDWIDTH=%d\n", uuid, bandwidth);
     htsbuf_qprintf(hq, "#EXT-X-PROGRAM-DATE-TIME:%s\n", buf);
 
-    snprintf(buf, sizeof(buf), "/dvrfile/%d", de->de_id);
+    snprintf(buf, sizeof(buf), "/dvrfile/%s", uuid);
     ticket_id = access_ticket_create(buf);
     htsbuf_qprintf(hq, "http://%s%s?ticket=%s\n", host, buf, ticket_id);
 
index a1bee1cd5be66af10911871d43b32bbb56f95c1a..819bb6a6f78b3ab79fd25f8c67f78428905e5d76 100644 (file)
@@ -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 );
 
 
index 6db06194c20c1f21e0b9fdaf558d53a569ebc4b6..895214431f34c10025ac4ec89de25ee711835902 100644 (file)
@@ -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;