-
Notifications
You must be signed in to change notification settings - Fork 0
API Reference
Version: 1.7.0
Base URL: https://your-instance.com/api/v1
Grimnir Radio provides a comprehensive REST API for programmatic access to all station management, scheduling, and playback features.
- OpenAPI Specification - Complete API spec in OpenAPI 3.0 format
- Python Client Library - Full-featured Python client
- API Guide - Quick start guide with examples
The full API is documented in OpenAPI 3.0 format. You can view it interactively:
# Using Swagger UI with Docker
docker run -p 8081:8080 -e SWAGGER_JSON=/api/openapi.yaml \
-v $(pwd)/api:/api swaggerapi/swagger-ui
# Then open http://localhost:8081A full-featured Python client is available for easy integration:
from grimnir_client import GrimnirClient
# Initialize with your API key (get it from your profile page)
client = GrimnirClient("https://your-instance.com", api_key="gr_your-api-key")
# Get stations
stations = client.get_stations()
# Get now playing
np = client.get_now_playing(station_id)
print(f"Now Playing: {np['title']} by {np['artist']}")
# Upload media
media = client.upload_media(station_id, "/path/to/song.mp3")See Client Libraries for installation and full documentation.
This section provides detailed documentation for all API endpoints.
- Stations
- Mounts
- Media
- Smart Blocks
- Clocks
- Schedule
- Live Input
- Playout Control
- Analytics
- Webhooks
- Events (WebSocket)
- Health
- ⏳ Planned Endpoints (Future Architecture)
- Data Models
- Error Responses
List all stations.
Authentication: Required
Response (200 OK):
[
{
"id": "uuid",
"name": "WGMR FM",
"description": "Community radio",
"timezone": "America/New_York",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]Example:
curl http://localhost:8080/api/v1/stations \
-H "X-API-Key: YOUR_API_KEY"Create a new station.
Authentication: Required (admin, manager)
Request Body:
{
"name": "WGMR FM",
"description": "Community radio station",
"timezone": "America/New_York"
}Response (201 Created):
{
"id": "uuid",
"name": "WGMR FM",
"description": "Community radio station",
"timezone": "America/New_York",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}Validation:
-
nameis required -
timezonedefaults to "UTC" if not provided
Error Codes:
-
name_required(400) - Name field is missing
Get details for a specific station.
Authentication: Required
URL Parameters:
-
stationID- Station UUID
Response (200 OK):
{
"id": "uuid",
"name": "WGMR FM",
"description": "Community radio",
"timezone": "America/New_York",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}Error Codes:
-
not_found(404) - Station does not exist
Status: NOT IMPLEMENTED
Update station details.
Authentication: Required (admin, manager)
List all mounts for a station.
Authentication: Required
URL Parameters:
-
stationID- Station UUID
Response (200 OK):
[
{
"id": "uuid",
"station_id": "uuid",
"name": "Main Stream",
"url": "icecast://server:8000/stream",
"format": "mp3",
"bitrate": 128,
"channels": 2,
"sample_rate": 44100,
"encoder_preset_id": "",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]Create a new mount for a station.
Authentication: Required (admin, manager)
URL Parameters:
-
stationID- Station UUID
Request Body:
{
"station_id": "optional-override",
"name": "Main Stream",
"url": "icecast://server:8000/stream",
"format": "mp3",
"bitrate_kbps": 128,
"channels": 2,
"sample_rate": 44100,
"encoder_preset": {}
}Response (201 Created):
{
"id": "uuid",
"station_id": "uuid",
"name": "Main Stream",
"url": "icecast://server:8000/stream",
"format": "mp3",
"bitrate": 128,
"channels": 2,
"sample_rate": 44100,
"encoder_preset_id": "",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}Required Fields:
nameurlformat
Error Codes:
-
missing_required_fields(400) - Required field is missing
Upload an audio file with metadata.
Authentication: Required (admin, manager, dj)
Request: multipart/form-data
Form Fields:
-
file(required) - Audio file binary -
station_id(optional) - Defaults to user's station from JWT -
title(optional) - Track title -
artist(optional) - Artist name -
album(optional) - Album name -
duration_seconds(optional) - Duration in seconds (float)
Response (201 Created):
{
"id": "uuid",
"station_id": "uuid",
"title": "Example Song",
"artist": "Example Artist",
"album": "Example Album",
"duration_seconds": 180.5,
"analysis_state": "pending",
"analysis_job_id": "uuid",
"filename": "song.mp3"
}Max File Size: 128 MB
Analysis: Media analysis is automatically queued after upload. Check /media/:mediaID for analysis results.
Error Codes:
-
invalid_multipart(400) - Invalid multipart form data -
file_required(400) - No file uploaded -
station_id_required(400) - Station ID missing -
invalid_duration(400) - Duration format invalid -
media_store_failed(500) - File storage failed -
analysis_queue_error(500) - Failed to enqueue analysis
Example:
curl -X POST http://localhost:8080/api/v1/media/upload \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@song.mp3" \
-F "title=Example Song" \
-F "artist=Example Artist" \
-F "station_id=$STATION_ID"Get media item details and analysis results.
Authentication: Required
URL Parameters:
-
mediaID- Media UUID
Response (200 OK):
{
"id": "uuid",
"station_id": "uuid",
"title": "Example Song",
"artist": "Example Artist",
"album": "Example Album",
"duration": 180500000000,
"path": "/media/station-uuid/media-uuid.mp3",
"storage_key": "",
"genre": "Rock",
"mood": "Energetic",
"label": "Independent",
"language": "en",
"explicit": false,
"loudness_lufs": -14.5,
"replay_gain": 0.0,
"bpm": 120.0,
"year": 2024,
"tags": [],
"cue_points": {
"intro_end": 5.2,
"outro_in": 175.3
},
"waveform": null,
"analysis_state": "complete",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}Analysis States:
-
pending- Waiting for analysis -
running- Analysis in progress -
complete- Analysis finished -
failed- Analysis failed
Error Codes:
-
not_found(404) - Media item does not exist
Smart Blocks are rule-based intelligent playlist generators.
List all smart blocks, optionally filtered by station.
Authentication: Required
Query Parameters:
-
station_id(optional) - Filter by station UUID
Response (200 OK):
[
{
"id": "uuid",
"station_id": "uuid",
"name": "Morning Rock",
"description": "Upbeat rock for morning drive",
"rules": {
"filters": [
{
"field": "genre",
"operator": "includes",
"value": ["Rock", "Alternative"]
}
],
"quotas": [],
"separation": {
"artist_minutes": 60,
"title_minutes": 240
}
},
"sequence": {
"mode": "weighted",
"weights": {}
},
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]Create a new smart block.
Authentication: Required (admin, manager)
Request Body:
{
"station_id": "uuid",
"name": "Morning Rock",
"description": "Upbeat rock for morning drive",
"rules": {
"filters": [
{
"field": "genre",
"operator": "includes",
"value": ["Rock", "Alternative"]
},
{
"field": "bpm",
"operator": "between",
"value": [100, 140]
}
],
"quotas": [
{
"field": "artist",
"min": 0,
"max": 2
}
],
"separation": {
"artist_minutes": 60,
"title_minutes": 240,
"album_minutes": 30,
"label_minutes": 0
}
},
"sequence": {
"mode": "weighted",
"weights": {
"genre": 1.0,
"mood": 0.5
}
}
}Response (201 Created):
{
"id": "uuid",
"station_id": "uuid",
"name": "Morning Rock",
"description": "Upbeat rock for morning drive",
"rules": { ... },
"sequence": { ... },
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}Required Fields:
station_idname
Rule Structure:
-
filters: Include/exclude rules for media selection
- Supported operators:
includes,excludes,between,equals - Supported fields:
genre,mood,artist,language,bpm,year,explicit,tags
- Supported operators:
- quotas: Min/max counts per field (e.g., max 2 tracks per artist)
- separation: Minimum minutes between repeats (artist/title/album/label)
Sequence Modes:
-
weighted: Score-based selection with field weights -
random: Random selection (default if not specified)
Generate a playlist from a smart block using its rules.
Authentication: Required
URL Parameters:
-
blockID- Smart Block UUID
Request Body:
{
"station_id": "uuid",
"mount_id": "optional-uuid",
"seed": 1234567890,
"duration_ms": 900000
}Request Fields:
-
station_id(required) - Station UUID -
seed(optional) - Random seed for deterministic generation (defaults to timestamp) -
duration_ms(optional) - Target duration in milliseconds (defaults to 900000 = 15 minutes) -
mount_id(optional) - Mount UUID for context
Response (200 OK):
{
"tracks": [
{
"media_id": "uuid",
"title": "Example Song",
"artist": "Example Artist",
"duration_ms": 180500
}
],
"total_duration_ms": 900000,
"seed": 1234567890,
"warnings": [],
"unresolved": false
}Error Codes:
-
block_id_required(400) - Block ID missing -
station_id_required(400) - Station ID missing -
unresolved(409) - Could not fill duration with available tracks matching rules -
materialize_failed(500) - Generation failed
Notes:
- Generation is deterministic: same seed + rules + media pool = same playlist
- If insufficient tracks match rules, returns 409 with
ErrUnresolved
Clock templates define hour-by-hour programming structure.
List all clocks for a station.
Authentication: Required
Query Parameters:
-
station_id(required) - Station UUID
Response (200 OK):
[
{
"id": "uuid",
"station_id": "uuid",
"name": "Morning Drive",
"slots": [
{
"id": "uuid",
"clock_hour_id": "uuid",
"position": 0,
"offset": 0,
"type": "smart_block",
"payload": {
"smart_block_id": "uuid",
"duration_ms": 900000
}
},
{
"id": "uuid",
"clock_hour_id": "uuid",
"position": 1,
"offset": 900000000000,
"type": "stopset",
"payload": {
"duration_ms": 120000
}
}
],
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]Slot Types:
-
smart_block- Generate music from smart block rules -
hard_item- Play specific media item -
stopset- Commercial/promo break
Create a new clock template.
Authentication: Required (admin, manager)
Request Body:
{
"station_id": "uuid",
"name": "Morning Drive",
"slots": [
{
"position": 0,
"offset_ms": 0,
"type": "smart_block",
"payload": {
"smart_block_id": "uuid",
"duration_ms": 900000
}
},
{
"position": 1,
"offset_ms": 900000,
"type": "stopset",
"payload": {
"duration_ms": 120000
}
}
]
}Response (201 Created):
{
"id": "uuid",
"station_id": "uuid",
"name": "Morning Drive",
"slots": [ ... ],
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}Required Fields:
station_idname
Slot Payload Formats:
smart_block:
{
"smart_block_id": "uuid",
"duration_ms": 900000
}hard_item:
{
"media_id": "uuid"
}stopset:
{
"duration_ms": 120000,
"playlist_id": "optional-uuid"
}Preview schedule generation for a clock.
Authentication: Required
URL Parameters:
-
clockID- Clock UUID
Query Parameters:
-
minutes(optional) - Simulation window in minutes (default: 60)
Response (200 OK):
[
{
"slot_position": 0,
"starts_at": "2024-01-01T00:00:00Z",
"ends_at": "2024-01-01T00:15:00Z",
"type": "smart_block",
"tracks": [
{
"media_id": "uuid",
"title": "Example Song",
"artist": "Example Artist"
}
]
}
]Error Codes:
-
clock_id_required(400) - Clock ID missing -
simulate_failed(500) - Simulation failed
The schedule is the materialized, time-ordered list of what will play.
Get upcoming schedule entries for a station.
Authentication: Required
Query Parameters:
-
station_id(required) - Station UUID -
hours(optional) - Lookahead window in hours (default: 6)
Response (200 OK):
[
{
"id": "uuid",
"station_id": "uuid",
"mount_id": "uuid",
"starts_at": "2024-01-01T00:00:00Z",
"ends_at": "2024-01-01T00:03:30Z",
"source_type": "media",
"source_id": "uuid",
"metadata": {
"title": "Example Song",
"artist": "Example Artist",
"album": "Example Album"
},
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]Source Types:
-
media- Music/audio file -
live- Live DJ input -
stopset- Commercial break -
webstream- External stream (planned, not yet implemented)
Rebuild the rolling schedule for a station.
Authentication: Required (admin, manager)
Request Body:
{
"station_id": "uuid"
}Response (202 Accepted):
{
"status": "refresh_queued"
}Notes:
- Refresh runs asynchronously
- Rebuilds next 48 hours (configurable via
GRIMNIR_SCHEDULER_LOOKAHEAD_MINUTES)
Modify an existing schedule entry.
Authentication: Required (admin, manager)
URL Parameters:
-
entryID- Schedule Entry UUID
Request Body (all fields optional):
{
"starts_at": "2024-01-01T00:05:00Z",
"ends_at": "2024-01-01T00:08:30Z",
"mount_id": "uuid",
"metadata": {
"title": "Updated Title",
"custom_field": "value"
}
}Response (200 OK):
{
"id": "uuid",
"station_id": "uuid",
"mount_id": "uuid",
"starts_at": "2024-01-01T00:05:00Z",
"ends_at": "2024-01-01T00:08:30Z",
"source_type": "media",
"source_id": "uuid",
"metadata": { ... },
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:10Z"
}Behavior:
- If
starts_atis changed withoutends_at, duration is preserved - Emits
schedule.updateevent on the event bus
Error Codes:
-
not_found(404) - Entry does not exist -
invalid_starts_at(400) - Invalid timestamp format -
invalid_ends_at(400) - Invalid timestamp format
Authorize a live source to connect to a mount.
Authentication: Required
Request Body:
{
"station_id": "uuid",
"mount_id": "uuid",
"token": "optional-auth-token"
}Response (200 OK):
{
"authorized": true
}Notes:
- Authorization logic is service-specific
- Used by Icecast/Shoutcast for DJ authentication
Trigger live DJ takeover for a mount.
Authentication: Required (admin, manager)
Request Body:
{
"station_id": "uuid",
"mount_id": "uuid"
}Response (200 OK):
{
"status": "handover_initiated"
}Behavior:
- Publishes
dj.connectevent to the event bus - Playout manager switches from scheduled content to live input
Reload (restart) the GStreamer pipeline for a mount.
Authentication: Required (admin, manager)
Request Body:
{
"station_id": "uuid",
"mount_id": "uuid",
"launch": "gst-launch command string"
}Response (200 OK):
{
"status": "reloaded"
}Notes:
- Stops existing pipeline if running
- Starts new pipeline with provided GStreamer launch string
- 15-second timeout for pipeline startup
Error Codes:
-
mount_and_launch_required(400) - Missing required fields -
pipeline_start_failed(500) - Pipeline failed to start
Skip the currently playing track.
Authentication: Required (admin, manager, dj)
Request Body:
{
"station_id": "uuid",
"mount_id": "uuid"
}Response (200 OK):
{
"status": "skipped"
}Behavior:
- Stops current pipeline
- Publishes
now_playingevent withskipped: true - Next scheduled track will play
Stop playout for a mount.
Authentication: Required (admin, manager)
Request Body:
{
"mount_id": "uuid"
}Response (200 OK):
{
"status": "stopped"
}Error Codes:
-
mount_id_required(400) - Mount ID missing -
stop_failed(500) - Stop operation failed
Get the currently playing track for a station.
Authentication: Required
Query Parameters:
-
station_id(required) - Station UUID
Response (200 OK):
{
"id": "uuid",
"station_id": "uuid",
"mount_id": "uuid",
"media_id": "uuid",
"artist": "Example Artist",
"title": "Example Song",
"album": "Example Album",
"label": "Independent",
"started_at": "2024-01-01T00:00:00Z",
"ended_at": "0001-01-01T00:00:00Z",
"transition": "crossfade",
"metadata": {}
}Notes:
- Returns most recent play history entry
-
ended_atis zero-time if currently playing - Returns empty object
{}if nothing has played yet
Get play history (rotation report) for a station.
Authentication: Required (admin, manager)
Query Parameters:
-
station_id(required) - Station UUID -
since(optional) - RFC3339 timestamp (defaults to 30 days ago)
Response (200 OK):
[
{
"artist": "Example Artist",
"title": "Example Song",
"count": 42
},
{
"artist": "Another Artist",
"title": "Another Song",
"count": 38
}
]Notes:
- Groups by artist and title
- Ordered by play count descending
- Useful for DMCA reporting and rotation analysis
Ingest track change events from external systems.
Authentication: Required (admin)
Request Body (flexible):
{
"station_id": "uuid",
"mount_id": "uuid",
"title": "Example Song",
"artist": "Example Artist",
"custom_field": "value"
}Response (202 Accepted):
{
"status": "received"
}Behavior:
- Accepts any JSON payload
- Publishes to
now_playingevent on the event bus - Useful for integration with external automation systems
Subscribe to real-time events via WebSocket.
Authentication: Required
Query Parameters:
-
types(optional) - Comma-separated event types (defaults tonow_playing,health)
Example Event Types:
-
now_playing- Track changes -
health- System health changes -
schedule.update- Schedule modifications -
dj.connect- Live DJ connections
Connection:
const ws = new WebSocket('ws://localhost:8080/api/v1/events?types=now_playing,health');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(data.type, data.payload);
};Message Format:
{
"type": "now_playing",
"payload": {
"station_id": "uuid",
"mount_id": "uuid",
"title": "Example Song",
"artist": "Example Artist"
}
}Ping Messages:
{
"type": "ping"
}Notes:
- Server sends ping every 15 seconds
- Connection auto-closes on error or context cancellation
API health check endpoint.
Authentication: Not required
Response (200 OK):
{
"status": "ok"
}The following endpoints are NOT YET IMPLEMENTED but are planned as part of the multi-process architecture (API Gateway, Planner, Executor Pool, Media Engine). See docs/ARCHITECTURE_ROADMAP.md for implementation timeline.
These endpoints implement the 5-tier priority ladder: Emergency (0) > Live Override (1) > Live Scheduled (2) > Automation (3) > Fallback (4).
Insert emergency content with highest priority (0). Preempts all other content immediately.
Authentication: Required (admin only)
Request Body:
{
"station_id": "uuid",
"source_type": "media" | "webstream",
"source_id": "uuid",
"crossfade_ms": 500
}Response (200 OK):
{
"priority_id": "uuid",
"state": "active",
"priority_level": 0,
"started_at": "timestamp"
}Behavior:
- Immediately preempts current content (any priority level)
- Fades out current track over
crossfade_ms - Starts emergency content
- Publishes
priority.emergencyevent to event bus - Returns to previous priority level when emergency content ends
Start a live override (priority 1). Used for unscheduled live content that should preempt automation.
Authentication: Required (admin, manager)
Request Body:
{
"station_id": "uuid",
"source_type": "live" | "webstream",
"source_id": "uuid",
"crossfade_ms": 2000
}Response (200 OK):
{
"priority_id": "uuid",
"state": "active",
"priority_level": 1,
"started_at": "timestamp"
}Behavior:
- Preempts automation (priority 3) and fallback (priority 4)
- Does NOT preempt emergency (priority 0) or live scheduled (priority 2)
- Crossfades out current track
- Publishes
priority.overrideevent
Get current priority state for all stations or a specific station.
Authentication: Required
Query Parameters:
-
station_id(optional) - Filter by station
Response (200 OK):
{
"states": [
{
"station_id": "uuid",
"current_priority": 3,
"current_source_type": "media",
"current_source_id": "uuid",
"active_priority_id": "uuid",
"state": "playing" | "fading" | "preloading",
"buffer_depth_samples": 4096,
"last_heartbeat": "timestamp"
}
]
}Release a live override, returning to scheduled content.
Authentication: Required (admin, manager)
Response (200 OK):
{
"status": "released",
"returned_to_priority": 2
}Behavior:
- Crossfades from override back to scheduled content
- Publishes
priority.releasedevent - State machine transitions to next highest priority
Monitor and control per-station executor state.
Get all executor states across all stations.
Authentication: Required (admin, manager)
Response (200 OK):
{
"executors": [
{
"id": "uuid",
"station_id": "uuid",
"state": "idle" | "preloading" | "playing" | "fading" | "live" | "emergency",
"current_priority": 3,
"current_source_id": "uuid",
"current_source_type": "media",
"buffer_depth_samples": 4096,
"last_heartbeat": "timestamp",
"metadata": {
"now_playing": {...},
"next_scheduled": {...}
}
}
]
}States:
-
idle- No content playing -
preloading- Loading next track into buffer -
playing- Currently playing content -
fading- Crossfade in progress -
live- Live input active -
emergency- Emergency content active
Get executor state for a specific station.
Authentication: Required
Response (200 OK):
{
"id": "uuid",
"station_id": "uuid",
"state": "playing",
"current_priority": 3,
"current_source_id": "uuid",
"current_source_type": "media",
"buffer_depth_samples": 4096,
"last_heartbeat": "timestamp",
"telemetry": {
"peak_level_dbfs": -6.2,
"rms_level_dbfs": -12.5,
"loudness_lufs": -16.1,
"true_peak_dbtp": -1.2,
"underruns": 0
}
}Manage graph-based DSP pipeline configurations for media engine.
List all DSP graph configurations.
Authentication: Required
Response (200 OK):
{
"graphs": [
{
"id": "uuid",
"station_id": "uuid",
"name": "Default Broadcast Chain",
"description": "Standard loudness + compression + limiting",
"nodes": [
{
"type": "loudness_normalize",
"config": {
"target_lufs": -16.0,
"true_peak_limit_dbtp": -1.0
}
},
{
"type": "agc",
"config": {
"target_level_db": -14.0,
"attack_ms": 5,
"release_ms": 100
}
},
{
"type": "compressor",
"config": {
"threshold_db": -18.0,
"ratio": 3.0,
"attack_ms": 5,
"release_ms": 50
}
},
{
"type": "limiter",
"config": {
"ceiling_dbtp": -1.0,
"attack_ms": 1,
"release_ms": 100
}
}
],
"created_at": "timestamp",
"updated_at": "timestamp"
}
]
}Create a new DSP graph configuration.
Authentication: Required (admin, manager)
Request Body:
{
"station_id": "uuid",
"name": "Custom Broadcast Chain",
"description": "Heavy compression for consistent sound",
"nodes": [
{
"type": "loudness_normalize",
"config": {
"target_lufs": -16.0,
"true_peak_limit_dbtp": -1.0
}
}
]
}Response (201 Created):
{
"id": "uuid",
"station_id": "uuid",
"name": "Custom Broadcast Chain",
"nodes": [...],
"created_at": "timestamp"
}Supported Node Types:
-
loudness_normalize- EBU R128/ATSC A/85 loudness normalization -
agc- Automatic Gain Control -
compressor- Dynamic range compression -
limiter- True peak limiting -
ducking- Side-chain ducking for voice-overs -
silence_detector- Detect and alert on silence
Get a specific DSP graph configuration.
Authentication: Required
Response (200 OK):
{
"id": "uuid",
"station_id": "uuid",
"name": "Graph Name",
"description": "Description",
"nodes": [...],
"created_at": "timestamp",
"updated_at": "timestamp"
}Update a DSP graph configuration.
Authentication: Required (admin, manager)
Request Body:
{
"name": "Updated Name",
"description": "Updated description",
"nodes": [...]
}Response (200 OK):
{
"id": "uuid",
"station_id": "uuid",
"name": "Updated Name",
"nodes": [...],
"updated_at": "timestamp"
}Notes:
- Updating an active graph requires media engine reload
- Changes are NOT applied to running pipelines until reload
Delete a DSP graph configuration.
Authentication: Required (admin)
Response (200 OK):
{
"status": "deleted"
}Error Codes:
-
graph_in_use(409) - Cannot delete graph currently in use
Manage external HTTP/ICY stream sources with failover support.
List all webstream configurations.
Authentication: Required
Response (200 OK):
{
"webstreams": [
{
"id": "uuid",
"station_id": "uuid",
"name": "DJ Remote Stream",
"urls": [
"http://primary-dj.example.com:8000/stream",
"http://backup-dj.example.com:8000/stream"
],
"health_check_interval_ms": 5000,
"retry_limit": 3,
"grace_window_ms": 10000,
"preflight": true,
"metadata_passthrough": true,
"created_at": "timestamp",
"updated_at": "timestamp"
}
]
}Create a new webstream configuration.
Authentication: Required (admin, manager)
Request Body:
{
"station_id": "uuid",
"name": "DJ Remote Stream",
"urls": [
"http://primary-dj.example.com:8000/stream",
"http://backup-dj.example.com:8000/stream"
],
"health_check_interval_ms": 5000,
"retry_limit": 3,
"grace_window_ms": 10000,
"preflight": true,
"metadata_passthrough": true
}Response (201 Created):
{
"id": "uuid",
"station_id": "uuid",
"name": "DJ Remote Stream",
"urls": [...],
"created_at": "timestamp"
}Configuration:
-
urls- Ordered list of stream URLs (first = primary, rest = failovers) -
health_check_interval_ms- How often to probe stream health -
retry_limit- Max retries before failover -
grace_window_ms- Time to wait before switching back to primary -
preflight- Test connection before going live -
metadata_passthrough- Extract and use ICY StreamTitle
Get a specific webstream configuration.
Authentication: Required
Response (200 OK):
{
"id": "uuid",
"station_id": "uuid",
"name": "DJ Remote Stream",
"urls": [...],
"health_check_interval_ms": 5000,
"current_url_index": 0,
"last_health_check": "timestamp",
"health_status": "ok" | "degraded" | "failed",
"created_at": "timestamp",
"updated_at": "timestamp"
}Update a webstream configuration.
Authentication: Required (admin, manager)
Request Body:
{
"name": "Updated Name",
"urls": [...],
"health_check_interval_ms": 10000
}Response (200 OK):
{
"id": "uuid",
"name": "Updated Name",
"urls": [...],
"updated_at": "timestamp"
}Delete a webstream configuration.
Authentication: Required (admin)
Response (200 OK):
{
"status": "deleted"
}Error Codes:
-
webstream_in_use(409) - Cannot delete webstream currently scheduled or playing
Import data from AzuraCast and LibreTime installations.
Import from an AzuraCast installation.
Authentication: Required (admin only)
Request Body:
{
"source_type": "backup" | "api",
"backup_path": "/path/to/azuracast-backup.tar.gz",
"api_url": "https://azuracast.example.com",
"api_key": "azuracast-api-key",
"dry_run": true,
"station_mapping": {
"azuracast_station_id": "grimnir_station_id"
}
}Response (200 OK):
{
"migration_id": "uuid",
"state": "pending" | "running" | "complete" | "failed",
"dry_run": true,
"preview": {
"stations": 2,
"mounts": 4,
"media_items": 1523,
"playlists": 12,
"schedule_entries": 48
},
"started_at": "timestamp"
}Migration Process:
- If
dry_run: true, generates preview without applying changes - Validates backup structure and API connectivity
- Creates migration job tracked by
migration_id - Imports data transactionally (rollback on failure)
- Publishes
migration.progressevents via WebSocket
Import from a LibreTime installation.
Authentication: Required (admin only)
Request Body:
{
"source_type": "backup" | "database",
"backup_path": "/path/to/libretime-backup.tar.gz",
"db_connection_string": "postgresql://user:pass@host/libretime",
"dry_run": true,
"station_mapping": {
"default": "grimnir_station_id"
}
}Response (200 OK):
{
"migration_id": "uuid",
"state": "pending",
"dry_run": true,
"preview": {
"stations": 1,
"shows": 15,
"media_items": 892,
"playlists": 8,
"schedule_entries": 24
},
"started_at": "timestamp"
}Notes:
- LibreTime shows are converted to clock templates where possible
- Smart playlists are converted to Smart Blocks (with rule translation)
- Complex scheduling rules may require manual review
Get migration job status and results.
Authentication: Required (admin)
Response (200 OK):
{
"migration_id": "uuid",
"state": "complete",
"dry_run": false,
"started_at": "timestamp",
"completed_at": "timestamp",
"results": {
"stations_imported": 2,
"mounts_imported": 4,
"media_imported": 1523,
"media_skipped": 12,
"playlists_imported": 12,
"schedule_entries_imported": 48
},
"errors": [],
"warnings": [
"Complex rule in playlist 'Evening Mix' requires manual review"
]
}States:
-
pending- Queued, not started -
running- In progress -
complete- Finished successfully -
failed- Error occurred
{
"id": "uuid",
"email": "user@example.com",
"role": "admin" | "manager" | "dj",
"created_at": "timestamp",
"updated_at": "timestamp"
}Roles:
-
admin- Full access -
manager- Station management, programming, analytics -
dj- Media upload, playout skip
{
"id": "uuid",
"name": "Station Name",
"description": "Description text",
"timezone": "America/New_York",
"created_at": "timestamp",
"updated_at": "timestamp"
}{
"id": "uuid",
"station_id": "uuid",
"name": "Mount Name",
"url": "icecast://server:8000/mount",
"format": "mp3" | "aac" | "ogg",
"bitrate": 128,
"channels": 2,
"sample_rate": 44100,
"encoder_preset_id": "uuid",
"created_at": "timestamp",
"updated_at": "timestamp"
}{
"id": "uuid",
"station_id": "uuid",
"title": "Song Title",
"artist": "Artist Name",
"album": "Album Name",
"duration": 180500000000, // nanoseconds
"path": "/media/station/file.mp3",
"storage_key": "",
"genre": "Rock",
"mood": "Energetic",
"label": "Record Label",
"language": "en",
"explicit": false,
"loudness_lufs": -14.5,
"replay_gain": 0.0,
"bpm": 120.0,
"year": 2024,
"tags": [],
"cue_points": {
"intro_end": 5.2,
"outro_in": 175.3
},
"waveform": null,
"analysis_state": "complete",
"created_at": "timestamp",
"updated_at": "timestamp"
}Analysis States: pending, running, complete, failed
{
"id": "uuid",
"station_id": "uuid",
"name": "Block Name",
"description": "Description",
"rules": {
"filters": [...],
"quotas": [...],
"separation": {...}
},
"sequence": {
"mode": "weighted" | "random",
"weights": {...}
},
"created_at": "timestamp",
"updated_at": "timestamp"
}{
"id": "uuid",
"station_id": "uuid",
"name": "Clock Name",
"slots": [
{
"id": "uuid",
"clock_hour_id": "uuid",
"position": 0,
"offset": 0, // nanoseconds
"type": "smart_block" | "hard_item" | "stopset",
"payload": {...}
}
],
"created_at": "timestamp",
"updated_at": "timestamp"
}{
"id": "uuid",
"station_id": "uuid",
"mount_id": "uuid",
"starts_at": "timestamp",
"ends_at": "timestamp",
"source_type": "media" | "live" | "stopset",
"source_id": "uuid",
"metadata": {...},
"created_at": "timestamp",
"updated_at": "timestamp"
}{
"id": "uuid",
"station_id": "uuid",
"mount_id": "uuid",
"media_id": "uuid",
"artist": "Artist Name",
"title": "Song Title",
"album": "Album Name",
"label": "Record Label",
"started_at": "timestamp",
"ended_at": "timestamp",
"transition": "crossfade" | "cut" | "fade",
"metadata": {...}
}The following data models are NOT YET IMPLEMENTED but are planned as part of the multi-process architecture.
Per-station executor state tracking. Managed by the Executor Pool, exposed via API.
{
"id": "uuid",
"station_id": "uuid",
"state": "idle" | "preloading" | "playing" | "fading" | "live" | "emergency",
"current_priority": 0 | 1 | 2 | 3 | 4,
"current_source_id": "uuid",
"current_source_type": "media" | "live" | "webstream" | "fallback",
"buffer_depth_samples": 4096,
"last_heartbeat": "timestamp",
"metadata": {
"now_playing": {...},
"next_scheduled": {...}
},
"telemetry": {
"peak_level_dbfs": -6.2,
"rms_level_dbfs": -12.5,
"loudness_lufs": -16.1,
"true_peak_dbtp": -1.2,
"underruns": 0
},
"created_at": "timestamp",
"updated_at": "timestamp"
}States:
-
idle- No content playing -
preloading- Loading next track into buffer -
playing- Currently playing scheduled content -
fading- Crossfade in progress -
live- Live input active (priority 1 or 2) -
emergency- Emergency content active (priority 0)
Priority Levels:
-
0- Emergency (highest priority, immediate preemption) -
1- Live Override (unscheduled live content) -
2- Live Scheduled (scheduled live shows) -
3- Automation (normal scheduled content) -
4- Fallback (lowest priority, used when nothing else available)
Tracks active priority sources for the priority ladder.
{
"id": "uuid",
"station_id": "uuid",
"priority_level": 0 | 1 | 2 | 3 | 4,
"source_type": "media" | "live" | "webstream" | "fallback",
"source_id": "uuid",
"state": "active" | "fading_in" | "fading_out" | "released",
"crossfade_ms": 2000,
"started_at": "timestamp",
"ends_at": "timestamp",
"preempted_source_id": "uuid",
"metadata": {...},
"created_at": "timestamp",
"updated_at": "timestamp"
}Behavior:
- Created when priority source becomes active
- Updated as state transitions occur
-
preempted_source_idpoints to the source that was interrupted - State machine: active → fading_out → released
Graph-based DSP pipeline configuration for media engine.
{
"id": "uuid",
"station_id": "uuid",
"name": "Default Broadcast Chain",
"description": "Standard loudness + compression + limiting",
"nodes": [
{
"type": "loudness_normalize",
"config": {
"target_lufs": -16.0,
"true_peak_limit_dbtp": -1.0,
"measurement_window_ms": 400
}
},
{
"type": "agc",
"config": {
"target_level_db": -14.0,
"attack_ms": 5,
"release_ms": 100,
"max_gain_db": 12.0
}
},
{
"type": "compressor",
"config": {
"threshold_db": -18.0,
"ratio": 3.0,
"attack_ms": 5,
"release_ms": 50,
"knee_db": 6.0
}
},
{
"type": "limiter",
"config": {
"ceiling_dbtp": -1.0,
"attack_ms": 1,
"release_ms": 100
}
}
],
"active": true,
"created_at": "timestamp",
"updated_at": "timestamp"
}Supported Node Types:
-
loudness_normalize- EBU R128/ATSC A/85 loudness normalization -
agc- Automatic Gain Control (slow-acting leveling) -
compressor- Dynamic range compression -
limiter- True peak limiting (brick-wall) -
ducking- Side-chain ducking for voice-overs -
silence_detector- Detect and alert on silence (deadair detection)
Graph Execution:
- Nodes execute in order (serial DSP chain)
- Each node outputs to next node's input
- Final output goes to encoder(s)
External HTTP/ICY stream source with failover support.
{
"id": "uuid",
"station_id": "uuid",
"name": "DJ Remote Stream",
"description": "Primary DJ stream with backup",
"urls": [
"http://primary-dj.example.com:8000/stream",
"http://backup-dj.example.com:8000/stream"
],
"health_check_interval_ms": 5000,
"retry_limit": 3,
"grace_window_ms": 10000,
"preflight": true,
"metadata_passthrough": true,
"current_url_index": 0,
"last_health_check": "timestamp",
"health_status": "ok" | "degraded" | "failed",
"created_at": "timestamp",
"updated_at": "timestamp"
}Configuration:
-
urls- Ordered list of stream URLs (first = primary, rest = failovers) -
health_check_interval_ms- How often to probe stream health (default: 5000ms) -
retry_limit- Max connection retries before failover (default: 3) -
grace_window_ms- Time to wait before switching back to primary after failover (default: 10000ms) -
preflight- Test connection before going live (default: true) -
metadata_passthrough- Extract and use ICY StreamTitle metadata (default: true)
Health Status:
-
ok- Primary stream healthy -
degraded- Using failover stream -
failed- All streams failed
Failover Behavior:
- Primary fails → retry
retry_limittimes - If still failing → switch to next URL in list
- Continue down chain until successful connection
- After
grace_window_msof stability, probe primary - If primary healthy, switch back
Tracks import jobs from AzuraCast/LibreTime.
{
"id": "uuid",
"source_platform": "azuracast" | "libretime",
"source_type": "backup" | "api" | "database",
"dry_run": false,
"state": "pending" | "running" | "complete" | "failed",
"progress_percent": 75,
"current_step": "Importing media metadata",
"started_at": "timestamp",
"completed_at": "timestamp",
"results": {
"stations_imported": 2,
"mounts_imported": 4,
"media_imported": 1523,
"media_skipped": 12,
"playlists_imported": 12,
"schedule_entries_imported": 48
},
"errors": [],
"warnings": [
"Complex rule in playlist 'Evening Mix' requires manual review"
],
"created_by_user_id": "uuid",
"created_at": "timestamp",
"updated_at": "timestamp"
}States:
-
pending- Queued, not started -
running- In progress -
complete- Finished successfully -
failed- Error occurred
Progress Events:
- Publishes
migration.progressevents via WebSocket - Updates
progress_percentandcurrent_stepas work proceeds - Final results available in
resultsobject
All errors follow this format:
{
"error": "error_code"
}400 Bad Request:
-
invalid_json- Request body is not valid JSON -
api_key_required- API key is required -
station_id_required- Station ID is missing -
name_required- Name field is required -
missing_required_fields- One or more required fields missing -
invalid_multipart- Invalid multipart form data -
file_required- File upload is required -
invalid_duration- Duration format is invalid -
media_id_required- Media ID is missing -
block_id_required- Block ID is missing -
clock_id_required- Clock ID is missing -
entry_id_required- Entry ID is missing -
mount_id_required- Mount ID is missing -
station_and_mount_required- Both station and mount IDs required -
mount_and_launch_required- Both mount ID and launch command required -
invalid_starts_at- Invalid timestamp format for starts_at -
invalid_ends_at- Invalid timestamp format for ends_at
401 Unauthorized:
-
unauthorized- Invalid, expired, or revoked API key
403 Forbidden:
-
insufficient_role- User role lacks permission for this action
404 Not Found:
-
not_found- Resource does not exist
409 Conflict:
-
unresolved- Smart block could not generate sufficient tracks
500 Internal Server Error:
-
db_error- Database operation failed -
media_store_failed- File storage failed -
analysis_queue_error- Failed to enqueue analysis job -
materialize_failed- Smart block materialization failed -
simulate_failed- Clock simulation failed -
refresh_failed- Schedule refresh failed -
update_failed- Update operation failed -
authorize_failed- Authorization check failed -
pipeline_start_failed- GStreamer pipeline failed to start -
stop_failed- Stop operation failed
501 Not Implemented:
-
not_implemented- Endpoint exists but is not yet implemented
All authenticated endpoints require an X-API-Key header with a valid API key.
API Key Management:
- Log into the web dashboard
- Go to Profile page
- Generate an API key (choose expiration: 30 days, 90 days, 180 days, or 1 year)
- Copy the key (shown only once)
- Include in requests:
X-API-Key: YOUR_API_KEY
API Key Format: gr_<32 random characters> (e.g., gr_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6)
Example:
curl http://localhost:8080/api/v1/stations \
-H "X-API-Key: gr_your-api-key-here"Role-Based Access Control:
| Endpoint | Admin | Manager | DJ |
|---|---|---|---|
| GET /stations | ✓ | ✓ | ✓ |
| POST /stations | ✓ | ✓ | |
| POST /mounts | ✓ | ✓ | |
| POST /media/upload | ✓ | ✓ | ✓ |
| POST /smart-blocks | ✓ | ✓ | |
| POST /clocks | ✓ | ✓ | |
| POST /schedule/refresh | ✓ | ✓ | |
| PATCH /schedule/:id | ✓ | ✓ | |
| POST /live/handover | ✓ | ✓ | |
| POST /playout/reload | ✓ | ✓ | |
| POST /playout/skip | ✓ | ✓ | ✓ |
| POST /playout/stop | ✓ | ✓ | |
| GET /analytics/spins | ✓ | ✓ | |
| POST /webhooks/* | ✓ |
Currently not implemented. May be added in future versions.
API version is specified in the URL path: /api/v1/...
Breaking changes will increment the version number.
For issues and feature requests, see the project repository.
Getting Started
Core Concepts
Scheduling
Deployment
Integration
Operations
Development
Reference