* 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
@@ -1,23 +1,22 @@
import got from 'got';
import { HttpSettings, HttpSubscription, HttpSubscriptionOptions, LogOrigin } from 'ontime-types';
import { HttpSettings, HttpSubscription, LogOrigin } from 'ontime-types';
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
import { parseTemplateNested } from './integrationUtils.js';
import { dbModel } from '../../models/dataModel.js';
import { logger } from '../../classes/Logger.js';
import { validateHttpSubscriptionObject } from '../../utils/parserFunctions.js';
type Action = TimerLifeCycleKey | string;
/**
* @description Class contains logic towards outgoing HTTP communications
* @class
*/
export class HttpIntegration implements IIntegration<HttpSubscriptionOptions> {
subscriptions: HttpSubscription;
export class HttpIntegration implements IIntegration<HttpSubscription, HttpSettings> {
subscriptions: HttpSubscription[];
enabled: boolean;
constructor() {
this.subscriptions = dbModel.http.subscriptions;
this.subscriptions = [];
this.enabled = false;
}
/**
@@ -25,75 +24,40 @@ export class HttpIntegration implements IIntegration<HttpSubscriptionOptions> {
*/
init(config: HttpSettings) {
const { subscriptions, enabledOut } = config;
if (!enabledOut) {
return {
success: false,
message: 'HTTP output disabled',
};
}
this.initSubscriptions(subscriptions);
try {
return {
success: true,
message: `HTTP integration client ready`,
};
} catch (error) {
return {
success: false,
message: `Failed initialising HTTP integration: ${error}`,
};
}
this.enabled = enabledOut;
}
initSubscriptions(subscriptionOptions: HttpSubscription) {
if (validateHttpSubscriptionObject(subscriptionOptions)) {
this.subscriptions = { ...subscriptionOptions };
}
initSubscriptions(subscriptions: HttpSubscription[]) {
this.subscriptions = subscriptions;
}
dispatch(action: Action, state?: object) {
if (!action) {
return {
success: false,
message: 'HTTP called with no action',
};
dispatch(action: TimerLifeCycleKey, state?: object) {
// noop
if (!this.enabled) {
return;
}
// check subscriptions for action
const eventSubscriptions = this.subscriptions?.[action] || [];
eventSubscriptions.forEach((sub) => {
const { enabled, message } = sub;
if (enabled && message) {
const parsedMessage = parseTemplateNested(message, state || {});
try {
const parsedUrl = new URL(parsedMessage);
this.emit(parsedUrl);
} catch (err) {
logger.error(LogOrigin.Tx, `HTTP Integration: ${err}`);
return {
success: false,
message: `${err}`,
};
}
for (let i = 0; i < this.subscriptions.length; i++) {
const { cycle, message, enabled } = this.subscriptions[i];
if (cycle !== action || !enabled || !message) {
continue;
}
const parsedMessage = parseTemplateNested(message, state || {});
this.emit(parsedMessage);
}
}
emit(path: string) {
got.get(path, { retry: { limit: 0 } }).catch((err) => {
logger.error(LogOrigin.Tx, `HTTP Integration: ${err.code}`);
});
}
async emit(path: URL) {
try {
await got.get(path, {
retry: { limit: 0 },
});
} catch (err) {
logger.error(LogOrigin.Tx, `HTTP integration: ${err}`);
}
shutdown() {
/** shutdown is a no-op here*/
}
shutdown() {}
}
export const httpIntegration = new HttpIntegration();
@@ -1,24 +1,11 @@
import { TimerLifeCycle, Subscription } from 'ontime-types';
import { TimerLifeCycle } from 'ontime-types';
export type TimerLifeCycleKey = keyof typeof TimerLifeCycle;
export default interface IIntegration<T> {
subscriptions: Subscription<T>;
init: (config: unknown) => OperationReturn;
dispatch: (action: TimerLifeCycleKey, state?: object) => OperationReturn;
emit: (...args: unknown[]) => unknown;
export default interface IIntegration<T, C> {
subscriptions: T[];
init: (config: C) => void;
dispatch: (action: TimerLifeCycleKey, state?: object) => void;
emit: (...args: never[]) => unknown;
shutdown: () => void;
}
// either went well, or explain what failed
type OperationReturn = ReturnOnSuccess | ReturnOnError;
type ReturnOnSuccess = {
success: true;
message?: string;
};
type ReturnOnError = {
success: false;
message: string;
};
@@ -1,18 +1,21 @@
import { LogOrigin } from 'ontime-types';
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
import { eventStore } from '../../stores/EventStore.js';
import { logger } from '../../classes/Logger.js';
class IntegrationService {
private integrations: IIntegration<unknown>[];
private integrations: IIntegration<unknown, unknown>[];
constructor() {
this.integrations = [];
}
register(integrationService: IIntegration<unknown>) {
register(integrationService: IIntegration<unknown, unknown>) {
this.integrations.push(integrationService);
}
unregister(integrationService: IIntegration<unknown>) {
unregister(integrationService: IIntegration<unknown, unknown>) {
this.integrations = this.integrations.filter((int) => int !== integrationService);
}
@@ -24,7 +27,7 @@ class IntegrationService {
}
shutdown() {
console.log('Shutdown integrations');
logger.info(LogOrigin.Tx, 'Shutdown Integrations');
this.integrations.forEach((integration) => {
integration.shutdown();
});
@@ -1,133 +1,144 @@
import { ArgumentType, Client, Message } from 'node-osc';
import { OSCSettings, OscSubscription, OscSubscriptionOptions } from 'ontime-types';
import { LogOrigin, MaybeNumber, MaybeString, OSCSettings, OscSubscription } from 'ontime-types';
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
import { parseTemplateNested } from './integrationUtils.js';
import { isObject } from '../../utils/varUtils.js';
import { dbModel } from '../../models/dataModel.js';
import { validateOscSubscriptionObject } from '../../utils/parserFunctions.js';
type Action = TimerLifeCycleKey | string;
import { logger } from '../../classes/Logger.js';
import { OscServer } from '../../adapters/OscAdapter.js';
/**
* @description Class contains logic towards outgoing OSC communications
* @class
*/
export class OscIntegration implements IIntegration<OscSubscriptionOptions> {
export class OscIntegration implements IIntegration<OscSubscription, OSCSettings> {
protected oscClient: null | Client;
subscriptions: OscSubscription;
protected oscServer: OscServer | null = null;
subscriptions: OscSubscription[];
targetIP: MaybeString;
portOut: MaybeNumber;
portIn: MaybeNumber;
enabledOut: boolean;
enabledIn: boolean;
constructor() {
this.oscClient = null;
this.subscriptions = dbModel.osc.subscriptions;
this.subscriptions = [];
this.targetIP = null;
this.portOut = null;
this.portIn = null;
this.enabledOut = false;
this.enabledIn = false;
}
/**
* Initializes oscClient
*/
init(config: OSCSettings) {
const { targetIP, portOut, subscriptions, enabledOut } = config;
const { targetIP, portOut, subscriptions, enabledOut, enabledIn, portIn } = config;
if (!enabledOut) {
this.oscClient?.close();
return {
success: false,
message: 'OSC output disabled',
};
this.initTX(enabledOut, targetIP, portOut, subscriptions);
this.initRX(enabledIn, portIn);
// return `OSC integration client connected to ${targetIP}:${portOut}`;
}
private initSubscriptions(subscriptions: OscSubscription[]) {
this.subscriptions = subscriptions;
}
dispatch(action: TimerLifeCycleKey, state?: object) {
// noop
if (!this.oscClient) {
return;
}
for (let i = 0; i < this.subscriptions.length; i++) {
const { cycle, address, payload, enabled } = this.subscriptions[i];
if (cycle !== action || !enabled || !address) {
continue;
}
const parsedAddress = parseTemplateNested(address, state || {});
const parsedPayload = payload ? parseTemplateNested(payload, state || {}) : undefined;
try {
this.emit(parsedAddress, parsedPayload);
} catch (error) {
logger.error(LogOrigin.Tx, `OSC Integration: ${error}`);
}
}
}
emit(address: string, payload?: ArgumentType) {
if (!this.oscClient) {
return;
}
const message = new Message(address);
if (payload) {
if (isObject(payload)) {
message.append(JSON.stringify(payload));
} else {
message.append(payload);
}
}
this.oscClient.send(message);
}
private initTX(enabledOut: boolean, targetIP: string, portOut: number, subscriptions: OscSubscription[]) {
this.initSubscriptions(subscriptions);
// runtime validation
const validateType = typeof targetIP !== 'string' || typeof portOut !== 'number';
const validateNull = !targetIP || !portOut;
if (validateType || validateNull) {
return {
success: false,
message: 'Config options incorrect',
};
if (!enabledOut && this.enabledOut) {
this.targetIP = targetIP;
this.portOut = portOut;
this.enabledOut = enabledOut;
this.shutdownTX();
return;
}
if (this.oscClient && targetIP === this.targetIP && portOut === this.portOut) {
// nothing changed that would mean we need a new client
return;
}
this.targetIP = targetIP;
this.portOut = portOut;
this.enabledOut = enabledOut;
try {
// this allows re-calling the init function during runtime
this.oscClient?.close();
this.oscClient = new Client(targetIP, portOut);
return {
success: true,
message: `OSC integration client connected to ${targetIP}:${portOut}`,
};
} catch (error) {
this.oscClient = null;
return {
success: false,
message: `Failed initialising OSC Client: ${error}`,
};
throw new Error(`Failed initialising OSC client: ${error}`);
}
}
initSubscriptions(subscriptionOptions: OscSubscription) {
if (validateOscSubscriptionObject(subscriptionOptions)) {
this.subscriptions = { ...subscriptionOptions };
}
}
dispatch(action: Action, state?: object) {
if (!this.oscClient) {
return {
success: false,
message: 'Client not initialised',
};
private initRX(enabledIn: boolean, portIn: number) {
if (!enabledIn && this.enabledIn) {
this.shutdownRX();
return;
}
if (!action) {
return {
success: false,
message: 'OSC called with no action',
};
}
// check subscriptions for action
const eventSubscriptions = this.subscriptions?.[action] || [];
eventSubscriptions.forEach((sub) => {
const { enabled, message } = sub;
if (enabled && message) {
const parsedMessage = parseTemplateNested(message, state || {});
this.emit(parsedMessage);
}
});
}
emit(path: string, payload?: ArgumentType) {
const message = new Message(path);
if (payload) {
try {
if (isObject(payload)) {
message.append(JSON.stringify(payload));
} else {
message.append(payload);
}
} catch (error) {
console.log('OSC ERROR', error, payload);
}
}
this.oscClient.send(message, (error) => {
if (error) {
return {
success: false,
message: `Error sending message: ${JSON.stringify(error)}`,
};
}
return {
success: true,
message: 'OSC Message sent',
};
});
// Start OSC Server
logger.info(LogOrigin.Rx, `Starting OSC Server on port: ${portIn}`);
this.oscServer = new OscServer(portIn);
}
shutdown() {
console.log('Shutting down OSC integration');
this.shutdownTX();
this.shutdownRX();
}
private shutdownTX() {
logger.info(LogOrigin.Rx, 'Shutting down OSC integration');
if (this.oscServer) {
this.oscServer?.shutdown();
this.oscServer = null;
}
}
private shutdownRX() {
logger.info(LogOrigin.Tx, 'Shutting down OSC integration');
if (this.oscClient) {
this.oscClient?.close();
this.oscClient = null;
@@ -1,10 +1,11 @@
// any value inside double curly braces {{val}}
import { formatDisplay } from 'ontime-utils';
import { MaybeNumber } from 'ontime-types';
import { millisToString, removeLeadingZero } from 'ontime-utils';
// any value inside double curly braces {{val}}
const placeholderRegex = /{{(.*?)}}/g;
function formatDisplayFromString(value: string, hideZero = false): string {
let valueInNumber = null;
let valueInNumber: MaybeNumber = null;
if (value !== 'null') {
const parsedValue = Number(value);
@@ -12,12 +13,16 @@ function formatDisplayFromString(value: string, hideZero = false): string {
valueInNumber = parsedValue;
}
}
return formatDisplay(valueInNumber, hideZero);
let formatted = millisToString(valueInNumber, { fallback: hideZero ? '00:00' : '00:00:00' });
if (hideZero) {
formatted = removeLeadingZero(formatted);
}
return formatted;
}
type AliasesDefinition = Record<string, { key: string; cb: (value: unknown) => string }>;
type AliasesDefinition = Record<string, { key: string; cb: (value: string) => string }>;
const quickAliases: AliasesDefinition = {
clock: { key: 'timer.clock', cb: (value: string) => formatDisplayFromString(value) },
clock: { key: 'clock', cb: (value: string) => formatDisplayFromString(value) },
duration: { key: 'timer.duration', cb: (value: string) => formatDisplayFromString(value, true) },
expectedEnd: {
key: 'timer.expectedFinish',
@@ -44,7 +49,7 @@ export function parseTemplateNested(template: string, state: object, humanReadab
for (const match of matches) {
const variableName = match[1];
const variableParts = variableName.split('.');
let value = undefined;
let value: string | undefined = undefined;
if (variableParts[0] === 'human') {
const lookupKey = variableParts[1];
@@ -57,9 +62,9 @@ export function parseTemplateNested(template: string, state: object, humanReadab
}
} else {
// iterate through variable parts, and look for the property in the state object
value = variableParts.reduce((obj, key) => obj && obj[key], state);
value = variableParts.reduce((obj, key) => obj?.[key], state);
}
if (typeof value !== 'undefined') {
if (value !== undefined) {
parsedTemplate = parsedTemplate.replace(match[0], value);
}
}