diff --git a/meteor/server/api/rest/v1/__tests__/playlists.spec.ts b/meteor/server/api/rest/v1/__tests__/playlists.spec.ts new file mode 100644 index 00000000000..70f8b0f7c7a --- /dev/null +++ b/meteor/server/api/rest/v1/__tests__/playlists.spec.ts @@ -0,0 +1,77 @@ +import { registerRoutes } from '../playlists' +import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { PlaylistsRestAPI } from '../../../../lib/rest/v1' + +describe('Playlists REST API Routes', () => { + let mockRegisterRoute: jest.Mock + let mockServerAPI: jest.Mocked + + beforeEach(() => { + mockRegisterRoute = jest.fn() + mockServerAPI = { + tTimerStartCountdown: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + tTimerStartFreeRun: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + tTimerPause: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + tTimerResume: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + tTimerRestart: jest.fn().mockResolvedValue(ClientAPI.responseSuccess(undefined)), + } as any + + registerRoutes(mockRegisterRoute) + }) + + test('should register T-timer countdown route', () => { + const countdownRoute = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/countdown' + ) + expect(countdownRoute).toBeDefined() + expect(countdownRoute[0]).toBe('post') + }) + + test('T-timer countdown handler should call serverAPI.tTimerStartCountdown', async () => { + const countdownRoute = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/countdown' + ) + const handler = countdownRoute[4] + + const params = { playlistId: 'playlist0', timerIndex: '1' } + const body = { duration: 60, stopAtZero: true, startPaused: false } + const connection = {} as any + const event = 'test-event' + + await handler(mockServerAPI, connection, event, params, body) + + expect(mockServerAPI.tTimerStartCountdown).toHaveBeenCalledWith( + connection, + event, + protectString('playlist0'), + 1, + 60, + true, + false + ) + }) + + test('should register T-timer pause route', () => { + const pauseRoute = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/pause' + ) + expect(pauseRoute).toBeDefined() + expect(pauseRoute[0]).toBe('post') + }) + + test('T-timer pause handler should call serverAPI.tTimerPause', async () => { + const pauseRoute = mockRegisterRoute.mock.calls.find( + (call) => call[1] === '/playlists/:playlistId/t-timers/:timerIndex/pause' + ) + const handler = pauseRoute[4] + + const params = { playlistId: 'playlist0', timerIndex: '2' } + const connection = {} as any + const event = 'test-event' + + await handler(mockServerAPI, connection, event, params, {}) + + expect(mockServerAPI.tTimerPause).toHaveBeenCalledWith(connection, event, protectString('playlist0'), 2) + }) +}) diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 2616f2e6f93..0f03db9a188 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -13,6 +13,7 @@ import { RundownPlaylistId, SegmentId, } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Match, check } from '../../../lib/check' import { PlaylistsRestAPI } from '../../../lib/rest/v1' import { Meteor } from 'meteor/meteor' @@ -544,6 +545,133 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { } ) } + + async tTimerStartCountdown( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + duration: number, + stopAtZero?: boolean, + startPaused?: boolean + ): Promise> { + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + rundownPlaylistId, + () => { + check(rundownPlaylistId, String) + check(timerIndex, Number) + check(duration, Number) + check(stopAtZero, Match.Optional(Boolean)) + check(startPaused, Match.Optional(Boolean)) + }, + StudioJobs.TTimerStartCountdown, + { + playlistId: rundownPlaylistId, + timerIndex, + duration, + stopAtZero: !!stopAtZero, + startPaused: !!startPaused, + } + ) + } + + async tTimerStartFreeRun( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + startPaused?: boolean + ): Promise> { + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + rundownPlaylistId, + () => { + check(rundownPlaylistId, String) + check(timerIndex, Number) + check(startPaused, Match.Optional(Boolean)) + }, + StudioJobs.TTimerStartFreeRun, + { + playlistId: rundownPlaylistId, + timerIndex, + startPaused: !!startPaused, + } + ) + } + + async tTimerPause( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> { + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + rundownPlaylistId, + () => { + check(rundownPlaylistId, String) + check(timerIndex, Number) + }, + StudioJobs.TTimerPause, + { + playlistId: rundownPlaylistId, + timerIndex, + } + ) + } + + async tTimerResume( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> { + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + rundownPlaylistId, + () => { + check(rundownPlaylistId, String) + check(timerIndex, Number) + }, + StudioJobs.TTimerResume, + { + playlistId: rundownPlaylistId, + timerIndex, + } + ) + } + + async tTimerRestart( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> { + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + rundownPlaylistId, + () => { + check(rundownPlaylistId, String) + check(timerIndex, Number) + }, + StudioJobs.TTimerRestart, + { + playlistId: rundownPlaylistId, + timerIndex, + } + ) + } } class PlaylistsAPIFactory implements APIFactory { @@ -877,4 +1005,102 @@ export function registerRoutes(registerRoute: APIRegisterHook) return await serverAPI.recallStickyPiece(connection, event, playlistId, sourceLayerId) } ) + + registerRoute< + { playlistId: string; timerIndex: string }, + { duration: number; stopAtZero?: boolean; startPaused?: boolean }, + void + >( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/countdown', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, body) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = Number.parseInt(params.timerIndex) as RundownTTimerIndex + logger.info(`API POST: t-timer countdown ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + return await serverAPI.tTimerStartCountdown( + connection, + event, + rundownPlaylistId, + timerIndex, + body.duration, + body.stopAtZero, + body.startPaused + ) + } + ) + + registerRoute<{ playlistId: string; timerIndex: string }, { startPaused?: boolean }, void>( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/free-run', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, body) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = Number.parseInt(params.timerIndex) as RundownTTimerIndex + logger.info(`API POST: t-timer free-run ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + return await serverAPI.tTimerStartFreeRun( + connection, + event, + rundownPlaylistId, + timerIndex, + body.startPaused + ) + } + ) + + registerRoute<{ playlistId: string; timerIndex: string }, never, void>( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/pause', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, _) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = Number.parseInt(params.timerIndex) as RundownTTimerIndex + logger.info(`API POST: t-timer pause ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + return await serverAPI.tTimerPause(connection, event, rundownPlaylistId, timerIndex) + } + ) + + registerRoute<{ playlistId: string; timerIndex: string }, never, void>( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/resume', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, _) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = Number.parseInt(params.timerIndex) as RundownTTimerIndex + logger.info(`API POST: t-timer resume ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + return await serverAPI.tTimerResume(connection, event, rundownPlaylistId, timerIndex) + } + ) + + registerRoute<{ playlistId: string; timerIndex: string }, never, void>( + 'post', + '/playlists/:playlistId/t-timers/:timerIndex/restart', + new Map([[404, [UserErrorMessage.RundownPlaylistNotFound]]]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, _) => { + const rundownPlaylistId = protectString(params.playlistId) + const timerIndex = Number.parseInt(params.timerIndex) as RundownTTimerIndex + logger.info(`API POST: t-timer restart ${rundownPlaylistId} ${timerIndex}`) + + check(rundownPlaylistId, String) + check(timerIndex, Number) + return await serverAPI.tTimerRestart(connection, event, rundownPlaylistId, timerIndex) + } + ) } diff --git a/meteor/server/lib/rest/v1/playlists.ts b/meteor/server/lib/rest/v1/playlists.ts index 74a60a29762..83069bea4d6 100644 --- a/meteor/server/lib/rest/v1/playlists.ts +++ b/meteor/server/lib/rest/v1/playlists.ts @@ -11,6 +11,7 @@ import { SegmentId, } from '@sofie-automation/corelib/dist/dataModel/Ids' import { QueueNextSegmentResult } from '@sofie-automation/corelib/dist/worker/studio' +import { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Meteor } from 'meteor/meteor' /* ************************************************************************* @@ -261,4 +262,78 @@ export interface PlaylistsRestAPI { rundownPlaylistId: RundownPlaylistId, sourceLayerId: string ): Promise> + /** + * Configure a T-timer as a countdown. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + * @param duration Duration in seconds. + * @param stopAtZero Whether to stop at zero. + * @param startPaused Whether to start paused. + */ + tTimerStartCountdown( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + duration: number, + stopAtZero?: boolean, + startPaused?: boolean + ): Promise> + + /** + * Configure a T-timer as a free-running timer. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + * @param startPaused Whether to start paused. + */ + tTimerStartFreeRun( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex, + startPaused?: boolean + ): Promise> + /** + * Pause a T-timer. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + */ + tTimerPause( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> + /** + * Resume a T-timer. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + */ + tTimerResume( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> + /** + * Restart a T-timer. + * @param connection Connection data including client and header details + * @param event User event string + * @param rundownPlaylistId Target Playlist. + * @param timerIndex Index of the timer (1-3). + */ + tTimerRestart( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + timerIndex: RundownTTimerIndex + ): Promise> } diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 6eb045fc5e0..367de698913 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -19,7 +19,7 @@ import { JSONBlob } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import { CoreRundownPlaylistSnapshot } from '../snapshots.js' import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { ITranslatableMessage } from '../TranslatableMessage.js' -import { QuickLoopMarker } from '../dataModel/RundownPlaylist.js' +import { QuickLoopMarker, RundownTTimerIndex } from '../dataModel/RundownPlaylist.js' /** List of all Jobs performed by the Worker related to a certain Studio */ export enum StudioJobs { @@ -211,6 +211,28 @@ export enum StudioJobs { * During playout it is hard to track removal of PieceInstances (particularly when resetting PieceInstances) */ CleanupOrphanedExpectedPackageReferences = 'cleanupOrphanedExpectedPackageReferences', + + /** + * Configure a T-timer as a countdown + */ + TTimerStartCountdown = 'tTimerStartCountdown', + + /** + * Configure a T-timer as a free-running timer + */ + TTimerStartFreeRun = 'tTimerStartFreeRun', + /** + * Pause a T-timer + */ + TTimerPause = 'tTimerPause', + /** + * Resume a T-timer + */ + TTimerResume = 'tTimerResume', + /** + * Restart a T-timer + */ + TTimerRestart = 'tTimerRestart', } export interface RundownPlayoutPropsBase { @@ -374,6 +396,21 @@ export interface SwitchRouteSetProps { routeSetId: string state: boolean | 'toggle' } +export interface TTimerPropsBase extends RundownPlayoutPropsBase { + timerIndex: RundownTTimerIndex +} +export interface TTimerStartCountdownProps extends TTimerPropsBase { + duration: number + stopAtZero: boolean + startPaused: boolean +} + +export interface TTimerStartFreeRunProps extends TTimerPropsBase { + startPaused: boolean +} +export type TTimerPauseProps = TTimerPropsBase +export type TTimerResumeProps = TTimerPropsBase +export type TTimerRestartProps = TTimerPropsBase export interface CleanupOrphanedExpectedPackageReferencesProps { playlistId: RundownPlaylistId @@ -438,6 +475,12 @@ export type StudioJobFunc = { [StudioJobs.SwitchRouteSet]: (data: SwitchRouteSetProps) => void [StudioJobs.CleanupOrphanedExpectedPackageReferences]: (data: CleanupOrphanedExpectedPackageReferencesProps) => void + [StudioJobs.TTimerStartCountdown]: (data: TTimerStartCountdownProps) => void + + [StudioJobs.TTimerStartFreeRun]: (data: TTimerStartFreeRunProps) => void + [StudioJobs.TTimerPause]: (data: TTimerPauseProps) => void + [StudioJobs.TTimerResume]: (data: TTimerResumeProps) => void + [StudioJobs.TTimerRestart]: (data: TTimerRestartProps) => void } export function getStudioQueueName(id: StudioId): string { diff --git a/packages/job-worker/src/playout/tTimersJobs.ts b/packages/job-worker/src/playout/tTimersJobs.ts new file mode 100644 index 00000000000..d1a0fe35e9e --- /dev/null +++ b/packages/job-worker/src/playout/tTimersJobs.ts @@ -0,0 +1,91 @@ +import { + TTimerPauseProps, + TTimerRestartProps, + TTimerResumeProps, + TTimerStartCountdownProps, + TTimerStartFreeRunProps, +} from '@sofie-automation/corelib/dist/worker/studio' +import { JobContext } from '../jobs/index.js' +import { runJobWithPlayoutModel } from './lock.js' +import { + createCountdownTTimer, + createFreeRunTTimer, + pauseTTimer, + restartTTimer, + resumeTTimer, + validateTTimerIndex, +} from './tTimers.js' + +export async function handleTTimerStartCountdown(_context: JobContext, data: TTimerStartCountdownProps): Promise { + return runJobWithPlayoutModel(_context, data, null, async (playoutModel) => { + validateTTimerIndex(data.timerIndex) + + const currentTimer = playoutModel.playlist.tTimers[data.timerIndex - 1] + playoutModel.updateTTimer({ + ...currentTimer, + ...createCountdownTTimer(data.duration * 1000, { + stopAtZero: data.stopAtZero, + startPaused: data.startPaused, + }), + }) + }) +} + +export async function handleTTimerStartFreeRun(_context: JobContext, data: TTimerStartFreeRunProps): Promise { + return runJobWithPlayoutModel(_context, data, null, async (playoutModel) => { + validateTTimerIndex(data.timerIndex) + + const currentTimer = playoutModel.playlist.tTimers[data.timerIndex - 1] + playoutModel.updateTTimer({ + ...currentTimer, + ...createFreeRunTTimer({ + startPaused: data.startPaused, + }), + }) + }) +} + +export async function handleTTimerPause(_context: JobContext, data: TTimerPauseProps): Promise { + return runJobWithPlayoutModel(_context, data, null, async (playoutModel) => { + validateTTimerIndex(data.timerIndex) + + const timerIndex = data.timerIndex - 1 + const currentTimer = playoutModel.playlist.tTimers[timerIndex] + if (!currentTimer.mode) return + + const updatedTimer = pauseTTimer(currentTimer) + if (updatedTimer) { + playoutModel.updateTTimer(updatedTimer) + } + }) +} + +export async function handleTTimerResume(_context: JobContext, data: TTimerResumeProps): Promise { + return runJobWithPlayoutModel(_context, data, null, async (playoutModel) => { + validateTTimerIndex(data.timerIndex) + + const timerIndex = data.timerIndex - 1 + const currentTimer = playoutModel.playlist.tTimers[timerIndex] + if (!currentTimer.mode) return + + const updatedTimer = resumeTTimer(currentTimer) + if (updatedTimer) { + playoutModel.updateTTimer(updatedTimer) + } + }) +} + +export async function handleTTimerRestart(_context: JobContext, data: TTimerRestartProps): Promise { + return runJobWithPlayoutModel(_context, data, null, async (playoutModel) => { + validateTTimerIndex(data.timerIndex) + + const timerIndex = data.timerIndex - 1 + const currentTimer = playoutModel.playlist.tTimers[timerIndex] + if (!currentTimer.mode) return + + const updatedTimer = restartTTimer(currentTimer) + if (updatedTimer) { + playoutModel.updateTTimer(updatedTimer) + } + }) +} diff --git a/packages/job-worker/src/workers/studio/jobs.ts b/packages/job-worker/src/workers/studio/jobs.ts index be5d81787da..5f4b204ca5f 100644 --- a/packages/job-worker/src/workers/studio/jobs.ts +++ b/packages/job-worker/src/workers/studio/jobs.ts @@ -49,6 +49,13 @@ import { handleActivateAdlibTesting } from '../../playout/adlibTesting.js' import { handleExecuteBucketAdLibOrAction } from '../../playout/bucketAdlibJobs.js' import { handleSwitchRouteSet } from '../../studio/routeSet.js' import { handleCleanupOrphanedExpectedPackageReferences } from '../../playout/expectedPackages.js' +import { + handleTTimerPause, + handleTTimerRestart, + handleTTimerResume, + handleTTimerStartCountdown, + handleTTimerStartFreeRun, +} from '../../playout/tTimersJobs.js' type ExecutableFunction = ( context: JobContext, @@ -113,4 +120,11 @@ export const studioJobHandlers: StudioJobHandlers = { [StudioJobs.SwitchRouteSet]: handleSwitchRouteSet, [StudioJobs.CleanupOrphanedExpectedPackageReferences]: handleCleanupOrphanedExpectedPackageReferences, + + [StudioJobs.TTimerStartCountdown]: handleTTimerStartCountdown, + + [StudioJobs.TTimerStartFreeRun]: handleTTimerStartFreeRun, + [StudioJobs.TTimerPause]: handleTTimerPause, + [StudioJobs.TTimerResume]: handleTTimerResume, + [StudioJobs.TTimerRestart]: handleTTimerRestart, } diff --git a/packages/openapi/api/actions.yaml b/packages/openapi/api/actions.yaml index 70f1788148c..c751efd1b29 100644 --- a/packages/openapi/api/actions.yaml +++ b/packages/openapi/api/actions.yaml @@ -71,6 +71,17 @@ paths: $ref: 'definitions/playlists.yaml#/resources/sourceLayer' /playlists/{playlistId}/sourceLayer/{sourceLayerId}/sticky: $ref: 'definitions/playlists.yaml#/resources/sourceLayer/sticky' + /playlists/{playlistId}/t-timers/{timerIndex}/countdown: + $ref: 'definitions/playlists.yaml#/resources/tTimerCountdown' + + /playlists/{playlistId}/t-timers/{timerIndex}/free-run: + $ref: 'definitions/playlists.yaml#/resources/tTimerFreeRun' + /playlists/{playlistId}/t-timers/{timerIndex}/pause: + $ref: 'definitions/playlists.yaml#/resources/tTimerPause' + /playlists/{playlistId}/t-timers/{timerIndex}/resume: + $ref: 'definitions/playlists.yaml#/resources/tTimerResume' + /playlists/{playlistId}/t-timers/{timerIndex}/restart: + $ref: 'definitions/playlists.yaml#/resources/tTimerRestart' # studio operations /studios: $ref: 'definitions/studios.yaml#/resources/studios' diff --git a/packages/openapi/api/definitions/playlists.yaml b/packages/openapi/api/definitions/playlists.yaml index 943778f6418..40654164766 100644 --- a/packages/openapi/api/definitions/playlists.yaml +++ b/packages/openapi/api/definitions/playlists.yaml @@ -714,6 +714,169 @@ resources: example: Rundown must be active! 500: $ref: '#/components/responses/internalServerError' + tTimerCountdown: + post: + operationId: tTimerCountdown + tags: + - playlists + summary: Start a countdown timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + enum: [1, 2, 3] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + duration: + type: number + description: Duration in seconds. + stopAtZero: + type: boolean + description: Whether to stop the timer at zero. + startPaused: + type: boolean + description: Whether to start the timer in a paused state. + required: + - duration + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' + + tTimerFreeRun: + post: + operationId: tTimerFreeRun + tags: + - playlists + summary: Start a free-running timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + enum: [1, 2, 3] + requestBody: + content: + application/json: + schema: + type: object + properties: + startPaused: + type: boolean + description: Whether to start the timer in a paused state. + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' + tTimerPause: + post: + operationId: tTimerPause + tags: + - playlists + summary: Pause a timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + enum: [1, 2, 3] + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' + tTimerResume: + post: + operationId: tTimerResume + tags: + - playlists + summary: Resume a timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + enum: [1, 2, 3] + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' + tTimerRestart: + post: + operationId: tTimerRestart + tags: + - playlists + summary: Restart a timer. + parameters: + - name: playlistId + in: path + description: Target playlist. + required: true + schema: + type: string + - name: timerIndex + in: path + description: Index of the timer (1, 2, or 3). + required: true + schema: + type: integer + enum: [1, 2, 3] + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' components: schemas: