commit
4e6e0eb1f5
|
|
@ -14,3 +14,4 @@ dist
|
|||
generatedLanguageFiles
|
||||
faces
|
||||
unknownFaces
|
||||
.idea/
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ if [ ! -e "/config/conf.json" ]; then
|
|||
node tools/modifyConfiguration.js cpuUsageMarker=CPU subscriptionId=$SUBSCRIPTION_ID thisIsDocker=true pluginKeys="$PLUGIN_KEYS" databaseType="$DB_TYPE" db="$DATABASE_CONFIG" ssl="$SSL_CONFIG"
|
||||
cp /config/conf.json conf.json
|
||||
fi
|
||||
sudo sed -i -e 's/change_this_to_something_very_random__just_anything_other_than_this/'"$cronKey"'/g' conf.json
|
||||
sed -i -e 's/change_this_to_something_very_random__just_anything_other_than_this/'"$cronKey"'/g' conf.json
|
||||
|
||||
|
||||
echo "============="
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ COPY . .
|
|||
#RUN rm -rf /home/Shinobi/plugins
|
||||
COPY ./plugins /home/Shinobi/plugins
|
||||
RUN chmod -R 777 /home/Shinobi/plugins
|
||||
RUN chmod 777 /home/Shinobi
|
||||
RUN npm i npm@latest -g && \
|
||||
npm install --unsafe-perm && \
|
||||
npm install pm2 -g
|
||||
|
|
|
|||
|
|
@ -7962,6 +7962,18 @@ module.exports = function(s,config,lang){
|
|||
isFormGroupGroup: true,
|
||||
'section-class': 'text-center',
|
||||
"info": [
|
||||
{
|
||||
"fieldType": "btn-group",
|
||||
"normalWidth": true,
|
||||
"btns": [
|
||||
{
|
||||
"fieldType": "btn",
|
||||
"class": `btn-success btn-sm`,
|
||||
"attribute": `powerVideo-control="downloadPlaying" title="${lang['Download']}"`,
|
||||
"btnContent": `<i class="fa fa-download"></i>`,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"fieldType": "btn-group",
|
||||
"normalWidth": true,
|
||||
|
|
|
|||
|
|
@ -858,6 +858,7 @@
|
|||
"Country of Plates": "Country of Plates",
|
||||
"Email on No Motion": "Email on \"No Motion\"",
|
||||
"Discord on No Motion": "Discord on \"No Motion\"",
|
||||
"No README found": "No README found",
|
||||
"Timeout": "Timeout",
|
||||
"Controllable": "Controllable",
|
||||
"Custom Base URL": "Custom Base URL <small>Leave blank to use Host URL</small>",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@ const rawMonitorConfig = jsonData.rawMonitorConfig
|
|||
// var writeToStderr = function(text){
|
||||
// process.stderr.write(Buffer.from(text))
|
||||
// }
|
||||
|
||||
var timeout = setTimeout(() => {
|
||||
exitAction()
|
||||
},10000)
|
||||
var snapProcess = spawn(ffmpegAbsolutePath,ffmpegCommandString,{detached: true})
|
||||
snapProcess.stderr.on('data',(data)=>{
|
||||
writeToStderr(data.toString())
|
||||
|
|
@ -54,6 +56,7 @@ snapProcess.stdout.on('data',(data)=>{
|
|||
writeToStderr(data.toString())
|
||||
})
|
||||
snapProcess.on('close',function(data){
|
||||
clearTimeout(timeout)
|
||||
if(useIcon){
|
||||
var iconStream = fs.createWriteStream(iconImageFile);
|
||||
var fileCopy = fs.createReadStream(temporaryImageFile).pipe(iconStream)
|
||||
|
|
|
|||
|
|
@ -47,11 +47,21 @@ module.exports = function(s,config,lang){
|
|||
ok: true,
|
||||
type: lang[doStart ? 'Control Triggered' : 'Control Trigger Ended']
|
||||
}
|
||||
const theRequest = fetchWithAuthentication(requestUrl,{
|
||||
method: controlUrlMethod || controlOptions.method,
|
||||
digestAuth: hasDigestAuthEnabled,
|
||||
postData: controlOptions.postData || null
|
||||
});
|
||||
var fetchWithAuthData;
|
||||
if (controlOptions.postData){
|
||||
fetchWithAuthData = {
|
||||
method: controlUrlMethod || controlOptions.method,
|
||||
digestAuth: hasDigestAuthEnabled,
|
||||
postData: controlOptions.postData
|
||||
}
|
||||
}
|
||||
else{
|
||||
fetchWithAuthData = {
|
||||
method: controlUrlMethod || controlOptions.method,
|
||||
digestAuth: hasDigestAuthEnabled
|
||||
}
|
||||
}
|
||||
const theRequest = fetchWithAuthentication(requestUrl,fetchWithAuthData);
|
||||
theRequest.then(res => res.text())
|
||||
.then((data) => {
|
||||
if(doStart){
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
module.exports = async function(s,config){
|
||||
s.debugLog('Updating database to 2023-03-11')
|
||||
const {
|
||||
alterColumn,
|
||||
} = require('../utils.js')(s,config)
|
||||
await alterColumn('Monitors',[
|
||||
{name: 'path', length: 255, type: 'string'},
|
||||
])
|
||||
await alterColumn('Videos',[
|
||||
{name: 'size', length: 15, type: 'bigInteger'},
|
||||
])
|
||||
await alterColumn('Cloud Videos',[
|
||||
{name: 'size', length: 15, type: 'bigInteger'},
|
||||
])
|
||||
await alterColumn('Files',[
|
||||
{name: 'size', length: 15, type: 'bigInteger'},
|
||||
])
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ module.exports = function(s,config){
|
|||
{name: 'ke', length: 50, type: 'string'},
|
||||
{name: 'mid', length: 100, type: 'string'},
|
||||
{name: 'name', length: 100, type: 'string'},
|
||||
{name: 'size', type: 'integer'},
|
||||
{name: 'size', type: 'bigint'},
|
||||
{name: 'details', type: 'text'},
|
||||
{name: 'status', type: 'integer', length: 1, defaultTo: 0},
|
||||
{name: 'archive', type: 'tinyint', length: 1, defaultTo: 0},
|
||||
|
|
@ -58,7 +58,7 @@ module.exports = function(s,config){
|
|||
{name: 'ke', length: 50, type: 'string'},
|
||||
{name: 'mid', length: 100, type: 'string'},
|
||||
{name: 'ext', type: 'string', length: 10, defaultTo: 'mp4'},
|
||||
{name: 'size', type: 'integer'},
|
||||
{name: 'size', type: 'bigint'},
|
||||
{name: 'status', type: 'tinyint', length: 1, defaultTo: 0},
|
||||
{name: 'archive', type: 'tinyint', length: 1, defaultTo: 0},
|
||||
{name: 'objects', length: 510, type: 'string'},
|
||||
|
|
@ -75,7 +75,7 @@ module.exports = function(s,config){
|
|||
{name: 'ke', length: 50, type: 'string'},
|
||||
{name: 'mid', length: 100, type: 'string'},
|
||||
{name: 'href', length: 50, type: 'text'},
|
||||
{name: 'size', type: 'integer'},
|
||||
{name: 'size', type: 'bigint'},
|
||||
{name: 'details', type: 'text'},
|
||||
{name: 'status', type: 'integer', length: 1, defaultTo: 0},
|
||||
{name: 'archive', type: 'tinyint', length: 1, defaultTo: 0},
|
||||
|
|
@ -112,7 +112,7 @@ module.exports = function(s,config){
|
|||
{name: 'mid', length: 100, type: 'string'},
|
||||
{name: 'filename', length: 50, type: 'string'},
|
||||
{name: 'time', type: 'timestamp', defaultTo: currentTimestamp()},
|
||||
{name: 'size', type: 'integer'},
|
||||
{name: 'size', type: 'bigint'},
|
||||
{name: 'archive', length: 1, type: 'tinyint', defaultTo: 0},
|
||||
{name: 'saveDir', length: 255, type: 'string'},
|
||||
{name: 'details', type: 'text'},
|
||||
|
|
@ -127,7 +127,7 @@ module.exports = function(s,config){
|
|||
{name: 'href', type: 'text'},
|
||||
{name: 'filename', length: 50, type: 'string'},
|
||||
{name: 'time', type: 'timestamp', defaultTo: currentTimestamp()},
|
||||
{name: 'size', type: 'integer'},
|
||||
{name: 'size', type: 'bigint'},
|
||||
{name: 'details', type: 'text'},
|
||||
]);
|
||||
await createTable('Logs',[
|
||||
|
|
@ -181,6 +181,7 @@ module.exports = function(s,config){
|
|||
// additional requirements for older installs
|
||||
await require('./migrate/2022-08-22.js')(s,config)
|
||||
await require('./migrate/2022-12-18.js')(s,config)
|
||||
await require('./migrate/2023-03-11.js')(s,config)
|
||||
delete(s.preQueries)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -442,6 +442,23 @@ module.exports = function(s,config){
|
|||
}
|
||||
}
|
||||
}
|
||||
async function alterColumn(tableName,columns){
|
||||
try{
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
const column = columns[i]
|
||||
if(!column)return;
|
||||
await s.databaseEngine.schema.alterTable(tableName, table => {
|
||||
let action = table[column.type](column.name,column.length)
|
||||
if(column.defaultTo !== null && column.defaultTo !== undefined){
|
||||
action = action.defaultTo(column.defaultTo)
|
||||
}
|
||||
action.alter()
|
||||
})
|
||||
}
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
}
|
||||
}
|
||||
async function createTable(tableName,columns,onSuccess){
|
||||
try{
|
||||
const exists = await s.databaseEngine.schema.hasTable(tableName)
|
||||
|
|
@ -478,6 +495,7 @@ module.exports = function(s,config){
|
|||
sqlQueryBetweenTimesWithPermissions: sqlQueryBetweenTimesWithPermissions,
|
||||
currentTimestamp,
|
||||
createTable,
|
||||
alterColumn,
|
||||
addColumn,
|
||||
isMySQL,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ module.exports = (s,config,lang) => {
|
|||
const acceptableOperators = ['indexOf','!indexOf','===','!==','>=','>','<','<=']
|
||||
// Event Filters />
|
||||
const {
|
||||
splitForFFPMEG
|
||||
splitForFFMPEG
|
||||
} = require('../ffmpeg/utils.js')(s,config,lang)
|
||||
const {
|
||||
moveCameraPtzToMatrix
|
||||
|
|
@ -556,7 +556,7 @@ module.exports = (s,config,lang) => {
|
|||
s.debugLog(ffmpegCommand)
|
||||
activeMonitor.eventBasedRecording.process = spawn(
|
||||
config.ffmpegDir,
|
||||
splitForFFPMEG(ffmpegCommand)
|
||||
splitForFFMPEG(ffmpegCommand)
|
||||
)
|
||||
activeMonitor.eventBasedRecording.process.stderr.on('data',function(data){
|
||||
s.userLog(d,{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ module.exports = async (s,config,lang,onFinish) => {
|
|||
const {
|
||||
sanitizedFfmpegCommand,
|
||||
createPipeArray,
|
||||
splitForFFPMEG,
|
||||
splitForFFMPEG,
|
||||
checkForWindows,
|
||||
checkForUnix,
|
||||
checkStaticBuilds,
|
||||
|
|
@ -66,7 +66,7 @@ module.exports = async (s,config,lang,onFinish) => {
|
|||
//hold ffmpeg command for log stream
|
||||
activeMonitor.ffmpeg = sanitizedFfmpegCommand(e,ffmpegCommandString)
|
||||
//clean the string of spatial impurities and split for spawn()
|
||||
const ffmpegCommandParsed = splitForFFPMEG(ffmpegCommandString)
|
||||
const ffmpegCommandParsed = splitForFFMPEG(ffmpegCommandString)
|
||||
try{
|
||||
fs.rmSync(e.sdir + 'cmd.txt')
|
||||
}catch(err){
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ module.exports = (s,config,lang) => {
|
|||
activeProbes[auth] = 1
|
||||
var stderr = ''
|
||||
var stdout = ''
|
||||
const probeCommand = splitForFFPMEG(`${customInput ? customInput + ' ' : ''}-analyzeduration 10000 -probesize 10000 -v quiet -print_format json -show_format -show_streams -i "${url}"`)
|
||||
const probeCommand = splitForFFMPEG(`${customInput ? customInput + ' ' : ''}-analyzeduration 10000 -probesize 10000 -v quiet -print_format json -show_format -show_streams -i "${url}"`)
|
||||
var processTimeout = null
|
||||
var ffprobeLocation = config.ffmpegDir.split('/')
|
||||
ffprobeLocation[ffprobeLocation.length - 1] = 'ffprobe'
|
||||
|
|
@ -184,7 +184,7 @@ module.exports = (s,config,lang) => {
|
|||
}
|
||||
return stdioPipes
|
||||
}
|
||||
const splitForFFPMEG = function(ffmpegCommandAsString) {
|
||||
const splitForFFMPEG = function(ffmpegCommandAsString) {
|
||||
return ffmpegCommandAsString.replace(/\s+/g,' ').trim().match(/\\?.|^$/g).reduce((p, c) => {
|
||||
if(c === '"'){
|
||||
p.quote ^= 1;
|
||||
|
|
@ -378,7 +378,7 @@ Run "npm install ffbinaries" to get this static FFmpeg downloader.`
|
|||
validateDimensions: validateDimensions,
|
||||
sanitizedFfmpegCommand: sanitizedFfmpegCommand,
|
||||
createPipeArray: createPipeArray,
|
||||
splitForFFPMEG: splitForFFPMEG,
|
||||
splitForFFMPEG: splitForFFMPEG,
|
||||
checkForWindows: checkForWindows,
|
||||
checkForUnix: checkForUnix,
|
||||
checkForNpmStatic: checkForNpmStatic,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ module.exports = function(s,config,lang){
|
|||
asyncSetTimeout,
|
||||
} = require('./basic/utils.js')(process.cwd(),config)
|
||||
const {
|
||||
splitForFFPMEG,
|
||||
splitForFFMPEG,
|
||||
} = require('./ffmpeg/utils.js')(s,config,lang)
|
||||
const {
|
||||
processKill,
|
||||
|
|
@ -194,7 +194,7 @@ module.exports = function(s,config,lang){
|
|||
var iconImageFile = streamDir + 'icon.jpg'
|
||||
const snapRawFilters = monitor.details.cust_snap_raw
|
||||
if(snapRawFilters)outputOptions.push(snapRawFilters);
|
||||
var ffmpegCmd = splitForFFPMEG(`-y -loglevel warning ${isDetectorStream ? '-live_start_index 2' : ''} -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`)
|
||||
var ffmpegCmd = splitForFFMPEG(`-y -loglevel warning ${isDetectorStream ? '-live_start_index 2' : ''} -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`)
|
||||
try{
|
||||
await fs.promises.mkdir(streamDir, {recursive: true}, (err) => {s.debugLog(err)})
|
||||
}catch(err){
|
||||
|
|
@ -316,7 +316,7 @@ module.exports = function(s,config,lang){
|
|||
//not exist
|
||||
var cat = 'cat '+copiedItems.join(' ')+' > '+allts
|
||||
exec(cat,function(){
|
||||
var merger = spawn(config.ffmpegDir,splitForFFPMEG(('-re -i '+allts+' -acodec copy -vcodec copy -t 00:00:' + videoLength + ' '+pathDir+mergedFile)))
|
||||
var merger = spawn(config.ffmpegDir,splitForFFMPEG(('-re -i '+allts+' -acodec copy -vcodec copy -t 00:00:' + videoLength + ' '+pathDir+mergedFile)))
|
||||
merger.stderr.on('data',function(data){
|
||||
s.userLog(monitor,{type:"Buffer Merge",msg:data.toString()})
|
||||
})
|
||||
|
|
@ -405,7 +405,7 @@ module.exports = function(s,config,lang){
|
|||
ke: groupKey,
|
||||
mid: '$USER'
|
||||
},{type:lang['Videos Merge'],msg:mergedFile})
|
||||
var merger = spawn(config.ffmpegDir,splitForFFPMEG(('-re -loglevel warning -i ' + mergedRawFilepath + ' -acodec copy -vcodec copy ' + mergedFilepath)))
|
||||
var merger = spawn(config.ffmpegDir,splitForFFMPEG(('-re -loglevel warning -i ' + mergedRawFilepath + ' -acodec copy -vcodec copy ' + mergedFilepath)))
|
||||
merger.stderr.on('data',function(data){
|
||||
s.userLog({
|
||||
ke: groupKey,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ module.exports = (s,config,lang) => {
|
|||
createWarningsForConfiguration,
|
||||
buildMonitorConfigPartialFromWarnings,
|
||||
createPipeArray,
|
||||
splitForFFPMEG,
|
||||
splitForFFMPEG,
|
||||
sanitizedFfmpegCommand,
|
||||
} = require('../ffmpeg/utils.js')(s,config,lang)
|
||||
const {
|
||||
|
|
@ -220,7 +220,7 @@ module.exports = (s,config,lang) => {
|
|||
})
|
||||
}
|
||||
const temporaryImageFile = streamDir + s.gid(5) + '.jpg'
|
||||
const ffmpegCmd = splitForFFPMEG(`-y -loglevel warning -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`)
|
||||
const ffmpegCmd = splitForFFMPEG(`-y -loglevel warning -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`)
|
||||
const snapProcess = spawn('ffmpeg',ffmpegCmd,{detached: true})
|
||||
snapProcess.stderr.on('data',function(data){
|
||||
// s.debugLog(data.toString())
|
||||
|
|
@ -306,7 +306,7 @@ module.exports = (s,config,lang) => {
|
|||
});
|
||||
const ffmpegCommandString = ffmpegCommand.join(' ')
|
||||
activeMonitor.ffmpegSubstream = sanitizedFfmpegCommand(e,ffmpegCommandString)
|
||||
const ffmpegCommandParsed = splitForFFPMEG(ffmpegCommandString)
|
||||
const ffmpegCommandParsed = splitForFFMPEG(ffmpegCommandString)
|
||||
activeMonitor.subStreamChannel = channelNumber;
|
||||
s.userLog({
|
||||
ke: groupKey,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ module.exports = function (s, config, lang, getSnapshot) {
|
|||
s.debugLog(result);
|
||||
});
|
||||
})
|
||||
console.log(sendBody)
|
||||
} catch (err) {
|
||||
s.debugLog(err)
|
||||
s.userLog(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const unzipper = require('unzipper')
|
|||
const spawn = require('child_process').spawn
|
||||
const exec = require('child_process').execSync
|
||||
const treekill = require('tree-kill');
|
||||
const marked = require('marked').parse;
|
||||
const {
|
||||
Worker
|
||||
} = require('worker_threads');
|
||||
|
|
@ -342,6 +343,20 @@ module.exports = async (s,config,lang,app,io,currentUse) => {
|
|||
}
|
||||
})
|
||||
}
|
||||
async function getPluginReadme(name,asHTML){
|
||||
const modulePath = getModulePath(name)
|
||||
const readmePath = modulePath + 'README.md'
|
||||
let readmeData = lang['No README found']
|
||||
try{
|
||||
readmeData = await fs.promises.readFile(readmePath,'utf8')
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
if(asHTML){
|
||||
readmeData = marked(readmeData)
|
||||
}
|
||||
return readmeData
|
||||
}
|
||||
/**
|
||||
* API : Superuser : Custom Auto Load Package Download.
|
||||
*/
|
||||
|
|
@ -549,6 +564,16 @@ module.exports = async (s,config,lang,app,io,currentUse) => {
|
|||
s.closeJsonResponse(res,{ok: true})
|
||||
},res,req)
|
||||
})
|
||||
/**
|
||||
* API : Superuser : Get Plugin README
|
||||
*/
|
||||
app.get(config.webPaths.superApiPrefix+':auth/plugins/readme/:pluginName', async (req,res) => {
|
||||
s.superAuth(req.params, async (resp) => {
|
||||
const name = req.params.pluginName
|
||||
const readme = await getPluginReadme(name,true);
|
||||
s.closeJsonResponse(res,{ok: true, readme: readme})
|
||||
},res,req)
|
||||
})
|
||||
s.onProcessReady(async () => {
|
||||
// Initialize Modules on Start
|
||||
await initializeAllModules();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
sendTimelapseFrameToMasterNode,
|
||||
} = require('./childNode/childUtils.js')(s,config,lang)
|
||||
const {
|
||||
splitForFFPMEG,
|
||||
splitForFFMPEG,
|
||||
} = require('./ffmpeg/utils.js')(s,config,lang)
|
||||
const {
|
||||
getFileDirectory,
|
||||
|
|
@ -49,7 +49,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
size: fileStats.size,
|
||||
time: timeNow
|
||||
}
|
||||
if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){
|
||||
if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host && config.dropTimeLapseFrames != true){
|
||||
var currentDate = s.formattedTime(timeNow,'YYYY-MM-DD')
|
||||
const childNodeData = {
|
||||
ke: e.ke,
|
||||
|
|
@ -60,7 +60,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
queryInfo: queryInfo
|
||||
}
|
||||
sendTimelapseFrameToMasterNode(filePath,childNodeData)
|
||||
}else{
|
||||
}else if (config.dropTimeLapseFrames != true ){
|
||||
s.insertTimelapseFrameDatabaseRow(e,queryInfo,filePath)
|
||||
}
|
||||
}
|
||||
|
|
@ -239,7 +239,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
const numberOfFrames = framesAccepted.length
|
||||
const commandString = `-y -threads 1 -re -f concat -safe 0 -r ${framesPerSecond} -i "${concatListFile}" -q:v 1 -c:v libx264 -preset ultrafast -r ${framesPerSecond} "${finalMp4OutputLocation}"`
|
||||
s.debugLog("ffmpeg",commandString)
|
||||
const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString))
|
||||
const videoBuildProcess = spawn(config.ffmpegDir,splitForFFMPEG(commandString))
|
||||
videoBuildProcess.stdout.on('data',function(data){
|
||||
s.debugLog('stdout',finalMp4OutputLocation,data.toString())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ module.exports = function(s,config,lang){
|
|||
if(!userDetails.whcs_endpoint ){
|
||||
userDetails.whcs_endpoint = 's3.wasabisys.com'
|
||||
}
|
||||
if(userDetails.whcs_endpoint.indexOf('://') === -1){
|
||||
userDetails.whcs_endpoint = `https://${userDetails.whcs_endpoint}`
|
||||
}
|
||||
s.group[e.ke].whcs = new S3Client({
|
||||
endpoint: userDetails.whcs_endpoint,
|
||||
credentials: {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const async = require('async');
|
|||
module.exports = (s,config,lang) => {
|
||||
const {
|
||||
ffprobe,
|
||||
splitForFFPMEG,
|
||||
splitForFFMPEG,
|
||||
} = require('../ffmpeg/utils.js')(s,config,lang)
|
||||
const {
|
||||
copyFile,
|
||||
|
|
@ -257,7 +257,7 @@ module.exports = (s,config,lang) => {
|
|||
const finalMp4OutputLocation = options.output
|
||||
const commandString = `-y -threads 1 -f concat -safe 0 -i "${concatListFile}" -c:v copy -an -preset ultrafast "${finalMp4OutputLocation}"`
|
||||
s.debugLog("stitchMp4Files",commandString)
|
||||
const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString))
|
||||
const videoBuildProcess = spawn(config.ffmpegDir,splitForFFMPEG(commandString))
|
||||
videoBuildProcess.stdout.on('data',function(data){
|
||||
s.debugLog('stdout',finalMp4OutputLocation,data.toString())
|
||||
})
|
||||
|
|
@ -286,7 +286,7 @@ module.exports = (s,config,lang) => {
|
|||
const outputFilePath = `${videoFolder}${tempFilename}`
|
||||
const commandString = `-y -threads 1 -re -i "${inputFilePath}" -c:v copy -c:a copy -preset ultrafast "${outputFilePath}"`
|
||||
fixingAlready[fixingId] = true
|
||||
const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString))
|
||||
const videoBuildProcess = spawn(config.ffmpegDir,splitForFFMPEG(commandString))
|
||||
videoBuildProcess.stdout.on('data',function(data){
|
||||
s.debugLog('stdout',outputFilePath,data.toString())
|
||||
})
|
||||
|
|
@ -409,7 +409,7 @@ module.exports = (s,config,lang) => {
|
|||
oldName: filename,
|
||||
name: finalFilename,
|
||||
},'GRP_'+groupKey);
|
||||
const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString))
|
||||
const videoBuildProcess = spawn(config.ffmpegDir,splitForFFMPEG(commandString))
|
||||
videoBuildProcess.stdout.on('data',function(data){
|
||||
s.debugLog('stdout',outputFilePath,data.toString())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -29,13 +29,13 @@
|
|||
"jsonfile": "^3.0.1",
|
||||
"knex": "^0.21.21",
|
||||
"ldapauth-fork": "^5.0.2",
|
||||
"marked": "^4.3.0",
|
||||
"moment": "^2.29.4",
|
||||
"mp4frag": "^0.2.0",
|
||||
"mp4frag": "^0.6.0",
|
||||
"mqtt": "^4.3.7",
|
||||
"mysql": "^2.18.1",
|
||||
"node-abort-controller": "^3.0.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"shinobi-node-moving-things-tracker": "^0.9.1",
|
||||
"node-onvif-events": "^2.0.5",
|
||||
"node-ssh": "^12.0.4",
|
||||
"node-telegram-bot-api": "^0.58.0",
|
||||
|
|
@ -46,6 +46,7 @@
|
|||
"pixel-change": "^1.1.0",
|
||||
"pushover-notifications": "^1.2.2",
|
||||
"sat": "^0.7.1",
|
||||
"shinobi-node-moving-things-tracker": "^0.9.1",
|
||||
"shinobi-onvif": "0.1.9",
|
||||
"shinobi-sound-detection": "^0.1.13",
|
||||
"shinobi-zwave": "^1.0.11",
|
||||
|
|
@ -4837,6 +4838,17 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
|
||||
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/md5": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
|
||||
|
|
@ -4969,9 +4981,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/mp4frag": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mp4frag/-/mp4frag-0.2.0.tgz",
|
||||
"integrity": "sha512-zrLws5vFuUvaivVXu4ZPg7fdJynSbcIT6kI00okZ+jCvxqMIs6zhhh7sw16BE+lL1OD6RyCsFgJEdzxZaeb5fQ=="
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mp4frag/-/mp4frag-0.6.0.tgz",
|
||||
"integrity": "sha512-MvBAaWkW94SSpam/QsCmbMi7+ZY2YHzAjj6Uno7AZ6qxH7gZstN+L3jFopdN5F3/5mRK25gvA4k0DVpCbDe7+g==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt": {
|
||||
"version": "4.3.7",
|
||||
|
|
@ -5226,20 +5241,6 @@
|
|||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-moving-things-tracker": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/node-moving-things-tracker/-/node-moving-things-tracker-0.9.1.tgz",
|
||||
"integrity": "sha512-JVa+DbQRgOsOcfIIxhw3kTUfO407RdXnVqNgPvAlhUHu7PWziUah+MuTcaN4rRktBicj/l2scI64a2crBgrzKw==",
|
||||
"dependencies": {
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"minimist": "^1.2.0",
|
||||
"munkres-js": "^1.2.2",
|
||||
"uuid": "^3.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"shinobi-node-moving-things-tracker": "main.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-onvif-events": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/node-onvif-events/-/node-onvif-events-2.0.5.tgz",
|
||||
|
|
@ -6329,6 +6330,19 @@
|
|||
"resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz",
|
||||
"integrity": "sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM="
|
||||
},
|
||||
"node_modules/shinobi-node-moving-things-tracker": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/shinobi-node-moving-things-tracker/-/shinobi-node-moving-things-tracker-0.9.1.tgz",
|
||||
"integrity": "sha512-pcI/IJ9D87RJiTGEsBYvtb2FTQXkHCkuyv6dYaXB5KVpnhQJGyf37BvAbvCNuCJmlffBGTMClY2dg84ZwHl/Ow==",
|
||||
"dependencies": {
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"minimist": "^1.2.0",
|
||||
"munkres-js": "^1.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"shinobi-node-moving-things-tracker": "main.js"
|
||||
}
|
||||
},
|
||||
"node_modules/shinobi-onvif": {
|
||||
"version": "0.1.9",
|
||||
"resolved": "https://registry.npmjs.org/shinobi-onvif/-/shinobi-onvif-0.1.9.tgz",
|
||||
|
|
@ -11490,6 +11504,11 @@
|
|||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"marked": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
|
||||
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="
|
||||
},
|
||||
"md5": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
|
||||
|
|
@ -11589,9 +11608,9 @@
|
|||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||
},
|
||||
"mp4frag": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mp4frag/-/mp4frag-0.2.0.tgz",
|
||||
"integrity": "sha512-zrLws5vFuUvaivVXu4ZPg7fdJynSbcIT6kI00okZ+jCvxqMIs6zhhh7sw16BE+lL1OD6RyCsFgJEdzxZaeb5fQ=="
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mp4frag/-/mp4frag-0.6.0.tgz",
|
||||
"integrity": "sha512-MvBAaWkW94SSpam/QsCmbMi7+ZY2YHzAjj6Uno7AZ6qxH7gZstN+L3jFopdN5F3/5mRK25gvA4k0DVpCbDe7+g=="
|
||||
},
|
||||
"mqtt": {
|
||||
"version": "4.3.7",
|
||||
|
|
@ -11782,17 +11801,6 @@
|
|||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="
|
||||
},
|
||||
"shinobi-node-moving-things-tracker": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/node-moving-things-tracker/-/node-moving-things-tracker-0.9.1.tgz",
|
||||
"integrity": "sha512-JVa+DbQRgOsOcfIIxhw3kTUfO407RdXnVqNgPvAlhUHu7PWziUah+MuTcaN4rRktBicj/l2scI64a2crBgrzKw==",
|
||||
"requires": {
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"minimist": "^1.2.0",
|
||||
"munkres-js": "^1.2.2",
|
||||
"uuid": "^3.2.1"
|
||||
}
|
||||
},
|
||||
"node-onvif-events": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/node-onvif-events/-/node-onvif-events-2.0.5.tgz",
|
||||
|
|
@ -12643,6 +12651,16 @@
|
|||
"resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz",
|
||||
"integrity": "sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM="
|
||||
},
|
||||
"shinobi-node-moving-things-tracker": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/shinobi-node-moving-things-tracker/-/shinobi-node-moving-things-tracker-0.9.1.tgz",
|
||||
"integrity": "sha512-pcI/IJ9D87RJiTGEsBYvtb2FTQXkHCkuyv6dYaXB5KVpnhQJGyf37BvAbvCNuCJmlffBGTMClY2dg84ZwHl/Ow==",
|
||||
"requires": {
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"minimist": "^1.2.0",
|
||||
"munkres-js": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"shinobi-onvif": {
|
||||
"version": "0.1.9",
|
||||
"resolved": "https://registry.npmjs.org/shinobi-onvif/-/shinobi-onvif-0.1.9.tgz",
|
||||
|
|
|
|||
|
|
@ -35,13 +35,13 @@
|
|||
"jsonfile": "^3.0.1",
|
||||
"knex": "^0.21.21",
|
||||
"ldapauth-fork": "^5.0.2",
|
||||
"marked": "^4.3.0",
|
||||
"moment": "^2.29.4",
|
||||
"mp4frag": "^0.2.0",
|
||||
"mp4frag": "^0.6.0",
|
||||
"mqtt": "^4.3.7",
|
||||
"mysql": "^2.18.1",
|
||||
"node-abort-controller": "^3.0.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"shinobi-node-moving-things-tracker": "^0.9.1",
|
||||
"node-onvif-events": "^2.0.5",
|
||||
"node-ssh": "^12.0.4",
|
||||
"node-telegram-bot-api": "^0.58.0",
|
||||
|
|
@ -52,6 +52,7 @@
|
|||
"pixel-change": "^1.1.0",
|
||||
"pushover-notifications": "^1.2.2",
|
||||
"sat": "^0.7.1",
|
||||
"shinobi-node-moving-things-tracker": "^0.9.1",
|
||||
"shinobi-onvif": "0.1.9",
|
||||
"shinobi-sound-detection": "^0.1.13",
|
||||
"shinobi-zwave": "^1.0.11",
|
||||
|
|
|
|||
|
|
@ -135,14 +135,14 @@ module.exports = function(s,config,lang,io){
|
|||
}
|
||||
},
|
||||
"ffmpeg.js" : {
|
||||
splitForFFPMEG : function(next){
|
||||
splitForFFMPEG : function(next){
|
||||
var expectedResult = [
|
||||
'flag1',
|
||||
'flag2',
|
||||
'fl ag3',
|
||||
]
|
||||
var testResult = s.splitForFFPMEG('flag1 flag2 "fl ag3"')
|
||||
checkResult('Internal Function : splitForFFPMEG',JSON.stringify(expectedResult),JSON.stringify(testResult))
|
||||
var testResult = s.splitForFFMPEG('flag1 flag2 "fl ag3"')
|
||||
checkResult('Internal Function : splitForFFMPEG',JSON.stringify(expectedResult),JSON.stringify(testResult))
|
||||
next()
|
||||
},
|
||||
"ffmpeg" : function(next){
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
#powerVideo .videoPlayer .videoPlayer-detection-info {
|
||||
position: absolute;
|
||||
padding: 20px 10px 20px 10px;
|
||||
/* height: 100%; */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
@ -183,3 +183,9 @@
|
|||
#powerVideo .vis-labelset .vis-label {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#powerVideo .videoPlayer-detection-info-buttons {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,3 +7,8 @@
|
|||
border-radius: 5px;
|
||||
max-height: 300px;
|
||||
}
|
||||
.readme-view {
|
||||
padding: 1rem 2rem;
|
||||
border: 1px solid #009dff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -875,11 +875,17 @@ function downloadJSON(jsonData,filename){
|
|||
.attr('download',filename)
|
||||
[0].click()
|
||||
}
|
||||
function downloadFile(downloadUrl,fileName){
|
||||
var a = document.createElement('a')
|
||||
a.href = downloadUrl
|
||||
a.download = fileName
|
||||
a.click()
|
||||
function downloadFile(url,filename) {
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
function getFilenameFromUrl(url) {
|
||||
const parts = url.split("/");
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
function notifyIfActionFailed(data){
|
||||
if(data.ok === false){
|
||||
|
|
@ -986,7 +992,7 @@ function convertJsonToAccordionHtml(theJson){
|
|||
keys.forEach((key) => {
|
||||
var value = innerJson[key]
|
||||
var isObject = typeof value === 'object' || typeof value === 'array'
|
||||
if(value)html += `<li><a class="badge btn btn-sm ${isObject ? `toggle-accordion-list btn-primary` : `btn-default`}"><i class="fa fa-${isObject ? `plus` : `circle`}"></i></a> ${key} ${isObject ? recurseJson(value,true) : `: ${value}`}</li>`
|
||||
if(value === 0 || value === false || value)html += `<li><a class="badge btn btn-sm ${isObject ? `toggle-accordion-list btn-primary` : `btn-default`}"><i class="fa fa-${isObject ? `plus` : `circle`}"></i></a> ${key} ${isObject ? recurseJson(value,true) : `: ${value}`}</li>`
|
||||
})
|
||||
html += `</ul>`
|
||||
return html
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ function generateDefaultMonitorSettings(){
|
|||
"hwaccel_vcodec": "",
|
||||
"hwaccel_device": "",
|
||||
"use_coprocessor": null,
|
||||
"stream_type": "hls",
|
||||
"stream_flv_type": "ws",
|
||||
"stream_type": "mp4",
|
||||
"stream_flv_type": "http",
|
||||
"stream_flv_maxLatency": "",
|
||||
"stream_mjpeg_clients": "",
|
||||
"stream_vcodec": "copy",
|
||||
|
|
|
|||
|
|
@ -456,12 +456,7 @@ $(document).ready(function(e){
|
|||
motionMeterProgressBarTextBox.text('0')
|
||||
}
|
||||
function resetWidthForActiveVideoPlayers(){
|
||||
var numberOfMonitors = 0
|
||||
powerVideoMonitorViewsElement.find(`.videoPlayer .videoNow`).each(function(n,videoEl){
|
||||
if(videoEl.currentTime > 0)numberOfMonitors += 1
|
||||
})
|
||||
var widthOfBlock = 100 / numberOfMonitors
|
||||
powerVideoMonitorViewsElement.find('.videoPlayer').css('width',`${widthOfBlock}%`)
|
||||
powerVideoMonitorViewsElement.find('.videoPlayer').css('width',`49.8%`)
|
||||
}
|
||||
function loadVideoIntoMonitorSlot(video,selectedTime){
|
||||
if(!video)return
|
||||
|
|
@ -472,12 +467,15 @@ $(document).ready(function(e){
|
|||
// if(numberOfMonitors > 3)numberOfMonitors = 3 //start new row after 3
|
||||
if(numberOfMonitors == 1)numberOfMonitors = 2 //make single monitor not look like a doofus
|
||||
if(timeToStartAt < 0)timeToStartAt = 0
|
||||
var widthOfBlock = 100 / numberOfMonitors
|
||||
var videoContainer = powerVideoMonitorViewsElement.find(`.videoPlayer[data-mid=${video.mid}] .videoPlayer-buffers`)
|
||||
if(videoContainer.length === 0){
|
||||
if(!monitorSlotPlaySpeeds)monitorSlotPlaySpeeds[video.mid] = {}
|
||||
powerVideoMonitorViewsElement.append(`<div class="videoPlayer" style="width:${widthOfBlock}%;max-width:500px" data-mid="${video.mid}">
|
||||
powerVideoMonitorViewsElement.append(`<div class="videoPlayer" style="width:49.8%;max-width:500px;min-width:250px;" data-mid="${video.mid}">
|
||||
<div class="videoPlayer-detection-info">
|
||||
<div class="videoPlayer-detection-info-buttons btn-group">
|
||||
<a powerVideo-control="downloadVideo" class="btn btn-sm btn-success"><i class="fa fa-download"></i></a>
|
||||
<a powerVideo-control="openVideoPlayer" class="btn btn-sm btn-default"><i class="fa fa-external-link"></i></a>
|
||||
</div>
|
||||
<canvas style="height:400px"></canvas>
|
||||
</div>
|
||||
<div class="videoPlayer-stream-objects"></div>
|
||||
|
|
@ -488,7 +486,7 @@ $(document).ready(function(e){
|
|||
</div>`)
|
||||
videoContainer = powerVideoMonitorViewsElement.find(`.videoPlayer[data-mid=${video.mid}] .videoPlayer-buffers`)
|
||||
}else{
|
||||
powerVideoMonitorViewsElement.find('.videoPlayer').css('width',`${widthOfBlock}%`)
|
||||
powerVideoMonitorViewsElement.find('.videoPlayer').css('width',`49.8%`)
|
||||
}
|
||||
var videoCurrentNow = videoContainer.find('.videoNow')
|
||||
var videoCurrentAfter = videoContainer.find('.videoAfter')
|
||||
|
|
@ -572,9 +570,15 @@ $(document).ready(function(e){
|
|||
var selectedMonitors = Object.keys(form).filter(key => form[key] == '1')
|
||||
return selectedMonitors
|
||||
}
|
||||
function getActiveVideoInSlot(monitorId){
|
||||
return powerVideoMonitorViewsElement.find(`.videoPlayer[data-mid="${monitorId}"] video.videoNow`)[0]
|
||||
}
|
||||
function getAllActiveVideosInSlots(){
|
||||
return powerVideoMonitorViewsElement.find('video.videoNow')
|
||||
}
|
||||
function getActiveVideoRow(monitorId){
|
||||
return currentlyPlayingVideos[monitorId]
|
||||
}
|
||||
function pauseAllSlots(){
|
||||
getAllActiveVideosInSlots().each(function(n,video){
|
||||
if(!video.paused)video.pause()
|
||||
|
|
@ -675,6 +679,27 @@ $(document).ready(function(e){
|
|||
})
|
||||
lastPowerVideoSelectedMonitors = ([]).concat(monitorIdsSelectedNow || [])
|
||||
}
|
||||
function downloadPlayingVideo(video){
|
||||
if(video.currentSrc){
|
||||
var filename = getFilenameFromUrl(video.currentSrc)
|
||||
downloadFile(video.currentSrc,filename)
|
||||
}
|
||||
}
|
||||
function downloadAllPlayingVideos(){
|
||||
getAllActiveVideosInSlots().each(function(n,video){
|
||||
downloadPlayingVideo(video)
|
||||
})
|
||||
}
|
||||
function openVideoPlayerTabFromViewer(el){
|
||||
var monitorId = el.attr('data-mid') || el.parents('[data-mid]').attr('data-mid')
|
||||
var video = getActiveVideoRow(monitorId)
|
||||
createVideoPlayerTab(video)
|
||||
}
|
||||
function downloadPlayingVideoTabFromViewer(el){
|
||||
var monitorId = el.attr('data-mid') || el.parents('[data-mid]').attr('data-mid')
|
||||
var video = getActiveVideoInSlot(monitorId)
|
||||
downloadPlayingVideo(video)
|
||||
}
|
||||
powerVideoMonitorsListElement.on('change','input',onPowerVideoSettingsChange);
|
||||
powerVideoVideoLimitElement.change(onPowerVideoSettingsChange);
|
||||
powerVideoEventLimitElement.change(onPowerVideoSettingsChange);
|
||||
|
|
@ -689,6 +714,17 @@ $(document).ready(function(e){
|
|||
var el = $(this)
|
||||
var controlType = el.attr('powerVideo-control')
|
||||
switch(controlType){
|
||||
// single video affected
|
||||
case'downloadVideo':
|
||||
downloadPlayingVideoTabFromViewer(el)
|
||||
break;
|
||||
case'openVideoPlayer':
|
||||
openVideoPlayerTabFromViewer(el)
|
||||
break;
|
||||
// all videos affected
|
||||
case'downloadPlaying':
|
||||
downloadAllPlayingVideos()
|
||||
break;
|
||||
case'toggleMute':
|
||||
toggleMute()
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,321 +1,376 @@
|
|||
$(document).ready(function(){
|
||||
var schema = {
|
||||
"title": "Main Configuration",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"debugLog": {
|
||||
$(document).ready(function () {
|
||||
var schema = {
|
||||
"title": "Main Configuration",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"debugLog": {
|
||||
"title": "Enable Debug Log",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"subscriptionId": {
|
||||
"title": "Fill in subscription ID",
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"port": {
|
||||
"title": "Server port",
|
||||
"type": "integer",
|
||||
"default": 8080
|
||||
},
|
||||
"passwordType": {
|
||||
"title": "Password type",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sha256",
|
||||
"sha512",
|
||||
"md5"
|
||||
],
|
||||
"default": "sha256"
|
||||
},
|
||||
"addStorage": {
|
||||
"type": "array",
|
||||
"format": "table",
|
||||
"title": "Additional Storage",
|
||||
"description": "Separate storage locations that can be set for different monitors.",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Storage Array",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"default": "__DIR__/videos2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": [
|
||||
{
|
||||
"name": "second",
|
||||
"path": "__DIR__/videos2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"type": "array",
|
||||
"format": "table",
|
||||
"title": "Plugins",
|
||||
"descripton": "Elaborate Plugin connection settings.",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Plugin",
|
||||
"properties": {
|
||||
"plug": {
|
||||
"type": "string",
|
||||
"default": "pluginName"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"host",
|
||||
"client"
|
||||
],
|
||||
"default": "client"
|
||||
},
|
||||
"https": {
|
||||
"type": "boolean",
|
||||
"descripton": "Only for Host mode.",
|
||||
"default": false
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"descripton": "Only for Host mode.",
|
||||
"default": "localhost"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"descripton": "Only for Host mode.",
|
||||
"default": 8082
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "detector"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pluginKeys": {
|
||||
"type": "object",
|
||||
"format": "table",
|
||||
"title": "Plugin Keys",
|
||||
"description": "Quick client connection setup for plugins. Just add the plugin key to make it ready for incoming connections.",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Plugin Key",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"db": {
|
||||
"type": "object",
|
||||
"format": "table",
|
||||
"title": "Database Options",
|
||||
"description": "Credentials to connect to where detailed information is stored.",
|
||||
"properties": {
|
||||
"host": {
|
||||
"title": "Hostname / IP",
|
||||
"type": "string",
|
||||
"default": "127.0.0.1"
|
||||
},
|
||||
"user": {
|
||||
"title": "Username",
|
||||
"type": "string",
|
||||
"default": "majesticflame"
|
||||
},
|
||||
"password": {
|
||||
"title": "Password",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"database": {
|
||||
"type": "string",
|
||||
"default": "ccio"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"default": 3306
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"host": "127.0.0.1",
|
||||
"user": "majesticflame",
|
||||
"password": "",
|
||||
"database": "ccio",
|
||||
"port": 3306
|
||||
}
|
||||
},
|
||||
"cron": {
|
||||
"type": "object",
|
||||
"format": "table",
|
||||
"title": "CRON Options",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
},
|
||||
"deleteOld": {
|
||||
"type": "boolean",
|
||||
"description": "cron will delete videos older than Max Number of Days per account.",
|
||||
"default": true
|
||||
},
|
||||
"deleteNoVideo": {
|
||||
"type": "boolean",
|
||||
"description": "cron will delete SQL rows that it thinks have no video files.",
|
||||
"default": true
|
||||
},
|
||||
"deleteOverMax": {
|
||||
"type": "boolean",
|
||||
"description": "cron will delete files that are over the set maximum storage per account.",
|
||||
"default": true
|
||||
},
|
||||
}
|
||||
},
|
||||
"mail": {
|
||||
"type": "object",
|
||||
"format": "table",
|
||||
"title": "Email Options",
|
||||
"properties": {
|
||||
"service": {
|
||||
"type": "string",
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "string",
|
||||
},
|
||||
"pass": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"secure": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"subscriptionId": {
|
||||
"type": "string",
|
||||
"ignoreTLS": {
|
||||
"type": "boolean",
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"default": 8080
|
||||
},
|
||||
"passwordType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sha256",
|
||||
"sha512",
|
||||
"md5"
|
||||
],
|
||||
"default": "sha256"
|
||||
},
|
||||
"addStorage": {
|
||||
"type": "array",
|
||||
"format": "table",
|
||||
"title": "Additional Storage",
|
||||
"description": "Separate storage locations that can be set for different monitors.",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Storage Array",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"default": "__DIR__/videos2"
|
||||
}
|
||||
}
|
||||
"requireTLS": {
|
||||
"type": "boolean",
|
||||
},
|
||||
"default": [
|
||||
{
|
||||
"name": "second",
|
||||
"path": "__DIR__/videos2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"type": "array",
|
||||
"format": "table",
|
||||
"title": "Plugins",
|
||||
"descripton": "Elaborate Plugin connection settings.",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Plugin",
|
||||
"properties": {
|
||||
"plug": {
|
||||
"type": "string",
|
||||
"default": "pluginName"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"host",
|
||||
"client"
|
||||
],
|
||||
"default": "client"
|
||||
},
|
||||
"https": {
|
||||
"type": "boolean",
|
||||
"descripton": "Only for Host mode.",
|
||||
"default": false
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"descripton": "Only for Host mode.",
|
||||
"default": "localhost"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"descripton": "Only for Host mode.",
|
||||
"default": 8082
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "detector"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": [
|
||||
{
|
||||
"name": "second",
|
||||
"path": "__DIR__/videos2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginKeys": {
|
||||
"type": "object",
|
||||
"format": "table",
|
||||
"title": "Plugin Keys",
|
||||
"description": "Quick client connection setup for plugins. Just add the plugin key to make it ready for incoming connections.",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Plugin Key",
|
||||
"properties": {}
|
||||
"port": {
|
||||
"type": "integer",
|
||||
}
|
||||
},
|
||||
"db": {
|
||||
"type": "object",
|
||||
"format": "table",
|
||||
"title": "Database Options",
|
||||
"description": "Credentials to connect to where detailed information is stored.",
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string",
|
||||
"default": "127.0.0.1"
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
"default": "majesticflame"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"database": {
|
||||
"type": "string",
|
||||
"default": "ccio"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"default": 3306
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"host": "127.0.0.1",
|
||||
"user": "majesticflame",
|
||||
"password": "",
|
||||
"database": "ccio",
|
||||
"port":3306
|
||||
}
|
||||
},
|
||||
"cron": {
|
||||
"type": "object",
|
||||
"format": "table",
|
||||
"title": "CRON Options",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
},
|
||||
"deleteOld": {
|
||||
"type": "boolean",
|
||||
"description": "cron will delete videos older than Max Number of Days per account.",
|
||||
"default": true
|
||||
},
|
||||
"deleteNoVideo": {
|
||||
"type": "boolean",
|
||||
"description": "cron will delete SQL rows that it thinks have no video files.",
|
||||
"default": true
|
||||
},
|
||||
"deleteOverMax": {
|
||||
"type": "boolean",
|
||||
"description": "cron will delete files that are over the set maximum storage per account.",
|
||||
"default": true
|
||||
},
|
||||
}
|
||||
},
|
||||
"mail": {
|
||||
"type": "object",
|
||||
"format": "table",
|
||||
"title": "Email Options",
|
||||
"properties": {
|
||||
"service": {
|
||||
"type": "string",
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "string",
|
||||
},
|
||||
"pass": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"secure": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"ignoreTLS": {
|
||||
"type": "boolean",
|
||||
},
|
||||
"requireTLS": {
|
||||
"type": "boolean",
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
}
|
||||
}
|
||||
},
|
||||
"detectorMergePamRegionTriggers": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"doSnapshot": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"discordBot": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"dropInEventServer": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"ftpServer": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"oldPowerVideo": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"wallClockTimestampAsDefault": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"defaultMjpeg": {
|
||||
"type": "string",
|
||||
},
|
||||
"streamDir": {
|
||||
"type": "string",
|
||||
},
|
||||
"videosDir": {
|
||||
"type": "string",
|
||||
},
|
||||
"windowsTempDir": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"detectorMergePamRegionTriggers": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"doSnapshot": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"discordBot": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"dropInEventServer": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"ftpServer": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"oldPowerVideo": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"wallClockTimestampAsDefault": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"defaultMjpeg": {
|
||||
"type": "string",
|
||||
},
|
||||
"streamDir": {
|
||||
"type": "string",
|
||||
},
|
||||
"videosDir": {
|
||||
"type": "string",
|
||||
},
|
||||
"windowsTempDir": {
|
||||
"type": "string",
|
||||
},
|
||||
"enableFaceManager": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"title": "Enable Face Manager",
|
||||
"description": "Enable / Disable face manager for face recognition plugins in the dashboard."
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var configurationTab = $('#config')
|
||||
var configurationForm = configurationTab.find('form')
|
||||
const configurationTab = $("#config");
|
||||
const configurationForm = configurationTab.find("form");
|
||||
|
||||
// Set default options
|
||||
JSONEditor.defaults.options.theme = 'bootstrap3';
|
||||
JSONEditor.defaults.options.iconlib = 'fontawesome4';
|
||||
const moduleData = {
|
||||
endpoint: null,
|
||||
configurationEditor: null
|
||||
}
|
||||
|
||||
// Initialize the editor
|
||||
var configurationEditor = new JSONEditor(document.getElementById("configForHumans"),{
|
||||
theme: 'bootstrap3',
|
||||
schema: schema
|
||||
const handleGetConfigurationData = data => {
|
||||
const dataConfig = data.config;
|
||||
const dataConfigKeys = Object.keys(dataConfig);
|
||||
const schemaItemsKeys = Object.keys(schema.properties);
|
||||
|
||||
const schemaWithoutData = schemaItemsKeys.filter(
|
||||
(sk) => !dataConfigKeys.includes(sk)
|
||||
);
|
||||
const dataWithoutSchema = dataConfigKeys.filter(
|
||||
(dk) => !schemaItemsKeys.includes(dk)
|
||||
);
|
||||
|
||||
schemaWithoutData.forEach((sk) => {
|
||||
const schemaItem = schema.properties[sk];
|
||||
const defaultConfig = schemaItem.default;
|
||||
|
||||
data.config[sk] = defaultConfig;
|
||||
});
|
||||
|
||||
function loadConfiguationIntoEditor(){
|
||||
$.get(superApiPrefix + $user.sessionKey + '/system/configure',function(data){
|
||||
configurationEditor.setValue(data.config);
|
||||
})
|
||||
if (dataWithoutSchema.length > 0) {
|
||||
dataWithoutSchema.forEach((dk) => {
|
||||
const schemaItem = {
|
||||
title: dk,
|
||||
options: {
|
||||
hidden: true,
|
||||
},
|
||||
};
|
||||
|
||||
schema.properties[dk] = schemaItem;
|
||||
});
|
||||
|
||||
// Set default options
|
||||
JSONEditor.defaults.options.theme = "bootstrap3";
|
||||
JSONEditor.defaults.options.iconlib = "fontawesome4";
|
||||
}
|
||||
// configurationEditor.on("change", function() {
|
||||
// // Do something...
|
||||
// });
|
||||
var submitConfiguration = function(){
|
||||
var errors = configurationEditor.validate();
|
||||
console.log(errors.length)
|
||||
console.log(errors)
|
||||
if(errors.length === 0) {
|
||||
var newConfiguration = JSON.stringify(configurationEditor.getValue(),null,3)
|
||||
var html = '<p>This is a change being applied to the configuration file (conf.json). Are you sure you want to do this? You must restart Shinobi for these changes to take effect. <b>The JSON below is what you are about to save.</b></p>'
|
||||
html += `<pre>${newConfiguration}</pre>`
|
||||
$.confirm.create({
|
||||
title: 'Save Configuration',
|
||||
body: html,
|
||||
clickOptions: {
|
||||
class: 'btn-success',
|
||||
title: lang.Save,
|
||||
},
|
||||
clickCallback: function(){
|
||||
$.post(superApiPrefix + $user.sessionKey + '/system/configure',{
|
||||
data: newConfiguration
|
||||
},function(data){
|
||||
// console.log(data)
|
||||
})
|
||||
}
|
||||
})
|
||||
}else{
|
||||
new PNotify({text:'Invalid JSON Syntax, Cannot Save.',type:'error'})
|
||||
}
|
||||
|
||||
const configurationEditor = new JSONEditor(
|
||||
document.getElementById("configForHumans"), {
|
||||
schema: schema,
|
||||
}
|
||||
configurationTab.find('.submit').click(function(){
|
||||
submitConfiguration()
|
||||
})
|
||||
configurationForm.submit(function(e){
|
||||
e.preventDefault()
|
||||
submitConfiguration()
|
||||
return false;
|
||||
})
|
||||
$.ccio.ws.on('f',function(d){
|
||||
switch(d.f){
|
||||
case'init_success':
|
||||
loadConfiguationIntoEditor()
|
||||
break;
|
||||
);
|
||||
|
||||
configurationEditor.setValue(data.config);
|
||||
|
||||
moduleData.configurationEditor = configurationEditor;
|
||||
window.configurationEditor = configurationEditor;
|
||||
};
|
||||
|
||||
const handlePostConfigurationData = data => {
|
||||
// console.log(data);
|
||||
}
|
||||
|
||||
function loadConfiguationIntoEditor(d) {
|
||||
moduleData.endpoint = `${superApiPrefix}${$user.sessionKey}/system/configure`;
|
||||
|
||||
$.get(moduleData.endpoint, handleGetConfigurationData);
|
||||
}
|
||||
|
||||
var submitConfiguration = function () {
|
||||
var errors = configurationEditor.validate();
|
||||
console.log(errors.length)
|
||||
console.log(errors)
|
||||
if (errors.length === 0) {
|
||||
var newConfiguration = JSON.stringify(configurationEditor.getValue(), null, 3)
|
||||
var html = '<p>This is a change being applied to the configuration file (conf.json). Are you sure you want to do this? You must restart Shinobi for these changes to take effect. <b>The JSON below is what you are about to save.</b></p>'
|
||||
html += `<pre>${newConfiguration}</pre>`
|
||||
$.confirm.create({
|
||||
title: 'Save Configuration',
|
||||
body: html,
|
||||
clickOptions: {
|
||||
class: 'btn-success',
|
||||
title: lang.Save,
|
||||
},
|
||||
clickCallback: function () {
|
||||
const requestData = {
|
||||
data: newConfiguration
|
||||
};
|
||||
|
||||
$.post(moduleData.endpoint, requestData, handlePostConfigurationData);
|
||||
}
|
||||
})
|
||||
window.configurationEditor = configurationEditor
|
||||
})
|
||||
} else {
|
||||
new PNotify({ text: 'Invalid JSON Syntax, Cannot Save.', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
configurationTab.find('.submit').click(function () {
|
||||
submitConfiguration();
|
||||
});
|
||||
|
||||
configurationForm.submit(function (e) {
|
||||
e.preventDefault();
|
||||
submitConfiguration();
|
||||
return false;
|
||||
});
|
||||
|
||||
$.ccio.ws.on("f", d => {
|
||||
if (d.f === "init_success") {
|
||||
loadConfiguationIntoEditor();
|
||||
}
|
||||
});
|
||||
|
||||
window.configurationEditor = configurationEditor;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ $(document).ready(function(){
|
|||
<div class="pb-2"><b>${lang['Time Created']} :</b> ${module.created}</div>
|
||||
<div class="pb-2"><b>${lang['Last Modified']} :</b> ${module.lastModified}</div>
|
||||
<div class="mb-2">
|
||||
<a class="btn btn-sm btn-default" plugin-manager-action="readmeToggle">${lang.Notes}</a>
|
||||
${module.hasInstaller ? `
|
||||
<a class="btn btn-sm btn-info" plugin-manager-action="install">${lang['Run Installer']}</a>
|
||||
<a class="btn btn-sm btn-danger" style="display:none" plugin-manager-action="cancelInstall">${lang['Stop']}</a>
|
||||
|
|
@ -74,6 +75,7 @@ $(document).ready(function(){
|
|||
<button type="button" class="btn btn-sm btn-danger btn-block" plugin-manager-action="command" command="N">${lang.No}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="readme-view d-none"></div>
|
||||
</div>
|
||||
</div>`)
|
||||
var newBlock = $(`.card[package-name="${module.name}"]`)
|
||||
|
|
@ -186,6 +188,24 @@ $(document).ready(function(){
|
|||
objDiv.scrollTop = objDiv.scrollHeight;
|
||||
},100)
|
||||
}
|
||||
function getReadme(packageName){
|
||||
return new Promise(function(resolve){
|
||||
$.get(`${superApiPrefix}${$user.sessionKey}/plugins/readme/${packageName}`,function(data){
|
||||
if(!data.ok)console.error(data);
|
||||
resolve(data.readme || '')
|
||||
})
|
||||
})
|
||||
}
|
||||
async function toggleReadme(packageName){
|
||||
var readmeView = loadedBlocks[packageName].block.find('.readme-view')
|
||||
var isHidden = readmeView.hasClass('d-none')
|
||||
if(isHidden){
|
||||
var readmeHTML = await getReadme(packageName)
|
||||
readmeView.removeClass('d-none').html(readmeHTML)
|
||||
}else{
|
||||
readmeView.addClass('d-none').empty()
|
||||
}
|
||||
}
|
||||
$('body')
|
||||
.on(`submit`,`[plugin-manager-command-line]`,function(e){
|
||||
e.preventDefault()
|
||||
|
|
@ -206,6 +226,9 @@ $(document).ready(function(){
|
|||
var card = el.parents('[package-name]')
|
||||
var packageName = card.attr('package-name')
|
||||
switch(action){
|
||||
case'readmeToggle':
|
||||
toggleReadme(packageName)
|
||||
break;
|
||||
case'run':
|
||||
var scriptName = el.attr('data-script')
|
||||
runModuleCommand(packageName,scriptName,function(data){
|
||||
|
|
|
|||
Loading…
Reference in New Issue