// 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, '
'); // 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(/ {
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 += '' + this.shortifyString(fTitl) + '';
Counter++;
break;
case "textfield":
html += '' + this.shortifyString(fValu) + '';
Counter++;
break;
case "number":
html += '' + this.shortifyString(fValu) + '';
Counter++;
break;
case "filelist":
html += '' + this.shortifyString(this.fileNameFromPath(fValu)) + '';
Counter++;
break;
case "dropdown":
html += '' + this.shortifyString(fValu) + '';
Counter++;
break;
default:
skip = true
break;
}
if (skip == false && html != "") {
html += '';
}
} // 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 SPX Graphics!';
image = 'new-year-sparks.png';
break;
case '08-03':
greet = 'Happy Women\'s Day from SPX Graphics!';
image = 'rose.png';
break;
case '04-07':
greet = 'Happy Birthday USA!';
image = 'usa.png';
break;
case '22-04':
greet = 'Happy Earth day!';
image = 'earth-day.png';
break;
case '04-10':
greet = 'Have a great World Animal Day!';
image = 'animal-day.png';
break;
case '09-10':
greet = 'Have a tasty Cinnamon Roll Day!';
image = 'korvamunkki.png';
break;
case '06-12':
greet = 'Hyvää itsenäisyyspäivää!';
image = 'finland.png';
break;
case '24-12':
case '25-12':
case '26-12':
greet = 'Merry Christmas from SPX Graphics!';
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 to prevent multiline strings in the SPX UI:
fullString = fullString.replace(/<\/?[^>]+(>|$)/g, "").replace(/\s+/g,' '); // remove 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 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])
}
})
}