Init WebCG

This commit is contained in:
2026-08-12 14:21:28 +09:00
commit 5c01660f97
170 changed files with 46248 additions and 0 deletions
+786
View File
@@ -0,0 +1,786 @@
/* ------------------------------------------
Public API routes for external controllers
such as Stream Deck or similar.
/api/v1/
Home route is a list of available commands.
--------------------------------------------- */
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('API-v1 route loading...');
const spx = require('../utils/spx_server_functions.js');
const xlsx = require('node-xlsx').default;
const axios = require('axios')
const PlayoutCCG = require('../utils/playout_casparCG.js');
const { query } = require("../utils/logger");
const spxAuth = require('../utils/spx_auth.js');
let apiCache = [] // used to cache [undocumented] API calls
let ack = 'Sent request to SPX server. Acknowledgement is not to be expected.'
let ack2 = 'Sent request to SPX Controller. Acknowledgement is not to be expected.'
const apiHandler = require('../utils/api-handlers.js');
const notInSolo = {
status: 501,
message: 'Not Implemented',
info: 'This API endpoint is not available in WebCG.',
more: 'For advanced API features, please see WebCG Production or Broadcast'
}
// ROUTES -------------------------------------------------------------------------------------------
router.get('/', function (req, res) {
let functionsDoc = {
"sections": [
{
"section": "Common API",
"info": "Generic API endpoints and utilities available in all WebCG versions without a license.",
"commands": [
{
"vers": "v1.1.2",
"method": "GET",
"param": "/api/v1/version",
"info": "Returns SPX version info and current host-id"
},
{
"vers": "v1.1.0",
"method": "GET",
"param": "/api/v1/panic",
"info": "Force clear to all output layers without out-animations. (Note, this does NOT save on-air state of rundown items to false, so when UI is reloaded the items will show the state before panic was triggered.) This is to be used for emergency situations only and not as a normal STOP command substitute."
},
{
"vers": "v1.0.14",
"method": "GET",
"param": "/api/v1/feedproxy?url=https://feeds.bbci.co.uk/news/rss.xml&format=xml",
"info": "A proxy endpoint for passing feed data from CORS protected datasources. (If you need to pass url parameters use <code>%26</code> instead of <code>&</code> to separate them)."
},
{
"vers": "v1.3.0",
"method": "POST",
"param": "/api/v1/feedproxy",
"info": "A POST version of the feedproxy endpoint. This endpoint is a helper for outgoing GET or POST requests requiring custom headers, such as <code>Authorization</code> or similar. Data is passed to the helper in the <code>body</code> of the POST request, in which <code>url</code> is the actual URL of the external endpoint. If the body contains a <code>postBody</code> -object, it will be passed to the outgoing API request as <code>body</code>. See principle in the example below, or search SPX Knowledge Base for more info with keyword <code>feedproxy</code>.",
"code": { url: "https://api.endpoint.com/requiring/customheaders/", headers: { "key1": "my first value", "key2": "second value" }, postBody: { info: "If postBody is found, the outgoing request will be done using POST method, otherwise as GET." } }
},
]
},
{
"section": "Controller API",
"restriction": "Some endpoints require WebCG Production license",
"info": "API endpoints for controlling the WebCG Graphics Controller rundown. Please note some endpoints will require a valid WebCG Production license.",
"commands": [
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/rundown/focusPrevious",
"info": "Move focus up to previous item, will not circle back to bottom when top is reached."
},
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/rundown/focusNext",
"info": "Move focus down to next item, will not circle back to top when end is reached."
},
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/rundown/stopAllLayers",
"info": "Animate all layers (used by the current rundown) out, but does not clear layers."
},
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/item/play",
"info": "Start focused item."
},
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/item/continue",
"info": "Issue continue command to selected item. Notice this needs support from the template itself and does not work as play or stop."
},
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/item/stop",
"info": "Stop focused item."
},
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/rundown/load?file=MyFirstProject/MyFirstRundown",
"info": "Open rundown from project / file."
},
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/rundown/focusFirst",
"info": "Move focus to the first item on the rundown.",
},
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/rundown/focusLast",
"info": "Move focus to the last item on the rundown.",
},
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/rundown/focusByID/1234567890",
"info": "Move focus by ID on the rundown.",
},
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/item/play/1234567890",
"info": "Start item by ID on the active rundown.",
"active": false,
},
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/item/continue/1234567890",
"info": "Continue to item by ID on the active rundown. Notice this needs support from the template itself and does not work as play or stop.",
"active": false,
},
{
"vers": "1.0",
"method": "GET",
"param": "/api/v1/item/stop/1234567890",
"info": "Stop item by ID on the active rundown.",
"active": false,
},
{
"vers": "v1.0.12",
"method": "GET",
"param": "/api/v1/invokeTemplateFunction?playserver=OVERLAY&playchannel=1&playlayer=19&webplayout=19&function=myCustomTemplateFunction&params=Hello%20World",
"info": "Uses an invoke handler to call a function in a template. See required parameters in the example call above. JSON objects can be passed as params by urlEncoding stringified JSON. Search SPX Knowledge Base for more info with keyword <code>invoke</code>.",
"active": false,
},
{
"vers": "v1.3.0",
"method": "GET",
"param": "/api/v1/invokeExtensionFunction?function=sendCmd&params=incrementNumber",
"info": "Uses SPX's messaging system to call a function in an extension. JSON objects can be passed as params by urlEncoding stringified JSON. The extension will need to implement SPX's messaging system, search SPX Knowledge Base for more info with keyword <code>invokeExtensionFunction</code>.",
"active": false,
},
]
},
{
"section": "Server API",
"restriction": "SPX Broadcast license required",
"info": "API endpoints targeting the SPX server directly without going through the rundown controller. These endpoints are for advanced uses and require a SPX Broadcast license.",
"commands": [
{
"vers": "v1.0.12, v.1.3.2",
"method": "POST",
"active": false,
"param": "/api/v1/directplayout",
"info": "Populate template and execute a play/continue/stop -command to it. Please note the optional <code>updateRundownItem</code> property. <code>updateRundownItemitemID</code> is an optional object for forcing UI updates and persisting to defined rundown file. <b>Please note: special charaters in values does not work at the moment!</b> Post request body example as JSON:",
"code": { casparServer: "OVERLAY", casparChannel: "1", casparLayer: "20", webplayoutLayer: "20", relativeTemplatePath: "/vendor/pack/template.html", out: "manual", DataFields: [{ field: "f0", value: "Firstname" }, { field: "f1", value: "Lastname" }], command: "play", updateRundownItem: { updateUI: true, itemID: "myItemID", persist: true, "project": "myFirstProject", "rundown": "myFirstRundown" } }
},
{
"vers": "v1.1.0",
"active": false,
"method": "GET",
"param": "/api/v1/controlRundownItemByID?file=MyProject/FirstRundown&item=1234567890&command=play",
"info": "Play / stop an item from a known rundown. (Remember you can rename rundown items from WebCG GUI)"
},
{
"vers": "v1.1.1",
"method": "GET",
"active": false,
"param": "/api/v1/getprojects",
"info": "Returns projects as an array of strings."
},
{
"vers": "v1.1.1",
"method": "GET",
"active": false,
"param": "/api/v1/getrundowns?project=MyProject",
"info": "Returns rundown names of a given project as an array of strings."
},
{
"vers": "v1.3.0",
"method": "GET",
"active": false,
"param": "/api/v1/allrundowns",
"info": "Returns all projects and rundowns"
},
{
"vers": "v1.1.1",
"method": "GET",
"active": false,
"param": "/api/v1/rundown/get",
"info": "Returns current rundown as json. "
},
{
"vers": "v1.3.0",
"method": "GET",
"active": false,
"param": "/api/v1/rundown/json?project=MyProject&rundown=FirstRundown",
"info": "Returns content of a specific rundown as json data."
},
{
"vers": "v1.3.0",
"method": "POST",
"active": false,
"param": "/api/v1/rundown/json",
"info": "Creates or updates a rundown file. This can be used for example with application extensions. POST <code>body:content</code> must contain valid rundown JSON data, otherwise SPX controller may not be able to read it. For more info search SPX Knowledge Base with keyword <code>api rundown/json</code>",
"code": { project: "myProjectName", file: "newRundown.json", content: { comment: "Playlist generated by MyApp", templates: [{ "description": "First template", "playserver": "OVERLAY", "etc": "..." }, { "description": "Second template", "playserver": "OVERLAY", "etc": "..." }] } }
},
{
"vers": "v1.1.1",
"method": "GET",
"active": false,
"param": "/api/v1/getlayerstate",
"info": "Returns current memory state of web-playout layers of the server (not UI). Please note, if API commands are used to load templates, this may not return them as expected!"
},
{
"vers": "v1.1.3",
"method": "GET",
"active": false,
"param": "/api/v1/gettemplates?project=MyProject",
"info": "Returns templates and their settings from a given project."
},
{
"vers": "v1.3.0",
"method": "GET",
"active": false,
"param": "/api/v1/executeScript?file=win-open-calculator.bat",
"info": "Execute a shell script/batch file in <code>ASSETS/scripts</code> folder using a shell associated with a given file extension."
},
{
"vers": "v1.3.0",
"method": "GET",
"active": false,
"param": "/api/v1/getFileList?assetsfolder=excel",
"info": "Returns an array of filenames fround in a given subfolder of ASSETS, such as <code>excel</code>."
},
{
"vers": "v1.3.0",
"method": "POST",
"active": false,
"param": "/api/v1/saveCustomJSON",
"info": "Creates or updates a JSON file in ASSETS/json folder. This can be used for persisting arbitrary data to a JSON file. The <code>content</code> property of the below example gets saved to <code>ASSETS/json/todoApp/myTodo.json</code>. Note the subfolder property is optional.",
"code": { subfolder: "todoApp", filename: "myData.json", content: { note: "Get these done by the end of month", items: [{ "task": "Grow a beard", "done": false }, { "task": "Get a haircut", "done": true }] } }
},
]
}
]
}
res.render('view-api-v1', {
layout: false,
functionList: functionsDoc,
version: global.vers
});
});
// COMMON API ----------------------------------------------------------
router.get('/version', async (req, res) => {
// let data = {};
// data.vendor = "SPX Graphics";
// data.product = "SPX Solo";
// data.version = global.vers;
// data.id = global.pmac;
// data.os = process.platform;
// return res.status(200).json(data)
let data = {};
data.vendor = 'SPX Graphics';
data.product = 'SPX Solo';
data.version = global.vers;
data.id = global.pmac;
data.hostname = config.general.hostname || '';
data.os = process.platform;
data.rootFolder = spx.getStartUpFolder();
data.uptime = {};
let uptime = spx.getUpTime();
data.uptime.seconds = uptime[0];
data.uptime.text = uptime[1];
// await spx.checkLicense();
// if (global.licensed) {
// data.license.type = global.license.productBrand;
// data.license.expiration = global.license.expiration;
// data.license.days = spx.getLicenseDaysRemaining();
// }
if (global.env && global.env.vendor) {
data.env = {};
data.env.vendor = global.env.vendor;
data.env.product = global.env.product;
data.env.version = global.env.version;
}
return res.status(200).json(data);
}); // end version
router.get('/panic', spxAuth.CheckAPIKey, async (req, res) => {
io.emit('SPXMessage2Client', { spxcmd: 'clearAllLayers' }); // clear webrenderers
io.emit('SPXMessage2Controller', { APIcmd: 'RundownAllStatesToStopped' }); // stop UI and save stopped values to rundown
if (spx.CCGServersConfigured) {
PlayoutCCG.clearChannelsFromGCServer(req.body.server) // server is optional
}
return res.status(200).json({ status: 200, message: 'OK', info: 'Panic executed. Layers cleared forcefully.' })
}); // end panic
router.get('/feedproxy', spxAuth.CheckAPIKey, async (req, res) => {
if (!req.query.url) {
logger.error('URL missing from feedproxy parameters');
return res.status(500).json({ type: 'error', message: 'URL missing from parameters' })
}
let URL = req.query.url
URL = URL.replace(/meta-data/g, '');
URL = URL.replace(/&path=/g, '###');
axios.get(URL)
.then(function (response) {
res.header('Access-Control-Allow-Origin', '*')
switch (req.query.format) {
case 'xml':
res.set('Content-Type', 'application/rss+xml')
break;
default:
res.set('Content-Type', 'application/json')
break
}
res.send(response.data)
})
.catch(function (error) {
console.log(error);
return res.status(500).json({ type: 'error', message: error.message })
});
}); // end feedproxy
router.post('/feedproxy', spxAuth.CheckAPIKey, async (req, res) => {
try {
if (!req.body.url) {
let errMsg = "url missing from feedproxy request. Make sure to have the correct JSON in your request.";
throw { status: 404, message: errMsg };
}
if (req.body.postBody) {
executePOSTRequest(req, res); // res handled in the function
} else {
executeGETRequest(req, res); // res handled in the function
}
} catch (error) {
res.status(error.status || 500).json({
status: error.status || 500,
error: error.message,
info: "Search SPX Knowledge Base for more using keyword 'feedproxy'."
});
}
}); // end feedproxy post handler
async function executePOSTRequest(req, res) {
try {
const { url, headers, postBody } = req.body;
if (!url || !headers || !postBody) {
return res.status(400).json({ error: 'Missing required parameters in request body' });
}
let requestBody;
try {
requestBody = JSON.parse(postBody);
} catch (error) {
return res.status(400).json({ error: 'Invalid postBody format' });
}
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(requestBody),
headers: headers
});
if (!response.ok) {
return res.status(response.status).json({ error: `Target API returned an error: ${response.status} ${response.statusText}` });
}
const data = await response.json();
res.status(response.status).json(data);
} catch (error) {
console.error('Error in executePOSTRequest:', error);
res.status(500).json({ error: 'Failed to forward POST request' });
}
}
function executeGETRequest(req, res) {
// This handles response to the client
// console.log('Using GET method', req.body);
axios.
get(req.body.url, {
headers: req.body.headers
})
.then((response) => {
res.header('Access-Control-Allow-Origin', '*')
// console.log('Response status:', response.statusText, response.status, response.data);
logger.verbose('executeGETRequest response: ' + response.status);
res.status(response.status || 200).send(response.data);
})
.catch((error) => {
if (error.response) {
// console.log('data......', error.response.data);
// console.log('status....', error.response.status);
// console.log('headers...', error.response.headers);
res.status(error.response.status || 500).send(error.response.data);
} else if (error.request) {
// The request was made but no response was received
// console.log('request...', error.request);
res.status(500).send(error.request);
} else {
// Something happened in setting up the request
// console.log('Error msg...', error.message);
res.status(500).send(error.message);
}
});
} // end executeGETRequest
router.get('/rundown/load/', spxAuth.CheckAPIKey, async (req, res) => {
// Improved in 1.3.1 to sanitize input
let file = await spx.strip(req.query.file);
console.log('Loading rundown: ' + file);
// Added in 1.3.1 check if it exists
let project = file.split('/')[0];
let rundown = file.split('/')[1];
let fullPath = path.resolve(spx.getDatarootFolder(), project, 'data', rundown + '.json');
if (!fs.existsSync(fullPath)) {
let errMsg = 'Rundown [' + file + '] not found, cannot load it.';
logger.error(errMsg);
return res.status(404).json({ status: 404, message: errMsg });
}
let dataOut = {};
dataOut.info = ack2
dataOut.APIcmd = 'RundownLoad';
dataOut.file = file;
dataOut.apikey = req.query.apikey || '';
io.emit('SPXMessage2Controller', dataOut);
res.status(200).json(dataOut);
}); // end load
router.get('/rundown/stopAllLayers', spxAuth.CheckAPIKey, async (req, res) => {
let dataOut = {};
dataOut.info = ack2
dataOut.APIcmd = 'RundownStopAll';
dataOut.apikey = req.query.apikey || '';
io.emit('SPXMessage2Controller', dataOut);
res.status(200).json(dataOut);
}); // end stopAllLayers
router.get('/changeItemID', async (req, res) => {
// Added in 1.0.15 - undocumented intentionally
// ID button in SPX controller uses this API endpoint:
// /api/v1/changeItemID?rundownfile=C:/SPX/DATAROOT/PROJECT/data/list.json&ID=0000001&newID=0000002
try {
let file = req.query.rundownfile || '';
let oldI = req.query.ID || '';
let newI = req.query.newID || '';
if (!file || !oldI || !newI) {
throw 'Missing data: file [' + file + '], old [' + oldI + '], new [' + newI + ']';
}
let datafile = path.normalize(file);
let RundownData = await spx.GetJsonData(datafile);
// First check for conflicts
RundownData.templates.forEach((item, index) => {
if (item.itemID === newI) {
throw 'ID-conflict'
}
});
RundownData.templates.forEach((item, index) => {
if (item.itemID === oldI) {
item.itemID = newI
}
});
RundownData.updated = new Date().toISOString();
RundownData = await spx.appendProjectFile(RundownData, datafile, "from changeItemID");
global.rundownData = RundownData;
await spx.writeFile(datafile, RundownData);
logger.verbose('Changed item ID to ' + newI);
return res.status(200).send('ID changed to ' + newI);
} catch (error) {
switch (error) {
case 'ID-conflict':
msg = 'ID conflict, ID was not changed.'
lvl = 'warn'
break;
default:
msg = 'Error in changeItemID: ' + error
lvl = 'error'
}
logger[lvl](msg);
return res.status(409).send('ID not changed');
}
});
router.get('/changeItemData', async (req, res) => {
// Added in 1.1.1
// ID button in SPX controller uses this API endpoint:
// /api/v1/changeItemData?rundownfile=C:/SPX/DATAROOT/PROJECT/data/list.json & ID=1234567890 & key=out & newValue=manual
try {
let file = req.query.rundownfile || '';
let epoc = req.query.ID || '';
let prop = req.query.key || '';
let valu = req.query.newValue || '';
if (!file || !epoc || !prop || !valu) {
logger.warn('Missing data from changeItemData: file: [' + file + '], ID: [' + epoc + '], key: [' + prop + '], value: [' + valu + ']')
throw 'Missing data, see log.';
}
let datafile = path.normalize(file);
let RundownData = await spx.GetJsonData(datafile);
RundownData.templates.forEach((item, index) => {
if (item.itemID === epoc) {
item[prop] = valu
}
});
RundownData.updated = new Date().toISOString();
RundownData = await spx.appendProjectFile(RundownData, datafile, "from changeItemData");
global.rundownData = RundownData; // push to memory also for next take
await spx.writeFile(datafile, RundownData);
logger.verbose('ChangeItemData: file: [' + file + '], ID: [' + epoc + '], key: [' + prop + '], value: [' + valu + ']')
return res.status(200).send(prop + ' changed to ' + valu);
} catch (error) {
console.log('Error', error);
logger.error('changeItemData error', error);
return res.status(409).send('Failed to change ' + prop + ' to ' + valu + '!');
}
});
// CONTROLLER API -----------------------------------------------------------------------------
router.get('/item/play', spxAuth.CheckAPIKey, async (req, res) => {
let dataOut = {};
dataOut.info = ack2
dataOut.status = 200;
dataOut.message = 'OK';
dataOut.APIcmd = 'ItemPlay';
io.emit('SPXMessage2Controller', dataOut);
res.status(200).json(dataOut);
});
router.get('/item/continue', spxAuth.CheckAPIKey, async (req, res) => {
let dataOut = {};
dataOut.status = 200;
dataOut.message = 'OK';
dataOut.info = ack2
dataOut.APIcmd = 'ItemContinue';
io.emit('SPXMessage2Controller', dataOut);
res.status(200).json(dataOut);
}); // end item continue
router.get('/item/stop', spxAuth.CheckAPIKey, async (req, res) => {
let dataOut = {};
dataOut.status = 200;
dataOut.message = 'OK';
dataOut.info = ack2
dataOut.APIcmd = 'ItemStop';
io.emit('SPXMessage2Controller', dataOut);
res.status(200).json(dataOut);
}); // end item stop
router.get('/rundown/focusNext/', spxAuth.CheckAPIKey, async (req, res) => {
let dataOut = {};
dataOut.info = ack2
dataOut.APIcmd = 'RundownFocusNext';
dataOut.apikey = req.query.apikey || '';
io.emit('SPXMessage2Controller', dataOut);
res.status(200).json(dataOut);
}); // end focusNext
router.get('/rundown/focusPrevious/', spxAuth.CheckAPIKey, async (req, res) => {
let dataOut = {};
dataOut.info = ack2
dataOut.APIcmd = 'RundownFocusPrevious';
dataOut.apikey = req.query.apikey || '';
io.emit('SPXMessage2Controller', dataOut);
res.status(200).json(dataOut);
}); // end focusPrevious
router.get('/rundown/focusFirst/', spxAuth.CheckAPIKey, async (req, res) => {
let dataOut = {};
dataOut.info = ack2
dataOut.APIcmd = 'RundownFocusFirst';
dataOut.apikey = req.query.apikey || '';
io.emit('SPXMessage2Controller', dataOut);
res.status(200).json(dataOut);
}); // end focusFirst
router.get('/rundown/focusLast/', spxAuth.CheckAPIKey, async (req, res) => {
let dataOut = {};
dataOut.info = ack2
dataOut.APIcmd = 'RundownFocusLast';
dataOut.apikey = req.query.apikey || '';
io.emit('SPXMessage2Controller', dataOut);
res.status(200).json(dataOut);
}); // end focusLast
router.get('/rundown/focusByID/:id', spxAuth.CheckAPIKey, async (req, res) => {
let dataOut = {};
dataOut.info = ack2;
dataOut.APIcmd = 'RundownFocusByID';
dataOut.apikey = req.query.apikey || '';
dataOut.itemID = req.params.id;
io.emit('SPXMessage2Controller', dataOut);
res.status(200).send('Sent request to controller: ' + JSON.stringify(dataOut));
}); // end focusByID
// router.get('/rundown/focusFirst/', spxAuth.CheckAPIKey, async (req, res) => {
// res.status(501).json(notInSolo);
// }); // end focusFirst
// router.get('/rundown/focusLast/', spxAuth.CheckAPIKey, async (req, res) => {
// res.status(501).json(notInSolo);
// }); // end focusLast
// router.get('/rundown/focusByID/:id', spxAuth.CheckAPIKey, async (req, res) => {
// res.status(501).json(notInSolo);
// }); // end focusByID
router.get('/item/play/:id', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end item play by ID
router.get('/item/continue/:id', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end item continue by ID
router.get('/item/stop/:id', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end item stop by ID
router.get('/invokeTemplateFunction/', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end invokeTemplateFunction
router.get('/invokeExtensionFunction/', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end invokeExtensionFunction
// SERVER API ----------------------------------------------------------------------------------
router.post('/directplayout', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end directplayout
router.get('/directplayout', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end directplayout
router.get('/getprojects', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end getprojects
router.get('/allrundowns', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end allrundowns
router.get('/getrundowns', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end getrundowns
router.get('/gettemplates', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end gettemplates
router.get('/getlayerstate', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end getlayerstate
router.get('/executeScript', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end executeScript file
router.get('/getFileList', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end getFileList
router.post('/saveCustomJSON', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end saveCustomJSON
router.get('/controlRundownItemByID', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end controlRundownItemByID
router.get('/rundown/get', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end get current rundown as JSON
router.get('/rundown/json', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end get specific rundown as JSON
router.post('/rundown/json', spxAuth.CheckAPIKey, async (req, res) => {
res.status(501).json(notInSolo);
}); // end create or update rundown as JSON
module.exports = router;
+440
View File
@@ -0,0 +1,440 @@
// -----------------------------------------
// Handle Express server routes for the API (at "/api/")
// -----------------------------------------
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('API-route loading...');
const spx = require('../utils/spx_server_functions.js');
const xlsx = require('node-xlsx').default;
// --- WATCHOUT!!!! v1.3.3 disabled --------
// const { now } = require("moment");
// const { constants } = require("buffer");
// ROUTES -------------------------------------------------------------------------------------------
router.get('/', function (req, res) {
res.send('Looking for this <a href="/api/v1/">api/v1</a>?');
});
router.get('/files', async (req, res) => {
const fileListAsJSON = await GetDataFiles();
res.send(fileListAsJSON);
}); // file
router.get('/openFileFolder/', async (req, res) => {
// Added in 1.3.1. And fixed in 1.3.2...
// Added optional parameter forceFolder to open a different folder than templates.
if (config.general.disableOpenFolderCommand == true) {
let msg = 'openFileFolder -command disabled in config.';
logger.warn(msg);
return res.status(403).send(msg);
}
let dirpath, folder = null;
if (req.query.openFolderOnly) {
folder = path.join(spx.getStartUpFolder(), 'ASSETS', req.query.openFolderOnly);
// folder = path.dirname(dirpath);
} else {
let relpath = req.query.file || '';
if (!req.query.file) {
let msg = 'openFileFolder -command requires a file parameter.';
logger.warn(msg);
return res.status(403).send(msg);
}
filepath = path.join(spx.getStartUpFolder(), 'ASSETS', 'templates', relpath);
folder = path.dirname(filepath);
}
// Added in 1.3.1 for security: if not found nothing is done.
if (!fs.existsSync) {
logger.error('Folder ' + folder + ' does not exist.');
return res.status(404).send('Folder ' + folder + ' does not exist.');
}
// open folder in each operating system
if (process.platform === 'darwin') {
require('child_process').exec('open "' + folder + '"');
} else if (process.platform === 'win32') {
require('child_process').exec('explorer "' + folder + '"');
} else if (process.platform === 'linux') {
require('child_process').exec('xdg-open "' + folder + '"');
} else {
logger.error('Unknown operating system: ' + process.platform);
}
res.sendStatus(200)
}); // openFileFolder of a template for editing
router.get('/licBasic/', async (req, res) => {
// added in 1.1.1 - license check. Minor tweaks in 1.1.3
// Format:
// 4x?rot(pmac)10x? | AA-AA-33-UQ-GZ-ZX-BB-BB-BB-BB
// Not safe because it is not encrypted.
let stripd = req.query.str.replace(/-/g, ''); // strip dashes
let rotlic = stripd.substring(4,12); // get 8 chars
// console.log('rotlic // from: ' + rotlic + ' to ' + spx.rot(rotlic, true) + ' vs ' + global.pmac.toUpperCase());
if (spx.rot(rotlic, true) === global.pmac.toUpperCase()) {
res.status(200).send('{result:ok}');
} else {
res.status(403).send('{result:invalid}');
}
}); // GET licBasic check ended
router.get('/rotBasic/', async (req, res) => {
// Require host-id, returns key.
let rotlic = spx.rot(req.query.id);
let soclic = spx.dashify(spx.salt(4) + rotlic + spx.salt(8));
let revlic = spx.rot(rotlic, true);
let json = "{\"pmac\":\"" + global.pmac + "\",\"rot\":\"" + rotlic + "\",\"soclic\":\"" + soclic + "\",\"chk\":\"" + revlic + "\"}";
res.send(json);
}); // GET rotBasic/?id=12345678
router.get('/logger/', async (req, res) => {
// Minimalistic GET logger for template messages
let message = req.query.message
let source = req.query.source
let level = req.query.level.toLowerCase()
let channel = req.query.channel
let msg = '(api/logger, ' + channel + ', ' + source + '): ' + message
// console.log(msg);
eval('logger.' + level + '(msg)'); // nasty, eh?
res.sendStatus(200)
}); // GET logger route ended
router.post('/logger/', async (req, res) => {
// Minimalistic POST logger for template messages
let message = req.body.message
let source = req.body.source
let level = req.body.level.toLowerCase()
let channel = req.body.channel
let msg = '(api/logger, ' + channel + ', ' + source + '): ' + message
// console.log(msg);
eval('logger.' + level + '(msg)'); // nasty, eh?
res.sendStatus(200)
}); // POST logger route ended
router.post('/browseFiles/', async (req, res) => {
// This is axios ajax handler for file browser on dbl click on a folder
// REQUEST: current folder and next folder name
// RETURNS: json data with folder and file arrays
// 1.1.0 - refactored navigation process to use '..' for parent folder.
let curFolder = req.body.curFolder || ".";
let tgtFolder = req.body.tgtFolder || "";
let extension = req.body.extension || "HTM";
let rootFolder = req.body.rootFolder || path.join(spx.getStartUpFolder(), 'ASSETS');
let BrowseFolder = path.join(curFolder, tgtFolder);
let osRootPath = path.resolve(rootFolder)
let osTargPath = path.resolve(BrowseFolder)
let navigateTo = osTargPath;
let feedbackMs = '';
if ( osTargPath.length <= osRootPath.length ) {
logger.verbose('Targeting beyond limits, sending root-identifier. Path: ' + osTargPath);
navigateTo = osRootPath;
feedbackMs = 'root';
} else {
feedbackMs = 'ok';
}
const fileListAsJSON = await spx.GetFilesAndFolders(navigateTo, extension);
fileListAsJSON.message=feedbackMs; // force feedback message to UI at RenderFolder()
res.send(fileListAsJSON);
}); // POST browseFiles API route ended
router.post('/heartbeat/', async (req, res) => {
// REQUEST: data = a heartbeat string
// RETURNS: none
// 1.1.0 submit anonymous usage stats
try {
if (global.config.general.allowstats===false || global.config.general.allowstats=='false') {
logger.verbose('Heartbeat / stats disabled.');
return
} // Stats disabled by config. Added in 1.1.1.
let d = req.body.data;
let h = 'smartpx.fi';
spx.collectSPXInfo('hello from api/heartbeat endpoint')
.then(function(si) {
let u = 'http://' + h + '/gc/messageservice2/?'+ si + '&d=' + d;
spx.httpGet(u);
return si
})
.then(function(si) {
logger.verbose('Stats ' + si + ' AND ' + d);
res.status(200).send('{all:good}'); // ok 200 AJAX RESPONSE
return;
})
} catch (error) {
logger.error('Error in api/heartbeat: ' + error);
res.status(500).send(error);
};
}); // POST heartbeat
router.post('/readExcelData', async (req, res) => {
// Function can be called from a template to get all data from
// an Excel file in the ASSETS/excel -folder.
// Data parsing / logic must be implemented in the template
// this just dumps data out as-is.
// var excelFile = path.join(__dirname, '..', 'ASSETS', req.body.filename); // fails when packaged
// Improved in 1.1.1 - Return cached data also if fileref is empty (for some reason).
try {
var excelFile = path.join(spx.getStartUpFolder(), 'ASSETS', req.body.filename); // v.1.0.15: getStartUpFolder()
var workSheetsData;
let timenow = Date.now();
// console.log('Excel cache age ' + (timenow - excel.readtime) + ' ms');
if ( excel.data && excel.filename == req.body.filename && (timenow - excel.readtime) <= 1000 || !req.body.filename ) { /* milliseconds */
// sama data requested less than a second ago, return data from memory
logger.verbose('Returning cached Excel data from memory')
workSheetsData = excel.data;
// console.log('Returning CACHED Excel data.\n');
} else {
// get it from Excel file
logger.verbose('Returning Excel data from FILE and saving to cache.')
workSheetsData = xlsx.parse(excelFile);
// console.log('Returning Excel FILE data and caching it.\n');
// cache excel data to a global variable
global.excel.readtime = Date.now();
global.excel.filename = req.body.filename;
global.excel.data = workSheetsData;
}
logger.verbose('OK API read Excel data from ' + excelFile);
res.status(200).send(workSheetsData); // ok 200 AJAX RESPONSE
return;
} catch (error) {
logger.error('Error in api/readExcelData while reading Excel ' + excelFile + ": " + error);
res.status(500).send(error); }; // Server error
return;
}); // POST readExcelData to get Excel data from file
router.post('/savefile/:filebasename', async (req, res) => {
spx.talk('Saving file ' + req.params.filebasename);
try {
if (!req.params.filebasename) {
throw new Error("Filename missing, cannot save file.");
}
let datafile = path.join(directoryPath, req.params.filebasename) + '.json';
logger.debug('Saving file ' + datafile + '...');
let data = req.body;
await spx.writeFile(datafile,data);
res.status(200).send('OK, created file ' + datafile); // ok 200 AJAX RESPONSE
} catch (error) {
logger.error('Error while saving ' + datafile + ': ' + err);
res.status(500).send(error);
}; //file written
}); // POST savefile API route ended
router.post('/saverundownfile/:projectName/:rundownName', async (req, res) => {
// Added in 1.3.0
// Used by extensions that will modify rundowns and save them back to the server.
// This will also send a message to the UI to request a reload.
console.log('Saving rundown file ' + req.params.rundownName + ' in project ' + req.params.projectName + '...');
try {
if (!req.params.projectName || !req.params.rundownName) {
throw new Error("Project or filename missing from request, cannot save file.");
}
let datafile = path.join(directoryPath, req.params.projectName, 'data', req.params.rundownName) + '.json';
logger.debug('Saving rundown file ' + datafile + '...');
let data = req.body;
await spx.writeFile(datafile,data);
io.emit('SPXMessage2Client', {
spxcmd: "showMessageSlider",
msg: "⛔ Rundown data was modified by API. Reload view!",
type: "warn",
persist: true
});
res.status(200).send('OK, created file ' + datafile); // ok 200 AJAX RESPONSE
} catch (error) {
logger.error('Error in api/saverundownfile' + error);
res.status(500).send(error);
}; //file written
}); // POST savefile API route ended
router.post('/exportCSVfile', async (req, res) => {
// console.log('Exporting CSV...');
try {
let showFolder = req.body.foldername || "";
let datafile = req.body.datafile || "";
let dataJSONfile= path.join(spx.getDatarootFolder(), showFolder, 'data', datafile + '.json'); // Changed in 1.3.1
let rundownData = await spx.GetJsonData(dataJSONfile);
let CSVdata = ''
let item_description,
item_playserver,
item_playchannel,
item_playlayer,
item_webplayout,
item_out,
item_uicolor,
item_dataformat,
item_relpath,
item_graphicPath,
item_version,
item_id
rundownData.templates.forEach((item,index) => {
// console.log('Iterating template index ' + index);
if (item.itemID == req.body.itemID) {
// This is the template to process.
// console.log('Exporting template ' + item.itemID);
item_description = item.description || '';
item_playserver = item.playserver || '';
item_playchannel = item.playchannel || '1';
item_playlayer = item.playlayer || '10';
item_webplayout = item.webplayout || '10';
item_out = item.out || 'manual';
item_uicolor = item.uicolor || '0';
item_dataformat = item.dataformat || 'json';
item_relpath = item.relpath || '';
// if OGraf, we need to add a few more properties:
if (item_relpath.toLowerCase().endsWith(".ograf.json")) {
item_graphicPath = item.ografProps.graphicPath || '';
item_version = item.ografProps.version || '';
item_id = item.ografProps.id || '';
}
CSVdata = '\r\n# SPX Rundown item CSV export. (More info: https://docs.spxgraphics.com/Guides/Tutorials/how+to+use+csv+files)\r\n\r\n'
CSVdata += '# description #;' + item_description + '\r\n'
CSVdata += '# playserver #;' + item_playserver + '\r\n'
CSVdata += '# playchannel #;' + item_playchannel + '\r\n'
CSVdata += '# playlayer #;' + item_playlayer + '\r\n'
CSVdata += '# webplayout #;' + item_webplayout + '\r\n'
CSVdata += '# out #;' + item_out + '\r\n'
CSVdata += '# uicolor #;' + item_uicolor + '\r\n'
CSVdata += '# dataformat #;' + item_dataformat + '\r\n'
CSVdata += '# relpath #;' + item_relpath + '\r\n'
// if OGraf, add graphicPath, version and id
if (item_relpath.toLowerCase().endsWith(".ograf.json")) {
CSVdata += '# graphicPath #;' + item_graphicPath + '\r\n';
CSVdata += '# version #;' + item_version + '\r\n';
CSVdata += '# id #;' + item_id + '\r\n';
}
CSVdata += '# onair #;false\r\n'
CSVdata += '# project #;' + showFolder + '\r\n'
CSVdata += '# rundown #;' + datafile + '\r\n'
CSVdata += '\r\n'
// print field ID's
CSVdata += '# FieldUUIDs #;'
item.DataFields.forEach((field,findex) => {
if (field.field && field.field!='' ) {
CSVdata += field.field + ';'
}
});
CSVdata += '\r\n';
// print field types
CSVdata += '# FieldTypes #;'
item.DataFields.forEach((field,findex) => {
if (field.field && field.field!='' ) {
CSVdata += field.ftype + ';'
}
});
CSVdata += '\r\n'
// print field titles
CSVdata += '# FieldTitls #;'
item.DataFields.forEach((field,findex) => {
if (field.field && field.field!='' ) {
CSVdata += field.title + ';'
}
});
CSVdata += '\r\n'
// print field fcalls if button
CSVdata += '# Fieldfcalls #;'
item.DataFields.forEach((field,findex) => {
if (field.field && field.field!='' && field.fcall) {
CSVdata += field.fcall + ";";
}
});
CSVdata += '\r\n'
// print field values
CSVdata += '\r\n'
let itemData = '# ID:auto;'
item.DataFields.forEach((field,findex) => {
if (field.field && field.field!='' && field.value) {
let dataToSave = field.value.replace(/\n/g,'<BR>') || ''; // replace all newlines with <BR>
itemData += dataToSave + ";";
}
});
CSVdata += itemData + '\r\n'
}
});
let timestamp = spx.prettifyDate(new Date(), 'YYYY-MM-DD-HHMMSS');
let filenameref = item_relpath.split('.')[0].replace('\\', '/').split('/').slice(-1)[0];
// generate CSV folder if not there
let CSVfolder = path.join(spx.getStartUpFolder(), 'ASSETS', 'csv')
fs.existsSync(CSVfolder) || fs.mkdirSync(CSVfolder)
let CSVfileRef = path.join(CSVfolder, filenameref + '_' + timestamp + '.csv');
await spx.writeTextFile(CSVfileRef,CSVdata);
// console.log(' Created CSV file ' + CSVfileRef);
logger.verbose('Created ' + CSVfileRef + ' from itemID ' + req.body.itemID + ' on ' + dataJSONfile + '. ');
res.status(200).send('Generated file ' + CSVfileRef);
} catch (error) {
logger.error('API error in exportCSVfile(): ', error);
}; //file written
}); // POST exportCSVfile end
// FUNCTIONS -------------------------------------------------------------------------------------------
async function GetDataFiles() {
// Get files
// const directoryPath = path.normalize("X:/01_Projects/Yle/CG/DEV/DATA_FOLDER/");
const directoryPath = path.normalize(config.general.dataroot);
let jsonData = {};
var key = 'files';
jsonData.folder = directoryPath;
jsonData[key] = [];
let id = 0;
try {
fs.readdirSync(directoryPath).forEach(file => {
let ext = path.extname(file).toUpperCase();
if (ext == ".JSON") {
var stats = fs.statSync(path.join(directoryPath, file));
var datem = moment(stats.mtime, 'DD.MM.YYYY').format();
var filedata = {
id: id,
name: file,
date: datem
};
id++;
jsonData[key].push(filedata);
}
});
return jsonData;
}
catch (error) {
logger.error('Error while reading files from ' + directoryPath + ': ' + err);
return (error);
}
} // GetDataFiles ended
module.exports = router;
File diff suppressed because it is too large Load Diff
+352
View File
@@ -0,0 +1,352 @@
// -------------------------------------------------------
// Handle Express server routes for the CasparCG commands.
// -------------------------------------------------------
var express = require("express");
const router = express.Router();
const path = require('path');
const logger = require('../utils/logger');
logger.debug('Caspar-route loading...');
const spx = require('../utils/spx_server_functions.js');
const ip = require('ip')
const ipad = ip.address(); // my ip address
const port = config.general.port || 5656;
const PlayoutCCG = require('../utils/playout_casparCG.js');
// ROUTES CCG -----------------------------------------------------------------------------------
router.get('/', function (req, res) {
res.send('Nothing here. Go away!');
});
router.get('/system', function (req, res) {
res.send('Nothing here. Get lost.');
});
router.get('/requestInfo', async (req, res) => {
var SERVER = req.query.server; // ?server=OVERLAY
var COMMAND = req.query.command; // &command=INFO
// console.log('requestInfo from ' + SERVER + ', cmd: ' + COMMAND);
const CCGHost = global.CCGSockets[PlayoutCCG.getSockIndex(SERVER)].spxhost;
const CCGPort = global.CCGSockets[PlayoutCCG.getSockIndex(SERVER)].spxport;
const net = require('net')
CasparRequest = new net.Socket();
CasparRequest.connect(CCGPort, CCGHost, function () {
CasparRequest.write(COMMAND + '\r\n');
CasparRequest.on('data', function (data) {
res.set('Content-Type', 'text/plain'); // format
res.status(200).send(data);
CasparRequest.destroy();
});
CasparRequest.on('error', function (error) {
return res.status(500).send(error);
CasparRequest.destroy();
});
});
});
router.get('/system/:data', (req, res) => {
// same principle as with /control/:data
// do OpSys level stuff, open folders etc..
//
// NOTE: This endpoint is in the WRONG PLACE
// it should be moved to a more generic place
//
// console.log('System utilities / ' + req.params.data);
data = JSON.parse(req.params.data);
let directoryPath = "";
logger.verbose('System utilities / ' + JSON.stringify(data));
res.sendStatus(200);
switch (data.command) {
case 'DATAFOLDER':
directoryPath = path.normalize(config.general.dataroot);
require('child_process').exec('start "" ' + directoryPath);
break;
case 'TEMPLATEFOLDER':
// directoryPath = path.normalize(config.general.templatefolder);
directoryPath = path.join(spx.getStartUpFolder(), 'ASSETS', 'templates');
require('child_process').exec('start "" ' + directoryPath);
break;
case 'CHECKCONNECTIONS':
// console.log('CCG: Checking server connections...');
spx.checkServerConnections();
break;
case 'RESTARTSERVER':
//
logger.error('RESTART SERVER REQUEST RECEIVED. Will try to kill and restart using process manager.');
process.exit(2);
break;
}
});
router.get('/control/:data', (req, res) => {
// Principle:
// We get data object which has
// - data.command (ADD | STOP | UPDATE)
// - data.fields [{ id: 'f0', value: 'Eka' }, { id: 'f1', value: 'Toka' }];
// - data.element headline1
// - data.profile News
//
// Then we read profiles-file and search for the required element
// such as 'headline1' and get needed data from it:
// - templatefile
// - server
// - channel
// - layer
data = JSON.parse(req.params.data);
logger.info('CCG/Control/ ' + JSON.stringify(data));
res.sendStatus(200);
if (!spx.CCGServersConfigured){ return } // exit early, no need to do any CasparCG work
var DataStr = "";
const directoryPath = path.normalize(config.general.dataroot);
let profilefile = path.join(directoryPath, 'config', 'profiles.json');
const profileDataAsJSON = spx.GetJsonData(profilefile);
var arr = profileDataAsJSON.profiles;
let GFX_Teml = "";
let GFX_Serv = "";
let GFX_Chan = "";
let GFX_Laye = "";
for (var i = 0; i < arr.length; i++) {
var obj = arr[i];
if (obj.name == data.profile) {
GFX_Teml = eval("obj.templates." + data.element + ".templatefile");
GFX_Serv = eval("obj.templates." + data.element + ".server");
GFX_Chan = eval("obj.templates." + data.element + ".channel");
GFX_Laye = eval("obj.templates." + data.element + ".layer");
}
}
logger.debug('CCG/Control - Profile ' + data.profile + "', Template: '" + GFX_Teml, "', CasparCG: " + GFX_Serv + ", " + GFX_Chan + ", " + GFX_Laye);
if (data.command == "ADD" || data.command == "UPDATE") {
let TEMPLATEDATA = "";
if (data.fields) {
data.fields.forEach(item => {
logger.verbose('Processing field for CCG: ' + item.id + ' with value: ' + item.value);
TEMPLATEDATA += spx.CGComponentFactory(item.id, item.value)
});
}
DataSta = "<templateData>";
DataEnd = "</templateData>";
DataStr = DataSta + TEMPLATEDATA + DataEnd;
}
try {
switch (data.command) {
case 'ADD':
global.CCGSockets[spx.getSockIndex(spx.getChannel(data))].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' ADD 1 "' + GFX_Teml + '" 1 "' + DataStr + '"\r\n');
break;
case 'UPDATE':
console.log('TODO: This is probably not used to send update to CasparCG... See playout_casparCG.js instead! ** FIXME:**');
global.CCGSockets[spx.getSockIndex(spx.getChannel(data))].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' UPDATE 1 "' + DataStr + '"\r\n');
break;
case 'NEXT':
global.CCGSockets[spx.getSockIndex(spx.getChannel(data))].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' NEXT 0\r\n');
break;
case 'STOP':
global.CCGSockets[spx.getSockIndex(spx.getChannel(data))].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' STOP 1\r\n');
break;
default:
logger.warn('CCG/Control - Unknown command: ' + data.command);
}
} catch (error) {
logger.error('ERROR in /control/:data, unable to send CCG command. Server up? ' + error)
}
}); // control ended
router.get('/controljson/:data', (req, res) => {
data = JSON.parse(req.params.data);
logger.info('CCG/controljson ' + JSON.stringify(data));
res.sendStatus(200);
if (!spx.CCGServersConfigured){ return } // exit early, no need to do any CasparCG work
const directoryPath = path.normalize(config.general.dataroot);
let profilefile = path.join(directoryPath, 'config', 'profiles.json');
const profileDataAsJSON = spx.GetJsonData(profilefile);
var arr = profileDataAsJSON.profiles;
let GFX_Teml = "";
let GFX_Serv = "";
let GFX_Chan = "";
let GFX_Laye = "";
let GFX_JSON = ""
let stng = ""
for (var i = 0; i < arr.length; i++) {
var obj = arr[i];
if (obj.name == data.profile) {
GFX_Teml = eval("obj.templates." + data.element + ".templatefile");
GFX_Serv = eval("obj.templates." + data.element + ".server");
GFX_Chan = eval("obj.templates." + data.element + ".channel");
GFX_Laye = eval("obj.templates." + data.element + ".layer");
GFX_JSON = JSON.stringify(data.jsonData);
}
}
logger.verbose("CCG/ControlJSON Profile: '" + data.profile + "', Template: '" + GFX_Teml, "', CasparCG: " + GFX_Serv + ", " + GFX_Chan + ", " + GFX_Laye)
logger.debug('json' + JSON.stringify(GFX_JSON));
stng = GFX_JSON;
stng = stng.split('§backslash§').join('&bsol;'); // replace §backslash§ with html corresponding entity
stng = stng.split('"').join('\\"'); // replace " with \"
try {
switch (data.command) {
case 'ADD':
global.CCGSockets[spx.getSockIndex(spx.getChannel(data))].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' ADD 1 "' + GFX_Teml + '" 1 "' + stng + '"\r\n');
break;
case 'UPDATE':
global.CCGSockets[spx.getSockIndex(spx.getChannel(data))].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' UPDATE 1 "' + stng + '"\r\n');
break;
default:
logger.warn('CCG/ControlJSON - Unknown command: ' + data.command);
}
} catch (error) {
logger.error('ERROR in /controljson/:data: ' + error)
}
});
router.post('/disable', async (req, res) => {
// Added in 1.1.0. Set "disabled" state of the given CasparCG server
// This only affects playback commands. Initialization works normally.
logger.verbose('CasparCG server ' + req.body.server + ' disabled to ' + req.body.disabled);
try {
var ConfigFile = global.configfileref;
var ConfigData = await spx.GetJsonData(ConfigFile);
if (ConfigData.casparcg.servers) {
ConfigData.casparcg.servers.forEach((serverItem,index) => {
if (serverItem.name == req.body.server) {
serverItem.disabled = req.body.disabled
}
});
}
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 CCG disable [' + error + ']';
logger.error(errmsg);
res.status(500).send(errmsg) // error 500 AJAX RESPONSE
};
});
// Create all required Socket Connections for CasparCG servers specified in config.json
const net = require('net')
let ServerDataForLogger = [];
if (config.casparcg) {
config.casparcg.servers.forEach((element,index) => {
const CurName = element.name;
const CurHost = element.host;
const CurPort = element.port;
ServerDataForLogger.push({ name: CurName, host: CurHost, port: CurPort });
// next two lines creates a dynamic variable for this loop iteration
// Changed in 1.3.0, from:
// var CurCCG = CurName + "= undefined";
// eval(CurCCG);
// ..to:
var CurCCG = new net.Socket();
// end of change
global.CCGSockets.push(CurCCG); // --> PUSH Socket object to a global array for later use
CurCCG.spxname = CurName; // save each entry a name for later searching!
CurCCG.spxhost = CurHost; // save each entry a host for later searching! (v.1.0.14)
CurCCG.spxport = CurPort; // save each entry a port for later searching! (v.1.0.14)
CurCCG.connect(CurPort, CurHost, function () {
ServerDataForLogger.push({ name: CurName, host: CurHost, port: CurPort });
data = { spxcmd: 'updateServerIndicator', indicator: 'indicator' + index, color: '#00CC00' };
io.emit('SPXMessage2Client', data);
data = { spxcmd: 'updateStatusText', status: 'Communication established with ' + CurName + '.' };
io.emit('SPXMessage2Client', data);
logger.verbose('SPX connected to CasparCG as \'' + CurName + '\' at ' + CurHost + ":" + CurPort + '.');
});
CurCCG.on('data', function (data) {
logger.verbose('SPX received data from CasparCG ' + CurName + ': ' + data);
// we must parse the data so we can evaluate it...
let CCG_RETURN_TEXT = String(data).replace('\r','').replace('\n',''); // convert return object to string, strip \r\n
let CCG_RETURN_CODE = CCG_RETURN_TEXT.substring(0, 2); // first two chars
switch (CCG_RETURN_CODE) {
case "20":
logger.verbose('Comms good with ' + CurName + ": " + CCG_RETURN_TEXT);
break;
case "40":
logger.error(CurName + ' CasparCG response: ' + CCG_RETURN_TEXT );
logger.debug('Verify CasparCG\'s (' + CurName + ') access to templates on SPX server at ' + spx.getTemplateSourcePath());
data = { spxcmd: 'updateStatusText', status: 'Error in comms with ' + CurName + '.' };
io.emit('SPXMessage2Client', data);
break;
case "50":
logger.error('Failed ' + CurName + ": " + CCG_RETURN_TEXT);
data = { spxcmd: 'updateStatusText', status: CurName + ' failed.' };
io.emit('SPXMessage2Client', data);
break;
default:
logger.error('Unknown status value ' + CurName + ' - ' + CCG_RETURN_CODE + ' - ' + CCG_RETURN_TEXT);
break;
}
// SocketIO call to client
data = { spxcmd: 'updateServerIndicator', indicator: 'indicator' + index, color: '#00CC00' };
io.emit('SPXMessage2Client', data);
if (data.toString().endsWith('exit')) {
CCGclient.destroy();
}
});
CurCCG.on('close', function () {
// SocketIO call to client
data = { spxcmd: 'updateServerIndicator', indicator: 'indicator' + index, color: '#CC0000' };
io.emit('SPXMessage2Client', data);
data = { spxcmd: 'updateStatusText', status: 'Connection to ' + CurName + ' was closed.' };
io.emit('SPXMessage2Client', data);
logger.verbose('SPX connection to CasparCG "' + CurName + '" closed (' + CurHost + ':' + CurPort + ').');
});
CurCCG.on('error', function (err) {
// console.log('Still 4?', CurCCG.connecting)
data = { spxcmd: 'updateServerIndicator', indicator: 'indicator' + index, color: '#CC0000' };
io.emit('SPXMessage2Client', data);
data = { spxcmd: 'updateStatusText', status: 'Communication error with ' + CurName + '.' };
io.emit('SPXMessage2Client', data);
logger.warn('Unable to connect CasparCG "' + CurName + '" (' + CurCCG.spxhost + ':' + CurCCG.spxport + '). Is it running?');
// console.log('Sockets: ', global.CCGSockets);
});
// console.log('Still 3?', CurCCG.connecting)
});
}; // end if
logger.debug('ServerDataForLogger during init: ' + JSON.stringify(ServerDataForLogger));
module.exports = router;
+19
View File
@@ -0,0 +1,19 @@
// 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('WebPlayer-route loading...');
// const spx = require('../utils/spx_server_functions.js');
// // ROUTES -------------------------------------------------------------------------------------------
// router.get('/', function (reg, res) {
// let htmlFile = path.join(__dirname+'/../http/renderer/index.html');
// // res.sendFile(htmlFile);
// res.send(htmlFile);
// });
// module.exports = router;