feat: event cue (#473)

* feat: parse cue

* refactor: get delay from backend

* feat: add cue to UI

* refactor: remove deprecated delay logic

* refactor: extract studio clock specific logic

* refactor: extract utilities

* refactor: fix issue with missing key

* style: prevent cue overflow

* style: prevent whitespace wrap

* feat: add support for cues in integrations
This commit is contained in:
Carlos Valente
2023-08-17 21:43:11 +02:00
committed by GitHub
parent 51c31adaf1
commit 657cc22b44
62 changed files with 1363 additions and 1290 deletions
@@ -2,7 +2,7 @@
* Class Event Provider is a mediator for handling the local db
* and adds logic specific to ontime data
*/
import { EventData, ViewSettings } from 'ontime-types';
import { EventData, OntimeRundown, ViewSettings } from 'ontime-types';
import { data, db } from '../../modules/loadDb.js';
import { safeMerge } from './DataProvider.utils.js';
@@ -22,19 +22,15 @@ export class DataProvider {
return data.eventData;
}
static async setRundown(newData) {
static async setRundown(newData: OntimeRundown) {
data.rundown = [...newData];
await this.persist();
}
static getIndexOf(eventId) {
static getIndexOf(eventId: string) {
return data.rundown.findIndex((e) => e.id === eventId);
}
static getEventById(eventId) {
return data.rundown.find((e) => e.id === eventId);
}
static getRundownLength() {
return data.rundown.length;
}
@@ -85,6 +85,16 @@ export class EventLoader {
return timedEvents.find((event) => event.id === eventId);
}
/**
* returns first event given its cue
* @param {string} cue
* @return {object | undefined}
*/
static getEventWithCue(cue) {
const timedEvents = EventLoader.getTimedEvents();
return timedEvents.find((event) => event.cue === cue);
}
/**
* loads an event given its id
* @param {string} eventId
@@ -123,6 +123,15 @@ export function dispatchFromAdapter(type: string, payload: unknown, source?: 'os
PlaybackService.startById(payload);
break;
}
case 'startcue': {
if (!payload || typeof payload !== 'string') {
throw new Error(`Event cue not recognised: ${payload}`);
}
PlaybackService.startByCue(payload);
break;
}
case 'pause': {
PlaybackService.pause();
break;
@@ -189,6 +198,19 @@ export function dispatchFromAdapter(type: string, payload: unknown, source?: 'os
}
break;
}
case 'gotocue':
case 'loadcue': {
if (!payload || typeof payload !== 'string') {
throw new Error(`Event cue not recognised: ${payload}`);
}
try {
PlaybackService.loadByCue(payload);
} catch (error) {
throw new Error(`OSC IN: error calling goto ${error}`);
}
break;
}
case 'get-playback': {
const playback = eventStore.get('playback');
+1 -1
View File
@@ -1,6 +1,6 @@
import { EndAction, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
export const event: Omit<OntimeEvent, 'id' | 'delay'> = {
export const event: Omit<OntimeEvent, 'id' | 'delay' | 'cue'> = {
title: '',
subtitle: '',
presenter: '',
@@ -62,6 +62,21 @@ export class PlaybackService {
return success;
}
/**
* starts first event matching given cue
* @param {string} cue
* @return {boolean} success
*/
static startByCue(cue: string): boolean {
const event = EventLoader.getEventWithCue(cue);
const success = PlaybackService.loadEvent(event);
if (success) {
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
PlaybackService.start();
}
return success;
}
/**
* loads event matching given ID
* @param {string} eventId
@@ -90,6 +105,20 @@ export class PlaybackService {
return success;
}
/**
* loads first event matching given cue
* @param {string} cue
* @return {boolean} success
*/
static loadByCue(cue: string): boolean {
const event = EventLoader.getEventWithCue(cue);
const success = PlaybackService.loadEvent(event);
if (success) {
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
}
return success;
}
/**
* Loads event before currently selected
*/
@@ -8,9 +8,9 @@ import {
Playback,
SupportedEvent,
} from 'ontime-types';
import { generateId } from 'ontime-utils';
import { generateId, getCueCandidate } from 'ontime-utils';
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
import { block as blockDef, delay as delayDef, event as eventDef } from '../../models/eventsDefinition.js';
import { block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js';
import { MAX_EVENTS } from '../../settings.js';
import { EventLoader, eventLoader } from '../../classes/event-loader/EventLoader.js';
import { eventTimer } from '../TimerService.js';
@@ -26,6 +26,7 @@ import {
delayedRundownCacheKey,
} from './delayedRundown.utils.js';
import { logger } from '../../classes/Logger.js';
import { validateEvent } from '../../utils/parser.js';
import { clock } from '../Clock.js';
/**
@@ -164,30 +165,30 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
let newEvent: Partial<OntimeBaseEvent> = {};
const id = generateId();
// TODO: filter the parameters that exist in the event, use the parserUtils
switch (eventData.type) {
case SupportedEvent.Event:
newEvent = { ...eventDef, ...eventData, id };
break;
case SupportedEvent.Delay:
newEvent = { ...delayDef, ...eventData, id };
break;
case SupportedEvent.Block:
newEvent = { ...blockDef, ...eventData, id };
break;
}
let insertIndex = 0;
if (typeof newEvent?.after !== 'undefined') {
const index = DataProvider.getIndexOf(newEvent.after);
if (eventData?.after !== 'undefined') {
const index = DataProvider.getIndexOf(eventData.after);
if (index < 0) {
logger.warning(LogOrigin.Server, `Could not find event with id ${newEvent.after}`);
logger.warning(LogOrigin.Server, `Could not find event with id ${eventData.after}`);
} else {
insertIndex = index + 1;
}
delete newEvent.after;
}
switch (eventData.type) {
case SupportedEvent.Event: {
newEvent = validateEvent(eventData, getCueCandidate(DataProvider.getRundown(), eventData?.after)) as OntimeEvent;
break;
}
case SupportedEvent.Delay:
newEvent = { ...delayDef, duration: eventData.duration, id } as OntimeDelay;
break;
case SupportedEvent.Block:
newEvent = { ...blockDef, title: eventData.title, id } as OntimeBlock;
break;
}
delete eventData.after;
// modify rundown
await cachedAdd(insertIndex, newEvent as OntimeEvent | OntimeDelay | OntimeBlock);
@@ -204,6 +205,10 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
}
export async function editEvent(eventData: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) {
if (eventData.type === SupportedEvent.Event && eventData?.cue === '') {
throw new Error(`Cue value invalid`);
}
const newEvent = await cachedEdit(eventData.id, eventData);
// notify timer service of changed events
@@ -1,4 +1,4 @@
import { EndAction, OntimeRundown, SupportedEvent, TimerType } from 'ontime-types';
import { EndAction, OntimeEvent, OntimeRundown, SupportedEvent, TimerType } from 'ontime-types';
import { calculateRuntimeDelays, calculateRuntimeDelaysFrom, getDelayAt } from '../delayedRundown.utils.js';
@@ -31,6 +31,7 @@ describe('calculateRuntimeDelays', () => {
type: SupportedEvent.Event,
revision: 0,
id: '659e1',
cue: '1',
},
{
duration: 600000,
@@ -64,6 +65,7 @@ describe('calculateRuntimeDelays', () => {
type: SupportedEvent.Event,
revision: 0,
id: '1c48f',
cue: '2',
},
{
duration: 1200000,
@@ -97,6 +99,7 @@ describe('calculateRuntimeDelays', () => {
type: SupportedEvent.Event,
revision: 0,
id: 'd48c2',
cue: '3',
},
{
title: '',
@@ -129,16 +132,17 @@ describe('calculateRuntimeDelays', () => {
type: SupportedEvent.Event,
revision: 0,
id: '2f185',
cue: '4',
},
];
const updatedRundown = calculateRuntimeDelays(rundown);
expect(rundown.length).toBe(updatedRundown.length);
expect(updatedRundown[0].delay).toBe(0);
expect(updatedRundown[2].delay).toBe(600000);
expect(updatedRundown[4].delay).toBe(600000 + 1200000);
expect(updatedRundown[6].delay).toBe(0);
expect((updatedRundown[0] as OntimeEvent).delay).toBe(0);
expect((updatedRundown[2] as OntimeEvent).delay).toBe(600000);
expect((updatedRundown[4] as OntimeEvent).delay).toBe(600000 + 1200000);
expect((updatedRundown[6] as OntimeEvent).delay).toBe(0);
});
});
@@ -171,6 +175,7 @@ describe('getDelayAt()', () => {
revision: 0,
id: '659e1',
delay: 0,
cue: '1',
},
{
duration: 600000,
@@ -205,6 +210,7 @@ describe('getDelayAt()', () => {
revision: 0,
id: '1c48f',
delay: 600000,
cue: '2',
},
{
duration: 1200000,
@@ -239,6 +245,7 @@ describe('getDelayAt()', () => {
revision: 0,
id: 'd48c2',
delay: 1800000,
cue: '3',
},
{
title: '',
@@ -272,6 +279,7 @@ describe('getDelayAt()', () => {
revision: 0,
id: '2f185',
delay: 0,
cue: '4',
},
];
@@ -331,6 +339,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
revision: 0,
id: '659e1',
delay: 0,
cue: '1',
},
{
duration: 600000,
@@ -365,6 +374,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
revision: 0,
id: '1c48f',
delay: 0,
cue: '2',
},
{
duration: 1200000,
@@ -399,6 +409,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
revision: 0,
id: 'd48c2',
delay: 1800000,
cue: '3',
},
{
title: '',
@@ -432,14 +443,15 @@ describe('calculateRuntimeDelaysFrom()', () => {
revision: 0,
id: '2f185',
delay: 0,
cue: '4',
},
];
const updatedRundown = calculateRuntimeDelaysFrom('07986', delayedRundown);
// we only update from the 4th on
expect(updatedRundown[0].delay).toBe(0);
expect((updatedRundown[0] as OntimeEvent).delay).toBe(0);
// 1 + 3
expect(updatedRundown[4].delay).toBe(600000 + 1200000);
expect((updatedRundown[4] as OntimeEvent).delay).toBe(600000 + 1200000);
});
});
@@ -1,54 +0,0 @@
import { getPreviousPlayable } from '../eventUtils.js';
describe('getPreviousPlayable()', () => {
describe('given a list of events', () => {
it('finds the previous playable event', () => {
const events = [
{ id: 100, type: 'delay' },
{ id: 101, type: 'event', skip: true },
{ id: 102, type: 'event', skip: true },
{ id: 103, type: 'event', skip: false },
{ id: 'not-this', type: 'block' },
{ id: 104, type: 'event' },
];
const { index, id } = getPreviousPlayable(events, events[4].id);
expect(index).toBe(3);
expect(id).toBe(103);
});
});
describe('handles common errors', () => {
it('returns null if id not found in list', () => {
const events = [
{ id: 0, type: 'delay' },
{ id: 1, type: 'event', skip: true },
{ id: 2, type: 'event', skip: true },
{ id: 3, type: 'event', skip: false },
{ id: 4, type: 'event' },
];
const { index, id } = getPreviousPlayable(events, 'no-valid-id');
expect(index).toBe(null);
expect(id).toBe(null);
});
it('returns null if there are no previous events to play', () => {
const events = [
{ id: 0, type: 'delay' },
{ id: 1, type: 'event', skip: true },
{ id: 2, type: 'event', skip: true },
{ id: 3, type: 'event', skip: true },
{ id: 4, type: 'event' },
];
const { index, id } = getPreviousPlayable(events, events[4].id);
expect(index).toBe(null);
expect(id).toBe(null);
});
it('returns null if list is empty', () => {
const events = [];
const { index, id } = getPreviousPlayable(events, 'made-up');
expect(index).toBe(null);
expect(id).toBe(null);
});
});
});
@@ -1,10 +1,11 @@
import { vi } from 'vitest';
import { dbModel } from '../../models/dataModel.ts';
import { parseExcel, parseJson, validateEvent } from '../parser.ts';
import { makeString } from '../parserUtils.ts';
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.ts';
import { EndAction, TimerType } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
import { dbModel } from '../../models/dataModel.js';
import { parseExcel, parseJson, validateEvent } from '../parser.js';
import { makeString } from '../parserUtils.js';
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.js';
describe('test json parser with valid def', () => {
const testData = {
@@ -220,64 +221,20 @@ describe('test json parser with valid def', () => {
const first = parseResponse?.rundown[0];
const expected = {
title: 'Guest Welcoming',
subtitle: '',
presenter: '',
note: '',
timeStart: 31500000,
timeEnd: 32400000,
duration: 32400000 - 31500000,
isPublic: false,
endAction: 'play-next',
timerType: 'clock',
skip: false,
colour: '',
type: 'event',
revision: 0,
id: '4b31',
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
};
expect(first).toStrictEqual(expected);
expect(first).toMatchObject(expected);
});
it('second event is as a match', () => {
const second = parseResponse?.rundown[1];
const expected = {
title: 'Good Morning',
subtitle: 'Days schedule',
presenter: 'Carlos Valente',
note: '',
timeStart: 32400000,
timeEnd: 36000000,
endAction: 'play-next',
timerType: 'count-up',
duration: 36000000 - 32400000,
isPublic: true,
skip: true,
colour: 'red',
type: 'event',
revision: 0,
id: 'f24d',
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
};
expect(second).toStrictEqual(expected);
expect(second).toMatchObject(expected);
});
it('third event end action is set as the default value', () => {
const third = parseResponse?.rundown[2];
@@ -448,7 +405,7 @@ describe('test corrupt data', () => {
it('handles missing event data', async () => {
const emptyEventData = {
rundown: [{}, {}, {}, {}, {}, {}, {}, {}],
event: {},
eventData: {},
settings: {
app: 'ontime',
version: 2,
@@ -459,7 +416,7 @@ describe('test corrupt data', () => {
};
const parsedDef = await parseJson(emptyEventData);
expect(parsedDef.event).toStrictEqual(dbModel.event);
expect(parsedDef.eventData).toStrictEqual(dbModel.eventData);
});
it('handles missing settings', async () => {
@@ -488,7 +445,7 @@ describe('test event validator', () => {
const event = {
title: 'test',
};
const validated = validateEvent(event);
const validated = validateEvent(event, 'test');
expect(validated).toEqual(
expect.objectContaining({
@@ -503,6 +460,7 @@ describe('test event validator', () => {
revision: expect.any(Number),
type: expect.any(String),
id: expect.any(String),
cue: 'test',
colour: expect.any(String),
user0: expect.any(String),
user1: expect.any(String),
@@ -520,7 +478,7 @@ describe('test event validator', () => {
it('fails an empty object', () => {
const event = {};
const validated = validateEvent(event);
const validated = validateEvent(event, 'none');
expect(validated).toEqual(null);
});
@@ -531,7 +489,8 @@ describe('test event validator', () => {
presenter: 3.2,
note: '1899-12-30T08:00:10.000Z',
};
const validated = validateEvent(event);
// @ts-expect-error -- we know this is wrong, testing imports outside domain
const validated = validateEvent(event, 'not-used');
expect(typeof validated.title).toEqual('string');
expect(typeof validated.subtitle).toEqual('string');
expect(typeof validated.presenter).toEqual('string');
@@ -543,6 +502,7 @@ describe('test event validator', () => {
timeStart: false,
timeEnd: '2',
};
// @ts-expect-error -- we know this is wrong, testing imports outside domain
const validated = validateEvent(event);
expect(typeof validated.timeStart).toEqual('number');
expect(validated.timeStart).toEqual(0);
@@ -554,6 +514,7 @@ describe('test event validator', () => {
const event = {
title: {},
};
// @ts-expect-error -- we know this is wrong, testing imports outside domain
const validated = validateEvent(event);
expect(typeof validated.title).toEqual('string');
});
@@ -571,11 +532,13 @@ describe('test makeString function', () => {
converted = makeString(val);
expect(converted).toBe(expected);
// @ts-expect-error -- we know this is wrong, testing imports outside domain
val = ['testing'];
expected = 'testing';
converted = makeString(val);
expect(converted).toBe(expected);
// @ts-expect-error -- we know this is wrong, testing imports outside domain
val = { doing: 'testing' };
converted = makeString(val, 'fallback');
expect(converted).toBe('fallback');
@@ -628,10 +591,6 @@ describe('test parseExcel function', () => {
'x',
'',
'Ballyhoo',
'',
'',
'',
'',
'a0',
'a1',
'a2',
@@ -650,15 +609,11 @@ describe('test parseExcel function', () => {
'A song from the hearth',
'Still Carlos',
'Derailing early',
'clock',
'load-next',
'clock',
'',
'',
'x',
'Rainbow chase',
'',
'',
'',
'',
'b0',
'',
'',
@@ -682,10 +637,11 @@ describe('test parseExcel function', () => {
backstageInfo: 'test backstage info',
};
// TODO: update tests once import is resolved
const expectedParsedRundown = [
{
timeStart: 25200000,
timeEnd: 28810000,
//timeStart: 28800000,
//timeEnd: 32410000,
title: 'Guest Welcome',
presenter: 'Carlos',
subtitle: 'Getting things started',
@@ -708,8 +664,8 @@ describe('test parseExcel function', () => {
type: 'event',
},
{
timeStart: 28800000,
timeEnd: 30600000,
//timeStart: 32400000,
//timeEnd: 34200000,
title: 'A song from the hearth',
presenter: 'Still Carlos',
subtitle: 'Derailing early',
@@ -728,13 +684,8 @@ describe('test parseExcel function', () => {
const parsedData = await parseExcel(testdata);
expect(parsedData.eventData).toStrictEqual(expectedParsedEvent);
expect(parsedData.rundown).toBeDefined();
expect(parsedData.rundown.title).toBe(expectedParsedRundown.title);
expect(parsedData.rundown.presenter).toBe(expectedParsedRundown.presenter);
expect(parsedData.rundown.subtitle).toBe(expectedParsedRundown.subtitle);
expect(parsedData.rundown.isPublic).toBe(expectedParsedRundown.isPublic);
expect(parsedData.rundown.skip).toBe(expectedParsedRundown.skip);
expect(parsedData.rundown.note).toBe(expectedParsedRundown.note);
expect(parsedData.rundown.type).toBe(expectedParsedRundown.type);
expect(parsedData.rundown[0]).toMatchObject(expectedParsedRundown[0]);
expect(parsedData.rundown[1]).toMatchObject(expectedParsedRundown[1]);
});
});
@@ -759,7 +710,6 @@ describe('test aliases import', () => {
expect(parsed.length).toBe(1);
// generates missing id
console.log(parsed);
expect(parsed[0].alias).toBeDefined();
});
});
@@ -874,7 +824,7 @@ describe('test views import', () => {
endMessage: '',
overrideStyles: false,
};
const parsed = parseViewSettings(testData);
const parsed = parseViewSettings(testData, false);
expect(parsed).toStrictEqual(expectedParsedViewSettings);
});
@@ -1,28 +0,0 @@
import { parseExcelDate } from '../time';
describe('parseExcelDate', () => {
it('parses a valid date string as expected from excel', () => {
const millis = parseExcelDate('1899-12-30T07:00:00.000Z');
expect(millis).not.toBe(0);
});
describe('parses a time string that passes validation', () => {
const validFields = ['10:00:00', '10:00'];
validFields.forEach((field) => {
it(`handles ${field}`, () => {
const millis = parseExcelDate(field);
expect(millis).not.toBe(0);
});
});
});
describe('returns 0 on other strings', () => {
const invalidFields = ['10', 'test', ''];
invalidFields.forEach((field) => {
it(`handles ${field}`, () => {
const millis = parseExcelDate(field);
expect(millis).toBe(0);
});
});
});
});
@@ -0,0 +1,58 @@
import { parseExcelDate } from '../time.js';
describe('parseExcelDate', () => {
describe.todo('parses a valid date string as expected from excel', () => {
const testCases = [
{
fromExcel: '1899-12-30T00:00:00.000Z',
expected: 3600000,
},
{
fromExcel: '1899-12-30T00:10:00.000Z',
expected: 4200000,
},
{
fromExcel: '1899-12-30T01:00:00.000Z',
expected: 7200000,
},
{
fromExcel: '1899-12-30T07:00:00.000Z',
expected: 28800000,
},
{
fromExcel: '1899-12-30T08:00:10.000Z',
expected: 32410000,
},
{
fromExcel: '1899-12-30T08:30:00.000Z',
expected: 34200000,
},
];
for (const scenario of testCases) {
it(`handles ${scenario.fromExcel}`, () => {
expect(parseExcelDate(scenario.fromExcel)).toBe(scenario.expected);
});
}
});
describe('parses a time string that passes validation', () => {
const validFields = ['10:00:00', '10:00'];
validFields.forEach((field) => {
it(`handles ${field}`, () => {
const millis = parseExcelDate(field);
expect(millis).not.toBe(0);
});
});
});
describe('returns 0 on other strings', () => {
const invalidFields = ['10', 'test', ''];
invalidFields.forEach((field) => {
it(`handles ${field}`, () => {
const millis = parseExcelDate(field);
expect(millis).toBe(0);
});
});
});
});
@@ -1,4 +1,4 @@
import { cleanURL } from '../url';
import { cleanURL } from '../url.js';
describe('url is correctly formatted', () => {
it('has no leading spaces', () => {
-25
View File
@@ -1,25 +0,0 @@
/**
* @description Returns id of previous played event
* @param {array} events
* @param {string} eventId
* @return {object}
*/
export function getPreviousPlayable(events, eventId) {
// find current index
const current = events.findIndex((event) => event.id === eventId);
if (current === -1) {
return { index: null, id: null };
}
let index = current - 1;
while (index >= 0) {
const event = events[index];
if (event.type === 'event' && !event.skip) {
return { index, id: event.id };
}
index--;
}
return { index: null, id: null };
}
+36 -13
View File
@@ -4,7 +4,16 @@
import fs from 'fs';
import xlsx from 'node-xlsx';
import { generateId, calculateDuration } from 'ontime-utils';
import { DatabaseModel, EventData, OntimeEvent, OntimeRundown, UserFields } from 'ontime-types';
import {
DatabaseModel,
EndAction,
EventData,
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';
@@ -38,6 +47,7 @@ export const parseExcel = async (excelData) => {
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;
@@ -91,6 +101,8 @@ export const parseExcel = async (excelData) => {
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) {
@@ -102,9 +114,17 @@ export const parseExcel = async (excelData) => {
} else if (j === notesIndex) {
event.note = column;
} else if (j === endActionIndex) {
event.endAction = column;
if (column === '') {
event.endAction = EndAction.None;
} else {
event.endAction = column;
}
} else if (j === timerTypeIndex) {
event.timerType = column;
if (column === '') {
event.timerType = TimerType.CountDown;
} else {
event.timerType = column;
}
} else if (j === colourIndex) {
event.colour = column;
} else if (j === user0Index) {
@@ -130,6 +150,7 @@ export const parseExcel = async (excelData) => {
} else {
if (typeof column === 'string') {
const col = column.toLowerCase();
// look for keywords
// need to make sure it is a string first
switch (col) {
@@ -157,6 +178,10 @@ export const parseExcel = async (excelData) => {
case 'finish':
timeEndIndex = j;
break;
case 'cue':
case 'page':
cueIndex = j;
break;
case 'event title':
case 'title':
titleIndex = j;
@@ -243,7 +268,7 @@ export const parseExcel = async (excelData) => {
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: 'event' });
rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent);
}
});
return {
@@ -284,7 +309,6 @@ export const parseJson = async (jsonData, enforce = false): Promise<DatabaseMode
// Import user fields if any
returnData.userFields = parseUserFields(jsonData);
// Import OSC settings if any
// @ts-expect-error -- we are unable to type just yet
returnData.osc = parseOsc(jsonData, enforce);
// Import HTTP settings if any
// returnData.http = parseHttp(jsonData, enforce);
@@ -295,12 +319,15 @@ export const parseJson = async (jsonData, enforce = false): Promise<DatabaseMode
/**
* @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) => {
export const validateEvent = (eventArgs: Partial<OntimeEvent>, 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
@@ -336,12 +363,9 @@ export const validateEvent = (eventArgs) => {
user7: makeString(e.user7, d.user7),
user8: makeString(e.user8, d.user8),
user9: makeString(e.user9, d.user9),
// deciding not to validate colour
// this adds flexibility to the user to write hex codes, rgb,
// but also colour names like blue and red
// CSS.supports is only available in frontend
colour: makeString(e.colour, d.colour),
id,
cue,
type: 'event',
};
}
@@ -357,7 +381,7 @@ type ResponseError = { error: true; message: string };
* @param {string} file - reference to file
* @return {object} - parse result message
*/
export const fileHandler = async (file): ResponseOK | ResponseError => {
export const fileHandler = async (file): Promise<ResponseOK | ResponseError> => {
let res: Partial<ResponseOK | ResponseError> = {};
// check which file type are we dealing with
@@ -376,8 +400,7 @@ export const fileHandler = async (file): ResponseOK | ResponseError => {
res.data.userFields = parseUserFields(dataFromExcel);
res.message = 'success';
} else {
const errorMessage = 'No sheet found named ontime or event schedule';
console.log(errorMessage);
const errorMessage = 'No sheet found named "ontime" or "event schedule"';
res = {
error: true,
message: errorMessage,
+11 -2
View File
@@ -30,6 +30,7 @@ export const parseRundown = (data): OntimeRundown => {
console.log('Found rundown definition, importing...');
const rundown = [];
try {
let eventIndex = 0;
const ids = [];
for (const e of data.rundown) {
// cap number of events
@@ -43,6 +44,7 @@ export const parseRundown = (data): OntimeRundown => {
console.log('ERROR: ID collision on import, skipping');
continue;
}
// validate the right endAction is used
if (e.endAction && !Object.values(EndAction).includes(e.endAction)) {
e.endAction = EndAction.None;
@@ -54,8 +56,10 @@ export const parseRundown = (data): OntimeRundown => {
e.timerType = TimerType.CountDown;
console.log('WARNING: invalid Timer Type provided, using default');
}
if (e.type === 'event') {
const event = validateEvent(e);
eventIndex += 1;
const event = validateEvent(e, eventIndex.toString());
if (event != null) {
rundown.push(event);
ids.push(event.id);
@@ -216,7 +220,12 @@ export const validateOscObject = (data: OscSubscription): boolean => {
/**
* Parse osc portion of an entry
*/
export const parseOsc = (data: { osc?: Partial<OSCSettings> }, enforce: boolean): Partial<OSCSettings> => {
export const parseOsc = (
data: {
osc?: Partial<OSCSettings>;
},
enforce: boolean,
): OSCSettings | Record<string, never> => {
if ('osc' in data) {
console.log('Found OSC definition, importing...');
-1
View File
@@ -1,5 +1,4 @@
import fs from 'fs';
import { dayInMs } from 'ontime-utils';
/**
* @description Ensures variable is string, it skips object types
+3 -2
View File
@@ -8,12 +8,13 @@ export const timeFormat = 'HH:mm';
export const timeFormatSeconds = 'HH:mm:ss';
/**
* @description Converts an excel date to milliseconds
* @argument {string} date - excel string date
* @description Converts a date object to milliseconds
* @argument {Date} date
* @returns {number} - time in milliseconds
*/
export const dateToMillis = (date: Date): number => {
// TODO: Use UTC
const h = date.getHours();
const m = date.getMinutes();
const s = date.getSeconds();