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
474 lines
10 KiB
Markdown
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.
|