From e5c35a7b4d3d4787c4f0b6cb146db98eb09fe76d Mon Sep 17 00:00:00 2001 From: Moe Date: Fri, 19 Aug 2022 17:38:24 -0700 Subject: [PATCH 1/6] Video Recording Archive Controls --- languages/en_CA.json | 5 ++ libs/cron/worker.js | 4 +- libs/database/utils.js | 2 +- libs/sql.js | 15 ++++++ libs/user/utils.js | 8 +-- libs/video/utils.js | 23 ++++++++ libs/webServerPaths.js | 8 +++ web/assets/css/bs5.videosTable.css | 24 +++++++++ web/assets/js/bs5.videoPlayer.js | 3 +- web/assets/js/bs5.videos.js | 77 +++++++++++++++++++++++++++ web/assets/js/bs5.videosTable.js | 47 +++++++++++++--- web/pages/blocks/home/videosTable.ejs | 5 +- 12 files changed, 206 insertions(+), 15 deletions(-) diff --git a/languages/en_CA.json b/languages/en_CA.json index 5bed1d6b..e165d282 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -331,6 +331,7 @@ "Start": "Start", "End": "End", "Archive": "Archive", + "Unarchive": "Unarchive", "Email Details": "Email Details", "Delete Matches": "Delete Matches", "Delete selected": "Delete selected", @@ -495,15 +496,19 @@ "Delete Filter": "Delete Filter", "confirmDeleteFilter": "Do you want to delete this filter? You cannot recover it.", "Fix Video": "Fix Video", + "Archived": "Archived", + "Archive Videos": "Archive Videos", "Compress Videos": "Compress Videos", "Compress Completed Videos": "Compress Completed Videos", "compressCompletedVideosFieldText": "Automatically compress videos to WebM once recorded. Doing this requires a powerful CPU or you must allow a large amount of time to allow compression. The rate in which videos are added to the database can't be faster than the rate for compression.", "FixVideoMsg": "Do you want to fix this video? This will create a new file and overwrite the old one. You cannot undo this action.", "DeleteVideoMsg": "Do you want to delete this video? You cannot recover it.", "CompressVideoMsg": "Do you want to compress this video? The original will be moved to your FileBin. Videos that are already completed compressing will be skipped if already queued.", + "ArchiveVideoMsg": "Do you want to Archive this video? This video won't be deleted by the automated cleanup processes.", "DeleteThisMsg": "Do you want to delete this? You cannot recover it.", "DeleteTheseMsg": "Do you want to delete these? You cannot recover them.", "CompressTheseMsg": "Do you want to compress these? The originals will be moved to your FileBin. Videos that are already WebM will be skipped. Videos that are already completed compressing will be skipped if ordered to compress again.", + "ArchiveTheseMsg": "Do you want to Archive these? They won't be deleted by the automated cleanup processes.", "dropBoxSuccess": "Success! Files saved to your Dropbox.", "API Key Deleted": "API Key Deleted", "APIKeyDeletedText": "Key has been deleted. It will no longer work.", diff --git a/libs/cron/worker.js b/libs/cron/worker.js index f3f8804c..683dce70 100644 --- a/libs/cron/worker.js +++ b/libs/cron/worker.js @@ -142,7 +142,7 @@ function beginProcessing(){ const whereQuery = [ ['ke','=',v.ke], ['status','!=',"0"], - ['details','NOT LIKE','%"archived":"1"%'], + ['archive','!=',`1`], ] b.where.forEach(function(condition){ if(condition.p1 === 'ke'){condition.p3 = v.ke} @@ -314,7 +314,7 @@ function beginProcessing(){ where: [ ['ke','=',v.ke], ['status','!=','0'], - ['details','NOT LIKE','%"archived":"1"%'], + ['archive','!=',`1`], ['time','<', sqlDate('10 MINUTE')], ] },(err,evs) => { diff --git a/libs/database/utils.js b/libs/database/utils.js index bfb5ae71..ecaefb5b 100644 --- a/libs/database/utils.js +++ b/libs/database/utils.js @@ -213,7 +213,7 @@ module.exports = function(s,config){ whereQuery.push(monitorRestrictions) } if(options.archived){ - whereQuery.push(['details','LIKE',`%"archived":"1"%`]) + whereQuery.push(['archive','=',`1`]) } if(options.filename){ whereQuery.push(['filename','=',options.filename]) diff --git a/libs/sql.js b/libs/sql.js index 831a5c9d..8d8c1485 100644 --- a/libs/sql.js +++ b/libs/sql.js @@ -176,6 +176,21 @@ module.exports = function(s,config){ }catch(err){ console.log(err) } + try{ + s.databaseEngine.schema.table('Videos', table => { + table.tinyint('archive',1).defaultTo(0) + table.string('saveDir',255).defaultTo('') + }).then(() => { + console.log(`objects added to Videos table`) + }).catch((err) => { + if(err && err.code !== 'ER_DUP_FIELDNAME'){ + console.log('error') + console.log(err) + } + }) + }catch(err){ + console.log(err) + } delete(s.preQueries) } } diff --git a/libs/user/utils.js b/libs/user/utils.js index c0cf4cb4..bb84e37c 100644 --- a/libs/user/utils.js +++ b/libs/user/utils.js @@ -221,7 +221,7 @@ module.exports = (s,config,lang) => { where: [ ['ke','=',groupKey], ['status','!=','0'], - ['details','NOT LIKE',`%"archived":"1"%`], + ['archive','!=',`1`], ['details','LIKE',`%"dir":"${storage.value}"%`], ], orderBy: ['time','asc'], @@ -266,7 +266,7 @@ module.exports = (s,config,lang) => { where: [ ['ke','=',groupKey], ['status','!=','0'], - ['details','NOT LIKE',`%"archived":"1"%`], + ['archive','!=',`1`], ['details','NOT LIKE',`%"dir"%`], ], orderBy: ['time','asc'], @@ -298,7 +298,7 @@ module.exports = (s,config,lang) => { table: "Timelapse Frames", where: [ ['ke','=',groupKey], - ['details','NOT LIKE',`%"archived":"1"%`], + ['archive','!=',`1`], ], orderBy: ['time','asc'], limit: 3 @@ -408,7 +408,7 @@ module.exports = (s,config,lang) => { table: "Cloud Timelapse Frames", where: [ ['ke','=',groupKey], - ['details','NOT LIKE',`%"archived":"1"%`], + ['archive','!=',`1`], ], orderBy: ['time','asc'], limit: 3 diff --git a/libs/video/utils.js b/libs/video/utils.js index 2ccb045d..bfe96f52 100644 --- a/libs/video/utils.js +++ b/libs/video/utils.js @@ -479,6 +479,28 @@ module.exports = (s,config,lang) => { } }) } + function archiveVideo(video,unarchive){ + return new Promise((resolve) => { + s.knexQuery({ + action: "update", + table: 'Videos', + update: { + archive: unarchive ? '0' : 1 + }, + where: { + ke: video.ke, + mid: video.mid, + time: video.time, + } + },function(err){ + resolve({ + ok: !err, + err: err, + archived: !unarchive + }) + }) + }) + } return { reEncodeVideoAndReplace, stitchMp4Files, @@ -488,5 +510,6 @@ module.exports = (s,config,lang) => { getVideosBasedOnTagFoundInMatrixOfAssociatedEvent, reEncodeVideoAndBinOriginal, reEncodeVideoAndBinOriginalAddToQueue, + archiveVideo, } } diff --git a/libs/webServerPaths.js b/libs/webServerPaths.js index 55598801..73d51f12 100644 --- a/libs/webServerPaths.js +++ b/libs/webServerPaths.js @@ -31,6 +31,7 @@ module.exports = function(s,config,lang,app,io){ destroySubstreamProcess, } = require('./monitor/utils.js')(s,config,lang) const { + archiveVideo, reEncodeVideoAndReplace, reEncodeVideoAndBinOriginalAddToQueue, getVideosBasedOnTagFoundInMatrixOfAssociatedEvent, @@ -1811,6 +1812,13 @@ module.exports = function(s,config,lang,app,io){ const originalFileName = `${s.formattedTime(r.time)+'.'+r.ext}` var details = s.parseJSON(r.details) || {} switch(req.params.mode){ + case'archive': + response.ok = true + const unarchive = s.getPostData(req,'unarchive') === '1'; + const archiveResponse = await archiveVideo(r,unarchive) + response.ok = archiveResponse.ok + response.archived = archiveResponse.archived + break; case'fix': await reEncodeVideoAndReplace(r) break; diff --git a/web/assets/css/bs5.videosTable.css b/web/assets/css/bs5.videosTable.css index ec34918f..cf896a52 100644 --- a/web/assets/css/bs5.videosTable.css +++ b/web/assets/css/bs5.videosTable.css @@ -1,4 +1,28 @@ +.video-thumbnail { + position: relative; + overflow: hidden; +} .video-thumbnail img { height: 75px; border-radius: 10px; } +.video-thumbnail-buttons { + position: absolute; + height: 100%; + width:100%; + top:0; + left:0; +} +.video-thumbnail-buttons > .video-thumbnail-button-cell { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + text-shadow: 0 0 15px #333; + cursor: pointer; +} +.video-thumbnail-buttons > .video-thumbnail-button-cell:hover { + background: rgba(0,0,0,0.6); + color: #fff; +} diff --git a/web/assets/js/bs5.videoPlayer.js b/web/assets/js/bs5.videoPlayer.js index 53faef26..19aae7e6 100644 --- a/web/assets/js/bs5.videoPlayer.js +++ b/web/assets/js/bs5.videoPlayer.js @@ -34,7 +34,7 @@ $(document).ready(function(){ }) eventMatrixHtml += `` } - var baseHtml = `
+ var baseHtml = `
${tabLabel}
@@ -60,6 +60,7 @@ $(document).ready(function(){
${lang.Download} ${permissionCheck('video_delete',video.mid) ? ` ${lang.Delete}` : ''} + ${permissionCheck('video_delete',video.mid) ? ` ${video.archive === 1 ? lang.Unarchive : lang.Archive}` : ''}
diff --git a/web/assets/js/bs5.videos.js b/web/assets/js/bs5.videos.js index 32419d1c..1472f748 100644 --- a/web/assets/js/bs5.videos.js +++ b/web/assets/js/bs5.videos.js @@ -548,6 +548,56 @@ async function compressVideos(videos){ await compressVideo(video) } } +function getArchiveButtons(video){ + return $(`[data-mid="${video.mid}"][data-ke="${video.ke}"][data-time="${video.time}"] .archive-video`) +} +function archiveVideo(video,unarchive){ + return new Promise((resolve) => { + var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename + // var currentlyArchived = video.archive === 1 + $.getJSON(videoEndpoint + '/archive' + `${unarchive ? `?unarchive=1` : ''}`,function(data){ + if(data.ok){ + var archiveButtons = getArchiveButtons(video) + var classToRemove = 'btn-default' + var classToAdd = 'btn-success status-archived' + var iconToRemove = 'fa-unlock-alt' + var iconToAdd = 'fa-lock' + var elTitle = `${lang.Unarchive}` + if(unarchive){ + console.log('Video Unarchived',unarchive) + classToRemove = 'btn-success status-archived' + classToAdd = 'btn-default' + iconToRemove = 'fa-lock' + iconToAdd = 'fa-unlock-alt' + elTitle = `${lang.Archive}` + }else{ + console.log('Video Archived',unarchive) + } + archiveButtons.removeClass(classToRemove).addClass(classToAdd).attr('title',elTitle) + archiveButtons.find('i').removeClass(iconToRemove).addClass(iconToAdd) + archiveButtons.find('span').text(elTitle) + }else{ + console.log('Video Archive status unchanged',data,videoEndpoint) + } + resolve(data) + }) + }) +} +async function archiveVideos(videos){ + for (let i = 0; i < videos.length; i++) { + var video = videos[i]; + await archiveVideo(video) + } +} +function unarchiveVideo(video){ + return archiveVideo(video,true) +} +async function unarchiveVideos(videos){ + for (let i = 0; i < videos.length; i++) { + var video = videos[i]; + await unarchiveVideo(video) + } +} onWebSocketEvent(function(d){ switch(d.f){ case'video_delete': @@ -641,6 +691,33 @@ $(document).ready(function(){ }); return false; }) + .on('click','.archive-video',function(e){ + e.preventDefault() + var el = $(this).parents('[data-mid]') + var monitorId = el.attr('data-mid') + var videoTime = el.attr('data-time') + var unarchive = $(this).hasClass('status-archived') + var video = loadedVideosInMemory[`${monitorId}${videoTime}`] + var ext = video.filename.split('.') + ext = ext[ext.length - 1] + var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename + if(unarchive){ + unarchiveVideo(video) + }else{ + // $.confirm.create({ + // title: lang["Archive"] + ' : ' + video.filename, + // body: `${lang.ArchiveVideoMsg}

`, + // clickOptions: { + // title: ' ' + lang.Archive, + // class: 'btn-primary btn-sm' + // }, + // clickCallback: function(){ + archiveVideo(video) + // } + // }); + } + return false; + }) .on('click','.fix-video',function(e){ e.preventDefault() var el = $(this).parents('[data-mid]') diff --git a/web/assets/js/bs5.videosTable.js b/web/assets/js/bs5.videosTable.js index bfd7231a..1a6bf1e1 100644 --- a/web/assets/js/bs5.videosTable.js +++ b/web/assets/js/bs5.videosTable.js @@ -33,7 +33,7 @@ $(document).ready(function(e){ const startDate = el.attr('data-time') const endDate = el.attr('data-end') const href = await getSnapshotFromVideoTimeFrame(monitorId,startDate,endDate) - imgEl.innerHTML = href ? `` : '' + imgEl.innerHTML = href ? imgEl.innerHTML + `` : '' }) } function openVideosTableView(monitorId,startDate,endDate){ @@ -140,7 +140,7 @@ $(document).ready(function(e){ title: lang['Objects Found'] }, { - field: 'ext', + field: 'tags', title: '' }, { @@ -156,21 +156,32 @@ $(document).ready(function(e){ var href = getFullOrigin(true) + file.href var loadedMonitor = loadedMonitors[file.mid] return { - image: `
`, + image: `
+ +
`, Monitor: loadedMonitor && loadedMonitor.name ? loadedMonitor.name : file.mid, mid: file.mid, time: formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA'), end: formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA'), - ext: file.ext, + tags: ` + ${file.ext} + `, objects: file.objects, size: convertKbToHumanSize(file.size), buttons: `
- ${permissionCheck('video_delete',file.mid) ? `` : ''} ${permissionCheck('video_delete',file.mid) ? `` : ''} + ${permissionCheck('video_delete',file.mid) ? `` : ''}
`, } @@ -255,6 +266,11 @@ $(document).ready(function(e){ drawPreviewVideo(href) return false; }) + .on('click','.open-snapshot',function(e){ + e.preventDefault() + var href = $(this).parents('.video-thumbnail').find('img').click() + return false; + }) .on('click','.delete-selected-videos',function(e){ e.preventDefault() var videos = getSelectedRows() @@ -287,7 +303,26 @@ $(document).ready(function(e){ }, clickCallback: function(){ compressVideos(videos).then(() => { - console.log(`Done Deleting Rows!`) + console.log(`Done Sending Compression Request!`) + }) + } + }); + return false; + }) + .on('click','.archive-selected-videos',function(e){ + e.preventDefault() + var videos = getSelectedRows() + if(videos.length === 0)return; + $.confirm.create({ + title: lang["Archive Videos"], + body: `${lang.ArchiveTheseMsg}`, + clickOptions: { + title: ' ' + lang.Archive, + class: 'btn-primary btn-sm' + }, + clickCallback: function(){ + archiveVideos(videos).then(() => { + console.log(`Done Archiving Rows!`) }) } }); diff --git a/web/pages/blocks/home/videosTable.ejs b/web/pages/blocks/home/videosTable.ejs index 69901b68..a61461fe 100644 --- a/web/pages/blocks/home/videosTable.ejs +++ b/web/pages/blocks/home/videosTable.ejs @@ -21,8 +21,11 @@