From 28b99926269c12805ad0a9bae573edf4e8c99dd1 Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Fri, 5 Jul 2024 16:13:26 +0100 Subject: [PATCH 001/178] Add LSG support for templateAdLibs --- .../ItemRenderers/ActionItemRenderer.tsx | 4 +- packages/blueprints-integration/src/action.ts | 6 ++- .../api/schemas/adLibs.yaml | 12 +++++ .../src/topics/adLibsTopic.ts | 54 ++++++++++++++++++- 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx b/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx index 361b09e2d8..0e0cbfe2d4 100644 --- a/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx +++ b/meteor/client/ui/Shelf/Inspector/ItemRenderers/ActionItemRenderer.tsx @@ -227,9 +227,9 @@ export default translateWithTracker((props: IProps) = />
- {action.userDataManifest && action.userDataManifest.editableFields && !targetAction ? ( + {action.userDataManifest && action.userDataManifest.optionsSchema && !targetAction ? ( - ) : action.userDataManifest && action.userDataManifest.editableFields && targetAction ? ( + ) : action.userDataManifest && action.userDataManifest.optionsSchema && targetAction ? ( Editable Fields are not currently supported. ) : null}
diff --git a/packages/blueprints-integration/src/action.ts b/packages/blueprints-integration/src/action.ts index 2639f7f8ef..53eeab3e86 100644 --- a/packages/blueprints-integration/src/action.ts +++ b/packages/blueprints-integration/src/action.ts @@ -99,8 +99,10 @@ export interface IBlueprintActionManifest + /** Schema for the executeAdLib adLibOptions property to allow for customising */ + optionsSchema?: JSONBlob + /** Sets this to be a template action, requiring adLibOptions to be provided and validated */ + template?: boolean // Potential future properties: // /** Execute the action after userData is changed. If not present ActionExecuteAfterChanged.none is assumed. */ // executeOnUserDataChanged?: ActionExecuteAfterChanged diff --git a/packages/live-status-gateway/api/schemas/adLibs.yaml b/packages/live-status-gateway/api/schemas/adLibs.yaml index b69918ae1f..3141ef17d5 100644 --- a/packages/live-status-gateway/api/schemas/adLibs.yaml +++ b/packages/live-status-gateway/api/schemas/adLibs.yaml @@ -22,6 +22,11 @@ $defs: type: array items: $ref: '#/$defs/globalAdLib' + templateAdLibs: + description: The available Template AdLibs + type: array + items: + $ref: '#/$defs/templateAdLib' required: [event, rundownPlaylistId, adLibs, globalAdLibs] additionalProperties: false examples: @@ -56,6 +61,9 @@ $defs: globalAdLib: $ref: '#/$defs/adLibBase' additionalProperties: false + templateAdLib: + $ref: '#/$defs/adLibBase' + additionalProperties: false adLibBase: type: object properties: @@ -92,6 +100,9 @@ $defs: type: string publicData: description: Optional arbitrary data + optionsSchema: + description: JSON schema definition of the adLib properties that can be modified using the adLibOptions property in executeAdLib + type: string required: [id, name, sourceLayer, actionType] examples: - id: 'C6K_yIMuGFUk8X_L9A9_jRT6aq4_' @@ -103,3 +114,4 @@ $defs: tags: ['music_video'] publicData: fileName: MV000123.mxf + optionsSchema: '{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Play Video Clip","type":"object","properties":{"type":"adlib_action_video_clip","label":{"type":"string"},"clipId":{"type":"string"},"vo":{"type":"boolean"},"target":{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Object Id","description":"Id of an object sent to Sofie","type":"string"},"duration":{"type":"number","exclusiveMinimum":0},"takeType":{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"AdLib Action Take Type","type":"string","enum":["take_immediate","queue"]},"transition":{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"AdLib Action Transition Type","oneOf":[{"type":"object","properties":{"type":"cut"},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":"mix","duration":{"type":"number","exclusiveMinimum":0,"description":"Duration in ms"}},"required":["type","duration"],"additionalProperties":false},{"type":"object","properties":{"type":"wipe","duration":{"type":"number","exclusiveMinimum":0,"description":"Duration in ms"},"patternId":{"type":"string","description":"Type of wipe to use"}},"required":["type","duration","patternId"],"additionalProperties":false},{"type":"object","properties":{"type":"macro","macroId":{"type":"string","description":"Macro template to recall"}},"required":["type","macroId"],"additionalProperties":false}]}},"required":["type","clipId","vo","target"],"additionalProperties":false}"' diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts index e5de8fd7d4..3ce6da452f 100644 --- a/packages/live-status-gateway/src/topics/adLibsTopic.ts +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -32,6 +32,7 @@ export interface AdLibsStatus { rundownPlaylistId: string | null adLibs: AdLibStatus[] globalAdLibs: GlobalAdLibStatus[] + templateAdLibs?: TemplateAdLibStatus[] } interface AdLibActionType { @@ -45,6 +46,7 @@ interface AdLibStatus extends AdLibStatusBase { } type GlobalAdLibStatus = AdLibStatusBase +type TemplateAdLibStatus = AdLibStatusBase interface AdLibStatusBase { id: string @@ -54,6 +56,7 @@ interface AdLibStatusBase { actionType: AdLibActionType[] tags?: string[] publicData: unknown + optionsSchema?: any } export class AdLibsTopic @@ -75,6 +78,7 @@ export class AdLibsTopic private _parts: ReadonlyMap = new Map() private _segments: ReadonlyMap = new Map() private _globalAdLibActions: RundownBaselineAdLibAction[] | undefined + private _templateAdLibActions: RundownBaselineAdLibAction[] | undefined private _globalAdLibs: RundownBaselineAdLibItem[] | undefined private throttledSendStatusToAll: () => void @@ -94,6 +98,7 @@ export class AdLibsTopic sendStatus(subscribers: Iterable): void { const adLibs: WithSortingMetadata[] = [] const globalAdLibs: WithSortingMetadata[] = [] + const templateAdLibs: WithSortingMetadata[] = [] if (this._adLibActions) { adLibs.push( @@ -125,6 +130,7 @@ export class AdLibsTopic actionType: triggerModes, tags: action.display.tags, publicData: action.publicData, + optionsSchema: action.userDataManifest.optionsSchema, }, id: unprotectString(action._id), label: name, @@ -193,6 +199,7 @@ export class AdLibsTopic actionType: triggerModes, tags: action.display.tags, publicData: action.publicData, + optionsSchema: action.userDataManifest.optionsSchema, }, id: unprotectString(action._id), label: name, @@ -227,12 +234,51 @@ export class AdLibsTopic ) } + if (this._templateAdLibActions) { + templateAdLibs.push( + ...this._templateAdLibActions.map((action) => { + const sourceLayerName = this._sourceLayersMap.get( + (action.display as IBlueprintActionManifestDisplayContent).sourceLayerId + ) + const outputLayerName = this._outputLayersMap.get( + (action.display as IBlueprintActionManifestDisplayContent).outputLayerId + ) + const triggerModes = action.triggerModes + ? action.triggerModes.map((t) => + literal({ + name: t.data, + label: t.display.label.key, + }) + ) + : [] + const name = interpollateTranslation(action.display.label.key, action.display.label.args) + return literal>({ + obj: { + id: unprotectString(action._id), + name, + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: triggerModes, + tags: action.display.tags, + publicData: action.publicData, + optionsSchema: action.userDataManifest.optionsSchema, + }, + id: unprotectString(action._id), + label: name, + rundownRank: this._activePlaylist?.rundownIdsInOrder.indexOf(action.rundownId), + itemRank: action.display._rank, + }) + }) + ) + } + const adLibsStatus: AdLibsStatus = this._activePlaylist ? { event: 'adLibs', rundownPlaylistId: unprotectString(this._activePlaylist._id), adLibs: sortContent(adLibs), globalAdLibs: sortContent(globalAdLibs), + templateAdLibs: templateAdLibs.length ? sortContent(templateAdLibs) : undefined, } : { event: 'adLibs', rundownPlaylistId: null, adLibs: [], globalAdLibs: [] } @@ -275,7 +321,13 @@ export class AdLibsTopic case GlobalAdLibActionsHandler.name: { const globalAdLibActions = data ? (data as RundownBaselineAdLibAction[]) : [] this.logUpdateReceived('globalAdLibActions', source) - this._globalAdLibActions = globalAdLibActions + this._globalAdLibActions = [] + globalAdLibActions.forEach((action) => { + if (action.userDataManifest?.template) { + if (!this._templateAdLibActions) this._templateAdLibActions = [action] + else this._templateAdLibActions.push(action) + } else this._globalAdLibActions?.push(action) + }) break } case AdLibsHandler.name: { From 7f77b3f348deb94a69136dd315eff7fd4e7a6f56 Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Mon, 8 Jul 2024 16:16:04 +0100 Subject: [PATCH 002/178] Add studio and showStyle blueprintConfig validation support via blueprints --- meteor/lib/api/rest/v1/showstyles.ts | 29 +++ meteor/lib/api/rest/v1/studios.ts | 28 +++ meteor/server/api/rest/v1/showstyles.ts | 187 +++++++++++++++- meteor/server/api/rest/v1/studios.ts | 145 +++++++++++- meteor/server/api/rest/v1/typeConversion.ts | 207 ++++++++++++++++-- .../src/api/showStyle.ts | 11 + .../blueprints-integration/src/api/studio.ts | 9 + 7 files changed, 587 insertions(+), 29 deletions(-) diff --git a/meteor/lib/api/rest/v1/showstyles.ts b/meteor/lib/api/rest/v1/showstyles.ts index 68dfa88d48..d50d9f15e8 100644 --- a/meteor/lib/api/rest/v1/showstyles.ts +++ b/meteor/lib/api/rest/v1/showstyles.ts @@ -59,6 +59,34 @@ export interface ShowStylesRestAPI { showStyleBaseId: ShowStyleBaseId, showStyleBase: APIShowStyleBase ): Promise> + /** + * Gets a ShowStyle config, if the ShowStyle id exists. + * + * Throws if the specified ShowStyle does not exist. + * @param connection Connection data including client and header details + * @param event User event string + * @param showStyleBaseId ShowStyleBaseId to fetch + */ + getShowStyleConfig( + connection: Meteor.Connection, + event: string, + showStyleBaseId: ShowStyleBaseId + ): Promise> + /** + * Updates a ShowStyle configuration. + * + * Throws if the ShowStyle is in use in an active Rundown. + * @param connection Connection data including client and header details + * @param event User event string + * @param showStyleBaseId Id of the ShowStyleBase to update + * @param object Blueprint configuration object + */ + updateShowStyleConfig( + connection: Meteor.Connection, + event: string, + showStyleBaseId: ShowStyleBaseId, + config: object + ): Promise> /** * Removed a ShowStyleBase. * @@ -192,6 +220,7 @@ export interface APIShowStyleBase { export interface APIShowStyleVariant { name: string showStyleBaseId: string + blueprintConfigPresetId?: string config: object rank: number } diff --git a/meteor/lib/api/rest/v1/studios.ts b/meteor/lib/api/rest/v1/studios.ts index ce3e967ed8..fd3a133283 100644 --- a/meteor/lib/api/rest/v1/studios.ts +++ b/meteor/lib/api/rest/v1/studios.ts @@ -56,6 +56,34 @@ export interface StudiosRestAPI { studioId: StudioId, studio: APIStudio ): Promise> + /** + * Gets a Studio config, if it exists. + * + * Throws if the specified Studio does not exist. + * @param connection Connection data including client and header details + * @param event User event string + * @param studioId Id of the Studio to fetch + */ + getStudioConfig( + connection: Meteor.Connection, + event: string, + studioId: StudioId + ): Promise> + /** + * Updates a Studio configuration. + * + * Throws if the Studio already exists and is in use in an active Rundown. + * @param connection Connection data including client and header details + * @param event User event string + * @param studioId Id of the Studio to update + * @param object Blueprint configuration object + */ + updateStudioConfig( + connection: Meteor.Connection, + event: string, + studioId: StudioId, + config: object + ): Promise> /** * Deletes a Studio. * diff --git a/meteor/server/api/rest/v1/showstyles.ts b/meteor/server/api/rest/v1/showstyles.ts index b97532d58a..52cc5ec477 100644 --- a/meteor/server/api/rest/v1/showstyles.ts +++ b/meteor/server/api/rest/v1/showstyles.ts @@ -20,6 +20,7 @@ import { APIShowStyleVariantFrom, showStyleBaseFrom, showStyleVariantFrom, + validateAPIBlueprintConfigForShowStyle, } from './typeConversion' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -60,16 +61,38 @@ class ShowStylesServerAPI implements ShowStylesRestAPI { const showStyleBase = await ShowStyleBases.findOneAsync(showStyleBaseId) if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase ${showStyleBaseId} does not exist`) - return ClientAPI.responseSuccess(APIShowStyleBaseFrom(showStyleBase)) + return ClientAPI.responseSuccess(await APIShowStyleBaseFrom(showStyleBase)) } async addOrUpdateShowStyleBase( _connection: Meteor.Connection, _event: string, showStyleBaseId: ShowStyleBaseId, - showStyleBase: APIShowStyleBase + apiShowStyleBase: APIShowStyleBase ): Promise> { - const showStyle = await showStyleBaseFrom(showStyleBase, showStyleBaseId) + const blueprintConfigValidation = await validateAPIBlueprintConfigForShowStyle( + apiShowStyleBase, + protectString(apiShowStyleBase.blueprintId) + ) + const blueprintConfigValidationOK = blueprintConfigValidation.reduce( + (acc, msg) => acc && msg.level === NoteSeverity.INFO, + true + ) + if (!blueprintConfigValidationOK) { + const details = JSON.stringify( + blueprintConfigValidation.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`addOrUpdateShowStyleBase failed blueprint config validation with errors: ${details}`) + throw new Meteor.Error( + 409, + `ShowStyleBase ${showStyleBaseId} has failed blueprint config validation`, + details + ) + } + + const showStyle = await showStyleBaseFrom(apiShowStyleBase, showStyleBaseId) if (!showStyle) throw new Meteor.Error(400, `Invalid ShowStyleBase`) const existingShowStyle = await ShowStyleBases.findOneAsync(showStyleBaseId) @@ -108,7 +131,104 @@ class ShowStylesServerAPI implements ShowStylesRestAPI { throw new Meteor.Error(409, `ShowStyleBase ${showStyleBaseId} has failed validation`, details) } - return ClientAPI.responseSuccess(await runUpgradeForShowStyleBase(showStyleBaseId)) + return ClientAPI.responseSuccess( + await new Promise((resolve) => + // wait for the upsert to complete before upgrade + setTimeout(async () => resolve(await runUpgradeForShowStyleBase(showStyleBaseId)), 200) + ) + ) + } + + async getShowStyleConfig( + _connection: Meteor.Connection, + _event: string, + showStyleBaseId: ShowStyleBaseId + ): Promise> { + const showStyleBase = await ShowStyleBases.findOneAsync(showStyleBaseId) + if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase ${showStyleBaseId} does not exist`) + + return ClientAPI.responseSuccess((await APIShowStyleBaseFrom(showStyleBase)).config) + } + + async updateShowStyleConfig( + _connection: Meteor.Connection, + _event: string, + showStyleBaseId: ShowStyleBaseId, + config: object + ): Promise> { + const existingShowStyleBase = await ShowStyleBases.findOneAsync(showStyleBaseId) + if (existingShowStyleBase) { + const existingShowStyle = await ShowStyleBases.findOneAsync(showStyleBaseId) + if (existingShowStyle) { + const rundowns = (await Rundowns.findFetchAsync( + { showStyleBaseId }, + { projection: { playlistId: 1 } } + )) as Array> + const playlists = (await RundownPlaylists.findFetchAsync( + { _id: { $in: rundowns.map((r) => r.playlistId) } }, + { + projection: { + activationId: 1, + }, + } + )) as Array> + if (playlists.some((playlist) => playlist.activationId !== undefined)) { + throw new Meteor.Error( + 412, + `Cannot update ShowStyleBase ${showStyleBaseId} as it is in use by an active Playlist` + ) + } + } + } else throw new Meteor.Error(404, `ShowStyleBase ${showStyleBaseId} not found`) + + const apiShowStyleBase = await APIShowStyleBaseFrom(existingShowStyleBase) + apiShowStyleBase.config = config + + const blueprintConfigValidation = await validateAPIBlueprintConfigForShowStyle( + apiShowStyleBase, + protectString(apiShowStyleBase.blueprintId) + ) + const blueprintConfigValidationOK = blueprintConfigValidation.reduce( + (acc, msg) => acc && msg.level === NoteSeverity.INFO, + true + ) + if (!blueprintConfigValidationOK) { + const details = JSON.stringify( + blueprintConfigValidation.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`updateShowStyleBase failed blueprint config validation with errors: ${details}`) + throw new Meteor.Error( + 409, + `ShowStyleBase ${showStyleBaseId} has failed blueprint config validation`, + details + ) + } + + const showStyle = await showStyleBaseFrom(apiShowStyleBase, showStyleBaseId) + if (!showStyle) throw new Meteor.Error(400, `Invalid ShowStyleBase`) + + await ShowStyleBases.upsertAsync(showStyleBaseId, showStyle) + + const validation = await validateConfigForShowStyleBase(showStyleBaseId) + const validateOK = validation.messages.reduce((acc, msg) => acc && msg.level === NoteSeverity.INFO, true) + if (!validateOK) { + const details = JSON.stringify( + validation.messages.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`addOrUpdateShowStyleBase failed validation with errors: ${details}`) + throw new Meteor.Error(409, `ShowStyleBase ${showStyleBaseId} has failed validation`, details) + } + + return ClientAPI.responseSuccess( + await new Promise((resolve) => + // wait for the upsert to complete before upgrade + setTimeout(async () => resolve(await runUpgradeForShowStyleBase(showStyleBaseId)), 200) + ) + ) } async deleteShowStyleBase( @@ -185,7 +305,7 @@ class ShowStylesServerAPI implements ShowStylesRestAPI { const variant = await ShowStyleVariants.findOneAsync(showStyleVariantId) if (!variant) throw new Meteor.Error(404, `ShowStyleVariant ${showStyleVariantId} not found`) - return ClientAPI.responseSuccess(APIShowStyleVariantFrom(variant)) + return ClientAPI.responseSuccess(await APIShowStyleVariantFrom(showStyleBase, variant)) } async addOrUpdateShowStyleVariant( @@ -193,12 +313,34 @@ class ShowStylesServerAPI implements ShowStylesRestAPI { _event: string, showStyleBaseId: ShowStyleBaseId, showStyleVariantId: ShowStyleVariantId, - showStyleVariant: APIShowStyleVariant + apiShowStyleVariant: APIShowStyleVariant ): Promise> { const showStyleBase = await ShowStyleBases.findOneAsync(showStyleBaseId) if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase ${showStyleBaseId} does not exist`) - const showStyle = showStyleVariantFrom(showStyleVariant, showStyleVariantId) + const blueprintConfigValidation = await validateAPIBlueprintConfigForShowStyle( + apiShowStyleVariant, + showStyleBase.blueprintId + ) + const blueprintConfigValidationOK = blueprintConfigValidation.reduce( + (acc, msg) => acc && msg.level === NoteSeverity.INFO, + true + ) + if (!blueprintConfigValidationOK) { + const details = JSON.stringify( + blueprintConfigValidation.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`addOrUpdateShowStyleVariant failed blueprint config validation with errors: ${details}`) + throw new Meteor.Error( + 409, + `ShowStyleBase ${showStyleBaseId} variant has failed blueprint config validation`, + details + ) + } + + const showStyle = showStyleVariantFrom(apiShowStyleVariant, showStyleVariantId) if (!showStyle) throw new Meteor.Error(400, `Invalid ShowStyleVariant`) const existingShowStyle = await ShowStyleVariants.findOneAsync(showStyleVariantId) @@ -335,6 +477,37 @@ export function registerRoutes(registerRoute: APIRegisterHook } ) + registerRoute<{ showStyleBaseId: string }, never, object>( + 'get', + '/showstyles/:showStyleBaseId/config', + new Map([[404, [UserErrorMessage.ShowStyleBaseNotFound]]]), + showStylesAPIFactory, + async (serverAPI, connection, event, params, _) => { + const showStyleBaseId = protectString(params.showStyleBaseId) + logger.info(`API GET: ShowStyleBase config ${showStyleBaseId}`) + + check(showStyleBaseId, String) + return await serverAPI.getShowStyleConfig(connection, event, showStyleBaseId) + } + ) + + registerRoute<{ showStyleBaseId: string }, object, void>( + 'put', + '/showstyles/:showStyleBaseId/config', + new Map([ + [404, [UserErrorMessage.ShowStyleBaseNotFound]], + [409, [UserErrorMessage.ValidationFailed]], + ]), + showStylesAPIFactory, + async (serverAPI, connection, event, params, body) => { + const showStyleBaseId = protectString(params.showStyleBaseId) + logger.info(`API PUT: Update ShowStyleBase config ${showStyleBaseId}`) + + check(showStyleBaseId, String) + return await serverAPI.updateShowStyleConfig(connection, event, showStyleBaseId, body) + } + ) + registerRoute<{ showStyleBaseId: string }, never, void>( 'delete', '/showstyles/:showStyleBaseId', diff --git a/meteor/server/api/rest/v1/studios.ts b/meteor/server/api/rest/v1/studios.ts index 9c5cfb3096..b33379a769 100644 --- a/meteor/server/api/rest/v1/studios.ts +++ b/meteor/server/api/rest/v1/studios.ts @@ -8,7 +8,7 @@ import { APIStudio, StudioAction, StudioActionType, StudiosRestAPI } from '../.. import { Meteor } from 'meteor/meteor' import { ClientAPI } from '../../../../lib/api/client' import { PeripheralDevices, RundownPlaylists, Studios } from '../../../collections' -import { APIStudioFrom, studioFrom } from './typeConversion' +import { APIStudioFrom, studioFrom, validateAPIBlueprintConfigForStudio } from './typeConversion' import { runUpgradeForStudio, validateConfigForStudio } from '../../../migration/upgrades' import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -35,9 +35,24 @@ class StudiosServerAPI implements StudiosRestAPI { async addStudio( _connection: Meteor.Connection, _event: string, - studio: APIStudio + apiStudio: APIStudio ): Promise> { - const newStudio = await studioFrom(studio) + const blueprintConfigValidation = await validateAPIBlueprintConfigForStudio(apiStudio) + const blueprintConfigValidationOK = blueprintConfigValidation.reduce( + (acc, msg) => acc && msg.level === NoteSeverity.INFO, + true + ) + if (!blueprintConfigValidationOK) { + const details = JSON.stringify( + blueprintConfigValidation.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`addStudio failed blueprint config validation with errors: ${details}`) + throw new Meteor.Error(409, `Studio has failed blueprint config validation`, details) + } + + const newStudio = await studioFrom(apiStudio) if (!newStudio) throw new Meteor.Error(400, `Invalid Studio`) const newStudioId = await Studios.insertAsync(newStudio) @@ -66,16 +81,31 @@ class StudiosServerAPI implements StudiosRestAPI { const studio = await Studios.findOneAsync(studioId) if (!studio) throw new Meteor.Error(404, `Studio ${studioId} not found`) - return ClientAPI.responseSuccess(APIStudioFrom(studio)) + return ClientAPI.responseSuccess(await APIStudioFrom(studio)) } async addOrUpdateStudio( _connection: Meteor.Connection, _event: string, studioId: StudioId, - studio: APIStudio + apiStudio: APIStudio ): Promise> { - const newStudio = await studioFrom(studio, studioId) + const blueprintConfigValidation = await validateAPIBlueprintConfigForStudio(apiStudio) + const blueprintConfigValidationOK = blueprintConfigValidation.reduce( + (acc, msg) => acc && msg.level === NoteSeverity.INFO, + true + ) + if (!blueprintConfigValidationOK) { + const details = JSON.stringify( + blueprintConfigValidation.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`addOrUpdateStudio failed blueprint config validation with errors: ${details}`) + throw new Meteor.Error(409, `Studio ${studioId} has failed blueprint config validation`, details) + } + + const newStudio = await studioFrom(apiStudio, studioId) if (!newStudio) throw new Meteor.Error(400, `Invalid Studio`) const existingStudio = await Studios.findOneAsync(studioId) @@ -107,7 +137,77 @@ class StudiosServerAPI implements StudiosRestAPI { throw new Meteor.Error(409, `Studio ${studioId} has failed validation`, details) } - return ClientAPI.responseSuccess(await runUpgradeForStudio(studioId)) + return ClientAPI.responseSuccess( + await new Promise((resolve) => + // wait for the upsert to complete before upgrade + setTimeout(async () => resolve(await runUpgradeForStudio(studioId)), 200) + ) + ) + } + + async getStudioConfig( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId + ): Promise> { + const studio = await Studios.findOneAsync(studioId) + if (!studio) throw new Meteor.Error(404, `Studio ${studioId} not found`) + + return ClientAPI.responseSuccess((await APIStudioFrom(studio)).config) + } + + async updateStudioConfig( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + config: object + ): Promise> { + const existingStudio = await Studios.findOneAsync(studioId) + if (!existingStudio) { + throw new Meteor.Error(404, `Studio ${studioId} not found`) + } + + const apiStudio = await APIStudioFrom(existingStudio) + apiStudio.config = config + + const blueprintConfigValidation = await validateAPIBlueprintConfigForStudio(apiStudio) + const blueprintConfigValidationOK = blueprintConfigValidation.reduce( + (acc, msg) => acc && msg.level === NoteSeverity.INFO, + true + ) + if (!blueprintConfigValidationOK) { + const details = JSON.stringify( + blueprintConfigValidation.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`updateStudioConfig failed blueprint config validation with errors: ${details}`) + throw new Meteor.Error(409, `Studio ${studioId} has failed blueprint config validation`, details) + } + + const newStudio = await studioFrom(apiStudio, studioId) + if (!newStudio) throw new Meteor.Error(400, `Invalid Studio`) + + await Studios.upsertAsync(studioId, newStudio) + + const validation = await validateConfigForStudio(studioId) + const validateOK = validation.messages.reduce((acc, msg) => acc && msg.level === NoteSeverity.INFO, true) + if (!validateOK) { + const details = JSON.stringify( + validation.messages.filter((msg) => msg.level < NoteSeverity.INFO).map((msg) => msg.message.key), + null, + 2 + ) + logger.error(`updateStudioConfig failed validation with errors: ${details}`) + throw new Meteor.Error(409, `Studio ${studioId} has failed validation`, details) + } + + return ClientAPI.responseSuccess( + await new Promise((resolve) => + // wait for the upsert to complete before upgrade + setTimeout(async () => resolve(await runUpgradeForStudio(studioId)), 200) + ) + ) } async deleteStudio( @@ -337,6 +437,37 @@ export function registerRoutes(registerRoute: APIRegisterHook): } ) + registerRoute<{ studioId: string }, never, object>( + 'get', + '/studios/:studioId/config', + new Map([[404, [UserErrorMessage.StudioNotFound]]]), + studiosAPIFactory, + async (serverAPI, connection, event, params, _) => { + const studioId = protectString(params.studioId) + logger.info(`API GET: studio config ${studioId}`) + + check(studioId, String) + return await serverAPI.getStudioConfig(connection, event, studioId) + } + ) + + registerRoute<{ studioId: string }, object, void>( + 'put', + '/studios/:studioId/config', + new Map([ + [404, [UserErrorMessage.StudioNotFound]], + [409, [UserErrorMessage.ValidationFailed]], + ]), + studiosAPIFactory, + async (serverAPI, connection, event, params, body) => { + const studioId = protectString(params.studioId) + logger.info(`API PUT: Update studio config ${studioId}`) + + check(studioId, String) + return await serverAPI.updateStudioConfig(connection, event, studioId, body) + } + ) + registerRoute<{ studioId: string }, never, void>( 'delete', '/studios/:studioId', diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index fbc1d7a870..c7eda619f4 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -1,14 +1,23 @@ import { BlueprintManifestType, IBlueprintConfig, + IConfigMessage, IOutputLayer, ISourceLayer, + ShowStyleBlueprintManifest, SourceLayerType, StatusCode, + StudioBlueprintManifest, } from '@sofie-automation/blueprints-integration' import { PeripheralDevice, PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' -import { BucketId, ShowStyleBaseId, ShowStyleVariantId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { + BlueprintId, + BucketId, + ShowStyleBaseId, + ShowStyleVariantId, + StudioId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { assertNever, getRandomId, literal } from '@sofie-automation/corelib/dist/lib' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' @@ -17,6 +26,8 @@ import { ObjectOverrideSetOp, wrapDefaultObject, updateOverrides, + convertObjectIntoOverrides, + ObjectWithOverrides, } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { APIBlueprint, @@ -33,6 +44,10 @@ import { import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { Blueprints, ShowStyleBases, Studios } from '../../../collections' +import { Meteor } from 'meteor/meteor' +import { evalBlueprint } from '../../blueprints/cache' +import { CommonContext } from '../../../migration/upgrades/context' +import { logger } from '../../../logging' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' import { Bucket } from '../../../../lib/collections/Buckets' @@ -68,9 +83,20 @@ export async function showStyleBaseFrom( ? updateOverrides(showStyleBase.sourceLayersWithOverrides, newSourceLayers) : wrapDefaultObject({}) - const blueprintConfig = showStyleBase - ? updateOverrides(showStyleBase.blueprintConfigWithOverrides, apiShowStyleBase.config as IBlueprintConfig) - : wrapDefaultObject({}) + const blueprintManifest = evalBlueprint(blueprint) as ShowStyleBlueprintManifest + let blueprintConfig: ObjectWithOverrides + if (typeof blueprintManifest.blueprintConfigFromAPI !== 'function') { + blueprintConfig = showStyleBase + ? updateOverrides(showStyleBase.blueprintConfigWithOverrides, apiShowStyleBase.config as IBlueprintConfig) + : wrapDefaultObject({}) + } else { + blueprintConfig = showStyleBase + ? updateOverrides( + showStyleBase.blueprintConfigWithOverrides, + await ShowStyleBaseBlueprintConfigFromAPI(apiShowStyleBase, blueprintManifest) + ) + : convertObjectIntoOverrides(await ShowStyleBaseBlueprintConfigFromAPI(apiShowStyleBase, blueprintManifest)) + } return { _id: existingId ?? getRandomId(), @@ -87,7 +113,7 @@ export async function showStyleBaseFrom( } } -export function APIShowStyleBaseFrom(showStyleBase: DBShowStyleBase): APIShowStyleBase { +export async function APIShowStyleBaseFrom(showStyleBase: DBShowStyleBase): Promise { return { name: showStyleBase.name, blueprintId: unprotectString(showStyleBase.blueprintId), @@ -98,7 +124,7 @@ export function APIShowStyleBaseFrom(showStyleBase: DBShowStyleBase): APIShowSty sourceLayers: Object.values( applyAndValidateOverrides(showStyleBase.sourceLayersWithOverrides).obj ).map((layer) => APISourceLayerFrom(layer!)), - config: applyAndValidateOverrides(showStyleBase.blueprintConfigWithOverrides).obj, + config: await APIShowStyleBlueprintConfigFrom(showStyleBase, showStyleBase.blueprintId), } } @@ -124,12 +150,16 @@ export function showStyleVariantFrom( } } -export function APIShowStyleVariantFrom(showStyleVariant: DBShowStyleVariant): APIShowStyleVariant { +export async function APIShowStyleVariantFrom( + showStyleBase: DBShowStyleBase, + showStyleVariant: DBShowStyleVariant +): Promise { return { name: showStyleVariant.name, rank: showStyleVariant._rank, showStyleBaseId: unprotectString(showStyleVariant.showStyleBaseId), - config: applyAndValidateOverrides(showStyleVariant.blueprintConfigWithOverrides).obj, + blueprintConfigPresetId: showStyleVariant.blueprintConfigPresetId, + config: await APIShowStyleBlueprintConfigFrom(showStyleVariant, showStyleBase.blueprintId), } } @@ -251,16 +281,27 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P let blueprint: Blueprint | undefined if (apiStudio.blueprintId) { blueprint = await Blueprints.findOneAsync(protectString(apiStudio.blueprintId)) - if (!blueprint) return undefined - if (blueprint.blueprintType !== BlueprintManifestType.STUDIO) return undefined + if (blueprint?.blueprintType !== BlueprintManifestType.STUDIO) return undefined } + if (!blueprint) return undefined let studio: DBStudio | undefined if (existingId) studio = await Studios.findOneAsync(existingId) - const blueprintConfig = studio - ? updateOverrides(studio.blueprintConfigWithOverrides, apiStudio.config as IBlueprintConfig) - : wrapDefaultObject({}) + const blueprintManifest = evalBlueprint(blueprint) as StudioBlueprintManifest + let blueprintConfig: ObjectWithOverrides + if (typeof blueprintManifest.blueprintConfigFromAPI !== 'function') { + blueprintConfig = studio + ? updateOverrides(studio.blueprintConfigWithOverrides, apiStudio.config as IBlueprintConfig) + : wrapDefaultObject({}) + } else { + blueprintConfig = studio + ? updateOverrides( + studio.blueprintConfigWithOverrides, + await StudioBlueprintConfigFromAPI(apiStudio, blueprintManifest) + ) + : convertObjectIntoOverrides(await StudioBlueprintConfigFromAPI(apiStudio, blueprintManifest)) + } return { _id: existingId ?? getRandomId(), @@ -288,14 +329,14 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P } } -export function APIStudioFrom(studio: DBStudio): APIStudio { +export async function APIStudioFrom(studio: DBStudio): Promise { const studioSettings = APIStudioSettingsFrom(studio.settings) return { name: studio.name, blueprintId: unprotectString(studio.blueprintId), blueprintConfigPresetId: studio.blueprintConfigPresetId, - config: applyAndValidateOverrides(studio.blueprintConfigWithOverrides).obj, + config: await APIStudioBlueprintConfigFrom(studio), settings: studioSettings, supportedShowStyleBase: studio.supportedShowStyleBase.map((id) => unprotectString(id)), } @@ -418,6 +459,142 @@ export function APIOutputLayerFrom(outputLayer: IOutputLayer): APIOutputLayer { } } +async function getBlueprint( + blueprintId: BlueprintId | undefined, + blueprintType: BlueprintManifestType +): Promise { + const blueprint = blueprintId + ? await Blueprints.findOneAsync({ + _id: blueprintId, + blueprintType, + }) + : undefined + if (!blueprint) throw new Meteor.Error(404, `Blueprint "${blueprintId}" not found!`) + + if (!blueprint.blueprintHash) throw new Meteor.Error(500, 'Blueprint is not valid') + + return blueprint +} + +export async function validateAPIBlueprintConfigForShowStyle( + apiShowStyle: APIShowStyleBase | APIShowStyleVariant, + blueprintId: BlueprintId +): Promise> { + if (!apiShowStyle.blueprintConfigPresetId) + throw new Meteor.Error(500, `ShowStyle ${apiShowStyle.name} is missing config preset`) + const blueprint = await getBlueprint(blueprintId, BlueprintManifestType.SHOWSTYLE) + const blueprintManifest = evalBlueprint(blueprint) as ShowStyleBlueprintManifest + + if (typeof blueprintManifest.validateConfigFromAPI !== 'function') { + logger.info(`Blueprint ${blueprintManifest.blueprintId} does not support Config validation`) + return [] + } + + const blueprintContext = new CommonContext( + 'validateAPIBlueprintConfig', + `showStyle:${apiShowStyle.name},blueprint:${blueprint._id}` + ) + + return blueprintManifest.validateConfigFromAPI(blueprintContext, apiShowStyle.config) +} + +export async function ShowStyleBaseBlueprintConfigFromAPI( + apiShowStyleBase: APIShowStyleBase, + blueprintManifest: ShowStyleBlueprintManifest +): Promise { + if (!apiShowStyleBase.blueprintConfigPresetId) + throw new Meteor.Error(500, `ShowStyleBase ${apiShowStyleBase.name} is missing config preset`) + + if (typeof blueprintManifest.blueprintConfigFromAPI !== 'function') + throw new Meteor.Error(500, `Blueprint ${blueprintManifest.blueprintId} does not support this config flow`) + + const blueprintContext = new CommonContext( + 'BlueprintConfigFromAPI', + `showStyleBase:${apiShowStyleBase.name},blueprint:${blueprintManifest.blueprintId}` + ) + + return blueprintManifest.blueprintConfigFromAPI(blueprintContext, apiShowStyleBase.config) +} + +export async function APIShowStyleBlueprintConfigFrom( + showStyle: DBShowStyleBase | DBShowStyleVariant, + blueprintId: BlueprintId +): Promise { + if (!showStyle.blueprintConfigPresetId) + throw new Meteor.Error(500, `ShowStyle ${showStyle._id} is missing config preset`) + const blueprint = await getBlueprint(blueprintId, BlueprintManifestType.SHOWSTYLE) + const blueprintManifest = evalBlueprint(blueprint) as ShowStyleBlueprintManifest + + if (typeof blueprintManifest.blueprintConfigToAPI !== 'function') + return applyAndValidateOverrides(showStyle.blueprintConfigWithOverrides).obj + + const blueprintContext = new CommonContext( + 'APIShowStyleBlueprintConfigFrom', + `showStyleBase:${showStyle._id},blueprint:${blueprint._id}` + ) + + return blueprintManifest.blueprintConfigToAPI( + blueprintContext, + applyAndValidateOverrides(showStyle.blueprintConfigWithOverrides).obj + ) +} + +export async function validateAPIBlueprintConfigForStudio(apiStudio: APIStudio): Promise> { + if (!apiStudio.blueprintConfigPresetId) + throw new Meteor.Error(500, `Studio ${apiStudio.name} is missing config preset`) + const blueprint = await getBlueprint(protectString(apiStudio.blueprintId), BlueprintManifestType.STUDIO) + const blueprintManifest = evalBlueprint(blueprint) as StudioBlueprintManifest + + if (typeof blueprintManifest.validateConfigFromAPI !== 'function') { + logger.info(`Blueprint ${blueprintManifest.blueprintId} does not support Config validation`) + return [] + } + + const blueprintContext = new CommonContext( + 'validateAPIBlueprintConfig', + `studio:${apiStudio.name},blueprint:${blueprint._id}` + ) + + return blueprintManifest.validateConfigFromAPI(blueprintContext, apiStudio.config) +} + +export async function StudioBlueprintConfigFromAPI( + apiStudio: APIStudio, + blueprintManifest: StudioBlueprintManifest +): Promise { + if (!apiStudio.blueprintConfigPresetId) + throw new Meteor.Error(500, `Studio ${apiStudio.name} is missing config preset`) + + if (typeof blueprintManifest.blueprintConfigFromAPI !== 'function') + throw new Meteor.Error(500, `Blueprint ${blueprintManifest.blueprintId} does not support this config flow`) + + const blueprintContext = new CommonContext( + 'BlueprintConfigFromAPI', + `studio:${apiStudio.name},blueprint:${blueprintManifest.blueprintId}` + ) + + return blueprintManifest.blueprintConfigFromAPI(blueprintContext, apiStudio.config) +} + +export async function APIStudioBlueprintConfigFrom(studio: DBStudio): Promise { + if (!studio.blueprintConfigPresetId) throw new Meteor.Error(500, `Studio ${studio._id} is missing config preset`) + const blueprint = await getBlueprint(studio.blueprintId, BlueprintManifestType.STUDIO) + const blueprintManifest = evalBlueprint(blueprint) as StudioBlueprintManifest + + if (typeof blueprintManifest.blueprintConfigToAPI !== 'function') + return applyAndValidateOverrides(studio.blueprintConfigWithOverrides).obj + + const blueprintContext = new CommonContext( + 'APIStudioBlueprintConfigFrom', + `studio:${studio.name},blueprint:${blueprint._id}` + ) + + return blueprintManifest.blueprintConfigToAPI( + blueprintContext, + applyAndValidateOverrides(studio.blueprintConfigWithOverrides).obj + ) +} + export function bucketFrom(apiBucket: APIBucket, existingId?: BucketId): Bucket { return { _id: existingId ?? getRandomId(), diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 52ec4559fd..30d2a154bd 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -169,6 +169,17 @@ export interface ShowStyleBlueprintManifest TProcessedConfig + /** + * Validate the blueprint config passed to this blueprint according to the API schema, returning a list of messages to display to the user. + */ + validateConfigFromAPI?: (context: ICommonContext, apiConfig: object) => Array + + /** transform API blueprint config to the database format */ + blueprintConfigFromAPI?: (context: ICommonContext, config: object) => TRawConfig + + /** transform blueprint config to the API format */ + blueprintConfigToAPI?: (context: ICommonContext, config: TRawConfig) => object + // Events onRundownActivate?: (context: IRundownActivationContext, wasActive: boolean) => Promise diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index a4be296f26..d295e088e2 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -75,6 +75,15 @@ export interface StudioBlueprintManifest TProcessedConfig + + /** Validate the blueprint config passed to this blueprint according to the API schema, returning a list of messages to display to the user. */ + validateConfigFromAPI?: (context: ICommonContext, apiConfig: object) => Array + + /** transform API blueprint config to the database format */ + blueprintConfigFromAPI?: (context: ICommonContext, config: object) => IBlueprintConfig + + /** transform blueprint config to the API format */ + blueprintConfigToAPI?: (context: ICommonContext, config: TRawConfig) => object } export interface BlueprintResultStudioBaseline { From c6326e475720ae17e0b4feb85762f2246c2164a9 Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Mon, 8 Jul 2024 17:08:22 +0100 Subject: [PATCH 003/178] update openapi yaml definition for added config endpoints --- packages/openapi/api/actions.yaml | 4 + .../openapi/api/definitions/showstyles.yaml | 111 ++++++++++++++++++ packages/openapi/api/definitions/studios.yaml | 70 +++++++++++ 3 files changed, 185 insertions(+) diff --git a/packages/openapi/api/actions.yaml b/packages/openapi/api/actions.yaml index 2c21b0677c..994dd78aaf 100644 --- a/packages/openapi/api/actions.yaml +++ b/packages/openapi/api/actions.yaml @@ -72,6 +72,8 @@ paths: $ref: 'definitions/studios.yaml#/resources/studios' /studios/{studioId}: $ref: 'definitions/studios.yaml#/resources/studio' + /studios/{studioId}/config: + $ref: 'definitions/studios.yaml#/resources/config' /studios/{studioId}/switch-route-set: $ref: 'definitions/studios.yaml#/resources/switchRouteSet' /studios/{studioId}/devices: @@ -85,6 +87,8 @@ paths: $ref: 'definitions/showstyles.yaml#/resources/showStyleBases' /showstyles/{showStyleBaseId}: $ref: 'definitions/showstyles.yaml#/resources/showStyleBase' + /showstyles/{showStyleBaseId}/config: + $ref: 'definitions/showstyles.yaml#/resources/config' /showstyles/{showStyleBaseId}/variants: $ref: 'definitions/showstyles.yaml#/resources/showStyleVariants' /showstyles/{showStyleBaseId}/variants/{showStyleVariantId}: diff --git a/packages/openapi/api/definitions/showstyles.yaml b/packages/openapi/api/definitions/showstyles.yaml index c2dc3f0af1..f1f66fdc60 100644 --- a/packages/openapi/api/definitions/showstyles.yaml +++ b/packages/openapi/api/definitions/showstyles.yaml @@ -224,6 +224,117 @@ resources: additionalProperties: false 500: $ref: '#/components/responses/internalServerError' + config: + get: + operationId: getShowStyleConfig + tags: + - showstyles + summary: Returns the requested ShowStyle config + parameters: + - name: showStyleBaseId + in: path + description: Id of ShowStyle to retrieve the config from + required: true + schema: + type: string + responses: + 200: + description: ShowStyle config found. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + description: Blueprint config. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + 404: + $ref: '#/components/responses/showStyleBaseNotFound' + 500: + $ref: '#/components/responses/internalServerError' + put: + operationId: updateShowStyleConfig + tags: + - showstyles + summary: Updates an existing ShowStyle config. + parameters: + - name: showStyleBaseId + in: path + description: Id of ShowStyle to update the config for. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + description: Blueprint config. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + responses: + 200: + description: Operation successful. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + 404: + $ref: '#/components/responses/showStyleBaseNotFound' + 409: + description: The specified ShowStyle config has failed validation. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 409 + message: + type: string + details: + type: array + items: + type: string + example: 'Invalid Union' + required: + - status + - message + additionalProperties: false + 412: + description: The specified ShowStyleBase is in use in an on-air Rundown. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 412 + message: + type: string + required: + - status + - message + additionalProperties: false + 500: + $ref: '#/components/responses/internalServerError' showStyleVariants: get: operationId: getShowStyleVariants diff --git a/packages/openapi/api/definitions/studios.yaml b/packages/openapi/api/definitions/studios.yaml index 1c707808ca..fd45cded9b 100644 --- a/packages/openapi/api/definitions/studios.yaml +++ b/packages/openapi/api/definitions/studios.yaml @@ -157,6 +157,76 @@ resources: $ref: '#/components/responses/studioInUse' 500: $ref: '#/components/responses/internalServerError' + config: + get: + operationId: getStudioConfig + tags: + - studios + summary: Gets a Studio blueprint configuration. + parameters: + - name: studioId + in: path + description: Id of Studio config to retrieve. + required: true + schema: + type: string + responses: + 200: + description: Configuration found. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + description: Blueprint configuration. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + 404: + $ref: '#/components/responses/studioNotFound' + 500: + $ref: '#/components/responses/internalServerError' + put: + operationId: updateStudioConfig + tags: + - studios + summary: Updates an existing Studio blueprint configuration. + parameters: + - name: studioId + in: path + description: Id of Studio to update/create. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + description: Blueprint configuration. + properties: + developerMode: + type: boolean + example: true + additionalProperties: true + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/studioNotFound' + 409: + $ref: '#/components/responses/studioNotValid' + 412: + $ref: '#/components/responses/studioInUse' + 500: + $ref: '#/components/responses/internalServerError' switchRouteSet: put: operationId: switchRouteSet From 4188a208ddb3a298880a368d82b1d481e37b75c9 Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Tue, 9 Jul 2024 12:22:14 +0100 Subject: [PATCH 004/178] Add support for executeAdLib actionOptions --- meteor/lib/api/rest/v1/playlists.ts | 3 ++- meteor/server/api/rest/v1/playlists.ts | 22 +++++++++++++++---- .../src/api/showStyle.ts | 3 ++- packages/corelib/src/worker/studio.ts | 1 + .../job-worker/src/playout/adlibAction.ts | 6 ++++- packages/job-worker/src/playout/adlibJobs.ts | 1 + .../openapi/api/definitions/playlists.yaml | 10 ++++++--- 7 files changed, 36 insertions(+), 10 deletions(-) diff --git a/meteor/lib/api/rest/v1/playlists.ts b/meteor/lib/api/rest/v1/playlists.ts index d3dcbb7adf..db4f4b82f4 100644 --- a/meteor/lib/api/rest/v1/playlists.ts +++ b/meteor/lib/api/rest/v1/playlists.ts @@ -74,7 +74,8 @@ export interface PlaylistsRestAPI { event: string, rundownPlaylistId: RundownPlaylistId, adLibId: AdLibActionId | RundownBaselineAdLibActionId | PieceId | BucketAdLibId, - triggerMode?: string + triggerMode?: string, + adLibOptions?: any ): Promise> /** * Executes the requested Bucket AdLib/AdLib Action. This is a Bucket AdLib (Action) that has been previously inserted into a Bucket. diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 7fa1dcfb47..207c0e9f4e 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -96,7 +96,8 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { event: string, rundownPlaylistId: RundownPlaylistId, adLibId: AdLibActionId | RundownBaselineAdLibActionId | PieceId | BucketAdLibId, - triggerMode?: string | null + triggerMode?: string | null, + adLibOptions?: { [key: string]: any } ): Promise> { const baselineAdLibPiece = RundownBaselineAdLibPieces.findOneAsync(adLibId as PieceId, { projection: { _id: 1 }, @@ -204,6 +205,7 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { actionId: adLibActionDoc.actionId, userData: adLibActionDoc.userData, triggerMode: triggerMode ?? undefined, + actionOptions: adLibOptions, } ) } else { @@ -576,7 +578,7 @@ export function registerRoutes(registerRoute: APIRegisterHook) } ) - registerRoute<{ playlistId: string }, { adLibId: string; actionType?: string }, object>( + registerRoute<{ playlistId: string }, { adLibId: string; actionType?: string; adLibOptions?: any }, object>( 'post', '/playlists/:playlistId/execute-adlib', new Map([ @@ -591,12 +593,24 @@ export function registerRoutes(registerRoute: APIRegisterHook) ) const actionTypeObj = body const triggerMode = actionTypeObj ? (actionTypeObj as { actionType: string }).actionType : undefined - logger.info(`API POST: execute-adlib ${rundownPlaylistId} ${adLibId} - triggerMode: ${triggerMode}`) + const adLibOptions = actionTypeObj ? actionTypeObj.adLibOptions : undefined + logger.info( + `API POST: execute-adlib ${rundownPlaylistId} ${adLibId} - actionType: ${triggerMode} - options: ${ + adLibOptions ? JSON.stringify(adLibOptions) : 'undefined' + }` + ) check(adLibId, String) check(rundownPlaylistId, String) - return await serverAPI.executeAdLib(connection, event, rundownPlaylistId, adLibId, triggerMode) + return await serverAPI.executeAdLib( + connection, + event, + rundownPlaylistId, + adLibId, + triggerMode, + adLibOptions + ) } ) diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 52ec4559fd..6c93a78a5e 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -130,7 +130,8 @@ export interface ShowStyleBlueprintManifest Promise /** Generate adlib piece from ingest data */ diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 4825df50fb..35b27e5bd8 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -243,6 +243,7 @@ export interface ExecuteActionProps extends RundownPlayoutPropsBase { actionId: string userData: any triggerMode?: string + actionOptions?: { [key: string]: any } } export interface ExecuteBucketAdLibOrActionProps extends RundownPlayoutPropsBase { bucketId: BucketId diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 736825c3ba..845ad84b4f 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -102,6 +102,7 @@ export async function executeAdlibActionAndSaveModel( userData: data.userData, triggerMode: data.triggerMode, privateData: adLibActionDoc?.privateData, + publicData: data.actionOptions, } try { @@ -159,6 +160,8 @@ export interface ExecuteActionParameters { userData: ActionUserData /** Arbitraty data storage for internal use in the blueprints */ privateData: unknown | undefined + /** Optional arbitraty data used to modify the action parameters */ + publicData: unknown | undefined triggerMode: string | undefined } @@ -207,7 +210,8 @@ export async function executeActionInner( actionParameters.actionId, actionParameters.userData, actionParameters.triggerMode, - actionParameters.privateData + actionParameters.privateData, + actionParameters.publicData ) } catch (err) { logger.error(`Error in showStyleBlueprint.executeAction: ${stringifyError(err)}`) diff --git a/packages/job-worker/src/playout/adlibJobs.ts b/packages/job-worker/src/playout/adlibJobs.ts index 31eeb8382c..f1a2f59a75 100644 --- a/packages/job-worker/src/playout/adlibJobs.ts +++ b/packages/job-worker/src/playout/adlibJobs.ts @@ -115,6 +115,7 @@ export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakeP ...executeProps, triggerMode: undefined, privateData: undefined, + publicData: undefined, } ) break diff --git a/packages/openapi/api/definitions/playlists.yaml b/packages/openapi/api/definitions/playlists.yaml index bb459f6d52..e894808c64 100644 --- a/packages/openapi/api/definitions/playlists.yaml +++ b/packages/openapi/api/definitions/playlists.yaml @@ -109,15 +109,19 @@ resources: schema: type: object properties: - actionType: - type: string - description: An actionType string to specify a particular variation for the AdLibAction, valid strings are to be read from the status API adLibId: type: string description: AdLib to execute + actionType: + type: string + description: An actionType string to specify a particular variation for the AdLibAction, valid strings are to be read from the status API + adLibOptions: + type: object + description: AdLibAction options object defined according to the optionsSchema provided in the adLib status API required: - adLibId example: + adLibId: adlib_action_camera actionType: pvw responses: 200: From 978a09fd2eb9933e7e1d0fe3071d21d33732c0c0 Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Tue, 9 Jul 2024 12:38:22 +0100 Subject: [PATCH 005/178] update openapi tests for new config methods --- .../openapi/src/__tests__/showstyles.spec.ts | 28 ++++++++++++++++++- .../openapi/src/__tests__/studios.spec.ts | 22 ++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/openapi/src/__tests__/showstyles.spec.ts b/packages/openapi/src/__tests__/showstyles.spec.ts index d510848272..c4a6d803da 100644 --- a/packages/openapi/src/__tests__/showstyles.spec.ts +++ b/packages/openapi/src/__tests__/showstyles.spec.ts @@ -1,5 +1,11 @@ // eslint-disable-next-line node/no-missing-import -import { Configuration, ShowStyleBase, ShowstylesApi, ShowStyleVariant } from '../../client/ts' +import { + Configuration, + GetShowStyleConfig200ResponseResult, + ShowStyleBase, + ShowstylesApi, + ShowStyleVariant, +} from '../../client/ts' import { checkServer } from '../checkServer' import Logging from '../httpLogging' @@ -72,6 +78,26 @@ describe('Network client', () => { expect(showStyle.status).toBe(200) }) + let showStyleConfig: GetShowStyleConfig200ResponseResult | undefined + test('can request a ShowStyle config by id', async () => { + const showStyle = await showStylesApi.getShowStyleConfig({ + showStyleBaseId: showStyleBaseIds[0], + }) + expect(showStyle.status).toBe(200) + expect(showStyle).toHaveProperty('result') + expect(showStyle.result).toHaveProperty('developerMode') + showStyleConfig = JSON.parse(JSON.stringify(showStyle.result)) + }) + + test('can update a ShowStyle config', async () => { + showStyleConfig.developerMode = !showStyleConfig.developerMode + const showStyle = await showStylesApi.updateShowStyleConfig({ + showStyleBaseId: showStyleBaseIds[0], + requestBody: showStyleConfig, + }) + expect(showStyle.status).toBe(200) + }) + const showStyleVariantIds: string[] = [] test('can request all ShowStyleVariants', async () => { const showStyleVariants = await showStylesApi.getShowStyleVariants({ diff --git a/packages/openapi/src/__tests__/studios.spec.ts b/packages/openapi/src/__tests__/studios.spec.ts index e81e4a7694..f0c601ffc4 100644 --- a/packages/openapi/src/__tests__/studios.spec.ts +++ b/packages/openapi/src/__tests__/studios.spec.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line node/no-missing-import -import { Configuration, Studio, StudiosApi } from '../../client/ts' +import { Configuration, GetStudioConfig200ResponseResult, Studio, StudiosApi } from '../../client/ts' import { checkServer } from '../checkServer' import Logging from '../httpLogging' @@ -58,6 +58,26 @@ describe('Network client', () => { expect(studio.status).toBe(200) }) + let studioConfig: GetStudioConfig200ResponseResult | undefined + test('can request a Studio config by id', async () => { + const studio = await studiosApi.getStudioConfig({ + studioId: studioIds[0], + }) + expect(studio.status).toBe(200) + expect(studio).toHaveProperty('result') + expect(studio.result).toHaveProperty('developerMode') + studioConfig = JSON.parse(JSON.stringify(studio.result)) + }) + + test('can update a studio config', async () => { + studioConfig.developerMode = !studioConfig.developerMode + const studio = await studiosApi.updateStudioConfig({ + studioId: studioIds[0], + requestBody: studioConfig, + }) + expect(studio.status).toBe(200) + }) + const studioDevices: string[] = [] test('can request a list of devices for a studio', async () => { const devices = await studiosApi.devices({ studioId: studioIds[0] }) From d38ed40adfb995e9aa597fc5744a1212e7f35094 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 23 Sep 2024 11:04:06 +0200 Subject: [PATCH 006/178] Contribution Guidelines: Require contributions to target in-dev (#1260) --- CONTRIBUTING.md | 3 ++- .../docs/for-developers/contribution-guidelines.md | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index beced98156..b609a43658 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,4 +9,5 @@ This repository uses the following branches: * **_master_** is our main branch. We consider it stable and it is used in production. * The **_releaseXX_** branches are our in-development branches. When a release is ready, we decide to “freeze” that branch and create a new **_releaseXX+1_** branch. -We encourage you to base your contributions on the latest **_releaseXX_** branch, alternatively the **_master_** branch or a recently frozen **_releaseXX_** branch. The [_Sofie Releases_](https://nrkno.github.io/sofie-core/releases) page collects the status and timeline of the releases. +We require contributions to be based based on the latest **_release\*_** branch. +The [_Sofie Releases_](https://nrkno.github.io/sofie-core/releases) page collects the status and timeline of the releases. diff --git a/packages/documentation/docs/for-developers/contribution-guidelines.md b/packages/documentation/docs/for-developers/contribution-guidelines.md index 4e6ffadc97..f97118ca00 100644 --- a/packages/documentation/docs/for-developers/contribution-guidelines.md +++ b/packages/documentation/docs/for-developers/contribution-guidelines.md @@ -7,6 +7,8 @@ sidebar_position: 2 # Contribution Guidelines +_Last updated september 2024_ + ## About the Sofie TV Studio Automation Project The Sofie project includes a number of open source applications and libraries developed and maintained by the Norwegian public service broadcaster, [NRK](https://www.nrk.no/about/). Sofie has been used to produce live shows at NRK since September 2018. @@ -35,8 +37,8 @@ However, Sofie is a big project with many differing users and use cases. **Large 3. (If needed) NRK establishes contact with the RFC author, who will be invited to a workshop where the RFC is discussed. Meeting notes are published publicly on the RFC thread. 4. The contributor references the RFC when a pull request is ready. -### Base contributions on the in-development branch (or the master branch) -In order to facilitate merging, we ask that contributions are based on the latest (at the time of the pull request) _in-development_ branch (often named `release*`), alternatively the stable (eg. `master`) branch. NRK will take responsibility for rebasing stable contributions to the latest in-development branch if needed. +### Base contributions on the in-development branch +In order to facilitate merging, we ask that contributions are based on the latest (at the time of the pull request) _in-development_ branch (often named `release*`). See **CONTRIBUTING.md** in each official repository for details on which branch to use as a base for contributions. ## Developer Guidelines From a16d9777a301a6d7d69ea00be02b70c53cb9bdcc Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 23 Sep 2024 12:33:17 +0200 Subject: [PATCH 007/178] fix(BucketPanel): Bucket AdLibs don't trigger when created before Rundown activation (SOFIE-3478) --- meteor/client/lib/RenderLimiter.tsx | 21 ------------------- meteor/client/ui/Shelf/BucketPanel.tsx | 2 +- meteor/client/ui/Shelf/DashboardPanel.tsx | 2 +- .../ui/Shelf/Inspector/ShelfInspector.tsx | 3 +-- .../ui/Shelf/TimelineDashboardPanel.tsx | 2 +- 5 files changed, 4 insertions(+), 26 deletions(-) delete mode 100644 meteor/client/lib/RenderLimiter.tsx diff --git a/meteor/client/lib/RenderLimiter.tsx b/meteor/client/lib/RenderLimiter.tsx deleted file mode 100644 index b95ed1ce36..0000000000 --- a/meteor/client/lib/RenderLimiter.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from 'react' - -type IWrappedComponent = new (props: IProps, state: IState) => React.Component - -export function withRenderLimiter( - shouldComponentUpdate: (currentProps: IProps, nextProps: IProps) => boolean -): ( - WrappedComponent: IWrappedComponent -) => new (props: IProps, context: any) => React.Component { - return (WrappedComponent) => { - return class WithRenderLimiterHOCComponent extends React.Component { - shouldComponentUpdate(nextProps: IProps, _nextState: IState): boolean { - return shouldComponentUpdate(this.props, nextProps) - } - - render(): JSX.Element { - return - } - } - } -} diff --git a/meteor/client/ui/Shelf/BucketPanel.tsx b/meteor/client/ui/Shelf/BucketPanel.tsx index be75b575e9..141bc82806 100644 --- a/meteor/client/ui/Shelf/BucketPanel.tsx +++ b/meteor/client/ui/Shelf/BucketPanel.tsx @@ -380,7 +380,7 @@ export const BucketPanel = React.memo( ) }, (props: IBucketPanelProps, nextProps: IBucketPanelProps) => { - return !_.isEqual(props, nextProps) + return _.isEqual(props, nextProps) } ) diff --git a/meteor/client/ui/Shelf/DashboardPanel.tsx b/meteor/client/ui/Shelf/DashboardPanel.tsx index 89f6589070..363125a038 100644 --- a/meteor/client/ui/Shelf/DashboardPanel.tsx +++ b/meteor/client/ui/Shelf/DashboardPanel.tsx @@ -627,6 +627,6 @@ export const DashboardPanel = React.memo( ) }, (props: IAdLibPanelProps, nextProps: IAdLibPanelProps) => { - return !_.isEqual(props, nextProps) + return _.isEqual(props, nextProps) } ) diff --git a/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx b/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx index bc34c096ed..f82cc2f133 100644 --- a/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx +++ b/meteor/client/ui/Shelf/Inspector/ShelfInspector.tsx @@ -39,7 +39,6 @@ export const ShelfInspector = React.memo( ) }, (prevProps, nextProps) => { - if (_.isEqual(nextProps, prevProps)) return false - return true + return _.isEqual(nextProps, prevProps) } ) diff --git a/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx b/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx index d4441b29a9..2dec1bdbb5 100644 --- a/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx +++ b/meteor/client/ui/Shelf/TimelineDashboardPanel.tsx @@ -32,7 +32,7 @@ export const TimelineDashboardPanel = React.memo( return }, (props: IAdLibPanelProps, nextProps: IAdLibPanelProps) => { - return !_.isEqual(props, nextProps) + return _.isEqual(props, nextProps) } ) From 56a5a6c2386edac2011fa7b147505eaff1f30b75 Mon Sep 17 00:00:00 2001 From: Silje Enge Kristensen Date: Tue, 24 Sep 2024 09:41:53 +0200 Subject: [PATCH 008/178] ci: add github token as environment variable to trivy scanning job --- .github/workflows/node.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 268c7d71dc..b4b86c3b94 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -232,6 +232,8 @@ jobs: - name: Trivy scanning if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 uses: aquasecurity/trivy-action@0.24.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: image-ref: "${{ steps.trivy-image.outputs.image }}" format: "table" From 3e873ee2a3bbfe28c6a3ef759eef350fdbb326eb Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 24 Sep 2024 15:18:35 +0200 Subject: [PATCH 009/178] chore(release): 1.51.0-in-testing.2 --- meteor/CHANGELOG.md | 11 ++++++ meteor/package.json | 2 +- meteor/yarn.lock | 12 +++--- packages/blueprints-integration/CHANGELOG.md | 8 ++++ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +-- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 ++-- packages/lerna.json | 4 +- packages/live-status-gateway/package.json | 10 ++--- packages/mos-gateway/CHANGELOG.md | 8 ++++ packages/mos-gateway/package.json | 6 +-- packages/openapi/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 8 ++++ packages/playout-gateway/package.json | 6 +-- packages/server-core-integration/CHANGELOG.md | 8 ++++ packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 38 +++++++++---------- 19 files changed, 96 insertions(+), 53 deletions(-) diff --git a/meteor/CHANGELOG.md b/meteor/CHANGELOG.md index df0291d5b7..31b3937078 100644 --- a/meteor/CHANGELOG.md +++ b/meteor/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) + + +### Bug Fixes + +* allow replacement in replaceInfinitesFromPreviousPlayhead ([ebb154d](https://github.com/nrkno/sofie-core/commit/ebb154d6b59369588da400d8d921a00e41b84dc8)) +* **BucketPanel:** Bucket AdLibs don't trigger when created before Rundown activation (SOFIE-3478) ([a16d977](https://github.com/nrkno/sofie-core/commit/a16d9777a301a6d7d69ea00be02b70c53cb9bdcc)) +* **LinePartTimeline:** make rules for findMainPiece consistent, make infinite graphics Pieces display correctly ([153d100](https://github.com/nrkno/sofie-core/commit/153d100fb659546201a654af5c566b513951df88)) +* **NoraFloatingInspector:** prevent Segment crash when trying to show a Piece with an invalid Nora `previewPayload` ([4a3a2e7](https://github.com/nrkno/sofie-core/commit/4a3a2e779c144b1c9e88c187cce2e5c80d34626d)) +* resolve an issue with prompter moving when Parts become PartInstances and the prompter position is juuuust right ([a670a73](https://github.com/nrkno/sofie-core/commit/a670a73fa6bfb8331921a2bedd9c927952cfffcf)) + ## [1.51.0-in-testing.0](https://github.com/nrkno/sofie-core/compare/v1.50.4...v1.51.0-in-testing.0) (2024-08-19) diff --git a/meteor/package.json b/meteor/package.json index 644f905f31..34d3e2fe35 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "private": true, "engines": { "node": ">=14.19.1" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index f589906d4c..e628496da6 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1321,7 +1321,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -1362,8 +1362,8 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/corelib@portal:../packages/corelib::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -1394,9 +1394,9 @@ __metadata: resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 - "@sofie-automation/corelib": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 + "@sofie-automation/corelib": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index 7647e8c41b..5115571a60 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) + +**Note:** Version bump only for package @sofie-automation/blueprints-integration + + + + + # [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index fcefd2ec03..b848b51f74 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 0a66bf5919..5b59b27ecc 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -39,8 +39,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.1", - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "fast-clone": "^1.5.13", "i18next": "^21.10.0", "influx": "^5.9.3", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index b986086916..4a3a75ec4b 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index febfae4014..d4b2e4b4b3 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -41,9 +41,9 @@ ], "dependencies": { "@slack/webhook": "^6.1.0", - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.1", - "@sofie-automation/corelib": "1.51.0-in-testing.1", - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.2", + "@sofie-automation/corelib": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", "elastic-apm-node": "^3.51.0", diff --git a/packages/lerna.json b/packages/lerna.json index ab06bef67f..20b18579eb 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "npmClient": "yarn", "useWorkspaces": true -} \ No newline at end of file +} diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index e543275a0d..2e8a241f0e 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -53,10 +53,10 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.1", - "@sofie-automation/corelib": "1.51.0-in-testing.1", - "@sofie-automation/server-core-integration": "1.51.0-in-testing.1", - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.2", + "@sofie-automation/corelib": "1.51.0-in-testing.2", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "debug": "^4.3.4", "fast-clone": "^1.5.13", "influx": "^5.9.3", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index bd46ea33b2..af8af2bb76 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) + +**Note:** Version bump only for package mos-gateway + + + + + # [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 7359ef1c65..64d347f02c 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -66,8 +66,8 @@ ], "dependencies": { "@mos-connection/connector": "4.1.1", - "@sofie-automation/server-core-integration": "1.51.0-in-testing.1", - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "tslib": "^2.6.2", "type-fest": "^3.13.1", "underscore": "^1.13.6", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index dd44d0a6b6..7b4bab25c5 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "license": "MIT", "repository": { "type": "git", diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index d68d91d33d..4f45f143ee 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) + +**Note:** Version bump only for package playout-gateway + + + + + # [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index aefe811407..dcc7c39d5b 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -56,8 +56,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "1.51.0-in-testing.1", - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "debug": "^4.3.4", "influx": "^5.9.3", "timeline-state-resolver": "9.2.0-alpha.0", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index b0f83d985d..94c6662cee 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + # [1.51.0-in-testing.1](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.0...v1.51.0-in-testing.1) (2024-09-06) **Note:** Version bump only for package @sofie-automation/server-core-integration diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 6060ea3d15..e000abe983 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/server-core-integration", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -70,7 +70,7 @@ "production" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-testing.1", + "@sofie-automation/shared-lib": "1.51.0-in-testing.2", "ejson": "^2.2.3", "eventemitter3": "^4.0.7", "faye-websocket": "^0.11.4", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 4373cec47d..6681e141cf 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "1.51.0-in-testing.1", + "version": "1.51.0-in-testing.2", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/yarn.lock b/packages/yarn.lock index 599aefa9da..a9171fc4be 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4565,11 +4565,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@1.51.0-in-testing.1, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@sofie-automation/blueprints-integration@1.51.0-in-testing.2, @sofie-automation/blueprints-integration@workspace:blueprints-integration": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -4606,12 +4606,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@1.51.0-in-testing.1, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@1.51.0-in-testing.2, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -4642,9 +4642,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 - "@sofie-automation/corelib": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 + "@sofie-automation/corelib": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 @@ -4674,11 +4674,11 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@1.51.0-in-testing.1, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@1.51.0-in-testing.2, @sofie-automation/server-core-integration@workspace:server-core-integration": version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 ejson: ^2.2.3 eventemitter3: ^4.0.7 faye-websocket: ^0.11.4 @@ -4688,7 +4688,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@1.51.0-in-testing.1, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@1.51.0-in-testing.2, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -15334,10 +15334,10 @@ asn1@evs-broadcast/node-asn1: "@asyncapi/generator": ^1.17.25 "@asyncapi/html-template": ^2.3.9 "@asyncapi/nodejs-ws-template": ^0.9.36 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.1 - "@sofie-automation/corelib": 1.51.0-in-testing.1 - "@sofie-automation/server-core-integration": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 + "@sofie-automation/corelib": 1.51.0-in-testing.2 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 debug: ^4.3.4 fast-clone: ^1.5.13 influx: ^5.9.3 @@ -17410,8 +17410,8 @@ asn1@evs-broadcast/node-asn1: resolution: "mos-gateway@workspace:mos-gateway" dependencies: "@mos-connection/connector": 4.1.1 - "@sofie-automation/server-core-integration": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 @@ -19397,8 +19397,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": 1.51.0-in-testing.1 - "@sofie-automation/shared-lib": 1.51.0-in-testing.1 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.2 debug: ^4.3.4 influx: ^5.9.3 timeline-state-resolver: 9.2.0-alpha.0 From 94d425a0c90192345288b056915d3b78fdf3fd27 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 25 Sep 2024 07:40:18 +0200 Subject: [PATCH 010/178] chore: revert changes in PR #1182 due to failing in CI --- .github/workflows/node.yaml | 33 ++++++++++++++------------- .github/workflows/prerelease-libs.yml | 31 +++++++++++++------------ 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 90038c9cdf..52ec4146bb 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -664,7 +664,7 @@ jobs: else # make dependencies of `determine-npm-tag` available yarn install --mode=skip-build - + cd packages PACKAGE_NAME="@sofie-automation/shared-lib" PUBLISHED_VERSION=$(yarn npm info --json $PACKAGE_NAME | jq -c '.version' -r) @@ -682,21 +682,22 @@ jobs: yarn build env: CI: true - - name: Generate OpenAPI client library - if: ${{ steps.do-publish.outputs.tag }} - uses: hatamiarash7/openapi-generator@v0.3.0 - with: - generator: typescript-fetch - openapi-file: ./packages/openapi/api/actions.yaml - output-dir: ./packages/openapi/client/ts - command-args: -p supportsES6=true - - name: Build OpenAPI client library - if: ${{ steps.do-publish.outputs.tag }} - run: | - cd packages/openapi - yarn build:main - env: - CI: true + # Temporarily disabled due to failing in CI: + # - name: Generate OpenAPI client library + # if: ${{ steps.do-publish.outputs.tag }} + # uses: hatamiarash7/openapi-generator@v0.3.0 + # with: + # generator: typescript-fetch + # openapi-file: ./packages/openapi/api/actions.yaml + # output-dir: ./packages/openapi/client/ts + # command-args: -p supportsES6=true + # - name: Build OpenAPI client library + # if: ${{ steps.do-publish.outputs.tag }} + # run: | + # cd packages/openapi + # yarn build:main + # env: + # CI: true - name: Modify dependencies to use npm packages run: node scripts/prepublish.js - name: Publish to NPM diff --git a/.github/workflows/prerelease-libs.yml b/.github/workflows/prerelease-libs.yml index bbd864e32c..1c5854e605 100644 --- a/.github/workflows/prerelease-libs.yml +++ b/.github/workflows/prerelease-libs.yml @@ -131,21 +131,22 @@ jobs: yarn build env: CI: true - - name: Generate OpenAPI client library - if: ${{ steps.do-publish.outputs.tag }} - uses: hatamiarash7/openapi-generator@v0.3.0 - with: - generator: typescript-fetch - openapi-file: ./packages/openapi/api/actions.yaml - output-dir: ./packages/openapi/client/ts - command-args: -p supportsES6=true - - name: Build OpenAPI client library - if: ${{ steps.do-publish.outputs.tag }} - run: | - cd packages/openapi - yarn build:main - env: - CI: true + # Temporarily disabled due to failing in CI: + # - name: Generate OpenAPI client library + # if: ${{ steps.do-publish.outputs.tag }} + # uses: hatamiarash7/openapi-generator@v0.3.0 + # with: + # generator: typescript-fetch + # openapi-file: ./packages/openapi/api/actions.yaml + # output-dir: ./packages/openapi/client/ts + # command-args: -p supportsES6=true + # - name: Build OpenAPI client library + # if: ${{ steps.do-publish.outputs.tag }} + # run: | + # cd packages/openapi + # yarn build:main + # env: + # CI: true - name: Modify dependencies to use npm packages run: node scripts/prepublish.js - name: Publish to NPM From 79d9b352e952455b734652975f651b96a798fcd4 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 25 Sep 2024 07:53:28 +0200 Subject: [PATCH 011/178] chore(release): 1.51.0-in-testing.3 --- meteor/CHANGELOG.md | 2 + meteor/package.json | 2 +- meteor/yarn.lock | 12 +++--- packages/blueprints-integration/CHANGELOG.md | 8 ++++ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +-- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 ++-- packages/lerna.json | 2 +- packages/live-status-gateway/package.json | 10 ++--- packages/mos-gateway/CHANGELOG.md | 8 ++++ packages/mos-gateway/package.json | 6 +-- packages/openapi/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 8 ++++ packages/playout-gateway/package.json | 6 +-- packages/server-core-integration/CHANGELOG.md | 8 ++++ packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 38 +++++++++---------- 19 files changed, 86 insertions(+), 52 deletions(-) diff --git a/meteor/CHANGELOG.md b/meteor/CHANGELOG.md index 31b3937078..7a08165001 100644 --- a/meteor/CHANGELOG.md +++ b/meteor/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) + ## [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) diff --git a/meteor/package.json b/meteor/package.json index 34d3e2fe35..c8a40aefa7 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "private": true, "engines": { "node": ">=14.19.1" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index e628496da6..e0667868a7 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1321,7 +1321,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -1362,8 +1362,8 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/corelib@portal:../packages/corelib::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -1394,9 +1394,9 @@ __metadata: resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 - "@sofie-automation/corelib": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 + "@sofie-automation/corelib": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index 5115571a60..7f7d19e7ac 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) + +**Note:** Version bump only for package @sofie-automation/blueprints-integration + + + + + # [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) **Note:** Version bump only for package @sofie-automation/blueprints-integration diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index b848b51f74..bed9b93615 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -38,7 +38,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 5b59b27ecc..f10e62e012 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -39,8 +39,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.2", - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.3", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "fast-clone": "^1.5.13", "i18next": "^21.10.0", "influx": "^5.9.3", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 4a3a75ec4b..402e20b927 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index d4b2e4b4b3..dbae10b7ca 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -41,9 +41,9 @@ ], "dependencies": { "@slack/webhook": "^6.1.0", - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.2", - "@sofie-automation/corelib": "1.51.0-in-testing.2", - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.3", + "@sofie-automation/corelib": "1.51.0-in-testing.3", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", "elastic-apm-node": "^3.51.0", diff --git a/packages/lerna.json b/packages/lerna.json index 20b18579eb..99d49ac919 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 2e8a241f0e..36ea25d94d 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -53,10 +53,10 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.51.0-in-testing.2", - "@sofie-automation/corelib": "1.51.0-in-testing.2", - "@sofie-automation/server-core-integration": "1.51.0-in-testing.2", - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/blueprints-integration": "1.51.0-in-testing.3", + "@sofie-automation/corelib": "1.51.0-in-testing.3", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.3", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "debug": "^4.3.4", "fast-clone": "^1.5.13", "influx": "^5.9.3", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index af8af2bb76..956e4bea81 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) + +**Note:** Version bump only for package mos-gateway + + + + + # [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) **Note:** Version bump only for package mos-gateway diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 64d347f02c..4aa3d551dd 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -66,8 +66,8 @@ ], "dependencies": { "@mos-connection/connector": "4.1.1", - "@sofie-automation/server-core-integration": "1.51.0-in-testing.2", - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.3", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "tslib": "^2.6.2", "type-fest": "^3.13.1", "underscore": "^1.13.6", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 7b4bab25c5..e885dc38f5 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "license": "MIT", "repository": { "type": "git", diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index 4f45f143ee..9dae36de7a 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) + +**Note:** Version bump only for package playout-gateway + + + + + # [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) **Note:** Version bump only for package playout-gateway diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index dcc7c39d5b..71c56cac17 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -56,8 +56,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "1.51.0-in-testing.2", - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/server-core-integration": "1.51.0-in-testing.3", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "debug": "^4.3.4", "influx": "^5.9.3", "timeline-state-resolver": "9.2.0-alpha.0", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index 94c6662cee..46ff10a210 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + # [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24) **Note:** Version bump only for package @sofie-automation/server-core-integration diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index e000abe983..90c8dc9d62 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/server-core-integration", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -70,7 +70,7 @@ "production" ], "dependencies": { - "@sofie-automation/shared-lib": "1.51.0-in-testing.2", + "@sofie-automation/shared-lib": "1.51.0-in-testing.3", "ejson": "^2.2.3", "eventemitter3": "^4.0.7", "faye-websocket": "^0.11.4", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 6681e141cf..b88ca025f9 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "1.51.0-in-testing.2", + "version": "1.51.0-in-testing.3", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/yarn.lock b/packages/yarn.lock index a9171fc4be..4627a0435e 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4565,11 +4565,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@1.51.0-in-testing.2, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@sofie-automation/blueprints-integration@1.51.0-in-testing.3, @sofie-automation/blueprints-integration@workspace:blueprints-integration": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -4606,12 +4606,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@1.51.0-in-testing.2, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@1.51.0-in-testing.3, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 fast-clone: ^1.5.13 i18next: ^21.10.0 influx: ^5.9.3 @@ -4642,9 +4642,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": ^6.1.0 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 - "@sofie-automation/corelib": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 + "@sofie-automation/corelib": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.51.0 @@ -4674,11 +4674,11 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@1.51.0-in-testing.2, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@1.51.0-in-testing.3, @sofie-automation/server-core-integration@workspace:server-core-integration": version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 ejson: ^2.2.3 eventemitter3: ^4.0.7 faye-websocket: ^0.11.4 @@ -4688,7 +4688,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@1.51.0-in-testing.2, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@1.51.0-in-testing.3, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -15334,10 +15334,10 @@ asn1@evs-broadcast/node-asn1: "@asyncapi/generator": ^1.17.25 "@asyncapi/html-template": ^2.3.9 "@asyncapi/nodejs-ws-template": ^0.9.36 - "@sofie-automation/blueprints-integration": 1.51.0-in-testing.2 - "@sofie-automation/corelib": 1.51.0-in-testing.2 - "@sofie-automation/server-core-integration": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/blueprints-integration": 1.51.0-in-testing.3 + "@sofie-automation/corelib": 1.51.0-in-testing.3 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 debug: ^4.3.4 fast-clone: ^1.5.13 influx: ^5.9.3 @@ -17410,8 +17410,8 @@ asn1@evs-broadcast/node-asn1: resolution: "mos-gateway@workspace:mos-gateway" dependencies: "@mos-connection/connector": 4.1.1 - "@sofie-automation/server-core-integration": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 tslib: ^2.6.2 type-fest: ^3.13.1 underscore: ^1.13.6 @@ -19397,8 +19397,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": 1.51.0-in-testing.2 - "@sofie-automation/shared-lib": 1.51.0-in-testing.2 + "@sofie-automation/server-core-integration": 1.51.0-in-testing.3 + "@sofie-automation/shared-lib": 1.51.0-in-testing.3 debug: ^4.3.4 influx: ^5.9.3 timeline-state-resolver: 9.2.0-alpha.0 From 7cf6470bcae95bf19eebb81ef1b7bac0ee271a06 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 25 Sep 2024 10:23:08 +0200 Subject: [PATCH 012/178] re-enable build step for openapi for test and publishing (#1265) * chore(ci): Add build step for openapi for test and publishing * chore: fix ci * chore(ci): openapi: re-add gendocs and genserver to CI --- .github/workflows/node.yaml | 33 ++++++++++++--------------- .github/workflows/prerelease-libs.yml | 24 +++++++------------ 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 52ec4146bb..e04f4aec23 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -569,13 +569,17 @@ jobs: yarn env: CI: true - - name: Run generator + - name: Build OpenAPI client library + run: | + cd packages/openapi + yarn build + env: + CI: true + - name: Generate OpenAPI docs and server run: | cd packages/openapi - yarn gendocs yarn genserver - yarn genclient:ts env: CI: true @@ -682,22 +686,13 @@ jobs: yarn build env: CI: true - # Temporarily disabled due to failing in CI: - # - name: Generate OpenAPI client library - # if: ${{ steps.do-publish.outputs.tag }} - # uses: hatamiarash7/openapi-generator@v0.3.0 - # with: - # generator: typescript-fetch - # openapi-file: ./packages/openapi/api/actions.yaml - # output-dir: ./packages/openapi/client/ts - # command-args: -p supportsES6=true - # - name: Build OpenAPI client library - # if: ${{ steps.do-publish.outputs.tag }} - # run: | - # cd packages/openapi - # yarn build:main - # env: - # CI: true + - name: Build OpenAPI client library + if: ${{ steps.do-publish.outputs.tag }} + run: | + cd packages/openapi + yarn build + env: + CI: true - name: Modify dependencies to use npm packages run: node scripts/prepublish.js - name: Publish to NPM diff --git a/.github/workflows/prerelease-libs.yml b/.github/workflows/prerelease-libs.yml index 1c5854e605..af155a8cce 100644 --- a/.github/workflows/prerelease-libs.yml +++ b/.github/workflows/prerelease-libs.yml @@ -131,22 +131,14 @@ jobs: yarn build env: CI: true - # Temporarily disabled due to failing in CI: - # - name: Generate OpenAPI client library - # if: ${{ steps.do-publish.outputs.tag }} - # uses: hatamiarash7/openapi-generator@v0.3.0 - # with: - # generator: typescript-fetch - # openapi-file: ./packages/openapi/api/actions.yaml - # output-dir: ./packages/openapi/client/ts - # command-args: -p supportsES6=true - # - name: Build OpenAPI client library - # if: ${{ steps.do-publish.outputs.tag }} - # run: | - # cd packages/openapi - # yarn build:main - # env: - # CI: true + + - name: Build OpenAPI client library + if: ${{ steps.do-publish.outputs.publish }} + run: | + cd packages/openapi + yarn build + env: + CI: true - name: Modify dependencies to use npm packages run: node scripts/prepublish.js - name: Publish to NPM From 31d94a8adf4b250d8df4129fa026f5df569c4f46 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 25 Sep 2024 12:00:28 +0200 Subject: [PATCH 013/178] fix(BucketPieceButton): doesn't show media status of Bucket Adlibs --- meteor/client/ui/Shelf/BucketPieceButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meteor/client/ui/Shelf/BucketPieceButton.tsx b/meteor/client/ui/Shelf/BucketPieceButton.tsx index 299d4e0981..80319dae20 100644 --- a/meteor/client/ui/Shelf/BucketPieceButton.tsx +++ b/meteor/client/ui/Shelf/BucketPieceButton.tsx @@ -12,7 +12,7 @@ import { } from 'react-dnd' import { DragDropItemTypes } from '../DragDropItemTypes' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' -import { useContentStatusForAdlibPiece } from '../SegmentTimeline/withMediaObjectStatus' +import { useContentStatusForItem } from '../SegmentTimeline/withMediaObjectStatus' import { BucketAdLibActionUi, BucketAdLibItem } from './RundownViewBuckets' import { IBlueprintActionTriggerMode } from '@sofie-automation/blueprints-integration' import { BucketId, PieceId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -60,7 +60,7 @@ class BucketPieceButtonBase extends DashboardPieceButtonBase & BucketPieceButtonBaseProps ): JSX.Element { - const contentStatus = useContentStatusForAdlibPiece(props.piece) + const contentStatus = useContentStatusForItem(props.piece) const [, connectDropTarget] = useDrop({ accept: DragDropItemTypes.BUCKET_ADLIB_PIECE, From 7fe60eab0a39907575ad05eabd2c1d45f01d0d99 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 25 Sep 2024 12:39:35 +0000 Subject: [PATCH 014/178] fix: currentPart timeline dur respects postroll in autonext --- packages/job-worker/src/playout/timeline/piece.ts | 2 +- packages/job-worker/src/playout/timeline/rundown.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/job-worker/src/playout/timeline/piece.ts b/packages/job-worker/src/playout/timeline/piece.ts index 9e011a3348..861946f01d 100644 --- a/packages/job-worker/src/playout/timeline/piece.ts +++ b/packages/job-worker/src/playout/timeline/piece.ts @@ -106,7 +106,7 @@ export function getPieceEnableInsidePart( if (partTimings.toPartPostroll) { if (!pieceEnable.duration) { // make sure that the control object is shortened correctly - pieceEnable.duration = `#${partGroupId} - ${partTimings.toPartPostroll}` + pieceEnable.end = `#${partGroupId} - ${partTimings.toPartPostroll}` } } diff --git a/packages/job-worker/src/playout/timeline/rundown.ts b/packages/job-worker/src/playout/timeline/rundown.ts index d8ee36ab7d..27299011a1 100644 --- a/packages/job-worker/src/playout/timeline/rundown.ts +++ b/packages/job-worker/src/playout/timeline/rundown.ts @@ -146,7 +146,8 @@ export function buildTimelineObjsForRundown( // If there is a valid autonext out of the current part, then calculate the duration currentPartEnable.duration = partInstancesInfo.current.partInstance.part.expectedDuration + - partInstancesInfo.current.calculatedTimings.toPartDelay + partInstancesInfo.current.calculatedTimings.toPartDelay + + partInstancesInfo.current.calculatedTimings.toPartPostroll // autonext should have the postroll added to it to not confuse the timeline } const currentPartGroup = createPartGroup(partInstancesInfo.current.partInstance, currentPartEnable) From ccaeedb8e0efac9b8e2b36487aaa2cd5cf98dbab Mon Sep 17 00:00:00 2001 From: Silje Enge Kristensen Date: Thu, 26 Sep 2024 13:22:28 +0200 Subject: [PATCH 015/178] ci: use custom trivy database image ref. discussion found here: https://github.com/aquasecurity/trivy/discussions/7538#top --- .github/workflows/node.yaml | 4 +++- .github/workflows/trivy.yml | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index b4b86c3b94..8cde20c429 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -233,7 +233,7 @@ jobs: if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 uses: aquasecurity/trivy-action@0.24.0 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: image-ref: "${{ steps.trivy-image.outputs.image }}" format: "table" @@ -383,6 +383,8 @@ jobs: - name: Trivy scanning if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 uses: aquasecurity/trivy-action@0.24.0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: image-ref: "${{ steps.trivy-image.outputs.image }}" format: "table" diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index caa51ef3a4..00af0aad61 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -14,6 +14,8 @@ jobs: steps: - name: Run Trivy vulnerability scanner (json) uses: aquasecurity/trivy-action@0.24.0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: image-ref: ghcr.io/nrkno/sofie-core-${{ matrix.image }}:latest format: json @@ -21,6 +23,8 @@ jobs: - name: Run Trivy vulnerability scanner (table) uses: aquasecurity/trivy-action@0.24.0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: image-ref: ghcr.io/nrkno/sofie-core-${{ matrix.image }}:latest output: '${{ matrix.image }}-trivy-scan-results.txt' @@ -37,6 +41,8 @@ jobs: - name: Run Trivy in GitHub SBOM mode and submit results to Dependency Graph uses: aquasecurity/trivy-action@0.24.0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: format: 'github' output: 'dependency-results-${{ matrix.image }}.sbom.json' From 6900d9af263515e9b352d8c1a7cc173ae58661ce Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 26 Sep 2024 14:54:53 +0100 Subject: [PATCH 016/178] chore: disable nrk specific workflows --- .github/workflows/prune-container-images.yml | 2 ++ .github/workflows/prune-tags.yml | 2 ++ .github/workflows/trivy.yml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/prune-container-images.yml b/.github/workflows/prune-container-images.yml index b7c5625124..11f35daef9 100644 --- a/.github/workflows/prune-container-images.yml +++ b/.github/workflows/prune-container-images.yml @@ -7,6 +7,8 @@ on: jobs: prune-container-images: + if: ${{ github.repository_owner == 'nrkno' }} + uses: nrkno/sofie-github-workflows/.github/workflows/prune-container-images.yml@main strategy: max-parallel: 1 diff --git a/.github/workflows/prune-tags.yml b/.github/workflows/prune-tags.yml index e9d9a5bbbc..2bd28d2b81 100644 --- a/.github/workflows/prune-tags.yml +++ b/.github/workflows/prune-tags.yml @@ -16,6 +16,8 @@ on: jobs: prune-tags: + if: ${{ github.repository_owner == 'nrkno' }} + name: Prune tags runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index caa51ef3a4..b9c3a7af00 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -6,6 +6,8 @@ on: jobs: trivy: + if: ${{ github.repository_owner == 'nrkno' }} + name: Trivy scan runs-on: ubuntu-latest strategy: From ced0e11fbe895ae3a0c00e501d5d8dd447f84a33 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 26 Sep 2024 17:35:01 +0100 Subject: [PATCH 017/178] fix: clear pieces with fixed duration --- packages/job-worker/src/playout/adlibUtils.ts | 108 ++++++++++-------- 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/packages/job-worker/src/playout/adlibUtils.ts b/packages/job-worker/src/playout/adlibUtils.ts index 1aea41cc52..b883369644 100644 --- a/packages/job-worker/src/playout/adlibUtils.ts +++ b/packages/job-worker/src/playout/adlibUtils.ts @@ -274,61 +274,69 @@ export function innerStopPieces( for (const resolvedPieceInstance of resolvedPieces) { const pieceInstance = resolvedPieceInstance.instance - if ( - !pieceInstance.userDuration && - !pieceInstance.piece.virtual && - filter(pieceInstance) && - resolvedPieceInstance.resolvedStart !== undefined && - resolvedPieceInstance.resolvedStart <= relativeStopAt && - !pieceInstance.plannedStoppedPlayback - ) { - switch (pieceInstance.piece.lifespan) { - case PieceLifespan.WithinPart: - case PieceLifespan.OutOnSegmentChange: - case PieceLifespan.OutOnRundownChange: { - logger.info(`Blueprint action: Cropping PieceInstance "${pieceInstance._id}" to ${stopAt}`) - - const pieceInstanceModel = playoutModel.findPieceInstance(pieceInstance._id) - if (pieceInstanceModel) { - const newDuration: Required['userDuration'] = playoutModel.isMultiGatewayMode - ? { - endRelativeToNow: offsetRelativeToNow, - } - : { - endRelativeToPart: relativeStopAt, - } - - pieceInstanceModel.pieceInstance.setDuration(newDuration) - - stoppedInstances.push(pieceInstance._id) - } else { - logger.warn( - `Blueprint action: Failed to crop PieceInstance "${pieceInstance._id}", it was not found` - ) - } - - break - } - case PieceLifespan.OutOnSegmentEnd: - case PieceLifespan.OutOnRundownEnd: - case PieceLifespan.OutOnShowStyleEnd: { - logger.info( - `Blueprint action: Cropping PieceInstance "${pieceInstance._id}" to ${stopAt} with a virtual` - ) - currentPartInstance.insertVirtualPiece( - relativeStopAt, - pieceInstance.piece.lifespan, - pieceInstance.piece.sourceLayerId, - pieceInstance.piece.outputLayerId - ) + // Virtual pieces aren't allowed a timed end + if (pieceInstance.piece.virtual) continue + + // Check if piece has already had an end defined + if (pieceInstance.userDuration) continue + + // Caller can filter out pieces + if (!filter(pieceInstance)) continue + + // Check if piece has started yet + if (resolvedPieceInstance.resolvedStart == undefined || resolvedPieceInstance.resolvedStart > relativeStopAt) + continue + + // If there end time of the piece is already known, make sure it is in the future + if (pieceInstance.plannedStoppedPlayback && pieceInstance.plannedStoppedPlayback <= stopAt) continue + + switch (pieceInstance.piece.lifespan) { + case PieceLifespan.WithinPart: + case PieceLifespan.OutOnSegmentChange: + case PieceLifespan.OutOnRundownChange: { + logger.info(`Blueprint action: Cropping PieceInstance "${pieceInstance._id}" to ${stopAt}`) + + const pieceInstanceModel = playoutModel.findPieceInstance(pieceInstance._id) + if (pieceInstanceModel) { + const newDuration: Required['userDuration'] = playoutModel.isMultiGatewayMode + ? { + endRelativeToNow: offsetRelativeToNow, + } + : { + endRelativeToPart: relativeStopAt, + } + + pieceInstanceModel.pieceInstance.setDuration(newDuration) stoppedInstances.push(pieceInstance._id) - break + } else { + logger.warn( + `Blueprint action: Failed to crop PieceInstance "${pieceInstance._id}", it was not found` + ) } - default: - assertNever(pieceInstance.piece.lifespan) + + break + } + case PieceLifespan.OutOnSegmentEnd: + case PieceLifespan.OutOnRundownEnd: + case PieceLifespan.OutOnShowStyleEnd: { + logger.info( + `Blueprint action: Cropping PieceInstance "${pieceInstance._id}" to ${stopAt} with a virtual` + ) + + currentPartInstance.insertVirtualPiece( + relativeStopAt, + pieceInstance.piece.lifespan, + pieceInstance.piece.sourceLayerId, + pieceInstance.piece.outputLayerId + ) + + stoppedInstances.push(pieceInstance._id) + break } + default: + assertNever(pieceInstance.piece.lifespan) } } From 298c0fd7e2c7be2ef0d958c567b95170bb2a65a2 Mon Sep 17 00:00:00 2001 From: olzzon Date: Fri, 27 Sep 2024 10:00:50 +0200 Subject: [PATCH 018/178] fix: In kiosk mode, rundown page gets stalled if rundown is removed while on the page. --- .../webui/src/client/styles/rundownView.scss | 15 +++++++++++++++ packages/webui/src/client/ui/RundownView.tsx | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index 2db0a2c0b7..63be075738 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -3375,6 +3375,21 @@ svg.icon { left: 50%; transform: translate(-150%, -50%); } + + > .rundown-view__label { + position: absolute; + top: 60%; + left: 1%; + right: 0; + text-align: center; + font-size: 3em; + transform: translateY(-50%); + + > p { + margin: 20px auto; + max-width: 1200px; + } + } } .rundown-view { diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index 40e0e68996..9f0097aa3c 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -3281,9 +3281,27 @@ const RundownViewContent = translateWithTracker +
+

+ ( + + )} + /> +

+
+ ) From e839b97c0205df38e5bc7ab816a9c79406a7d767 Mon Sep 17 00:00:00 2001 From: Kasper Olsson Hans Date: Mon, 23 Sep 2024 16:24:00 +0200 Subject: [PATCH 019/178] feat: routeset config defined in blueprints --- meteor/__mocks__/defaultCollectionObjects.ts | 4 +- .../StudioDeviceTriggerManager.ts | 5 +- .../api/deviceTriggers/triggersContext.ts | 1 + meteor/server/api/ingest/mosDevice/actions.ts | 5 +- meteor/server/api/playout/playout.ts | 38 +- meteor/server/api/rest/v1/typeConversion.ts | 4 +- meteor/server/api/studio/api.ts | 6 +- meteor/server/api/userActions.ts | 2 +- meteor/server/migration/0_1_0.ts | 4 +- meteor/server/migration/1_42_0.ts | 15 +- meteor/server/migration/1_50_0.ts | 22 +- meteor/server/migration/X_X_X.ts | 81 +- .../migration/__tests__/migrations.test.ts | 12 +- .../expectedPackages/publication.ts | 11 +- .../__tests__/checkPieceContentStatus.test.ts | 9 +- .../checkPieceContentStatus.ts | 8 +- .../pieceContentStatusUI/common.ts | 6 +- meteor/server/publications/studio.ts | 2 +- meteor/server/publications/studioUI.ts | 16 +- meteor/server/publications/timeline.ts | 3 +- .../blueprints-integration/src/api/studio.ts | 8 + .../src/context/adlibActionContext.ts | 2 + packages/blueprints-integration/src/index.ts | 1 + .../blueprints-integration/src/triggers.ts | 8 + packages/corelib/src/dataModel/Studio.ts | 76 +- packages/corelib/src/overrideOpHelper.ts | 329 ++++++++ .../src/settings/objectWithOverrides.ts | 4 + packages/corelib/src/worker/studio.ts | 13 + .../src/__mocks__/defaultCollectionObjects.ts | 4 +- .../src/blueprints/context/adlibActions.ts | 4 + .../src/playout/model/PlayoutModel.ts | 7 + .../model/implementation/PlayoutModelImpl.ts | 4 + packages/job-worker/src/playout/upgrade.ts | 25 + .../src/studio/model/StudioBaselineHelper.ts | 70 +- .../src/studio/model/StudioPlayoutModel.ts | 7 + .../studio/model/StudioPlayoutModelImpl.ts | 5 + packages/job-worker/src/studio/routeSet.ts | 9 + .../job-worker/src/workers/studio/jobs.ts | 3 + packages/meteor-lib/src/api/userActions.ts | 2 +- .../meteor-lib/src/collections/Studios.ts | 9 +- .../meteor-lib/src/triggers/actionFactory.ts | 20 +- .../shared-lib/src/core/model/ShowStyle.ts | 1 + .../src/core/model/StudioRouteSet.ts | 52 ++ .../src/__mocks__/defaultCollectionObjects.ts | 4 +- .../src/client/lib/Components/Checkbox.tsx | 15 +- .../lib/Components/LabelAndOverrides.tsx | 15 +- .../lib/forms/SchemaFormWithOverrides.tsx | 18 +- .../src/client/lib/forms/schemaFormUtil.tsx | 5 + .../client/lib/triggers/TriggersHandler.tsx | 4 + packages/webui/src/client/ui/RundownView.tsx | 1 + .../RundownView/RundownRightHandControls.tsx | 10 +- .../webui/src/client/ui/Settings/Forms.scss | 10 +- .../ui/Settings/ShowStyleBaseSettings.tsx | 2 + .../client/ui/Settings/Studio/Mappings.tsx | 5 +- .../client/ui/Settings/Studio/Routings.tsx | 768 ------------------ .../Studio/Routings/ExclusivityGroups.tsx | 279 +++++++ .../ui/Settings/Studio/Routings/RouteSets.tsx | 742 +++++++++++++++++ .../ui/Settings/Studio/Routings/index.tsx | 68 ++ .../client/ui/Settings/SystemManagement.tsx | 7 +- .../TriggeredActionsEditor.tsx | 14 +- .../actionSelector/ActionSelector.tsx | 11 + .../ui/Settings/util/OverrideOpHelper.tsx | 299 +------ 62 files changed, 1970 insertions(+), 1224 deletions(-) create mode 100644 packages/corelib/src/overrideOpHelper.ts create mode 100644 packages/job-worker/src/studio/routeSet.ts create mode 100644 packages/shared-lib/src/core/model/StudioRouteSet.ts delete mode 100644 packages/webui/src/client/ui/Settings/Studio/Routings.tsx create mode 100644 packages/webui/src/client/ui/Settings/Studio/Routings/ExclusivityGroups.tsx create mode 100644 packages/webui/src/client/ui/Settings/Studio/Routings/RouteSets.tsx create mode 100644 packages/webui/src/client/ui/Settings/Studio/Routings/index.tsx diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index 163c569f38..bab46f41aa 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -112,8 +112,8 @@ export function defaultStudio(_id: StudioId): DBStudio { fallbackPartDuration: DEFAULT_FALLBACK_PART_DURATION, }, _rundownVersionHash: '', - routeSets: {}, - routeSetExclusivityGroups: {}, + routeSetsWithOverrides: wrapDefaultObject({}), + routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainers: {}, previewContainerIds: [], thumbnailContainerIds: [], diff --git a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts index c1ea42f0b3..f710070aba 100644 --- a/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts +++ b/meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts @@ -56,7 +56,7 @@ export class StudioDeviceTriggerManager { return } - const context = createCurrentContextFromCache(cache) + const context = createCurrentContextFromCache(cache, studioId) const actionManager = StudioActionManagers.get(studioId) if (!actionManager) throw new Meteor.Error( @@ -271,7 +271,7 @@ function convertDocument(doc: ReadonlyObjectDeep): UITrigger }) } -function createCurrentContextFromCache(cache: ContentCache): ReactivePlaylistActionContext { +function createCurrentContextFromCache(cache: ContentCache, studioId: StudioId): ReactivePlaylistActionContext { const rundownPlaylist = cache.RundownPlaylists.findOne({ activationId: { $exists: true, @@ -301,6 +301,7 @@ function createCurrentContextFromCache(cache: ContentCache): ReactivePlaylistAct : [] return { + studioId: new DummyReactiveVar(studioId), currentPartInstanceId: new DummyReactiveVar(currentPartInstance?._id ?? null), currentPartId: new DummyReactiveVar(currentPartInstance?.part._id ?? null), nextPartId: new DummyReactiveVar(nextPartInstance?.part._id ?? null), diff --git a/meteor/server/api/deviceTriggers/triggersContext.ts b/meteor/server/api/deviceTriggers/triggersContext.ts index 85397cc531..c8c95db6b5 100644 --- a/meteor/server/api/deviceTriggers/triggersContext.ts +++ b/meteor/server/api/deviceTriggers/triggersContext.ts @@ -132,6 +132,7 @@ function createContextForRundownPlaylistChain( } return { + studioId: new DummyReactiveVar(studioId), rundownPlaylistId: new DummyReactiveVar(playlist?._id), rundownPlaylist: new DummyReactiveVar(playlist), currentRundownId: new DummyReactiveVar(currentPartInstance?.rundownId ?? playlist.rundownIdsInOrder[0] ?? null), diff --git a/meteor/server/api/ingest/mosDevice/actions.ts b/meteor/server/api/ingest/mosDevice/actions.ts index d719916249..c2bc59275c 100644 --- a/meteor/server/api/ingest/mosDevice/actions.ts +++ b/meteor/server/api/ingest/mosDevice/actions.ts @@ -1,4 +1,3 @@ -import { MOS } from '@sofie-automation/corelib' import { logger } from '../../../logging' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Meteor } from 'meteor/meteor' @@ -13,7 +12,7 @@ import { generateRundownSource, getPeripheralDeviceFromRundown, runIngestOperati import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { DEFAULT_MOS_TIMEOUT_TIME } from '@sofie-automation/shared-lib/dist/core/constants' import { executePeripheralDeviceFunctionWithCustomTimeout } from '../../peripheralDevice/executeFunction' -import { getMosTypes } from '@mos-connection/helper' +import { MOS } from '@sofie-automation/meteor-lib/dist/mos' export namespace MOSDeviceActions { export async function reloadRundown( @@ -73,7 +72,7 @@ export namespace MOSDeviceActions { if (!mosPayload.Body) throw new Meteor.Error(500, `Part Cache for "${partCache.externalId}" missing FullStory content!`) - const mosTypes = getMosTypes(false) + const mosTypes = MOS.getMosTypes(false) const story = mosPayload.Body.find( (item) => diff --git a/meteor/server/api/playout/playout.ts b/meteor/server/api/playout/playout.ts index f5b11df21f..20fb5e40c3 100644 --- a/meteor/server/api/playout/playout.ts +++ b/meteor/server/api/playout/playout.ts @@ -1,12 +1,10 @@ /* tslint:disable:no-use-before-declare */ -import { Meteor } from 'meteor/meteor' -import * as _ from 'underscore' -import { StudioRouteBehavior } from '@sofie-automation/corelib/dist/dataModel/Studio' import { PackageInfo } from '../../coreSystem' import { StudioContentAccess } from '../../security/studio' import { shouldUpdateStudioBaselineInner } from '@sofie-automation/corelib/dist/studio/baseline' -import { logger } from '../../logging' -import { Blueprints, RundownPlaylists, Studios, Timeline } from '../../collections' +import { Blueprints, RundownPlaylists, Timeline } from '../../collections' +import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' +import { QueueStudioJob } from '../../worker/worker' export namespace ServerPlayoutAPI { export async function shouldUpdateStudioBaseline(access: StudioContentAccess): Promise { @@ -38,32 +36,12 @@ export namespace ServerPlayoutAPI { export async function switchRouteSet( access: StudioContentAccess, routeSetId: string, - state: boolean + state: boolean | 'toggle' ): Promise { - logger.debug(`switchRouteSet "${access.studioId}" "${routeSetId}"=${state}`) - - const studio = access.studio - - if (studio.routeSets[routeSetId] === undefined) - throw new Meteor.Error(404, `RouteSet "${routeSetId}" not found!`) - const routeSet = studio.routeSets[routeSetId] - if (routeSet.behavior === StudioRouteBehavior.ACTIVATE_ONLY && state === false) - throw new Meteor.Error(400, `RouteSet "${routeSetId}" is ACTIVATE_ONLY`) - - const modification: Record = {} - modification[`routeSets.${routeSetId}.active`] = state - - if (studio.routeSets[routeSetId].exclusivityGroup && state === true) { - _.each(studio.routeSets, (otherRouteSet, otherRouteSetId) => { - if (otherRouteSetId === routeSetId) return - if (otherRouteSet.exclusivityGroup === routeSet.exclusivityGroup) { - modification[`routeSets.${otherRouteSetId}.active`] = false - } - }) - } - - await Studios.updateAsync(studio._id, { - $set: modification, + const queuedJob = await QueueStudioJob(StudioJobs.SwitchRouteSet, access.studioId, { + routeSetId, + state, }) + await queuedJob.complete } } diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index 91d79a6d1d..b8788de6e3 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -276,9 +276,9 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P supportedShowStyleBase: apiStudio.supportedShowStyleBase?.map((id) => protectString(id)) ?? [], organizationId: null, mappingsWithOverrides: wrapDefaultObject({}), - routeSets: {}, + routeSetsWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', - routeSetExclusivityGroups: {}, + routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainers: {}, previewContainerIds: [], thumbnailContainerIds: [], diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index e6ff0ce43f..4a646967d2 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -50,8 +50,8 @@ export async function insertStudioInner(organizationId: OrganizationId | null, n minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, }, _rundownVersionHash: '', - routeSets: {}, - routeSetExclusivityGroups: {}, + routeSetsWithOverrides: wrapDefaultObject({}), + routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainers: {}, thumbnailContainerIds: [], previewContainerIds: [], @@ -141,7 +141,7 @@ Studios.observeChanges( { fields: { mappingsWithOverrides: 1, - routeSets: 1, + routeSetsWithOverrides: 1, }, } ) diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index 10fb20c44e..8ccc6dda65 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -1114,7 +1114,7 @@ class ServerUserActionAPI eventTime: Time, studioId: StudioId, routeSetId: string, - state: boolean + state: boolean | 'toggle' ): Promise> { return ServerClientAPI.runUserActionInLog( this, diff --git a/meteor/server/migration/0_1_0.ts b/meteor/server/migration/0_1_0.ts index d4fb2930fc..15aafc3451 100644 --- a/meteor/server/migration/0_1_0.ts +++ b/meteor/server/migration/0_1_0.ts @@ -445,8 +445,8 @@ export const addSteps = addMigrationSteps('0.1.0', [ mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', - routeSets: {}, - routeSetExclusivityGroups: {}, + routeSetsWithOverrides: wrapDefaultObject({}), + routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainers: {}, thumbnailContainerIds: [], previewContainerIds: [], diff --git a/meteor/server/migration/1_42_0.ts b/meteor/server/migration/1_42_0.ts index 0d6278e580..cdd49aab99 100644 --- a/meteor/server/migration/1_42_0.ts +++ b/meteor/server/migration/1_42_0.ts @@ -9,6 +9,15 @@ export const addSteps = addMigrationSteps('1.42.0', [ id: 'Add new routeType property to routeSets where missing', canBeRunAutomatically: true, validate: async () => { + // If routeSets has been converted to ObjectWithOverrides, + // it will have a defaults property, and shouln't be migrated + if ( + (await Studios.countDocuments({ + routeSetsWithOverrides: { $exists: true }, + })) > 0 + ) { + return false + } return ( (await Studios.countDocuments({ routeSets: { $exists: false }, @@ -19,8 +28,12 @@ export const addSteps = addMigrationSteps('1.42.0', [ const studios = await Studios.findFetchAsync({}) for (const studio of studios) { - const routeSets = studio.routeSets + // If routeSets has been converted to ObjectWithOverrides, + // it will have a defaults property, and shouln't be migrated + if (studio.routeSetsWithOverrides) return + //@ts-expect-error routeSets is not typed as ObjectWithOverrides + const routeSets = studio.routeSets as any as Record Object.entries(routeSets).forEach(([routeSetId, routeSet]) => { routeSet.routes.forEach((route) => { if (!route.routeType) { diff --git a/meteor/server/migration/1_50_0.ts b/meteor/server/migration/1_50_0.ts index a804a72ad7..9d85f90c98 100644 --- a/meteor/server/migration/1_50_0.ts +++ b/meteor/server/migration/1_50_0.ts @@ -83,10 +83,10 @@ function convertMappingsOverrideOps(studio: DBStudio) { return changed && newOverrides } -function convertRouteSetMappings(studio: DBStudio) { +function convertRouteSetMappings(routeSets: Record) { let changed = false - const newRouteSets = clone(studio.routeSets || {}) + const newRouteSets = clone(routeSets || {}) for (const routeSet of Object.values(newRouteSets)) { for (const route of routeSet.routes) { if (route.remapping && !route.remapping.options) { @@ -95,7 +95,7 @@ function convertRouteSetMappings(studio: DBStudio) { ..._.pick(route.remapping, ...mappingBaseOptions), options: _.omit(route.remapping, ...mappingBaseOptions), } - console.log('new route', route) + // console.log('new route', route) changed = true } } @@ -247,10 +247,13 @@ export const addSteps = addMigrationSteps('1.50.0', [ canBeRunAutomatically: true, validate: async () => { const studios = await Studios.findFetchAsync({ routeSets: { $exists: true } }) - for (const studio of studios) { - const newOverrides = convertRouteSetMappings(studio) - if (newOverrides) { + // Ignore this if the routeSets has been converted into an OverrideWithObjects: + if (studio.routeSetsWithOverrides) continue + //@ts-expect-error routeSets are not part of the typings: + const plainRouteSets = studio.routeSets as any as Record + const newRouteSets = convertRouteSetMappings(plainRouteSets) + if (newRouteSets) { return `object needs to be updated` } } @@ -261,7 +264,12 @@ export const addSteps = addMigrationSteps('1.50.0', [ const studios = await Studios.findFetchAsync({ routeSets: { $exists: true } }) for (const studio of studios) { - const newRouteSets = convertRouteSetMappings(studio) + // Ignore this if the routeSets already has been converted into an OverrideWithObjects: + if (studio.routeSetsWithOverrides) continue + //@ts-expect-error routeSets are not part of the typings: + const plainRouteSets = studio.routeSets as any as Record + + const newRouteSets = convertRouteSetMappings(plainRouteSets) if (newRouteSets) { await Studios.updateAsync(studio._id, { diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index ea8bc303e5..06d2547dbb 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -1,5 +1,8 @@ import { addMigrationSteps } from './databaseMigration' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' +import { Studios } from '../collections' +import { convertObjectIntoOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { StudioRouteSet, StudioRouteSetExclusivityGroup } from '@sofie-automation/corelib/dist/dataModel/Studio' /* * ************************************************************************************** @@ -12,5 +15,81 @@ import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' */ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ - // Add some migrations here: + { + id: `convert routesets to ObjectWithOverrides`, + canBeRunAutomatically: true, + validate: async () => { + const studios = await Studios.findFetchAsync({ routeSets: { $exists: true } }) + + for (const studio of studios) { + //@ts-expect-error routeSets is not typed as ObjectWithOverrides + if (studio.routeSets) { + return 'routesets must be converted to an ObjectWithOverrides' + } + } + + return false + }, + migrate: async () => { + const studios = await Studios.findFetchAsync({ routeSets: { $exists: true } }) + + for (const studio of studios) { + //@ts-expect-error routeSets is not typed as ObjectWithOverrides + if (!studio.routeSets) continue + //@ts-expect-error routeSets is not typed as ObjectWithOverrides + const oldRouteSets = studio.routeSets as any as Record + + const newRouteSets = convertObjectIntoOverrides(oldRouteSets) + + await Studios.updateAsync(studio._id, { + $set: { + routeSetsWithOverrides: newRouteSets, + }, + $unset: { + routeSets: 1, + }, + }) + } + }, + }, + { + id: `convert routeSetExclusivityGroups to ObjectWithOverrides`, + canBeRunAutomatically: true, + validate: async () => { + const studios = await Studios.findFetchAsync({ routeSetExclusivityGroups: { $exists: true } }) + + for (const studio of studios) { + //@ts-expect-error routeSetExclusivityGroups is not typed as ObjectWithOverrides + if (studio.routeSetExclusivityGroups) { + return 'routesets must be converted to an ObjectWithOverrides' + } + } + + return false + }, + migrate: async () => { + const studios = await Studios.findFetchAsync({ routeSetExclusivityGroups: { $exists: true } }) + + for (const studio of studios) { + //@ts-expect-error routeSetExclusivityGroups is not typed as ObjectWithOverrides + if (!studio.routeSetExclusivityGroups) return + //@ts-expect-error routeSetExclusivityGroups is not typed as ObjectWithOverrides + const oldRouteSetExclusivityGroups = studio.routeSetExclusivityGroups as any as Record< + string, + StudioRouteSetExclusivityGroup + > + + const newRouteSetExclusivityGroups = convertObjectIntoOverrides(oldRouteSetExclusivityGroups) + + await Studios.updateAsync(studio._id, { + $set: { + routeSetExclusivityGroupsWithOverrides: newRouteSetExclusivityGroups, + }, + $unset: { + routeSetExclusivityGroups: 1, + }, + }) + } + }, + }, ]) diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index e3a69a6498..3278b7a9ee 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -130,8 +130,8 @@ describe('Migrations', () => { mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', - routeSets: {}, - routeSetExclusivityGroups: {}, + routeSetsWithOverrides: wrapDefaultObject({}), + routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainers: {}, previewContainerIds: [], thumbnailContainerIds: [], @@ -168,8 +168,8 @@ describe('Migrations', () => { mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', - routeSets: {}, - routeSetExclusivityGroups: {}, + routeSetsWithOverrides: wrapDefaultObject({}), + routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainers: {}, previewContainerIds: [], thumbnailContainerIds: [], @@ -206,8 +206,8 @@ describe('Migrations', () => { mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', - routeSets: {}, - routeSetExclusivityGroups: {}, + routeSetsWithOverrides: wrapDefaultObject({}), + routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainers: {}, previewContainerIds: [], thumbnailContainerIds: [], diff --git a/meteor/server/publications/packageManager/expectedPackages/publication.ts b/meteor/server/publications/packageManager/expectedPackages/publication.ts index bc5bfd262e..969786b2b4 100644 --- a/meteor/server/publications/packageManager/expectedPackages/publication.ts +++ b/meteor/server/publications/packageManager/expectedPackages/publication.ts @@ -54,14 +54,14 @@ interface ExpectedPackagesPublicationState { export type StudioFields = | '_id' - | 'routeSets' + | 'routeSetsWithOverrides' | 'mappingsWithOverrides' | 'packageContainers' | 'previewContainerIds' | 'thumbnailContainerIds' const studioFieldSpecifier = literal>>({ _id: 1, - routeSets: 1, + routeSetsWithOverrides: 1, mappingsWithOverrides: 1, packageContainers: 1, previewContainerIds: 1, @@ -102,7 +102,7 @@ async function setupExpectedPackagesPublicationObservers( { fields: { // mappingsHash gets updated when either of these omitted fields changes - ...omit(studioFieldSpecifier, 'mappingsWithOverrides', 'routeSets'), + ...omit(studioFieldSpecifier, 'mappingsWithOverrides', 'routeSetsWithOverrides'), mappingsHash: 1, }, } @@ -143,7 +143,10 @@ async function manipulateExpectedPackagesPublicationData( state.layerNameToDeviceIds = new Map() } else { const studioMappings = applyAndValidateOverrides(state.studio.mappingsWithOverrides).obj - state.layerNameToDeviceIds = buildMappingsToDeviceIdMap(state.studio.routeSets, studioMappings) + state.layerNameToDeviceIds = buildMappingsToDeviceIdMap( + applyAndValidateOverrides(state.studio.routeSetsWithOverrides).obj, + studioMappings + ) } } diff --git a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts index d691c2c1d2..9d4138eadb 100644 --- a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts +++ b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts @@ -175,18 +175,15 @@ describe('lib/mediaObjects', () => { const mockDefaultStudio = defaultStudio(protectString('studio0')) const mockStudio: Complete< - Pick< - DBStudio, - '_id' | 'settings' | 'packageContainers' | 'previewContainerIds' | 'thumbnailContainerIds' | 'routeSets' - > & - Pick + Pick & + Pick > = { _id: mockDefaultStudio._id, settings: mockStudioSettings, packageContainers: mockDefaultStudio.packageContainers, previewContainerIds: ['previews0'], thumbnailContainerIds: ['thumbnails0'], - routeSets: mockDefaultStudio.routeSets, + routeSets: applyAndValidateOverrides(mockDefaultStudio.routeSetsWithOverrides).obj, mappings: applyAndValidateOverrides(mockDefaultStudio.mappingsWithOverrides).obj, } diff --git a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index e3bc20e68a..e90b34a2d8 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -24,6 +24,7 @@ import { MappingsExt, ResultingMappingRoutes, StudioPackageContainer, + StudioRouteSet, } from '@sofie-automation/corelib/dist/dataModel/Studio' import { literal, Complete, assertNever } from '@sofie-automation/corelib/dist/lib' import { ReadonlyDeep } from 'type-fest' @@ -172,12 +173,11 @@ export type PieceContentStatusPiece = Pick { + extends Pick { /** Mappings between the physical devices / outputs and logical ones */ mappings: MappingsExt + /** Route sets with overrides */ + routeSets: Record } export async function checkPieceContentStatusAndDependencies( diff --git a/meteor/server/publications/pieceContentStatusUI/common.ts b/meteor/server/publications/pieceContentStatusUI/common.ts index 2f595f8002..f271150973 100644 --- a/meteor/server/publications/pieceContentStatusUI/common.ts +++ b/meteor/server/publications/pieceContentStatusUI/common.ts @@ -18,7 +18,7 @@ export type StudioFields = | 'previewContainerIds' | 'thumbnailContainerIds' | 'mappingsWithOverrides' - | 'routeSets' + | 'routeSetsWithOverrides' export const studioFieldSpecifier = literal>>({ _id: 1, settings: 1, @@ -26,7 +26,7 @@ export const studioFieldSpecifier = literal @@ -117,6 +117,6 @@ export async function fetchStudio(studioId: StudioId): Promise): UIStudio { settings: studio.settings, - routeSets: studio.routeSets, - routeSetExclusivityGroups: studio.routeSetExclusivityGroups, + routeSets: applyAndValidateOverrides(studio.routeSetsWithOverrides).obj, + routeSetExclusivityGroups: applyAndValidateOverrides(studio.routeSetExclusivityGroupsWithOverrides).obj, }) } -type StudioFields = '_id' | 'name' | 'mappingsWithOverrides' | 'settings' | 'routeSets' | 'routeSetExclusivityGroups' +type StudioFields = + | '_id' + | 'name' + | 'mappingsWithOverrides' + | 'settings' + | 'routeSetsWithOverrides' + | 'routeSetExclusivityGroupsWithOverrides' const fieldSpecifier = literal>>({ _id: 1, name: 1, mappingsWithOverrides: 1, settings: 1, - routeSets: 1, - routeSetExclusivityGroups: 1, + routeSetsWithOverrides: 1, + routeSetExclusivityGroupsWithOverrides: 1, }) async function setupUIStudioPublicationObservers( diff --git a/meteor/server/publications/timeline.ts b/meteor/server/publications/timeline.ts index c9511ee4d2..d4f24d782a 100644 --- a/meteor/server/publications/timeline.ts +++ b/meteor/server/publications/timeline.ts @@ -36,6 +36,7 @@ import { PeripheralDevicePubSub, PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' meteorPublish(CorelibPubSub.timelineDatastore, async function (studioId: StudioId, token: string | undefined) { if (!studioId) throw new Meteor.Error(400, 'selector argument missing') @@ -209,7 +210,7 @@ async function manipulateTimelinePublicationData( if (!state.routes) { // Routes need recalculating - state.routes = getActiveRoutes(state.studio.routeSets) + state.routes = getActiveRoutes(applyAndValidateOverrides(state.studio.routeSetsWithOverrides).obj) invalidateTimeline = true } diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index a4be296f26..2883582091 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -11,6 +11,10 @@ import type { ExpectedPlayoutItemGeneric, IBlueprintResultRundownPlaylist, IBlue import type { BlueprintMappings } from '../studio' import type { TimelineObjectCoreExt, TSR } from '../timeline' import type { ExpectedPackage } from '../package' +import type { + StudioRouteSet, + StudioRouteSetExclusivityGroup, +} from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' export interface StudioBlueprintManifest extends BlueprintManifestBase { @@ -108,6 +112,10 @@ export interface BlueprintResultApplyStudioConfig { ingestDevices: Record /** Input-gateway subdevices */ inputDevices: Record + /** Route Sets */ + routeSets?: Record + /** Route Set Exclusivity Groups */ + routeSetExclusivityGroups?: Record } export interface IStudioConfigPreset { diff --git a/packages/blueprints-integration/src/context/adlibActionContext.ts b/packages/blueprints-integration/src/context/adlibActionContext.ts index f6dbd60cc4..a1115590b5 100644 --- a/packages/blueprints-integration/src/context/adlibActionContext.ts +++ b/packages/blueprints-integration/src/context/adlibActionContext.ts @@ -38,6 +38,8 @@ export interface IActionExecutionContext /** Insert a queued part to follow the current part */ queuePart(part: IBlueprintPart, pieces: IBlueprintPiece[]): Promise + /** Switch RouteSet State*/ + switchRouteSet(routeSetId: string, state: boolean): Promise /** Misc actions */ // updateAction(newManifest: Pick): void // only updates itself. to allow for the next one to do something different // executePeripheralDeviceAction(deviceId: string, functionName: string, args: any[]): Promise diff --git a/packages/blueprints-integration/src/index.ts b/packages/blueprints-integration/src/index.ts index 5353cc2c9e..95246bc308 100644 --- a/packages/blueprints-integration/src/index.ts +++ b/packages/blueprints-integration/src/index.ts @@ -25,3 +25,4 @@ export { MOS } from '@sofie-automation/shared-lib/dist/mos' export { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' export * from '@sofie-automation/shared-lib/dist/lib/JSONBlob' export * from '@sofie-automation/shared-lib/dist/lib/JSONSchemaUtil' +export * from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' diff --git a/packages/blueprints-integration/src/triggers.ts b/packages/blueprints-integration/src/triggers.ts index 3ebaaa9561..89600ecb69 100644 --- a/packages/blueprints-integration/src/triggers.ts +++ b/packages/blueprints-integration/src/triggers.ts @@ -194,6 +194,13 @@ export interface IRundownPlaylistActivateAdlibTestingAction extends ITriggeredAc filterChain: (IRundownPlaylistFilterLink | IGUIContextFilterLink)[] } +export interface ISwitchRouteSetAction extends ITriggeredActionBase { + action: PlayoutActions.switchRouteSet + filterChain: (IRundownPlaylistFilterLink | IGUIContextFilterLink)[] + routeSetId: string + state: boolean +} + export interface ITakeAction extends ITriggeredActionBase { action: PlayoutActions.take filterChain: (IRundownPlaylistFilterLink | IGUIContextFilterLink)[] @@ -316,6 +323,7 @@ export type SomeAction = | IShowEntireCurrentSegmentAction | IMiniShelfQueueAdLib | IModifyShiftRegister + | ISwitchRouteSetAction export interface IBlueprintTriggeredActions { _id: string diff --git a/packages/corelib/src/dataModel/Studio.ts b/packages/corelib/src/dataModel/Studio.ts index e0060d6396..1315895466 100644 --- a/packages/corelib/src/dataModel/Studio.ts +++ b/packages/corelib/src/dataModel/Studio.ts @@ -1,12 +1,33 @@ -import { BlueprintMapping, IBlueprintConfig, PackageContainer, TSR } from '@sofie-automation/blueprints-integration' +import { IBlueprintConfig, PackageContainer, TSR } from '@sofie-automation/blueprints-integration' import { ObjectWithOverrides } from '../settings/objectWithOverrides' import { StudioId, OrganizationId, BlueprintId, ShowStyleBaseId, MappingsHash, PeripheralDeviceId } from './Ids' import { BlueprintHash, LastBlueprintConfig } from './Blueprint' import { MappingsExt, MappingExt } from '@sofie-automation/shared-lib/dist/core/model/Timeline' import { ForceQuickLoopAutoNext } from './RundownPlaylist' +import { + ResultingMappingRoute, + RouteMapping, + StudioRouteBehavior, + ResultingMappingRoutes, + StudioRouteSet, + StudioRouteSetExclusivityGroup, + StudioRouteType, +} from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' export { MappingsExt, MappingExt, MappingsHash } +// RouteSet functions has been moved to shared-lib: +// So we need to re-export them here: +export { + StudioRouteSetExclusivityGroup, + ResultingMappingRoute, + RouteMapping, + StudioRouteBehavior, + ResultingMappingRoutes, + StudioRouteSet, + StudioRouteType, +} + export interface IStudioSettings { /** The framerate (frames per second) used to convert internal timing information (in milliseconds) * into timecodes and timecode-like strings and interpret timecode user input @@ -101,8 +122,8 @@ export interface DBStudio { _rundownVersionHash: string - routeSets: Record - routeSetExclusivityGroups: Record + routeSetsWithOverrides: ObjectWithOverrides> + routeSetExclusivityGroupsWithOverrides: ObjectWithOverrides> /** Contains settings for which Package Containers are present in the studio. * (These are used by the Package Manager and the Expected Packages) @@ -167,52 +188,3 @@ export interface StudioPackageContainer { deviceIds: string[] container: PackageContainer } -export interface StudioRouteSetExclusivityGroup { - name: string -} - -export interface StudioRouteSet { - /** User-presentable name */ - name: string - /** Whether this group is active or not */ - active: boolean - /** Default state of this group */ - defaultActive?: boolean - /** Only one Route can be active at the same time in the exclusivity-group */ - exclusivityGroup?: string - /** If true, should be displayed and toggleable by user */ - behavior: StudioRouteBehavior - - routes: RouteMapping[] -} -export enum StudioRouteBehavior { - HIDDEN = 0, - TOGGLE = 1, - ACTIVATE_ONLY = 2, -} - -export enum StudioRouteType { - /** Default */ - REROUTE = 0, - /** Replace all properties with a new mapping */ - REMAP = 1, -} - -export interface RouteMapping extends ResultingMappingRoute { - /** Which original layer to route. If false, a "new" layer will be inserted during routing */ - mappedLayer: string | undefined -} -export interface ResultingMappingRoutes { - /** Routes that route existing layers */ - existing: { - [mappedLayer: string]: ResultingMappingRoute[] - } - /** Routes that create new layers, from nothing */ - inserted: ResultingMappingRoute[] -} -export interface ResultingMappingRoute { - outputMappedLayer: string - deviceType?: TSR.DeviceType - remapping?: Partial - routeType: StudioRouteType -} diff --git a/packages/corelib/src/overrideOpHelper.ts b/packages/corelib/src/overrideOpHelper.ts new file mode 100644 index 0000000000..c909e4f6a7 --- /dev/null +++ b/packages/corelib/src/overrideOpHelper.ts @@ -0,0 +1,329 @@ +import { clone, literal, objectPathSet } from './lib' +import { + SomeObjectOverrideOp, + ObjectWithOverrides, + ObjectOverrideDeleteOp, + ObjectOverrideSetOp, + applyAndValidateOverrides, + filterOverrideOpsForPrefix, + findParentOpToUpdate, +} from './settings/objectWithOverrides' +import { ReadonlyDeep } from 'type-fest' + +export interface WrappedOverridableItemDeleted { + type: 'deleted' + id: string + computed: undefined + defaults: ReadonlyDeep + overrideOps: ReadonlyDeep +} +export interface WrappedOverridableItemNormal { + type: 'normal' + id: string + computed: T + defaults: ReadonlyDeep | undefined + overrideOps: ReadonlyDeep +} + +export type WrappedOverridableItem = + | WrappedOverridableItemDeleted + | WrappedOverridableItemNormal + +/** + * Compile a sorted array of all the items currently in the ObjectWithOverrides, and those that have been deleted + * @param rawObject The ObjectWithOverrides to look at + * @param comparitor Comparitor for sorting the items + * @returns Sorted items, with sorted deleted items at the end + */ +export function getAllCurrentAndDeletedItemsFromOverrides( + rawObject: ReadonlyDeep>>, + comparitor: + | ((a: [id: string, obj: T | ReadonlyDeep], b: [id: string, obj: T | ReadonlyDeep]) => number) + | null +): WrappedOverridableItem[] { + // Sort and wrap in the return type + const sortedItems = getAllCurrentItemsFromOverrides(rawObject, comparitor) + + const removedOutputLayers: WrappedOverridableItemDeleted[] = [] + + // Find the items which have been deleted with an override + const computedOutputLayerIds = new Set(sortedItems.map((l) => l.id)) + for (const [id, output] of Object.entries>(rawObject.defaults)) { + if (!computedOutputLayerIds.has(id) && output) { + removedOutputLayers.push( + literal>({ + type: 'deleted', + id: id, + computed: undefined, + defaults: output, + overrideOps: filterOverrideOpsForPrefix(rawObject.overrides, id).opsForPrefix, + }) + ) + } + } + + if (comparitor) removedOutputLayers.sort((a, b) => comparitor([a.id, a.defaults], [b.id, b.defaults])) + + return [...sortedItems, ...removedOutputLayers] +} + +/** + * Compile a sorted array of all the items currently active in the ObjectWithOverrides + * @param rawObject The ObjectWithOverrides to look at + * @param comparitor Comparitor for sorting the items + * @returns Sorted items + */ +export function getAllCurrentItemsFromOverrides( + rawObject: ReadonlyDeep>>, + comparitor: + | ((a: [id: string, obj: T | ReadonlyDeep], b: [id: string, obj: T | ReadonlyDeep]) => number) + | null +): WrappedOverridableItemNormal[] { + const resolvedObject = applyAndValidateOverrides(rawObject).obj + + // Convert the items into an array + const validItems: Array<[id: string, obj: T]> = [] + for (const [id, obj] of Object.entries(resolvedObject)) { + if (obj) validItems.push([id, obj]) + } + + if (comparitor) validItems.sort((a, b) => comparitor(a, b)) + + // Sort and wrap in the return type + const sortedItems = validItems.map(([id, obj]) => + literal>({ + type: 'normal', + id: id, + computed: obj, + defaults: rawObject.defaults[id], + overrideOps: filterOverrideOpsForPrefix(rawObject.overrides, id).opsForPrefix, + }) + ) + + return sortedItems +} + +type SaveOverridesFunction = (newOps: SomeObjectOverrideOp[]) => void + +export type OverrideOpHelperForItemContents = () => OverrideOpHelperForItemContentsBatcher + +export interface OverrideOpHelperForItemContentsBatcher { + /** + * Clear all of the overrides for an value inside of an item + * This acts as a reset of property of its child properties + * Has no effect if there are no `overrideOps` on the `WrappedOverridableItemNormal` + */ + clearItemOverrides(itemId: string, subPath: string): this + + /** + * Set the value of a property of an item. + * Note: the id cannot be changed in this way + */ + setItemValue(itemId: string, subPath: string, value: unknown): this + + /** + * Finish the batch operation + */ + commit(): void +} + +export interface OverrideOpHelperBatcher extends OverrideOpHelperForItemContentsBatcher { + /** + * Clear all of the overrides for an item + * This acts as a reset to defaults or undelete + * Has no effect if there are no `overrideOps` on the `WrappedOverridableItemNormal` + */ + resetItem(itemId: string): this + + /** + * Delete an item from the object + */ + deleteItem(itemId: string): this + + /** + * Change the id of an item. + * This is only possible for ones which were created by an override, and does not exist in the defaults + * Only possible when the item being renamed does not exist in the defaults + */ + changeItemId(oldItemId: string, newItemId: string): this + + /** + * Replace a whole item with a new object + * Note: the id cannot be changed in this way + */ + replaceItem(itemId: string, value: any): this + + /** + * Finish the batch operation + */ + commit(): void +} + +export type OverrideOpHelper = () => OverrideOpHelperBatcher + +export class OverrideOpHelperImpl implements OverrideOpHelperBatcher { + readonly #saveOverrides: SaveOverridesFunction + readonly #object: ObjectWithOverrides + + constructor(saveOverrides: SaveOverridesFunction, object: ObjectWithOverrides) { + this.#saveOverrides = saveOverrides + this.#object = { ...object } + } + + clearItemOverrides = (itemId: string, subPath: string): this => { + const opPath = `${itemId}.${subPath}` + + const newOps = filterOverrideOpsForPrefix(this.#object.overrides, opPath).otherOps + + this.#object.overrides = newOps + + return this + } + + resetItem = (itemId: string): this => { + const newOps = filterOverrideOpsForPrefix(this.#object.overrides, itemId).otherOps + + this.#object.overrides = newOps + + return this + } + + deleteItem = (itemId: string): this => { + const newOps = filterOverrideOpsForPrefix(this.#object.overrides, itemId).otherOps + if (this.#object.defaults[itemId]) { + // If it was from the defaults, we need to mark it deleted + newOps.push( + literal({ + op: 'delete', + path: itemId, + }) + ) + } + + this.#object.overrides = newOps + + return this + } + + changeItemId = (oldItemId: string, newItemId: string): this => { + const { otherOps: newOps, opsForPrefix: opsForId } = filterOverrideOpsForPrefix( + this.#object.overrides, + oldItemId + ) + + if (!newItemId || newOps.find((op) => op.path === newItemId) || this.#object.defaults[newItemId]) { + throw new Error('Id is invalid or already in use') + } + + if (this.#object.defaults[oldItemId]) { + // Future: should we be able to handle this? + throw new Error("Can't change id of object with defaults") + } else { + // Change the id prefix of the ops + for (const op of opsForId) { + const newPath = `${newItemId}${op.path.substring(oldItemId.length)}` + + const newOp = { + ...op, + path: newPath, + } + newOps.push(newOp) + + if (newOp.path === newItemId && newOp.op === 'set') { + newOp.value._id = newItemId + } + } + + this.#object.overrides = newOps + + return this + } + } + + setItemValue = (itemId: string, subPath: string, value: unknown): this => { + if (subPath === '_id') { + throw new Error('Item id cannot be changed through this helper') + } else { + // Set a property + const { otherOps: newOps, opsForPrefix: opsForId } = filterOverrideOpsForPrefix( + this.#object.overrides, + itemId + ) + + const setRootOp = opsForId.find((op) => op.path === itemId) + if (setRootOp && setRootOp.op === 'set') { + // This is as its base an override, so modify that instead + const newOp = clone(setRootOp) + + objectPathSet(newOp.value, subPath, value) + + newOps.push(newOp) + } else { + // Look for a op which encompasses this new value + const parentOp = findParentOpToUpdate(opsForId, subPath) + if (parentOp) { + // Found an op at a higher level that can be modified instead + objectPathSet(parentOp.op.value, parentOp.newSubPath, value) + } else { + // Insert new op + const newOp = literal({ + op: 'set', + path: `${itemId}.${subPath}`, + value: value, + }) + + const newOpAsPrefix = `${newOp.path}.` + + // Preserve any other overrides + for (const op of opsForId) { + if (op.path === newOp.path || op.path.startsWith(newOpAsPrefix)) { + // ignore, as op has been replaced by the one at a higher path + } else { + // Retain unrelated op + newOps.push(op) + } + } + // Add the new override + newOps.push(newOp) + } + } + + this.#object.overrides = newOps + + return this + } + } + + replaceItem = (itemId: string, value: unknown): this => { + // Set a property + const { otherOps: newOps } = filterOverrideOpsForPrefix(this.#object.overrides, itemId) + + // TODO - is this too naive? + + newOps.push( + literal({ + op: 'set', + path: `${itemId}`, + value: value, + }) + ) + + this.#object.overrides = newOps + + return this + } + + commit = (): void => { + this.#saveOverrides(this.#object.overrides) + } +} + +/** + * A helper to work with modifying an ObjectWithOverrides + */ +export function useOverrideOpHelperBackend( + saveOverrides: (newOps: SomeObjectOverrideOp[]) => void, + objectWithOverrides: ObjectWithOverrides +): OverrideOpHelperBatcher { + return new OverrideOpHelperImpl(saveOverrides, objectWithOverrides) +} diff --git a/packages/corelib/src/settings/objectWithOverrides.ts b/packages/corelib/src/settings/objectWithOverrides.ts index 228234eae7..03783ab565 100644 --- a/packages/corelib/src/settings/objectWithOverrides.ts +++ b/packages/corelib/src/settings/objectWithOverrides.ts @@ -51,6 +51,10 @@ export function wrapDefaultObject(obj: T): ObjectWithOverrides overrides: [], } } +export function isObjectWithOverrides(o: ObjectWithOverrides | T): o is ObjectWithOverrides { + const oAny = o as any + return typeof oAny.defaults === 'object' && Array.isArray(oAny.overrides) +} /** * In some cases, an ObjectWithOverrides should have no defaults. This is common for when the user owns the object containing the ObjectWithOverrides. * This helper takes an ObjectWithOverrides, and converts it to have no defaults, and have each contained object as an override diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 81dd8fdc9a..c86324ed4a 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -193,6 +193,12 @@ export enum StudioJobs { * Set QuickLoop marker */ SetQuickLoopMarker = 'setQuickLoopMarker', + + /** + * Switch the route of the studio + * for use in ad.lib actions and other triggers + */ + SwitchRouteSet = 'switchRouteSet', } export interface RundownPlayoutPropsBase { @@ -345,6 +351,11 @@ export interface SetQuickLoopMarkerProps extends RundownPlayoutPropsBase { marker: QuickLoopMarker | null } +export interface SwitchRouteSetProps { + routeSetId: string + state: boolean | 'toggle' +} + /** * Set of valid functions, of form: * `id: (data) => return` @@ -398,6 +409,8 @@ export type StudioJobFunc = { [StudioJobs.ActivateAdlibTesting]: (data: ActivateAdlibTestingProps) => void [StudioJobs.SetQuickLoopMarker]: (data: SetQuickLoopMarkerProps) => void + + [StudioJobs.SwitchRouteSet]: (data: SwitchRouteSetProps) => void } export function getStudioQueueName(id: StudioId): string { diff --git a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts index 819ac3aff1..8f1ce6389f 100644 --- a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts @@ -108,8 +108,8 @@ export function defaultStudio(_id: StudioId): DBStudio { minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowAdlibTestingSegment: true, }, - routeSets: {}, - routeSetExclusivityGroups: {}, + routeSetsWithOverrides: wrapDefaultObject({}), + routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainers: {}, previewContainerIds: [], thumbnailContainerIds: [], diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index bd2f66db7e..20b94317b6 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -184,6 +184,10 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct partInstance.blockTakeUntil(time) } + async switchRouteSet(routeSetId: string, state: boolean): Promise { + this._playoutModel.switchRouteSet(routeSetId, state) + } + async hackGetMediaObjectDuration(mediaId: string): Promise { return this.partAndPieceInstanceService.hackGetMediaObjectDuration(mediaId) } diff --git a/packages/job-worker/src/playout/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index a21b3502a6..10caee9a47 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -191,6 +191,13 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa */ activatePlaylist(rehearsal: boolean): RundownPlaylistActivationId + /** + * Update the active state of a RouteSet + * @param routeSetId + * @param isActive + */ + switchRouteSet(routeSetId: string, isActive: boolean): void + /** * Clear the currently selected PartInstances, so that nothing is selected for playback */ diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index cb53c18586..6ee433b9dd 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -480,6 +480,10 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou return partInstance } + switchRouteSet(routeSetId: string, isActive: boolean): void { + this.#baselineHelper.updateRouteSetActive(routeSetId, isActive) + } + cycleSelectedPartInstances(): void { this.playlistImpl.previousPartInfo = this.playlistImpl.currentPartInfo this.playlistImpl.currentPartInfo = this.playlistImpl.nextPartInfo diff --git a/packages/job-worker/src/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index b03dc1d286..25a38d4813 100644 --- a/packages/job-worker/src/playout/upgrade.ts +++ b/packages/job-worker/src/playout/upgrade.ts @@ -4,6 +4,8 @@ import { StudioIngestDevice, StudioInputDevice, StudioPlayoutDevice, + StudioRouteSet, + StudioRouteSetExclusivityGroup, } from '@sofie-automation/corelib/dist/dataModel/Studio' import { Complete, clone, literal } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' @@ -67,6 +69,27 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data }), ]) ) + const routeSets = Object.fromEntries( + Object.entries(result.routeSets ?? {}).map((dev) => [ + dev[0], + literal>({ + name: (dev[1] as StudioRouteSet).name ?? '', + active: (dev[1] as StudioRouteSet).active ?? false, + defaultActive: (dev[1] as StudioRouteSet).defaultActive ?? false, + behavior: (dev[1] as StudioRouteSet).behavior ?? {}, + exclusivityGroup: (dev[1] as StudioRouteSet).exclusivityGroup ?? undefined, + routes: (dev[1] as StudioRouteSet).routes, + }), + ]) + ) + const routeSetExclusivityGroups = Object.fromEntries( + Object.entries(result.routeSetExclusivityGroups ?? {}).map((dev) => [ + dev[0], + literal>({ + name: (dev[1] as StudioRouteSetExclusivityGroup).name, + }), + ]) + ) await context.directCollections.Studios.update(context.studioId, { $set: { @@ -74,6 +97,8 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data 'peripheralDeviceSettings.playoutDevices.defaults': playoutDevices, 'peripheralDeviceSettings.ingestDevices.defaults': ingestDevices, 'peripheralDeviceSettings.inputDevices.defaults': inputDevices, + 'routeSetsWithOverrides.defaults': routeSets, + 'routeSetExclusivityGroupsWithOverrides.defaults': routeSetExclusivityGroups, lastBlueprintConfig: { blueprintHash: blueprint.blueprintDoc.blueprintHash, blueprintId: blueprint.blueprintId, diff --git a/packages/job-worker/src/studio/model/StudioBaselineHelper.ts b/packages/job-worker/src/studio/model/StudioBaselineHelper.ts index 5b41352248..c7e9772938 100644 --- a/packages/job-worker/src/studio/model/StudioBaselineHelper.ts +++ b/packages/job-worker/src/studio/model/StudioBaselineHelper.ts @@ -6,19 +6,33 @@ import { } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { ExpectedPlayoutItemStudio } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' import { saveIntoDb } from '../../db/changes' +import { StudioRouteBehavior, StudioRouteSet } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { logger } from '../../logging' +import { + WrappedOverridableItemNormal, + useOverrideOpHelperBackend, + getAllCurrentItemsFromOverrides, +} from '@sofie-automation/corelib/dist/overrideOpHelper' +import { ObjectWithOverrides, SomeObjectOverrideOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' export class StudioBaselineHelper { readonly #context: JobContext + #overridesRouteSetBuffer: ObjectWithOverrides> #pendingExpectedPackages: ExpectedPackageDBFromStudioBaselineObjects[] | undefined #pendingExpectedPlayoutItems: ExpectedPlayoutItemStudio[] | undefined + #routeSetChanged: boolean constructor(context: JobContext) { this.#context = context + this.#overridesRouteSetBuffer = { ...context.studio.routeSetsWithOverrides } as ObjectWithOverrides< + Record + > + this.#routeSetChanged = false } hasChanges(): boolean { - return !!this.#pendingExpectedPackages || !!this.#pendingExpectedPlayoutItems + return !!this.#pendingExpectedPackages || !!this.#pendingExpectedPlayoutItems || this.#routeSetChanged } setExpectedPackages(packages: ExpectedPackageDBFromStudioBaselineObjects[]): void { @@ -49,9 +63,63 @@ export class StudioBaselineHelper { this.#pendingExpectedPackages ) : undefined, + this.#routeSetChanged + ? this.#context.directCollections.Studios.update( + { + _id: this.#context.studioId, + }, + { + $set: { 'routeSetsWithOverrides.overrides': this.#overridesRouteSetBuffer.overrides }, + } + ) + : undefined, ]) this.#pendingExpectedPlayoutItems = undefined this.#pendingExpectedPackages = undefined + this.#routeSetChanged = false + this.#overridesRouteSetBuffer = { ...this.#context.studio.routeSetsWithOverrides } as ObjectWithOverrides< + Record + > + } + + updateRouteSetActive(routeSetId: string, isActive: boolean | 'toggle'): void { + const studio = this.#context.studio + const saveOverrides = (newOps: SomeObjectOverrideOp[]) => { + // this.#overridesRouteSetBuffer = { defaults: this.#overridesRouteSetBuffer.defaults, overrides: newOps } + this.#overridesRouteSetBuffer.overrides = newOps + this.#routeSetChanged = true + } + const overrideHelper = useOverrideOpHelperBackend(saveOverrides, this.#overridesRouteSetBuffer) + + const routeSets: WrappedOverridableItemNormal[] = getAllCurrentItemsFromOverrides( + this.#overridesRouteSetBuffer, + null + ) + + const routeSet = routeSets.find((routeSet) => { + return routeSet.id === routeSetId + }) + + if (routeSet === undefined) throw new Error(`RouteSet "${routeSetId}" not found!`) + + if (isActive === 'toggle') isActive = !routeSet.computed.active + + if (routeSet.computed?.behavior === StudioRouteBehavior.ACTIVATE_ONLY && isActive === false) + throw new Error(`RouteSet "${routeSet.id}" is ACTIVATE_ONLY`) + + logger.debug(`switchRouteSet "${studio._id}" "${routeSet.id}"=${isActive}`) + overrideHelper.setItemValue(routeSet.id, `active`, isActive).commit() + + // Deactivate other routeSets in the same exclusivity group: + if (routeSet.computed.exclusivityGroup && isActive === true) { + for (const [, otherRouteSet] of Object.entries>(routeSets)) { + if (otherRouteSet.id === routeSet.id) continue + if (otherRouteSet.computed?.exclusivityGroup === routeSet.computed.exclusivityGroup) { + logger.debug(`switchRouteSet Other ID "${studio._id}" "${otherRouteSet.id}"=false`) + overrideHelper.setItemValue(otherRouteSet.id, `active`, false).commit() + } + } + } } } diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModel.ts b/packages/job-worker/src/studio/model/StudioPlayoutModel.ts index a4b22ed20a..b54a4d3af2 100644 --- a/packages/job-worker/src/studio/model/StudioPlayoutModel.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModel.ts @@ -68,4 +68,11 @@ export interface StudioPlayoutModel extends StudioPlayoutModelBase, BaseModel { * @param excludeRundownPlaylistId Ignore a given RundownPlaylist, useful to see if any other RundownPlaylists are active */ getActiveRundownPlaylists(excludeRundownPlaylistId?: RundownPlaylistId): ReadonlyDeep + + /** + * Update the active state of a RouteSet + * @param routeSetId The RouteSet to update + * @param isActive The new active state of the RouteSet + */ + switchRouteSet(routeSetId: string, isActive: boolean | 'toggle'): void } diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts index 06e9689f93..528a15ef3a 100644 --- a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts @@ -34,6 +34,7 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { #timelineHasChanged = false #timeline: TimelineComplete | null + public get timeline(): TimelineComplete | null { return this.#timeline } @@ -100,6 +101,10 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { return this.#timeline } + switchRouteSet(routeSetId: string, isActive: boolean | 'toggle'): void { + this.#baselineHelper.updateRouteSetActive(routeSetId, isActive) + } + /** * Discards all documents in this model, and marks it as unusable */ diff --git a/packages/job-worker/src/studio/routeSet.ts b/packages/job-worker/src/studio/routeSet.ts new file mode 100644 index 0000000000..89cb910f24 --- /dev/null +++ b/packages/job-worker/src/studio/routeSet.ts @@ -0,0 +1,9 @@ +import { SwitchRouteSetProps } from '@sofie-automation/corelib/dist/worker/studio' +import { JobContext } from '../jobs' +import { runJobWithStudioPlayoutModel } from './lock' + +export async function handleSwitchRouteSet(context: JobContext, data: SwitchRouteSetProps): Promise { + await runJobWithStudioPlayoutModel(context, async (studioPlayoutModel) => { + studioPlayoutModel.switchRouteSet(data.routeSetId, data.state) + }) +} diff --git a/packages/job-worker/src/workers/studio/jobs.ts b/packages/job-worker/src/workers/studio/jobs.ts index a90a65d233..a7741e6e56 100644 --- a/packages/job-worker/src/workers/studio/jobs.ts +++ b/packages/job-worker/src/workers/studio/jobs.ts @@ -47,6 +47,7 @@ import { handleTakeNextPart } from '../../playout/take' import { handleSetQuickLoopMarker } from '../../playout/quickLoopMarkers' import { handleActivateAdlibTesting } from '../../playout/adlibTesting' import { handleExecuteBucketAdLibOrAction } from '../../playout/bucketAdlibJobs' +import { handleSwitchRouteSet } from '../../studio/routeSet' type ExecutableFunction = ( context: JobContext, @@ -106,4 +107,6 @@ export const studioJobHandlers: StudioJobHandlers = { [StudioJobs.ActivateAdlibTesting]: handleActivateAdlibTesting, [StudioJobs.SetQuickLoopMarker]: handleSetQuickLoopMarker, + + [StudioJobs.SwitchRouteSet]: handleSwitchRouteSet, } diff --git a/packages/meteor-lib/src/api/userActions.ts b/packages/meteor-lib/src/api/userActions.ts index 55cc2e2bfe..91c964bbec 100644 --- a/packages/meteor-lib/src/api/userActions.ts +++ b/packages/meteor-lib/src/api/userActions.ts @@ -301,7 +301,7 @@ export interface NewUserActionAPI { eventTime: Time, studioId: StudioId, routeSetId: string, - state: boolean + state: boolean | 'toggle' ): Promise> moveRundown( userEvent: string, diff --git a/packages/meteor-lib/src/collections/Studios.ts b/packages/meteor-lib/src/collections/Studios.ts index b5f8238944..d7caa2e49f 100644 --- a/packages/meteor-lib/src/collections/Studios.ts +++ b/packages/meteor-lib/src/collections/Studios.ts @@ -4,20 +4,19 @@ import { MappingExt, StudioRouteType, StudioRouteSet, - RouteMapping, } from '@sofie-automation/corelib/dist/dataModel/Studio' import { omit } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { ReadonlyDeep } from 'type-fest' -export function getActiveRoutes(routeSets: ReadonlyDeep>): ResultingMappingRoutes { +export function getActiveRoutes(routeSets: Record): ResultingMappingRoutes { const routes: ResultingMappingRoutes = { existing: {}, inserted: [], } const exclusivityGroups: { [groupId: string]: true } = {} - for (const routeSet of Object.values>(routeSets)) { + + for (const routeSet of Object.values(routeSets)) { if (routeSet.active) { let useRoute = true if (routeSet.exclusivityGroup) { @@ -28,7 +27,7 @@ export function getActiveRoutes(routeSets: ReadonlyDeep>(routeSet.routes)) { + for (const routeMapping of routeSet.routes || []) { if (routeMapping.outputMappedLayer) { if (routeMapping.mappedLayer) { // Route an existing layer diff --git a/packages/meteor-lib/src/triggers/actionFactory.ts b/packages/meteor-lib/src/triggers/actionFactory.ts index 26b944a591..3e7d93b54d 100644 --- a/packages/meteor-lib/src/triggers/actionFactory.ts +++ b/packages/meteor-lib/src/triggers/actionFactory.ts @@ -16,7 +16,13 @@ import RundownViewEventBus, { RundownViewEvents } from '../triggers/RundownViewE import { UserAction } from '../userAction' import { AdLibFilterChainLink, compileAdLibFilter, IWrappedAdLib } from './actionFilterChainCompilers' import { ClientAPI } from '../api/client' -import { PartId, PartInstanceId, RundownId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { + PartId, + PartInstanceId, + RundownId, + RundownPlaylistId, + StudioId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' import { DeviceActions } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { MountedAdLibTriggerType } from '../api/MountedTriggers' @@ -30,6 +36,7 @@ type Without = { [P in Exclude]?: never } type XOR = T | U extends object ? (Without & U) | (Without & T) : T | U export interface ReactivePlaylistActionContext { + studioId: ReactiveVar rundownPlaylistId: ReactiveVar rundownPlaylist: ReactiveVar< Pick @@ -104,6 +111,7 @@ function createRundownPlaylistContext( } else if (filterChain[0].object === 'view' && context.rundownPlaylist) { const playlistContext = context as PlainPlaylistContext return { + studioId: new DummyReactiveVar(playlistContext.rundownPlaylist.studioId), rundownPlaylistId: new DummyReactiveVar(playlistContext.rundownPlaylist._id), rundownPlaylist: new DummyReactiveVar(playlistContext.rundownPlaylist), currentRundownId: new DummyReactiveVar(playlistContext.currentRundownId), @@ -589,6 +597,16 @@ export function createAction( async (e, ts, ctx) => triggersContext.MeteorCall.userAction.resyncRundownPlaylist(e, ts, ctx.rundownPlaylistId.get()) ) + case PlayoutActions.switchRouteSet: + return createUserActionWithCtx(triggersContext, action, UserAction.SWITCH_ROUTE_SET, async (e, ts, ctx) => + triggersContext.MeteorCall.userAction.switchRouteSet( + e, + ts, + ctx.studioId.get(), + action.routeSetId, + 'toggle' + ) + ) case ClientActions.showEntireCurrentSegment: return createShowEntireCurrentSegmentAction(action.filterChain, action.on) case ClientActions.miniShelfQueueAdLib: diff --git a/packages/shared-lib/src/core/model/ShowStyle.ts b/packages/shared-lib/src/core/model/ShowStyle.ts index 4a7a2ec8da..9c1825ff28 100644 --- a/packages/shared-lib/src/core/model/ShowStyle.ts +++ b/packages/shared-lib/src/core/model/ShowStyle.ts @@ -95,6 +95,7 @@ export enum PlayoutActions { reloadRundownPlaylistData = 'reloadRundownPlaylistData', disableNextPiece = 'disableNextPiece', activateAdlibTestingMode = 'activateAdlibTestingMode', + switchRouteSet = 'switchRouteSet', } export enum ClientActions { diff --git a/packages/shared-lib/src/core/model/StudioRouteSet.ts b/packages/shared-lib/src/core/model/StudioRouteSet.ts new file mode 100644 index 0000000000..3c0ae4562c --- /dev/null +++ b/packages/shared-lib/src/core/model/StudioRouteSet.ts @@ -0,0 +1,52 @@ +import { BlueprintMapping } from './Timeline' +import { TSR } from '../../tsr' + +export interface StudioRouteSetExclusivityGroup { + name: string +} + +export interface StudioRouteSet { + /** User-presentable name */ + name: string + /** Whether this group is active or not */ + active: boolean + /** Default state of this group */ + defaultActive?: boolean + /** Only one Route can be active at the same time in the exclusivity-group */ + exclusivityGroup?: string + /** If true, should be displayed and toggleable by user */ + behavior: StudioRouteBehavior + + routes: RouteMapping[] +} +export enum StudioRouteBehavior { + HIDDEN = 0, + TOGGLE = 1, + ACTIVATE_ONLY = 2, +} + +export enum StudioRouteType { + /** Default */ + REROUTE = 0, + /** Replace all properties with a new mapping */ + REMAP = 1, +} + +export interface RouteMapping extends ResultingMappingRoute { + /** Which original layer to route. If false, a "new" layer will be inserted during routing */ + mappedLayer: string | undefined +} +export interface ResultingMappingRoutes { + /** Routes that route existing layers */ + existing: { + [mappedLayer: string]: ResultingMappingRoute[] + } + /** Routes that create new layers, from nothing */ + inserted: ResultingMappingRoute[] +} +export interface ResultingMappingRoute { + outputMappedLayer: string + deviceType?: TSR.DeviceType + remapping?: Partial + routeType: StudioRouteType +} diff --git a/packages/webui/src/__mocks__/defaultCollectionObjects.ts b/packages/webui/src/__mocks__/defaultCollectionObjects.ts index 2ac60d54a7..4a699eb99e 100644 --- a/packages/webui/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/webui/src/__mocks__/defaultCollectionObjects.ts @@ -107,8 +107,8 @@ export function defaultStudio(_id: StudioId): DBStudio { minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, }, _rundownVersionHash: '', - routeSets: {}, - routeSetExclusivityGroups: {}, + routeSetsWithOverrides: wrapDefaultObject({}), + routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainers: {}, previewContainerIds: [], thumbnailContainerIds: [], diff --git a/packages/webui/src/client/lib/Components/Checkbox.tsx b/packages/webui/src/client/lib/Components/Checkbox.tsx index c357624805..6932ef5e6d 100644 --- a/packages/webui/src/client/lib/Components/Checkbox.tsx +++ b/packages/webui/src/client/lib/Components/Checkbox.tsx @@ -6,6 +6,7 @@ import ClassNames from 'classnames' interface ICheckboxControlProps { classNames?: string disabled?: boolean + title?: string value: boolean handleUpdate: (value: boolean) => void @@ -15,6 +16,7 @@ export function CheckboxControl({ value, disabled, handleUpdate, + title, }: Readonly): JSX.Element { const handleChange = useCallback( (e: React.ChangeEvent) => { @@ -25,11 +27,18 @@ export function CheckboxControl({ return ( - - + + - + diff --git a/packages/webui/src/client/lib/Components/LabelAndOverrides.tsx b/packages/webui/src/client/lib/Components/LabelAndOverrides.tsx index 25cb48894a..ef291cafc8 100644 --- a/packages/webui/src/client/lib/Components/LabelAndOverrides.tsx +++ b/packages/webui/src/client/lib/Components/LabelAndOverrides.tsx @@ -16,6 +16,7 @@ export interface LabelAndOverridesProps { opPrefix: string overrideHelper: OverrideOpHelperForItemContents + showClearButton?: boolean formatDefaultValue?: (value: any) => JSX.Element | string | null children: (value: TValue, setValue: (value: TValue) => void) => React.ReactNode @@ -33,6 +34,7 @@ export function LabelAndOverrides({ itemKey, opPrefix, overrideHelper, + showClearButton, formatDefaultValue, }: Readonly>): JSX.Element { const { t } = useTranslation() @@ -51,7 +53,7 @@ export function LabelAndOverrides({ let displayValue: JSX.Element | string | null = '""' if (item.defaults) { - const defaultValue: any = item.defaults[itemKey] + const defaultValue: any = objectPathGet(item.defaults, String(itemKey)) // Special cases for formatting of the default if (formatDefaultValue) { displayValue = formatDefaultValue(defaultValue) @@ -75,7 +77,16 @@ export function LabelAndOverrides({