openapi: 3.0.3
info:
  title: PLAYDECK Control API
  description: |
    HTTP/REST control and query API for PLAYDECK (v1).

    This contract describes the customer-facing API. Implementation may follow later;
    payloads for status and project mirror the existing WebSocket `status|` / `project|`
    JSON (see HTML-Templates and Commands-List / Events-List documentation).

    ## Design notes

    - **Read once** (project, playlist, status snapshot): use these HTTP endpoints.
    - **Live updates** (status ~250 ms, playout events): continue to use WebSocket on
      the same host/port with the existing pipe protocol (`status|`, `event|`, …).
    - **Legacy pipe commands** (`<play|1|…>`) remain supported via TCP Commands In,
      WebSocket, and the escape-hatch endpoint `POST /api/v1/commands`.
    - Channel, block, clip, stream, overlay, and recording indices are **1-based**,
      matching the existing command language.
    - Within `v1`, endpoints may be **added**. Breaking changes require `v2`.

  version: 1.0.0
  contact:
    name: Joy Event & Media / PLAYDECK
    url: https://playdeck.tv/playdeck-api/
  license:
    name: Proprietary

servers:
  - url: /
    description: This PLAYDECK instance (same host/port as this page)
  - url: http://127.0.0.1:11411
    description: Local PLAYDECK (only when calling from elsewhere)
  - url: http://{host}:{port}
    description: Remote PLAYDECK on LAN
    variables:
      host:
        default: 192.168.1.10
      port:
        default: '11411'

tags:
  - name: Meta
    description: API discovery and health
  - name: Status
    description: Live playout snapshot (same data as WebSocket status push)
  - name: Project
    description: Full project / playlist tree (same data as WebSocket project pull)
  - name: Channels
    description: Channel-scoped playout control
  - name: Assets
    description: Overlays, actions, desktop, streams, recording
  - name: Commands
    description: Escape hatch for the full legacy command language

paths:
  /api/v1/docs:
    get:
      tags: [Meta]
      summary: API Reference (interactive docs)
      operationId: getApiReference
      responses:
        '200':
          description: Interactive OpenAPI documentation (HTML)
          content:
            text/html:
              schema:
                type: string

  /api/v1/openapi.yaml:
    get:
      tags: [Meta]
      summary: OpenAPI document (YAML)
      operationId: getOpenApiYaml
      responses:
        '200':
          description: OpenAPI 3 document (YAML)
          content:
            application/yaml:
              schema:
                type: string

  /api/v1/openapi.json:
    get:
      tags: [Meta]
      summary: OpenAPI document (JSON)
      operationId: getOpenApiJson
      responses:
        '200':
          description: OpenAPI 3 document
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /api/v1/health:
    get:
      tags: [Meta]
      summary: Health check
      operationId: getHealth
      responses:
        '200':
          description: Service is reachable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'

  /api/v1/status:
    get:
      tags: [Status]
      summary: Current status snapshot
      description: |
        Returns the same JSON object currently broadcast as WebSocket `status|<json>`
        (typically every 250 ms to connected clients). Prefer WebSocket for continuous
        monitoring; use this endpoint for one-shot reads and integrations without WS.
      operationId: getStatus
      responses:
        '200':
          description: Status snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusRoot'

  /api/v1/project:
    get:
      tags: [Project]
      summary: Full project / playlist data
      description: |
        Returns the same JSON object currently sent as WebSocket `project|<json>`
        after a `<projectdata>` request. Suitable for building UIs, dumping playlists,
        and one-shot automation — not for high-frequency polling of playhead position
        (use `/api/v1/status` or WebSocket status for that).
      operationId: getProject
      responses:
        '200':
          description: Project tree
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectRoot'

  /api/v1/channels:
    get:
      tags: [Channels]
      summary: List channels (from status)
      description: Convenience view of `StatusRoot.Channel` with 1-based `id`.
      operationId: listChannels
      responses:
        '200':
          description: Channel list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelListResponse'

  /api/v1/channels/{channelId}:
    get:
      tags: [Channels]
      summary: Single channel status
      operationId: getChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          description: Channel status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelStatus'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/cue:
    post:
      tags: [Channels]
      summary: Cue clip or block
      description: Maps to `<cue|…>`, `<cueid|…>`, `<cueflex|…>`, or `<cuelist|…>` depending on body.
      operationId: cueChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlayoutTarget'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/play:
    post:
      tags: [Channels]
      summary: Play clip or block
      description: Maps to `<play|…>` / `<playid|…>` / `<playflex|…>` / `<playlist|…>`.
      operationId: playChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlayoutTarget'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/fadein:
    post:
      tags: [Channels]
      summary: Fade in clip or block
      operationId: fadeInChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlayoutTarget'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/pause:
    post:
      tags: [Channels]
      summary: Toggle pause (maps to `<pause|CHANNEL>`)
      operationId: pauseChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/hold:
    post:
      tags: [Channels]
      summary: Hold / force pause (`<hold|CHANNEL>`)
      operationId: holdChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/resume:
    post:
      tags: [Channels]
      summary: Resume from pause (`<resume|CHANNEL>`)
      operationId: resumeChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/stop:
    post:
      tags: [Channels]
      summary: Stop playout (`<stop|CHANNEL>`)
      operationId: stopChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/fadeout:
    post:
      tags: [Channels]
      summary: Fade out (`<fadeout|CHANNEL>`)
      operationId: fadeOutChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/playnext:
    post:
      tags: [Channels]
      summary: Play next clip (`<playnext|CHANNEL>`)
      operationId: playNextChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/cuenext:
    post:
      tags: [Channels]
      summary: Cue next clip (`<cuenext|CHANNEL>`)
      operationId: cueNextChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/playnextblock:
    post:
      tags: [Channels]
      summary: Play next block (`<playnextblock|CHANNEL>`)
      operationId: playNextBlockChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/cuenextblock:
    post:
      tags: [Channels]
      summary: Cue next block (`<cuenextblock|CHANNEL>`)
      operationId: cueNextBlockChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/position:
    post:
      tags: [Channels]
      summary: Seek playhead (`<position|CHANNEL|TIMESTAMP>`)
      operationId: positionChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PositionRequest'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/select:
    post:
      tags: [Channels]
      summary: Select block/clip (`<select|…>` / `<selectid|…>`)
      operationId: selectChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SelectTarget'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/mute:
    post:
      tags: [Channels]
      summary: Mute channel audio (`<mutechannel|CHANNEL>`)
      operationId: muteChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/unmute:
    post:
      tags: [Channels]
      summary: Unmute channel audio (`<unmutechannel|CHANNEL>`)
      operationId: unmuteChannel
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/desktop/start:
    post:
      tags: [Assets]
      summary: Start desktop output (`<startdesktop|CHANNEL>`)
      operationId: startDesktop
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/desktop/stop:
    post:
      tags: [Assets]
      summary: Stop desktop output (`<stopdesktop|CHANNEL>`)
      operationId: stopDesktop
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/overlays/{overlayId}/start:
    post:
      tags: [Assets]
      summary: Start overlay (`<startoverlay|CHANNEL|OVERLAY>`)
      operationId: startOverlay
      parameters:
        - $ref: '#/components/parameters/channelId'
        - $ref: '#/components/parameters/overlayId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/overlays/{overlayId}/stop:
    post:
      tags: [Assets]
      summary: Stop overlay (`<stopoverlay|CHANNEL|OVERLAY>`)
      operationId: stopOverlay
      parameters:
        - $ref: '#/components/parameters/channelId'
        - $ref: '#/components/parameters/overlayId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/overlays/{overlayId}/toggle:
    post:
      tags: [Assets]
      summary: Toggle overlay (`<toggleoverlay|CHANNEL|OVERLAY>`)
      operationId: toggleOverlay
      parameters:
        - $ref: '#/components/parameters/channelId'
        - $ref: '#/components/parameters/overlayId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/overlays/stopall:
    post:
      tags: [Assets]
      summary: Stop all overlays (`<stopalloverlays|CHANNEL>`)
      operationId: stopAllOverlays
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/actions/{actionId}/start:
    post:
      tags: [Assets]
      summary: Start action (`<startaction|CHANNEL|ACTION>`)
      operationId: startAction
      parameters:
        - $ref: '#/components/parameters/channelId'
        - $ref: '#/components/parameters/actionId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/actions/{actionId}/stop:
    post:
      tags: [Assets]
      summary: Stop action (`<stopaction|CHANNEL|ACTION>`)
      operationId: stopAction
      parameters:
        - $ref: '#/components/parameters/channelId'
        - $ref: '#/components/parameters/actionId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/channels/{channelId}/actions/stopall:
    post:
      tags: [Assets]
      summary: Stop all actions (`<stopallactions|CHANNEL>`)
      operationId: stopAllActions
      parameters:
        - $ref: '#/components/parameters/channelId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/streams/{streamId}/start:
    post:
      tags: [Assets]
      summary: Start stream (`<startstream|STREAM>`)
      operationId: startStream
      parameters:
        - $ref: '#/components/parameters/streamId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/streams/{streamId}/stop:
    post:
      tags: [Assets]
      summary: Stop stream (`<stopstream|STREAM>`)
      operationId: stopStream
      parameters:
        - $ref: '#/components/parameters/streamId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/recordings/{recordingId}/start:
    post:
      tags: [Assets]
      summary: Start recording (`<startrecord|RECORD>`)
      operationId: startRecording
      parameters:
        - $ref: '#/components/parameters/recordingId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/recordings/{recordingId}/stop:
    post:
      tags: [Assets]
      summary: Stop recording (`<stoprecord|RECORD>`)
      operationId: stopRecording
      parameters:
        - $ref: '#/components/parameters/recordingId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/inputs/{inputId}/mute:
    post:
      tags: [Assets]
      summary: Mute input (`<muteinput|INPUT>`)
      operationId: muteInput
      parameters:
        - $ref: '#/components/parameters/inputId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/inputs/{inputId}/unmute:
    post:
      tags: [Assets]
      summary: Unmute input (`<unmuteinput|INPUT>`)
      operationId: unmuteInput
      parameters:
        - $ref: '#/components/parameters/inputId'
      responses:
        '200':
          $ref: '#/components/responses/ActionOk'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/commands:
    post:
      tags: [Commands]
      summary: Execute legacy command(s)
      description: |
        Escape hatch for the full command language documented in Commands-List.txt.
        Prefer dedicated REST routes when available; use this for advanced patterns
        (`wait`, variables, chains, flex patterns, playlist edit, loadproject, …).

        Exactly one of `raw` or `name` must be provided.
      operationId: executeCommand
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommandRequest'
            examples:
              raw:
                summary: Raw pipe command
                value:
                  raw: '<cueandplay|1|1|1>'
              named:
                summary: Named command with args
                value:
                  name: play
                  args: ['1', '1', '1']
              chain:
                summary: Chained commands with wait
                value:
                  raw: '<cue|1|1><wait|1000><play|1>'
      responses:
        '200':
          description: Command accepted / executed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommandResponse'
        '400':
          $ref: '#/components/responses/BadRequest'

components:
  parameters:
    channelId:
      name: channelId
      in: path
      required: true
      description: Channel number (1–8 depending on license)
      schema:
        type: integer
        minimum: 1
        maximum: 8
        example: 1
    overlayId:
      name: overlayId
      in: path
      required: true
      description: Overlay button index (1–99). Groups and multi-select (`3+7+12`) use POST /commands.
      schema:
        type: integer
        minimum: 1
        maximum: 99
    actionId:
      name: actionId
      in: path
      required: true
      description: Action button index (1–99)
      schema:
        type: integer
        minimum: 1
        maximum: 99
    streamId:
      name: streamId
      in: path
      required: true
      description: Stream slot (1–15)
      schema:
        type: integer
        minimum: 1
        maximum: 15
    recordingId:
      name: recordingId
      in: path
      required: true
      description: Recording slot (1–4 depending on license)
      schema:
        type: integer
        minimum: 1
        maximum: 4
    inputId:
      name: inputId
      in: path
      required: true
      description: Input slot (1–12)
      schema:
        type: integer
        minimum: 1
        maximum: 12

  responses:
    ActionOk:
      description: Action accepted
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ActionResponse'
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Resource not found (e.g. channel out of license range)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  schemas:
    HealthResponse:
      type: object
      required: [ok, apiVersion]
      properties:
        ok:
          type: boolean
          example: true
        apiVersion:
          type: string
          example: '1.0.0'
        appVersion:
          type: string
          description: PLAYDECK application version when available
          example: '4.x'

    ErrorResponse:
      type: object
      required: [ok, error]
      properties:
        ok:
          type: boolean
          enum: [false]
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Machine-readable error code
              example: invalid_argument
            message:
              type: string
              example: Missing required field 'timestamp'
            details:
              type: object
              additionalProperties: true

    ActionResponse:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
          example: true
        command:
          type: string
          description: Legacy command that was executed (when applicable)
          example: '<play|1|1|1>'

    CommandRequest:
      type: object
      properties:
        raw:
          type: string
          description: |
            One or more pipe commands including angle brackets, as in Commands-List.txt.
            Example: `<play|1>` or `<cue|1|1><wait|500><play|1>`.
          example: '<play|1|1|1>'
        name:
          type: string
          description: Command name without brackets/pipes (e.g. `play`, `startstream`)
          example: play
        args:
          type: array
          description: Positional arguments for `name` (strings; numbers as strings are fine)
          items:
            type: string
          example: ['1', '1', '1']
      description: Provide either `raw` or `name` (+ optional `args`).

    CommandResponse:
      allOf:
        - $ref: '#/components/schemas/ActionResponse'
        - type: object
          properties:
            executed:
              type: array
              items:
                type: string
              description: Individual commands that were parsed/executed from a chain

    PlayoutTarget:
      type: object
      description: |
        Target for cue/play/fadein. Omit fields to use current selection / cued clip
        (same rules as Commands-List.txt). Prefer one addressing mode:
        block+clip, uid, pattern, or list+block+clip.
      properties:
        block:
          type: integer
          minimum: 1
          description: Block number as shown in playlist
        clip:
          type: integer
          minimum: 1
          description: Clip number within block
        uid:
          type: integer
          description: Unique ID (block or clip); maps to `*id` commands
        pattern:
          type: string
          description: Flex pattern (e.g. `c:intro`, `b#5 c:name p:0:05:00`); maps to `*flex`
          example: 'c:intro'
        list:
          type: integer
          minimum: 1
          maximum: 2
          description: Playlist side (1=left, 2=right); maps to `*list` commands

    SelectTarget:
      type: object
      properties:
        block:
          type: integer
          minimum: 1
        clip:
          type: integer
          minimum: 1
          description: Omit to select the block
        uid:
          type: integer
          description: Maps to `<selectid|UID>`

    PositionRequest:
      type: object
      required: [timestamp]
      properties:
        timestamp:
          type: string
          description: |
            Playhead position. Formats: `HH:MM:SS:FF`, shortened `SS:FF` / `SS`.
            Negative values mean duration minus value (see Commands-List).
          example: '0:05:00:00'
        marker:
          type: string
          description: If set, maps to `<positionmarker|CHANNEL|MARKER>` instead of timestamp seek
          example: mymarker

    ChannelListResponse:
      type: object
      required: [channels]
      properties:
        channels:
          type: array
          items:
            allOf:
              - type: object
                required: [id]
                properties:
                  id:
                    type: integer
                    minimum: 1
                    description: 1-based channel number
              - $ref: '#/components/schemas/ChannelStatus'

    # --- Status (mirrors GetStatusJS / WebSocket status|) ---

    StatusRoot:
      type: object
      description: Root object of WebSocket status JSON
      properties:
        Project:
          $ref: '#/components/schemas/StatusProjectMeta'
        Channel:
          type: array
          description: One entry per licensed channel (array index 0 = channel 1)
          items:
            $ref: '#/components/schemas/ChannelStatus'
        DirectorView:
          type: array
          items:
            $ref: '#/components/schemas/DirectorViewConfig'
        ObjectState:
          $ref: '#/components/schemas/ObjectState'
      additionalProperties: true

    StatusProjectMeta:
      type: object
      properties:
        ProjectName:
          type: string
        ProjectFilename:
          type: string
        ClockTime:
          type: string
        Timestamp:
          type: string
          example: '2026-08-01 12:00:00'
        TimestampUnix:
          type: integer
          format: int64
        LicenseEdition:
          type: string
          enum: [Lite, Plus, Studio]
        LicenseCustomer:
          type: string
        LicenseLease:
          type: string
      additionalProperties: true

    ChannelStatus:
      type: object
      description: |
        Per-channel live status. Dynamic keys `OverlayActiveN` / `ActionActiveN`
        (N = 1..99) appear when that overlay/action button is active.
      properties:
        ChannelState:
          type: integer
          description: '0=not started, 1=starting, 2=ready, 3=error'
          enum: [0, 1, 2, 3]
        ChannelName:
          type: string
        BlockCount:
          type: integer
        TallyStatus:
          type: integer
        PreviewNote:
          type: string
        StageWidth:
          type: integer
        StageHeight:
          type: integer
        SelectedID:
          type: integer
          description: Unique ID of current selection
        PlayState:
          type: integer
          description: '0=stopped, 1=cueing, 2=playing, 3=paused, 4=in-between'
          enum: [0, 1, 2, 3, 4]
        ProgressVisible:
          type: boolean
        ScheduleVisible:
          type: boolean
        AutoplayVisible:
          type: boolean
        BlockName:
          type: string
        BlockNumber:
          type: integer
        BlockID:
          type: integer
        BlockPosition:
          type: number
        BlockPositionString:
          type: string
        BlockDuration:
          type: number
        BlockDurationString:
          type: string
        BlockProgress:
          type: number
        BlockRemainCaption:
          type: string
        BlockRemain:
          type: number
        BlockRemainString:
          type: string
        BlockEndCaption:
          type: string
        BlockEnd:
          type: integer
          format: int64
        BlockEndString:
          type: string
        BlockProgressAlert:
          type: boolean
        ClipName:
          type: string
        ClipNumber:
          type: integer
        ClipID:
          type: integer
        ClipPosition:
          type: number
        ClipPositionString:
          type: string
        ClipDuration:
          type: number
        ClipDurationString:
          type: string
        ClipProgress:
          type: number
        ClipProgressAlert:
          type: boolean
        ClipRemainCaption:
          type: string
        ClipRemain:
          type: number
        ClipRemainString:
          type: string
        ClipEnd:
          type: integer
          format: int64
        ClipEndString:
          type: string
        ScheduleBlockName:
          type: string
        ScheduleTime:
          type: integer
          format: int64
        ScheduleTimeString:
          type: string
        ScheduleRemain:
          type: number
        ScheduleRemainString:
          type: string
        ScheduleIcons:
          type: string
        ScheduleAlert:
          type: boolean
        AutoplayBlockName:
          type: string
        AutoplayTime:
          type: integer
          format: int64
        AutoplayTimeString:
          type: string
        AutoplayRemain:
          type: number
        AutoplayRemainString:
          type: string
        AutoplayAlert:
          type: boolean
      additionalProperties: true

    DirectorViewConfig:
      type: object
      properties:
        DirectorType:
          type: integer
        ChannelIDs:
          type: string
        ShowTimer:
          type: boolean
        ShowName:
          type: boolean
        ShowTally:
          type: boolean
        ShowNotes:
          type: boolean
      additionalProperties: true

    ObjectState:
      type: object
      description: |
        Flat map of subsystem states. Keys include Channel1..8, Output1..8,
        Input1..12, Stream1..15, Director1..4, Recording1..4. Values are engine
        state ordinals (0 = none/off when unused).
      additionalProperties:
        type: integer

    # --- Project (mirrors GetProjectJS / WebSocket project|) ---

    ProjectRoot:
      type: object
      properties:
        Project:
          $ref: '#/components/schemas/ProjectMeta'
        Channel:
          type: array
          description: One entry per licensed channel (array index 0 = channel 1)
          items:
            $ref: '#/components/schemas/ProjectChannel'
      additionalProperties: true

    ProjectMeta:
      type: object
      properties:
        ProjectName:
          type: string
        ProjectFilename:
          type: string
        ClockTime:
          type: string
        Timestamp:
          type: string
        TimestampUnix:
          type: integer
          format: int64
      additionalProperties: true

    ProjectChannel:
      type: object
      properties:
        ChannelState:
          type: integer
        ChannelName:
          type: string
        BlockCount:
          type: integer
        StageWidth:
          type: integer
        StageHeight:
          type: integer
        Block:
          type: array
          items:
            $ref: '#/components/schemas/ProjectBlock'
        Overlays:
          type: array
          description: Up to 99 overlay button slots
          items:
            $ref: '#/components/schemas/ProjectOverlay'
        Actions:
          type: array
          description: Up to 99 action button slots
          items:
            $ref: '#/components/schemas/ProjectAction'
      additionalProperties: true

    ProjectBlock:
      type: object
      properties:
        CanPlay:
          type: boolean
        Name:
          type: string
        Number:
          type: integer
        ID:
          type: integer
          description: Unique ID (UID)
        ClipCount:
          type: integer
        Active:
          type: boolean
        StopType:
          type: string
        DurationString:
          type: string
        StopFollowTime:
          type: number
        LoopClips:
          type: integer
        BreakOption:
          type: integer
        JumpUniqueID:
          type: integer
        ShuffleType:
          type: string
        ShuffleClips:
          type: integer
        ScheduledUnix:
          type: integer
          format: int64
        ScheduledWithDay:
          type: boolean
        ScheduledWithRepeat:
          type: boolean
        ScheduledRepeatDuration:
          type: number
        ScheduledMethod:
          type: integer
        ScheduledWeekdays:
          type: string
        Clip:
          type: array
          items:
            $ref: '#/components/schemas/ProjectClip'
      additionalProperties: true

    ProjectClip:
      type: object
      properties:
        CanPlay:
          type: boolean
        Name:
          type: string
        Number:
          type: integer
        ID:
          type: integer
          description: Unique ID (UID)
        ItemType:
          type: integer
        Active:
          type: boolean
        FileName:
          type: string
        ItemInput:
          type: integer
        DurationString:
          type: string
        FileSize:
          type: integer
          format: int64
        FileSizeString:
          type: string
        FileDateUnix:
          type: integer
          format: int64
        FileDateString:
          type: string
        FileType:
          type: integer
        FileTypeString:
          type: string
        TransitionType:
          type: string
        TransitionTime:
          type: number
        DurationGfx:
          type: number
        DurationGfxType:
          type: integer
        DurationGfxClockUnix:
          type: integer
          format: int64
        PauseMode:
          type: integer
        PauseFollow:
          type: integer
        PauseFollowTime:
          type: number
        MuteAudio:
          type: boolean
        Gain:
          type: number
        GainCustom:
          type: boolean
        EBUGain:
          type: number
        EBUScanned:
          type: boolean
        ScaleType:
          type: string
        ScaleAspect:
          type: string
        CutIn:
          type: number
        CutOut:
          type: number
        CutOutRelative:
          type: boolean
        CropLeft:
          type: number
        CropTop:
          type: number
        CropRight:
          type: number
        CropBottom:
          type: number
        LoopType:
          type: integer
        LoopSeconds:
          type: number
        LoopCount:
          type: integer
        LoopClock:
          type: integer
          format: int64
        LoopPartial:
          type: boolean
        VideoTrack:
          type: integer
        AudioTrack:
          type: integer
        SubtitleTrack:
          type: integer
        SubtitleOffset:
          type: number
      additionalProperties: true

    ProjectOverlay:
      type: object
      properties:
        ButtonName:
          type: string
        HasContent:
          type: boolean
        IsGroup:
          type: boolean
        OverlayType:
          type: integer
      additionalProperties: true

    ProjectAction:
      type: object
      properties:
        ButtonName:
          type: string
        HasContent:
          type: boolean
      additionalProperties: true
