More rundown metadata (#1429)

* add dayOffset to OntimeEvent

* refactor: isNewLatest

* calculate totalDays

* refactor: getTimeFromPrevious

* add gap as dataset in OntimeEvent

* use in ui

* format overlap is just a simple text formatting, big test is not needed

* add new fields where needed

* update apply delay

* show nex day eaven if there is no gap

* create test

* use buildin day offset in timeline

* remove todo

* consistent naming

* make a calculateDayOffset util for rundownCache

* refactor: checkIsNextDay to use dayOffset

* spelling

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

* remove unneeded test

* remove todo

* update test db

* use data-testid

* spelling

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

* update comments

* remove null option

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
This commit is contained in:
Alex Christoffer Rasmussen
2025-01-09 13:35:11 +01:00
committed by GitHub
parent 9ba40c35a1
commit e9a7cd0400
30 changed files with 866 additions and 660 deletions
+28
View File
@@ -21,6 +21,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -47,6 +49,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -73,6 +77,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -99,6 +105,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -125,6 +133,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -156,6 +166,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -182,6 +194,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -208,6 +222,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -234,6 +250,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -260,6 +278,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -291,6 +311,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -317,6 +339,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -343,6 +367,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -369,6 +395,8 @@ export const demoDb: DatabaseModel = {
colour: '',
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 500000,
timeDanger: 100000,
custom: {
@@ -25,6 +25,8 @@ export const event: Omit<OntimeEvent, 'id' | 'cue'> = {
type: SupportedEvent.Event,
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
@@ -150,11 +150,11 @@ describe('apply()', () => {
makeOntimeDelay(100),
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
// gap 50
makeOntimeEvent({ id: '2', timeStart: 150, timeEnd: 200, duration: 50 }),
makeOntimeEvent({ id: '2', timeStart: 150, timeEnd: 200, duration: 50, gap: 50 }),
// gap 0
makeOntimeEvent({ id: '3', timeStart: 200, timeEnd: 250, duration: 50, gap: 0 }),
// gap 50
makeOntimeEvent({ id: '3', timeStart: 200, timeEnd: 250, duration: 50 }),
// gap 50
makeOntimeEvent({ id: '4', timeStart: 300, timeEnd: 350, duration: 50 }),
makeOntimeEvent({ id: '4', timeStart: 300, timeEnd: 350, duration: 50, gap: 50 }),
// linked
makeOntimeEvent({ id: '5', timeStart: 350, timeEnd: 400, duration: 50, linkStart: '4' }),
];
@@ -165,7 +165,7 @@ describe('apply()', () => {
// gap 50 (100 - 50)
{ id: '2', timeStart: 150 + 50, timeEnd: 200 + 50, duration: 50, revision: 2 },
// gap 50 (50 - 50)
{ id: '3', timeStart: 200 + 50, timeEnd: 250 + 50, duration: 50, revision: 2 },
{ id: '3', timeStart: 200 + 50, timeEnd: 250 + 50, duration: 50, revision: 2, gap: 0 },
// gap (delay is 0)
{ id: '4', timeStart: 300, timeEnd: 350, duration: 50, revision: 1 },
// linked
@@ -178,6 +178,8 @@ describe('apply()', () => {
makeOntimeDelay(2 * MILLIS_PER_HOUR),
makeOntimeEvent({
id: '1',
gap: 0,
dayOffset: 0,
timeStart: 46800000, // 13:00:00
timeEnd: 50400000, // 14:00:00
duration: MILLIS_PER_HOUR,
@@ -185,6 +187,8 @@ describe('apply()', () => {
// gap 1h
makeOntimeEvent({
id: '2',
gap: 1 * MILLIS_PER_HOUR,
dayOffset: 0,
timeStart: 54000000, // 15:00:00
timeEnd: 57600000, // 16:00:00
duration: MILLIS_PER_HOUR,
@@ -577,6 +577,8 @@ describe('calculateRuntimeDelays', () => {
type: SupportedEvent.Event,
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '659e1',
@@ -605,6 +607,8 @@ describe('calculateRuntimeDelays', () => {
type: SupportedEvent.Event,
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '1c48f',
@@ -633,6 +637,8 @@ describe('calculateRuntimeDelays', () => {
type: SupportedEvent.Event,
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: 'd48c2',
@@ -661,6 +667,8 @@ describe('calculateRuntimeDelays', () => {
type: SupportedEvent.Event,
revision: 0,
delay: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '2f185',
@@ -701,6 +709,8 @@ describe('getDelayAt()', () => {
timeDanger: 60000,
id: '659e1',
delay: 0,
dayOffset: 0,
gap: 0,
cue: '1',
custom: {},
},
@@ -725,6 +735,8 @@ describe('getDelayAt()', () => {
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '1c48f',
@@ -753,6 +765,8 @@ describe('getDelayAt()', () => {
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: 'd48c2',
@@ -781,6 +795,8 @@ describe('getDelayAt()', () => {
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '2f185',
@@ -835,6 +851,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '659e1',
@@ -863,6 +881,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '1c48f',
@@ -891,6 +911,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: 'd48c2',
@@ -919,6 +941,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
colour: '',
type: SupportedEvent.Event,
revision: 0,
dayOffset: 0,
gap: 0,
timeWarning: 120000,
timeDanger: 60000,
id: '2f185',
@@ -9,12 +9,14 @@ import {
} from 'ontime-types';
import {
addToCustomAssignment,
calculateDayOffset,
getLink,
handleCustomField,
handleLink,
hasChanges,
isDataStale,
} from '../rundownCacheUtils.js';
import { MILLIS_PER_HOUR } from 'ontime-utils';
describe('getLink()', () => {
it('should return null if there is no link', () => {
@@ -247,3 +249,61 @@ describe('hasChanges()', () => {
expect(hasChanges(existing, newEvent)).toBe(false);
});
});
describe('calculateDayOffset', () => {
it('returns 0 if there is no previous event', () => {
expect(calculateDayOffset({ timeStart: 0 })).toBe(0);
});
it('returns 0 if the previous event duration is 0', () => {
expect(calculateDayOffset({ timeStart: 0 }, { timeStart: 0, duration: 0 })).toBe(0);
});
it('returns 0 if event starts after previous', () => {
expect(calculateDayOffset({ timeStart: 11 }, { timeStart: 10, duration: 2 })).toBe(0);
});
it('returns 1 if event starts before previous', () => {
expect(calculateDayOffset({ timeStart: 9 }, { timeStart: 10, duration: 2 })).toBe(1);
});
it('returns 1 if event starts at the same time as one before', () => {
expect(calculateDayOffset({ timeStart: 10 }, { timeStart: 10, duration: 2 })).toBe(1);
});
it('should account for an event that crossed midnight and there is a overlap', () => {
expect(
calculateDayOffset(
{ timeStart: MILLIS_PER_HOUR }, // starts at 01:00:00
{ timeStart: 20 * MILLIS_PER_HOUR, duration: 6 * MILLIS_PER_HOUR }, // ends at 02:00:00
),
).toBe(1);
});
it('should account for an event that crossed midnight and there is a gap', () => {
expect(
calculateDayOffset(
{ timeStart: 2 * MILLIS_PER_HOUR }, // starts at 02:00:00
{ timeStart: 23 * MILLIS_PER_HOUR, duration: 2 * MILLIS_PER_HOUR }, // ends at 01:00:00
),
).toBe(1);
});
it('should account for an event that crossed midnight with no overlaps or gaps', () => {
expect(
calculateDayOffset(
{ timeStart: 2 * MILLIS_PER_HOUR }, // starts at 02:00:00
{ timeStart: 20 * MILLIS_PER_HOUR, duration: 6 * MILLIS_PER_HOUR }, // ends at 02:00:00
),
).toBe(1);
});
it('should account for an event that finishes exactly at midnight', () => {
expect(
calculateDayOffset(
{ timeStart: 2 * MILLIS_PER_HOUR }, // starts at 02:00:00
{ timeStart: 23 * MILLIS_PER_HOUR, duration: 6 * MILLIS_PER_HOUR }, // ends at 24:00:00
),
).toBe(1);
});
});
@@ -1,5 +1,5 @@
import { OntimeRundown, isOntimeDelay, isOntimeBlock, isOntimeEvent, OntimeEvent } from 'ontime-types';
import { getTimeFromPrevious, deleteAtIndex } from 'ontime-utils';
import { deleteAtIndex } from 'ontime-utils';
/**
* Calculates all delays in a given rundown
@@ -133,21 +133,14 @@ export function apply(eventId: string, rundown: OntimeRundown): OntimeRundown {
// if the event is not linked, we try and maintain gaps
if (lastEntry !== null) {
const timeFromPrevious: number = getTimeFromPrevious(
currentEntry.timeStart,
lastEntry.timeStart,
lastEntry.timeEnd,
lastEntry.duration,
);
// when applying negative delays, we need to unlink the event
// if the previous event was fully consumed by the delay
if (currentEntry.linkStart && delayValue < 0 && lastEntry.timeStart + delayValue < 0) {
shouldUnlink = true;
}
if (timeFromPrevious > 0) {
delayValue = Math.max(delayValue - timeFromPrevious, 0);
if (currentEntry.gap > 0) {
delayValue = Math.max(delayValue - currentEntry.gap, 0);
}
if (delayValue === 0) {
@@ -24,7 +24,7 @@ import {
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import { createPatch } from '../../utils/parser.js';
import { apply } from './delayUtils.js';
import { handleCustomField, handleLink, hasChanges, isDataStale } from './rundownCacheUtils.js';
import { calculateDayOffset, handleCustomField, handleLink, hasChanges, isDataStale } from './rundownCacheUtils.js';
type EventID = string;
type NormalisedRundown = Record<EventID, OntimeRundownEntry>;
@@ -55,6 +55,7 @@ function setIsStale() {
let totalDelay = 0;
let totalDuration = 0;
let totalDays = 0;
let firstStart: MaybeNumber = null;
let lastEnd: MaybeNumber = null;
@@ -107,6 +108,7 @@ export function generate(
firstStart = null;
lastEnd = null;
totalDuration = 0;
totalDays = 0;
totalDelay = 0;
let lastEntry: PlayableEvent | null = null;
@@ -117,6 +119,7 @@ export function generate(
if (isOntimeEvent(currentEntry)) {
currentEntry.delay = 0;
currentEntry.gap = 0;
// 1. handle links - mutates updatedEvent
handleLink(i, initialRundown, currentEntry, links);
@@ -124,6 +127,9 @@ export function generate(
// 2. handle custom fields - mutates updatedEvent
handleCustomField(customFields, customFieldChangelog, currentEntry, assignedCustomFields);
totalDays += calculateDayOffset(currentEntry, lastEntry);
currentEntry.dayOffset = totalDays;
// update rundown metadata, it only concerns playable events
if (isPlayableEvent(currentEntry)) {
// fist start is always the first event
@@ -131,36 +137,31 @@ export function generate(
firstStart = currentEntry.timeStart;
}
const timeFromPrevious: number = getTimeFromPrevious(
currentEntry.timeStart,
lastEntry?.timeStart,
lastEntry?.timeEnd,
lastEntry?.duration,
);
currentEntry.gap = getTimeFromPrevious(currentEntry, lastEntry);
if (timeFromPrevious === 0) {
if (currentEntry.gap === 0) {
// event starts on previous finish, we add its duration
totalDuration += currentEntry.duration;
} else if (timeFromPrevious > 0) {
} else if (currentEntry.gap > 0) {
// event has a gap, we add the gap and the duration
totalDuration += timeFromPrevious + currentEntry.duration;
} else if (timeFromPrevious < 0) {
totalDuration += currentEntry.gap + currentEntry.duration;
} else if (currentEntry.gap < 0) {
// there is an overlap, we remove the overlap from the duration
// ensuring that the sum is not negative (ie: fully overlapped events)
// NOTE: we add the gap since it is a negative number
totalDuration += Math.max(currentEntry.duration + timeFromPrevious, 0);
totalDuration += Math.max(currentEntry.duration + currentEntry.gap, 0);
}
// remove eventual gaps from the accumulated delay
// we only affect positive delays (time forwards)
if (totalDelay > 0 && timeFromPrevious > 0) {
totalDelay = Math.max(totalDelay - timeFromPrevious, 0);
if (totalDelay > 0 && currentEntry.gap > 0) {
totalDelay = Math.max(totalDelay - currentEntry.gap, 0);
}
// current event delay is the current accumulated delay
currentEntry.delay = totalDelay;
// lastEntry is the event with the latest end time
if (isNewLatest(currentEntry.timeStart, currentEntry.timeEnd, lastEntry?.timeStart, lastEntry?.timeEnd)) {
if (isNewLatest(currentEntry, lastEntry)) {
lastEntry = currentEntry;
}
}
@@ -7,7 +7,7 @@ import {
OntimeRundownEntry,
OntimeBaseEvent,
} from 'ontime-types';
import { getLinkedTimes } from 'ontime-utils';
import { dayInMs, getLinkedTimes } from 'ontime-utils';
/**
* Get linked event
@@ -147,3 +147,36 @@ export function hasChanges<T extends OntimeBaseEvent>(existingEvent: T, newEvent
(key) => !Object.hasOwn(existingEvent, key) || existingEvent[key as keyof T] !== newEvent[key as keyof T],
);
}
/**
* Utility for calculating if the current events should have a day offset
* @param current the current event under test
* @param previous the previous event
* @returns 0 or 1 for easy accumulation with the total days
*/
export function calculateDayOffset(
current: Pick<OntimeEvent, 'timeStart'>,
previous?: Pick<OntimeEvent, 'timeStart' | 'duration'>,
) {
// if there is no previous there can't be a day offset
if (!previous) {
return 0;
}
// if the previous events duration is zero it will push the current event to next day
if (previous.duration === 0) {
return 0;
}
// if the previous event crossed midnight then the current event is in the next day
if (previous.timeStart + previous.duration >= dayInMs) {
return 1;
}
// if the current events starts at the same time or before the previous event then it is the next day
if (current.timeStart <= previous.timeStart) {
return 1;
}
return 0;
}
@@ -37,6 +37,8 @@ describe('cellRequestFromEvent()', () => {
skip: false,
colour: 'red',
delay: 0,
gap: 0,
dayOffset: 0,
revision: 0,
id: '1358',
timeWarning: 0,
@@ -84,6 +86,8 @@ describe('cellRequestFromEvent()', () => {
colour: 'red',
revision: 0,
delay: 0,
gap: 0,
dayOffset: 0,
id: '1358',
timeWarning: 0,
timeDanger: 0,
@@ -132,6 +136,8 @@ describe('cellRequestFromEvent()', () => {
colour: 'red',
revision: 0,
delay: 0,
gap: 0,
dayOffset: 0,
id: '1358',
timeWarning: 0,
timeDanger: 0,
@@ -178,6 +184,8 @@ describe('cellRequestFromEvent()', () => {
skip: false,
colour: 'red',
delay: 0,
gap: 0,
dayOffset: 0,
revision: 0,
id: '1358',
timeWarning: 0,
@@ -212,6 +220,8 @@ describe('cellRequestFromEvent()', () => {
colour: 'red',
revision: 0,
delay: 0,
gap: 0,
dayOffset: 0,
id: '1358',
timeWarning: 0,
timeDanger: 0,
@@ -246,6 +256,8 @@ describe('cellRequestFromEvent()', () => {
colour: 'red',
revision: 0,
delay: 0,
gap: 0,
dayOffset: 0,
id: '1358',
timeWarning: 0,
timeDanger: 0,
+3 -1
View File
@@ -384,7 +384,9 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip,
note: makeString(patchEvent.note, originalEvent.note),
colour: makeString(patchEvent.colour, originalEvent.colour),
delay: 0, //delay is always regenerated by the cache
delay: 0, // is always regenerated by the cache
dayOffset: 0, // is always regenerated by the cache
gap: 0, // is always regenerated by the cache
// short circuit empty string
cue: makeString(patchEvent.cue ?? null, originalEvent.cue),
revision: originalEvent.revision,
+419 -295
View File
@@ -1,307 +1,420 @@
{
"rundown": [
{
"title": "Albania",
"note": "SF1.01",
"endAction": "none",
"timerType": "count-down",
"timeStart": 36000000,
"timeEnd": 37200000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "32d31",
"cue": "SF1.01",
"custom": {
"song": "Sekret",
"artist": "Ronela Hajati"
}
},
{
"title": "Latvia",
"note": "SF1.02",
"endAction": "none",
"timerType": "count-down",
"timeStart": 37500000,
"timeEnd": 38700000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "21cd2",
"cue": "SF1.02",
"custom": {
"song": "Eat Your Salad",
"artist": "Citi Zeni"
}
},
{
"title": "Lithuania",
"note": "SF1.03",
"endAction": "none",
"timerType": "count-down",
"timeStart": 39000000,
"timeEnd": 40200000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "0b371",
"cue": "SF1.03",
"custom": {
"song": "Sentimentai",
"artist": "Monika Liu"
}
},
{
"title": "Switzerland",
"note": "SF1.04",
"endAction": "none",
"timerType": "count-down",
"timeStart": 40500000,
"timeEnd": 41700000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "3cd28",
"cue": "SF1.04",
"custom": {
"song": "Boys Do Cry",
"artist": "Marius Bear"
}
},
{
"title": "Slovenia",
"note": "SF1.05",
"endAction": "none",
"timerType": "count-down",
"timeStart": 42000000,
"timeEnd": 43200000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "e457f",
"cue": "SF1.05",
"custom": {
"song": "Disko",
"artist": "LPS"
}
},
{
"title": "Lunch break",
"type": "block",
"id": "01e85"
},
{
"title": "Ukraine",
"note": "SF1.06",
"endAction": "none",
"timerType": "count-down",
"timeStart": 47100000,
"timeEnd": 48300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "1c420",
"cue": "SF1.06",
"custom": {
"song": "Stefania",
"artist": "Kalush Orchestra"
}
},
{
"title": "Bulgaria",
"note": "SF1.07",
"endAction": "none",
"timerType": "count-down",
"timeStart": 48600000,
"timeEnd": 49800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "b7737",
"cue": "SF1.07",
"custom": {
"song": "Intention",
"artist": "Intelligent Music Project"
}
},
{
"title": "Netherlands",
"note": "SF1.08",
"endAction": "none",
"timerType": "count-down",
"timeStart": 50100000,
"timeEnd": 51300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "d3a80",
"cue": "SF1.08",
"custom": {
"song": "De Diepte",
"artist": "S10"
}
},
{
"title": "Moldova",
"note": "SF1.09",
"endAction": "none",
"timerType": "count-down",
"timeStart": 51600000,
"timeEnd": 52800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "8276c",
"cue": "SF1.09",
"custom": {
"song": "Trenuletul",
"artist": "Zdob si Zdub"
}
},
{
"title": "Portugal",
"note": "SF1.10",
"endAction": "none",
"timerType": "count-down",
"timeStart": 53100000,
"timeEnd": 54300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "2340b",
"cue": "SF1.10",
"custom": {
"song": "Saudade Saudade",
"artist": "Maro"
}
},
{
"title": "Afternoon break",
"type": "block",
"id": "cb90b"
},
{
"title": "Croatia",
"note": "SF1.11",
"endAction": "none",
"timerType": "count-down",
"timeStart": 56100000,
"timeEnd": 57300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "503c4",
"cue": "SF1.11",
"custom": {
"song": "Guilty Pleasure",
"artist": "Mia Dimsic"
}
},
{
"title": "Denmark",
"note": "SF1.12",
"endAction": "none",
"timerType": "count-down",
"timeStart": 57600000,
"timeEnd": 58800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "5e965",
"cue": "SF1.12",
"custom": {
"song": "The Show",
"artist": "Reddi"
}
},
{
"title": "Austria",
"note": "SF1.13",
"endAction": "none",
"timerType": "count-down",
"timeStart": 59100000,
"timeEnd": 60300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "bab4a",
"cue": "SF1.13",
"custom": {
"song": "Halo",
"artist": "LUM!X & Pia Maria"
}
},
{
"title": "Greece",
"note": "SF1.14",
"endAction": "none",
"timerType": "count-down",
"timeStart": 60600000,
"timeEnd": 61800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "d3eb1",
"cue": "SF1.14",
"custom": {
"song": "Die Together",
"artist": "Amanda Tenfjord"
}
{
"id": "32d31",
"type": "event",
"title": "Albania",
"timeStart": 36000000,
"timeEnd": 37200000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.01",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.01",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "Sekret",
"artist": "Ronela Hajati"
}
],
},
{
"id": "21cd2",
"type": "event",
"title": "Latvia",
"timeStart": 37500000,
"timeEnd": 38700000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.02",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.02",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "Eat Your Salad",
"artist": "Citi Zeni"
}
},
{
"id": "0b371",
"type": "event",
"title": "Lithuania",
"timeStart": 39000000,
"timeEnd": 40200000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.03",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.03",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "Sentimentai",
"artist": "Monika Liu"
}
},
{
"id": "3cd28",
"type": "event",
"title": "Switzerland",
"timeStart": 40500000,
"timeEnd": 41700000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.04",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.04",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "Boys Do Cry",
"artist": "Marius Bear"
}
},
{
"id": "e457f",
"type": "event",
"title": "Slovenia",
"timeStart": 42000000,
"timeEnd": 43200000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.05",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.05",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "Disko",
"artist": "LPS"
}
},
{
"title": "Lunch break",
"type": "block",
"id": "01e85"
},
{
"id": "1c420",
"type": "event",
"title": "Ukraine",
"timeStart": 47100000,
"timeEnd": 48300000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.06",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.06",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "Stefania",
"artist": "Kalush Orchestra"
}
},
{
"id": "b7737",
"type": "event",
"title": "Bulgaria",
"timeStart": 48600000,
"timeEnd": 49800000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.07",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.07",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "Intention",
"artist": "Intelligent Music Project"
}
},
{
"id": "d3a80",
"type": "event",
"title": "Netherlands",
"timeStart": 50100000,
"timeEnd": 51300000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.08",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.08",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "De Diepte",
"artist": "S10"
}
},
{
"id": "8276c",
"type": "event",
"title": "Moldova",
"timeStart": 51600000,
"timeEnd": 52800000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.09",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.09",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "Trenuletul",
"artist": "Zdob si Zdub"
}
},
{
"id": "2340b",
"type": "event",
"title": "Portugal",
"timeStart": 53100000,
"timeEnd": 54300000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.10",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.10",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "Saudade Saudade",
"artist": "Maro"
}
},
{
"title": "Afternoon break",
"type": "block",
"id": "cb90b"
},
{
"id": "503c4",
"type": "event",
"title": "Croatia",
"timeStart": 56100000,
"timeEnd": 57300000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.11",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.11",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "Guilty Pleasure",
"artist": "Mia Dimsic"
}
},
{
"id": "5e965",
"type": "event",
"title": "Denmark",
"timeStart": 57600000,
"timeEnd": 58800000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.12",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.12",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "The Show",
"artist": "Reddi"
}
},
{
"id": "bab4a",
"type": "event",
"title": "Austria",
"timeStart": 59100000,
"timeEnd": 60300000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.13",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.13",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "Halo",
"artist": "LUM!X & Pia Maria"
}
},
{
"id": "d3eb1",
"type": "event",
"title": "Greece",
"timeStart": 60600000,
"timeEnd": 61800000,
"duration": 1200000,
"timeStrategy": "lock-duration",
"linkStart": null,
"endAction": "none",
"timerType": "count-down",
"countToEnd": false,
"isPublic": true,
"skip": false,
"note": "SF1.14",
"colour": "",
"delay": 0,
"dayOffset": 0,
"gap": 0,
"cue": "SF1.14",
"revision": 0,
"timeWarning": 120000,
"timeDanger": 60000,
"custom": {
"song": "Die Together",
"artist": "Amanda Tenfjord"
}
}
],
"project": {
"title": "Eurovision Song Contest",
"description": "Turin 2022",
"publicUrl": "www.getontime.no",
"publicInfo": "Rehearsal Schedule - Turin 2022",
"backstageUrl": "www.github.com/cpvalente/ontime",
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal"
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
"projectLogo": null
},
"settings": {
"app": "ontime",
"version": "2.0.0",
"version": "3.10.2",
"serverPort": 4001,
"editorKey": null,
"operatorKey": null,
@@ -312,10 +425,9 @@
"overrideStyles": false,
"normalColor": "#ffffffcc",
"warningColor": "#FFAB33",
"warningThreshold": 120000,
"dangerColor": "#ED3333",
"dangerThreshold": 60000,
"endMessage": ""
"endMessage": "",
"freezeEnd": false
},
"urlPresets": [
{
@@ -335,5 +447,17 @@
"http": {
"enabledOut": true,
"subscriptions": []
},
"customFields": {
"song": {
"type": "string",
"colour": "",
"label": "song"
},
"artist": {
"type": "string",
"colour": "",
"label": "artist"
}
}
}