From a4495487b05ed12a8e331cca82c8cc2de2b1716f Mon Sep 17 00:00:00 2001 From: ianshade Date: Wed, 13 May 2026 15:10:17 +0200 Subject: [PATCH 1/2] feat(EAV-880): add `createdByAdLib` to ActivePlaylistTopic parts --- .../part/partBase/partBase-example.yaml | 1 + .../components/part/partBase/partBase.yaml | 4 + .../src/generated/asyncapi.yaml | 8 ++ .../src/generated/schema.ts | 12 +++ .../topics/__tests__/activePlaylist.spec.ts | 83 +++++++++++++++++++ .../src/topics/activePlaylistTopic.ts | 29 ++++--- .../helpers/__tests__/segmentParts.test.ts | 53 ++++++++++++ .../src/topics/helpers/segmentParts.ts | 13 +-- 8 files changed, 185 insertions(+), 18 deletions(-) create mode 100644 packages/live-status-gateway/src/topics/helpers/__tests__/segmentParts.test.ts diff --git a/packages/live-status-gateway-api/api/components/part/partBase/partBase-example.yaml b/packages/live-status-gateway-api/api/components/part/partBase/partBase-example.yaml index 9bb311856e0..457eabd39c7 100644 --- a/packages/live-status-gateway-api/api/components/part/partBase/partBase-example.yaml +++ b/packages/live-status-gateway-api/api/components/part/partBase/partBase-example.yaml @@ -1,3 +1,4 @@ id: 'H5CBGYjThrMSmaYvRaa5FVKJIzk_' name: 'Intro' autoNext: false +createdByAdLib: false diff --git a/packages/live-status-gateway-api/api/components/part/partBase/partBase.yaml b/packages/live-status-gateway-api/api/components/part/partBase/partBase.yaml index 1b69e09781a..af837894e4a 100644 --- a/packages/live-status-gateway-api/api/components/part/partBase/partBase.yaml +++ b/packages/live-status-gateway-api/api/components/part/partBase/partBase.yaml @@ -9,6 +9,10 @@ $defs: name: description: User-presentable name of the part type: string + createdByAdLib: + description: Whether this part was created by an adlib + type: boolean + default: false autoNext: description: If this part will progress to the next automatically type: boolean diff --git a/packages/live-status-gateway-api/src/generated/asyncapi.yaml b/packages/live-status-gateway-api/src/generated/asyncapi.yaml index 1632bfd455d..e724382658b 100644 --- a/packages/live-status-gateway-api/src/generated/asyncapi.yaml +++ b/packages/live-status-gateway-api/src/generated/asyncapi.yaml @@ -381,6 +381,10 @@ channels: name: description: User-presentable name of the part type: string + createdByAdLib: + description: Whether this part was created by an adlib + type: boolean + default: false autoNext: description: If this part will progress to the next automatically type: boolean @@ -392,6 +396,7 @@ channels: - id: H5CBGYjThrMSmaYvRaa5FVKJIzk_ name: Intro autoNext: false + createdByAdLib: false - type: object title: PartStatus properties: @@ -485,6 +490,7 @@ channels: id: H5CBGYjThrMSmaYvRaa5FVKJIzk_ name: Intro autoNext: false + createdByAdLib: false - type: object title: CurrentPartStatus properties: @@ -532,6 +538,7 @@ channels: id: H5CBGYjThrMSmaYvRaa5FVKJIzk_ name: Intro autoNext: false + createdByAdLib: false - type: "null" currentSegment: oneOf: @@ -625,6 +632,7 @@ channels: id: H5CBGYjThrMSmaYvRaa5FVKJIzk_ name: Intro autoNext: false + createdByAdLib: false required: - timing - parts diff --git a/packages/live-status-gateway-api/src/generated/schema.ts b/packages/live-status-gateway-api/src/generated/schema.ts index a589641f698..dd043d37ab7 100644 --- a/packages/live-status-gateway-api/src/generated/schema.ts +++ b/packages/live-status-gateway-api/src/generated/schema.ts @@ -207,6 +207,10 @@ interface CurrentPartStatus { * User-presentable name of the part */ name: string + /** + * Whether this part was created by an adlib + */ + createdByAdLib?: boolean /** * If this part will progress to the next automatically */ @@ -348,6 +352,10 @@ interface CurrentSegmentPart { * User-presentable name of the part */ name: string + /** + * Whether this part was created by an adlib + */ + createdByAdLib?: boolean /** * If this part will progress to the next automatically */ @@ -373,6 +381,10 @@ interface PartStatus { * User-presentable name of the part */ name: string + /** + * Whether this part was created by an adlib + */ + createdByAdLib?: boolean /** * If this part will progress to the next automatically */ diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 57855cef858..dba04ffac3d 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -142,6 +142,7 @@ describe('ActivePlaylistTopic', () => { id: 'PART_1', name: 'Test Part', segmentId: 'SEGMENT_1', + createdByAdLib: false, timing: { startTime: 1600000060000, expectedDurationMs: 10000, projectedEndTime: 1600000070000 }, pieces: [], autoNext: undefined, @@ -158,6 +159,7 @@ describe('ActivePlaylistTopic', () => { { id: 'PART_1', name: 'Test Part', + createdByAdLib: false, timing: { expectedDurationMs: 10000, }, @@ -181,6 +183,85 @@ describe('ActivePlaylistTopic', () => { ) }) + it('marks adlib-created parts in active playlist status', async () => { + const handlers = makeMockHandlers() + const topic = new ActivePlaylistTopic(makeMockLogger(), handlers) + const mockSubscriber = makeMockSubscriber() + + const currentPartInstanceId = 'CURRENT_PART_INSTANCE_ID' + const nextPartInstanceId = 'NEXT_PART_INSTANCE_ID' + + const playlist = makeTestPlaylist() + playlist.activationId = protectString('somethingRandom') + playlist.currentPartInfo = { + consumesQueuedSegmentId: false, + manuallySelected: false, + partInstanceId: protectString(currentPartInstanceId), + rundownId: playlist.rundownIdsInOrder[0], + } + playlist.nextPartInfo = { + consumesQueuedSegmentId: false, + manuallySelected: false, + partInstanceId: protectString(nextPartInstanceId), + rundownId: playlist.rundownIdsInOrder[0], + } + handlers.playlistHandler.notify(playlist) + + const testShowStyleBase = makeTestShowStyleBase() + handlers.showStyleBaseHandler.notify(testShowStyleBase as ShowStyleBaseExt) + + const segment1id = protectString('SEGMENT_1') + const currentPart: Partial = { + _id: protectString('PART_1'), + title: 'Current AdLib Part', + segmentId: segment1id, + expectedDurationWithTransition: 10000, + expectedDuration: 10000, + } + const nextPart: Partial = { + _id: protectString('PART_2'), + title: 'Next AdLib Part', + segmentId: segment1id, + expectedDurationWithTransition: 8000, + expectedDuration: 8000, + } + const testPartInstances: PartialDeep = { + current: { + _id: currentPartInstanceId, + part: currentPart, + segmentId: segment1id, + orphaned: 'adlib-part', + }, + next: { + _id: nextPartInstanceId, + part: nextPart, + segmentId: segment1id, + orphaned: 'adlib-part', + }, + firstInSegmentPlayout: {}, + inCurrentSegment: [ + literal>({ + _id: protectString(currentPartInstanceId), + part: currentPart, + segmentId: segment1id, + orphaned: 'adlib-part', + }), + ] as DBPartInstance[], + } + handlers.partInstancesHandler.notify(testPartInstances as SelectedPartInstances) + handlers.partsHandler.notify([currentPart as DBPart, nextPart as DBPart]) + handlers.segmentHandler.notify({ + _id: segment1id, + } as DBSegment) + + topic.addSubscriber(mockSubscriber) + + const sentStatus = JSON.parse(mockSubscriber.send.mock.calls[0][0] as string) as ActivePlaylistEvent + expect(sentStatus.currentPart?.createdByAdLib).toBe(true) + expect(sentStatus.nextPart?.createdByAdLib).toBe(true) + expect(sentStatus.currentSegment?.parts[0].createdByAdLib).toBe(true) + }) + it('provides segment and part with segment timing', async () => { const handlers = makeMockHandlers() const topic = new ActivePlaylistTopic(makeMockLogger(), handlers) @@ -247,6 +328,7 @@ describe('ActivePlaylistTopic', () => { id: 'PART_1', name: 'Test Part', segmentId: 'SEGMENT_1', + createdByAdLib: false, timing: { startTime: 1600000060000, expectedDurationMs: 10000, projectedEndTime: 1600000070000 }, pieces: [], autoNext: undefined, @@ -265,6 +347,7 @@ describe('ActivePlaylistTopic', () => { { id: 'PART_1', name: 'Test Part', + createdByAdLib: false, timing: { expectedDurationMs: 10000, }, diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index a2c75962da7..b0c169e8faa 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -127,6 +127,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket id: unprotectString(currentPart._id), name: currentPart.title, autoNext: currentPart.autoNext, + createdByAdLib: this._currentPartInstance.orphaned === 'adlib-part', segmentId: unprotectString(currentPart.segmentId), timing: calculateCurrentPartTiming( this._currentPartInstance, @@ -156,19 +157,21 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket ), }) : null, - nextPart: nextPart - ? literal({ - id: unprotectString(nextPart._id), - name: nextPart.title, - autoNext: nextPart.autoNext, - segmentId: unprotectString(nextPart.segmentId), - pieces: - this._pieceInstancesInNextPartInstance?.map((piece) => - toPieceStatus(piece, this._showStyleBaseExt) - ) ?? [], - publicData: nextPart.publicData, - }) - : null, + nextPart: + this._nextPartInstance && nextPart + ? literal({ + id: unprotectString(nextPart._id), + name: nextPart.title, + autoNext: nextPart.autoNext, + createdByAdLib: this._nextPartInstance.orphaned === 'adlib-part', + segmentId: unprotectString(nextPart.segmentId), + pieces: + this._pieceInstancesInNextPartInstance?.map((piece) => + toPieceStatus(piece, this._showStyleBaseExt) + ) ?? [], + publicData: nextPart.publicData, + }) + : null, quickLoop: this.transformQuickLoopStatus(), publicData: this._activePlaylist.publicData, playoutState: this._activePlaylist.publicPlayoutPersistentState, diff --git a/packages/live-status-gateway/src/topics/helpers/__tests__/segmentParts.test.ts b/packages/live-status-gateway/src/topics/helpers/__tests__/segmentParts.test.ts new file mode 100644 index 00000000000..ab0cb3fab91 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/__tests__/segmentParts.test.ts @@ -0,0 +1,53 @@ +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { getCurrentSegmentParts } from '../segmentParts.js' + +function makePart(id: string, title: string): DBPart { + return { + _id: protectString(id), + _rank: 0, + rundownId: protectString('rundown_1'), + segmentId: protectString('segment_1'), + notes: [], + externalId: id, + expectedDuration: 1000, + expectedDurationWithTransition: 1000, + title, + } +} + +describe('segmentParts - getCurrentSegmentParts', () => { + it('marks adlib-created parts', () => { + const adlibPart = makePart('part_1', 'AdLib Part') + const normalPart = makePart('part_2', 'Normal Part') + + const result = getCurrentSegmentParts( + [ + { + _id: protectString('partInstance_1'), + part: adlibPart, + orphaned: 'adlib-part', + } as DBPartInstance, + ], + [adlibPart, normalPart] + ) + + expect(result).toEqual([ + { + id: 'part_1', + name: 'AdLib Part', + autoNext: undefined, + createdByAdLib: true, + timing: { expectedDurationMs: 1000 }, + }, + { + id: 'part_2', + name: 'Normal Part', + autoNext: undefined, + createdByAdLib: false, + timing: { expectedDurationMs: 1000 }, + }, + ]) + }) +}) diff --git a/packages/live-status-gateway/src/topics/helpers/segmentParts.ts b/packages/live-status-gateway/src/topics/helpers/segmentParts.ts index 6fbe4bac9ef..b9b9c9bae0e 100644 --- a/packages/live-status-gateway/src/topics/helpers/segmentParts.ts +++ b/packages/live-status-gateway/src/topics/helpers/segmentParts.ts @@ -9,10 +9,10 @@ export function getCurrentSegmentParts( segmentPartInstances: DBPartInstance[], segmentParts: DBPart[] ): CurrentSegmentPart[] { - const partInstancesByPartId: Record = _.indexBy( - segmentPartInstances, - (partInstance) => unprotectString(partInstance.part._id) - ) + const partInstancesByPartId: Record< + string, + { _id: string | PartInstanceId; part: DBPart; orphaned?: DBPartInstance['orphaned'] } + > = _.indexBy(segmentPartInstances, (partInstance) => unprotectString(partInstance.part._id)) segmentParts.forEach((part) => { const partId = unprotectString(part._id) if (partInstancesByPartId[partId]) return @@ -22,13 +22,16 @@ export function getCurrentSegmentParts( } partInstancesByPartId[partId] = partInstance }) - return Object.values<{ _id: string | PartInstanceId; part: DBPart }>(partInstancesByPartId) + return Object.values<{ _id: string | PartInstanceId; part: DBPart; orphaned?: DBPartInstance['orphaned'] }>( + partInstancesByPartId + ) .sort((a, b) => a.part._rank - b.part._rank) .map( (partInstance): CurrentSegmentPart => ({ id: unprotectString(partInstance.part._id), name: partInstance.part.title, autoNext: partInstance.part.autoNext, + createdByAdLib: partInstance.orphaned === 'adlib-part', timing: { expectedDurationMs: partInstance.part.expectedDuration, }, From 86afa83193863aceaca47b8de38f6cec9398491f Mon Sep 17 00:00:00 2001 From: ianshade Date: Fri, 22 May 2026 14:18:30 +0200 Subject: [PATCH 2/2] chore(EAV-880): move `createdByAdLib` to partBase --- .../api/components/part/partBase/partBase.yaml | 2 +- .../components/part/resolvedPart/resolvedPart.yaml | 5 +---- .../src/generated/asyncapi.yaml | 5 +---- .../src/generated/schema.ts | 14 +++++++------- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/live-status-gateway-api/api/components/part/partBase/partBase.yaml b/packages/live-status-gateway-api/api/components/part/partBase/partBase.yaml index af837894e4a..8cc8dc6ded6 100644 --- a/packages/live-status-gateway-api/api/components/part/partBase/partBase.yaml +++ b/packages/live-status-gateway-api/api/components/part/partBase/partBase.yaml @@ -17,6 +17,6 @@ $defs: description: If this part will progress to the next automatically type: boolean default: false - required: [id, name] + required: [id, name, createdByAdLib] examples: - $ref: './partBase-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/part/resolvedPart/resolvedPart.yaml b/packages/live-status-gateway-api/api/components/part/resolvedPart/resolvedPart.yaml index bfe96a31976..f25032d456d 100644 --- a/packages/live-status-gateway-api/api/components/part/resolvedPart/resolvedPart.yaml +++ b/packages/live-status-gateway-api/api/components/part/resolvedPart/resolvedPart.yaml @@ -39,9 +39,6 @@ $defs: state: description: Set only for the current or next part $ref: '#/$defs/resolvedPartState' - createdByAdLib: - type: boolean - description: Whether this part was created by an adlib publicData: description: Optional arbitrary data timing: @@ -51,6 +48,6 @@ $defs: type: array items: $ref: '../../piece/resolvedPiece/resolvedPiece.yaml#/$defs/resolvedPiece' - required: [instanceId, externalId, rank, createdByAdLib, timing, pieces, invalid, floated, untimed] + required: [instanceId, externalId, rank, timing, pieces, invalid, floated, untimed] examples: - $ref: './resolvedPart-example.yaml' diff --git a/packages/live-status-gateway-api/src/generated/asyncapi.yaml b/packages/live-status-gateway-api/src/generated/asyncapi.yaml index e724382658b..b4de6b607a1 100644 --- a/packages/live-status-gateway-api/src/generated/asyncapi.yaml +++ b/packages/live-status-gateway-api/src/generated/asyncapi.yaml @@ -392,6 +392,7 @@ channels: required: - id - name + - createdByAdLib examples: - id: H5CBGYjThrMSmaYvRaa5FVKJIzk_ name: Intro @@ -1358,9 +1359,6 @@ channels: enum: - current - next - createdByAdLib: - type: boolean - description: Whether this part was created by an adlib publicData: description: Optional arbitrary data timing: @@ -1507,7 +1505,6 @@ channels: - instanceId - externalId - rank - - createdByAdLib - timing - pieces - invalid diff --git a/packages/live-status-gateway-api/src/generated/schema.ts b/packages/live-status-gateway-api/src/generated/schema.ts index dd043d37ab7..6c4ca402bd2 100644 --- a/packages/live-status-gateway-api/src/generated/schema.ts +++ b/packages/live-status-gateway-api/src/generated/schema.ts @@ -210,7 +210,7 @@ interface CurrentPartStatus { /** * Whether this part was created by an adlib */ - createdByAdLib?: boolean + createdByAdLib: boolean /** * If this part will progress to the next automatically */ @@ -355,7 +355,7 @@ interface CurrentSegmentPart { /** * Whether this part was created by an adlib */ - createdByAdLib?: boolean + createdByAdLib: boolean /** * If this part will progress to the next automatically */ @@ -384,7 +384,7 @@ interface PartStatus { /** * Whether this part was created by an adlib */ - createdByAdLib?: boolean + createdByAdLib: boolean /** * If this part will progress to the next automatically */ @@ -869,6 +869,10 @@ interface ResolvedPart { * User-presentable name of the part */ name: string + /** + * Whether this part was created by an adlib + */ + createdByAdLib: boolean /** * If this part will progress to the next automatically */ @@ -905,10 +909,6 @@ interface ResolvedPart { * Set only for the current or next part */ state?: ResolvedPartState - /** - * Whether this part was created by an adlib - */ - createdByAdLib: boolean /** * Optional arbitrary data */