Files
cottongin 0f99e7914b docs: add IRC commands/plugin setup to README, update WebSocket docs
README was missing IRC command reference, plugin installation/config
guidance, and referenced nonexistent plugin READMEs. The WebSocket
docs in api.md were stale — subscribe message now documents role and
client_id fields, status message now includes the clients array.

Made-with: Cursor
2026-03-12 08:26:16 -04:00

474 lines
10 KiB
Markdown

# NtR SoundCloud Fetcher -- API Reference
Base URL: `http://127.0.0.1:8000` (configurable via `NTR_HOST` / `NTR_PORT`)
---
## Public Endpoints
### `GET /health`
Service health check.
**Response**
```json
{
"status": "ok",
"poller_alive": true,
"last_fetch": "2026-03-12T02:00:00+00:00",
"current_week_track_count": 9
}
```
| Field | Type | Description |
|-------|------|-------------|
| `status` | string | Always `"ok"` |
| `poller_alive` | boolean | Whether the background poller is running |
| `last_fetch` | string \| null | ISO 8601 timestamp of last successful poll, or `null` if never |
| `current_week_track_count` | integer | Number of tracks in the current week's playlist |
---
### `GET /playlist`
Returns the current week's full playlist.
**Response**
```json
{
"show_id": 10,
"episode_number": 530,
"week_start": "2026-03-05T02:00:00+00:00",
"week_end": "2026-03-12T02:00:00+00:00",
"tracks": [
{
"show_id": 10,
"track_id": 12345,
"position": 1,
"title": "Night Drive",
"artist": "SomeArtist",
"permalink_url": "https://soundcloud.com/someartist/night-drive",
"artwork_url": "https://i1.sndcdn.com/artworks-...-large.jpg",
"duration_ms": 245000,
"license": "cc-by",
"liked_at": "2026-03-06T14:23:00+00:00",
"raw_json": "{...}"
}
]
}
```
| Field | Type | Description |
|-------|------|-------------|
| `show_id` | integer | Internal database ID for this show |
| `episode_number` | integer \| null | Episode number (e.g. 530), or `null` if not assigned |
| `week_start` | string | ISO 8601 UTC timestamp -- start of the show's like window |
| `week_end` | string | ISO 8601 UTC timestamp -- end of the show's like window |
| `tracks` | array | Ordered list of tracks (see Track Object below) |
---
### `GET /playlist/{position}`
Returns a single track by its position in the current week's playlist. Positions are 1-indexed (matching IRC commands `!1`, `!2`, etc.).
**Path Parameters**
| Parameter | Type | Description |
|-----------|------|-------------|
| `position` | integer | 1-based position in the playlist |
**Response** -- a single Track Object (see below).
**Errors**
| Status | Detail |
|--------|--------|
| 404 | `"No track at position {n}"` |
---
### `GET /shows`
Lists all shows, ordered by week start date (newest first).
**Query Parameters**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | integer | 20 | Max number of shows to return |
| `offset` | integer | 0 | Number of shows to skip |
**Response**
```json
[
{
"id": 10,
"episode_number": 530,
"week_start": "2026-03-05T02:00:00+00:00",
"week_end": "2026-03-12T02:00:00+00:00",
"created_at": "2026-03-05T03:00:00+00:00"
}
]
```
---
### `GET /shows/by-episode/{episode_number}`
Look up a show by its episode number. This is the recommended endpoint for IRC bot integrations (e.g. `!playlist 530`).
**Path Parameters**
| Parameter | Type | Description |
|-----------|------|-------------|
| `episode_number` | integer | The episode number (e.g. 530) |
**Response**
```json
{
"show_id": 10,
"episode_number": 530,
"week_start": "2026-03-05T02:00:00+00:00",
"week_end": "2026-03-12T02:00:00+00:00",
"tracks": [...]
}
```
**Errors**
| Status | Detail |
|--------|--------|
| 404 | `"No show with episode number {n}"` |
---
### `GET /shows/{show_id}`
Returns a specific show by internal database ID.
**Path Parameters**
| Parameter | Type | Description |
|-----------|------|-------------|
| `show_id` | integer | The show's internal database ID |
**Response**
```json
{
"show_id": 10,
"episode_number": 530,
"week_start": "2026-03-05T02:00:00+00:00",
"week_end": "2026-03-12T02:00:00+00:00",
"tracks": [...]
}
```
**Errors**
| Status | Detail |
|--------|--------|
| 404 | `"Show not found"` |
---
## Dashboard Endpoints
These routes are only available when the dashboard is enabled (all three `NTR_WEB_*` env vars set).
### `GET /login`
Serves the login page.
### `POST /login`
Authenticates with username and password. Sets a session cookie on success, redirects to `/dashboard`.
**Form Data**
| Field | Type | Description |
|-------|------|-------------|
| `username` | string | Dashboard username |
| `password` | string | Dashboard password |
### `GET /logout`
Clears the session cookie and redirects to `/login`.
### `GET /dashboard`
Serves the playlist dashboard page. Requires a valid session cookie (redirects to `/login` if absent).
---
## Admin Endpoints
All admin endpoints require a bearer token via the `Authorization` header:
```
Authorization: Bearer <NTR_ADMIN_TOKEN>
```
Returns `401` with `"Missing or invalid token"` if the header is absent or the token doesn't match.
---
### `POST /admin/refresh`
Triggers an immediate SoundCloud fetch for the current week's show.
**Request Body** (optional)
```json
{
"full": false
}
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `full` | boolean | `false` | Reserved for future use (full vs incremental refresh) |
**Response**
```json
{
"status": "refreshed",
"track_count": 9
}
```
---
### `POST /admin/tracks`
Manually add a track to the current week's show.
**Request Body**
```json
{
"track_id": 12345,
"position": 3
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `track_id` | integer | yes | SoundCloud track ID (must already exist in the `tracks` table) |
| `position` | integer | no | Insert at this position (shifts others down). Omit to append at end. |
**Response**
```json
{
"status": "added"
}
```
---
### `DELETE /admin/tracks/{track_id}`
Remove a track from the current week's show. Remaining positions are re-compacted.
**Path Parameters**
| Parameter | Type | Description |
|-----------|------|-------------|
| `track_id` | integer | SoundCloud track ID to remove |
**Response**
```json
{
"status": "removed"
}
```
**Errors**
| Status | Detail |
|--------|--------|
| 404 | `"Track not in current show"` |
---
### `PUT /admin/tracks/{track_id}/position`
Move a track to a new position within the current week's show.
**Path Parameters**
| Parameter | Type | Description |
|-----------|------|-------------|
| `track_id` | integer | SoundCloud track ID to move |
**Request Body**
```json
{
"position": 1
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `position` | integer | yes | New 1-based position for the track |
**Response**
```json
{
"status": "moved"
}
```
**Errors**
| Status | Detail |
|--------|--------|
| 404 | `"Track not in current show"` |
---
### `POST /admin/announce`
Broadcasts a track announcement to all connected WebSocket subscribers (IRC bots). The announcement is formatted as: `Now Playing: Song #N: Title by Artist - URL`.
Accepts either a bearer token OR a valid session cookie.
**Request Body**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `show_id` | integer | yes | Show database ID |
| `position` | integer | yes | Track position in the show |
**Response**
```json
{
"status": "announced",
"message": "Now Playing: Song #1: Night Drive by SomeArtist - https://soundcloud.com/..."
}
```
**Errors**
| Status | Detail |
|--------|--------|
| 401 | `"Unauthorized"` |
| 404 | `"No track at position {n}"` |
---
## Track Object
Returned inside playlist and show detail responses.
| Field | Type | Description |
|-------|------|-------------|
| `show_id` | integer | The show this track belongs to |
| `track_id` | integer | SoundCloud track ID |
| `position` | integer | 1-based position in the playlist |
| `title` | string | Track title |
| `artist` | string | Uploader's SoundCloud username |
| `permalink_url` | string | Full URL to the track on SoundCloud |
| `artwork_url` | string \| null | URL to artwork image, or `null` |
| `duration_ms` | integer | Track duration in milliseconds |
| `license` | string | License string (e.g. `"cc-by"`, `"cc-by-sa"`) |
| `liked_at` | string | ISO 8601 timestamp of when the host liked the track |
| `raw_json` | string | Full SoundCloud API response for this track (JSON string) |
---
## Week Boundaries
Shows follow a weekly cadence aligned to **Wednesday 22:00 Eastern Time** (EST or EDT depending on DST). The like window for a show runs from the previous Wednesday 22:00 ET to the current Wednesday 22:00 ET.
All timestamps in API responses are UTC. The boundary shifts by 1 hour across DST transitions:
| Period | Eastern | UTC boundary |
|--------|---------|--------------|
| EST (Nov -- Mar) | Wed 22:00 | Thu 03:00 |
| EDT (Mar -- Nov) | Wed 22:00 | Thu 02:00 |
---
## WebSocket
### `WS /ws/announce`
WebSocket endpoint for receiving announce broadcasts. Used by IRC bot plugins.
**Authentication**
After connecting, send a subscribe message:
```json
{
"type": "subscribe",
"token": "<NTR_ADMIN_TOKEN>",
"role": "bot",
"client_id": "limnoria"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | Must be `"subscribe"` |
| `token` | string | yes | `NTR_ADMIN_TOKEN` value |
| `role` | string | no | Client role, defaults to `"bot"` |
| `client_id` | string | no | Identifier for this client (e.g. `"sopel"`, `"limnoria"`) |
Invalid token closes the connection with code `4001`.
**Messages from server**
Announce broadcast:
```json
{"type": "announce", "message": "Now Playing: Song #1: Title by Artist - URL"}
```
Status update (sent on subscriber connect/disconnect):
```json
{
"type": "status",
"subscribers": 2,
"clients": [
{
"client_id": "limnoria",
"remote_addr": "1.2.3.4",
"connected_at": "2026-03-12T02:00:00+00:00"
}
]
}
```
| Field | Type | Description |
|-------|------|-------------|
| `subscribers` | integer | Number of connected bot clients |
| `clients` | array | List of connected bot clients with their `client_id`, `remote_addr`, and `connected_at` timestamp |
---
## Dashboard Configuration
The web dashboard is optional. Enable it by setting all three environment variables:
| Variable | Default | Description |
|----------|---------|-------------|
| `NTR_WEB_USER` | *(required)* | Dashboard login username |
| `NTR_WEB_PASSWORD` | *(required)* | Dashboard login password |
| `NTR_SECRET_KEY` | *(required)* | Secret key for session cookie signing |
If any of these are absent, dashboard routes are not mounted and the API works exactly as before.