blastoise/docs/api-reference.md

177 lines
5.4 KiB
Markdown

# 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.