# Uploading media to Radio Cult

You are helping get audio files into a Radio Cult station's track library. This skill covers two situations:

1. **Direct upload (the main use case)**: the user asks you to upload files for them, for example "upload this folder to my station". You run the uploads yourself from their machine, following the workflow below.
2. **Building upload flows**: the user wants an upload script or automation for their own workflow. The same endpoint rules apply; use the code examples as the foundation.

Before uploading or writing code, check the live API reference for up-to-date details: https://www.radiocult.fm/docs/api#upload-a-track

## Before you start

- **The endpoint is disabled by default** and enabled per account on request. If uploads return an authorization error, tell the user to contact Radio Cult support to enable it.
- **This endpoint uses the secret API key** (starts with `sk_`, from https://app.radiocult.fm/settings/cms/api). Secret keys must only be used server-side or in CLI sessions. Reference it via an environment variable (for example `RADIOCULT_SECRET_KEY`) so it stays out of shell history, logs, and code. Never put it in client-side code, never commit it to a repository, and never echo it back to the user.

## File requirements

Validate before uploading, and skip and report any file that fails:

- Format: **mp3 or m4a** only.
- Size: **less than 750MB**.
- The file must have a **valid duration** (a corrupt or zero-length file is rejected). If `ffprobe` is available you can pre-check; otherwise let the API reject it and report.

## Direct upload workflow

When the user asks you to upload files directly:

1. **Gather what you need**: station ID, the secret key in an environment variable, and the files or folder to upload.
2. **Scan and validate**: expand folders, keep only mp3 and m4a files under 750MB, and note everything you are skipping and why.
3. **Confirm before uploading**: show the user the list of files that will be uploaded (and the skipped ones) and get a go-ahead.
4. **Resolve playlists and tags first** if the user wants tracks assigned: `GET /api/station/:stationId/playlists` and `GET /api/station/:stationId/tags` (both use the secret key) return the objects so you can map names to the IDs the upload expects.
5. **Upload sequentially**, one file at a time, reporting progress as you go.
6. **Summarise at the end**: uploaded, skipped (with reasons), failed, and any failed playlist or tag assignments.

Each upload is a multipart form POST, which works well with curl:

```bash
curl -X POST "https://api.radiocult.fm/api/station/$STATION_ID/media/track" \
  -H "x-api-key: $RADIOCULT_SECRET_KEY" \
  -F "stationMedia=@/path/to/track.mp3" \
  -F 'metadata={"title":"Show Title","artist":"Host Name"}' \
  -F "playlistIds=<playlist_id>" \
  -F "tagIds=<tag_id>"
```

## Request fields

- `stationMedia`: the audio file. Required.
- `metadata`: optional overrides for the tags extracted from the file. Must be **stringified JSON**, not a plain object. Fields, all optional: `title`, `filename`, `album`, `artist`, `isrc`, `notes` (max 4000 characters). `notes` becomes the default description when publishing to Mixcloud or Soundcloud from Radio Cult. If the user does not ask for overrides, omit `metadata` and Radio Cult reads the file's own tags (or falls back to the filename).
- `playlistIds` and `tagIds`: optional, up to 5 each, appended as repeated form fields. Assigns the uploaded track to existing playlists and tags.

Node example for built flows (Node 20+, no dependencies):

```js
import { openAsBlob } from 'node:fs';
import path from 'node:path';

const uploadTrack = async ({ stationId, secretKey, filePath, metadata }) => {
  const formData = new FormData();
  formData.append(
    'stationMedia',
    await openAsBlob(filePath),
    path.basename(filePath)
  );

  if (metadata) {
    formData.append('metadata', JSON.stringify(metadata));
  }

  const response = await fetch(
    `https://api.radiocult.fm/api/station/${stationId}/media/track`,
    {
      method: 'POST',
      headers: { 'x-api-key': secretKey },
      body: formData,
    }
  );

  return response;
};
```

## Handling the response

A successful upload returns `{ success: true, track: { id }, isFirstTrackUploaded }`. If the request included `playlistIds` or `tagIds`, the response also includes an `assignments` object, nested per type:

```json
{
  "success": true,
  "track": { "id": "..." },
  "isFirstTrackUploaded": false,
  "assignments": {
    "playlists": { "succeeded": ["playlist-1"], "failed": [] },
    "tags": { "succeeded": ["tag-1"], "failed": [] }
  }
}
```

`assignments.playlists` is present only when you sent `playlistIds`, and `assignments.tags` only when you sent `tagIds`. A failed assignment does not fail the upload, so always check each `failed` array and report it to the user.

## Batch uploads and rate limits

- Upload **sequentially**, one file at a time. Do not parallelise uploads.
- The API rate limits automation endpoints per station with burst (per minute) and daily limits. Every response includes `X-RateLimit-Remaining-*` headers. Slow down proactively when they get low.
- On a `429` response, wait the number of seconds in the `Retry-After` header before retrying, and use exponential backoff on repeated failures.

## Related: the Radio Cult MCP server

If the user has the Radio Cult MCP server connected (https://mcp.radiocult.fm), it can manage tracks, tags, playlists, and other station data with tools. It cannot upload files, because MCP transports are not suited to large file uploads. Use this REST endpoint for uploads even when MCP is available.
