// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck -- not ready to fully type import fs from 'fs'; import xlsx from 'node-xlsx'; import { generateId, calculateDuration } from 'ontime-utils'; import { DatabaseModel, EndAction, OntimeEvent, OntimeRundown, SupportedEvent, TimerType, UserFields, } from 'ontime-types'; import { event as eventDef } from '../models/eventsDefinition.js'; import { dbModel } from '../models/dataModel.js'; import { deleteFile, makeString } from './parserUtils.js'; import { parseAliases, parseProject, parseOsc, parseRundown, parseSettings, parseUserFields, parseViewSettings, } from './parserFunctions.js'; import { parseExcelDate } from './time.js'; export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; export const JSON_MIME = 'application/json'; /** * @description Excel array parser * @param {array} excelData - array with excel sheet * @returns {object} - parsed object */ export const parseExcel = async (excelData) => { const projectData: Partial = { title: '', description: '', publicUrl: '', backstageUrl: '', }; const customUserFields: Partial = {}; const rundown: OntimeRundown = []; let timeStartIndex: number | null = null; let timeEndIndex: number | null = null; let titleIndex: number | null = null; let cueIndex: number | null = null; let presenterIndex: number | null = null; let subtitleIndex: number | null = null; let isPublicIndex: number | null = null; let skipIndex: number | null = null; let notesIndex: number | null = null; let colourIndex: number | null = null; let user0Index: number | null = null; let user1Index: number | null = null; let user2Index: number | null = null; let user3Index: number | null = null; let user4Index: number | null = null; let user5Index: number | null = null; let user6Index: number | null = null; let user7Index: number | null = null; let user8Index: number | null = null; let user9Index: number | null = null; let endActionIndex: number | null = null; let timerTypeIndex: number | null = null; excelData .filter((e) => e.length > 0) .forEach((row) => { let eventTitleNext = false; let projectTitleNext = false; let publicUrlNext = false; let publicInfoNext = false; let backstageUrlNext = false; let backstageInfoNext = false; const event: Partial = {}; row.forEach((column, j) => { // check flags if (eventTitleNext) { projectData.title = column; eventTitleNext = false; } else if (projectTitleNext) { projectData.description = column; projectTitleNext = false; } else if (publicUrlNext) { projectData.publicUrl = column; publicUrlNext = false; } else if (publicInfoNext) { projectData.publicInfo = column; publicInfoNext = false; } else if (backstageUrlNext) { projectData.backstageUrl = column; backstageUrlNext = false; } else if (backstageInfoNext) { projectData.backstageInfo = column; backstageInfoNext = false; } else if (j === timeStartIndex) { event.timeStart = parseExcelDate(column); } else if (j === timeEndIndex) { event.timeEnd = parseExcelDate(column); } else if (j === titleIndex) { event.title = column; } else if (j === cueIndex) { event.cue = column; } else if (j === presenterIndex) { event.presenter = column; } else if (j === subtitleIndex) { event.subtitle = column; } else if (j === isPublicIndex) { event.isPublic = Boolean(column); } else if (j === skipIndex) { event.skip = Boolean(column); } else if (j === notesIndex) { event.note = column; } else if (j === endActionIndex) { if (column === '') { event.endAction = EndAction.None; } else { event.endAction = column; } } else if (j === timerTypeIndex) { if (column === '') { event.timerType = TimerType.CountDown; } else { event.timerType = column; } } else if (j === colourIndex) { event.colour = column; } else if (j === user0Index) { event.user0 = column; } else if (j === user1Index) { event.user1 = column; } else if (j === user2Index) { event.user2 = column; } else if (j === user3Index) { event.user3 = column; } else if (j === user4Index) { event.user4 = column; } else if (j === user5Index) { event.user5 = column; } else if (j === user6Index) { event.user6 = column; } else if (j === user7Index) { event.user7 = column; } else if (j === user8Index) { event.user8 = column; } else if (j === user9Index) { event.user9 = column; } else { if (typeof column === 'string') { const col = column.toLowerCase(); // look for keywords // need to make sure it is a string first switch (col) { case 'project name': eventTitleNext = true; break; case 'project description': projectTitleNext = true; break; case 'public url': publicUrlNext = true; break; case 'public info': publicInfoNext = true; break; case 'backstage url': backstageUrlNext = true; break; case 'backstage info': backstageInfoNext = true; break; case 'time start': case 'start': timeStartIndex = j; break; case 'time end': case 'end': case 'finish': timeEndIndex = j; break; case 'cue': case 'page': cueIndex = j; break; case 'event title': case 'title': titleIndex = j; break; case 'presenter name': case 'speaker': case 'presenter': presenterIndex = j; break; case 'event subtitle': case 'subtitle': subtitleIndex = j; break; case 'is public? (x)': case 'is public': case 'public': isPublicIndex = j; break; case 'skip? (x)': case 'skip?': case 'skip': skipIndex = j; break; case 'note': case 'notes': notesIndex = j; break; case 'colour': case 'color': colourIndex = j; break; case 'end action': endActionIndex = j; break; case 'timer type': timerTypeIndex = j; break; default: // look for user defined if (col.startsWith('user')) { const index = column.charAt(4); // name is the bit after the : const [, name] = column.split(':'); if (typeof name !== 'undefined') { if (index === '0') { customUserFields.user0 = name; user0Index = j; } else if (index === '1') { customUserFields.user1 = name; user1Index = j; } else if (index === '2') { customUserFields.user2 = name; user2Index = j; } else if (index === '3') { customUserFields.user3 = name; user3Index = j; } else if (index === '4') { customUserFields.user4 = name; user4Index = j; } else if (index === '5') { customUserFields.user5 = name; user5Index = j; } else if (index === '6') { customUserFields.user6 = name; user6Index = j; } else if (index === '7') { customUserFields.user7 = name; user7Index = j; } else if (index === '8') { customUserFields.user8 = name; user8Index = j; } else if (index === '9') { customUserFields.user9 = name; user9Index = j; } } } break; } } } }); if (Object.keys(event).length > 0) { // if any data was found, push to array // take care of it in the next step rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent); } }); return { rundown, project: projectData, settings: { app: 'ontime', version: 2, }, userFields: { ...dbModel.userFields, ...customUserFields }, }; }; /** * @description JSON parser function for v1 of data system * @param {object} jsonData - json data JSON object to be parsed * @param {boolean} [enforce=false] - flag, tells to create an object anyway * @returns {object} - parsed object */ export const parseJson = async (jsonData, enforce = false): Promise => { if (!jsonData || typeof jsonData !== 'object') { return null; } // object containing the parsed data const returnData: Partial = {}; // parse Events returnData.rundown = parseRundown(jsonData); // parse Event returnData.project = parseProject(jsonData, enforce); // Settings handled partially returnData.settings = parseSettings(jsonData, enforce); // View settings handled partially returnData.viewSettings = parseViewSettings(jsonData, enforce); // Import Aliases if any returnData.aliases = parseAliases(jsonData); // Import user fields if any returnData.userFields = parseUserFields(jsonData); // Import OSC settings if any returnData.osc = parseOsc(jsonData, enforce); // Import HTTP settings if any // returnData.http = parseHttp(jsonData, enforce); return returnData as DatabaseModel; }; /** * @description Enforces formatting for events * @param {object} eventArgs - attributes of event * @param cueFallback * @returns {object|null} - formatted object or null in case is invalid */ export const validateEvent = (eventArgs: Partial, cueFallback: string) => { // ensure id is defined and unique const id = eventArgs.id || generateId(); const cue = eventArgs.cue || cueFallback; let event = null; // return if object is empty if (Object.keys(eventArgs).length > 0) { // make sure all properties exits // dont load any extra properties than the ones known const e = eventArgs; const d = eventDef; const start = e.timeStart != null && typeof e.timeStart === 'number' ? e.timeStart : d.timeStart; const end = e.timeEnd != null && typeof e.timeEnd === 'number' ? e.timeEnd : d.timeEnd; event = { ...d, title: makeString(e.title, d.title), subtitle: makeString(e.subtitle, d.subtitle), presenter: makeString(e.presenter, d.presenter), timeStart: start, timeEnd: end, endAction: makeString(e.endAction, d.endAction), timerType: makeString(e.timerType, d.timerType), duration: calculateDuration(start, end), isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic, skip: typeof e.skip === 'boolean' ? e.skip : d.skip, note: makeString(e.note, d.note), user0: makeString(e.user0, d.user0), user1: makeString(e.user1, d.user1), user2: makeString(e.user2, d.user2), user3: makeString(e.user3, d.user3), user4: makeString(e.user4, d.user4), user5: makeString(e.user5, d.user5), user6: makeString(e.user6, d.user6), user7: makeString(e.user7, d.user7), user8: makeString(e.user8, d.user8), user9: makeString(e.user9, d.user9), colour: makeString(e.colour, d.colour), id, cue, type: 'event', }; } return event; }; type ResponseOK = { data: Partial; message: 'success' }; type ResponseError = { error: true; message: string }; /** * @description Middleware function that checks file type and calls relevant parser * @param {string} file - reference to file * @return {object} - parse result message */ export const fileHandler = async (file): Promise => { let res: Partial = {}; // check which file type are we dealing with if (file.endsWith('.xlsx')) { try { const excelData = xlsx .parse(file, { cellDates: true }) .find(({ name }) => name.toLowerCase() === 'ontime' || name.toLowerCase() === 'event schedule'); // we only look at worksheets called ontime or event schedule if (excelData?.data) { const dataFromExcel = await parseExcel(excelData.data); res.data = {}; res.data.rundown = parseRundown(dataFromExcel); res.data.project = parseProject(dataFromExcel, true); res.data.userFields = parseUserFields(dataFromExcel); res.message = 'success'; } else { const errorMessage = 'No sheet found named "ontime" or "event schedule"'; res = { error: true, message: errorMessage, }; } } catch (error) { res = { error: true, message: `Error parsing file: ${error}` }; } } if (file.endsWith('.json')) { // if json check version const rawdata = fs.readFileSync(file); let uploadedJson = null; try { uploadedJson = JSON.parse(rawdata); } catch (error) { return { error: true, message: 'Error parsing JSON file' }; } if (uploadedJson.settings.version === 2) { try { res.data = await parseJson(uploadedJson); res.message = 'success'; } catch (error) { res = { error: true, message: `Error parsing file: ${error}` }; } } else { res = { error: true, message: 'Error parsing file, version unknown' }; } } // delete file await deleteFile(file); return res; };