* refactor: cleanup routes

* style: smaller base font

* chore: upgrade dependencies

* chore: lock node version to electron

* refactor: pass HTTP to integration controller (#652)

* refactor: deprecate onair control

* refactor: remove playback router

* Several project files user folder (#617)

* chore: automated screenshots (#667)

* feat: app settings (#658)

* refactor: remove deprecated event data (#674)

* Studio clock (#663)

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>

* Feat: reorder events with alt+ctrl + arrow up/down (#645)

* Warning and danger per event (#677)

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* refactor: stabilise actionHandler (#683)

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* improvement: hide seconds (#675)

* wip: overview (#688)

* fix: focus cursor (#695)

* refactor: update lower third (#665)

* Refactor/time formatting (#696)

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Carlos Valente <carlosvalente@pm.me>

* feat: multiple selection (#703)

---------

Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com>
Co-authored-by: Alex <ac@omnivox.dk>

* fix: test - go to `Edit mode` befor tying to click `Event options` button (#708)

* refactor: runtime service (#715)

* fix: issue with loosing cursor position on message (#719)

* remove info panel (#721)

* Event editor continue (#722)

* update API - part  (#709)

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Carlos Valente <carlosvalente@pm.me>

* refactor: update timers (#729)

* feat: many timers (#706)

---------

Co-authored-by: arc-alex <ac@omnivox.dk>

* refactor: excel cleanup (#734)

* refactor: allow import of blocks and skip import (#735)

* Project manager (#697)

* refactor: UI for linking events (#763)

* upgraded pipeline actions (#777)

* Over under (#771)

* custom fields (#744)


---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>

* Sheets settings (#774)

---------

Co-authored-by: arc-alex <ac@omnivox.dk>

* style: tweaks to lower thirds (#785)

* refactor: delays account for gaps (#784)

* refactor: partial state updates (#780)

* feat: generate crash report (#787)

* Sheet use limited input device auth flow (#782)

---------

Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Carlos Valente <carlosvalente@pm.me>

* Custom fields views (#789)

* refactor: deprecate presenter and subtitle (#795)

* refactor: organise API around resources (#798)

---------

Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com>

* Time to end (#804)

* Skip fixes (#805)

* fix: onair derives from playback

* Param nav (#822)

---------

Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk>

* refactor: download files from interface (#831)

* Quick options (#814)

* End pause (#832)

* chore: bump node version in docker (#834)

* refactor: follow in run mode (#840)

* fix: uncaught error in http integration (#837)

* Apply project (#843)

Co-authored-by: Matteo Gheza <matteo.gheza07@gmail.com>
Co-authored-by: Ary <arylmoraesn@gmail.com>
Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk>
Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>
Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: Alex Rohleder <alexrohleder96@gmail.com>
Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com>
Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com>
Co-authored-by: Fabian Posenau <fabianpos99+github@gmail.com>
This commit is contained in:
Carlos Valente
2024-04-13 10:06:46 +02:00
committed by GitHub
parent debdbd1c5a
commit 55d1aca9b6
640 changed files with 26702 additions and 21623 deletions
+210 -279
View File
@@ -1,334 +1,265 @@
import { generateId } from 'ontime-utils';
import {
Alias,
OntimeRundown,
CustomFields,
DatabaseModel,
HttpSettings,
HttpSubscription,
OSCSettings,
OntimeBlock,
OntimeDelay,
OntimeEvent,
OntimeRundown,
OscSubscription,
ProjectData,
Settings,
TimerLifeCycle,
UserFields,
URLPreset,
ViewSettings,
OscSubscription,
HttpSubscription,
OscSubscriptionOptions,
HttpSubscriptionOptions,
isOntimeBlock,
isOntimeCycle,
isOntimeDelay,
isOntimeEvent,
} from 'ontime-types';
import { generateId } from 'ontime-utils';
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
import { dbModel } from '../models/dataModel.js';
import { validateEvent } from './parser.js';
import { MAX_EVENTS } from '../settings.js';
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
import { createEvent } from './parser.js';
/**
* Parse events array of an entry
* @param {object} data - data object
* @returns {object} - event object data
* Parse rundown array of an entry
*/
export const parseRundown = (data): OntimeRundown => {
let newRundown: OntimeRundown = [];
if ('rundown' in data) {
console.log('Found rundown definition, importing...');
const rundown = [];
try {
let eventIndex = 0;
const ids = [];
for (const e of data.rundown) {
// cap number of events
if (rundown.length >= MAX_EVENTS) {
console.log(`ERROR: Reached limit number of ${MAX_EVENTS} events`);
break;
}
// double check unique ids
if (ids.includes(e?.id)) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
if (e.type === 'event') {
eventIndex += 1;
const event = validateEvent(e, eventIndex.toString());
if (event != null) {
rundown.push(event);
ids.push(event.id);
}
} else if (e.type === 'delay') {
rundown.push({
...delayDef,
duration: e.duration,
id: e.id || generateId(),
});
} else if (e.type === 'block') {
rundown.push({ ...blockDef, title: e.title, id: e.id || generateId() });
} else {
console.log('ERROR: undefined event type, skipping');
}
}
} catch (error) {
console.log(`Error ${error}`);
}
// write to db
newRundown = rundown;
console.log(`Uploaded file with ${newRundown.length} entries`);
export const parseRundown = (data: Partial<DatabaseModel>): OntimeRundown => {
if (!data.rundown) {
return [];
}
return newRundown;
console.log('Found rundown, importing...');
const rundown: OntimeRundown = [];
let eventIndex = 0;
const ids: string[] = [];
for (const event of data.rundown) {
if (ids.includes(event.id)) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
const id = event.id || generateId();
let newEvent: OntimeEvent | OntimeDelay | OntimeBlock | null;
if (isOntimeEvent(event)) {
newEvent = createEvent(event, eventIndex.toString());
// skip if event is invalid
if (newEvent == null) {
continue;
}
eventIndex += 1;
} else if (isOntimeDelay(event)) {
newEvent = { ...delayDef, duration: event.duration, id };
} else if (isOntimeBlock(event)) {
newEvent = { ...blockDef, title: event.title, id };
} else {
console.log('ERROR: unknown event type, skipping');
continue;
}
if (newEvent) {
rundown.push(newEvent);
ids.push(id);
}
}
console.log(`Uploaded rundown with ${rundown.length} entries`);
return rundown;
};
/**
* Parse event portion of an entry
* @param {object} data - data object
* @returns {object} - event object data
*/
export const parseProject = (data): ProjectData => {
let newProjectData: Partial<ProjectData> = {};
// we are adding this here to aid transition, should be removed once enough time has past that users have fully migrated
// TODO: Remove eventually
if ('project' in data || 'eventData' in data) {
console.log('Found project data, importing...');
const project = data.project ?? data.eventData;
// filter known properties and write to db
newProjectData = {
...dbModel.project,
title: project.title || dbModel.project.title,
description: project.description || dbModel.project.description,
publicUrl: project.publicUrl || dbModel.project.publicUrl,
publicInfo: project.publicInfo || dbModel.project.publicInfo,
backstageUrl: project.backstageUrl || dbModel.project.backstageUrl,
backstageInfo: project.backstageInfo || dbModel.project.backstageInfo,
};
export const parseProject = (data: Partial<DatabaseModel>): ProjectData => {
if (!data.project) {
return { ...dbModel.project };
}
return newProjectData as ProjectData;
console.log('Found project data, importing...');
return {
title: data.project.title ?? dbModel.project.title,
description: data.project.description ?? dbModel.project.description,
publicUrl: data.project.publicUrl ?? dbModel.project.publicUrl,
publicInfo: data.project.publicInfo ?? dbModel.project.publicInfo,
backstageUrl: data.project.backstageUrl ?? dbModel.project.backstageUrl,
backstageInfo: data.project.backstageInfo ?? dbModel.project.backstageInfo,
};
};
/**
* Parse settings portion of an entry
* @param {object} data - data object
* @returns {object} - event object data
*/
export const parseSettings = (data): Settings => {
let newSettings: Partial<Settings> = {};
if ('settings' in data) {
console.log('Found settings definition, importing...');
const s = data.settings;
// skip if file definition is missing
if (s.app == null || s.version == null) {
console.log('ERROR: unknown app version, skipping');
} else {
const settings = {
version: dbModel.settings.version,
serverPort: s.serverPort || dbModel.settings.serverPort,
editorKey: s.editorKey || null,
operatorKey: s.operatorKey || null,
timeFormat: s.timeFormat || '24',
language: s.language || 'en',
};
// write to db
newSettings = {
...dbModel.settings,
...settings,
};
}
export const parseSettings = (data: Partial<DatabaseModel>): Settings => {
if (!data.settings) {
return { ...dbModel.settings };
}
return newSettings as Settings;
// skip if file definition is missing
if (data.settings?.app !== 'ontime' || data.settings?.version == null) {
throw new Error('ERROR: unable to parse settings, missing app or version');
}
console.log('Found settings, importing...');
return {
app: dbModel.settings.app,
version: dbModel.settings.version,
serverPort: data.settings.serverPort ?? dbModel.settings.serverPort,
editorKey: data.settings.editorKey ?? null,
operatorKey: data.settings.operatorKey ?? null,
timeFormat: data.settings.timeFormat ?? '24',
language: data.settings.language ?? 'en',
};
};
/**
* Parse settings portion of an entry
* @param {object} data - data object
* @returns {object} - event object data
* Parse view settings portion of an entry
*/
export const parseViewSettings = (data): ViewSettings => {
let newViews: Partial<ViewSettings> = {};
if ('viewSettings' in data) {
console.log('Found view definition, importing...');
const v = data.viewSettings;
const viewSettings = {
overrideStyles: v.overrideStyles ?? dbModel.viewSettings.overrideStyles,
normalColor: v.normalColor ?? dbModel.viewSettings.normalColor,
warningColor: v.warningColor ?? dbModel.viewSettings.warningColor,
warningThreshold: v.warningThreshold ?? dbModel.viewSettings.warningThreshold,
dangerColor: v.dangerColor ?? dbModel.viewSettings.dangerColor,
dangerThreshold: v.dangerThreshold ?? dbModel.viewSettings.dangerThreshold,
endMessage: v.endMessage ?? dbModel.viewSettings.endMessage,
};
newViews = { ...viewSettings };
export const parseViewSettings = (data: Partial<DatabaseModel>): ViewSettings => {
if (!data.viewSettings) {
return { ...dbModel.viewSettings };
}
return newViews as ViewSettings;
console.log('Found view settings, importing...');
return {
dangerColor: data.viewSettings.dangerColor ?? dbModel.viewSettings.dangerColor,
endMessage: data.viewSettings.endMessage ?? dbModel.viewSettings.endMessage,
freezeEnd: data.viewSettings.freezeEnd ?? dbModel.viewSettings.freezeEnd,
normalColor: data.viewSettings.normalColor ?? dbModel.viewSettings.normalColor,
overrideStyles: data.viewSettings.overrideStyles ?? dbModel.viewSettings.overrideStyles,
warningColor: data.viewSettings.warningColor ?? dbModel.viewSettings.warningColor,
};
};
/**
* Parses and validates OSC subscription cycle options
* @param data
* Sanitises an OSC Subscriptions array
*/
export const validateOscSubscriptionCycle = (data: OscSubscriptionOptions[]): boolean => {
for (const subscriptionOption of data) {
if (typeof subscriptionOption.message !== 'string' || typeof subscriptionOption.enabled !== 'boolean') {
return false;
}
}
return true;
};
/**
* Parses and validates OSC subscription object
* @param data
*/
export const validateOscSubscriptionObject = (data: OscSubscription): boolean => {
if (!data) {
return false;
export function sanitiseOscSubscriptions(subscriptions?: OscSubscription[]): OscSubscription[] {
if (!Array.isArray(subscriptions)) {
return [];
}
const timerKeys = Object.keys(TimerLifeCycle);
for (const key of timerKeys) {
// must contains all keys and be an array
if (!(key in data) || !Array.isArray(data[key])) {
return false;
}
const isValid = validateOscSubscriptionCycle(data[key]);
if (!isValid) {
return false;
}
}
return true;
};
return subscriptions.filter(
({ id, cycle, address, payload, enabled }) =>
typeof id === 'string' &&
isOntimeCycle(cycle) &&
typeof address === 'string' &&
typeof payload === 'string' &&
typeof enabled === 'boolean',
);
}
/**
* Parse osc portion of an entry
*/
export const parseOsc = (data: { osc?: Partial<OSCSettings> }): OSCSettings => {
if ('osc' in data) {
console.log('Found OSC definition, importing...');
// TODO: this can be improved by only merging known keys
const loadedConfig = data.osc || {};
const validatedSubscriptions = validateOscSubscriptionObject(loadedConfig.subscriptions)
? loadedConfig.subscriptions
: dbModel.osc.subscriptions;
return {
portIn: loadedConfig.portIn ?? dbModel.osc.portIn,
portOut: loadedConfig.portOut ?? dbModel.osc.portOut,
targetIP: loadedConfig.targetIP ?? dbModel.osc.targetIP,
enabledIn: loadedConfig.enabledIn ?? dbModel.osc.enabledIn,
enabledOut: loadedConfig.enabledOut ?? dbModel.osc.enabledOut,
subscriptions: validatedSubscriptions,
};
export const parseOsc = (data: Partial<DatabaseModel>): OSCSettings => {
if (!data.osc) {
return { ...dbModel.osc };
}
console.log('Found OSC settings, importing...');
return {
portIn: data.osc.portIn ?? dbModel.osc.portIn,
portOut: data.osc.portOut ?? dbModel.osc.portOut,
targetIP: data.osc.targetIP ?? dbModel.osc.targetIP,
enabledIn: data.osc.enabledIn ?? dbModel.osc.enabledIn,
enabledOut: data.osc.enabledOut ?? dbModel.osc.enabledOut,
subscriptions: sanitiseOscSubscriptions(data.osc.subscriptions),
};
};
/**
* Parses and validates HTTP subscription cycle options
* @param data
* Sanitises an HTTP Subscriptions array
*/
export const validateHttpSubscriptionCycle = (data: HttpSubscriptionOptions[]): boolean => {
for (const subscriptionOption of data) {
const isHttp = subscriptionOption.message?.startsWith('http://');
if (typeof subscriptionOption.message !== 'string' || !isHttp || typeof subscriptionOption.enabled !== 'boolean') {
return false;
}
export function sanitiseHttpSubscriptions(subscriptions?: HttpSubscription[]): HttpSubscription[] {
if (!Array.isArray(subscriptions)) {
return [];
}
return true;
};
/**
* Parses and validates HTTP subscription object
* @param data
*/
export const validateHttpSubscriptionObject = (data: HttpSubscription): boolean => {
if (!data) {
return false;
}
const timerKeys = Object.keys(TimerLifeCycle);
// must contains all keys and be an array
for (const key of timerKeys) {
if (!(key in data) || !Array.isArray(data[key])) {
return false;
}
const isValid = validateHttpSubscriptionCycle(data[key]);
if (!isValid) {
return false;
}
}
return true;
};
return subscriptions.filter(
({ id, cycle, message, enabled }) =>
typeof id === 'string' &&
isOntimeCycle(cycle) &&
typeof message === 'string' &&
message.startsWith('http://') &&
typeof enabled === 'boolean',
);
}
/**
* Parse Http portion of an entry
* @param {object} data - data object
* @param {boolean} enforce - whether to create a definition if one is missing
* @returns {object} - event object data
*/
export const parseHttp = (data: { http?: Partial<HttpSettings> }): HttpSettings => {
if ('http' in data) {
console.log('Found HTTP definition, importing...');
export const parseHttp = (data: Partial<DatabaseModel>): HttpSettings => {
if (!data.http) {
return { ...dbModel.http };
}
// TODO: this can be improved by only merging known keys
const loadedConfig = data?.http || {};
const validatedSubscriptions = validateHttpSubscriptionObject(loadedConfig.subscriptions)
? loadedConfig.subscriptions
: dbModel.http.subscriptions;
console.log('Found HTTP settings, importing...');
return {
enabledOut: loadedConfig.enabledOut ?? dbModel.http.enabledOut,
subscriptions: validatedSubscriptions,
return {
enabledOut: data.http.enabledOut ?? dbModel.http.enabledOut,
subscriptions: sanitiseHttpSubscriptions(data.http.subscriptions),
};
};
/**
* Parse URL preset portion of an entry
*/
export const parseUrlPresets = (data: Partial<DatabaseModel>): URLPreset[] => {
if (!data.urlPresets) {
return [];
}
console.log('Found URL presets, importing...');
const newPresets: URLPreset[] = [];
for (const preset of data.urlPresets) {
const newPreset = {
enabled: preset.enabled ?? false,
alias: preset.alias ?? '',
pathAndParams: preset.pathAndParams ?? '',
};
newPresets.push(newPreset);
}
console.log(`Uploaded ${newPresets.length} preset(s)`);
return newPresets;
};
/**
* Parse customFields entry
*/
export const parseCustomFields = (data: Partial<DatabaseModel>): CustomFields => {
if (typeof data.customFields !== 'object') {
return { ...dbModel.customFields };
}
console.log('Found Custom Fields, importing...');
const newCustomFields: CustomFields = {};
for (const fieldLabel in data.customFields) {
const field = data.customFields[fieldLabel];
if (!field.label || !field.type || !field.colour) {
console.log('ERROR: missing required field, skipping');
continue;
}
newCustomFields[field.label] = {
type: field.type,
colour: field.colour,
label: field.label,
};
}
return newCustomFields;
};
/**
* Parse aliases portion of an entry
* @param {object} data - data object
* @returns {object} - event object data
*/
export const parseAliases = (data): Alias[] => {
const newAliases: Alias[] = [];
if ('aliases' in data) {
console.log('Found Aliases definition, importing...');
try {
for (const a of data.aliases) {
const newAlias = {
enabled: a.enabled || false,
alias: a.alias || '',
pathAndParams: a.pathAndParams || '',
};
newAliases.push(newAlias);
}
console.log(`Uploaded ${newAliases?.length || 0} alias(es)`);
} catch (error) {
console.log(`Error: ${error}`);
}
}
return newAliases;
};
/**
* Parse userFields entry
* @param {object} data - data object
* @returns {object} - event object data
*/
export const parseUserFields = (data): UserFields => {
const newUserFields: UserFields = { ...dbModel.userFields };
if ('userFields' in data) {
console.log('Found User Fields definition, importing...');
// we will only be importing the fields we know, so look for that
try {
let fieldsFound = 0;
for (const n in newUserFields) {
if (n in data.userFields) {
fieldsFound++;
newUserFields[n] = data.userFields[n];
}
}
console.log(`Uploaded ${fieldsFound} user fields`);
} catch (error) {
console.log(`Error: ${error}`);
}
}
return { ...newUserFields };
};