master
Moe 2024-09-28 16:55:50 +00:00
parent 03898a3382
commit 9be4e6b95a
36 changed files with 1136 additions and 217 deletions

View File

@ -1,8 +1,27 @@
#!/bin/sh
# Get the Ubuntu version
UBUNTU_VERSION=$(lsb_release -rs)
NODE_MAJOR=18
# Check if Ubuntu version is 18.04
if [ "$UBUNTU_VERSION" = "18.04" ]; then
NODE_MAJOR=16
fi
echo "Installing Node version: $NODE_MAJOR"
# Update and install necessary packages
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
# Setup NodeSource keyring and sources list
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=18
# Add NodeSource repository
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
# Update package list and install Node.js
sudo apt-get update
sudo apt-get install nodejs -y
sudo apt-get install -y nodejs

View File

@ -56,7 +56,7 @@ if ! [ -x "$(command -v ifconfig)" ]; then
fi
echo "============="
echo "Shinobi - Installing Node.js"
sh $DIR/nodejs-ubuntu.sh
bash $DIR/nodejs-ubuntu.sh
if ! [ -x "$(command -v npm)" ]; then
sudo apt install npm -y
fi

View File

@ -3151,12 +3151,30 @@ module.exports = function(s,config,lang){
}
]
},
{
hidden: true,
"id": "detectorsSelected",
"name": "detail=detectors_selected",
"field": lang["Detectors Selected"],
"description": lang.fieldTextDetectorsSelected,
"default": "all",
"attribute": "multiple",
"fieldType": "select",
"form-group-class": "h_casc_input h_casc_1",
"possible": [
{
"name": `${lang.All} (${lang.Default})`,
"value": "all"
}
]
},
{
"name": "detail=detector_object_ignore_not_move",
"field": lang["Ignore Non-Moving"],
"default": "0",
"fieldType": "select",
"selector": "h_obj_ignore_move",
"form-group-class": "h_casc_input h_casc_1",
"possible": [
{
"name": lang.No,
@ -3183,6 +3201,7 @@ module.exports = function(s,config,lang){
"description": lang["fieldTextDetectorSendFramesObject"],
"default": "1",
"fieldType": "select",
"form-group-class": "h_casc_input h_casc_1",
"possible": [
{
"name": lang.No,
@ -3222,6 +3241,7 @@ module.exports = function(s,config,lang){
"default": "1",
"example": "",
"fieldType": "select",
"form-group-class": "h_casc_input h_casc_1",
"possible": [
{
"name": lang.No,
@ -3242,6 +3262,7 @@ module.exports = function(s,config,lang){
"example": "",
"selector": "h_det_mot_fir",
"fieldType": "select",
"form-group-class": "h_casc_input h_casc_1",
"possible": [
{
"name": lang.No,
@ -3290,7 +3311,6 @@ module.exports = function(s,config,lang){
]
},
{
isAdvanced: true,
hidden: true,
"name": lang['Event-Based Recording'],
"input-mapping": "detector_sip_buffer",

View File

@ -18,10 +18,17 @@
"accountEditError": "Account Edit Error",
"Monitor Map": "Monitor Map",
"Geolocation": "Geolocation",
"Tested on": "Tested on",
"Architecture": "Architecture",
"Operating Systems": "Operating Systems",
"fieldTextGeolocation": "The map coordinates of this camera in the real world. This will plot a point for your camera on the Monitor Map.",
"playUntilVideoEnd": "Play until video end",
"Monitor Saved": "Monitor Saved",
"Auto Placement": "Auto Placement",
"Downloaded": "Downloaded",
"GPU Required": "GPU Required",
"Experimental": "Experimental",
"Market": "Market",
"Unmute": "Unmute",
"byUser": "by user",
"accountDeleted": "Account Deleted",
@ -468,6 +475,8 @@
"Max Storage Amount": "Max Storage Amount",
"Video Share": "Video Share",
"FileBin": "FileBin",
"File Saved": "File Saved",
"checkFileBinForNewFile": "Check the FileBin for the newly created file.",
"File Download Ready": "File Download Ready",
"Timelapse Video Build Complete": "Timelapse Video Build Complete",
"yourFileDownloadedShortly": "Please wait. Your file will be downloaded shortly.",
@ -535,6 +544,7 @@
"Time-lapse Tool": "Time-lapse Tool",
"total": "total",
"MB": "MB",
"All": "All",
"Calendar": "Calendar",
"Leave blank for random.": "Leave blank for random.",
"Currently viewing": "Currently viewing",
@ -570,6 +580,10 @@
"clientStreamFailedattemptingReconnect": "Client side stream check failed, attempting reconnect.",
"Export Video": "Export Video",
"Merge Video": "Merge Video",
"Merge Videos": "Merge Videos",
"Merge": "Merge",
"MergeAllSelected": "Merge all selected Videos?",
"MergeAllInRange": "Merge all Videos in date range selected?",
"Delete Filter": "Delete Filter",
"Delete Files": "Delete Files",
"confirmDeleteFilter": "Do you want to delete this filter? You cannot recover it.",
@ -684,6 +698,7 @@
"Minutes": "Minutes",
"Custom": "Custom",
"Detector": "Detector",
"Detectors Selected": "Detectors Selected",
"Audio Detector": "Audio Detector",
"Audio Detection": "Audio Detection",
"Minimum dB": "Minimum dB",
@ -1093,6 +1108,7 @@
"Can't Connect": "Can't Connect",
"Video Finished": "Video Finished",
"No Monitors Selected": "No Monitors Selected",
"No Monitor Selected": "No Monitor Selected",
"Nothing Selected": "Nothing Selected",
"makeASelection": "Make a selection and try again.",
"monSavedButNotCopied": "Your monitor was saved but not copied to any other monitor.",
@ -1204,6 +1220,7 @@
"Preview": "Preview",
"Websocket Connected": "Websocket Connected",
"Websocket Disconnected": "Websocket Disconnected",
"Disconnected": "Disconnected",
"Videos Merge": "Videos Merge",
"Channel ID": "Channel ID",
"Recipient ID": "Recipient ID",
@ -1574,6 +1591,7 @@
"MQTT Client": "MQTT Client",
"Buffer Time from Event": "Buffer Time from Event",
"detected": "detected",
"fieldTextDetectorsSelected": "Select which detectors to send frames to.",
"fieldTextEventFilters": "Enable to have all Events honor your Event Filter rules.",
"fieldTextBufferTimeFromEvent": "The amount of seconds to record before the trigger happened. If this is consistently inaccurate you will need to look at the <a target='_blank' href='https://hub.shinobi.video/articles/view/DmWIID78VtvEfnf'>optimization guide</a> or force encoding on the server.",
"fieldTextMode": "This is the primary task of the monitor.",
@ -1918,6 +1936,8 @@
"HowToConnectDes1": "<b>This feature is available to Mobile License subscribers.</b> To get an API Key please login to your <a href='https: //licenses.shinobi.video/login' target='_blank'>Shinobi<b>Shop</b></a> account and create a key associated to <b>any active Subscription ID</b>. <a href='https://hub.shinobi.video/articles/view/3Yhivc6djTtuBPE' target='_blank'>Learn More.</a>",
"HowToConnectDes2": "If you would like to get access to a private (dedicated) P2P server please create an account at the <a href='https: //licenses.shinobi.video/login' target='_blank'>Shinobi<b>Shop</b></a> and contact us via the Live Chat widget",
"User": "User",
"Save Unknown Faces": "Save Unknown Faces",
"saveUnknownFacesFieldText": "Save Unknown faces to the Face Manager. Manual sorting may still be required.",
"Current Version": "Current Version",
"Default is Global value": "Default is Global value",
"rejectUnauth": "Ignore server certificate"

View File

@ -1,4 +1,5 @@
const fs = require('fs');
const fsP = require('fs').promises;
const path = require('path');
const moment = require('moment');
const fetch = require('node-fetch');
@ -217,6 +218,13 @@ module.exports = (processCwd,config) => {
readStream.pipe(writeStream)
})
}
async function moveFile(inputFilePath,outputFilePath) {
try{
await fsP.rm(outputFilePath)
}catch(err){}
await copyFile(inputFilePath, outputFilePath)
await fsP.rm(inputFilePath)
}
function hmsToSeconds(str) {
var p = str.split(':'),
s = 0, m = 1;
@ -240,6 +248,17 @@ module.exports = (processCwd,config) => {
}
}
}
async function deleteFilesInFolder(folderPath) {
try {
const files = await fsP.readdir(folderPath);
for (const file of files) {
const filePath = path.join(folderPath, file);
await fsP.rm(filePath, { recursive: true });
}
} catch (error) {
console.error(`Error deleting files: ${error.message}`);
}
}
return {
parseJSON: parseJSON,
stringJSON: stringJSON,
@ -261,5 +280,7 @@ module.exports = (processCwd,config) => {
copyFile: copyFile,
hmsToSeconds,
setDefaultIfUndefined,
deleteFilesInFolder,
moveFile,
}
}

View File

@ -1,5 +1,8 @@
const fs = require('fs');
module.exports = function(s,config,lang,app,io){
const {
postProcessCompletedMp4Video,
} = require('../video/utils.js')(s,config,lang)
const masterDoWorkToo = config.childNodes.masterDoWorkToo;
const maxCpuPercent = config.childNodes.maxCpuPercent || 75;
const maxRamPercent = config.childNodes.maxRamPercent || 75;
@ -177,7 +180,9 @@ module.exports = function(s,config,lang,app,io){
filename : filename,
filesizeMB : parseFloat((data.filesize/1048576).toFixed(2))
}
s.insertDatabaseRow(monitorConfig,insert)
s.insertDatabaseRow(monitorConfig,insert,function(response){
postProcessCompletedMp4Video(response.insertQuery).then((isGood) => {
if(!isGood)return console.error(`FAILED VIDEO INSERT`);
s.insertCompletedVideoExtensions.forEach(function(extender){
extender(activeMonitor, monitorConfig, insert)
})
@ -190,6 +195,8 @@ module.exports = function(s,config,lang,app,io){
resolve(response)
})
})
})
})
}
function initiateTimelapseFrameWriteFromChildNode(client,data,connectionId){
return new Promise((resolve,reject) => {

View File

@ -13,6 +13,15 @@ module.exports = (s,config,lang) => {
workerProcess.on('message',function(data){
if(data.time === 'moment()')data.time = moment();
switch(data.f){
case'knexQuery':
s.knexQuery(...data.args, function(...args){
workerProcess.postMessage({
f: 'callback',
rid: data.rid,
args,
})
});
break;
case'debugLog':
s.debugLog(...data.data)
break;

View File

@ -42,6 +42,13 @@ parentPort.on('message',(data) => {
setDefaultConfigOptions()
beginProcessing()
break;
case'callback':
if(pendingCallbacks[data.rid]){
pendingCallbacks[data.rid](...data.args)
// console.log(data.rid,typeof pendingCallbacks[data.rid])
delete(pendingCallbacks[data.rid])
}
break;
case'start':case'restart':
setIntervalForCron()
break;
@ -51,7 +58,7 @@ parentPort.on('message',(data) => {
}
})
function debugLog(...args){
if(config.debugLog === true){
if(config.debugLog === true || config.logCronInfo === true){
console.log(...([`CRON.js DEBUG LOG ${new Date()}`].concat(args)))
}
}
@ -64,6 +71,7 @@ function errorLog(...args){
const s = {
debugLog,
}
const pendingCallbacks = {};
function beginProcessing(){
normalLog(`Worker Processing!`)
const {
@ -74,8 +82,6 @@ function beginProcessing(){
} = require('../basic/utils.js')(process.cwd())
const {
sqlDate,
knexQuery,
knexQueryPromise,
initiateDatabaseEngine
} = require('../sql/utils.js')(s,config)
var theCronInterval = null
@ -111,6 +117,23 @@ function beginProcessing(){
const setDiskUsedForGroup = (groupKey,size,target,videoRow) => {
postMessage({f:'s.setDiskUsedForGroup', ke: groupKey, size: size, target: target, videoRow: videoRow})
}
const knexQuery = (...args) => {
const requestId = generateRandomId();
const callback = args.pop();
pendingCallbacks[requestId] = callback;
postMessage({ f: 'knexQuery', args: args, rid: requestId })
}
const knexQueryPromise = (options) => {
return new Promise((resolve,reject) => {
knexQuery(options,(err,rows) => {
resolve({
ok: !err,
err: err,
rows: rows,
})
})
})
}
const getVideoDirectory = function(e){
if(e.mid&&!e.id){e.id=e.mid};
if(e.details&&(e.details instanceof Object)===false){

View File

@ -26,6 +26,9 @@ module.exports = function(s,config,lang,app,io){
const {
triggerEvent,
} = require('./events/utils.js')(s,config,lang)
const {
deleteFilesInFolder,
} = require('./basic/utils.js')(process.cwd(), config)
if(config.dropInEventServer === true){
if(config.dropInEventForceSaveEvent === undefined)config.dropInEventForceSaveEvent = true
if(config.dropInEventDeleteFileAfterTrigger === undefined)config.dropInEventDeleteFileAfterTrigger = true
@ -142,7 +145,7 @@ module.exports = function(s,config,lang,app,io){
if(config.dropInEventDeleteFileAfterTrigger){
clearTimeout(fileQueue[filePath])
fileQueue[filePath] = setTimeout(function(){
exec('rm -rf ' + filePath,function(err){
fs.rm(filePath, { recursive: true },(err) => {
delete(fileQueue[filePath])
})
},1000 * 60 * 5)
@ -152,7 +155,7 @@ module.exports = function(s,config,lang,app,io){
if(config.dropInEventDeleteFileAfterTrigger){
clearTimeout(fileQueueForDeletion[deletionKey])
fileQueueForDeletion[deletionKey] = setTimeout(function(){
exec('rm -rf ' + deletionKey,function(err){
fs.rm(filePath, { recursive: true },(err) => {
delete(fileQueueForDeletion[deletionKey])
})
},1000 * 60 * 5)
@ -194,7 +197,7 @@ module.exports = function(s,config,lang,app,io){
directory = s.dir.dropInEvents + e.ke + '/' + (e.id || e.mid) + '/'
fs.mkdir(directory,function(err){
s.handleFolderError(err)
exec('rm -rf "' + directory + '*"',function(){})
deleteFilesInFolder(directory)
callback(err,directory)
})
})

View File

@ -678,18 +678,38 @@ module.exports = (s,config,lang) => {
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
const theEmitter = activeMonitor.secondaryDetectorOutput
if(!activeMonitor.sendingFromSecondaryDetectorOuput){
s.debugLog('start sending object frames',groupKey,monitorId)
theEmitter.on('data',activeMonitor.secondaryDetectorOuputContentWriter = (data) => {
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
const monitorDetails = monitorConfig.details;
let chosenDetector = monitorDetails.detectors_selected;
if(chosenDetector instanceof Array)chosenDetector = chosenDetector.join(',');
let sendToDetector = (data) => {
s.ocvTx({
f : 'frame',
mon : s.group[groupKey].rawMonitorConfigurations[monitorId].details,
mon : monitorDetails,
ke : groupKey,
id : monitorId,
time : s.formattedTime(),
frame : data
})
}
if(chosenDetector && !(chosenDetector.includes('all'))){
const pluginsGettingIt = chosenDetector.split(',').map(item => item.trim()).filter(item => !!item);
sendToDetector = (data) => {
for(pluginName of pluginsGettingIt){
s.sendToDetector(pluginName, {
f : 'frame',
mon : monitorDetails,
ke : groupKey,
id : monitorId,
time : s.formattedTime(),
frame : data
})
}
}
}
s.debugLog('start sending object frames',groupKey,monitorId)
theEmitter.on('data', activeMonitor.secondaryDetectorOuputContentWriter = sendToDetector)
}
clearTimeout(activeMonitor.sendingFromSecondaryDetectorOuput)
activeMonitor.sendingFromSecondaryDetectorOuput = setTimeout(() => {
theEmitter.removeListener('data',activeMonitor.secondaryDetectorOuputContentWriter)

View File

@ -87,6 +87,8 @@ module.exports = function(s,config){
createExtension(`onSubscriptionCheck`)
createExtension(`onDataPortMessage`)
createExtension(`onHttpRequestUpgrade`,null,true)
createExtension(`onPluginConnected`)
createExtension(`onPluginDisconnected`)
/////// CRON ////////
createExtension(`onCronGroupProcessed`)
createExtension(`onCronGroupProcessedAwaited`)

View File

@ -790,6 +790,8 @@ module.exports = (s,config,lang) => {
return ``
}
const getDefaultSubstreamFields = function(monitor){
const otherInputFlags = []
const otherOutputFlags = []
const subStreamFields = parseJSON(monitor.details.substream || {input:{},output:{}})
const inputAndConnectionFields = Object.assign({
"type":"h264",
@ -825,7 +827,15 @@ module.exports = (s,config,lang) => {
"svf":"",
"cust_stream":""
},subStreamFields.output);
const isMp4Input = inputAndConnectionFields.type === 'mp4';
const inputTypeCanLoop = isMp4Input || inputAndConnectionFields.type === 'local'
if(inputTypeCanLoop){
otherInputFlags.push('-stream_loop', '-1');
if(isMp4Input)otherInputFlags.push('-re');
}
return {
otherInputFlags,
otherOutputFlags,
inputAndConnectionFields,
outputFields,
}

View File

@ -192,6 +192,11 @@ module.exports = function(s,config,lang,app,io){
})
})
}
s.notifyFileBinUploaded = function(fileBinInsertQuery){
s.tx(Object.assign({
f: 'fileBin_item_added',
},fileBinInsertQuery),'GRP_'+fileBinInsertQuery.ke);
}
s.getFileBinDirectory = getFileBinDirectory
s.getFileBinEntry = getFileBinEntry
s.getFileBinBuffer = getFileBinBuffer

View File

@ -278,6 +278,15 @@ module.exports = (s,config,lang) => {
}
})
}
const sendSubstreamEvent = function(groupKey, monitorId, eventName = 'substream_start'){
const activeMonitor = getActiveMonitor(groupKey,monitorId)
s.tx({
f: eventName,
mid: monitorId,
ke: groupKey,
channel: activeMonitor.subStreamChannel
},'GRP_'+groupKey);
}
const spawnSubstreamProcess = function(e){
// e = monitorConfig
try{
@ -295,9 +304,13 @@ module.exports = (s,config,lang) => {
substreamConfig.input.fulladdress = substreamConfig.input.fulladdress || s.buildMonitorUrl(monitorConfig)
substreamConfig.input.rtsp_transport = substreamConfig.input.rtsp_transport || monitorConfig.details.rtsp_transport
const {
otherInputFlags,
otherOutputFlags,
inputAndConnectionFields,
outputFields,
} = getDefaultSubstreamFields(monitorConfig);
ffmpegCommand.push(...otherInputFlags);
ffmpegCommand.push(...otherOutputFlags);
([
buildSubstreamString(channelNumber + config.pipeAddition,e),
]).forEach(function(commandStringPart){
@ -375,12 +388,7 @@ module.exports = (s,config,lang) => {
}
})
activeMonitor.subStreamProcess = subStreamProcess
s.tx({
f: 'substream_start',
mid: monitorId,
ke: groupKey,
channel: activeMonitor.subStreamChannel
},'GRP_'+groupKey);
sendSubstreamEvent(groupKey, monitorId)
return subStreamProcess
}catch(err){
s.systemLog(err)
@ -403,11 +411,7 @@ module.exports = (s,config,lang) => {
response.hadSubStream = true
response.closeResponse = closeResponse
delete(activeMonitor.subStreamProcess)
s.tx({
f: 'substream_end',
mid: activeMonitor.mid,
ke: activeMonitor.ke
},'GRP_'+activeMonitor.ke);
sendSubstreamEvent(activeMonitor.mid, activeMonitor.ke, 'substream_end')
activeMonitor.subStreamProcessClosing = false
}
}catch(err){
@ -975,6 +979,7 @@ module.exports = (s,config,lang) => {
const groupKey = e.ke
const monitorId = e.mid || e.id
const activeMonitor = getActiveMonitor(groupKey,monitorId)
if(!activeMonitor)return;
clearTimeout(activeMonitor.streamChecker)
activeMonitor.streamChecker = setTimeout(function(){
if(activeMonitor && activeMonitor.isStarted === true){
@ -1181,7 +1186,6 @@ module.exports = (s,config,lang) => {
})
}
if(e.details.detector === '1'){
s.ocvTx({f:'init_monitor',id:monitorId,ke:groupKey})
//frames from motion detect
if(e.details.detector_pam === '1'){
// activeMonitor.spawn.stdio[3].pipe(activeMonitor.p2p).pipe(activeMonitor.pamDiff)
@ -1856,5 +1860,6 @@ module.exports = (s,config,lang) => {
setTimedActiveViewerForHttp: setTimedActiveViewerForHttp,
attachMainProcessHandlers: attachMainProcessHandlers,
removeSenstiveInfoFromMonitorConfig,
sendSubstreamEvent,
}
}

View File

@ -43,34 +43,30 @@ module.exports = function(s,config,lang,app,io){
s.detectorPluginArray = []
s.isAtleatOneDetectorPluginConnected = false
s.addDetectorPlugin = function(name,d){
const newDetector = {
started: s.timeObject(),
id: d.id,
plug: d.plug,
notice: d.notice,
isClientPlugin: d.isClientPlugin,
isHostPlugin: d.isHostPlugin,
connectionType: d.connectionType
};
if(config.useOldPluginConnectionMethod === true){
s.ocv = {
started: s.timeObject(),
id: d.id,
plug: d.plug,
notice: d.notice,
isClientPlugin: d.isClientPlugin,
isHostPlugin: d.isHostPlugin,
connectionType: d.connectionType
}
}
s.connectedDetectorPlugins[d.plug] = {
started: s.timeObject(),
id: d.id,
plug: d.plug,
notice: d.notice,
isClientPlugin: d.isClientPlugin,
isHostPlugin: d.isHostPlugin,
connectionType: d.connectionType
s.ocv = newDetector
}
s.connectedDetectorPlugins[d.plug] = newDetector
s.resetDetectorPluginArray()
s.runExtensionsForArray('onPluginConnected', null, [d.plug, newDetector])
}
s.removeDetectorPlugin = function(name){
const theDetector = Object.assign({}, s.connectedDetectorPlugins[name])
if(config.oldPluginConnectionMethod === true && s.ocv && s.ocv.plug === name){
delete(s.ocv)
}
delete(s.connectedDetectorPlugins[name])
s.resetDetectorPluginArray(name)
s.runExtensionsForArray('onPluginDisconnected', null, [name, theDetector])
}
s.resetDetectorPluginArray = function(){
pluginArray = []
@ -164,6 +160,10 @@ module.exports = function(s,config,lang,app,io){
})
}
}
s.sendToDetector = function(pluginName, data){
const detector = s.connectedPlugins[pluginName];
if(detector)detector.tx(data);
}
s.sendDetectorInfoToClient = function(data,txFunction){
s.detectorPluginArray.forEach(function(name){
var detectorData = Object.assign(data,{
@ -363,7 +363,33 @@ module.exports = function(s,config,lang,app,io){
}
}
}
function onMonitorUpdate(monitorConfig){
// console.log('Sending Monitor Info to Plugin', monitorConfig.mid)
s.sendToAllDetectors({ f: 'monitorUpdate', monitorConfig });
}
function sendCopyOfAllMonitorConfigs(){
const groupKeys = Object.keys(s.group);
for(groupKey of groupKeys){
const monitorConfigs = Object.values(s.group[groupKey].rawMonitorConfigurations);
for(monitorConfig of monitorConfigs){
onMonitorUpdate(monitorConfig)
}
}
}
/**
* API : Get List of Connected Plugins
*/
app.get(config.webPaths.apiPrefix+':auth/plugins/list', async (req,res) => {
s.auth(req.params, async (resp) => {
s.closeJsonResponse(res,{
ok: true,
plugins: s.connectedDetectorPlugins
})
},res,req)
})
s.onSocketAuthentication(onSocketAuthentication)
s.onWebSocketDisconnection(onWebSocketDisconnection)
s.onWebSocketConnection(onWebSocketConnection)
s.onMonitorStart(onMonitorUpdate)
s.onPluginConnected(sendCopyOfAllMonitorConfigs)
}

View File

@ -94,12 +94,13 @@ module.exports = async (s,config,lang,app,io,currentUse) => {
const downloadModule = (downloadUrl,packageName) => {
const downloadPath = modulesBasePath + packageName
try{
fs.mkdirSync(downloadPath)
fs.mkdirSync(downloadPath, { recursive: true })
}catch(err){
s.debugLog(err)
}
return new Promise(async (resolve, reject) => {
fs.mkdir(downloadPath, () => {
fs.mkdir(downloadPath, { recursive: true }, (err) => {
if(err)console.error(err)
fetchDownloadAndWrite(downloadUrl,downloadPath + '.zip', 1)
.then((readStream) => {
readStream.pipe(unzipper.Parse())
@ -228,11 +229,15 @@ module.exports = async (s,config,lang,app,io,currentUse) => {
}
const enableModule = (name,status) => {
// set status to `false` to enable
try{
const modulePath = getModulePath(name)
const confJson = getModuleConfiguration(name)
const confPath = modulePath + 'conf.json'
confJson.enabled = status;
fs.writeFileSync(confPath,s.prettyPrint(confJson))
}catch(err){
console.error('Failed to Toggle Enable Status for Module.', name, status)
}
}
const deleteModule = (name) => {
// requires restart for changes to take effect

View File

@ -34,6 +34,7 @@ module.exports = function(s,config,lang,app,io){
s.createTimelapseFrameAndInsert = function(e,location,filename,eventTime,frameDetails){
//e = monitor object
//location = file location
var monitorId = e.id || e.mid;
var filePath = location + filename
var fileStats = fs.statSync(filePath)
var details = Object.assign({},frameDetails || {})
@ -43,7 +44,7 @@ module.exports = function(s,config,lang,app,io){
const timeNow = eventTime || new Date()
const queryInfo = {
ke: e.ke,
mid: e.id,
mid: monitorId,
details: s.s(details),
filename: filename,
size: fileStats.size,
@ -53,7 +54,7 @@ module.exports = function(s,config,lang,app,io){
var currentDate = s.formattedTime(timeNow,'YYYY-MM-DD')
const childNodeData = {
ke: e.ke,
mid: e.id,
mid: monitorId,
time: currentDate,
filename: filename,
currentDate: currentDate,
@ -559,8 +560,7 @@ module.exports = function(s,config,lang,app,io){
actionParameter && (
isRestrictedApiKey && apiKeyPermissions.delete_videos_disallowed ||
isRestricted && !monitorPermissions[`${monitorId}_video_delete`]
) ||
!actionParameter && (
) || !actionParameter && (
isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed ||
isRestricted && monitorId && !monitorPermissions[`${monitorId}_video_view`]
)

View File

@ -1,6 +1,9 @@
const fs = require('fs')
const { spawn } = require('child_process')
const async = require('async');
const path = require('path');
const moment = require('moment');
const fsP = require('fs').promises;
module.exports = (s,config,lang) => {
const {
ffprobe,
@ -9,7 +12,9 @@ module.exports = (s,config,lang) => {
const {
copyFile,
hmsToSeconds,
moveFile,
} = require('../basic/utils.js')(process.cwd(),config)
const chunkReadSize = 4096;
// orphanedVideoCheck : new function
const checkIfVideoIsOrphaned = (monitor,videosDirectory,filename) => {
const response = {ok: true}
@ -593,6 +598,7 @@ module.exports = (s,config,lang) => {
time: video.time,
}
await s.insertFileBinEntry(fileBinInsertQuery)
s.notifyFileBinUploaded(fileBinInsertQuery)
s.tx(Object.assign({
f: 'fileBin_item_added',
slicedVideo: true,
@ -604,6 +610,374 @@ module.exports = (s,config,lang) => {
}
return response
}
const mergingVideos = {};
const mergeVideos = async function({
groupKey,
monitorId,
filePaths,
outputFilePath,
videoCodec = 'libx265',
audioCodec = 'aac',
onStdout = (data) => {s.systemLog(`${data}`)},
onStderr = (data) => {s.systemLog(`${data}`)},
}) {
if (!Array.isArray(filePaths) || filePaths.length === 0) {
throw new Error('First parameter must be a non-empty array of absolute file paths.');
}
if(mergingVideos[outputFilePath])return;
const currentDate = new Date();
const fileExtensions = filePaths.map(file => path.extname(file).toLowerCase());
const allSameExtension = fileExtensions.every(ext => ext === fileExtensions[0]);
const fileList = filePaths.map(file => `file '${file}'`).join('\n');
const tempFileListPath = path.join(s.dir.streams, groupKey, monitorId, `video_merge_${currentDate}.txt`);
mergingVideos[outputFilePath] = currentDate;
try {
await fsP.writeFile(tempFileListPath, fileList);
let ffmpegArgs;
// if (allSameExtension) {
// ffmpegArgs = [
// '-f', 'concat',
// '-safe', '0',
// '-i', tempFileListPath,
// '-c', 'copy',
// '-y',
// outputFilePath
// ];
// } else {
ffmpegArgs = [
'-loglevel', 'warning',
'-f', 'concat',
'-safe', '0',
'-i', tempFileListPath,
'-c:v', videoCodec,
'-c:a', audioCodec,
'-strict', '-2',
'-crf', '1',
'-y',
outputFilePath
];
// }
s.debugLog(fileList)
s.debugLog(ffmpegArgs)
await new Promise((resolve, reject) => {
const ffmpegProcess = spawn(config.ffmpegDir, ffmpegArgs);
ffmpegProcess.stdout.on('data', onStdout);
ffmpegProcess.stderr.on('data', onStderr);
ffmpegProcess.on('close', (code) => {
delete(mergingVideos[outputFilePath]);
if (code === 0) {
console.log(`FFmpeg process exited with code ${code}`);
resolve();
} else {
reject(new Error(`FFmpeg process exited with code ${code}`));
}
});
ffmpegProcess.on('error', (err) => {
reject(new Error(`FFmpeg error: ${err}`));
});
});
} finally {
await fsP.unlink(tempFileListPath);
}
};
async function mergeVideosAndBin(videos){
const currentTime = new Date();
const firstVideo = videos[0];
const lastVideo = videos[videos.length - 1];
const groupKey = firstVideo.ke;
const monitorId = firstVideo.mid;
const logTarget = { ke: groupKey, mid: '$USER' };
try{
try{
await fsP.stat(outputFilePath)
return outputFilePath
}catch(err){
}
const filePaths = videos.map(video => {
const monitorConfig = s.group[video.ke].rawMonitorConfigurations[video.mid];
const filePath = path.join(s.getVideoDirectory(video), `${s.formattedTime(video.time)}.mp4`);
return filePath
});
const filename = `${s.formattedTime(firstVideo.time)}-${s.formattedTime(lastVideo.time)}-${filePaths.length}.mp4`
const fileBinFolder = s.getFileBinDirectory(firstVideo);
const outputFilePath = path.join(fileBinFolder, filename);
s.userLog(logTarget,{
type: 'mergeVideos ffmpeg START',
msg: {
monitorId,
numberOfVideos: filePaths.length,
}
});
await mergeVideos({
groupKey,
monitorId,
filePaths,
outputFilePath,
onStdout: (data) => {
s.debugLog(data.toString())
s.userLog(logTarget,{
type: 'mergeVideos ffmpeg LOG',
msg: `${data}`
});
},
onStderr: (data) => {
s.debugLog(data.toString())
s.userLog(logTarget,{
type: 'mergeVideos ffmpeg ERROR',
msg: `${data}`
});
},
});
const fileSize = (await fsP.stat(outputFilePath)).size;
const fileBinInsertQuery = {
ke: groupKey,
mid: monitorId,
name: filename,
size: fileSize,
details: {},
status: 1,
time: currentTime,
}
await s.insertFileBinEntry(fileBinInsertQuery);
s.notifyFileBinUploaded(fileBinInsertQuery);
return outputFilePath
}catch(err){
console.log('mergeVideos process ERROR', err)
s.userLog(logTarget,{
type: 'mergeVideos process ERROR',
msg: `${err.toString()}`
});
return null;
}
}
async function readChunkForMoov(filePath, start, end) {
const stream = fs.createReadStream(filePath, { start, end });
let hasMoov = false;
for await (const chunk of stream) {
if (chunk.includes('moov')) {
hasMoov = true;
break;
}
}
return hasMoov;
}
async function checkMoovAtBeginning(filePath) {
return await readChunkForMoov(filePath, 0, chunkReadSize - 1);
}
async function checkMoovAtEnd(filePath) {
const stats = await fs.promises.stat(filePath);
const fileSize = stats.size;
return await readChunkForMoov(filePath, fileSize - chunkReadSize, fileSize - 1);
}
async function hasMoovAtom(filePath) {
const foundAtBeginning = await checkMoovAtBeginning(filePath);
if (foundAtBeginning) {
return true;
}
const foundAtEnd = await checkMoovAtEnd(filePath);
return foundAtEnd;
}
const addMoovAtom = async (inputFilePath, outputFilePath, videoCodec = 'libx264', audioCodec = 'aac') => {
try {
const ffmpegArgs = [
'-i', inputFilePath,
'-c:v', videoCodec,
];
if(audioCodec){
ffmpegArgs.push('-c:a', audioCodec, '-strict', '-2')
}else{
ffmpegArgs.push('-an')
}
ffmpegArgs.push(
'-movflags', '+faststart',
'-crf', '0',
'-q:a', '0',
outputFilePath
);
console.log(config.ffmpegDir + ' ' + ffmpegArgs.join(' '))
return new Promise((resolve, reject) => {
const ffmpegProcess = spawn(config.ffmpegDir, ffmpegArgs);
ffmpegProcess.stdout.on('data', (data) => {
console.log(`FFmpeg stdout: ${data}`);
});
ffmpegProcess.stderr.on('data', (data) => {
console.error(`FFmpeg stderr: ${data}`);
});
ffmpegProcess.on('close', (code) => {
if (code === 0) {
resolve(outputFilePath);
} else {
reject(new Error(`FFmpeg process exited with code ${code}`));
}
});
ffmpegProcess.on('error', (err) => {
reject(err);
});
});
} catch (error) {
throw new Error(`Failed to re-encode file: ${error.message}`);
}
};
async function getVideoFrameAsJpeg(filePath, seconds = 7){
return new Promise((resolve, reject) => {
const ffmpegArgs = [
'-loglevel', 'warning',
'-ss', seconds.toString(),
'-i', filePath,
'-frames:v', '1',
'-q:v', '2',
'-f', 'image2pipe',
'-vcodec', 'mjpeg',
'pipe:1'
];
const ffmpegProcess = spawn(config.ffmpegDir, ffmpegArgs);
let buffer = Buffer.alloc(0);
ffmpegProcess.stdout.on('data', (data) => {
buffer = Buffer.concat([buffer, data]);
});
ffmpegProcess.stderr.on('data', (data) => {
s.debugLog(`getVideoFrameAsJpeg FFmpeg stderr: ${data}`);
});
ffmpegProcess.on('close', (code) => {
if (code === 0) {
resolve(buffer);
} else {
reject(new Error(`FFmpeg process exited with code ${code}`));
}
});
ffmpegProcess.on('error', (err) => {
reject(err);
});
});
};
function getVideoPath(video){
const videoPath = path.join(s.getVideoDirectory(video), `${s.formattedTime(video.time)}.${video.ext}`);
return videoPath
}
async function saveVideoFrameToTimelapse(video, secondsIn = 7){
// console.error(video)
const monitorConfig = s.group[video.ke].rawMonitorConfigurations[video.mid];
const activeMonitor = s.group[video.ke].activeMonitors[video.mid];
const frameTime = moment(video.time).add(secondsIn, 'seconds');
const frameDate = s.formattedTime(frameTime,'YYYY-MM-DD');
const timelapseRecordingDirectory = s.getTimelapseFrameDirectory(monitorConfig);
const videoPath = getVideoPath(video);
const frameFilename = s.formattedTime(frameTime) + '.jpg';
const location = timelapseRecordingDirectory + frameDate + '/';
const framePath = path.join(location, frameFilename);
try{
await fsP.stat(framePath)
}catch(err){
try{
const frameBuffer = await getVideoFrameAsJpeg(videoPath, secondsIn);
await fsP.mkdir(location, { recursive: true })
await fsP.writeFile(framePath, frameBuffer)
await s.createTimelapseFrameAndInsert(activeMonitor,location,frameFilename, frameTime._d)
}catch(err){
console.error(err)
}
}
// console.error('Completed Saving Frame from New Video!', framePath)
}
function getVideoCodecsFromMonitorConfig(video){
const monitorConfig = s.group[video.ke].rawMonitorConfigurations[video.mid];
const modeIsRecord = monitorConfig.mode === 'record'
let eventBasedVideoCodec = monitorConfig.details.detector_buffer_vcodec
let eventBasedAudioCodec = monitorConfig.details.detector_buffer_acodec
let recordingVideoCodec = monitorConfig.details.vcodec
let recordingAudioCodec = monitorConfig.details.acodec
switch(eventBasedVideoCodec){
case null:case '':case undefined:case'auto':
eventBasedVideoCodec = 'libx264'
break;
}
switch(eventBasedAudioCodec){
case null:case '':case undefined:case'auto':
eventBasedAudioCodec = 'aac'
break;
case'no':
eventBasedAudioCodec = null
break;
}
switch(recordingVideoCodec){
case null:case '':case undefined:case'auto':case'default':case'copy':
recordingVideoCodec = 'libx264'
break;
}
switch(recordingAudioCodec){
case null:case '':case undefined:case'auto':case'copy':
recordingAudioCodec = 'aac'
break;
case'no':
recordingAudioCodec = null
break;
}
return {
videoCodec: modeIsRecord ? recordingVideoCodec : eventBasedVideoCodec,
audioCodec: modeIsRecord ? recordingAudioCodec : eventBasedAudioCodec,
recordingVideoCodec,
recordingAudioCodec,
eventBasedVideoCodec,
eventBasedAudioCodec,
}
}
async function postProcessCompletedMp4Video(chosenVideo){
try {
const video = Object.assign({
ext: 'mp4'
},chosenVideo);
const videoPath = getVideoPath(video);
// const moovExists = await hasMoovAtom(videoPath);
// if (moovExists) {
// s.debugLog('The file already has a moov atom.');
// } else {
// return true;
// // const { videoCodec, audioCodec } = getVideoCodecsFromMonitorConfig(video);
// // const tempPath = path.join(s.getVideoDirectory(video), `TEMP_${s.formattedTime(video.time)}.${video.ext}`);
// // await addMoovAtom(videoPath, tempPath, videoCodec, audioCodec);
// // await moveFile(tempPath, videoPath)
// // const newFileSize = (await fsP.stat(videoPath)).size;
// // const updateResponse = await s.knexQueryPromise({
// // action: "update",
// // table: "Videos",
// // update: {
// // size: newFileSize
// // },
// // where: [
// // ['ke','=',video.ke],
// // ['mid','=',video.mid],
// // ['time','=',video.time],
// // ['end','=',video.end],
// // ['ext','=',video.ext],
// // ]
// // });
// }
// await saveVideoFrameToTimelapse(video, 0)
await saveVideoFrameToTimelapse(video, 7)
return true;
} catch (error) {
console.error('Error processing MP4 file:', error);
return false;
}
};
return {
reEncodeVideoAndReplace,
stitchMp4Files,
@ -615,5 +989,13 @@ module.exports = (s,config,lang) => {
reEncodeVideoAndBinOriginalAddToQueue,
archiveVideo,
sliceVideo,
mergeVideos,
mergeVideosAndBin,
saveVideoFrameToTimelapse,
postProcessCompletedMp4Video,
readChunkForMoov,
checkMoovAtBeginning,
checkMoovAtEnd,
hasMoovAtom
}
}

View File

@ -5,6 +5,9 @@ module.exports = function(s,config,lang){
const {
sendVideoToMasterNode,
} = require('./childNode/childUtils.js')(s,config,lang)
const {
postProcessCompletedMp4Video,
} = require('./video/utils.js')(s,config,lang)
/**
* Gets the video directory of the supplied video or monitor database row.
* @constructor
@ -141,7 +144,7 @@ module.exports = function(s,config,lang){
ext: k.ext,
size: k.filesize,
filesize: k.filesize,
objects: k.objects,
objects: k.objects.substring(0, 510),
time: s.timeObject(k.startTime).format('YYYY-MM-DD HH:mm:ss'),
end: s.timeObject(k.endTime).format('YYYY-MM-DD HH:mm:ss')
}
@ -180,10 +183,13 @@ module.exports = function(s,config,lang){
})
s.insertDatabaseRow(e,k,(err,response) => {
if(callback)callback(err,response);
postProcessCompletedMp4Video(response.insertQuery).then((isGood) => {
if(!isGood)return console.error(`FAILED VIDEO INSERT`);
s.insertCompletedVideoExtensions.forEach(function(extender){
extender(e,k,response.insertQuery,response)
})
})
})
}
}
s.group[e.ke].activeMonitors[e.mid].detector_motion_count = []

View File

@ -32,6 +32,7 @@ module.exports = function(s,config,lang,app,io){
spawnSubstreamProcess,
destroySubstreamProcess,
removeSenstiveInfoFromMonitorConfig,
sendSubstreamEvent,
} = require('./monitor/utils.js')(s,config,lang)
const {
sliceVideo,
@ -39,6 +40,7 @@ module.exports = function(s,config,lang,app,io){
reEncodeVideoAndReplace,
reEncodeVideoAndBinOriginalAddToQueue,
getVideosBasedOnTagFoundInMatrixOfAssociatedEvent,
mergeVideosAndBin,
} = require('./video/utils.js')(s,config,lang)
s.renderPage = function(req,res,paths,passables,callback){
passables.window = {}
@ -915,17 +917,39 @@ module.exports = function(s,config,lang,app,io){
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
const substreamConfig = monitorConfig.details.substream
const theAction = req.query.action
if(
substreamConfig.output
){
switch(theAction){
case'status':
response.ok = true
response.isRunning = !!activeMonitor.subStreamProcess;
response.channel = activeMonitor.subStreamChannel;
break;
case'stop':
activeMonitor.allowDestroySubstream = true
await destroySubstreamProcess(activeMonitor)
break;
default:
if(!activeMonitor.subStreamProcess){
response.ok = true
activeMonitor.allowDestroySubstream = false;
spawnSubstreamProcess(monitorConfig)
response.channel = activeMonitor.subStreamChannel;
}else{
activeMonitor.allowDestroySubstream = true
await destroySubstreamProcess(activeMonitor)
sendSubstreamEvent(groupKey, monitorId)
}
break;
}
// if(!activeMonitor.subStreamProcess){
// response.ok = true
// activeMonitor.allowDestroySubstream = false;
// spawnSubstreamProcess(monitorConfig)
// }else{
// activeMonitor.allowDestroySubstream = true
// await destroySubstreamProcess(activeMonitor)
// }
}else{
response.msg = lang['Invalid Settings']
}
@ -1970,7 +1994,69 @@ module.exports = function(s,config,lang,app,io){
res.end(s.prettyPrint(response));
})
},res,req);
});
/**
* API : Merge Videos and Bin it
*/
app.post(config.webPaths.apiPrefix+':auth/mergeVideos/:ke/:id', function (req,res){
s.auth(req.params, async function(user){
const monitorId = req.params.id
const groupKey = req.params.ke
const {
monitorPermissions,
monitorRestrictions,
} = s.getMonitorsPermitted(user.details,monitorId,'video_view')
const {
isRestricted,
isRestrictedApiKey,
apiKeyPermissions,
} = s.checkPermission(user);
if(
isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed ||
isRestricted && (
monitorId && !monitorPermissions[`${monitorId}_video_view`] ||
monitorRestrictions.length === 0
)
){
s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized'], videos: []});
return
}
const response = { ok: false }
const selectedVideos = s.getPostData(req,'videos');
console.log('selected',selectedVideos)
if(selectedVideos && selectedVideos.length > 1){
const mergedFilePath = await mergeVideosAndBin(selectedVideos);
response.ok = !!mergedFilePath;
s.closeJsonResponse(res, response);
}else{
s.sqlQueryBetweenTimesWithPermissions({
table: 'Videos',
user: user,
noCount: true,
groupKey,
monitorId,
startTime: s.getPostData(req,'start'),
endTime: s.getPostData(req,'end'),
startTimeOperator: s.getPostData(req,'startOperator'),
endTimeOperator: s.getPostData(req,'endOperator'),
noLimit: s.getPostData(req,'noLimit'),
limit: s.getPostData(req,'limit'),
archived: s.getPostData(req,'archived'),
endIsStartTo: !!s.getPostData(req,'endIsStartTo'),
parseRowDetails: false,
rowName: 'videos',
monitorRestrictions: monitorRestrictions,
preliminaryValidationFailed: false
}, async ({ videos }) => {
if(videos){
const mergedFilePath = await mergeVideosAndBin(videos);
response.ok = !!mergedFilePath;
}
s.closeJsonResponse(res, response);
})
}
},res,req);
});
/**
* API : Get Login Tokens
*/

View File

@ -56,8 +56,11 @@ module.exports = function(__dirname, config){
if(!config.hostPort){config.hostPort = 8082}
if(config.systemLog === undefined){config.systemLog = true}
if(config.connectionType === undefined)config.connectionType = 'websocket'
const imageBuffers = {}
s = {
group: {},
monitors: {},
monitorInfo: {},
dir: {},
isWin: (process.platform === 'win32'),
s: (json) => {
@ -217,16 +220,26 @@ module.exports = function(__dirname, config){
cn.emit('init',{ok:true,plug:config.plug,notice:config.notice,type:config.type})
}
break;
case'init_monitor':
retryConnection = 0
if(s.group[d.ke] && s.group[d.ke][d.id]){
s.group[d.ke][d.id].numberOfTriggers = 0
delete(s.group[d.ke][d.id].cords)
delete(s.group[d.ke][d.id].buffer)
s.onCameraInitExtensions.forEach((extender) => {
extender(d,cn,tx)
})
case'monitorUpdate':
var monitorConfig = d.monitorConfig;
var groupKey = monitorConfig.ke;
var monitorId = monitorConfig.mid;
var monitorDetails = monitorConfig.details;
var monitorKey = `${groupKey}${monitorId}`
if(!s.monitors[monitorKey])s.monitors[monitorKey] = Object.assign({}, monitorConfig);
var isObjectDetectionSeparate = monitorDetails.detector_use_detect_object === '1'
var width = parseFloat(isObjectDetectionSeparate && monitorDetails.detector_scale_x_object ? monitorDetails.detector_scale_x_object : monitorDetails.detector_scale_x)
var height = parseFloat(isObjectDetectionSeparate && monitorDetails.detector_scale_y_object ? monitorDetails.detector_scale_y_object : monitorDetails.detector_scale_y)
s.monitorInfo[monitorKey] = {
isObjectDetectionSeparate,
width,
height,
}
delete(imageBuffers[monitorKey])
for(extender of s.onCameraInitExtensions){
extender(monitorConfig, cn, tx)
}
// console.log(monitorId, 'registered', s.monitorInfo[monitorKey])
break;
case'frameFromRam':
if(!s.group[d.ke]){
@ -239,29 +252,22 @@ module.exports = function(__dirname, config){
break;
case'frame':
try{
if(!s.group[d.ke]){
s.group[d.ke]={}
}
if(!s.group[d.ke][d.id]){
s.group[d.ke][d.id] = {}
s.onCameraInitExtensions.forEach((extender) => {
extender(d,cn,tx)
})
}
if(!s.group[d.ke][d.id].buffer){
s.group[d.ke][d.id].buffer = [d.frame];
const monitorKey = `${d.id}${d.ke}`;
imageBuffers[monitorKey]
if(!imageBuffers[monitorKey]){
imageBuffers[monitorKey] = [d.frame];
}else{
s.group[d.ke][d.id].buffer.push(d.frame)
imageBuffers[monitorKey].push(d.frame)
}
if(d.frame[d.frame.length-2] === 0xFF && d.frame[d.frame.length-1] === 0xD9){
var buffer = Buffer.concat(s.group[d.ke][d.id].buffer);
var buffer = Buffer.concat(imageBuffers[monitorKey]);
processImage(buffer,d,tx)
s.group[d.ke][d.id].buffer = null
imageBuffers[monitorKey] = null
}
}catch(err){
if(err){
s.systemLog(err)
delete(s.group[d.ke][d.id].buffer)
delete(imageBuffers[monitorKey])
}
}
break;

View File

@ -32,8 +32,11 @@ module.exports = function(__dirname, config){
if(!config.hostPort){config.hostPort = 8082}
if(config.systemLog === undefined){config.systemLog = true}
if(config.connectionType === undefined)config.connectionType = 'websocket'
const imageBuffers = {}
s = {
group: {},
monitors: {},
monitorInfo: {},
dir: {},
isWin: (process.platform === 'win32'),
s: (json) => {
@ -192,16 +195,26 @@ module.exports = function(__dirname, config){
cn.emit('init',{ok:true,plug:config.plug,notice:config.notice,type:config.type})
}
break;
case'init_monitor':
retryConnection = 0
if(s.group[d.ke] && s.group[d.ke][d.id]){
s.group[d.ke][d.id].numberOfTriggers = 0
delete(s.group[d.ke][d.id].cords)
delete(s.group[d.ke][d.id].buffer)
s.onCameraInitExtensions.forEach((extender) => {
extender(d,cn,tx)
})
case'monitorUpdate':
var monitorConfig = d.monitorConfig;
var groupKey = monitorConfig.ke;
var monitorId = monitorConfig.mid;
var monitorDetails = monitorConfig.details;
var monitorKey = `${groupKey}${monitorId}`
if(!s.monitors[monitorKey])s.monitors[monitorKey] = Object.assign({}, monitorConfig);
var isObjectDetectionSeparate = monitorDetails.detector_use_detect_object === '1'
var width = parseFloat(isObjectDetectionSeparate && monitorDetails.detector_scale_x_object ? monitorDetails.detector_scale_x_object : monitorDetails.detector_scale_x)
var height = parseFloat(isObjectDetectionSeparate && monitorDetails.detector_scale_y_object ? monitorDetails.detector_scale_y_object : monitorDetails.detector_scale_y)
s.monitorInfo[monitorKey] = {
isObjectDetectionSeparate,
width,
height,
}
delete(imageBuffers[monitorKey])
for(extender of s.onCameraInitExtensions){
extender(monitorConfig, cn, tx)
}
// console.log(monitorId, 'registered', s.monitorInfo[monitorKey])
break;
case'frameFromRam':
if(!s.group[d.ke]){
@ -214,29 +227,22 @@ module.exports = function(__dirname, config){
break;
case'frame':
try{
if(!s.group[d.ke]){
s.group[d.ke]={}
}
if(!s.group[d.ke][d.id]){
s.group[d.ke][d.id] = {}
s.onCameraInitExtensions.forEach((extender) => {
extender(d,cn,tx)
})
}
if(!s.group[d.ke][d.id].buffer){
s.group[d.ke][d.id].buffer = [d.frame];
const monitorKey = `${d.id}${d.ke}`;
imageBuffers[monitorKey]
if(!imageBuffers[monitorKey]){
imageBuffers[monitorKey] = [d.frame];
}else{
s.group[d.ke][d.id].buffer.push(d.frame)
imageBuffers[monitorKey].push(d.frame)
}
if(d.frame[d.frame.length-2] === 0xFF && d.frame[d.frame.length-1] === 0xD9){
var buffer = Buffer.concat(s.group[d.ke][d.id].buffer);
var buffer = Buffer.concat(imageBuffers[monitorKey]);
processImage(buffer,d,tx)
s.group[d.ke][d.id].buffer = null
imageBuffers[monitorKey] = null
}
}catch(err){
if(err){
s.systemLog(err)
delete(s.group[d.ke][d.id].buffer)
delete(imageBuffers[monitorKey])
}
}
break;

View File

@ -1,5 +1,5 @@
const fetch = require('node-fetch');
const AdmZip = require('adm-zip');
const unzipper = require('unzipper');
const fs = require('fs').promises;
const path = require('path');
@ -21,13 +21,14 @@ async function fetchAndDownloadFolder(folderName) {
throw new Error(`Failed to download folder: ${response.statusText}`);
}
const buffer = await response.buffer();
const zip = new AdmZip(buffer);
// Extract the ZIP to a temporary location
// Create a temporary path for extraction
const tempExtractPath = path.join(process.cwd(), 'temp_extracted');
await fs.mkdir(tempExtractPath, { recursive: true });
zip.extractAllTo(tempExtractPath, true);
// Extract the ZIP using unzipper
await response.body
.pipe(unzipper.Extract({ path: tempExtractPath }))
.promise();
// Find the folder ending with the target name
const extractedFolder = (await fs.readdir(tempExtractPath)).find(dir => dir.endsWith(folderName));
@ -66,3 +67,5 @@ if (!folderName) {
console.error('Usage: node script.js <folderName>');
process.exit(1);
}
fetchAndDownloadFolder(folderName);

View File

@ -12,3 +12,6 @@
border: 1px solid #009dff;
border-radius: 10px;
}
#superPluginManager .badge-success {
background-color: #2fa523;
}

View File

@ -1102,3 +1102,11 @@ function makeButton({ color, link, text, class: classes}){
function replaceBrokenImage(_this){
$(_this).attr('src', `${libURL}/libs/img/bg.jpg`)
}
function getQueryString(){
var theObject = {}
location.search.substring(1).split('&').forEach(function(string){
var parts = string.split('=')
theObject[parts[0]] = parts[1]
})
return theObject
}

View File

@ -2,6 +2,7 @@ var loadedLiveGrids = {}
var monitorPops = {}
var liveGridElements = {}
var runningJpegStreams = {}
var containerElement = $(`#monitors_live`)
var liveGrid = $('#monitors_live .stream-element-container')
var websocketPath = checkCorrectPathEnding(urlPrefix) + 'socket.io'
//
@ -75,7 +76,7 @@ function buildLiveGridBlock(monitor){
var monitorDetails = safeJsonParse(monitor.details)
var monitorLiveId = `monitor_live_${monitor.mid}`
var subStreamChannel = monitor.subStreamChannel
var streamType = subStreamChannel ? monitorDetails.substream ? monitorDetails.substream.output.stream_type : 'hls' : monitorDetails.stream_type
var streamType = monitorDetails.stream_type === 'useSubstream' ? monitorDetails.substream.output.stream_type : monitorDetails.stream_type
var streamElement = buildStreamElementHtml(streamType)
var streamBlockInfo = definitions['Monitor Stream Window']
if(!loadedLiveGrids[monitor.mid])loadedLiveGrids[monitor.mid] = {}
@ -99,10 +100,8 @@ function buildLiveGridBlock(monitor){
function drawLiveGridBlock(monitorConfig,subStreamChannel){
var monitorId = monitorConfig.mid
if($('#monitor_live_' + monitorId).length === 0){
var html = buildLiveGridBlock(monitorConfig)
liveGrid.html(html);
console.log(liveGrid.length,html)
var theBlock = $('#monitor_live_' + monitorId);
var streamElement = theBlock.find('.stream-element')
liveGridElements[monitorId] = {
@ -114,7 +113,6 @@ function drawLiveGridBlock(monitorConfig,subStreamChannel){
width: streamElement.width(),
height: streamElement.height(),
}
}
initiateLiveGridPlayer(loadedMonitors[monitorId],subStreamChannel)
}
function unmuteVideoPlayer(){
@ -131,15 +129,15 @@ function unmuteVideoPlayer(){
},3000)
$('.unmute-embed-audio').remove()
}
function initiateLiveGridPlayer(monitor,subStreamChannel){
function initiateLiveGridPlayer(monitor){
var livePlayerElement = loadedLiveGrids[monitor.mid]
var details = monitor.details
var groupKey = monitor.ke
var monitorId = monitor.mid
var loadedMonitor = loadedMonitors[monitorId]
var loadedPlayer = loadedLiveGrids[monitor.mid]
var containerElement = $(`#monitor_live_${monitor.mid}`)
var streamType = subStreamChannel ? details.substream ? details.substream.output.stream_type : 'hls' : details.stream_type
var subStreamChannel = loadedMonitor.subStreamChannel
var streamType = details.stream_type === 'useSubstream' ? details.substream.output.stream_type : details.stream_type
switch(streamType){
case'jpeg':
startJpegStream(monitorId)
@ -542,11 +540,21 @@ function toggleSubStream(monitorId,callback){
}
if(monitor.subStreamToggleLock)return false;
monitor.subStreamToggleLock = true
$.getJSON(getApiPrefix() + '/toggleSubstream/'+$user.ke+'/'+monitorId,function(d){
var substreamUrl = getApiPrefix() + '/toggleSubstream/'+$user.ke+'/'+monitorId;
$.getJSON(`${substreamUrl}?action=status`,function(response){
if(!response.isRunning){
$.getJSON(substreamUrl,function(d){
monitor.subStreamChannel = d.channel
monitor.subStreamToggleLock = false
debugLog(d)
if(callback)callback()
})
}else{
monitor.subStreamChannel = response.channel
monitor.subStreamToggleLock = false
if(callback)callback()
}
})
}
$(document).ready(function(e){
$('body')
@ -595,8 +603,8 @@ $(document).ready(function(e){
case'monitor_watch_on':
var monitorId = d.mid || d.id
var loadedMonitor = loadedMonitors[monitorId]
var subStreamChannel = d.subStreamChannel
if(!loadedMonitor.subStreamChannel && loadedMonitor.details.stream_type === 'useSubstream'){
var subStreamChannel = loadedMonitor.subStreamChannel || d.channel
if(loadedMonitor.details.stream_type === 'useSubstream'){
toggleSubStream(monitorId,function(){
drawLiveGridBlock(loadedMonitors[monitorId],subStreamChannel)
})
@ -619,7 +627,6 @@ $(document).ready(function(e){
$('body').addClass('jpegMode')
break;
case'detector_trigger':
console.log(d)
var monitorId = d.id
var liveGridElement = liveGridElements[monitorId]
if(!window.dontShowDetection && liveGridElement){

View File

@ -73,3 +73,11 @@ function drawMatrices(event,options){
})
theContainer.append(html)
}
function getQueryString(){
var theObject = {}
location.search.substring(1).split('&').forEach(function(string){
var parts = string.split('=')
theObject[parts[0]] = parts[1]
})
return theObject
}

View File

@ -15,6 +15,7 @@ var monSectionPresets = $('#monSectionPresets')
var copySettingsSelector = $('#copy_settings')
var monitorPresetsSelection = $('#monitorPresetsSelection')
var monitorPresetsNameField = $('#monitorPresetsName')
var detectorsSelected = $('#detectorsSelected')
var monitorsList = monitorEditorWindow.find('.monitors_list')
var editorForm = monitorEditorWindow.find('form')
var tagsInput = monitorEditorWindow.find('[name="tags"]')
@ -566,7 +567,37 @@ function drawInputMapSelectorHtml(options,parent){
</div>`
parent.prepend(html)
}
function importIntoMonitorEditor(options){
function getPluginsList(monitorConfig){
return new Promise((resolve) => {
const chosenDetectors = safeJsonParse(monitorConfig.details).detectors_selected || [];
$.get(getApiPrefix() + '/plugins/list',function(data){
var plugins = data.plugins || {};
var pluginNames = Object.keys(plugins)
var disconnectedPlugins = chosenDetectors.filter(item => !pluginNames.includes(item));
var html = createOptionHtml({
value: 'all',
label: `${lang.All} (${lang.Default})`
});
$.each(plugins, function(name, pluginInfo){
html += createOptionHtml({
value: name,
label: name,
selected: chosenDetectors.includes(name),
})
});
$.each(disconnectedPlugins, function(n, name){
html += createOptionHtml({
value: name,
label: `${name} (${lang.Disconnected})`,
selected: true,
})
});
detectorsSelected.html(html)
resolve(plugins)
})
})
}
async function importIntoMonitorEditor(options){
var monitorConfig = options.values || options
var monitorId = monitorConfig.mid
var monitorDetails = safeJsonParse(monitorConfig.details);
@ -686,6 +717,9 @@ function importIntoMonitorEditor(options){
}
}
});
//
await getPluginsList(monitorConfig)
//
copySettingsSelector.val('0').change()
var tmp = '';
$.each(loadedMonitors,function(n,monitor){
@ -1319,9 +1353,11 @@ editorForm.find('[name="type"]').change(function(e){
break;
case'detector_plugged':
addDetectorPlugin(d.plug,d)
if(monitorEditorSelectedMonitor)getPluginsList(monitorEditorSelectedMonitor);
break;
case'detector_unplugged':
removeDetectorPlugin(d.plug)
if(monitorEditorSelectedMonitor)getPluginsList(monitorEditorSelectedMonitor);
break;
}
})
@ -1335,6 +1371,7 @@ editorForm.find('[name="type"]').change(function(e){
drawMonitorListToSelector(monitorsList.find('optgroup'),false,'host')
monitorsList.val(theSelected)
checkToOpenSideMenu()
if(monitorEditorSelectedMonitor)getPluginsList(monitorEditorSelectedMonitor)
}
addOnTabAway('monitorSettings', function(){
if(isSideBarMenuCollapsed()){

View File

@ -1168,6 +1168,9 @@ function getRunningMonitors(asArray){
})
return asArray ? Object.values(foundMonitors) : foundMonitors
}
function buildFileBinUrl(data){
return apiBaseUrl + '/fileBin/' + data.ke + '/' + data.mid + '/' + data.name
}
$(document).ready(function(){
$('body')
.on('click','[system]',function(){

View File

@ -311,9 +311,6 @@ $(document).ready(function(e){
function downloadTimelapseFrame(frame){
downloadFile(frame.href,frame.filename)
}
function buildFileBinUrl(data){
return apiBaseUrl + '/fileBin/' + data.ke + '/' + data.mid + '/' + data.name
}
function downloadTimelapseVideo(data){
var downloadUrl = buildFileBinUrl(data)
downloadFile(downloadUrl,data.name)

View File

@ -442,9 +442,7 @@ function loadEventsData(videoEvents){
loadedEventsInMemory[`${anEvent.mid}${anEvent.time}`] = anEvent
})
}
function getVideos(options,callback,noEvents){
return new Promise((resolve,reject) => {
options = options ? options : {}
function getVideoSearchRequestQueries(options){
var searchQuery = options.searchQuery
var requestQueries = []
var monitorId = options.monitorId
@ -455,8 +453,6 @@ function getVideos(options,callback,noEvents){
var doLimitOnFames = options.doLimitOnFames || false
var eventStartTime
var eventEndTime
// var startDate = options.startDate
// var endDate = options.endDate
if(options.startDate){
eventStartTime = formattedTimeForFilename(options.startDate,false)
requestQueries.push(`start=${eventStartTime}`)
@ -471,6 +467,69 @@ function getVideos(options,callback,noEvents){
if(archived){
requestQueries.push(`archived=1`)
}
return {
searchQuery,
monitorId,
archived,
customVideoSet,
limit,
eventLimit,
doLimitOnFames,
eventStartTime,
eventEndTime,
requestQueries,
}
}
function mergeVideosAndBin(options,callback){
const {
searchQuery,
monitorId,
archived,
customVideoSet,
limit,
eventLimit,
doLimitOnFames,
eventStartTime,
eventEndTime,
requestQueries,
} = getVideoSearchRequestQueries(options);
const videos = options.videos.map(video => {
const newVideo = {
ke: video.ke,
mid: video.mid,
time: video.time,
end: video.end,
ext: video.ext,
saveDir: video.saveDir,
details: video.details,
};
delete(newVideo.timelapseFrames)
return newVideo
});
console.log(videos)
return new Promise((resolve) => {
$.post(`${getApiPrefix(`mergeVideos`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([limit ? `limit=${limit}` : `noLimit=1`]).join('&')}`, {
videos,
},function(data){
resolve(data)
})
})
}
function getVideos(options,callback,noEvents){
return new Promise((resolve,reject) => {
options = options ? options : {}
const {
searchQuery,
monitorId,
archived,
customVideoSet,
limit,
eventLimit,
doLimitOnFames,
eventStartTime,
eventEndTime,
requestQueries,
} = getVideoSearchRequestQueries(options);
$.getJSON(`${getApiPrefix(customVideoSet ? customVideoSet : searchQuery ? `videosByEventTag` : `videos`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([limit ? `limit=${limit}` : `noLimit=1`]).join('&')}`,function(data){
var videos = data.videos.map((video) => {
return Object.assign({},video,{

View File

@ -189,7 +189,7 @@ $(document).ready(function(e){
<div>${timeAgo(file.time)}</div>
<div><small><b>${lang.Start} :</b> ${formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA')}</small></div>
<div><small><b>${lang.End} :</b> ${formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA')}</small></div>`,
objects: file.objects,
objects: `<div style="word-break: break-word;max-width:125px;">${file.objects}</div>`,
tags: `
${file.ext ? `<span class="badge badge-${file.ext ==='webm' ? `primary` : 'danger'}">${file.ext}</span>` : ''}
${!isLocalVideo ? `<span class="badge badge-success">${file.type}</span>` : ''}
@ -274,6 +274,43 @@ $(document).ready(function(e){
var downloadUrl = buildNewFileLink(data)
downloadFile(downloadUrl,data.name)
}
function mergeSelectedVideos(){
var videos = getSelectedRows(true)
var dateRange = getSelectedTime(dateSelector);
var searchQuery = objectTagSearchField.val() || null;
var startDate = dateRange.startDate;
var endDate = dateRange.endDate;
var monitorId = monitorsList.val();
var wantsArchivedVideo = getVideoSetSelected() === 'archive';
if(!monitorId){
new PNotify({
title: lang['No Monitor Selected'],
text: lang['No Monitor Found, Ignoring Request'],
type: 'danger',
})
return
}
$.confirm.create({
title: lang["Merge Videos"],
body: `${videos.length > 0 ? lang.MergeAllSelected : lang.MergeAllInRange}`,
clickOptions: {
title: '<i class="fa fa-check"></i> ' + lang.Save,
class: 'btn-success btn-sm'
},
clickCallback: async function(){
console.log('Merging Video...')
var result = await mergeVideosAndBin({
monitorId,
startDate,
endDate,
videos,
searchQuery,
archived: wantsArchivedVideo,
});
console.log('Merged Video! Check Filebin.')
}
});
}
$('body')
.on('click','.open-videosTable',function(e){
e.preventDefault()
@ -307,6 +344,11 @@ $(document).ready(function(e){
zipVideosAndDownloadWithConfirm(videos)
return false;
})
.on('click','.merge-selected-videos',function(e){
e.preventDefault()
mergeSelectedVideos();
return false;
})
.on('click','.refresh-data',function(e){
e.preventDefault()
drawVideosTableViewElements()
@ -410,6 +452,14 @@ $(document).ready(function(e){
})
onWebSocketEvent((data) => {
switch(data.f){
case'fileBin_item_added':
new PNotify({
title: lang['File Saved'],
text: `${lang.checkFileBinForNewFile}<br><br><a class="btn btn-sm btn-success" href="${buildFileBinUrl(data)}" download>${lang.Download}</a>`,
type: 'success',
sticky: true,
})
break;
case'video_delete':
case'video_delete_cloud':
if(tabTree.name === 'videosTableView'){

View File

@ -10,6 +10,14 @@ $(document).ready(function(){
var theWindow = $(window);
var lastWindowWidth = theWindow.width()
var lastWindowHeight = theWindow.height()
function getQueryString(){
var theObject = {}
location.search.substring(1).split('&').forEach(function(string){
var parts = string.split('=')
theObject[parts[0]] = parts[1]
})
return theObject
}
function featureIsActivated(showNotice){
if(userHasSubscribed){
return true
@ -61,6 +69,7 @@ $(document).ready(function(){
function selectMonitor(monitorId, css){
css = css || {};
var embedHost = getQueryString().host || `/`;
var isSelected = selectedMonitors[monitorId]
if(isSelected)return;
var numberOfSelected = Object.keys(selectedMonitors)
@ -69,7 +78,7 @@ $(document).ready(function(){
}
++selectedMonitorsCount
selectedMonitors[monitorId] = Object.assign({}, loadedMonitors[monitorId]);
wallViewCanvas.append(`<div class="wallview-video p-0 m-0" live-stream="${monitorId}" style="left:${css.left || 0}px;top:${css.top || 0}px;width:${css.width ? css.width + 'px' : '50vw'};height:${css.height ? css.height + 'px' : '50vh'};"><div class="overlay"><div class="wallview-item-controls text-end"><a class="btn btn-sm btn-outline-danger wallview-item-close"><i class="fa fa-times"></i></a></div></div><iframe src="${getApiPrefix('embed')}/${monitorId}/fullscreen%7Cjquery%7Crelative?host=/"></iframe></div>`)
wallViewCanvas.append(`<div class="wallview-video p-0 m-0" live-stream="${monitorId}" style="left:${css.left || 0}px;top:${css.top || 0}px;width:${css.width ? css.width + 'px' : '50vw'};height:${css.height ? css.height + 'px' : '50vh'};"><div class="overlay"><div class="wallview-item-controls text-end"><a class="btn btn-sm btn-outline-danger wallview-item-close"><i class="fa fa-times"></i></a></div></div><iframe src="${getApiPrefix('embed')}/${monitorId}/fullscreen%7Cjquery%7Crelative?host=${embedHost}"></iframe></div>`)
wallViewCanvas.find(`[live-stream="${monitorId}"]`)
.draggable({
grid: [40, 40],

View File

@ -1,21 +1,69 @@
$(document).ready(function(){
var loadedModules = {}
var downloadablePlugins = {}
var theEnclosure = $('#superPluginManager')
var listElement = $('#pluginManagerList')
var downloadListElement = $('#pluginManagerDownloadble')
var quickSelect = $('#pluginQuickSelect')
var pluginDownloadForm = $('#downloadNewPlugin')
var pluginCommandLine = $('#pluginCommandLine')
var pluginDownloadableSearch = $('#pluginManagerDownloadbleListSearch')
var getModules = function(callback) {
$.get(superApiPrefix + $user.sessionKey + '/plugins/list',callback)
}
function getDlPluginId(plugin){
return `${plugin.name}_${plugin.link}_${plugin.dir}`
}
function drawDownloadablePlugins(data){
var html = ''
$.each(data,function(n,plugin){
html += `
<div class="col-md-6" dl-plugin="${getDlPluginId(plugin)}">
<div class="card bg-dark text-white mb-3">
<div class="card-body">
<div class="pb-1">
${plugin.type.map(item => `<span class="small">${item}</span>`).join(', ')}
${plugin.gpuRequired ? `<span class="badge badge-primary">${plugin.gpuRequired instanceof Array ? plugin.gpuRequired.join(', ') : lang['GPU Required']}</span>` : ''}
${plugin.experimental ? `<span class="badge badge-warning">${lang.Experimental}</span>` : ''}
</div>
<h4 class="title">${plugin.name}</h4>
<div><span class="small">${lang['Tested on']}</span> : ${plugin.os.map(item => `<span class="small">${item}</span>`).join(', ')}</div>
<div class="pb-1"><span class="badge badge-warning">${plugin.engine}</span> ${plugin.arch.map(item => `<span title="${lang.Architecture}" class="badge badge-primary">${item}</span>`).join(' ')}</div>
<div><a class="btn btn-info btn-sm cursor-pointer download">${lang.Download}</a></div>
</div>
</div>
</div>`
})
downloadListElement.html(html)
}
function filterDownloadablePlugins(theSearch = '') {
var searchQuery = theSearch.trim().toLowerCase();
if(searchQuery === ''){
downloadListElement.find(`[dl-plugin]`).show()
return;
}
var rows = Object.values(downloadablePlugins);
var filtered = []
rows.forEach((row) => {
var searchInString = JSON.stringify(row).toLowerCase();
var theElement = downloadListElement.find(`[dl-plugin="${getDlPluginId(row)}"]`)
console.log(searchInString)
if(searchInString.indexOf(searchQuery) > -1){
theElement.show()
}else{
theElement.hide()
}
})
return filtered
}
function getDownloadableModules(callback) {
return new Promise((resolve,reject) => {
const pluginListUrl = `https://cdn.shinobi.video/plugins/list.json`
$.getJSON(pluginListUrl,function(data){
var html = ''
$.each(data,function(n,plugin){
html += `<option value="${plugin.link}${plugin.dir ? `,${plugin.dir}` : ''}">${plugin.name}</option>`
downloadablePlugins[getDlPluginId(plugin)] = plugin;
})
quickSelect.html(html)
drawDownloadablePlugins(data)
resolve(data)
})
})
@ -99,7 +147,11 @@ $(document).ready(function(){
$.post(superApiPrefix + $user.sessionKey + '/plugins/download',{
downloadUrl: url,
packageRoot: packageRoot,
},callback)
},function(data){
setTimeout(function(){
callback(data)
},3000)
})
}
})
}
@ -328,13 +380,14 @@ $(document).ready(function(){
break;
}
})
pluginDownloadForm.submit(function(e){
e.preventDefault();
var el = $(this)
var form = el.serializeObject()
downloadModule(form.downloadUrl,form.packageRoot,function(data){
console.log(data)
theEnclosure.on('click','[dl-plugin] .download',function(e){
var pluginName = $(this).parents('[dl-plugin]').attr('dl-plugin')
console.log(pluginName)
var theDlPlugin = downloadablePlugins[pluginName]
downloadModule(theDlPlugin.link,theDlPlugin.dir,function(data){
if(data.ok){
$('[data-bs-target="#pluginManagerList"],#pluginManagerList').addClass('active')
$('[data-bs-target="#pluginManagerDownloadbleList"],#pluginManagerDownloadbleList').removeClass('active')
var theModule = data.newModule
theModule.config.enabled = false
drawModuleBlock(theModule)
@ -351,7 +404,6 @@ $(document).ready(function(){
}
}
})
return false
})
$('#pluginQuickSelectExec').click(function(){
var currentVal = quickSelect.val()
@ -362,6 +414,10 @@ $(document).ready(function(){
pluginDownloadForm.find(`[name="packageRoot"]`).val(packageRoot)
pluginDownloadForm.submit()
})
pluginDownloadableSearch.keyup(function(){
var searchQuery = $(this).val()
filterDownloadablePlugins(searchQuery)
})
function getObjectAlphabetically(theObject,key){
return Object.values(theObject).sort(function( a, b ) {
const aName = new Date(a[key]).getTime()

View File

@ -21,6 +21,7 @@
<li><a class="dropdown-item unarchive-selected-videos cursor-pointer"><%- lang.Unarchive %></a></li>
<li><a class="dropdown-item compress-selected-videos cursor-pointer"><%- lang.Compress %></a></li>
<li><a class="dropdown-item zip-selected-videos cursor-pointer"><%- lang['Zip and Download'] %></a></li>
<li><a class="dropdown-item merge-selected-videos cursor-pointer"><%- lang['Merge'] %></a></li>
<!-- <li><a class="dropdown-item merge-selected-videos cursor-pointer"><%- lang.Merge %></a></li> -->
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item delete-selected-videos cursor-pointer"><%- lang.Delete %></a></li>

View File

@ -1,29 +1,18 @@
<div class="row">
<div class="col-md-6">
<div class="card bg-dark grey mt-1">
<div class="card-header">
<%- lang['Download Plugins'] %>
</div>
<div class="card-body">
<p><%- lang.pluginDownloadText %></p>
<div class="form-group">
<select class="form-control" id="pluginQuickSelect">
<% [
{
"label": "tensorflow-4-1-0",
"value": "https://gitlab.com/Shinobi-Systems/shinobi-plugins/-/archive/master/shinobi-plugins-master.zip,plugins/tensorflow-4-1-0"
},
{
"label": "tensorflow-2-3-0",
"value": "https://gitlab.com/Shinobi-Systems/shinobi-plugins/-/archive/master/shinobi-plugins-master.zip,plugins/tensorflow-2-3-0"
}
].forEach((option) => { %>
<option value="<%- option.value %>"><%- option.label %></option>
<% }) %>
</select>
</div>
<div><button id="pluginQuickSelectExec" class="btn btn-round btn-block btn-default mb-0"><i class="fa fa-download"></i> <%- lang.Download %></button></div>
<%- lang['Plugin Manager'] %>
</div>
<ul class="nav nav-tabs nav-tabs-neutral justify-content-center bg-dark" id="tablist" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" data-bs-target="#pluginManagerList" role="tab"><%-lang['Downloaded']%></a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#pluginManagerDownloadbleList" role="tab"><%-lang['Download Plugins']%></a>
</li>
</ul>
</div>
</div>
<div class="col-md-6">
@ -40,8 +29,16 @@
</form>
</div>
</div>
<div class="pt-4 pb-0 m-0" id="pluginManagerList">
<div class="tab-content pt-4 pb-0 m-0">
<div class="tab-pane active" id="pluginManagerList"></div>
<div class="tab-pane" id="pluginManagerDownloadbleList">
<div class="form-group">
<input id="pluginManagerDownloadbleListSearch" type="text" placeholder="<%- lang.Search %>" class="form-control" />
</div>
<div class="row d-flex" id="pluginManagerDownloadble">
</div>
</div>
</div>
<link rel="stylesheet" href="<%-window.libURL%>assets/css/super.pluginManager.css">
<script src="<%-window.libURL%>assets/js/super.pluginManager.js" type="text/javascript"></script>