Merge branch 'master' into feat/http-integration

This commit is contained in:
arc-alex
2023-11-19 21:05:31 +01:00
162 changed files with 5342 additions and 3259 deletions
-5
View File
@@ -3,11 +3,6 @@
"browser": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
+6 -5
View File
@@ -6,15 +6,16 @@
"private": true,
"description": "shared typings for ontime",
"scripts": {
"cleanup": "rm -rf .turbo && rm -rf node_modules"
"cleanup": "rm -rf .turbo && rm -rf node_modules",
"lint": "eslint . --quiet"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"eslint": "^8.31.0",
"typescript": "^4.9.4"
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"eslint": "^8.53.0",
"typescript": "^5.2.2"
}
}
@@ -0,0 +1,14 @@
import { OSCSettings } from '../../definitions/core/OscSettings.type.js';
export type NetworkInterface = {
name: string;
address: string;
};
export interface GetInfo {
networkInterfaces: NetworkInterface[];
version: string;
serverPort: number;
osc: OSCSettings;
cssOverride: string;
}
@@ -0,0 +1,6 @@
import { OntimeRundown } from '../../definitions/core/Rundown.type.js';
export interface GetRundownCached {
rundown: OntimeRundown;
revision: number;
}
@@ -2,4 +2,4 @@ export type Alias = {
enabled: boolean;
alias: string;
pathAndParams: string;
}
};
@@ -1,8 +1,8 @@
import { TimeFormat } from './TimeFormat.type';
import { TimeFormat } from './TimeFormat.type.js';
export type Settings = {
app: 'ontime';
version: 2;
version: string;
serverPort: number;
editorKey: null | string;
operatorKey: null | string;
@@ -9,5 +9,4 @@ export type UserFields = {
user7: string;
user8: string;
user9: string;
}
};
@@ -6,4 +6,4 @@ export type ViewSettings = {
warningThreshold: number;
dangerColor: string;
dangerThreshold: number;
}
};
@@ -23,5 +23,5 @@ export enum LogOrigin {
Rx = 'RX',
Server = 'SERVER',
Tx = 'TX',
User = 'USER'
User = 'USER',
}
@@ -6,4 +6,4 @@ export type Message = {
export type TimerMessage = Message & {
timerBlink: boolean;
timerBlackout: boolean;
}
};
+4
View File
@@ -35,6 +35,10 @@ export type { OSCSettings } from './definitions/core/OscSettings.type.js';
// ---> HTTP
export type { HTTPSettings } from './definitions/core/HttpSettings.type.js';
// SERVER RESPONSES
export type { NetworkInterface, GetInfo } from './api/ontime-controller/BackendResponse.type.js';
export type { GetRundownCached } from './api/rundown-controller/BackendResponse.type.js';
// SERVER RUNTIME
export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';
export { Playback } from './definitions/runtime/Playback.type.js';
+1 -1
View File
@@ -1,7 +1,7 @@
import { OntimeRundownEntry } from '../definitions/core/Rundown.type.js';
import { OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent } from '../definitions/core/OntimeEvent.type.js';
type MaybeEvent = OntimeRundownEntry | null | undefined;
type MaybeEvent = Partial<OntimeRundownEntry> | null | undefined;
export function isOntimeEvent(event: MaybeEvent): event is OntimeEvent {
return event?.type === SupportedEvent.Event;
+1 -14
View File
@@ -3,27 +3,14 @@
"browser": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"plugins": [
"simple-import-sort",
"prettier"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
"simple-import-sort"
],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
],
"simple-import-sort/exports": "error",
"simple-import-sort/imports": "error"
}
+18 -1
View File
@@ -1,12 +1,13 @@
// runtime utils
export { getFirst, getFirstEvent, getLastEvent, getNext, getPrevious } from './src/rundown-utils/rundownUtils.js';
export { validatePlayback } from './src/validate-action/validatePlayback.js';
export { validateTimes } from './src/validate-events/validateEvent.js';
export { calculateDuration } from './src/validate-events/validateEvent.js';
// rundown utils
export { sanitiseCue } from './src/cue-utils/cueUtils.js';
export { getCueCandidate } from './src/cue-utils/cueUtils.js';
export { generateId } from './src/generate-id/generateId.js';
export { calculateDuration } from './src/rundown-utils/rundownUtils.js';
export { swapOntimeEvents } from './src/rundown-utils/rundownUtils.js';
// format utils
@@ -18,5 +19,21 @@ export { millisToString } from './src/date-utils/millisToString.js';
// time utils
export { dayInMs, mts } from './src/timeConstants.js';
// helpers from externals
export { deepmerge } from './src/externals/deepmerge.js';
// generic utilities
export { isNumeric } from './src/types/types.js';
// model validation
export { validateEndAction, validateTimerType } from './src/validate-events/validateEvent.js';
// feature business logic
// feature business logic - excel import
export {
type ExcelImportMap,
type ExcelImportOptions,
defaultExcelImportMap,
isExcelImportMap,
} from './src/feature/excel-import/excelImport.js';
+9 -8
View File
@@ -5,26 +5,27 @@
"private": true,
"description": "shared logic for ontime",
"scripts": {
"lint": "eslint .",
"lint": "eslint . --quiet",
"test": "vitest",
"test:pipeline": "vitest run",
"cleanup": "rm -rf .turbo && rm -rf node_modules"
},
"dependencies": {
"deepmerge-ts": "^5.1.0",
"luxon": "^3.3.0",
"nanoid": "^4.0.1"
},
"devDependencies": {
"@types/luxon": "^3.2.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-simple-import-sort": "^8.0.0",
"ontime-types": "workspace:*",
"prettier": "^2.8.3",
"typescript": "^4.9.4",
"prettier": "^3.0.3",
"typescript": "^5.2.2",
"vitest": "^0.30.1"
}
}
+1
View File
@@ -17,6 +17,7 @@ export function getIncrement(input: string): string {
const match = regex.exec(input);
if (match) {
// If a number is found, extract the non-numeric prefix, integer part, and decimal part
// eslint-disable-next-line prefer-const -- some items in the destructuring are modified
let [, prefix, integerPart, decimalPart] = match;
if (decimalPart) {
+2
View File
@@ -0,0 +1,2 @@
// evaluating the library, we re-export to make it easy to detach
export { deepmerge } from 'deepmerge-ts';
@@ -0,0 +1,44 @@
export type ExcelImportOptions = keyof typeof defaultExcelImportMap;
export type ExcelImportMap = typeof defaultExcelImportMap;
export const defaultExcelImportMap = {
worksheet: 'event schedule',
projectName: 'project name',
projectDescription: 'project description',
publicUrl: 'public url',
publicInfo: 'public info',
backstageUrl: 'backstage url',
backstageInfo: 'backstage info',
timeStart: 'time start',
timeEnd: 'time end',
duration: 'duration',
cue: 'cue',
title: 'title',
presenter: 'presenter',
subtitle: 'subtitle',
isPublic: 'public',
skip: 'skip',
note: 'notes',
colour: 'colour',
endAction: 'end action',
timerType: 'timer type',
user0: 'user0',
user1: 'user1',
user2: 'user2',
user3: 'user3',
user4: 'user4',
user5: 'user5',
user6: 'user6',
user7: 'user7',
user8: 'user8',
user9: 'user9',
};
export function isExcelImportMap(obj: unknown): obj is ExcelImportMap {
if (typeof obj !== 'object' || obj === null) {
return false;
}
const keys = Object.keys(obj);
return keys.every((key) => Object.hasOwn(defaultExcelImportMap, key));
}
@@ -1,8 +1,6 @@
import { OntimeRundown, SupportedEvent } from 'ontime-types';
import { dayInMs } from '../timeConstants.js';
import { getNextEvent, getPreviousEvent } from './rundownUtils';
import { calculateDuration } from './rundownUtils.js';
describe('getNextEvent()', () => {
it('returns the next event of type event', () => {
@@ -71,25 +69,3 @@ describe('getPreviousEvent()', () => {
expect(previous).toBe(null);
});
});
describe('calculateDuration()', () => {
describe('Given start and end values', () => {
it('is the difference between end and start', () => {
const duration = calculateDuration(10, 20);
expect(duration).toBe(10);
});
});
describe('Handles edge cases', () => {
it('handles events that go over midnight', () => {
const duration = calculateDuration(51, 50);
expect(duration).toBe(dayInMs - 1);
});
it('handles no difference', () => {
const duration1 = calculateDuration(0, 0);
const duration2 = calculateDuration(dayInMs, dayInMs);
expect(duration1).toBe(0);
expect(duration2).toBe(0);
});
});
});
@@ -1,7 +1,5 @@
import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
import { dayInMs } from '../timeConstants.js';
/**
* Gets first event in rundown, if it exists
* @param {OntimeRundownEntry[]} rundown
@@ -113,20 +111,6 @@ export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: strin
return null;
}
/**
* @description calculates event duration considering midnight
* @param {number} timeStart
* @param {number} timeEnd
* @returns {number}
*/
export const calculateDuration = (timeStart: number, timeEnd: number): number => {
// Durations must be positive
if (timeEnd < timeStart) {
return timeEnd + dayInMs - timeStart;
}
return timeEnd - timeStart;
};
/**
* @description swaps two OntimeEvents in the rundown
* @param {OntimeRundown} rundown
+1 -1
View File
@@ -1,4 +1,4 @@
export function isNumeric(num: any) {
export function isNumeric(num: unknown) {
if (typeof num === 'number' && !isNaN(num)) {
return true;
}
@@ -0,0 +1,118 @@
import { EndAction, TimerType } from 'ontime-types';
import { expect } from 'vitest';
import { dayInMs } from '../timeConstants.js';
import { calculateDuration, validateEndAction, validateTimerType, validateTimes } from './validateEvent.js';
describe('validateEndAction()', () => {
it('recognises a string representation of an action', () => {
const endAction = validateEndAction('load-next');
expect(endAction).toBe(EndAction.LoadNext);
});
it('returns fallback otherwise', () => {
const emptyAction = validateEndAction('', EndAction.Stop);
const invalidAction = validateEndAction('this-does-not-exist', EndAction.PlayNext);
expect(emptyAction).toBe(EndAction.Stop);
expect(invalidAction).toBe(EndAction.PlayNext);
});
});
describe('validateTimerType()', () => {
it('recognises a string representation of an action', () => {
const timerType = validateTimerType('time-to-end');
expect(timerType).toBe(TimerType.TimeToEnd);
});
it('returns fallback otherwise', () => {
const emptyType = validateTimerType('', TimerType.Clock);
const invalidType = validateTimerType('this-does-not-exist', TimerType.CountDown);
expect(emptyType).toBe(TimerType.Clock);
expect(invalidType).toBe(TimerType.CountDown);
});
});
describe('validateTimes()', () => {
it('passes through a well defined time list', () => {
const { timeStart, timeEnd, duration } = validateTimes(5, 10, 5);
expect(timeStart).toBe(5);
expect(timeEnd).toBe(10);
expect(duration).toBe(5);
});
it('handles cases when no times are given', () => {
const { timeStart, timeEnd, duration } = validateTimes(null, undefined, null);
expect(timeStart).toBe(0);
expect(timeEnd).toBe(0);
expect(duration).toBe(0);
});
it('calculates duration', () => {
const { timeStart, timeEnd, duration } = validateTimes(5, 10);
expect(timeStart).toBe(5);
expect(timeEnd).toBe(10);
expect(duration).toBe(5);
});
it('calculates end time', () => {
const { timeStart, timeEnd, duration } = validateTimes(5, undefined, 10);
expect(timeStart).toBe(5);
expect(timeEnd).toBe(15);
expect(duration).toBe(10);
});
it('handles events that finish the day after', () => {
const { timeStart, timeEnd, duration } = validateTimes(100, 10);
expect(timeStart).toBe(100);
expect(timeEnd).toBe(10);
expect(duration).toBe(dayInMs - 90);
});
it('corrects time in case of conflicts', () => {
const { timeStart, timeEnd, duration } = validateTimes(5, 15, 15);
expect(timeStart).toBe(5);
expect(timeEnd).toBe(15);
expect(duration).toBe(10);
});
it('calculates start time', () => {
const { timeStart, timeEnd, duration } = validateTimes(undefined, 15, 10);
expect(timeStart).toBe(5);
expect(timeEnd).toBe(15);
expect(duration).toBe(10);
});
it('calculates start and end time', () => {
const { timeStart, timeEnd, duration } = validateTimes(undefined, undefined, 10);
expect(timeStart).toBe(0);
expect(timeEnd).toBe(10);
expect(duration).toBe(10);
});
it('ensures values are integers', () => {
const { timeStart, timeEnd, duration } = validateTimes(0.000001, 10.312335342, 10);
expect(timeStart).toBe(0);
expect(timeEnd).toBe(10);
expect(duration).toBe(10);
});
});
describe('calculateDuration()', () => {
describe('Given start and end values', () => {
it('is the difference between end and start', () => {
const duration = calculateDuration(10, 20);
expect(duration).toBe(10);
});
});
describe('Handles edge cases', () => {
it('handles events that go over midnight', () => {
const duration = calculateDuration(51, 50);
expect(duration).toBe(dayInMs - 1);
});
it('handles no difference', () => {
const duration1 = calculateDuration(0, 0);
const duration2 = calculateDuration(dayInMs, dayInMs);
expect(duration1).toBe(0);
expect(duration2).toBe(0);
});
});
});
@@ -0,0 +1,85 @@
import { EndAction, TimerType } from 'ontime-types';
import { dayInMs } from '../timeConstants.js';
/**
* Checks if given value is a valid type of EndAction, returns the fallback otherwise
* @param {EndAction} maybeAction
* @param {EndAction} [fallback]
*/
export function validateEndAction(maybeAction: unknown, fallback = EndAction.None) {
return Object.values(EndAction).includes(maybeAction as EndAction) ? (maybeAction as EndAction) : fallback;
}
/**
* Checks if given value is a valid type of TimerType, returns the fallback otherwise
* @param {TimerType} maybeTimerType
* @param {TimerType} [fallback]
*/
export function validateTimerType(maybeTimerType: unknown, fallback = TimerType.CountDown) {
return Object.values(TimerType).includes(maybeTimerType as TimerType) ? (maybeTimerType as TimerType) : fallback;
}
/**
* @description calculates event duration considering midnight
* @param {number} timeStart
* @param {number} timeEnd
* @returns {number}
*/
export const calculateDuration = (timeStart: number, timeEnd: number): number => {
// Durations must be positive
if (timeEnd < timeStart) {
return timeEnd + dayInMs - timeStart;
}
return timeEnd - timeStart;
};
/**
* Converts a given value to an int, returns 0 otherwise
* @param value
* number
*/
function convertToInteger(value: unknown): number {
const result = Number(value);
return isNaN(result) ? 0 : Math.floor(result);
}
/**
* Ensures the time input variables are valid in relationship to each other
* Infers values if necessary
* @param _start
* @param _end
* @param _duration
*/
export function validateTimes(_start?: unknown, _end?: unknown, _duration?: unknown) {
const timeStart = convertToInteger(_start);
const timeEnd = convertToInteger(_end);
const duration = convertToInteger(_duration);
if (_start != null && _end != null) {
// Case 1. if we have start and end, duration must be derived
return { timeStart, duration: calculateDuration(timeStart, timeEnd), timeEnd };
}
if (_start == null && _end == null) {
if (_duration == null) {
// Case 2. no valid times were given
return { timeStart, duration, timeEnd };
}
// Case 3. we have a duration and infer the rest
return { timeStart, duration, timeEnd: duration };
}
if (_start != null) {
// Case 5. with only start, we can calculate the rest
return { timeStart, duration, timeEnd: timeStart + duration };
}
if (_end != null) {
// Case 6. with only end, we can calculate the rest
return { timeStart: timeEnd - duration, duration, timeEnd };
}
// we should have covered all cases
return { timeStart, duration, timeEnd };
}