folder restructure

This commit is contained in:
cv
2021-05-28 13:16:34 +02:00
parent 1491fbbb97
commit aa53f2c9cf
18 changed files with 17 additions and 71 deletions
+87
View File
@@ -0,0 +1,87 @@
// get config
import { config } from './config/config.js';
// init database
import { Low, JSONFile } from 'lowdb';
const adapter = new JSONFile(config.database.filename);
export const db = new Low(adapter);
// dependencies
import express from 'express';
import http from 'http';
import cors from 'cors';
import { dbModel } from './data/dataModel.js';
// Read data from JSON file, this will set db.data content
await db.read();
// If file.json doesn't exist, db.data will be null
// Set default data
// db.data ||= { events: [] }; NODE v15 - v16
if (db.data == null) {
db.data = dbModel;
db.write();
}
// get data
export const data = db.data;
// Import Routes
import { router as eventsRouter } from './routes/eventsRouter.js';
import { router as eventRouter } from './routes/eventRouter.js';
import { router as ontimeRouter } from './routes/ontimeRouter.js';
// Setup default port
const port = process.env.PORT || config.server.port;
// Global Objects
import { EventTimer } from './classes/EventTimer.js';
// Create express APP
const app = express();
// setup cors for all routes
app.use(cors());
// enable pre-flight cors
app.options('*', cors());
// Implement middleware
app.use('/uploads', express.static('uploads'));
app.use(express.urlencoded({ extended: true }));
app.use(express.json({ limit: '1mb' }));
// Implement route endpoints
app.use('/events', eventsRouter);
app.use('/event', eventRouter);
app.use('/ontime', ontimeRouter);
// implement general router
app.get('/', (req, res) => {
res.send('ontime API');
});
// Implement route for errors
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
// create HTTP server
const server = http.createServer(app);
// init timer
global.timer = new EventTimer(server, config);
global.timer.setupWithEventList(data.events);
// Start server
server.listen(port, () =>
console.log(`HTTP Server is listening on port ${port}`)
);
// Start OSC server
import { initiateOSC } from './controllers/OscController.js';
initiateOSC(config.osc);
+799
View File
@@ -0,0 +1,799 @@
import { Timer } from './Timer.js';
import { Server } from 'socket.io';
/*
* EventTimer adds functions specific to APP
* namely:
* - Presenter message, text and status
* - Public message, text and status
*
*/
export class EventTimer extends Timer {
// AUX
DAYMS = 86400000;
// Socket IO Object
io = null;
_numClients = 0;
_interval = null;
presenter = {
text: '',
visible: false,
};
public = {
text: '',
visible: false,
};
lower = {
text: '',
visible: false,
};
titlesPublic = {
titleNow: null,
subtitleNow: null,
presenterNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
};
titles = {
titleNow: null,
subtitleNow: null,
presenterNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
};
selectedEventIndex = null;
selectedEventId = null;
nextEventId = null;
selectedPublicEventId = null;
nextPublicEventId = null;
numEvents = null;
_eventlist = null;
constructor(httpServer, config) {
// call super constructor
super();
// initialise socketIO server
this.io = new Server(httpServer, {
cors: {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
},
});
// set recurrent emits
this._interval = setInterval(
() => this.broadcastTimer(),
config.timer.refresh
);
// listen to new connections
this._listenToConnections();
}
// send current timer
broadcastTimer() {
this.io.emit('timer', this.getObject());
}
// broadcast state
broadcastState() {
this.io.emit('timer', this.getObject());
this.io.emit('playstate', this.state);
this.io.emit('selected', {
id: this.selectedEventId,
index: this.selectedEventIndex,
});
this.io.emit('selected-id', this.selectedEventId);
this.io.emit('next-id', this.nextEventId);
this.io.emit('publicselected-id', this.selectedPublicEventId);
this.io.emit('publicnext-id', this.nextPublicEventId);
this.io.emit('titles', this.titles);
this.io.emit('publictitles', this.titlesPublic);
}
// broadcast message
broadcastThis(address, payload) {
this.io.emit(address, payload);
}
update() {
// if there is nothing selected, no nothing
if (this.selectedEventId == null) return;
super.update();
}
start() {
// if there is nothing selected, no nothing
if (this.selectedEventId == null) return;
super.start();
this.broadcastState();
}
pause() {
// if there is nothing selected, no nothing
if (this.selectedEventId == null) return;
super.pause();
this.broadcastState();
}
_setterManager(action, payload) {
switch (action) {
/*******************************************/
// playstate
case 'set-playstate':
// check state is defined
if (payload === 'start') this.start();
else if (payload === 'pause') this.pause();
else if (payload === 'stop') this.stop();
else if (payload === 'previous') this.previous();
else if (payload === 'next') this.next();
else if (payload === 'reload') this.reload();
else if (payload === 'unload') this.unload();
// Not yet implemented
// else if (payload === 'roll') this.roll();
this.broadcastThis('playstate', this.state);
this.broadcastThis('selected-id', this.selectedEventId);
this.broadcastThis('titles', this.titles);
break;
/*******************************************/
// Presenter message
case 'set-presenter-text':
this.presenter.text = payload;
this.broadcastThis('messages-presenter', this.presenter);
break;
case 'set-presenter-visible':
this.presenter.visible = payload;
this.broadcastThis('messages-presenter', this.presenter);
break;
/*******************************************/
// Public message
case 'set-public-text':
this.public.text = payload;
this.broadcastThis('messages-public', this.public);
break;
case 'set-public-visible':
this.public.visible = payload;
this.broadcastThis('messages-public', this.public);
break;
/*******************************************/
// Lower third message
case 'set-lower-text':
this.lower.text = payload;
this.broadcastThis('messages-lower', this.lower);
break;
case 'set-lower-visible':
this.lower.visible = payload;
this.broadcastThis('messages-lower', this.lower);
break;
default:
break;
}
}
_listenToConnections() {
this.io.on('connection', (socket) => {
/*******************************/
/*** HANDLE NEW CONNECTION ***/
/*** --------------------- ***/
/*******************************/
// keep track of connections
this._numClients++;
console.log(
`EventTimer: ${this._numClients} Clients with new connection: ${socket.id}`
);
// send state
socket.emit('timer', this.getObject());
socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId);
socket.emit('publicselected-id', this.selectedPublicEventId);
socket.emit('publicnext-id', this.nextPublicEventId);
/********************************/
/*** HANDLE DISCONNECT USER ***/
/*** ---------------------- ***/
/********************************/
socket.on('disconnect', () => {
this._numClients--;
console.log(
`EventTimer: Client disconnected, total now: ${this._numClients}`
);
});
/***************************************/
/*** TIMER STATE GETTERS / SETTERS ***/
/*** ----------------------------- ***/
/***************************************/
/*******************************************/
// general playback state
socket.on('get-state', () => {
socket.emit('timer', this.getObject());
socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId);
socket.emit('publicselected-id', this.selectedPublicEventId);
socket.emit('publicnext-id', this.this.nextPublicEventId);
});
/*******************************************/
// timer
socket.on('get-current', () => {
socket.emit('current', this.getCurrentInSeconds());
});
socket.on('get-timer', () => {
socket.emit('timer', this.getObject());
});
socket.on('increment-timer', (data) => {
if (isNaN(parseInt(data))) return;
if (data < -5 || data > 5) return;
this.increment(data * 1000 * 60);
});
/*******************************************/
// playstate
socket.on('set-playstate', (data) => {
this._setterManager('set-playstate', data);
});
socket.on('get-playstate', () => {
socket.emit('playstate', this.state);
});
/*******************************************/
// selection data
socket.on('get-selected', () => {
socket.emit('selected', {
id: this.selectedEventId,
index: this.selectedEventIndex,
});
});
socket.on('get-selected-id', () => {
socket.emit('selected-id', this.selectedEventId);
});
socket.on('get-next-id', () => {
socket.emit('next-id', this.nextEventId);
});
socket.on('get-publicselected-id', () => {
socket.emit('publicselected-id', this.selectedPublicEventId);
});
socket.on('get-publicnext-id', () => {
socket.emit('publicnext-id', this.nextPublicEventId);
});
// title data
socket.on('get-titles', () => {
socket.emit('titles', this.titles);
});
// title data
socket.on('get-publictitles', () => {
socket.emit('publictitles', this.titlesPublic);
});
/***********************************/
/*** MESSAGE GETTERS / SETTERS ***/
/*** ------------------------- ***/
/***********************************/
/*******************************************/
// Messages
socket.on('get-messages', () => {
this.broadcastThis('messages-presenter', this.presenter);
this.broadcastThis('messages-public', this.public);
this.broadcastThis('messages-lower', this.lower);
});
// Presenter message
socket.on('set-presenter-text', (data) => {
this._setterManager('set-presenter-text', data);
});
socket.on('set-presenter-visible', (data) => {
this._setterManager('set-presenter-visible', data);
});
socket.on('get-presenter', () => {
this.broadcastThis('messages-presenter', this.presenter);
});
/*******************************************/
// Public message
socket.on('set-public-text', (data) => {
this._setterManager('set-public-text', data);
});
socket.on('set-public-visible', (data) => {
this._setterManager('set-public-visible', data);
});
socket.on('get-public', () => {
socket.emit('messages-public', this.public);
});
/*******************************************/
// Lower third message
socket.on('set-lower-text', (data) => {
this._setterManager('set-lower-text', data);
});
socket.on('set-lower-visible', (data) => {
this._setterManager('set-lower-visible', data);
});
socket.on('get-lower', () => {
socket.emit('messages-lower', this.lower);
});
});
}
setupWithEventList(eventlist) {
if (!Array.isArray(eventlist) || eventlist.length < 1) return;
// filter only events
const events = eventlist.filter((e) => e.type === 'event');
const numEvents = events.length;
// set general
this._eventlist = events;
this.numEvents = numEvents;
// list may be empty
if (numEvents < 1) return;
// load first event
this.loadEvent(0);
}
updateEventList(eventlist) {
// filter only events
const events = eventlist.filter((e) => e.type === 'event');
const numEvents = events.length;
// set general
this._eventlist = events;
this.numEvents = numEvents;
// list may be empty
if (numEvents < 1) {
this.unload();
return;
}
// handle reload selected
if (this.selectedEventId != null) {
// Look for event (order might have changed)
const eventIndex = this._eventlist.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);
}
this.broadcastState();
}
updateSingleEvent(id, entry) {
// find object in events
const eventIndex = this._eventlist.findIndex((e) => e.id === id);
if (eventIndex === -1) return;
// update event in memory
const e = this._eventlist[eventIndex];
this._eventlist[eventIndex] = { ...e, ...entry };
try {
// check if entry is running
if (e.id === this.selectedEventId) {
// handle reload selected
// Reload data if running
let type =
this.selectedEventId === id && this._startedAt != null
? 'reload'
: 'load';
this.loadEvent(this.selectedEventIndex, type);
} else if ('title' in e || 'subtitle' in e || 'presenter') {
// TODO: should be more selective on the need to load titles
this._loadTitlesNext();
this._loadTitlesNow();
}
} catch (error) {
console.log(error);
}
this.broadcastState();
}
deleteId(eventId) {
// find object in events
const eventIndex = this._eventlist.findIndex((e) => e.id === eventId);
if (eventIndex === -1) return;
// delete event and update count
this._eventlist.splice(eventIndex, 1);
this.numEvents = this._eventlist.length;
// reload data if necessary
if (eventId === this.selectedEventId) {
this.unload();
return;
}
// update selected event index
this.selectedEventIndex = this._eventlist.findIndex(
(e) => e.id === eventId
);
// reload titles if necessary
if (eventId === this.nextEventId || eventId === this.nextPublicEventId) {
this._loadTitlesNext();
} else if (eventId === this.selectedPublicEventId) {
this._loadTitlesNow();
}
this.broadcastState();
}
loadEventById(eventId) {
let eventIndex = this._eventlist.findIndex((e) => e.id === eventId);
if (eventIndex === -1) return;
this.pause();
this.loadEvent(eventIndex, true, true);
}
// Loads a given event
// load timers
// load selectedEventIndex
// load titles
loadEvent(eventIndex, type = 'load', broadcastChange = 'false') {
const e = this._eventlist[eventIndex];
if (e == null) return;
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 += this.DAYMS;
// time stuff changes on wheter we keep the running clock
if (type === 'load') {
this._resetTimers();
this.duration = end - start;
this.current = this.duration;
this.selectedEventIndex = eventIndex;
this.selectedEventId = e.id;
} else if (type === 'reload') {
const now = this._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();
if (broadcastChange)
// broadcast current state
this.broadcastState();
}
_loadTitlesNow() {
const e = this._eventlist[this.selectedEventIndex];
if (e == null) return;
// private title is always current
this.titles.titleNow = e.title;
this.titles.subtitleNow = e.subtitle;
this.titles.presenterNow = e.presenter;
this.selectedEventId = e.id;
// check if current is also public
if (e.isPublic) {
this.titlesPublic.titleNow = e.title;
this.titlesPublic.subtitleNow = e.subtitle;
this.titlesPublic.presenterNow = e.presenter;
this.selectedPublicEventId = e.id;
} else {
// 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._eventlist[i].type === 'event' &&
this._eventlist[i].isPublic
) {
this.titlesPublic.titleNow = this._eventlist[i].title;
this.titlesPublic.subtitleNow = this._eventlist[i].subtitle;
this.titlesPublic.presenterNow = this._eventlist[i].presenter;
this.selectedPublicEventId = this._eventlist[i].id;
break;
}
}
}
}
_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.nextEventId = null;
this.titlesPublic.titleNext = null;
this.titlesPublic.subtitleNext = null;
this.titlesPublic.presenterNext = null;
this.nextPublicEventId = null;
if (this.selectedEventIndex < this.numEvents - 1) {
let nextPublic = false;
let nextPrivate = false;
for (let i = this.selectedEventIndex + 1; i < this.numEvents; i++) {
// check that is the right type
if (this._eventlist[i].type === 'event') {
// if we have not set private
if (!nextPrivate) {
this.titles.titleNext = this._eventlist[i].title;
this.titles.subtitleNext = this._eventlist[i].subtitle;
this.titles.presenterNext = this._eventlist[i].presenter;
this.nextEventId = this._eventlist[i].id;
nextPrivate = true;
}
// if event is public
if (this._eventlist[i].isPublic) {
this.titlesPublic.titleNext = this._eventlist[i].title;
this.titlesPublic.subtitleNext = this._eventlist[i].subtitle;
this.titlesPublic.presenterNext = this._eventlist[i].presenter;
this.nextPublicEventId = this._eventlist[i].id;
nextPublic = true;
}
}
// Stop if both are set
if (nextPublic && nextPrivate) break;
}
}
}
_resetSelection() {
this.titles = {
titleNow: null,
subtitleNow: null,
presenterNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
};
this.publicTitles = {
titleNow: null,
subtitleNow: null,
presenterNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
};
this.selectedEventIndex = null;
this.selectedEventId = null;
this.nextEventId = null;
this.selectedPublicEventId = null;
this.nextPublicEventId = null;
}
print() {
return `
Timer
=========
Playback
------------------------------
state = ${this.state}
current = ${this.current}
duration = ${this.duration}
Events
------------------------------
numEvents = ${this.numEvents}
selectedEventIndex = ${this.selectedEventIndex}
selectedEventId = ${this.selectedEventId}
nextEventId = ${this.nextEventId}
selectedPublicEventId = ${this.selectedPublicEventId}
nextPublicEventId = ${this.nextPublicEventId}
Private Titles
------------------------------
NowID = ${this.selectedEventId}
NextID = ${this.nextEventId}
Title Now = ${this.titles.titleNow}
Subtitle Now = ${this.titles.subtitleNow}
Presenter Now = ${this.titles.presenterNow}
Title Next = ${this.titles.titleNext}
Subtitle Next = ${this.titles.subtitleNext}
Presenter Next = ${this.titles.presenterNext}
Public Titles
------------------------------
NowID = ${this.selectedPublicEventId}
NextID = ${this.nextPublicEventId}
Title Now = ${this.titlesPublic.titleNow}
Subtitle Now = ${this.titlesPublic.subtitleNow}
Presenter Now = ${this.titlesPublic.presenterNow}
Title Next = ${this.titlesPublic.titleNext}
Subtitle Next = ${this.titlesPublic.subtitleNext}
Presenter Next = ${this.titlesPublic.presenterNext}
Messages
------------------------------
presenter text = ${this.presenter.text}
presenter vis = ${this.presenter.visible}
public text = ${this.public.text}
public vis = ${this.public.visible}
lower text = ${this.lower.text}
lower vis = ${this.lower.visible}
Private
------------------------------
finishAt = ${this._finishAt}
finished = ${this._finishedAt}
startedAt = ${this._startedAt}
pausedAt = ${this._pausedAt}
pausedInterval = ${this._pausedInterval}
pausedTotal = ${this._pausedTotal}
Socket
------------------------------
numClients = ${this._numClients}
`;
}
start() {
// call super
super.start();
// broadcast current state
this.broadcastState();
}
pause() {
// call super
super.pause();
// broadcast current state
this.broadcastState();
}
stop() {
// call super
super.stop();
// broadcast current state
this.broadcastState();
}
increment(amount) {
// call super
super.increment(amount);
// broadcast current state
this.broadcastState();
}
roll() {
console.log('roll: not yet implemented');
return false;
this.state = 'roll';
}
previous() {
// check that we have events to run
if (this.numEvents < 1) return;
// if there is no event running, go to first
if (this.selectedEventIndex == null) {
this.loadEvent(0);
return;
}
// change playstate
this.pause();
const gotoEvent =
this.selectedEventIndex > 0 ? this.selectedEventIndex - 1 : 0;
if (gotoEvent === this.selectedEventIndex) return;
this.loadEvent(gotoEvent);
}
next() {
// check that we have events to run
if (this.numEvents < 1) return;
// if there is no event running, go to first
if (this.selectedEventIndex == null) {
this.loadEvent(0);
return;
}
// change playstate
this.pause();
const gotoEvent =
this.selectedEventIndex < this.numEvents - 1
? this.selectedEventIndex + 1
: this.numEvents - 1;
if (gotoEvent === this.selectedEventIndex) return;
this.loadEvent(gotoEvent);
}
unload() {
// reset duration
this.duration = null;
// reset selected
this._resetSelection();
// reset playstate
this.stop();
}
reload() {
// reset playstate
this.pause();
// reload data
this.loadEvent(this.selectedEventIndex);
}
}
+213
View File
@@ -0,0 +1,213 @@
/*
* Timer implements simple countdown timer functions
* User needs to use setup function to be able to use
*
*/
export class Timer {
clock = null;
duration = null;
current = null;
_finishAt = null;
_finishedAt = null;
_startedAt = null;
_pausedAt = null;
_pausedInterval = null;
_pausedTotal = null;
state = 'stop';
constructor() {}
// call setup separately
setupWithSeconds(seconds, autoStart = false) {
// aux
const now = this._getCurrentTime();
this.clock = now;
// populate targets
this.duration = seconds * 1000;
this._finishAt = now + seconds * 1000;
// start counting
this._startedAt = now;
if (autoStart) {
this.state = 'start';
} else {
this._pausedAt = now;
this._pausedInterval = 0;
}
this._pausedTotal = 0;
this.update();
}
// update()
update() {
// get current time
const now = this._getCurrentTime();
this.clock = now;
// check playstate
switch (this.state) {
case 'start':
// update current timer
this.current =
this._startedAt + this.duration + this._pausedTotal - now;
break;
case 'pause':
// update paused time
this._pausedInterval = now - this._pausedAt;
if (this._startedAt != null) {
// update current timer
this.current =
this._startedAt +
this.duration +
this._pausedTotal +
this._pausedInterval -
now;
}
break;
case 'stop':
// nothing here yet
break;
default:
console.error('Timer: no playstate on update call', this.state);
break;
}
// Cleanup
if (this.current <= 0 && this._finishedAt == null) this._finishedAt = now;
}
// helpers
static toSeconds(millis) {
if (millis == null) return null;
return Math.floor(Math.max(millis * 0.001), 0);
}
// get current time in epoc
_getCurrentTime() {
// date today at midnight
const now = new Date();
const midnight = new Date(now).setHours(0, 0, 0);
// return diffence
return now - midnight;
}
_getExpectedFinish() {
if (this._startedAt == null) return null;
if (this._finishedAt) return this._finishedAt;
return Math.max(
this._startedAt +
this.duration +
this._pausedInterval +
this._pausedTotal,
this._startedAt
);
}
_resetTimers() {
this.current = this.duration;
this._finishAt = null;
this._finishedAt = null;
this._startedAt = null;
this._pausedAt = null;
this._pausedInterval = null;
this._pausedTotal = null;
}
// get elapsed time
getElapsed() {
return this.duration - this.current;
}
// getObject
getObject() {
this.update();
return {
clock: this.clock,
running: Timer.toSeconds(this.current),
currentSeconds: Timer.toSeconds(Math.max(this.current, 0)),
durationSeconds: Timer.toSeconds(this.duration),
expectedFinish: this._getExpectedFinish(),
startedAt: this._startedAt,
};
}
// current time in seconds
getCurrentInSeconds() {
// update timeStamp
this.update();
return Timer.toSeconds(this.current);
}
// playback
start() {
// do we need to change
if (this.state === 'start') return;
else if (this._startedAt == null) {
// it hasnt started yet
const now = this._getCurrentTime();
// set start time as now
this._startedAt = now;
// calculate expected finish time
this._finishAt = now + this.duration;
// reset pauses
this._pausedTotal = null;
this._pausedInterval = null;
} else {
// check if there is paused time
if (this._pausedInterval) {
this._pausedTotal += this._pausedInterval;
this._pausedInterval = null;
}
}
// change state
this.state = 'start';
}
pause() {
// do we need to change
if (this.state === 'pause') return;
if (this._pausedInterval) {
this._pausedTotal += this._pausedInterval;
this._pausedInterval = null;
}
// set pause time
this._pausedAt = this._getCurrentTime();
// change state
this.state = 'pause';
}
stop() {
// do we need to change
if (this.state === 'stop') return;
// clear all timers
this._resetTimers();
// change state
this.state = 'stop';
}
increment(amount) {
this.duration += amount;
if (amount < 0 && Math.abs(amount) > this.current) {
// if we will make the clock negative
if (this._finishedAt == null) this._finishedAt = this._getCurrentTime();
} else if (this.current < 0 && this.current + amount > 0) {
// clock will go from negative to positive
this._finishedAt = null;
}
}
}
+15
View File
@@ -0,0 +1,15 @@
export const config = {
timer: {
refresh: 1000,
},
server: {
port: 4001,
},
database: {
filename: 'db.json',
tablename: 'events',
},
osc: {
port: 8888,
},
};
+85
View File
@@ -0,0 +1,85 @@
import { Server } from 'node-osc';
export const initiateOSC = (config) => {
const oscServer = new Server(config.port, '0.0.0.0', () => {
console.log(`OSC Server is listening on port ${config.port}`);
});
oscServer.on('message', function (msg) {
// 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)
console.log('OSC received', msg);
// split message
const [, address, path] = msg[0].split('/');
const args = msg[1];
// get first part before (ontime)
if (address !== 'ontime') return;
// get second part (command)
switch (path.toLocaleLowerCase()) {
case 'start':
case 'play':
console.log('calling play');
global.timer.start();
break;
case 'pause':
console.log('calling pause');
global.timer.pause();
break;
case 'prev':
console.log('calling prev');
global.timer.previous();
break;
case 'next':
console.log('calling next');
global.timer.next();
break;
case 'unload':
console.log('calling unload');
global.timer.unload();
break;
case 'reload':
console.log('calling reload');
global.timer.reload();
break;
case 'roll':
console.log('calling roll');
break;
case 'delay':
console.log('calling delay with', args);
try {
let t = parseInt(args);
if (!isNaN(t)) global.timer.increment(t * 1000 * 60);
} catch (error) {
console.log('error parsing: ', error);
}
break;
case 'goto':
console.log('calling goto with', args);
try {
let eventIndex = parseInt(args);
if (isNaN(eventIndex) || eventIndex <= 0) return;
global.timer.loadEvent(eventIndex - 1, undefined, true);
} catch (error) {
console.log('error calling goto: ', error);
}
break;
case 'gotoid':
console.log('calling gotoid with', args);
try {
global.timer.loadEventById(args.toLowerCase());
} catch (error) {
console.log('error calling goto: ', error);
}
break;
default:
console.log('Error: not recognised');
break;
}
});
};
+26
View File
@@ -0,0 +1,26 @@
// get database
import { db, data } from '../app.js';
// Create controller for GET request to 'event'
// Returns ACK message
export const getEvent = async (req, res) => {
res.json(data.event);
};
// Create controller for POST request to 'event'
// Returns ACK message
export const postEvent = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
// TODO: validate data
try {
data.event = { ...data.event, ...req.body };
await db.write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
console.log(error);
}
};
+329
View File
@@ -0,0 +1,329 @@
// get database
import { db, data } from '../app.js';
// utils
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('1234567890abcdef', 4);
import {
event as eventDef,
delay as delayDef,
block as blockDef,
} from '../data/eventsDefinition.js';
function _getEventsCount() {
return Array.from(data.events).length;
}
function _pushNew(entry) {
return data.events.push(entry).write();
}
async function _insertAt(entry, index) {
// get events
let events = data.events;
let count = events.length;
let order = entry.order;
// Remove order field from object
delete entry.order;
// Insert at beggining
if (order === 0) {
events.unshift(entry);
}
// insert at end
else if (order >= count) {
events.push(entry);
}
// insert in the middle
else {
events.splice(index, 0, entry);
}
// save events
data.events = events;
await db.write();
}
async function _removeById(eventId) {
data.events = Array.from(data.events).filter((e) => e.id != eventId);
await db.write();
}
function getEventEvents() {
// return data.events.filter((e) => e.type === 'event');
return Array.from(data.events).filter((e) => e.type === 'event');
}
// Updates timer object
function _updateTimers() {
const results = getEventEvents();
global.timer.updateEventList(results);
}
// Updates timer object single event
function _updateTimersSingle(id, entry) {
global.timer.updateSingleEvent(id, entry);
}
// Delete a single entry in timer object
function _deleteTimerId(entryId) {
global.timer.deleteId(entryId);
}
// Create controller for GET request to '/events'
// Returns -
export const eventsGetAll = async (req, res) => {
res.json(data.events);
};
// Create controller for GET request to '/events/:eventId'
// Returns -
export const eventsGetById = async (req, res) => {
const e = data.events.find({ id: req.params.eventId }).value();
console.log('event by id', e);
res.json(e);
};
// Create controller for POST request to '/events/'
// Returns -
export const eventsPost = async (req, res) => {
// TODO: Validate event
if (!req.body) {
res.status(400).send(`No object found in request`);
return;
}
// ensure structure
let newEvent = {};
req.body.id = nanoid();
switch (req.body.type) {
case 'event':
newEvent = { ...eventDef, ...req.body };
break;
case 'delay':
newEvent = { ...delayDef, ...req.body };
break;
case 'block':
newEvent = { ...blockDef, ...req.body };
break;
default:
res
.status(400)
.send(`Object type missing or unrecognised: ${req.body.type}`);
break;
}
try {
// get place where event should be
const index = newEvent.order || 0;
// add new event in place
_insertAt(newEvent, index);
// update timers
_updateTimers();
// reply OK
res.sendStatus(201);
} catch (error) {
console.log(error);
res.status(400).send(error);
}
};
// Create controller for PUT request to '/events/'
// Returns -
export const eventsPut = async (req, res) => {
// no valid params
if (!req.body) {
res.status(400).send(`No object found`);
return;
}
let eventId = req.body.id;
if (!eventId) {
res.status(400).send(`Object malformed: id missing`);
return;
}
try {
const eventIndex = data.events.findIndex((e) => e.id === req.body.id);
if (eventIndex === -1) {
res.status(400).send(`No Id found found`);
return;
}
const e = data.events[eventIndex];
data.events[eventIndex] = { ...e, ...req.body };
data.events[eventIndex].revision++;
db.write();
// update timer
_updateTimersSingle(req.body.id, req.body);
res.sendStatus(200);
} catch (error) {
console.log(error);
res.status(400).send(error);
}
};
// Create controller for PATCH request to '/events/'
// Returns -
export const eventsPatch = async (req, res) => {
// Code is the same as put, call that
eventsPut(req, res);
};
export const eventsReorder = async (req, res) => {
// TODO: Validate event
if (!req.body) {
res.status(400).send(`No object found in request`);
return;
}
const { index, from, to } = req.body;
// get events
let events = data.events;
let idx = events.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] = events.splice(from, 1);
// reinsert item at to
events.splice(to, 0, reorderedItem);
// save events
data.events = events;
db.write();
// TODO: would it be more efficient to reorder at timer?
// update timer
_updateTimers();
res.sendStatus(200);
} catch (error) {
console.log(error);
res.status(400).send(error);
}
};
// Create controller for PATCH request to '/events/applydelay/:eventId'
// Returns -
export const eventsApplyDelay = async (req, res) => {
// no valid params
if (!req.params.eventId) {
res.status(400).send(`No id found in request`);
return;
}
try {
// get events
let events = data.events;
// AUX
let delayIndex = null;
let blockIndex = null;
let delayValue = 0;
for (const [index, e] of events.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
events.splice(delayIndex, 1);
// delete block
// index would have moved down since we deleted delay
if (blockIndex) events.splice(blockIndex - 1, 1);
// update events
data.events = events;
db.write();
// update timer
_updateTimers();
res.sendStatus(201);
} catch (error) {
console.log('debug:', error);
res.status(400).send(error);
}
};
// Create controller for DELETE request to '/events/:eventId'
// Returns -
export const eventsDelete = async (req, res) => {
// no valid params
if (!req.params.eventId) {
res.status(400).send(`No id found in request`);
return;
}
try {
// remove new event
_removeById(req.params.eventId);
// update timer
_deleteTimerId(req.params.eventId);
res.sendStatus(201);
} catch (error) {
console.log('debug:', error);
res.status(400).send(error);
}
};
// Create controller for DELETE request to '/events/:eventId'
// Returns -
export const eventsDeleteAll = async (req, res) => {
try {
// set with nothing
data.events = [];
db.write();
// update timer object
_updateTimersSingle();
res.sendStatus(201);
} catch (error) {
res.status(400).send(error);
}
};
+128
View File
@@ -0,0 +1,128 @@
// get database
import { db, data } from '../app.js';
import fs from 'fs';
import {
event as eventDef,
delay as delayDef,
block as blockDef,
} from '../data/eventsDefinition.js';
import { dbModel } from '../data/dataModel.js';
function getEventTitle() {
return data.event.title;
}
async function deleteFile(file) {
// delete a file
fs.unlink(file, (err) => {
if (err) {
console.log(err);
}
});
}
// parses version 1 of the data system
async function parsev1(jsonData) {
if ('events' in jsonData) {
let events = [];
let ids = [];
for (const e of jsonData.events) {
if (e.type === 'event') {
// doublecheck unique ids
if (e.id == null || ids.indexOf(e.id) !== -1) continue;
ids.push(e.id);
// make sure all properties exits
// dont load any extra properties than the ones known
events.push({
...eventDef,
title: e.title,
subtitle: e.subtitle,
presenter: e.presenter,
note: e.note,
timeStart: e.timeStart,
timeEnd: e.timeEnd,
isPublic: e.isPublic,
id: e.id,
});
} else if (e.type === 'delay') {
events.push({ ...delayDef, duration: e.duration });
} else if (e.type === 'block') {
events.push({ ...blockDef });
}
}
// write to db
db.data.events = events;
db.write();
}
if ('event' in jsonData) {
const e = jsonData.event;
// filter known properties
const event = {
...dbModel.event,
title: e.title,
url: e.url,
publicInfo: e.publicInfo,
backstageInfo: e.backstageInfo,
};
// write to db
db.data.event = event;
db.write();
}
// Not handling settings yet
// let settings = {};
// if ('settings' in jsonData) {
// }
}
// Create controller for GET request to '/ontime/db'
// Returns -
export const dbDownload = async (req, res) => {
const fileTitle = getEventTitle() || 'ontime events';
res.download('db.json', `${fileTitle}.json`, (err) => {
if (err) {
res.status(500).send({
message: 'Could not download the file. ' + err,
});
}
});
};
// Create controller for POST request to '/ontime/db'
// Returns -
export const dbUpload = async (req, res) => {
if (!req.file) {
res.status(400).send({ message: 'File not found' });
return;
}
const file = req.file.path;
if (!fs.existsSync(file)) {
res.status(500).send({ message: 'Upload failed' });
return;
}
try {
// get file
let rawdata = fs.readFileSync(file);
let uploadedJson = JSON.parse(rawdata);
// delete file
deleteFile(file);
// check version
if (uploadedJson.settings.version === 1) parsev1(uploadedJson);
else {
res.status(400).send({ message: 'Error parsing file, version unknown' });
return;
}
res.sendStatus(200);
} catch (error) {
console.log('Error parsing file', error);
res.status(400).send({ message: error });
}
};
@@ -0,0 +1,64 @@
// Create controller for GET request to '/playback'
// Returns ACK message
export const pbGet = async (req, res) => {
res.send(global.timer.playState);
};
// Create controller for GET request to '/playback/start'
// Starts timer object
export const pbStart = async (req, res) => {
global.timer.start();
res.sendStatus(200);
};
// Create controller for GET request to '/playback/pause'
// Pauses timer object
export const pbPause = async (req, res) => {
global.timer.pause();
res.sendStatus(200);
};
// Create controller for GET request to '/playback/stop'
// Stops timer object
export const pbStop = async (req, res) => {
global.timer.stop();
res.sendStatus(200);
};
// Create controller for GET request to '/playback/roll'
// Sets timer object to roll mode
export const pbRoll = async (req, res) => {
global.timer.roll();
res.sendStatus(501);
};
// Create controller for GET request to '/playback/previous'
// Sets timer object to roll mode
export const pbPrevious = async (req, res) => {
global.timer.previous();
res.sendStatus(200);
};
// Create controller for GET request to '/playback/next'
// Sets timer object to roll mode
export const pbNext = async (req, res) => {
global.timer.next();
res.sendStatus(200);
};
// Create controller for GET request to '/playback/unload'
// Unloads any events
export const pbUnload = async (req, res) => {
global.timer.unload();
console.log('debug: unload called');
res.sendStatus(200);
};
// Create controller for GET request to '/playback/reload'
// Reloads current event
export const pbReload = async (req, res) => {
global.timer.reload();
console.log('debug: reload called');
res.sendStatus(200);
};
+16
View File
@@ -0,0 +1,16 @@
export const dbModel = {
events: [],
event: {
title: '',
url: '',
publicInfo: '',
backstageInfo: '',
},
settings: {
app: 'ontime',
version: 1,
osc_enabled: false,
osc_port: 8888,
lock: false,
},
};
+21
View File
@@ -0,0 +1,21 @@
export const event = {
title: '',
subtitle: '',
presenter: '',
note: '',
timeStart: 0,
timeEnd: 0,
isPublic: false,
type: 'event',
revision: 0,
};
export const delay = {
duration: 0,
type: 'delay',
revision: 0,
};
export const block = {
type: 'block',
};
+11
View File
@@ -0,0 +1,11 @@
import express from 'express';
export const router = express.Router();
// import event controller
import { getEvent, postEvent } from '../controllers/eventController.js';
// create route between controller and 'GET /event' endpoint
router.get('/', getEvent);
// create route between controller and 'POST /event' endpoint
router.post('/', postEvent);
+42
View File
@@ -0,0 +1,42 @@
import express from 'express';
export const router = express.Router();
// import events controller
import {
eventsGetAll,
eventsGetById,
eventsPost,
eventsPut,
eventsPatch,
eventsReorder,
eventsApplyDelay,
eventsDeleteAll,
eventsDelete,
} from '../controllers/eventsController.js';
// create route between controller and '/events/' endpoint
router.get('/', eventsGetAll);
// create route between controller and '/events/:eventId' endpoint
router.get('/:eventId', eventsGetById);
// create route between controller and '/events/' endpoint
router.post('/', eventsPost);
// create route between controller and '/events/' endpoint
router.put('/', eventsPut);
// create route between controller and '/events/' endpoint
router.patch('/', eventsPatch);
// create route between controller and '/events/reorder' endpoint
router.patch('/reorder/', eventsReorder);
// create route between controller and '/events/applydelay/:eventId' endpoint
router.patch('/applydelay/:eventId', eventsApplyDelay);
// create route between controller and '/events/all' endpoint
router.delete('/all', eventsDeleteAll);
// create route between controller and '/events/:eventId' endpoint
router.delete('/:eventId', eventsDelete);
+11
View File
@@ -0,0 +1,11 @@
import express from 'express';
import uploadJson from '../utils/upload.js';
export const router = express.Router();
import { dbDownload, dbUpload } from '../controllers/ontimeController.js';
// create route between controller and '/ontime/db' endpoint
router.get('/db', dbDownload);
// create route between controller and '/ontime/db' endpoint
router.post('/db', uploadJson, dbUpload);
+32
View File
@@ -0,0 +1,32 @@
import express from 'express';
export const router = express.Router();
// import event controller
const playbackController = require('../controllers/playbackController');
// create route between controller and '/playback/' endpoint
router.get('/', playbackController.pbGet);
// create route between controller and '/playback/start' endpoint
router.get('/start', playbackController.pbStart);
// create route between controller and '/playback/pause' endpoint
router.get('/pause', playbackController.pbPause);
// create route between controller and '/playback/stop' endpoint
router.get('/stop', playbackController.pbStop);
// create route between controller and '/playback/roll' endpoint
router.get('/roll', playbackController.pbRoll);
// create route between controller and '/playback/previous' endpoint
router.get('/previous', playbackController.pbPrevious);
// create route between controller and '/playback/next' endpoint
router.get('/next', playbackController.pbNext);
// create route between controller and '/playback/unload' endpoint
router.get('/unload', playbackController.pbUnload);
// create route between controller and '/playback/reload' endpoint
router.get('/reload', playbackController.pbReload);
+23
View File
@@ -0,0 +1,23 @@
import multer from 'multer';
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + '--' + file.originalname);
},
});
// filter only json
const filterJson = (req, file, cb) => {
if (file.mimetype.includes('application/json')) {
cb(null, true);
} else {
cb(null, false);
}
};
const uploadJson = multer({ storage: storage, fileFilter: filterJson });
export default uploadJson.single('jsondb');