Skip to content

Commit dfc25cc

Browse files
feat(packageManager): expose rundown piece content status to peripheral devices (#6)
Add peripheralDevice.packageManager.getContentStatusForRundown, reusing checkPieceContentStatus.ts for PieceStatusCode computation. Returns a minimal per-piece payload (externalId, statusCode, ready, reason) scoped to the calling device's studio — intended for Rundown Editor hybrid readiness without subscribing to uiPieceContentStatuses. Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 9100611 commit dfc25cc

4 files changed

Lines changed: 158 additions & 0 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Meteor } from 'meteor/meteor'
2+
import { check } from '../../lib/check'
3+
import { MethodContext } from '../methodContext'
4+
import { checkAccessAndGetPeripheralDevice } from '../../security/check'
5+
import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids'
6+
import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece'
7+
import {
8+
RundownContentStatusResponse,
9+
RundownPieceContentStatus,
10+
} from '@sofie-automation/shared-lib/dist/peripheralDevice/rundownContentStatus'
11+
import { Blueprints, Parts, Pieces, Rundowns, ShowStyleBases } from '../../collections'
12+
import { fetchStudio } from '../../publications/pieceContentStatusUI/common'
13+
import {
14+
checkPieceContentStatusAndDependencies,
15+
PieceContentStatusPiece,
16+
} from '../../publications/pieceContentStatusUI/checkPieceContentStatus'
17+
import { PieceContentStatusMessageFactory } from '../../publications/pieceContentStatusUI/messageFactory'
18+
import { interpollateTranslation, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage'
19+
20+
function formatStatusReason(
21+
status: Awaited<ReturnType<typeof checkPieceContentStatusAndDependencies>>[0]
22+
): string | undefined {
23+
if (status.status === PieceStatusCode.OK) {
24+
return undefined
25+
}
26+
27+
const firstMessage = status.messages[0]
28+
if (!firstMessage) {
29+
return undefined
30+
}
31+
32+
return translateMessage(firstMessage, interpollateTranslation)
33+
}
34+
35+
export namespace RundownContentStatusIntegration {
36+
export async function getContentStatusForRundown(
37+
context: MethodContext,
38+
deviceId: PeripheralDeviceId,
39+
deviceToken: string,
40+
rundownExternalId: string
41+
): Promise<RundownContentStatusResponse> {
42+
check(rundownExternalId, String)
43+
44+
const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, deviceToken, context)
45+
if (!peripheralDevice.studioAndConfigId) {
46+
throw new Meteor.Error(400, 'Device "' + peripheralDevice._id + '" has no studio')
47+
}
48+
49+
const studioId = peripheralDevice.studioAndConfigId.studioId
50+
const studio = await fetchStudio(studioId)
51+
if (!studio) {
52+
throw new Meteor.Error(404, `Studio "${studioId}" not found`)
53+
}
54+
55+
const rundown = await Rundowns.findOneAsync({
56+
studioId,
57+
externalId: rundownExternalId,
58+
})
59+
if (!rundown) {
60+
return {
61+
rundownExternalId,
62+
pieces: [],
63+
}
64+
}
65+
66+
const showStyleBase = await ShowStyleBases.findOneAsync(rundown.showStyleBaseId)
67+
const blueprint = showStyleBase ? await Blueprints.findOneAsync(showStyleBase.blueprintId) : undefined
68+
const messageFactory = new PieceContentStatusMessageFactory(blueprint)
69+
70+
const parts = await Parts.findFetchAsync({ rundownId: rundown._id })
71+
const partExternalIds = new Map(parts.map((part) => [part._id, part.externalId]))
72+
73+
const pieceDocs = await Pieces.findFetchAsync({
74+
startRundownId: rundown._id,
75+
invalid: { $ne: true },
76+
})
77+
78+
const pieces: RundownPieceContentStatus[] = []
79+
80+
for (const pieceDoc of pieceDocs) {
81+
const sourceLayer = showStyleBase?.sourceLayers?.[pieceDoc.sourceLayerId]
82+
if (!sourceLayer) {
83+
continue
84+
}
85+
86+
const statusPiece: PieceContentStatusPiece = {
87+
_id: pieceDoc._id,
88+
content: pieceDoc.content,
89+
expectedPackages: pieceDoc.expectedPackages,
90+
name: pieceDoc.name,
91+
}
92+
93+
const [status] = await checkPieceContentStatusAndDependencies(
94+
studio,
95+
rundown._id,
96+
messageFactory,
97+
statusPiece,
98+
sourceLayer
99+
)
100+
101+
pieces.push({
102+
pieceExternalId: pieceDoc.externalId,
103+
partExternalId: pieceDoc.startPartId ? partExternalIds.get(pieceDoc.startPartId) : undefined,
104+
statusCode: status.status,
105+
ready: status.status === PieceStatusCode.OK,
106+
reason: formatStatusReason(status),
107+
})
108+
}
109+
110+
return {
111+
rundownExternalId,
112+
pieces,
113+
}
114+
}
115+
}

meteor/server/api/peripheralDevice.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { triggerWriteAccess, triggerWriteAccessBecauseNoCheckNecessary } from '.
3636
import { checkAccessAndGetPeripheralDevice } from '../security/check'
3737
import { UserActionsLogItem } from '@sofie-automation/meteor-lib/dist/collections/UserActionsLog'
3838
import { PackageManagerIntegration } from './integration/expectedPackages'
39+
import { RundownContentStatusIntegration } from './integration/rundownContentStatus'
3940
import { profiler } from './profiler'
4041
import { QueueStudioJob, QueueOrUpdateStudioJob } from '../worker/worker'
4142
import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio'
@@ -1434,6 +1435,18 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri
14341435
) {
14351436
await PackageManagerIntegration.removePackageInfo(this, deviceId, deviceToken, type, packageId, removeDelay)
14361437
}
1438+
async getContentStatusForRundown(
1439+
deviceId: PeripheralDeviceId,
1440+
deviceToken: string,
1441+
rundownExternalId: string
1442+
) {
1443+
return RundownContentStatusIntegration.getContentStatusForRundown(
1444+
this,
1445+
deviceId,
1446+
deviceToken,
1447+
rundownExternalId
1448+
)
1449+
}
14371450
// --- Triggers ---
14381451
/**
14391452
* This receives an arbitrary input from an Input-handling Peripheral Device. See

packages/shared-lib/src/peripheralDevice/methodsAPI.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import type {
3434
} from './peripheralDeviceAPI.js'
3535
import type { PeripheralDeviceExternalEvent } from './externalEvents.js'
3636
import type { MediaObject } from '../core/model/MediaObjects.js'
37+
import type { RundownContentStatusResponse } from './rundownContentStatus.js'
3738

3839
export type UpdateExpectedPackageWorkStatusesChanges =
3940
| {
@@ -323,6 +324,13 @@ export interface NewPeripheralDeviceAPI {
323324
removeDelay?: number
324325
): Promise<void>
325326

327+
/** Read-only piece content status for a rundown (by ingest external id). */
328+
getContentStatusForRundown(
329+
deviceId: PeripheralDeviceId,
330+
deviceToken: string,
331+
rundownExternalId: string
332+
): Promise<RundownContentStatusResponse>
333+
326334
/**
327335
* This method is being called by a Peripheral Device handling external triggers when it receives an external
328336
* trigger event or an external input changes it's state (a knob changes it's rotation, a joystick is moved, etc.)
@@ -432,6 +440,7 @@ export enum PeripheralDeviceAPIMethods {
432440
'fetchPackageInfoMetadata' = 'peripheralDevice.packageManager.fetchPackageInfoMetadata',
433441
'updatePackageInfo' = 'peripheralDevice.packageManager.updatePackageInfo',
434442
'removePackageInfo' = 'peripheralDevice.packageManager.removePackageInfo',
443+
'getContentStatusForRundown' = 'peripheralDevice.packageManager.getContentStatusForRundown',
435444

436445
'requestUserAuthToken' = 'peripheralDevice.spreadsheet.requestUserAuthToken',
437446
'storeAccessToken' = 'peripheralDevice.spreadsheet.storeAccessToken',
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Minimal per-piece content status returned to peripheral devices (e.g. Rundown Editor)
3+
* that need READY/NOT READY badges without subscribing to the WebUI publication.
4+
*/
5+
export interface RundownPieceContentStatus {
6+
/** Piece `externalId` as stored in Core (matches RE piece id when synced). */
7+
pieceExternalId: string
8+
/** Part `externalId` the piece belongs to, when known. */
9+
partExternalId?: string
10+
/** Numeric {@link PieceStatusCode} value from corelib. */
11+
statusCode: number
12+
/** True when `statusCode` is OK (0). */
13+
ready: boolean
14+
/** Human-readable summary for tooltips; omitted when ready. */
15+
reason?: string
16+
}
17+
18+
export interface RundownContentStatusResponse {
19+
rundownExternalId: string
20+
pieces: RundownPieceContentStatus[]
21+
}

0 commit comments

Comments
 (0)