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;
char *de_channel_name;
gtimer_t de_timer;
+ gtimer_t de_watched_timer;
mtimer_t de_deferred_timer;
/**
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;
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);
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 )
{
.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",
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);
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;
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");
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);
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
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);
+}
/**
*
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);
+}
+
/**
*
*/
}
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) {
.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",
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)) {
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
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)
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);
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;
}
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);
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;
}
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)
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);
}
/**
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);