Init WebCG

This commit is contained in:
2026-08-12 14:21:28 +09:00
commit 5c01660f97
170 changed files with 46248 additions and 0 deletions
+978
View File
@@ -0,0 +1,978 @@
// ===============================================================
//
// "Web Graphics Controller"
// (c) 2020- Sh Graphics
//
// No front-end frameworks.
// Core functionality: NodeJS, Express, Handlebars -templating.
//
// Most serverside code is in /routes and /utils.
// Most clientside code is in /static/js/spx_gc.js
//
// Basic usage:
// node server.js [config.json]
//
// ===============================================================
const express = require('express')
const session = require('express-session')
const bodyParser = require('body-parser')
const cors = require('cors')
const exphbs = require('express-handlebars')
const app = express()
const ip = require('ip')
const fs = require('fs')
const morgan = require('morgan')
const path = require('path')
const rfs = require('rotating-file-stream')
var macaddress = require('macaddress');
console.log('\n WebCG Controller server is starting.\n Closing this window/process will stop the server.\n');
// EXIT HANDLER
process.on('exit', function(code) {
// usage: process.exit(1)
let codestr="Unspecified exit code";
switch (code) {
case 1:
codestr = "Error while reading config.json"
break;
case 2:
codestr = "Kill request received from WebCG client. If pm2 used, the server will restart automatically."
break;
case 3:
codestr = "Port is already in use on this machine."
break;
default:
break;
}
return console.log(`\n\nWebCG exit! (Errorcode ${code}: ${codestr})\n\n`);
});
// global.config
const cfg = require('./utils/spx_getconf.js');
cfg.readConfig()
if (!configfileref) {
process.exit(1)
}
const spx = require('./utils/spx_server_functions.js');
const logger = require('./utils/logger.js');
// STATICS
app.use(express.static(path.join(__dirname,('static'))))
app.use(express.static(path.resolve(spx.getStartUpFolder(),'ASSETS')))
const ipad = ip.address();
var pjson = require('./package.json');
var packageversion = pjson.version;
const vers = process.env.npm_package_version || packageversion || 'X.X.X';
global.vers = vers;
global.excel = {'readtime':0, 'filename':'', 'data':''}; // used as Excel cache
// Added in 1.0.15. An memory persistent object to handle -------------
// rundown state to avoid unnecessary disk I/O and improve performance.
global.rundownData = {}
// --------------------------------------------------------------------
const port = config.general.port || 5656;
global.CCGSockets = [];
global.LastBrowsedTemplateFolder = '';
macaddress.one(function (err, mc) {
let macaddress = String(mc);
let pseudomac = macaddress.split(':').join('').substring(0,8);
global.hwid = config.general.hostname || pseudomac;
global.pmac = pseudomac;
});
// Setup Morgan
let LOGFOLDER = config.general.logfolder || spx.getStartUpFolder() + '/LOG';
var logDirectory = path.normalize(LOGFOLDER);
fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory)
var accessLogStream = rfs.createStream('access.log', {
interval: '1d', // rotate daily
path: logDirectory
})
app.use(morgan('combined', { stream: accessLogStream }))
// Handlebars templating, updated in 1.3.3
app.engine('handlebars', exphbs.engine({
extname: '.handlebars',
//defaultLayout: 'filelist',
helpers: {
max5: function (data) {
if (parseInt(data)>5) { data = 5; }
return String(data);
},
// Convert JSON data and return as JSONstring.
// (used to store arrays to hidden form fields)
DataToJSONString: function (data) {
let s = encodeURI(JSON.stringify(data));
return s;
},
// conditional IF helper
ifValue: function(a, b, options) {
// console.log('a: ' + a + ', b: ' + b);
if(String(a) === String(b) ) {
return options.fn(this);
}
return options.inverse(this);
},
// conditional IF helper return value A if "value" == "compareTo", B if not
ifValueMatch: function(value, compareTo, A, B ) {
if(value == compareTo) {
return A;
} else {
return B;
}
},
// conditional IF helper
itemCount: function(arr) {
// console.log('a: ' + a + ', b: ' + b);
return arr.length;
},
// conditional IF helper
ifGreater: function(a, b, options) {
if(a > b) {
// console.log('TRUE. A (' + a + ') was bigger.');
return options.fn(this);
}
// console.log('FALSE. B (' + b + ') was bigger.');
return options.inverse(this);
},
// populate CasparCG options for show config templates
generateDataFormatOptions(currentDataformat='json') {
let html="";
let AvailableFormats=['json', 'xml'];
AvailableFormats.forEach((Option,i) => {
let curFormat = Option.toLowerCase();
let cfgFormat = currentDataformat.toLowerCase();
let sel = "";
if (curFormat==cfgFormat){ sel="selected" }
html += '<option value="' + curFormat + '" ' + sel + '>' + curFormat + '</option>';
});
return html
},
// populate CasparCG options for show config templates
generateCasparCgPlayoutOptions(currentServer) {
let html="";
let AvailableServers=['-'];
if (config.casparcg){
config.casparcg.servers.forEach((element,i) => {
AvailableServers.push(element.name);
});
}
AvailableServers.forEach((serverOption,i) => {
let curSrv = serverOption.toUpperCase();
let cfgSrv = currentServer.toUpperCase();
let sel = "";
if (curSrv==cfgSrv){ sel="selected" }
html += '<option value="' + curSrv + '" ' + sel + '>' + curSrv + '</option>';
});
return html
},
// generate Open datafolder -commands in the UI menu for different operating systems
GenerateOpenFolderCommands(){
let platform = process.platform;
let html="";
switch (platform) {
case 'win32':
html += '<!-- Below folder commands only available on Windows in version 1.0 -->\n';
html += '<br><A HREF="" onClick="spx_system(\'openDatafolder\');return false;"><i class="far fa-folder-open"></i>&nbsp; '+ spx.lang('button.exploredataroot') +'</A>\n';
html += '<br><A HREF="" onClick="spx_system(\'openTemplatefolder\');return false;"><i class="far fa-folder-open"></i>&nbsp; '+ spx.lang('button.exploretemplates') +'</A>\n';
break;
default:
html = "<!-- Sorry, only windows supported for Open Folder -commands in version 1.0 -->\n";
break;
}
return html;
},
// draw all custom controls in the argument array
GenerateCustomControls(arr){
if (!arr) {return}; // if no extras, do nuthin'
let html = "";
html += '<table style="width: 100%;" cellspacing="5" border="0">\n';
arr.forEach((control,index) => {
html += '<tr>'
html += '<td width="50%">'
html += '<span class="spxTableHead">' + control.description + '</span>'
html += '</td>'
html += '<td>'
switch (control.ftype){
case 'button':
html += '<button type="button" class="wide spxFlexItem btn ripple ' + control.bgclass + '" ';
html += 'data-spx-color="' + control.bgclass + '" ';
html += 'onClick="' + control.fcall + '" ';
html += '>' + control.text + '</button>\n';
break;
case 'togglebutton':
html += '<button type="button" class="wide spxFlexItem btn ripple ' + control.bgclass + '" ';
html += 'data-spx-color="' + control.bgclass + '" ';
html += 'data-spx-playtext="' + control.text0 + '" ';
html += 'data-spx-stoptext="' + control.text1 + '" ';
html += 'data-spx-status="false" ';
html += 'onClick="' + control.fcall + '" ';
html += '>' + control.text0 + '</button>\n';
break;
case 'selectbutton':
html += '<table class="wide" style="margin:0, padding:0; border:0px;">\n';
html += '<tr><td width="60%">\n';
html += '<select id="select' + index + '" class="wide btn ' + control.bgclass + '">\n';
if (control.items) {
control.items.forEach((option,nro) => {
html += '<option value="' + option.value + '">' + option.text + '</option>\n';
});
}
html += '</td><td>\n';
html += '<button type="button" class="wide spxFlexItem btn ripple ' + control.bgclass + '" ';
html += 'data-spx-color="' + control.bgclass + '" ';
html += 'onClick="' + control.fcall + '(\'select' + index + '\');" ';
html += '>' + control.text + '</button>\n';
html += '</td></tr></table>\n';
break;
default:
logger.warn('server.GenerateCustomControls error in item [' + index + '] unknown controltype [' + control.ftype + '].');
break;
}
html += '</td></tr>\n'
});
html += '</table><BR>\n'
return html;
},
// generate a pretty string for each template to display playout configs
GeneratePlayoutInfo(playserver='', playchannel='', playlayer='', webplayout='', out='' , itemid='') {
webplayout = spx.max5(webplayout);
let html = '<div data-spx-name="playoutConfig">';
// html += '<input type="text" data-clipboard-action="copy" data-clipboard-target="#copy' + itemid + '">';
if (playserver !="-") {
html += "<span onmouseover=\"tip('" + spx.lang('hover.ccgserver') + "');\" data-name='srv'>" + playserver + "</span>-";
html += "<span onmouseover=\"tip('" + spx.lang('hover.ccgchannel') + "');\" data-name='cha'>" + playchannel + "</span>-";
html += "<span onmouseover=\"tip('" + spx.lang('hover.ccglayer') + "');\" data-name='lay'>" + playlayer + "</span>";
html += "&nbsp;";
// html += " <span class='delim'></span> ";
};
if (webplayout !="-") {
html += "<span class=\"copyid\" onclick=\"revealItemLayer(this);\" id=\"layer" + itemid + "\" onmouseover=\"tip('" + spx.lang('hover.weblayer') + "');\" data-name='web'>" + webplayout + "</span>";
// html += " <span class='delim'></span> ";
};
if (out) {
html += "<span class=\"copyid\" onclick=\"revealItemTiming(this);\" id=\"out" + itemid + "\" onmouseover=\"tip('" + spx.lang('hover.outmode') + "');\" data-name='out'>" + out + "</span>";
};
html += "<span id=\"copy" + itemid + "\" class=\"copyid\" onclick=\"revealItemID(this);\" onmouseover=\"tip('Copy ID " + itemid + "');\" data-name='id'>ID</span>";
// help button
html += "<span class=\"helpid\" onclick=\"help('ITEM-DETAILS');\" onmouseover=\"tip('Help');\">?</span>";
html += "</div>"
return html
},
// populate filelist for the controller add CSV file view (for instance)
generateFileList(requestType) {
let folder = '' // folder to start to scan
let filter = '' // extensions to show ('csv' for example)
// requestType = search command, such as "CSV"
switch (requestType) {
case 'CSV':
folder = path.join(config.general.dataroot, currentShow, 'profile.json');
filter = 'csv';
break;
default:
logger.error('Unknown requestType ' + requestType + ', doing nothing');
return; // exit function
break;
}
let html="";
// get show profile
// iterate all templates
let profileFile = path.join(config.general.dataroot, currentShow, 'profile.json');
showProfileData = spx.GetJsonData(profileFile);
if(!showProfileData.templates) {
// if no templates assigned to this project
html += '<span style="display: inline-block; margin: 10px;padding: 0.6em 1.7em; background: #0000001c; color:#ffffff; font-size:0.7em; border-radius:2em; text-align:center;">' + spx.lang('warning.notemplates') + '</span>';
return html
}
showProfileData.templates.forEach((template,i) => {
html += '<div onClick="addSelectedTemplate(' + i + ');" class="addTemplateItem row_accent' + template.uicolor + '">\n';
html += '<button type="button" class="addbtn bg_blue ripple" onClick="addSelectedTemplate(' + i + ');" style="float:right;">+</button>\n';
html += '<span class="templateName">' + spx.prettifyName(template.relpath) + '</span><BR>\n';
html += '<span class="templateDesc">' + template.description + '&nbsp;</span>\n';
html += '</div>\n';
});
return html
},
// populate templatelist for the controller add template view
generateTemplateList(currentShow) {
let html="";
// get show profile
// iterate all templates
let profileFile = path.join(config.general.dataroot, currentShow, 'profile.json');
showProfileData = spx.GetJsonData(profileFile);
if(!showProfileData.templates) {
// if no templates assigned to this project
html += '<span style="display: inline-block; width: 100%; margin: 10px;padding: 0.6em 1.7em; background: #0000001c; color:#ffffff; font-size:0.7em; border-radius:2em; text-align:center;">' + spx.lang('warning.notemplates') + '</span><br><br>';
return html
}
showProfileData.templates.forEach((template,i) => {
html += '<div onClick="addSelectedTemplate(' + i + ');" class="addTemplateItem row_accent' + template.uicolor + '">\n';
html += '<button type="button" class="addbtn bg_blue ripple" onClick="event.stopPropagation(); addSelectedTemplate(' + i + ');" style="float:right;">+</button>\n';
html += '<span class="templateName">' + spx.prettifyName(template.relpath) + '</span><BR>\n';
html += '<span class="templateDesc">' + template.description + '&nbsp;</span>\n';
html += '</div>\n';
});
return html
},
// convert timestamp (epoch) to locale date string "31.12.2021 17:45"
generateTimestamp(epoch) {
let date = new Date(parseInt(epoch));
let fDate = date.toLocaleDateString();
let fTime = date.toLocaleTimeString();
return fDate + ' ' + fTime;
},
// populate WebPlayout options for show config templates
generateButtonColorClasses(currentValue)
{
let html="";
let AvailableOptions=[
{
text:spx.lang('color.red'),
value:'bg_red'
},
{
text:spx.lang('color.green'),
value:'bg_green'
},
{
text:spx.lang('color.grey'),
value:'bg_grey'
},
{
text:spx.lang('color.orange'),
value:'bg_orange'
},
{
text:spx.lang('color.blue'),
value:'bg_blue'
},
{
text:spx.lang('color.black'),
value:'bg_black'
},
{
text:spx.lang('color.light'),
value:'bg_grey15'
}
];
AvailableOptions.forEach((OptionItem,i) => {
let sel = "";
if (OptionItem.value==currentValue){ sel="selected" }
html += '<option value="' + OptionItem.value + '" ' + sel + '>' + OptionItem.text + '</option>';
});
return html
},
// return username
CurrentUserName() {
return global.user;
},
// populate out options for show config templates
generateOutOptions(currentOut) {
logger.debug('Generating out options. This selection: ' + currentOut + '.');
let html="";
let AvailableOptions=[
{value:'manual', text:'Manually'},
{value:'none', text:'None (just play)'},
{value:'1000', text:'In 1 second'},
{value:'2000', text:'In 2 seconds'},
{value:'4000', text:'In 4 seconds'},
{value:'6000', text:'In 6 seconds'},
{value:'10000', text:'In 10 seconds'}
];
AvailableOptions.forEach((outOption,i) => {
let sel = "";
let CurVal = currentOut.toLowerCase();
let CurOpt = outOption.value.toLowerCase();
if (CurVal==CurOpt){ sel="selected" }
html += '<option value="' + CurOpt + '" ' + sel + '>' + outOption.text + '</option>';
});
return html
},
// populate WebPlayout options for show config templates
generateWebPlayoutOptions(currentLayer='-') {
// console.log('generateWebPlayoutOptions got currentLayer: ' + currentLayer);
currentLayer = spx.max5(currentLayer);
logger.debug('Generating webplayout options. This selection: '+ currentLayer);
let html="";
let AvailableLayers=['-',1,2,3,4,5];
AvailableLayers.forEach((layerOption,i) => {
let curSrv = layerOption || '-';
let cfgSrv = currentLayer || '-';
let sel = "";
if (curSrv==cfgSrv){ sel="selected" }
html += '<option value="' + curSrv + '" ' + sel + '>' + curSrv + '</option>';
});
return html
},
// preview span for collapsed view of each rundown item
generateCollapsedHeadline(DataFields) {
return spx.generateCollapsedHeadline(DataFields);
},
// generate radio buttons for show config templates
generateColorAccents(selectedIndex, templateIndex) {
let colors=['gra','red','ora','gre','blu','pin','vio','bla'];
let html ="";
let sel = "";
colors.forEach((color,loopindex) => {
if (selectedIndex==loopindex) { sel = "checked" } else {sel=""};
html += '<input type="radio" name="templateData[uicolor]" value="' + loopindex + '" id="' + color + '_' + templateIndex + '" ' + sel +'/><label for="'+ color + '_' + templateIndex +'"><span class="' + color + '"></span></label>';
});
return html
},
// Returns IP for socket server of this app's IP address
getServerAddress: function () {
var ip = require('ip');
var myIP = ip.address() + ':' + port;
return myIP;
},
// Returns resolution options for App Config > resolution
// Added in 1.3.0
generateResolutionOptions: function (resolutionOption) {
let html = '';
let options = [
{ 'value': 'HD', 'text': 'HD (1080p)' },
{ 'value': '4K', 'text': '4K (2160p)' },
{ 'value': 'AUTO', 'text': 'AUTO (responsive)' },
];
let selectedFound = false;
options.forEach((opt,i) => {
let sele = '';
if (opt.value.toUpperCase() == resolutionOption.toUpperCase()) {
selectedFound = true;
sele = 'selected'
};
html += '<option ' + sele + ' value=' + opt.value + '>' + opt.text + '</option>\n';
});
return html;
},
// See if cssSizeValue has 'vh' or 'vw' in it then return HD values
// Added in 1.3.0
localPreviewSize: function (cssSizeValue) {
if (cssSizeValue.includes('vw')) {
return '1920';
} else if (cssSizeValue.includes('vh')) {
return '1080';
} else {
return cssSizeValue;
}
},
// Returns unique ID for this host machine
getHostID: function () {
return global.pmac;
},
hookLoadControllerPlugins: function () {
// Loads init.js files from all sub folders of ASSETS/plugins/<plugName>/
let html = '';
try {
let BrowseFolder = path.join(spx.getStartUpFolder(), 'ASSETS', 'plugins');
const list = spx.GetFilesAndFolders(BrowseFolder);
list.foldArr.forEach(pluginName => {
if ( pluginName.charAt(0) == '_' || pluginName.charAt(0) == '.' || pluginName === 'lib' ) {
html += '<!-- Note: disabled plugin ' + pluginName + ' skipped. -->\n'
} else {
logger.verbose('Loading plugin ' + pluginName );
html += '<script type="module" src="/plugins/' + pluginName + '/init.js"></script>\n'
}
})
} catch (error) {
html = '<!-- hookLoadControllerPlugins error: ' + error + ' -->';
}
return html
},
// Convert JSON \\n to newline for textarea
injectNewlines(jsonString) {
let multiline = jsonString.replace('\\n', '\r\n')
return multiline;
},
// Language string helper
lang(str) {
return spx.lang(str);
},
// Replace underscores with spaces (used by breadcrumbs)
prettyfyBreadcrumb(str) {
let fixed = str
fixed = fixed.replace(/_/g, ' ');
fixed = fixed.replace(/-/g, ' ');
return fixed;
},
// Return object as JSON string
myStringify(obj) {
return "// Stringified JSON\n\n" + JSON.stringify(obj, null, 4);
},
// Get template name from filepath
nameFromTemplatePath(filepath) {
return spx.prettifyName(filepath);
},
// Check config and return value
allowOpeningFolder() {
let disableButton = config.general.disableOpenFolderCommand || false;
// console.log('allowOpeningFolder: ' + disableButton);
if (disableButton==true || disableButton=="true") {
return 'disabled';
} else {
return '';
}
},
// Make long strings nicer to look at
shortifyUIstrings(str) {
return spx.shortifyName(str);
},
// generate checkbox to appconfig
OpenChromeCheck() {
let launch = config.general.launchBrowser || false;
if (launch){
return '<input type="checkbox" checked name="general[launchBrowser]">';
}
else {
return '<input type="checkbox" name="general[launchBrowser]">';
}
},
// Get a correct class for a play button
playButtonClass(onair) {
let value = onair || "false";
let ONAIR = String(value).toUpperCase();
if (ONAIR=="TRUE") {
return "bg_red";
} else {
return "bg_green";
}
},
// Get a correct class for a play button
playButtonText(onair) {
let value = onair || "false";
let ONAIR = String(value).toUpperCase();
if (ONAIR=="TRUE") {
return spx.lang('button.stop');
} else {
return spx.lang('button.play');
}
},
// Define a class for the playicon
playIconClass(onair) {
// console.log('Play icon [' + onair + ']');
let value = onair || "false";
let ONAIR = String(value).toUpperCase();
if (ONAIR=="TRUE") {
return "playTrue";
} else {
return "playFalse";
}
},
// Show username / password in the login view
ShowDemoLoginInfo() {
let showuserpass = config.general.showusercommapass || '';
if (showuserpass==""){
return "<!-- not present config.general.showusercommapass:user,pass -->";
}
let u = showuserpass.split(',')[0]
let p = showuserpass.split(',')[1]
let html="";
html += '<!-- config.general.showusercommapass -->';
html += '<tr>';
html += '<td colspan="2" class="center vmid" style="background-color: #1d1e252f; padding:0.8em; border-radius:0.8em; font-size:0.6em; line-height:1.5em;">';
html += '<span >';
html += 'Login to this instance with<BR>';
html += 'user <span style="color: rgb(252, 174, 30);"><B>' + u + '</B></span>, ';
html += 'pass <span style="color: rgb(252, 174, 30);"><B>' + p + '</B></span>';
html += '</span>';
html += '</td>';
html += '</tr>';
return html;
},
// Print out version number
ShowVersion() {
return vers;
},
// Return available lang files as options with selected one
DropdownOptionsLANG() {
let html = '';
let sel = '';
let localesFolder = path.join(spx.getStartUpFolder(), 'locales');
let fileListData = spx.getJSONFileList(localesFolder);
fileListData.files.forEach((element,i) => {
sel = '';
if (element.name == config.general.langfile)
{
sel = 'selected';
}
html += '<option value="' + element.name + '" ' + sel + '>' + element.name.replace('.json', '') + '</option>';
});
return html
},
// This is used to populate filelist dropdown control type in templates.
// Note: these files are returned as http-assets from SPX server
// Feature added in 1.0.3. and improved in 1.0.6, 1.0.9
// v1.0.15 adds relative assets (within template root folder).
PopulateFilelistOptions(assetfolder, extension, value, relpath='') {
let html = '';
let sel = '';
let fullFilePath = '';
let fullPath = '';
if ( assetfolder.startsWith('./') ) {
// relative path support added in 1.0.15
let templateFile = path.join(spx.getStartUpFolder(), 'ASSETS', 'templates', relpath);
let templateRootFolder = path.dirname(templateFile)
fullPath = path.join(templateRootFolder, assetfolder)
} else {
// absolute path in ASSETS
let assetPath = path.normalize(assetfolder || '');
fullPath = path.join(spx.getStartUpFolder(), 'ASSETS', assetPath); // fixed (again in 1.0.12)
}
let selectedValue = value;
let fileList = spx.getFileList(fullPath, extension);
// console.log('files', fileList);
if (fileList) {
fileList.forEach((fileRef,index) => {
if (assetfolder.substr(assetfolder.length - 1)!="/") { // fixed a bug in 1.1.1
assetfolder = assetfolder + "/";
}
fullFilePath = assetfolder + fileRef;
sel = "";
if (fullFilePath == selectedValue) {
sel = 'selected';
}
html += '<option value="' + assetfolder + fileRef + '" ' + sel + '>' + fileRef + '</option>';
});
}
return html
},
// Servers status check request
ServerStatus() {
let html = "";
if (config.casparcg) {
config.casparcg.servers.forEach((element,i) => {
let disabledClass = ''
if (element.disabled) {
disabledClass = 'disabledServer';
}
html += '<div class="dropdown serverbtn">';
html += ' <i id="indicator' + i + '" class="fas fa-check-circle" style="color: 00CC00;"></i>&nbsp;<span class="serverIndicator ' + disabledClass + '" id="server' + element.name + '">' + element.name + '</span>';
html += ' <div class="dropdown-content-right">';
html += ' <span style="display: block; opacity: 0.4; background-color: #000; width:100%;">' + element.host + ':' + element.port + '</span>';
html += ' <A href="#" OnClick="spx_system(\'CHECKCONNECTIONS\');return false;">Connection check</A><br>';
html += ' <A href="#" OnClick="clearUsedChannels(\'' + element.name + '\');">Clear all layers</A><br>';
html += ' <A href="#" OnClick="toggleActive(\'' + element.name + '\');">Toggle on/off</A><br>';
html += '</div>';
html += '</div>';
});
}
return html;
// end serverstatus
},
// See if there is custom content packages in the ASSETS folder
DetectCustomContentPackages() {
try {
const fileref = path.join(spx.getStartUpFolder(), 'ASSETS', 'spx-content-repo.txt');
if (fs.existsSync(fileref)) {
var contents = fs.readFileSync(fileref, 'utf8') || '';
if (contents == 'CONTENT_FULL') {
logger.debug('DetectCustomContentPackages: CONTENT_FULL');
return '';
} else {
logger.debug('DetectCustomContentPackages: returns ' + contents);
return '<div class="namedContentIndicator">' + contents + '</div>';
}
} else {
// No file found, return empty string
return '';
}
}
catch (error) {
logger.error('DetectCustomContentPackages Error: ' + error);
return "";
}
// end DetectCustomContentPackages
},
GenerateLocalRendererOptions(mode) {
return '<!-- Added in 1.1.0 but LocalRendererOptions are not in use yet -->\n';
} // end LocalRendererOptions
} // end helpers
})); // end app.engine
app.set('view engine', 'handlebars');
app.set('views', path.join(__dirname, 'views'));
// Middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors());
app.use(session({
secret: 'salainenSalaisuus#007',
resave: false,
saveUninitialized: false
}));
global.user = "";
// Router files
const ROUTEfiles = require('./routes/routes-api.js');
app.use('/api/', ROUTEfiles);
const ROUTEpubAPIv1 = require('./routes/routes-api-v1.js');
app.use('/api/v1/', ROUTEpubAPIv1);
const ROUTEapp = require('./routes/routes-application.js');
app.use('/', ROUTEapp);
const ROUTEccg = require('./routes/routes-casparcg.js');
app.use('/CCG', ROUTEccg);
process.on('uncaughtException', function(err) {
logger.error('Uncaught Exception: ' + err);
if(err.errno === 'EADDRINUSE') {
console.log('Process failed! Port is in use? [' + err + ']'); // most likely app running on this port already
setTimeout(function(){ process.exit(3); }, 2000);
} else if (err.errno === 'ENOENT') {
console.log('File not found error detected! [' + err + ']');
} else {
console.log('\n\nThis error in unhandled, consider SPX restart! ', err); // Added in 1.1.1
}
});
var server = app.listen(port, (err) => {
let splash = ' Copyright 2020- SPX Graphics\n\n' +
` SPX Solo version .......... ${global.vers}\n` +
' License ................... See LICENSE.txt\n' +
` Config file ............... ${configfileref}\n` +
` Cfg / locale .............. ${config.general.langfile}\n` +
` Cfg / host-id (name) ...... ${global.pmac} (${config.general.hostname})\n` +
` Cfg / loglevel ............ ${config.general.loglevel} (options: error | warn | info | verbose | debug )\n` +
` Cfg / dataroot ............ ${path.resolve(config.general.dataroot)}\n` +
` Cfg / logfolder ........... ${logDirectory}\n`;
/* ` Cfg / lauchchrome ...... ${config.general.launchchrome}\n` */
if (config.general.apikey && config.general.apikey != '') {
splash += ` Cfg / apikey ........... Set in config.json\n`;
}
if (config.osc?.enable) {
splash += ` OSC enabled ............ port ${OSCport}\n`;
}
let TemplatesFromInfo;
let tmplSourceAddress = spx.getTemplateSourcePath();
if ( tmplSourceAddress == "") {
TemplatesFromInfo = "See caspar.config"
} else {
TemplatesFromInfo = tmplSourceAddress
}
splash +=
`\n Explore WebCG related resources:` +
`\n` +
`\n This is WebCG Solo. For professional features and support\n options see WebCG Production and WebCG Broadcast.`;
console.log(splash);
let prompt = 'Open WebCG Solo in a browser:';
let spxUrl = `http://${ipad}:${port}`;
let spxLoc = `http://localhost:${port}`;
let urLeng = spxUrl.length;
let loLeng = spxLoc.length;
let prLeng = prompt.length;
let maxWid = Math.max(urLeng, loLeng, prLeng)
let minWid = Math.min(urLeng, loLeng, prLeng)
let line1s = '╭' + "─".repeat(maxWid+4) + "╮"
let line2s = prompt + " ".repeat(maxWid-prLeng)
let line3s = '╰' + "─".repeat(maxWid+4) + "╯"
let spaCnt, spacer1, spacer2
spacer1 = " ".repeat(maxWid-urLeng)
spacer2 = " ".repeat(maxWid-loLeng)
console.log('\n ' + line1s);
console.log(' │ ' + line2s + ' │');
// console.log(' │ ' + spxUrl + spacer1 + ' │');
console.log(' │ ' + spxLoc + spacer2 + ' │');
console.log(' ' + line3s);
console.log('');
if ( config.general.launchBrowser || global.generatingDefaultConfig ) {
try {
(async () => {
require("openurl").open(`http://${ipad}:${port}/`)
})();
} catch (error) {
console.warn('Could not open browser.')
}
}
});
global.io = require('socket.io')(server);
var clients = {}
io.sockets.on('connection', function (socket) {
logger.verbose('*** Socket connection (' + socket.id + ") Connections: " + io.engine.clientsCount);
clients[socket.id] = socket;
notifyMultipleControllers(); // on Connection
socket.on('disconnect', async function () {
let SPXClientName = clients[socket.id].SPXClientName || '** no name **';
logger.verbose('*** Socket disconnected (' + socket.id + ") Connections: " + io.engine.clientsCount);
delete clients[socket.id];
// Notify controller of other clients lost (such as renderers closed with X)
data = {};
data.spxcmd = 'clientLostNotification'
data.source = 'windowClose'
data.clientName = SPXClientName
io.emit('SPXMessage2Client', data);
notifyMultipleControllers(); // on Disconnect
}); // end disconnect
socket.on('SPXWebRendererMessage', function (data) {
io.emit('SPXMessage2Client', data);
});
socket.on('SPXMessage2Server', function (data) {
logger.verbose('SPXMessage2Server received', data)
switch (data.spxcmd) {
case 'identifyClient':
socket.SPXClientName = data.name;
logger.verbose('Identified [' + socket.id + '] as [' + data.name + ']');
break;
case 'saveToLog': // Added in 1.3.0
let level = data.level || 'info';
let ref = data.fileref || 'unknown';
logger[level]('TemplateLog [' + ref + '] --> ' + data.message);
break;
case 'command-name-here':
// action here
break;
default:
logger.warn('Unknown SPXMessage2Server command: ' + data.command);
}
}); // end message to server
}); // end socket-io
function notifyMultipleControllers() {
if ( config.general?.disableSeveralControllersWarning==true ) {
logger.debug('notifyMultipleControllers feature is disabled with a config flag.');
return;
}
setTimeout(function(){
let count = 0;
for (const [key, value] of Object.entries(clients)) {
if (value.SPXClientName == 'SPX_CONTROLLER') {
count++;
}
}
logger.debug('notifyMultipleControllers: ' + count + ' controllers connected.');
let data = {};
data.spxcmd = 'notifyMultipleControllers'
data.count = count
io.emit('SPXMessage2Client', data);
}, 100); // small delay
}