Skip to content

Commit cc866ca

Browse files
committed
Merge branch 'release51' into release52
2 parents 8c78ed2 + 0ae53c4 commit cc866ca

File tree

18 files changed

+286
-65
lines changed

18 files changed

+286
-65
lines changed

meteor/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
44

5+
### [1.51.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.51.1...v1.51.2) (2024-11-21)
6+
7+
8+
### Bug Fixes
9+
10+
* Include previousPartInstance in check to orphan segments rather than remove them. ([2c113b5](https://github.com/nrkno/tv-automation-server-core/commit/2c113b58b205198d13f0fc7e2114704311eb915b))
11+
* updatePartInstancesSegmentIds: take into account when multiple segments have been merged into one. ([bdab8c4](https://github.com/nrkno/tv-automation-server-core/commit/bdab8c4e4ee1e67a3568cccc98106bb7f1258673))
12+
513
## [1.51.0-in-testing.3](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.2...v1.51.0-in-testing.3) (2024-09-25)
614

715
## [1.51.0-in-testing.2](https://github.com/nrkno/sofie-core/compare/v1.51.0-in-testing.1...v1.51.0-in-testing.2) (2024-09-24)

packages/blueprints-integration/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## [1.51.3](https://github.com/nrkno/sofie-core/compare/v1.51.2...v1.51.3) (2024-11-21)
7+
8+
**Note:** Version bump only for package @sofie-automation/blueprints-integration
9+
10+
11+
12+
13+
14+
## [1.51.2](https://github.com/nrkno/sofie-core/compare/v1.51.1...v1.51.2) (2024-11-21)
15+
16+
**Note:** Version bump only for package @sofie-automation/blueprints-integration
17+
18+
19+
20+
21+
622
## [1.51.1](https://github.com/nrkno/sofie-core/compare/v1.51.1-2...v1.51.1) (2024-11-13)
723

824
**Note:** Version bump only for package @sofie-automation/blueprints-integration

packages/corelib/src/dataModel/Segment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { SegmentNote } from './Notes'
55
export enum SegmentOrphanedReason {
66
/** Segment is deleted from the NRCS but we still need it */
77
DELETED = 'deleted',
8-
/** Segment should be hidden, but it is still playing */
8+
/** Blueprints want the Segment to be hidden, but it is still playing so is must not be hidden right now. */
99
HIDDEN = 'hidden',
1010
/** Segment is owned by playout, and is for AdlibTesting in its rundown */
1111
ADLIB_TESTING = 'adlib-testing',

packages/job-worker/src/ingest/commit.ts

Lines changed: 103 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
4242
import { DatabasePersistedModel } from '../modelBase'
4343
import { updateSegmentIdsForAdlibbedPartInstances } from './commit/updateSegmentIdsForAdlibbedPartInstances'
4444
import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError'
45+
import { AnyBulkWriteOperation } from 'mongodb'
4546

4647
export type BeforePartMapItem = { id: PartId; rank: number }
4748
export type BeforeIngestOperationPartMap = ReadonlyMap<SegmentId, Array<BeforePartMapItem>>
@@ -176,6 +177,9 @@ export async function CommitIngestOperation(
176177
// Ensure any adlibbed parts are updated to follow the segmentId of the previous part
177178
await updateSegmentIdsForAdlibbedPartInstances(context, ingestModel, beforePartMap)
178179

180+
if (data.renamedSegments && data.renamedSegments.size > 0) {
181+
logger.debug(`Renamed segments: ${JSON.stringify(Array.from(data.renamedSegments.entries()))}`)
182+
}
179183
// ensure instances have matching segmentIds with the parts
180184
await updatePartInstancesSegmentIds(context, ingestModel, data.renamedSegments, beforePartMap)
181185

@@ -261,11 +265,17 @@ export async function CommitIngestOperation(
261265
}
262266

263267
function canRemoveSegment(
268+
prevPartInstance: ReadonlyDeep<DBPartInstance> | undefined,
264269
currentPartInstance: ReadonlyDeep<DBPartInstance> | undefined,
265270
nextPartInstance: ReadonlyDeep<DBPartInstance> | undefined,
266271
segmentId: SegmentId
267272
): boolean {
268-
if (currentPartInstance?.segmentId === segmentId || nextPartInstance?.segmentId === segmentId) {
273+
if (prevPartInstance?.segmentId === segmentId) {
274+
// Don't allow removing an active rundown
275+
logger.warn(`Not allowing removal of previous playing segment "${segmentId}", making segment unsynced instead`)
276+
return false
277+
}
278+
if (currentPartInstance?.segmentId === segmentId) {
269279
// Don't allow removing an active rundown
270280
logger.warn(`Not allowing removal of current playing segment "${segmentId}", making segment unsynced instead`)
271281
return false
@@ -294,26 +304,32 @@ async function updatePartInstancesSegmentIds(
294304
renamedSegments: ReadonlyMap<SegmentId, SegmentId> | null,
295305
beforePartMap: BeforeIngestOperationPartMap
296306
) {
297-
// A set of rules which can be translated to mongo queries for PartInstances to update
307+
/**
308+
* Maps new SegmentId ->
309+
* A set of rules which can be translated to mongo queries for PartInstances to update
310+
*/
298311
const renameRules = new Map<
299312
SegmentId,
300313
{
314+
/** Parts that have been moved to the new SegmentId */
301315
partIds: PartId[]
302-
fromSegmentId: SegmentId | null
316+
/** Segments that have been renamed to the new SegmentId */
317+
fromSegmentIds: SegmentId[]
303318
}
304319
>()
305320

306321
// Add whole segment renames to the set of rules
307322
if (renamedSegments) {
308323
for (const [fromSegmentId, toSegmentId] of renamedSegments) {
309-
const rule = renameRules.get(toSegmentId) ?? { partIds: [], fromSegmentId: null }
324+
const rule = renameRules.get(toSegmentId) ?? { partIds: [], fromSegmentIds: [] }
310325
renameRules.set(toSegmentId, rule)
311326

312-
rule.fromSegmentId = fromSegmentId
327+
rule.fromSegmentIds.push(fromSegmentId)
313328
}
314329
}
315330

316-
// Reverse the structure
331+
// Reverse the Map structure
332+
/** Maps Part -> SegmentId-of-the-part-before-ingest-changes */
317333
const beforePartSegmentIdMap = new Map<PartId, SegmentId>()
318334
for (const [segmentId, partItems] of beforePartMap.entries()) {
319335
for (const partItem of partItems) {
@@ -324,8 +340,11 @@ async function updatePartInstancesSegmentIds(
324340
// Some parts may have gotten a different segmentId to the base rule, so track those seperately in the rules
325341
for (const partModel of ingestModel.getAllOrderedParts()) {
326342
const oldSegmentId = beforePartSegmentIdMap.get(partModel.part._id)
343+
327344
if (oldSegmentId && oldSegmentId !== partModel.part.segmentId) {
328-
const rule = renameRules.get(partModel.part.segmentId) ?? { partIds: [], fromSegmentId: null }
345+
// The part has moved to another segment, add a rule to update its corresponding PartInstances:
346+
347+
const rule = renameRules.get(partModel.part.segmentId) ?? { partIds: [], fromSegmentIds: [] }
329348
renameRules.set(partModel.part.segmentId, rule)
330349

331350
rule.partIds.push(partModel.part._id)
@@ -334,30 +353,80 @@ async function updatePartInstancesSegmentIds(
334353

335354
// Perform a mongo update to modify the PartInstances
336355
if (renameRules.size > 0) {
337-
await context.directCollections.PartInstances.bulkWrite(
338-
Array.from(renameRules.entries()).map(([newSegmentId, rule]) => ({
339-
updateMany: {
340-
filter: {
341-
$or: _.compact([
342-
rule.fromSegmentId
343-
? {
344-
segmentId: rule.fromSegmentId,
345-
}
346-
: undefined,
347-
{
348-
'part._id': { $in: rule.partIds },
356+
const rulesInOrder = Array.from(renameRules.entries()).sort((a, b) => {
357+
// Ensure that the ones with partIds are processed last,
358+
// as that should take precedence.
359+
360+
if (a[1].partIds.length && !b[1].partIds.length) return 1
361+
if (!a[1].partIds.length && b[1].partIds.length) return -1
362+
return 0
363+
})
364+
365+
const writeOps: AnyBulkWriteOperation<DBPartInstance>[] = []
366+
367+
for (const [newSegmentId, rule] of rulesInOrder) {
368+
if (rule.fromSegmentIds.length) {
369+
writeOps.push({
370+
updateMany: {
371+
filter: {
372+
rundownId: ingestModel.rundownId,
373+
segmentId: { $in: rule.fromSegmentIds },
374+
},
375+
update: {
376+
$set: {
377+
segmentId: newSegmentId,
378+
'part.segmentId': newSegmentId,
349379
},
350-
]),
380+
},
351381
},
352-
update: {
353-
$set: {
354-
segmentId: newSegmentId,
355-
'part.segmentId': newSegmentId,
382+
})
383+
}
384+
if (rule.partIds.length) {
385+
writeOps.push({
386+
updateMany: {
387+
filter: {
388+
rundownId: ingestModel.rundownId,
389+
'part._id': { $in: rule.partIds },
390+
},
391+
update: {
392+
$set: {
393+
segmentId: newSegmentId,
394+
'part.segmentId': newSegmentId,
395+
},
356396
},
357397
},
358-
},
359-
}))
360-
)
398+
})
399+
}
400+
}
401+
if (writeOps.length) await context.directCollections.PartInstances.bulkWrite(writeOps)
402+
403+
// Double check that there are no parts using the old segment ids:
404+
const oldSegmentIds = Array.from(renameRules.keys())
405+
const [badPartInstances, badParts] = await Promise.all([
406+
await context.directCollections.PartInstances.findFetch({
407+
rundownId: ingestModel.rundownId,
408+
segmentId: { $in: oldSegmentIds },
409+
}),
410+
await context.directCollections.Parts.findFetch({
411+
rundownId: ingestModel.rundownId,
412+
segmentId: { $in: oldSegmentIds },
413+
}),
414+
])
415+
if (badPartInstances.length > 0) {
416+
logger.error(
417+
`updatePartInstancesSegmentIds: Failed to update all PartInstances using old SegmentIds "${JSON.stringify(
418+
oldSegmentIds
419+
)}": ${JSON.stringify(badPartInstances)}, writeOps: ${JSON.stringify(writeOps)}`
420+
)
421+
}
422+
423+
if (badParts.length > 0) {
424+
logger.error(
425+
`updatePartInstancesSegmentIds: Failed to update all Parts using old SegmentIds "${JSON.stringify(
426+
oldSegmentIds
427+
)}": ${JSON.stringify(badParts)}, writeOps: ${JSON.stringify(writeOps)}`
428+
)
429+
}
361430
}
362431
}
363432

@@ -661,7 +730,7 @@ async function removeSegments(
661730
_changedSegmentIds: ReadonlyDeep<SegmentId[]>,
662731
removedSegmentIds: ReadonlyDeep<SegmentId[]>
663732
) {
664-
const { currentPartInstance, nextPartInstance } = await getSelectedPartInstances(
733+
const { previousPartInstance, currentPartInstance, nextPartInstance } = await getSelectedPartInstances(
665734
context,
666735
newPlaylist,
667736
rundownsInPlaylist.map((r) => r._id)
@@ -671,7 +740,7 @@ async function removeSegments(
671740
const orphanDeletedSegmentIds = new Set<SegmentId>()
672741
const orphanHiddenSegmentIds = new Set<SegmentId>()
673742
for (const segmentId of removedSegmentIds) {
674-
if (canRemoveSegment(currentPartInstance, nextPartInstance, segmentId)) {
743+
if (canRemoveSegment(previousPartInstance, currentPartInstance, nextPartInstance, segmentId)) {
675744
purgeSegmentIds.add(segmentId)
676745
} else {
677746
logger.warn(
@@ -684,8 +753,10 @@ async function removeSegments(
684753
for (const segment of ingestModel.getAllSegments()) {
685754
const segmentId = segment.segment._id
686755
if (segment.segment.isHidden) {
687-
if (!canRemoveSegment(currentPartInstance, nextPartInstance, segmentId)) {
688-
// Protect live segment from being hidden
756+
// Blueprints want to hide the Segment
757+
758+
if (!canRemoveSegment(previousPartInstance, currentPartInstance, nextPartInstance, segmentId)) {
759+
// The Segment is live, so we need to protect it from being hidden
689760
logger.warn(`Cannot hide live segment ${segmentId}, it will be orphaned`)
690761
switch (segment.segment.orphaned) {
691762
case SegmentOrphanedReason.DELETED:
@@ -705,7 +776,7 @@ async function removeSegments(
705776
} else if (!orphanDeletedSegmentIds.has(segmentId) && segment.parts.length === 0) {
706777
// No parts in segment
707778

708-
if (!canRemoveSegment(currentPartInstance, nextPartInstance, segmentId)) {
779+
if (!canRemoveSegment(previousPartInstance, currentPartInstance, nextPartInstance, segmentId)) {
709780
// Protect live segment from being hidden
710781
logger.warn(`Cannot hide live segment ${segmentId}, it will be orphaned`)
711782
orphanHiddenSegmentIds.add(segmentId)

packages/job-worker/src/ingest/model/IngestSegmentModel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export interface IngestSegmentModel extends IngestSegmentModelReadonly {
6767
setOrphaned(orphaned: SegmentOrphanedReason | undefined): void
6868

6969
/**
70-
* Mark this Part as being hidden
70+
* Mark this Segment as being hidden
7171
* @param hidden New hidden state
7272
*/
7373
setHidden(hidden: boolean): void

packages/job-worker/src/ingest/model/implementation/DocumentChangeTracker.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ export class DocumentChangeTracker<TDoc extends { _id: ProtectedString<any> }> {
9494
}
9595
}
9696

97+
getDeletedIds(): TDoc['_id'][] {
98+
return Array.from(this.#deletedIds.values())
99+
}
100+
97101
/**
98102
* Generate the mongodb BulkWrite operations for the documents known to this tracker
99103
* @returns mongodb BulkWrite operations

packages/job-worker/src/ingest/model/implementation/SaveIngestModel.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { JobContext } from '../../../jobs'
1111
import { ExpectedPackagesStore } from './ExpectedPackagesStore'
1212
import { IngestSegmentModelImpl } from './IngestSegmentModelImpl'
1313
import { DocumentChangeTracker } from './DocumentChangeTracker'
14+
import { logger } from '../../../logging'
15+
import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString'
1416

1517
export class SaveIngestModelHelper {
1618
#expectedPackages = new DocumentChangeTracker<ExpectedPackageDB>()
@@ -55,6 +57,23 @@ export class SaveIngestModelHelper {
5557
}
5658

5759
commit(context: JobContext): Array<Promise<unknown>> {
60+
// Log deleted ids:
61+
const deletedIds: { [key: string]: ProtectedString<any>[] } = {
62+
expectedPackages: this.#expectedPackages.getDeletedIds(),
63+
expectedPlayoutItems: this.#expectedPlayoutItems.getDeletedIds(),
64+
expectedMediaItems: this.#expectedMediaItems.getDeletedIds(),
65+
segments: this.#segments.getDeletedIds(),
66+
parts: this.#parts.getDeletedIds(),
67+
pieces: this.#pieces.getDeletedIds(),
68+
adLibPieces: this.#adLibPieces.getDeletedIds(),
69+
adLibActions: this.#adLibActions.getDeletedIds(),
70+
}
71+
for (const [key, ids] of Object.entries<ProtectedString<any>[]>(deletedIds)) {
72+
if (ids.length > 0) {
73+
logger.debug(`Deleted ${key}: ${JSON.stringify(ids)} `)
74+
}
75+
}
76+
5877
return [
5978
context.directCollections.ExpectedPackages.bulkWrite(this.#expectedPackages.generateWriteOps()),
6079
context.directCollections.ExpectedPlayoutItems.bulkWrite(this.#expectedPlayoutItems.generateWriteOps()),

packages/job-worker/src/playout/infinites.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/
2323
import { sortRundownIDsInPlaylist } from '@sofie-automation/corelib/dist/playout/playlist'
2424
import { mongoWhere } from '@sofie-automation/corelib/dist/mongo'
2525
import { PlayoutRundownModel } from './model/PlayoutRundownModel'
26+
import { logger } from '../logging'
2627

2728
/** When we crop a piece, set the piece as "it has definitely ended" this far into the future. */
2829
export const DEFINITELY_ENDED_FUTURE_DURATION = 1 * 1000
@@ -330,7 +331,26 @@ export function getPieceInstancesForPart(
330331
if (!playingRundown) throw new Error(`Rundown "${playingPartInstance.partInstance.rundownId}" not found!`)
331332

332333
playingSegment = playingRundown.getSegment(playingPartInstance.partInstance.segmentId)
333-
if (!playingSegment) throw new Error(`Segment "${playingPartInstance.partInstance.segmentId}" not found!`)
334+
if (!playingSegment) {
335+
const rundownId = playingRundown.rundown._id
336+
context.directCollections.Segments.findFetch({
337+
rundownId: rundownId,
338+
})
339+
.then((segment) => {
340+
logger.error(
341+
`TROUBLESHOOT: Segment not found, rundown "${rundownId}", segments in db: ${JSON.stringify(
342+
segment.map((s) => s._id)
343+
)}`
344+
)
345+
})
346+
.catch((e) => logger.error(e))
347+
348+
throw new Error(
349+
`Segment "${playingPartInstance.partInstance.segmentId}" in Rundown "${
350+
playingRundown.rundown._id
351+
}" not found! (other segments: ${JSON.stringify(playingRundown.segments.map((s) => s.segment._id))})`
352+
)
353+
}
334354
}
335355

336356
const segment = rundown.getSegment(part.segmentId)

0 commit comments

Comments
 (0)