From 4cdf9c9f4b8df4ae9d10c6e683ee85cf3527a8b3 Mon Sep 17 00:00:00 2001 From: Glenn-1990 Date: Tue, 4 Oct 2016 22:03:05 +0200 Subject: [PATCH] Implemented "moved recordings" in order to improve the user experience --- src/api/api_dvr.c | 42 +++++++++++++- src/dvr/dvr.h | 5 +- src/dvr/dvr_autorec.c | 2 +- src/dvr/dvr_config.c | 37 +----------- src/dvr/dvr_db.c | 91 ++++++++++++++--------------- src/dvr/dvr_vfsmgr.c | 8 +-- src/htsp_server.c | 2 +- src/webui/static/app/dvr.js | 110 ++++++++++++++++++++++++++++++++++-- 8 files changed, 192 insertions(+), 105 deletions(-) diff --git a/src/api/api_dvr.c b/src/api/api_dvr.c index 574c3c60f..510572eca 100644 --- a/src/api/api_dvr.c +++ b/src/api/api_dvr.c @@ -69,10 +69,16 @@ 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, 0) != -1 || entry->de_file_removed) && + dvr_get_filesize(entry, 0) != -1 && !entry->de_file_removed && entry->de_data_errors < DVR_MAX_DATA_ERRORS; } +static int is_dvr_entry_removed(dvr_entry_t *entry) +{ + dvr_entry_sched_state_t state = entry->de_sched_state; + return ((state == DVR_COMPLETED || state == DVR_MISSED_TIME) && entry->de_file_removed); +} + static int is_dvr_entry_upcoming(dvr_entry_t *entry) { dvr_entry_sched_state_t state = entry->de_sched_state; @@ -85,6 +91,8 @@ static int is_dvr_entry_failed(dvr_entry_t *entry) return 0; if (is_dvr_entry_upcoming(entry)) return 0; + if (is_dvr_entry_removed(entry)) + return 0; return 1; } @@ -131,6 +139,17 @@ api_dvr_entry_grid_failed idnode_set_add(ins, (idnode_t*)de, &conf->filter, perm->aa_lang_ui); } +static void +api_dvr_entry_grid_removed + ( 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_removed(de)) + idnode_set_add(ins, (idnode_t*)de, &conf->filter, perm->aa_lang_ui); +} + static int api_dvr_entry_create ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) @@ -325,6 +344,21 @@ api_dvr_entry_cancel return api_idnode_handler(perm, args, resp, api_dvr_cancel, "cancel", 0); } +static void +api_dvr_remove(access_t *perm, idnode_t *self) +{ + dvr_entry_t *de = (dvr_entry_t *)self; + if (de->de_sched_state != DVR_SCHEDULED && de->de_sched_state != DVR_NOSTATE) + dvr_entry_cancel_remove(de, 0); +} + +static int +api_dvr_entry_remove + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + return api_idnode_handler(perm, args, resp, api_dvr_remove, "remove", 0); +} + static void api_dvr_move_finished(access_t *perm, idnode_t *self) { @@ -508,13 +542,15 @@ void api_dvr_init ( void ) { "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/grid_removed", ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid_removed }, { "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/rerecord/toggle", ACCESS_RECORDER, api_dvr_entry_rerecord_toggle, NULL }, { "dvr/entry/rerecord/deny", ACCESS_RECORDER, api_dvr_entry_rerecord_deny, NULL }, { "dvr/entry/rerecord/allow", ACCESS_RECORDER, api_dvr_entry_rerecord_allow, NULL }, - { "dvr/entry/stop", ACCESS_RECORDER, api_dvr_entry_stop, NULL }, - { "dvr/entry/cancel", ACCESS_RECORDER, api_dvr_entry_cancel, NULL }, + { "dvr/entry/stop", ACCESS_RECORDER, api_dvr_entry_stop, NULL }, /* Stop active recording gracefully */ + { "dvr/entry/cancel", ACCESS_RECORDER, api_dvr_entry_cancel, NULL }, /* Cancel scheduled or active recording */ + { "dvr/entry/remove", ACCESS_RECORDER, api_dvr_entry_remove, NULL }, /* Remove recorded files from storage */ { "dvr/entry/filemoved", ACCESS_ADMIN, api_dvr_entry_file_moved, NULL }, { "dvr/entry/move/finished", ACCESS_RECORDER, api_dvr_entry_move_finished, NULL }, { "dvr/entry/move/failed", ACCESS_RECORDER, api_dvr_entry_move_failed, NULL }, diff --git a/src/dvr/dvr.h b/src/dvr/dvr.h index 3e6b1e3d1..054dcd129 100644 --- a/src/dvr/dvr.h +++ b/src/dvr/dvr.h @@ -54,7 +54,6 @@ typedef struct dvr_config { int dvr_clone; uint32_t dvr_rerecord_errors; uint32_t dvr_retention_days; - uint32_t dvr_retention_minimal; uint32_t dvr_removal_days; uint32_t dvr_autorec_max_count; uint32_t dvr_autorec_max_sched_count; @@ -588,9 +587,9 @@ void dvr_entry_dec_ref(dvr_entry_t *de); int dvr_entry_delete(dvr_entry_t *de); -void dvr_entry_cancel_delete(dvr_entry_t *de, int rerecord, int forcedestroy); +void dvr_entry_cancel_delete(dvr_entry_t *de, int rerecord); -void dvr_entry_trydestroy(dvr_entry_t *de); +void dvr_entry_cancel_remove(dvr_entry_t *de, int rerecord); int dvr_entry_file_moved(const char *src, const char *dst); diff --git a/src/dvr/dvr_autorec.c b/src/dvr/dvr_autorec.c index 58889e303..5258f87fb 100644 --- a/src/dvr/dvr_autorec.c +++ b/src/dvr/dvr_autorec.c @@ -125,7 +125,7 @@ dvr_autorec_completed(dvr_autorec_entry_t *dae, int error_code) if (de_prev) { tvhinfo(LS_DVR, "autorec %s removing recordings %s (allowed count %u total %u)", dae->dae_name, idnode_uuid_as_str(&de_prev->de_id, ubuf), max_count, total); - dvr_entry_cancel_delete(de_prev, 0, 0); + dvr_entry_cancel_remove(de_prev, 0); } } } diff --git a/src/dvr/dvr_config.c b/src/dvr/dvr_config.c index 643710450..32f2fca09 100644 --- a/src/dvr/dvr_config.c +++ b/src/dvr/dvr_config.c @@ -178,7 +178,6 @@ 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 = DVR_RET_ONREMOVE; - cfg->dvr_retention_minimal = DVR_RET_MIN_DISABLED; cfg->dvr_removal_days = DVR_RET_REM_FOREVER; cfg->dvr_clone = 1; cfg->dvr_tag_files = 1; @@ -762,29 +761,6 @@ dvr_config_class_retention_list ( void *o, const char *lang ) return strtab2htsmsg_u32(tab, 1, lang); } -static htsmsg_t * -dvr_config_class_retention_list_minimal ( void *o, const char *lang ) -{ - static const struct strtab_u32 tab[] = { - { N_("Disabled"), DVR_RET_MIN_DISABLED }, - { N_("1 day"), DVR_RET_REM_1DAY }, - { N_("3 days"), DVR_RET_REM_3DAY }, - { N_("5 days"), DVR_RET_REM_5DAY }, - { N_("1 week"), DVR_RET_REM_1WEEK }, - { N_("2 weeks"), DVR_RET_REM_2WEEK }, - { N_("3 weeks"), DVR_RET_REM_3WEEK }, - { N_("1 month"), DVR_RET_REM_1MONTH }, - { N_("2 months"), DVR_RET_REM_2MONTH }, - { N_("3 months"), DVR_RET_REM_3MONTH }, - { N_("6 months"), DVR_RET_REM_6MONTH }, - { N_("1 year"), DVR_RET_REM_1YEAR }, - { N_("2 years"), DVR_RET_REM_2YEARS }, - { N_("3 years"), DVR_RET_REM_3YEARS }, - { N_("Forever"), DVR_RET_REM_FOREVER }, - }; - return strtab2htsmsg_u32(tab, 1, lang); -} - static htsmsg_t * dvr_config_class_extra_list(void *o, const char *lang) { @@ -924,24 +900,13 @@ const idclass_t dvr_config_class = { .type = PT_U32, .id = "retention-days", .name = N_("DVR log retention period"), - .desc = N_("Number of days to retain information about recordings. Once this period is exceeded, duplicate detection will not be possible for this recording."), + .desc = N_("Number of days to retain information about recordings. Once this period is exceeded, duplicate detection will not be possible anymore."), .off = offsetof(dvr_config_t, dvr_retention_days), .def.u32 = DVR_RET_ONREMOVE, .list = dvr_config_class_retention_list, .opts = PO_EXPERT | PO_DOC_NLIST, .group = 1, }, - { - .type = PT_U32, - .id = "retention-minimal", - .name = N_("Minimal log retention period"), - .desc = N_("Minimal number of days to retain information from recordings that where deleted manually. Once this period is exceeded, duplicate detection will not be possible for this recording."), - .off = offsetof(dvr_config_t, dvr_retention_minimal), - .def.u32 = DVR_RET_MIN_DISABLED, - .list = dvr_config_class_retention_list_minimal, - .opts = PO_EXPERT | PO_DOC_NLIST, - .group = 1, - }, { .type = PT_U32, .id = "removal-days", diff --git a/src/dvr/dvr_db.c b/src/dvr/dvr_db.c index 7414a4e6f..ba752410e 100644 --- a/src/dvr/dvr_db.c +++ b/src/dvr/dvr_db.c @@ -494,8 +494,10 @@ dvr_entry_retention_timer(dvr_entry_t *de) dvr_entry_deferred_destroy(de); // also remove database entry return; } - if (save) + if (save) { idnode_changed(&de->de_id); + htsp_dvr_entry_update(de); + } } if (retention < DVR_RET_ONREMOVE) { @@ -1082,12 +1084,12 @@ dvr_entry_rerecord(dvr_entry_t *de) if (fsize1 / 5 < fsize2 / 6) { goto not_so_good; } else { - dvr_entry_cancel_delete(de2, 1, 1); + dvr_entry_cancel_delete(de2, 1); } } else if (de->de_sched_state == DVR_COMPLETED) { if(dvr_get_filesize(de, 0) == -1) { delete_me: - dvr_entry_cancel_delete(de, 0, 1); + dvr_entry_cancel_delete(de, 0); dvr_entry_rerecord(de2); return 1; } @@ -2062,7 +2064,7 @@ dvr_timer_start_recording(void *aux) // if duplicate, then delete it now, don't record! if (_dvr_duplicate_event(de)) { - dvr_entry_cancel_delete(de, 1, 1); + dvr_entry_cancel_delete(de, 1); return; } @@ -2153,7 +2155,7 @@ static void dvr_entry_class_delete(idnode_t *self) { dvr_entry_t *de = (dvr_entry_t *)self; - dvr_entry_cancel_delete(de, 0, 0); + dvr_entry_cancel_delete(de, 0); } static int @@ -3507,7 +3509,7 @@ dvr_entry_set_rerecord(dvr_entry_t *de, int cmd) if (cmd == 0 && !de->de_dont_rerecord) { de->de_dont_rerecord = 1; if (de->de_child) - dvr_entry_cancel_delete(de->de_child, 0, 1); + dvr_entry_cancel_delete(de->de_child, 0); } else { de->de_dont_rerecord = 0; dvr_entry_rerecord(de); @@ -3540,7 +3542,7 @@ dvr_entry_stop(dvr_entry_t *de) } /** - * + * Cancels an upcoming or active recording */ dvr_entry_t * dvr_entry_cancel(dvr_entry_t *de, int rerecord) @@ -3551,10 +3553,11 @@ dvr_entry_cancel(dvr_entry_t *de, int rerecord) case DVR_RECORDING: dvr_stop_recording(de, SM_CODE_ABORTED, 1, 0); break; - - case DVR_SCHEDULED: + /* Cancel is not valid for finished recordings */ case DVR_COMPLETED: case DVR_MISSED_TIME: + return de; + case DVR_SCHEDULED: case DVR_NOSTATE: dvr_entry_destroy(de, 1); de = NULL; @@ -3575,27 +3578,31 @@ dvr_entry_cancel(dvr_entry_t *de, int rerecord) } /** - * + * Called by 'dvr_entry_cancel_remove' and 'dvr_entry_cancel_delete' + * delete = 0 -> remove finished and active recordings (visible as removed) + * delete = 1 -> delete finished and active recordings (not visible anymore) */ -void -dvr_entry_cancel_delete(dvr_entry_t *de, int rerecord, int forcedestroy) +static void +dvr_entry_cancel_delete_remove(dvr_entry_t *de, int rerecord, int _delete) { dvr_entry_t *parent = de->de_parent; dvr_autorec_entry_t *dae = de->de_autorec; + int save; switch(de->de_sched_state) { case DVR_RECORDING: dvr_stop_recording(de, SM_CODE_ABORTED, 1, 0); + case DVR_MISSED_TIME: case DVR_COMPLETED: - dvr_entry_delete(de); - if (forcedestroy) - dvr_entry_destroy(de, 1); - else - dvr_entry_trydestroy(de); + save = dvr_entry_delete(de); /* Remove files */ + if (_delete || dvr_entry_get_retention_days(de) == DVR_RET_ONREMOVE) + dvr_entry_destroy(de, 1); /* Delete database */ + else if (save) { + idnode_changed(&de->de_id); + htsp_dvr_entry_update(de); + } break; - case DVR_SCHEDULED: - case DVR_MISSED_TIME: case DVR_NOSTATE: dvr_entry_destroy(de, 1); break; @@ -3617,39 +3624,27 @@ dvr_entry_cancel_delete(dvr_entry_t *de, int rerecord, int forcedestroy) } /** - * Destroy db entry if possible. - * The deletion of the db entry can be prevented by the minimal retention setting. - * Prevention is needed in order to keep duplicate detection happy. + * Upcoming recording -> cancel + * Active recording -> cancel + remove + * Finished recordings -> remove + * The latter 2 will be visible as "removed recording" */ void -dvr_entry_trydestroy(dvr_entry_t *de) +dvr_entry_cancel_remove(dvr_entry_t *de, int rerecord) { - char ubuf[UUID_HEX_SIZE]; - uint32_t minretention, removal; - - if (!de->de_config || de->de_config->dvr_retention_minimal == DVR_RET_MIN_DISABLED) - dvr_entry_destroy(de, 1); - else { - minretention = time_t_out_of_range((int64_t)de->de_stop + de->de_config->dvr_retention_minimal * (int64_t)86400); - if (minretention < gclk()) /* Minimal retention period expired -> deleting db entry allowed */ - dvr_entry_destroy(de, 1); - else { - removal = (gclk() - (int64_t)de->de_stop)/(int64_t)86400; - - de->de_dont_reschedule = 1; - de->de_removal = removal > DVR_RET_REM_DVRCONFIG ? - removal : DVR_RET_REM_1DAY; /* Update removal to the current value */ - de->de_retention = de->de_config->dvr_retention_minimal; /* Update the retention to the minimum allowed value */ - idnode_changed(&de->de_id); - dvr_entry_retention_timer(de); /* Rearm timer as retention was changed */ + dvr_entry_cancel_delete_remove(de, rerecord, 0); +} - tvhinfo(LS_DVR, "delete entry %s not allowed \"%s\" on \"%s\", current retention period %"PRIu32", " - "minimal retention period %"PRIu32"", - idnode_uuid_as_str(&de->de_id, ubuf), - lang_str_get(de->de_title, NULL), DVR_CH_NAME(de), - removal, de->de_config->dvr_retention_minimal); - } - } +/** + * Upcoming recording -> cancel + * Active recording -> abort + delete + * Finished recordings -> delete + * The latter 2 will NOT be visible as "removed recording" + */ +void +dvr_entry_cancel_delete(dvr_entry_t *de, int rerecord) +{ + dvr_entry_cancel_delete_remove(de, rerecord, 1); } /** diff --git a/src/dvr/dvr_vfsmgr.c b/src/dvr/dvr_vfsmgr.c index 60b8ebc5c..a5ae2d7ee 100644 --- a/src/dvr/dvr_vfsmgr.c +++ b/src/dvr/dvr_vfsmgr.c @@ -262,13 +262,7 @@ dvr_disk_space_cleanup(dvr_config_t *cfg) lang_str_get(oldest->de_title, NULL), tbuf, TOMIB(fileSize)); dvr_disk_space_config_lastdelete = mclk(); - if (dvr_entry_get_retention_days(oldest) == DVR_RET_ONREMOVE) { - dvr_entry_delete(oldest); // delete actual file - dvr_entry_destroy(oldest, 1); // also delete database entry - } else { - if (dvr_entry_delete(oldest)) // delete actual file - idnode_changed(&oldest->de_id); - } + dvr_entry_cancel_remove(oldest, 0); /* Remove stored files and mark as "removed" */ } else { tvhwarn(LS_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); diff --git a/src/htsp_server.c b/src/htsp_server.c index 43a27e04e..dc0095622 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -1998,7 +1998,7 @@ htsp_method_deleteDvrEntry(htsp_connection_t *htsp, htsmsg_t *in) if (de == NULL) return out; - dvr_entry_cancel_delete(de, 0, 0); + dvr_entry_cancel_remove(de, 0); return htsp_success(); } diff --git a/src/webui/static/app/dvr.js b/src/webui/static/app/dvr.js index 1da74d23f..941b3e3da 100644 --- a/src/webui/static/app/dvr.js +++ b/src/webui/static/app/dvr.js @@ -407,6 +407,36 @@ tvheadend.dvr_finished = function(panel, index) { } } }; + + var removeButton = { + name: 'remove', + builder: function() { + return new Ext.Toolbar.Button({ + tooltip: _('Remove the selected recording from storage'), + iconCls: 'remove', + text: _('Remove'), + disabled: true + }); + }, + callback: function(conf, e, store, select) { + var r = select.getSelections(); + if (r && r.length > 0) { + var uuids = []; + for (var i = 0; i < r.length; i++) + uuids.push(r[i].id); + tvheadend.AjaxConfirm({ + url: 'api/dvr/entry/remove', + params: { + uuid: Ext.encode(uuids) + }, + success: function(d) { + store.reload(); + }, + question: _('Do you really want to remove the selected recordings from storage?') + }); + } + } + }; function selected(s, abuttons) { var r = s.getSelections(); @@ -414,6 +444,7 @@ tvheadend.dvr_finished = function(panel, index) { abuttons.download.setDisabled(!b); abuttons.rerecord.setDisabled(!b); abuttons.move.setDisabled(!b); + abuttons.remove.setDisabled(!b); } tvheadend.idnode_grid(panel, { @@ -425,9 +456,7 @@ tvheadend.dvr_finished = function(panel, index) { iconCls: 'finishedRec', tabIndex: index, edit: { params: { list: tvheadend.admin ? "owner,retention,removal,comment" : "comment" } }, - del: true, - delquestion: _('Do you really want to delete the selected recordings?') + '

' + - _('The associated file will be removed from storage.'), + del: false, list: 'disp_title,disp_subtitle,episode,start_real,stop_real,' + 'duration,filesize,channelname,owner,creator,' + 'config_name,sched_status,errors,data_errors,url,comment', @@ -454,7 +483,7 @@ tvheadend.dvr_finished = function(panel, index) { return tvheadend.playLink('play/dvrfile/' + r.id, title); } }], - tbar: [downloadButton, rerecordButton, moveButton], + tbar: [removeButton, downloadButton, rerecordButton, moveButton], selected: selected }); @@ -598,6 +627,74 @@ tvheadend.dvr_failed = function(panel, index) { return panel; }; +/** + * + */ +tvheadend.dvr_removed = function(panel, index) { + + var actions = tvheadend.dvrRowActions(); + + var rerecordButton = { + name: 'rerecord', + builder: function() { + return new Ext.Toolbar.Button({ + tooltip: _('Toggle re-record functionality'), + iconCls: 'rerecord', + text: _('Re-record'), + disabled: true + }); + }, + callback: function(conf, e, store, select) { + var r = select.getSelections(); + if (r && r.length > 0) { + var uuids = []; + for (var i = 0; i < r.length; i++) + uuids.push(r[i].id); + tvheadend.Ajax({ + url: 'api/dvr/entry/rerecord/toggle', + params: { + uuid: Ext.encode(uuids) + }, + success: function(d) { + store.reload(); + } + }); + } + } + }; + + function selected(s, abuttons) { + var r = s.getSelections(); + abuttons.rerecord.setDisabled(r.length <= 0); + } + + tvheadend.idnode_grid(panel, { + url: 'api/dvr/entry', + gridURL: 'api/dvr/entry/grid_removed', + readonly: true, + titleS: _('Removed Recording'), + titleP: _('Removed Recordings'), + iconCls: 'remove', + tabIndex: index, + uilevel: 'expert', + edit: { params: { list: tvheadend.admin ? "retention,owner,comment" : "retention,comment" } }, + del: true, + list: 'disp_title,disp_subtitle,episode,start_real,stop_real,' + + 'duration,channelname,owner,creator,config_name,' + + 'sched_status,errors,data_errors,url,comment', + sort: { + field: 'start_real', + direction: 'ASC' + }, + plugins: [actions], + lcol: [actions], + tbar: [rerecordButton], + selected: selected + }); + + return panel; +}; + /** * */ @@ -751,7 +848,8 @@ tvheadend.dvr = function(panel, index) { tvheadend.dvr_upcoming(p, 0); tvheadend.dvr_finished(p, 1); tvheadend.dvr_failed(p, 2); - tvheadend.autorec_editor(p, 3); - tvheadend.timerec_editor(p, 4); + tvheadend.dvr_removed(p, 3); + tvheadend.autorec_editor(p, 4); + tvheadend.timerec_editor(p, 5); return p; } -- 2.47.3