@@ -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