@@ -43,6 +43,7 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
4343import { DatabasePersistedModel } from '../modelBase'
4444import { updateSegmentIdsForAdlibbedPartInstances } from './commit/updateSegmentIdsForAdlibbedPartInstances'
4545import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError'
46+ import { AnyBulkWriteOperation } from 'mongodb'
4647
4748export type BeforePartMapItem = { id : PartId ; rank : number }
4849export type BeforeIngestOperationPartMap = ReadonlyMap < SegmentId , Array < BeforePartMapItem > >
@@ -177,6 +178,9 @@ export async function CommitIngestOperation(
177178 // Ensure any adlibbed parts are updated to follow the segmentId of the previous part
178179 await updateSegmentIdsForAdlibbedPartInstances ( context , ingestModel , beforePartMap )
179180
181+ if ( data . renamedSegments && data . renamedSegments . size > 0 ) {
182+ logger . debug ( `Renamed segments: ${ JSON . stringify ( Array . from ( data . renamedSegments . entries ( ) ) ) } ` )
183+ }
180184 // ensure instances have matching segmentIds with the parts
181185 await updatePartInstancesSegmentIds ( context , ingestModel , data . renamedSegments , beforePartMap )
182186
@@ -259,10 +263,16 @@ export async function CommitIngestOperation(
259263}
260264
261265function canRemoveSegment (
266+ prevPartInstance : ReadonlyDeep < DBPartInstance > | undefined ,
262267 currentPartInstance : ReadonlyDeep < DBPartInstance > | undefined ,
263268 nextPartInstance : ReadonlyDeep < DBPartInstance > | undefined ,
264269 segmentId : SegmentId
265270) : boolean {
271+ if ( prevPartInstance ?. segmentId === segmentId ) {
272+ // Don't allow removing an active rundown
273+ logger . warn ( `Not allowing removal of previous playing segment "${ segmentId } ", making segment unsynced instead` )
274+ return false
275+ }
266276 if (
267277 currentPartInstance ?. segmentId === segmentId ||
268278 ( nextPartInstance ?. segmentId === segmentId && isTooCloseToAutonext ( currentPartInstance , false ) )
@@ -295,26 +305,32 @@ async function updatePartInstancesSegmentIds(
295305 renamedSegments : ReadonlyMap < SegmentId , SegmentId > | null ,
296306 beforePartMap : BeforeIngestOperationPartMap
297307) {
298- // A set of rules which can be translated to mongo queries for PartInstances to update
308+ /**
309+ * Maps new SegmentId ->
310+ * A set of rules which can be translated to mongo queries for PartInstances to update
311+ */
299312 const renameRules = new Map <
300313 SegmentId ,
301314 {
315+ /** Parts that have been moved to the new SegmentId */
302316 partIds : PartId [ ]
303- fromSegmentId : SegmentId | null
317+ /** Segments that have been renamed to the new SegmentId */
318+ fromSegmentIds : SegmentId [ ]
304319 }
305320 > ( )
306321
307322 // Add whole segment renames to the set of rules
308323 if ( renamedSegments ) {
309324 for ( const [ fromSegmentId , toSegmentId ] of renamedSegments ) {
310- const rule = renameRules . get ( toSegmentId ) ?? { partIds : [ ] , fromSegmentId : null }
325+ const rule = renameRules . get ( toSegmentId ) ?? { partIds : [ ] , fromSegmentIds : [ ] }
311326 renameRules . set ( toSegmentId , rule )
312327
313- rule . fromSegmentId = fromSegmentId
328+ rule . fromSegmentIds . push ( fromSegmentId )
314329 }
315330 }
316331
317- // Reverse the structure
332+ // Reverse the Map structure
333+ /** Maps Part -> SegmentId-of-the-part-before-ingest-changes */
318334 const beforePartSegmentIdMap = new Map < PartId , SegmentId > ( )
319335 for ( const [ segmentId , partItems ] of beforePartMap . entries ( ) ) {
320336 for ( const partItem of partItems ) {
@@ -325,8 +341,11 @@ async function updatePartInstancesSegmentIds(
325341 // Some parts may have gotten a different segmentId to the base rule, so track those seperately in the rules
326342 for ( const partModel of ingestModel . getAllOrderedParts ( ) ) {
327343 const oldSegmentId = beforePartSegmentIdMap . get ( partModel . part . _id )
344+
328345 if ( oldSegmentId && oldSegmentId !== partModel . part . segmentId ) {
329- const rule = renameRules . get ( partModel . part . segmentId ) ?? { partIds : [ ] , fromSegmentId : null }
346+ // The part has moved to another segment, add a rule to update its corresponding PartInstances:
347+
348+ const rule = renameRules . get ( partModel . part . segmentId ) ?? { partIds : [ ] , fromSegmentIds : [ ] }
330349 renameRules . set ( partModel . part . segmentId , rule )
331350
332351 rule . partIds . push ( partModel . part . _id )
@@ -335,30 +354,80 @@ async function updatePartInstancesSegmentIds(
335354
336355 // Perform a mongo update to modify the PartInstances
337356 if ( renameRules . size > 0 ) {
338- await context . directCollections . PartInstances . bulkWrite (
339- Array . from ( renameRules . entries ( ) ) . map ( ( [ newSegmentId , rule ] ) => ( {
340- updateMany : {
341- filter : {
342- $or : _ . compact ( [
343- rule . fromSegmentId
344- ? {
345- segmentId : rule . fromSegmentId ,
346- }
347- : undefined ,
348- {
349- 'part._id' : { $in : rule . partIds } ,
357+ const rulesInOrder = Array . from ( renameRules . entries ( ) ) . sort ( ( a , b ) => {
358+ // Ensure that the ones with partIds are processed last,
359+ // as that should take precedence.
360+
361+ if ( a [ 1 ] . partIds . length && ! b [ 1 ] . partIds . length ) return 1
362+ if ( ! a [ 1 ] . partIds . length && b [ 1 ] . partIds . length ) return - 1
363+ return 0
364+ } )
365+
366+ const writeOps : AnyBulkWriteOperation < DBPartInstance > [ ] = [ ]
367+
368+ for ( const [ newSegmentId , rule ] of rulesInOrder ) {
369+ if ( rule . fromSegmentIds . length ) {
370+ writeOps . push ( {
371+ updateMany : {
372+ filter : {
373+ rundownId : ingestModel . rundownId ,
374+ segmentId : { $in : rule . fromSegmentIds } ,
375+ } ,
376+ update : {
377+ $set : {
378+ segmentId : newSegmentId ,
379+ 'part.segmentId' : newSegmentId ,
350380 } ,
351- ] ) ,
381+ } ,
352382 } ,
353- update : {
354- $set : {
355- segmentId : newSegmentId ,
356- 'part.segmentId' : newSegmentId ,
383+ } )
384+ }
385+ if ( rule . partIds . length ) {
386+ writeOps . push ( {
387+ updateMany : {
388+ filter : {
389+ rundownId : ingestModel . rundownId ,
390+ 'part._id' : { $in : rule . partIds } ,
391+ } ,
392+ update : {
393+ $set : {
394+ segmentId : newSegmentId ,
395+ 'part.segmentId' : newSegmentId ,
396+ } ,
357397 } ,
358398 } ,
359- } ,
360- } ) )
361- )
399+ } )
400+ }
401+ }
402+ if ( writeOps . length ) await context . directCollections . PartInstances . bulkWrite ( writeOps )
403+
404+ // Double check that there are no parts using the old segment ids:
405+ const oldSegmentIds = Array . from ( renameRules . keys ( ) )
406+ const [ badPartInstances , badParts ] = await Promise . all ( [
407+ await context . directCollections . PartInstances . findFetch ( {
408+ rundownId : ingestModel . rundownId ,
409+ segmentId : { $in : oldSegmentIds } ,
410+ } ) ,
411+ await context . directCollections . Parts . findFetch ( {
412+ rundownId : ingestModel . rundownId ,
413+ segmentId : { $in : oldSegmentIds } ,
414+ } ) ,
415+ ] )
416+ if ( badPartInstances . length > 0 ) {
417+ logger . error (
418+ `updatePartInstancesSegmentIds: Failed to update all PartInstances using old SegmentIds "${ JSON . stringify (
419+ oldSegmentIds
420+ ) } ": ${ JSON . stringify ( badPartInstances ) } , writeOps: ${ JSON . stringify ( writeOps ) } `
421+ )
422+ }
423+
424+ if ( badParts . length > 0 ) {
425+ logger . error (
426+ `updatePartInstancesSegmentIds: Failed to update all Parts using old SegmentIds "${ JSON . stringify (
427+ oldSegmentIds
428+ ) } ": ${ JSON . stringify ( badParts ) } , writeOps: ${ JSON . stringify ( writeOps ) } `
429+ )
430+ }
362431 }
363432}
364433
@@ -662,7 +731,7 @@ async function removeSegments(
662731 _changedSegmentIds : ReadonlyDeep < SegmentId [ ] > ,
663732 removedSegmentIds : ReadonlyDeep < SegmentId [ ] >
664733) {
665- const { currentPartInstance, nextPartInstance } = await getSelectedPartInstances (
734+ const { previousPartInstance , currentPartInstance, nextPartInstance } = await getSelectedPartInstances (
666735 context ,
667736 newPlaylist ,
668737 rundownsInPlaylist . map ( ( r ) => r . _id )
@@ -672,7 +741,7 @@ async function removeSegments(
672741 const orphanDeletedSegmentIds = new Set < SegmentId > ( )
673742 const orphanHiddenSegmentIds = new Set < SegmentId > ( )
674743 for ( const segmentId of removedSegmentIds ) {
675- if ( canRemoveSegment ( currentPartInstance , nextPartInstance , segmentId ) ) {
744+ if ( canRemoveSegment ( previousPartInstance , currentPartInstance , nextPartInstance , segmentId ) ) {
676745 purgeSegmentIds . add ( segmentId )
677746 } else {
678747 logger . warn (
@@ -685,8 +754,10 @@ async function removeSegments(
685754 for ( const segment of ingestModel . getAllSegments ( ) ) {
686755 const segmentId = segment . segment . _id
687756 if ( segment . segment . isHidden ) {
688- if ( ! canRemoveSegment ( currentPartInstance , nextPartInstance , segmentId ) ) {
689- // Protect live segment from being hidden
757+ // Blueprints want to hide the Segment
758+
759+ if ( ! canRemoveSegment ( previousPartInstance , currentPartInstance , nextPartInstance , segmentId ) ) {
760+ // The Segment is live, so we need to protect it from being hidden
690761 logger . warn ( `Cannot hide live segment ${ segmentId } , it will be orphaned` )
691762 switch ( segment . segment . orphaned ) {
692763 case SegmentOrphanedReason . DELETED :
@@ -706,7 +777,7 @@ async function removeSegments(
706777 } else if ( ! orphanDeletedSegmentIds . has ( segmentId ) && segment . parts . length === 0 ) {
707778 // No parts in segment
708779
709- if ( ! canRemoveSegment ( currentPartInstance , nextPartInstance , segmentId ) ) {
780+ if ( ! canRemoveSegment ( previousPartInstance , currentPartInstance , nextPartInstance , segmentId ) ) {
710781 // Protect live segment from being hidden
711782 logger . warn ( `Cannot hide live segment ${ segmentId } , it will be orphaned` )
712783 orphanHiddenSegmentIds . add ( segmentId )
0 commit comments