# 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 ``` 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": "", "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.