diff --git a/meteor/server/api/buckets.ts b/meteor/server/api/buckets.ts index 109ae82ba9..04e1fce3cf 100644 --- a/meteor/server/api/buckets.ts +++ b/meteor/server/api/buckets.ts @@ -1,6 +1,6 @@ import * as _ from 'underscore' import { Meteor } from 'meteor/meteor' -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { getRandomId, getRandomString, literal } from '../lib/tempLib' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { AdLibAction, AdLibActionCommon } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index aefdfbe439..657ebe5884 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -52,7 +52,7 @@ import { DEFAULT_MINIMUM_TAKE_SPAN, DEFAULT_FALLBACK_PART_DURATION, } from '@sofie-automation/shared-lib/dist/core/constants' -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' /* diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index 348380cbf6..043152f287 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -15,7 +15,7 @@ import { MOSDeviceActions } from './ingest/mosDevice/actions' import { MethodContextAPI } from './methodContext' import { ServerClientAPI } from './client' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { BucketsAPI } from './buckets' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { AdLibActionCommon } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' diff --git a/meteor/server/collections/bucket.ts b/meteor/server/collections/bucket.ts index 5479fcfeef..8742f762ff 100644 --- a/meteor/server/collections/bucket.ts +++ b/meteor/server/collections/bucket.ts @@ -1,7 +1,7 @@ import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { createAsyncOnlyMongoCollection } from './collection' import { registerIndex } from './indices' diff --git a/meteor/server/publications/buckets.ts b/meteor/server/publications/buckets.ts index 589f63d3bb..2c9d7c6ea4 100644 --- a/meteor/server/publications/buckets.ts +++ b/meteor/server/publications/buckets.ts @@ -1,7 +1,6 @@ import { FindOptions } from '@sofie-automation/meteor-lib/dist/collections/lib' import { meteorPublish } from './lib/lib' -import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { BucketAdLibActions, BucketAdLibs, Buckets } from '../collections' import { check, Match } from 'meteor/check' import { StudioId, BucketId, ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -9,7 +8,7 @@ import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' meteorPublish( - MeteorPubSub.buckets, + CorelibPubSub.buckets, async function (studioId: StudioId, bucketId: BucketId | null, _token: string | undefined) { check(studioId, String) check(bucketId, Match.Maybe(String)) @@ -21,14 +20,10 @@ meteorPublish( } return Buckets.findWithCursor( - bucketId - ? { - _id: bucketId, - studioId, - } - : { - studioId, - }, + { + _id: bucketId ?? undefined, + studioId, + }, modifier ) } @@ -36,9 +31,9 @@ meteorPublish( meteorPublish( CorelibPubSub.bucketAdLibPieces, - async function (studioId: StudioId, bucketId: BucketId, showStyleVariantIds: ShowStyleVariantId[]) { + async function (studioId: StudioId, bucketId: BucketId | null, showStyleVariantIds: ShowStyleVariantId[]) { check(studioId, String) - check(bucketId, String) + check(bucketId, Match.Maybe(String)) check(showStyleVariantIds, Array) triggerWriteAccessBecauseNoCheckNecessary() @@ -46,7 +41,7 @@ meteorPublish( return BucketAdLibs.findWithCursor( { studioId: studioId, - bucketId: bucketId, + bucketId: bucketId ?? undefined, showStyleVariantId: { $in: [null, ...showStyleVariantIds], // null = valid for all variants }, @@ -62,7 +57,7 @@ meteorPublish( meteorPublish( CorelibPubSub.bucketAdLibActions, - async function (studioId: StudioId, bucketId: BucketId, showStyleVariantIds: ShowStyleVariantId[]) { + async function (studioId: StudioId, bucketId: BucketId | null, showStyleVariantIds: ShowStyleVariantId[]) { check(studioId, String) check(bucketId, String) check(showStyleVariantIds, Array) diff --git a/meteor/server/publications/pieceContentStatusUI/bucket/publication.ts b/meteor/server/publications/pieceContentStatusUI/bucket/publication.ts index 8942d5f75d..838fca39f8 100644 --- a/meteor/server/publications/pieceContentStatusUI/bucket/publication.ts +++ b/meteor/server/publications/pieceContentStatusUI/bucket/publication.ts @@ -21,7 +21,7 @@ import { SetupObserversResult, } from '../../../lib/customPublication' import { BucketContentCache, createReactiveContentCache } from './bucketContentCache' -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { addItemsWithDependenciesChangesToChangedSet, fetchStudio, diff --git a/packages/meteor-lib/src/collections/Buckets.ts b/packages/corelib/src/dataModel/Bucket.ts similarity index 90% rename from packages/meteor-lib/src/collections/Buckets.ts rename to packages/corelib/src/dataModel/Bucket.ts index 72750e7507..519dadb87e 100644 --- a/packages/meteor-lib/src/collections/Buckets.ts +++ b/packages/corelib/src/dataModel/Bucket.ts @@ -1,4 +1,4 @@ -import { BucketId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { BucketId, StudioId } from './Ids' /** * A Bucket is an container for AdLib pieces that do not come from a MOS gateway and are diff --git a/packages/corelib/src/pubsub.ts b/packages/corelib/src/pubsub.ts index a8436a1403..b09c67c589 100644 --- a/packages/corelib/src/pubsub.ts +++ b/packages/corelib/src/pubsub.ts @@ -37,6 +37,7 @@ import { } from '@sofie-automation/shared-lib/dist/core/model/Ids' import { BlueprintId, BucketId, RundownPlaylistActivationId, SegmentId, ShowStyleVariantId } from './dataModel/Ids' import { PackageInfoDB } from './dataModel/PackageInfos' +import { Bucket } from './dataModel/Bucket' /** * Ids of possible DDP subscriptions for any the UI and gateways accessing the Rundown & RundownPlaylist model. @@ -135,12 +136,16 @@ export enum CorelibPubSub { packageContainerStatuses = 'packageContainerStatuses', /** - * Fetch all bucket adlib pieces for the specified Studio and Bucket. + * Fetch either all buckets for the given Studio, or the Bucket specified. + */ + buckets = 'buckets', + /** + * Fetch all bucket adlib pieces for the specified Studio and Bucket (or all buckets in a Studio). * The result will be limited to ones valid to the ShowStyleVariants specified, as well as ones marked as valid in any ShowStyleVariant */ bucketAdLibPieces = 'bucketAdLibPieces', /** - * Fetch all bucket adlib action for the specified Studio and Bucket. + * Fetch all bucket adlib action for the specified Studio and Bucket (or all buckets in a Studio). * The result will be limited to ones valid to the ShowStyleVariants specified, as well as ones marked as valid in any ShowStyleVariant */ bucketAdLibActions = 'bucketAdLibActions', @@ -297,14 +302,15 @@ export interface CorelibPubSubTypes { token?: string ) => CollectionName.Studios [CorelibPubSub.timelineDatastore]: (studioId: StudioId, token?: string) => CollectionName.TimelineDatastore + [CorelibPubSub.buckets]: (studioId: StudioId, bucketId: BucketId | null, token?: string) => CollectionName.Buckets [CorelibPubSub.bucketAdLibPieces]: ( studioId: StudioId, - bucketId: BucketId, + bucketId: BucketId | null, showStyleVariantIds: ShowStyleVariantId[] ) => CollectionName.BucketAdLibPieces [CorelibPubSub.bucketAdLibActions]: ( studioId: StudioId, - bucketId: BucketId, + bucketId: BucketId | null, showStyleVariantIds: ShowStyleVariantId[] ) => CollectionName.BucketAdLibActions [CorelibPubSub.expectedPackages]: (studioIds: StudioId[], token?: string) => CollectionName.ExpectedPackages @@ -323,6 +329,7 @@ export type CorelibPubSubCollections = { [CollectionName.AdLibActions]: AdLibAction [CollectionName.AdLibPieces]: AdLibPiece [CollectionName.Blueprints]: Blueprint + [CollectionName.Buckets]: Bucket [CollectionName.BucketAdLibActions]: BucketAdLibAction [CollectionName.BucketAdLibPieces]: BucketAdLib [CollectionName.ExpectedMediaItems]: ExpectedMediaItem diff --git a/packages/documentation/docs/for-developers/data-model.md b/packages/documentation/docs/for-developers/data-model.md index 975f29747c..5c6496dc34 100644 --- a/packages/documentation/docs/for-developers/data-model.md +++ b/packages/documentation/docs/for-developers/data-model.md @@ -24,7 +24,7 @@ Currently, there is not a very clearly defined flow for modifying these document This includes: - [Blueprints](https://github.com/nrkno/sofie-core/blob/master/packages/corelib/src/dataModel/Blueprint.ts) -- [Buckets](https://github.com/nrkno/sofie-core/blob/master/meteor/lib/collections/Buckets.ts) +- [Buckets](https://github.com/nrkno/sofie-core/blob/master/packages/corelib/src/dataModel/Bucket.ts) - [CoreSystem](https://github.com/nrkno/sofie-core/blob/master/meteor/lib/collections/CoreSystem.ts) - [Evaluations](https://github.com/nrkno/sofie-core/blob/master/meteor/lib/collections/Evaluations.ts) - [ExternalMessageQueue](https://github.com/nrkno/sofie-core/blob/master/packages/corelib/src/dataModel/ExternalMessageQueue.ts) diff --git a/packages/documentation/versioned_docs/version-1.50.0/for-developers/data-model.md b/packages/documentation/versioned_docs/version-1.50.0/for-developers/data-model.md index 975f29747c..5c6496dc34 100644 --- a/packages/documentation/versioned_docs/version-1.50.0/for-developers/data-model.md +++ b/packages/documentation/versioned_docs/version-1.50.0/for-developers/data-model.md @@ -24,7 +24,7 @@ Currently, there is not a very clearly defined flow for modifying these document This includes: - [Blueprints](https://github.com/nrkno/sofie-core/blob/master/packages/corelib/src/dataModel/Blueprint.ts) -- [Buckets](https://github.com/nrkno/sofie-core/blob/master/meteor/lib/collections/Buckets.ts) +- [Buckets](https://github.com/nrkno/sofie-core/blob/master/packages/corelib/src/dataModel/Bucket.ts) - [CoreSystem](https://github.com/nrkno/sofie-core/blob/master/meteor/lib/collections/CoreSystem.ts) - [Evaluations](https://github.com/nrkno/sofie-core/blob/master/meteor/lib/collections/Evaluations.ts) - [ExternalMessageQueue](https://github.com/nrkno/sofie-core/blob/master/packages/corelib/src/dataModel/ExternalMessageQueue.ts) diff --git a/packages/live-status-gateway/api/asyncapi.yaml b/packages/live-status-gateway/api/asyncapi.yaml index 52dd541133..447bfc93b2 100644 --- a/packages/live-status-gateway/api/asyncapi.yaml +++ b/packages/live-status-gateway/api/asyncapi.yaml @@ -48,6 +48,7 @@ channels: - $ref: '#/components/messages/activePieces' - $ref: '#/components/messages/segments' - $ref: '#/components/messages/adLibs' + - $ref: '#/components/messages/buckets' components: messages: ping: @@ -124,3 +125,8 @@ components: description: AdLibs in active Playlist payload: $ref: './schemas/adLibs.yaml#/$defs/adLibs' + buckets: + name: buckets + description: Buckets in Studio + payload: + $ref: './schemas/buckets.yaml#/$defs/buckets' diff --git a/packages/live-status-gateway/api/schemas/buckets.yaml b/packages/live-status-gateway/api/schemas/buckets.yaml new file mode 100644 index 0000000000..cfc81db99e --- /dev/null +++ b/packages/live-status-gateway/api/schemas/buckets.yaml @@ -0,0 +1,39 @@ +title: Buckets +description: Buckets schema for websocket subscriptions +$defs: + buckets: + type: object + properties: + event: + type: string + const: buckets + buckets: + description: Buckets available in the Studio + type: array + items: + $ref: '#/$defs/bucket' + required: [event, buckets] + additionalProperties: false + examples: + - event: buckets + buckets: + $ref: '#/$defs/bucket/examples' + bucket: + type: object + properties: + id: + description: Unique id of the bucket + type: string + name: + description: The user defined bucket name + type: string + adLibs: + description: The AdLibs in this bucket + type: array + items: + $ref: './adLibs.yaml#/$defs/adLib' + examples: + - id: 'C6K_yIMuGFUk8X_L9A9_jRT6aq4_' + name: My Bucket + adLibs: + $ref: './adLibs.yaml#/$defs/adLib/examples' diff --git a/packages/live-status-gateway/src/collections/bucketAdLibActionsHandler.ts b/packages/live-status-gateway/src/collections/bucketAdLibActionsHandler.ts new file mode 100644 index 0000000000..3dd02c95b8 --- /dev/null +++ b/packages/live-status-gateway/src/collections/bucketAdLibActionsHandler.ts @@ -0,0 +1,31 @@ +import { Logger } from 'winston' +import { CoreHandler } from '../coreHandler' +import { Collection, PublicationCollection } from '../wsHandler' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { CollectionHandlers } from '../liveStatusServer' + +export class BucketAdLibActionsHandler + extends PublicationCollection< + BucketAdLibAction[], + CorelibPubSub.bucketAdLibActions, + CollectionName.BucketAdLibActions + > + implements Collection +{ + constructor(logger: Logger, coreHandler: CoreHandler) { + super(CollectionName.BucketAdLibActions, CorelibPubSub.bucketAdLibActions, logger, coreHandler) + } + + changed(): void { + const collection = this.getCollectionOrFail() + this._collectionData = collection.find(undefined) + this.notify(this._collectionData) + } + + init(handlers: CollectionHandlers): void { + super.init(handlers) + this.setupSubscription(this._studioId, null, []) // This only matches adLibs avilable to all variants + } +} diff --git a/packages/live-status-gateway/src/collections/bucketAdLibsHandler.ts b/packages/live-status-gateway/src/collections/bucketAdLibsHandler.ts new file mode 100644 index 0000000000..b6edf80aa8 --- /dev/null +++ b/packages/live-status-gateway/src/collections/bucketAdLibsHandler.ts @@ -0,0 +1,27 @@ +import { Logger } from 'winston' +import { CoreHandler } from '../coreHandler' +import { Collection, PublicationCollection } from '../wsHandler' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { CollectionHandlers } from '../liveStatusServer' + +export class BucketAdLibsHandler + extends PublicationCollection + implements Collection +{ + constructor(logger: Logger, coreHandler: CoreHandler) { + super(CollectionName.BucketAdLibPieces, CorelibPubSub.bucketAdLibPieces, logger, coreHandler) + } + + changed(): void { + const collection = this.getCollectionOrFail() + this._collectionData = collection.find(undefined) + this.notify(this._collectionData) + } + + init(handlers: CollectionHandlers): void { + super.init(handlers) + this.setupSubscription(this._studioId, null, []) // This only matches adLibs avilable to all variants + } +} diff --git a/packages/live-status-gateway/src/collections/bucketsHandler.ts b/packages/live-status-gateway/src/collections/bucketsHandler.ts new file mode 100644 index 0000000000..3277941d34 --- /dev/null +++ b/packages/live-status-gateway/src/collections/bucketsHandler.ts @@ -0,0 +1,27 @@ +import { Logger } from 'winston' +import { CoreHandler } from '../coreHandler' +import { Collection, PublicationCollection } from '../wsHandler' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' +import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { CollectionHandlers } from '../liveStatusServer' + +export class BucketsHandler + extends PublicationCollection + implements Collection +{ + constructor(logger: Logger, coreHandler: CoreHandler) { + super(CollectionName.Buckets, CorelibPubSub.buckets, logger, coreHandler) + } + + changed(): void { + const collection = this.getCollectionOrFail() + this._collectionData = collection.find(undefined) + this.notify(this._collectionData) + } + + init(handlers: CollectionHandlers): void { + super.init(handlers) + this.setupSubscription(this._studioId, null) + } +} diff --git a/packages/live-status-gateway/src/liveStatusServer.ts b/packages/live-status-gateway/src/liveStatusServer.ts index 7f0094993e..22e3557a94 100644 --- a/packages/live-status-gateway/src/liveStatusServer.ts +++ b/packages/live-status-gateway/src/liveStatusServer.ts @@ -23,6 +23,10 @@ import { PartsHandler } from './collections/partsHandler' import { PieceInstancesHandler } from './collections/pieceInstancesHandler' import { AdLibsTopic } from './topics/adLibsTopic' import { ActivePiecesTopic } from './topics/activePiecesTopic' +import { BucketsHandler } from './collections/bucketsHandler' +import { BucketAdLibsHandler } from './collections/bucketAdLibsHandler' +import { BucketAdLibActionsHandler } from './collections/bucketAdLibActionsHandler' +import { BucketsTopic } from './topics/bucketsTopic' export interface CollectionHandlers { studioHandler: StudioHandler @@ -40,6 +44,9 @@ export interface CollectionHandlers { adLibsHandler: AdLibsHandler globalAdLibActionsHandler: GlobalAdLibActionsHandler globalAdLibsHandler: GlobalAdLibsHandler + bucketsHandler: BucketsHandler + bucketAdLibsHandler: BucketAdLibsHandler + bucketAdLibActionsHandler: BucketAdLibActionsHandler } export class LiveStatusServer { @@ -72,6 +79,9 @@ export class LiveStatusServer { const adLibsHandler = new AdLibsHandler(this._logger, this._coreHandler) const globalAdLibActionsHandler = new GlobalAdLibActionsHandler(this._logger, this._coreHandler) const globalAdLibsHandler = new GlobalAdLibsHandler(this._logger, this._coreHandler) + const bucketsHandler = new BucketsHandler(this._logger, this._coreHandler) + const bucketAdLibsHandler = new BucketAdLibsHandler(this._logger, this._coreHandler) + const bucketAdLibActionsHandler = new BucketAdLibActionsHandler(this._logger, this._coreHandler) const handlers: CollectionHandlers = { studioHandler, @@ -89,6 +99,9 @@ export class LiveStatusServer { adLibsHandler, globalAdLibActionsHandler, globalAdLibsHandler, + bucketsHandler, + bucketAdLibsHandler, + bucketAdLibActionsHandler, } for (const handlerName in handlers) { @@ -100,12 +113,14 @@ export class LiveStatusServer { const activePlaylistTopic = new ActivePlaylistTopic(this._logger, handlers) const segmentsTopic = new SegmentsTopic(this._logger, handlers) const adLibsTopic = new AdLibsTopic(this._logger, handlers) + const bucketsTopic = new BucketsTopic(this._logger, handlers) rootChannel.addTopic(StatusChannels.studio, studioTopic) rootChannel.addTopic(StatusChannels.activePlaylist, activePlaylistTopic) rootChannel.addTopic(StatusChannels.activePieces, activePiecesTopic) rootChannel.addTopic(StatusChannels.segments, segmentsTopic) rootChannel.addTopic(StatusChannels.adLibs, adLibsTopic) + rootChannel.addTopic(StatusChannels.buckets, bucketsTopic) const wss = new WebSocketServer({ port: 8080 }) wss.on('connection', (ws, request) => { diff --git a/packages/live-status-gateway/src/topics/__tests__/bucketsTopic.spec.ts b/packages/live-status-gateway/src/topics/__tests__/bucketsTopic.spec.ts new file mode 100644 index 0000000000..0a041a62b4 --- /dev/null +++ b/packages/live-status-gateway/src/topics/__tests__/bucketsTopic.spec.ts @@ -0,0 +1,144 @@ +import { protectString } from '@sofie-automation/server-core-integration' +import { + makeMockHandlers, + makeMockLogger, + makeMockSubscriber, + makeTestParts, + makeTestPlaylist, + makeTestShowStyleBase, +} from './utils' +import { ShowStyleBaseExt } from '../../collections/showStyleBaseHandler' +import { BucketsStatus, BucketsTopic } from '../bucketsTopic' +import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' +import { RundownImportVersions } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { BucketAdLib, BucketAdLibIngestInfo } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' +import { PieceLifespan } from '@sofie-automation/blueprints-integration' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' + +describe('BucketsTopic', () => { + it('notifies subscribers', async () => { + const handlers = makeMockHandlers() + const topic = new BucketsTopic(makeMockLogger(), handlers) + const mockSubscriber = makeMockSubscriber() + + const playlist = makeTestPlaylist() + playlist.activationId = protectString('somethingRandom') + handlers.playlistHandler.notify(playlist) + + const parts = makeTestParts() + handlers.partsHandler.notify(parts) + + const testShowStyleBase = makeTestShowStyleBase() + handlers.showStyleBaseHandler.notify(testShowStyleBase as ShowStyleBaseExt) + + const testBuckets = makeTestBuckets() + handlers.bucketsHandler.notify(testBuckets) + + const testAdLibActions = makeTestAdLibActions() + handlers.bucketAdLibActionsHandler.notify(testAdLibActions) + + const testGlobalAdLibActions = makeTestAdLibs() + handlers.bucketAdLibsHandler.notify(testGlobalAdLibActions) + + topic.addSubscriber(mockSubscriber) + + const expectedStatus: BucketsStatus = { + event: 'buckets', + buckets: [ + { + id: 'BUCKET_0', + name: 'A Bucket', + adLibs: [ + { + actionType: [], + id: 'ADLIB_0', + name: 'Bucket AdLib', + outputLayer: 'PGM', + sourceLayer: 'Layer 0', + tags: ['adlib_tag'], + publicData: { c: 'd' }, + externalId: 'BUCKET_ADLIB_0', + }, + { + actionType: [], + id: 'ACTION_0', + name: 'Bucket Action', + outputLayer: 'PGM', + sourceLayer: 'Layer 0', + tags: ['adlib_action_tag'], + publicData: { a: 'b' }, + externalId: 'BUCKET_ACTION_0', + }, + ], + }, + ], + } + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockSubscriber.send).toHaveBeenCalledTimes(1) + expect(JSON.parse(mockSubscriber.send.mock.calls[0][0] as string)).toMatchObject(expectedStatus) + }) +}) + +function makeTestAdLibActions(): BucketAdLibAction[] { + return [ + { + _id: protectString('ACTION_0'), + actionId: 'ACTION_0', + bucketId: protectString('BUCKET_0'), + studioId: protectString('STUDIO_0'), + importVersions: {} as RundownImportVersions, + ingestInfo: {} as BucketAdLibIngestInfo, + showStyleBaseId: protectString('SHOWSTYLE_0'), + showStyleVariantId: null, + display: { + content: {}, + label: { key: 'Bucket Action' }, + sourceLayerId: 'layer0', + outputLayerId: 'pgm', + tags: ['adlib_action_tag'], + }, + externalId: 'BUCKET_ACTION_0', + userData: {}, + userDataManifest: {}, + publicData: { a: 'b' }, + }, + ] +} + +function makeTestAdLibs(): BucketAdLib[] { + return [ + { + _id: protectString('ADLIB_0'), + bucketId: protectString('BUCKET_0'), + studioId: protectString('STUDIO_0'), + importVersions: {} as RundownImportVersions, + ingestInfo: {} as BucketAdLibIngestInfo, + showStyleBaseId: protectString('SHOWSTYLE_0'), + showStyleVariantId: null, + externalId: 'BUCKET_ADLIB_0', + _rank: 0, + content: {}, + lifespan: PieceLifespan.WithinPart, + name: 'Bucket AdLib', + outputLayerId: 'pgm', + sourceLayerId: 'layer0', + publicData: { c: 'd' }, + timelineObjectsString: protectString(''), + tags: ['adlib_tag'], + }, + ] +} + +function makeTestBuckets(): Bucket[] { + return [ + { + _id: protectString('BUCKET_0'), + studioId: protectString('STUDIO_0'), + _rank: 0, + name: 'A Bucket', + buttonHeightScale: 1, + buttonWidthScale: 1, + }, + ] +} diff --git a/packages/live-status-gateway/src/topics/adLibsTopic.ts b/packages/live-status-gateway/src/topics/adLibsTopic.ts index cfcd40644d..3140524feb 100644 --- a/packages/live-status-gateway/src/topics/adLibsTopic.ts +++ b/packages/live-status-gateway/src/topics/adLibsTopic.ts @@ -26,12 +26,12 @@ export interface AdLibsStatus { globalAdLibs: GlobalAdLibStatus[] } -interface AdLibActionType { +export interface AdLibActionType { name: string label: string } -interface AdLibStatus extends AdLibStatusBase { +export interface AdLibStatus extends AdLibStatusBase { segmentId: string partId: string } diff --git a/packages/live-status-gateway/src/topics/bucketsTopic.ts b/packages/live-status-gateway/src/topics/bucketsTopic.ts new file mode 100644 index 0000000000..c501b30602 --- /dev/null +++ b/packages/live-status-gateway/src/topics/bucketsTopic.ts @@ -0,0 +1,136 @@ +import { Logger } from 'winston' +import { WebSocket } from 'ws' +import { WebSocketTopicBase, WebSocketTopic, PickArr } from '../wsHandler' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import _ = require('underscore') +import { IBlueprintActionManifestDisplayContent } from '@sofie-automation/blueprints-integration' +import { ShowStyleBaseExt } from '../collections/showStyleBaseHandler' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' +import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' +import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' +import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { AdLibActionType, AdLibStatus } from './adLibsTopic' +import { CollectionHandlers } from '../liveStatusServer' + +const THROTTLE_PERIOD_MS = 100 + +export interface BucketsStatus { + event: 'buckets' + buckets: BucketStatus[] +} + +interface BucketAdLibStatus extends Omit { + externalId: string +} + +export interface BucketStatus { + id: string + name: string + adLibs: BucketAdLibStatus[] +} + +const SHOW_STYLE_BASE_KEYS = ['sourceLayerNamesById', 'outputLayerNamesById'] as const +type ShowStyle = PickArr + +export class BucketsTopic extends WebSocketTopicBase implements WebSocketTopic { + private _buckets: Bucket[] = [] + private _adLibActionsByBucket: Record | undefined + private _adLibsByBucket: Record | undefined + private _sourceLayersMap: ReadonlyMap = new Map() + private _outputLayersMap: ReadonlyMap = new Map() + + constructor(logger: Logger, handlers: CollectionHandlers) { + super(BucketsTopic.name, logger, THROTTLE_PERIOD_MS) + + handlers.bucketsHandler.subscribe(this.onBucketsUpdate) + handlers.bucketAdLibActionsHandler.subscribe(this.onBucketAdLibActionsUpdate) + handlers.bucketAdLibsHandler.subscribe(this.onBucketAdLibsUpdate) + handlers.showStyleBaseHandler.subscribe(this.onShowStyleBaseUpdate) + } + + sendStatus(subscribers: Iterable): void { + const buckets: BucketStatus[] = this._buckets.map((bucket) => { + const bucketId = unprotectString(bucket._id) + const bucketAdLibs = (this._adLibsByBucket?.[bucketId] ?? []).map((adLib) => { + const sourceLayerName = this._sourceLayersMap.get(adLib.sourceLayerId) + const outputLayerName = this._outputLayersMap.get(adLib.outputLayerId) + return literal({ + id: unprotectString(adLib._id), + name: adLib.name, + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: [], + tags: adLib.tags, + externalId: adLib.externalId, + publicData: adLib.publicData, + }) + }) + const bucketAdLibActions = (this._adLibActionsByBucket?.[bucketId] ?? []).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: interpollateTranslation(t.display.label.key, t.display.label.args), + }) + ) + : [] + return literal({ + id: unprotectString(action._id), + name: interpollateTranslation(action.display.label.key, action.display.label.args), + sourceLayer: sourceLayerName ?? 'invalid', + outputLayer: outputLayerName ?? 'invalid', + actionType: triggerModes, + tags: action.display.tags, + externalId: action.externalId, + publicData: action.publicData, + }) + }) + return { + id: bucketId, + name: bucket.name, + adLibs: [...bucketAdLibs, ...bucketAdLibActions], + } + }) + + const bucketsStatus: BucketsStatus = { + event: 'buckets', + buckets: buckets, + } + + for (const subscriber of subscribers) { + this.sendMessage(subscriber, bucketsStatus) + } + } + + private onShowStyleBaseUpdate = (showStyleBase: ShowStyle | undefined): void => { + this.logUpdateReceived('showStyleBase') + this._sourceLayersMap = showStyleBase?.sourceLayerNamesById ?? new Map() + this._outputLayersMap = showStyleBase?.outputLayerNamesById ?? new Map() + this.throttledSendStatusToAll() + } + + private onBucketsUpdate = (buckets: Bucket[] | undefined): void => { + this.logUpdateReceived('buckets') + this._buckets = buckets ?? [] + this.throttledSendStatusToAll() + } + + private onBucketAdLibActionsUpdate = (adLibActions: BucketAdLibAction[] | undefined): void => { + this.logUpdateReceived('buketAdLibActions') + this._adLibActionsByBucket = _.groupBy(adLibActions ?? [], 'bucketId') + this.throttledSendStatusToAll() + } + + private onBucketAdLibsUpdate = (adLibs: BucketAdLib[] | undefined): void => { + this.logUpdateReceived('bucketAdLibs') + this._adLibsByBucket = _.groupBy(adLibs ?? [], 'bucketId') + this.throttledSendStatusToAll() + } +} diff --git a/packages/live-status-gateway/src/topics/root.ts b/packages/live-status-gateway/src/topics/root.ts index 0c3f3cdca1..d716bb7cb5 100644 --- a/packages/live-status-gateway/src/topics/root.ts +++ b/packages/live-status-gateway/src/topics/root.ts @@ -37,6 +37,7 @@ export enum StatusChannels { activePieces = 'activePieces', segments = 'segments', adLibs = 'adLibs', + buckets = 'buckets', } interface RootMsg { diff --git a/packages/meteor-lib/src/api/pubsub.ts b/packages/meteor-lib/src/api/pubsub.ts index 9c61409c18..377e806754 100644 --- a/packages/meteor-lib/src/api/pubsub.ts +++ b/packages/meteor-lib/src/api/pubsub.ts @@ -8,7 +8,7 @@ import { ShowStyleBaseId, StudioId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Bucket } from '../collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { ICoreSystem } from '../collections/CoreSystem' import { Evaluation } from '../collections/Evaluations' import { ExpectedPlayoutItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' @@ -102,10 +102,6 @@ export enum MeteorPubSub { * If null is provided, nothing will be returned */ organization = 'organization', - /** - * Fetch either all buckets for the given Studio, or the Bucket specified. - */ - buckets = 'buckets', /** * Fetch all translation bundles */ @@ -218,7 +214,6 @@ export interface MeteorPubSubTypes { token?: string ) => CollectionName.RundownLayouts [MeteorPubSub.organization]: (organizationId: OrganizationId | null, token?: string) => CollectionName.Organizations - [MeteorPubSub.buckets]: (studioId: StudioId, bucketId: BucketId | null, token?: string) => CollectionName.Buckets [MeteorPubSub.translationsBundles]: (token?: string) => CollectionName.TranslationsBundles [MeteorPubSub.notificationsForRundown]: (studioId: StudioId, rundownId: RundownId) => CollectionName.Notifications [MeteorPubSub.notificationsForRundownPlaylist]: ( diff --git a/packages/meteor-lib/src/api/userActions.ts b/packages/meteor-lib/src/api/userActions.ts index 5ff3351320..c67aa1ba80 100644 --- a/packages/meteor-lib/src/api/userActions.ts +++ b/packages/meteor-lib/src/api/userActions.ts @@ -1,6 +1,6 @@ import { ClientAPI } from './client' import { EvaluationBase } from '../collections/Evaluations' -import { Bucket } from '../collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { IngestAdlib, ActionUserData, UserOperationTarget } from '@sofie-automation/blueprints-integration' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { AdLibActionCommon } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' diff --git a/packages/meteor-lib/src/triggers/RundownViewEventBus.ts b/packages/meteor-lib/src/triggers/RundownViewEventBus.ts index 8460e67108..9bda66f384 100644 --- a/packages/meteor-lib/src/triggers/RundownViewEventBus.ts +++ b/packages/meteor-lib/src/triggers/RundownViewEventBus.ts @@ -1,5 +1,4 @@ import * as EventEmitter from 'events' -import { Bucket } from '../collections/Buckets' import { BucketId, PartId, @@ -14,6 +13,7 @@ import type { PieceUi } from '../uiTypes/Piece' import type { ShelfTabs } from '../uiTypes/ShelfTabs' import type { IAdLibListItem } from '../uiTypes/Adlib' import type { BucketAdLibItem } from '../uiTypes/Bucket' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' export enum RundownViewEvents { ACTIVATE_RUNDOWN_PLAYLIST = 'activateRundownPlaylist', diff --git a/packages/webui/src/client/collections/index.ts b/packages/webui/src/client/collections/index.ts index 0a8e5dd5bd..56be7799a5 100644 --- a/packages/webui/src/client/collections/index.ts +++ b/packages/webui/src/client/collections/index.ts @@ -16,7 +16,7 @@ import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataMode import { PackageContainerStatusDB } from '@sofie-automation/corelib/dist/dataModel/PackageContainerStatus' import { MediaWorkFlow } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlows' import { MediaWorkFlowStep } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlowSteps' -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { ICoreSystem, SYSTEM_ID } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { Evaluation } from '@sofie-automation/meteor-lib/dist/collections/Evaluations' import { ExpectedPackageDB } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index 225b06c567..711f5f5d5b 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -91,7 +91,7 @@ import { import { VirtualElement } from '../lib/VirtualElement' import { SEGMENT_TIMELINE_ELEMENT_ID } from './SegmentTimeline/SegmentTimeline' import { NoraPreviewRenderer } from './FloatingInspectors/NoraFloatingInspector' -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { contextMenuHoldToDisplayTime, isEventInInputField } from '../lib/lib' import { OffsetPosition } from '../utils/positions' import { MeteorCall } from '../lib/meteorApi' @@ -1263,7 +1263,7 @@ export function RundownView(props: Readonly): JSX.Element { useSubscriptionIfEnabled(MeteorPubSub.uiStudio, !!playlistStudioId, playlistStudioId ?? protectString('')) ) auxSubsReady.push( - useSubscriptionIfEnabled(MeteorPubSub.buckets, !!playlistStudioId, playlistStudioId ?? protectString(''), null) + useSubscriptionIfEnabled(CorelibPubSub.buckets, !!playlistStudioId, playlistStudioId ?? protectString(''), null) ) const playlistActivationId = useTracker(() => { diff --git a/packages/webui/src/client/ui/Shelf/BucketPanel.tsx b/packages/webui/src/client/ui/Shelf/BucketPanel.tsx index f6246f9513..35fb953c6a 100644 --- a/packages/webui/src/client/ui/Shelf/BucketPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/BucketPanel.tsx @@ -28,7 +28,7 @@ import { literal, unprotectString, protectString } from '../../lib/tempLib' import { contextMenuHoldToDisplayTime, UserAgentPointer, USER_AGENT_POINTER_PROPERTY } from '../../lib/lib' import { IDashboardPanelTrackedProps } from './DashboardPanel' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { Events as MOSEvents } from '../../lib/data/mos/plugin-support' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { MeteorCall } from '../../lib/meteorApi' @@ -273,7 +273,7 @@ interface BucketTargetCollectedProps { export const BucketPanel = React.memo( function BucketPanel(props: Readonly): JSX.Element | null { // Data subscriptions: - useSubscription(MeteorPubSub.buckets, props.playlist.studioId, props.bucket._id) + useSubscription(CorelibPubSub.buckets, props.playlist.studioId, props.bucket._id) useSubscription(MeteorPubSub.uiBucketContentStatuses, props.playlist.studioId, props.bucket._id) useSubscription(MeteorPubSub.uiStudio, props.playlist.studioId) diff --git a/packages/webui/src/client/ui/Shelf/RundownViewBuckets.tsx b/packages/webui/src/client/ui/Shelf/RundownViewBuckets.tsx index b688ef355b..269fffbe6a 100644 --- a/packages/webui/src/client/ui/Shelf/RundownViewBuckets.tsx +++ b/packages/webui/src/client/ui/Shelf/RundownViewBuckets.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { BucketPanel } from './BucketPanel' import { doUserAction, UserAction } from '../../lib/clientUserAction' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' diff --git a/packages/webui/src/client/ui/Shelf/Shelf.tsx b/packages/webui/src/client/ui/Shelf/Shelf.tsx index e342d2c315..3b548e0ddb 100644 --- a/packages/webui/src/client/ui/Shelf/Shelf.tsx +++ b/packages/webui/src/client/ui/Shelf/Shelf.tsx @@ -22,7 +22,7 @@ import { contextMenuHoldToDisplayTime } from '../../lib/lib' import { ErrorBoundary } from '../../lib/ErrorBoundary' import { ShelfRundownLayout } from './ShelfRundownLayout' import { ShelfDashboardLayout } from './ShelfDashboardLayout' -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { RundownViewBuckets, BucketAdLibItem } from './RundownViewBuckets' import { ContextMenuTrigger } from '@jstarpl/react-contextmenu' import { ShelfInspector } from './Inspector/ShelfInspector' diff --git a/packages/webui/src/client/ui/Shelf/ShelfContextMenu.tsx b/packages/webui/src/client/ui/Shelf/ShelfContextMenu.tsx index f9039c0eab..f26a73be71 100644 --- a/packages/webui/src/client/ui/Shelf/ShelfContextMenu.tsx +++ b/packages/webui/src/client/ui/Shelf/ShelfContextMenu.tsx @@ -4,7 +4,7 @@ import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import Escape from './../../lib/Escape' import { ContextMenu, MenuItem } from '@jstarpl/react-contextmenu' import { ReactiveVar } from 'meteor/reactive-var' -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' +import { Bucket } from '@sofie-automation/corelib/dist/dataModel/Bucket' import { BucketAdLibItem, BucketAdLibActionUi } from './RundownViewBuckets' import RundownViewEventBus, { RundownViewEvents } from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' import { IAdLibListItem } from './AdLibListItem'