Files
WebCG/routes/routes-application.js
T
2026-08-12 14:21:28 +09:00

2314 lines
79 KiB
JavaScript

// --------------------------------------------
// server routes (at "/")
// --------------------------------------------
var express = require("express");
const router = express.Router();
const path = require('path');
const fs = require('fs');
const moment = require('moment');
const directoryPath = path.normalize(config.general.dataroot);
const logger = require('../utils/logger');
logger.debug('Application-route loading...');
const spx = require('../utils/spx_server_functions.js');
const cfg = require('../utils/spx_getconf.js');
const PlayoutCCG = require('../utils/playout_casparCG.js');
const PlayoutWEB = require('../utils/playout_webplayer.js');
const spxAuth = require('../utils/spx_auth.js');
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const cors = require('cors');
const { timeStamp } = require("console");
const http = require('http');
const axios = require('axios');
// ROOT ROUTES ----------------------------------------------------------------------------------------------
router.get('/', spxAuth.CheckLogin, cors(), spx.getNotificationsMiddleware, async function (req, res) {
await SaveRundownDataToDisc(); // Added in 1.0.15
let currVer = vers;
let greeting = config.general.greeting || '';
let recents = config.general.recents || [] // added in 1.1.0
let disableConfigUI = config.general.disableConfigUI || false // added in 1.1.1
res.render('view-home', {
layout: false,
greeting: greeting,
curVerInfo: req.curVerInfo,
currVer: currVer,
user: req.session.user,
recents: recents,
disableConfigUI: disableConfigUI,
config: config,
showMessage: req.session.showMessage
});
req.session.showMessage = null;
}); // get / end
router.get('/admin', spxAuth.CheckLogin, function (req, res) {
res.render('view-admin', { layout: false });
}); // get /admin end
router.get('/fileBrowser', spxAuth.CheckLogin, function (req, res) {
// added in 1.1.1
res.render('view-fileBrowser', { layout: false });
}); // get /fileBrowser end
router.get('/templates/empty.html', function (req, res) {
res.render('view-empty', { layout: false });
}); // get /templates/empty.html end
router.get(['/renderer/index.html', '/renderer'], function (req, res) {
let hideCursor = false;
// Added in 1.3.0
if (global.config.general.hideRendererCursor && global.config.general.hideRendererCursor == true) {
hideCursor = true;
}
// Added in 1.3.0
let outputSize = getResolutionFromConfig(global.config, req.query);
res.render('view-renderer', {
layout: false,
width: outputSize.width,
height: outputSize.height,
hideCursor: hideCursor
});
}); // get /renderer end
router.get('/renderer/scalable', function (req, res) {
// Added in 1.3.0
let outputSize = getResolutionFromConfig(global.config, req.query);
res.render('view-rendererscalable', {
layout: false,
width: outputSize.width,
height: outputSize.height
});
}); // get /renderer/scalable end
router.get('/renderwindow/:type', function (req, res) {
// Added in 1.3.0
let outputSize = getResolutionFromConfig(global.config, req.query);
let viewFile = '';
viewFile = 'view-rendererscalable';
if (viewFile == '') {
res.send('Invalid URL');
} else {
res.render(viewFile, {
layout: false,
width: outputSize.width,
height: outputSize.height,
mode: req.params.type
});
}
}); // get /renderwindow/:type end
router.get('/login', function (req, res) {
// Added in 1.3.2
res.render('view-login', { layout: false });
}); // login
router.get('/logout', spxAuth.Logout, function (req, res) {
res.redirect('/');
}); // get /logout end
router.post('/', spxAuth.CheckLogin, function (req, res) {
//logger.info('User "' + req.session.user + '" logged in');
res.redirect('/');
}); // post / end
router.post('/saveauthpolicy', function (req, res) {
// save settings to config and move on
let configFile = global.configfileref; // './config.json';
let configData;
let user;
let pass;
if (req.body.policy == "1") {
logger.debug('saveauthpolicy: Use auth, set user-pass fields in config')
user = req.body.username;
pass = spx.hash(req.body.password);
}
else {
logger.debug('saveauthpolicy: Dont use auth, clear user-pass fields in config');
user = "";
pass = "";
}
try {
async function saveAndReadConfig() {
// console.log('Reading config');
configData = await GetJsonData(configFile);
// console.log('Changing values');
configData.general.username = user;
configData.general.password = pass;
// console.log('Writing config');
await spx.writeFile(configFile, configData);
// console.log('Re-reading config');
await cfg.readConfig();
res.redirect('/');
};
saveAndReadConfig();
}
catch (error) {
logger.error('saveAndReadConfig() Error while saving file: ', error);
res.redirect('/');
};
}); // post /saveauthpolicy end
router.get('/config', cors(), spxAuth.CheckLogin, async (req, res) => {
// show application config (send global.config as "config" data to the view, see options object below)
await SaveRundownDataToDisc(); // Added in 1.0.15
let recents = config.general.recents || [] // added in 1.1.0
let disableConfigUI = config.general.disableConfigUI || false // added in 1.1.1
res.render('view-appconfig', {
layout: false,
config: config,
user: req.session.user,
configfile: configfileref,
recents: recents,
disableConfigUI: disableConfigUI
});
}); // get /config end
router.post('/config', spxAuth.CheckLogin, async (req, res) => {
// save incoming json form data to config.json and reload the page
let ConfigData = req.body;
if (req.body.NewName && req.body.NewHost && req.body.NewPort) {
// new server was added
var obj = {};
obj["name"] = req.body.NewName;
obj["host"] = req.body.NewHost;
obj["port"] = req.body.NewPort;
if (!ConfigData.casparcg) {
// we are adding new servers to non-existing array
ConfigData.casparcg = {};
}
if (ConfigData.casparcg.servers) {
ConfigData.casparcg.servers.push(obj);
} else {
ConfigData.casparcg.servers = [obj];
}
}
if (ConfigData.casparcg) {
// iterate all casparcg servers and remove if missing data
let KillItems = []; // a list of items to delete from server array
if (ConfigData.casparcg.servers != undefined && ConfigData.casparcg.servers.length != 0) {
ConfigData.casparcg.servers.forEach((element, i) => {
if (element.name == "" || element.host == "" || element.port == "") {
KillItems.push(i);
}
});
KillItems.forEach((item) => {
ConfigData.casparcg.servers.splice(item, 1);
});
}
} else {
// console.log('Not a single CasparCG server in config.');
}
// remove un-necessary temp-fields
delete ConfigData.NewName;
delete ConfigData.NewHost;
delete ConfigData.NewPort;
// Added in 1.1.1
ConfigData.general.recents = config.general.recents || []
// Overwrite config.json
// let datafile = './config.json'; // hey! This is wrong!
let datafile = global.configfileref; // hey! This is wrong!
logger.debug('Saving config file ' + datafile + '...');
try {
async function saveAndReadConfig() {
// console.log('*** WRITING ***');
await spx.writeFile(datafile, ConfigData);
// console.log('*** READING ***');
await cfg.readConfig();
// console.log('*** RENDERING ***');
res.render('view-appconfig', { recents: ConfigData.general.recents, layout: false, config: config, message: 'Changes saved', user: req.session.user });
};
// -----------------------------------------------------------------------------
saveAndReadConfig(); // a nice solution
} catch (error) {
logger.error('router.post/config - Error while saving file: ', error);
res.render('view-appconfig', { recents: ConfigData.general.recents, layout: false, config: config, error: 'Error saving config: ' + err, user: req.session.user });
}; //file written
}); // post /config end
router.get('/register', cors(), spxAuth.CheckLogin, async (req, res) => {
// show application config (send global.config as "config" data to the view, see options object below)
await SaveRundownDataToDisc(); // Added in 1.0.15
let recents = config.general.recents || [] // added in 1.1.0
let disableConfigUI = config.general.disableConfigUI || false // added in 1.1.1
res.render('view-registration', {
layout: false,
config: config,
user: req.session.user,
configfile: configfileref,
});
}); // get /register end
router.post('/register', async (req, res) => {
// Handles modification changes of registratiom.
// Request: form data as JSON
// Returns: Ajax response
logger.verbose('Saving registration [' + + '] in rundown [' + req.body.rundownfile + '] with updated values.');
try {
if (!config.registration) {
config.registration = {};
}
config.registration.updated = new Date().toISOString();
config.registration.country = req.body.country;
config.registration.indus = req.body.indus;
config.registration.usage = req.body.usage
config.registration.devlevel = req.body.skill
config.registration.freetext = req.body.freetext
config.registration.email = req.body.email
if (req.body.inviteDiscord == "on") {
config.registration.discord = true
} else {
config.registration.discord = false
}
if (req.body.newsletter == "on") {
config.registration.newsletter = true
} else {
config.registration.newsletter = false
}
await spx.writeFile(global.configfileref, config);
config.registration.platform = req.body.platform;
config.registration.task = req.body.task;
await axios.post('https://spx-formbot-1093142801465.us-central1.run.app/api/submit', config.registration);
req.session.showMessage = 'Thank you for the registration! 👍';
res.redirect('/');
} catch (error) {
console.error('ERROR', error);
let errmsg = 'Server error in /register [' + error + ']';
logger.error(errmsg);
res.status(500).send(errmsg) // error 500 AJAX RESPONSE
};
}); // post /register end
router.get('/shows', cors(), spxAuth.CheckLogin, async (req, res) => {
// show list of shows (folders)
await SaveRundownDataToDisc(); // Added in 1.0.15
const folderListAsJSON = await spx.GetSubfolders(config.general.dataroot);
let recents = config.general.recents || [] // added in 1.1.0
let disableConfigUI = config.general.disableConfigUI || false // added in 1.1.1
res.render('view-shows', {
layout: false,
folders: folderListAsJSON,
errorMsg: '',
user: req.session.user,
recents: recents,
config: config,
disableConfigUI: disableConfigUI
});
}); // get /shows end
router.get('/show/:foldername', cors(), spxAuth.CheckLogin, async (req, res) => {
// Show episodes (files in folder 'data')
const fileListAsJSON = await spx.GetDataFiles(config.general.dataroot + "/" + req.params.foldername + "/data/");
await SaveRundownDataToDisc(); // Added in 1.0.15
let recents = config.general.recents || [] // added in 1.1.0
let disableConfigUI = config.general.disableConfigUI || false // added in 1.1.1
let datafile = path.join(
config.general.dataroot,
req.params.foldername,
'profile.json',
);
const fileDataAsJSON = await GetJsonData(datafile);
res.render('view-episodes', {
layout: false,
files: fileListAsJSON,
folder: req.params.foldername,
errorMsg: '',
user: req.session.user,
recents: recents,
config: config,
showconfig: fileDataAsJSON,
disableConfigUI: disableConfigUI
});
}); // get /show/:foldername end
router.get(
'/show/:foldername/config',
cors(),
spxAuth.CheckLogin,
async (req, res) => {
// Show Configuration
await SaveRundownDataToDisc(); // Added in 1.0.15
let datafile = path.join(
config.general.dataroot,
req.params.foldername,
'profile.json',
);
const fileDataAsJSON = await GetJsonData(datafile);
let disableConfigUI = config.general.disableConfigUI || false; // added in 1.1.1
// a "previous folder" functionality
let treeData = '';
let templateRootPath = path.join(
spx.getStartUpFolder(),
'ASSETS',
'templates',
);
if (LastBrowsedTemplateFolder == '') {
treeData = JSON.stringify(spx.GetFilesAndFolders(templateRootPath));
} else {
treeData = JSON.stringify(
spx.GetFilesAndFolders(LastBrowsedTemplateFolder),
);
}
let recents = config.general.recents || []; // added in 1.1.0
let ERRCODE = '';
if (req.query.ERR) {
ERRCODE = 'error.' + req.query.ERR;
}
res.render('view-showconfig', {
layout: false,
TemplateFiles: treeData,
TemplateRootFolder: templateRootPath,
showconfig: fileDataAsJSON,
folder: req.params.foldername,
messageCode: '',
errorCode: ERRCODE,
user: req.session.user,
recents: recents,
config: config,
disableConfigUI: disableConfigUI,
});
},
); // get /show/:foldername/config end
router.post('/show/:foldername/config/removeTemplate', spxAuth.CheckLogin, async (req, res) => {
// Remove template index (0 based)
logger.verbose('Removing template from profile ' + req.body.showFolder);
let showFolder = req.body.showFolder || "";
let TemplateIdx = req.body.TemplIndex || 0;
let ProfileFile = path.join(config.general.dataroot, showFolder, 'profile.json');
let profileData = await GetJsonData(ProfileFile);
let templateRef = profileData.templates[TemplateIdx].relpath;
profileData.templates.splice(TemplateIdx, 1);
// Then remove references from the users-arrays of each variables
if (profileData.variables && profileData.variables.length > 0) {
profileData.variables.forEach(function (variable) {
var filtered = variable.users.filter(function (value, index, arr) {
return value != templateRef;
});
variable.users = filtered;
});
}
try {
await spx.writeFile(ProfileFile, profileData);
res.redirect('/show/' + showFolder + '/config')
} catch (error) {
logger.error('removeTemplate Error while saving file: ' + error);
// console.log('removeTemplate Error while saving file: ', error);
}; //file written
}); // removeTemplate end
router.post('/show/:foldername/config/removeExtra', spxAuth.CheckLogin, async (req, res) => {
// Remove extra index (0 based)
logger.verbose('Removing extra from profile ' + req.body.showFolder);
let showFolder = req.body.showFolder || "";
let ExtraIndex = req.body.ExtraIndex || 0;
let ProfileFile = path.join(config.general.dataroot, showFolder, 'profile.json');
let profileData = await GetJsonData(ProfileFile);
profileData.showExtras.CustomControls.splice(ExtraIndex, 1);
// spx.talk('Remving extra.');
try {
await spx.writeFile(ProfileFile, profileData);
res.redirect('/show/' + showFolder + '/config')
} catch (error) {
logger.error('removeExtra Error while saving file: ' + error);
}; //file written
}); // removeExtra end
router.post('/show/:foldername/config/saveExtra', spxAuth.CheckLogin, async (req, res) => {
// Save extra index (0 based)
// Executes when Save (in show / extra button) is clicked
logger.verbose('Overwriting an extra settings in profile ' + req.body.showFolder + ' with updated values.');
let showFolder = req.body.showFolder || "";
let ExtraIndex = req.body.ExtraIndex || 0;
let ProfileFile = path.join(config.general.dataroot, showFolder, 'profile.json');
let profileData = await GetJsonData(ProfileFile);
profileData.showExtras.CustomControls[ExtraIndex] = req.body.extraData; // replace an item in array
if (req.body.newTXT && req.body.newVAL) {
// new option was added
var obj = {};
obj["text"] = req.body.newTXT;
obj["value"] = req.body.newVAL;
if (profileData.showExtras.CustomControls[ExtraIndex].items) {
profileData.showExtras.CustomControls[ExtraIndex].items.push(obj);
}
else {
profileData.showExtras.CustomControls[ExtraIndex].items = [obj];
}
}
// check if some needs to be removed
let KillItems = []; // a list of items to delete from server array
if (profileData.showExtras.CustomControls[ExtraIndex].items) {
profileData.showExtras.CustomControls[ExtraIndex].items.forEach((element, i) => {
if (element.text == "" || element.value == "") {
KillItems.push(i);
}
});
KillItems.forEach((item) => {
profileData.showExtras.CustomControls[ExtraIndex].items.splice(item, 1);
});
}
try {
await spx.writeFile(ProfileFile, profileData);
res.redirect('/show/' + showFolder + '/config')
} catch (error) {
logger.error('saveExtra Error while saving file: ' + error);
}; //file written
}); // saveExtra end
router.post(
'/show/:foldername/config/saveTemplate',
spxAuth.CheckLogin,
async (req, res) => {
// Save template index (0 based)
// This runs when template is modified in show config
logger.verbose(
'Overwriting a template in profile ' +
req.body.showFolder +
' with updated values.',
);
let showFolder = req.body.showFolder || '';
let TemplateIdx = req.body.TemplIndex || 0;
let ProfileFile = path.join(
config.general.dataroot,
showFolder,
'profile.json',
);
let profileData = await GetJsonData(ProfileFile);
// Save steps as otherwise they disappear when template is saved
let steps;
if (profileData.templates[TemplateIdx].steps) {
steps = profileData.templates[TemplateIdx].steps;
}
let newData = req.body.templateData || {};
if (profileData.projectFormat === 'OGRAF') {
let ografProps =
profileData.templates[TemplateIdx].ografProps || {};
newData.ografProps = ografProps;
}
profileData.templates[TemplateIdx] = newData; // replace an item in array
// Save steps to new data
if (steps) {
profileData.templates[TemplateIdx].steps = steps;
}
// get DataFields and replace that
let DataFieldArray = JSON.parse(
decodeURI(profileData.templates[TemplateIdx].DataFields),
);
profileData.templates[TemplateIdx].DataFields = DataFieldArray;
// spx.talk('Saving template changes.');
try {
await spx.writeFile(ProfileFile, profileData);
res.redirect('/show/' + showFolder + '/config');
} catch (error) {
logger.error('saveTemplate Error while saving file: ' + error);
} //file written
},
); // saveTemplate end
router.post('/show/:foldername/config/saveGeneralSettings', spxAuth.CheckLogin, async (req, res) => {
// Added in 1.0.15
// Save General Settigs of a project
// console.log("Profile: " + req.body.showFolder)
// console.log("Background value: " + req.body.background)
logger.verbose('Overwriting a profile ' + req.body.showFolder + ' with updated values.');
let showFolder = req.body.showFolder || "";
let ProfileFile = path.join(config.general.dataroot, showFolder, 'profile.json');
let profileData = await GetJsonData(ProfileFile) || {};
profileData.general = {};
profileData.general.background = req.body.background;
io.emit('SPXMessage2Client', {
spxcmd: 'setRendererBackgroundImage',
background: req.body.background
});
try {
await spx.writeFile(ProfileFile, profileData);
res.redirect('/show/' + showFolder + '/config')
} catch (error) {
logger.error('saveGeneralSettings Error while saving file: ' + error);
}; //file written
}); // saveGeneralSettings end
router.post('/show/:foldername/config', spxAuth.CheckLogin, async (req, res) => {
// a POST handler for adding content to the <show>/profile.json.
//
// Called from templateChooser -modal, for instance.
// Also here we scan the source file and extract needed fields into the profile.
// Also handles adding new extras to the show profile.
//
// REQUEST .... data with template folder / name and profle folder name
// RETURNS .... json data with folder and file arrays
//
try { // added in 1.1.1
logger.verbose('Adding new stuff to profile in ' + req.body.showFolder);
let curFolder = req.body.curFolder || "";
let template = req.body.template || "";
let showFolder = req.body.showFolder || "";
let command = req.body.command || "";
let ftype = req.body.ftype || "";
let customscript = req.body.customscript || "";
let TemplatePath = path.join(curFolder, template).trim();
let ProfileFile = path.join(config.general.dataroot, showFolder, 'profile.json');
let profileData = await GetJsonData(ProfileFile) || {}; // or empty
let replaceIndex = req.body.replaceIndex || ""; // v.1.0.11 replace existing, a reimport
switch (command) {
case 'addtemplates':
// Added in 1.1.4
// ADD SEVERAL TEMPLATES AT ONCE
let templates = JSON.parse(req.body.templates || []);
templates.forEach((template, index) => {
TemplatePath = path.join(curFolder, template).trim();
profileData = addTemplateToProfile(profileData, TemplatePath, showFolder, curFolder, replaceIndex)
});
break;
case 'addtemplate':
// ADD (or RE-IMPORT) A TEMPLATE
profileData = addTemplateToProfile(profileData, TemplatePath, showFolder, curFolder, replaceIndex)
if (profileData.error) {
res.redirect('/show/' + showFolder + '/config?ERR=templateDefinitionMissing')
}
break;
case 'addshowextra':
// ------------------------------------- ADDING EXTRA -----------------
newExtra = {};
switch (ftype) {
case 'button':
newExtra.description = 'A new push button';
newExtra.ftype = 'button';
newExtra.bgclass = 'bg_blue';
newExtra.text = 'Hello world';
newExtra.fcall = "demo_popup('Hello world!')";
break;
case 'togglebutton':
newExtra.description = 'A new toggle button';
newExtra.ftype = 'togglebutton';
newExtra.bgclass = 'bg_blue';
newExtra.text0 = 'START ME';
newExtra.text1 = 'STOP ME';
newExtra.fcall = 'demo_toggle(this)';
break;
case 'selectbutton':
newExtra.description = 'Selectbutton demo';
newExtra.ftype = 'selectbutton';
newExtra.bgclass = 'bg_blue';
newExtra.text = 'Play';
newExtra.fcall = 'playSelectedAudio';
newExtra.value = 'media/wav/applause.wav';
newExtra.items = [];
let option0 = {};
option0.text = 'Applause';
option0.value = 'media/wav/applause.wav';
newExtra.items.push(option0);
let option1 = {};
option1.text = 'No!';
option1.value = 'media/wav/no.wav';
newExtra.items.push(option1);
let option2 = {};
option2.text = 'Yesss!';
option2.value = 'media/wav/yes.wav';
newExtra.items.push(option2);
break;
default:
logger.warn('Warning: Extra type ' + ftype + ' is unknown. Adding nothing to the profile-file.');
break;
}
if (!profileData.showExtras) {
// lets add a showExtras section to profile
profileData.showExtras = {};
profileData.showExtras.CustomControls = [];
}
profileData.showExtras.CustomControls.push(newExtra);
break;
case 'addshowextrascript':
// ------------------------------- ADDING SHOW EXTRA SCRIPT -----------------
if (!profileData.showExtras) {
// lets add a showExtras section to profile
profileData.showExtras = {};
profileData.showExtras.CustomControls = [];
}
profileData.showExtras.customscript = customscript;
break;
default:
// --------------------------- UNKNOWN COMMAND -----------------
logger.warn('Warning: Command ' + command + ' is unknown. Adding nothing to the profile-file.');
break;
}
// await spx.writeFile(ProfileFile,profileData);
await spx.writeFile(ProfileFile, profileData)
.then(
res.redirect('/show/' + showFolder + '/config')
)
.catch(function (error) {
console.log('\nEnd of template import errors.', error)
// res.redirect('/show/' + showFolder + '/config')
})
} catch (error) {
logger.error('showconfig / ' + error);
// res.redirect('/show/' + showFolder + '/config')
}; //file written
}); // browseTemplates API post request end
router.post('/shows/', spxAuth.CheckLogin, async (req, res) => {
// add a new showfolder (with /data -subfolder) and open it for configuration
let targetFolder = path.join(config.general.dataroot, req.body.foldername);
let targetDataFo = path.join(config.general.dataroot, req.body.foldername, "data");
let folderListAsJSON = ""
if (fs.existsSync(targetFolder)) {
folderListAsJSON = await spx.GetSubfolders(config.general.dataroot);
res.render('view-shows', { layout: false, folders: folderListAsJSON, error: 'Folder exists, try again with another name.', user: req.session.user });
}
else {
fs.mkdirSync(targetFolder);
fs.mkdirSync(targetDataFo);
// save a new empty profile.json
let profileFile = {};
profileFile.projectFormat = req.body.format.toUpperCase() || 'SPX';
let profileFilePath = path.join(targetFolder, 'profile.json');
await spx.writeFile(profileFilePath, profileFile);
res.redirect('/show/' + req.body.foldername + '/config');
}
}); // post /shows/ end
router.post('/show/:foldername', spxAuth.CheckLogin, async (req, res) => {
// add a new json file and open it for editing
let datafile = path.join(config.general.dataroot + '/' + req.params.foldername + '/data/' + req.body.filebasename) + '.json';
logger.verbose('Creating file', datafile);
// check if exist
if (fs.existsSync(datafile)) {
//file exists
logger.warn('Episode file exists [' + datafile + '], going back to episode list.');
const fileListAsJSON = await spx.GetDataFiles(config.general.dataroot + "/" + req.params.foldername + "/data/");
res.render('view-episodes', { layout: false, files: fileListAsJSON, folder: req.params.foldername, error: 'File exists, use another name.', user: req.session.user });
}
else {
// file was not there
// spx.talk('Adding a new empty rundown file...');
try {
let EmptyJSON = {};
await spx.writeFile(datafile, EmptyJSON);
res.redirect('/gc/' + req.params.foldername + '/' + req.body.filebasename);
} catch (error) {
logger.error('Error while creating file: ' + error);
}; //file written
}
}); // post /show/:foldername end
router.delete('/show/:folder/:file', spxAuth.CheckLogin, async (req, res) => {
res.send("would delete file " + req.params.folder + "/data/" + req.params.file + ".json");
let datatemp = path.join(config.general.dataroot, req.params.folder, "data", req.params.file) + '.json';
let datafile = path.normalize(datatemp);
fs.unlink(datafile, function (err) {
if (err) {
logger.error('Error while deleting ' + datafile + ': ' + err)
// res.send(err);
}
else {
logger.verbose('Deleted file: ' + datafile);
global.rundownData = {};
// client will reload in 500ms
}
});
}); // delete /show/:folder/:file end
router.delete('/shows/:foldername', spxAuth.CheckLogin, async (req, res) => {
// delete a showfolder (an Ajax call, therefor the redirect does not work, must do from client side.)
let datafolder = path.join(config.general.dataroot, req.params.foldername);
const deleteFolderRecursive = function (datafolder) {
if (fs.existsSync(datafolder)) {
fs.readdirSync(datafolder).forEach((file, index) => {
const curPath = path.join(datafolder, file);
logger.debug('Deleting ' + curPath);
if (fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
} else { // delete file
logger.verbose('Deleted file: ' + curPath);
fs.unlinkSync(curPath);
}
});
logger.debug('Deleting folder ' + datafolder);
fs.rmdirSync(datafolder);
}
else {
logger.warn('Folder ' + datafolder + ' did not exist, cannot delete.');
}
}
try {
deleteFolderRecursive(datafolder);
logger.verbose('Deleted folder: ' + datafolder);
}
catch (error) {
logger.error('Error while deleting ' + datafolder + ': ' + error)
res.send(error);
}
const folderListAsJSON = await spx.GetSubfolders(config.general.dataroot);
res.render('view-shows', { layout: false, folders: folderListAsJSON, errorMsg: '', user: req.session.user });
}); // delete /shows/:foldername end
router.get('/gc/:foldername/:filename/:mode?', cors(), spxAuth.CheckLogin, async (req, res) => {
// G R A P H I C C O N T R O L L E R M A I N V I E W
let datafile = path.join(config.general.dataroot, req.params.foldername, 'data', req.params.filename) + '.json';
let [easterImg, easterTxt] = spx.getEasterEggImage();
const fileDataAsJSON = await spx.RemoveFilepathKey(GetJsonData(datafile));
if (fileDataAsJSON?.ografs) {
fileDataAsJSON.templates = fileDataAsJSON.ografs;
fileDataAsJSON.ografs = null
fileDataAsJSON.projectFormat = "OGRAF"
}
// fileDataAsJSON = spx.RemoveFilepathKey(fileDataAsJSON); // Added in 1.3.0
fileDataAsJSON.project = req.params.foldername; // Added in 1.3.2
fileDataAsJSON.rundown = req.params.filename; // Added in 1.3.2
// Please note, these does not show in the rundownfile itself,
// but is used in the controller view and is in memory only for now.
// Only when something is changed (or played), it will be written to the file.
global.rundownData = fileDataAsJSON;
let showprofile = path.join(config.general.dataroot, req.params.foldername, 'profile.json');
const profileDataJSONobj = await GetJsonData(showprofile);
let profileData = JSON.stringify(profileDataJSONobj);
let projectBackground = '';
if (profileDataJSONobj && profileDataJSONobj.general && profileDataJSONobj.general.background) {
projectBackground = profileDataJSONobj.general.background;
}
// list of CSV files
// TODO:
let csvFileList = '';
csvFileList = JSON.stringify(spx.GetFilesAndFolders(path.resolve(spx.getStartUpFolder(), 'ASSETS', 'csv'), 'csv'));
// Added in 1.3.0
let outputSize = getResolutionFromConfig(global.config, req.query);
let recents = config.general.recents || [] // added in 1.1.0
let preview = config.general.preview || 'none' // added in 1.1.0
let rnrtype = config.general.renderer || 'normal' // added in 1.1.0
let assetsFolder = path.resolve(spx.getStartUpFolder(), 'ASSETS') || '' // added in 1.1.0
let disableConfigUI = config.general.disableConfigUI || false // added in 1.1.1
let disableLRenderer = config.general.disableLocalRenderer || false // added in 1.2.0
function getPageTemplate() {
// Added in 1.3.0
if (req.params.mode && req.params.mode.toLowerCase() === 'light') {
return 'view-controllermini';
} else {
return 'view-controller';
}
}
res.render(getPageTemplate(), {
layout: false,
globalExtras: config.globalExtras,
filedata: fileDataAsJSON,
folder: req.params.foldername,
datafile: datafile,
filebasename: req.params.filename,
profiledata: profileData,
profiledataObj: profileDataJSONobj,
user: req.session.user,
background: projectBackground,
csvFileList: csvFileList,
width: outputSize.width,
height: outputSize.height,
previewMode: preview,
recents: recents,
renderer: rnrtype,
assetsFolder: assetsFolder,
disableConfigUI: disableConfigUI,
disableLRender: disableLRenderer,
autoplayLocalRenderer: config.general.autoplayLocalRenderer,
easteregg: easterImg,
easterText: easterTxt
});
let bgImage = ''
if (profileDataJSONobj && profileDataJSONobj.general && profileDataJSONobj.general.background) {
bgImage = profileDataJSONobj.general.background
}
setTimeout(function () {
io.emit('SPXMessage2Client', {
spxcmd: 'setRendererBackgroundImage',
background: bgImage
});
}, 500);
spx.setRecents(req.params.foldername + '/' + req.params.filename) // Added in 1.1.0
}); // get /gc/:foldername/:filename end
router.post('/gc/:foldername/:filename/', spxAuth.CheckLogin, async (req, res) => {
//
// Can handle several commands from the controller page
//
var rundownDataJSONobj;
let profileDataJSONobj;
let showprofile = "";
let data = req.body;
let filepath;
switch (data.command) {
case 'importCSVdata':
try {
// CSV IMPORT function. The source CSV must have been exported via the EXPORT function.
let fieldsFound = false; // check at the end
logger.verbose('Importing CSV from ' + data.curFolder + '/' + data.importFile + ' to be appended to ' + data.RundownFile);
rundownDataJSONobj = await GetJsonData(path.resolve(data.RundownFile));
if (!rundownDataJSONobj.templates) {
rundownDataJSONobj.templates = []
}
// now get the CSV file and start parsing line-by-line
let CSVfileData = await GetTextFileData(path.join(data.curFolder, data.importFile))
// console.log('CSV content', CSVfileData.toString());
let startSeconds = String(Date.now());
let csvLines = CSVfileData.split('\n')
var t_description
var t_playserver
var t_playchannel
var t_playlayer
var t_webplayout
var t_out
var t_uicolor
var t_onair
var t_dataformat
var t_relpath
var t_graphicPath
var t_version
var t_id
var t_project
var t_rundown
let curLineItems = []
var protoFields = [];
var protoFtypes = [];
var protoTitles = [];
var protoFcalls = [];
var protoValues = [];
var dataFiProto = {}; // placeholder obj for fields
var templateTmp = []; // placeholder arr for objects
var fieldIndex = 0; // iterate
// First we get METADATA (another lines forEach for content afterwards)
csvLines.forEach((line, lineindex) => {
if (line.trim() === '') { return; }
curLineItems = line.split(';')
// console.log('Line ' + lineindex, curLineItems);
switch (curLineItems[0].trim()) {
case '# description #':
t_description = curLineItems[1].trim();
break;
case '# playserver #':
t_playserver = curLineItems[1].trim();
break;
case '# playchannel #':
t_playchannel = curLineItems[1].trim();
break;
case '# playlayer #':
t_playlayer = curLineItems[1].trim();
break;
case '# webplayout #':
t_webplayout = curLineItems[1].trim();
break;
case '# out #':
t_out = curLineItems[1].trim();
break;
case '# uicolor #':
t_uicolor = curLineItems[1].trim();
break;
case '# onair #':
t_onair = curLineItems[1].trim();
break;
case '# dataformat #':
t_dataformat = curLineItems[1].trim();
break;
case '# relpath #':
t_relpath = curLineItems[1].trim();
break;
case '# graphicPath #':
t_graphicPath = curLineItems[1].trim();
break;
case '# version #':
t_version = curLineItems[1].trim();
break;
case '# id #':
t_id = curLineItems[1].trim();
break;
case '# project #':
t_project = curLineItems[1].trim();
break;
case '# rundown #':
t_rundown = curLineItems[1].trim();
break;
case '# FieldUUIDs #':
// this line carries f-field ids (f0, f1, etc)
// and will generate required objects
fieldsFound = true;
for (let uuidindex = 1; uuidindex < curLineItems.length - 1; uuidindex++) {
protoFields.push(curLineItems[uuidindex].trim());
}
break;
case '# FieldTypes #':
for (let typeindex = 1; typeindex < curLineItems.length - 1; typeindex++) {
let thisFieldType = curLineItems[typeindex].trim()
protoFtypes.push(thisFieldType)
}
break;
case '# FieldTitls #':
for (let titindex = 1; titindex < curLineItems.length - 1; titindex++) {
protoTitles.push(curLineItems[titindex].trim())
}
break;
case '# Fieldfcalls #':
for (let fcallsindex = 1; fcallsindex < curLineItems.length - 1; fcallsindex++) {
protoFcalls.push(curLineItems[fcallsindex].trim())
}
} // switch line type
}); // for each line METADATA done
// Then iterate all CONTENT LINES and append template to OBJ array
var templateData
csvLines.forEach((line, loopIndex) => {
if (line.trim() === '') { return; } // empty line
curLineItems = line.split(';')
templateData = {}
templateData.DataFields = []
if (line.startsWith('# ID:')) {
fieldIndex++; // template counter for GUI message
for (let index = 1; index < curLineItems.length - 1; index++) {
protoValues.push(curLineItems[index].trim())
}
// generate ID
if (line.startsWith('# ID:auto')) {
// generate EPOCH
templateData.itemID = startSeconds + loopIndex
} else {
// use the given value
templateData.itemID = curLineItems[0].trim().replace('# ID:', '').trim()
}
templateData.description = t_description
templateData.playserver = t_playserver
templateData.playchannel = t_playchannel
templateData.playlayer = t_playlayer
templateData.webplayout = t_webplayout
templateData.out = t_out
templateData.uicolor = t_uicolor
templateData.onair = t_onair
templateData.dataformat = t_dataformat
templateData.relpath = t_relpath
// If ograf, then add ograf properties
if (t_graphicPath && t_version && t_id) {
templateData.ografProps = {}
templateData.ografProps.graphicPath = t_graphicPath
templateData.ografProps.version = t_version
templateData.ografProps.id = t_id
}
// Append warning
templateData.DataFields.push({
ftype: "instruction",
value: "⚠ WARNING: This item was generated with CSV import and typically is not intended for manual editing. Please use the original template for manual input."
});
// Then iterate all fields and assing values
for (let i = 0; i < protoFields.length; i++) {
dataFiProto = {}
dataFiProto.field = protoFields[i]
if (protoFtypes[i] == 'textarea') {
dataFiProto.ftype = 'textarea' // for multiline texts
} else if (protoFtypes[i] == 'button') {
dataFiProto.ftype = 'button'
dataFiProto.fcall = protoFcalls[i]
} else if (protoFtypes[i] == 'checkbox')
dataFiProto.ftype = 'checkbox'
else {
dataFiProto.ftype = 'textfield' // protoFtypes[i] !forced!
}
// dataFiProto.ftype = "textfield" // protoFtypes[i] !forced!
dataFiProto.title = protoTitles[i]
if (curLineItems[i + 1].length > 0) {
dataFiProto.value = curLineItems[i + 1].replace(/<BR>/g, '\n') // add newlines back
}
templateData.DataFields.push(dataFiProto);
}
rundownDataJSONobj.templates.push(templateData)
} // content line process ended
});
// saving new items to disk
rundownDataJSONobj.project = t_project; // Added in 1.3.2
rundownDataJSONobj.rundown = t_rundown; // Added in 1.3.2
global.rundownData = rundownDataJSONobj;
await SaveRundownDataToDisc(data.RundownFile) // Bug fixed in 1.3.2
if (fieldsFound) {
res.redirect('/gc/' + data.foldername + '/' + data.filebasename + '?msg=Added ' + fieldIndex + ' templates.');
break;
} else {
res.redirect('/gc/' + data.foldername + '/' + data.filebasename + '?msg=invalidCSV');
}
} catch (error) {
logger.error('importCSVdata Error: ', error);
}
break;
case 'addAllItemsToRundown':
// ADD ALL TEMPLATES TO RUNDOWN /// Added in 1.2.0
try {
logger.verbose('Adding all show profile templates to rundown ' + data.listname);
showprofile = path.join(config.general.dataroot, data.foldername, 'profile.json');
profileDataJSONobj = await GetJsonData(showprofile);
let ProfileTemplatesArray = profileDataJSONobj.templates || [];
ProfileTemplatesArray.forEach((template, index) => {
template.itemID = String(Date.now() + index); // generate EPOCH for each template
});
rundownDataJSONobj = await GetJsonData(data.datafile);
rundownDataJSONobj.projectFormat = profileDataJSONobj.projectFormat;
if (!rundownDataJSONobj.templates) {
rundownDataJSONobj.templates = ProfileTemplatesArray;
} else {
rundownDataJSONobj.templates = rundownDataJSONobj.templates.concat(ProfileTemplatesArray);
}
await spx.writeFile(data.datafile, rundownDataJSONobj);
res.redirect('/gc/' + data.foldername + '/' + data.listname);
return;
} catch (error) {
logger.error('addAllItemsToRundown Error while saving file: ', error);
}; //file written
break;
case 'addItemToRundown':
// ADD TEMPLATE ITEM TO RUNDOWN ///////
try {
logger.verbose('Adding show profile template[' + data.templateindex + '] to rundown ' + data.listname);
showprofile = path.join(config.general.dataroot, data.foldername, 'profile.json');
profileDataJSONobj = await GetJsonData(showprofile);
let TemplateModel = profileDataJSONobj.templates[data.templateindex];
TemplateModel.itemID = String(Date.now()); // Generate ID (epoch milliseconds) for this rundown item (from v. 0.9.8 onwards)
// This will be used to control UI (play, remove, sort, etc...)
rundownDataJSONobj = await GetJsonData(data.datafile);
if (!rundownDataJSONobj.templates) {
rundownDataJSONobj.templates = [];
}
rundownDataJSONobj.templates.push(TemplateModel);
rundownDataJSONobj.projectFormat = profileDataJSONobj.projectFormat;
await spx.writeFile(data.datafile, rundownDataJSONobj);
res.redirect('/gc/' + data.foldername + '/' + data.listname);
return;
} catch (error) {
logger.error('addItemToRundown Error while saving file: ', error);
// console.log('addItemToRundown Error while saving file: ', error);
}; //file written
case 'removeItemFromRundown':
// REMOVE TEMPLATE ITEM FROM RUNDOWN (without reloading page) ///////////////////////////////////////////////////////////////////////////
logger.verbose('Removing template epoch [' + data.epoch + '] from rundown ' + data.listname);
rundownDataJSONobj = await GetJsonData(data.datafile);
rundownDataJSONobj = spx.RemoveFilepathKey(rundownDataJSONobj); // Added in 1.3.0
let templateIndex = -1;
rundownDataJSONobj.templates.forEach((template, index) => {
if (template.itemID == data.epoch) {
templateIndex = index;
}
//
});
if (templateIndex > -1) {
rundownDataJSONobj.templates.splice(templateIndex, 1);
}
try {
rundownDataJSONobj = await spx.appendProjectFile(rundownDataJSONobj, data.datafile, "from gc/removeItemFromRundown");
global.rundownData = rundownDataJSONobj;
await spx.writeFile(data.datafile, rundownDataJSONobj);
res.status(200).send('Item removed ok.'); // ok 200 AJAX RESPONSE
return;
} catch (error) {
logger.error('removeItemFromRundown Error while saving file: ', error);
// console.log('removeItemFromRundown Error while saving file: ', error);
}; //file written
break;
case 'duplicateRundownItem':
// CLONE TEMPLATE ITEM ON RUNDOWN (without reloading page) ///////////////////////////////////////////////////////////////////////////
let freshID = data.cloneEpoch;
var newItemData = "";
var duplicateData = "";
var duplicated = false;
logger.verbose('Cloning item [' + data.sourceEpoch + '] to [' + freshID + '] in file [' + data.listname + '].');
rundownDataJSONobj = await GetJsonData(data.datafile);
var NewTemplateArray = [];
for (var i = 0; i < rundownDataJSONobj.templates.length; i++) {
NewTemplateArray.push(rundownDataJSONobj.templates[i]);
let duplicateData = JSON.parse(JSON.stringify(rundownDataJSONobj.templates[i])); // copy values and NOT a reference! Sheeeeeet!!!
duplicateData.itemID = freshID;
duplicateData.onair = 'false';
if (rundownDataJSONobj.templates[i].itemID == data.sourceEpoch && !duplicated) {
// console.log('Yes, found ID ' + rundownDataJSONobj.templates[i].itemID + ' which gets duplicated once.');
// console.log('Copy', duplicateData);
NewTemplateArray.push(duplicateData);
global.rundownData.templates.push(duplicateData); // Fixed "duplicate play issue" in 1.1.1
duplicated = true;
}
}
rundownDataJSONobj.templates = NewTemplateArray;
// console.log('Saving new data ', rundownDataJSONobj.templates);
try {
await spx.writeFile(data.datafile, rundownDataJSONobj);
res.status(200).send('Item duplicated ok.'); // ok 200 AJAX RESPONSE
return;
} catch (error) {
logger.error('duplicateRundownItem Error while saving file: ', error);
// console.log('duplicateRundownItem Error while saving file: ', error);
}; //file written
break;
case 'saveRundownItemModifications':
// SAVE CHANGES MADE TO A SINGLE RUNDOWN ITEM /////////////////////////////////////////////////////////////////////////////////////////
// Not in use, see --> /gc/saveItemChanges
break;
default:
logger.warn('Warning: unknown gc-post command: ' + data.command);
break;
}
}); // gc post end
router.post('/gc/playout', spxAuth.CheckLogin, async (req, res) => {
// Request: data object (command, datafile, templateIndex)
// Returns: AJAX response
//
// Handles playout commands of the rundown items in the controller.
// This function will collect data from datafile and will send out "stupid"
// playout commands to all needed renderer interfaces.
//
// This will also persist the onair state to rundown file.
//
// 1.0.15 Performance improvements added by prioritizing memory usage over
// disk I/O. Problem was with concurrent events. Now this is done
// in memory and data is written to disk only when we SHOW folders
// after being in the rundown view (so memory has been populated).
//
// 1.3.0 MAJOR BUG FIXED! If rundown was populated to memory it was used
// when controlRundownItemByID API call was made. This caused the
// rundown to be read from memory data and not actual rundown file
// that was requested. Now a new forceFileRead parameter is added.
//
// console.log('Playout command received.', req.body);
let templateIndex = -1; // was 0
try {
let dataOut = {}; // new object
if (req.body.command == 'customAction') {
dataOut.spxcmd = 'customAction'
dataOut.id = req.body.id
dataOut.webplayout = req.body.webplayout
PlayoutWEB.webPlayoutController(dataOut)
return
}
dataOut.fields = []; // init it with an empty arr as placeholder
let RundownData = "" // file JSON
let preventSave = false;
let forceDiskRead = req.body.forceFileReadOnce ? true : false; // Added in 1.3.0. Read Release Notes!
let cacheRundownData = null; // Added in 1.3.0
let autoOutTimerID = null; // Added in 1.3.0
let itemID = null; // Added in 1.3.2
let RundownFile = path.normalize(req.body?.datafile || '') // file reference (in a normal call)
let ItemData = {}; // Modified in 1.3.2
if (req.body?.updateRundownItem?.persist) {
let project = req.body?.updateRundownItem?.project || '';
let rundown = req.body?.updateRundownItem?.rundown || '';
RundownFile = path.resolve(spx.getDatarootFolder(), project, 'data', rundown + '.json');
}
// console.log('RundownFile', RundownFile);
// console.log('Body', req.body);
let skipJSONRead = true;
if (req.body?.prepopulated != "true") { skipJSONRead = false; }
if (req.body?.updateRundownItem?.persist) { skipJSONRead = false; }
if (skipJSONRead) {
// data in pre-generated coming in. So we can just pass that along.
logger.verbose('Playout command prepopulated [' + req.body.command + '] template [' + req.body.relpath + '].');
dataOut = req.body;
if (req.body.command != 'invoke' && req.body.command != 'stop') { // added stop in 1.1.4
dataOut.relpathCCG = req.body.relpath.split('.htm')[0]; // casparCG needs template path without extension
}
preventSave = true;
// console.log('IF', dataOut);
} else {
// Normal method of working (or when using directplayout persist request)
// We collect data from rundown first.
itemID = req.body?.epoch || req.body?.updateRundownItem?.itemID || '';
logger.verbose('Playout command [' + req.body.command + '] item [' + itemID + '] of [' + RundownFile + '].');
// Refactored in 1.0.15 to prioritize memory over disk I/O
// 1.3.0 added forceDiskRead parameter to use when API call is made.
let notifyDataSource = ''
if (Object.keys(global.rundownData).length === 0 || forceDiskRead) {
if (Object.keys(global.rundownData).length > 0) {
cacheRundownData = global.rundownData; // cache it
}
notifyDataSource = 'from file:'
RundownData = await GetJsonData(RundownFile);
} else {
notifyDataSource = 'from memory:'
RundownData = global.rundownData; // use memory only
}
let projectFormat = RundownData.projectFormat || 'SPX';
RundownData.templates.forEach((template, index) => {
logger.debug('Matching ' + index + ': [' + template.itemID + '] (' + template.description + ') and [' + itemID + ']');
if (template.itemID == itemID) {
templateIndex = index;
}
});
if (templateIndex == -1) {
throw 'Requested itemID [' + itemID + '] not found on the rundown [' + RundownFile + ']! Aborting ' + req.body.command + ' command.';
}
ItemData = RundownData.templates[templateIndex];
dataOut.ografProps = ItemData.ografProps; // OGRAF Support
dataOut.projectFormat = projectFormat; // OGRAF Support
dataOut.graphicPath = ItemData.graphicPath; // OGRAF Support
dataOut.relpath = ItemData.relpath;
dataOut.relpathCCG = ItemData.relpath.split('.htm')[0]; // casparCG needs template path without htm/html -extension.
dataOut.playserver = ItemData.playserver;
dataOut.playchannel = ItemData.playchannel;
dataOut.playlayer = ItemData.playlayer;
dataOut.webplayout = ItemData.webplayout;
dataOut.out = ItemData.out; // 1.3.0 only used with direct API calls
dataOut.dataformat = ItemData.dataformat || 'json'; // Changed to JSON in 1.3.0
ItemData.DataFields.forEach(item => {
// We pass data as custom JSON format and the format is changed
// downstream in the playout controller as per renderer's needs.
let FieldItem = {};
if (item.field) {
FieldItem.field = item.field;
if (item.value) { // TODO: bug alert (&& item.field.value) check if there IS value to begin with. Added 29.01.2021
FieldItem.value = spx.cleanUpString(item.value); // Added in 1.3.2
}
dataOut.fields.push(FieldItem);
}
});
dataOut.fields.push({ 'field': 'epochID', 'value': itemID });
} // else (ie read from JSON)
let playOutCommand = "";
if (req.body.command == 'playonce') {
playOutCommand = "play";
preventSave = true;
logger.verbose('Note playonce command received, preventSave = true');
} else {
playOutCommand = req.body.command;
}
// Added in 1.3.2 if directplayout is used, we need to
// update incoming data with the itemID.
// If data from API, then override
if (req.body?.updateRundownItem?.persist) {
let overrideTemplateIndex = -1;
RundownData.templates.forEach((template, index) => {
if (template.itemID == req.body.updateRundownItem.itemID) {
// console.log('Template FOUND', index, template.description);
overrideTemplateIndex = index;
template.DataFields.forEach((templateField, dfIndex) => {
// console.log('templateField', dfIndex)
req.body.fields.forEach((dataField, daIndex) => {
// note, "fields" not "DataFields"
// console.log('.....dataField', daIndex)
if (templateField.field == dataField.field) {
RundownData.templates[overrideTemplateIndex].DataFields[dfIndex].value = dataField.value;
}
});
})
}
});
dataOut = req.body; // override data for rendering
}
// console.log('RundownData after mods', RundownData.templates[templateIndex]);
switch (playOutCommand) {
// == PREVIEW ===================================================
case 'preview':
// Send PlayoutWEB.functionCalls() if any ---------------------
if (dataOut.webplayout != '-' && config.general.preview != 'none') {
dataOut.webplayout = '1'; // force to layer 1! Changed to string in 1.0.15
logger.verbose('Webplayout PREVIEW: ' + dataOut.webplayout);
dataOut.playmode = 'preview';
dataOut.spxcmd = 'playTemplate';
PlayoutWEB.webPlayoutController(dataOut);
} else {
logger.verbose('No Webplayout playout');
}
break;
// == AUTOPLAY ==================================================
case 'autoPlayLocal':
// Update local renderer when a rundown is reloaded
logger.verbose('Webplayout autoPlayLocal: ' + dataOut.webplayout);
dataOut.spxcmd = 'playTemplate';
dataOut.modify = 'autoPlayLocal';
PlayoutWEB.webPlayoutController(dataOut);
break;
// == PLAY =====================================================
case 'play':
let playingSomewhere = false; // Added in 1.3.0
// console.log('DataOut', dataOut);
// Send PlayoutCCG.functionCalls() if any ---------------------
if (dataOut.playserver && dataOut.playserver != '-' && spx.CCGServersConfigured) { // 1.1.3 added dataOut.playserver
dataOut.command = "ADD";
logger.verbose('CasparCG play: [' + dataOut.relpathCCG + '] ' + dataOut.playserver + '/' + dataOut.playchannel + '-' + dataOut.playlayer);
playingSomewhere = true;
PlayoutCCG.playoutController(dataOut);
} else {
logger.verbose('No CasparCG playout');
}
// Send PlayoutWEB.functionCalls() if any ---------------------
if (dataOut.webplayout && dataOut.webplayout != '-') {
logger.verbose('Webplayout PLAY: ' + dataOut.webplayout);
dataOut.spxcmd = 'playTemplate';
playingSomewhere = true;
PlayoutWEB.webPlayoutController(dataOut);
} else {
logger.warn('No Webplayout playout layer given');
}
// iterate all templates and allow only the latest update
// (for same playout) to be on-air at once
if (RundownData.templates) {
// prepopulated does not necessarily have "templates"
RundownData.templates.forEach((item, index) => {
let thisOnair = item.onair || '';
let thisServer = item.playserver || '-';
let thisChannel = item.playchannel || '1';
let thisLayer = item.playlayer || '1';
let thisWeb = item.webplayout || '-';
let thisOutput = thisServer.trim() + thisChannel.trim() + thisLayer.trim() + thisWeb.trim() + thisOnair.trim();
let dataOnair = 'true' // because we are in play;
let dataServer = dataOut.playserver || '-';
let dataChannel = dataOut.playchannel || '1';
let dataLayer = dataOut.playlayer || '1';
let dataWeb = dataOut.webplayout || '-';
let dataOutput = dataServer.trim() + dataChannel.trim() + dataLayer.trim() + dataWeb.trim() + dataOnair.trim();
if (index != templateIndex && thisOutput == dataOutput) {
RundownData.templates[index].onair = 'false';
}
if (index == templateIndex && playingSomewhere) {
RundownData.templates[index].onair = 'true';
}
});
}
// API Call executed. Should we set an autostop?
if (forceDiskRead || req.body.referrer == 'directplayout') {
let numeric = parseInt(dataOut.out);
if (isNaN(numeric)) {
logger.verbose('API call: out was not a number, skipping autostop.');
} else {
logger.verbose('API call: autostop item in ' + numeric + ' ms.');
autoOutTimerID = setTimeout(function () {
playoutSTOP(dataOut, templateIndex, RundownData);
}, numeric);
}
}
break;
// == NEXT / aka CONTINUE =====================================
case 'next':
if (dataOut.playserver != '-' || dataOut.webplayout != '-') {
RundownData.templates[templateIndex].onair = 'true';
}
// Send PlayoutCCG.functionCalls() if any ---------------------
if (dataOut.playserver != '-') {
dataOut.command = "NEXT";
logger.verbose('CasparCG next: [' + dataOut.relpathCCG + '] ' + dataOut.playserver + '/' + dataOut.playchannel + '-' + dataOut.playlayer);
PlayoutCCG.playoutController(dataOut);
} else {
logger.verbose('No CasparCG playout');
}
// Send PlayoutWEB.functionCalls() if any ---------------------
if (dataOut.webplayout != '-') {
logger.verbose('Webplayout NEXT: ' + dataOut.webplayout);
dataOut.spxcmd = 'nextTemplate';
PlayoutWEB.webPlayoutController(dataOut); // refactored 20.08.2020 (note, this function handles all play, stop, next, update commands)
// io.emit('SPXMessage2Client', dataOut);
} else {
logger.verbose('No Webplayout playout');
}
break;
// == STOP ===================================================
case 'stop':
await playoutSTOP(dataOut);
if (templateIndex > -1 && RundownData.templates[templateIndex]) {
// Added check in 1.3.0
RundownData.templates[templateIndex].onair = 'false';
}
break;
// == UPDATE ===================================================
case 'update':
logger.verbose('Updating [' + dataOut.relpath + ']');
// RundownData.templates[templateIndex].onair='true'; // yes. no?
if (dataOut.playserver != '-') {
// TODO: Update functions not deeply properly tested.
dataOut.command = "UPDATE";
logger.verbose('CasparCG update: [' + dataOut.relpathCCG + '] ' + dataOut.playserver + '/' + dataOut.playchannel + '-' + dataOut.playlayer + ', dataOut: ', dataOut);
PlayoutCCG.playoutController(dataOut);
} else {
logger.verbose('No CasparCG playout');
}
// Send PlayoutWEB.functionCalls() if any
if (dataOut.webplayout != '-') {
logger.verbose('Webplayout UPDATE: ' + dataOut.webplayout);
dataOut.spxcmd = 'updateTemplate';
dataOut.fields = req.body.fields;
PlayoutWEB.webPlayoutController(dataOut);
} else {
logger.verbose('No Webplayout playout');
}
break;
// == INVOKE ===================================================
case 'invoke':
logger.verbose('Invoke [' + dataOut.invoke + ']');
// Example of expected dataOut down stream from here:
// dataOut.command = "invoke"
// dataOut.invoke = "myTemplateFunction('hello world')"
if (dataOut.playserver != '-') {
dataOut.command = "INVOKE";
PlayoutCCG.playoutController(dataOut);
}
if (dataOut.webplayout != '-') {
dataOut.spxcmd = 'invokeFunction';
PlayoutWEB.webPlayoutController(dataOut);
} // if web
break;
default:
logger.warn('/gc/playout did not recognize command "' + playOutCommand + '".');
break;
}
// Added in 1.3.2 / updateRundownItem in the UI if set
if (req.body?.updateRundownItem?.updateUI) {
logger.verbose('API call: updateRundownItem in the UI.');
io.emit('SPXMessage2Client', {
spxcmd: 'updateRundownItem',
itemID: req.body.updateRundownItem.itemID,
relpath: dataOut.relpath,
command: playOutCommand
});
}
// persist to memory and to disk
RundownData.updated = new Date().toISOString();
RundownData = await spx.appendProjectFile(RundownData, RundownFile, "from gc/playout");
global.rundownData = RundownData // memory only
if (playOutCommand == 'play' || playOutCommand == 'stop') {
if (req.body?.updateRundownItem?.persist) {
preventSave = false;
}
if (preventSave) {
logger.verbose('preventSave enabled, no need to persist to disk.');
} else {
logger.verbose('Persisting rundown data to disk....');
await spx.writeFile(RundownFile, RundownData);
}
}
// Added in 1.3.0 / Restore cache (after an API call)
if (cacheRundownData) {
logger.verbose('Restoring rundown data to memory after an API call.');
global.rundownData = cacheRundownData; // restore it
}
// Check if file exists. Function improved in 1.1.3
if (dataOut.relpath) {
let templateAbsPath = path.join(spx.getStartUpFolder(), 'ASSETS', 'templates', dataOut.relpath);
if (!fs.existsSync(templateAbsPath)) {
throw (playOutCommand + ' warning! Template not found: ' + dataOut.relpath);
}
}
res.status(200).send(); // ok 200 AJAX RESPONSE
} // end try
catch (error) {
console.error('ERROR in /gc/playout', error);
logger.error('ERROR in /gc/playout. ' + error);
res.status(500).send('Error in /gc/playout: ' + error) // error 500 AJAX RESPONSE
};
}); // playout ended
router.post('/gc/playaudio', spxAuth.CheckLogin, (req, res) => {
// handle incoming sound fx requests
// TODO: Enable this playaudio ajax call for template based audio triggers!
try {
logger.info('gc/playaudio ' + req.body.sound + ', ' + req.body.info, null, 4);
spx.playAudio(req.body.sound, req.body.info);
res.status(200).send('Playaudio command processed.'); // ok 200 AJAX RESPONSE
} catch (error) {
logger.error('ERROR, unable to send playaudio command. Server up? ' + error)
res.status(500).send('Server error in /gc/playaudio [' + error + '].') // error 500 AJAX RESPONSE
}
}); // playaudio ended
router.post('/gc/controlvideo', spxAuth.CheckLogin, async (req, res) => {
try {
// handle video commands
logger.debug('Forwarding command to PlayoutCCG.VideoPlayerController');
PlayoutCCG.VideoPlayerController(req.body);
res.status(200).send('All ok'); // ok 200 AJAX RESPONSE
} catch (error) {
logger.error('Server error in /gc/controlvideo. Server up? ' + error)
res.status(500).send('Server error in /gc/controlvideo: ' + error); // error 500 AJAX RESPONSE
}
}); // control ended
router.post('/gc/clearPlayouts', spxAuth.CheckLogin, async (req, res) => {
// Slightly improved in v.1.0.12
try {
logger.verbose('Clearing all layers');
res.status(200).send('Graphics clear requests sent') // ok 200 AJAX RESPONSE
// Clear Webplayout ---------------------------
io.emit('SPXMessage2Client', { spxcmd: 'clearAllLayers' }); // clear webrenderers
io.emit('SPXMessage2Controller', { APIcmd: 'RundownAllStatesToStopped' }); // stop UI and save stopped values to rundown
// Change all items to offair on the playlist file
let RundownFile = path.normalize(req.body.datafile);
let RundownData = await GetJsonData(RundownFile);
RundownData.templates.forEach((item, index) => {
item.onair = "false"
});
RundownData.updated = new Date().toISOString();
await spx.writeFile(RundownFile, RundownData);
// Clear CasparCG -------------------
if (!spx.CCGServersConfigured) { return } // exit early, no need to do any CasparCG work
PlayoutCCG.clearChannelsFromGCServer(req.body.server) // server is optional
}
catch (error) {
logger.error('Error in gc/clearPlayouts while clearing channels. [' + error + '].');
res.status(500).send('SPX error in gc/clearPlayouts [' + error + '].') // error 500 AJAX RESPONSE
};
}); // clearPlayouts ended
router.post('/gc/sortTemplates', spxAuth.CheckLogin, async (req, res) => {
// Handles modification changes of a template in the rundown.
// Request: data with newTemplateOrderArr -array (of epochs)
// Returns: AJAX response
logger.verbose('Sorting templates in ' + req.body.rundownfile + ' to ' + req.body.newTemplateOrderArr);
try {
let RundownFile = path.normalize(req.body.rundownfile);
let RundownData = await GetJsonData(RundownFile);
let TempArr = [];
req.body.newTemplateOrderArr.forEach(function (sortedID, sortIndex) {
RundownData.templates.forEach(function (template, templateIndex) {
if (template.itemID === sortedID) {
logger.debug("Sorting. Saving to position " + templateIndex + " itemID " + sortedID);
TempArr.push(template);
}
})
});
RundownData.templates = TempArr;
RundownData.updated = new Date().toISOString();
await spx.writeFile(RundownFile, RundownData);
global.rundownData = RundownData; // Added in 1.1.2
res.status(200).send('Rundown sorting saved.'); // ok 200 AJAX RESPONSE
}
catch (error) {
logger.error('Error in /gc/sortTemplates while sorting data. [' + error + '].');
res.status(500).send('Server error in /gc/sortTemplates [' + error + '].') // error 500 AJAX RESPONSE
};
}); // sortTemplates ended
router.post('/gc/duplicateRundown', spxAuth.CheckLogin, async (req, res) => {
// Create a duplicate
// Request: data obj
let foldname = req.body.foldname;
let filename = req.body.filename;
let filerefe = path.normalize(path.join(config.general.dataroot, foldname, 'data', filename));
try {
spx.duplicateFile(filerefe, ' copy');
res.status(200).send('Item duplicated.'); // ok 200 AJAX RESPONSE
} catch (error) {
let errmsg = 'Server error in /gc/duplicateRundown [' + error + ']';
logger.error(errmsg);
res.status(500).send(errmsg) // error 500 AJAX RESPONSE
}
}); // duplicateRundown ended
router.post('/gc/renameRundown', spxAuth.CheckLogin, async (req, res) => {
// Rename a rundown file
// Request: data obj
let foldnam = req.body.foldnam;
let orgname = req.body.orgname;
let newname = req.body.newname;
let fileref = path.normalize(path.join(config.general.dataroot, foldnam, 'data', orgname));
try {
spx.renameRundown(fileref, newname);
res.status(200).send('Item renamed.'); // ok 200 AJAX RESPONSE
} catch (error) {
let errmsg = 'Server error in /gc/renameRundown [' + error + ']';
logger.error(errmsg);
res.status(500).send(errmsg) // error 500 AJAX RESPONSE
}
}); // renameRundown ended
router.post('/gc/saveConfigChanges', spxAuth.CheckLogin, async (req, res) => {
// Added in 1.1.0. Handles modification changes of given config items.
// Request: key / val pair as a data object.
// Returns: Ajax response
logger.verbose('Overwriting config with updated values.');
try {
var ConfigFile = global.configfileref;
var ConfigData = await GetJsonData(ConfigFile);
let key = req.body.key || null;
let val = req.body.val;
// Fixed in v1.3.0 (check truthiness)
if (key == null || val == null || val == undefined) {
throw 'Key [' + key + '] or a supported value [' + val + '] was missing from request, skipping config save.';
}
ConfigData.general[key] = val;
global.config = ConfigData; // update mem version also
await spx.writeFile(ConfigFile, ConfigData);
let response = ['Config changed']
res.status(200).send(response); // ok 200 AJAX RESPONSE
} catch (error) {
console.error('ERROR', error);
let errmsg = 'Server error in /gc/saveConfigChanges [' + error + ']';
logger.error(errmsg);
res.status(500).send(errmsg) // error 500 AJAX RESPONSE
};
}); // saveConfigChanges ended
router.post('/gc/saveItemChanges', spxAuth.CheckLogin, async (req, res) => {
// Handles modification changes of a template in the rundown.
// Request: form data as JSON
// Returns: Ajax response
logger.verbose('Overwriting a template itemID [' + req.body.epoch + '] in rundown [' + req.body.rundownfile + '] with updated values.');
try {
// spx.talk('Saving template changes from rundown.');
var RundownFile = path.normalize(req.body.rundownfile);
var RundownData = await GetJsonData(RundownFile);
RundownData = spx.RemoveFilepathKey(RundownData); // Added in 1.3.0
var DataFieldsForCollapsePreview;
RundownData.templates.forEach((template, TemplateIdx) => {
if (template.itemID == req.body.epoch) {
req.body.DataFields.forEach(function (PostField) {
RundownData.templates[TemplateIdx].DataFields.forEach(function (JsonField) {
if (PostField.field == JsonField.field) {
JsonField.value = PostField.value;
}
});
DataFieldsForCollapsePreview = RundownData.templates[TemplateIdx].DataFields;
});
}
});
global.rundownData = RundownData; // push to memory also for next take
await spx.writeFile(RundownFile, RundownData);
var htmlSnippetForCollapse = spx.generateCollapsedHeadline(DataFieldsForCollapsePreview);
let response = ['Item changes saved', htmlSnippetForCollapse]
res.status(200).send(response); // ok 200 AJAX RESPONSE
} catch (error) {
console.error('ERROR', error);
let errmsg = 'Server error in /gc/saveItemChanges [' + error + ']';
logger.error(errmsg);
res.status(500).send(errmsg) // error 500 AJAX RESPONSE
};
}); // saveItemChanges ended
// FUNCTIONS --------------------------------------------------------------------------------------------------
// This functions is OGraf specific and handles ograf templates.
function addOgrafTemplateToProfile(profileData, TemplatePath, showFolder, curFolder, replaceIndex = null) {
// console.log("addOgrafTemplateToProfile called with TemplatePath:", TemplatePath);
if (replaceIndex) {
// when replacing, the path does not come with absolute path, must append
TemplatePath = path.join(spx.getStartUpFolder(), 'ASSETS', 'templates', TemplatePath)
global.LastBrowsedTemplateFolder = path.join(spx.getStartUpFolder(), 'ASSETS', 'templates', curFolder); // Bugfix added in 1.1.1
} else {
// we are adding (not re-importing) so this works
global.LastBrowsedTemplateFolder = curFolder;
}
let rawJson = fs.readFileSync(TemplatePath, 'utf8');
let ografJson = JSON.parse(rawJson);
// Convert schema.properties to DataFields
const properties = ografJson.schema?.properties || {};
const DataFields = Object.entries(properties).map(([key, prop]) => {
let ftype = null;
let dataProps = {
field: key,
ftype: 'textfield', // default type
value: prop.default || '',
title: prop.title || key
}
if (prop.v_spx) {
// SPX VENDOR TYPES
dataProps.ftype = prop.v_spx.ftype;
switch (dataProps.ftype.toLowerCase()) {
case 'instruction':
break;
case 'dropdown':
dataProps.items = prop.v_spx.items || [];
break;
case 'textarea':
break;
case 'divider':
break;
case 'filelist':
dataProps.assetfolder = prop.v_spx.assetfolder || '/media/images/';
dataProps.extension = prop.v_spx.extension || 'png';
break;
case 'number':
break;
case 'checkbox':
break;
case 'button':
dataProps.fcall = prop.v_spx.fcall;
break;
case 'spacer':
break;
case 'color':
break;
case 'plugin':
dataProps.plugin = prop.v_spx.fcall || 'UnknownFolder';
dataProps.title = prop.v_spx.title || 'Extension';
dataProps.descr = prop.v_spx.descr || 'Info';
dataProps.value = prop.v_spx.value || 'alert';
break;
} // end spx switch
} else {
// OGRAF TYPE CONVERSIONS TO SPX
switch (prop.type.toLowerCase()) {
case 'boolean':
dataProps.ftype = 'checkbox';
dataProps.value = prop.default ? "1" : "0";
break;
case 'number':
dataProps.ftype = 'number';
break;
default:
dataProps.ftype = 'textfield';
} // end ograf switch
}
return dataProps;
});
if (ografJson.customActions) {
let webplayout = ografJson.v_spx?.webplayout || "1"
ografJson.customActions.forEach((index) => {
DataFields.push({
field: index.id,
ftype: "button",
title: index.name || index.id,
fcall: `customActionHandler('${index.id}', '${webplayout}')`
})
})
}
const SPXGCTemplateDefinition = {
description: ografJson.description || '',
DataFields: DataFields,
steps: ografJson.stepCount || 1,
playserver: ografJson.v_spx?.playserver || "OVERLAY",
playchannel: ografJson.v_spx?.playchannel || "1",
playlayer: spx.max5(ografJson.v_spx?.playlayer) || "1",
webplayout: spx.max5(ografJson.v_spx?.webplayout) || "1",
out: ografJson.v_spx?.out || "manual",
uicolor: ografJson.v_spx?.uicolor || "0",
ografProps: {
id: ografJson.id,
version: ografJson.version,
},
imported: String(Date.now())
};
if (!SPXGCTemplateDefinition.DataFields || SPXGCTemplateDefinition.DataFields.length === 0) {
let emptyData = {}
emptyData.ftype = 'instruction'
emptyData.value = 'No editable fields'
SPXGCTemplateDefinition.DataFields = [emptyData];
}
let absPath = TemplatePath.split("\\").join("/");
let templateRootPath = path.join(spx.getStartUpFolder(), 'ASSETS', 'templates');
let tmpRoot = templateRootPath.split("\\").join("/");
let relpath = absPath.split(tmpRoot)[1];
SPXGCTemplateDefinition.relpath = relpath;
if (relpath && relpath.match(/\/[^\/]+\.ograf\.json$/)) {
relpath = relpath.replace(/\/[^\/]+\.ograf\.json$/, '');
}
graphicPath = relpath + '/' + ografJson.main;
SPXGCTemplateDefinition.ografProps.graphicPath = graphicPath;
if (profileData.templates) {
if (replaceIndex) {
profileData.templates[replaceIndex] = SPXGCTemplateDefinition;
} else {
profileData.templates.push(SPXGCTemplateDefinition);
}
} else {
profileData.templates = [SPXGCTemplateDefinition];
}
return profileData;
}
function addTemplateToProfile(profileData, TemplatePath, showFolder, curFolder, replaceIndex = null) {
if (TemplatePath.toLowerCase().endsWith(".ograf.json")) {
profileData = addOgrafTemplateToProfile(profileData, TemplatePath, showFolder, curFolder, replaceIndex);
return profileData;
}
let SPXGCTemplateDefinition = null;
let gddDefinition = null; // Graphics Data Definition
let spxDefinition = null; // SPX Template Definition
if (replaceIndex) {
TemplatePath = path.join(spx.getStartUpFolder(), 'ASSETS', 'templates', TemplatePath)
global.LastBrowsedTemplateFolder = path.join(spx.getStartUpFolder(), 'ASSETS', 'templates', curFolder); // Bugfix added in 1.1.1
} else {
global.LastBrowsedTemplateFolder = curFolder;
}
logger.verbose('Added template: ' + TemplatePath);
let templateContents = fs.readFileSync(TemplatePath, "utf8")
let templatehtml = templateContents.toString();
let importMessage = '\n\n- SPX TEMPLATE IMPORT NOTIFICATION --------------------------------\n';
importMessage += ' Potential errors below this notification are from the template:\n';
importMessage += ' ' + TemplatePath + '\n\n';
importMessage += ' onLoad() or other root functions in the template MAY yield errors\n';
importMessage += ' or other messages during import and these can usually be ignored\n';
importMessage += ' safely. For more info search SPX KnowledgeBase for "Import errors".\n';
importMessage += '-------------------------------------------------------------------\n';
console.log(importMessage)
const dom = new JSDOM(templatehtml, { runScripts: "dangerously" });
SPXGCTemplateDefinition = dom.window.SPXGCTemplateDefinition; // Mandatory!
if (!SPXGCTemplateDefinition.description) { SPXGCTemplateDefinition.description = "" };
if (!SPXGCTemplateDefinition.playserver) { SPXGCTemplateDefinition.playserver = "-" };
if (!SPXGCTemplateDefinition.playchannel) { SPXGCTemplateDefinition.playchannel = "1" };
if (!SPXGCTemplateDefinition.playlayer) { SPXGCTemplateDefinition.playlayer = "5" };
if (!SPXGCTemplateDefinition.webplayout) { SPXGCTemplateDefinition.webplayout = "5" };
if (!SPXGCTemplateDefinition.onair) { SPXGCTemplateDefinition.onair = "false" };
if (!SPXGCTemplateDefinition.out) { SPXGCTemplateDefinition.out = "manual" };
if (!SPXGCTemplateDefinition.dataformat) { SPXGCTemplateDefinition.dataformat = "json" };
if (!SPXGCTemplateDefinition.uicolor) { SPXGCTemplateDefinition.uicolor = "0" };
if (!SPXGCTemplateDefinition.webplayout) { SPXGCTemplateDefinition.webplayout = "5" };
if (!SPXGCTemplateDefinition.playlayer) { SPXGCTemplateDefinition.playlayer = "5" };
if (parseInt(SPXGCTemplateDefinition.webplayout) > 5) {
SPXGCTemplateDefinition.webplayout = "5";
}
if (parseInt(SPXGCTemplateDefinition.playlayer) > 5) {
SPXGCTemplateDefinition.playlayer = "5";
}
SPXGCTemplateDefinition.imported = String(Date.now());
if (!SPXGCTemplateDefinition.DataFields || SPXGCTemplateDefinition.DataFields.length === 0) {
let emptyData = {}
emptyData.ftype = 'instruction'
emptyData.value = 'No editable fields'
SPXGCTemplateDefinition.DataFields = [emptyData];
}
let absPath = TemplatePath.split("\\").join("/");
let templateRootPath = path.join(spx.getStartUpFolder(), 'ASSETS', 'templates');
let tmpRoot = templateRootPath.split("\\").join("/");
SPXGCTemplateDefinition.relpath = absPath.split(tmpRoot)[1];
if (profileData.templates) {
if (replaceIndex) {
profileData.templates[replaceIndex] = SPXGCTemplateDefinition;
} else {
profileData.templates.push(SPXGCTemplateDefinition);
}
} else {
profileData.templates = [SPXGCTemplateDefinition];
}
profileData.templates.forEach(function (template, index) {
template.DataFields.forEach(function (field, index) {
if (field.prvar) {
console.log('WIP - Found prvar in template field: ' + field.prvar);
if (!profileData.variables) {
profileData.variables = [];
}
// TODO: Bug here! This will overwrite existing variables with same name!
let foundOld = false;
profileData.variables.forEach(function (variable, index) {
if (variable.prvar === field.prvar) {
console.log('WIP - Found existing variable: ' + variable.prvar);
foundOld = true;
variable.ftype = field.ftype;
variable.title = field.title;
variable.value = field.value;
if (!variable.users.includes(template.relpath)) {
variable.users.push(template.relpath);
}
}
})
if (!foundOld) {
let varItem = {}
varItem.prvar = field.prvar;
varItem.ftype = field.ftype;
varItem.title = field.title;
varItem.value = field.value;
varItem.users = [template.relpath];
profileData.variables.push(varItem);
}
}
})
})
return profileData;
} // addTemplateToProfile ended
async function orgGetDataFiles(FOLDERstr) {
logger.debug("Getting json files from " + FOLDERstr + "...");
let FOLDER = path.normalize(FOLDERstr);
let jsonData = {};
var key = 'files';
jsonData.folder = FOLDER;
jsonData[key] = [];
let id = 0;
try {
fs.readdirSync(FOLDER).forEach(file => {
let bname = path.basename(file, '.json');
logger.debug("Getting file " + bname + "...");
let ext = path.extname(file).toUpperCase();
if (ext == ".JSON") {
var stats = fs.statSync(path.join(FOLDER, file));
var datem = moment(stats.mtime, 'DD.MM.YYYY').format();
var filedata = {
id: id,
name: bname,
date: datem
};
id++;
jsonData[key].push(filedata);
}
});
return jsonData;
}
catch (error) {
logger.error('orgGetDataFiles / Failed to read folder ' + FOLDERstr + ': ' + error);
return (error);
}
} // orgGetDataFiles ended
async function SaveRundownDataToDisc(filepathFromCSVimport = false) {
try {
let targetRundownFile;
if (!global.rundownData) {
return
}; // no data to store
if (Object.keys(global.rundownData).length > 0) {
let RundownData = global.rundownData
if (filepathFromCSVimport) {
targetRundownFile = filepathFromCSVimport;
}
if (targetRundownFile) {
RundownData.updated = new Date().toISOString();
RundownData = await spx.RemoveFilepathKey(RundownData); // Added in 1.3.0
await spx.writeFile(targetRundownFile, RundownData); // 1.1.0
} else {
logger.verbose('No targetRundownFile known, cannot save.');
}
} else {
logger.verbose('No RundownData data yet in memory, nothing to save.');
}
} catch (error) {
logger.error('SaveRundownDataToDisc Error: ' + error);
return ('SaveRundownDataToDisc: error', error);
}
}
async function GetTextFileData(fileref) {
try {
if (fs.existsSync(fileref)) {
var contents = fs.readFileSync(fileref, 'utf8') || '';
logger.debug('GetTextFileData: returns data from [' + fileref + ']');
return contents;
}
else {
logger.debug('GetTextFileData: file does not exist [' + fileref + '], returning empty string.');
return '';
}
}
catch (error) {
logger.error('GetTextFileData Error: ' + error);
return ('GetTextFileData: error', error);
}
} // GetTextFileData ended
async function GetJsonData(fileref) {
try {
if (fs.existsSync(fileref)) {
var contents = fs.readFileSync(fileref, 'utf8') || '';
logger.debug('GetJsonData: returns data from [' + fileref + ']');
return JSON.parse(contents);
}
else {
logger.debug('GetJsonData: file does not exist [' + fileref + '], returning null.');
return false;
}
}
catch (error) {
let msg = 'GetJsonData error while reading ' + fileref + '. Invalid JSON format...? ' + error;
setTimeout(function () {
io.emit('SPXMessage2Controller', {
APIcmd: 'showMessageSlider',
msg: '<center>Invalid rundown file.<br>Please validate JSON data.</center>',
type: 'warn',
persist: true
});
}, 1000);
logger.error(msg);
return (msg);
}
} // GetJsonData ended
async function playoutSTOP(dataOut) {
logger.verbose('Stopping [' + dataOut.relpath + ']');
if (dataOut.playserver != '-') {
dataOut.command = "STOP";
logger.verbose('CasparCG stop: [' + dataOut.relpathCCG + '] ' + dataOut.playserver + '/' + dataOut.playchannel + '-' + dataOut.playlayer);
PlayoutCCG.playoutController(dataOut);
} else {
logger.verbose('No CasparCG playout');
}
if (dataOut.webplayout != '-') {
logger.verbose('Webplayout STOP: ' + dataOut.webplayout);
dataOut.spxcmd = 'stopTemplate';
PlayoutWEB.webPlayoutController(dataOut);
} else {
logger.verbose('No Webplayout playout');
}
} // playoutSTOP ended
function getResolutionFromConfig(cfg, postRequest) {
let sizeID = cfg.general.resolution || 'HD';
let width = '1920px';
let height = '1080px';
if (postRequest.width) { width = postRequest.width }
if (postRequest.height) { height = postRequest.height }
switch (sizeID) {
case '4K':
width = '3840px'
height = '2160px'
break;
case 'AUTO':
width = '100vw'
height = '100vh'
break;
default:
// HD is the default
width = '1920px'
height = '1080px'
break;
};
return {
width: width,
height: height,
};
} // getResolutionFromConfig ended
module.exports = router;