From 2686035214cb0f74f6682490b95f434167372ab4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 17 Jul 2026 08:59:15 +0000 Subject: [PATCH] feat(packageManager): expose rundown piece content status to peripheral devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../api/integration/rundownContentStatus.ts | 115 ++++++++++++++++++ meteor/server/api/peripheralDevice.ts | 13 ++ .../src/peripheralDevice/methodsAPI.ts | 9 ++ .../peripheralDevice/rundownContentStatus.ts | 21 ++++ 4 files changed, 158 insertions(+) create mode 100644 meteor/server/api/integration/rundownContentStatus.ts create mode 100644 packages/shared-lib/src/peripheralDevice/rundownContentStatus.ts diff --git a/meteor/server/api/integration/rundownContentStatus.ts b/meteor/server/api/integration/rundownContentStatus.ts new file mode 100644 index 00000000000..feae14e4c68 --- /dev/null +++ b/meteor/server/api/integration/rundownContentStatus.ts @@ -0,0 +1,115 @@ +import { Meteor } from 'meteor/meteor' +import { check } from '../../lib/check' +import { MethodContext } from '../methodContext' +import { checkAccessAndGetPeripheralDevice } from '../../security/check' +import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { + RundownContentStatusResponse, + RundownPieceContentStatus, +} from '@sofie-automation/shared-lib/dist/peripheralDevice/rundownContentStatus' +import { Blueprints, Parts, Pieces, Rundowns, ShowStyleBases } from '../../collections' +import { fetchStudio } from '../../publications/pieceContentStatusUI/common' +import { + checkPieceContentStatusAndDependencies, + PieceContentStatusPiece, +} from '../../publications/pieceContentStatusUI/checkPieceContentStatus' +import { PieceContentStatusMessageFactory } from '../../publications/pieceContentStatusUI/messageFactory' +import { interpollateTranslation, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' + +function formatStatusReason( + status: Awaited>[0] +): string | undefined { + if (status.status === PieceStatusCode.OK) { + return undefined + } + + const firstMessage = status.messages[0] + if (!firstMessage) { + return undefined + } + + return translateMessage(firstMessage, interpollateTranslation) +} + +export namespace RundownContentStatusIntegration { + export async function getContentStatusForRundown( + context: MethodContext, + deviceId: PeripheralDeviceId, + deviceToken: string, + rundownExternalId: string + ): Promise { + check(rundownExternalId, String) + + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, deviceToken, context) + if (!peripheralDevice.studioAndConfigId) { + throw new Meteor.Error(400, 'Device "' + peripheralDevice._id + '" has no studio') + } + + const studioId = peripheralDevice.studioAndConfigId.studioId + const studio = await fetchStudio(studioId) + if (!studio) { + throw new Meteor.Error(404, `Studio "${studioId}" not found`) + } + + const rundown = await Rundowns.findOneAsync({ + studioId, + externalId: rundownExternalId, + }) + if (!rundown) { + return { + rundownExternalId, + pieces: [], + } + } + + const showStyleBase = await ShowStyleBases.findOneAsync(rundown.showStyleBaseId) + const blueprint = showStyleBase ? await Blueprints.findOneAsync(showStyleBase.blueprintId) : undefined + const messageFactory = new PieceContentStatusMessageFactory(blueprint) + + const parts = await Parts.findFetchAsync({ rundownId: rundown._id }) + const partExternalIds = new Map(parts.map((part) => [part._id, part.externalId])) + + const pieceDocs = await Pieces.findFetchAsync({ + startRundownId: rundown._id, + invalid: { $ne: true }, + }) + + const pieces: RundownPieceContentStatus[] = [] + + for (const pieceDoc of pieceDocs) { + const sourceLayer = showStyleBase?.sourceLayers?.[pieceDoc.sourceLayerId] + if (!sourceLayer) { + continue + } + + const statusPiece: PieceContentStatusPiece = { + _id: pieceDoc._id, + content: pieceDoc.content, + expectedPackages: pieceDoc.expectedPackages, + name: pieceDoc.name, + } + + const [status] = await checkPieceContentStatusAndDependencies( + studio, + rundown._id, + messageFactory, + statusPiece, + sourceLayer + ) + + pieces.push({ + pieceExternalId: pieceDoc.externalId, + partExternalId: pieceDoc.startPartId ? partExternalIds.get(pieceDoc.startPartId) : undefined, + statusCode: status.status, + ready: status.status === PieceStatusCode.OK, + reason: formatStatusReason(status), + }) + } + + return { + rundownExternalId, + pieces, + } + } +} diff --git a/meteor/server/api/peripheralDevice.ts b/meteor/server/api/peripheralDevice.ts index a49d7774107..85600c10924 100644 --- a/meteor/server/api/peripheralDevice.ts +++ b/meteor/server/api/peripheralDevice.ts @@ -36,6 +36,7 @@ import { triggerWriteAccess, triggerWriteAccessBecauseNoCheckNecessary } from '. import { checkAccessAndGetPeripheralDevice } from '../security/check' import { UserActionsLogItem } from '@sofie-automation/meteor-lib/dist/collections/UserActionsLog' import { PackageManagerIntegration } from './integration/expectedPackages' +import { RundownContentStatusIntegration } from './integration/rundownContentStatus' import { profiler } from './profiler' import { QueueStudioJob, QueueOrUpdateStudioJob } from '../worker/worker' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' @@ -1434,6 +1435,18 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri ) { await PackageManagerIntegration.removePackageInfo(this, deviceId, deviceToken, type, packageId, removeDelay) } + async getContentStatusForRundown( + deviceId: PeripheralDeviceId, + deviceToken: string, + rundownExternalId: string + ) { + return RundownContentStatusIntegration.getContentStatusForRundown( + this, + deviceId, + deviceToken, + rundownExternalId + ) + } // --- Triggers --- /** * This receives an arbitrary input from an Input-handling Peripheral Device. See diff --git a/packages/shared-lib/src/peripheralDevice/methodsAPI.ts b/packages/shared-lib/src/peripheralDevice/methodsAPI.ts index 5b9b09aae3d..d1ddfe13f12 100644 --- a/packages/shared-lib/src/peripheralDevice/methodsAPI.ts +++ b/packages/shared-lib/src/peripheralDevice/methodsAPI.ts @@ -34,6 +34,7 @@ import type { } from './peripheralDeviceAPI.js' import type { PeripheralDeviceExternalEvent } from './externalEvents.js' import type { MediaObject } from '../core/model/MediaObjects.js' +import type { RundownContentStatusResponse } from './rundownContentStatus.js' export type UpdateExpectedPackageWorkStatusesChanges = | { @@ -323,6 +324,13 @@ export interface NewPeripheralDeviceAPI { removeDelay?: number ): Promise + /** Read-only piece content status for a rundown (by ingest external id). */ + getContentStatusForRundown( + deviceId: PeripheralDeviceId, + deviceToken: string, + rundownExternalId: string + ): Promise + /** * This method is being called by a Peripheral Device handling external triggers when it receives an external * 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 { 'fetchPackageInfoMetadata' = 'peripheralDevice.packageManager.fetchPackageInfoMetadata', 'updatePackageInfo' = 'peripheralDevice.packageManager.updatePackageInfo', 'removePackageInfo' = 'peripheralDevice.packageManager.removePackageInfo', + 'getContentStatusForRundown' = 'peripheralDevice.packageManager.getContentStatusForRundown', 'requestUserAuthToken' = 'peripheralDevice.spreadsheet.requestUserAuthToken', 'storeAccessToken' = 'peripheralDevice.spreadsheet.storeAccessToken', diff --git a/packages/shared-lib/src/peripheralDevice/rundownContentStatus.ts b/packages/shared-lib/src/peripheralDevice/rundownContentStatus.ts new file mode 100644 index 00000000000..bbec2168b03 --- /dev/null +++ b/packages/shared-lib/src/peripheralDevice/rundownContentStatus.ts @@ -0,0 +1,21 @@ +/** + * Minimal per-piece content status returned to peripheral devices (e.g. Rundown Editor) + * that need READY/NOT READY badges without subscribing to the WebUI publication. + */ +export interface RundownPieceContentStatus { + /** Piece `externalId` as stored in Core (matches RE piece id when synced). */ + pieceExternalId: string + /** Part `externalId` the piece belongs to, when known. */ + partExternalId?: string + /** Numeric {@link PieceStatusCode} value from corelib. */ + statusCode: number + /** True when `statusCode` is OK (0). */ + ready: boolean + /** Human-readable summary for tooltips; omitted when ready. */ + reason?: string +} + +export interface RundownContentStatusResponse { + rundownExternalId: string + pieces: RundownPieceContentStatus[] +}