feat: add historical backfill with --init CLI and episode numbering
Adds a --init mode that seeds the database with past shows from a given anchor episode/date forward, batch-fetching likes from SoundCloud and partitioning them into weekly buckets. Episode numbers are tracked in the shows table and auto-incremented by the poller for new shows. Includes full API documentation (docs/api.md) and updated README. Made-with: Cursor
This commit is contained in:
310
docs/api.md
Normal file
310
docs/api.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# 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/{show_id}`
|
||||
|
||||
Returns a specific show with its full track listing.
|
||||
|
||||
**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"` |
|
||||
|
||||
---
|
||||
|
||||
## 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"` |
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
Reference in New Issue
Block a user