1097 lines
38 KiB
JavaScript
1097 lines
38 KiB
JavaScript
|
|
// SPX server functions
|
|
|
|
const logger = require('./logger.js');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const moment = require('moment');
|
|
const wavplayer = require('node-wav-player');
|
|
const PlayoutCCG = require('./playout_casparCG.js');
|
|
const glob = require("glob");
|
|
const ip = require('ip')
|
|
const axios = require('axios')
|
|
const http = require('http');
|
|
// ----------------------------------------- good until here
|
|
// const { config } = require('process');
|
|
|
|
if (!config.general) {
|
|
console.error('\n\n****\n\tDEV NOTE:\n\tCheck spx_server_functions.js file\n\theader for invalid imports\n***\n\n')
|
|
// This may happen is VSC adds phantom require statements above.
|
|
// How the f**k can I prevent that!?
|
|
}
|
|
|
|
const port = config.general.port || 5656;
|
|
let crypto;
|
|
try {
|
|
crypto = require('crypto');
|
|
} catch (err) {
|
|
logger.warn('Crypto support is disabled!');
|
|
}
|
|
|
|
module.exports = {
|
|
|
|
getUpTime: function() {
|
|
try {
|
|
let uptimeSeconds = process.uptime();
|
|
let d = String(Math.floor(uptimeSeconds / (3600 * 24))).padStart(2, '0');
|
|
let h = String(
|
|
Math.floor((uptimeSeconds % (3600 * 24)) / 3600),
|
|
).padStart(2, '0');
|
|
let m = String(Math.floor((uptimeSeconds % 3600) / 60)).padStart(2, '0');
|
|
let s = String(Math.floor(uptimeSeconds % 60)).padStart(2, '0');
|
|
let ddhhmmss = d + 'd ' + h + 'h ' + m + 'm ' + s + 's';
|
|
return [uptimeSeconds, ddhhmmss];
|
|
} catch (error) {
|
|
logger.error('Error in getUpTime: ' + error);
|
|
return 0;
|
|
}
|
|
}, // getUpTime
|
|
|
|
max5: function (num) {
|
|
let res = Math.min(5, parseInt(num));
|
|
return String(res);
|
|
},
|
|
|
|
appendProjectFile: async function (rundownData, fullpath, referrer='') {
|
|
// Added in 1.3.2
|
|
// Append project and rundown info to the rundownData object
|
|
// Yeah, this could be fancier.
|
|
if (referrer) {
|
|
// console.log('DEV - appendProjectFile project/filename ' + fullpath);
|
|
}
|
|
let fldrname = path.dirname(fullpath);
|
|
let rundname = path.basename(fullpath).replace('.json', '');
|
|
let pathItems = fldrname.split('\\'); // Works on all platforms?
|
|
let project = pathItems[pathItems.length-2];
|
|
rundownData.project = project
|
|
rundownData.rundown = rundname
|
|
return rundownData
|
|
},
|
|
|
|
|
|
httpGet: function (url) {
|
|
// A generic http/get sender utility
|
|
// used by heartbeat pusher (and OSC)
|
|
axios.get(url)
|
|
.then(function (response) {
|
|
return response
|
|
})
|
|
.catch(function (error) {
|
|
logger.error('spx_server_functions httpGet: ' + error);
|
|
});
|
|
}, // httpGet
|
|
|
|
|
|
httpPost: function (JSONdata, endPoint) {
|
|
// Send a http POST to an endpoint on the SPX server.
|
|
// v1.3.2 Fixed bug with special characters
|
|
// v1.3.3 Fixed a bug with JSONdata type check (JSON or string)
|
|
|
|
let dataOut = JSONdata;
|
|
|
|
var stringConstructor = "mockup string".constructor;
|
|
if (JSONdata.constructor === stringConstructor) {
|
|
dataOut = JSON.parse(JSONdata);
|
|
}
|
|
|
|
try {
|
|
const options = { port: port, path: endPoint, method: 'POST', headers: {
|
|
'Content-Type': 'application/json; charset=UTF-8',
|
|
}
|
|
}
|
|
const httpreq = http.request(options, result => {
|
|
result.on('data', d => {
|
|
process.stdout.write(d)
|
|
return 'Sent request to server:' + port + ':' + endPoint + ' ' + JSON.stringify(dataOut)
|
|
})
|
|
})
|
|
httpreq.on('error', error => {
|
|
logger.error(error)
|
|
return error
|
|
})
|
|
httpreq.write(JSON.stringify(dataOut))
|
|
httpreq.end()
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.httpPost()', error);
|
|
}
|
|
}, // httpPost
|
|
|
|
CCGServersConfigured: function () {
|
|
// helper function which will return true / false
|
|
if (config?.casparcg?.servers && config?.casparcg?.servers?.length > 0) {
|
|
return true;
|
|
} else {
|
|
return false
|
|
}
|
|
}, // CCGServersConfigured
|
|
|
|
checkServerConnections: function () {
|
|
try {
|
|
// SocketIO call to client
|
|
// require ..... nothing
|
|
// returns ..... sends socket.io instruction to client of each CCG server in config
|
|
let CCGServers = this.CCGServersConfigured()
|
|
if (!CCGServers){ return } // exit early, no need to do any CasparCG work
|
|
logger.verbose('checkServerConnections -function excecuting...');
|
|
data = { spxcmd: 'updateStatusText', status: 'Checking server connections...' };
|
|
io.emit('SPXMessage2Client', data);
|
|
|
|
config.casparcg.servers.forEach((element,i) => {
|
|
let SrvName = element.name;
|
|
let SocketIndex = PlayoutCCG.getSockIndex(SrvName);
|
|
logger.verbose('Pinging ' + SrvName + ': ' + CCGSockets[SocketIndex]);
|
|
// console.log('Pinging ' + SrvName + ': ' + CCGSockets[SocketIndex]);
|
|
data = { spxcmd: 'updateServerIndicator', indicator: 'indicator' + i, color: '#CC0000' };
|
|
let status = {}
|
|
status.server = i + ':' + SrvName;
|
|
status.connecting = CCGSockets[SocketIndex].connecting;
|
|
status.isWritable = CCGSockets[SocketIndex].writable;
|
|
status.isReadable = CCGSockets[SocketIndex].readable;
|
|
if (status.isWritable && status.isReadable){
|
|
data.color = '#00CC00';
|
|
logger.verbose("Connection OK to " + SrvName)
|
|
}
|
|
else{
|
|
logger.verbose("Connection failed to " + SrvName)
|
|
}
|
|
io.emit('SPXMessage2Client', data);
|
|
})
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.checkServerConnections()', error);
|
|
}
|
|
}, // checkServerConnections
|
|
|
|
|
|
cleanUpString: function (str) {
|
|
// Utility added in 1.3.2
|
|
// Clean up a string for HTML output
|
|
// console.log('cleanUpString', str);
|
|
let temp1 = str.replace(/\n/g, '<br>'); // remove \n globally to support text areas
|
|
let temp2 = temp1.replace(/\r/g, ''); // remove \r globally to support text areas
|
|
// I am losing sleep over this - but it works! (Added 26.10.2020)
|
|
temp2 = temp2.replace(/&/g, "&");
|
|
temp2 = temp2.replace(/>/g, ">");
|
|
temp2 = temp2.replace(/</g, "<");
|
|
temp2 = temp2.replace(/"/g, """);
|
|
temp2 = temp2.replace(/'/g, "'");
|
|
temp2 = temp2.replace(/\\/g, "\");
|
|
return temp2;
|
|
}, // cleanUpString
|
|
|
|
|
|
duplicateFile: function (fileRefe, suffix) {
|
|
try {
|
|
return new Promise(resolve => {
|
|
let fldrname = path.dirname(fileRefe);
|
|
let extename = path.extname(fileRefe);
|
|
let basename = path.basename(fileRefe, extename);
|
|
let copyfile = path.normalize(path.join(fldrname, basename + suffix + extename));
|
|
fs.copyFile(fileRefe, copyfile, (err) => {
|
|
if (err) throw err;
|
|
logger.verbose('Rundown file ' + fileRefe + ' was copied to ' + copyfile + '.');
|
|
resolve()
|
|
return true
|
|
});
|
|
})
|
|
} catch (error) {
|
|
logger.error('spx.duplicateFile - Error while duplicating: ' + fileRefe + ': ' + error);
|
|
return false
|
|
}
|
|
}, // duplicateFile
|
|
|
|
fileNameFromPath: function (filepath) {
|
|
// returns the last item from slashed string
|
|
filepath.split("\\").join("/"); // force forward slashes
|
|
if (filepath.includes('/')) {
|
|
let items = filepath.split('/');
|
|
let justFileName = items[items.length-1];
|
|
return justFileName;
|
|
} else {
|
|
return filepath
|
|
}
|
|
}, // fileNameFromPath
|
|
|
|
generateCollapsedHeadline: function (DataFields) {
|
|
// (Added in 1.0.7)
|
|
// Gets template's datafields and generates a nice UI for the collapsed
|
|
// rundown item. This is being used both when rendering a controller view
|
|
// and while item is changed (with ajax call). I dig this :-)
|
|
//
|
|
// require ... an array of datafields
|
|
// returns ... html snippet
|
|
//
|
|
var html = "";
|
|
var skip;
|
|
|
|
|
|
if (!DataFields) {
|
|
console.log('DataFields was null!', DataFields);
|
|
logger.warn('generateCollapsedHeadline DataFields is falsy');
|
|
return
|
|
}
|
|
|
|
let Iterations = DataFields.length || 0; // bugfix?
|
|
|
|
logger.verbose('SPX.generateCollapsedHeadline() DataField (length: ' + Iterations + ')' ,DataFields);
|
|
var Counter = 0;
|
|
if (DataFields.length>0) {
|
|
for (let fieldIndex = 0; fieldIndex < Iterations; fieldIndex++) {
|
|
if (Counter>2) { break };
|
|
let fType = DataFields[fieldIndex].ftype || '';
|
|
let fTitl = DataFields[fieldIndex].title || '';
|
|
let fValu = DataFields[fieldIndex].value || '';
|
|
skip = false;
|
|
switch ( fType ) {
|
|
case "hidden":
|
|
html += '<span id="datapreview_' + fieldIndex + '">' + this.shortifyString(fTitl) + '</span>';
|
|
Counter++;
|
|
break;
|
|
|
|
case "textfield":
|
|
html += '<span id="datapreview_' + fieldIndex + '">' + this.shortifyString(fValu) + '</span>';
|
|
Counter++;
|
|
break;
|
|
|
|
case "number":
|
|
html += '<span id="datapreview_' + fieldIndex + '">' + this.shortifyString(fValu) + '</span>';
|
|
Counter++;
|
|
break;
|
|
|
|
case "filelist":
|
|
html += '<span id="datapreview_' + fieldIndex + '">' + this.shortifyString(this.fileNameFromPath(fValu)) + '</span>';
|
|
Counter++;
|
|
break;
|
|
|
|
case "dropdown":
|
|
html += '<span id="datapreview_' + fieldIndex + '">' + this.shortifyString(fValu) + '</span>';
|
|
Counter++;
|
|
break;
|
|
|
|
default:
|
|
skip = true
|
|
break;
|
|
}
|
|
|
|
if (skip == false && html != "") {
|
|
html += '<span class="delim"></span>';
|
|
}
|
|
} // for ended
|
|
}
|
|
|
|
// if (html=="") {
|
|
// html = '';
|
|
// }
|
|
return html
|
|
}, // generateCollapsedHeadline
|
|
|
|
getFileList: function (FOLDER, EXTENSION) {
|
|
try {
|
|
// Get files from a given folder and return an array of files
|
|
const directoryPath = path.normalize(FOLDER);
|
|
let fileList=[];
|
|
fs.readdirSync(directoryPath).forEach((file, index) => {
|
|
// console.log(index + ': ' + file + '(' + path.extname(file) + ')');
|
|
let TARGETFILETYPE = EXTENSION.toUpperCase()
|
|
let CURRENTFILETYPE = path.extname(file).toUpperCase();
|
|
if (CURRENTFILETYPE.includes(TARGETFILETYPE) ) {
|
|
fileList.push(file);
|
|
}
|
|
});
|
|
return fileList;
|
|
}
|
|
catch (error) {
|
|
logger.error('ERROR in spx.getFileList (Folder: ' + FOLDER + ', Extension: ' + EXTENSION + '): ' + error);
|
|
return false;
|
|
}
|
|
}, // getFileList ended
|
|
|
|
getNotificationsMiddleware: async function(req,res,next) {
|
|
logger.verbose('DEPRECATION WARNING. spx.getNotificationsMiddleware() will be removed in future version.');
|
|
// Please note! Below code is duplicated! See spx.collectSPXInfo.
|
|
// This will probably be removed completely in v1.1.1
|
|
try {
|
|
datarootSize()
|
|
.then(function(sizeArr) {
|
|
let pform = process.platform;
|
|
let paramstring =""
|
|
paramstring += "v=" + vers
|
|
paramstring += "&o=" + lPad(pform.replace(" ", "_"), 25, ".")
|
|
paramstring += "&p=" + lPad(sizeArr[0],5, "0")
|
|
paramstring += "&r=" + lPad(sizeArr[1],5, "0")
|
|
paramstring += "&h=" + lPad(global.hwid.replace(" ", "_"), 25, ".")
|
|
req.curVerInfo = paramstring;
|
|
next();
|
|
})
|
|
} catch (error) {
|
|
logger.error('Error in getNotificationsMiddleware: ' + error);
|
|
next(); // note! This will just HIDE THE PROBLEM from UI.
|
|
}
|
|
}, // getNotificationsMiddleware
|
|
|
|
GetJsonData: function (fileref) {
|
|
try {
|
|
// require ..... Full file path
|
|
// returns ..... File contents as a JSON object
|
|
if (!fs.existsSync(fileref)) {
|
|
logger.warn('Tried to read JSON from non-existing file ' + fileref)
|
|
return false;
|
|
}
|
|
|
|
var contents = fs.readFileSync(fileref);
|
|
return JSON.parse(contents);
|
|
}
|
|
catch (error) {
|
|
logger.error('ERROR in spx.GetJsonData() Invalid JSON Data?: ' + error);
|
|
return (error);
|
|
}
|
|
}, // GetJsonData ended
|
|
|
|
getJSONFileList: function (FOLDER) {
|
|
try {
|
|
// Get files from a given folder
|
|
const directoryPath = path.normalize(FOLDER);
|
|
let jsonData = {};
|
|
var key = 'files';
|
|
jsonData.folder = directoryPath;
|
|
jsonData[key] = [];
|
|
let id = 0;
|
|
fs.readdirSync(directoryPath).forEach(file => {
|
|
let ext = path.extname(file).toUpperCase();
|
|
if (ext == ".JSON") {
|
|
var stats = fs.statSync(path.join(directoryPath, file));
|
|
var datem = moment(stats.mtime, 'DD.MM.YYYY').format();
|
|
var filedata = {
|
|
id: id,
|
|
name: file,
|
|
date: datem
|
|
};
|
|
id++;
|
|
jsonData[key].push(filedata);
|
|
}
|
|
});
|
|
return jsonData;
|
|
}
|
|
catch (error) {
|
|
logger.error('ERROR in spx.getJSONFileList (Folder: ' + FOLDER + '): ' + error);
|
|
return (error);
|
|
}
|
|
}, // getJSONFileList ended
|
|
|
|
getTemplateSourcePath: function () {
|
|
// Added in 1.0.9
|
|
// Evaluate serversource from config > general.templatesource.
|
|
// Supported values
|
|
// - 'casparcg-template-path' : Uses file protocol and CasparCG's template-path folder value during CassparCG playout
|
|
// - 'spx-ip-address' : Uses current SPX-GC server's IP address
|
|
// - '
|
|
//
|
|
let TemplateSource = config.general.templatesource;
|
|
let TemplateServer = ''
|
|
if ( TemplateSource == 'casparcg-template-path') {
|
|
TemplateServer = "";
|
|
logger.verbose('Using filesystem and caspar.config for template-path.');
|
|
} else if ( !TemplateSource || TemplateSource == 'spx-ip-address') {
|
|
TemplateServer = 'http://' + ip.address(); // For https see https://docs.spxgraphics.com/Documentation/Control+Interfaces/https
|
|
logger.verbose('Using ip.address() for TemplateServer IP address: ' + TemplateServer);
|
|
} else {
|
|
if (TemplateSource.substring(0, 4)!='http') {TemplateSource = 'http://' + TemplateSource}
|
|
TemplateServer = TemplateSource;
|
|
logger.verbose('A custom templateServer given in config. Using ' + TemplateServer);
|
|
}
|
|
return TemplateServer; // return empty for file system source or full server url with "http://" prefix
|
|
}, // getTemplateSourcePath
|
|
|
|
hash: function (textToHash){
|
|
// generate a hashed string using CRYPTO
|
|
const algorithm = 'aes-192-cbc';
|
|
const password = 'Password used to generate key';
|
|
const key = crypto.scryptSync(password, 'salt', 24);
|
|
const iv = Buffer.alloc(16, 0); // Initialization vector.
|
|
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
|
let encrypted = cipher.update(textToHash, 'utf8', 'hex');
|
|
encrypted += cipher.final('hex');
|
|
return encrypted
|
|
}, // hash
|
|
|
|
hashcompare: function (password, hash){
|
|
// use bcrypt to compare given password to a hashed one
|
|
// let value = bcrypt.compareSync(password, hash); // true or false
|
|
// console.log('PASS ....... ' + this.hash(password));
|
|
// console.log('HASH ....... ' + hash);
|
|
// console.log('Compare .... ' + value);
|
|
|
|
if (this.hash(password) == hash) { return true }
|
|
return false;
|
|
}, // hashcompare
|
|
|
|
|
|
isJson: function(item) {
|
|
let value = typeof item !== "string" ? JSON.stringify(item) : item;
|
|
try {
|
|
value = JSON.parse(value);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return typeof value === "object" && value !== null;
|
|
}, // isJson
|
|
|
|
lang: function (str) {
|
|
try {
|
|
const spxlangfile = config.general.langfile || 'english.json';
|
|
let langpath = path.join(this.getStartUpFolder(), 'locales', spxlangfile);
|
|
|
|
var lang = require(langpath);
|
|
json = 'lang.' + str;
|
|
return eval(json) || str;
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.lang (str: ' + str + '): ' + error);
|
|
return str + " missing from " + spxlangfile;
|
|
}
|
|
}, // lang
|
|
|
|
getEasterEggImage: function () {
|
|
// Added in 0.8.9
|
|
let ddmm = moment().format('DD-MM');
|
|
let image = false;
|
|
let greet = '';
|
|
// ddmm = '08-03' // DEV TESTING Day-Month
|
|
|
|
switch (ddmm) {
|
|
case '31-12':
|
|
case '01-01':
|
|
greet = 'Happy New Year from <A target="egg" href="http://spxgraphics.com">SPX Graphics</A>!';
|
|
image = 'new-year-sparks.png';
|
|
break;
|
|
|
|
case '08-03':
|
|
greet = 'Happy Women\'s Day from <A target="egg" href="http://spxgraphics.com">SPX Graphics</A>!';
|
|
image = 'rose.png';
|
|
break;
|
|
|
|
case '04-07':
|
|
greet = 'Happy Birthday USA!';
|
|
image = 'usa.png';
|
|
break;
|
|
|
|
case '22-04':
|
|
greet = 'Happy <A target="egg" href="https://www.earthday.org/">Earth day!</A>';
|
|
image = 'earth-day.png';
|
|
break;
|
|
|
|
case '04-10':
|
|
greet = 'Have a great <A target="egg" href="https://en.wikipedia.org/wiki/World_Animal_Day">World Animal Day!</A>';
|
|
image = 'animal-day.png';
|
|
break;
|
|
|
|
case '09-10':
|
|
greet = 'Have a tasty Cinnamon Roll Day!';
|
|
image = 'korvamunkki.png';
|
|
break;
|
|
|
|
case '06-12':
|
|
greet = '<A target="egg" href="https://en.wikipedia.org/wiki/Independence_Day_(Finland)">Hyvää itsenäisyyspäivää!</A>';
|
|
image = 'finland.png';
|
|
break;
|
|
|
|
case '24-12':
|
|
case '25-12':
|
|
case '26-12':
|
|
greet = 'Merry Christmas from <A href="http://spxgraphics.com">SPX Graphics</A>!';
|
|
image = 'animated-santa.png';
|
|
break;
|
|
|
|
}
|
|
|
|
if (image) {
|
|
logger.verbose('Easter egg activated for ' + ddmm + ': ' + image);
|
|
}
|
|
|
|
return [image, greet];
|
|
}, // getEasterEggImage
|
|
|
|
getStartUpFolder: function () {
|
|
// a workaround to resolve the path to the current startup folder
|
|
// on this runtime session (node vs binary pkg)
|
|
if ( process.pkg ) {
|
|
return path.resolve(process.execPath + '/..');
|
|
} else {
|
|
return process.cwd();
|
|
}
|
|
}, // getStartUpFolder
|
|
|
|
getDatarootFolder: function () {
|
|
// Added in 1.3.1
|
|
let datapath = config.general.dataroot || this.getStartUpFolder() + '/' + 'DATAROOT';
|
|
if (fs.existsSync(datapath)) {
|
|
return path.normalize(datapath);
|
|
} else {
|
|
logger.error('Data root folder "' + datapath + '" not found! Verify config!');
|
|
return null;
|
|
}
|
|
}, // getDatarootFolder
|
|
|
|
|
|
GetSubfolders: async function (strFOLDER) {
|
|
// return a list of all subfolders in a given folder
|
|
// console.log('Trying to get subfolders of ' + strFOLDER);
|
|
// ignores folders starting with . or _ (Added in 1.3.0)
|
|
try {
|
|
let FOLDER = path.normalize(strFOLDER);
|
|
if (fs.existsSync(FOLDER)) {
|
|
return fs.readdirSync(FOLDER).filter(function (file) {
|
|
return (fs.statSync(FOLDER+'/'+file).isDirectory() &&
|
|
file.charAt(0) != '.' &&
|
|
file.charAt(0) != '_');
|
|
});
|
|
} else {
|
|
logger.error('Source folder didnt exist "' + strFOLDER + '", so it was created.');
|
|
fs.mkdirSync(FOLDER);
|
|
return;
|
|
}
|
|
}
|
|
catch (error) {
|
|
logger.error('GetSubfolders / Failed to read folder ' + strFOLDER + ': ' + error);
|
|
return (error);
|
|
}
|
|
}, // GetSubfolders
|
|
|
|
|
|
GetDataFiles: async function (FOLDERstr) {
|
|
// return a list of all json files in the dataroot folder
|
|
logger.debug("Getting json files from " + FOLDERstr + "...");
|
|
let FOLDER = path.normalize(FOLDERstr);
|
|
if (!fs.existsSync(FOLDER)) {
|
|
logger.debug("Fixed an issue, created a missing folder " + FOLDER + "...");
|
|
fs.mkdirSync(FOLDER);
|
|
}
|
|
|
|
let jsonData = [];
|
|
try {
|
|
|
|
fs.readdirSync(FOLDER).forEach(file => {
|
|
let bname = path.basename(file, '.json');
|
|
let ext = path.extname(file).toUpperCase();
|
|
if (ext == ".JSON") {
|
|
// console.log('Filename', bname);
|
|
jsonData.push(bname);
|
|
}
|
|
});
|
|
return jsonData;
|
|
}
|
|
catch (error) {
|
|
let errPrefix = 'GetDataFiles / Failed to read folder '
|
|
logger.error(errPrefix + FOLDERstr + ': ' + error);
|
|
return (errPrefix + FOLDERstr);
|
|
}
|
|
}, // GetDataFiles ended
|
|
|
|
GetTemplatesFromProfileFile: async function (FOLDERstr) {
|
|
// Return templates of the given project. Added in 1.1.4.
|
|
try {
|
|
let showFolder = path.normalize(FOLDERstr);
|
|
let dataJSONfile = path.join(this.getDatarootFolder(), showFolder, 'profile.json'); // Changed in v1.3.1.
|
|
if (fs.existsSync(dataJSONfile)) {
|
|
logger.debug("Getting templates from " + dataJSONfile + "...");
|
|
let profileData = await this.GetJsonData(dataJSONfile);
|
|
return [200,profileData.templates];
|
|
} else {
|
|
logger.error("No profile.json found in " + showFolder);
|
|
return [404,{"error":"No profile.json found for project " + showFolder + "."}];
|
|
}
|
|
|
|
}
|
|
catch (error) {
|
|
logger.error('Failed to read profile file from ' + FOLDERstr + ': ' + error);
|
|
return ('Failed to read profile file from: ' + FOLDERstr);
|
|
}
|
|
}, // GetTemplatesFromProfileFile ended
|
|
|
|
|
|
|
|
GetFilesAndFolders: function (datafolder, filter='HTM') {
|
|
|
|
try {
|
|
// This is used by the AJAX function and returns a data object with folders
|
|
// and HTML files of a given sourcefolder
|
|
let data = {
|
|
folder: datafolder,
|
|
fileArr: [],
|
|
foldArr: []
|
|
};
|
|
|
|
// FIXME: Try to improve in 1.1.1
|
|
// Trying to survive invalid datafolder path.
|
|
// This happens on a PC (at least) when re-importing templates.
|
|
if (!fs.existsSync(datafolder)) {
|
|
logger.verbose('GetFilesAndFolders got invalid path 1 "' + datafolder + '"')
|
|
datafolder = path.join(this.getStartUpFolder(),'/ASSETS/templates', datafolder);
|
|
logger.verbose('Trying failover #1: "' + datafolder + '"')
|
|
} // -------------------- lets hope it works ------------------------------------
|
|
|
|
if (fs.existsSync(datafolder)) {
|
|
fs.readdirSync(datafolder).forEach((file, index) => {
|
|
const curPath = path.join(datafolder, file);
|
|
if (fs.lstatSync(curPath).isDirectory()) {
|
|
// it is folder, and does not start with . or _
|
|
if (file.charAt(0) != '.' && file.charAt(0) != '_') {
|
|
data.foldArr.push(path.basename(curPath));
|
|
}
|
|
}
|
|
else {
|
|
// it is file
|
|
let ext = path.extname(curPath).toUpperCase();
|
|
let fil = filter.toUpperCase();
|
|
let fir = path.basename(curPath).charAt(0); // first character (dot files) Added in 1.1.0
|
|
|
|
// Skip all the files that start with a "." or "_"
|
|
if (fil === 'HTM' && (ext ==".HTM" || ext ==".HTML") && fir != '.' && fir != '_') {
|
|
data.fileArr.push(path.basename(curPath));
|
|
}
|
|
|
|
if (fil === 'CSV' && ext ==".CSV" && fir != '.' && fir != '_') {
|
|
data.fileArr.push(path.basename(curPath));
|
|
}
|
|
// Add support for OGRAF files
|
|
if (fil === 'OGRAF' && ext === ".JSON" && fir != '.' && fir != '_') {
|
|
data.fileArr.push(path.basename(curPath));
|
|
}
|
|
}
|
|
});
|
|
|
|
// Sort elements within arrays
|
|
data.fileArr.sort();
|
|
data.foldArr.sort();
|
|
return data;
|
|
} else {
|
|
return "not-found";
|
|
}
|
|
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.GetFilesAndFolders (datafolder: ' + datafolder + '): ' + error);
|
|
}
|
|
}, // GetFilesAndFolders
|
|
|
|
playAudio: async function (wavFileName, msg='') {
|
|
try {
|
|
// request ..... a name of soundFX
|
|
// return ...... plays audio on the server
|
|
// Usage ....... spx. -or- this.playAudio('beep.wav', 'Log message');
|
|
let cwd = this.getStartUpFolder();
|
|
let AudioFilePath = path.join(cwd, 'ASSETS', wavFileName);
|
|
|
|
console.log('Trying to play audio file [' + AudioFilePath + ']...');
|
|
|
|
// EXTERNAL AUDIO PLAYER IS CURRENTLY DISABLED. -- See also view-appconfig.
|
|
// let AudioPlayPath = path.normalize(config.general.audioplayer);
|
|
// let AudioPlayOpts = config.general.playerflags;
|
|
// let AudioPlayExec = "\"" + AudioPlayPath + "\" " + AudioPlayOpts.replace('%FILE%', AudioFilePath);
|
|
// logger.debug('spx.PlayAudio - message: [' + msg + '] Audio command: ' + AudioPlayExec);
|
|
// const { exec } = require("child_process");
|
|
// exec(AudioPlayExec, (error, stdout, stderr) => {
|
|
// if (error) {
|
|
// console.log(`error: ${error.message}`);
|
|
// return;
|
|
// }
|
|
// if (stderr) {
|
|
// console.log(`stderr: ${stderr}`);
|
|
// return;
|
|
// }
|
|
// console.log(`stdout: ${stdout}`);
|
|
// });
|
|
|
|
wavplayer.play({
|
|
path: AudioFilePath,
|
|
}).then(() => {
|
|
logger.debug('spx.PlayAudio OK ['+ wavFileName +'], ref.message: [' + msg + '].');
|
|
}).catch((error) => {
|
|
logger.error('spx.PlayAudio Error ['+ wavFileName +'], ref.message: [' + msg + '], error: ' + error);
|
|
});
|
|
return;
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.playAudio (wavFileName: ' + wavFileName + ', ref: ' + msg + '): ' + error);
|
|
}
|
|
|
|
|
|
|
|
}, // playAudio
|
|
|
|
talk: async function (message){
|
|
|
|
logger.debug('FYI: spx.talk function is DISABLED. [' + message + ']')
|
|
return;
|
|
|
|
try {
|
|
// THIS ONLY WORKS ON WINDOWS!
|
|
// Usage:
|
|
// this.talk('hello there');
|
|
// spx.talk('All your base are belong to us');
|
|
var isWin = process.platform === "win32";
|
|
if (isWin==false){
|
|
logger.warn('Talk synthesis currently only works on Windows.');
|
|
return
|
|
}
|
|
let ScriptPath = path.join(this.getStartUpFolder(), 'utils/talk.vbs');
|
|
let Executable = "wscript " + ScriptPath + " " + message;
|
|
logger.debug('spx.Talk [' + message + ']');
|
|
const { exec } = require("child_process");
|
|
exec(Executable, (error, stdout, stderr) => {
|
|
if (error) {
|
|
console.log(`error: ${error.message}`);
|
|
return;
|
|
}
|
|
if (stderr) {
|
|
console.log(`stderr: ${stderr}`);
|
|
return;
|
|
}
|
|
// console.log(`stdout: ${stdout}`);
|
|
});
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.talk (message: ' + message + '): ' + error);
|
|
}
|
|
}, // talk
|
|
|
|
prettifyDate: function (d, presetName){
|
|
// prettify date format to a desired preset format
|
|
try {
|
|
let YEAR = d.getFullYear()
|
|
let MONT = (d.getMonth()+1).toString().padStart(2, '0')
|
|
let DNUM = d.getDate().toString().padStart(2, '0')
|
|
let HOUR = d.getHours().toString().padStart(2, '0')
|
|
let MINU = d.getMinutes().toString().padStart(2, '0')
|
|
let SECO = d.getSeconds().toString().padStart(2, '0')
|
|
|
|
switch (presetName) {
|
|
case 'YYYY-MM-DD-HHMMSS':
|
|
return YEAR + '-' + MONT + '-' + DNUM + '-' + HOUR + MINU + SECO;
|
|
break;
|
|
|
|
default:
|
|
return d
|
|
break;
|
|
}
|
|
} catch(error) {
|
|
logger.error('ERROR in spx.prettifyDate (d: ' + d + ', presetName: ' + presetName + ' ): ' + error);
|
|
return ""
|
|
}
|
|
|
|
|
|
}, // prettifyDate
|
|
|
|
prettifyName: function (fullFilePath){
|
|
if (!fullFilePath) return "";
|
|
try {
|
|
if (fullFilePath.toLowerCase().endsWith(".ograf.json")) {
|
|
// This uses the data.name property from the OGraf manifest file rather than the filename
|
|
const absolutePath = path.join("ASSETS", "templates", fullFilePath);
|
|
const data = this.GetJsonData(absolutePath)
|
|
return data.name
|
|
}
|
|
// request ..... a long file name with some slashes ("c:/temp/some_file-name.html")
|
|
// return ...... nice name ("some file name")
|
|
// logger.debug('Prettifying ' + fullFilePath);
|
|
let slashedPath = fullFilePath.split("\\").join("/");
|
|
let items = slashedPath.split("/");
|
|
let templateName = items[items.length-1];
|
|
let niceStringOut = templateName;
|
|
niceStringOut = niceStringOut.replace(".html", "");
|
|
niceStringOut = niceStringOut.replace(".htm", "");
|
|
niceStringOut = niceStringOut.replace(".HTML", "");
|
|
niceStringOut = niceStringOut.replace(".HTM", "");
|
|
niceStringOut = niceStringOut.split("_").join(" ");
|
|
niceStringOut = niceStringOut.split("-").join(" ");
|
|
return niceStringOut;
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.prettifyName (fullFilePath: ' + fullFilePath + '): ' + error);
|
|
return ""
|
|
}
|
|
}, // prettifyName
|
|
|
|
RemoveFilepathKey: function (RundownData) {
|
|
if (RundownData.filepath) {
|
|
// Some version of SPX stored filepath into the
|
|
// rundown file. This is not needed and should
|
|
// be removed, so the below line fixes the file.
|
|
logger.verbose('Removing ".filepath" from "' + RundownData.filepath + '"...');
|
|
delete RundownData.filepath;
|
|
}
|
|
return RundownData;
|
|
}, // RemoveFilepathKey
|
|
|
|
renameRundown: function (orgfile, newname) {
|
|
// Rename a file:
|
|
// request ..... full original name (c:/temp/volvo.txt), new basename (toyota)
|
|
// returns ..... not much, it just does it
|
|
try {
|
|
return new Promise(resolve => {
|
|
let fldrname = path.dirname(orgfile);
|
|
let extename = path.extname(orgfile);
|
|
let newFilePath = path.join(fldrname, newname + extename);
|
|
// Helper variables for possible renaming if file exists
|
|
let finalNewName = newname;
|
|
let counter = 1;
|
|
|
|
// Check if file exists and add counter if it does
|
|
while (fs.existsSync(newFilePath)) {
|
|
finalNewName = newname + ' (' + counter + ')';
|
|
newFilePath = path.join(fldrname, finalNewName + extename);
|
|
counter++;
|
|
}
|
|
|
|
let renafile = path.normalize(newFilePath);
|
|
|
|
fs.rename(orgfile, renafile, (err) => {
|
|
if (err) throw err;
|
|
logger.verbose('Rundown file ' + orgfile + ' was renamed to ' + renafile + '.');
|
|
global.rundownData = {}; // Added in 1.1.2
|
|
resolve()
|
|
});
|
|
})
|
|
} catch (error) {
|
|
logger.error('spx.renameRundown - Error while renaming: ' + orgfile + ': ' + error);
|
|
}
|
|
}, // renameRundown
|
|
|
|
|
|
salt: function (length) {
|
|
// Generate a salt for hashing
|
|
// request ..... length of salt (16)
|
|
// returns ..... salt string
|
|
try {
|
|
let hex = crypto.randomBytes(Math.ceil(length/2))
|
|
.toString('hex')
|
|
.slice(0,length);
|
|
return this.rot(hex)
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.salt (length: ' + length + '): ' + error);
|
|
return ""
|
|
}
|
|
},
|
|
|
|
|
|
rot: function (str, reverse=false) {
|
|
// Added in 1.1.1. - used by basic lic service
|
|
// Un/Scramble. Always returns uppercased string.
|
|
// rot('car') ......... = "ROI"
|
|
// rot('ROI', true) ... = "CAR"
|
|
try {
|
|
let orig = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
let mixd = "OJN59B6VKTQEMIFZ87WUDCS4X1PHR0A23LGY"
|
|
let src = str.toUpperCase();
|
|
if (reverse) {
|
|
return src.replace(/./gi, c => mixd[orig.indexOf(c)])
|
|
// return src.replace(/./gi, c => mixd[orig.indexOf(c)])
|
|
} else {
|
|
return src.replace(/./gi, c => orig[mixd.indexOf(c)])
|
|
// return src.replace(/./gi, c => orig[mixd.indexOf(c)])
|
|
}
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.rot (str: ' + str + '): ' + error);
|
|
return str
|
|
}
|
|
}, // rot
|
|
|
|
dashify: function (str) {
|
|
// add dashes to a string
|
|
// request ..... string to be dashed
|
|
// returns ..... dashed string
|
|
try {
|
|
// break string into two character groups with dashes
|
|
return str.match(/.{1,2}/g).join(""); // removed -
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.dashify (str: ' + str + '): ' + error);
|
|
return ""
|
|
}
|
|
},
|
|
|
|
shortifyString: function (fullString){
|
|
try {
|
|
// request ..... a long string
|
|
// return ...... return just a short part
|
|
// Added in 1.1.0 to strip <TAGs> to prevent multiline strings in the SPX UI:
|
|
fullString = fullString.replace(/<\/?[^>]+(>|$)/g, "").replace(/\s+/g,' '); // remove <TAGS> and double spaces
|
|
let shorter = fullString.substring(0, 30);
|
|
shorter = shorter.trim()
|
|
if (fullString.length > (shorter.length+2)) {
|
|
shorter += '...'
|
|
}
|
|
return shorter;
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.shortifyString(fullString: ' + fullString + '): ' + error);
|
|
}
|
|
}, // shortifyString
|
|
|
|
setRecents: function (rundownRef) {
|
|
try {
|
|
// request ..... string "folder/file" reference
|
|
// return ...... nothing
|
|
// Will manage up to 3 recents in global.config.general.recents[]
|
|
let recentsArray = config.general.recents || []; // ['FOLDER1/FILE1','FOLDER2/FILE2','FOLDER3/FILE3',];
|
|
recentsArray.forEach((item,index) => {
|
|
if (item == rundownRef) {
|
|
recentsArray.splice(index,1);
|
|
}
|
|
});
|
|
recentsArray.unshift(rundownRef);
|
|
if (recentsArray.length > 5 ) {recentsArray.length = 5}; // limit to 5
|
|
config.general.recents = recentsArray;
|
|
this.writeFile(configfileref,config);
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.setRecents (fileref: ' + rundownRef + '): ' + error);
|
|
}
|
|
},
|
|
|
|
shortifyName: function (fullString){
|
|
try {
|
|
// request ..... a long string, such as server asset url
|
|
// return ...... max length returned
|
|
|
|
let SERVER_URL = 'http://' + ip.address() + ':' + port;
|
|
let serverRemoved = fullString.split(SERVER_URL).join("");
|
|
return serverRemoved;
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.shortifyName (fullString: ' + fullString + '): ' + error);
|
|
}
|
|
}, // shortifyName
|
|
|
|
|
|
strip: async function (html) {
|
|
// Strip HTML tags from a string to sanitize it
|
|
try {
|
|
let noHtml = html.replace(/<\/?[^>]+(>|$)/g, "").replace(/\s+/g,' '); // remove <TAGS> and double spaces
|
|
return noHtml;
|
|
} catch (error) {
|
|
logger.error('ERROR in spx.strip (html: ' + html + '): ' + error);
|
|
return ""
|
|
}
|
|
}, // strip
|
|
|
|
|
|
versInt: function (semver){
|
|
// Returns a numeric value representing "1.0.0" formatted semantic version string.
|
|
// This works as long as max value of each field is 99!
|
|
let parts = semver.split(".");
|
|
let MajorInt = parseInt(parts[0].trim())*100000
|
|
let MinorInt = parseInt(parts[1].trim())*1000
|
|
let PatchInt = parseInt(parts[2].trim())
|
|
let versInt = (MajorInt + MinorInt + PatchInt);
|
|
// console.log('Semver ' + semver + ' = versInt ' + versInt);
|
|
return versInt
|
|
}, // versInt
|
|
|
|
writeFile: function (filepath, data) {
|
|
try {
|
|
return new Promise(resolve => {
|
|
// this.talk('Writing file');
|
|
// this.playAudio('beep.wav', 'spx.writeFile');
|
|
data.warning = "Modifications done in the WebCG will overwrite this file.";
|
|
data.copyright = "(c) 2020- WebCG Graphics";
|
|
data.updated = new Date().toISOString();
|
|
let filedata = JSON.stringify(data, null, 2);
|
|
|
|
if (!filepath) {
|
|
logger.warn('spx.writeFile // No filepath given, not saving anything.');
|
|
return
|
|
}
|
|
|
|
fs.writeFile(filepath, filedata, 'utf8', function (err) {
|
|
if (err){
|
|
console.error('spx.writeFile // Error saving: [' + filepath + ']: ' + err);
|
|
return
|
|
}
|
|
logger.verbose('spx.writeFile - File written OK: ' + filepath);
|
|
resolve()
|
|
});
|
|
}
|
|
)
|
|
} catch (error) {
|
|
logger.error(error);
|
|
return
|
|
}
|
|
}, // writeFile (.json)
|
|
|
|
writeTextFile: function (filepath, filedata) {
|
|
// console.log('writing text file ' + filepath);
|
|
try {
|
|
return new Promise(resolve => {
|
|
filedata.updated = new Date().toISOString();
|
|
fs.writeFile(filepath, filedata, 'utf8', function (err) {
|
|
if (err) {
|
|
logger.error('spx.writeFile - Error while saving: ' + filepath + ': ' + err);
|
|
return;
|
|
// throw err;
|
|
}
|
|
logger.verbose('spx.writeFile - File written OK: ' + filepath);
|
|
resolve()
|
|
});
|
|
}
|
|
)
|
|
} catch (error) {
|
|
logger.error('spx.writeFile - Error while saving: ' + filepath + ': ' + error);
|
|
}
|
|
}, // writeTextFile (other)
|
|
|
|
collectSPXInfo: async function (from='') {
|
|
return new Promise(resolve => {
|
|
try {
|
|
datarootSize()
|
|
.then(function(sizeArr) {
|
|
let paramstring =""
|
|
paramstring += "v=" + vers
|
|
paramstring += "&o=" + lPad(process.platform, 8, ".")
|
|
paramstring += "&p=" + lPad(sizeArr[0],5, "0")
|
|
paramstring += "&r=" + lPad(sizeArr[1],5, "0")
|
|
paramstring += "&h=" + global.pmac
|
|
resolve(paramstring)
|
|
})
|
|
}
|
|
catch (error) {
|
|
logger.error('Error in collectSPXInfo: ' + error);
|
|
return([0,0])
|
|
}
|
|
})
|
|
} // collectSPXInfo
|
|
|
|
|
|
} // end of exports (spx.)
|
|
|
|
function lPad(nro,len,char){
|
|
// leftpad string beginning with char's
|
|
let padString = "";
|
|
for (let index = 0; index < 60; index++) {
|
|
padString += char;
|
|
}
|
|
let padded = padString + String(nro).trim();
|
|
return padded.substring(padded.length - len, padded.length);
|
|
}
|
|
|
|
function checkInternetConnection(icheck) {
|
|
require('dns').lookup('google.com',function(err) {
|
|
if (err && err.code == "ENOTFOUND") {
|
|
icheck(false);
|
|
} else {
|
|
icheck(true);
|
|
}
|
|
})
|
|
}
|
|
|
|
function datarootSize () {
|
|
// counts projects and playlists
|
|
return new Promise(resolve => {
|
|
try {
|
|
let folderpath = path.normalize(config.general.dataroot);
|
|
let projects = 0;
|
|
let rundowns = 0;
|
|
let filesArr = glob.sync(folderpath + "/**/*.json")
|
|
filesArr.forEach((file,index) => {
|
|
if (file.includes('profile.json')) {projects = projects + 1}
|
|
});
|
|
rundowns = (filesArr.length - projects);
|
|
logger.debug('datarootSize: projects ' + projects + ', rundowns ' + rundowns);
|
|
resolve([projects,rundowns])
|
|
}
|
|
catch (error) {
|
|
logger.error('Error in datarootSize: ' + error);
|
|
return([0,0])
|
|
}
|
|
})
|
|
} |