Init WebCG
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user