mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 06:04:05 +00:00
refactor: remove legacy service
This commit is contained in:
committed by
Carlos Valente
parent
1f71d4578c
commit
2cc434b0e9
@@ -1,21 +0,0 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
|
||||||
import { HttpSettings } from 'ontime-types';
|
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
|
||||||
|
|
||||||
const httpPath = `${apiEntryUrl}/http`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to retrieve http settings
|
|
||||||
*/
|
|
||||||
export async function getHTTP(): Promise<HttpSettings> {
|
|
||||||
const res = await axios.get(httpPath);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to mutate http settings
|
|
||||||
*/
|
|
||||||
export async function postHTTP(data: HttpSettings): Promise<AxiosResponse<HttpSettings>> {
|
|
||||||
return axios.post(httpPath, data);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
|
||||||
import { OSCSettings } from 'ontime-types';
|
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
|
||||||
|
|
||||||
const oscPath = `${apiEntryUrl}/osc`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to retrieve osc settings
|
|
||||||
*/
|
|
||||||
export async function getOSC(): Promise<OSCSettings> {
|
|
||||||
const res = await axios.get(oscPath);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to mutate osc settings
|
|
||||||
*/
|
|
||||||
export async function postOSC(data: OSCSettings): Promise<AxiosResponse<OSCSettings>> {
|
|
||||||
return axios.post(oscPath, data);
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
|
||||||
import { HTTP_SETTINGS } from '../api/constants';
|
|
||||||
import { getHTTP, postHTTP } from '../api/http';
|
|
||||||
import { logAxiosError } from '../api/utils';
|
|
||||||
import { httpPlaceholder } from '../models/Http';
|
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
|
||||||
|
|
||||||
export function useHttpSettings() {
|
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
|
||||||
queryKey: HTTP_SETTINGS,
|
|
||||||
queryFn: getHTTP,
|
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
|
||||||
retry: 5,
|
|
||||||
retryDelay: (attempt: number) => attempt * 2500,
|
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
|
||||||
networkMode: 'always',
|
|
||||||
});
|
|
||||||
|
|
||||||
return { data: data ?? httpPlaceholder, status, isFetching, isError, refetch };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePostHttpSettings() {
|
|
||||||
const { isPending, mutateAsync } = useMutation({
|
|
||||||
mutationFn: postHTTP,
|
|
||||||
onError: (error) => logAxiosError('Error saving HTTP settings', error),
|
|
||||||
onSuccess: (res) => {
|
|
||||||
ontimeQueryClient.setQueryData(HTTP_SETTINGS, res.data);
|
|
||||||
},
|
|
||||||
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: HTTP_SETTINGS }),
|
|
||||||
});
|
|
||||||
return { isPending, mutateAsync };
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
|
||||||
import { OSC_SETTINGS } from '../api/constants';
|
|
||||||
import { getOSC, postOSC } from '../api/osc';
|
|
||||||
import { logAxiosError } from '../api/utils';
|
|
||||||
import { oscPlaceholderSettings } from '../models/OscSettings';
|
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
|
||||||
|
|
||||||
export default function useOscSettings() {
|
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
|
||||||
queryKey: OSC_SETTINGS,
|
|
||||||
queryFn: getOSC,
|
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
|
||||||
retry: 5,
|
|
||||||
retryDelay: (attempt: number) => attempt * 2500,
|
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
|
||||||
networkMode: 'always',
|
|
||||||
});
|
|
||||||
|
|
||||||
return { data: data ?? oscPlaceholderSettings, status, isFetching, isError, refetch };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useOscSettingsMutation() {
|
|
||||||
const { isPending, mutateAsync } = useMutation({
|
|
||||||
mutationFn: postOSC,
|
|
||||||
onError: (error) => logAxiosError('Error saving OSC settings', error),
|
|
||||||
onSuccess: (res) => {
|
|
||||||
ontimeQueryClient.setQueryData(OSC_SETTINGS, res.data);
|
|
||||||
},
|
|
||||||
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
|
|
||||||
});
|
|
||||||
return { isPending, mutateAsync };
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { HttpSettings } from 'ontime-types';
|
|
||||||
|
|
||||||
export const httpPlaceholder: HttpSettings = {
|
|
||||||
enabledOut: false,
|
|
||||||
subscriptions: [],
|
|
||||||
};
|
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
import { GetInfo } from 'ontime-types';
|
import { GetInfo } from 'ontime-types';
|
||||||
|
|
||||||
import { oscPlaceholderSettings } from './OscSettings';
|
|
||||||
|
|
||||||
export const ontimePlaceholderInfo: GetInfo = {
|
export const ontimePlaceholderInfo: GetInfo = {
|
||||||
networkInterfaces: [],
|
networkInterfaces: [],
|
||||||
version: '2.0.0',
|
version: '2.0.0',
|
||||||
serverPort: 4001,
|
serverPort: 4001,
|
||||||
osc: oscPlaceholderSettings,
|
|
||||||
publicDir: '',
|
publicDir: '',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { OSCSettings } from 'ontime-types';
|
|
||||||
|
|
||||||
export const oscPlaceholderSettings: OSCSettings = {
|
|
||||||
portIn: 8888,
|
|
||||||
portOut: 9999,
|
|
||||||
targetIP: '127.0.0.1',
|
|
||||||
enabledIn: false,
|
|
||||||
enabledOut: false,
|
|
||||||
subscriptions: [],
|
|
||||||
};
|
|
||||||
@@ -7,7 +7,6 @@ import AutomationPanel from './panel/automations-panel/AutomationPanel';
|
|||||||
import ClientControlPanel from './panel/client-control-panel/ClientControlPanel';
|
import ClientControlPanel from './panel/client-control-panel/ClientControlPanel';
|
||||||
import FeatureSettingsPanel from './panel/feature-settings-panel/FeatureSettingsPanel';
|
import FeatureSettingsPanel from './panel/feature-settings-panel/FeatureSettingsPanel';
|
||||||
import GeneralPanel from './panel/general-panel/GeneralPanel';
|
import GeneralPanel from './panel/general-panel/GeneralPanel';
|
||||||
import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
|
|
||||||
import NetworkLogPanel from './panel/network-panel/NetworkLogPanel';
|
import NetworkLogPanel from './panel/network-panel/NetworkLogPanel';
|
||||||
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||||
import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel';
|
import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel';
|
||||||
@@ -31,7 +30,6 @@ export default function AppSettings() {
|
|||||||
{panel === 'general' && <GeneralPanel location={location} />}
|
{panel === 'general' && <GeneralPanel location={location} />}
|
||||||
{panel === 'feature_settings' && <FeatureSettingsPanel location={location} />}
|
{panel === 'feature_settings' && <FeatureSettingsPanel location={location} />}
|
||||||
{panel === 'sources' && <SourcesPanel />}
|
{panel === 'sources' && <SourcesPanel />}
|
||||||
{panel === 'integrations' && <IntegrationsPanel location={location} />}
|
|
||||||
{panel === 'automation' && <AutomationPanel location={location} />}
|
{panel === 'automation' && <AutomationPanel location={location} />}
|
||||||
{panel === 'client_control' && <ClientControlPanel />}
|
{panel === 'client_control' && <ClientControlPanel />}
|
||||||
{panel === 'about' && <AboutPanel />}
|
{panel === 'about' && <AboutPanel />}
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
|
||||||
import { Button, IconButton, Input, Select, Switch } from '@chakra-ui/react';
|
|
||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
|
||||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
|
||||||
import { HttpSettings } from 'ontime-types';
|
|
||||||
import { generateId } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
|
||||||
import { useHttpSettings, usePostHttpSettings } from '../../../../common/hooks-query/useHttpSettings';
|
|
||||||
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
|
||||||
import { startsWithHttp } from '../../../../common/utils/regex';
|
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
|
||||||
|
|
||||||
import { cycles } from './integrationUtils';
|
|
||||||
|
|
||||||
import style from './IntegrationsPanel.module.css';
|
|
||||||
|
|
||||||
export default function HttpIntegrations() {
|
|
||||||
const { data, status } = useHttpSettings();
|
|
||||||
const { mutateAsync } = usePostHttpSettings();
|
|
||||||
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
register,
|
|
||||||
setError,
|
|
||||||
formState: { errors, isSubmitting, isDirty, isValid },
|
|
||||||
} = useForm<HttpSettings>({
|
|
||||||
mode: 'onChange',
|
|
||||||
defaultValues: data,
|
|
||||||
values: data,
|
|
||||||
resetOptions: {
|
|
||||||
keepDirtyValues: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { fields, prepend, remove } = useFieldArray({
|
|
||||||
name: 'subscriptions',
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (values: HttpSettings) => {
|
|
||||||
try {
|
|
||||||
await mutateAsync(values);
|
|
||||||
} catch (error) {
|
|
||||||
setError('root', { message: maybeAxiosError(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const preventEscape = (event: React.KeyboardEvent) => {
|
|
||||||
if (isKeyEscape(event)) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddNewSubscription = () => {
|
|
||||||
prepend({
|
|
||||||
id: generateId(),
|
|
||||||
cycle: 'onLoad',
|
|
||||||
message: '',
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteSubscription = (index: number) => {
|
|
||||||
remove(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const canSubmit = !isSubmitting && isDirty && isValid;
|
|
||||||
const isLoading = status === 'pending';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Panel.Section>
|
|
||||||
<Panel.Card>
|
|
||||||
<Panel.SubHeader>
|
|
||||||
HTTP settings
|
|
||||||
<div className={style.flex}>
|
|
||||||
<Button variant='ontime-ghosted' size='sm' onClick={() => reset()} isDisabled={!canSubmit}>
|
|
||||||
Revert to saved
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='ontime-filled'
|
|
||||||
size='sm'
|
|
||||||
type='submit'
|
|
||||||
form='http-form'
|
|
||||||
isDisabled={!canSubmit}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Panel.SubHeader>
|
|
||||||
<Panel.Divider />
|
|
||||||
<Panel.Section as='form' id='http-form' onSubmit={handleSubmit(onSubmit)} onKeyDown={preventEscape}>
|
|
||||||
<Panel.Loader isLoading={isLoading} />
|
|
||||||
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
|
||||||
<Panel.ListGroup>
|
|
||||||
<Panel.ListItem>
|
|
||||||
<Panel.Field title='HTTP Output' description='Provide feedback from Ontime through HTTP' />
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name='enabledOut'
|
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
|
||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
|
||||||
</Panel.ListGroup>
|
|
||||||
|
|
||||||
<Panel.Divider />
|
|
||||||
|
|
||||||
<Panel.Title>
|
|
||||||
HTTP Integration
|
|
||||||
<Button variant='ontime-subtle' size='sm' rightIcon={<IoAdd />} onClick={handleAddNewSubscription}>
|
|
||||||
New
|
|
||||||
</Button>
|
|
||||||
</Panel.Title>
|
|
||||||
{fields.length > 0 && (
|
|
||||||
<Panel.Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Enabled</th>
|
|
||||||
<th>Cycle</th>
|
|
||||||
<th className={style.fullWidth}>Message</th>
|
|
||||||
<th />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{fields.map((integration, index) => {
|
|
||||||
// @ts-expect-error -- not sure why it is not finding the type, it is ok
|
|
||||||
const maybeError = errors.subscriptions?.[index]?.message?.message;
|
|
||||||
return (
|
|
||||||
<tr key={integration.id}>
|
|
||||||
<td>
|
|
||||||
<Switch variant='ontime' {...register(`subscriptions.${index}.enabled`)} />
|
|
||||||
</td>
|
|
||||||
<td className={style.autoWidth}>
|
|
||||||
<Select
|
|
||||||
size='sm'
|
|
||||||
variant='ontime'
|
|
||||||
className={style.fitContents}
|
|
||||||
{...register(`subscriptions.${index}.cycle`)}
|
|
||||||
>
|
|
||||||
{cycles.map((cycle) => (
|
|
||||||
<option key={cycle.id} value={cycle.value}>
|
|
||||||
{cycle.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</td>
|
|
||||||
<td className={style.fullWidth}>
|
|
||||||
<Input
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-filled'
|
|
||||||
autoComplete='off'
|
|
||||||
placeholder='http://third-party/vt1/{{timer.current}}'
|
|
||||||
{...register(`subscriptions.${index}.message`, {
|
|
||||||
required: { value: true, message: 'Required field' },
|
|
||||||
pattern: {
|
|
||||||
value: startsWithHttp,
|
|
||||||
message: 'HTTP messages should start with http://',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{maybeError && <Panel.Error>{maybeError}</Panel.Error>}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<IconButton
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-ghosted'
|
|
||||||
color='#FA5656' // $red-500
|
|
||||||
icon={<IoTrash />}
|
|
||||||
aria-label='Delete entry'
|
|
||||||
onClick={() => handleDeleteSubscription(index)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</Panel.Table>
|
|
||||||
)}
|
|
||||||
</Panel.Section>
|
|
||||||
</Panel.Card>
|
|
||||||
</Panel.Section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
.fullWidth {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.halfWidth {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fitContents.fitContents {
|
|
||||||
width: max-content; /* override chakra */
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
|
||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
|
||||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
|
||||||
|
|
||||||
import HttpIntegrations from './HttpIntegrations';
|
|
||||||
import OscIntegrations from './OscIntegrations';
|
|
||||||
|
|
||||||
const integrationDocsUrl = 'https://docs.getontime.no/api/integrations/';
|
|
||||||
|
|
||||||
export default function IntegrationsPanel({ location }: PanelBaseProps) {
|
|
||||||
const oscRef = useScrollIntoView<HTMLDivElement>('osc', location);
|
|
||||||
const httpRef = useScrollIntoView<HTMLDivElement>('http', location);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Panel.Header>Integration settings</Panel.Header>
|
|
||||||
<Panel.Section>
|
|
||||||
<Alert status='info' variant='ontime-on-dark-info'>
|
|
||||||
<AlertIcon />
|
|
||||||
<AlertDescription>
|
|
||||||
Integrations allow Ontime to receive commands or send its data to other systems in your workflow. <br />
|
|
||||||
<br />
|
|
||||||
Currently supported protocols are OSC (Open Sound Control), HTTP and Websockets. <br />
|
|
||||||
WebSockets are used for Ontime and cannot be configured independently. <br />
|
|
||||||
<ExternalLink href={integrationDocsUrl}>See the docs</ExternalLink>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</Panel.Section>
|
|
||||||
<Panel.Section>
|
|
||||||
<div ref={oscRef}>
|
|
||||||
<OscIntegrations />
|
|
||||||
</div>
|
|
||||||
<div ref={httpRef}>
|
|
||||||
<HttpIntegrations />
|
|
||||||
</div>
|
|
||||||
</Panel.Section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
|
||||||
import { Button, IconButton, Input, Select, Switch } from '@chakra-ui/react';
|
|
||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
|
||||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
|
||||||
import { OSCSettings } from 'ontime-types';
|
|
||||||
import { generateId } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
|
||||||
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
|
||||||
import { isASCII, isASCIIorEmpty, isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
|
|
||||||
import { isOntimeCloud } from '../../../../externals';
|
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
|
||||||
|
|
||||||
import { cycles } from './integrationUtils';
|
|
||||||
|
|
||||||
import style from './IntegrationsPanel.module.css';
|
|
||||||
|
|
||||||
export default function OscIntegrations() {
|
|
||||||
const { data, status } = useOscSettings();
|
|
||||||
const { mutateAsync } = useOscSettingsMutation();
|
|
||||||
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
register,
|
|
||||||
setError,
|
|
||||||
formState: { errors, isSubmitting, isDirty, isValid },
|
|
||||||
} = useForm<OSCSettings>({
|
|
||||||
mode: 'onChange',
|
|
||||||
defaultValues: data,
|
|
||||||
values: data,
|
|
||||||
resetOptions: {
|
|
||||||
keepDirtyValues: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { fields, prepend, remove } = useFieldArray({
|
|
||||||
name: 'subscriptions',
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
// update form if we get new data from server
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
reset(data);
|
|
||||||
}
|
|
||||||
}, [data, reset]);
|
|
||||||
|
|
||||||
const onSubmit = async (values: OSCSettings) => {
|
|
||||||
if (values.portIn === values.portOut) {
|
|
||||||
setError('portIn', { message: 'OSC IN and OUT Ports cant be the same' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedValues = { ...values, portIn: Number(values.portIn), portOut: Number(values.portOut) };
|
|
||||||
try {
|
|
||||||
await mutateAsync(parsedValues);
|
|
||||||
} catch (error) {
|
|
||||||
setError('root', { message: maybeAxiosError(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddNewSubscription = () => {
|
|
||||||
prepend({
|
|
||||||
id: generateId(),
|
|
||||||
cycle: 'onLoad',
|
|
||||||
address: '',
|
|
||||||
payload: '',
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteSubscription = (index: number) => {
|
|
||||||
remove(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const canSubmit = !isSubmitting && isDirty && isValid;
|
|
||||||
const isLoading = status === 'pending';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Panel.Card>
|
|
||||||
<Panel.SubHeader>
|
|
||||||
OSC settings
|
|
||||||
<div className={style.flex}>
|
|
||||||
<Button variant='ontime-ghosted' size='sm' onClick={() => reset()} isDisabled={!canSubmit}>
|
|
||||||
Revert to saved
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='ontime-filled'
|
|
||||||
size='sm'
|
|
||||||
type='submit'
|
|
||||||
form='osc-form'
|
|
||||||
isDisabled={!canSubmit}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Panel.SubHeader>
|
|
||||||
{isOntimeCloud && (
|
|
||||||
<Panel.Highlight>For security reasons OSC integrations are not available in the cloud service.</Panel.Highlight>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Panel.Divider />
|
|
||||||
|
|
||||||
<Panel.Section as='form' id='osc-form' onSubmit={handleSubmit(onSubmit)} onKeyDown={preventEscape}>
|
|
||||||
<Panel.Loader isLoading={isLoading} />
|
|
||||||
<Panel.Title>General OSC settings</Panel.Title>
|
|
||||||
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
|
||||||
<Panel.ListGroup>
|
|
||||||
<Panel.ListItem>
|
|
||||||
<Panel.Field title='OSC input' description='Allow control of Ontime through OSC' />
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name='enabledIn'
|
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
|
||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
|
||||||
<Panel.ListItem>
|
|
||||||
<Panel.Field
|
|
||||||
title='Listen on port'
|
|
||||||
description='Port for incoming OSC. Default: 8888'
|
|
||||||
error={errors.portIn?.message}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
id='portIn'
|
|
||||||
placeholder='8888'
|
|
||||||
width='5rem'
|
|
||||||
maxLength={5}
|
|
||||||
size='sm'
|
|
||||||
textAlign='right'
|
|
||||||
variant='ontime-filled'
|
|
||||||
type='number'
|
|
||||||
autoComplete='off'
|
|
||||||
{...register('portIn', {
|
|
||||||
required: { value: true, message: 'Required field' },
|
|
||||||
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
|
||||||
min: { value: 1024, message: 'Port must be within range 1024 - 65535' },
|
|
||||||
pattern: {
|
|
||||||
value: isOnlyNumbers,
|
|
||||||
message: 'Value should be numeric',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
|
||||||
</Panel.ListGroup>
|
|
||||||
<Panel.ListGroup>
|
|
||||||
<Panel.ListItem>
|
|
||||||
<Panel.Field title='OSC output' description='Provide feedback from Ontime with OSC' />
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name='enabledOut'
|
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
|
||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
|
||||||
<Panel.ListItem>
|
|
||||||
<Panel.Field
|
|
||||||
title='OSC target IP'
|
|
||||||
description='IP address Ontime will send OSC messages to'
|
|
||||||
error={errors.targetIP?.message}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
id='targetIP'
|
|
||||||
placeholder='127.0.0.1'
|
|
||||||
width='9rem'
|
|
||||||
size='sm'
|
|
||||||
textAlign='right'
|
|
||||||
variant='ontime-filled'
|
|
||||||
autoComplete='off'
|
|
||||||
{...register('targetIP', {
|
|
||||||
required: { value: true, message: 'Required field' },
|
|
||||||
pattern: {
|
|
||||||
value: isIPAddress,
|
|
||||||
message: 'Invalid IP address',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
|
||||||
<Panel.ListItem>
|
|
||||||
<Panel.Field
|
|
||||||
title='OSC target port'
|
|
||||||
description='Port number Ontime will send OSC messages to'
|
|
||||||
error={errors.portOut?.message}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
id='portOut'
|
|
||||||
placeholder='8888'
|
|
||||||
width='75px'
|
|
||||||
size='sm'
|
|
||||||
textAlign='right'
|
|
||||||
variant='ontime-filled'
|
|
||||||
autoComplete='off'
|
|
||||||
{...register('portOut', {
|
|
||||||
required: { value: true, message: 'Required field' },
|
|
||||||
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
|
||||||
min: { value: 1024, message: 'Port must be within range 1024 - 65535' },
|
|
||||||
pattern: {
|
|
||||||
value: isOnlyNumbers,
|
|
||||||
message: 'Value should be numeric',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
|
||||||
</Panel.ListGroup>
|
|
||||||
|
|
||||||
<Panel.Divider />
|
|
||||||
|
|
||||||
<Panel.Title>
|
|
||||||
OSC integrations
|
|
||||||
<Button variant='ontime-subtle' size='sm' rightIcon={<IoAdd />} onClick={handleAddNewSubscription}>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</Panel.Title>
|
|
||||||
|
|
||||||
{fields.length > 0 && (
|
|
||||||
<Panel.Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Enabled</th>
|
|
||||||
<th>Cycle</th>
|
|
||||||
<th className={style.halfWidth}>Address</th>
|
|
||||||
<th className={style.halfWidth}>Arguments</th>
|
|
||||||
<th />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{fields.map((field, index) => {
|
|
||||||
const maybeAddressError = errors.subscriptions?.[index]?.address?.message;
|
|
||||||
const maybePayloadError = errors.subscriptions?.[index]?.payload?.message;
|
|
||||||
return (
|
|
||||||
<tr key={field.id}>
|
|
||||||
<td>
|
|
||||||
<Switch variant='ontime' {...register(`subscriptions.${index}.enabled`)} />
|
|
||||||
</td>
|
|
||||||
<td className={style.autoWidth}>
|
|
||||||
<Select
|
|
||||||
size='sm'
|
|
||||||
variant='ontime'
|
|
||||||
className={style.fitContents}
|
|
||||||
{...register(`subscriptions.${index}.cycle`)}
|
|
||||||
>
|
|
||||||
{cycles.map((cycle) => (
|
|
||||||
<option key={cycle.id} value={cycle.value}>
|
|
||||||
{cycle.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</td>
|
|
||||||
<td className={style.halfWidth}>
|
|
||||||
<Input
|
|
||||||
key={field.id}
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-filled'
|
|
||||||
autoComplete='off'
|
|
||||||
placeholder='/from-ontime/'
|
|
||||||
{...register(`subscriptions.${index}.address`, {
|
|
||||||
required: { value: true, message: 'Required field' },
|
|
||||||
validate: {
|
|
||||||
oscStartsWithSlash: (value) =>
|
|
||||||
startsWithSlash.test(value) || 'OSC address should start with a forward slash',
|
|
||||||
oscStringIsAscii: (value) =>
|
|
||||||
isASCII.test(value) || 'OSC address only allow ASCII characters',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{maybeAddressError && <Panel.Error>{maybeAddressError}</Panel.Error>}
|
|
||||||
</td>
|
|
||||||
<td className={style.halfWidth}>
|
|
||||||
<Input
|
|
||||||
key={field.id}
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-filled'
|
|
||||||
autoComplete='off'
|
|
||||||
placeholder='{{timer.current}}'
|
|
||||||
{...register(`subscriptions.${index}.payload`, {
|
|
||||||
validate: {
|
|
||||||
oscStringIsAscii: (value) =>
|
|
||||||
isASCIIorEmpty.test(value) || 'OSC arguments only allow ASCII characters',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{maybePayloadError && <Panel.Error>{maybePayloadError}</Panel.Error>}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<IconButton
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-ghosted'
|
|
||||||
color='#FA5656' // $red-500
|
|
||||||
icon={<IoTrash />}
|
|
||||||
aria-label='Delete entry'
|
|
||||||
onClick={() => handleDeleteSubscription(index)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</Panel.Table>
|
|
||||||
)}
|
|
||||||
</Panel.Section>
|
|
||||||
</Panel.Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { TimerLifeCycle } from 'ontime-types';
|
|
||||||
|
|
||||||
type CycleLabel = {
|
|
||||||
id: number;
|
|
||||||
label: string;
|
|
||||||
value: keyof typeof TimerLifeCycle;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cycles: CycleLabel[] = [
|
|
||||||
{ id: 1, label: 'On Load', value: 'onLoad' },
|
|
||||||
{ id: 2, label: 'On Start', value: 'onStart' },
|
|
||||||
{ id: 3, label: 'On Pause', value: 'onPause' },
|
|
||||||
{ id: 4, label: 'On Stop', value: 'onStop' },
|
|
||||||
{ id: 5, label: 'Every second', value: 'onClock' },
|
|
||||||
{ id: 5, label: 'On Timer Update', value: 'onUpdate' },
|
|
||||||
{ id: 6, label: 'On Finish', value: 'onFinish' },
|
|
||||||
{ id: 7, label: 'On Warning', value: 'onWarning' },
|
|
||||||
{ id: 8, label: 'On Danger', value: 'onDanger' },
|
|
||||||
];
|
|
||||||
@@ -23,8 +23,7 @@ type ProjectMergeFormValues = {
|
|||||||
rundown: boolean;
|
rundown: boolean;
|
||||||
viewSettings: boolean;
|
viewSettings: boolean;
|
||||||
urlPresets: boolean;
|
urlPresets: boolean;
|
||||||
osc: boolean;
|
automation: boolean;
|
||||||
http: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
||||||
@@ -42,8 +41,7 @@ export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
|||||||
rundown: false,
|
rundown: false,
|
||||||
viewSettings: false,
|
viewSettings: false,
|
||||||
urlPresets: false,
|
urlPresets: false,
|
||||||
osc: false,
|
automation: false,
|
||||||
http: false,
|
|
||||||
},
|
},
|
||||||
resetOptions: {
|
resetOptions: {
|
||||||
keepDirtyValues: true,
|
keepDirtyValues: true,
|
||||||
@@ -115,12 +113,8 @@ export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
|||||||
URL Presets
|
URL Presets
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<Switch variant='ontime' {...register('osc')} />
|
<Switch variant='ontime' {...register('automation')} />
|
||||||
OSC Integration
|
Automation Settings
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<Switch variant='ontime' {...register('http')} />
|
|
||||||
HTTP Integration
|
|
||||||
</label>
|
</label>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
|
|||||||
@@ -46,14 +46,6 @@ const staticOptions = [
|
|||||||
],
|
],
|
||||||
split: true,
|
split: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'integrations',
|
|
||||||
label: 'Integrations',
|
|
||||||
secondary: [
|
|
||||||
{ id: 'integrations__osc', label: 'OSC settings' },
|
|
||||||
{ id: 'integrations__http', label: 'HTTP settings' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'automation',
|
id: 'automation',
|
||||||
label: 'Automation',
|
label: 'Automation',
|
||||||
|
|||||||
@@ -3,11 +3,40 @@ import { DatabaseModel, AutomationSettings, Automation, NormalisedAutomationBlue
|
|||||||
import { dbModel } from '../../models/dataModel.js';
|
import { dbModel } from '../../models/dataModel.js';
|
||||||
import type { ErrorEmitter } from '../../utils/parser.js';
|
import type { ErrorEmitter } from '../../utils/parser.js';
|
||||||
|
|
||||||
export function parseAutomationSettings(data: Partial<DatabaseModel>, emitError?: ErrorEmitter): AutomationSettings {
|
interface LegacyData extends Partial<DatabaseModel> {
|
||||||
|
http?: unknown;
|
||||||
|
osc?: {
|
||||||
|
enabledIn?: boolean;
|
||||||
|
portIn?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAutomationSettings(data: LegacyData, emitError?: ErrorEmitter): AutomationSettings {
|
||||||
|
/**
|
||||||
|
* Leaving a path for migrating users to the new automations
|
||||||
|
* This should be removed after a few releases
|
||||||
|
*/
|
||||||
|
if (data.http || data.osc) {
|
||||||
|
emitError?.('Found legacy integrations');
|
||||||
|
console.log('Found legacy integrations...');
|
||||||
|
if (data.osc) {
|
||||||
|
return {
|
||||||
|
enabledAutomations: dbModel.automation.enabledAutomations,
|
||||||
|
enabledOscIn: data.osc?.enabledIn ?? dbModel.automation.enabledOscIn,
|
||||||
|
oscPortIn: data.osc?.portIn ?? dbModel.automation.oscPortIn,
|
||||||
|
automations: [],
|
||||||
|
blueprints: {},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { ...dbModel.automation };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!data.automation) {
|
if (!data.automation) {
|
||||||
emitError?.('No data found to import');
|
emitError?.('No data found to import');
|
||||||
return { ...dbModel.automation };
|
return { ...dbModel.automation };
|
||||||
}
|
}
|
||||||
|
console.log('Found Automation settings, importing...');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabledAutomations: data.automation.enabledAutomations ?? dbModel.automation.enabledAutomations,
|
enabledAutomations: data.automation.enabledAutomations ?? dbModel.automation.enabledAutomations,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import * as projectService from '../../services/project-service/ProjectService.j
|
|||||||
|
|
||||||
export async function patchPartialProjectFile(req: Request, res: Response<DatabaseModel | ErrorResponse>) {
|
export async function patchPartialProjectFile(req: Request, res: Response<DatabaseModel | ErrorResponse>) {
|
||||||
try {
|
try {
|
||||||
const { rundown, project, settings, viewSettings, urlPresets, customFields, osc, http, automation } = req.body;
|
const { rundown, project, settings, viewSettings, urlPresets, customFields, automation } = req.body;
|
||||||
const patchDb: DatabaseModel = {
|
const patchDb: DatabaseModel = {
|
||||||
rundown,
|
rundown,
|
||||||
project,
|
project,
|
||||||
@@ -26,8 +26,6 @@ export async function patchPartialProjectFile(req: Request, res: Response<Databa
|
|||||||
viewSettings,
|
viewSettings,
|
||||||
urlPresets,
|
urlPresets,
|
||||||
customFields,
|
customFields,
|
||||||
osc,
|
|
||||||
http,
|
|
||||||
automation,
|
automation,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import type { ErrorResponse, HttpSettings } from 'ontime-types';
|
|
||||||
import { getErrorMessage } from 'ontime-utils';
|
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
|
||||||
|
|
||||||
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
|
||||||
import { httpIntegration } from '../../services/integration-service/HttpIntegration.js';
|
|
||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
|
||||||
|
|
||||||
export async function getHTTP(_req: Request, res: Response<HttpSettings>) {
|
|
||||||
const http = getDataProvider().getHttp();
|
|
||||||
res.status(200).send(http);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function postHTTP(req: Request, res: Response<HttpSettings | ErrorResponse>) {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const httpSettings = req.body;
|
|
||||||
|
|
||||||
httpIntegration.init(httpSettings);
|
|
||||||
// we persist the data after init to avoid persisting invalid data
|
|
||||||
const result = await getDataProvider().setHttp(httpSettings);
|
|
||||||
res.send(result).status(200);
|
|
||||||
} catch (error) {
|
|
||||||
const message = getErrorMessage(error);
|
|
||||||
res.status(400).send({ message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
|
|
||||||
import { validateHTTP } from './http.validation.js';
|
|
||||||
import { getHTTP, postHTTP } from './http.controller.js';
|
|
||||||
|
|
||||||
export const router = express.Router();
|
|
||||||
|
|
||||||
router.get('/', getHTTP);
|
|
||||||
router.post('/', validateHTTP, postHTTP);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { body, validationResult } from 'express-validator';
|
|
||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
|
|
||||||
import { sanitiseHttpSubscriptions } from '../../utils/parserFunctions.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validates object for POST /ontime/http
|
|
||||||
*/
|
|
||||||
export const validateHTTP = [
|
|
||||||
body('enabledOut').exists().isBoolean(),
|
|
||||||
body('subscriptions')
|
|
||||||
.exists()
|
|
||||||
.isArray()
|
|
||||||
.custom((value) => sanitiseHttpSubscriptions(value)),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -4,8 +4,6 @@ import { router as automationsRouter } from './automation/automation.router.js';
|
|||||||
import { router as urlPresetsRouter } from './url-presets/urlPresets.router.js';
|
import { router as urlPresetsRouter } from './url-presets/urlPresets.router.js';
|
||||||
import { router as customFieldsRouter } from './custom-fields/customFields.router.js';
|
import { router as customFieldsRouter } from './custom-fields/customFields.router.js';
|
||||||
import { router as dbRouter } from './db/db.router.js';
|
import { router as dbRouter } from './db/db.router.js';
|
||||||
import { router as httpRouter } from './http/http.router.js';
|
|
||||||
import { router as oscRouter } from './osc/osc.router.js';
|
|
||||||
import { router as projectRouter } from './project/project.router.js';
|
import { router as projectRouter } from './project/project.router.js';
|
||||||
import { router as rundownRouter } from './rundown/rundown.router.js';
|
import { router as rundownRouter } from './rundown/rundown.router.js';
|
||||||
import { router as settingsRouter } from './settings/settings.router.js';
|
import { router as settingsRouter } from './settings/settings.router.js';
|
||||||
@@ -19,8 +17,6 @@ export const appRouter = express.Router();
|
|||||||
appRouter.use('/automations', automationsRouter);
|
appRouter.use('/automations', automationsRouter);
|
||||||
appRouter.use('/custom-fields', customFieldsRouter);
|
appRouter.use('/custom-fields', customFieldsRouter);
|
||||||
appRouter.use('/db', dbRouter);
|
appRouter.use('/db', dbRouter);
|
||||||
appRouter.use('/http', httpRouter);
|
|
||||||
appRouter.use('/osc', oscRouter);
|
|
||||||
appRouter.use('/project', projectRouter);
|
appRouter.use('/project', projectRouter);
|
||||||
appRouter.use('/rundown', rundownRouter);
|
appRouter.use('/rundown', rundownRouter);
|
||||||
appRouter.use('/settings', settingsRouter);
|
appRouter.use('/settings', settingsRouter);
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import type { ErrorResponse, OSCSettings } from 'ontime-types';
|
|
||||||
import { getErrorMessage } from 'ontime-utils';
|
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
|
||||||
|
|
||||||
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
|
||||||
import { oscIntegration } from '../../services/integration-service/OscIntegration.js';
|
|
||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
|
||||||
|
|
||||||
export async function getOSC(_req: Request, res: Response<OSCSettings>) {
|
|
||||||
const osc = getDataProvider().getOsc();
|
|
||||||
res.status(200).send(osc);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function postOSC(req: Request, res: Response<OSCSettings | ErrorResponse>) {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const oscSettings = req.body;
|
|
||||||
|
|
||||||
oscIntegration.init(oscSettings);
|
|
||||||
// we persist the data after init to avoid persisting invalid data
|
|
||||||
const result = await getDataProvider().setOsc(oscSettings);
|
|
||||||
|
|
||||||
res.send(result).status(200);
|
|
||||||
} catch (error) {
|
|
||||||
const message = getErrorMessage(error);
|
|
||||||
res.status(400).send({ message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { getOSC, postOSC } from './osc.controller.js';
|
|
||||||
import { validateOSC } from './osc.validation.js';
|
|
||||||
|
|
||||||
export const router = express.Router();
|
|
||||||
|
|
||||||
router.get('/', getOSC);
|
|
||||||
router.post('/', validateOSC, postOSC);
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { body, validationResult } from 'express-validator';
|
|
||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
|
|
||||||
import { sanitiseOscSubscriptions } from '../../utils/parserFunctions.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validates object for POST /ontime/osc
|
|
||||||
*/
|
|
||||||
export const validateOSC = [
|
|
||||||
body('portIn').exists().isPort(),
|
|
||||||
body('portOut').exists().isPort(),
|
|
||||||
body('targetIP').exists().isIP(),
|
|
||||||
body('enabledIn').exists().isBoolean(),
|
|
||||||
body('enabledOut').exists().isBoolean(),
|
|
||||||
body('subscriptions')
|
|
||||||
.exists()
|
|
||||||
.isArray()
|
|
||||||
.custom((value) => sanitiseOscSubscriptions(value)),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -34,7 +34,6 @@ export async function getSessionStats(): Promise<SessionStats> {
|
|||||||
*/
|
*/
|
||||||
export async function getInfo(): Promise<GetInfo> {
|
export async function getInfo(): Promise<GetInfo> {
|
||||||
const { version, serverPort } = getDataProvider().getSettings();
|
const { version, serverPort } = getDataProvider().getSettings();
|
||||||
const osc = getDataProvider().getOsc();
|
|
||||||
|
|
||||||
// get nif and inject localhost
|
// get nif and inject localhost
|
||||||
const ni = getNetworkInterfaces();
|
const ni = getNetworkInterfaces();
|
||||||
@@ -44,7 +43,6 @@ export async function getInfo(): Promise<GetInfo> {
|
|||||||
networkInterfaces: ni,
|
networkInterfaces: ni,
|
||||||
version,
|
version,
|
||||||
serverPort,
|
serverPort,
|
||||||
osc,
|
|
||||||
publicDir: publicDir.root,
|
publicDir: publicDir.root,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-33
@@ -27,10 +27,7 @@ import { socket } from './adapters/WebsocketAdapter.js';
|
|||||||
import { getDataProvider } from './classes/data-provider/DataProvider.js';
|
import { getDataProvider } from './classes/data-provider/DataProvider.js';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
import { integrationService } from './services/integration-service/IntegrationService.js';
|
|
||||||
import { logger } from './classes/Logger.js';
|
import { logger } from './classes/Logger.js';
|
||||||
import { oscIntegration } from './services/integration-service/OscIntegration.js';
|
|
||||||
import { httpIntegration } from './services/integration-service/HttpIntegration.js';
|
|
||||||
import { populateStyles } from './setup/loadStyles.js';
|
import { populateStyles } from './setup/loadStyles.js';
|
||||||
import { eventStore } from './stores/EventStore.js';
|
import { eventStore } from './stores/EventStore.js';
|
||||||
import { runtimeService } from './services/runtime-service/RuntimeService.js';
|
import { runtimeService } from './services/runtime-service/RuntimeService.js';
|
||||||
@@ -134,7 +131,7 @@ let expressServer: Server | null = null;
|
|||||||
const checkStart = (currentState: OntimeStartOrder) => {
|
const checkStart = (currentState: OntimeStartOrder) => {
|
||||||
if (step !== currentState) {
|
if (step !== currentState) {
|
||||||
step = OntimeStartOrder.Error;
|
step = OntimeStartOrder.Error;
|
||||||
throw new Error('Init order error: initAssets > startServer > startOsc > startIntegrations');
|
throw new Error('Init order error: initAssets > startServer');
|
||||||
} else {
|
} else {
|
||||||
if (step === 1 || step === 2) {
|
if (step === 1 || step === 2) {
|
||||||
step = step + 1;
|
step = step + 1;
|
||||||
@@ -234,34 +231,6 @@ export const startServer = async (
|
|||||||
*/
|
*/
|
||||||
export const startIntegrations = async () => {
|
export const startIntegrations = async () => {
|
||||||
checkStart(OntimeStartOrder.InitIO);
|
checkStart(OntimeStartOrder.InitIO);
|
||||||
|
|
||||||
// if a config is not provided, we use the persisted one
|
|
||||||
const { osc, http } = getDataProvider().getData();
|
|
||||||
|
|
||||||
if (http) {
|
|
||||||
logger.info(LogOrigin.Tx, 'Initialising HTTP Integration...');
|
|
||||||
try {
|
|
||||||
httpIntegration.init(http);
|
|
||||||
integrationService.register(httpIntegration);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(LogOrigin.Tx, `HTTP Integration initialisation failed: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOntimeCloud) {
|
|
||||||
logger.info(LogOrigin.Tx, 'Skipping OSC in Cloud environment...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (osc) {
|
|
||||||
logger.info(LogOrigin.Tx, 'Initialising OSC Integration...');
|
|
||||||
try {
|
|
||||||
oscIntegration.init(osc);
|
|
||||||
integrationService.register(oscIntegration);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(LogOrigin.Tx, 'OSC Integration initialisation failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -283,8 +252,8 @@ export const shutdown = async (exitCode = 0) => {
|
|||||||
|
|
||||||
expressServer?.close();
|
expressServer?.close();
|
||||||
runtimeService.shutdown();
|
runtimeService.shutdown();
|
||||||
integrationService.shutdown();
|
|
||||||
logger.shutdown();
|
logger.shutdown();
|
||||||
|
oscServer.shutdown();
|
||||||
socket.shutdown();
|
socket.shutdown();
|
||||||
process.exit(exitCode);
|
process.exit(exitCode);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import {
|
|||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
ViewSettings,
|
ViewSettings,
|
||||||
DatabaseModel,
|
DatabaseModel,
|
||||||
OSCSettings,
|
|
||||||
Settings,
|
Settings,
|
||||||
CustomFields,
|
CustomFields,
|
||||||
HttpSettings,
|
|
||||||
URLPreset,
|
URLPreset,
|
||||||
AutomationSettings,
|
AutomationSettings,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
@@ -49,14 +47,10 @@ export function getDataProvider() {
|
|||||||
setRundown,
|
setRundown,
|
||||||
getSettings,
|
getSettings,
|
||||||
setSettings,
|
setSettings,
|
||||||
getOsc,
|
|
||||||
getHttp,
|
|
||||||
getUrlPresets,
|
getUrlPresets,
|
||||||
setUrlPresets,
|
setUrlPresets,
|
||||||
getViewSettings,
|
getViewSettings,
|
||||||
setViewSettings,
|
setViewSettings,
|
||||||
setOsc,
|
|
||||||
setHttp,
|
|
||||||
getAutomation,
|
getAutomation,
|
||||||
setAutomation,
|
setAutomation,
|
||||||
getRundown,
|
getRundown,
|
||||||
@@ -104,14 +98,6 @@ async function setSettings(newData: Settings): ReadonlyPromise<Settings> {
|
|||||||
return db.data.settings;
|
return db.data.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOsc(): Readonly<OSCSettings> {
|
|
||||||
return db.data.osc;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHttp(): Readonly<HttpSettings> {
|
|
||||||
return db.data.http;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUrlPresets(): Readonly<URLPreset[]> {
|
function getUrlPresets(): Readonly<URLPreset[]> {
|
||||||
return db.data.urlPresets;
|
return db.data.urlPresets;
|
||||||
}
|
}
|
||||||
@@ -132,19 +118,6 @@ async function setViewSettings(newData: ViewSettings): ReadonlyPromise<ViewSetti
|
|||||||
return db.data.viewSettings;
|
return db.data.viewSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setOsc(newData: OSCSettings): ReadonlyPromise<OSCSettings> {
|
|
||||||
db.data.osc = { ...newData };
|
|
||||||
await persist();
|
|
||||||
return db.data.osc;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setHttp(newData: HttpSettings): ReadonlyPromise<HttpSettings> {
|
|
||||||
db.data.http = { ...newData };
|
|
||||||
|
|
||||||
await persist();
|
|
||||||
return db.data.http;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAutomation(): Readonly<AutomationSettings> {
|
function getAutomation(): Readonly<AutomationSettings> {
|
||||||
return db.data.automation;
|
return db.data.automation;
|
||||||
}
|
}
|
||||||
@@ -165,8 +138,6 @@ async function mergeIntoData(newData: Partial<DatabaseModel>): ReadonlyPromise<D
|
|||||||
db.data.settings = mergedData.settings;
|
db.data.settings = mergedData.settings;
|
||||||
db.data.viewSettings = mergedData.viewSettings;
|
db.data.viewSettings = mergedData.viewSettings;
|
||||||
db.data.automation = mergedData.automation;
|
db.data.automation = mergedData.automation;
|
||||||
db.data.osc = mergedData.osc;
|
|
||||||
db.data.http = mergedData.http;
|
|
||||||
db.data.urlPresets = mergedData.urlPresets;
|
db.data.urlPresets = mergedData.urlPresets;
|
||||||
db.data.customFields = mergedData.customFields;
|
db.data.customFields = mergedData.customFields;
|
||||||
db.data.rundown = mergedData.rundown;
|
db.data.rundown = mergedData.rundown;
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseMode
|
|||||||
viewSettings = {},
|
viewSettings = {},
|
||||||
urlPresets = existing.urlPresets,
|
urlPresets = existing.urlPresets,
|
||||||
customFields = existing.customFields,
|
customFields = existing.customFields,
|
||||||
osc = {},
|
|
||||||
http = {},
|
|
||||||
automation = existing.automation,
|
automation = existing.automation,
|
||||||
} = newData;
|
} = newData;
|
||||||
|
|
||||||
|
|||||||
@@ -35,18 +35,6 @@ describe('safeMerge', () => {
|
|||||||
lighting: { type: 'string', label: 'lighting', colour: 'red' },
|
lighting: { type: 'string', label: 'lighting', colour: 'red' },
|
||||||
vfx: { type: 'string', label: 'vfx', colour: 'blue' },
|
vfx: { type: 'string', label: 'vfx', colour: 'blue' },
|
||||||
},
|
},
|
||||||
osc: {
|
|
||||||
portIn: 8888,
|
|
||||||
portOut: 9999,
|
|
||||||
targetIP: '127.0.0.1',
|
|
||||||
enabledIn: false,
|
|
||||||
enabledOut: false,
|
|
||||||
subscriptions: [],
|
|
||||||
},
|
|
||||||
http: {
|
|
||||||
enabledOut: false,
|
|
||||||
subscriptions: [],
|
|
||||||
},
|
|
||||||
automation: {
|
automation: {
|
||||||
enabledAutomations: false,
|
enabledAutomations: false,
|
||||||
enabledOscIn: false,
|
enabledOscIn: false,
|
||||||
@@ -107,38 +95,6 @@ describe('safeMerge', () => {
|
|||||||
language: 'pt',
|
language: 'pt',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('merges the osc key', () => {
|
|
||||||
const newData = {
|
|
||||||
osc: {
|
|
||||||
portIn: 7777,
|
|
||||||
subscriptions: [
|
|
||||||
{
|
|
||||||
id: 'unique',
|
|
||||||
cycle: 'onStart',
|
|
||||||
message: 'new message',
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
//@ts-expect-error -- testing partial merge
|
|
||||||
const mergedData = safeMerge(existing, newData);
|
|
||||||
expect(mergedData.osc).toMatchObject({
|
|
||||||
portIn: 7777,
|
|
||||||
portOut: 9999,
|
|
||||||
targetIP: '127.0.0.1',
|
|
||||||
enabledIn: false,
|
|
||||||
enabledOut: false,
|
|
||||||
subscriptions: [
|
|
||||||
{
|
|
||||||
id: 'unique',
|
|
||||||
cycle: 'onStart',
|
|
||||||
message: 'new message',
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should merge the urlPresets key when present', () => {
|
it('should merge the urlPresets key when present', () => {
|
||||||
const existingData = {
|
const existingData = {
|
||||||
@@ -167,17 +123,12 @@ describe('safeMerge', () => {
|
|||||||
} as ViewSettings,
|
} as ViewSettings,
|
||||||
urlPresets: [],
|
urlPresets: [],
|
||||||
customFields: {},
|
customFields: {},
|
||||||
osc: {
|
automation: {
|
||||||
portIn: 8888,
|
enabledAutomations: false,
|
||||||
portOut: 9999,
|
enabledOscIn: false,
|
||||||
targetIP: '127.0.0.1',
|
oscPortIn: 8000,
|
||||||
enabledIn: false,
|
automations: [],
|
||||||
enabledOut: false,
|
blueprints: {},
|
||||||
subscriptions: [],
|
|
||||||
},
|
|
||||||
http: {
|
|
||||||
enabledOut: false,
|
|
||||||
subscriptions: [],
|
|
||||||
},
|
},
|
||||||
} as DatabaseModel;
|
} as DatabaseModel;
|
||||||
|
|
||||||
|
|||||||
@@ -31,18 +31,6 @@ export const dbModel: DatabaseModel = {
|
|||||||
},
|
},
|
||||||
urlPresets: [],
|
urlPresets: [],
|
||||||
customFields: {},
|
customFields: {},
|
||||||
osc: {
|
|
||||||
portIn: 8888,
|
|
||||||
portOut: 9999,
|
|
||||||
targetIP: '127.0.0.1',
|
|
||||||
enabledIn: false,
|
|
||||||
enabledOut: false,
|
|
||||||
subscriptions: [],
|
|
||||||
},
|
|
||||||
http: {
|
|
||||||
enabledOut: false,
|
|
||||||
subscriptions: [],
|
|
||||||
},
|
|
||||||
automation: {
|
automation: {
|
||||||
enabledAutomations: true,
|
enabledAutomations: true,
|
||||||
enabledOscIn: true,
|
enabledOscIn: true,
|
||||||
|
|||||||
@@ -450,18 +450,6 @@ export const demoDb: DatabaseModel = {
|
|||||||
pathAndParams: 'lower?bg=ff2&text=f00&size=0.6&transition=5',
|
pathAndParams: 'lower?bg=ff2&text=f00&size=0.6&transition=5',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
osc: {
|
|
||||||
portIn: 8888,
|
|
||||||
portOut: 9999,
|
|
||||||
targetIP: '127.0.0.1',
|
|
||||||
enabledIn: true,
|
|
||||||
enabledOut: false,
|
|
||||||
subscriptions: [],
|
|
||||||
},
|
|
||||||
http: {
|
|
||||||
enabledOut: false,
|
|
||||||
subscriptions: [],
|
|
||||||
},
|
|
||||||
automation: {
|
automation: {
|
||||||
enabledAutomations: false,
|
enabledAutomations: false,
|
||||||
enabledOscIn: true,
|
enabledOscIn: true,
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import got from 'got';
|
|
||||||
|
|
||||||
import { HttpSettings, HttpSubscription, LogOrigin } from 'ontime-types';
|
|
||||||
|
|
||||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
|
||||||
import { parseTemplateNested } from './integrationUtils.js';
|
|
||||||
import { logger } from '../../classes/Logger.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Class contains logic towards outgoing HTTP communications
|
|
||||||
* @class
|
|
||||||
*/
|
|
||||||
export class HttpIntegration implements IIntegration<HttpSubscription, HttpSettings> {
|
|
||||||
subscriptions: HttpSubscription[];
|
|
||||||
enabled: boolean;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.subscriptions = [];
|
|
||||||
this.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes httpClient
|
|
||||||
*/
|
|
||||||
init(config: HttpSettings) {
|
|
||||||
const { subscriptions, enabledOut } = config;
|
|
||||||
this.initSubscriptions(subscriptions);
|
|
||||||
this.enabled = enabledOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
initSubscriptions(subscriptions: HttpSubscription[]) {
|
|
||||||
this.subscriptions = subscriptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(action: TimerLifeCycleKey, state?: object) {
|
|
||||||
// noop
|
|
||||||
if (!this.enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.warning(LogOrigin.Tx, `HTTP Integration: ${err.message}`);
|
|
||||||
|
|
||||||
if (err.code === 'ECONNREFUSED') {
|
|
||||||
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' The server refused the connection`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.code === 'ENOTFOUND') {
|
|
||||||
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' DNS lookup failed`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.code === 'ETIMEDOUT') {
|
|
||||||
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' The connection timed out`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.warning(LogOrigin.Tx, `HTTP Integration: ${err.code}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown() {
|
|
||||||
/** shutdown is a no-op here*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const httpIntegration = new HttpIntegration();
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { TimerLifeCycle } from 'ontime-types';
|
|
||||||
|
|
||||||
export type TimerLifeCycleKey = keyof typeof TimerLifeCycle;
|
|
||||||
|
|
||||||
export default interface IIntegration<T, C> {
|
|
||||||
subscriptions: T[];
|
|
||||||
init: (config: C) => void;
|
|
||||||
dispatch: (action: TimerLifeCycleKey, state?: object) => void;
|
|
||||||
emit: (...args: never[]) => unknown;
|
|
||||||
shutdown: () => void;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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, unknown>[];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.integrations = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
register(integrationService: IIntegration<unknown, unknown>) {
|
|
||||||
this.integrations.push(integrationService);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregister(integrationService: IIntegration<unknown, unknown>) {
|
|
||||||
this.integrations = this.integrations.filter((int) => int !== integrationService);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(action: TimerLifeCycleKey) {
|
|
||||||
const state = eventStore.poll();
|
|
||||||
this.integrations.forEach((integration) => {
|
|
||||||
integration.dispatch(action, state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown() {
|
|
||||||
logger.info(LogOrigin.Tx, 'Shutdown Integrations');
|
|
||||||
this.integrations.forEach((integration) => {
|
|
||||||
integration.shutdown();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const integrationService = new IntegrationService();
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import { ArgumentType, Client, Message } from 'node-osc';
|
|
||||||
import { LogOrigin, MaybeNumber, MaybeString, OSCSettings, OscSubscription } from 'ontime-types';
|
|
||||||
|
|
||||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
|
||||||
import { parseTemplateNested } from './integrationUtils.js';
|
|
||||||
import { logger } from '../../classes/Logger.js';
|
|
||||||
import { OscServer } from '../../adapters/OscAdapter.js';
|
|
||||||
import { stringToOSCArgs } from '../../utils/oscArgParser.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Class contains logic towards outgoing OSC communications
|
|
||||||
* @class
|
|
||||||
*/
|
|
||||||
export class OscIntegration implements IIntegration<OscSubscription, OSCSettings> {
|
|
||||||
protected oscClient: null | Client;
|
|
||||||
protected oscServer: OscServer | null = null;
|
|
||||||
|
|
||||||
subscriptions: OscSubscription[];
|
|
||||||
targetIP: MaybeString;
|
|
||||||
portOut: MaybeNumber;
|
|
||||||
portIn: MaybeNumber;
|
|
||||||
enabledOut: boolean;
|
|
||||||
enabledIn: boolean;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.oscClient = null;
|
|
||||||
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, enabledIn, portIn } = config;
|
|
||||||
|
|
||||||
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 || !this.enabledOut) {
|
|
||||||
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;
|
|
||||||
const parsedArguments = stringToOSCArgs(parsedPayload);
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.emit(parsedAddress, parsedArguments);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(LogOrigin.Tx, `OSC Integration: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(address: string, args: ArgumentType[]) {
|
|
||||||
if (!this.oscClient) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Look into using bundles
|
|
||||||
const message = new Message(address);
|
|
||||||
message.append(args);
|
|
||||||
|
|
||||||
this.oscClient.send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private initTX(enabledOut: boolean, targetIP: string, portOut: number, subscriptions: OscSubscription[]) {
|
|
||||||
this.initSubscriptions(subscriptions);
|
|
||||||
|
|
||||||
if (!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.oscClient = new Client(targetIP, portOut);
|
|
||||||
logger.info(LogOrigin.Tx, `Starting OSC Clint on port: ${portOut}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.oscClient = null;
|
|
||||||
throw new Error(`Failed initialising OSC client: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private initRX(enabledIn: boolean, portIn: number) {
|
|
||||||
if (!enabledIn) {
|
|
||||||
this.shutdownRX();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start OSC Server
|
|
||||||
logger.info(LogOrigin.Rx, `Starting OSC Server on port: ${portIn}`);
|
|
||||||
this.oscServer = new OscServer(portIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown() {
|
|
||||||
this.shutdownTX();
|
|
||||||
this.shutdownRX();
|
|
||||||
}
|
|
||||||
|
|
||||||
private shutdownTX() {
|
|
||||||
if (this.oscClient) {
|
|
||||||
logger.info(LogOrigin.Tx, 'Shutting down OSC integration');
|
|
||||||
this.oscClient?.close();
|
|
||||||
this.oscClient = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private shutdownRX() {
|
|
||||||
if (this.oscServer) {
|
|
||||||
logger.info(LogOrigin.Rx, 'Shutting down OSC integration');
|
|
||||||
this.oscServer?.shutdown();
|
|
||||||
this.oscServer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const oscIntegration = new OscIntegration();
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
import { stringToOSCArgs } from '../../utils/oscArgParser.js';
|
|
||||||
import { parseTemplateNested } from './integrationUtils.js';
|
|
||||||
|
|
||||||
describe('parseTemplateNested()', () => {
|
|
||||||
it('parses string with a single-level variable name', () => {
|
|
||||||
const store = { timer: 10 };
|
|
||||||
const templateString = '/test/{{timer}}';
|
|
||||||
const result = parseTemplateNested(templateString, store);
|
|
||||||
expect(result).toEqual('/test/10');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses string with a nested variable name', () => {
|
|
||||||
const store = { timer: { clock: 10 } };
|
|
||||||
const templateString = '/timer/{{timer.clock}}';
|
|
||||||
const result = parseTemplateNested(templateString, store);
|
|
||||||
expect(result).toEqual('/timer/10');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses string with multiple variables', () => {
|
|
||||||
const mockState = { test1: 'that', test2: 'this' };
|
|
||||||
const testString = '{{test1}} should replace {{test2}}';
|
|
||||||
const expected = `${mockState.test1} should replace ${mockState.test2}`;
|
|
||||||
|
|
||||||
const result = parseTemplateNested(testString, mockState);
|
|
||||||
expect(result).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('correctly parses a string without templates', () => {
|
|
||||||
const testString = 'That should replace {test}';
|
|
||||||
|
|
||||||
const result = parseTemplateNested(testString, {});
|
|
||||||
expect(result).toStrictEqual(testString);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles scenarios with missing variables', () => {
|
|
||||||
// by failing to provide a value, we give visibility to
|
|
||||||
// potential issues in the given string
|
|
||||||
const mockState = { test1: 'that', test2: 'this' };
|
|
||||||
const testString = '{{test1}} should replace {{test2}}, but not {{test3}}';
|
|
||||||
const expected = `${mockState.test1} should replace ${mockState.test2}, but not {{test3}}`;
|
|
||||||
|
|
||||||
const result = parseTemplateNested(testString, mockState);
|
|
||||||
expect(result).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseNestedTemplate() -> resolveAliasData()', () => {
|
|
||||||
it('resolves data through callback', () => {
|
|
||||||
const data = {
|
|
||||||
not: {
|
|
||||||
so: {
|
|
||||||
easy: '3',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const aliases = {
|
|
||||||
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
|
|
||||||
};
|
|
||||||
|
|
||||||
const easyParse = parseTemplateNested('{{human.easy}}', data, aliases);
|
|
||||||
expect(easyParse).toBe('testing-3');
|
|
||||||
});
|
|
||||||
it('handles a mixed operation', () => {
|
|
||||||
const data = {
|
|
||||||
not: {
|
|
||||||
so: {
|
|
||||||
easy: '3',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
other: {
|
|
||||||
value: 42,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const aliases = {
|
|
||||||
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
|
|
||||||
};
|
|
||||||
|
|
||||||
const easyParse = parseTemplateNested('{{other.value}} to {{human.easy}}', data, aliases);
|
|
||||||
expect(easyParse).toBe('42 to testing-3');
|
|
||||||
});
|
|
||||||
it('returns given key when not found', () => {
|
|
||||||
const data = {
|
|
||||||
not: {
|
|
||||||
so: {
|
|
||||||
easy: '3',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
other: {
|
|
||||||
value: 5,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const aliases = {
|
|
||||||
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
|
|
||||||
};
|
|
||||||
|
|
||||||
const easyParse = parseTemplateNested('{{other.value}} to {{human.easy}} {{human.not.found}}', data, aliases);
|
|
||||||
expect(easyParse).toBe('5 to testing-3 {{human.not.found}}');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseNestedTemplate() -> stringToOSCArgs()', () => {
|
|
||||||
it('specific osc requirements', () => {
|
|
||||||
const data = {
|
|
||||||
not: {
|
|
||||||
so: {
|
|
||||||
easy: 'data with space',
|
|
||||||
empty: '',
|
|
||||||
number: 1234,
|
|
||||||
stringNumber: '1234',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const payloads = [
|
|
||||||
{
|
|
||||||
test: '"string with space and {{not.so.easy}}"',
|
|
||||||
expect: [{ type: 'string', value: 'string with space and data with space' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: '',
|
|
||||||
expect: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: ' ',
|
|
||||||
expect: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: '""',
|
|
||||||
expect: [{ type: 'string', value: '' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: '"string with space and {{not.so.empty}}"',
|
|
||||||
expect: [{ type: 'string', value: 'string with space and ' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: '"string with space and {{not.so.number}}"',
|
|
||||||
expect: [{ type: 'string', value: 'string with space and 1234' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: '"string with space and {{not.so.stringNumber}}"',
|
|
||||||
expect: [{ type: 'string', value: 'string with space and 1234' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: '"{{not.so.easy}}" 1',
|
|
||||||
expect: [
|
|
||||||
{ type: 'string', value: 'data with space' },
|
|
||||||
{ type: 'integer', value: 1 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: '"{{not.so.empty}}" 1',
|
|
||||||
expect: [
|
|
||||||
{ type: 'string', value: '' },
|
|
||||||
{ type: 'integer', value: 1 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: '',
|
|
||||||
expect: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
payloads.forEach((payload) => {
|
|
||||||
const parsedPayload = parseTemplateNested(payload.test, data);
|
|
||||||
const parsedArguments = stringToOSCArgs(parsedPayload);
|
|
||||||
expect(parsedArguments).toStrictEqual(payload.expect);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
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: MaybeNumber = null;
|
|
||||||
|
|
||||||
if (value !== 'null') {
|
|
||||||
const parsedValue = Number(value);
|
|
||||||
if (!Number.isNaN(parsedValue)) {
|
|
||||||
valueInNumber = parsedValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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: string) => string }>;
|
|
||||||
const quickAliases: AliasesDefinition = {
|
|
||||||
clock: { key: 'clock', cb: (value: string) => formatDisplayFromString(value) },
|
|
||||||
duration: { key: 'timer.duration', cb: (value: string) => formatDisplayFromString(value, true) },
|
|
||||||
expectedEnd: {
|
|
||||||
key: 'timer.expectedFinish',
|
|
||||||
cb: (value: string) => formatDisplayFromString(value),
|
|
||||||
},
|
|
||||||
runningTimer: {
|
|
||||||
key: 'timer.current',
|
|
||||||
cb: (value: string) => formatDisplayFromString(value, true),
|
|
||||||
},
|
|
||||||
elapsedTime: {
|
|
||||||
key: 'timer.elapsed',
|
|
||||||
cb: (value: string) => formatDisplayFromString(value, true),
|
|
||||||
},
|
|
||||||
startedAt: { key: 'timer.startedAt', cb: (value: string) => formatDisplayFromString(value) },
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a templated string to values in a nested object
|
|
||||||
*/
|
|
||||||
export function parseTemplateNested(template: string, state: object, humanReadable = quickAliases): string {
|
|
||||||
let parsedTemplate = template;
|
|
||||||
const matches = Array.from(parsedTemplate.matchAll(placeholderRegex));
|
|
||||||
|
|
||||||
for (const match of matches) {
|
|
||||||
const variableName = match[1];
|
|
||||||
const variableParts = variableName.split('.');
|
|
||||||
let value: string | undefined = undefined;
|
|
||||||
|
|
||||||
if (variableParts[0] === 'human') {
|
|
||||||
const lookupKey = variableParts[1];
|
|
||||||
if (lookupKey in humanReadable) {
|
|
||||||
const newTemplate = `{{${humanReadable[lookupKey].key}}}`;
|
|
||||||
const parsed = parseTemplateNested(newTemplate, state, humanReadable);
|
|
||||||
value = humanReadable[lookupKey].cb(parsed);
|
|
||||||
} else {
|
|
||||||
value = undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// iterate through variable parts, and look for the property in the state object
|
|
||||||
value = variableParts.reduce((obj, key) => obj?.[key], state);
|
|
||||||
}
|
|
||||||
if (value !== undefined) {
|
|
||||||
parsedTemplate = parsedTemplate.replace(match[0], value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedTemplate;
|
|
||||||
}
|
|
||||||
@@ -28,8 +28,6 @@ import {
|
|||||||
setLastLoadedProject,
|
setLastLoadedProject,
|
||||||
} from '../app-state-service/AppStateService.js';
|
} from '../app-state-service/AppStateService.js';
|
||||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||||
import { oscIntegration } from '../integration-service/OscIntegration.js';
|
|
||||||
import { httpIntegration } from '../integration-service/HttpIntegration.js';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
copyCorruptFile,
|
copyCorruptFile,
|
||||||
@@ -176,14 +174,10 @@ export async function loadProjectFile(name: string) {
|
|||||||
// apply data model
|
// apply data model
|
||||||
runtimeService.stop();
|
runtimeService.stop();
|
||||||
|
|
||||||
const { rundown, customFields, osc, http } = result.data;
|
const { rundown, customFields } = result.data;
|
||||||
|
|
||||||
// apply the rundown
|
// apply the rundown
|
||||||
await initRundown(rundown, customFields);
|
await initRundown(rundown, customFields);
|
||||||
|
|
||||||
// apply integrations
|
|
||||||
oscIntegration.init(osc);
|
|
||||||
httpIntegration.init(http);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -250,14 +244,10 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
|
|||||||
// apply data model
|
// apply data model
|
||||||
runtimeService.stop();
|
runtimeService.stop();
|
||||||
|
|
||||||
const { rundown, customFields, osc, http } = result.data;
|
const { rundown, customFields } = result.data;
|
||||||
|
|
||||||
// apply the rundown
|
// apply the rundown
|
||||||
await initRundown(rundown, customFields);
|
await initRundown(rundown, customFields);
|
||||||
|
|
||||||
// apply integrations
|
|
||||||
oscIntegration.init(osc);
|
|
||||||
httpIntegration.init(http);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {
|
|||||||
getRundown,
|
getRundown,
|
||||||
getTimedEvents,
|
getTimedEvents,
|
||||||
} from '../rundown-service/rundownUtils.js';
|
} from '../rundown-service/rundownUtils.js';
|
||||||
import { integrationService } from '../integration-service/IntegrationService.js';
|
|
||||||
|
|
||||||
import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './rundownService.utils.js';
|
import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './rundownService.utils.js';
|
||||||
import { skippedOutOfEvent } from '../timerUtils.js';
|
import { skippedOutOfEvent } from '../timerUtils.js';
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ describe('test parser edge cases', () => {
|
|||||||
//@ts-expect-error -- we know this is wrong, testing imports outside domain
|
//@ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||||
const { data, errors } = parseDatabaseModel(testData);
|
const { data, errors } = parseDatabaseModel(testData);
|
||||||
expect(data.rundown.length).toBe(1);
|
expect(data.rundown.length).toBe(1);
|
||||||
expect(errors.length).toBe(7);
|
expect(errors.length).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles incomplete datasets', () => {
|
it('handles incomplete datasets', () => {
|
||||||
@@ -724,8 +724,6 @@ describe('test import of v2 datamodel', () => {
|
|||||||
);
|
);
|
||||||
// @ts-expect-error -- checking if the field is removed
|
// @ts-expect-error -- checking if the field is removed
|
||||||
expect(parsed?.userFields).toBeUndefined();
|
expect(parsed?.userFields).toBeUndefined();
|
||||||
expect(parsed.osc).toMatchObject({ subscriptions: [] });
|
|
||||||
expect(parsed.http).toMatchObject({ enabledOut: false, subscriptions: [] });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,8 @@ import {
|
|||||||
CustomFields,
|
CustomFields,
|
||||||
DatabaseModel,
|
DatabaseModel,
|
||||||
EndAction,
|
EndAction,
|
||||||
HttpSettings,
|
|
||||||
HttpSubscription,
|
|
||||||
OSCSettings,
|
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
OscSubscription,
|
|
||||||
Settings,
|
Settings,
|
||||||
SupportedEvent,
|
SupportedEvent,
|
||||||
TimeStrategy,
|
TimeStrategy,
|
||||||
@@ -17,16 +13,12 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
parseCustomFields,
|
parseCustomFields,
|
||||||
parseHttp,
|
|
||||||
parseOsc,
|
|
||||||
parseProject,
|
parseProject,
|
||||||
parseRundown,
|
parseRundown,
|
||||||
parseSettings,
|
parseSettings,
|
||||||
parseUrlPresets,
|
parseUrlPresets,
|
||||||
parseViewSettings,
|
parseViewSettings,
|
||||||
sanitiseCustomFields,
|
sanitiseCustomFields,
|
||||||
sanitiseHttpSubscriptions,
|
|
||||||
sanitiseOscSubscriptions,
|
|
||||||
} from '../parserFunctions.js';
|
} from '../parserFunctions.js';
|
||||||
|
|
||||||
describe('parseRundown()', () => {
|
describe('parseRundown()', () => {
|
||||||
@@ -111,66 +103,6 @@ describe('parseViewSettings()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseOsc()', () => {
|
|
||||||
it('returns an a base model if nothing is given', () => {
|
|
||||||
const errorEmitter = vi.fn();
|
|
||||||
const result = parseOsc({}, errorEmitter);
|
|
||||||
expect(result).toBeTypeOf('object');
|
|
||||||
expect(errorEmitter).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses data, skipping invalid results', () => {
|
|
||||||
const errorEmitter = vi.fn();
|
|
||||||
const osc = {
|
|
||||||
subscriptions: [
|
|
||||||
{ id: '1', cycle: 'onLoad', address: '/test', payload: 'test', enabled: true }, // OK
|
|
||||||
{}, // no data
|
|
||||||
{ id: '2', cycle: 'onStart', payload: 'test', enabled: true }, // no address
|
|
||||||
],
|
|
||||||
} as OSCSettings;
|
|
||||||
const result = parseOsc({ osc }, errorEmitter);
|
|
||||||
expect(result.subscriptions.length).toEqual(1);
|
|
||||||
expect(result.subscriptions.at(0)).toMatchObject({
|
|
||||||
id: '1',
|
|
||||||
cycle: 'onLoad',
|
|
||||||
address: '/test',
|
|
||||||
payload: 'test',
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
expect(errorEmitter).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseHttp()', () => {
|
|
||||||
it('returns an a base model if nothing is given', () => {
|
|
||||||
const errorEmitter = vi.fn();
|
|
||||||
const result = parseHttp({}, errorEmitter);
|
|
||||||
expect(result).toBeTypeOf('object');
|
|
||||||
expect(errorEmitter).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses data, skipping invalid results', () => {
|
|
||||||
const errorEmitter = vi.fn();
|
|
||||||
const http = {
|
|
||||||
subscriptions: [
|
|
||||||
{ id: '1', cycle: 'onLoad', message: 'http://', enabled: true }, // OK
|
|
||||||
{}, // no data
|
|
||||||
{ id: '2', cycle: 'onStart', enabled: true }, // no message
|
|
||||||
{ id: '3', cycle: 'onLoad', message: '/test', enabled: true }, // doesnt start with http
|
|
||||||
],
|
|
||||||
} as HttpSettings;
|
|
||||||
const result = parseHttp({ http }, errorEmitter);
|
|
||||||
expect(result.subscriptions.length).toEqual(1);
|
|
||||||
expect(result.subscriptions.at(0)).toMatchObject({
|
|
||||||
id: '1',
|
|
||||||
cycle: 'onLoad',
|
|
||||||
message: 'http://',
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
expect(errorEmitter).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseUrlPresets()', () => {
|
describe('parseUrlPresets()', () => {
|
||||||
it('returns an a base model if nothing is given', () => {
|
it('returns an a base model if nothing is given', () => {
|
||||||
const errorEmitter = vi.fn();
|
const errorEmitter = vi.fn();
|
||||||
@@ -223,81 +155,6 @@ describe('parseCustomFields()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sanitiseOscSubscriptions()', () => {
|
|
||||||
it('throws if not an array an empty array if not an array', () => {
|
|
||||||
expect(() => sanitiseOscSubscriptions(undefined)).toThrow();
|
|
||||||
// @ts-expect-error -- data is external, we check bad types
|
|
||||||
expect(() => sanitiseOscSubscriptions({})).toThrow();
|
|
||||||
expect(() => sanitiseOscSubscriptions(null)).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns an array of valid entries', () => {
|
|
||||||
const oscSubscriptions: OscSubscription[] = [
|
|
||||||
{ id: '1', cycle: 'onLoad', address: '/test', payload: 'test', enabled: true },
|
|
||||||
{ id: '2', cycle: 'onStart', address: '/test', payload: 'test', enabled: false },
|
|
||||||
{ id: '3', cycle: 'onPause', address: '/test', payload: 'test', enabled: true },
|
|
||||||
{ id: '4', cycle: 'onStop', address: '/test', payload: 'test', enabled: false },
|
|
||||||
{ id: '5', cycle: 'onUpdate', address: '/test', payload: 'test', enabled: true },
|
|
||||||
{ id: '6', cycle: 'onFinish', address: '/test', payload: 'test', enabled: false },
|
|
||||||
{ id: '7', cycle: 'onWarning', address: '/test', payload: 'test', enabled: false },
|
|
||||||
{ id: '8', cycle: 'onDanger', address: '/test', payload: 'test', enabled: false },
|
|
||||||
];
|
|
||||||
const sanitationResult = sanitiseOscSubscriptions(oscSubscriptions);
|
|
||||||
expect(sanitationResult).toStrictEqual(oscSubscriptions);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filters invalid entries', () => {
|
|
||||||
const oscSubscriptions = [
|
|
||||||
{ id: '1', cycle: 'onLoad', address: 4, payload: 'test', enabled: true },
|
|
||||||
{ cycle: 'onLoad', payload: 'test', enabled: true },
|
|
||||||
{ id: '2', cycle: 'unknown', payload: 'test', enabled: false },
|
|
||||||
{ id: '3', payload: 'test', enabled: true },
|
|
||||||
{ id: '4', cycle: 'onStop', enabled: false },
|
|
||||||
{ id: '5', cycle: 'onUpdate', payload: 'test' },
|
|
||||||
{ id: '6', cycle: 'onFinish', payload: 'test', enabled: 'true' },
|
|
||||||
] as OscSubscription[];
|
|
||||||
const sanitationResult = sanitiseOscSubscriptions(oscSubscriptions);
|
|
||||||
expect(sanitationResult.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sanitiseHttpSubscriptions()', () => {
|
|
||||||
it('throws if the data is unexpected', () => {
|
|
||||||
expect(() => sanitiseHttpSubscriptions(undefined)).toThrow();
|
|
||||||
// @ts-expect-error -- data is external, we check bad types
|
|
||||||
expect(() => sanitiseHttpSubscriptions({})).toThrow();
|
|
||||||
expect(() => sanitiseHttpSubscriptions(null)).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns an array of valid entries', () => {
|
|
||||||
const httpSubscription: HttpSubscription[] = [
|
|
||||||
{ id: '1', cycle: 'onLoad', message: 'http://test', enabled: true },
|
|
||||||
{ id: '2', cycle: 'onStart', message: 'http://test', enabled: false },
|
|
||||||
{ id: '3', cycle: 'onPause', message: 'http://test', enabled: true },
|
|
||||||
{ id: '4', cycle: 'onStop', message: 'http://test', enabled: false },
|
|
||||||
{ id: '5', cycle: 'onUpdate', message: 'http://test', enabled: true },
|
|
||||||
{ id: '6', cycle: 'onFinish', message: 'http://test', enabled: false },
|
|
||||||
{ id: '7', cycle: 'onWarning', message: 'http://test', enabled: false },
|
|
||||||
{ id: '8', cycle: 'onDanger', message: 'http://test', enabled: false },
|
|
||||||
];
|
|
||||||
const sanitationResult = sanitiseHttpSubscriptions(httpSubscription);
|
|
||||||
expect(sanitationResult).toStrictEqual(httpSubscription);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filters invalid entries', () => {
|
|
||||||
const httpSubscription = [
|
|
||||||
{ cycle: 'onLoad', message: 'http://test', enabled: true },
|
|
||||||
{ id: '2', cycle: 'unknown', message: 'http://test', enabled: false },
|
|
||||||
{ id: '3', message: 'http://test', enabled: true },
|
|
||||||
{ id: '4', cycle: 'onStop', enabled: false },
|
|
||||||
{ id: '5', cycle: 'onUpdate', message: 'http://test' },
|
|
||||||
{ id: '6', cycle: 'onFinish', message: 'ftp://test', enabled: 'true' },
|
|
||||||
];
|
|
||||||
const sanitationResult = sanitiseHttpSubscriptions(httpSubscription as HttpSubscription[]);
|
|
||||||
expect(sanitationResult.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sanitiseCustomFields()', () => {
|
describe('sanitiseCustomFields()', () => {
|
||||||
it('returns an empty array if not an array', () => {
|
it('returns an empty array if not an array', () => {
|
||||||
expect(sanitiseCustomFields({})).toEqual({});
|
expect(sanitiseCustomFields({})).toEqual({});
|
||||||
|
|||||||
@@ -27,15 +27,7 @@ import { logger } from '../classes/Logger.js';
|
|||||||
import { event as eventDef } from '../models/eventsDefinition.js';
|
import { event as eventDef } from '../models/eventsDefinition.js';
|
||||||
|
|
||||||
import { makeString } from './parserUtils.js';
|
import { makeString } from './parserUtils.js';
|
||||||
import {
|
import { parseProject, parseRundown, parseSettings, parseUrlPresets, parseViewSettings } from './parserFunctions.js';
|
||||||
parseHttp,
|
|
||||||
parseOsc,
|
|
||||||
parseProject,
|
|
||||||
parseRundown,
|
|
||||||
parseSettings,
|
|
||||||
parseUrlPresets,
|
|
||||||
parseViewSettings,
|
|
||||||
} from './parserFunctions.js';
|
|
||||||
import { parseExcelDate } from './time.js';
|
import { parseExcelDate } from './time.js';
|
||||||
|
|
||||||
export type ErrorEmitter = (message: string) => void;
|
export type ErrorEmitter = (message: string) => void;
|
||||||
@@ -334,8 +326,6 @@ export function parseDatabaseModel(jsonData: Partial<DatabaseModel>): { data: Da
|
|||||||
viewSettings: parseViewSettings(jsonData, makeEmitError('View Settings')),
|
viewSettings: parseViewSettings(jsonData, makeEmitError('View Settings')),
|
||||||
urlPresets: parseUrlPresets(jsonData, makeEmitError('URL Presets')),
|
urlPresets: parseUrlPresets(jsonData, makeEmitError('URL Presets')),
|
||||||
customFields,
|
customFields,
|
||||||
osc: parseOsc(jsonData, makeEmitError('OSC')),
|
|
||||||
http: parseHttp(jsonData, makeEmitError('HTTP')),
|
|
||||||
automation: parseAutomationSettings(jsonData),
|
automation: parseAutomationSettings(jsonData),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,25 +2,20 @@ import {
|
|||||||
CustomField,
|
CustomField,
|
||||||
CustomFields,
|
CustomFields,
|
||||||
DatabaseModel,
|
DatabaseModel,
|
||||||
HttpSettings,
|
|
||||||
HttpSubscription,
|
|
||||||
OSCSettings,
|
|
||||||
OntimeBlock,
|
OntimeBlock,
|
||||||
OntimeDelay,
|
OntimeDelay,
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
OscSubscription,
|
|
||||||
ProjectData,
|
ProjectData,
|
||||||
Settings,
|
Settings,
|
||||||
TimerType,
|
TimerType,
|
||||||
URLPreset,
|
URLPreset,
|
||||||
ViewSettings,
|
ViewSettings,
|
||||||
isOntimeBlock,
|
isOntimeBlock,
|
||||||
isOntimeCycle,
|
|
||||||
isOntimeDelay,
|
isOntimeDelay,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { customFieldLabelToKey, generateId, getErrorMessage, isAlphanumericWithSpace } from 'ontime-utils';
|
import { customFieldLabelToKey, generateId, isAlphanumericWithSpace } from 'ontime-utils';
|
||||||
|
|
||||||
import { dbModel } from '../models/dataModel.js';
|
import { dbModel } from '../models/dataModel.js';
|
||||||
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
|
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
|
||||||
@@ -165,102 +160,6 @@ export function parseViewSettings(data: Partial<DatabaseModel>, emitError?: Erro
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitises an OSC Subscriptions array
|
|
||||||
*/
|
|
||||||
export function sanitiseOscSubscriptions(subscriptions?: OscSubscription[]): OscSubscription[] {
|
|
||||||
if (!Array.isArray(subscriptions)) {
|
|
||||||
throw new Error('ERROR: invalid OSC subscriptions');
|
|
||||||
}
|
|
||||||
|
|
||||||
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 function parseOsc(data: Partial<DatabaseModel>, emitError?: ErrorEmitter): OSCSettings {
|
|
||||||
if (!data.osc) {
|
|
||||||
emitError?.('No data found to import');
|
|
||||||
return { ...dbModel.osc };
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Found OSC settings, importing...');
|
|
||||||
|
|
||||||
let newSubscriptions: OscSubscription[] = [];
|
|
||||||
try {
|
|
||||||
newSubscriptions = sanitiseOscSubscriptions(data.osc.subscriptions);
|
|
||||||
} catch (error) {
|
|
||||||
emitError?.(getErrorMessage(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSubscriptions.length !== data.osc.subscriptions.length) {
|
|
||||||
emitError?.('Skipped invalid subscriptions');
|
|
||||||
}
|
|
||||||
|
|
||||||
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: newSubscriptions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitises an HTTP Subscriptions array
|
|
||||||
*/
|
|
||||||
export function sanitiseHttpSubscriptions(subscriptions?: HttpSubscription[]): HttpSubscription[] {
|
|
||||||
if (!Array.isArray(subscriptions)) {
|
|
||||||
throw new Error('ERROR: invalid HTTP subscriptions');
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
export function parseHttp(data: Partial<DatabaseModel>, emitError?: ErrorEmitter): HttpSettings {
|
|
||||||
if (!data.http) {
|
|
||||||
emitError?.('No data found to import');
|
|
||||||
return { ...dbModel.http };
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Found HTTP settings, importing...');
|
|
||||||
|
|
||||||
let newSubscriptions: HttpSubscription[] = [];
|
|
||||||
try {
|
|
||||||
newSubscriptions = sanitiseHttpSubscriptions(data.http.subscriptions);
|
|
||||||
} catch (error) {
|
|
||||||
emitError?.(getErrorMessage(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSubscriptions.length !== data.http?.subscriptions.length) {
|
|
||||||
emitError?.('Skipped invalid subscriptions');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabledOut: data.http.enabledOut ?? dbModel.http.enabledOut,
|
|
||||||
subscriptions: newSubscriptions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse URL preset portion of an entry
|
* Parse URL preset portion of an entry
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -436,17 +436,12 @@
|
|||||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"osc": {
|
"automation": {
|
||||||
"portIn": 8888,
|
"enabledAutomations": false,
|
||||||
"portOut": 9999,
|
"enabledOscIn": true,
|
||||||
"targetIP": "127.0.0.1",
|
"oscPortIn": 8888,
|
||||||
"enabledIn": true,
|
"automations": [],
|
||||||
"enabledOut": true,
|
"blueprints": {}
|
||||||
"subscriptions": []
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"enabledOut": true,
|
|
||||||
"subscriptions": []
|
|
||||||
},
|
},
|
||||||
"customFields": {
|
"customFields": {
|
||||||
"song": {
|
"song": {
|
||||||
|
|||||||
Vendored
+6
-11
@@ -436,17 +436,12 @@
|
|||||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"osc": {
|
"automation": {
|
||||||
"portIn": 8888,
|
"enabledAutomations": false,
|
||||||
"portOut": 9999,
|
"enabledOscIn": true,
|
||||||
"targetIP": "127.0.0.1",
|
"oscPortIn": 8888,
|
||||||
"enabledIn": true,
|
"automations": [],
|
||||||
"enabledOut": true,
|
"blueprints": {}
|
||||||
"subscriptions": []
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"enabledOut": true,
|
|
||||||
"subscriptions": []
|
|
||||||
},
|
},
|
||||||
"customFields": {
|
"customFields": {
|
||||||
"song": {
|
"song": {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { OSCSettings } from '../../definitions/core/OscSettings.type.js';
|
|
||||||
import type { OntimeRundown } from '../../definitions/core/Rundown.type.js';
|
import type { OntimeRundown } from '../../definitions/core/Rundown.type.js';
|
||||||
import type { Playback } from '../../definitions/runtime/Playback.type.js';
|
import type { Playback } from '../../definitions/runtime/Playback.type.js';
|
||||||
import type { MaybeString } from '../../utils/utils.type.js';
|
import type { MaybeString } from '../../utils/utils.type.js';
|
||||||
@@ -22,7 +21,6 @@ export interface GetInfo {
|
|||||||
networkInterfaces: NetworkInterface[];
|
networkInterfaces: NetworkInterface[];
|
||||||
version: string;
|
version: string;
|
||||||
serverPort: number;
|
serverPort: number;
|
||||||
osc: OSCSettings;
|
|
||||||
publicDir: string;
|
publicDir: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
AutomationSettings,
|
AutomationSettings,
|
||||||
CustomFields,
|
CustomFields,
|
||||||
HttpSettings,
|
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
OSCSettings,
|
|
||||||
ProjectData,
|
ProjectData,
|
||||||
Settings,
|
Settings,
|
||||||
URLPreset,
|
URLPreset,
|
||||||
@@ -17,7 +15,5 @@ export type DatabaseModel = {
|
|||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
urlPresets: URLPreset[];
|
urlPresets: URLPreset[];
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
osc: OSCSettings;
|
|
||||||
http: HttpSettings;
|
|
||||||
automation: AutomationSettings;
|
automation: AutomationSettings;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import type { TimerLifeCycleKey } from './TimerLifecycle.type.js';
|
|
||||||
|
|
||||||
export type HttpSubscription = { id: string; cycle: TimerLifeCycleKey; message: string; enabled: boolean };
|
|
||||||
|
|
||||||
export interface HttpSettings {
|
|
||||||
enabledOut: boolean;
|
|
||||||
subscriptions: HttpSubscription[];
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { TimerLifeCycleKey } from './TimerLifecycle.type.js';
|
|
||||||
|
|
||||||
export type OscSubscription = {
|
|
||||||
id: string;
|
|
||||||
cycle: TimerLifeCycleKey;
|
|
||||||
address: string;
|
|
||||||
payload: string; // TODO: we should be using arguments to keep in line with protocol language
|
|
||||||
enabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface OSCSettings {
|
|
||||||
portIn: number;
|
|
||||||
portOut: number;
|
|
||||||
targetIP: string;
|
|
||||||
enabledIn: boolean;
|
|
||||||
enabledOut: boolean;
|
|
||||||
subscriptions: OscSubscription[];
|
|
||||||
}
|
|
||||||
@@ -52,10 +52,6 @@ export type {
|
|||||||
EventCustomFields,
|
EventCustomFields,
|
||||||
} from './definitions/core/CustomFields.type.js';
|
} from './definitions/core/CustomFields.type.js';
|
||||||
|
|
||||||
// ---> Integration, Subscription
|
|
||||||
export type { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js';
|
|
||||||
export type { HttpSettings, HttpSubscription } from './definitions/core/HttpSettings.type.js';
|
|
||||||
|
|
||||||
// SERVER RESPONSES
|
// SERVER RESPONSES
|
||||||
export type {
|
export type {
|
||||||
AuthenticationStatus,
|
AuthenticationStatus,
|
||||||
|
|||||||
Reference in New Issue
Block a user