From ff5735fe3625d824700758448a486a4811596810 Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Tue, 22 Nov 2022 21:25:55 +0100 Subject: [PATCH] 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 --- client/src/common/hooks/useEventAction.js | 6 +- client/src/features/rundown/Rundown.jsx | 34 +- .../rundown/event-block/EventBlock.tsx | 9 +- client/src/features/table/TableWrapper.jsx | 16 +- server/jest.config.js | 8 + server/main.js | 3 + server/package.json | 6 +- server/src/app.js | 27 +- .../src/classes/data-provider/DataProvider.js | 4 +- .../src/classes/event-loader/EventLoader.js | 369 +++++++++ .../classes/message-manager/MessageManager.js | 4 +- server/src/classes/socket/SocketController.js | 73 +- server/src/classes/timer/EventTimer.js | 770 +++--------------- server/src/classes/timer/Timer.js | 3 +- .../timer/__tests__/eventtimer.test.js | 55 -- server/src/controllers/OscController.js | 92 ++- server/src/controllers/ontimeController.js | 11 +- server/src/controllers/playbackController.js | 70 +- server/src/controllers/rundownController.js | 244 +----- .../controllers/rundownController.validate.js | 11 + server/src/modules/loadDb.js | 20 +- .../src/routes/__tests__/eventRouter.test.js | 7 +- .../src/routes/__tests__/ontimeRouter.test.js | 21 +- .../routes/__tests__/playbackRouter.tests.js | 75 +- .../routes/__tests__/rundownRouter.test.js | 7 +- server/src/routes/playbackRouter.js | 2 + server/src/routes/rundownRouter.js | 5 +- server/src/run.js | 4 + server/src/services/playbackService.js | 198 +++++ server/src/services/rundownService.js | 250 ++++++ server/src/utils/__tests__/parser.tests.js | 12 +- server/src/utils/getRandomName.js | 6 - 32 files changed, 1236 insertions(+), 1186 deletions(-) create mode 100644 server/jest.config.js create mode 100644 server/src/classes/event-loader/EventLoader.js create mode 100644 server/src/services/playbackService.js create mode 100644 server/src/services/rundownService.js diff --git a/client/src/common/hooks/useEventAction.js b/client/src/common/hooks/useEventAction.js index 72fca1e26..b4b19ac24 100644 --- a/client/src/common/hooks/useEventAction.js +++ b/client/src/common/hooks/useEventAction.js @@ -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, }; diff --git a/client/src/features/rundown/Rundown.jsx b/client/src/features/rundown/Rundown.jsx index 59dbb10ce..c965eb6a2 100644 --- a/client/src/features/rundown/Rundown.jsx +++ b/client/src/features/rundown/Rundown.jsx @@ -193,51 +193,51 @@ export default function Rundown(props) { {(provided) => (
- {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 (
{((showQuickEntry && index === cursor) || isLast) && ( )}
diff --git a/client/src/features/rundown/event-block/EventBlock.tsx b/client/src/features/rundown/event-block/EventBlock.tsx index 3d1ae52d7..e6dc3700d 100644 --- a/client/src/features/rundown/event-block/EventBlock.tsx +++ b/client/src/features/rundown/event-block/EventBlock.tsx @@ -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 ( diff --git a/client/src/features/table/TableWrapper.jsx b/client/src/features/table/TableWrapper.jsx index cd9f83c77..4bea5e94b 100644 --- a/client/src/features/table/TableWrapper.jsx +++ b/client/src/features/table/TableWrapper.jsx @@ -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 loading...; } return ( @@ -89,7 +89,7 @@ export default function TableWrapper() { > { try { + const dbLoader = await import('./src/modules/loadDb.js'); + + await dbLoader.promise; const { startServer, startOSCServer } = await import(nodePath); // Start express server loaded = await startServer(); diff --git a/server/package.json b/server/package.json index 3791a19f9..6b45343b1 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/app.js b/server/src/app.js index 4b2c8bbbe..205a5c501 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -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} */ 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(); diff --git a/server/src/classes/data-provider/DataProvider.js b/server/src/classes/data-provider/DataProvider.js index a6940341a..a37137dc2 100644 --- a/server/src/classes/data-provider/DataProvider.js +++ b/server/src/classes/data-provider/DataProvider.js @@ -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); } diff --git a/server/src/classes/event-loader/EventLoader.js b/server/src/classes/event-loader/EventLoader.js new file mode 100644 index 000000000..2db9fbfe6 --- /dev/null +++ b/server/src/classes/event-loader/EventLoader.js @@ -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(); diff --git a/server/src/classes/message-manager/MessageManager.js b/server/src/classes/message-manager/MessageManager.js index 496d980b4..96df52e0c 100644 --- a/server/src/classes/message-manager/MessageManager.js +++ b/server/src/classes/message-manager/MessageManager.js @@ -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(); diff --git a/server/src/classes/socket/SocketController.js b/server/src/classes/socket/SocketController.js index 52374977c..bf9efd2d7 100644 --- a/server/src/classes/socket/SocketController.js +++ b/server/src/classes/socket/SocketController.js @@ -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); }); /*******************************************/ diff --git a/server/src/classes/timer/EventTimer.js b/server/src/classes/timer/EventTimer.js index cffe3eb55..abae5368c 100644 --- a/server/src/classes/timer/EventTimer.js +++ b/server/src/classes/timer/EventTimer.js @@ -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; } /****************************************************************************/ diff --git a/server/src/classes/timer/Timer.js b/server/src/classes/timer/Timer.js index 40ed0a187..fc8aeb704 100644 --- a/server/src/classes/timer/Timer.js +++ b/server/src/classes/timer/Timer.js @@ -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(); diff --git a/server/src/classes/timer/__tests__/eventtimer.test.js b/server/src/classes/timer/__tests__/eventtimer.test.js index 15d592a27..662dd2912 100644 --- a/server/src/classes/timer/__tests__/eventtimer.test.js +++ b/server/src/classes/timer/__tests__/eventtimer.test.js @@ -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(); }); diff --git a/server/src/controllers/OscController.js b/server/src/controllers/OscController.js index 0233a547e..e6671ab8b 100644 --- a/server/src/controllers/OscController.js +++ b/server/src/controllers/OscController.js @@ -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; } } diff --git a/server/src/controllers/ontimeController.js b/server/src/controllers/ontimeController.js index 9c9e2d930..a0122a1ee 100644 --- a/server/src/controllers/ontimeController.js +++ b/server/src/controllers/ontimeController.js @@ -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} + */ 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 { diff --git a/server/src/controllers/playbackController.js b/server/src/controllers/playbackController.js index 5224f0097..8b288c978 100644 --- a/server/src/controllers/playbackController.js +++ b/server/src/controllers/playbackController.js @@ -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); }; diff --git a/server/src/controllers/rundownController.js b/server/src/controllers/rundownController.js index 5ffa8bb90..452aa2246 100644 --- a/server/src/controllers/rundownController.js +++ b/server/src/controllers/rundownController.js @@ -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); diff --git a/server/src/controllers/rundownController.validate.js b/server/src/controllers/rundownController.validate.js index 34b9dad28..05f2f481b 100644 --- a/server/src/controllers/rundownController.validate.js +++ b/server/src/controllers/rundownController.validate.js @@ -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) => { diff --git a/server/src/modules/loadDb.js b/server/src/modules/loadDb.js index c6dc00e8d..89d0ab186 100644 --- a/server/src/modules/loadDb.js +++ b/server/src/modules/loadDb.js @@ -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}>} */ -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(); diff --git a/server/src/routes/__tests__/eventRouter.test.js b/server/src/routes/__tests__/eventRouter.test.js index d0191679b..7b34ef113 100644 --- a/server/src/routes/__tests__/eventRouter.test.js +++ b/server/src/routes/__tests__/eventRouter.test.js @@ -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', () => { diff --git a/server/src/routes/__tests__/ontimeRouter.test.js b/server/src/routes/__tests__/ontimeRouter.test.js index 4ab2875fb..9ec126b59 100644 --- a/server/src/routes/__tests__/ontimeRouter.test.js +++ b/server/src/routes/__tests__/ontimeRouter.test.js @@ -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('')).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(); }); }); diff --git a/server/src/routes/__tests__/playbackRouter.tests.js b/server/src/routes/__tests__/playbackRouter.tests.js index 2d306ca22..bc3dcc9f3 100644 --- a/server/src/routes/__tests__/playbackRouter.tests.js +++ b/server/src/routes/__tests__/playbackRouter.tests.js @@ -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('')).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('')).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('')).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('')).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('')).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('')).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('')).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('')).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) diff --git a/server/src/routes/__tests__/rundownRouter.test.js b/server/src/routes/__tests__/rundownRouter.test.js index f4d9f4a1f..2605e5525 100644 --- a/server/src/routes/__tests__/rundownRouter.test.js +++ b/server/src/routes/__tests__/rundownRouter.test.js @@ -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 = { diff --git a/server/src/routes/playbackRouter.js b/server/src/routes/playbackRouter.js index 621a4c8b9..5b9d8b024 100644 --- a/server/src/routes/playbackRouter.js +++ b/server/src/routes/playbackRouter.js @@ -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)) diff --git a/server/src/routes/rundownRouter.js b/server/src/routes/rundownRouter.js index f5e4d8a9e..b3d7120c1 100644 --- a/server/src/routes/rundownRouter.js +++ b/server/src/routes/rundownRouter.js @@ -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); diff --git a/server/src/run.js b/server/src/run.js index d96068fc9..b6249f5d4 100644 --- a/server/src/run.js +++ b/server/src/run.js @@ -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(); diff --git a/server/src/services/playbackService.js b/server/src/services/playbackService.js new file mode 100644 index 000000000..5d069b287 --- /dev/null +++ b/server/src/services/playbackService.js @@ -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`); + } +} diff --git a/server/src/services/rundownService.js b/server/src/services/rundownService.js new file mode 100644 index 000000000..7c160aff2 --- /dev/null +++ b/server/src/services/rundownService.js @@ -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} + */ +export async function deleteEvent(eventId) { + await DataProvider.deleteEvent(eventId); + updateTimer([eventId]); +} + +/** + * deletes all events in database + * @returns {Promise} + */ +export async function deleteAllEvents() { + await DataProvider.clearRundown(); + updateTimer(); +} + +/** + * reorders a given event + * @param {string} eventId + * @param {number} from + * @param {number} to + * @returns {Promise} + */ +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} + */ +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(); +} diff --git a/server/src/utils/__tests__/parser.tests.js b/server/src/utils/__tests__/parser.tests.js index 8a9e4e25f..4a47ee878 100644 --- a/server/src/utils/__tests__/parser.tests.js +++ b/server/src/utils/__tests__/parser.tests.js @@ -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), - }), + }) ); }); diff --git a/server/src/utils/getRandomName.js b/server/src/utils/getRandomName.js index dfee4efa0..48506a763 100644 --- a/server/src/utils/getRandomName.js +++ b/server/src/utils/getRandomName.js @@ -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',