2314 lines
79 KiB
JavaScript
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;
|