mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 11:23:50 +00:00
refactor: v2 event loader (#260)
* refactor: extract jest config * refactor: rename params in reoder endpoint * refactor: avoid app exports * refactor: small code smells * refactor: run development server * refactor: extract rundown service and event loader logic * refactor: rename events > rundown * refactor: code style * fix: handle external change of title * refactor: cleanup dictionary * refactor: rename MessageService * refactor: use eventID for operations * refactor: migrate OSC controller to service * refactor: migrate Socket controller to service * refactor: migrate HTTP controller to service * refactor: extract logic into discrete services * refactor: remove rundown from event timer * refactor: remove duplicate * refactor: remove unused * chore: update tests
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { RUNDOWN_TABLE_KEY,RUNDOWN_TABLE } from '../api/apiConstants';
|
||||
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
|
||||
import {
|
||||
requestApplyDelay,
|
||||
requestDelete,
|
||||
@@ -106,7 +106,7 @@ export const useEventAction = () => {
|
||||
emitError(`Error updating event: ${error.message}`);
|
||||
}
|
||||
},
|
||||
[_updateEventMutation, emitError]
|
||||
[_updateEventMutation, emitError],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -272,7 +272,7 @@ export const useEventAction = () => {
|
||||
async (eventId, from, to) => {
|
||||
try {
|
||||
const reorderObject = {
|
||||
index: eventId,
|
||||
eventId: eventId,
|
||||
from: from,
|
||||
to: to,
|
||||
};
|
||||
|
||||
@@ -193,51 +193,51 @@ export default function Rundown(props) {
|
||||
<Droppable droppableId='eventlist'>
|
||||
{(provided) => (
|
||||
<div className={style.list} {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{entries.map((e, index) => {
|
||||
{entries.map((entry, index) => {
|
||||
if (index === 0) {
|
||||
cumulativeDelay = 0;
|
||||
eventIndex = -1;
|
||||
}
|
||||
if (e.type === 'delay' && e.duration != null) {
|
||||
cumulativeDelay += e.duration;
|
||||
} else if (e.type === 'block') {
|
||||
if (entry.type === 'delay' && entry.duration != null) {
|
||||
cumulativeDelay += entry.duration;
|
||||
} else if (entry.type === 'block') {
|
||||
cumulativeDelay = 0;
|
||||
} else if (e.type === 'event') {
|
||||
} else if (entry.type === 'event') {
|
||||
eventIndex++;
|
||||
previousEnd = thisEnd;
|
||||
thisEnd = e.timeEnd;
|
||||
previousEventId = e.id;
|
||||
thisEnd = entry.timeEnd;
|
||||
previousEventId = entry.id;
|
||||
}
|
||||
const isLast = index === entries.length - 1;
|
||||
return (
|
||||
<div
|
||||
key={e.id}
|
||||
key={entry.id}
|
||||
className={`${style.bgElement}
|
||||
${e.type === 'event' && cumulativeDelay !== 0 ? style.delayed : ''}`}
|
||||
${entry.type === 'event' && cumulativeDelay !== 0 ? style.delayed : ''}`}
|
||||
>
|
||||
<div
|
||||
ref={cursor === index ? cursorRef : undefined}
|
||||
className={cursor === index ? style.cursor : ''}
|
||||
>
|
||||
<RundownEntry
|
||||
type={e.type}
|
||||
type={entry.type}
|
||||
index={index}
|
||||
eventIndex={eventIndex}
|
||||
data={e}
|
||||
selected={selectedId === e.id}
|
||||
next={nextId === e.id}
|
||||
data={entry}
|
||||
selected={selectedId === entry.id}
|
||||
next={nextId === entry.id}
|
||||
delay={cumulativeDelay}
|
||||
previousEnd={previousEnd}
|
||||
playback={selectedId === e.id ? data.playback : undefined}
|
||||
playback={selectedId === entry.id ? data.playback : undefined}
|
||||
/>
|
||||
</div>
|
||||
{((showQuickEntry && index === cursor) || isLast) && (
|
||||
<QuickAddBlock
|
||||
showKbd={index === cursor}
|
||||
previousId={e.id}
|
||||
previousId={entry.id}
|
||||
previousEventId={previousEventId}
|
||||
disableAddDelay={e.type === 'delay'}
|
||||
disableAddBlock={e.type === 'block'}
|
||||
disableAddDelay={entry.type === 'delay'}
|
||||
disableAddBlock={entry.type === 'block'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/react';
|
||||
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
|
||||
@@ -85,6 +85,11 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
const binderColours = colour && getAccessibleColour(colour);
|
||||
const hasDelay = delay !== 0 && delay !== null;
|
||||
|
||||
// Todo: could I re-render the item without causing a state change here?
|
||||
useEffect(() => {
|
||||
setBlockTitle(title);
|
||||
}, [title]);
|
||||
|
||||
const handleTitle = useCallback(
|
||||
(text: string) => {
|
||||
if (text === title) {
|
||||
@@ -107,7 +112,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
if (!skip && eventIsPlaying) {
|
||||
playBtnStyles._hover = { bg: '#c05621' };
|
||||
} else if (!skip && !eventIsPlaying) {
|
||||
playBtnStyles._hover = { };
|
||||
playBtnStyles._hover = {};
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,7 +14,7 @@ import { makeCSV, makeTable } from './utils';
|
||||
import style from './Table.module.scss';
|
||||
|
||||
export default function TableWrapper() {
|
||||
const { data: events } = useRundown();
|
||||
const { data: rundown } = useRundown();
|
||||
const { data: userFields } = useUserFields();
|
||||
const { data: featureData } = useCuesheet();
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function TableWrapper() {
|
||||
}
|
||||
|
||||
// check if value is the same
|
||||
const event = events[rowIndex];
|
||||
const event = rundown[rowIndex];
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
@@ -59,15 +59,15 @@ export default function TableWrapper() {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [mutation, events]);
|
||||
}, [mutation, rundown]);
|
||||
|
||||
const exportHandler = useCallback(
|
||||
(headerData) => {
|
||||
if (!headerData || !events || !userFields) {
|
||||
if (!headerData || !rundown || !userFields) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sheetData = makeTable(headerData, events, userFields);
|
||||
const sheetData = makeTable(headerData, rundown, userFields);
|
||||
const csvContent = makeCSV(sheetData);
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement('a');
|
||||
@@ -76,10 +76,10 @@ export default function TableWrapper() {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
},
|
||||
[events, userFields]
|
||||
[rundown, userFields]
|
||||
);
|
||||
|
||||
if (typeof events === 'undefined' || typeof userFields === 'undefined') {
|
||||
if (typeof rundown === 'undefined' || typeof userFields === 'undefined') {
|
||||
return <span>loading...</span>;
|
||||
}
|
||||
return (
|
||||
@@ -89,7 +89,7 @@ export default function TableWrapper() {
|
||||
>
|
||||
<TableHeader handleCSVExport={exportHandler} featureData={featureData} />
|
||||
<OntimeTable
|
||||
tableData={events}
|
||||
tableData={rundown}
|
||||
userFields={userFields}
|
||||
handleUpdate={handleUpdate}
|
||||
selectedId={featureData.selectedEventId}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const config = {
|
||||
verbose: true,
|
||||
testEnvironment: 'node',
|
||||
rootDir: 'src',
|
||||
transform: {},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -25,6 +25,9 @@ const nodePath = isProduction
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const dbLoader = await import('./src/modules/loadDb.js');
|
||||
|
||||
await dbLoader.promise;
|
||||
const { startServer, startOSCServer } = await import(nodePath);
|
||||
// Start express server
|
||||
loaded = await startServer();
|
||||
|
||||
+1
-5
@@ -32,7 +32,7 @@
|
||||
"prep": "yarn clean && yarn setdb",
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
||||
"start": "NODE_ENV=development electron .",
|
||||
"start:server": "nodemon --experimental-modules --es-module-specifier-resolution=node src/run.js",
|
||||
"start:server": "NODE_ENV=development nodemon --experimental-modules --es-module-specifier-resolution=node src/run.js",
|
||||
"e2e": "set IS_TEST=true && playwright test",
|
||||
"pack": "electron-builder --dir",
|
||||
"dist": "electron-builder",
|
||||
@@ -41,10 +41,6 @@
|
||||
"dist-linux": "electron-builder --publish=never --x64 --linux",
|
||||
"dist-all": "electron-builder -mw"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"build": {
|
||||
"productName": "ontime",
|
||||
"appId": "no.lightdev.ontime",
|
||||
|
||||
+11
-16
@@ -6,8 +6,7 @@ import { config } from './config/config.js';
|
||||
|
||||
// import dependencies
|
||||
import { dirname, join, resolve } from 'path';
|
||||
// init database
|
||||
import loadDb from './modules/loadDb.js';
|
||||
|
||||
// dependencies
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
@@ -22,6 +21,7 @@ import { router as playbackRouter } from './routes/playbackRouter.js';
|
||||
// Global Objects
|
||||
import { EventTimer } from './classes/timer/EventTimer.js';
|
||||
import { socketProvider } from './classes/socket/SocketController.js';
|
||||
|
||||
// Start OSC server
|
||||
import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
@@ -33,7 +33,6 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const isTest = process.env.IS_TEST;
|
||||
|
||||
export const { db, data } = await loadDb(__dirname);
|
||||
console.log(`Starting ontime version ${process.env.npm_package_version}`);
|
||||
|
||||
// import socket provider
|
||||
@@ -74,14 +73,12 @@ const resolvedPath = () => {
|
||||
app.use(express.static(join(__dirname, resolvedPath(), 'client/build')));
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(
|
||||
resolve(__dirname, resolvedPath(), 'client', 'build', 'index.html'),
|
||||
);
|
||||
res.sendFile(resolve(__dirname, resolvedPath(), 'client', 'build', 'index.html'));
|
||||
});
|
||||
|
||||
// Implement route for errors
|
||||
app.use((err, req, res, next) => {
|
||||
res.status(500).send(err.stack);
|
||||
// Implement catch all
|
||||
app.use((error, response, _next) => {
|
||||
response.status(400).send('Unhandled request');
|
||||
});
|
||||
|
||||
/*************** START SERVICES ***************/
|
||||
@@ -94,12 +91,12 @@ app.use((err, req, res, next) => {
|
||||
*
|
||||
*/
|
||||
|
||||
const { osc, settings } = DataProvider.getData();
|
||||
const { osc } = DataProvider.getData();
|
||||
const oscIP = osc?.targetIP || config.osc.targetIP;
|
||||
const oscOutPort = osc?.portOut || config.osc.portOut;
|
||||
const oscInPort = osc?.port || config.osc.port;
|
||||
const oscInEnabled = osc?.enabled !== undefined ? osc.enabled : config.osc.inputEnabled;
|
||||
const serverPort = settings.serverPort || config.server.port;
|
||||
const serverPort = 4001; // hardcoded for now
|
||||
|
||||
/**
|
||||
* @description starts OSC server
|
||||
@@ -132,12 +129,11 @@ const server = http.createServer(app);
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
export const startServer = async (overrideConfig = null) => {
|
||||
const port = 4001; // port hardcoded
|
||||
const { rundown, http } = DataProvider.getData();
|
||||
const { http } = DataProvider.getData();
|
||||
|
||||
// Start server
|
||||
const returnMessage = `Ontime is listening on port ${port}`;
|
||||
server.listen(port, '0.0.0.0');
|
||||
const returnMessage = `Ontime is listening on port ${serverPort}`;
|
||||
server.listen(serverPort, '0.0.0.0');
|
||||
|
||||
// init socket controller
|
||||
await socket.initServer(server);
|
||||
@@ -151,7 +147,6 @@ export const startServer = async (overrideConfig = null) => {
|
||||
|
||||
// init timer
|
||||
global.timer = new EventTimer(socket, config.timer, oscConfig, http);
|
||||
global.timer.setupWithEventList(rundown.filter((entry) => entry.type === 'event'));
|
||||
|
||||
socket.info('SERVER', returnMessage);
|
||||
socket.startListener();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Class Event Provider is a mediator for handling the local db
|
||||
* and adds logic specific to ontime data
|
||||
*/
|
||||
import { data, db } from '../../app.js';
|
||||
import { db, data } from '../../modules/loadDb.js';
|
||||
|
||||
export class DataProvider {
|
||||
static getData() {
|
||||
@@ -96,7 +96,7 @@ export class DataProvider {
|
||||
static async insertEventAfterId(entry, id) {
|
||||
const index = [...data.rundown].findIndex((event) => event.id === id);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { _after, ...sanitisedEvent } = entry;
|
||||
const { after, ...sanitisedEvent } = entry;
|
||||
await DataProvider.insertEventAt(sanitisedEvent, index + 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
import { DataProvider } from '../data-provider/DataProvider.js';
|
||||
import { getSelectionByRoll } from '../timer/classUtils.js';
|
||||
import { Timer } from '../timer/Timer.js';
|
||||
|
||||
let instance;
|
||||
|
||||
/**
|
||||
* Manages business logic around loading events
|
||||
*/
|
||||
export class EventLoader {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
throw new Error('There can be only one');
|
||||
}
|
||||
instance = this;
|
||||
this.reset();
|
||||
this.loadedEvent = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns all events that contain time data
|
||||
* @return {array}
|
||||
*/
|
||||
static getTimedEvents() {
|
||||
// return mockLoaderData.filter((event) => event.type === 'event');
|
||||
return DataProvider.getRundown().filter((event) => event.type === 'event');
|
||||
}
|
||||
|
||||
/**
|
||||
* returns all events that can be loaded
|
||||
* @return {array}
|
||||
*/
|
||||
static getPlayableEvents() {
|
||||
// return mockLoaderData.filter((event) => event.type === 'event' && !event.skip);
|
||||
return DataProvider.getRundown().filter((event) => event.type === 'event' && !event.skip);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns number of events
|
||||
* @return {number}
|
||||
*/
|
||||
static getNumEvents() {
|
||||
return EventLoader.getTimedEvents().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an event given its index
|
||||
* @param {number} eventIndex
|
||||
* @return {object | undefined}
|
||||
*/
|
||||
static getEventAtIndex(eventIndex) {
|
||||
const timedEvents = EventLoader.getTimedEvents();
|
||||
return timedEvents?.[eventIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an event given its index
|
||||
* @param {number} eventIndex
|
||||
* @return {object | undefined}
|
||||
*/
|
||||
static getPlayableAtIndex(eventIndex) {
|
||||
const timedEvents = EventLoader.getPlayableEvents();
|
||||
return timedEvents?.[eventIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an event given its id
|
||||
* @param {string} eventId
|
||||
* @return {object | undefined}
|
||||
*/
|
||||
static getEventWithId(eventId) {
|
||||
const timedEvents = EventLoader.getTimedEvents();
|
||||
return timedEvents.find((event) => event.id === eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* loads an event given its id
|
||||
* @param {string} eventId
|
||||
* @returns {{loadedEvent: null, selectedEventId: null, nextEventId: null, loadedEventId: *, selectedPublicEventId: null, nextPublicEventId: null, numEvents: null, titles: {presenterNext: null, titleNow: null, subtitleNow: null, titleNext: null, subtitleNext: null, presenterNow: null, noteNow: null, noteNext: null}, titlesPublic: {presenterNext: null, titleNow: null, subtitleNow: null, titleNext: null, subtitleNext: null, presenterNow: null}, selectedEventIndex: null}}
|
||||
*/
|
||||
loadById(eventId) {
|
||||
const event = EventLoader.getEventWithId(eventId);
|
||||
return this._loadEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* loads an event given its index
|
||||
* @param {number} eventIndex
|
||||
* @returns {{loadedEvent: null, selectedEventId: null, nextEventId: null, loadedEventId: *, selectedPublicEventId: null, nextPublicEventId: null, numEvents: null, titles: {presenterNext: null, titleNow: null, subtitleNow: null, titleNext: null, subtitleNext: null, presenterNow: null, noteNow: null, noteNext: null}, titlesPublic: {presenterNext: null, titleNow: null, subtitleNow: null, titleNext: null, subtitleNext: null, presenterNow: null}, selectedEventIndex: null}}
|
||||
*/
|
||||
loadByIndex(eventIndex) {
|
||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
||||
return this._loadEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* finds the ID of the previous event
|
||||
* @returns {{id: string}|null}
|
||||
*/
|
||||
findPrevious() {
|
||||
const timedEvents = EventLoader.getPlayableEvents();
|
||||
if (timedEvents === null || this.selectedEventIndex === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (this.selectedEventIndex === null) {
|
||||
return { id: timedEvents[0].id };
|
||||
} else {
|
||||
const newIndex = this.selectedEventIndex - 1;
|
||||
return { id: timedEvents?.[newIndex].id };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* finds the ID of the next event
|
||||
* @returns {{id: string}|null}
|
||||
*/
|
||||
findNext() {
|
||||
const timedEvents = EventLoader.getPlayableEvents();
|
||||
if (timedEvents === null || this.selectedEventIndex === this.numEvents - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (this.selectedEventIndex === null) {
|
||||
return { id: timedEvents[0].id };
|
||||
} else {
|
||||
const newIndex = this.selectedEventIndex + 1;
|
||||
return { id: timedEvents?.[newIndex].id };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* finds next event within Roll context
|
||||
* @returns {{nowIndex: null, timers: null, nowId: null, publicNextIndex: null, nextIndex: null, timeToNext: null, publicIndex: null}|{nowIndex: null, timers: null, nowId: null, publicNextIndex: null, nextIndex: null, timeToNext: null, publicIndex: null}}
|
||||
*/
|
||||
findRoll() {
|
||||
const timedEvents = EventLoader.getPlayableEvents();
|
||||
const millisNow = Timer.getCurrentTime();
|
||||
return getSelectionByRoll(timedEvents, millisNow);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns data for currently loaded event
|
||||
* @returns {{loadedEvent: null, selectedEventId: (null|*), nextEventId: (null|*), loadedEventId, selectedPublicEventId: (null|*), nextPublicEventId: (null|*), numEvents: (null|number|*), titles: (*|{presenterNext: null, titleNow: null, subtitleNow: null, titleNext: null, subtitleNext: null, presenterNow: null, noteNow: null, noteNext: null}), titlesPublic: (*|{presenterNext: null, titleNow: null, subtitleNow: null, titleNext: null, subtitleNext: null, presenterNow: null}), selectedEventIndex: (null|number|*)}}
|
||||
*/
|
||||
getLoaded() {
|
||||
return {
|
||||
loadedEvent: this.loadedEvent,
|
||||
loadedEventId: this.loadedEventId,
|
||||
selectedEventIndex: this.selectedEventIndex,
|
||||
selectedEventId: this.selectedEventId,
|
||||
selectedPublicEventId: this.selectedPublicEventId,
|
||||
nextEventId: this.nextEventId,
|
||||
nextPublicEventId: this.nextPublicEventId,
|
||||
numEvents: this.numEvents,
|
||||
titles: this.titles,
|
||||
titlesPublic: this.titlesPublic,
|
||||
};
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.loadedEvent = null;
|
||||
this.selectedEventIndex = null;
|
||||
this.selectedEventId = null;
|
||||
this.selectedPublicEventId = null;
|
||||
this.nextEventId = null;
|
||||
this.nextPublicEventId = null;
|
||||
this.numEvents = null;
|
||||
this.titles = {
|
||||
titleNow: null,
|
||||
subtitleNow: null,
|
||||
presenterNow: null,
|
||||
noteNow: null,
|
||||
titleNext: null,
|
||||
subtitleNext: null,
|
||||
presenterNext: null,
|
||||
noteNext: null,
|
||||
};
|
||||
this.titlesPublic = {
|
||||
titleNow: null,
|
||||
subtitleNow: null,
|
||||
presenterNow: null,
|
||||
titleNext: null,
|
||||
subtitleNext: null,
|
||||
presenterNext: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* loads an event given its id
|
||||
* @param {object} event
|
||||
*/
|
||||
_loadEvent(event) {
|
||||
if (typeof event === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
const timedEvents = EventLoader.getPlayableEvents();
|
||||
const eventIndex = timedEvents.findIndex((eventInMemory) => eventInMemory.id === event.id);
|
||||
const playableEvents = EventLoader.getPlayableEvents();
|
||||
|
||||
// we know some stuff now
|
||||
this.loadedEvent = event;
|
||||
this.selectedEventIndex = eventIndex;
|
||||
this.loadedEventId = event.id;
|
||||
this.numEvents = timedEvents.length;
|
||||
this._loadTitlesNow(event, playableEvents);
|
||||
this._loadTitlesNext(playableEvents);
|
||||
|
||||
return this.getLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description loads given title (now)
|
||||
* @private
|
||||
* @param {object} event
|
||||
* @param {array} rundown
|
||||
*/
|
||||
_loadTitlesNow(event, rundown) {
|
||||
// private title is always current
|
||||
// check if current is also public
|
||||
if (event.isPublic) {
|
||||
this._loadThisTitles(event, 'now');
|
||||
} else {
|
||||
this._loadThisTitles(event, 'now-private');
|
||||
|
||||
// assume there is no public event
|
||||
this.titlesPublic.titleNow = null;
|
||||
this.titlesPublic.subtitleNow = null;
|
||||
this.titlesPublic.presenterNow = null;
|
||||
this.selectedPublicEventId = null;
|
||||
|
||||
// if there is nothing before, return
|
||||
if (this.selectedEventIndex === 0) return;
|
||||
|
||||
// iterate backwards to find it
|
||||
for (let i = this.selectedEventIndex; i >= 0; i--) {
|
||||
if (rundown[i].isPublic) {
|
||||
this._loadThisTitles(rundown[i], 'now-public');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description look for next titles to load
|
||||
* @private
|
||||
*/
|
||||
_loadTitlesNext(rundown) {
|
||||
// Todo: is there a scenario where this gets called without an event?
|
||||
// maybe there is nothing to load
|
||||
if (this.selectedEventIndex === null) return;
|
||||
|
||||
// assume there is no next event
|
||||
this.titles.titleNext = null;
|
||||
this.titles.subtitleNext = null;
|
||||
this.titles.presenterNext = null;
|
||||
this.titles.noteNext = null;
|
||||
this.nextEventId = null;
|
||||
|
||||
this.titlesPublic.titleNext = null;
|
||||
this.titlesPublic.subtitleNext = null;
|
||||
this.titlesPublic.presenterNext = null;
|
||||
this.nextPublicEventId = null;
|
||||
|
||||
const numEvents = rundown.length;
|
||||
|
||||
if (this.selectedEventIndex < numEvents - 1) {
|
||||
let nextPublic = false;
|
||||
let nextPrivate = false;
|
||||
|
||||
for (let i = this.selectedEventIndex + 1; i < numEvents; i++) {
|
||||
// if we have not set private
|
||||
if (!nextPrivate) {
|
||||
this._loadThisTitles(rundown[i], 'next-private');
|
||||
nextPrivate = true;
|
||||
}
|
||||
|
||||
// if event is public
|
||||
if (rundown[i].isPublic) {
|
||||
this._loadThisTitles(rundown[i], 'next-public');
|
||||
nextPublic = true;
|
||||
}
|
||||
|
||||
// Stop if both are set
|
||||
if (nextPublic && nextPrivate) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description loads given title
|
||||
* @param event
|
||||
* @param type
|
||||
* @private
|
||||
*/
|
||||
_loadThisTitles(event, type) {
|
||||
switch (type) {
|
||||
// now, load to both public and private
|
||||
case 'now':
|
||||
// public
|
||||
this.titlesPublic.titleNow = event.title;
|
||||
this.titlesPublic.subtitleNow = event.subtitle;
|
||||
this.titlesPublic.presenterNow = event.presenter;
|
||||
this.selectedPublicEventId = event.id;
|
||||
|
||||
// private
|
||||
this.titles.titleNow = event.title;
|
||||
this.titles.subtitleNow = event.subtitle;
|
||||
this.titles.presenterNow = event.presenter;
|
||||
this.titles.noteNow = event.note;
|
||||
this.selectedEventId = event.id;
|
||||
break;
|
||||
|
||||
case 'now-public':
|
||||
this.titlesPublic.titleNow = event.title;
|
||||
this.titlesPublic.subtitleNow = event.subtitle;
|
||||
this.titlesPublic.presenterNow = event.presenter;
|
||||
this.selectedPublicEventId = event.id;
|
||||
break;
|
||||
|
||||
case 'now-private':
|
||||
this.titles.titleNow = event.title;
|
||||
this.titles.subtitleNow = event.subtitle;
|
||||
this.titles.presenterNow = event.presenter;
|
||||
this.titles.noteNow = event.note;
|
||||
this.selectedEventId = event.id;
|
||||
break;
|
||||
|
||||
// next, load to both public and private
|
||||
case 'next':
|
||||
// public
|
||||
this.titlesPublic.titleNext = event.title;
|
||||
this.titlesPublic.subtitleNext = event.subtitle;
|
||||
this.titlesPublic.presenterNext = event.presenter;
|
||||
this.nextPublicEventId = event.id;
|
||||
|
||||
// private
|
||||
this.titles.titleNext = event.title;
|
||||
this.titles.subtitleNext = event.subtitle;
|
||||
this.titles.presenterNext = event.presenter;
|
||||
this.titles.noteNext = event.note;
|
||||
this.nextEventId = event.id;
|
||||
break;
|
||||
|
||||
case 'next-public':
|
||||
this.titlesPublic.titleNext = event.title;
|
||||
this.titlesPublic.subtitleNext = event.subtitle;
|
||||
this.titlesPublic.presenterNext = event.presenter;
|
||||
this.nextPublicEventId = event.id;
|
||||
break;
|
||||
|
||||
case 'next-private':
|
||||
this.titles.titleNext = event.title;
|
||||
this.titles.subtitleNext = event.subtitle;
|
||||
this.titles.presenterNext = event.presenter;
|
||||
this.titles.noteNext = event.note;
|
||||
this.nextEventId = event.id;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const eventLoader = new EventLoader();
|
||||
@@ -1,6 +1,6 @@
|
||||
let instance;
|
||||
|
||||
class MessageManager {
|
||||
class MessageService {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
throw new Error('There can be only one');
|
||||
@@ -99,4 +99,4 @@ class MessageManager {
|
||||
}
|
||||
}
|
||||
|
||||
export const messageManager = new MessageManager();
|
||||
export const messageManager = new MessageService();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { generateId } from '../../utils/generate_id.js';
|
||||
import { stringFromMillis } from '../../utils/time.js';
|
||||
import { Timer } from '../timer/Timer.js';
|
||||
import { messageManager } from '../message-manager/MessageManager.js';
|
||||
import { PlaybackService } from '../../services/playbackService.js';
|
||||
|
||||
import { ADDRESS_MESSAGE_CONTROL } from './socketConfig.js';
|
||||
|
||||
@@ -58,6 +59,7 @@ class SocketController {
|
||||
}`;
|
||||
this.info('CLIENT', message);
|
||||
|
||||
// Todo: review in favour of features
|
||||
// send state
|
||||
socket.emit('timer', global.timer.getTimeObject());
|
||||
socket.emit('playstate', global.timer.state);
|
||||
@@ -101,78 +103,69 @@ class SocketController {
|
||||
});
|
||||
|
||||
socket.on('set-start', () => {
|
||||
global.timer.trigger('start');
|
||||
socket.emit('playstate', global.timer.state);
|
||||
PlaybackService.start();
|
||||
});
|
||||
|
||||
socket.on('set-startid', (data) => {
|
||||
global.timer.trigger('startById', data);
|
||||
socket.emit('playstate', global.timer.state);
|
||||
if (data) {
|
||||
PlaybackService.startById(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('set-startindex', (data) => {
|
||||
const eventIndex = Number(data);
|
||||
if (isNaN(eventIndex)) {
|
||||
return;
|
||||
if (!isNaN(eventIndex)) {
|
||||
PlaybackService.startByIndex(eventIndex);
|
||||
}
|
||||
global.timer.trigger('startByIndex', data);
|
||||
socket.emit('playstate', global.timer.state);
|
||||
});
|
||||
|
||||
socket.on('set-loadid', (data) => {
|
||||
global.timer.trigger('loadById', data);
|
||||
socket.emit('playstate', global.timer.state);
|
||||
if (data) {
|
||||
PlaybackService.loadById(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('set-loadindex', (data) => {
|
||||
const eventIndex = Number(data);
|
||||
if (isNaN(eventIndex)) {
|
||||
return;
|
||||
if (!isNaN(eventIndex)) {
|
||||
PlaybackService.loadByIndex(eventIndex - 1);
|
||||
}
|
||||
global.timer.trigger('loadByIndex', data);
|
||||
socket.emit('playstate', global.timer.state);
|
||||
});
|
||||
|
||||
socket.on('set-pause', () => {
|
||||
global.timer.trigger('pause');
|
||||
socket.emit('playstate', global.timer.state);
|
||||
PlaybackService.pause();
|
||||
});
|
||||
|
||||
socket.on('set-stop', () => {
|
||||
global.timer.trigger('stop');
|
||||
socket.emit('playstate', global.timer.state);
|
||||
PlaybackService.stop();
|
||||
});
|
||||
|
||||
socket.on('set-reload', () => {
|
||||
global.timer.trigger('reload');
|
||||
socket.emit('playstate', global.timer.state);
|
||||
PlaybackService.reload();
|
||||
});
|
||||
|
||||
socket.on('set-previous', () => {
|
||||
global.timer.trigger('previous');
|
||||
socket.emit('playstate', global.timer.state);
|
||||
PlaybackService.loadPrevious();
|
||||
});
|
||||
|
||||
socket.on('set-next', () => {
|
||||
global.timer.trigger('next');
|
||||
socket.emit('playstate', global.timer.state);
|
||||
PlaybackService.loadNext();
|
||||
});
|
||||
|
||||
socket.on('set-roll', () => {
|
||||
global.timer.trigger('roll');
|
||||
socket.emit('playstate', global.timer.state);
|
||||
PlaybackService.roll();
|
||||
});
|
||||
|
||||
socket.on('set-delay', (data) => {
|
||||
const delayTime = Number(data);
|
||||
if (isNaN(delayTime)) {
|
||||
return;
|
||||
if (!isNaN(delayTime)) {
|
||||
PlaybackService.setDelay(delayTime);
|
||||
}
|
||||
global.timer.increment(delayTime * 1000 * 60);
|
||||
});
|
||||
|
||||
/*******************************************/
|
||||
// general playback state, useful for external sync
|
||||
// Todo: add delayed value (will come from rundownService)
|
||||
socket.on('ontime-poll', () => {
|
||||
const timerPoll = global.timer.poll();
|
||||
const isDelayed = false;
|
||||
@@ -180,34 +173,14 @@ class SocketController {
|
||||
socket.emit('ontime-poll', { isDelayed, colour, ...timerPoll });
|
||||
});
|
||||
|
||||
/*******************************************/
|
||||
|
||||
// ** TO BE DEPRECATED ** //
|
||||
socket.on('get-timer', () => {
|
||||
socket.emit('timer', global.timer.getTimeObject());
|
||||
});
|
||||
|
||||
// ** TO BE DEPRECATED IN FAVOR OF DELAY ** //
|
||||
socket.on('increment-timer', (data) => {
|
||||
if (isNaN(parseInt(data, 10))) return;
|
||||
if (data < -5 || data > 5) return;
|
||||
global.timer.increment(data * 1000 * 60);
|
||||
});
|
||||
|
||||
/*******************************************/
|
||||
// playstate
|
||||
socket.on('set-playstate', (data) => {
|
||||
global.timer.trigger(data);
|
||||
global.timer._broadcastFeaturePlaybackControl();
|
||||
global.timer._broadcastFeatureInfo();
|
||||
});
|
||||
|
||||
socket.on('get-playstate', () => {
|
||||
socket.emit('playstate', global.timer.state);
|
||||
});
|
||||
|
||||
socket.on('get-onAir', () => {
|
||||
socket.emit('onAir', global.timer.onAir);
|
||||
socket.emit('onAir', messageManager.onAir);
|
||||
});
|
||||
|
||||
/*******************************************/
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Timer } from './Timer.js';
|
||||
import { DAY_TO_MS, getSelectionByRoll, replacePlaceholder, updateRoll } from './classUtils.js';
|
||||
import { DAY_TO_MS, replacePlaceholder, updateRoll } from './classUtils.js';
|
||||
import { OSCIntegration } from './integrations/Osc.js';
|
||||
import { HTTPIntegration } from './integrations/Http.js';
|
||||
import { cleanURL } from '../../utils/url.js';
|
||||
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
|
||||
import { DataProvider } from '../data-provider/DataProvider.js';
|
||||
|
||||
/*
|
||||
* Class EventTimer adds functions specific to APP
|
||||
* @extends Timer
|
||||
*/
|
||||
|
||||
export class EventTimer extends Timer {
|
||||
/**
|
||||
* Instantiates an event timer object
|
||||
@@ -50,8 +51,6 @@ export class EventTimer extends Timer {
|
||||
// call general title reset
|
||||
this._resetSelection();
|
||||
|
||||
this.rundown = [];
|
||||
|
||||
// set recurrent emits
|
||||
this._interval = setInterval(() => this.runCycle(), timerConfig?.refresh || 1000);
|
||||
|
||||
@@ -144,10 +143,11 @@ export class EventTimer extends Timer {
|
||||
* @private
|
||||
*/
|
||||
_broadcastFeaturePlaybackControl() {
|
||||
const numEvents = DataProvider.getNumEvents();
|
||||
const featureData = {
|
||||
playback: this.state,
|
||||
selectedEventId: this.selectedEventId,
|
||||
numEvents: this.rundown.length,
|
||||
numEvents: numEvents,
|
||||
};
|
||||
this.socket.send('feat-playbackcontrol', featureData);
|
||||
}
|
||||
@@ -157,22 +157,24 @@ export class EventTimer extends Timer {
|
||||
* @private
|
||||
*/
|
||||
_broadcastFeatureInfo() {
|
||||
const numEvents = DataProvider.getNumEvents();
|
||||
const featureData = {
|
||||
titles: this.titles,
|
||||
playback: this.state,
|
||||
selectedEventId: this.selectedEventId,
|
||||
selectedEventIndex: this.selectedEventIndex,
|
||||
numEvents: this.rundown.length,
|
||||
numEvents: numEvents,
|
||||
};
|
||||
this.socket.send('feat-info', featureData);
|
||||
}
|
||||
|
||||
_broadcastFeatureCuesheet() {
|
||||
const numEvents = DataProvider.getNumEvents();
|
||||
const featureData = {
|
||||
playback: this.state,
|
||||
selectedEventId: this.selectedEventId,
|
||||
selectedEventIndex: this.selectedEventIndex,
|
||||
numEvents: this.rundown.length,
|
||||
numEvents: numEvents,
|
||||
titleNow: this.titles.titleNow,
|
||||
};
|
||||
this.socket.send('feat-cuesheet', featureData);
|
||||
@@ -189,146 +191,16 @@ export class EventTimer extends Timer {
|
||||
this._broadcastFeatureCuesheet();
|
||||
this._broadcastFeatureTimer();
|
||||
|
||||
const numEvents = this.rundown.length;
|
||||
this.broadcastTimer();
|
||||
this.socket.send('playstate', this.state);
|
||||
this.socket.send('selected', {
|
||||
id: this.selectedEventId,
|
||||
index: this.selectedEventIndex,
|
||||
total: numEvents,
|
||||
});
|
||||
this.socket.send('selected-id', this.selectedEventId);
|
||||
this.socket.send('next-id', this.nextEventId);
|
||||
this.socket.send('numevents', numEvents);
|
||||
this.socket.send('publicselected-id', this.selectedPublicEventId);
|
||||
this.socket.send('publicnext-id', this.nextPublicEventId);
|
||||
this.socket.send('titles', this.titles);
|
||||
this.socket.send('publictitles', this.titlesPublic);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Interface for triggering playback actions
|
||||
* @param {string} action - state to be triggered
|
||||
* @param {string|number} [payload] - optional action payload
|
||||
* @returns {boolean} Whether action was called
|
||||
*/
|
||||
trigger(action, payload) {
|
||||
let success = true;
|
||||
const numEvents = this.rundown.length;
|
||||
switch (action) {
|
||||
case 'start': {
|
||||
if (!numEvents) return false;
|
||||
// Call action and force update
|
||||
this.socket.info('PLAYBACK', 'Play Mode Start');
|
||||
this.start();
|
||||
break;
|
||||
}
|
||||
case 'startById': {
|
||||
if (!numEvents) return false;
|
||||
const loaded = this.loadEventById(payload);
|
||||
if (loaded) {
|
||||
this.socket.info('PLAYBACK', `Loaded event with ID ${payload}`);
|
||||
this.socket.info('PLAYBACK', 'Play Mode Start');
|
||||
this.start();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'startByIndex': {
|
||||
if (!numEvents) return false;
|
||||
const loaded = this.loadEventByIndex(payload);
|
||||
if (loaded) {
|
||||
this.socket.info('PLAYBACK', `Loaded event with index ${payload}`);
|
||||
this.socket.info('PLAYBACK', 'Play Mode Start');
|
||||
this.start();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'pause': {
|
||||
if (!numEvents) return false;
|
||||
// Call action and force update
|
||||
this.socket.info('PLAYBACK', 'Play Mode Pause');
|
||||
this.pause();
|
||||
break;
|
||||
}
|
||||
case 'stop': {
|
||||
if (!numEvents) return false;
|
||||
// Call action and force update
|
||||
this.socket.info('PLAYBACK', 'Play Mode Stop');
|
||||
this.stop();
|
||||
break;
|
||||
}
|
||||
case 'roll': {
|
||||
if (!numEvents) return false;
|
||||
// Call action and force update
|
||||
this.socket.info('PLAYBACK', 'Play Mode Roll');
|
||||
this.roll();
|
||||
break;
|
||||
}
|
||||
case 'previous': {
|
||||
if (!numEvents) return false;
|
||||
// Call action and force update
|
||||
this.socket.info('PLAYBACK', 'Play Mode Previous');
|
||||
this.previous();
|
||||
break;
|
||||
}
|
||||
case 'next': {
|
||||
if (!numEvents) return false;
|
||||
// Call action and force update
|
||||
this.socket.info('PLAYBACK', 'Play Mode Next');
|
||||
this.next();
|
||||
break;
|
||||
}
|
||||
case 'loadById': {
|
||||
if (!numEvents) return false;
|
||||
const loaded = this.loadEventById(payload);
|
||||
if (loaded) {
|
||||
this.socket.info('PLAYBACK', `Loaded event with ID ${payload}`);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'loadByIndex': {
|
||||
if (!numEvents) return false;
|
||||
const loaded = this.loadEventByIndex(payload);
|
||||
if (loaded) {
|
||||
this.socket.info('PLAYBACK', `Loaded event with index ${payload}`);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'unload': {
|
||||
if (!numEvents) return false;
|
||||
// Call action and force update
|
||||
this.socket.info('PLAYBACK', 'Events unloaded');
|
||||
this.unload();
|
||||
break;
|
||||
}
|
||||
case 'reload': {
|
||||
if (!numEvents) return false;
|
||||
// Call action and force update
|
||||
this.socket.info('PLAYBACK', 'Reloaded event');
|
||||
this.reload();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Error, disable flag
|
||||
this.socket.error('RX', `Unhandled action triggered ${action}`);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// update state
|
||||
this.runCycle();
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description State machine checks what actions need to
|
||||
* happen at every app cycle
|
||||
@@ -530,467 +402,59 @@ export class EventTimer extends Timer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes running event list from object
|
||||
*/
|
||||
clearEventList() {
|
||||
// unload events
|
||||
this.unload();
|
||||
|
||||
// set general
|
||||
this.rundown = [];
|
||||
|
||||
// update lifecycle: onStop
|
||||
this.ontimeCycle = this.cycleState.onStop;
|
||||
|
||||
// update clients
|
||||
this.socket.send('numevents', this.rundown.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event list to object
|
||||
* @param {array} eventlist
|
||||
*/
|
||||
setupWithEventList(eventlist) {
|
||||
if (!Array.isArray(eventlist) || !eventlist.length) return;
|
||||
|
||||
// filter only events
|
||||
const events = eventlist.filter((e) => e.type === 'event');
|
||||
const numEvents = events.length;
|
||||
|
||||
// set general
|
||||
this.rundown = events;
|
||||
|
||||
// list may contain no events
|
||||
if (numEvents < 1) return;
|
||||
|
||||
// load first event
|
||||
this.loadEvent(0);
|
||||
|
||||
// update clients
|
||||
this.broadcastState();
|
||||
|
||||
// run cycle
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates event list in object
|
||||
* @param {array} eventlist
|
||||
*/
|
||||
updateEventList(eventlist) {
|
||||
if (!Array.isArray(eventlist) || !eventlist.length) return;
|
||||
|
||||
// filter only events
|
||||
const events = eventlist.filter((e) => e.type === 'event' && !e.skip);
|
||||
const numEvents = events.length;
|
||||
|
||||
// set general
|
||||
this.rundown = events;
|
||||
|
||||
// list may be empty
|
||||
if (numEvents < 1) {
|
||||
this.unload();
|
||||
return;
|
||||
}
|
||||
|
||||
// auto load if is the there was nothing before
|
||||
if (!this.rundown.length) {
|
||||
this.loadEvent(0);
|
||||
} else if (this.selectedEventId != null) {
|
||||
// handle reload selected
|
||||
// Look for event (order might have changed)
|
||||
const eventIndex = this.rundown.findIndex((e) => e.id === this.selectedEventId);
|
||||
|
||||
// Maybe is missing
|
||||
if (eventIndex === -1) {
|
||||
this._resetTimers();
|
||||
this._resetSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reload data if running
|
||||
const type = this._startedAt != null ? 'reload' : 'load';
|
||||
this.loadEvent(eventIndex, type);
|
||||
}
|
||||
|
||||
// update clients
|
||||
this.broadcastState();
|
||||
|
||||
// run cycle
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a single id in the object list
|
||||
* @param {string} id
|
||||
* @param {object} entry - new event object
|
||||
*/
|
||||
updateSingleEvent(id, entry) {
|
||||
// find object in events
|
||||
const eventIndex = this.rundown.findIndex((e) => e.id === id);
|
||||
if (eventIndex === -1) {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
// check if event is set to be skipped
|
||||
if (entry.skip) {
|
||||
// stop event if running
|
||||
if (id === this.selectedEventId) {
|
||||
this.trigger('stop');
|
||||
}
|
||||
|
||||
// delete event
|
||||
this.deleteId(id);
|
||||
}
|
||||
|
||||
// update event in memory
|
||||
const e = this.rundown[eventIndex];
|
||||
this.rundown[eventIndex] = { ...e, ...entry };
|
||||
|
||||
try {
|
||||
// check if entry is running
|
||||
if (e.id === this.selectedEventId) {
|
||||
// handle reload selected
|
||||
// Reload data if running
|
||||
const type = this.selectedEventId === id && this._startedAt != null ? 'reload' : 'load';
|
||||
this.loadEvent(this.selectedEventIndex, type);
|
||||
} else if (e.id === this.nextEventId) {
|
||||
// roll needs to recalculate
|
||||
if (this.state === 'roll') {
|
||||
this.rollLoad();
|
||||
}
|
||||
}
|
||||
|
||||
// load titles
|
||||
if ('title' in e || 'subtitle' in e || 'presenter' in e) {
|
||||
this._loadTitlesNext();
|
||||
this._loadTitlesNow();
|
||||
}
|
||||
} catch (error) {
|
||||
this.socket.error('SERVER', error);
|
||||
}
|
||||
|
||||
// update clients
|
||||
this.broadcastState();
|
||||
|
||||
// run cycle
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description inserts an event after a given id
|
||||
* @param event
|
||||
* @param previousId
|
||||
*/
|
||||
insertEventAfterId(event, previousId) {
|
||||
if (typeof previousId === 'undefined') {
|
||||
// Insert at beginning
|
||||
this.rundown.unshift(event);
|
||||
} else {
|
||||
// find object in events
|
||||
const previousIndex = this.rundown.findIndex((e) => e.id === previousId);
|
||||
if (previousIndex === -1) {
|
||||
throw 'Event not found';
|
||||
}
|
||||
|
||||
if (previousIndex + 1 >= this.rundown.length) {
|
||||
this.rundown.push(event);
|
||||
} else {
|
||||
this.rundown.splice(previousIndex + 1, 0, event);
|
||||
}
|
||||
|
||||
try {
|
||||
// check if entry is running
|
||||
if (event.id === this.selectedEventId) {
|
||||
// handle reload selected
|
||||
// Reload data if running
|
||||
const type =
|
||||
this.selectedEventId === event.id && this._startedAt != null ? 'reload' : 'load';
|
||||
this.loadEvent(this.selectedEventIndex, type);
|
||||
} else if (event.id === this.nextEventId) {
|
||||
// roll needs to recalculate
|
||||
if (this.state === 'roll') {
|
||||
this.rollLoad();
|
||||
}
|
||||
}
|
||||
|
||||
// load titles
|
||||
if ('title' in event || 'subtitle' in event || 'presenter' in event) {
|
||||
this._loadTitlesNext();
|
||||
this._loadTitlesNow();
|
||||
}
|
||||
} catch (error) {
|
||||
this.socket.error('SERVER', error);
|
||||
}
|
||||
}
|
||||
|
||||
// update clients
|
||||
this.broadcastState();
|
||||
|
||||
// run cycle
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleted an event from the list by its id
|
||||
*
|
||||
* @param {string} eventId
|
||||
*/
|
||||
deleteId(eventId) {
|
||||
// find object in events
|
||||
const eventIndex = this.rundown.findIndex((e) => e.id === eventId);
|
||||
if (eventIndex === -1) return;
|
||||
|
||||
// delete event and update count
|
||||
this.rundown.splice(eventIndex, 1);
|
||||
|
||||
// reload data if necessary
|
||||
if (eventId === this.selectedEventId) {
|
||||
this.unload();
|
||||
return;
|
||||
syncLoaded(eventId) {
|
||||
if (this.state === 'roll') {
|
||||
this.rollLoad();
|
||||
} else {
|
||||
const event = EventLoader.getEventWithId(eventId);
|
||||
this.loadEvent(event, 'reload');
|
||||
}
|
||||
|
||||
// update selected event index
|
||||
this.selectedEventIndex = this.rundown.findIndex((e) => e.id === this.selectedEventId);
|
||||
|
||||
// reload titles if necessary
|
||||
if (eventId === this.nextEventId || eventId === this.nextPublicEventId) {
|
||||
this._loadTitlesNext();
|
||||
} else if (eventId === this.selectedPublicEventId) {
|
||||
this._loadTitlesNow();
|
||||
}
|
||||
|
||||
// update clients
|
||||
this.broadcastState();
|
||||
|
||||
// run cycle
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description loads an event with a given Id
|
||||
* @param {string} eventId - ID of event in eventlist
|
||||
*/
|
||||
loadEventById(eventId) {
|
||||
const eventIndex = this.rundown.findIndex((e) => e.id === eventId);
|
||||
|
||||
if (eventIndex === -1) return false;
|
||||
this.pause();
|
||||
this.loadEvent(eventIndex, 'load');
|
||||
// run cycle
|
||||
this.runCycle();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description loads an event with a given index
|
||||
* @param {number} eventIndex - Index of event in eventlist
|
||||
*/
|
||||
loadEventByIndex(eventIndex) {
|
||||
if (eventIndex === -1 || eventIndex > this.rundown.length) return false;
|
||||
this.pause();
|
||||
this.loadEvent(eventIndex, 'load');
|
||||
// run cycle
|
||||
this.runCycle();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a given event by index
|
||||
* @param {object} eventIndex
|
||||
* @typedef ('load'|'reload') loadEventOptions
|
||||
* @param {object} event
|
||||
* @param {string} [type='load'] - 'load' or 'reload', whether we are keeping running time
|
||||
*/
|
||||
loadEvent(eventIndex, type = 'load') {
|
||||
const e = this.rundown?.[eventIndex];
|
||||
if (e == null) return;
|
||||
loadEvent(event, type = 'load') {
|
||||
const loadedData = eventLoader.loadById(event.id);
|
||||
if (!loadedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { loadedEvent, loadedEventIndex, loadedEventId, titles, titlesPublic } = loadedData;
|
||||
|
||||
const start = loadedEvent.timeStart || 0;
|
||||
let end = loadedEvent.timeEnd || 0;
|
||||
|
||||
const start = e.timeStart == null || e.timeStart === '' ? 0 : e.timeStart;
|
||||
let end = e.timeEnd == null || e.timeEnd === '' ? 0 : e.timeEnd;
|
||||
// in case the end is earlier than start, we assume is the day after
|
||||
if (end < start) end += DAY_TO_MS;
|
||||
if (end < start) {
|
||||
end += DAY_TO_MS;
|
||||
}
|
||||
|
||||
// time stuff changes on whether we keep the running clock
|
||||
this.duration = end - start;
|
||||
this.selectedEventIndex = loadedEventIndex;
|
||||
this.selectedEventId = loadedEventId;
|
||||
if (type === 'load') {
|
||||
this._resetTimers();
|
||||
|
||||
this.duration = end - start;
|
||||
this.current = this.duration;
|
||||
this.selectedEventIndex = eventIndex;
|
||||
this.selectedEventId = e.id;
|
||||
} else if (type === 'reload') {
|
||||
} else {
|
||||
const now = Timer.getCurrentTime();
|
||||
const elapsed = this.getElapsed();
|
||||
|
||||
this.duration = end - start;
|
||||
this.selectedEventIndex = eventIndex;
|
||||
this._finishAt = now + (this.duration - elapsed);
|
||||
}
|
||||
|
||||
// load current titles
|
||||
this._loadTitlesNow();
|
||||
|
||||
// look for event after
|
||||
this._loadTitlesNext();
|
||||
this.titles = titles;
|
||||
this.titlesPublic = titlesPublic;
|
||||
|
||||
// update lifecycle: onLoad
|
||||
this.ontimeCycle = this.cycleState.onLoad;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description loads given title (now)
|
||||
* @private
|
||||
*/
|
||||
_loadTitlesNow() {
|
||||
const e = this.rundown[this.selectedEventIndex];
|
||||
if (e == null) return;
|
||||
|
||||
// private title is always current
|
||||
// check if current is also public
|
||||
if (e.isPublic) {
|
||||
this._loadThisTitles(e, 'now');
|
||||
} else {
|
||||
this._loadThisTitles(e, 'now-private');
|
||||
|
||||
// assume there is no public event
|
||||
this.titlesPublic.titleNow = null;
|
||||
this.titlesPublic.subtitleNow = null;
|
||||
this.titlesPublic.presenterNow = null;
|
||||
this.selectedPublicEventId = null;
|
||||
|
||||
// if there is nothing before, return
|
||||
if (this.selectedEventIndex === 0) return;
|
||||
|
||||
// iterate backwards to find it
|
||||
for (let i = this.selectedEventIndex; i >= 0; i--) {
|
||||
if (this.rundown[i].type === 'event' && this.rundown[i].isPublic) {
|
||||
this._loadThisTitles(this.rundown[i], 'now-public');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description loads given title
|
||||
* @param e
|
||||
* @param type
|
||||
* @private
|
||||
*/
|
||||
_loadThisTitles(e, type) {
|
||||
if (e == null) return;
|
||||
|
||||
switch (type) {
|
||||
// now, load to both public and private
|
||||
case 'now':
|
||||
// public
|
||||
this.titlesPublic.titleNow = e.title;
|
||||
this.titlesPublic.subtitleNow = e.subtitle;
|
||||
this.titlesPublic.presenterNow = e.presenter;
|
||||
this.selectedPublicEventId = e.id;
|
||||
|
||||
// private
|
||||
this.titles.titleNow = e.title;
|
||||
this.titles.subtitleNow = e.subtitle;
|
||||
this.titles.presenterNow = e.presenter;
|
||||
this.titles.noteNow = e.note;
|
||||
this.selectedEventId = e.id;
|
||||
|
||||
break;
|
||||
case 'now-public':
|
||||
this.titlesPublic.titleNow = e.title;
|
||||
this.titlesPublic.subtitleNow = e.subtitle;
|
||||
this.titlesPublic.presenterNow = e.presenter;
|
||||
this.selectedPublicEventId = e.id;
|
||||
break;
|
||||
case 'now-private':
|
||||
this.titles.titleNow = e.title;
|
||||
this.titles.subtitleNow = e.subtitle;
|
||||
this.titles.presenterNow = e.presenter;
|
||||
this.titles.noteNow = e.note;
|
||||
this.selectedEventId = e.id;
|
||||
break;
|
||||
|
||||
// next, load to both public and private
|
||||
case 'next':
|
||||
// public
|
||||
this.titlesPublic.titleNext = e.title;
|
||||
this.titlesPublic.subtitleNext = e.subtitle;
|
||||
this.titlesPublic.presenterNext = e.presenter;
|
||||
this.nextPublicEventId = e.id;
|
||||
|
||||
// private
|
||||
this.titles.titleNext = e.title;
|
||||
this.titles.subtitleNext = e.subtitle;
|
||||
this.titles.presenterNext = e.presenter;
|
||||
this.titles.noteNext = e.note;
|
||||
this.nextEventId = e.id;
|
||||
break;
|
||||
case 'next-public':
|
||||
this.titlesPublic.titleNext = e.title;
|
||||
this.titlesPublic.subtitleNext = e.subtitle;
|
||||
this.titlesPublic.presenterNext = e.presenter;
|
||||
this.nextPublicEventId = e.id;
|
||||
break;
|
||||
case 'next-private':
|
||||
this.titles.titleNext = e.title;
|
||||
this.titles.subtitleNext = e.subtitle;
|
||||
this.titles.presenterNext = e.presenter;
|
||||
this.titles.noteNext = e.note;
|
||||
this.nextEventId = e.id;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description look for next titles to load
|
||||
* @private
|
||||
*/
|
||||
_loadTitlesNext() {
|
||||
// maybe there is nothing to load
|
||||
if (this.selectedEventIndex == null) return;
|
||||
|
||||
// assume there is no next event
|
||||
this.titles.titleNext = null;
|
||||
this.titles.subtitleNext = null;
|
||||
this.titles.presenterNext = null;
|
||||
this.titles.noteNext = null;
|
||||
this.nextEventId = null;
|
||||
|
||||
this.titlesPublic.titleNext = null;
|
||||
this.titlesPublic.subtitleNext = null;
|
||||
this.titlesPublic.presenterNext = null;
|
||||
this.nextPublicEventId = null;
|
||||
|
||||
const numEvents = this.rundown.length;
|
||||
|
||||
if (this.selectedEventIndex < numEvents - 1) {
|
||||
let nextPublic = false;
|
||||
let nextPrivate = false;
|
||||
|
||||
for (let i = this.selectedEventIndex + 1; i < numEvents; i++) {
|
||||
// check that is the right type
|
||||
if (this.rundown[i].type === 'event') {
|
||||
// if we have not set private
|
||||
if (!nextPrivate) {
|
||||
this._loadThisTitles(this.rundown[i], 'next-private');
|
||||
nextPrivate = true;
|
||||
}
|
||||
|
||||
// if event is public
|
||||
if (this.rundown[i].isPublic) {
|
||||
this._loadThisTitles(this.rundown[i], 'next-public');
|
||||
nextPublic = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop if both are set
|
||||
if (nextPublic && nextPrivate) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description resets selected event data
|
||||
* @private
|
||||
@@ -1025,51 +489,58 @@ export class EventTimer extends Timer {
|
||||
|
||||
/**
|
||||
* @description start timer
|
||||
* @return {('start'|'pause'|'stop'|'roll')} Playback state
|
||||
*/
|
||||
start() {
|
||||
// do we need to change
|
||||
if (this.state === 'start') return;
|
||||
|
||||
// if there is nothing selected, no nothing
|
||||
if (this.selectedEventId == null) return;
|
||||
if (this.state === 'start') return 'start';
|
||||
|
||||
// call super
|
||||
super.start();
|
||||
|
||||
// update lifecycle: onStart
|
||||
this.ontimeCycle = this.cycleState.onStart;
|
||||
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description pause timer
|
||||
* @return {('start'|'pause'|'stop')} Playback state
|
||||
*/
|
||||
pause() {
|
||||
// do we need to change
|
||||
if (this.state === 'pause') return;
|
||||
|
||||
// if there is nothing selected, no nothing
|
||||
if (this.selectedEventId == null) return;
|
||||
if (this.state === 'pause') return 'pause';
|
||||
|
||||
// call super
|
||||
super.pause();
|
||||
|
||||
// update lifecycle: onPause
|
||||
this.ontimeCycle = this.cycleState.onPause;
|
||||
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description stop timer
|
||||
* @return {('start'|'pause'|'stop'|'roll')} Playback state
|
||||
*/
|
||||
stop() {
|
||||
// do we need to change
|
||||
if (this.state === 'stop') return;
|
||||
if (this.state === 'stop') return 'stop';
|
||||
|
||||
// call super
|
||||
super.stop();
|
||||
this._resetTimers(true);
|
||||
this._resetSelection();
|
||||
|
||||
// update lifecycle: onPause
|
||||
// update lifecycle: onStop
|
||||
this.ontimeCycle = this.cycleState.onStop;
|
||||
|
||||
// broadcast state
|
||||
this.broadcastState();
|
||||
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1088,7 +559,6 @@ export class EventTimer extends Timer {
|
||||
* @description Look for current event considering local clock
|
||||
*/
|
||||
rollLoad() {
|
||||
const now = Timer.getCurrentTime();
|
||||
const prevLoaded = this.selectedEventId;
|
||||
|
||||
// maybe roll has already been loaded
|
||||
@@ -1098,11 +568,11 @@ export class EventTimer extends Timer {
|
||||
}
|
||||
|
||||
const { nowIndex, nowId, publicIndex, nextIndex, publicNextIndex, timers, timeToNext } =
|
||||
getSelectionByRoll(this.rundown, now);
|
||||
eventLoader.findRoll();
|
||||
|
||||
// nothing to play, unload
|
||||
if (nowIndex === null && nextIndex === null) {
|
||||
this.unload();
|
||||
this.stop();
|
||||
this.socket.warning('SERVER', 'Roll: no events found');
|
||||
return;
|
||||
}
|
||||
@@ -1126,6 +596,8 @@ export class EventTimer extends Timer {
|
||||
|
||||
// found something to run next
|
||||
if (nextIndex != null) {
|
||||
const eventNext = EventLoader.getPlayableAtIndex(nextIndex);
|
||||
|
||||
// Set running timers
|
||||
if (nowIndex === null) {
|
||||
// only warn the first time
|
||||
@@ -1139,25 +611,61 @@ export class EventTimer extends Timer {
|
||||
|
||||
// timer counts to next event
|
||||
this.secondaryTimer = timeToNext;
|
||||
this._secondaryTarget = this.rundown[nextIndex].timeStart;
|
||||
this._secondaryTarget = eventNext.timeStart;
|
||||
}
|
||||
|
||||
// TITLES: Load next private
|
||||
this._loadThisTitles(this.rundown[nextIndex], 'next-private');
|
||||
// Todo: this should be an ID
|
||||
// todo: this logic should be removed
|
||||
if (eventNext) {
|
||||
this.titles.titleNext = eventNext.title;
|
||||
this.titles.subtitleNext = eventNext.subtitle;
|
||||
this.titles.presenterNext = eventNext.presenter;
|
||||
this.titles.noteNext = eventNext.note;
|
||||
this.nextEventId = eventNext.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: this should be an ID
|
||||
// todo: this logic should be removed
|
||||
// TITLES: Load next public
|
||||
if (publicNextIndex !== null) {
|
||||
this._loadThisTitles(this.rundown[publicNextIndex], 'next-public');
|
||||
const eventNextPublic = EventLoader.getPlayableAtIndex(publicNextIndex);
|
||||
if (eventNextPublic) {
|
||||
this.titlesPublic.titleNext = eventNextPublic.title;
|
||||
this.titlesPublic.subtitleNext = eventNextPublic.subtitle;
|
||||
this.titlesPublic.presenterNext = eventNextPublic.presenter;
|
||||
this.titlesPublic.noteNext = eventNextPublic.note;
|
||||
this.nextPublicEventId = eventNextPublic.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: this should be an ID
|
||||
// todo: this logic should be removed
|
||||
// TITLES: Load now private
|
||||
if (nowIndex !== null) {
|
||||
this._loadThisTitles(this.rundown[nowIndex], 'now-private');
|
||||
const eventNowPrivate = EventLoader.getPlayableAtIndex(nowIndex);
|
||||
if (eventNowPrivate) {
|
||||
this.titles.titleNow = eventNowPrivate.title;
|
||||
this.titles.subtitleNow = eventNowPrivate.subtitle;
|
||||
this.titles.presenterNow = eventNowPrivate.presenter;
|
||||
this.titles.noteNow = eventNowPrivate.note;
|
||||
this.selectedEventId = eventNowPrivate.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: this should be an ID
|
||||
// todo: this logic should be removed
|
||||
// TITLES: Load now public
|
||||
if (publicIndex !== null) {
|
||||
this._loadThisTitles(this.rundown[publicIndex], 'now-public');
|
||||
const eventNowPublic = EventLoader.getPlayableAtIndex(nowIndex);
|
||||
if (eventNowPublic) {
|
||||
this.titlesPublic.titleNow = eventNowPublic.title;
|
||||
this.titlesPublic.subtitleNow = eventNowPublic.subtitle;
|
||||
this.titlesPublic.presenterNow = eventNowPublic.presenter;
|
||||
this.titlesPublic.noteNow = eventNowPublic.note;
|
||||
this.selectedPublicEventId = eventNowPublic.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevLoaded !== this.selectedEventId) {
|
||||
@@ -1168,13 +676,15 @@ export class EventTimer extends Timer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts roll mode
|
||||
* @return {('start'|'pause'|'stop'|'roll')} Playback state
|
||||
*/
|
||||
roll() {
|
||||
// do we need to change
|
||||
if (this.state === 'roll') return;
|
||||
if (this.state === 'roll') {
|
||||
return 'roll';
|
||||
}
|
||||
|
||||
if (!this.rundown.length) return;
|
||||
|
||||
// set state
|
||||
this.state = 'roll';
|
||||
|
||||
// update lifecycle: armed
|
||||
@@ -1182,75 +692,30 @@ export class EventTimer extends Timer {
|
||||
|
||||
// load into event
|
||||
this.rollLoad();
|
||||
|
||||
return this.state;
|
||||
}
|
||||
|
||||
previous() {
|
||||
// check that we have events to run
|
||||
if (!this.rundown.length) return;
|
||||
|
||||
// maybe this is the first event?
|
||||
if (this.selectedEventIndex === 0) return;
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (!this.selectedEventIndex) {
|
||||
this.loadEvent(0);
|
||||
} else {
|
||||
const gotoEvent = this.selectedEventIndex > 0 ? this.selectedEventIndex - 1 : 0;
|
||||
if (gotoEvent === this.selectedEventIndex) return;
|
||||
this.loadEvent(gotoEvent);
|
||||
}
|
||||
|
||||
// send OSC
|
||||
this.sendOsc(this.osc.implemented.previous);
|
||||
|
||||
// change playstate
|
||||
this.pause();
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
next() {
|
||||
const numEvents = this.rundown.length;
|
||||
// check that we have events to run
|
||||
if (!numEvents) return;
|
||||
|
||||
// maybe this is the last event?
|
||||
if (this.selectedEventIndex === numEvents - 1) return;
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (this.selectedEventIndex === null) {
|
||||
this.loadEvent(0);
|
||||
} else {
|
||||
const gotoEvent =
|
||||
this.selectedEventIndex < numEvents - 1 ? this.selectedEventIndex + 1 : numEvents - 1;
|
||||
if (gotoEvent === this.selectedEventIndex) return;
|
||||
this.loadEvent(gotoEvent);
|
||||
}
|
||||
|
||||
// send OSC
|
||||
this.sendOsc(this.osc.implemented.next);
|
||||
|
||||
// change playstate
|
||||
this.pause();
|
||||
}
|
||||
|
||||
unload() {
|
||||
// reset timer
|
||||
this._resetTimers(true);
|
||||
|
||||
// reset selected
|
||||
this._resetSelection();
|
||||
|
||||
// broadcast state
|
||||
this.broadcastState();
|
||||
|
||||
// reset playstate
|
||||
this.stop();
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description reloads current event
|
||||
* @return {('start'|'pause'|'stop'|'roll')} Playback state
|
||||
*/
|
||||
reload() {
|
||||
if (!this.rundown.length) return;
|
||||
if (!this.selectedEventId) {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
// change playstate
|
||||
this.pause();
|
||||
@@ -1259,7 +724,12 @@ export class EventTimer extends Timer {
|
||||
this.sendOsc(this.osc.implemented.reload);
|
||||
|
||||
// reload data
|
||||
this.loadEvent(this.selectedEventIndex);
|
||||
const event = EventLoader.getEventWithId(this.selectedEventId);
|
||||
this.loadEvent(event);
|
||||
|
||||
this.runCycle();
|
||||
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
|
||||
@@ -75,9 +75,8 @@ export class Timer {
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get current time in epoc
|
||||
* @description get current time in ms from midnight
|
||||
* @return {number}
|
||||
* @private
|
||||
*/
|
||||
static getCurrentTime() {
|
||||
const now = new Date();
|
||||
|
||||
@@ -60,61 +60,6 @@ test('object instantiates correctly', async () => {
|
||||
expect(t.nextEventId).toBeNull();
|
||||
expect(t.selectedPublicEventId).toBeNull();
|
||||
expect(t.nextPublicEventId).toBeNull();
|
||||
expect(t.rundown.length).toBe(0);
|
||||
expect(t.rundown).toStrictEqual([]);
|
||||
expect(t.onAir).toBeFalsy();
|
||||
|
||||
t.shutdown();
|
||||
});
|
||||
|
||||
describe('test triggers behaviour', () => {
|
||||
const t = new EventTimer(mockSocket, timerConfig);
|
||||
|
||||
test('ignores bad commands', (done) => {
|
||||
const success = t.trigger('test');
|
||||
expect(success).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
|
||||
test('does not allow triggering events with an empty list', (done) => {
|
||||
expect(t.rundown.length).toBe(0);
|
||||
|
||||
expect(t.trigger('start')).toBeFalsy();
|
||||
expect(t.trigger('pause')).toBeFalsy();
|
||||
expect(t.trigger('stop')).toBeFalsy();
|
||||
expect(t.trigger('roll')).toBeFalsy();
|
||||
expect(t.trigger('previous')).toBeFalsy();
|
||||
expect(t.trigger('next')).toBeFalsy();
|
||||
expect(t.trigger('reload')).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
|
||||
test('...and is consistent by calling the class methods', (done) => {
|
||||
expect(t.rundown.length).toBe(0);
|
||||
expect(t.state).toBe('stop');
|
||||
|
||||
t.start();
|
||||
expect(t.state).toBe('stop');
|
||||
|
||||
t.pause();
|
||||
expect(t.state).toBe('stop');
|
||||
|
||||
t.stop();
|
||||
expect(t.state).toBe('stop');
|
||||
|
||||
t.roll();
|
||||
expect(t.state).toBe('stop');
|
||||
|
||||
t.previous();
|
||||
expect(t.state).toBe('stop');
|
||||
|
||||
t.next();
|
||||
expect(t.state).toBe('stop');
|
||||
|
||||
t.reload();
|
||||
expect(t.state).toBe('stop');
|
||||
done();
|
||||
});
|
||||
|
||||
t.shutdown();
|
||||
});
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Server } from 'node-osc';
|
||||
import { PlaybackService } from '../services/playbackService';
|
||||
import { messageManager } from '../classes/message-manager/MessageManager.js';
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
import { ADDRESS_MESSAGE_CONTROL } from '../classes/socket/socketConfig.js';
|
||||
|
||||
let oscServer = null;
|
||||
|
||||
/**
|
||||
* @description utilty function to shutdown osc server
|
||||
* @description utility function to shut down osc server
|
||||
*/
|
||||
export const shutdownOSCServer = () => {
|
||||
if (oscServer != null) oscServer.close();
|
||||
@@ -11,19 +15,18 @@ export const shutdownOSCServer = () => {
|
||||
|
||||
/**
|
||||
* @description initialises OSC server
|
||||
* @param config
|
||||
* @param {object} config
|
||||
*/
|
||||
export const initiateOSC = (config) => {
|
||||
oscServer = new Server(config.port, '0.0.0.0');
|
||||
|
||||
// error
|
||||
oscServer.on('error', console.error);
|
||||
|
||||
oscServer.on('message', function (msg) {
|
||||
// message should look like /ontime/{path}/{args} where
|
||||
// message should look like /ontime/{path} {args} where
|
||||
// ontime: fixed message for app
|
||||
// path: command to be called
|
||||
// args: extra data, only used on some of the API entries (delay, goto)
|
||||
// args: extra data, only used on some API entries (delay, goto)
|
||||
|
||||
// split message
|
||||
const [, address, path] = msg[0].split('/');
|
||||
@@ -31,122 +34,123 @@ export const initiateOSC = (config) => {
|
||||
|
||||
// get first part before (ontime)
|
||||
if (address !== 'ontime') {
|
||||
console.error(`OSC IN: Message address ${address} not recognised`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (path == null) {
|
||||
console.error('OSC IN: No path found');
|
||||
console.error('RX', `OSC IN: Message address ${address} not recognised`);
|
||||
return;
|
||||
}
|
||||
|
||||
// get second part (command)
|
||||
if (!path) {
|
||||
console.error('RX', 'OSC IN: No path found');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (path.toLowerCase()) {
|
||||
case 'onair': {
|
||||
global.timer.setonAir(true);
|
||||
const featureData = messageManager.setOnAir(true);
|
||||
socketProvider.send(ADDRESS_MESSAGE_CONTROL, featureData);
|
||||
break;
|
||||
}
|
||||
case 'offair': {
|
||||
global.timer.setonAir(false);
|
||||
const featureData = messageManager.setOnAir(false);
|
||||
socketProvider.send(ADDRESS_MESSAGE_CONTROL, featureData);
|
||||
break;
|
||||
}
|
||||
case 'play': {
|
||||
global.timer.trigger('start');
|
||||
PlaybackService.start();
|
||||
break;
|
||||
}
|
||||
case 'start': {
|
||||
try {
|
||||
const eventIndex = Number(args);
|
||||
if (isNaN(eventIndex)) {
|
||||
global.timer.error('RX', `OSC IN: event index not recognised ${args}`);
|
||||
socketProvider.error('RX', `OSC IN: event index not recognised ${args}`);
|
||||
return;
|
||||
}
|
||||
const success = global.timer.trigger('startByIndex', eventIndex);
|
||||
if (!success) {
|
||||
global.timer.error('RX', `OSC IN: event index not recognised ${args}`);
|
||||
}
|
||||
PlaybackService.startByIndex(eventIndex);
|
||||
} catch (error) {
|
||||
console.log('error parsing: ', error);
|
||||
console.log('Error loading event: ', error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'startid': {
|
||||
const success = global.timer.trigger('startById', args);
|
||||
if (!success) {
|
||||
global.timer.error('RX', `OSC IN: event ID not recognised ${args}`);
|
||||
if (!args) {
|
||||
socketProvider.error('RX', `OSC IN: No ID in request`);
|
||||
return;
|
||||
}
|
||||
PlaybackService.loadById(args);
|
||||
break;
|
||||
}
|
||||
case 'pause': {
|
||||
global.timer.trigger('pause');
|
||||
PlaybackService.pause();
|
||||
break;
|
||||
}
|
||||
case 'prev': {
|
||||
global.timer.trigger('previous');
|
||||
PlaybackService.loadPrevious();
|
||||
break;
|
||||
}
|
||||
case 'next': {
|
||||
global.timer.trigger('next');
|
||||
PlaybackService.loadNext();
|
||||
break;
|
||||
}
|
||||
case 'unload':
|
||||
case 'stop': {
|
||||
global.timer.trigger('unload');
|
||||
PlaybackService.stop();
|
||||
break;
|
||||
}
|
||||
case 'reload': {
|
||||
global.timer.trigger('reload');
|
||||
PlaybackService.reload();
|
||||
break;
|
||||
}
|
||||
case 'roll': {
|
||||
global.timer.trigger('roll');
|
||||
PlaybackService.roll();
|
||||
break;
|
||||
}
|
||||
case 'delay': {
|
||||
try {
|
||||
const t = parseInt(args, 10);
|
||||
if (isNaN(t)) {
|
||||
global.timer.error('RX', `OSC IN: delay time not recognised ${args}`);
|
||||
const delayTime = Number(args);
|
||||
if (isNaN(delayTime)) {
|
||||
socketProvider.error('RX', `OSC IN: delay time not recognised ${args}`);
|
||||
return;
|
||||
}
|
||||
global.timer.increment(t * 1000 * 60);
|
||||
PlaybackService.setDelay(delayTime);
|
||||
} catch (error) {
|
||||
console.log('error parsing: ', error);
|
||||
console.log('Error adding delay: ', error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'goto':
|
||||
case 'load': {
|
||||
try {
|
||||
const eventIndex = parseInt(args, 10);
|
||||
const eventIndex = Number(args);
|
||||
if (isNaN(eventIndex) || eventIndex <= 0) {
|
||||
global.timer.error(
|
||||
socketProvider.error(
|
||||
'RX',
|
||||
`OSC IN: event index not recognised or out of range ${eventIndex}`
|
||||
`OSC IN: event index not recognised or out of range ${eventIndex}`,
|
||||
);
|
||||
} else {
|
||||
PlaybackService.loadByIndex(eventIndex - 1);
|
||||
}
|
||||
global.timer.loadEventByIndex(eventIndex - 1);
|
||||
} catch (error) {
|
||||
global.timer.error('RX', `OSC IN: error calling goto ${error}`);
|
||||
socketProvider.error('RX', `OSC IN: error calling goto ${error}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'gotoid':
|
||||
case 'loadid': {
|
||||
if (args == null) {
|
||||
global.timer.error('RX', `OSC IN: event id not recognised or out of range ${args}}`);
|
||||
if (!args) {
|
||||
socketProvider.error('RX', `OSC IN: event ID not recognised: ${args}}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
global.timer.loadEventById(args.toString().toLowerCase());
|
||||
PlaybackService.loadById(args.toString().toLowerCase());
|
||||
} catch (error) {
|
||||
global.timer.error('RX', `OSC IN: error calling goto ${error}`);
|
||||
socketProvider.error('RX', `OSC IN: error calling goto ${error}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
global.timer.warning('RX', `OSC IN: unhandled message ${path}`);
|
||||
socketProvider.warning('RX', `OSC IN: unhandled message ${path}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { resolveDbPath } from '../modules/loadDb.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
||||
import { mergeObject } from '../utils/parserUtils.js';
|
||||
import { PlaybackService } from '../services/playbackService.js';
|
||||
|
||||
// Create controller for GET request to '/ontime/poll'
|
||||
// Returns data for current state
|
||||
@@ -36,6 +37,14 @@ export const dbDownload = async (req, res) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* handles file upload
|
||||
* @param file
|
||||
* @param req
|
||||
* @param res
|
||||
* @param options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const uploadAndParse = async (file, req, res, options) => {
|
||||
if (!fs.existsSync(file)) {
|
||||
res.status(500).send({ message: 'Upload failed' });
|
||||
@@ -48,6 +57,7 @@ const uploadAndParse = async (file, req, res, options) => {
|
||||
if (result?.error) {
|
||||
res.status(400).send({ message: result.message });
|
||||
} else if (result.message === 'success') {
|
||||
PlaybackService.stop();
|
||||
// explicitly write objects
|
||||
if (typeof result !== 'undefined') {
|
||||
const newRundown = result.data.rundown || [];
|
||||
@@ -56,7 +66,6 @@ const uploadAndParse = async (file, req, res, options) => {
|
||||
} else {
|
||||
await DataProvider.mergeIntoData(result.data);
|
||||
}
|
||||
global.timer.setupWithEventList(newRundown.filter((entry) => entry.type === 'event'));
|
||||
}
|
||||
res.sendStatus(200);
|
||||
} else {
|
||||
|
||||
@@ -1,75 +1,81 @@
|
||||
// Create controller for GET request to '/playback'
|
||||
// Returns ACK message
|
||||
import { PlaybackService } from '../services/playbackService.js';
|
||||
|
||||
// Create controller for POST request to '/playback'
|
||||
// Returns playback state
|
||||
export const pbGet = async (req, res) => {
|
||||
res.send({ playback: global.timer.state });
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/playback/start'
|
||||
// Create controller for POST request to '/playback/start'
|
||||
// Starts timer object
|
||||
export const pbStart = async (req, res) => {
|
||||
const { eventId, eventIndex } = req.query;
|
||||
if (eventId) {
|
||||
global.timer.trigger('startById', eventId)
|
||||
? res.sendStatus(200)
|
||||
: res.status(400).send('Invalid event ID');
|
||||
const success = PlaybackService.startById(eventId);
|
||||
success ? res.sendStatus(202) : res.status(400).send('Invalid event ID');
|
||||
} else if (eventIndex) {
|
||||
const index = Number(eventIndex);
|
||||
if (!isNaN(index)) {
|
||||
global.timer.trigger('startByIndex', index - 1)
|
||||
? res.sendStatus(200)
|
||||
: res.status(400).send('Invalid event index');
|
||||
const success = PlaybackService.startByIndex(eventIndex - 1);
|
||||
success ? res.sendStatus(202) : res.status(400).send('Invalid event index');
|
||||
} else {
|
||||
res.status(400).send('Invalid event index');
|
||||
}
|
||||
} else {
|
||||
global.timer.trigger('start') ? res.sendStatus(200) : res.sendStatus(400);
|
||||
PlaybackService.start();
|
||||
res.sendStatus(202);
|
||||
}
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/playback/pause'
|
||||
// Create controller for POST request to '/playback/pause'
|
||||
// Pauses timer object
|
||||
export const pbPause = async (req, res) => {
|
||||
global.timer.trigger('pause') ? res.sendStatus(200) : res.sendStatus(400);
|
||||
PlaybackService.pause();
|
||||
res.sendStatus(202);
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/playback/stop'
|
||||
// Create controller for POST request to '/playback/stop'
|
||||
// Stops timer object
|
||||
export const pbStop = async (req, res) => {
|
||||
global.timer.trigger('stop') ? res.sendStatus(200) : res.sendStatus(400);
|
||||
PlaybackService.stop();
|
||||
res.sendStatus(202);
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/playback/roll'
|
||||
// Create controller for POST request to '/playback/roll'
|
||||
// Sets timer object to roll mode
|
||||
export const pbRoll = async (req, res) => {
|
||||
global.timer.trigger('roll') ? res.sendStatus(200) : res.sendStatus(400);
|
||||
PlaybackService.roll();
|
||||
res.sendStatus(202);
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/playback/previous'
|
||||
// Sets timer object to roll mode
|
||||
// Create controller for POST request to '/playback/previous'
|
||||
// Loads previous event
|
||||
export const pbPrevious = async (req, res) => {
|
||||
global.timer.trigger('previous') ? res.sendStatus(200) : res.sendStatus(400);
|
||||
PlaybackService.loadPrevious();
|
||||
res.sendStatus(202);
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/playback/next'
|
||||
// Sets timer object to roll mode
|
||||
// Create controller for POST request to '/playback/next'
|
||||
// Loads Next event
|
||||
export const pbNext = async (req, res) => {
|
||||
global.timer.trigger('next') ? res.sendStatus(200) : res.sendStatus(400);
|
||||
PlaybackService.loadNext();
|
||||
res.sendStatus(202);
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/playback/load'
|
||||
// Create controller for POST request to '/playback/load'
|
||||
// Load requested event
|
||||
export const pbLoad = async (req, res) => {
|
||||
const { eventId, eventIndex } = req.query;
|
||||
if (eventId) {
|
||||
global.timer.trigger('loadById', eventId)
|
||||
? res.sendStatus(200)
|
||||
: res.status(400).send('Invalid event ID');
|
||||
const success = PlaybackService.loadById(eventId);
|
||||
success ? res.sendStatus(202) : res.status(400).send('Invalid event ID');
|
||||
} else if (eventIndex) {
|
||||
const index = Number(eventIndex);
|
||||
if (!isNaN(index)) {
|
||||
global.timer.trigger('loadByIndex', index - 1)
|
||||
? res.sendStatus(200)
|
||||
: res.status(400).send('Invalid event index');
|
||||
const success = PlaybackService.loadByIndex(eventIndex - 1);
|
||||
success ? res.sendStatus(202) : res.status(400).send('Invalid event index');
|
||||
} else {
|
||||
res.status(400).send('Invalid event index');
|
||||
}
|
||||
@@ -78,14 +84,16 @@ export const pbLoad = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/playback/unload'
|
||||
// Create controller for POST request to '/playback/unload'
|
||||
// Unloads any events
|
||||
export const pbUnload = async (req, res) => {
|
||||
global.timer.trigger('unload') ? res.sendStatus(200) : res.sendStatus(400);
|
||||
PlaybackService.stop();
|
||||
res.sendStatus(202);
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/playback/reload'
|
||||
// Create controller for POST request to '/playback/reload'
|
||||
// Reloads current event
|
||||
export const pbReload = async (req, res) => {
|
||||
global.timer.trigger('reload') ? res.sendStatus(200) : res.sendStatus(400);
|
||||
PlaybackService.reload();
|
||||
res.sendStatus(202);
|
||||
};
|
||||
|
||||
@@ -1,80 +1,13 @@
|
||||
import {
|
||||
block as blockDef,
|
||||
delay as delayDef,
|
||||
event as eventDef,
|
||||
} from '../models/eventsDefinition.js';
|
||||
import { generateId } from '../utils/generate_id.js';
|
||||
import { MAX_EVENTS } from '../settings.js';
|
||||
import { getPreviousPlayable } from '../utils/eventUtils.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { failEmptyObjects } from '../utils/routerUtils.js';
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
|
||||
// import socket provider
|
||||
const socket = socketProvider;
|
||||
|
||||
async function _insertAndSync(newEvent) {
|
||||
const afterId = newEvent?.after;
|
||||
if (typeof afterId === 'undefined') {
|
||||
await DataProvider.insertEventAt(newEvent, 0);
|
||||
if (newEvent.type === 'event') {
|
||||
_insertEventInTimerAfterId(newEvent);
|
||||
}
|
||||
} else {
|
||||
delete newEvent.after;
|
||||
await DataProvider.insertEventAfterId(newEvent, afterId);
|
||||
if (newEvent.type === 'event') {
|
||||
const rundown = DataProvider.getRundown();
|
||||
const { id } = getPreviousPlayable(rundown, newEvent.id);
|
||||
_insertEventInTimerAfterId(newEvent, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns all events of type event
|
||||
* @return {unknown[]}
|
||||
*/
|
||||
function getEventEvents() {
|
||||
// return data.events.filter((e) => e.type === 'event');
|
||||
const rundown = DataProvider.getRundown();
|
||||
return Array.from(rundown).filter((e) => e.type === 'event');
|
||||
}
|
||||
|
||||
// Updates timer object
|
||||
function _updateTimers() {
|
||||
const results = getEventEvents();
|
||||
global.timer.updateEventList(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Adds an event to the timer after an event with given id
|
||||
* @param {object} event
|
||||
* @param {string} [previousId]
|
||||
* @private
|
||||
*/
|
||||
function _insertEventInTimerAfterId(event, previousId) {
|
||||
try {
|
||||
global.timer.insertEventAfterId(event, previousId);
|
||||
} catch (error) {
|
||||
socket.error('SERVER', `Unable to update object: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Updates timer object single event
|
||||
* @param {string} id
|
||||
* @param {object} event
|
||||
* @private
|
||||
*/
|
||||
function _updateTimersSingle(id, event) {
|
||||
global.timer.updateSingleEvent(id, event);
|
||||
}
|
||||
|
||||
// Delete a single entry in timer object
|
||||
function _deleteTimerId(entryId) {
|
||||
global.timer.deleteId(entryId);
|
||||
}
|
||||
import {
|
||||
addEvent,
|
||||
applyDelay,
|
||||
deleteAllEvents,
|
||||
deleteEvent,
|
||||
editEvent,
|
||||
reorderEvent,
|
||||
} from '../services/rundownService.js';
|
||||
|
||||
// Create controller for GET request to '/eventlist'
|
||||
// Returns -
|
||||
@@ -85,14 +18,7 @@ export const rundownGetAll = async (req, res) => {
|
||||
// Create controller for GET request to '/eventlist/:eventId'
|
||||
// Returns -
|
||||
export const getEventById = async (req, res) => {
|
||||
const id = req.params?.eventId;
|
||||
|
||||
if (id == null) {
|
||||
res.status(400).send(`No eventId found in request`);
|
||||
} else {
|
||||
const event = DataProvider.getEventById(id);
|
||||
res.json(event);
|
||||
}
|
||||
res.json(DataProvider.getEventById(req.params?.eventId));
|
||||
};
|
||||
|
||||
// Create controller for POST request to '/eventlist/'
|
||||
@@ -102,36 +28,9 @@ export const rundownPost = async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const numEvents = DataProvider.getNumEvents();
|
||||
if (numEvents > MAX_EVENTS) {
|
||||
const error = `ERROR: Reached limit number of ${MAX_EVENTS} events`;
|
||||
res.status(400).send(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure structure
|
||||
let newEvent = {};
|
||||
const id = generateId();
|
||||
|
||||
switch (req.body.type) {
|
||||
case 'event':
|
||||
newEvent = { ...eventDef, ...req.body, id };
|
||||
break;
|
||||
case 'delay':
|
||||
newEvent = { ...delayDef, ...req.body, id };
|
||||
break;
|
||||
case 'block':
|
||||
newEvent = { ...blockDef, ...req.body, id };
|
||||
break;
|
||||
|
||||
default:
|
||||
res.status(400).send(`Object type missing or unrecognised: ${req.body.type}`);
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
await _insertAndSync(newEvent);
|
||||
res.sendStatus(201);
|
||||
const newEvent = await addEvent(req.body);
|
||||
res.status(201).send(newEvent);
|
||||
} catch (error) {
|
||||
res.status(400).send(error);
|
||||
}
|
||||
@@ -144,35 +43,9 @@ export const rundownPut = async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventDataFromRequest = req.body;
|
||||
const eventId = eventDataFromRequest.id;
|
||||
const eventInMemory = DataProvider.getEventById(eventId);
|
||||
|
||||
if (typeof eventInMemory === 'undefined') {
|
||||
res.status(400).send(`No event with ID found`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const patchedObject = await DataProvider.updateEventById(eventId, eventDataFromRequest);
|
||||
|
||||
if (patchedObject.type === 'event') {
|
||||
if (patchedObject.skip) {
|
||||
// if it is a skip, make sure it is deleted from timer
|
||||
_deleteTimerId(patchedObject.id);
|
||||
} else {
|
||||
if (eventInMemory.skip) {
|
||||
// if it was skipped before we add it to the timer
|
||||
const rundown = DataProvider.getRundown();
|
||||
const { id } = getPreviousPlayable(rundown, patchedObject.id);
|
||||
_insertEventInTimerAfterId(patchedObject, id);
|
||||
} else {
|
||||
// otherwise update as normal
|
||||
_updateTimersSingle(patchedObject.id, patchedObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.sendStatus(200);
|
||||
const event = await editEvent(req.body);
|
||||
res.status(200).send(event);
|
||||
} catch (error) {
|
||||
res.status(400).send(error);
|
||||
}
|
||||
@@ -183,32 +56,10 @@ export const rundownReorder = async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { index, from, to } = req.body;
|
||||
|
||||
// get rundown
|
||||
const rundown = DataProvider.getRundown();
|
||||
const idx = rundown.findIndex((e) => e.id === index, from);
|
||||
|
||||
// Check if item is at given index
|
||||
if (idx !== from) {
|
||||
res.status(400).send(`Id not found at index`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// remove item at from
|
||||
const [reorderedItem] = rundown.splice(from, 1);
|
||||
|
||||
// reinsert item at to
|
||||
rundown.splice(to, 0, reorderedItem);
|
||||
|
||||
// save rundown
|
||||
await DataProvider.setEventData(rundown);
|
||||
|
||||
// update timer
|
||||
_updateTimers();
|
||||
|
||||
res.sendStatus(200);
|
||||
const { eventId, from, to } = req.body;
|
||||
const event = await reorderEvent(eventId, from, to);
|
||||
res.status(200).send(event);
|
||||
} catch (error) {
|
||||
res.status(400).send(error);
|
||||
}
|
||||
@@ -218,54 +69,8 @@ export const rundownReorder = async (req, res) => {
|
||||
// Returns -
|
||||
export const rundownApplyDelay = async (req, res) => {
|
||||
try {
|
||||
// get rundown
|
||||
const rundown = DataProvider.getRundown();
|
||||
|
||||
// AUX
|
||||
let delayIndex = null;
|
||||
let blockIndex = null;
|
||||
let delayValue = 0;
|
||||
|
||||
for (const [index, e] of rundown.entries()) {
|
||||
if (delayIndex == null) {
|
||||
// look for delay
|
||||
if (e.id === req.params.eventId && e.type === 'delay') {
|
||||
delayValue = e.duration;
|
||||
delayIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
// apply delay value to all items until block or end
|
||||
else {
|
||||
if (e.type === 'event') {
|
||||
// update times
|
||||
e.timeStart += delayValue;
|
||||
e.timeEnd += delayValue;
|
||||
|
||||
// increment revision
|
||||
e.revision += 1;
|
||||
} else if (e.type === 'block') {
|
||||
// save id and stop
|
||||
blockIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete delay
|
||||
rundown.splice(delayIndex, 1);
|
||||
|
||||
// delete block
|
||||
// index would have moved down since we deleted delay
|
||||
if (blockIndex) rundown.splice(blockIndex - 1, 1);
|
||||
|
||||
// update rundown
|
||||
await DataProvider.setRundown(rundown);
|
||||
|
||||
// update timer
|
||||
_updateTimers();
|
||||
|
||||
res.sendStatus(201);
|
||||
await applyDelay(req.params.eventId);
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
res.status(400).send(error);
|
||||
}
|
||||
@@ -275,25 +80,18 @@ export const rundownApplyDelay = async (req, res) => {
|
||||
// Returns -
|
||||
export const deleteEventById = async (req, res) => {
|
||||
try {
|
||||
const eventId = req.params.eventId;
|
||||
|
||||
// remove new event
|
||||
await DataProvider.deleteEvent(eventId);
|
||||
// update timer
|
||||
_deleteTimerId(eventId);
|
||||
|
||||
await deleteEvent(req.params.eventId);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
res.status(400).send(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Create controller for DELETE request to '/eventlist/:eventId'
|
||||
// Create controller for DELETE request to '/eventlist/'
|
||||
// Returns -
|
||||
export const rundownDelete = async (req, res) => {
|
||||
try {
|
||||
await DataProvider.clearRundown();
|
||||
global.timer.clearEventList();
|
||||
await deleteAllEvents();
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
res.status(400).send(error);
|
||||
|
||||
@@ -18,6 +18,17 @@ export const rundownPutValidator = [
|
||||
},
|
||||
];
|
||||
|
||||
export const rundownReorderValidator = [
|
||||
body('eventId').isString().exists(),
|
||||
body('from').isNumeric().exists(),
|
||||
body('to').isNumeric().exists(),
|
||||
(req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
export const paramsMustHaveEventId = [
|
||||
param('eventId').exists(),
|
||||
(req, res, next) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { JSONFile, Low } from 'lowdb';
|
||||
import { join } from 'path';
|
||||
import { dirname, join } from 'path';
|
||||
import { copyFileSync, existsSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { ensureDirectory, getAppDataPath } from '../utils/fileManagement.js';
|
||||
import { config } from '../config/config.js';
|
||||
import { validateFile } from '../utils/parserUtils.js';
|
||||
@@ -63,7 +64,7 @@ const parseDb = async (fileToRead, adapterToUse) => {
|
||||
* @param runningDirectory
|
||||
* @return {Promise<{data: (number|*), db: Low<unknown>}>}
|
||||
*/
|
||||
export default async function loadDb(runningDirectory) {
|
||||
async function loadDb(runningDirectory) {
|
||||
const dbInDisk = populateDb(runningDirectory);
|
||||
|
||||
const adapter = new JSONFile(dbInDisk);
|
||||
@@ -76,3 +77,18 @@ export default async function loadDb(runningDirectory) {
|
||||
|
||||
return { db, data };
|
||||
}
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
export const dbDirectory = dirname(join(filename, '../'));
|
||||
|
||||
export let db = {};
|
||||
export let data = {};
|
||||
export const promise = loadDb(dbDirectory);
|
||||
|
||||
const init = async () => {
|
||||
const dbProvider = await promise;
|
||||
db = dbProvider.db;
|
||||
data = dbProvider.data;
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { server, shutdown, startServer } from '../../app.js';
|
||||
import supertest from 'supertest';
|
||||
import { promise } from '../../modules/loadDb.js';
|
||||
|
||||
beforeAll(async () => {
|
||||
await promise;
|
||||
startServer();
|
||||
});
|
||||
|
||||
beforeAll(() => startServer());
|
||||
afterAll(() => shutdown());
|
||||
|
||||
describe('When a GET request request is sent', () => {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { server, shutdown, startServer } from '../../app.js';
|
||||
import supertest from 'supertest';
|
||||
import { server, shutdown, startServer } from '../../app.js';
|
||||
import { promise } from '../../modules/loadDb.js';
|
||||
|
||||
beforeAll(async () => {
|
||||
await promise;
|
||||
startServer();
|
||||
});
|
||||
|
||||
beforeAll(() => startServer());
|
||||
afterAll(() => shutdown());
|
||||
|
||||
describe('When a GET request request is sent', () => {
|
||||
@@ -12,12 +17,12 @@ describe('When a GET request request is sent', () => {
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(typeof response.body.currentId).toBe('string');
|
||||
expect(typeof response.body.timer).toBe('string');
|
||||
expect(typeof response.body.clock).toBe('number');
|
||||
expect(typeof response.body.playback).toBe('string');
|
||||
expect(typeof response.body.title).toBe('string');
|
||||
expect(typeof response.body.presenter).toBe('string');
|
||||
expect(response.body.currentId).toBeDefined();
|
||||
expect(response.body.timer).toBeDefined();
|
||||
expect(response.body.clock).toBeDefined();
|
||||
expect(response.body.playback).toBeDefined();
|
||||
expect(response.body.title).toBeDefined();
|
||||
expect(response.body.presenter).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { server, shutdown, startServer } from '../../app.js';
|
||||
import supertest from 'supertest';
|
||||
import { server, shutdown, startServer } from '../../app.js';
|
||||
import { promise } from '../../modules/loadDb.js';
|
||||
|
||||
beforeAll(async () => {
|
||||
await promise;
|
||||
startServer();
|
||||
});
|
||||
|
||||
beforeAll(() => startServer());
|
||||
afterAll(() => shutdown());
|
||||
|
||||
describe('When a GET request request is sent', () => {
|
||||
@@ -19,73 +24,73 @@ describe('When a GET request request is sent', () => {
|
||||
});
|
||||
|
||||
describe('When a POST state change is sent', () => {
|
||||
test('POST /playback/start returns 200', async () => {
|
||||
test('POST /playback/start returns 202', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/start')
|
||||
.expect(200)
|
||||
.expect(202)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /playback/pause returns 200', async () => {
|
||||
test('POST /playback/pause returns 202', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/pause')
|
||||
.expect(200)
|
||||
.expect(202)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /playback/stop returns 200', async () => {
|
||||
test('POST /playback/stop returns 202', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/stop')
|
||||
.expect(200)
|
||||
.expect(202)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /playback/roll returns 200', async () => {
|
||||
test('POST /playback/roll returns 202', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/roll')
|
||||
.expect(200)
|
||||
.expect(202)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /playback/previous returns 200', async () => {
|
||||
test('POST /playback/previous returns 202', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/previous')
|
||||
.expect(200)
|
||||
.expect(202)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /playback/next returns 200', async () => {
|
||||
test('POST /playback/next returns 202', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/next')
|
||||
.expect(200)
|
||||
.expect(202)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /playback/unload returns 200', async () => {
|
||||
test('POST /playback/unload returns 202', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/unload')
|
||||
.expect(200)
|
||||
.expect(202)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /playback/reload returns 200', async () => {
|
||||
test('POST /playback/reload returns 202', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/reload')
|
||||
.expect(200)
|
||||
.expect(202)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
@@ -100,24 +105,18 @@ describe('When a POST state change is sent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('POST of unknown request returns 404', async () => {
|
||||
await supertest(server).post('/playback/madeup').expect(404);
|
||||
test('POST of unknown request returns 400', async () => {
|
||||
await supertest(server).post('/playback/madeup').expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test POST requests with payload', () => {
|
||||
describe('Start given event', () => {
|
||||
test('POST /playback/start with correctly given ID', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/start')
|
||||
.query({eventId: '5946'})
|
||||
.expect(200);
|
||||
await supertest(server).post('/playback/start').query({ eventId: '5946' }).expect(202);
|
||||
});
|
||||
test('POST /playback/start with correctly given index', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/start')
|
||||
.query({eventIndex: '2'})
|
||||
.expect(200);
|
||||
await supertest(server).post('/playback/start').query({ eventIndex: '2' }).expect(202);
|
||||
});
|
||||
test('POST /playback/start with incorrectly given ID', async () => {
|
||||
await supertest(server)
|
||||
@@ -147,28 +146,16 @@ describe('Test POST requests with payload', () => {
|
||||
|
||||
describe('Load given event', () => {
|
||||
test('POST /playback/load with correctly given ID', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/load')
|
||||
.query({eventId: '5946'})
|
||||
.expect(200);
|
||||
await supertest(server).post('/playback/load').query({ eventId: '5946' }).expect(202);
|
||||
});
|
||||
test('POST /playback/load with correctly given index', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/load')
|
||||
.query({eventIndex: '2'})
|
||||
.expect(200);
|
||||
await supertest(server).post('/playback/load').query({ eventIndex: '2' }).expect(202);
|
||||
});
|
||||
test('POST /playback/load with incorrectly given ID', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/load')
|
||||
.query({eventId: 'doesntexist'})
|
||||
.expect(400);
|
||||
await supertest(server).post('/playback/load').query({ eventId: 'doesntexist' }).expect(400);
|
||||
});
|
||||
test('POST /playback/load with incorrectly given index', async () => {
|
||||
await supertest(server)
|
||||
.post('/playback/load')
|
||||
.query({eventIndex: '25'})
|
||||
.expect(400);
|
||||
await supertest(server).post('/playback/load').query({ eventIndex: '25' }).expect(400);
|
||||
});
|
||||
test('POST /playback/load with incorrectly given index (NaN)', async () => {
|
||||
await supertest(server)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { server, shutdown, startServer } from '../../app.js';
|
||||
import supertest from 'supertest';
|
||||
import { promise } from '../../modules/loadDb.js';
|
||||
|
||||
beforeAll(async () => {
|
||||
await promise;
|
||||
startServer();
|
||||
});
|
||||
|
||||
beforeAll(() => startServer());
|
||||
afterAll(() => shutdown());
|
||||
|
||||
const testEvent = {
|
||||
|
||||
@@ -44,3 +44,5 @@ router.post('/unload', pbUnload);
|
||||
|
||||
// create route between controller and '/playback/reload' endpoint
|
||||
router.post('/reload', pbReload);
|
||||
|
||||
// router.post('*', (req, res) => res.return(404))
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
paramsMustHaveEventId,
|
||||
rundownPostValidator,
|
||||
rundownPutValidator,
|
||||
rundownReorderValidator,
|
||||
} from '../controllers/rundownController.validate.js';
|
||||
|
||||
export const router = express.Router();
|
||||
@@ -21,7 +22,7 @@ export const router = express.Router();
|
||||
router.get('/', rundownGetAll);
|
||||
|
||||
// create route between controller and '/eventlist/:eventId' endpoint
|
||||
router.get('/:eventId', getEventById);
|
||||
router.get('/:eventId', paramsMustHaveEventId, getEventById);
|
||||
|
||||
// create route between controller and '/eventlist/' endpoint
|
||||
router.post('/', rundownPostValidator, rundownPost);
|
||||
@@ -30,7 +31,7 @@ router.post('/', rundownPostValidator, rundownPost);
|
||||
router.put('/', rundownPutValidator, rundownPut);
|
||||
|
||||
// create route between controller and '/eventlist/reorder' endpoint
|
||||
router.patch('/reorder/', rundownReorder);
|
||||
router.patch('/reorder/', rundownReorderValidator, rundownReorder);
|
||||
|
||||
// create route between controller and '/eventlist/applydelay/:eventId' endpoint
|
||||
router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { promise } from './modules/loadDb.js';
|
||||
|
||||
(async () => {
|
||||
let loaded;
|
||||
try {
|
||||
await promise;
|
||||
|
||||
const { startServer, startOSCServer } = await import('./app.js');
|
||||
// Start express server
|
||||
loaded = await startServer();
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* starts loaded timer
|
||||
*/
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
|
||||
/**
|
||||
* Service manages playback status of app
|
||||
* Coordinating with necessary services
|
||||
*/
|
||||
export class PlaybackService {
|
||||
/**
|
||||
* makes calls for loading and starting given event
|
||||
* @param {object} event
|
||||
* @return {boolean} success
|
||||
*/
|
||||
static loadEvent(event) {
|
||||
if (!event) {
|
||||
socketProvider.error('PLAYBACK', 'No event found');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.skip) {
|
||||
socketProvider.warning('PLAYBACK', `Refused playback of skipped event ID ${event.id}`);
|
||||
return false;
|
||||
}
|
||||
global.timer.pause();
|
||||
global.timer.loadEvent(event);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* starts event matching given ID
|
||||
* @param {string} eventId
|
||||
* @return {boolean} success
|
||||
*/
|
||||
static startById(eventId) {
|
||||
const event = EventLoader.getEventWithId(eventId);
|
||||
const success = PlaybackService.loadEvent(event);
|
||||
if (success) {
|
||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
PlaybackService.start();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* starts an event at index
|
||||
* @param {number} eventIndex
|
||||
* @return {boolean} success
|
||||
*/
|
||||
static startByIndex(eventIndex) {
|
||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
||||
const success = PlaybackService.loadEvent(event);
|
||||
if (success) {
|
||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
PlaybackService.start();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* loads event matching given ID
|
||||
* @param {string} eventId
|
||||
* @return {boolean} success
|
||||
*/
|
||||
static loadById(eventId) {
|
||||
const event = EventLoader.getEventWithId(eventId);
|
||||
const success = PlaybackService.loadEvent(event);
|
||||
if (success) {
|
||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* loads event matching given ID
|
||||
* @param {number} eventIndex
|
||||
* @return {boolean} success
|
||||
*/
|
||||
static loadByIndex(eventIndex) {
|
||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
||||
const success = PlaybackService.loadEvent(event);
|
||||
if (success) {
|
||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads event before currently selected
|
||||
*/
|
||||
static loadPrevious() {
|
||||
const previousEvent = eventLoader.findPrevious();
|
||||
if (previousEvent) {
|
||||
PlaybackService.loadById(previousEvent.id);
|
||||
global.timer.previous();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads event after currently selected
|
||||
*/
|
||||
static loadNext() {
|
||||
const nextEvent = eventLoader.findNext();
|
||||
if (nextEvent) {
|
||||
PlaybackService.loadById(nextEvent.id);
|
||||
global.timer.next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playback on selected event
|
||||
*/
|
||||
static start() {
|
||||
if (!eventLoader.selectedEventId) {
|
||||
return;
|
||||
}
|
||||
const newState = global.timer.start();
|
||||
if (newState === 'start') {
|
||||
socketProvider.info('PLAYBACK', 'Play Mode Start');
|
||||
}
|
||||
socketProvider.send('playstate', newState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses playback on selected event
|
||||
*/
|
||||
static pause() {
|
||||
if (!eventLoader.selectedEventId) {
|
||||
return;
|
||||
}
|
||||
const newState = global.timer.pause();
|
||||
if (newState === 'pause') {
|
||||
socketProvider.info('PLAYBACK', 'Play Mode Paused');
|
||||
}
|
||||
socketProvider.send('playstate', newState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops timer and unloads any events
|
||||
*/
|
||||
static stop() {
|
||||
if (!eventLoader.selectedEventId && global.timer.state !== 'roll') {
|
||||
return;
|
||||
}
|
||||
eventLoader.reset();
|
||||
const newState = global.timer.stop();
|
||||
if (newState === 'stop') {
|
||||
socketProvider.info('PLAYBACK', 'Play Mode Stopped');
|
||||
}
|
||||
socketProvider.send('playstate', newState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads current event
|
||||
*/
|
||||
static reload() {
|
||||
if (!eventLoader.selectedEventId) {
|
||||
return;
|
||||
}
|
||||
const newState = global.timer.reload();
|
||||
socketProvider.info('PLAYBACK', 'Reloaded event');
|
||||
socketProvider.send('playstate', newState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets playback to roll
|
||||
*/
|
||||
static roll() {
|
||||
if (!EventLoader.getNumEvents()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (global.timer.state === 'roll') {
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = global.timer.roll();
|
||||
if (newState === 'roll') {
|
||||
socketProvider.info('PLAYBACK', 'Play Mode Roll');
|
||||
}
|
||||
socketProvider.send('playstate', newState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds delay to current event
|
||||
* @param {number} delayTime time in ms
|
||||
*/
|
||||
static setDelay(delayTime) {
|
||||
if (!eventLoader.selectedEventId) {
|
||||
return;
|
||||
}
|
||||
const delayInMs = delayTime * 1000 * 60;
|
||||
global.timer.increment(delayInMs);
|
||||
socketProvider.info('PLAYBACK', `Added ${delayTime} min delay`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { generateId } from '../utils/generate_id.js';
|
||||
import {
|
||||
block as blockDef,
|
||||
delay as delayDef,
|
||||
event as eventDef,
|
||||
} from '../models/eventsDefinition.js';
|
||||
import { MAX_EVENTS } from '../settings.js';
|
||||
import { eventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
|
||||
const affectedLoaded = (affectedIds) => {
|
||||
const now = eventLoader.selectedEventId;
|
||||
const nowPublic = eventLoader.selectedPublicEventId;
|
||||
const next = eventLoader.nextEventId;
|
||||
const nextPublic = eventLoader.nextPublicEventId;
|
||||
return (
|
||||
affectedIds.includes(now) ||
|
||||
affectedIds.includes(now) ||
|
||||
affectedIds.includes(nowPublic) ||
|
||||
affectedIds.includes(next) ||
|
||||
affectedIds.includes(nextPublic)
|
||||
);
|
||||
};
|
||||
|
||||
const isNewNext = () => {
|
||||
const timedEvents = getTimedEvents();
|
||||
const now = eventLoader.selectedEventId;
|
||||
const next = eventLoader.nextEventId;
|
||||
|
||||
// check whether the index of now and next are consecutive
|
||||
const indexNow = timedEvents.findIndex((event) => event.id === now);
|
||||
const indexNext = timedEvents.findIndex((event) => event.id === next);
|
||||
|
||||
if (indexNext - indexNow !== 1) {
|
||||
return true;
|
||||
}
|
||||
// iterate through timed events and see if there are public events between nowPublic and nextPublic
|
||||
const nowPublic = eventLoader.selectedPublicEventId;
|
||||
const nextPublic = eventLoader.nextPublicEventId;
|
||||
|
||||
let foundNew = false;
|
||||
let isAfter = false;
|
||||
for (const event of timedEvents) {
|
||||
if (!isAfter) {
|
||||
if (event.id === nowPublic) {
|
||||
isAfter = true;
|
||||
}
|
||||
} else {
|
||||
if (event.id === nextPublic) {
|
||||
break;
|
||||
}
|
||||
if (event.isPublic) {
|
||||
foundNew = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundNew;
|
||||
};
|
||||
|
||||
/**
|
||||
* updates timer object
|
||||
* @param {array} [affectedIds]
|
||||
*/
|
||||
export function updateTimer(affectedIds) {
|
||||
const runningEventId = eventLoader.selectedEventId;
|
||||
if (runningEventId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// we need to reload in a few scenarios:
|
||||
// 1. we are not confident that changes do not affect running event
|
||||
// 2. the edited event is currently being used (now or next)
|
||||
// 3. the edited event replaces one of the previous (next)
|
||||
if (typeof affectedIds === 'undefined') {
|
||||
global.timer.syncLoaded(runningEventId);
|
||||
return true;
|
||||
}
|
||||
if (affectedLoaded(affectedIds)) {
|
||||
global.timer.syncLoaded(runningEventId);
|
||||
return true;
|
||||
}
|
||||
if (isNewNext()) {
|
||||
global.timer.syncLoaded(runningEventId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns all events of type event
|
||||
* @return {unknown[]}
|
||||
*/
|
||||
export function getTimedEvents() {
|
||||
// return data.events.filter((e) => e.type === 'event');
|
||||
const rundown = DataProvider.getRundown();
|
||||
return rundown.filter((e) => e.type === 'event');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description creates a new event with given data
|
||||
* @param {object} eventData
|
||||
* @return {unknown[]}
|
||||
*/
|
||||
export async function addEvent(eventData) {
|
||||
const numEvents = DataProvider.getNumEvents();
|
||||
if (numEvents > MAX_EVENTS) {
|
||||
throw new Error(`ERROR: Reached limit number of ${MAX_EVENTS} events`);
|
||||
}
|
||||
|
||||
let newEvent = {};
|
||||
const id = generateId();
|
||||
|
||||
switch (eventData.type) {
|
||||
case 'event':
|
||||
newEvent = { ...eventDef, ...eventData, id };
|
||||
break;
|
||||
case 'delay':
|
||||
newEvent = { ...delayDef, ...eventData, id };
|
||||
break;
|
||||
case 'block':
|
||||
newEvent = { ...blockDef, ...eventData, id };
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const afterId = newEvent?.after;
|
||||
if (typeof afterId === 'undefined') {
|
||||
await DataProvider.insertEventAt(newEvent, 0);
|
||||
} else {
|
||||
delete newEvent.after;
|
||||
await DataProvider.insertEventAfterId(newEvent, afterId);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
updateTimer([id]);
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
export async function editEvent(eventData) {
|
||||
const eventId = eventData.id;
|
||||
const eventInMemory = DataProvider.getEventById(eventId);
|
||||
if (typeof eventInMemory === 'undefined') {
|
||||
throw new Error('No event with ID found');
|
||||
}
|
||||
const newEvent = await DataProvider.updateEventById(eventId, eventData);
|
||||
updateTimer([eventId]);
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes event by its ID
|
||||
* @param eventId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function deleteEvent(eventId) {
|
||||
await DataProvider.deleteEvent(eventId);
|
||||
updateTimer([eventId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes all events in database
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function deleteAllEvents() {
|
||||
await DataProvider.clearRundown();
|
||||
updateTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* reorders a given event
|
||||
* @param {string} eventId
|
||||
* @param {number} from
|
||||
* @param {number} to
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function reorderEvent(eventId, from, to) {
|
||||
const rundown = DataProvider.getRundown();
|
||||
const index = rundown.findIndex((event) => event.id === eventId);
|
||||
|
||||
if (index !== from) {
|
||||
throw new Error('ID not found at index');
|
||||
}
|
||||
const [reorderedItem] = rundown.splice(from, 1);
|
||||
|
||||
// reinsert item at to
|
||||
rundown.splice(to, 0, reorderedItem);
|
||||
|
||||
// save rundown
|
||||
await DataProvider.setEventData(rundown);
|
||||
updateTimer();
|
||||
|
||||
return reorderedItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* applies delay value for given event
|
||||
* @param eventId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function applyDelay(eventId) {
|
||||
const rundown = DataProvider.getRundown();
|
||||
// AUX
|
||||
let delayIndex = null;
|
||||
let blockIndex = null;
|
||||
let delayValue = 0;
|
||||
|
||||
for (const [index, e] of rundown.entries()) {
|
||||
// look for delay
|
||||
if (delayIndex === null) {
|
||||
if (e.id === eventId && e.type === 'delay') {
|
||||
delayValue = e.duration;
|
||||
delayIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
// apply delay value to all items until block or end
|
||||
else {
|
||||
if (e.type === 'event') {
|
||||
// update times
|
||||
e.timeStart += delayValue;
|
||||
e.timeEnd += delayValue;
|
||||
|
||||
// increment revision
|
||||
e.revision += 1;
|
||||
} else if (e.type === 'block') {
|
||||
// save id and stop
|
||||
blockIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (delayIndex === null) {
|
||||
throw new Error(`Delay event with ID ${eventId} not found`);
|
||||
}
|
||||
|
||||
// delete delay
|
||||
rundown.splice(delayIndex, 1);
|
||||
|
||||
// delete block
|
||||
// index would have moved down since we deleted delay
|
||||
if (blockIndex) rundown.splice(blockIndex - 1, 1);
|
||||
|
||||
// update rundown
|
||||
await DataProvider.setRundown(rundown);
|
||||
updateTimer();
|
||||
}
|
||||
@@ -4,16 +4,6 @@ import { isStringEmpty, parseExcel, parseJson, validateEvent } from '../parser.j
|
||||
import { makeString, validateDuration } from '../parserUtils.js';
|
||||
import { parseAliases, parseUserFields, parseViews } from '../parserFunctions.js';
|
||||
|
||||
describe('refuses import of old / unknown versions', () => {
|
||||
test('a v1 file', () => {
|
||||
const testFile = {
|
||||
settings: {
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe('test json parser with valid def', () => {
|
||||
const testData = {
|
||||
rundown: [
|
||||
@@ -499,7 +489,7 @@ describe('test event validator', () => {
|
||||
user7: expect.any(String),
|
||||
user8: expect.any(String),
|
||||
user9: expect.any(String),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -2350,13 +2350,11 @@ const object = [
|
||||
'specialist',
|
||||
'stroke',
|
||||
'switch',
|
||||
'trash',
|
||||
'tune',
|
||||
'zone',
|
||||
'anger',
|
||||
'award',
|
||||
'bid',
|
||||
'bitter',
|
||||
'boot',
|
||||
'bug',
|
||||
'camp',
|
||||
@@ -2579,8 +2577,6 @@ const object = [
|
||||
'touch',
|
||||
'cancel',
|
||||
'chemical',
|
||||
'cry',
|
||||
'dump',
|
||||
'extreme',
|
||||
'push',
|
||||
'conflict',
|
||||
@@ -2596,7 +2592,6 @@ const object = [
|
||||
'total',
|
||||
'treat',
|
||||
'vast',
|
||||
'abuse',
|
||||
'beat',
|
||||
'burn',
|
||||
'deposit',
|
||||
@@ -2668,7 +2663,6 @@ const object = [
|
||||
'rough',
|
||||
'sad',
|
||||
'scratch',
|
||||
'sick',
|
||||
'strike',
|
||||
'employ',
|
||||
'external',
|
||||
|
||||
Reference in New Issue
Block a user