diff --git a/meteor/lib/collections/ExpectedPackages.ts b/meteor/lib/collections/ExpectedPackages.ts index cfffad7842..280bcaffd6 100644 --- a/meteor/lib/collections/ExpectedPackages.ts +++ b/meteor/lib/collections/ExpectedPackages.ts @@ -6,9 +6,10 @@ import { htmlTemplateGetFileNamesFromSteps, } from '@sofie-automation/shared-lib/dist/package-manager/helpers' import deepExtend from 'deep-extend' +import { ReadonlyDeep } from 'type-fest' export function getPreviewPackageSettings( - expectedPackage: ExpectedPackage.Any + expectedPackage: ReadonlyDeep ): ExpectedPackage.SideEffectPreviewSettings | undefined { if (expectedPackage.type === ExpectedPackage.PackageType.MEDIA_FILE) { const packagePath = expectedPackage.content.filePath @@ -29,7 +30,7 @@ export function getPreviewPackageSettings( } } export function getThumbnailPackageSettings( - expectedPackage: ExpectedPackage.Any + expectedPackage: ReadonlyDeep ): ExpectedPackage.SideEffectThumbnailSettings | undefined { if (expectedPackage.type === ExpectedPackage.PackageType.MEDIA_FILE) { const packagePath = expectedPackage.content.filePath @@ -51,7 +52,7 @@ export function getThumbnailPackageSettings( } } export function getSideEffect( - expectedPackage: ExpectedPackage.Base, + expectedPackage: ReadonlyDeep, studio: Pick ): ExpectedPackage.Base['sideEffect'] { return deepExtend( @@ -59,8 +60,8 @@ export function getSideEffect( literal({ previewContainerId: studio.previewContainerIds[0], // just pick the first. Todo: something else? thumbnailContainerId: studio.thumbnailContainerIds[0], // just pick the first. Todo: something else? - previewPackageSettings: getPreviewPackageSettings(expectedPackage as ExpectedPackage.Any), - thumbnailPackageSettings: getThumbnailPackageSettings(expectedPackage as ExpectedPackage.Any), + previewPackageSettings: getPreviewPackageSettings(expectedPackage as ReadonlyDeep), + thumbnailPackageSettings: getThumbnailPackageSettings(expectedPackage as ReadonlyDeep), }), expectedPackage.sideEffect ) diff --git a/meteor/server/api/__tests__/cleanup.test.ts b/meteor/server/api/__tests__/cleanup.test.ts index b9f2f34ec4..1f5e9882bf 100644 --- a/meteor/server/api/__tests__/cleanup.test.ts +++ b/meteor/server/api/__tests__/cleanup.test.ts @@ -256,19 +256,14 @@ async function setDefaultDatatoDB(env: DefaultEnvironment, now: number) { _id: getRandomId(), blueprintPackageId: '', bucketId, - content: {} as any, + package: {} as any, contentVersionHash: '', created: 0, fromPieceType: '' as any, - layers: [], pieceId, rundownId, segmentId, - sideEffect: {} as any, studioId, - sources: {} as any, - type: '' as any, - version: {} as any, }) await ExpectedPackageWorkStatuses.insertAsync({ _id: getRandomId(), diff --git a/meteor/server/api/ingest/packageInfo.ts b/meteor/server/api/ingest/packageInfo.ts index 44452774e0..5223d6ab85 100644 --- a/meteor/server/api/ingest/packageInfo.ts +++ b/meteor/server/api/ingest/packageInfo.ts @@ -27,7 +27,7 @@ export async function onUpdatedPackageInfo(packageId: ExpectedPackageId, _doc: P return } - if (pkg.listenToPackageInfoUpdates) { + if (pkg.package.listenToPackageInfoUpdates) { switch (pkg.fromPieceType) { case ExpectedPackageDBType.PIECE: case ExpectedPackageDBType.ADLIB_PIECE: @@ -44,6 +44,9 @@ export async function onUpdatedPackageInfo(packageId: ExpectedPackageId, _doc: P case ExpectedPackageDBType.STUDIO_BASELINE_OBJECTS: onUpdatedPackageInfoForStudioBaselineDebounce(pkg) break + case ExpectedPackageDBType.PIECE_INSTANCE: + // No-op, we can't handle these updates + break default: assertNever(pkg) break diff --git a/meteor/server/publications/packageManager/expectedPackages/generate.ts b/meteor/server/publications/packageManager/expectedPackages/generate.ts index 7d2194eca5..324d324a27 100644 --- a/meteor/server/publications/packageManager/expectedPackages/generate.ts +++ b/meteor/server/publications/packageManager/expectedPackages/generate.ts @@ -47,7 +47,7 @@ export async function updateCollectionForExpectedPackageIds( // Map the expectedPackages onto their specified layer: const allDeviceIds = new Set() - for (const layerName of packageDoc.layers) { + for (const layerName of packageDoc.package.layers) { const layerDeviceIds = layerNameToDeviceIds.get(layerName) for (const deviceId of layerDeviceIds || []) { allDeviceIds.add(deviceId) @@ -61,8 +61,9 @@ export async function updateCollectionForExpectedPackageIds( const routedPackage = generateExpectedPackageForDevice( studio, { - ...packageDoc, + ...packageDoc.package, _id: unprotectString(packageDoc._id), + rundownId: 'rundownId' in packageDoc ? packageDoc.rundownId : undefined, }, deviceId, null, @@ -207,11 +208,13 @@ function generateExpectedPackageForDevice( if (!combinedTargets.length) { logger.warn(`Pub.expectedPackagesForDevice: No targets found for "${expectedPackage._id}"`) } - expectedPackage.sideEffect = getSideEffect(expectedPackage, studio) return { _id: protectString(`${expectedPackage._id}_${deviceId}_${pieceInstanceId}`), - expectedPackage: expectedPackage, + expectedPackage: { + ...expectedPackage, + sideEffect: getSideEffect(expectedPackage, studio), + }, sources: combinedSources, targets: combinedTargets, priority: priority, @@ -239,7 +242,7 @@ function calculateCombinedSource( for (const accessorId of accessorIds) { const sourceAccessor: Accessor.Any | undefined = lookedUpSource.container.accessors[accessorId] - const packageAccessor: AccessorOnPackage.Any | undefined = packageSource.accessors?.[accessorId] + const packageAccessor: ReadonlyDeep | undefined = packageSource.accessors?.[accessorId] if (packageAccessor && sourceAccessor && packageAccessor.type === sourceAccessor.type) { combinedSource.accessors[accessorId] = deepExtend({}, sourceAccessor, packageAccessor) diff --git a/packages/corelib/src/dataModel/AdLibPiece.ts b/packages/corelib/src/dataModel/AdLibPiece.ts index 1c21e5a61a..6194a7f53e 100644 --- a/packages/corelib/src/dataModel/AdLibPiece.ts +++ b/packages/corelib/src/dataModel/AdLibPiece.ts @@ -2,7 +2,7 @@ import { IBlueprintAdLibPiece, SomeContent } from '@sofie-automation/blueprints- import { RundownId, PartId } from './Ids' import { PieceGeneric } from './Piece' -export interface AdLibPiece extends PieceGeneric, Omit { +export interface AdLibPiece extends PieceGeneric, Omit { /** Rundown this AdLib belongs to */ rundownId: RundownId diff --git a/packages/corelib/src/dataModel/AdlibAction.ts b/packages/corelib/src/dataModel/AdlibAction.ts index 86a05c9734..ab93b3d2aa 100644 --- a/packages/corelib/src/dataModel/AdlibAction.ts +++ b/packages/corelib/src/dataModel/AdlibAction.ts @@ -1,13 +1,13 @@ import { IBlueprintActionManifest } from '@sofie-automation/blueprints-integration' import { ArrayElement } from '../lib' import { ITranslatableMessage } from '../TranslatableMessage' -import { ProtectedStringProperties } from '../protectedString' import { RundownId, AdLibActionId, PartId } from './Ids' +import { PieceExpectedPackage } from './Piece' /** The following extended interface allows assigning namespace information to the actions as they are stored in the * database after being emitted from the blueprints */ -export interface AdLibActionCommon extends ProtectedStringProperties { +export interface AdLibActionCommon extends Omit { rundownId: RundownId display: IBlueprintActionManifest['display'] & { // this property can be a string if the name is modified by the User @@ -22,6 +22,8 @@ export interface AdLibActionCommon extends ProtectedStringProperties { +export interface BucketAdLibAction extends Omit { _id: BucketAdLibActionId bucketId: BucketId + // nocommit - temporary copy to avoid type errors + expectedPackages: IBlueprintActionManifest['expectedPackages'] + externalId: string /** diff --git a/packages/corelib/src/dataModel/ExpectedPackages.ts b/packages/corelib/src/dataModel/ExpectedPackages.ts index 404fd2bfdd..7ea9589124 100644 --- a/packages/corelib/src/dataModel/ExpectedPackages.ts +++ b/packages/corelib/src/dataModel/ExpectedPackages.ts @@ -5,7 +5,6 @@ import { AdLibActionId, BucketAdLibActionId, BucketAdLibId, - BucketId, ExpectedPackageId, PartId, PieceId, @@ -26,20 +25,13 @@ import { ReadonlyDeep } from 'type-fest' The Package Manager will then copy the file to the right place. */ -export type ExpectedPackageFromRundown = ExpectedPackageDBFromPiece | ExpectedPackageDBFromAdLibAction - -export type ExpectedPackageFromRundownBaseline = - | ExpectedPackageDBFromBaselineAdLibAction - | ExpectedPackageDBFromBaselineAdLibPiece - | ExpectedPackageDBFromRundownBaselineObjects - export type ExpectedPackageDBFromBucket = ExpectedPackageDBFromBucketAdLib | ExpectedPackageDBFromBucketAdLibAction -export type ExpectedPackageDB = - | ExpectedPackageFromRundown - | ExpectedPackageDBFromBucket - | ExpectedPackageFromRundownBaseline - | ExpectedPackageDBFromStudioBaselineObjects +// export type ExpectedPackageDB = +// | ExpectedPackageFromRundown +// | ExpectedPackageDBFromBucket +// | ExpectedPackageFromRundownBaseline +// | ExpectedPackageDBFromStudioBaselineObjects export enum ExpectedPackageDBType { PIECE = 'piece', @@ -51,24 +43,50 @@ export enum ExpectedPackageDBType { BUCKET_ADLIB_ACTION = 'bucket_adlib_action', RUNDOWN_BASELINE_OBJECTS = 'rundown_baseline_objects', STUDIO_BASELINE_OBJECTS = 'studio_baseline_objects', + PIECE_INSTANCE = 'piece_instance', } -export interface ExpectedPackageDBBase extends Omit { - _id: ExpectedPackageId - /** The local package id - as given by the blueprints */ - blueprintPackageId: string + +/* + * What about this new concept. The aim here is to avoid the constant inserting and deleting of expectedPackages during playout, and avoiding duplicate packages with the same content. + * The idea is to have a single expectedPackage for each 'content'. + * Ingest will 'deduplicate' the packages produced by the blueprints, with playout able to reference them with pieceInstanceIds. + * + * During the ingest save phase, it will need to reload the `playoutSources` property, in case it has changed. And if there are uses remaining, it will need to keep the package after clearing the `ingestSources`. + * During playout operations, pieceInstanceIds will be added and removed as needed. If there remains no sources (of either type), then the document can be removed. If an in-progress ingest tried to reclaim it, it will get reinserted. + * + * Playout can then load just the ones referenced by piece instances, and just before it needs to use them (for bluerpint types or something), can ensure that everything needed has been loaded. + * During a take, any packages referenced by the previous(?) partinstance must be removed. + * When doing a reset of the rundown, all playout references must be removed. + * When inserting/removing pieceinstances, the expectedPackages must be updated. + */ +export interface ExpectedPackageDBNew { + _id: ExpectedPackageId // derived from rundownId and hash of `package` + + // /** The local package id - as given by the blueprints */ + // blueprintPackageId: string // TODO - remove this? /** The studio of the Rundown of the Piece this package belongs to */ studioId: StudioId + /** The rundown of the Piece this package belongs to */ + rundownId: RundownId + /** Hash that changes whenever the content or version changes. See getContentVersionHash() */ contentVersionHash: string - // pieceId: ProtectedString | null - fromPieceType: ExpectedPackageDBType - created: Time + + package: ReadonlyDeep + + ingestSources: ExpectedPackageIngestSource[] + + playoutSources: { + /** Any playout PieceInstance. This is limited to the current and next partInstances */ // nocommit - verify this + pieceInstanceIds: PieceInstanceId[] + } } -export interface ExpectedPackageDBFromPiece extends ExpectedPackageDBBase { + +export interface ExpectedPackageIngestSourcePiece { fromPieceType: ExpectedPackageDBType.PIECE | ExpectedPackageDBType.ADLIB_PIECE /** The Piece this package belongs to */ pieceId: PieceId @@ -76,64 +94,60 @@ export interface ExpectedPackageDBFromPiece extends ExpectedPackageDBBase { partId: PartId /** The Segment this package belongs to */ segmentId: SegmentId - /** The rundown of the Piece this package belongs to */ - rundownId: RundownId -} - -export interface ExpectedPackageDBFromBaselineAdLibPiece extends ExpectedPackageDBBase { - fromPieceType: ExpectedPackageDBType.BASELINE_ADLIB_PIECE - /** The Piece this package belongs to */ - pieceId: PieceId - /** The rundown of the Piece this package belongs to */ - rundownId: RundownId } - -export interface ExpectedPackageDBFromAdLibAction extends ExpectedPackageDBBase { +export interface ExpectedPackageIngestSourceAdlibAction { fromPieceType: ExpectedPackageDBType.ADLIB_ACTION - /** The Adlib Action this package belongs to */ + /** The Piece this package belongs to */ pieceId: AdLibActionId /** The Part this package belongs to */ partId: PartId /** The Segment this package belongs to */ segmentId: SegmentId - /** The rundown of the Piece this package belongs to */ - rundownId: RundownId } -export interface ExpectedPackageDBFromBaselineAdLibAction extends ExpectedPackageDBBase { +export interface ExpectedPackageIngestSourceBaselineAdlibPiece { + fromPieceType: ExpectedPackageDBType.BASELINE_ADLIB_PIECE + /** The Piece this package belongs to */ + pieceId: PieceId +} +export interface ExpectedPackageIngestSourceBaselineAdlibAction { fromPieceType: ExpectedPackageDBType.BASELINE_ADLIB_ACTION /** The Piece this package belongs to */ pieceId: RundownBaselineAdLibActionId - /** The rundown of the Piece this package belongs to */ - rundownId: RundownId } - -export interface ExpectedPackageDBFromRundownBaselineObjects extends ExpectedPackageDBBase { +export interface ExpectedPackageIngestSourceBaselineObjects { fromPieceType: ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS - /** The rundown of the Piece this package belongs to */ - rundownId: RundownId - pieceId: null -} -export interface ExpectedPackageDBFromStudioBaselineObjects extends ExpectedPackageDBBase { - fromPieceType: ExpectedPackageDBType.STUDIO_BASELINE_OBJECTS - pieceId: null } -export interface ExpectedPackageDBFromBucketAdLib extends ExpectedPackageDBBase { - fromPieceType: ExpectedPackageDBType.BUCKET_ADLIB - bucketId: BucketId - /** The Bucket adlib this package belongs to */ - pieceId: BucketAdLibId - /** The `externalId` of the Bucket adlib this package belongs to */ - pieceExternalId: string -} -export interface ExpectedPackageDBFromBucketAdLibAction extends ExpectedPackageDBBase { - fromPieceType: ExpectedPackageDBType.BUCKET_ADLIB_ACTION - bucketId: BucketId - /** The Bucket adlib-action this package belongs to */ - pieceId: BucketAdLibActionId - /** The `externalId` of the Bucket adlib-action this package belongs to */ - pieceExternalId: string -} +export type ExpectedPackageIngestSourcePart = ExpectedPackageIngestSourcePiece | ExpectedPackageIngestSourceAdlibAction + +export type ExpectedPackageIngestSourceRundownBaseline = + | ExpectedPackageIngestSourceBaselineAdlibPiece + | ExpectedPackageIngestSourceBaselineAdlibAction + | ExpectedPackageIngestSourceBaselineObjects + +export type ExpectedPackageIngestSource = ExpectedPackageIngestSourcePart | ExpectedPackageIngestSourceRundownBaseline + +// export interface ExpectedPackageDBFromStudioBaselineObjects extends ExpectedPackageDBBase { +// fromPieceType: ExpectedPackageDBType.STUDIO_BASELINE_OBJECTS +// pieceId: null +// } + +// export interface ExpectedPackageDBFromBucketAdLib extends ExpectedPackageDBBase { +// fromPieceType: ExpectedPackageDBType.BUCKET_ADLIB +// bucketId: BucketId +// /** The Bucket adlib this package belongs to */ +// pieceId: BucketAdLibId +// /** The `externalId` of the Bucket adlib this package belongs to */ +// pieceExternalId: string +// } +// export interface ExpectedPackageDBFromBucketAdLibAction extends ExpectedPackageDBBase { +// fromPieceType: ExpectedPackageDBType.BUCKET_ADLIB_ACTION +// bucketId: BucketId +// /** The Bucket adlib-action this package belongs to */ +// pieceId: BucketAdLibActionId +// /** The `externalId` of the Bucket adlib-action this package belongs to */ +// pieceExternalId: string +// } export function getContentVersionHash(expectedPackage: ReadonlyDeep>): string { return hashObj({ @@ -159,3 +173,18 @@ export function getExpectedPackageId( ): ExpectedPackageId { return protectString(`${ownerId}_${getHash(localExpectedPackageId)}`) } + +export function getExpectedPackageIdNew( + /** _id of the rundown*/ + rundownId: RundownId, + /** The locally unique id of the expectedPackage */ + expectedPackage: ReadonlyDeep +): ExpectedPackageId { + // This may be too agressive, but we don't know how to merge some of the properties + const objHash = hashObj({ + ...expectedPackage, + listenToPackageInfoUpdates: false, // Not relevant for the hash + } satisfies ReadonlyDeep) + + return protectString(`${rundownId}_${getHash(objHash)}`) +} diff --git a/packages/corelib/src/dataModel/ExternalMessageQueue.ts b/packages/corelib/src/dataModel/ExternalMessageQueue.ts index d84fbd08f1..46b6101aee 100644 --- a/packages/corelib/src/dataModel/ExternalMessageQueue.ts +++ b/packages/corelib/src/dataModel/ExternalMessageQueue.ts @@ -3,10 +3,9 @@ import { Time, IBlueprintExternalMessageQueueType, } from '@sofie-automation/blueprints-integration' -import { ProtectedStringProperties } from '../protectedString' import { ExternalMessageQueueObjId, StudioId, RundownId } from './Ids' -export interface ExternalMessageQueueObj extends ProtectedStringProperties { +export interface ExternalMessageQueueObj extends Omit { _id: ExternalMessageQueueObjId /** Id of the studio this message originates from */ studioId: StudioId diff --git a/packages/corelib/src/dataModel/Piece.ts b/packages/corelib/src/dataModel/Piece.ts index a340c45fff..21e08620f2 100644 --- a/packages/corelib/src/dataModel/Piece.ts +++ b/packages/corelib/src/dataModel/Piece.ts @@ -6,7 +6,7 @@ import { SomeContent, } from '@sofie-automation/blueprints-integration' import { ProtectedString, protectString, unprotectString } from '../protectedString' -import { PieceId, RundownId, SegmentId, PartId } from './Ids' +import { PieceId, RundownId, SegmentId, PartId, ExpectedPackageId } from './Ids' /** A generic list of playback availability statuses for a Piece */ export enum PieceStatusCode { @@ -38,19 +38,18 @@ export enum PieceStatusCode { } /** A Single item in a Part: script, VT, cameras */ -export interface PieceGeneric extends Omit { +export interface PieceGeneric extends Omit { _id: PieceId // TODO - this should be moved to the implementation types content: SomeContent - /** A flag to signal that a given Piece has no content, and exists only as a marker on the timeline */ - virtual?: boolean + expectedPackages: PieceExpectedPackage[] /** Stringified timelineObjects */ timelineObjectsString: PieceTimelineObjectsBlob } -export interface Piece extends PieceGeneric, Omit { +export interface Piece extends PieceGeneric, Omit { /** * This is the id of the rundown this piece starts playing in. * Currently this is the only rundown the piece could be playing in @@ -74,6 +73,17 @@ export interface Piece extends PieceGeneric, Omit export function deserializePieceTimelineObjectsBlob( diff --git a/packages/corelib/src/playout/infinites.ts b/packages/corelib/src/playout/infinites.ts index eac8b92f5a..9fafdc9211 100644 --- a/packages/corelib/src/playout/infinites.ts +++ b/packages/corelib/src/playout/infinites.ts @@ -13,7 +13,7 @@ import { PieceInstance, PieceInstancePiece, rewrapPieceToInstance } from '../dat import { DBPartInstance } from '../dataModel/PartInstance' import { DBRundown } from '../dataModel/Rundown' import { ReadonlyDeep } from 'type-fest' -import { assertNever, clone, flatten, getRandomId, groupByToMapFunc, max, normalizeArrayToMapFunc } from '../lib' +import { assertNever, clone, getRandomId, groupByToMapFunc, max, normalizeArrayToMapFunc } from '../lib' import { protectString } from '../protectedString' import _ = require('underscore') import { MongoQuery } from '../mongo' @@ -258,11 +258,9 @@ export function getPlayheadTrackingInfinitesForPart( return undefined } - return flatten( - Array.from(piecesOnSourceLayers.values()).map((ps) => { - return _.compact(Object.values(ps as any).map(rewrapInstance)) - }) - ) + return Array.from(piecesOnSourceLayers.values()).flatMap((ps) => { + return _.compact(Object.values(ps as any).map(rewrapInstance)) + }) } function markPieceInstanceAsContinuation(previousInstance: ReadonlyDeep, instance: PieceInstance) { diff --git a/packages/job-worker/src/blueprints/context/RundownTimingEventContext.ts b/packages/job-worker/src/blueprints/context/RundownTimingEventContext.ts index 8b25072596..f4cbc23697 100644 --- a/packages/job-worker/src/blueprints/context/RundownTimingEventContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownTimingEventContext.ts @@ -115,7 +115,7 @@ export class RundownTimingEventContext extends RundownDataChangedEventContext im partInstanceId: { $in: protectStringArray(partInstanceIds) }, }) - return pieceInstances.map(convertPieceInstanceToBlueprints) + return pieceInstances.map((p) => convertPieceInstanceToBlueprints(p, [])) } async getSegment(segmentId: string): Promise> | undefined> { diff --git a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts index 7d28375728..accbbb8f25 100644 --- a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts +++ b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts @@ -1,6 +1,6 @@ import { PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { normalizeArrayToMap, omit } from '@sofie-automation/corelib/dist/lib' +import { normalizeArrayToMapFunc, omit } from '@sofie-automation/corelib/dist/lib' import { protectString, protectStringArray, unprotectStringArray } from '@sofie-automation/corelib/dist/protectedString' import { PlayoutPartInstanceModel } from '../../playout/model/PlayoutPartInstanceModel' import { ReadonlyDeep } from 'type-fest' @@ -31,6 +31,7 @@ import { serializePieceTimelineObjectsBlob, } from '@sofie-automation/corelib/dist/dataModel/Piece' import { EXPECTED_INGEST_TO_PLAYOUT_TIME } from '@sofie-automation/shared-lib/dist/core/constants' +import { PlayoutExpectedPackagesModel } from '../../playout/model/PlayoutExpectedPackagesModel' export class SyncIngestUpdateToPartInstanceContext extends RundownUserContext @@ -48,6 +49,7 @@ export class SyncIngestUpdateToPartInstanceContext rundown: ReadonlyDeep, partInstance: PlayoutPartInstanceModel, proposedPieceInstances: ReadonlyDeep, + private readonly expectedPackages: PlayoutExpectedPackagesModel, private playStatus: 'previous' | 'current' | 'next' ) { super( @@ -61,7 +63,7 @@ export class SyncIngestUpdateToPartInstanceContext this.partInstance = partInstance - this._proposedPieceInstances = normalizeArrayToMap(proposedPieceInstances, '_id') + this._proposedPieceInstances = normalizeArrayToMapFunc(proposedPieceInstances, (p) => p._id) } syncPieceInstance( @@ -76,7 +78,7 @@ export class SyncIngestUpdateToPartInstanceContext if (!this.partInstance) throw new Error(`PartInstance has been removed`) // filter the submission to the allowed ones - const piece = modifiedPiece + const postProcessed = modifiedPiece ? postProcessPieces( this._context, [ @@ -91,16 +93,26 @@ export class SyncIngestUpdateToPartInstanceContext this.partInstance.partInstance.segmentId, this.partInstance.partInstance.part._id, this.playStatus === 'current' - )[0] - : proposedPieceInstance.piece + ) + : null + + if (postProcessed) { + this.expectedPackages.createPackagesIfMissingFromMap( + this.partInstance.partInstance.rundownId, + postProcessed.expectedPackages + ) + } const newPieceInstance: ReadonlyDeep = { ...proposedPieceInstance, - piece: piece, + piece: postProcessed?.docs[0] ?? proposedPieceInstance.piece, } this.partInstance.mergeOrInsertPieceInstance(newPieceInstance) - return convertPieceInstanceToBlueprints(newPieceInstance) + return convertPieceInstanceToBlueprints( + newPieceInstance, + this.expectedPackages.getPackagesForPieceInstance(newPieceInstance.rundownId, newPieceInstance._id) + ) } insertPieceInstance(piece0: IBlueprintPiece): IBlueprintPieceInstance { @@ -108,7 +120,7 @@ export class SyncIngestUpdateToPartInstanceContext if (!this.partInstance) throw new Error(`PartInstance has been removed`) - const piece = postProcessPieces( + const processedPieces = postProcessPieces( this._context, [trimmedPiece], this.showStyleCompound.blueprintId, @@ -116,11 +128,23 @@ export class SyncIngestUpdateToPartInstanceContext this.partInstance.partInstance.segmentId, this.partInstance.partInstance.part._id, this.playStatus === 'current' - )[0] + ) + const piece = processedPieces.docs[0] + + this.expectedPackages.createPackagesIfMissingFromMap( + this.partInstance.partInstance.rundownId, + processedPieces.expectedPackages + ) const newPieceInstance = this.partInstance.insertPlannedPiece(piece) - return convertPieceInstanceToBlueprints(newPieceInstance.pieceInstance) + return convertPieceInstanceToBlueprints( + newPieceInstance.pieceInstance, + this.expectedPackages.getPackagesForPieceInstance( + newPieceInstance.pieceInstance.rundownId, + newPieceInstance.pieceInstance._id + ) + ) } updatePieceInstance(pieceInstanceId: string, updatedPiece: Partial): IBlueprintPieceInstance { // filter the submission to the allowed ones @@ -158,8 +182,27 @@ export class SyncIngestUpdateToPartInstanceContext timelineObjectsString, }) } + if (trimmedPiece.expectedPackages) { + this.expectedPackages.createPackagesIfMissing( + pieceInstance.pieceInstance.rundownId, + trimmedPiece.expectedPackages + ) + + this.expectedPackages.setPieceInstanceReferenceToPackages( + pieceInstance.pieceInstance.rundownId, + pieceInstance.pieceInstance.partInstanceId, + pieceInstance.pieceInstance._id, + pieceInstance.pieceInstance.piece.expectedPackages.map((p) => p.expectedPackageId) + ) + } - return convertPieceInstanceToBlueprints(pieceInstance.pieceInstance) + return convertPieceInstanceToBlueprints( + pieceInstance.pieceInstance, + this.expectedPackages.getPackagesForPieceInstance( + pieceInstance.pieceInstance.rundownId, + pieceInstance.pieceInstance._id + ) + ) } updatePartInstance(updatePart: Partial): IBlueprintPartInstance { if (!this.partInstance) throw new Error(`PartInstance has been removed`) diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index 19811519d1..3f898e0499 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -111,7 +111,8 @@ export const IBlueprintMutatablePartSampleKeys = allKeysOfObject + pieceInstance: ReadonlyDeep, + expectedPackages: ReadonlyDeep ): Complete { const obj: Complete = { _id: unprotectString(pieceInstance._id), @@ -128,7 +129,7 @@ function convertPieceInstanceToBlueprintsInner( fromPreviousPlayhead: pieceInstance.infinite.fromPreviousPlayhead, }) : undefined, - piece: convertPieceToBlueprints(pieceInstance.piece), + piece: convertPieceToBlueprints(pieceInstance.piece, expectedPackages), } return obj @@ -139,8 +140,11 @@ function convertPieceInstanceToBlueprintsInner( * @param pieceInstance the PieceInstance to convert * @returns a cloned complete and clean IBlueprintPieceInstance */ -export function convertPieceInstanceToBlueprints(pieceInstance: ReadonlyDeep): IBlueprintPieceInstance { - return convertPieceInstanceToBlueprintsInner(pieceInstance) +export function convertPieceInstanceToBlueprints( + pieceInstance: ReadonlyDeep, + expectedPackages: ReadonlyDeep +): IBlueprintPieceInstance { + return convertPieceInstanceToBlueprintsInner(pieceInstance, expectedPackages) } /** @@ -152,7 +156,7 @@ export function convertResolvedPieceInstanceToBlueprints( pieceInstance: ResolvedPieceInstance ): IBlueprintResolvedPieceInstance { const obj: Complete = { - ...convertPieceInstanceToBlueprintsInner(pieceInstance.instance), + ...convertPieceInstanceToBlueprintsInner(pieceInstance.instance, []), // nocommit - is this ok? resolvedStart: pieceInstance.resolvedStart, resolvedDuration: pieceInstance.resolvedDuration, } @@ -180,7 +184,10 @@ export function convertPartInstanceToBlueprints(partInstance: ReadonlyDeep): Complete { +function convertPieceGenericToBlueprintsInner( + piece: ReadonlyDeep, + expectedPackages: ReadonlyDeep +): Complete { const obj: Complete = { externalId: piece.externalId, name: piece.name, @@ -195,7 +202,7 @@ function convertPieceGenericToBlueprintsInner(piece: ReadonlyDeep) expectedPlayoutItems: clone(piece.expectedPlayoutItems), tags: clone(piece.tags), allowDirectPlay: clone(piece.allowDirectPlay), - expectedPackages: clone(piece.expectedPackages), + expectedPackages: clone(expectedPackages), hasSideEffects: piece.hasSideEffects, content: { ...clone(piece.content), @@ -212,9 +219,12 @@ function convertPieceGenericToBlueprintsInner(piece: ReadonlyDeep) * @param piece the Piece to convert * @returns a cloned complete and clean IBlueprintPieceDB */ -export function convertPieceToBlueprints(piece: ReadonlyDeep): IBlueprintPieceDB { +export function convertPieceToBlueprints( + piece: ReadonlyDeep, + expectedPackages: ReadonlyDeep +): IBlueprintPieceDB { const obj: Complete = { - ...convertPieceGenericToBlueprintsInner(piece), + ...convertPieceGenericToBlueprintsInner(piece, expectedPackages), _id: unprotectString(piece._id), enable: clone(piece.enable), virtual: piece.virtual, @@ -272,9 +282,12 @@ export function convertPartToBlueprints(part: ReadonlyDeep): IBlueprintP * @param adLib the AdLibPiece to convert * @returns a cloned complete and clean IBlueprintAdLibPieceDB */ -export function convertAdLibPieceToBlueprints(adLib: ReadonlyDeep): IBlueprintAdLibPieceDB { +export function convertAdLibPieceToBlueprints( + adLib: ReadonlyDeep, + expectedPackages: ReadonlyDeep +): IBlueprintAdLibPieceDB { const obj: Complete = { - ...convertPieceGenericToBlueprintsInner(adLib), + ...convertPieceGenericToBlueprintsInner(adLib, expectedPackages), _id: unprotectString(adLib._id), _rank: adLib._rank, invalid: adLib.invalid, @@ -294,7 +307,10 @@ export function convertAdLibPieceToBlueprints(adLib: ReadonlyDeep): * @param action the AdLibAction to convert * @returns a cloned complete and clean IBlueprintActionManifest */ -export function convertAdLibActionToBlueprints(action: ReadonlyDeep): IBlueprintActionManifest { +export function convertAdLibActionToBlueprints( + action: ReadonlyDeep, + expectedPackages: ReadonlyDeep +): IBlueprintActionManifest { const obj: Complete = { externalId: action.externalId, actionId: action.actionId, @@ -307,7 +323,7 @@ export function convertAdLibActionToBlueprints(action: ReadonlyDeep display: clone(action.display), // TODO - type mismatch triggerModes: clone(action.triggerModes), // TODO - type mismatch expectedPlayoutItems: clone(action.expectedPlayoutItems), - expectedPackages: clone(action.expectedPackages), + expectedPackages: clone(expectedPackages), } return obj diff --git a/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts b/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts index 6d35a6bf01..86a39fd0d9 100644 --- a/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts +++ b/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts @@ -109,7 +109,17 @@ export class PartAndPieceInstanceActionService { } async getPieceInstances(part: 'current' | 'next'): Promise { const partInstance = this._getPartInstance(part) - return partInstance?.pieceInstances?.map((p) => convertPieceInstanceToBlueprints(p.pieceInstance)) ?? [] + return ( + partInstance?.pieceInstances?.map((p) => + convertPieceInstanceToBlueprints( + p.pieceInstance, + this._playoutModel.expectedPackages.getPackagesForPieceInstance( + p.pieceInstance.rundownId, + p.pieceInstance._id + ) + ) + ) ?? [] + ) } async getResolvedPieceInstances(part: 'current' | 'next'): Promise { const partInstance = this._getPartInstance(part) @@ -158,7 +168,18 @@ export class PartAndPieceInstanceActionService { query ) - return lastPieceInstance && convertPieceInstanceToBlueprints(lastPieceInstance) + if (!lastPieceInstance) return undefined + + // Ensure that any referenced packages are loaded into memory + await this._playoutModel.expectedPackages.ensurePackagesAreLoaded(lastPieceInstance.piece.expectedPackages) + + return convertPieceInstanceToBlueprints( + lastPieceInstance, + this._playoutModel.expectedPackages.getPackagesForPieceInstance( + lastPieceInstance.rundownId, + lastPieceInstance._id + ) + ) } async findLastScriptedPieceOnLayer( @@ -192,7 +213,11 @@ export class PartAndPieceInstanceActionService { query ) - return lastPiece && convertPieceToBlueprints(lastPiece) + if (!lastPiece) return undefined + + const packages = await this._playoutModel.expectedPackages.ensurePackagesAreLoaded(lastPiece.expectedPackages) + + return convertPieceToBlueprints(lastPiece, packages) } async getPartInstanceForPreviousPiece(piece: IBlueprintPieceInstance): Promise { @@ -250,7 +275,7 @@ export class PartAndPieceInstanceActionService { const trimmedPiece: IBlueprintPiece = _.pick(rawPiece, IBlueprintPieceObjectsSampleKeys) - const piece = postProcessPieces( + const postProcessed = postProcessPieces( this._context, [trimmedPiece], this.showStyleCompound.blueprintId, @@ -258,9 +283,15 @@ export class PartAndPieceInstanceActionService { partInstance.partInstance.segmentId, partInstance.partInstance.part._id, part === 'current' - )[0] + ) + const piece = postProcessed.docs[0] piece._id = getRandomId() // Make id random, as postProcessPieces is too predictable (for ingest) + this._playoutModel.expectedPackages.createPackagesIfMissingFromMap( + this._rundown.rundown._id, + postProcessed.expectedPackages + ) + // Do the work const newPieceInstance = partInstance.insertAdlibbedPiece(piece, undefined) @@ -270,7 +301,13 @@ export class PartAndPieceInstanceActionService { this.nextPartState = Math.max(this.nextPartState, ActionPartChange.SAFE_CHANGE) } - return convertPieceInstanceToBlueprints(newPieceInstance.pieceInstance) + return convertPieceInstanceToBlueprints( + newPieceInstance.pieceInstance, + this._playoutModel.expectedPackages.getPackagesForPieceInstance( + newPieceInstance.pieceInstance.rundownId, + newPieceInstance.pieceInstance._id + ) + ) } async updatePieceInstance( @@ -319,6 +356,14 @@ export class PartAndPieceInstanceActionService { trimmedPiece.content = omit(trimmedPiece.content, 'timelineObjects') as WithTimeline } + if (trimmedPiece.expectedPackages) { + // nocommit - this needs to go through some postProcess + this._playoutModel.expectedPackages.createPackagesIfMissing( + pieceInstance.pieceInstance.rundownId, + trimmedPiece.expectedPackages + ) + } + pieceInstance.updatePieceProps(trimmedPiece as any) // TODO: this needs to be more type safe if (timelineObjectsString !== undefined) pieceInstance.updatePieceProps({ timelineObjectsString }) @@ -327,7 +372,13 @@ export class PartAndPieceInstanceActionService { this.nextPartState = Math.max(this.nextPartState, updatesNextPart) this.currentPartState = Math.max(this.currentPartState, updatesCurrentPart) - return convertPieceInstanceToBlueprints(pieceInstance.pieceInstance) + return convertPieceInstanceToBlueprints( + pieceInstance.pieceInstance, + this._playoutModel.expectedPackages.getPackagesForPieceInstance( + pieceInstance.pieceInstance.rundownId, + pieceInstance.pieceInstance._id + ) + ) } async updatePartInstance( @@ -401,6 +452,11 @@ export class PartAndPieceInstanceActionService { throw new Error('Cannot queue a part which is not playable') } + this._playoutModel.expectedPackages.createPackagesIfMissingFromMap( + this._rundown.rundown._id, + pieces.expectedPackages + ) + // Do the work const newPartInstance = await insertQueuedPartWithPieces( this._context, @@ -408,7 +464,7 @@ export class PartAndPieceInstanceActionService { this._rundown, currentPartInstance, newPart, - pieces, + pieces.docs, undefined ) diff --git a/packages/job-worker/src/blueprints/context/watchedPackages.ts b/packages/job-worker/src/blueprints/context/watchedPackages.ts index 3ae20ec9fe..dc761cbcf9 100644 --- a/packages/job-worker/src/blueprints/context/watchedPackages.ts +++ b/packages/job-worker/src/blueprints/context/watchedPackages.ts @@ -1,16 +1,13 @@ -import { - ExpectedPackageDB, - ExpectedPackageDBBase, - ExpectedPackageFromRundown, -} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { ExpectedPackageDB, ExpectedPackageDBBase } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' import { JobContext } from '../../jobs' import { ExpectedPackageId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Filter as FilterQuery } from 'mongodb' import { PackageInfo } from '@sofie-automation/blueprints-integration' import { unprotectObjectArray } from '@sofie-automation/corelib/dist/protectedString' -import { ExpectedPackageForIngestModel, IngestModelReadonly } from '../../ingest/model/IngestModel' +import { IngestModelReadonly } from '../../ingest/model/IngestModel' import { ReadonlyDeep } from 'type-fest' +import { IngestExpectedPackage } from '../../ingest/model/implementation/IngestExpectedPackage' /** * This is a helper class to simplify exposing packageInfo to various places in the blueprints @@ -65,7 +62,7 @@ export class WatchedPackagesHelper { context: JobContext, ingestModel: IngestModelReadonly ): Promise { - const packages: ReadonlyDeep[] = [] + const packages: ReadonlyDeep[] = [] packages.push(...ingestModel.expectedPackagesForRundownBaseline) @@ -77,7 +74,7 @@ export class WatchedPackagesHelper { return this.#createFromPackages( context, - packages.filter((pkg) => !!pkg.listenToPackageInfoUpdates) + packages.filter((pkg) => !!pkg.package.listenToPackageInfoUpdates) ) } @@ -92,7 +89,7 @@ export class WatchedPackagesHelper { ingestModel: IngestModelReadonly, segmentExternalIds: string[] ): Promise { - const packages: ReadonlyDeep[] = [] + const packages: ReadonlyDeep[] = [] for (const externalId of segmentExternalIds) { const segment = ingestModel.getSegmentByExternalId(externalId) @@ -105,11 +102,11 @@ export class WatchedPackagesHelper { return this.#createFromPackages( context, - packages.filter((pkg) => !!pkg.listenToPackageInfoUpdates) + packages.filter((pkg) => !!pkg.package.listenToPackageInfoUpdates) ) } - static async #createFromPackages(context: JobContext, packages: ReadonlyDeep[]) { + static async #createFromPackages(context: JobContext, packages: ReadonlyDeep[]) { // Load all the packages and the infos that are watched const watchedPackageInfos = packages.length > 0 diff --git a/packages/job-worker/src/blueprints/postProcess.ts b/packages/job-worker/src/blueprints/postProcess.ts index 6e9d2f8fca..356d834fb5 100644 --- a/packages/job-worker/src/blueprints/postProcess.ts +++ b/packages/job-worker/src/blueprints/postProcess.ts @@ -13,11 +13,13 @@ import { PieceLifespan, IBlueprintPieceType, ITranslatableMessage, + ExpectedPackage, } from '@sofie-automation/blueprints-integration' import { AdLibActionId, BlueprintId, BucketId, + ExpectedPackageId, PartId, PieceId, RundownId, @@ -27,12 +29,13 @@ import { JobContext, ProcessedShowStyleCompound } from '../jobs' import { EmptyPieceTimelineObjectsBlob, Piece, + PieceExpectedPackage, serializePieceTimelineObjectsBlob, } from '@sofie-automation/corelib/dist/dataModel/Piece' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' -import { ArrayElement, getHash, literal, omit } from '@sofie-automation/corelib/dist/lib' +import { ArrayElement, Complete, getHash, literal, omit } from '@sofie-automation/corelib/dist/lib' 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' @@ -44,6 +47,7 @@ import { setDefaultIdOnExpectedPackages } from '../ingest/expectedPackages' import { logger } from '../logging' import { validateTimeline } from 'superfly-timeline' import { ReadonlyDeep } from 'type-fest' +import { getExpectedPackageIdNew } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' function getIdHash(docType: string, usedIds: Map, uniqueId: string): string { const count = usedIds.get(uniqueId) @@ -58,6 +62,15 @@ function getIdHash(docType: string, usedIds: Map, uniqueId: stri } } +export interface PostProcessDocs { + docs: T[] + expectedPackages: Map> +} + +// export function unwrapPostProccessDocs(docs: PostProcessDoc[]): T[] { +// return docs.map((doc) => doc.doc) +// } + /** * Process and validate some IBlueprintPiece into Piece * @param context Context from the job queue @@ -78,13 +91,15 @@ export function postProcessPieces( partId: PartId, allowNowForPiece: boolean, setInvalid?: boolean -): Piece[] { +): PostProcessDocs { const span = context.startSpan('blueprints.postProcess.postProcessPieces') + const expectedPackages = new Map>() + const uniqueIds = new Map() const timelineUniqueIds = new Set() - const processedPieces = pieces.map((orgPiece: IBlueprintPiece) => { + const processedPieces = pieces.map((orgPiece: IBlueprintPiece): Piece => { if (!orgPiece.externalId) throw new Error( `Error in blueprint "${blueprintId}" externalId not set for adlib piece in ${partId}! ("${orgPiece.name}")` @@ -96,10 +111,12 @@ export function postProcessPieces( `${rundownId}_${blueprintId}_${partId}_piece_${orgPiece.sourceLayerId}_${orgPiece.externalId}` ) - const piece: Piece = { + // Fill in ids of unnamed expectedPackages + const processedExpectedPackages = setDefaultIdOnExpectedPackages(orgPiece.expectedPackages) + + const piece: Complete = { pieceType: IBlueprintPieceType.Normal, - ...orgPiece, content: omit(orgPiece.content, 'timelineObjects'), _id: protectString(docId), @@ -108,6 +125,29 @@ export function postProcessPieces( startPartId: partId, invalid: setInvalid ?? false, timelineObjectsString: EmptyPieceTimelineObjectsBlob, + + virtual: orgPiece.virtual, + externalId: orgPiece.externalId, + sourceLayerId: orgPiece.sourceLayerId, + outputLayerId: orgPiece.outputLayerId, + enable: orgPiece.enable, + lifespan: orgPiece.lifespan, + extendOnHold: orgPiece.extendOnHold, + name: orgPiece.name, + privateData: orgPiece.privateData, + publicData: orgPiece.publicData, + + prerollDuration: orgPiece.prerollDuration, + postrollDuration: orgPiece.postrollDuration, + + toBeQueued: orgPiece.toBeQueued, + allowDirectPlay: orgPiece.allowDirectPlay, + expectedPlayoutItems: orgPiece.expectedPlayoutItems, + tags: orgPiece.tags, + hasSideEffects: orgPiece.hasSideEffects, + abSessions: orgPiece.abSessions, + notInVision: orgPiece.notInVision, + expectedPackages: convertExpectedPackages(rundownId, processedExpectedPackages, expectedPackages), } if (piece.pieceType !== IBlueprintPieceType.Normal) { @@ -132,14 +172,14 @@ export function postProcessPieces( ) piece.timelineObjectsString = serializePieceTimelineObjectsBlob(timelineObjects) - // Fill in ids of unnamed expectedPackages - setDefaultIdOnExpectedPackages(piece.expectedPackages) - return piece }) span?.end() - return processedPieces + return { + docs: processedPieces, + expectedPackages, + } } function isNow(enable: TimelineObjectCoreExt['enable']): boolean { @@ -222,9 +262,11 @@ export function postProcessAdLibPieces( rundownId: RundownId, partId: PartId | undefined, adLibPieces: Array -): AdLibPiece[] { +): PostProcessDocs { const span = context.startSpan('blueprints.postProcess.postProcessAdLibPieces') + const expectedPackages = new Map>() + const uniqueIds = new Map() const timelineUniqueIds = new Set() @@ -240,13 +282,46 @@ export function postProcessAdLibPieces( `${rundownId}_${blueprintId}_${partId}_adlib_piece_${orgAdlib.sourceLayerId}_${orgAdlib.externalId}` ) - const piece: AdLibPiece = { - ...orgAdlib, + // Fill in ids of unnamed expectedPackages + const processedExpectedPackages = setDefaultIdOnExpectedPackages(orgAdlib.expectedPackages) + + const piece: Complete = { content: omit(orgAdlib.content, 'timelineObjects'), _id: protectString(docId), rundownId: rundownId, partId: partId, timelineObjectsString: EmptyPieceTimelineObjectsBlob, + externalId: orgAdlib.externalId, + _rank: orgAdlib._rank, + + expectedPackages: convertExpectedPackages(rundownId, processedExpectedPackages, expectedPackages), + expectedPlayoutItems: orgAdlib.expectedPlayoutItems, + + privateData: orgAdlib.privateData, + publicData: orgAdlib.publicData, + invalid: orgAdlib.invalid, + floated: orgAdlib.floated, + name: orgAdlib.name, + expectedDuration: orgAdlib.expectedDuration, + + currentPieceTags: orgAdlib.currentPieceTags, + nextPieceTags: orgAdlib.nextPieceTags, + uniquenessId: orgAdlib.uniquenessId, + invertOnAirState: orgAdlib.invertOnAirState, + + lifespan: orgAdlib.lifespan, + sourceLayerId: orgAdlib.sourceLayerId, + outputLayerId: orgAdlib.outputLayerId, + + prerollDuration: orgAdlib.prerollDuration, + postrollDuration: orgAdlib.postrollDuration, + toBeQueued: orgAdlib.toBeQueued, + + allowDirectPlay: orgAdlib.allowDirectPlay, + tags: orgAdlib.tags, + + hasSideEffects: orgAdlib.hasSideEffects, + abSessions: orgAdlib.abSessions, } if (!piece.externalId) @@ -262,14 +337,14 @@ export function postProcessAdLibPieces( ) piece.timelineObjectsString = serializePieceTimelineObjectsBlob(timelineObjects) - // Fill in ids of unnamed expectedPackages - setDefaultIdOnExpectedPackages(piece.expectedPackages) - return piece }) span?.end() - return processedPieces + return { + docs: processedPieces, + expectedPackages, + } } /** @@ -282,10 +357,12 @@ export function postProcessGlobalAdLibActions( blueprintId: BlueprintId, rundownId: RundownId, adlibActions: IBlueprintActionManifest[] -): RundownBaselineAdLibAction[] { +): PostProcessDocs { + const expectedPackages = new Map>() + const uniqueIds = new Map() - return adlibActions.map((action) => { + const processedActions = adlibActions.map((action) => { if (!action.externalId) throw new Error( `Error in blueprint "${blueprintId}" externalId not set for baseline adlib action! ("${ @@ -300,17 +377,34 @@ export function postProcessGlobalAdLibActions( ) // Fill in ids of unnamed expectedPackages - setDefaultIdOnExpectedPackages(action.expectedPackages) + const processedExpectedPackages = setDefaultIdOnExpectedPackages(action.expectedPackages) - return literal({ - ...action, - actionId: action.actionId, + return literal>({ + externalId: action.externalId, _id: protectString(docId), rundownId: rundownId, - partId: undefined, ...processAdLibActionITranslatableMessages(action, blueprintId), + + expectedPackages: convertExpectedPackages(rundownId, processedExpectedPackages, expectedPackages), + expectedPlayoutItems: action.expectedPlayoutItems, + + privateData: action.privateData, + publicData: action.publicData, + + actionId: action.actionId, + userData: action.userData, + userDataManifest: action.userDataManifest, + allVariants: action.allVariants, + + // Not used? + uniquenessId: undefined, }) }) + + return { + docs: processedActions, + expectedPackages, + } } /** @@ -325,10 +419,12 @@ export function postProcessAdLibActions( rundownId: RundownId, partId: PartId, adlibActions: IBlueprintActionManifest[] -): AdLibAction[] { +): PostProcessDocs { + const expectedPackages = new Map>() + const uniqueIds = new Map() - return adlibActions.map((action) => { + const processedActions = adlibActions.map((action) => { if (!action.externalId) throw new Error( `Error in blueprint "${blueprintId}" externalId not set for adlib action in ${partId}! ("${action.display.label}")` @@ -341,17 +437,34 @@ export function postProcessAdLibActions( ) // Fill in ids of unnamed expectedPackages - setDefaultIdOnExpectedPackages(action.expectedPackages) + const processedExpectedPackages = setDefaultIdOnExpectedPackages(action.expectedPackages) - return literal({ - ...action, - actionId: action.actionId, + const processedAction = literal>({ + externalId: action.externalId, _id: protectString(docId), rundownId: rundownId, partId: partId, ...processAdLibActionITranslatableMessages(action, blueprintId), + + expectedPackages: convertExpectedPackages(rundownId, processedExpectedPackages, expectedPackages), + expectedPlayoutItems: action.expectedPlayoutItems, + + privateData: action.privateData, + publicData: action.publicData, + + actionId: action.actionId, + userData: action.userData, + userDataManifest: action.userDataManifest, + allVariants: action.allVariants, + + // Not used? + uniquenessId: undefined, }) + + return processedAction }) + + return { docs: processedActions, expectedPackages } } /** @@ -404,8 +517,11 @@ export function postProcessBucketAdLib( `${showStyleCompound.showStyleVariantId}_${context.studioId}_${bucketId}_bucket_adlib_${ingestInfo.payload.externalId}` ) ) - const piece: BucketAdLib = { - ...itemOrig, + + // Fill in ids of unnamed expectedPackages + const processedExpectedPackages = setDefaultIdOnExpectedPackages(itemOrig.expectedPackages) + + const piece: Complete = { content: omit(itemOrig.content, 'timelineObjects'), _id: id, externalId: ingestInfo.payload.externalId, @@ -417,13 +533,45 @@ export function postProcessBucketAdLib( ingestInfo, _rank: rank || itemOrig._rank, timelineObjectsString: EmptyPieceTimelineObjectsBlob, + + expectedPackages: processedExpectedPackages, // convertExpectedPackages(processedExpectedPackages), + expectedPlayoutItems: itemOrig.expectedPlayoutItems, + + privateData: itemOrig.privateData, + publicData: itemOrig.publicData, + invalid: itemOrig.invalid, + floated: itemOrig.floated, + name: itemOrig.name, + expectedDuration: itemOrig.expectedDuration, + + currentPieceTags: itemOrig.currentPieceTags, + nextPieceTags: itemOrig.nextPieceTags, + uniquenessId: itemOrig.uniquenessId, + invertOnAirState: itemOrig.invertOnAirState, + + lifespan: itemOrig.lifespan, + sourceLayerId: itemOrig.sourceLayerId, + outputLayerId: itemOrig.outputLayerId, + + prerollDuration: itemOrig.prerollDuration, + postrollDuration: itemOrig.postrollDuration, + toBeQueued: itemOrig.toBeQueued, + + allowDirectPlay: itemOrig.allowDirectPlay, + tags: itemOrig.tags, + + hasSideEffects: itemOrig.hasSideEffects, + abSessions: itemOrig.abSessions, } - // Fill in ids of unnamed expectedPackages - setDefaultIdOnExpectedPackages(piece.expectedPackages) const timelineObjects = postProcessTimelineObjects(piece._id, blueprintId, itemOrig.content.timelineObjects) piece.timelineObjectsString = serializePieceTimelineObjectsBlob(timelineObjects) + // nocommit: TODO + // return { + // doc: piece, + // expectedPackages: processedExpectedPackages, + // } return piece } @@ -453,8 +601,11 @@ export function postProcessBucketAction( `${showStyleCompound.showStyleVariantId}_${context.studioId}_${bucketId}_bucket_adlib_${ingestInfo.payload.externalId}` ) ) - const action: BucketAdLibAction = { - ...omit(itemOrig, 'partId'), + + // Fill in ids of unnamed expectedPackages + const processedExpectedPackages = setDefaultIdOnExpectedPackages(itemOrig.expectedPackages) + + const action: Complete = { _id: id, externalId: ingestInfo.payload.externalId, studioId: context.studioId, @@ -464,11 +615,26 @@ export function postProcessBucketAction( importVersions, ingestInfo, ...processAdLibActionITranslatableMessages(itemOrig, blueprintId, rank), - } + expectedPackages: processedExpectedPackages, // convertExpectedPackages(processedExpectedPackages), + expectedPlayoutItems: itemOrig.expectedPlayoutItems, - // Fill in ids of unnamed expectedPackages - setDefaultIdOnExpectedPackages(action.expectedPackages) + privateData: itemOrig.privateData, + publicData: itemOrig.publicData, + + actionId: itemOrig.actionId, + userData: itemOrig.userData, + userDataManifest: itemOrig.userDataManifest, + allVariants: itemOrig.allVariants, + // Not used? + uniquenessId: undefined, + } + + // nocommit: TODO + // return { + // doc: action, + // expectedPackages: processedExpectedPackages, + // } return action } @@ -498,7 +664,7 @@ function processAdLibActionITranslatableMessages< })[] }, T extends IBlueprintActionManifest ->(itemOrig: T, blueprintId: BlueprintId, rank?: number): Pick { +>(itemOrig: T, blueprintId: BlueprintId, rank?: number): Complete> { return { display: { ...itemOrig.display, @@ -527,3 +693,18 @@ function processAdLibActionITranslatableMessages< ), } } + +function convertExpectedPackages( + rundownId: RundownId, + expectedPackages: ExpectedPackage.Any[], + expectedPackagesMap: Map> +): Complete[] { + if (!expectedPackages) return [] + + return expectedPackages.map((expectedPackage) => { + const expectedPackageId = getExpectedPackageIdNew(rundownId, expectedPackage) + expectedPackagesMap.set(expectedPackageId, expectedPackage) + + return { blueprintPackageId: expectedPackage._id, expectedPackageId } + }) +} diff --git a/packages/job-worker/src/ingest/__tests__/expectedPackages.test.ts b/packages/job-worker/src/ingest/__tests__/expectedPackages.test.ts index ec920872bd..5f066561be 100644 --- a/packages/job-worker/src/ingest/__tests__/expectedPackages.test.ts +++ b/packages/job-worker/src/ingest/__tests__/expectedPackages.test.ts @@ -6,7 +6,7 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { defaultPart, defaultPiece, defaultAdLibPiece } from '../../__mocks__/defaultCollectionObjects' import { LAYER_IDS } from '../../__mocks__/presetCollections' import { ExpectedPackage, PieceLifespan, VTContent } from '@sofie-automation/blueprints-integration' -import { updateExpectedPackagesForPartModel } from '../expectedPackages' +import { updateExpectedMediaAndPlayoutItemsForPartModel } from '../expectedPackages' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context' import { ReadonlyDeep } from 'type-fest' import { IngestPartModel } from '../model/IngestPartModel' @@ -141,7 +141,7 @@ describe('Expected Media Items', () => { }, } - updateExpectedPackagesForPartModel(context, partModel) + updateExpectedMediaAndPlayoutItemsForPartModel(context, partModel) expect(setExpectedPackages).toHaveBeenCalledTimes(1) expect(setExpectedPackages.mock.calls[0][0]).toHaveLength(4) diff --git a/packages/job-worker/src/ingest/expectedMediaItems.ts b/packages/job-worker/src/ingest/expectedMediaItems.ts index f694ab3bc7..73f3e67dac 100644 --- a/packages/job-worker/src/ingest/expectedMediaItems.ts +++ b/packages/job-worker/src/ingest/expectedMediaItems.ts @@ -119,7 +119,7 @@ function generateExpectedMediaItemsFull( ...generateExpectedMediaItems( doc._id, { - partId: doc.partId, + partId: 'partId' in doc ? doc.partId : undefined, rundownId: rundownId, }, studioId, diff --git a/packages/job-worker/src/ingest/expectedPackages.ts b/packages/job-worker/src/ingest/expectedPackages.ts index c1d6099a9e..3bb60243da 100644 --- a/packages/job-worker/src/ingest/expectedPackages.ts +++ b/packages/job-worker/src/ingest/expectedPackages.ts @@ -1,24 +1,15 @@ -import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' -import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { ExpectedPackageDBType, - ExpectedPackageDBFromPiece, - ExpectedPackageDBFromBaselineAdLibPiece, - ExpectedPackageDBFromAdLibAction, - ExpectedPackageDBFromBaselineAdLibAction, ExpectedPackageDBFromBucketAdLib, ExpectedPackageDBFromBucketAdLibAction, - ExpectedPackageDBBase, - ExpectedPackageDBFromRundownBaselineObjects, ExpectedPackageDBFromStudioBaselineObjects, getContentVersionHash, getExpectedPackageId, - ExpectedPackageFromRundown, + ExpectedPackageDBBaseSimple, } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { - SegmentId, RundownId, AdLibActionId, PieceId, @@ -26,10 +17,8 @@ import { BucketAdLibActionId, BucketAdLibId, StudioId, + PieceInstanceId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' -import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { saveIntoDb } from '../db/changes' import { PlayoutModel } from '../playout/model/PlayoutModel' import { StudioPlayoutModel } from '../studio/model/StudioPlayoutModel' @@ -42,220 +31,30 @@ import { updateExpectedPlayoutItemsForRundownBaseline, } from './expectedPlayoutItems' import { JobContext } from '../jobs' -import { ExpectedPackageForIngestModelBaseline, IngestModel } from './model/IngestModel' +import { IngestModel } from './model/IngestModel' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { IngestPartModel } from './model/IngestPartModel' import { clone } from '@sofie-automation/corelib/dist/lib' -export function updateExpectedPackagesForPartModel(context: JobContext, part: IngestPartModel): void { +export function updateExpectedMediaAndPlayoutItemsForPartModel(context: JobContext, part: IngestPartModel): void { updateExpectedMediaItemsForPartModel(context, part) updateExpectedPlayoutItemsForPartModel(context, part) - - const expectedPackages: ExpectedPackageFromRundown[] = [ - ...generateExpectedPackagesForPiece( - context.studio, - part.part.rundownId, - part.part.segmentId, - part.pieces, - ExpectedPackageDBType.PIECE - ), - ...generateExpectedPackagesForPiece( - context.studio, - part.part.rundownId, - part.part.segmentId, - part.adLibPieces, - ExpectedPackageDBType.ADLIB_PIECE - ), - ...generateExpectedPackagesForAdlibAction( - context.studio, - part.part.rundownId, - part.part.segmentId, - part.adLibActions - ), - ] - - part.setExpectedPackages(expectedPackages) } -export async function updateExpectedPackagesForRundownBaseline( +export async function updateExpectedMediaAndPlayoutItemsForRundownBaseline( context: JobContext, ingestModel: IngestModel, - baseline: BlueprintResultBaseline | undefined, - forceBaseline = false + baseline: BlueprintResultBaseline | undefined ): Promise { await updateExpectedMediaItemsForRundownBaseline(context, ingestModel) await updateExpectedPlayoutItemsForRundownBaseline(context, ingestModel, baseline) - - const expectedPackages: ExpectedPackageForIngestModelBaseline[] = [] - - const preserveTypesDuringSave = new Set() - - // Only regenerate the baseline types if they are already loaded into memory - // If the data isn't already loaded, then we haven't made any changes to the baseline adlibs - // This means we can skip regenerating them as it is guaranteed there will be no changes - const baselineAdlibPieceCache = forceBaseline - ? await ingestModel.rundownBaselineAdLibPieces.get() - : ingestModel.rundownBaselineAdLibPieces.getIfLoaded() - if (baselineAdlibPieceCache) { - expectedPackages.push( - ...generateExpectedPackagesForBaselineAdlibPiece( - context.studio, - ingestModel.rundownId, - baselineAdlibPieceCache - ) - ) - } else { - // We haven't regenerated anything, so preserve the values in the save - preserveTypesDuringSave.add(ExpectedPackageDBType.BASELINE_ADLIB_PIECE) - } - const baselineAdlibActionCache = forceBaseline - ? await ingestModel.rundownBaselineAdLibActions.get() - : ingestModel.rundownBaselineAdLibActions.getIfLoaded() - if (baselineAdlibActionCache) { - expectedPackages.push( - ...generateExpectedPackagesForBaselineAdlibAction( - context.studio, - ingestModel.rundownId, - baselineAdlibActionCache - ) - ) - } else { - // We haven't regenerated anything, so preserve the values in the save - preserveTypesDuringSave.add(ExpectedPackageDBType.BASELINE_ADLIB_ACTION) - } - - if (baseline) { - // Fill in ids of unnamed expectedPackages - setDefaultIdOnExpectedPackages(baseline.expectedPackages) - - const bases = generateExpectedPackageBases( - context.studio, - ingestModel.rundownId, - baseline.expectedPackages ?? [] - ) - - expectedPackages.push( - ...bases.map((item): ExpectedPackageDBFromRundownBaselineObjects => { - return { - ...item, - fromPieceType: ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS, - rundownId: ingestModel.rundownId, - pieceId: null, - } - }) - ) - } else { - // We haven't regenerated anything, so preserve the values in the save - preserveTypesDuringSave.add(ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS) - } - - // Preserve anything existing - for (const expectedPackage of ingestModel.expectedPackagesForRundownBaseline) { - if (preserveTypesDuringSave.has(expectedPackage.fromPieceType)) { - expectedPackages.push(clone(expectedPackage)) - } - } - - ingestModel.setExpectedPackagesForRundownBaseline(expectedPackages) } -function generateExpectedPackagesForPiece( - studio: ReadonlyDeep, - rundownId: RundownId, - segmentId: SegmentId, - pieces: ReadonlyDeep[], - type: ExpectedPackageDBType.PIECE | ExpectedPackageDBType.ADLIB_PIECE -) { - const packages: ExpectedPackageDBFromPiece[] = [] - for (const piece of pieces) { - const partId = 'startPartId' in piece ? piece.startPartId : piece.partId - if (piece.expectedPackages && partId) { - const bases = generateExpectedPackageBases(studio, piece._id, piece.expectedPackages) - for (const base of bases) { - packages.push({ - ...base, - rundownId, - segmentId, - partId, - pieceId: piece._id, - fromPieceType: type, - }) - } - } - } - return packages -} -function generateExpectedPackagesForBaselineAdlibPiece( - studio: ReadonlyDeep, - rundownId: RundownId, - pieces: ReadonlyDeep -) { - const packages: ExpectedPackageDBFromBaselineAdLibPiece[] = [] - for (const piece of pieces) { - if (piece.expectedPackages) { - const bases = generateExpectedPackageBases(studio, piece._id, piece.expectedPackages) - for (const base of bases) { - packages.push({ - ...base, - rundownId, - pieceId: piece._id, - fromPieceType: ExpectedPackageDBType.BASELINE_ADLIB_PIECE, - }) - } - } - } - return packages -} -function generateExpectedPackagesForAdlibAction( - studio: ReadonlyDeep, - rundownId: RundownId, - segmentId: SegmentId, - actions: ReadonlyDeep -) { - const packages: ExpectedPackageDBFromAdLibAction[] = [] - for (const action of actions) { - if (action.expectedPackages) { - const bases = generateExpectedPackageBases(studio, action._id, action.expectedPackages) - for (const base of bases) { - packages.push({ - ...base, - rundownId, - segmentId, - partId: action.partId, - pieceId: action._id, - fromPieceType: ExpectedPackageDBType.ADLIB_ACTION, - }) - } - } - } - return packages -} -function generateExpectedPackagesForBaselineAdlibAction( - studio: ReadonlyDeep, - rundownId: RundownId, - actions: ReadonlyDeep -) { - const packages: ExpectedPackageDBFromBaselineAdLibAction[] = [] - for (const action of actions) { - if (action.expectedPackages) { - const bases = generateExpectedPackageBases(studio, action._id, action.expectedPackages) - for (const base of bases) { - packages.push({ - ...base, - rundownId, - pieceId: action._id, - fromPieceType: ExpectedPackageDBType.BASELINE_ADLIB_ACTION, - }) - } - } - } - return packages -} function generateExpectedPackagesForBucketAdlib(studio: ReadonlyDeep, adlibs: BucketAdLib[]) { const packages: ExpectedPackageDBFromBucketAdLib[] = [] for (const adlib of adlibs) { if (adlib.expectedPackages) { - const bases = generateExpectedPackageBases(studio, adlib._id, adlib.expectedPackages) + const bases = generateExpectedPackageBases(studio._id, adlib._id, adlib.expectedPackages) for (const base of bases) { packages.push({ ...base, @@ -276,7 +75,7 @@ function generateExpectedPackagesForBucketAdlibAction( const packages: ExpectedPackageDBFromBucketAdLibAction[] = [] for (const action of adlibActions) { if (action.expectedPackages) { - const bases = generateExpectedPackageBases(studio, action._id, action.expectedPackages) + const bases = generateExpectedPackageBases(studio._id, action._id, action.expectedPackages) for (const base of bases) { packages.push({ ...base, @@ -291,7 +90,7 @@ function generateExpectedPackagesForBucketAdlibAction( return packages } function generateExpectedPackageBases( - studio: ReadonlyDeep, + studioId: StudioId, ownerId: | PieceId | AdLibActionId @@ -299,21 +98,22 @@ function generateExpectedPackageBases( | BucketAdLibId | BucketAdLibActionId | RundownId - | StudioId, + | StudioId + | PieceInstanceId, expectedPackages: ReadonlyDeep -) { - const bases: Omit[] = [] +): ExpectedPackageDBBaseSimple[] { + const bases: ExpectedPackageDBBaseSimple[] = [] for (let i = 0; i < expectedPackages.length; i++) { const expectedPackage = expectedPackages[i] const id = expectedPackage._id || '__unnamed' + i bases.push({ - ...clone(expectedPackage), + package: clone(expectedPackage), _id: getExpectedPackageId(ownerId, id), blueprintPackageId: id, contentVersionHash: getContentVersionHash(expectedPackage), - studioId: studio._id, + studioId: studioId, created: Date.now(), }) } @@ -369,7 +169,7 @@ export function updateBaselineExpectedPackagesOnStudio( // Fill in ids of unnamed expectedPackages setDefaultIdOnExpectedPackages(baseline.expectedPackages) - const bases = generateExpectedPackageBases(context.studio, context.studio._id, baseline.expectedPackages ?? []) + const bases = generateExpectedPackageBases(context.studio._id, context.studio._id, baseline.expectedPackages ?? []) playoutModel.setExpectedPackagesForStudioBaseline( bases.map((item): ExpectedPackageDBFromStudioBaselineObjects => { return { @@ -381,14 +181,18 @@ export function updateBaselineExpectedPackagesOnStudio( ) } -export function setDefaultIdOnExpectedPackages(expectedPackages: ExpectedPackage.Any[] | undefined): void { +export function setDefaultIdOnExpectedPackages( + expectedPackages: ExpectedPackage.Any[] | undefined +): ExpectedPackage.Any[] { // Fill in ids of unnamed expectedPackage - if (expectedPackages) { - for (let i = 0; i < expectedPackages.length; i++) { - const expectedPackage = expectedPackages[i] - if (!expectedPackage._id) { - expectedPackage._id = `__index${i}` - } + if (!expectedPackages) return [] + + for (let i = 0; i < expectedPackages.length; i++) { + const expectedPackage = expectedPackages[i] + if (!expectedPackage._id) { + expectedPackage._id = `__index${i}` } } + + return expectedPackages } diff --git a/packages/job-worker/src/ingest/generationRundown.ts b/packages/job-worker/src/ingest/generationRundown.ts index f41ed7db49..f0bd051bc7 100644 --- a/packages/job-worker/src/ingest/generationRundown.ts +++ b/packages/job-worker/src/ingest/generationRundown.ts @@ -1,5 +1,16 @@ -import { ExpectedPackageDBType } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' -import { BlueprintId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { + ExpectedPackageDBNew, + ExpectedPackageDBType, + getContentVersionHash, + getExpectedPackageIdNew, +} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { + BlueprintId, + ExpectedPackageId, + RundownId, + SegmentId, + StudioId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownNote } from '@sofie-automation/corelib/dist/dataModel/Notes' import { serializePieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { DBRundown, RundownSource } from '@sofie-automation/corelib/dist/dataModel/Rundown' @@ -10,6 +21,7 @@ import { StudioUserContext, GetRundownContext } from '../blueprints/context' import { WatchedPackagesHelper } from '../blueprints/context/watchedPackages' import { postProcessAdLibPieces, + PostProcessDocs, postProcessGlobalAdLibActions, postProcessRundownBaselineItems, } from '../blueprints/postProcess' @@ -21,12 +33,18 @@ import { extendIngestRundownCore, canRundownBeUpdated } from './lib' import { JobContext } from '../jobs' import { CommitIngestData } from './lock' import { SelectedShowStyleVariant, selectShowStyleVariant } from './selectShowStyleVariant' -import { updateExpectedPackagesForRundownBaseline } from './expectedPackages' +import { updateExpectedMediaAndPlayoutItemsForRundownBaseline } from './expectedPackages' import { ReadonlyDeep } from 'type-fest' -import { BlueprintResultRundown, ExtendedIngestRundown } from '@sofie-automation/blueprints-integration' +import { + BlueprintResultRundown, + ExpectedPackage, + ExtendedIngestRundown, +} from '@sofie-automation/blueprints-integration' import { wrapTranslatableMessageFromBlueprints } from '@sofie-automation/corelib/dist/TranslatableMessage' import { convertRundownToBlueprintSegmentRundown } from '../blueprints/context/lib' import { calculateSegmentsAndRemovalsFromIngestData } from './generationSegment' +import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' +import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' /** * Regenerate and save a whole Rundown @@ -342,9 +360,98 @@ export async function regenerateRundownAndBaselineFromIngestData( rundownRes.globalActions || [] ) - await ingestModel.setRundownBaseline(timelineObjectsBlob, adlibPieces, adlibActions) + const expectedPackages = generateExpectedPackagesForBaseline( + context.studioId, + dbRundown._id, + adlibPieces, + adlibActions, + rundownRes.baseline.expectedPackages ?? [] + ) + + await ingestModel.setRundownBaseline(timelineObjectsBlob, adlibPieces.docs, adlibActions.docs, expectedPackages) - await updateExpectedPackagesForRundownBaseline(context, ingestModel, rundownRes.baseline) + await updateExpectedMediaAndPlayoutItemsForRundownBaseline(context, ingestModel, rundownRes.baseline) return dbRundown } + +function generateExpectedPackagesForBaseline( + studioId: StudioId, + rundownId: RundownId, + adLibPieces: PostProcessDocs, + adLibActions: PostProcessDocs, + expectedPackages: ExpectedPackage.Any[] +): ExpectedPackageDBNew[] { + const packages = new Map() + + const baselineExpectedPackages = new Map>() + for (const expectedPackage of expectedPackages) { + const packageId = getExpectedPackageIdNew(rundownId, expectedPackage) + baselineExpectedPackages.set(packageId, expectedPackage) + } + + // Generate the full version of each expectedPackage + // nocommit - deduplicate this work with the other files + const allRawPackages = [ + ...adLibPieces.expectedPackages, + ...adLibActions.expectedPackages, + ...baselineExpectedPackages, + ] + for (const [packageId, expectedPackage] of allRawPackages) { + if (packages.has(packageId)) continue + + packages.set(packageId, { + _id: packageId, + + studioId, + rundownId, + + contentVersionHash: getContentVersionHash(expectedPackage), + + created: Date.now(), // nocommit - avoid churn on this? + + package: expectedPackage, + + ingestSources: [], + + playoutSources: { + // nocommit - avoid this here? + pieceInstanceIds: [], + }, + }) + } + + // Populate the ingestSources + for (const piece of adLibPieces.docs) { + for (const expectedPackage of piece.expectedPackages) { + const expectedPackageDoc = packages.get(expectedPackage.expectedPackageId) + if (!expectedPackageDoc) continue // nocommit - log error? this should never happen + + expectedPackageDoc.ingestSources.push({ + fromPieceType: ExpectedPackageDBType.BASELINE_ADLIB_PIECE, + pieceId: piece._id, + }) + } + } + for (const piece of adLibActions.docs) { + for (const expectedPackage of piece.expectedPackages) { + const expectedPackageDoc = packages.get(expectedPackage.expectedPackageId) + if (!expectedPackageDoc) continue // nocommit - log error? this should never happen + + expectedPackageDoc.ingestSources.push({ + fromPieceType: ExpectedPackageDBType.BASELINE_ADLIB_ACTION, + pieceId: piece._id, + }) + } + } + for (const packageId of baselineExpectedPackages.keys()) { + const expectedPackageDoc = packages.get(packageId) + if (!expectedPackageDoc) continue // nocommit - log error? this should never happen + + expectedPackageDoc.ingestSources.push({ + fromPieceType: ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS, + }) + } + + return Array.from(packages.values()) +} diff --git a/packages/job-worker/src/ingest/generationSegment.ts b/packages/job-worker/src/ingest/generationSegment.ts index e90d17dcdf..585d0b0775 100644 --- a/packages/job-worker/src/ingest/generationSegment.ts +++ b/packages/job-worker/src/ingest/generationSegment.ts @@ -19,7 +19,7 @@ import { NoteSeverity, } from '@sofie-automation/blueprints-integration' import { wrapTranslatableMessageFromBlueprints } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { updateExpectedPackagesForPartModel } from './expectedPackages' +import { updateExpectedMediaAndPlayoutItemsForPartModel } from './expectedPackages' import { IngestReplacePartType, IngestSegmentModel } from './model/IngestSegmentModel' import { ReadonlyDeep } from 'type-fest' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' @@ -190,7 +190,7 @@ async function checkIfSegmentReferencesUnloadedPackageInfos( // check if there are any updates right away? for (const part of segmentModel.parts) { for (const expectedPackage of part.expectedPackages) { - if (expectedPackage.listenToPackageInfoUpdates) { + if (expectedPackage.package.listenToPackageInfoUpdates) { const loadedPackage = segmentWatchedPackages.getPackage(expectedPackage._id) if (!loadedPackage) { // The package didn't exist prior to the blueprint running @@ -402,7 +402,7 @@ function updateModelWithGeneratedPart( ) const partModel = segmentModel.replacePart(part, processedPieces, adlibPieces, adlibActions) - updateExpectedPackagesForPartModel(context, partModel) + updateExpectedMediaAndPlayoutItemsForPartModel(context, partModel) } /** diff --git a/packages/job-worker/src/ingest/model/IngestModel.ts b/packages/job-worker/src/ingest/model/IngestModel.ts index 305a4197cd..247cc764ab 100644 --- a/packages/job-worker/src/ingest/model/IngestModel.ts +++ b/packages/job-worker/src/ingest/model/IngestModel.ts @@ -1,9 +1,7 @@ import { ExpectedMediaItemRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedMediaItem' import { - ExpectedPackageDBFromBaselineAdLibAction, - ExpectedPackageDBFromBaselineAdLibPiece, - ExpectedPackageDBFromRundownBaselineObjects, - ExpectedPackageFromRundown, + ExpectedPackageDBNew, + ExpectedPackageIngestSource, } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { ExpectedPlayoutItemRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' import { @@ -30,12 +28,7 @@ import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { ProcessedShowStyleBase, ProcessedShowStyleVariant } from '../../jobs/showStyle' import { WrappedShowStyleBlueprint } from '../../blueprints/cache' import { IBlueprintRundown } from '@sofie-automation/blueprints-integration' - -export type ExpectedPackageForIngestModelBaseline = - | ExpectedPackageDBFromBaselineAdLibAction - | ExpectedPackageDBFromBaselineAdLibPiece - | ExpectedPackageDBFromRundownBaselineObjects -export type ExpectedPackageForIngestModel = ExpectedPackageFromRundown | ExpectedPackageForIngestModelBaseline +import { IngestExpectedPackage } from './implementation/IngestExpectedPackage' export interface IngestModelReadonly { /** @@ -64,7 +57,7 @@ export interface IngestModelReadonly { /** * The ExpectedPackages for the baseline of this Rundown */ - readonly expectedPackagesForRundownBaseline: ReadonlyDeep[] + readonly expectedPackagesForRundownBaseline: ReadonlyDeep[] /** * The baseline Timeline objects of this Rundown @@ -144,7 +137,7 @@ export interface IngestModelReadonly { * Search for an ExpectedPackage through the whole Rundown * @param id Id of the ExpectedPackage */ - findExpectedPackage(packageId: ExpectedPackageId): ReadonlyDeep | undefined + findExpectedPackageIngestSources(packageId: ExpectedPackageId): ReadonlyDeep[] } export interface IngestModel extends IngestModelReadonly, BaseModel { @@ -212,11 +205,11 @@ export interface IngestModel extends IngestModelReadonly, BaseModel { */ setExpectedMediaItemsForRundownBaseline(expectedMediaItems: ExpectedMediaItemRundown[]): void - /** - * Set the ExpectedPackages for the baseline of this Rundown - * @param expectedPackages The new ExpectedPackages - */ - setExpectedPackagesForRundownBaseline(expectedPackages: ExpectedPackageForIngestModelBaseline[]): void + // /** + // * Set the ExpectedPackages for the baseline of this Rundown + // * @param expectedPackages The new ExpectedPackages + // */ + // setExpectedPackagesForRundownBaseline(expectedPackages: ExpectedPackageForIngestModelBaseline[]): void /** * Set the data for this Rundown. @@ -246,7 +239,8 @@ export interface IngestModel extends IngestModelReadonly, BaseModel { setRundownBaseline( timelineObjectsBlob: PieceTimelineObjectsBlob, adlibPieces: RundownBaselineAdLibItem[], - adlibActions: RundownBaselineAdLibAction[] + adlibActions: RundownBaselineAdLibAction[], + expectedPackages: ExpectedPackageDBNew[] ): Promise /** diff --git a/packages/job-worker/src/ingest/model/IngestPartModel.ts b/packages/job-worker/src/ingest/model/IngestPartModel.ts index 610be862c6..595c512c49 100644 --- a/packages/job-worker/src/ingest/model/IngestPartModel.ts +++ b/packages/job-worker/src/ingest/model/IngestPartModel.ts @@ -3,9 +3,9 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { ExpectedMediaItemRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedMediaItem' -import { ExpectedPackageFromRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { ExpectedPlayoutItemRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { IngestExpectedPackage } from './implementation/IngestExpectedPackage' export interface IngestPartModelReadonly { /** @@ -37,7 +37,7 @@ export interface IngestPartModelReadonly { /** * The ExpectedPackages belonging to this Part */ - readonly expectedPackages: ReadonlyDeep[] + readonly expectedPackages: ReadonlyDeep[] } /** * Wrap a Part and its contents in a view for Ingest operations @@ -61,9 +61,9 @@ export interface IngestPartModel extends IngestPartModelReadonly { */ setExpectedMediaItems(expectedMediaItems: ExpectedMediaItemRundown[]): void - /** - * Set the ExpectedPackages for the contents of this Part - * @param expectedPackages The new ExpectedPackages - */ - setExpectedPackages(expectedPackages: ExpectedPackageFromRundown[]): void + // /** + // * Set the ExpectedPackages for the contents of this Part + // * @param expectedPackages The new ExpectedPackages + // */ + // setExpectedPackages(expectedPackages: ExpectedPackageFromRundown[]): void } diff --git a/packages/job-worker/src/ingest/model/IngestSegmentModel.ts b/packages/job-worker/src/ingest/model/IngestSegmentModel.ts index d708cb5228..1291915398 100644 --- a/packages/job-worker/src/ingest/model/IngestSegmentModel.ts +++ b/packages/job-worker/src/ingest/model/IngestSegmentModel.ts @@ -6,6 +6,7 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' +import { PostProcessDocs } from '../../blueprints/postProcess' export interface IngestSegmentModelReadonly { /** @@ -81,14 +82,14 @@ export interface IngestSegmentModel extends IngestSegmentModelReadonly { * Replace or insert a Part into this Segment * @param part New part data * @param pieces Pieces to add to the Part - * @param adLibPiece AdLib Pieces to add to the Part + * @param adLibPieces AdLib Pieces to add to the Part * @param adLibActions AdLib Actions to add to the Part */ replacePart( part: IngestReplacePartType, - pieces: Piece[], - adLibPiece: AdLibPiece[], - adLibActions: AdLibAction[] + pieces: PostProcessDocs, + adLibPieces: PostProcessDocs, + adLibActions: PostProcessDocs ): IngestPartModel } diff --git a/packages/job-worker/src/ingest/model/implementation/ExpectedPackagesStore.ts b/packages/job-worker/src/ingest/model/implementation/ExpectedPackagesStore.ts index 10226d15bf..c1f4b81bbb 100644 --- a/packages/job-worker/src/ingest/model/implementation/ExpectedPackagesStore.ts +++ b/packages/job-worker/src/ingest/model/implementation/ExpectedPackagesStore.ts @@ -1,5 +1,5 @@ import { ExpectedMediaItemRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedMediaItem' -import { ExpectedPackageDBBase } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { ExpectedPackageDBNew } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { ExpectedPlayoutItemRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' import { ExpectedMediaItemId, @@ -11,11 +11,9 @@ import { } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ReadonlyDeep } from 'type-fest' import { diffAndReturnLatestObjects, DocumentChanges, getDocumentChanges, setValuesAndTrackChanges } from './utils' +import { IngestExpectedPackage } from './IngestExpectedPackage' -function mutateExpectedPackage( - oldObj: ExpectedPackageType, - newObj: ExpectedPackageType -): ExpectedPackageType { +function mutateExpectedPackage(oldObj: IngestExpectedPackage, newObj: IngestExpectedPackage): IngestExpectedPackage { return { ...newObj, // Retain the created property @@ -23,10 +21,10 @@ function mutateExpectedPackage { +export class ExpectedPackagesStore { #expectedMediaItems: ExpectedMediaItemRundown[] #expectedPlayoutItems: ExpectedPlayoutItemRundown[] - #expectedPackages: ExpectedPackageType[] + #expectedPackages: IngestExpectedPackage[] #expectedMediaItemsWithChanges = new Set() #expectedPlayoutItemsWithChanges = new Set() @@ -38,9 +36,8 @@ export class ExpectedPackagesStore { return this.#expectedPlayoutItems } - get expectedPackages(): ReadonlyDeep { - // Typescript is not happy with turning ExpectedPackageType into ReadonlyDeep because it can be a union - return this.#expectedPackages as any[] + get expectedPackages(): ReadonlyDeep { + return this.#expectedPackages } get hasChanges(): boolean { @@ -57,7 +54,7 @@ export class ExpectedPackagesStore { return getDocumentChanges(this.#expectedPlayoutItemsWithChanges, this.#expectedPlayoutItems) } - get expectedPackagesChanges(): DocumentChanges { + get expectedPackagesChanges(): DocumentChanges { return getDocumentChanges(this.#expectedPackagesWithChanges, this.#expectedPackages) } @@ -78,7 +75,7 @@ export class ExpectedPackagesStore): void { + compareToPreviousData(oldStore: ExpectedPackagesStore): void { // Diff the objects, but don't update the stored copies diffAndReturnLatestObjects( this.#expectedPlayoutItemsWithChanges, @@ -169,19 +167,19 @@ export class ExpectedPackagesStore ({ - ...pkg, - partId: this.#partId, - segmentId: this.#segmentId, - rundownId: this.#rundownId, - })) - - this.#expectedPackages = diffAndReturnLatestObjects( - this.#expectedPackagesWithChanges, - this.#expectedPackages, - newExpectedPackages, - mutateExpectedPackage - ) + setExpectedPackages(expectedPackages: IngestExpectedPackage[]): void { + // nocommit - the whole packages flow needs reimplementing + // const newExpectedPackages: ExpectedPackageDBNew[] = expectedPackages.map((pkg) => ({ + // ...pkg, + // partId: this.#partId, + // segmentId: this.#segmentId, + // rundownId: this.#rundownId, + // })) + // this.#expectedPackages = diffAndReturnLatestObjects( + // this.#expectedPackagesWithChanges, + // this.#expectedPackages, + // newExpectedPackages, + // mutateExpectedPackage + // ) } } diff --git a/packages/job-worker/src/ingest/model/implementation/IngestExpectedPackage.ts b/packages/job-worker/src/ingest/model/implementation/IngestExpectedPackage.ts new file mode 100644 index 0000000000..5281251fd7 --- /dev/null +++ b/packages/job-worker/src/ingest/model/implementation/IngestExpectedPackage.ts @@ -0,0 +1,21 @@ +import { ExpectedPackage } from '@sofie-automation/blueprints-integration' +import { + ExpectedPackageIngestSourcePart, + ExpectedPackageIngestSourceRundownBaseline, +} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { ExpectedPackageId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { Time } from 'superfly-timeline' +import { ReadonlyDeep } from 'type-fest' + +export interface IngestExpectedPackage { + _id: ExpectedPackageId + + /** Hash that changes whenever the content or version changes. See getContentVersionHash() */ + contentVersionHash: string + + created: Time + + package: ReadonlyDeep + + ingestSources: Array +} diff --git a/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts b/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts index f441921f1a..325126570a 100644 --- a/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts +++ b/packages/job-worker/src/ingest/model/implementation/IngestModelImpl.ts @@ -2,9 +2,11 @@ import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibActio import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { ExpectedMediaItemRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedMediaItem' import { - ExpectedPackageDB, + ExpectedPackageDBNew, ExpectedPackageDBType, - ExpectedPackageFromRundown, + ExpectedPackageIngestSource, + ExpectedPackageIngestSourcePart, + ExpectedPackageIngestSourceRundownBaseline, } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { ExpectedPlayoutItemRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' import { @@ -32,23 +34,20 @@ import { IngestSegmentModel } from '../IngestSegmentModel' import { IngestSegmentModelImpl } from './IngestSegmentModelImpl' import { IngestPartModel } from '../IngestPartModel' import { + assertNever, clone, Complete, deleteAllUndefinedProperties, getRandomId, groupByToMap, + groupByToMapFunc, literal, } from '@sofie-automation/corelib/dist/lib' import { IngestPartModelImpl } from './IngestPartModelImpl' import { DatabasePersistedModel } from '../../../modelBase' import { ExpectedPackagesStore } from './ExpectedPackagesStore' import { ReadonlyDeep } from 'type-fest' -import { - ExpectedPackageForIngestModel, - ExpectedPackageForIngestModelBaseline, - IngestModel, - IngestReplaceSegmentType, -} from '../IngestModel' +import { IngestModel, IngestReplaceSegmentType } from '../IngestModel' import { RundownNote } from '@sofie-automation/corelib/dist/dataModel/Notes' import { diffAndReturnLatestObjects } from './utils' import _ = require('underscore') @@ -60,6 +59,7 @@ import { SaveIngestModelHelper } from './SaveIngestModel' import { generateWriteOpsForLazyDocuments } from './DocumentChangeTracker' import { IS_PRODUCTION } from '../../../environment' import { logger } from '../../../logging' +import { IngestExpectedPackage } from './IngestExpectedPackage' export interface IngestModelImplExistingData { rundown: DBRundown @@ -70,7 +70,7 @@ export interface IngestModelImplExistingData { adLibActions: AdLibAction[] expectedMediaItems: ExpectedMediaItemRundown[] expectedPlayoutItems: ExpectedPlayoutItemRundown[] - expectedPackages: ExpectedPackageDB[] + expectedPackages: ExpectedPackageDBNew[] } /** @@ -113,7 +113,7 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { protected readonly segmentsImpl: Map - readonly #rundownBaselineExpectedPackagesStore: ExpectedPackagesStore + readonly #rundownBaselineExpectedPackagesStore: ExpectedPackagesStore get rundownBaselineTimelineObjects(): LazyInitialiseReadonly { // Return a simplified view of what we store, of just `timelineObjectsString` @@ -145,7 +145,7 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { get expectedPlayoutItemsForRundownBaseline(): ReadonlyDeep[] { return [...this.#rundownBaselineExpectedPackagesStore.expectedPlayoutItems] } - get expectedPackagesForRundownBaseline(): ReadonlyDeep[] { + get expectedPackagesForRundownBaseline(): ReadonlyDeep[] { return [...this.#rundownBaselineExpectedPackagesStore.expectedPackages] } @@ -172,18 +172,8 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { const groupedExpectedMediaItems = groupByToMap(existingData.expectedMediaItems, 'partId') const groupedExpectedPlayoutItems = groupByToMap(existingData.expectedPlayoutItems, 'partId') - const rundownExpectedPackages = existingData.expectedPackages.filter( - (pkg): pkg is ExpectedPackageFromRundown => - pkg.fromPieceType === ExpectedPackageDBType.PIECE || - pkg.fromPieceType === ExpectedPackageDBType.ADLIB_PIECE || - pkg.fromPieceType === ExpectedPackageDBType.ADLIB_ACTION - ) - const groupedExpectedPackages = groupByToMap(rundownExpectedPackages, 'partId') - const baselineExpectedPackages = existingData.expectedPackages.filter( - (pkg): pkg is ExpectedPackageForIngestModelBaseline => - pkg.fromPieceType === ExpectedPackageDBType.BASELINE_ADLIB_ACTION || - pkg.fromPieceType === ExpectedPackageDBType.BASELINE_ADLIB_PIECE || - pkg.fromPieceType === ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS + const { baselineExpectedPackages, groupedExpectedPackagesByPart } = groupExpectedPackages( + existingData.expectedPackages ) this.#rundownBaselineExpectedPackagesStore = new ExpectedPackagesStore( @@ -211,11 +201,11 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { groupedAdLibActions.get(part._id) ?? [], groupedExpectedMediaItems.get(part._id) ?? [], groupedExpectedPlayoutItems.get(part._id) ?? [], - groupedExpectedPackages.get(part._id) ?? [] + groupedExpectedPackagesByPart.get(part._id) ?? [] ) ) this.segmentsImpl.set(segment._id, { - segmentModel: new IngestSegmentModelImpl(false, segment, parts), + segmentModel: new IngestSegmentModelImpl(this.context, false, segment, parts), deleted: false, }) } @@ -340,24 +330,28 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { findAdlibPiece(adLibPieceId: PieceId): ReadonlyDeep | undefined { for (const part of this.getAllOrderedParts()) { for (const adlib of part.adLibPieces) { - if (adlib._id === adLibPieceId) return adlib + if (adlib._id === adLibPieceId) { + return adlib + } } } return undefined } - findExpectedPackage(packageId: ExpectedPackageId): ReadonlyDeep | undefined { + findExpectedPackageIngestSources(packageId: ExpectedPackageId): ReadonlyDeep[] { + const sources: ReadonlyDeep[] = [] + const baselinePackage = this.#rundownBaselineExpectedPackagesStore.expectedPackages.find( (pkg) => pkg._id === packageId ) - if (baselinePackage) return baselinePackage + if (baselinePackage) sources.push(...baselinePackage.ingestSources) for (const part of this.getAllOrderedParts()) { const partPackage = part.expectedPackages.find((pkg) => pkg._id === packageId) - if (partPackage) return partPackage + if (partPackage) sources.push(...partPackage.ingestSources) } - return undefined + return sources } removeSegment(id: SegmentId): void { @@ -375,7 +369,7 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { } const oldSegment = this.segmentsImpl.get(segment._id) - const newSegment = new IngestSegmentModelImpl(true, segment, [], oldSegment?.segmentModel) + const newSegment = new IngestSegmentModelImpl(this.context, true, segment, [], oldSegment?.segmentModel) this.segmentsImpl.set(segment._id, { segmentModel: newSegment, deleted: false, @@ -394,7 +388,12 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { this.segmentsImpl.set(oldId, { // Make a minimal clone of the old segment, the reference is needed to issue a mongo delete - segmentModel: new IngestSegmentModelImpl(false, clone(existingSegment.segmentModel.segment), []), + segmentModel: new IngestSegmentModelImpl( + this.context, + false, + clone(existingSegment.segmentModel.segment), + [] + ), deleted: true, }) @@ -410,10 +409,10 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { setExpectedMediaItemsForRundownBaseline(expectedMediaItems: ExpectedMediaItemRundown[]): void { this.#rundownBaselineExpectedPackagesStore.setExpectedMediaItems(expectedMediaItems) } - setExpectedPackagesForRundownBaseline(expectedPackages: ExpectedPackageForIngestModelBaseline[]): void { - // Future: should these be here, or held as part of each adlib? - this.#rundownBaselineExpectedPackagesStore.setExpectedPackages(expectedPackages) - } + // setExpectedPackagesForRundownBaseline(expectedPackages: ExpectedPackageForIngestModelBaseline[]): void { + // // Future: should these be here, or held as part of each adlib? + // this.#rundownBaselineExpectedPackagesStore.setExpectedPackages(expectedPackages) + // } setRundownData( rundownData: IBlueprintRundown, @@ -469,7 +468,8 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { async setRundownBaseline( timelineObjectsBlob: PieceTimelineObjectsBlob, adlibPieces: RundownBaselineAdLibItem[], - adlibActions: RundownBaselineAdLibAction[] + adlibActions: RundownBaselineAdLibAction[], + expectedPackages: ExpectedPackageDBNew[] ): Promise { const [loadedRundownBaselineObjs, loadedRundownBaselineAdLibPieces, loadedRundownBaselineAdLibActions] = await Promise.all([ @@ -517,6 +517,9 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { newAdlibActions ) ) + + // Future: should these be here, or held as part of each adlib? + this.#rundownBaselineExpectedPackagesStore.setExpectedPackages(expectedPackages) } setRundownOrphaned(orphaned: RundownOrphanedReason | undefined): void { @@ -680,3 +683,53 @@ export class IngestModelImpl implements IngestModel, DatabasePersistedModel { span?.end() } } + +function groupExpectedPackages(expectedPackages: ExpectedPackageDBNew[]) { + const baselineExpectedPackages: ExpectedPackageDBNew[] = [] + const groupedExpectedPackagesByPart = new Map() + + for (const expectedPackage of expectedPackages) { + const baselineIngestSources: ExpectedPackageIngestSourceRundownBaseline[] = [] + const rundownIngestSources: ExpectedPackageIngestSourcePart[] = [] + + for (const src of expectedPackage.ingestSources) { + switch (src.fromPieceType) { + case ExpectedPackageDBType.BASELINE_ADLIB_ACTION: + case ExpectedPackageDBType.BASELINE_ADLIB_PIECE: + case ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS: + baselineIngestSources.push(src) + break + case ExpectedPackageDBType.PIECE: + case ExpectedPackageDBType.ADLIB_PIECE: + case ExpectedPackageDBType.ADLIB_ACTION: + rundownIngestSources.push(src) + break + default: + assertNever(src) + break + } + } + + if (baselineIngestSources.length > 0) { + baselineExpectedPackages.push({ + ...expectedPackage, + ingestSources: baselineIngestSources, + }) + } + + const sourcesByPartId = groupByToMapFunc(rundownIngestSources, (src) => src.partId) + for (const [partId, sources] of sourcesByPartId.entries()) { + const partPackages = groupedExpectedPackagesByPart.get(partId) ?? [] + partPackages.push({ + ...expectedPackage, + ingestSources: sources, + }) + groupedExpectedPackagesByPart.set(partId, partPackages) + } + } + + return { + baselineExpectedPackages, + groupedExpectedPackagesByPart, + } +} diff --git a/packages/job-worker/src/ingest/model/implementation/IngestPartModelImpl.ts b/packages/job-worker/src/ingest/model/implementation/IngestPartModelImpl.ts index a830b34cb4..24ceb95800 100644 --- a/packages/job-worker/src/ingest/model/implementation/IngestPartModelImpl.ts +++ b/packages/job-worker/src/ingest/model/implementation/IngestPartModelImpl.ts @@ -7,7 +7,7 @@ import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { ExpectedMediaItemRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedMediaItem' import { ExpectedPlayoutItemRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' -import { ExpectedPackageFromRundown } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { ExpectedPackageDBNew } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { ExpectedPackagesStore } from './ExpectedPackagesStore' import { @@ -17,13 +17,14 @@ import { getDocumentChanges, setValuesAndTrackChanges, } from './utils' +import { IngestExpectedPackage } from './IngestExpectedPackage' export class IngestPartModelImpl implements IngestPartModel { readonly partImpl: DBPart readonly #pieces: Piece[] readonly #adLibPieces: AdLibPiece[] readonly #adLibActions: AdLibAction[] - readonly expectedPackagesStore: ExpectedPackagesStore + readonly expectedPackagesStore: ExpectedPackagesStore #setPartValue(key: T, newValue: DBPart[T]): void { if (newValue === undefined) { @@ -90,7 +91,7 @@ export class IngestPartModelImpl implements IngestPartModel { get expectedPlayoutItems(): ReadonlyDeep[] { return [...this.expectedPackagesStore.expectedPlayoutItems] } - get expectedPackages(): ReadonlyDeep[] { + get expectedPackages(): ReadonlyDeep[] { return [...this.expectedPackagesStore.expectedPackages] } @@ -140,7 +141,7 @@ export class IngestPartModelImpl implements IngestPartModel { adLibActions: AdLibAction[], expectedMediaItems: ExpectedMediaItemRundown[], expectedPlayoutItems: ExpectedPlayoutItemRundown[], - expectedPackages: ExpectedPackageFromRundown[] + expectedPackages: ExpectedPackageDBNew[] ) { this.partImpl = part this.#pieces = pieces @@ -234,8 +235,8 @@ export class IngestPartModelImpl implements IngestPartModel { setExpectedMediaItems(expectedMediaItems: ExpectedMediaItemRundown[]): void { this.expectedPackagesStore.setExpectedMediaItems(expectedMediaItems) } - setExpectedPackages(expectedPackages: ExpectedPackageFromRundown[]): void { - // Future: should these be here, or held as part of each adlib/piece? - this.expectedPackagesStore.setExpectedPackages(expectedPackages) - } + // setExpectedPackages(expectedPackages: ExpectedPackageFromRundown[]): void { + // // Future: should these be here, or held as part of each adlib/piece? + // this.expectedPackagesStore.setExpectedPackages(expectedPackages) + // } } diff --git a/packages/job-worker/src/ingest/model/implementation/IngestSegmentModelImpl.ts b/packages/job-worker/src/ingest/model/implementation/IngestSegmentModelImpl.ts index aae742db65..c4e4389a91 100644 --- a/packages/job-worker/src/ingest/model/implementation/IngestSegmentModelImpl.ts +++ b/packages/job-worker/src/ingest/model/implementation/IngestSegmentModelImpl.ts @@ -1,4 +1,4 @@ -import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ExpectedPackageId, PartId, RundownId, SegmentId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ReadonlyDeep } from 'type-fest' import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { IngestReplacePartType, IngestSegmentModel } from '../IngestSegmentModel' @@ -12,6 +12,13 @@ import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { calculatePartExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' import { clone } from '@sofie-automation/corelib/dist/lib' import { getPartId } from '../../lib' +import { + ExpectedPackageDBNew, + ExpectedPackageDBType, + getContentVersionHash, +} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { JobContext } from '../../../jobs' +import { PostProcessDocs } from '../../../blueprints/postProcess' /** * A light wrapper around the IngestPartModel, so that we can track the deletions while still accessing the contents @@ -22,6 +29,7 @@ interface PartWrapper { } export class IngestSegmentModelImpl implements IngestSegmentModel { + readonly #context: JobContext readonly segmentImpl: DBSegment readonly partsImpl: Map @@ -105,11 +113,14 @@ export class IngestSegmentModelImpl implements IngestSegmentModel { } constructor( + context: JobContext, isBeingCreated: boolean, segment: DBSegment, currentParts: IngestPartModelImpl[], previousSegment?: IngestSegmentModelImpl ) { + this.#context = context + currentParts.sort((a, b) => a.part._rank - b.part._rank) this.#segmentHasChanges = isBeingCreated @@ -206,16 +217,16 @@ export class IngestSegmentModelImpl implements IngestSegmentModel { replacePart( rawPart: IngestReplacePartType, - pieces: Piece[], - adLibPiece: AdLibPiece[], - adLibActions: AdLibAction[] + pieces: PostProcessDocs, + adLibPieces: PostProcessDocs, + adLibActions: PostProcessDocs ): IngestPartModel { const part: DBPart = { ...rawPart, _id: this.getPartIdFromExternalId(rawPart.externalId), rundownId: this.segment.rundownId, segmentId: this.segment._id, - expectedDurationWithTransition: calculatePartExpectedDurationWithTransition(rawPart, pieces), + expectedDurationWithTransition: calculatePartExpectedDurationWithTransition(rawPart, pieces.docs), } // We don't need to worry about this being present on other Segments. The caller must make sure it gets removed if needed, @@ -224,15 +235,25 @@ export class IngestSegmentModelImpl implements IngestSegmentModel { const oldPart = this.partsImpl.get(part._id) + const expectedPackages = generateExpectedPackagesForPart( + this.#context.studioId, + part.rundownId, + part.segmentId, + part._id, + pieces, + adLibPieces, + adLibActions + ) + const partModel = new IngestPartModelImpl( !oldPart, clone(part), - clone(pieces), - clone(adLibPiece), - clone(adLibActions), + clone(pieces.docs), + clone(adLibPieces.docs), + clone(adLibActions.docs), [], [], - [] + expectedPackages ) partModel.setOwnerIds(this.segment.rundownId, this.segment._id) @@ -243,3 +264,88 @@ export class IngestSegmentModelImpl implements IngestSegmentModel { return partModel } } + +function generateExpectedPackagesForPart( + studioId: StudioId, + rundownId: RundownId, + segmentId: SegmentId, + partId: PartId, + pieces: PostProcessDocs, + adLibPieces: PostProcessDocs, + adLibActions: PostProcessDocs +): ExpectedPackageDBNew[] { + const packages = new Map() + + // Generate the full version of each expectedPackage + const allRawPackages = [ + ...pieces.expectedPackages, + ...adLibPieces.expectedPackages, + ...adLibActions.expectedPackages, + ] + for (const [packageId, expectedPackage] of allRawPackages) { + if (packages.has(packageId)) continue + + packages.set(packageId, { + _id: packageId, + + studioId, + rundownId, + + contentVersionHash: getContentVersionHash(expectedPackage), + + created: Date.now(), // nocommit - avoid churn on this? + + package: expectedPackage, + + ingestSources: [], + + playoutSources: { + // nocommit - avoid this here? + pieceInstanceIds: [], + }, + }) + } + + // Populate the ingestSources + for (const piece of pieces.docs) { + for (const expectedPackage of piece.expectedPackages) { + const expectedPackageDoc = packages.get(expectedPackage.expectedPackageId) + if (!expectedPackageDoc) continue // nocommit - log error? this should never happen + + expectedPackageDoc.ingestSources.push({ + fromPieceType: ExpectedPackageDBType.PIECE, + pieceId: piece._id, + partId: partId, + segmentId: segmentId, + }) + } + } + for (const piece of adLibPieces.docs) { + for (const expectedPackage of piece.expectedPackages) { + const expectedPackageDoc = packages.get(expectedPackage.expectedPackageId) + if (!expectedPackageDoc) continue // nocommit - log error? this should never happen + + expectedPackageDoc.ingestSources.push({ + fromPieceType: ExpectedPackageDBType.ADLIB_PIECE, + pieceId: piece._id, + partId: partId, + segmentId: segmentId, + }) + } + } + for (const piece of adLibActions.docs) { + for (const expectedPackage of piece.expectedPackages) { + const expectedPackageDoc = packages.get(expectedPackage.expectedPackageId) + if (!expectedPackageDoc) continue // nocommit - log error? this should never happen + + expectedPackageDoc.ingestSources.push({ + fromPieceType: ExpectedPackageDBType.ADLIB_ACTION, + pieceId: piece._id, + partId: partId, + segmentId: segmentId, + }) + } + } + + return Array.from(packages.values()) +} diff --git a/packages/job-worker/src/ingest/model/implementation/SaveIngestModel.ts b/packages/job-worker/src/ingest/model/implementation/SaveIngestModel.ts index c2995869cf..59afb3bfe5 100644 --- a/packages/job-worker/src/ingest/model/implementation/SaveIngestModel.ts +++ b/packages/job-worker/src/ingest/model/implementation/SaveIngestModel.ts @@ -1,9 +1,8 @@ import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' import { ExpectedMediaItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedMediaItem' -import { ExpectedPackageDB } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { ExpectedPackageDBNew } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { ExpectedPlayoutItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' -import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' @@ -13,7 +12,7 @@ import { IngestSegmentModelImpl } from './IngestSegmentModelImpl' import { DocumentChangeTracker } from './DocumentChangeTracker' export class SaveIngestModelHelper { - #expectedPackages = new DocumentChangeTracker() + #expectedPackages = new DocumentChangeTracker() #expectedPlayoutItems = new DocumentChangeTracker() #expectedMediaItems = new DocumentChangeTracker() @@ -23,10 +22,7 @@ export class SaveIngestModelHelper { #adLibPieces = new DocumentChangeTracker() #adLibActions = new DocumentChangeTracker() - addExpectedPackagesStore( - store: ExpectedPackagesStore, - deleteAll?: boolean - ): void { + addExpectedPackagesStore(store: ExpectedPackagesStore, deleteAll?: boolean): void { this.#expectedPackages.addChanges(store.expectedPackagesChanges, deleteAll ?? false) this.#expectedPlayoutItems.addChanges(store.expectedPlayoutItemsChanges, deleteAll ?? false) this.#expectedMediaItems.addChanges(store.expectedMediaItemsChanges, deleteAll ?? false) @@ -56,7 +52,8 @@ export class SaveIngestModelHelper { commit(context: JobContext): Array> { return [ - context.directCollections.ExpectedPackages.bulkWrite(this.#expectedPackages.generateWriteOps()), + // nocommit - reimplement this save + // context.directCollections.ExpectedPackages.bulkWrite(this.#expectedPackages.generateWriteOps()), context.directCollections.ExpectedPlayoutItems.bulkWrite(this.#expectedPlayoutItems.generateWriteOps()), context.directCollections.ExpectedMediaItems.bulkWrite(this.#expectedMediaItems.generateWriteOps()), diff --git a/packages/job-worker/src/ingest/packageInfo.ts b/packages/job-worker/src/ingest/packageInfo.ts index a669f65d19..47e4e1755f 100644 --- a/packages/job-worker/src/ingest/packageInfo.ts +++ b/packages/job-worker/src/ingest/packageInfo.ts @@ -8,8 +8,8 @@ import { logger } from '../logging' import { JobContext } from '../jobs' import { regenerateSegmentsFromIngestData } from './generationSegment' import { UpdateIngestRundownAction, runIngestJob, runWithRundownLock } from './lock' -import { updateExpectedPackagesForPartModel, updateExpectedPackagesForRundownBaseline } from './expectedPackages' import { loadIngestModelFromRundown } from './model/implementation/LoadIngestModel' +import { assertNever } from '@sofie-automation/corelib/dist/lib' /** * Debug: Regenerate ExpectedPackages for a Rundown @@ -23,11 +23,13 @@ export async function handleExpectedPackagesRegenerate( const ingestModel = await loadIngestModelFromRundown(context, rundownLock, rundown) - for (const part of ingestModel.getAllOrderedParts()) { - updateExpectedPackagesForPartModel(context, part) - } + // nocommit - reimplement? + + // for (const part of ingestModel.getAllOrderedParts()) { + // updateExpectedMediaAndPlayoutItemsForPartModel(context, part) + // } - await updateExpectedPackagesForRundownBaseline(context, ingestModel, undefined, true) + // await updateExpectedPackagesForRundownBaseline(context, ingestModel, undefined, true) await ingestModel.saveAllToDatabase() }) @@ -64,23 +66,26 @@ export async function handleUpdatedPackageInfoForRundown( let regenerateRundownBaseline = false for (const packageId of data.packageIds) { - const pkg = ingestModel.findExpectedPackage(packageId) - if (pkg) { - if ( - pkg.fromPieceType === ExpectedPackageDBType.PIECE || - pkg.fromPieceType === ExpectedPackageDBType.ADLIB_PIECE || - pkg.fromPieceType === ExpectedPackageDBType.ADLIB_ACTION - ) { - segmentsToUpdate.add(pkg.segmentId) - } else if ( - pkg.fromPieceType === ExpectedPackageDBType.BASELINE_ADLIB_ACTION || - pkg.fromPieceType === ExpectedPackageDBType.BASELINE_ADLIB_PIECE || - pkg.fromPieceType === ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS - ) { - regenerateRundownBaseline = true + const pkgIngestSources = ingestModel.findExpectedPackageIngestSources(packageId) + for (const source of pkgIngestSources) { + switch (source.fromPieceType) { + case ExpectedPackageDBType.PIECE: + case ExpectedPackageDBType.ADLIB_PIECE: + case ExpectedPackageDBType.ADLIB_ACTION: + segmentsToUpdate.add(source.segmentId) + break + + case ExpectedPackageDBType.BASELINE_ADLIB_ACTION: + case ExpectedPackageDBType.BASELINE_ADLIB_PIECE: + case ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS: + regenerateRundownBaseline = true + break + default: + assertNever(source) } - } else { - logger.warn(`onUpdatedPackageInfoForRundown: Missing package: "${packageId}"`) + } + if (pkgIngestSources.length === 0) { + logger.warn(`onUpdatedPackageInfoForRundown: Missing ingestSources for package: "${packageId}"`) } } diff --git a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts index 4c3981c4ca..2a477edd15 100644 --- a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts +++ b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts @@ -32,6 +32,7 @@ import { import { validateAdlibTestingPartInstanceProperties } from '../playout/adlibTesting' import { ReadonlyDeep } from 'type-fest' import { convertIngestModelToPlayoutRundownWithSegments } from './commit' +import { unwrapExpectedPackages } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' type PlayStatus = 'previous' | 'current' | 'next' type SyncedInstance = { @@ -129,10 +130,19 @@ export async function syncChangesToPartInstances( const partId = existingPartInstance.partInstance.part._id const existingResultPartInstance: BlueprintSyncIngestPartInstance = { partInstance: convertPartInstanceToBlueprints(existingPartInstance.partInstance), - pieceInstances: pieceInstancesInPart.map((p) => convertPieceInstanceToBlueprints(p.pieceInstance)), + pieceInstances: pieceInstancesInPart.map((p) => + convertPieceInstanceToBlueprints( + p.pieceInstance, + // nocommit - make sure the expectedPackages are loaded? + playoutModel.expectedPackages.getPackagesForPieceInstance( + p.pieceInstance.rundownId, + p.pieceInstance._id + ) + ) + ), } - const proposedPieceInstances = getPieceInstancesForPart( + const proposedPieceInstances = await getPieceInstancesForPart( context, playoutModel, previousPartInstance, @@ -149,12 +159,24 @@ export async function syncChangesToPartInstances( const referencedAdlibs: IBlueprintAdLibPieceDB[] = [] for (const adLibPieceId of _.compact(pieceInstancesInPart.map((p) => p.pieceInstance.adLibSourceId))) { const adLibPiece = ingestModel.findAdlibPiece(adLibPieceId) - if (adLibPiece) referencedAdlibs.push(convertAdLibPieceToBlueprints(adLibPiece)) + if (adLibPiece) + referencedAdlibs.push( + convertAdLibPieceToBlueprints( + adLibPiece, + unwrapExpectedPackages(adLibPiece.expectedPackages) + ) + ) } const newResultData: BlueprintSyncIngestNewData = { part: newPart ? convertPartToBlueprints(newPart) : undefined, - pieceInstances: proposedPieceInstances.map(convertPieceInstanceToBlueprints), + pieceInstances: proposedPieceInstances.map((p) => + convertPieceInstanceToBlueprints( + p, + // nocommit - make sure the expectedPackages are loaded? + playoutModel.expectedPackages.getPackagesForPieceInstance(p.rundownId, p._id) + ) + ), adLibPieces: newPart && ingestPart ? ingestPart.adLibPieces.map(convertAdLibPieceToBlueprints) : [], actions: newPart && ingestPart ? ingestPart.adLibActions.map(convertAdLibActionToBlueprints) : [], referencedAdlibs: referencedAdlibs, @@ -162,6 +184,8 @@ export async function syncChangesToPartInstances( const partInstanceSnapshot = existingPartInstance.snapshotMakeCopy() + const expectedPackagesSnapshot = playoutModel.expectedPackages.snapshotMakeCopy() + const syncContext = new SyncIngestUpdateToPartInstanceContext( context, { @@ -173,6 +197,7 @@ export async function syncChangesToPartInstances( playoutRundownModel.rundown, existingPartInstance, proposedPieceInstances, + playoutModel.expectedPackages, playStatus ) // TODO - how can we limit the frequency we run this? (ie, how do we know nothing affecting this has changed) @@ -189,6 +214,7 @@ export async function syncChangesToPartInstances( // Operation failed, rollback the changes existingPartInstance.snapshotRestore(partInstanceSnapshot) + playoutModel.expectedPackages.snapshotRestore(expectedPackagesSnapshot) } if (playStatus === 'next') { diff --git a/packages/job-worker/src/playout/adlibJobs.ts b/packages/job-worker/src/playout/adlibJobs.ts index 31eeb8382c..f4a040ec8e 100644 --- a/packages/job-worker/src/playout/adlibJobs.ts +++ b/packages/job-worker/src/playout/adlibJobs.ts @@ -23,13 +23,21 @@ import { syncPlayheadInfinitesForNextPartInstance } from './infinites' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { PieceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { IBlueprintDirectPlayType, IBlueprintPieceType } from '@sofie-automation/blueprints-integration' +import { + ExpectedPackage, + IBlueprintDirectPlayType, + IBlueprintPieceType, +} from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' import { WatchedPackagesHelper } from '../blueprints/context/watchedPackages' import { innerFindLastPieceOnLayer, innerStartOrQueueAdLibPiece, innerStopPieces } from './adlibUtils' import _ = require('underscore') import { executeActionInner } from './adlibAction' import { PlayoutPieceInstanceModel } from './model/PlayoutPieceInstanceModel' +import { + ExpectedPackageDBType, + unwrapExpectedPackages, +} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' /** * Play an existing Piece in the Rundown as an AdLib @@ -88,16 +96,26 @@ export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakeP } switch (pieceToCopy.allowDirectPlay.type) { - case IBlueprintDirectPlayType.AdLibPiece: + case IBlueprintDirectPlayType.AdLibPiece: { + const expectedPackages = pieceInstanceToCopy + ? pieceInstanceToCopy.pieceInstance.expectedPackages + : await context.directCollections.ExpectedPackages.findFetch({ + fromPieceType: ExpectedPackageDBType.PIECE, + fromPieceId: pieceToCopy._id, + rundownId: { $in: rundownIds }, + }) + await pieceTakeNowAsAdlib( context, playoutModel, showStyleCompound, currentPartInstance, pieceToCopy, + unwrapExpectedPackages(expectedPackages), pieceInstanceToCopy ) break + } case IBlueprintDirectPlayType.AdLibAction: { const executeProps = pieceToCopy.allowDirectPlay @@ -137,10 +155,13 @@ async function pieceTakeNowAsAdlib( showStyleBase: ReadonlyDeep, currentPartInstance: PlayoutPartInstanceModel, pieceToCopy: PieceInstancePiece, + expectedPackages: ReadonlyDeep, pieceInstanceToCopy: | { partInstance: PlayoutPartInstanceModel; pieceInstance: PlayoutPieceInstanceModel } | undefined ): Promise { + playoutModel.expectedPackages.createPackagesIfMissing(currentPartInstance.partInstance.rundownId, expectedPackages) // nocommit - what if the genericAdlibPiece doesn't quite match the set of packages? + const genericAdlibPiece = convertAdLibToGenericPiece(pieceToCopy, false) /*const newPieceInstance = */ currentPartInstance.insertAdlibbedPiece(genericAdlibPiece, pieceToCopy._id) @@ -222,16 +243,29 @@ export async function handleAdLibPieceStart(context: JobContext, data: AdlibPiec .map((r) => r.rundown._id) let adLibPiece: AdLibPiece | BucketAdLib | undefined + let expectedPackages: ReadonlyDeep if (data.pieceType === 'baseline') { adLibPiece = await context.directCollections.RundownBaselineAdLibPieces.findOne({ _id: data.adLibPieceId, rundownId: { $in: safeRundownIds }, }) + const rawExpectedPackages = await context.directCollections.ExpectedPackages.findFetch({ + fromPieceType: ExpectedPackageDBType.BASELINE_ADLIB_PIECE, + fromPieceId: data.adLibPieceId, + rundownId: { $in: safeRundownIds }, + }) + expectedPackages = unwrapExpectedPackages(rawExpectedPackages) } else if (data.pieceType === 'normal') { adLibPiece = await context.directCollections.AdLibPieces.findOne({ _id: data.adLibPieceId, rundownId: { $in: safeRundownIds }, }) + const rawExpectedPackages = await context.directCollections.ExpectedPackages.findFetch({ + fromPieceType: ExpectedPackageDBType.ADLIB_PIECE, + fromPieceId: data.adLibPieceId, + rundownId: { $in: safeRundownIds }, + }) + expectedPackages = unwrapExpectedPackages(rawExpectedPackages) } else if (data.pieceType === 'bucket') { const bucketAdlib = await context.directCollections.BucketAdLibPieces.findOne({ _id: data.adLibPieceId, @@ -248,6 +282,12 @@ export async function handleAdLibPieceStart(context: JobContext, data: AdlibPiec } adLibPiece = bucketAdlib + expectedPackages = bucketAdlib?.expectedPackages ?? [] + } else { + throw UserError.from( + new Error(`AdLib type "${data.pieceType}" not supported!`), + UserErrorMessage.AdlibNotFound + ) } if (!adLibPiece) @@ -266,7 +306,15 @@ export async function handleAdLibPieceStart(context: JobContext, data: AdlibPiec UserErrorMessage.AdlibUnplayable ) - await innerStartOrQueueAdLibPiece(context, playoutModel, rundown, !!data.queue, partInstance, adLibPiece) + await innerStartOrQueueAdLibPiece( + context, + playoutModel, + rundown, + !!data.queue, + partInstance, + adLibPiece, + expectedPackages + ) } ) } @@ -316,8 +364,22 @@ export async function handleStartStickyPieceOnSourceLayer( throw UserError.create(UserErrorMessage.SourceLayerStickyNothingFound) } + // Ensure that any referenced packages are loaded into memory + await playoutModel.expectedPackages.ensurePackagesAreLoaded(lastPieceInstance.piece.expectedPackages) + const lastPiece = convertPieceToAdLibPiece(context, lastPieceInstance.piece) - await innerStartOrQueueAdLibPiece(context, playoutModel, rundown, false, currentPartInstance, lastPiece) + await innerStartOrQueueAdLibPiece( + context, + playoutModel, + rundown, + false, + currentPartInstance, + lastPiece, + playoutModel.expectedPackages.getPackagesForPieceInstance( + lastPieceInstance.rundownId, + lastPieceInstance._id + ) + ) } ) } diff --git a/packages/job-worker/src/playout/adlibUtils.ts b/packages/job-worker/src/playout/adlibUtils.ts index 1aea41cc52..14cf9fad72 100644 --- a/packages/job-worker/src/playout/adlibUtils.ts +++ b/packages/job-worker/src/playout/adlibUtils.ts @@ -17,7 +17,7 @@ import { import { convertAdLibToGenericPiece } from './pieces' import { getResolvedPiecesForCurrentPartInstance } from './resolvedPieces' import { updateTimeline } from './timeline/generate' -import { PieceLifespan } from '@sofie-automation/blueprints-integration' +import { ExpectedPackage, PieceLifespan } from '@sofie-automation/blueprints-integration' import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { updatePartInstanceRanksAfterAdlib } from '../updatePartInstanceRanksAndOrphanedState' import { setNextPart } from './setNext' @@ -34,7 +34,8 @@ export async function innerStartOrQueueAdLibPiece( rundown: PlayoutRundownModel, queue: boolean, currentPartInstance: PlayoutPartInstanceModel, - adLibPiece: AdLibPiece | BucketAdLib + adLibPiece: AdLibPiece | BucketAdLib, + expectedPackages: ReadonlyDeep ): Promise { const span = context.startSpan('innerStartOrQueueAdLibPiece') let queuedPartInstanceId: PartInstanceId | undefined @@ -48,6 +49,11 @@ export async function innerStartOrQueueAdLibPiece( expectedDurationWithTransition: adLibPiece.expectedDuration, // Filled in later } + playoutModel.expectedPackages.createPackagesIfMissing( + currentPartInstance.partInstance.rundownId, + expectedPackages + ) // nocommit - what if the genericAdlibPiece doesn't quite match the set of packages? + const genericAdlibPiece = convertAdLibToGenericPiece(adLibPiece, true) const newPartInstance = await insertQueuedPartWithPieces( context, @@ -62,6 +68,11 @@ export async function innerStartOrQueueAdLibPiece( // syncPlayheadInfinitesForNextPartInstance is handled by setNextPart } else { + playoutModel.expectedPackages.createPackagesIfMissing( + currentPartInstance.partInstance.rundownId, + expectedPackages + ) // nocommit - what if the adLibPiece doesn't quite match the set of packages? + const genericAdlibPiece = convertAdLibToGenericPiece(adLibPiece, false) currentPartInstance.insertAdlibbedPiece(genericAdlibPiece, adLibPiece._id) @@ -180,9 +191,9 @@ export async function innerFindLastScriptedPieceOnLayer( } const fullPiece = await context.directCollections.Pieces.findOne(piece._id) - if (!fullPiece) return if (span) span.end() + return fullPiece } @@ -225,7 +236,7 @@ export async function insertQueuedPartWithPieces( // Find any rundown defined infinites that we should inherit const possiblePieces = await fetchPiecesThatMayBeActiveForPart(context, playoutModel, undefined, newPartFull) - const infinitePieceInstances = getPieceInstancesForPart( + const infinitePieceInstances = await getPieceInstancesForPart( context, playoutModel, currentPartInstance, diff --git a/packages/job-worker/src/playout/bucketAdlibJobs.ts b/packages/job-worker/src/playout/bucketAdlibJobs.ts index a6cbff8248..784e700442 100644 --- a/packages/job-worker/src/playout/bucketAdlibJobs.ts +++ b/packages/job-worker/src/playout/bucketAdlibJobs.ts @@ -53,7 +53,8 @@ export async function handleExecuteBucketAdLibOrAction( fullRundown, !!bucketAdLib.toBeQueued, partInstance, - bucketAdLib + bucketAdLib, + bucketAdLib.expectedPackages ?? [] ) await playoutModel.saveAllToDatabase() return {} diff --git a/packages/job-worker/src/playout/infinites.ts b/packages/job-worker/src/playout/infinites.ts index 5924cb4a5c..7dc1215dec 100644 --- a/packages/job-worker/src/playout/infinites.ts +++ b/packages/job-worker/src/playout/infinites.ts @@ -296,7 +296,7 @@ export async function syncPlayheadInfinitesForNextPartInstance( * @param newInstanceId Id of the PartInstance * @returns Array of PieceInstances for the specified PartInstance */ -export function getPieceInstancesForPart( +export async function getPieceInstancesForPart( context: JobContext, playoutModel: PlayoutModel, playingPartInstance: PlayoutPartInstanceModel | null, @@ -304,7 +304,7 @@ export function getPieceInstancesForPart( part: ReadonlyDeep, possiblePieces: ReadonlyDeep[], newInstanceId: PartInstanceId -): PieceInstance[] { +): Promise { const span = context.startSpan('getPieceInstancesForPart') const { partsToReceiveOnSegmentEndFrom, segmentsToReceiveOnRundownEndFrom, rundownsToReceiveOnShowStyleEndFrom } = getIdsBeforeThisPart(context, playoutModel, part) @@ -354,6 +354,7 @@ export function getPieceInstancesForPart( nextPartIsAfterCurrentPart, false ) + if (span) span.end() return res } diff --git a/packages/job-worker/src/playout/model/PlayoutExpectedPackagesModel.ts b/packages/job-worker/src/playout/model/PlayoutExpectedPackagesModel.ts new file mode 100644 index 0000000000..73f7b8d2a1 --- /dev/null +++ b/packages/job-worker/src/playout/model/PlayoutExpectedPackagesModel.ts @@ -0,0 +1,56 @@ +import { ExpectedPackage } from '@sofie-automation/blueprints-integration' +import { + ExpectedPackageId, + PartInstanceId, + PieceInstanceId, + RundownId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PieceExpectedPackage } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { ReadonlyDeep } from 'type-fest' + +/** + * Token returned when making a backup copy of a PlayoutExpectedPackagesModel + * The contents of this type is opaque and will vary fully across implementations + */ +export interface PlayoutExpectedPackagesModelSnapshot { + __isPlayoutExpectedPackagesModelBackup: true +} + +export interface PlayoutExpectedPackagesModelReadonly { + getPackagesForPieceInstance( + rundownId: RundownId, + pieceInstanceId: PieceInstanceId + ): ReadonlyDeep[] +} + +export interface PlayoutExpectedPackagesModel extends PlayoutExpectedPackagesModelReadonly { + /** + * Take a snapshot of the current state of this PlayoutExpectedPackagesModel + * This can be restored with `snapshotRestore` to rollback to a previous state of the model + */ + snapshotMakeCopy(): PlayoutExpectedPackagesModelSnapshot + + /** + * Restore a snapshot of this PlayoutExpectedPackagesModel, to rollback to a previous state + * Note: It is only possible to restore each snapshot once. + * Note: Any references to child documents may no longer be valid after this operation + * @param snapshot Snapshot to restore + */ + snapshotRestore(snapshot: PlayoutExpectedPackagesModelSnapshot): void + + ensurePackagesAreLoaded(expectedPackages: PieceExpectedPackage[]): Promise + + createPackagesIfMissing(rundownId: RundownId, expectedPackages: ReadonlyDeep): void + + createPackagesIfMissingFromMap( + rundownId: RundownId, + expectedPackages: ReadonlyMap> + ): void + + setPieceInstanceReferenceToPackages( + rundownId: RundownId, + partInstanceId: PartInstanceId, + pieceInstanceId: PieceInstanceId, + expectedPackageIds: ExpectedPackageId[] + ): void +} diff --git a/packages/job-worker/src/playout/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index 93390558ee..ebf31d3113 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -26,6 +26,7 @@ import { PlayoutPartInstanceModel } from './PlayoutPartInstanceModel' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { PlayoutPieceInstanceModel } from './PlayoutPieceInstanceModel' +import { PlayoutExpectedPackagesModel, PlayoutExpectedPackagesModelReadonly } from './PlayoutExpectedPackagesModel' export type DeferredFunction = (playoutModel: PlayoutModel) => void | Promise export type DeferredAfterSaveFunction = (playoutModel: PlayoutModelReadonly) => void | Promise @@ -81,6 +82,8 @@ export interface PlayoutModelReadonly extends StudioPlayoutModelBaseReadonly { */ readonly playlistLock: PlaylistLock + readonly expectedPackages: PlayoutExpectedPackagesModelReadonly + /** * The RundownPlaylist this PlayoutModel operates for */ @@ -176,6 +179,8 @@ export interface PlayoutModelReadonly extends StudioPlayoutModelBaseReadonly { * A view of a `RundownPlaylist` and its content in a `Studio` */ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBase, BaseModel { + readonly expectedPackages: PlayoutExpectedPackagesModel + /** * Temporary hack for debug logging */ diff --git a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts index 769c911a7e..97724462e9 100644 --- a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts @@ -22,6 +22,8 @@ import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/Perip import { PlayoutModel, PlayoutModelPreInit } from '../PlayoutModel' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownBaselineObj } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineObj' +import { PlayoutExpectedPackagesModelImpl } from './PlayoutExpectedPackagesModelImpl' +import { ExpectedPackageDBNew } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' /** * Load a PlayoutModelPreInit for the given RundownPlaylist @@ -87,12 +89,18 @@ export async function createPlayoutModelFromIngestModel( const [peripheralDevices, playlist, rundowns] = await loadInitData(context, loadedPlaylist, false, newRundowns) const rundownIds = rundowns.map((r) => r._id) - const [partInstances, rundownsWithContent, timeline] = await Promise.all([ - loadPartInstances(context, loadedPlaylist, rundownIds), + const expectedPackages = new PlayoutExpectedPackagesModelImpl() + // nocommit - populate some content from the ingestModel + + const [partInstances, rundownsWithContent, timeline, expectedPackagesDocs] = await Promise.all([ + loadPartInstances(context, expectedPackages, loadedPlaylist, rundownIds), loadRundowns(context, ingestModel, rundowns), loadTimeline(context), + loadExpectedPackages(context), ]) + expectedPackages.populateWithPackages(expectedPackagesDocs) + const res = new PlayoutModelImpl( context, playlistLock, @@ -101,7 +109,8 @@ export async function createPlayoutModelFromIngestModel( playlist, partInstances, rundownsWithContent, - timeline + timeline, + expectedPackages ) return res @@ -150,12 +159,17 @@ export async function createPlayoutModelfromInitModel( const rundownIds = initModel.rundowns.map((r) => r._id) - const [partInstances, rundownsWithContent, timeline] = await Promise.all([ - loadPartInstances(context, initModel.playlist, rundownIds), + const expectedPackages = new PlayoutExpectedPackagesModelImpl() + + const [partInstances, rundownsWithContent, timeline, expectedPackagesDocs] = await Promise.all([ + loadPartInstances(context, expectedPackages, initModel.playlist, rundownIds), loadRundowns(context, null, initModel.rundowns), loadTimeline(context), + loadExpectedPackages(context), ]) + expectedPackages.populateWithPackages(expectedPackagesDocs) + const res = new PlayoutModelImpl( context, initModel.playlistLock, @@ -164,7 +178,8 @@ export async function createPlayoutModelfromInitModel( clone(initModel.playlist), partInstances, rundownsWithContent, - timeline + timeline, + expectedPackages ) if (span) span.end() @@ -244,6 +259,7 @@ async function loadRundowns( async function loadPartInstances( context: JobContext, + expectedPackages: PlayoutExpectedPackagesModelImpl, playlist: ReadonlyDeep, rundownIds: RundownId[] ): Promise { @@ -295,18 +311,29 @@ async function loadPartInstances( // Filter the PieceInstances to the activationId, if possible pieceInstancesSelector.playlistActivationId = playlist.activationId || { $exists: false } - const [partInstances, pieceInstances] = await Promise.all([ + const [partInstances, pieceInstances /*expectedPackageDocs*/] = await Promise.all([ partInstancesCollection, context.directCollections.PieceInstances.findFetch(pieceInstancesSelector), + // context.directCollections.ExpectedPackages.findFetch({ + // fromPieceType: ExpectedPackageDBType.PIECE_INSTANCE, + // // Future: this could be optimised to only limit the loading to only those which match `activationId` + // partInstanceId: { $in: selectedPartInstanceIds }, + // rundownId: { $in: rundownIds }, + // }) as Promise, ]) + // nocommit - populate expectedPackagesStore based on the partInstances/pieceInstances? + const groupedPieceInstances = groupByToMap(pieceInstances, 'partInstanceId') const allPartInstances: PlayoutPartInstanceModelImpl[] = [] for (const partInstance of partInstances) { + const pieceInstances = groupedPieceInstances.get(partInstance._id) ?? [] + const wrappedPartInstance = new PlayoutPartInstanceModelImpl( + expectedPackages, partInstance, - groupedPieceInstances.get(partInstance._id) ?? [], + pieceInstances, false ) allPartInstances.push(wrappedPartInstance) @@ -314,3 +341,8 @@ async function loadPartInstances( return allPartInstances } + +async function loadExpectedPackages(context: JobContext): Promise { + // nocommit: load the expectedPackages from the database + return [] +} diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutExpectedPackagesModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutExpectedPackagesModelImpl.ts new file mode 100644 index 0000000000..e0c384471e --- /dev/null +++ b/packages/job-worker/src/playout/model/implementation/PlayoutExpectedPackagesModelImpl.ts @@ -0,0 +1,57 @@ +import { ExpectedPackage } from '@sofie-automation/blueprints-integration' +import { + PartInstanceId, + PieceInstanceId, + ExpectedPackageId, + RundownId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ReadonlyDeep } from 'type-fest' +import { PlayoutExpectedPackagesModel, PlayoutExpectedPackagesModelSnapshot } from '../PlayoutExpectedPackagesModel' +import { PieceExpectedPackage } from '@sofie-automation/corelib/dist/dataModel/Piece' + +export class PlayoutExpectedPackagesModelImpl implements PlayoutExpectedPackagesModel { + getPackagesForPieceInstance( + _rundownId: RundownId, + _pieceInstanceId: PieceInstanceId + ): ReadonlyDeep[] { + throw new Error('Method not implemented.') + } + + snapshotMakeCopy(): PlayoutExpectedPackagesModelSnapshot { + throw new Error('Method not implemented.') + } + + snapshotRestore(_snapshot: PlayoutExpectedPackagesModelSnapshot): void { + throw new Error('Method not implemented.') + } + + async ensurePackagesAreLoaded(_expectedPackages: PieceExpectedPackage[]): Promise { + throw new Error('Method not implemented.') + } + + createPackagesIfMissing(_rundownId: RundownId, _expectedPackages: ReadonlyDeep): void { + throw new Error('Method not implemented.') + } + + createPackagesIfMissingFromMap( + _rundownId: RundownId, + _expectedPackages: ReadonlyMap> + ): void { + throw new Error('Method not implemented.') + } + + setPieceInstanceReferenceToPackages( + _rundownId: RundownId, + _partInstanceId: PartInstanceId, + _pieceInstanceId: PieceInstanceId, + _expectedPackageIds: ExpectedPackageId[] + ): void { + throw new Error('Method not implemented.') + } + + populateWithPackages(packages: ExpectedPackageDBNew[]): void {} + + async saveAllToDatabase(): Promise { + throw new Error('Method not implemented.') + } +} diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index f6d23ce52d..d35744bb15 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -56,6 +56,8 @@ import { ExpectedPlayoutItemStudio } from '@sofie-automation/corelib/dist/dataMo import { StudioBaselineHelper } from '../../../studio/model/StudioBaselineHelper' import { EventsJobs } from '@sofie-automation/corelib/dist/worker/events' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { PlayoutExpectedPackagesModel, PlayoutExpectedPackagesModelReadonly } from '../PlayoutExpectedPackagesModel' +import { PlayoutExpectedPackagesModelImpl } from './PlayoutExpectedPackagesModelImpl' export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { public readonly playlistId: RundownPlaylistId @@ -79,6 +81,11 @@ export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { return this.timelineImpl } + protected readonly expectedPackagesImpl: PlayoutExpectedPackagesModelImpl + public get expectedPackages(): PlayoutExpectedPackagesModelReadonly { + return this.expectedPackagesImpl + } + protected allPartInstances: Map public constructor( @@ -89,7 +96,8 @@ export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { playlist: DBRundownPlaylist, partInstances: PlayoutPartInstanceModelImpl[], rundowns: PlayoutRundownModelImpl[], - timeline: TimelineComplete | undefined + timeline: TimelineComplete | undefined, + expectedPackages: PlayoutExpectedPackagesModelImpl ) { this.playlistId = playlistId this.playlistLock = playlistLock @@ -101,6 +109,8 @@ export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { this.timelineImpl = timeline ?? null + this.expectedPackagesImpl = expectedPackages + this.allPartInstances = normalizeArrayToMapFunc(partInstances, (p) => p.partInstance._id) } @@ -242,6 +252,10 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou #pendingPartInstanceTimingEvents = new Set() #pendingNotifyCurrentlyPlayingPartEvent = new Map() + get expectedPackages(): PlayoutExpectedPackagesModel { + return this.expectedPackagesImpl + } + get hackDeletedPartInstanceIds(): PartInstanceId[] { const result: PartInstanceId[] = [] for (const [id, doc] of this.allPartInstances) { @@ -258,9 +272,20 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou playlist: DBRundownPlaylist, partInstances: PlayoutPartInstanceModelImpl[], rundowns: PlayoutRundownModelImpl[], - timeline: TimelineComplete | undefined + timeline: TimelineComplete | undefined, + expectedPackages: PlayoutExpectedPackagesModelImpl ) { - super(context, playlistLock, playlistId, peripheralDevices, playlist, partInstances, rundowns, timeline) + super( + context, + playlistLock, + playlistId, + peripheralDevices, + playlist, + partInstances, + rundowns, + timeline, + expectedPackages + ) context.trackCache(this) this.#baselineHelper = new StudioBaselineHelper(context) @@ -326,7 +351,12 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.#fixupPieceInstancesForPartInstance(newPartInstance, infinitePieceInstances) - const partInstance = new PlayoutPartInstanceModelImpl(newPartInstance, infinitePieceInstances, true) + const partInstance = new PlayoutPartInstanceModelImpl( + this.expectedPackages, + newPartInstance, + infinitePieceInstances, + true + ) for (const piece of pieces) { partInstance.insertAdlibbedPiece(piece, fromAdlibId) @@ -367,11 +397,25 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.#fixupPieceInstancesForPartInstance(newPartInstance, pieceInstances) - const partInstance = new PlayoutPartInstanceModelImpl(newPartInstance, pieceInstances, true) + const partInstance = new PlayoutPartInstanceModelImpl( + this.expectedPackages, + newPartInstance, + pieceInstances, + true + ) partInstance.recalculateExpectedDurationWithTransition() this.allPartInstances.set(newPartInstance._id, partInstance) + for (const pieceInstance of partInstance.pieceInstances) { + this.expectedPackages.setPieceInstanceReferenceToPackages( + partInstance.partInstance.rundownId, + partInstance.partInstance._id, + pieceInstance.pieceInstance._id, + pieceInstance.pieceInstance.piece.expectedPackages.map((p) => p.expectedPackageId) + ) + } + return partInstance } @@ -406,7 +450,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou }, } - const partInstance = new PlayoutPartInstanceModelImpl(newPartInstance, [], true) + const partInstance = new PlayoutPartInstanceModelImpl(this.expectedPackages, newPartInstance, [], true) partInstance.recalculateExpectedDurationWithTransition() this.allPartInstances.set(newPartInstance._id, partInstance) @@ -562,6 +606,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou ...writePartInstancesAndPieceInstances(this.context, this.allPartInstances), writeAdlibTestingSegments(this.context, this.rundownsImpl), this.#baselineHelper.saveAllToDatabase(), + this.expectedPackagesImpl.saveAllToDatabase(), ]) this.#playlistHasChanged = false diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts index a152084f08..3595b6196b 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPartInstanceModelImpl.ts @@ -29,6 +29,7 @@ import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/da import _ = require('underscore') import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { IBlueprintMutatablePartSampleKeys } from '../../../blueprints/context/lib' +import { PlayoutExpectedPackagesModel } from '../PlayoutExpectedPackagesModel' interface PlayoutPieceInstanceModelSnapshotImpl { PieceInstance: PieceInstance @@ -47,6 +48,8 @@ class PlayoutPartInstanceModelSnapshotImpl implements PlayoutPartInstanceModelSn this.partInstance = clone(copyFrom.partInstanceImpl) this.partInstanceHasChanges = copyFrom.partInstanceHasChanges + // nocommit - should this be concerned with expectedPackages? + const pieceInstances = new Map() for (const [pieceInstanceId, pieceInstance] of copyFrom.pieceInstancesImpl) { if (pieceInstance) { @@ -62,6 +65,8 @@ class PlayoutPartInstanceModelSnapshotImpl implements PlayoutPartInstanceModelSn } } export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { + #expectedPackages: PlayoutExpectedPackagesModel + partInstanceImpl: DBPartInstance pieceInstancesImpl: Map @@ -154,7 +159,13 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { return result } - constructor(partInstance: DBPartInstance, pieceInstances: PieceInstance[], hasChanges: boolean) { + constructor( + expectedPackages: PlayoutExpectedPackagesModel, + partInstance: DBPartInstance, + pieceInstances: PieceInstance[], + hasChanges: boolean + ) { + this.#expectedPackages = expectedPackages this.partInstanceImpl = partInstance this.#partInstanceHasChanges = hasChanges @@ -234,6 +245,13 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(pieceInstance, true) this.pieceInstancesImpl.set(pieceInstance._id, pieceInstanceModel) + this.#expectedPackages.setPieceInstanceReferenceToPackages( + this.partInstanceImpl.rundownId, + this.partInstanceImpl._id, + pieceInstance._id, + pieceInstance.piece.expectedPackages.map((p) => p.expectedPackageId) + ) + return pieceInstanceModel } @@ -274,6 +292,14 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newInstance, true) this.pieceInstancesImpl.set(newInstance._id, pieceInstanceModel) + // Don't preserve any ExpectedPackages, the existing ones will suffice // nocommit - verify this + // this.#expectedPackages.setPieceInstanceReferenceToPackages( + // this.partInstanceImpl.rundownId, + // this.partInstanceImpl._id, + // newPieceInstance._id, + // newPieceInstance.piece.expectedPackages.map((p) => p.expectedPackageId) + // ) + return pieceInstanceModel } @@ -299,6 +325,13 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { const pieceInstanceModel = new PlayoutPieceInstanceModelImpl(newPieceInstance, true) this.pieceInstancesImpl.set(pieceInstanceId, pieceInstanceModel) + this.#expectedPackages.setPieceInstanceReferenceToPackages( + this.partInstanceImpl.rundownId, + this.partInstanceImpl._id, + newPieceInstance._id, + newPieceInstance.piece.expectedPackages.map((p) => p.expectedPackageId) + ) + return pieceInstanceModel } @@ -328,6 +361,7 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { virtual: true, content: {}, timelineObjectsString: EmptyPieceTimelineObjectsBlob, + expectedPackages: [], }, dynamicallyInserted: getCurrentTime(), @@ -395,6 +429,13 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { // Future: should this do any deeper validation of the PieceInstances? this.pieceInstancesImpl.set(pieceInstance._id, new PlayoutPieceInstanceModelImpl(pieceInstance, true)) + + this.#expectedPackages.setPieceInstanceReferenceToPackages( + this.partInstanceImpl.rundownId, + this.partInstanceImpl._id, + pieceInstance._id, + pieceInstance.piece.expectedPackages.map((p) => p.expectedPackageId) + ) } } @@ -405,10 +446,24 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel { if (existingPieceInstance) { existingPieceInstance.mergeProperties(doc) + this.#expectedPackages.setPieceInstanceReferenceToPackages( + this.partInstanceImpl.rundownId, + this.partInstanceImpl._id, + existingPieceInstance.pieceInstance._id, + existingPieceInstance.pieceInstance.piece.expectedPackages.map((p) => p.expectedPackageId) + ) + return existingPieceInstance } else { const newPieceInstance = new PlayoutPieceInstanceModelImpl(clone(doc), true) this.pieceInstancesImpl.set(newPieceInstance.pieceInstance._id, newPieceInstance) + + this.#expectedPackages.setPieceInstanceReferenceToPackages( + this.partInstanceImpl.rundownId, + this.partInstanceImpl._id, + newPieceInstance.pieceInstance._id, + newPieceInstance.pieceInstance.piece.expectedPackages.map((p) => p.expectedPackageId) + ) return newPieceInstance } } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts index c9390119ff..661eaefbf1 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts @@ -136,4 +136,31 @@ export class PlayoutPieceInstanceModelImpl implements PlayoutPieceInstanceModel true ) } + + // /** + // * Update the expected packages for the PieceInstance + // * @param expectedPackages The new packages + // */ + // setExpectedPackages(expectedPackages: ReadonlyDeep[]): void { + // // nocommit - refactor this into a simpler type than `ExpectedPackagesStore` or just reuse that? + + // const bases = generateExpectedPackageBases(this.#context.studioId, this.PieceInstanceImpl._id, expectedPackages) + // const newExpectedPackages: ExpectedPackageDBFromPieceInstance[] = bases.map((base) => ({ + // ...base, + + // fromPieceType: ExpectedPackageDBType.PIECE_INSTANCE, + // partInstanceId: this.PieceInstanceImpl.partInstanceId, + // pieceInstanceId: this.PieceInstanceImpl._id, + // segmentId: this.PieceInstanceImpl.segmentId, // TODO + // rundownId: this.PieceInstanceImpl.rundownId, + // pieceId: null, + // })) + + // this.#expectedPackages = diffAndReturnLatestObjects( + // this.ExpectedPackagesWithChanges, + // this.#expectedPackages, + // newExpectedPackages, + // mutateExpectedPackage + // ) + // } } diff --git a/packages/job-worker/src/playout/setNext.ts b/packages/job-worker/src/playout/setNext.ts index ab6fcf7514..63a6c3d4f5 100644 --- a/packages/job-worker/src/playout/setNext.ts +++ b/packages/job-worker/src/playout/setNext.ts @@ -204,7 +204,7 @@ async function preparePartInstanceForPartBeingNexted( if (!rundown) throw new Error(`Could not find rundown ${nextPart.rundownId}`) const possiblePieces = await fetchPiecesThatMayBeActiveForPart(context, playoutModel, undefined, nextPart) - const newPieceInstances = getPieceInstancesForPart( + const newPieceInstances = await getPieceInstancesForPart( context, playoutModel, currentPartInstance, diff --git a/packages/job-worker/src/playout/snapshot.ts b/packages/job-worker/src/playout/snapshot.ts index 9b479f0968..b0ca9edcd2 100644 --- a/packages/job-worker/src/playout/snapshot.ts +++ b/packages/job-worker/src/playout/snapshot.ts @@ -279,7 +279,7 @@ export async function handleRestorePlaylistSnapshot( ...snapshot.baselineAdLibActions, ]) { const oldId = adlib._id - if (adlib.partId) adlib.partId = partIdMap.get(adlib.partId) + if ('partId' in adlib && adlib.partId) adlib.partId = partIdMap.get(adlib.partId) adlib._id = getRandomId() pieceIdMap.set(oldId, adlib._id) } diff --git a/packages/shared-lib/src/package-manager/helpers.ts b/packages/shared-lib/src/package-manager/helpers.ts index c6eb6d8bb1..d9591b1df5 100644 --- a/packages/shared-lib/src/package-manager/helpers.ts +++ b/packages/shared-lib/src/package-manager/helpers.ts @@ -1,11 +1,14 @@ +import { ReadonlyDeep } from 'type-fest' import { ExpectedPackage } from './package' // Note: These functions are copied from Package Manager type Steps = Required['steps'] -export function htmlTemplateGetSteps(version: ExpectedPackage.ExpectedPackageHtmlTemplate['version']): Steps { - let steps: Steps +export function htmlTemplateGetSteps( + version: ReadonlyDeep +): ReadonlyDeep { + let steps: ReadonlyDeep if (version.casparCG) { // Generate a set of steps for standard CasparCG templates const casparData = version.casparCG.data @@ -29,7 +32,7 @@ export function htmlTemplateGetSteps(version: ExpectedPackage.ExpectedPackageHtm } return steps } -export function htmlTemplateGetFileNamesFromSteps(steps: Steps): { +export function htmlTemplateGetFileNamesFromSteps(steps: ReadonlyDeep): { /** List of all file names that will be output from in the steps */ fileNames: string[] /** The "main file", ie the file that will carry the main metadata */ diff --git a/packages/shared-lib/src/package-manager/publications.ts b/packages/shared-lib/src/package-manager/publications.ts index 48ffbe0953..09d1c845ed 100644 --- a/packages/shared-lib/src/package-manager/publications.ts +++ b/packages/shared-lib/src/package-manager/publications.ts @@ -1,6 +1,7 @@ import { ExpectedPackage, PackageContainer, PackageContainerOnPackage } from './package' import { PeripheralDeviceId, PieceInstanceId, RundownId, RundownPlaylistId } from '../core/model/Ids' import { ProtectedString } from '../lib/protectedString' +import { ReadonlyDeep } from 'type-fest' export interface PackageManagerPlayoutContext { _id: PeripheralDeviceId @@ -27,7 +28,7 @@ export interface PackageManagerPackageContainers { export type PackageManagerExpectedPackageId = ProtectedString<'PackageManagerExpectedPackage'> -export type PackageManagerExpectedPackageBase = ExpectedPackage.Base & { rundownId?: RundownId } +export type PackageManagerExpectedPackageBase = ReadonlyDeep & { rundownId?: RundownId } export interface PackageManagerExpectedPackage { /** Unique id of the expectedPackage */