# Blastoise API Reference

Blastoise is a synchronized music server. The server owns channel time and
queues; clients play audio locally.

```text
Reference HTTP: http://mhsgroove.peterino.com:3001
Reference WS:   ws://mhsgroove.peterino.com:3001
Local HTTP:     http://localhost:3001
Local WS:       ws://localhost:3001
```

Auth is an HttpOnly cookie named `blastoise_session`. Same-origin browser apps
can use normal `fetch`. Separate-origin browser apps need a same-origin proxy
or CORS with credentials. Native apps must store `Set-Cookie` and send it as
`Cookie` on HTTP and WebSocket requests.

Full details: [api-reference-full.md](./api-reference-full.md)

## Golden Rule

Use `track.id` for every machine operation:

```text
GET /api/tracks/:trackId
```

`track.id` is a content hash like `sha256:...`. `filename` and `title` are only
for display. Queue entries, playlists, cache keys, direct links, and audio URLs
should all use `track.id`.

## Core Shapes

```ts
type PlaybackMode = "once" | "repeat-all" | "repeat-one" | "shuffle";

type Track = {
  id: string; filename: string; title: string | null;
  artist?: string | null; album?: string | null; duration: number;
  replayGainDb?: number | null; replayPeak?: number | null; available?: boolean;
};

type ChannelInfo = {
  id: string; name: string; description: string; trackCount: number;
  listenerCount: number; listeners: string[]; isDefault: boolean;
  createdBy: number | null;
};

type ChannelState = {
  track: Track | null; currentTimestamp: number; channelName: string;
  channelId: string; description: string; paused: boolean; currentIndex: number;
  listenerCount: number; isDefault: boolean; playbackMode: PlaybackMode;
  queue?: Track[];
};

type Playlist = {
  id: string; name: string; description: string; ownerId: number;
  ownerName?: string; isPublic: boolean; shareToken: string | null;
  trackIds: string[]; createdAt: number; updatedAt: number;
};
```

`ChannelState.queue` is optional. It appears on WebSocket connect, queue
changes, and periodic refreshes. Keep the last known queue when omitted.

## Startup

```text
GET /api/status
GET /api/auth/me
GET /api/library
GET /api/channels
WS  /api/channels/:channelId/ws
```

Choose a channel: saved channel, else `isDefault`, else first channel.

## Endpoints

| Area | Endpoints |
|---|---|
| Status | `GET /api/status` |
| Auth | `POST /api/auth/signup`, `POST /api/auth/login`, `POST /api/auth/logout`, `GET /api/auth/me`, `POST /api/auth/kick-others` |
| Channels | `GET /api/channels`, `POST /api/channels`, `GET/PATCH/DELETE /api/channels/:id` |
| Playback control | `POST /api/channels/:id/jump`, `POST /api/channels/:id/seek`, `POST /api/channels/:id/mode` |
| Queue | `PATCH /api/channels/:id/queue` |
| Library/audio | `GET /api/library`, `GET /api/tracks/:trackId`, `POST /api/upload` |
| Playlists | `GET/POST /api/playlists`, `GET/PATCH/DELETE /api/playlists/:id`, `PATCH /api/playlists/:id/tracks` |
| Sharing | `POST/DELETE /api/playlists/:id/share`, `GET/POST /api/playlists/shared/:token` |
| URL import | `POST /api/fetch`, `POST /api/fetch/confirm`, `GET /api/fetch`, `DELETE /api/fetch/:itemId`, `DELETE /api/fetch` |

Common bodies:

```json
{ "username": "test", "password": "testuser" }
{ "name": "Channel or playlist name", "description": "optional" }
{ "mode": "repeat-all" }
{ "index": 3 }
{ "timestamp": 45.5 }
```

Queue and playlist track mutation:

```json
{ "set": ["sha256:a", "sha256:b"] }
{ "add": ["sha256:c"], "insertAt": 2 }
{ "remove": [3, 4] }
{ "move": [5, 6], "to": 1 }
```

Remove/move use positions, not track IDs. Duplicate tracks are allowed.

Audio supports range requests:

```text
Range: bytes=0-999999
```

## WebSocket

Connect to:

```text
ws://mhsgroove.peterino.com:3001/api/channels/:channelId/ws
```

Client messages:

```json
{ "action": "switch", "channelId": "abc123" }
{ "action": "pause" }
{ "action": "unpause" }
{ "action": "seek", "timestamp": 45.5 }
{ "action": "jump", "index": 3 }
```

Server messages:

```json
{ "type": "channel_list", "channels": [] }
{ "type": "switched", "channelId": "abc123" }
{ "type": "kick", "reason": "Kicked by another session" }
{ "type": "toast", "message": "Added: Song", "toastType": "info" }
{ "type": "scan_progress", "scanning": true, "processed": 1, "total": 20 }
{ "type": "fetch_progress", "id": "job", "status": "downloading", "progress": 50 }
```

Any message without `type` is a `ChannelState`.

Guests can listen and switch channels, but cannot control playback or mutate
queues. Unauthorized WebSocket control messages are ignored.

## Sync Algorithm

On every `ChannelState`:

1. Store the state and `performance.now()`.
2. If `state.queue` exists, replace the local queue cache.
3. If `state.track` is null, pause and clear the player.
4. If `state.track.id` changed, set `audio.src` to `/api/tracks/:trackId` and
   seek to `state.currentTimestamp`.
5. If same track and drift is `>= 2s`, seek to `state.currentTimestamp`.
6. If `state.paused`, pause. Otherwise call `audio.play()`.
7. Between WebSocket updates, estimate time as
   `state.currentTimestamp + elapsedSeconds`, unless paused.

The server is the source of truth.

## Gotchas

- Some errors are JSON `{ "error": "..." }`; some are plain text. Handle both.
- `GET /api/channels/:id` does not include the queue. WebSocket connect does.
- `POST /api/playlists/shared/:token` copies a playlist; there is no `/copy`.
- Cache by `track.id`, never by filename.
- The server does not decode audio. Clients are synchronized local players.

