openapi: 3.1.0
info:
  title: GlueArrow Box OS — Local API
  version: "1.6.0"
  summary: REST API exposed by the on-premises GlueArrow Box OS engine.
  description: |
    The GlueArrow Box OS engine exposes a local REST API (and a Socket.IO
    real-time channel) on TCP port 5002 of every box. This API is the
    integration surface for station automation, third-party DJ software,
    custom dashboards, monitoring systems, and partner integrations.

    Spec scope: stable, integrator-facing endpoints in Box OS 1.6.0. Internal
    development and admin-UI-only routes are intentionally omitted from this
    v1 publication and will appear in v1.1 if and when they stabilise.

    For the cloud API surface (`cloud.gluearrow.com`), see the separate
    GlueArrow Cloud API specification.

    Real-time events delivered over Socket.IO are documented in the API
    Reference page at <https://docs.gluearrow.com/api-reference#real-time-events>.
  contact:
    name: GlueArrow Engineering
    email: enterprise@gluearrow.com
    url: https://docs.gluearrow.com
  license:
    name: Proprietary — see EULA
    url: https://gluearrow.com/legal/eula
  termsOfService: https://gluearrow.com/legal/eula
  x-api-id: gluearrow-box-os-local
  x-audience: external-public

externalDocs:
  description: API Reference (narrative documentation)
  url: https://docs.gluearrow.com/api-reference

servers:
  - url: http://{box_address}:5002
    description: On-premises Box OS engine (LAN-local)
    variables:
      box_address:
        default: localhost
        description: IP or hostname of the GlueArrow Box on the station LAN.

tags:
  - name: Health
    description: Service health and observability
    externalDocs:
      url: https://docs.gluearrow.com/api-reference#6-endpoint-categories
  - name: State
    description: Current broadcast state, devices, and connectivity
  - name: Schedule
    description: Show schedules and music clip planning
  - name: Streaming
    description: RTMP destinations and multi-platform restream
  - name: Music
    description: Local music library and catalog
  - name: SmartVault
    description: Music library scanning and fingerprint intelligence
  - name: Ads
    description: Advertising bids, delivery, and history
  - name: Artist Claims
    description: Royalty and rights claim workflow
  - name: Updates
    description: Software updates and AI content verification

security:
  - sessionCookie: []
  - bearerToken: []

paths:
  /health:
    get:
      tags: [Health]
      operationId: getHealth
      summary: Liveness probe
      description: |
        Returns HTTP 200 when the engine process is responsive. Used by load
        balancers, Cloud Run health checks, and station monitoring systems.
      security: []
      responses:
        '200':
          description: Engine is alive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              example:
                status: ok
                version: "1.6.0"
                uptime_seconds: 87234

  /metrics:
    get:
      tags: [Health]
      operationId: getMetrics
      summary: Prometheus metrics
      description: |
        Plain-text Prometheus exposition format. Includes counters for
        broadcast events, gauges for current state, and histograms for
        sync latency. Restrict access to your monitoring subnet at the
        firewall layer.
      security: []
      responses:
        '200':
          description: Prometheus exposition format
          content:
            text/plain:
              schema:
                type: string
              example: |
                # HELP gluearrow_engine_up Whether the engine is running (1) or not (0)
                # TYPE gluearrow_engine_up gauge
                gluearrow_engine_up 1
                # HELP gluearrow_streams_active Number of active RTMP outputs
                # TYPE gluearrow_streams_active gauge
                gluearrow_streams_active 2

  /api/devices:
    get:
      tags: [State]
      operationId: listDevices
      summary: List available capture devices
      description: |
        Returns audio and video capture devices available to the engine.
        Cross-platform: enumerates DirectShow on Windows, AVFoundation on
        macOS, and v4l2 + PulseAudio on Linux.
      responses:
        '200':
          description: Device inventory
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceInventory'
              example:
                audio_inputs:
                  - id: dshow:Stereo Mix (Realtek Audio)
                    name: Stereo Mix (Realtek Audio)
                    kind: audio_in
                    platform_native_id: "@device_cm_{33D9A762}"
                    is_default: true
                audio_outputs: []
                video_inputs:
                  - id: dshow:Logitech BRIO
                    name: Logitech BRIO
                    kind: video_in
                    platform_native_id: "@device_pnp_{6F0FCB3B}"
                    is_default: false
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/connectivity:
    get:
      tags: [State]
      operationId: getConnectivity
      summary: Cloud reachability and offline-queue status
      responses:
        '200':
          description: Connectivity snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectivityStatus'
              example:
                cloud_reachable: true
                last_sync_at: "2026-04-24T14:23:01.451Z"
                queued_submissions: 0
                playtunes_url: "https://cloud.gluearrow.com"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/schedule:
    get:
      tags: [Schedule]
      operationId: listSchedule
      summary: List today's broadcast schedule
      parameters:
        - name: date
          in: query
          schema:
            type: string
            format: date
          description: Optional ISO date (YYYY-MM-DD); defaults to today.
      responses:
        '200':
          description: Ordered list of scheduled shows for the requested day
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ScheduledShow'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/shows/{show_id}/music-clips:
    parameters:
      - $ref: '#/components/parameters/ShowId'
    get:
      tags: [Schedule]
      operationId: listShowMusicClips
      summary: Get music clips for a show
      responses:
        '200':
          description: Ordered clip rundown for the show
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/MusicClip'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [Schedule]
      operationId: createShowMusicClip
      summary: Add a music clip to a show rundown
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MusicClipCreate'
      responses:
        '201':
          description: Clip added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MusicClip'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/music-clips/{clip_id}:
    parameters:
      - name: clip_id
        in: path
        required: true
        schema:
          type: integer
          format: int64
    patch:
      tags: [Schedule]
      operationId: updateMusicClip
      summary: Update a music clip
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MusicClipUpdate'
      responses:
        '200':
          description: Clip updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MusicClip'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      tags: [Schedule]
      operationId: deleteMusicClip
      summary: Remove a music clip
      responses:
        '204':
          description: Clip removed
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/destinations:
    get:
      tags: [Streaming]
      operationId: listStreamDestinations
      summary: List configured RTMP destinations
      responses:
        '200':
          description: All destinations (active and inactive)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/StreamDestination'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [Streaming]
      operationId: createStreamDestination
      summary: Add a new RTMP destination
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StreamDestinationCreate'
      responses:
        '201':
          description: Destination created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StreamDestination'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/destinations/{dest_id}:
    parameters:
      - name: dest_id
        in: path
        required: true
        schema:
          type: integer
          format: int64
    patch:
      tags: [Streaming]
      operationId: updateStreamDestination
      summary: Update a destination (toggle, rename, change RTMP URL)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StreamDestinationUpdate'
      responses:
        '200':
          description: Destination updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StreamDestination'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      tags: [Streaming]
      operationId: deleteStreamDestination
      summary: Remove a destination
      responses:
        '204':
          description: Destination removed
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/stream/config:
    post:
      tags: [Streaming]
      operationId: updateStreamConfig
      summary: Update overall RTMP stream configuration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StreamConfig'
      responses:
        '200':
          description: Configuration applied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StreamConfig'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/restream/channels:
    get:
      tags: [Streaming]
      operationId: listRestreamChannels
      summary: List Restream.io channels (if Restream integration is configured)
      responses:
        '200':
          description: Channel list
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  additionalProperties: true
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/restream/connect:
    post:
      tags: [Streaming]
      operationId: connectRestream
      summary: Connect a Restream.io account via OAuth
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                  description: Restream OAuth authorization code
      responses:
        '200':
          description: Connection successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  connected:
                    type: boolean
                  account:
                    type: string
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/music/library:
    get:
      tags: [Music]
      operationId: listMusicLibrary
      summary: Paginated local music library
      description: |
        Returns the SmartVault-catalogued music library with full metadata.
        Used by the PlaytunesDJ workspace, third-party DJ software, and
        partner integrations.
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: q
          in: query
          schema:
            type: string
          description: Optional search filter applied to title, artist, album.
      responses:
        '200':
          description: Page of catalog entries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MusicLibraryPage'
              example:
                tracks:
                  - id: 18432
                    title: "Sunday Morning"
                    artist: "K. Achampong"
                    album: "Highlife Sessions Vol. 2"
                    genre: "Highlife"
                    isrc: "GHASA2604001"
                    duration_seconds: 247
                total: 18432
                offset: 0
                limit: 100
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/music/identified:
    get:
      tags: [Music]
      operationId: listIdentifiedTracks
      summary: List identified tracks discovered from station drives
      responses:
        '200':
          description: Identified tracks (paginated)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MusicLibraryPage'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/music/catalog:
    get:
      tags: [Music]
      operationId: listMusicCatalog
      summary: Compact track catalog for selectors and pickers
      responses:
        '200':
          description: Lightweight catalog list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Track'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/music/drives:
    get:
      tags: [Music]
      operationId: listMusicDrives
      summary: List attached music storage drives
      responses:
        '200':
          description: Drives detected by the engine
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Drive'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/songs/stream:
    get:
      tags: [Music]
      operationId: streamDiscoveredSongs
      summary: Server-Sent Events stream of newly discovered audio files
      description: |
        Long-lived `text/event-stream` connection. Each event is a JSON
        payload describing a newly discovered audio file on the supplied
        drive. The stream completes when discovery on the drive completes.
      x-rate-limit-by: ip
      x-concurrent-connections: 4
      parameters:
        - name: drive
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: SSE stream
          content:
            text/event-stream:
              schema:
                type: string
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/search:
    get:
      tags: [Music]
      operationId: searchTracks
      summary: Full-text track search (FTS5, sub-200 ms)
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
            minLength: 1
      responses:
        '200':
          description: Matching tracks
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Track'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/search/cursor:
    get:
      tags: [Music]
      operationId: searchTracksCursor
      summary: Cursor-paginated full-text track search (for very large libraries)
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
        - name: after_id
          in: query
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Page of matching tracks plus next-cursor
          content:
            application/json:
              schema:
                type: object
                properties:
                  tracks:
                    type: array
                    items:
                      $ref: '#/components/schemas/Track'
                  next_after_id:
                    type: integer
                    format: int64
                    nullable: true
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/fingerprint/stats:
    get:
      tags: [Music]
      operationId: getFingerprintStats
      summary: Fingerprint database statistics
      responses:
        '200':
          description: Counts of locally known tracks and last-update times
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FingerprintStats'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/submit/status:
    get:
      tags: [Music]
      operationId: getSubmitStatus
      summary: Cloud submission queue status
      responses:
        '200':
          description: Pending / submitted / failed counts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitStatus'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/batch/scan/start:
    post:
      tags: [SmartVault]
      operationId: startBatchScan
      summary: Start a batch vault scan
      description: |
        Initiates a batch scan of the supplied drive. If a previous incomplete
        scan exists for the same drive (matched by volume serial), the engine
        resumes from the last checkpoint instead of starting over.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScanStart'
      responses:
        '202':
          description: Scan accepted; subscribe to /api/batch/scan/status for progress
          content:
            application/json:
              schema:
                type: object
                properties:
                  session_id:
                    type: string
                  resumed_from:
                    type: string
                    nullable: true
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/batch/scan/resume/{session_id}:
    parameters:
      - name: session_id
        in: path
        required: true
        schema:
          type: string
    post:
      tags: [SmartVault]
      operationId: resumeBatchScan
      summary: Resume a paused scan session
      responses:
        '200':
          description: Scan resumed
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/batch/scan/stop:
    post:
      tags: [SmartVault]
      operationId: stopBatchScan
      summary: Stop the active scan (marks as paused, preserves progress)
      responses:
        '200':
          description: Scan stopped
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/batch/scan/status:
    get:
      tags: [SmartVault]
      operationId: streamBatchScanStatus
      summary: SSE stream of live scan progress
      x-rate-limit-by: ip
      x-concurrent-connections: 4
      responses:
        '200':
          description: Server-Sent Events
          content:
            text/event-stream:
              schema:
                type: string
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/batch/scan/active:
    get:
      tags: [SmartVault]
      operationId: getBatchScanActive
      summary: Whether a scan is currently running
      responses:
        '200':
          description: Active state
          content:
            application/json:
              schema:
                type: object
                properties:
                  active:
                    type: boolean
                  session_id:
                    type: string
                    nullable: true
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/batch/sessions:
    get:
      tags: [SmartVault]
      operationId: listBatchScanSessions
      summary: List recent scan sessions
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Recent sessions, newest first
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ScanSession'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/batch/session/{session_id}/summary:
    parameters:
      - name: session_id
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [SmartVault]
      operationId: getBatchScanSessionSummary
      summary: Status counts for a scan session
      responses:
        '200':
          description: Per-status counts (identified, unidentified, error, duplicate, pending)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanSessionSummary'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/batch/session/{session_id}/files:
    parameters:
      - name: session_id
        in: path
        required: true
        schema:
          type: string
      - name: page
        in: query
        schema:
          type: integer
          minimum: 1
          default: 1
      - name: page_size
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 500
          default: 50
      - name: status
        in: query
        schema:
          type: string
          enum: [identified, unidentified, error, duplicate, pending]
    get:
      tags: [SmartVault]
      operationId: listBatchScanSessionFiles
      summary: Paginated file listing for a scan session
      responses:
        '200':
          description: Page of files
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanFilePage'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/batch/session/{session_id}/files/cursor:
    parameters:
      - name: session_id
        in: path
        required: true
        schema:
          type: string
      - name: after_id
        in: query
        schema:
          type: integer
          format: int64
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 1000
          default: 100
    get:
      tags: [SmartVault]
      operationId: listBatchScanSessionFilesCursor
      summary: Cursor-based file listing (for sessions exceeding 1M rows)
      responses:
        '200':
          description: Page of files plus next-cursor
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items:
                      $ref: '#/components/schemas/ScanFile'
                  next_after_id:
                    type: integer
                    format: int64
                    nullable: true
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/batch/session/{session_id}/export.csv:
    parameters:
      - name: session_id
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [SmartVault]
      operationId: exportBatchScanSessionCsv
      summary: Streaming CSV export of a scan session
      responses:
        '200':
          description: CSV file
          content:
            text/csv:
              schema:
                type: string
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/batch/file/{file_id}:
    parameters:
      - name: file_id
        in: path
        required: true
        schema:
          type: integer
          format: int64
    get:
      tags: [SmartVault]
      operationId: getBatchScanFile
      summary: Full details for a scanned file
      responses:
        '200':
          description: File detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanFile'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/batch/file/{file_id}/retry:
    parameters:
      - name: file_id
        in: path
        required: true
        schema:
          type: integer
          format: int64
    post:
      tags: [SmartVault]
      operationId: retryBatchScanFile
      summary: Reset an errored or unidentified file for re-processing
      responses:
        '200':
          description: File reset to pending
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanFile'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ads/bids:
    get:
      tags: [Ads]
      operationId: listAdBids
      summary: List inbound ad bids targeted at this station
      responses:
        '200':
          description: Ad bids (paginated)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AdBid'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [Ads]
      operationId: createAdBid
      summary: Create a local ad bid (for testing or direct insertion)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdBidCreate'
      responses:
        '201':
          description: Bid created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdBid'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ads/bids/{bid_id}:
    parameters:
      - name: bid_id
        in: path
        required: true
        schema:
          type: integer
          format: int64
    patch:
      tags: [Ads]
      operationId: updateAdBid
      summary: Approve, reject, or update an ad bid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdBidUpdate'
      responses:
        '200':
          description: Bid updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdBid'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ads/history:
    get:
      tags: [Ads]
      operationId: listAdHistory
      summary: Recent ad play history (proof-of-play)
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
        - name: since
          in: query
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Ordered ad play events
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AdPlayEvent'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/artist-claims:
    get:
      tags: [Artist Claims]
      operationId: listArtistClaims
      summary: List artist royalty claims at this station
      responses:
        '200':
          description: Claims
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ArtistClaim'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [Artist Claims]
      operationId: createArtistClaim
      summary: Submit a new artist claim
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ArtistClaimCreate'
      responses:
        '201':
          description: Claim recorded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ArtistClaim'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/artist-claims/{claim_id}:
    parameters:
      - name: claim_id
        in: path
        required: true
        schema:
          type: integer
          format: int64
    patch:
      tags: [Artist Claims]
      operationId: updateArtistClaim
      summary: Update the status or details of a claim
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum: [pending, verified, paid, rejected]
                notes:
                  type: string
      responses:
        '200':
          description: Claim updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ArtistClaim'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ai-verify:
    post:
      tags: [Updates]
      operationId: verifyContent
      summary: AI content safety check on uploaded media
      description: |
        Server-side keyword and content safety screen against the configured
        flagged/review lists. Returns one of three verdicts: pass, review,
        or flagged. Does not transmit raw audio off the box.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [path]
              properties:
                path:
                  type: string
                  description: Server-relative path to the media file to verify
      responses:
        '200':
          description: Verdict
          content:
            application/json:
              schema:
                type: object
                properties:
                  verdict:
                    type: string
                    enum: [pass, review, flagged]
                  matches:
                    type: array
                    items:
                      type: string
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /update:
    post:
      tags: [Updates]
      operationId: triggerUpdate
      summary: Trigger an immediate over-the-air update check
      responses:
        '200':
          description: Update check started
          content:
            application/json:
              schema:
                type: object
                properties:
                  current_version:
                    type: string
                  available_version:
                    type: string
                    nullable: true
                  status:
                    type: string
                    enum: [up_to_date, downloading, applied, failed]
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

components:
  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: ga_session
      description: Browser session issued by accounts.gluearrow.com SSO.
    bearerToken:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: |
        Machine credential bound to the box's license key. Issued by the
        cloud admin console; rotatable without downtime.

  parameters:
    ShowId:
      name: show_id
      in: path
      required: true
      schema:
        type: integer
        format: int64

  responses:
    BadRequest:
      description: Malformed request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Missing required field 'rtmp_url'
            code: invalid_request
            request_id: req_01J9X4P5K7VHQ2ZPKQDTQ4M0
    Unauthorized:
      description: Authentication required or session expired
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Authentication required
            code: unauthenticated
            request_id: req_01J9X4P5K7VHQ2ZPKQDTQ4M0
    Forbidden:
      description: Authenticated but lacks the required role
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Operator role cannot approve ad bids
            code: insufficient_role
            request_id: req_01J9X4P5K7VHQ2ZPKQDTQ4M0
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Show 12345 not found
            code: not_found
            request_id: req_01J9X4P5K7VHQ2ZPKQDTQ4M0
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Rate limit exceeded; retry after 30 seconds
            code: rate_limited
            request_id: req_01J9X4P5K7VHQ2ZPKQDTQ4M0

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: Machine-readable stable error code
        request_id:
          type: string
          description: ULID identifier for correlation with engine logs

    HealthResponse:
      type: object
      required: [status, version]
      properties:
        status:
          type: string
          enum: [ok]
        version:
          type: string
          example: "1.6.0"
        uptime_seconds:
          type: integer
          minimum: 0

    Device:
      type: object
      required: [id, name, kind]
      properties:
        id:
          type: string
        name:
          type: string
        kind:
          type: string
          enum: [audio_in, audio_out, video_in]
        platform_native_id:
          type: string
          description: Platform-specific identifier (DirectShow GUID on Windows, AVCaptureDevice ID on macOS, /dev/video* path on Linux)
        is_default:
          type: boolean

    DeviceInventory:
      type: object
      properties:
        audio_inputs:
          type: array
          items:
            $ref: '#/components/schemas/Device'
        audio_outputs:
          type: array
          items:
            $ref: '#/components/schemas/Device'
        video_inputs:
          type: array
          items:
            $ref: '#/components/schemas/Device'

    ConnectivityStatus:
      type: object
      properties:
        cloud_reachable:
          type: boolean
        last_sync_at:
          type: string
          format: date-time
          nullable: true
        queued_submissions:
          type: integer
          minimum: 0
        playtunes_url:
          type: string
          format: uri

    ScheduledShow:
      type: object
      properties:
        id:
          type: integer
          format: int64
        title:
          type: string
        host:
          type: string
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        recurring:
          type: boolean
        notes:
          type: string

    MusicClip:
      type: object
      properties:
        id:
          type: integer
          format: int64
        show_id:
          type: integer
          format: int64
        track_id:
          type: integer
          format: int64
        title:
          type: string
        artist:
          type: string
        position:
          type: integer
          minimum: 0
        duration_seconds:
          type: integer
          minimum: 0
        scheduled_at:
          type: string
          format: date-time
          nullable: true

    MusicClipCreate:
      type: object
      required: [track_id, position]
      properties:
        track_id:
          type: integer
          format: int64
        position:
          type: integer
          minimum: 0
        scheduled_at:
          type: string
          format: date-time
          nullable: true

    MusicClipUpdate:
      type: object
      properties:
        position:
          type: integer
          minimum: 0
        scheduled_at:
          type: string
          format: date-time
          nullable: true

    StreamDestination:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        platform:
          type: string
          example: youtube
        rtmp_url:
          type: string
        stream_key:
          type: string
          writeOnly: true
        enabled:
          type: boolean
        last_started_at:
          type: string
          format: date-time
          nullable: true

    StreamDestinationCreate:
      type: object
      required: [name, rtmp_url, stream_key]
      properties:
        name:
          type: string
        platform:
          type: string
        rtmp_url:
          type: string
          format: uri
        stream_key:
          type: string
        enabled:
          type: boolean
          default: true

    StreamDestinationUpdate:
      type: object
      properties:
        name:
          type: string
        rtmp_url:
          type: string
          format: uri
        stream_key:
          type: string
        enabled:
          type: boolean

    StreamConfig:
      type: object
      properties:
        video_bitrate_kbps:
          type: integer
          minimum: 100
          maximum: 100000
          example: 4500
        audio_bitrate_kbps:
          type: integer
          minimum: 32
          maximum: 320
          example: 192
        resolution:
          type: string
          enum: [480p, 720p, 1080p, 1440p, 2160p]
        framerate:
          type: integer
          enum: [24, 25, 30, 50, 60]
        keyframe_interval_seconds:
          type: integer
          minimum: 1
          maximum: 10
          example: 2

    Track:
      type: object
      properties:
        id:
          type: integer
          format: int64
        title:
          type: string
        artist:
          type: string
        album:
          type: string
          nullable: true
        genre:
          type: string
          nullable: true
        isrc:
          type: string
          nullable: true
          description: International Standard Recording Code, if known
        duration_seconds:
          type: integer
          nullable: true

    MusicLibraryPage:
      type: object
      properties:
        tracks:
          type: array
          items:
            $ref: '#/components/schemas/Track'
        total:
          type: integer
          minimum: 0
        offset:
          type: integer
          minimum: 0
        limit:
          type: integer
          minimum: 1

    Drive:
      type: object
      properties:
        path:
          type: string
        label:
          type: string
          nullable: true
        volume_serial:
          type: string
          description: Stable serial used to track scan progress across drive-letter changes
        size_bytes:
          type: integer
          format: int64
        free_bytes:
          type: integer
          format: int64

    FingerprintStats:
      type: object
      properties:
        known_tracks:
          type: integer
          minimum: 0
        unique_artists:
          type: integer
          minimum: 0
        last_updated_at:
          type: string
          format: date-time
          nullable: true

    SubmitStatus:
      type: object
      properties:
        pending:
          type: integer
          minimum: 0
        submitted:
          type: integer
          minimum: 0
        failed:
          type: integer
          minimum: 0
        last_attempt_at:
          type: string
          format: date-time
          nullable: true

    ScanStart:
      type: object
      required: [drive]
      properties:
        drive:
          type: string
          description: Drive path to scan (e.g. D:\\ on Windows, /media/usb on Linux)
        incremental:
          type: boolean
          default: true
          description: When true, only new files are processed if the drive was previously scanned.

    ScanSession:
      type: object
      properties:
        id:
          type: string
        drive_label:
          type: string
          nullable: true
        volume_serial:
          type: string
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
        status:
          type: string
          enum: [running, paused, completed, failed]
        files_total:
          type: integer
          minimum: 0
        files_processed:
          type: integer
          minimum: 0

    ScanSessionSummary:
      type: object
      properties:
        identified:
          type: integer
          minimum: 0
        unidentified:
          type: integer
          minimum: 0
        error:
          type: integer
          minimum: 0
        duplicate:
          type: integer
          minimum: 0
        pending:
          type: integer
          minimum: 0

    ScanFile:
      type: object
      properties:
        id:
          type: integer
          format: int64
        session_id:
          type: string
        path:
          type: string
        size_bytes:
          type: integer
          format: int64
        sha256_prefix:
          type: string
          description: First 256 KB SHA-256 prefix used for fast deduplication
        status:
          type: string
          enum: [identified, unidentified, error, duplicate, pending]
        track:
          oneOf:
            - $ref: '#/components/schemas/Track'
            - type: 'null'
        error_message:
          type: string
          nullable: true
        processed_at:
          type: string
          format: date-time
          nullable: true

    ScanFilePage:
      type: object
      properties:
        files:
          type: array
          items:
            $ref: '#/components/schemas/ScanFile'
        page:
          type: integer
          minimum: 1
        page_size:
          type: integer
          minimum: 1
        total:
          type: integer
          minimum: 0

    AdBid:
      type: object
      properties:
        id:
          type: integer
          format: int64
        cloud_ad_id:
          type: integer
          format: int64
          nullable: true
        client_name:
          type: string
        title:
          type: string
        rate_cents:
          type: integer
          minimum: 0
          description: Per-slot rate in the smallest currency unit (USD cents)
        slot_count:
          type: integer
          minimum: 1
        campaign_start:
          type: string
          format: date
        campaign_end:
          type: string
          format: date
        status:
          type: string
          enum: [pending, approved, rejected, expired, fulfilled]
        created_at:
          type: string
          format: date-time

    AdBidCreate:
      type: object
      required: [client_name, title, rate_cents, slot_count, campaign_start, campaign_end]
      properties:
        client_name:
          type: string
        title:
          type: string
        rate_cents:
          type: integer
          minimum: 0
        slot_count:
          type: integer
          minimum: 1
        campaign_start:
          type: string
          format: date
        campaign_end:
          type: string
          format: date
        notes:
          type: string

    AdBidUpdate:
      type: object
      properties:
        status:
          type: string
          enum: [pending, approved, rejected, expired, fulfilled]
        rate_cents:
          type: integer
          minimum: 0
        slot_count:
          type: integer
          minimum: 1
        notes:
          type: string

    AdPlayEvent:
      type: object
      properties:
        id:
          type: integer
          format: int64
        cloud_ad_id:
          type: integer
          format: int64
          nullable: true
        title:
          type: string
        client_name:
          type: string
        played_at:
          type: string
          format: date-time
        duration_seconds:
          type: integer
          minimum: 0
        synced:
          type: boolean
          description: True once the play event has been mirrored to the cloud ledger

    ArtistClaim:
      type: object
      properties:
        id:
          type: integer
          format: int64
        artist:
          type: string
        track_title:
          type: string
        isrc:
          type: string
          nullable: true
        play_count:
          type: integer
          minimum: 0
        period_start:
          type: string
          format: date
        period_end:
          type: string
          format: date
        status:
          type: string
          enum: [pending, verified, paid, rejected]
        submitted_at:
          type: string
          format: date-time

    ArtistClaimCreate:
      type: object
      required: [artist, track_title, period_start, period_end]
      properties:
        artist:
          type: string
        track_title:
          type: string
        isrc:
          type: string
        period_start:
          type: string
          format: date
        period_end:
          type: string
          format: date
        contact_email:
          type: string
          format: email

# vim: set sw=2 ts=2 et:
