]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
dvr: Add option to automatically delete recording after playback.
authorE.Smith <31170571+azlm8t@users.noreply.github.com>
Wed, 17 Oct 2018 14:28:46 +0000 (15:28 +0100)
committerJaroslav Kysela <perex@perex.cz>
Mon, 19 Nov 2018 12:38:53 +0000 (13:38 +0100)
Previously when watching a programme, the user usually has to then
manually delete the programme to recover disk space, or wait for its
retention to expire.

So we now add an option to Config->Recording->DVR Profile. This allows
the user to select time after watching to automatically delete the
recording (unless it is marked as "keep forever"). Default is disabled
(do not delete after playback).

For example, if the user specifies "2 days" then we'd delete the
recording two days after playback, even if the retention period is "3
months".

"Playback" can vary based on client. Some clients read and cache the
entire file before starting playback, so the file would be marked as
watched immediately. Other clients only buffer a small amount, so the
file will be marked as watched near the end of the show.

src/dvr/dvr.h
src/dvr/dvr_config.c
src/dvr/dvr_db.c
src/htsp_server.c
src/satip/server.c
src/webui/webui.c
src/webui/webui.h

index 41763ef0fcfcdf9fa8d08c96c1071f6a10003873..2227721283f6e7e33eee17655b7dcdf4a94742f7 100644 (file)
@@ -80,6 +80,7 @@ typedef struct dvr_config {
   uint32_t dvr_rerecord_errors;
   uint32_t dvr_retention_days;
   uint32_t dvr_removal_days;
+  uint32_t dvr_removal_after_playback;
   uint32_t dvr_autorec_max_count;
   uint32_t dvr_autorec_max_sched_count;
   char *dvr_charset;
@@ -200,6 +201,7 @@ typedef struct dvr_entry {
   char *de_channel_name;
 
   gtimer_t de_timer;
+  gtimer_t de_watched_timer;
   mtimer_t de_deferred_timer;
 
   /**
@@ -211,6 +213,7 @@ typedef struct dvr_entry {
 
   int de_enabled;
   time_t de_create;             ///< Time entry was created
+  time_t de_watched;            ///< Time entry was last watched
   time_t de_start;
   time_t de_stop;
 
@@ -558,6 +561,9 @@ void dvr_entry_destroy_by_config(dvr_config_t *cfg, int delconf);
 int dvr_entry_set_state(dvr_entry_t *de, dvr_entry_sched_state_t state,
                         dvr_rs_state_t rec_state, int error_code);
 
+int dvr_entry_set_playcount(dvr_entry_t *de, uint32_t playcount);
+int dvr_entry_incr_playcount(dvr_entry_t *de);
+
 const char *dvr_entry_status(dvr_entry_t *de);
 
 const char *dvr_entry_schedstatus(dvr_entry_t *de);
index ec6a147cc3348fb9a8bbb51c11a8e5a96d73c77a..4ff590d523485288ca473f7ddd27816d5bce2225 100644 (file)
@@ -746,6 +746,47 @@ dvr_config_class_removal_list ( void *o, const char *lang )
   return strtab2htsmsg_u32(tab, 1, lang);
 }
 
+static htsmsg_t *
+dvr_config_class_remove_after_playback_list ( void *o, const char *lang )
+{
+  enum {
+    ONE_MINUTE = 60,
+    ONE_HOUR = ONE_MINUTE * 60,
+    ONE_DAY = ONE_HOUR * 24
+  };
+
+  /* We want a few "soon" options (other than immediately) since that
+   * gives the user time to restart the playback if they accidentally
+   * skipped to the end and marked it as watched, whereas immediately
+   * would immediately delete that recording (and we don't yet support
+   * undelete).
+   */
+  static const struct strtab_u32 tab[] = {
+    { N_("Never"),              0 },
+    { N_("Immediately"),        1 },
+    { N_("1 minute"),           ONE_MINUTE },
+    { N_("10 minutes"),         ONE_MINUTE * 10 },
+    { N_("30 minutes"),         ONE_MINUTE * 30 },
+    { N_("1 hour"),             ONE_HOUR },
+    { N_("2 hours"),            ONE_HOUR * 2 },
+    { N_("4 hour"),             ONE_HOUR * 4 },
+    { N_("8 hour"),             ONE_HOUR * 8 },
+    { N_("12 hours"),           ONE_HOUR * 12 },
+    { N_("1 day"),              ONE_DAY },
+    { N_("2 days"),             ONE_DAY * 2 },
+    { N_("3 days"),             ONE_DAY * 3 },
+    { N_("5 days"),             ONE_DAY * 5 },
+    { N_("1 week"),             ONE_DAY * 7 },
+    { N_("2 weeks"),            ONE_DAY * 14 },
+    { N_("3 weeks"),            ONE_DAY * 21 },
+    { N_("1 month"),            ONE_DAY * 31 }, /* Approximations based on RET_REM */
+    { N_("2 months"),           ONE_DAY * 62 },
+    { N_("3 months"),           ONE_DAY * 92 },
+  };
+  return strtab2htsmsg_u32(tab, 1, lang);
+}
+
+
 static htsmsg_t *
 dvr_config_class_retention_list ( void *o, const char *lang )
 {
@@ -930,6 +971,25 @@ const idclass_t dvr_config_class = {
       .opts     = PO_DOC_NLIST,
       .group    = 1,
     },
+    {
+      .type     = PT_U32,
+      .id       = "remove-after-playback",
+      .name     = N_("Automatically delete played recordings"),
+      .desc     = N_("Number of minutes after playback has finished "
+                     "before file should be automatically removed "
+                     "(unless its retention is 'forever'). "
+                     "Note that some clients may pre-cache playback "
+                     "which means the recording will be marked as "
+                     "played when the client has cached the data, "
+                     "which may be before the end of the programme is "
+                     "actually watched."
+                    ),
+      .off      = offsetof(dvr_config_t, dvr_removal_after_playback),
+      .def.u32  = 0,
+      .list     = dvr_config_class_remove_after_playback_list,
+      .opts     = PO_ADVANCED,
+      .group    = 1,
+    },
     {
       .type     = PT_U32,
       .id       = "pre-extra-time",
index e77df547ecc0bd8e56a4966abfb8fe5e205afe32..b4d2fee7007bde4495724f6e1d01e6ee884ce183 100644 (file)
@@ -60,6 +60,8 @@ static void dvr_timer_start_recording(void *aux);
 static void dvr_timer_stop_recording(void *aux);
 static int dvr_entry_rerecord(dvr_entry_t *de);
 static time_t dvr_entry_get_segment_stop_extra(dvr_entry_t *de);
+static void dvr_entry_watched_timer_arm(dvr_entry_t* de);
+static void dvr_entry_watched_timer_disarm(dvr_entry_t* de);
 
 static dvr_entry_t *_dvr_duplicate_event(dvr_entry_t *de);
 
@@ -1039,7 +1041,7 @@ dvr_entry_t *
 dvr_entry_create(const char *uuid, htsmsg_t *conf, int clone)
 {
   dvr_entry_t *de, *de2;
-  int64_t start, stop, create, now;
+  int64_t start, stop, create, watched, now;
   htsmsg_t *m;
   char ubuf[UUID_HEX_SIZE], ubuf2[UUID_HEX_SIZE];
   const char *s;
@@ -1089,6 +1091,11 @@ dvr_entry_create(const char *uuid, htsmsg_t *conf, int clone)
           create = now;
       de->de_create = create;
   }
+  if (!htsmsg_get_s64(conf, "watched", &watched)) {
+    de->de_watched = watched;
+  } else {
+    de->de_watched = 0;
+  }
 
   /* Extract episode info */
   s = htsmsg_get_str(conf, "episode");
@@ -1151,6 +1158,11 @@ dvr_entry_create(const char *uuid, htsmsg_t *conf, int clone)
 
   if (!clone)
     dvr_entry_set_timer(de);
+
+  /* Entry is marked for deletion, so set timer. */
+  if (de->de_watched)
+    dvr_entry_watched_timer_arm(de);
+
   htsp_dvr_entry_add(de);
   dvr_vfs_refresh_entry(de);
 
@@ -2142,6 +2154,7 @@ dvr_entry_destroy(dvr_entry_t *de, int delconf)
 
   gtimer_disarm(&de->de_timer);
   mtimer_disarm(&de->de_deferred_timer);
+  gtimer_disarm(&de->de_watched_timer);
 #if ENABLE_DBUS_1
   mtimer_arm_rel(&dvr_dbus_timer, dvr_dbus_timer_cb, NULL, sec2mono(2));
 #endif
@@ -2226,9 +2239,77 @@ static void
 dvr_timer_expire(void *aux)
 {
   dvr_entry_t *de = aux;
-  dvr_entry_destroy(de, 1);
+  tvhinfo(LS_DVR, "Watched timer expiring \"%s\"",
+          lang_str_get(de->de_title, NULL));
+  /* Force the watched timer to be reset. This ensures it
+   * cannot be re-triggered on a restart of tvheadend.
+   */
+  de->de_watched = 0;
+  dvr_entry_changed(de);
+  /* Allow re-record if recording errors */
+  dvr_entry_cancel_remove(de, 1);
+}
+
+static void
+dvr_entry_watched_timer_cb(void *aux)
+{
+  tvhtrace(LS_DVR, "Entry watched cb");
+  dvr_timer_expire(aux);
+}
+
+static void
+dvr_entry_watched_timer_arm(dvr_entry_t* de)
+{
+  if (!de || !de->de_watched ||
+      dvr_entry_get_removal_days(de) == DVR_RET_REM_FOREVER ||
+      !de->de_config || !de->de_config->dvr_removal_after_playback)
+    return;
+
+  char t1buf[32];
+  const time_t now = gclk();
+  char ubuf[UUID_HEX_SIZE];
+  time_t when = de->de_watched + de->de_config->dvr_removal_after_playback;
+
+  /* We could just call the cb ourselves, but we'll set a timer if
+   * the event is in the past so that we are always async.
+   * This avoids any potential problems with caller trying to
+   * reference our de after it is destroyed.
+   */
+  if (when < now)
+    when = now;
+  tvhinfo(LS_DVR, "Arming watched timer to delete \"%s\" %s @ %s (now+%"PRId64")",
+          lang_str_get(de->de_title, NULL),
+          idnode_uuid_as_str(&de->de_id, ubuf),
+          gmtime2local(when, t1buf, sizeof t1buf),
+          (int64_t)when-now);
+  gtimer_arm_absn(&de->de_watched_timer, dvr_entry_watched_timer_cb, de, when);
+}
+
+static void
+dvr_entry_watched_timer_disarm(dvr_entry_t* de)
+{
+  if (de) {
+    dvr_entry_trace(de, "watched timer - disarm");
+    de->de_watched = 0;
+    gtimer_disarm(&de->de_watched_timer);
+  }
 }
 
+/// Check if user wants played entries to be automatically deleted
+/// If so, arm the timers.
+static void
+dvr_entry_watched_set_watched(dvr_entry_t* de)
+{
+  if (!de)
+    return;
+
+  /* User wants entry deleted after it is played, so mark earliest
+   * "deleted" timestamp and arm.
+   */
+  de->de_watched = gclk();
+  if (de->de_config && de->de_config->dvr_removal_after_playback)
+    dvr_entry_watched_timer_arm(de);
+}
 
 /**
  *
@@ -2292,6 +2373,25 @@ static char *dvr_updated_str(char *buf, size_t buflen, int flags)
   return buf;
 }
 
+int
+dvr_entry_set_playcount(dvr_entry_t *de, uint32_t playcount)
+{
+  if (de->de_playcount == playcount)
+    return 0;
+  de->de_playcount = playcount;
+  if (de->de_playcount)
+    dvr_entry_watched_set_watched(de);
+  else                          /* Not watched, so disarm timer */
+    dvr_entry_watched_timer_disarm(de);
+  return 1;
+}
+
+int
+dvr_entry_incr_playcount(dvr_entry_t *de)
+{
+  return dvr_entry_set_playcount(de, de->de_playcount + 1);
+}
+
 /**
  *
  */
@@ -2342,7 +2442,7 @@ static dvr_entry_t *_dvr_entry_update
     }
     if (de->de_sched_state == DVR_RECORDING || de->de_sched_state == DVR_COMPLETED) {
       if (playcount >= 0 && playcount != de->de_playcount) {
-        de->de_playcount = playcount;
+        dvr_entry_set_playcount(de, playcount);
         save |= DVR_UPDATED_PLAYCOUNT;
       }
       if (playposition >= 0 && playposition != de->de_playposition) {
@@ -3945,6 +4045,14 @@ const idclass_t dvr_entry_class = {
       .off      = offsetof(dvr_entry_t, de_create),
       .opts     = PO_HIDDEN | PO_RDONLY | PO_NOUI,
     },
+    {
+      .type     = PT_TIME,
+      .id       = "watched",
+      .name     = N_("Time the entry was last watched"),
+      .desc     = N_("Time the entry was last watched."),
+      .off      = offsetof(dvr_entry_t, de_watched),
+      .opts     = PO_RDONLY,
+    },
     {
       .type     = PT_TIME,
       .id       = "start",
index aa5f02b39f9c88bb30a31e914562ae99b53ef7fb..9c86805cdf27ab81d6fe39f676179308c37d06d0 100644 (file)
@@ -2913,7 +2913,7 @@ htsp_method_file_close(htsp_connection_t *htsp, htsmsg_t *in)
     int save = 0;
     /* Only allow incrementing playcount on file close, the rest can be done with "updateDvrEntry" */
     if (htsp->htsp_version < 27 || htsmsg_get_u32_or_default(in, "playcount", HTSP_DVR_PLAYCOUNT_INCR) == HTSP_DVR_PLAYCOUNT_INCR) {
-      de->de_playcount++;
+      dvr_entry_incr_playcount(de);
       save = 1;
     }
     if(htsp->htsp_version >= 27 && !htsmsg_get_u32(in, "playposition", &u32)) {
index 96a97c2ee1f79a0950664afe864ba4e473c4fbbf..c5003df4c32ee958bba7ebff1e79643dfe86c52d 100644 (file)
@@ -257,7 +257,7 @@ satip_server_satip_m3u(http_connection_t *hc)
   if (hts_settings_buildpath(path, sizeof(path), "satip.m3u"))
     return HTTP_STATUS_SERVICE;
 
-  return http_serve_file(hc, path, 0, MIME_M3U, NULL, NULL, NULL);
+  return http_serve_file(hc, path, 0, MIME_M3U, NULL, NULL, NULL, NULL);
 }
 
 int
index 942a104f9f2c2049c4adb885ee352211e6014b57..6999cb4c2911f31259e6d923a2ed85686f9fe171 100644 (file)
@@ -1620,17 +1620,19 @@ http_serve_file(http_connection_t *hc, const char *fname,
                 int fconv, const char *content,
                 int (*preop)(http_connection_t *hc, off_t file_start,
                              size_t content_len, void *opaque),
+                int (*postop)(http_connection_t *hc, off_t file_start,
+                              size_t content_len, off_t file_size, void *opaque),
                 void (*stats)(http_connection_t *hc, size_t len, void *opaque),
                 void *opaque)
 {
-  int fd, ret;
+  int fd, ret, close_ret;
   struct stat st;
   const char *range;
   char *basename;
   char *str, *str0;
   char range_buf[255];
   char *disposition = NULL;
-  off_t content_len, chunk;
+  off_t content_len, total_len, chunk;
   intmax_t file_start, file_end;
   htsbuf_queue_t q;
 #if defined(PLATFORM_LINUX)
@@ -1695,7 +1697,7 @@ http_serve_file(http_connection_t *hc, const char *fname,
     return HTTP_STATUS_OK;
   }
 
-  content_len = file_end - file_start+1;
+  content_len = total_len = file_end - file_start+1;
   
   sprintf(range_buf, "bytes %jd-%jd/%jd",
           file_start, file_end, (intmax_t)st.st_size);
@@ -1754,8 +1756,17 @@ http_serve_file(http_connection_t *hc, const char *fname,
         stats(hc, r, opaque);
     }
   }
+
   http_send_end(hc);
-  close(fd);
+  close_ret = close(fd);
+  if (close_ret != 0)
+    ret = close_ret;
+
+  /* We do postop _after_ the close since close will block until the
+   * buffers have been received by the client.
+   */
+  if (ret == 0 && postop)
+    ret = postop(hc, file_start, total_len, st.st_size, opaque);
 
   return ret;
 }
@@ -1777,7 +1788,6 @@ page_dvrfile_preop(http_connection_t *hc, off_t file_start,
                    size_t content_len, void *opaque)
 {
   page_dvrfile_priv_t *priv = opaque;
-  dvr_entry_t *de;
 
   pthread_mutex_lock(&global_lock);
   priv->tcp_id = http_stream_preop(hc);
@@ -1792,19 +1802,41 @@ page_dvrfile_preop(http_connection_t *hc, off_t file_start,
       priv->tcp_id = NULL;
     }
   }
-  /* Play count + 1 when write access */
-  if (!hc->hc_no_output && file_start <= 0) {
+  pthread_mutex_unlock(&global_lock);
+  if (priv->tcp_id == NULL)
+    return HTTP_STATUS_NOT_ALLOWED;
+  return 0;
+}
+
+static int
+page_dvrfile_postop(http_connection_t *hc,
+                    off_t file_start,
+                    size_t content_len,
+                    off_t file_size,
+                    void *opaque)
+{
+  dvr_entry_t *de;
+  const page_dvrfile_priv_t *priv = opaque;
+  /* We are fully played when we send the last segment */
+  const int is_fully_played = file_start + content_len >= file_size;
+  tvhdebug(LS_HTTP, "page_dvrfile_postop: file start=%ld content len=%ld file size=%ld done=%d",
+           (long)file_start, (long)content_len, (long)file_size, is_fully_played);
+
+  if (!is_fully_played)
+    return 0;
+
+  pthread_mutex_lock(&global_lock);
+  /* Play count + 1 when not doing HEAD */
+  if (!hc->hc_no_output) {
     de = dvr_entry_find_by_uuid(priv->uuid);
     if (de == NULL)
       de = dvr_entry_find_by_id(atoi(priv->uuid));
     if (de && !dvr_entry_verify(de, hc->hc_access, 0)) {
-      de->de_playcount = de->de_playcount + 1;
+      dvr_entry_incr_playcount(de);
       dvr_entry_changed(de);
     }
   }
   pthread_mutex_unlock(&global_lock);
-  if (priv->tcp_id == NULL)
-    return HTTP_STATUS_NOT_ALLOWED;
   return 0;
 }
 
@@ -1859,7 +1891,7 @@ page_dvrfile(http_connection_t *hc, const char *remain, void *opaque)
   pthread_mutex_unlock(&global_lock);
 
   ret = http_serve_file(hc, priv.fname, 1, priv.content,
-                        page_dvrfile_preop, page_dvrfile_stats, &priv);
+                        page_dvrfile_preop, page_dvrfile_postop, page_dvrfile_stats, &priv);
 
   pthread_mutex_lock(&global_lock);
   if (priv.sub)
@@ -1906,7 +1938,7 @@ page_imagecache(http_connection_t *hc, const char *remain, void *opaque)
   if (r)
     return HTTP_STATUS_NOT_FOUND;
 
-  return http_serve_file(hc, fname, 0, NULL, NULL, NULL, NULL);
+  return http_serve_file(hc, fname, 0, NULL, NULL, NULL, NULL, NULL);
 }
 
 /**
index 8706d766c365915453e6c07e5025e543f94add21..344caeae02b8dc493508f3255692de5c093170a2 100644 (file)
@@ -42,6 +42,8 @@ http_serve_file(http_connection_t *hc, const char *fname,
                 int fconv, const char *content,
                 int (*preop)(http_connection_t *hc, off_t file_start,
                              size_t content_len, void *opaque),
+                int (*postop)(http_connection_t *hc, off_t file_start,
+                              size_t content_len, off_t file_size, void *opaque),
                 void (*stats)(http_connection_t *hc, size_t len, void *opaque),
                 void *opaque);