# Building a custom Radio Cult player

You are helping build a custom web player for a Radio Cult station. A correct player has two independent halves:

1. **Audio**: an `<audio>` element that plays the station's stream URL, attached and detached according to the lifecycle rules below.
2. **Metadata**: realtime now-playing info delivered over a Socket.IO connection. Never poll for metadata.

Before writing code, check the live API reference for up-to-date endpoint paths and response shapes: https://www.radiocult.fm/docs/api

## What you need from the user

1. **Station ID**: found on the API settings page at https://app.radiocult.fm/settings/cms/api
2. **Publishable (public) API key**: starts with `pk_`, from the same settings page.

**Security rule (non-negotiable):** only the publishable key may appear in client-side code. Never use a secret key (starts with `sk_`) in a website, app bundle, or anything delivered to browsers. If the user provides a secret key for the player, stop and tell them to create a publishable key instead.

The base URL for all API requests is `https://api.radiocult.fm`. Authenticate every request with the `x-api-key` header.

## Step 1: Get the streaming URL

Fetch the station's public profile and read the streaming URL from the first channel:

```js
const response = await fetch(
  `https://api.radiocult.fm/api/station/${stationId}`,
  {
    headers: { 'x-api-key': publicKey },
  }
);
const { station } = await response.json();
const streamingUrl = station.channels[0].streamingUrl;
```

## Step 2: Realtime metadata over Socket.IO (never poll)

Radio Cult pushes now-playing updates over WebSockets using Socket.IO. This is instant and puts less load on the API than polling. Do not set up `setInterval` polling loops.

Fetch the current state **once on load** so the UI is correct immediately, then open the socket and let realtime updates take over. Do not re-fetch on a timer.

Install the Socket.IO client (`socket.io-client`), then:

```js
import SocketIo from 'socket.io-client';

// 1. Fetch the current now-playing state once, on load.
const response = await fetch(
  `https://api.radiocult.fm/api/station/${stationId}/schedule/live`,
  { headers: { 'x-api-key': publicKey } }
);
const { result } = await response.json();
renderNowPlaying(result);

// 2. Switch to realtime updates. Every subsequent change arrives here.
const socket = SocketIo('https://api.radiocult.fm', {
  auth: { 'x-api-key': publicKey },
  transports: ['websocket'],
  query: { stationId },
});

socket.on('player-metadata', ({ status, content, metadata }) => {
  renderNowPlaying({ status, content, metadata });
});
```

Handle all three values of `status`:

- `schedule`: a scheduled event is playing. `content` is the Event object (show name, artist ids, artwork).
- `offAir`: nothing is playing. Show an off-air state.
- `defaultPlaylist`: the station's default playlist is playing. `content` has `name`, `numberOfSongs`, `duration`.

The `metadata` object carries the current track: `title`, `artist`, `album`, `duration`, `playoutStartUnixTimestamp`, and `artwork` (a map of pre-resized URLs: `'256x256'`, `'512x512'`, etc.). Artwork sizes can briefly be undefined right after upload, so fall back gracefully, for example `artwork?.['256x256'] ?? artwork?.original`.

## Step 3: Audio element lifecycle (critical)

Live streams are not files. Getting the `<audio>` lifecycle right keeps the station's listener analytics accurate and prevents stale audio. The rules:

1. **On play: attach, load, then play.** Set `audio.src = streamingUrl`, call `audio.load()`, then `audio.play()`. This joins the stream at the live edge.
2. **On pause: pause, then detach.** Call `audio.pause()`, then `audio.removeAttribute('src')` and `audio.load()`. Detaching disconnects the listener from the stream so the station's listener session ends cleanly, and it prevents the browser buffering a stream nobody is hearing.
3. **On resume: repeat the play steps.** Never call `play()` on a paused stream without re-attaching the source. Resuming an old buffer plays stale audio from minutes ago instead of the live broadcast.

Canonical vanilla JS implementation:

```js
const createPlayer = (streamingUrl) => {
  const audio = new Audio();
  audio.preload = 'none';

  let state = 'paused'; // 'paused' | 'buffering' | 'playing' | 'error'
  const setState = (next) => {
    state = next;
    renderPlayerControls(state);
  };

  audio.addEventListener('playing', () => setState('playing'));
  audio.addEventListener('waiting', () => setState('buffering'));
  audio.addEventListener('error', () => {
    if (audio.getAttribute('src')) {
      setState('error');
    }
  });

  const play = async () => {
    setState('buffering');
    audio.src = streamingUrl;
    audio.load();
    try {
      await audio.play();
    } catch {
      setState('error');
    }
  };

  const pause = () => {
    audio.pause();
    audio.removeAttribute('src');
    audio.load();
    setState('paused');
  };

  // Retry from the error state by re-attaching a fresh stream.
  const retry = play;

  return { play, pause, retry };
};
```

Wire the UI so the play button calls `play()`, the pause button calls `pause()`, and an error state offers `retry()`.

## Step 4: Adapt to the site

The pattern above is canonical. Adapt it to whatever the user's site uses (React, Vue, Svelte, plain HTML). In React, mirror the embed player approach: derive `src` from state (`playing ? streamingUrl : ''`) so pausing detaches the source, and drive UI state from the audio element's events rather than assumptions.

## Final checklist

- Publishable key only in client code, never a secret key.
- Metadata arrives over Socket.IO. No polling loops anywhere.
- One `schedule/live` fetch on load for the initial state, then socket updates. Nothing on a timer.
- Pausing detaches the audio source. Playing re-attaches and reloads it.
- All three statuses handled: `schedule`, `offAir`, `defaultPlaylist`.
- Artwork falls back when a size is missing.
