]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
DVR: implement "keep forever" and "keep until space needed"
authorGlenn-1990 <g_christiaensen@msn.com>
Wed, 2 Dec 2015 20:35:53 +0000 (21:35 +0100)
committerJaroslav Kysela <perex@perex.cz>
Thu, 3 Dec 2015 07:50:13 +0000 (08:50 +0100)
src/api/api_dvr.c
src/dvr/dvr.h
src/dvr/dvr_autorec.c
src/dvr/dvr_config.c
src/dvr/dvr_db.c
src/dvr/dvr_rec.c
src/dvr/dvr_timerec.c
src/hts_strtab.h
src/htsp_server.c
src/webui/simpleui.c

index bfd2aef820cf6d9846acca4b286add977c9df309..3ee87f0a3aa205559970ccaf9e8a44f72a46447c 100644 (file)
@@ -240,7 +240,7 @@ api_dvr_entry_create_by_event
                                        e, 0, 0,
                                        perm->aa_username,
                                        perm->aa_representative,
-                                       NULL, DVR_PRIO_NORMAL, 0, 0, comment);
+                                       NULL, DVR_PRIO_NORMAL, DVR_RET_DVRCONFIG, DVR_RET_DVRCONFIG, comment);
         if (de)
           dvr_entry_save(de);
       }
index 137c167d2536337267efb1e692e297abf7323aae..1a012c18d218fcbcdfbf53a4674fb43fdccc7dba 100644 (file)
@@ -54,6 +54,7 @@ typedef struct dvr_config {
   uint32_t dvr_extra_time_post;
   uint32_t dvr_update_window;
   int dvr_running;
+  uint32_t dvr_cleanup_threshold;
 
   muxer_config_t dvr_muxcnf;
 
@@ -119,7 +120,24 @@ typedef enum {
   DVR_RS_EPG_WAIT,
   DVR_RS_FINISHED
 } dvr_rs_state_t;
-  
+
+typedef enum {
+  DVR_RET_DVRCONFIG = 0,
+  DVR_RET_1DAY      = 1,
+  DVR_RET_3DAY      = 3,
+  DVR_RET_5DAY      = 5,
+  DVR_RET_1WEEK     = 7,
+  DVR_RET_2WEEK     = 14,
+  DVR_RET_3WEEK     = 21,
+  DVR_RET_1MONTH    = 30,
+  DVR_RET_2MONTH    = 60,
+  DVR_RET_3MONTH    = 90,
+  DVR_RET_6MONTH    = 180,
+  DVR_RET_1YEAR     = 365,
+  DVR_RET_ONREMOVE  = UINT32_MAX-1, // for retention only
+  DVR_RET_SPACENEED = UINT32_MAX-1, // for removal only
+  DVR_RET_FOREVER   = UINT32_MAX
+} dvr_retention_t;
 
 typedef struct dvr_entry {
 
@@ -430,6 +448,10 @@ static inline int dvr_entry_is_valid(dvr_entry_t *de)
 
 int dvr_entry_get_mc(dvr_entry_t *de);
 
+const char *dvr_entry_get_retention_string ( dvr_entry_t *de );
+
+const char *dvr_entry_get_removal_string ( dvr_entry_t *de );
+
 uint32_t dvr_entry_get_retention_days( dvr_entry_t *de );
 
 uint32_t dvr_entry_get_removal_days( dvr_entry_t *de );
@@ -531,6 +553,8 @@ const char *dvr_get_filename(dvr_entry_t *de);
 
 int64_t dvr_get_filesize(dvr_entry_t *de);
 
+int64_t dvr_entry_claenup(dvr_entry_t *de, int64_t requiredBytes);
+
 void dvr_entry_set_rerecord(dvr_entry_t *de, int cmd);
 
 dvr_entry_t *dvr_entry_stop(dvr_entry_t *de);
@@ -543,10 +567,14 @@ void dvr_entry_delete(dvr_entry_t *de, int no_missed_time_resched);
 
 void dvr_entry_cancel_delete(dvr_entry_t *de, int rerecord);
 
+void dvr_entry_destroy(dvr_entry_t *de, int delconf);
+
 htsmsg_t *dvr_entry_class_mc_list (void *o, const char *lang);
 htsmsg_t *dvr_entry_class_pri_list(void *o, const char *lang);
 htsmsg_t *dvr_entry_class_config_name_list(void *o, const char *lang);
 htsmsg_t *dvr_entry_class_duration_list(void *o, const char *not_set, int max, int step, const char *lang);
+htsmsg_t *dvr_entry_class_retention_list ( void *o, const char *lang );
+htsmsg_t *dvr_entry_class_removal_list ( void *o, const char *lang );
 
 int dvr_entry_verify(dvr_entry_t *de, access_t *a, int readonly);
 
index 3d2266914b9b720c0014f385d423b871e458e0dd..7e9122727ef54d5ba3bd5db17d141ae63a8c58f2 100644 (file)
@@ -1092,15 +1092,19 @@ const idclass_t dvr_autorec_entry_class = {
     {
       .type     = PT_U32,
       .id       = "retention",
-      .name     = N_("DVR log retention (days)"),
+      .name     = N_("DVR log retention"),
+      .def.i    = DVR_RET_DVRCONFIG,
       .off      = offsetof(dvr_autorec_entry_t, dae_retention),
+      .list     = dvr_entry_class_retention_list,
       .opts     = PO_HIDDEN | PO_EXPERT,
     },
     {
       .type     = PT_U32,
       .id       = "removal",
-      .name     = N_("DVR file retention period (days)"),
+      .name     = N_("DVR file retention period"),
+      .def.i    = DVR_RET_DVRCONFIG,
       .off      = offsetof(dvr_autorec_entry_t, dae_removal),
+      .list     = dvr_entry_class_removal_list,
       .opts     = PO_HIDDEN | PO_EXPERT,
     },
     {
@@ -1389,8 +1393,14 @@ dvr_autorec_get_extra_time_post( dvr_autorec_entry_t *dae )
 uint32_t
 dvr_autorec_get_retention_days( dvr_autorec_entry_t *dae )
 {
-  if (dae->dae_retention > 0)
+  if (dae->dae_retention > 0) {
+    /* As we need the db entry when deleting the file on disk */
+    if (dvr_autorec_get_removal_days(dae) != DVR_RET_FOREVER &&
+        dvr_autorec_get_removal_days(dae) > dae->dae_retention)
+      return DVR_RET_ONREMOVE;
+
     return dae->dae_retention;
+  }
   return dvr_retention_cleanup(dae->dae_config->dvr_retention_days);
 }
 
index 988d3d1f4bafcff8b622542358a330192ab1d8cd..4bdd142844714f80969d5bd6923c464a282801c0 100644 (file)
@@ -177,7 +177,8 @@ dvr_config_create(const char *name, const char *uuid, htsmsg_t *conf)
 
   cfg->dvr_enabled = 1;
   cfg->dvr_config_name = strdup(name);
-  cfg->dvr_retention_days = 31;
+  cfg->dvr_retention_days = DVR_RET_ONREMOVE;
+  cfg->dvr_removal_days = DVR_RET_FOREVER;
   cfg->dvr_clone = 1;
   cfg->dvr_tag_files = 1;
   cfg->dvr_skip_commercials = 1;
@@ -185,6 +186,7 @@ dvr_config_create(const char *name, const char *uuid, htsmsg_t *conf)
   cfg->dvr_warm_time = 30;
   cfg->dvr_update_window = 24 * 3600;
   cfg->dvr_pathname = strdup("$t$n.$x");
+  cfg->dvr_cleanup_threshold = 2000;
 
   /* Muxer config */
   cfg->dvr_muxcnf.m_cache  = MC_CACHE_DONTKEEP;
@@ -514,8 +516,11 @@ dvr_config_save(dvr_config_t *cfg)
   lock_assert(&global_lock);
 
   dvr_config_storage_check(cfg);
-  if (cfg->dvr_removal_days > cfg->dvr_retention_days)
-    cfg->dvr_removal_days = cfg->dvr_retention_days;
+  if (cfg->dvr_cleanup_threshold < 100)
+    cfg->dvr_cleanup_threshold = 100; // as checking is only periodically, lower is not save
+  if (cfg->dvr_removal_days != DVR_RET_FOREVER &&
+      cfg->dvr_removal_days > cfg->dvr_retention_days)
+    cfg->dvr_retention_days = DVR_RET_ONREMOVE;
   idnode_save(&cfg->dvr_id, m);
   hts_settings_save(m, "dvr/config/%s", idnode_uuid_as_sstr(&cfg->dvr_id));
   htsmsg_destroy(m);
@@ -694,6 +699,48 @@ dvr_config_class_cache_list(void *o, const char *lang)
   return strtab2htsmsg(tab, 1, lang);
 }
 
+static htsmsg_t *
+dvr_config_class_removal_list ( void *o, const char *lang )
+{
+  static const struct strtab_u32 tab[] = {
+    { N_("1 day"),              DVR_RET_1DAY },
+    { N_("3 days"),             DVR_RET_3DAY },
+    { N_("5 days"),             DVR_RET_5DAY },
+    { N_("1 week"),             DVR_RET_1WEEK },
+    { N_("2 weeks"),            DVR_RET_2WEEK },
+    { N_("3 weeks"),            DVR_RET_3WEEK },
+    { N_("1 month"),            DVR_RET_1MONTH },
+    { N_("2 months"),           DVR_RET_2MONTH },
+    { N_("3 months"),           DVR_RET_3MONTH },
+    { N_("6 months"),           DVR_RET_6MONTH },
+    { N_("1 year"),             DVR_RET_1YEAR },
+    { N_("Until space needed"), DVR_RET_SPACENEED },
+    { N_("Forever"),            DVR_RET_FOREVER },
+  };
+  return strtab2htsmsg_u32(tab, 1, lang);
+}
+
+static htsmsg_t *
+dvr_config_class_retention_list ( void *o, const char *lang )
+{
+  static const struct strtab_u32 tab[] = {
+    { N_("1 day"),              DVR_RET_1DAY },
+    { N_("3 days"),             DVR_RET_3DAY },
+    { N_("5 days"),             DVR_RET_5DAY },
+    { N_("1 week"),             DVR_RET_1WEEK },
+    { N_("2 weeks"),            DVR_RET_2WEEK },
+    { N_("3 weeks"),            DVR_RET_3WEEK },
+    { N_("1 month"),            DVR_RET_1MONTH },
+    { N_("2 months"),           DVR_RET_2MONTH },
+    { N_("3 months"),           DVR_RET_3MONTH },
+    { N_("6 months"),           DVR_RET_6MONTH },
+    { N_("1 year"),             DVR_RET_1YEAR },
+    { N_("On file removal"),    DVR_RET_ONREMOVE },
+    { N_("Forever"),            DVR_RET_FOREVER },
+  };
+  return strtab2htsmsg_u32(tab, 1, lang);
+}
+
 static htsmsg_t *
 dvr_config_class_extra_list(void *o, const char *lang)
 {
@@ -813,16 +860,19 @@ const idclass_t dvr_config_class = {
     {
       .type     = PT_U32,
       .id       = "retention-days",
-      .name     = N_("DVR log retention period (days)"),
+      .name     = N_("DVR log retention period"),
       .off      = offsetof(dvr_config_t, dvr_retention_days),
-      .def.u32  = 31,
+      .def.u32  = DVR_RET_ONREMOVE,
+      .list     = dvr_config_class_retention_list,
       .group    = 1,
     },
     {
       .type     = PT_U32,
       .id       = "removal-days",
-      .name     = N_("DVR file retention period (days)"),
+      .name     = N_("DVR file retention period"),
       .off      = offsetof(dvr_config_t, dvr_removal_days),
+      .def.u32  = DVR_RET_FOREVER,
+      .list     = dvr_config_class_removal_list,
       .group    = 1,
     },
     {
@@ -927,6 +977,14 @@ const idclass_t dvr_config_class = {
       .off      = offsetof(dvr_config_t, dvr_storage),
       .group    = 2,
     },
+    {
+      .type     = PT_U32,
+      .id       = "storage-cleanup",
+      .name     = N_("\"Until space needed\" cleanup below (MB)"),
+      .off      = offsetof(dvr_config_t, dvr_cleanup_threshold),
+      .def.i    = 2000, //2000 MB
+      .group    = 2,
+    },
     {
       .type     = PT_PERM,
       .id       = "file-permissions",
index 9540ee37efdf47fca1f3ee66bad8debbe73c38c1..79c9ab892ccccfb3e68ee0fb016df258e6db0308 100644 (file)
@@ -42,7 +42,6 @@ static int dvr_in_init;
 static gtimer_t dvr_dbus_timer;
 #endif
 
-static void dvr_entry_destroy(dvr_entry_t *de, int delconf);
 static void dvr_entry_set_timer(dvr_entry_t *de);
 static void dvr_timer_rerecord(void *aux);
 static void dvr_timer_expire(void *aux);
@@ -256,11 +255,49 @@ dvr_entry_get_extra_time_post( dvr_entry_t *de )
   return extra;
 }
 
+const char *
+dvr_entry_get_retention_string ( dvr_entry_t *de )
+{
+  char buf[24];
+  uint32_t retention = dvr_entry_get_retention_days(de);
+
+  if (retention < DVR_RET_ONREMOVE)
+    snprintf(buf, sizeof(buf), "%i days", retention);
+  else if (retention == DVR_RET_ONREMOVE)
+    return strdup("On file removal");
+  else
+    return strdup("Forever");
+
+  return strdup(buf);
+}
+
+const char *
+dvr_entry_get_removal_string ( dvr_entry_t *de )
+{
+  char buf[24];
+  uint32_t removal = dvr_entry_get_removal_days(de);
+
+  if (removal < DVR_RET_SPACENEED)
+    snprintf(buf, sizeof(buf), "%i days", removal);
+  else if (removal == DVR_RET_SPACENEED)
+    return strdup("Until space needed");
+  else
+    return strdup("Forever");
+
+  return strdup(buf);
+}
+
 uint32_t
 dvr_entry_get_retention_days( dvr_entry_t *de )
 {
-  if (de->de_retention > 0)
+  if (de->de_retention > 0) {
+    /* As we need the db entry when deleting the file on disk */
+    if (dvr_entry_get_removal_days(de) != DVR_RET_FOREVER &&
+        dvr_entry_get_removal_days(de) > de->de_retention)
+      return DVR_RET_ONREMOVE;
+
     return de->de_retention;
+  }
   return dvr_retention_cleanup(de->de_config->dvr_retention_days);
 }
 
@@ -349,19 +386,25 @@ dvr_entry_retention_timer(dvr_entry_t *de)
   time_t stop = de->de_stop;
   uint32_t removal = dvr_entry_get_removal_days(de);
   uint32_t retention = dvr_entry_get_retention_days(de);
-  if (removal > retention)
-    removal = retention;
+
   stop = de->de_stop + removal * (time_t)86400;
-  if (removal > 0 || retention == 0) {
+  if ((removal > 0 || retention == 0) &&  removal < DVR_RET_SPACENEED) {
     if (stop > dispatch_clock) {
       dvr_entry_retention_arm(de, dvr_timer_remove_files, stop);
       return;
     }
     if (dvr_get_filename(de))
-      dvr_entry_delete(de, 1);
+      dvr_entry_delete(de, 1);    // delete actual file
+    if (retention == DVR_RET_ONREMOVE) {
+      dvr_entry_destroy(de, 1);   // also remove database entry
+      return;
+    }
+  }
+
+  if (retention  < DVR_RET_ONREMOVE) {
+    stop = de->de_stop + retention * (time_t)86400;
+    dvr_entry_retention_arm(de, dvr_timer_expire, stop);
   }
-  stop = de->de_stop + retention * (time_t)86400;
-  dvr_entry_retention_arm(de, dvr_timer_expire, stop);
 }
 
 /*
@@ -935,7 +978,8 @@ delete_me:
         return 1;
       }
 not_so_good:
-      de->de_retention = 1;
+      de->de_retention = DVR_RET_ONREMOVE;
+      de->de_removal = DVR_RET_1DAY;
       dvr_entry_change_parent_child(de->de_parent, NULL, NULL, 1);
       dvr_entry_completed(de, SM_CODE_WEAK_STREAM);
       return 0;
@@ -1173,7 +1217,7 @@ dvr_entry_dec_ref(dvr_entry_t *de)
 /**
  *
  */
-static void
+void
 dvr_entry_destroy(dvr_entry_t *de, int delconf)
 {
   if (delconf)
@@ -1772,7 +1816,7 @@ dvr_entry_start_recording(dvr_entry_t *de, int clone)
     return;
   }
 
-  gtimer_arm_abs(&de->de_timer, dvr_timer_stop_recording, de, 
+  gtimer_arm_abs(&de->de_timer, dvr_timer_stop_recording, de,
                  dvr_entry_get_stop_time(de));
 }
 
@@ -2137,18 +2181,48 @@ dvr_entry_class_pri_list ( void *o, const char *lang )
   return strtab2htsmsg(tab, 1, lang);
 }
 
-static int
-dvr_entry_class_retention_set(void *o, const void *v)
-{
-  dvr_entry_t *de = (dvr_entry_t *)o;
-  return dvr_entry_class_int_set(de, (int *)&de->de_retention, *(int *)v);
+htsmsg_t *
+dvr_entry_class_retention_list ( void *o, const char *lang )
+{
+  static const struct strtab_u32 tab[] = {
+    { N_("DVR configuration"),  DVR_RET_DVRCONFIG },
+    { N_("1 day"),              DVR_RET_1DAY },
+    { N_("3 days"),             DVR_RET_3DAY },
+    { N_("5 days"),             DVR_RET_5DAY },
+    { N_("1 week"),             DVR_RET_1WEEK },
+    { N_("2 weeks"),            DVR_RET_2WEEK },
+    { N_("3 weeks"),            DVR_RET_3WEEK },
+    { N_("1 month"),            DVR_RET_1MONTH },
+    { N_("2 months"),           DVR_RET_2MONTH },
+    { N_("3 months"),           DVR_RET_3MONTH },
+    { N_("6 months"),           DVR_RET_6MONTH },
+    { N_("1 year"),             DVR_RET_1YEAR },
+    { N_("On file removal"),    DVR_RET_ONREMOVE },
+    { N_("Forever"),            DVR_RET_FOREVER },
+  };
+  return strtab2htsmsg_u32(tab, 1, lang);
 }
 
-static int
-dvr_entry_class_removal_set(void *o, const void *v)
-{
-  dvr_entry_t *de = (dvr_entry_t *)o;
-  return dvr_entry_class_int_set(de, (int *)&de->de_removal, *(int *)v);
+htsmsg_t *
+dvr_entry_class_removal_list ( void *o, const char *lang )
+{
+  static const struct strtab_u32 tab[] = {
+    { N_("DVR configuration"),  DVR_RET_DVRCONFIG },
+    { N_("1 day"),              DVR_RET_1DAY },
+    { N_("3 days"),             DVR_RET_3DAY },
+    { N_("5 days"),             DVR_RET_5DAY },
+    { N_("1 week"),             DVR_RET_1WEEK },
+    { N_("2 weeks"),            DVR_RET_2WEEK },
+    { N_("3 weeks"),            DVR_RET_3WEEK },
+    { N_("1 month"),            DVR_RET_1MONTH },
+    { N_("2 months"),           DVR_RET_2MONTH },
+    { N_("3 months"),           DVR_RET_3MONTH },
+    { N_("6 months"),           DVR_RET_6MONTH },
+    { N_("1 year"),             DVR_RET_1YEAR },
+    { N_("Until space needed"), DVR_RET_SPACENEED },
+    { N_("Forever"),            DVR_RET_FOREVER },
+  };
+  return strtab2htsmsg_u32(tab, 1, lang);
 }
 
 static int
@@ -2711,17 +2785,19 @@ const idclass_t dvr_entry_class = {
     {
       .type     = PT_U32,
       .id       = "retention",
-      .name     = N_("DVR log retention (days)"),
+      .name     = N_("DVR log retention"),
       .off      = offsetof(dvr_entry_t, de_retention),
-      .set      = dvr_entry_class_retention_set,
+      .def.i    = DVR_RET_DVRCONFIG,
+      .list     = dvr_entry_class_retention_list,
       .opts     = PO_HIDDEN | PO_ADVANCED
     },
     {
       .type     = PT_U32,
       .id       = "removal",
-      .name     = N_("DVR file retention period (days)"),
+      .name     = N_("DVR file retention period"),
       .off      = offsetof(dvr_entry_t, de_removal),
-      .set      = dvr_entry_class_removal_set,
+      .def.i    = DVR_RET_DVRCONFIG,
+      .list     = dvr_entry_class_removal_list,
       .opts     = PO_HIDDEN | PO_ADVANCED
     },
     {
@@ -3017,12 +3093,12 @@ dvr_entry_delete(dvr_entry_t *de, int no_missed_time_resched)
     *tbuf = 0;
 
   tvhlog(LOG_INFO, "dvr", "delete entry %s \"%s\" on \"%s\" start time %s, "
-        "scheduled for recording by \"%s\", retention %d days, removal %d days",
+        "scheduled for recording by \"%s\", retention \"%s\" removal \"%s\"",
          idnode_uuid_as_sstr(&de->de_id),
         lang_str_get(de->de_title, NULL), DVR_CH_NAME(de), tbuf,
         de->de_creator ?: "",
-        dvr_entry_get_retention_days(de),
-        dvr_entry_get_removal_days(de));
+        dvr_entry_get_retention_string(de),
+        dvr_entry_get_removal_string(de));
 
   if(!htsmsg_is_empty(de->de_files)) {
 #if ENABLE_INOTIFY
index ce0449c19d30a403965d727d7b38b7a44f0df832..6e78eb2d048d085df991287e7bc13d2f816554a1 100644 (file)
@@ -1541,12 +1541,183 @@ dvr_thread_epilog(dvr_entry_t *de, const char *dvr_postproc)
 /**
  *
  */
+static int dvr_disk_space_config_idx;
+static int dvr_disk_space_config_size;
+static time_t dvr_disk_space_config_lastdelete;
 static int64_t dvr_bfree;
 static int64_t dvr_btotal;
 static pthread_mutex_t dvr_disk_space_mutex;
 static gtimer_t dvr_disk_space_timer;
 static tasklet_t dvr_disk_space_tasklet;
 
+/**
+ * Cleanup old recordings for this config until the dvr_cleanup_threshold is reached
+ * Only "Keep until space needed" recordings are deleted, starting with the oldest one
+ */
+static int64_t
+dvr_disk_space_cleanup(dvr_config_t *cfg)
+{
+  dvr_entry_t *de, *oldest;
+  time_t stoptime;
+  int64_t requiredBytes, availBytes;
+  int64_t clearedBytes = 0, fileSize;
+  unsigned long int filesystemId;
+  struct statvfs diskdata;
+  struct tm tm;
+  int loops = 0;
+  char tbuf[64];
+  const char *configName;
+
+  if (!cfg || !cfg->dvr_enabled || statvfs(cfg->dvr_storage, &diskdata) == -1)
+    return -1;
+
+  filesystemId  = diskdata.f_fsid;
+  availBytes    = diskdata.f_bsize * (int64_t)diskdata.f_bavail;
+  requiredBytes = (int64_t)cfg->dvr_cleanup_threshold*(int64_t)1024*(int64_t)1024;
+  configName    = cfg != dvr_config_find_by_name(NULL) ? cfg->dvr_config_name : "Default profile";
+
+  /* When deleting a file from the disk, the system needs some time to actually do this */
+  /* If calling this function to fast after the previous call, statvfs might be wrong/not updated yet */
+  /* So we are risking to delete more files than needed, so allow 10s for the system to handle previous deletes */
+  if (dvr_disk_space_config_lastdelete + 10 > dispatch_clock) {
+    tvhlog(LOG_WARNING, "dvr","disk space cleanup for config \"%s\" is not allowed now", configName);
+    return -1;
+  }
+
+  if (diskdata.f_bsize * (int64_t)diskdata.f_blocks < requiredBytes) {
+    tvhlog(LOG_WARNING, "dvr","disk space cleanup for config \"%s\", required free space \"%ld MB\" is smaller than the total disk size!",
+        configName, requiredBytes/(int64_t)1024/(int64_t)1024);
+    return -1;
+  }
+
+  tvhlog(LOG_INFO, "dvr","disk space cleanup for config \"%s\", required free space \"%ld MB\", current free space \"%ld MB\"",
+      configName, requiredBytes/(int64_t)1024/(int64_t)1024, availBytes/(int64_t)1024/(int64_t)1024);
+
+  while (availBytes < requiredBytes) {
+    oldest = NULL;
+    stoptime = dispatch_clock;
+
+    LIST_FOREACH(de, &dvrentries, de_global_link) {
+      if (de->de_sched_state != DVR_COMPLETED &&
+          de->de_sched_state != DVR_MISSED_TIME)
+        continue;
+
+      if (dvr_entry_get_stop_time(de) > stoptime)
+        continue;
+
+      if (dvr_entry_get_removal_days(de) != DVR_RET_SPACENEED) // only remove the allowed ones
+        continue;
+
+      if (dvr_get_filename(de) == NULL || dvr_get_filesize(de) <= 0)
+        continue;
+
+      if(statvfs(dvr_get_filename(de), &diskdata) == -1)
+        continue;
+
+      /* Checking for the same config is useless as it's storage path might be changed meanwhile */
+      /* Check for the same file system instead */
+      if (filesystemId == 0 || diskdata.f_fsid != filesystemId)
+        continue;
+
+      oldest = de; // the oldest one until now
+      stoptime = dvr_entry_get_stop_time(de);
+    }
+
+    if (oldest) {
+      fileSize = dvr_get_filesize(oldest);
+      availBytes += fileSize;
+      clearedBytes += fileSize;
+
+      localtime_r(&stoptime, &tm);
+      if (strftime(tbuf, sizeof(tbuf), "%F %T", &tm) <= 0)
+        *tbuf = 0;
+      tvhlog(LOG_INFO, "dvr","Delete \"until space needed\" recording \"%s\" with stop time \"%s\" and file size \"%ld MB\"",
+          lang_str_get(oldest->de_title, NULL), tbuf, fileSize/(int64_t)1024/(int64_t)1024);
+
+      dvr_disk_space_config_lastdelete = dispatch_clock;
+      dvr_entry_delete(oldest, 1);    // delete actual file
+      if (dvr_entry_get_retention_days(oldest) == DVR_RET_ONREMOVE)
+        dvr_entry_destroy(oldest, 1); // also delete database entry
+    }
+    else {
+      tvhlog(LOG_WARNING, "dvr","%s \"until space needed\" recordings found for config \"%s\", you are running out of disk space very soon!",
+          loops > 0 ? "Not enough" : "No", configName);
+      goto finish;
+    }
+
+    loops++;
+    if (loops >= 10) {
+      tvhlog(LOG_WARNING, "dvr","Not able to clear the required disk space after deleting %i \"until space needed\" recordings...", loops);
+      goto finish;
+    }
+  }
+
+finish:
+  tvhlog(LOG_INFO, "dvr","disk space cleanup for config \"%s\", cleared \"%ld MB\" of disk space, new free disk space \"%ld MB\"",
+      configName, clearedBytes/(int64_t)1024/(int64_t)1024, availBytes/(int64_t)1024/(int64_t)1024);
+
+  return clearedBytes;
+}
+
+/**
+ * Check for each dvr config if the free disk size is below the dvr_cleanup_threshold
+ * If so and we are using the dvr config ATM (active recording), we start the cleanup procedure
+ */
+static void
+dvr_disk_space_check()
+{
+  dvr_config_t *cfg;
+  dvr_entry_t *de;
+  struct statvfs diskdata;
+  int64_t requiredBytes, availBytes;
+  int idx = 0, cleanupDone = 0;
+
+  dvr_disk_space_config_idx++;
+  if (dvr_disk_space_config_idx > dvr_disk_space_config_size)
+    dvr_disk_space_config_idx = 1;
+
+  LIST_FOREACH(cfg, &dvrconfigs, config_link)
+  {
+    idx++;
+
+    if (cfg->dvr_enabled &&
+        !cleanupDone &&
+        idx >= dvr_disk_space_config_idx &&
+        statvfs(cfg->dvr_storage, &diskdata) != -1)
+    {
+      availBytes = diskdata.f_bsize * (int64_t)diskdata.f_bavail;
+      requiredBytes = (int64_t)cfg->dvr_cleanup_threshold*(int64_t)1024*(int64_t)1024;
+
+      if (availBytes < requiredBytes) {
+        LIST_FOREACH(de, &dvrentries, de_global_link) {
+
+          /* only start cleanup if we are actually writing files right now */
+          if (de->de_sched_state != DVR_RECORDING || !de->de_config || de->de_config != cfg)
+            continue;
+
+          tvhlog(LOG_WARNING, "dvr","running out of free disk space for dvr config \"%s\", required free space \"%ld MB\", current free space \"%ld MB\"",
+              cfg != dvr_config_find_by_name(NULL) ? cfg->dvr_config_name : "Default profile",
+              requiredBytes/(int64_t)1024/(int64_t)1024, availBytes/(int64_t)1024/(int64_t)1024);
+
+          /* only cleanup one directory at the time as the system needs time to delete the actual files */
+          dvr_disk_space_cleanup(de->de_config);
+          cleanupDone = 1;
+          dvr_disk_space_config_idx = idx;
+          break;
+        }
+      }
+      else {
+        tvhlog(LOG_DEBUG, "dvr","checking free disk space for config \"%s\" : OK",
+            cfg != dvr_config_find_by_name(NULL) ? cfg->dvr_config_name : "Default profile");
+      }
+    }
+  }
+
+  if (!cleanupDone)
+    dvr_disk_space_config_idx = 0;
+
+  dvr_disk_space_config_size = idx;
+}
 /**
  *
  */
@@ -1571,9 +1742,14 @@ dvr_get_disk_space_tcb(void *opaque, int dearmed)
   if (!dearmed) {
     htsmsg_t *m = htsmsg_create_map();
     pthread_mutex_lock(&dvr_disk_space_mutex);
+
+    /* update disk space from default dvr config */
     dvr_get_disk_space_update((char *)opaque);
     htsmsg_add_s64(m, "freediskspace", dvr_bfree);
     htsmsg_add_s64(m, "totaldiskspace", dvr_btotal);
+
+    /* check free disk space for each dvr config and start cleanup if needed */
+    dvr_disk_space_check();
     pthread_mutex_unlock(&dvr_disk_space_mutex);
 
     notify_by_msg("diskspaceUpdate", m, 0);
@@ -1595,7 +1771,7 @@ dvr_get_disk_space_cb(void *aux)
     path = strdup(cfg->dvr_storage);
     tasklet_arm(&dvr_disk_space_tasklet, dvr_get_disk_space_tcb, path);
   }
-  gtimer_arm(&dvr_disk_space_timer, dvr_get_disk_space_cb, NULL, 60);
+  gtimer_arm(&dvr_disk_space_timer, dvr_get_disk_space_cb, NULL, 15);
 }
 
 /**
@@ -1607,7 +1783,7 @@ dvr_disk_space_init(void)
   dvr_config_t *cfg = dvr_config_find_by_name_default(NULL);
   pthread_mutex_init(&dvr_disk_space_mutex, NULL);
   dvr_get_disk_space_update(cfg->dvr_storage);
-  gtimer_arm(&dvr_disk_space_timer, dvr_get_disk_space_cb, NULL, 60);
+  gtimer_arm(&dvr_disk_space_timer, dvr_get_disk_space_cb, NULL, 5);
 }
 
 /**
index 14d9236435d552d90bb714566a2dfa3f5beab243..052b8a9bd1e0f485cef8d84e2b91bbb23300fca5 100644 (file)
@@ -609,15 +609,19 @@ const idclass_t dvr_timerec_entry_class = {
     {
       .type     = PT_U32,
       .id       = "retention",
-      .name     = N_("DVR log retention (days)"),
+      .name     = N_("DVR log retention"),
+      .def.i    = DVR_RET_DVRCONFIG,
       .off      = offsetof(dvr_timerec_entry_t, dte_retention),
+      .list     = dvr_entry_class_retention_list,
       .opts     = PO_EXPERT
     },
     {
       .type     = PT_U32,
       .id       = "removal",
-      .name     = N_("DVR file retention period (days)"),
+      .name     = N_("DVR file retention period"),
+      .def.i    = DVR_RET_DVRCONFIG,
       .off      = offsetof(dvr_timerec_entry_t, dte_removal),
+      .list     = dvr_entry_class_removal_list,
       .opts     = PO_EXPERT
     },
     {
@@ -760,8 +764,14 @@ timerec_destroy_by_config(dvr_config_t *kcfg, int delconf)
 uint32_t
 dvr_timerec_get_retention_days( dvr_timerec_entry_t *dte )
 {
-  if (dte->dte_retention > 0)
+  if (dte->dte_retention > 0) {
+    /* As we need the db entry when deleting the file on disk */
+    if (dvr_timerec_get_removal_days(dte) != DVR_RET_FOREVER &&
+        dvr_timerec_get_removal_days(dte) > dte->dte_retention)
+      return DVR_RET_ONREMOVE;
+
     return dte->dte_retention;
+  }
   return dvr_retention_cleanup(dte->dte_config->dvr_retention_days);
 }
 
index ac71067dfdb5f12280139866f49bab27eb345ad1..93619950664716acaebf22ac16c41d165365b2da 100644 (file)
@@ -29,6 +29,11 @@ struct strtab {
   int val;
 };
 
+struct strtab_u32 {
+  const char *str;
+  uint32_t val;
+};
+
 static int str2val0(const char *str, const struct strtab tab[], int l)
      __attribute((unused));
 
@@ -96,4 +101,20 @@ strtab2htsmsg0(const struct strtab tab[], int n, int i18n, const char *lang)
 
 #define strtab2htsmsg(tab,i18n,lang) strtab2htsmsg0(tab, sizeof(tab) / sizeof(tab[0]), i18n, lang)
 
+static inline htsmsg_t *
+strtab2htsmsg0_u32(const struct strtab_u32 tab[], uint32_t n, int i18n, const char *lang)
+{
+  uint32_t i;
+  htsmsg_t *e, *l = htsmsg_create_list();
+  for (i = 0; i < n; i++) {
+    e = htsmsg_create_map();
+    htsmsg_add_u32(e, "key", tab[i].val);
+    htsmsg_add_str(e, "val", i18n ? tvh_gettext_lang(lang, tab[i].str) : tab[i].str);
+    htsmsg_add_msg(l, NULL, e);
+  }
+  return l;
+}
+
+#define strtab2htsmsg_u32(tab,i18n,lang) strtab2htsmsg0_u32(tab, sizeof(tab) / sizeof(tab[0]), i18n, lang)
+
 #endif /* STRTAB_H_ */
index b0ba34da3e9d74779b125197fe194892894ede9b..d61f15d1d5d0861d495c1a83831e76efecbbc783 100644 (file)
@@ -596,9 +596,9 @@ serierec_convert(htsp_connection_t *htsp, htsmsg_t *in, channel_t *ch, int autor
   if (!(retval = htsmsg_get_u32(in, "enabled", &u32)) || add)
     htsmsg_add_u32(conf, "enabled", !retval ? (u32 > 0 ? 1 : 0) : 1); // default on
   if (!(retval = htsmsg_get_u32(in, "retention", &u32)) || add)
-    htsmsg_add_u32(conf, "retention", !retval ? u32 : 0); // 0 = dvr config
+    htsmsg_add_u32(conf, "retention", !retval ? u32 : DVR_RET_DVRCONFIG);
   if (!(retval = htsmsg_get_u32(in, "removal", &u32)) || add)
-    htsmsg_add_u32(conf, "removal", !retval ? u32 : 0);   // 0 = dvr config
+    htsmsg_add_u32(conf, "removal", !retval ? u32 : DVR_RET_DVRCONFIG);
   if(!(retval = htsmsg_get_u32(in, "priority", &u32)) || add)
     htsmsg_add_u32(conf, "pri", !retval ? u32 : DVR_PRIO_NORMAL);
   if ((str = htsmsg_get_str(in, "name")) || add)
@@ -843,7 +843,7 @@ htsp_build_tag(channel_tag_t *ct, const char *method, int include_channels)
  *
  */
 static htsmsg_t *
-htsp_build_dvrentry(dvr_entry_t *de, const char *method, const char *lang)
+htsp_build_dvrentry(htsp_connection_t *htsp, dvr_entry_t *de, const char *method, const char *lang)
 {
   htsmsg_t *out = htsmsg_create_map(), *l, *m, *e;
   htsmsg_field_t *f;
@@ -869,7 +869,13 @@ htsp_build_dvrentry(dvr_entry_t *de, const char *method, const char *lang)
   htsmsg_add_s64(out, "stop",        de->de_stop);
   htsmsg_add_s64(out, "startExtra",  dvr_entry_get_extra_time_pre(de));
   htsmsg_add_s64(out, "stopExtra",   dvr_entry_get_extra_time_post(de));
-  htsmsg_add_u32(out, "retention",   dvr_entry_get_retention_days(de));
+
+  if (htsp->htsp_version > 25)
+    htsmsg_add_u32(out, "retention",   dvr_entry_get_retention_days(de));
+  else
+    htsmsg_add_u32(out, "retention",   dvr_entry_get_retention_days(de) == DVR_RET_ONREMOVE ?
+        dvr_entry_get_removal_days(de) : dvr_entry_get_retention_days(de));
+
   htsmsg_add_u32(out, "removal",     dvr_entry_get_removal_days(de));
   htsmsg_add_u32(out, "priority",    de->de_pri);
   htsmsg_add_u32(out, "contentType", de->de_content_type);
@@ -959,7 +965,7 @@ htsp_build_dvrentry(dvr_entry_t *de, const char *method, const char *lang)
  *
  */
 static htsmsg_t *
-htsp_build_autorecentry(dvr_autorec_entry_t *dae, const char *method)
+htsp_build_autorecentry(htsp_connection_t *htsp, dvr_autorec_entry_t *dae, const char *method)
 {
   htsmsg_t *out = htsmsg_create_map();
   int tad;
@@ -968,7 +974,13 @@ htsp_build_autorecentry(dvr_autorec_entry_t *dae, const char *method)
   htsmsg_add_u32(out, "enabled",     dae->dae_enabled >= 1 ? 1 : 0);
   htsmsg_add_u32(out, "maxDuration", dae->dae_maxduration);
   htsmsg_add_u32(out, "minDuration", dae->dae_minduration);
-  htsmsg_add_u32(out, "retention",   dvr_autorec_get_retention_days(dae));
+
+  if (htsp->htsp_version > 25)
+    htsmsg_add_u32(out, "retention",   dvr_autorec_get_retention_days(dae));
+  else
+    htsmsg_add_u32(out, "retention",   dvr_autorec_get_retention_days(dae) == DVR_RET_ONREMOVE ?
+        dvr_autorec_get_removal_days(dae) : dvr_autorec_get_retention_days(dae));
+
   htsmsg_add_u32(out, "removal",     dvr_autorec_get_removal_days(dae));
   htsmsg_add_u32(out, "daysOfWeek",  dae->dae_weekdays);
   if (dae->dae_start >= 0 && dae->dae_start_window >= 0) {
@@ -1014,14 +1026,20 @@ htsp_build_autorecentry(dvr_autorec_entry_t *dae, const char *method)
  *
  */
 static htsmsg_t *
-htsp_build_timerecentry(dvr_timerec_entry_t *dte, const char *method)
+htsp_build_timerecentry(htsp_connection_t *htsp, dvr_timerec_entry_t *dte, const char *method)
 {
   htsmsg_t *out = htsmsg_create_map();
 
   htsmsg_add_str(out, "id",          idnode_uuid_as_sstr(&dte->dte_id));
   htsmsg_add_u32(out, "enabled",     dte->dte_enabled >= 1 ? 1 : 0);
   htsmsg_add_u32(out, "daysOfWeek",  dte->dte_weekdays);
-  htsmsg_add_u32(out, "retention",   dvr_timerec_get_retention_days(dte));
+
+  if (htsp->htsp_version > 25)
+    htsmsg_add_u32(out, "retention",   dvr_timerec_get_retention_days(dte));
+  else
+    htsmsg_add_u32(out, "retention",   dvr_timerec_get_retention_days(dte) == DVR_RET_ONREMOVE ?
+        dvr_timerec_get_removal_days(dte) : dvr_timerec_get_retention_days(dte));
+
   htsmsg_add_u32(out, "removal",     dvr_timerec_get_removal_days(dte));
   htsmsg_add_u32(out, "priority",    dte->dte_pri);
   htsmsg_add_s32(out, "start",       dte->dte_start);
@@ -1379,18 +1397,18 @@ htsp_method_async(htsp_connection_t *htsp, htsmsg_t *in)
   /* Send all autorecs */
   TAILQ_FOREACH(dae, &autorec_entries, dae_link)
     if (!dvr_autorec_entry_verify(dae, htsp->htsp_granted_access, 1))
-      htsp_send_message(htsp, htsp_build_autorecentry(dae, "autorecEntryAdd"), NULL);
+      htsp_send_message(htsp, htsp_build_autorecentry(htsp, dae, "autorecEntryAdd"), NULL);
 
   /* Send all timerecs */
   TAILQ_FOREACH(dte, &timerec_entries, dte_link)
     if (!dvr_timerec_entry_verify(dte, htsp->htsp_granted_access, 1))
-      htsp_send_message(htsp, htsp_build_timerecentry(dte, "timerecEntryAdd"), NULL);
+      htsp_send_message(htsp, htsp_build_timerecentry(htsp, dte, "timerecEntryAdd"), NULL);
 
   /* Send all DVR entries */
   LIST_FOREACH(de, &dvrentries, de_global_link)
     if (!dvr_entry_verify(de, htsp->htsp_granted_access, 1) &&
         htsp_user_access_channel(htsp, de->de_channel))
-      htsp_send_message(htsp, htsp_build_dvrentry(de, "dvrEntryAdd", htsp->htsp_language), NULL);
+      htsp_send_message(htsp, htsp_build_dvrentry(htsp, de, "dvrEntryAdd", htsp->htsp_language), NULL);
 
   /* Send EPG updates */
   if (epg) {
@@ -1705,9 +1723,9 @@ htsp_method_addDvrEntry(htsp_connection_t *htsp, htsmsg_t *in)
   if(htsmsg_get_u32(in, "priority", &priority))
     priority = DVR_PRIO_NORMAL;
   if(htsmsg_get_u32(in, "retention", &retention))
-    retention = 0;
+    retention = DVR_RET_DVRCONFIG;
   if(htsmsg_get_u32(in, "removal", &removal))
-    removal = 0;
+    removal = DVR_RET_DVRCONFIG;
   comment = htsmsg_get_str(in, "comment");
   if (!(lang  = htsmsg_get_str(in, "language")))
     lang = htsp->htsp_language;
@@ -1814,8 +1832,8 @@ htsp_method_updateDvrEntry(htsp_connection_t *htsp, htsmsg_t *in)
   stop        = htsmsg_get_s64_or_default(in, "stop",       0);
   start_extra = htsmsg_get_s64_or_default(in, "startExtra", 0);
   stop_extra  = htsmsg_get_s64_or_default(in, "stopExtra",  0);
-  retention   = htsmsg_get_u32_or_default(in, "retention",  0);
-  removal     = htsmsg_get_u32_or_default(in, "removal",    0);
+  retention   = htsmsg_get_u32_or_default(in, "retention",  DVR_RET_DVRCONFIG);
+  removal     = htsmsg_get_u32_or_default(in, "removal",    DVR_RET_DVRCONFIG);
   priority    = htsmsg_get_u32_or_default(in, "priority",   DVR_PRIO_NORMAL);
   title       = htsmsg_get_str(in, "title");
   subtitle    = htsmsg_get_str(in, "subtitle");
@@ -3390,7 +3408,7 @@ _htsp_dvr_entry_update(dvr_entry_t *de, const char *method, htsmsg_t *msg)
       if (!dvr_entry_verify(de, htsp->htsp_granted_access, 1) &&
           htsp_user_access_channel(htsp, de->de_channel)) {
         htsmsg_t *m = msg ? htsmsg_copy(msg)
-                        : htsp_build_dvrentry(de, method, htsp->htsp_language);
+                        : htsp_build_dvrentry(htsp, de, method, htsp->htsp_language);
         htsp_send_message(htsp, m, NULL);
       }
   }
@@ -3441,7 +3459,7 @@ _htsp_autorec_entry_update(dvr_autorec_entry_t *dae, const char *method, htsmsg_
       if ((dae->dae_channel == NULL || htsp_user_access_channel(htsp, dae->dae_channel)) &&
           !dvr_autorec_entry_verify(dae, htsp->htsp_granted_access, 1)) {
         htsmsg_t *m = msg ? htsmsg_copy(msg)
-                          : htsp_build_autorecentry(dae, method);
+                          : htsp_build_autorecentry(htsp, dae, method);
         htsp_send_message(htsp, m, NULL);
       }
     }
@@ -3494,7 +3512,7 @@ _htsp_timerec_entry_update(dvr_timerec_entry_t *dte, const char *method, htsmsg_
       if ((dte->dte_channel == NULL || htsp_user_access_channel(htsp, dte->dte_channel)) &&
           !dvr_timerec_entry_verify(dte, htsp->htsp_granted_access, 1)) {
         htsmsg_t *m = msg ? htsmsg_copy(msg)
-                          : htsp_build_timerecentry(dte, method);
+                          : htsp_build_timerecentry(htsp, dte, method);
         htsp_send_message(htsp, m, NULL);
       }
     }
index 2134dd81ad384d198c639243c1a19edb891a656a..d6888a005f00d3e6cb12855538b91bb84e816f0d 100644 (file)
@@ -336,7 +336,8 @@ page_einfo(http_connection_t *hc, const char *remain, void *opaque)
   if((http_arg_get(&hc->hc_req_args, "rec")) != NULL) {
     de = dvr_entry_create_by_event(1, NULL, e, 0, 0, hc->hc_username ?: NULL,
                                    hc->hc_representative ?: NULL, NULL,
-                                  DVR_PRIO_NORMAL, 0, 0, "simpleui");
+                                                          DVR_PRIO_NORMAL, DVR_RET_DVRCONFIG,
+                                                          DVR_RET_DVRCONFIG, "simpleui");
   } else if(de != NULL && (http_arg_get(&hc->hc_req_args, "cancel")) != NULL) {
     de = dvr_entry_cancel(de, 0);
   }