From f50e07781734dd372059ecfe5a0563c8aeacd84d Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 8 Jan 2026 02:23:39 +0100 Subject: [PATCH] SOFIE-295 | add informative REST results for TAKE failures --- meteor/server/api/rest/v1/playlists.ts | 6 +-- meteor/server/lib/rest/v1/playlists.ts | 4 +- packages/corelib/src/worker/studio.ts | 6 ++- packages/job-worker/src/playout/take.ts | 43 ++++++++++++++++++---- packages/meteor-lib/src/api/userActions.ts | 8 +++- 5 files changed, 51 insertions(+), 16 deletions(-) diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 2616f2e6f9..8cb73b082c 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -29,7 +29,7 @@ import { } from '../../../collections' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { ServerClientAPI } from '../../client' -import { QueueNextSegmentResult, StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' +import { QueueNextSegmentResult, StudioJobs, TakeNextPartResult } from '@sofie-automation/corelib/dist/worker/studio' import { getCurrentTime } from '../../../lib/lib' import { TriggerReloadDataResponse } from '@sofie-automation/meteor-lib/dist/api/userActions' import { ServerRundownAPI } from '../../rundown' @@ -457,7 +457,7 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { event: string, rundownPlaylistId: RundownPlaylistId, fromPartInstanceId: PartInstanceId | undefined - ): Promise> { + ): Promise> { triggerWriteAccess() const rundownPlaylist = await RundownPlaylists.findOneAsync(rundownPlaylistId) if (!rundownPlaylist) throw new Error(`Rundown playlist ${rundownPlaylistId} does not exist`) @@ -801,7 +801,7 @@ export function registerRoutes(registerRoute: APIRegisterHook) } ) - registerRoute<{ playlistId: string }, { fromPartInstanceId?: string }, void>( + registerRoute<{ playlistId: string }, { fromPartInstanceId?: string }, TakeNextPartResult>( 'post', '/playlists/:playlistId/take', new Map([ diff --git a/meteor/server/lib/rest/v1/playlists.ts b/meteor/server/lib/rest/v1/playlists.ts index 74a60a2976..780226aa4d 100644 --- a/meteor/server/lib/rest/v1/playlists.ts +++ b/meteor/server/lib/rest/v1/playlists.ts @@ -10,7 +10,7 @@ import { RundownPlaylistId, SegmentId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { QueueNextSegmentResult } from '@sofie-automation/corelib/dist/worker/studio' +import { QueueNextSegmentResult, TakeNextPartResult } from '@sofie-automation/corelib/dist/worker/studio' import { Meteor } from 'meteor/meteor' /* ************************************************************************* @@ -228,7 +228,7 @@ export interface PlaylistsRestAPI { event: string, rundownPlaylistId: RundownPlaylistId, fromPartInstanceId: PartInstanceId | undefined - ): Promise> + ): Promise> /** * Clears the specified SourceLayers. * diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index e5df4d1311..395c31ce4e 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -369,6 +369,10 @@ export interface SwitchRouteSetProps { state: boolean | 'toggle' } +export interface TakeNextPartResult { + nextTakeTime: number +} + /** * Set of valid functions, of form: * `id: (data) => return` @@ -393,7 +397,7 @@ export type StudioJobFunc = { [StudioJobs.QueueNextSegment]: (data: QueueNextSegmentProps) => QueueNextSegmentResult [StudioJobs.ExecuteAction]: (data: ExecuteActionProps) => ExecuteActionResult [StudioJobs.ExecuteBucketAdLibOrAction]: (data: ExecuteBucketAdLibOrActionProps) => ExecuteActionResult - [StudioJobs.TakeNextPart]: (data: TakeNextPartProps) => void + [StudioJobs.TakeNextPart]: (data: TakeNextPartProps) => TakeNextPartResult [StudioJobs.DisableNextPiece]: (data: DisableNextPieceProps) => void [StudioJobs.RemovePlaylist]: (data: RemovePlaylistProps) => void [StudioJobs.RegeneratePlaylist]: (data: RegeneratePlaylistProps) => void diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index e1e2f47d22..8cd5d0d0fa 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -36,10 +36,12 @@ import { PlayoutRundownModel } from './model/PlayoutRundownModel.js' import { convertNoteToNotification } from '../notifications/util.js' import { PersistentPlayoutStateStore } from '../blueprints/context/services/PersistantStateStore.js' +import { TakeNextPartResult } from '@sofie-automation/corelib/dist/worker/studio' + /** * Take the currently Next:ed Part (start playing it) */ -export async function handleTakeNextPart(context: JobContext, data: TakeNextPartProps): Promise { +export async function handleTakeNextPart(context: JobContext, data: TakeNextPartProps): Promise { const now = getCurrentTime() return runJobWithPlayoutModel( @@ -73,17 +75,29 @@ export async function handleTakeNextPart(context: JobContext, data: TakeNextPart } } if (lastTakeTime && now - lastTakeTime < context.studio.settings.minimumTakeSpan) { + const nextTakeTime = lastTakeTime + context.studio.settings.minimumTakeSpan logger.debug( `Time since last take is shorter than ${context.studio.settings.minimumTakeSpan} for ${ playlist.currentPartInfo?.partInstanceId }: ${now - lastTakeTime}` ) - throw UserError.create(UserErrorMessage.TakeRateLimit, { - duration: context.studio.settings.minimumTakeSpan, - }) + throw UserError.create( + UserErrorMessage.TakeRateLimit, + { + duration: context.studio.settings.minimumTakeSpan, + nextAllowedTakeTime: nextTakeTime, + }, + 429 + ) } - return performTakeToNextedPart(context, playoutModel, now) + const nextTakeTime = now + context.studio.settings.minimumTakeSpan + + await performTakeToNextedPart(context, playoutModel, now) + + return { + nextTakeTime, + } } ) } @@ -154,7 +168,14 @@ export async function performTakeToNextedPart( logger.debug( `Take is blocked until ${currentPartInstance.partInstance.blockTakeUntil}. Which is in: ${remainingTime}` ) - throw UserError.create(UserErrorMessage.TakeBlockedDuration, { duration: remainingTime }) + throw UserError.create( + UserErrorMessage.TakeBlockedDuration, + { + duration: remainingTime, + nextAllowedTakeTime: currentPartInstance.partInstance.blockTakeUntil, + }, + 425 + ) } // If there was a transition from the previous Part, then ensure that has finished before another take is permitted @@ -166,11 +187,17 @@ export async function performTakeToNextedPart( start && now < start + currentPartInstance.partInstance.part.inTransition.blockTakeDuration ) { - throw UserError.create(UserErrorMessage.TakeDuringTransition) + throw UserError.create( + UserErrorMessage.TakeDuringTransition, + { + nextAllowedTakeTime: start + currentPartInstance.partInstance.part.inTransition.blockTakeDuration, + }, + 425 + ) } if (currentPartInstance.isTooCloseToAutonext(true)) { - throw UserError.create(UserErrorMessage.TakeCloseToAutonext) + throw UserError.create(UserErrorMessage.TakeCloseToAutonext, undefined, 425) } } diff --git a/packages/meteor-lib/src/api/userActions.ts b/packages/meteor-lib/src/api/userActions.ts index fd1a07347e..227ce3f7d3 100644 --- a/packages/meteor-lib/src/api/userActions.ts +++ b/packages/meteor-lib/src/api/userActions.ts @@ -6,7 +6,11 @@ import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLi import { AdLibActionCommon } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' import { Time } from '@sofie-automation/blueprints-integration' -import { ExecuteActionResult, QueueNextSegmentResult } from '@sofie-automation/corelib/dist/worker/studio' +import { + ExecuteActionResult, + QueueNextSegmentResult, + TakeNextPartResult, +} from '@sofie-automation/corelib/dist/worker/studio' import { AdLibActionId, BucketAdLibActionId, @@ -34,7 +38,7 @@ export interface NewUserActionAPI { eventTime: Time, rundownPlaylistId: RundownPlaylistId, fromPartInstanceId: PartInstanceId | null - ): Promise> + ): Promise> setNext( userEvent: string, eventTime: Time,