soulsync/Support/API.md
2026-03-04 13:42:47 -08:00

2400 lines
68 KiB
Markdown

# SoulSync REST API
SoulSync includes a full REST API at `/api/v1/` that lets you control everything from external apps, scripts, Discord bots, Home Assistant, or anything that can make HTTP requests.
## Quick Start
### 1. Generate an API Key
Go to **Settings** in the SoulSync web UI and find the **SoulSync API** section. Click **Generate API Key**, give it a label, and copy the key immediately — it's only shown once.
Alternatively, if no keys exist yet, use the bootstrap endpoint:
```bash
curl -X POST http://localhost:8008/api/v1/api-keys/bootstrap \
-H "Content-Type: application/json" \
-d '{"label": "My First Key"}'
```
### 2. Make Requests
Pass your key via the `Authorization` header:
```bash
curl -H "Authorization: Bearer sk_your_key_here" \
http://localhost:8008/api/v1/system/status
```
Or as a query parameter:
```
http://localhost:8008/api/v1/system/status?api_key=sk_your_key_here
```
### 3. Response Format
Every response follows this envelope:
```json
{
"success": true,
"data": { ... },
"error": null,
"pagination": null
}
```
Error responses:
```json
{
"success": false,
"data": null,
"error": {
"code": "NOT_FOUND",
"message": "Artist 999 not found."
},
"pagination": null
}
```
Paginated responses include:
```json
{
"pagination": {
"page": 1,
"limit": 50,
"total": 347,
"total_pages": 7,
"has_next": true,
"has_prev": false
}
}
```
---
## Authentication
All `/api/v1/` endpoints require an API key (except the bootstrap endpoint).
| Method | Details |
|--------|---------|
| Header | `Authorization: Bearer sk_...` |
| Query | `?api_key=sk_...` |
Keys are generated as `sk_` followed by a random token. Only the SHA-256 hash is stored — the raw key is shown once at creation.
### Error Codes
| Status | Code | Meaning |
|--------|------|---------|
| 401 | `AUTH_REQUIRED` | No API key provided |
| 403 | `INVALID_KEY` | API key is wrong or revoked |
---
## Rate Limiting
Requests are rate-limited to **60 per minute** per IP address.
Exceeding the limit returns `429 RATE_LIMITED`.
---
## Global Query Parameters
These optional parameters work on all endpoints that return entity data:
| Param | Type | Description |
|-------|------|-------------|
| `fields` | string | Comma-separated list of fields to return (e.g. `?fields=id,name,thumb_url`). Omit to return all fields. |
---
## Multi-Profile Support
SoulSync supports multiple user profiles. Profile-scoped endpoints (watchlist, wishlist, discovery) accept a profile identifier:
| Method | Details |
|--------|---------|
| Header | `X-Profile-Id: 2` |
| Query | `?profile_id=2` |
If omitted, defaults to profile 1 (admin). Profile scoping applies to: watchlist, wishlist, and discovery endpoints.
---
## Endpoints
### System
#### `GET /api/v1/system/status`
Server status, uptime, and service connectivity.
```json
{
"data": {
"uptime": "2h 15m 30s",
"uptime_seconds": 8130,
"services": {
"spotify": true,
"soulseek": true,
"hydrabase": false
}
}
}
```
#### `GET /api/v1/system/activity`
Recent activity feed.
```json
{
"data": {
"activities": [
{ "type": "download", "message": "Downloaded Track Name", "timestamp": "..." },
...
]
}
}
```
#### `GET /api/v1/system/stats`
Combined library and download statistics.
```json
{
"data": {
"library": {
"artists": 1250,
"albums": 4830,
"tracks": 52100
},
"database": {
"size_mb": 145.2,
"last_update": "2026-03-04T09:00:00"
},
"downloads": {
"active": 3
}
}
}
```
---
### Library — Artists
#### `GET /api/v1/library/artists`
List library artists with search, letter filtering, and pagination.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `search` | string | | Filter by name |
| `letter` | string | `all` | Filter by first letter (a-z, `#` for non-alpha) |
| `page` | int | 1 | Page number |
| `limit` | int | 50 | Items per page (max 200) |
| `watchlist` | string | `all` | `all`, `watched`, or `unwatched` |
| `fields` | string | | Comma-separated field list |
**Response:**
```json
{
"data": {
"artists": [
{
"id": 42,
"name": "Radiohead",
"thumb_url": null,
"banner_url": null,
"genres": ["alternative rock", "art rock"],
"summary": null,
"style": null,
"mood": null,
"label": null,
"server_source": null,
"created_at": null,
"updated_at": null,
"musicbrainz_id": "a74b1b7f-71a5-4011-9441-d0b5e4122711",
"spotify_artist_id": "4Z8W4fKeB5YxbusRsdQVPb",
"itunes_artist_id": "657515",
"audiodb_id": "111239",
"deezer_id": "399",
"musicbrainz_match_status": null,
"spotify_match_status": null,
"itunes_match_status": null,
"audiodb_match_status": null,
"deezer_match_status": null,
"musicbrainz_last_attempted": null,
"spotify_last_attempted": null,
"itunes_last_attempted": null,
"audiodb_last_attempted": null,
"deezer_last_attempted": null,
"album_count": 9,
"track_count": 101,
"is_watched": true,
"image_url": "https://..."
}
]
},
"pagination": { "page": 1, "limit": 50, "total": 1250, "total_pages": 25, "has_next": true, "has_prev": false }
}
```
> **Note:** The list endpoint returns a subset of metadata fields. Some fields like `summary`, `style`, `mood`, `label`, `banner_url`, and all `*_match_status` / `*_last_attempted` timestamps may be `null` in list view. Use the detail endpoint below for the complete record.
#### `GET /api/v1/library/artists/<artist_id>`
Get a single artist by ID with **all metadata** and their album list.
```json
{
"data": {
"artist": {
"id": 42,
"name": "Radiohead",
"thumb_url": "https://i.scdn.co/image/abc123...",
"banner_url": "https://www.theaudiodb.com/images/media/artist/fanart/...",
"genres": ["alternative rock", "art rock", "experimental"],
"summary": "Radiohead are an English rock band formed in Abingdon...",
"style": "Alternative/Indie",
"mood": "Melancholy",
"label": "XL Recordings",
"server_source": "plex",
"created_at": "2025-12-01T14:30:00",
"updated_at": "2026-02-15T09:12:00",
"musicbrainz_id": "a74b1b7f-71a5-4011-9441-d0b5e4122711",
"spotify_artist_id": "4Z8W4fKeB5YxbusRsdQVPb",
"itunes_artist_id": "657515",
"audiodb_id": "111239",
"deezer_id": "399",
"musicbrainz_match_status": "matched",
"spotify_match_status": "matched",
"itunes_match_status": "matched",
"audiodb_match_status": "matched",
"deezer_match_status": "matched",
"musicbrainz_last_attempted": "2026-01-10T08:00:00",
"spotify_last_attempted": "2026-01-10T08:00:00",
"itunes_last_attempted": "2026-01-10T08:00:00",
"audiodb_last_attempted": "2026-01-10T08:00:00",
"deezer_last_attempted": "2026-01-10T08:00:00"
},
"albums": [
{
"id": 87,
"artist_id": 42,
"title": "OK Computer",
"year": 1997,
"...": "..."
}
]
}
}
```
**Artist fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Internal database ID |
| `name` | string | Artist name |
| `thumb_url` | string? | Artist thumbnail/profile image URL |
| `banner_url` | string? | Artist banner/fanart image URL (from AudioDB) |
| `genres` | string[] | List of genre tags |
| `summary` | string? | Artist biography/description |
| `style` | string? | Musical style (from AudioDB) |
| `mood` | string? | Musical mood (from AudioDB) |
| `label` | string? | Record label (from AudioDB) |
| `server_source` | string? | Media server source (`plex`, `jellyfin`, `navidrome`) |
| `created_at` | string? | ISO 8601 timestamp when added to library |
| `updated_at` | string? | ISO 8601 timestamp of last update |
| `musicbrainz_id` | string? | MusicBrainz artist MBID |
| `spotify_artist_id` | string? | Spotify artist ID |
| `itunes_artist_id` | string? | Apple Music / iTunes artist ID |
| `audiodb_id` | string? | TheAudioDB artist ID |
| `deezer_id` | string? | Deezer artist ID |
| `musicbrainz_match_status` | string? | MusicBrainz enrichment status (`matched`, `not_found`, `error`) |
| `spotify_match_status` | string? | Spotify enrichment status |
| `itunes_match_status` | string? | iTunes enrichment status |
| `audiodb_match_status` | string? | AudioDB enrichment status |
| `deezer_match_status` | string? | Deezer enrichment status |
| `musicbrainz_last_attempted` | string? | ISO 8601 timestamp of last MusicBrainz lookup |
| `spotify_last_attempted` | string? | ISO 8601 timestamp of last Spotify lookup |
| `itunes_last_attempted` | string? | ISO 8601 timestamp of last iTunes lookup |
| `audiodb_last_attempted` | string? | ISO 8601 timestamp of last AudioDB lookup |
| `deezer_last_attempted` | string? | ISO 8601 timestamp of last Deezer lookup |
> Fields marked `?` may be `null` if the data hasn't been enriched from that provider yet.
#### `GET /api/v1/library/artists/<artist_id>/albums`
List all albums for a specific artist.
---
### Library — Albums
#### `GET /api/v1/library/albums`
List/search all albums with pagination and optional filters.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `search` | string | | Filter by album title |
| `artist_id` | int | | Filter by artist ID |
| `year` | int | | Filter by release year |
| `page` | int | 1 | Page number |
| `limit` | int | 50 | Items per page (max 200) |
| `fields` | string | | Comma-separated field list |
#### `GET /api/v1/library/albums/<album_id>`
Get a single album by ID with **all metadata** and embedded track list.
```json
{
"data": {
"album": {
"id": 87,
"artist_id": 42,
"title": "OK Computer",
"year": 1997,
"thumb_url": "https://i.scdn.co/image/...",
"genres": ["alternative rock"],
"track_count": 12,
"duration": 3198000,
"style": "Art Rock",
"mood": "Anxious",
"label": "Parlophone",
"explicit": false,
"record_type": "album",
"server_source": "plex",
"created_at": "2025-12-01T14:30:00",
"updated_at": "2026-02-15T09:12:00",
"musicbrainz_release_id": "a1c35a51-d102-4ce7-b7b0-8a4f68385bb2",
"spotify_album_id": "6dVIqQ8qmQ5GBnJ9shOYGE",
"itunes_album_id": "1097862703",
"audiodb_id": "2110483",
"deezer_id": "6575789",
"musicbrainz_match_status": "matched",
"spotify_match_status": "matched",
"itunes_match_status": "matched",
"audiodb_match_status": "matched",
"deezer_match_status": "matched",
"musicbrainz_last_attempted": "2026-01-10T08:00:00",
"spotify_last_attempted": "2026-01-10T08:00:00",
"itunes_last_attempted": "2026-01-10T08:00:00",
"audiodb_last_attempted": "2026-01-10T08:00:00",
"deezer_last_attempted": "2026-01-10T08:00:00"
},
"tracks": [
{
"id": 510,
"title": "Airbag",
"track_number": 1,
"...": "..."
}
]
}
}
```
**Album fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Internal database ID |
| `artist_id` | int | Parent artist ID |
| `title` | string | Album title |
| `year` | int? | Release year |
| `thumb_url` | string? | Album cover art URL |
| `genres` | string[] | Genre tags |
| `track_count` | int? | Number of tracks |
| `duration` | int? | Total duration in milliseconds |
| `style` | string? | Musical style (from AudioDB) |
| `mood` | string? | Musical mood (from AudioDB) |
| `label` | string? | Record label |
| `explicit` | bool? | Whether album contains explicit content |
| `record_type` | string? | Album type (`album`, `single`, `ep`, `compilation`) |
| `server_source` | string? | Media server source |
| `created_at` | string? | ISO 8601 timestamp |
| `updated_at` | string? | ISO 8601 timestamp |
| `musicbrainz_release_id` | string? | MusicBrainz release MBID |
| `spotify_album_id` | string? | Spotify album ID |
| `itunes_album_id` | string? | Apple Music / iTunes album ID |
| `audiodb_id` | string? | TheAudioDB album ID |
| `deezer_id` | string? | Deezer album ID |
| `musicbrainz_match_status` | string? | MusicBrainz enrichment status |
| `spotify_match_status` | string? | Spotify enrichment status |
| `itunes_match_status` | string? | iTunes enrichment status |
| `audiodb_match_status` | string? | AudioDB enrichment status |
| `deezer_match_status` | string? | Deezer enrichment status |
| `musicbrainz_last_attempted` | string? | ISO 8601 timestamp |
| `spotify_last_attempted` | string? | ISO 8601 timestamp |
| `itunes_last_attempted` | string? | ISO 8601 timestamp |
| `audiodb_last_attempted` | string? | ISO 8601 timestamp |
| `deezer_last_attempted` | string? | ISO 8601 timestamp |
#### `GET /api/v1/library/albums/<album_id>/tracks`
List all tracks in an album with full metadata.
---
### Library — Tracks
#### `GET /api/v1/library/tracks/<track_id>`
Get a single track by ID with **all metadata**.
```json
{
"data": {
"track": {
"id": 512,
"album_id": 87,
"artist_id": 42,
"title": "Paranoid Android",
"artist_name": "Radiohead",
"album_title": "OK Computer",
"track_number": 2,
"duration": 383000,
"file_path": "/music/Radiohead/OK Computer/02 - Paranoid Android.flac",
"bitrate": 1024,
"bpm": 82.5,
"explicit": false,
"style": "Art Rock",
"mood": "Anxious",
"repair_status": null,
"repair_last_checked": null,
"server_source": "plex",
"created_at": "2025-12-01T14:30:00",
"updated_at": "2026-02-15T09:12:00",
"musicbrainz_recording_id": "b3e2b7e0-a147-4b3c-8eab-fd90bfff7e74",
"spotify_track_id": "6LgJvl0Xdtc73RJ1mN1a7Z",
"itunes_track_id": "1097863011",
"audiodb_id": null,
"deezer_id": "119606528",
"musicbrainz_match_status": "matched",
"spotify_match_status": "matched",
"itunes_match_status": "matched",
"audiodb_match_status": null,
"deezer_match_status": "matched",
"musicbrainz_last_attempted": "2026-01-10T08:00:00",
"spotify_last_attempted": "2026-01-10T08:00:00",
"itunes_last_attempted": "2026-01-10T08:00:00",
"audiodb_last_attempted": null,
"deezer_last_attempted": "2026-01-10T08:00:00"
}
}
}
```
**Track fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Internal database ID |
| `album_id` | int | Parent album ID |
| `artist_id` | int | Parent artist ID |
| `title` | string | Track title |
| `artist_name` | string? | Artist name (joined from artists table) |
| `album_title` | string? | Album title (joined from albums table) |
| `track_number` | int? | Track number on the album |
| `duration` | int? | Duration in milliseconds |
| `file_path` | string? | File path on the media server |
| `bitrate` | int? | Audio bitrate in kbps |
| `bpm` | float? | Beats per minute |
| `explicit` | bool? | Whether track contains explicit content |
| `style` | string? | Musical style (from AudioDB) |
| `mood` | string? | Musical mood (from AudioDB) |
| `repair_status` | string? | Track repair status |
| `repair_last_checked` | string? | ISO 8601 timestamp of last repair check |
| `server_source` | string? | Media server source |
| `created_at` | string? | ISO 8601 timestamp |
| `updated_at` | string? | ISO 8601 timestamp |
| `musicbrainz_recording_id` | string? | MusicBrainz recording MBID |
| `spotify_track_id` | string? | Spotify track ID |
| `itunes_track_id` | string? | Apple Music / iTunes track ID |
| `audiodb_id` | string? | TheAudioDB track ID |
| `deezer_id` | string? | Deezer track ID |
| `musicbrainz_match_status` | string? | MusicBrainz enrichment status |
| `spotify_match_status` | string? | Spotify enrichment status |
| `itunes_match_status` | string? | iTunes enrichment status |
| `audiodb_match_status` | string? | AudioDB enrichment status |
| `deezer_match_status` | string? | Deezer enrichment status |
| `musicbrainz_last_attempted` | string? | ISO 8601 timestamp |
| `spotify_last_attempted` | string? | ISO 8601 timestamp |
| `itunes_last_attempted` | string? | ISO 8601 timestamp |
| `audiodb_last_attempted` | string? | ISO 8601 timestamp |
| `deezer_last_attempted` | string? | ISO 8601 timestamp |
#### `GET /api/v1/library/tracks`
Search tracks by title and/or artist. At least one of `title` or `artist` is required.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `title` | string | | Track title to search |
| `artist` | string | | Artist name to search |
| `limit` | int | 50 | Max results (max 200) |
| `fields` | string | | Comma-separated field list |
---
### Library — Genres
#### `GET /api/v1/library/genres`
List all genres in the library with occurrence counts.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `source` | string | `artists` | Table to aggregate from: `artists` or `albums` |
```json
{
"data": {
"genres": [
{ "name": "rock", "count": 234 },
{ "name": "alternative rock", "count": 189 },
{ "name": "indie rock", "count": 156 },
{ "name": "electronic", "count": 98 },
{ "name": "pop", "count": 87 }
],
"source": "artists"
}
}
```
---
### Library — Recently Added
#### `GET /api/v1/library/recently-added`
Get recently added content, ordered by creation date.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `type` | string | `albums` | Entity type: `albums`, `artists`, or `tracks` |
| `limit` | int | 50 | Max items (max 200) |
| `fields` | string | | Comma-separated field list |
```json
{
"data": {
"items": [
{
"id": 4831,
"artist_id": 42,
"title": "A Moon Shaped Pool",
"year": 2016,
"thumb_url": "https://...",
"genres": ["art rock"],
"...": "..."
}
],
"type": "albums"
}
}
```
---
### Library — External ID Lookup
#### `GET /api/v1/library/lookup`
Look up a library entity by its external provider ID. Useful for cross-referencing with Spotify, MusicBrainz, iTunes, Deezer, or AudioDB.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | Yes | `artist`, `album`, or `track` |
| `provider` | string | Yes | `spotify`, `musicbrainz`, `itunes`, `deezer`, or `audiodb` |
| `id` | string | Yes | The external ID value |
| `fields` | string | No | Comma-separated field list |
**Example — find an artist by Spotify ID:**
```
GET /api/v1/library/lookup?type=artist&provider=spotify&id=4Z8W4fKeB5YxbusRsdQVPb
```
```json
{
"data": {
"artist": {
"id": 42,
"name": "Radiohead",
"spotify_artist_id": "4Z8W4fKeB5YxbusRsdQVPb",
"...": "..."
}
}
}
```
**Example — find a track by MusicBrainz recording ID:**
```
GET /api/v1/library/lookup?type=track&provider=musicbrainz&id=b3e2b7e0-a147-4b3c-8eab-fd90bfff7e74
```
Returns `404 NOT_FOUND` if no matching entity exists in the library.
---
### Library — Stats
#### `GET /api/v1/library/stats`
Library statistics (counts and database info).
```json
{
"data": {
"artists": 1250,
"albums": 4830,
"tracks": 52100,
"database_size_mb": 145.2,
"last_update": "2026-03-04T09:00:00"
}
}
```
---
### Search
Search external music sources (Spotify, iTunes, Hydrabase). These endpoints search **external services**, not your local library (use `/library/tracks` or `/library/lookup` for that).
#### `POST /api/v1/search/tracks`
```json
{
"query": "Daft Punk Around the World",
"source": "auto",
"limit": 20
}
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `query` | string | *required* | Search query |
| `source` | string | `auto` | `auto` (Hydrabase > Spotify > iTunes), `spotify`, or `itunes` |
| `limit` | int | 20 | Max results (max 50) |
**Response:**
```json
{
"data": {
"tracks": [
{
"id": "2cGxRwrMyEAp8dEbuZaVv6",
"name": "Around the World",
"artists": ["Daft Punk"],
"album": "Homework",
"duration_ms": 428000,
"popularity": 78,
"preview_url": "https://...",
"image_url": "https://i.scdn.co/image/...",
"release_date": "1997-01-17"
}
],
"source": "spotify"
}
}
```
#### `POST /api/v1/search/albums`
```json
{
"query": "Discovery",
"limit": 10
}
```
**Response:**
```json
{
"data": {
"albums": [
{
"id": "2noRn2Aes5aoNVsU6iWThc",
"name": "Discovery",
"artists": ["Daft Punk"],
"release_date": "2001-03-12",
"total_tracks": 14,
"album_type": "album",
"image_url": "https://..."
}
],
"source": "spotify"
}
}
```
#### `POST /api/v1/search/artists`
```json
{
"query": "Daft Punk",
"limit": 10
}
```
**Response:**
```json
{
"data": {
"artists": [
{
"id": "4tZwfgrHOc3mvqYlEYSvnL",
"name": "Daft Punk",
"popularity": 82,
"genres": ["electro", "french house"],
"followers": 21000000,
"image_url": "https://..."
}
],
"source": "spotify"
}
}
```
---
### Downloads
#### `GET /api/v1/downloads`
List active and recent download tasks.
```json
{
"data": {
"downloads": [
{
"id": "task_abc123",
"status": "downloading",
"track_name": "Paranoid Android",
"artist_name": "Radiohead",
"album_name": "OK Computer",
"username": "soulseek_user_42",
"filename": "02 - Paranoid Android.flac",
"progress": 67,
"size": 45000000,
"error": null,
"batch_id": "batch_xyz",
"track_index": 2,
"retry_count": 0,
"metadata_enhanced": false,
"status_change_time": 1709550000.123
}
]
}
}
```
**Download fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique task identifier |
| `status` | string | `pending`, `searching`, `downloading`, `completed`, `failed` |
| `track_name` | string? | Track being downloaded |
| `artist_name` | string? | Artist name |
| `album_name` | string? | Album name |
| `username` | string? | Soulseek peer username |
| `filename` | string? | Remote filename |
| `progress` | int | Download progress percentage (0-100) |
| `size` | int? | File size in bytes |
| `error` | string? | Error message if failed |
| `batch_id` | string? | Batch download group ID |
| `track_index` | int? | Track position in batch |
| `retry_count` | int | Number of retry attempts |
| `metadata_enhanced` | bool | Whether metadata was enhanced post-download |
| `status_change_time` | float? | Unix timestamp of last status change |
#### `POST /api/v1/downloads/<download_id>/cancel`
Cancel a specific download.
```json
{
"username": "soulseek_username"
}
```
#### `POST /api/v1/downloads/cancel-all`
Cancel all active downloads and clear completed ones.
---
### Wishlist
Tracks that failed to download, queued for retry. Profile-scoped via `X-Profile-Id`.
#### `GET /api/v1/wishlist`
List wishlist tracks with standardized format.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `category` | string | | `singles` or `albums` |
| `page` | int | 1 | Page number |
| `limit` | int | 50 | Items per page (max 200) |
| `fields` | string | | Comma-separated field list |
```json
{
"data": {
"tracks": [
{
"id": 15,
"spotify_track_id": "6LgJvl0Xdtc73RJ1mN1a7Z",
"track_name": "Paranoid Android",
"artist_name": "Radiohead",
"album_name": "OK Computer",
"spotify_data": { "...full Spotify track object..." },
"failure_reason": "No sources found",
"retry_count": 3,
"last_attempted": "2026-03-03T15:30:00",
"date_added": "2026-03-01T10:00:00",
"source_type": "playlist",
"source_info": { "playlist_name": "My Playlist", "playlist_id": "..." },
"profile_id": 1
}
]
},
"pagination": { "..." }
}
```
**Wishlist track fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Internal database ID |
| `spotify_track_id` | string | Spotify track ID |
| `track_name` | string | Extracted track name |
| `artist_name` | string | Extracted artist name(s) |
| `album_name` | string? | Extracted album name |
| `spotify_data` | object | Full Spotify track metadata object |
| `failure_reason` | string? | Why the download failed |
| `retry_count` | int | Number of retry attempts |
| `last_attempted` | string? | ISO 8601 timestamp of last attempt |
| `date_added` | string? | ISO 8601 timestamp when added |
| `source_type` | string? | How it was added: `playlist`, `album`, `manual`, `api` |
| `source_info` | object? | Context about the source (playlist name, etc.) |
| `profile_id` | int? | Profile this track belongs to |
#### `POST /api/v1/wishlist`
Add a track to the wishlist.
```json
{
"spotify_track_data": {
"id": "6LgJvl0Xdtc73RJ1mN1a7Z",
"name": "Paranoid Android",
"artists": [{ "name": "Radiohead" }],
"album": { "name": "OK Computer", "album_type": "album" }
},
"failure_reason": "No sources found",
"source_type": "api"
}
```
#### `DELETE /api/v1/wishlist/<spotify_track_id>`
Remove a track from the wishlist by its Spotify track ID.
#### `POST /api/v1/wishlist/process`
Trigger wishlist download processing (retries all failed tracks).
---
### Watchlist
Artists being monitored for new releases. Profile-scoped via `X-Profile-Id`.
#### `GET /api/v1/watchlist`
List all watched artists for the current profile.
```json
{
"data": {
"artists": [
{
"id": 5,
"spotify_artist_id": "4tZwfgrHOc3mvqYlEYSvnL",
"itunes_artist_id": "5468295",
"artist_name": "Daft Punk",
"image_url": "https://i.scdn.co/image/...",
"date_added": "2026-01-15T10:00:00",
"last_scan_timestamp": "2026-03-04T06:00:00",
"created_at": "2026-01-15T10:00:00",
"updated_at": "2026-03-04T06:00:00",
"profile_id": 1,
"include_albums": true,
"include_eps": true,
"include_singles": true,
"include_live": false,
"include_remixes": false,
"include_acoustic": false,
"include_compilations": false
}
]
}
}
```
**Watchlist artist fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Internal database ID |
| `spotify_artist_id` | string? | Spotify artist ID |
| `itunes_artist_id` | string? | iTunes artist ID |
| `artist_name` | string | Artist name |
| `image_url` | string? | Artist image URL |
| `date_added` | string? | ISO 8601 timestamp |
| `last_scan_timestamp` | string? | ISO 8601 timestamp of last scan |
| `created_at` | string? | ISO 8601 timestamp |
| `updated_at` | string? | ISO 8601 timestamp |
| `profile_id` | int? | Profile this entry belongs to |
| `include_albums` | bool | Monitor for new albums |
| `include_eps` | bool | Monitor for new EPs |
| `include_singles` | bool | Monitor for new singles |
| `include_live` | bool | Include live recordings |
| `include_remixes` | bool | Include remixes |
| `include_acoustic` | bool | Include acoustic versions |
| `include_compilations` | bool | Include compilations |
#### `POST /api/v1/watchlist`
Add an artist to the watchlist.
```json
{
"artist_id": "4tZwfgrHOc3mvqYlEYSvnL",
"artist_name": "Daft Punk"
}
```
#### `PATCH /api/v1/watchlist/<artist_id>`
Update content type filters for a watched artist without having to remove and re-add them. Only the fields you include in the body will be updated.
```json
{
"include_live": true,
"include_remixes": true,
"include_compilations": false
}
```
Accepts any combination of: `include_albums`, `include_eps`, `include_singles`, `include_live`, `include_remixes`, `include_acoustic`, `include_compilations`.
**Response:**
```json
{
"data": {
"message": "Watchlist filters updated.",
"updated": {
"include_live": true,
"include_remixes": true,
"include_compilations": false
}
}
}
```
#### `DELETE /api/v1/watchlist/<artist_id>`
Remove an artist from the watchlist. `artist_id` can be a Spotify or iTunes artist ID.
#### `POST /api/v1/watchlist/scan`
Trigger a watchlist scan for new releases. Returns `409 CONFLICT` if a scan is already running.
---
### Discovery
Browse discovery pool, similar artists, and recent releases. Profile-scoped via `X-Profile-Id`.
#### `GET /api/v1/discover/pool`
List discovery pool tracks with pagination and optional filters.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `new_releases_only` | string | `false` | Set to `true` to filter to new releases only |
| `source` | string | | `spotify` or `itunes` (omit for all) |
| `page` | int | 1 | Page number |
| `limit` | int | 100 | Items per page (max 500) |
| `fields` | string | | Comma-separated field list |
```json
{
"data": {
"tracks": [
{
"id": 1024,
"spotify_track_id": "3n3Ppam7vgaVa1iaRUc9Lp",
"spotify_album_id": "2noRn2Aes5aoNVsU6iWThc",
"spotify_artist_id": "4tZwfgrHOc3mvqYlEYSvnL",
"itunes_track_id": null,
"itunes_album_id": null,
"itunes_artist_id": null,
"source": "spotify",
"track_name": "Something About Us",
"artist_name": "Daft Punk",
"album_name": "Discovery",
"album_cover_url": "https://i.scdn.co/image/...",
"duration_ms": 232000,
"popularity": 76,
"release_date": "2001-03-12",
"is_new_release": false,
"artist_genres": ["electro", "french house"],
"added_date": "2026-03-01T12:00:00"
}
]
},
"pagination": { "page": 1, "limit": 100, "total": 450, "..." }
}
```
#### `GET /api/v1/discover/similar-artists`
List top similar artists discovered from watchlist analysis.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `limit` | int | 50 | Max artists (max 200) |
| `fields` | string | | Comma-separated field list |
```json
{
"data": {
"artists": [
{
"id": 88,
"source_artist_id": "4tZwfgrHOc3mvqYlEYSvnL",
"similar_artist_spotify_id": "12Chz98pHFMPJEknJQMWvI",
"similar_artist_itunes_id": null,
"similar_artist_name": "Justice",
"similarity_rank": 1,
"occurrence_count": 5,
"last_updated": "2026-03-01T12:00:00",
"last_featured": "2026-03-03T08:00:00"
}
]
}
}
```
#### `GET /api/v1/discover/recent-releases`
List recent releases from watched artists.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `limit` | int | 50 | Max releases (max 200) |
| `fields` | string | | Comma-separated field list |
```json
{
"data": {
"releases": [
{
"id": 12,
"watchlist_artist_id": 5,
"album_spotify_id": "2noRn2Aes5aoNVsU6iWThc",
"album_itunes_id": null,
"source": "spotify",
"album_name": "Random Access Memories (10th Anniversary Edition)",
"release_date": "2023-05-12",
"album_cover_url": "https://...",
"track_count": 22,
"added_date": "2026-03-01T06:00:00"
}
]
}
}
```
#### `GET /api/v1/discover/pool/metadata`
Get discovery pool metadata (when it was last populated, track count).
```json
{
"data": {
"last_populated": "2026-03-04T06:00:00",
"track_count": 450,
"updated_at": "2026-03-04T06:00:00"
}
}
```
#### `GET /api/v1/discover/bubbles`
List all bubble snapshots for the current profile. Returns snapshots for three types: `artist_bubbles`, `search_bubbles`, `discover_downloads`. Each snapshot is `null` if it hasn't been created yet.
```json
{
"data": {
"snapshots": {
"artist_bubbles": {
"data": {
"bubbles": [
{ "name": "Radiohead", "value": 45, "genre": "alternative rock" },
{ "name": "Daft Punk", "value": 32, "genre": "electronic" },
{ "name": "Portishead", "value": 18, "genre": "trip hop" }
],
"total_artists": 3,
"generated_at": "2026-03-04T06:00:00"
},
"timestamp": "2026-03-04T06:00:00"
},
"search_bubbles": {
"data": {
"bubbles": [
{ "query": "ambient electronic", "count": 12 },
{ "query": "shoegaze", "count": 8 }
],
"generated_at": "2026-03-03T14:00:00"
},
"timestamp": "2026-03-03T14:00:00"
},
"discover_downloads": null
}
}
}
```
#### `GET /api/v1/discover/bubbles/<snapshot_type>`
Get a specific bubble snapshot by type.
| Param | Type | Description |
|-------|------|-------------|
| `snapshot_type` | path | `artist_bubbles`, `search_bubbles`, or `discover_downloads` |
```json
{
"data": {
"snapshot": {
"data": {
"bubbles": [
{ "name": "Radiohead", "value": 45, "genre": "alternative rock" },
{ "name": "Daft Punk", "value": 32, "genre": "electronic" },
{ "name": "Portishead", "value": 18, "genre": "trip hop" }
],
"total_artists": 3,
"generated_at": "2026-03-04T06:00:00"
},
"timestamp": "2026-03-04T06:00:00"
}
}
}
```
**Bubble snapshot fields:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | object | Parsed snapshot data (structure varies by type — contains bubble arrays and metadata) |
| `timestamp` | string | ISO 8601 timestamp when the snapshot was created/updated |
> **Snapshot types:** `artist_bubbles` — artist listening weight visualization data. `search_bubbles` — search frequency visualization data. `discover_downloads` — discovery download activity visualization data. Returns `404` if the requested snapshot type doesn't exist for this profile.
---
### Playlists
#### `GET /api/v1/playlists`
List user playlists from Spotify or Tidal.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `source` | string | `spotify` | `spotify` or `tidal` |
```json
{
"data": {
"playlists": [
{
"id": "37i9dQZF1DXcBWIGoYBM5M",
"name": "Today's Top Hits",
"owner": "spotify",
"track_count": 50,
"image_url": "https://..."
}
],
"source": "spotify"
}
}
```
#### `GET /api/v1/playlists/<playlist_id>`
Get playlist details with full track list.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `source` | string | `spotify` | Currently only `spotify` supported for detail view |
```json
{
"data": {
"playlist": {
"id": "37i9dQZF1DXcBWIGoYBM5M",
"name": "Today's Top Hits",
"owner": "Spotify",
"total_tracks": 50,
"tracks": [
{
"id": "2cGxRwrMyEAp8dEbuZaVv6",
"name": "Around the World",
"artists": ["Daft Punk"],
"album": "Homework",
"duration_ms": 428000,
"image_url": "https://..."
}
]
},
"source": "spotify"
}
}
```
#### `POST /api/v1/playlists/<playlist_id>/sync`
Trigger playlist sync/download.
```json
{
"playlist_name": "My Playlist",
"tracks": [
{
"id": "2cGxRwrMyEAp8dEbuZaVv6",
"name": "Around the World",
"artists": [{ "name": "Daft Punk" }]
}
]
}
```
---
### Profiles
Manage user profiles. Profiles scope watchlist, wishlist, and discovery data.
#### `GET /api/v1/profiles`
List all profiles.
```json
{
"data": {
"profiles": [
{
"id": 1,
"name": "Admin",
"avatar_color": "#6366f1",
"avatar_url": null,
"is_admin": true,
"has_pin": false,
"created_at": "2026-01-15T10:00:00",
"updated_at": "2026-02-20T14:30:00"
},
{
"id": 2,
"name": "Family",
"avatar_color": "#10b981",
"avatar_url": "https://example.com/avatar.jpg",
"is_admin": false,
"has_pin": true,
"created_at": "2026-02-01T15:00:00",
"updated_at": "2026-03-01T09:45:00"
}
]
}
}
```
#### `GET /api/v1/profiles/<profile_id>`
Get a single profile by ID.
```json
{
"data": {
"profile": {
"id": 2,
"name": "Family",
"avatar_color": "#10b981",
"avatar_url": "https://example.com/avatar.jpg",
"is_admin": false,
"has_pin": true,
"created_at": "2026-02-01T15:00:00",
"updated_at": "2026-03-01T09:45:00"
}
}
}
```
**Profile fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Profile ID (auto-increment) |
| `name` | string | Display name (unique) |
| `avatar_color` | string | Hex color for the default avatar circle (default `#6366f1`) |
| `avatar_url` | string? | Custom avatar image URL |
| `is_admin` | bool | Whether this profile has admin privileges |
| `has_pin` | bool | Whether a PIN is set (the PIN hash itself is never exposed) |
| `created_at` | string | ISO 8601 timestamp when profile was created |
| `updated_at` | string | ISO 8601 timestamp of last profile update |
> **Note:** The `pin_hash` column exists in the database but is never returned by the API. Only the computed `has_pin` boolean is exposed.
#### `POST /api/v1/profiles`
Create a new profile.
**Request body:**
```json
{
"name": "Family",
"avatar_color": "#10b981",
"avatar_url": "https://example.com/avatar.jpg",
"is_admin": false,
"pin": "1234"
}
```
All fields except `name` are optional. The `pin` field accepts a raw PIN string — it is hashed with PBKDF2-SHA256 before storage.
**Response (201 Created):**
```json
{
"data": {
"profile": {
"id": 3,
"name": "Family",
"avatar_color": "#10b981",
"avatar_url": "https://example.com/avatar.jpg",
"is_admin": false,
"has_pin": true,
"created_at": "2026-03-04T12:00:00",
"updated_at": "2026-03-04T12:00:00"
}
}
}
```
Returns `409 CONFLICT` if the profile name already exists.
#### `PUT /api/v1/profiles/<profile_id>`
Update a profile. Only include the fields you want to change.
**Request body:**
```json
{
"name": "New Name",
"avatar_color": "#ef4444",
"pin": "5678"
}
```
Set `"pin": ""` or `"pin": null` to remove a PIN.
**Response:**
```json
{
"data": {
"profile": {
"id": 2,
"name": "New Name",
"avatar_color": "#ef4444",
"avatar_url": "https://example.com/avatar.jpg",
"is_admin": false,
"has_pin": true,
"created_at": "2026-02-01T15:00:00",
"updated_at": "2026-03-04T12:05:00"
}
}
}
```
#### `DELETE /api/v1/profiles/<profile_id>`
Delete a profile and all its per-profile data (watchlist, wishlist, discovery pool, bubble snapshots). Cannot delete profile 1 (admin) — returns `403 FORBIDDEN`.
**Response:**
```json
{
"data": {
"message": "Profile 3 deleted."
}
}
```
---
### Retag Queue
Browse and manage the retag queue — albums/tracks pending metadata corrections.
#### `GET /api/v1/retag/groups`
List all retag groups with track counts. Groups are ordered by artist name (ascending) then creation date (descending).
```json
{
"data": {
"groups": [
{
"id": 1,
"group_type": "album",
"artist_name": "Radiohead",
"album_name": "OK Computer",
"image_url": "https://i.scdn.co/image/ab67616d0000b273c8b444df094c596ea9da41f6",
"spotify_album_id": "6dVIqQ8qmQ5GBnJ9shOYGE",
"itunes_album_id": "1097862703",
"total_tracks": 12,
"release_date": "1997-06-16",
"created_at": "2026-03-04T10:00:00",
"track_count": 12
},
{
"id": 2,
"group_type": "album",
"artist_name": "Radiohead",
"album_name": "In Rainbows",
"image_url": "https://i.scdn.co/image/ab67616d0000b2737de1fcc2ebeab8b6e22a1b8a",
"spotify_album_id": "7eyQXxuf2nGj9d2367Gi5f",
"itunes_album_id": "1109731429",
"total_tracks": 10,
"release_date": "2007-10-10",
"created_at": "2026-03-04T10:05:00",
"track_count": 10
}
]
}
}
```
**Retag group fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Group ID (auto-increment) |
| `group_type` | string | Type of retag group (currently always `album`) |
| `artist_name` | string | Artist name for this retag group |
| `album_name` | string | Album name for this retag group |
| `image_url` | string? | Album cover image URL |
| `spotify_album_id` | string? | Spotify album ID for metadata lookup |
| `itunes_album_id` | string? | iTunes/Apple Music album ID for metadata lookup |
| `total_tracks` | int | Expected total tracks for the album |
| `release_date` | string? | Album release date (YYYY-MM-DD) |
| `created_at` | string | ISO 8601 timestamp when group was created |
| `track_count` | int | Computed: number of tracks currently in this group |
#### `GET /api/v1/retag/groups/<group_id>`
Get a retag group with its full track list. Tracks are ordered by disc number then track number.
```json
{
"data": {
"group": {
"id": 1,
"group_type": "album",
"artist_name": "Radiohead",
"album_name": "OK Computer",
"image_url": "https://i.scdn.co/image/ab67616d0000b273c8b444df094c596ea9da41f6",
"spotify_album_id": "6dVIqQ8qmQ5GBnJ9shOYGE",
"itunes_album_id": "1097862703",
"total_tracks": 12,
"release_date": "1997-06-16",
"created_at": "2026-03-04T10:00:00",
"track_count": 12
},
"tracks": [
{
"id": 1,
"group_id": 1,
"track_number": 1,
"disc_number": 1,
"title": "Airbag",
"file_path": "/downloads/Radiohead/OK Computer/01 - Airbag.flac",
"file_format": "flac",
"spotify_track_id": "6LgJvl0Xdtc73RJ1mN1a7Z",
"itunes_track_id": null,
"created_at": "2026-03-04T10:00:00"
},
{
"id": 2,
"group_id": 1,
"track_number": 2,
"disc_number": 1,
"title": "Paranoid Android",
"file_path": "/downloads/Radiohead/OK Computer/02 - Paranoid Android.flac",
"file_format": "flac",
"spotify_track_id": "6LgJvl0Xdtc73RJ1mN1a7Z",
"itunes_track_id": "1097863062",
"created_at": "2026-03-04T10:00:00"
},
{
"id": 3,
"group_id": 1,
"track_number": 3,
"disc_number": 1,
"title": "Subterranean Homesick Alien",
"file_path": "/downloads/Radiohead/OK Computer/03 - Subterranean Homesick Alien.flac",
"file_format": "flac",
"spotify_track_id": "3sFhbVuZGMAsmFSIFGVPgS",
"itunes_track_id": null,
"created_at": "2026-03-04T10:00:00"
}
]
}
}
```
**Retag track fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Track ID (auto-increment) |
| `group_id` | int | Parent retag group ID (foreign key) |
| `track_number` | int? | Track number on disc |
| `disc_number` | int | Disc number (default 1) |
| `title` | string | Track title |
| `file_path` | string | Full file path to the downloaded audio file |
| `file_format` | string? | Audio format (`flac`, `mp3`, `opus`, etc.) |
| `spotify_track_id` | string? | Spotify track ID for metadata lookup |
| `itunes_track_id` | string? | iTunes/Apple Music track ID for metadata lookup |
| `created_at` | string | ISO 8601 timestamp when track was added to queue |
#### `DELETE /api/v1/retag/groups/<group_id>`
Delete a specific retag group and all its tracks (cascade delete).
```json
{
"data": {
"message": "Retag group 1 deleted."
}
}
```
#### `DELETE /api/v1/retag/groups`
Clear all retag groups and tracks from the queue.
```json
{
"data": {
"message": "Cleared 5 retag groups."
}
}
```
#### `GET /api/v1/retag/stats`
Get retag queue statistics.
```json
{
"data": {
"groups": 5,
"tracks": 47,
"artists": 3
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `groups` | int | Total number of retag groups in queue |
| `tracks` | int | Total number of tracks across all groups |
| `artists` | int | Number of distinct artists in the queue |
---
### ListenBrainz
Browse cached ListenBrainz playlists and their tracks. Playlists are cached locally when SoulSync pulls recommendations from ListenBrainz.
#### `GET /api/v1/listenbrainz/playlists`
List cached ListenBrainz playlists with optional type filtering and pagination.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `type` | string | | Filter by playlist_type (e.g. `weekly-jams`, `weekly-exploration`) |
| `page` | int | 1 | Page number |
| `limit` | int | 50 | Items per page (max 200) |
```json
{
"data": {
"playlists": [
{
"id": 1,
"playlist_mbid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Weekly Jams for user123",
"creator": "listenbrainz",
"playlist_type": "weekly-jams",
"track_count": 50,
"annotation_data": {
"algorithm": "collaborative-filtering",
"source_patch": "weekly-jams"
},
"last_updated": "2026-03-03T06:00:00",
"cached_date": "2026-03-03T06:05:00"
},
{
"id": 2,
"playlist_mbid": "f9e8d7c6-b5a4-3210-fedc-ba9876543210",
"title": "Weekly Exploration for user123",
"creator": "listenbrainz",
"playlist_type": "weekly-exploration",
"track_count": 50,
"annotation_data": {
"algorithm": "collaborative-filtering",
"source_patch": "weekly-exploration"
},
"last_updated": "2026-03-03T06:00:00",
"cached_date": "2026-03-03T06:08:00"
}
]
},
"pagination": { "page": 1, "limit": 50, "total": 8, "total_pages": 1, "has_next": false, "has_prev": false }
}
```
**ListenBrainz playlist fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Internal database ID |
| `playlist_mbid` | string | MusicBrainz playlist MBID (unique) |
| `title` | string | Playlist title as given by ListenBrainz |
| `creator` | string? | Playlist creator (usually `listenbrainz`) |
| `playlist_type` | string | Type: `weekly-jams`, `weekly-exploration`, etc. |
| `track_count` | int | Number of tracks in the playlist |
| `annotation_data` | object? | Parsed JSON annotation metadata from ListenBrainz |
| `last_updated` | string | ISO 8601 timestamp of last update from ListenBrainz |
| `cached_date` | string | ISO 8601 timestamp when SoulSync cached this playlist |
#### `GET /api/v1/listenbrainz/playlists/<playlist_id>`
Get a ListenBrainz playlist with its full track list. `playlist_id` can be the internal database ID (integer) or the MusicBrainz playlist MBID (UUID string). Tracks are returned in playlist order (by position).
```json
{
"data": {
"playlist": {
"id": 1,
"playlist_mbid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Weekly Jams for user123",
"creator": "listenbrainz",
"playlist_type": "weekly-jams",
"track_count": 50,
"annotation_data": {
"algorithm": "collaborative-filtering",
"source_patch": "weekly-jams"
},
"last_updated": "2026-03-03T06:00:00",
"cached_date": "2026-03-03T06:05:00"
},
"tracks": [
{
"id": 1,
"playlist_id": 1,
"position": 1,
"track_name": "Paranoid Android",
"artist_name": "Radiohead",
"album_name": "OK Computer",
"duration_ms": 383000,
"recording_mbid": "b3e2b7e0-a147-4b3c-8eab-fd90bfff7e74",
"release_mbid": "a1c35a51-d102-4ce7-b7b0-8a4f68385bb2",
"album_cover_url": "https://coverartarchive.org/release/a1c35a51-d102-4ce7-b7b0-8a4f68385bb2/front-250.jpg",
"additional_metadata": {
"artist_mbids": ["a74b1b7f-71a5-4011-9441-d0b5e4122711"],
"caa_id": 12345678901,
"caa_release_mbid": "a1c35a51-d102-4ce7-b7b0-8a4f68385bb2"
}
},
{
"id": 2,
"playlist_id": 1,
"position": 2,
"track_name": "All I Need",
"artist_name": "Radiohead",
"album_name": "In Rainbows",
"duration_ms": 226000,
"recording_mbid": "c7d9e0f1-2345-6789-abcd-ef0123456789",
"release_mbid": "d4e5f6a7-8901-2345-bcde-f67890123456",
"album_cover_url": "https://coverartarchive.org/release/d4e5f6a7-8901-2345-bcde-f67890123456/front-250.jpg",
"additional_metadata": {
"artist_mbids": ["a74b1b7f-71a5-4011-9441-d0b5e4122711"],
"caa_id": 23456789012,
"caa_release_mbid": "d4e5f6a7-8901-2345-bcde-f67890123456"
}
},
{
"id": 3,
"playlist_id": 1,
"position": 3,
"track_name": "Something About Us",
"artist_name": "Daft Punk",
"album_name": "Discovery",
"duration_ms": 232000,
"recording_mbid": "e8f9a0b1-c2d3-e4f5-6789-012345678901",
"release_mbid": "f0a1b2c3-d4e5-f6a7-8901-234567890123",
"album_cover_url": "https://coverartarchive.org/release/f0a1b2c3-d4e5-f6a7-8901-234567890123/front-250.jpg",
"additional_metadata": {
"artist_mbids": ["056e4f3e-d505-4dad-8ec1-d04f521cbb56"],
"caa_id": 34567890123,
"caa_release_mbid": "f0a1b2c3-d4e5-f6a7-8901-234567890123"
}
}
]
}
}
```
**ListenBrainz track fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Internal track ID |
| `playlist_id` | int | Parent playlist ID (foreign key) |
| `position` | int | Position in the playlist (1-based) |
| `track_name` | string | Track title |
| `artist_name` | string | Artist name |
| `album_name` | string | Album name |
| `duration_ms` | int | Track duration in milliseconds |
| `recording_mbid` | string? | MusicBrainz recording MBID |
| `release_mbid` | string? | MusicBrainz release MBID |
| `album_cover_url` | string? | Album cover art URL (usually from Cover Art Archive) |
| `additional_metadata` | object? | Parsed JSON with extra MusicBrainz data (artist MBIDs, CAA info, etc.) |
---
### Cache
Browse internal enrichment caches. Useful for debugging enrichment issues, understanding match quality, or auditing what metadata has been resolved.
#### `GET /api/v1/cache/musicbrainz`
List cached MusicBrainz lookups with optional filtering and pagination. Results ordered by `last_updated` descending.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `entity_type` | string | | Filter by type: `artist`, `album`, `track` |
| `search` | string | | Filter by entity name (case-insensitive partial match) |
| `page` | int | 1 | Page number |
| `limit` | int | 50 | Items per page (max 200) |
```json
{
"data": {
"entries": [
{
"id": 42,
"entity_type": "artist",
"entity_name": "Radiohead",
"artist_name": null,
"musicbrainz_id": "a74b1b7f-71a5-4011-9441-d0b5e4122711",
"spotify_id": "4Z8W4fKeB5YxbusRsdQVPb",
"itunes_id": "657515",
"metadata_json": {
"name": "Radiohead",
"type": "Group",
"country": "GB",
"disambiguation": "English rock band",
"begin_date": "1985",
"end_date": null,
"tags": ["alternative rock", "art rock", "experimental"]
},
"match_confidence": 95,
"last_updated": "2026-03-01T12:00:00"
},
{
"id": 88,
"entity_type": "album",
"entity_name": "OK Computer",
"artist_name": "Radiohead",
"musicbrainz_id": "b1f5a82e-5cfa-36f8-b084-4a93d7e7e608",
"spotify_id": "6dVIqQ8qmQ5GBnJ9shOYGE",
"itunes_id": "1097862703",
"metadata_json": {
"title": "OK Computer",
"date": "1997-06-16",
"country": "XE",
"status": "Official",
"packaging": "Jewel Case",
"barcode": "724385522925"
},
"match_confidence": 100,
"last_updated": "2026-02-28T08:30:00"
},
{
"id": 215,
"entity_type": "track",
"entity_name": "Paranoid Android",
"artist_name": "Radiohead",
"musicbrainz_id": "b3e2b7e0-a147-4b3c-8eab-fd90bfff7e74",
"spotify_id": "6LgJvl0Xdtc73RJ1mN1a7Z",
"itunes_id": "1097863062",
"metadata_json": {
"title": "Paranoid Android",
"length": 383000,
"isrcs": ["GBAYE9700074"]
},
"match_confidence": 98,
"last_updated": "2026-02-28T08:35:00"
}
]
},
"pagination": { "page": 1, "limit": 50, "total": 1250, "total_pages": 25, "has_next": true, "has_prev": false }
}
```
**MusicBrainz cache fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Cache entry ID |
| `entity_type` | string | Entity type: `artist`, `album`, or `track` |
| `entity_name` | string | Name that was looked up |
| `artist_name` | string? | Associated artist name (for album/track lookups) |
| `musicbrainz_id` | string? | Resolved MusicBrainz MBID (`null` if no match) |
| `spotify_id` | string? | Cross-referenced Spotify ID |
| `itunes_id` | string? | Cross-referenced iTunes/Apple Music ID |
| `metadata_json` | object? | Parsed JSON with full MusicBrainz metadata (tags, dates, ISRCs, etc.) |
| `match_confidence` | int? | Match confidence score (0-100) |
| `last_updated` | string | ISO 8601 timestamp of last cache update |
> **Unique constraint:** `(entity_type, entity_name, artist_name)` — each entity is cached once per type+name combination.
#### `GET /api/v1/cache/musicbrainz/stats`
Get MusicBrainz cache statistics — total entries, matched vs unmatched, and breakdown by entity type.
```json
{
"data": {
"total": 1250,
"matched": 1100,
"unmatched": 150,
"by_type": {
"artist": 500,
"album": 450,
"track": 300
}
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `total` | int | Total cache entries |
| `matched` | int | Entries with a resolved `musicbrainz_id` |
| `unmatched` | int | Entries where no MusicBrainz match was found |
| `by_type` | object | Entry counts keyed by `entity_type` |
#### `GET /api/v1/cache/discovery-matches`
List cached discovery provider matches. These are stored when the discovery system resolves tracks from external providers. Results ordered by `last_used_at` descending.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `provider` | string | | Filter by provider (e.g. `spotify`, `itunes`) |
| `search` | string | | Filter by title or artist (case-insensitive partial match on both) |
| `page` | int | 1 | Page number |
| `limit` | int | 50 | Items per page (max 200) |
```json
{
"data": {
"entries": [
{
"id": 100,
"normalized_title": "paranoid android",
"normalized_artist": "radiohead",
"provider": "spotify",
"match_confidence": 0.95,
"matched_data_json": {
"track_id": "6LgJvl0Xdtc73RJ1mN1a7Z",
"track_name": "Paranoid Android",
"artist_name": "Radiohead",
"album_name": "OK Computer",
"album_id": "6dVIqQ8qmQ5GBnJ9shOYGE",
"duration_ms": 383000,
"popularity": 72,
"preview_url": "https://p.scdn.co/mp3-preview/..."
},
"original_title": "Paranoid Android",
"original_artist": "Radiohead",
"created_at": "2026-02-15T10:00:00",
"last_used_at": "2026-03-04T09:00:00",
"use_count": 12
},
{
"id": 101,
"normalized_title": "something about us",
"normalized_artist": "daft punk",
"provider": "itunes",
"match_confidence": 0.92,
"matched_data_json": {
"track_id": "724633277",
"track_name": "Something About Us",
"artist_name": "Daft Punk",
"album_name": "Discovery",
"collection_id": "724633180",
"duration_ms": 232000,
"artwork_url": "https://is1-ssl.mzstatic.com/image/..."
},
"original_title": "Something About Us",
"original_artist": "Daft Punk",
"created_at": "2026-02-20T14:30:00",
"last_used_at": "2026-03-03T18:00:00",
"use_count": 5
}
]
},
"pagination": { "page": 1, "limit": 50, "total": 3200, "total_pages": 64, "has_next": true, "has_prev": false }
}
```
**Discovery match cache fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Cache entry ID |
| `normalized_title` | string | Lowercase normalized track title used for matching |
| `normalized_artist` | string | Lowercase normalized artist name used for matching |
| `provider` | string | Provider that was matched against (`spotify`, `itunes`) |
| `match_confidence` | float | Match confidence score (0.0-1.0) |
| `matched_data_json` | object? | Parsed JSON with full matched track data from the provider |
| `original_title` | string? | Original (un-normalized) track title |
| `original_artist` | string? | Original (un-normalized) artist name |
| `created_at` | string | ISO 8601 timestamp when match was first cached |
| `last_used_at` | string | ISO 8601 timestamp when match was last reused |
| `use_count` | int | Number of times this cached match has been reused |
> **Unique constraint:** `(normalized_title, normalized_artist, provider)` — each title+artist+provider combination is cached once.
#### `GET /api/v1/cache/discovery-matches/stats`
Get discovery match cache statistics — total entries, total reuses, average confidence, and breakdown by provider.
```json
{
"data": {
"total": 3200,
"total_uses": 15000,
"avg_confidence": 0.872,
"by_provider": {
"spotify": 2800,
"itunes": 400
}
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `total` | int | Total cached match entries |
| `total_uses` | int | Sum of all `use_count` values across entries |
| `avg_confidence` | float? | Average match confidence (rounded to 3 decimals, `null` if empty) |
| `by_provider` | object | Entry counts keyed by provider name |
---
### Settings
#### `GET /api/v1/settings`
Get current settings. Sensitive values (passwords, tokens, secrets) are redacted.
#### `PATCH /api/v1/settings`
Update settings (partial update). Uses dot-notation keys.
```json
{
"soulseek.search_timeout": 90,
"logging.level": "DEBUG"
}
```
**Response:**
```json
{
"data": {
"message": "Settings updated.",
"updated_keys": ["soulseek.search_timeout", "logging.level"]
}
}
```
---
### API Key Management
#### `GET /api/v1/api-keys`
List all API keys (shows prefix and label only, never the full key).
```json
{
"data": {
"keys": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"label": "Discord Bot",
"key_prefix": "sk_a3Bf9x2",
"created_at": "2026-03-01T12:00:00",
"last_used_at": "2026-03-04T09:15:00"
}
]
}
}
```
#### `POST /api/v1/api-keys`
Generate a new API key. The raw key is returned **once** — save it immediately.
```json
{
"label": "Discord Bot"
}
```
**Response:**
```json
{
"data": {
"key": "sk_a3Bf9x2Kp7Qm4Rn8Yt6Wv0Xz1Cb5Dj9Fg",
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"label": "Discord Bot",
"key_prefix": "sk_a3Bf9x2",
"created_at": "2026-03-04T10:00:00"
}
}
```
#### `DELETE /api/v1/api-keys/<key_id>`
Revoke an API key by its UUID.
#### `POST /api/v1/api-keys/bootstrap`
Generate the first API key when none exist. **No authentication required.** Returns `403` if keys already exist.
```json
{
"label": "My First Key"
}
```
---
## Field Filtering
All library, watchlist, wishlist, and discovery endpoints support the `?fields=` parameter to request only specific fields. This reduces response size when you only need a few fields.
```
GET /api/v1/library/artists/42?fields=id,name,genres,spotify_artist_id
```
```json
{
"data": {
"artist": {
"id": 42,
"name": "Radiohead",
"genres": ["alternative rock", "art rock"],
"spotify_artist_id": "4Z8W4fKeB5YxbusRsdQVPb"
}
}
}
```
---
## Examples
### Python
```python
import requests
API_URL = "http://localhost:8008/api/v1"
API_KEY = "sk_your_key_here"
headers = {"Authorization": f"Bearer {API_KEY}"}
# Get full artist details with all enrichment metadata
artist = requests.get(f"{API_URL}/library/artists/42", headers=headers).json()
print(f"Artist: {artist['data']['artist']['name']}")
print(f"Spotify: {artist['data']['artist']['spotify_artist_id']}")
print(f"MusicBrainz: {artist['data']['artist']['musicbrainz_id']}")
print(f"Albums: {len(artist['data']['albums'])}")
# Get a specific album with tracks
album = requests.get(f"{API_URL}/library/albums/87", headers=headers).json()
for track in album["data"]["tracks"]:
print(f" {track['track_number']}. {track['title']} ({track['duration']}ms)")
# Look up by Spotify ID
result = requests.get(f"{API_URL}/library/lookup",
headers=headers,
params={"type": "artist", "provider": "spotify", "id": "4Z8W4fKeB5YxbusRsdQVPb"}
).json()
# Browse genres
genres = requests.get(f"{API_URL}/library/genres", headers=headers).json()
for g in genres["data"]["genres"][:10]:
print(f" {g['name']}: {g['count']} artists")
# Recently added albums
recent = requests.get(f"{API_URL}/library/recently-added?type=albums&limit=10",
headers=headers).json()
# Search external sources
search = requests.post(f"{API_URL}/search/tracks",
headers=headers,
json={"query": "Daft Punk", "limit": 5})
# Add to watchlist (as profile 2)
requests.post(f"{API_URL}/watchlist",
headers={**headers, "X-Profile-Id": "2"},
json={"artist_id": "4tZwfgrHOc3mvqYlEYSvnL", "artist_name": "Daft Punk"})
# Update watchlist filters
requests.patch(f"{API_URL}/watchlist/4tZwfgrHOc3mvqYlEYSvnL",
headers=headers,
json={"include_live": True, "include_remixes": True})
# Get discovery pool
pool = requests.get(f"{API_URL}/discover/pool?limit=50", headers=headers).json()
# Get only specific fields to reduce payload
minimal = requests.get(
f"{API_URL}/library/artists?fields=id,name,thumb_url&limit=100",
headers=headers).json()
```
### JavaScript
```javascript
const API_URL = 'http://localhost:8008/api/v1';
const API_KEY = 'sk_your_key_here';
const headers = {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
};
// Browse library artists
const artists = await fetch(`${API_URL}/library/artists?page=1&limit=25`, { headers })
.then(r => r.json());
// Get album with full metadata and tracks
const album = await fetch(`${API_URL}/library/albums/87`, { headers })
.then(r => r.json());
// Look up by external ID
const lookup = await fetch(
`${API_URL}/library/lookup?type=track&provider=spotify&id=6LgJvl0Xdtc73RJ1mN1a7Z`,
{ headers }
).then(r => r.json());
// Trigger watchlist scan
await fetch(`${API_URL}/watchlist/scan`, { method: 'POST', headers });
// Get discovery similar artists for profile 2
const similar = await fetch(`${API_URL}/discover/similar-artists?limit=20`, {
headers: { ...headers, 'X-Profile-Id': '2' }
}).then(r => r.json());
```
### curl
```bash
# System status
curl -H "Authorization: Bearer sk_..." http://localhost:8008/api/v1/system/status
# Get artist with full metadata
curl -H "Authorization: Bearer sk_..." \
http://localhost:8008/api/v1/library/artists/42
# Get album with tracks
curl -H "Authorization: Bearer sk_..." \
http://localhost:8008/api/v1/library/albums/87
# Get single track
curl -H "Authorization: Bearer sk_..." \
http://localhost:8008/api/v1/library/tracks/512
# Look up by Spotify ID
curl -H "Authorization: Bearer sk_..." \
"http://localhost:8008/api/v1/library/lookup?type=artist&provider=spotify&id=4Z8W4fKeB5YxbusRsdQVPb"
# Browse genres
curl -H "Authorization: Bearer sk_..." \
http://localhost:8008/api/v1/library/genres
# Recently added albums
curl -H "Authorization: Bearer sk_..." \
"http://localhost:8008/api/v1/library/recently-added?type=albums&limit=10"
# Search external tracks
curl -X POST http://localhost:8008/api/v1/search/tracks \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{"query": "Boards of Canada", "limit": 5}'
# Watchlist with profile
curl -H "Authorization: Bearer sk_..." \
-H "X-Profile-Id: 2" \
http://localhost:8008/api/v1/watchlist
# Update watchlist filters
curl -X PATCH http://localhost:8008/api/v1/watchlist/4tZwfgrHOc3mvqYlEYSvnL \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{"include_live": true, "include_remixes": true}'
# Discovery pool
curl -H "Authorization: Bearer sk_..." \
"http://localhost:8008/api/v1/discover/pool?limit=50&new_releases_only=true"
# Field filtering — only get id, name, and Spotify ID
curl -H "Authorization: Bearer sk_..." \
"http://localhost:8008/api/v1/library/artists?fields=id,name,spotify_artist_id&limit=100"
```
---
## Endpoint Reference
| Method | Endpoint | Description |
|--------|----------|-------------|
| **System** | | |
| GET | `/system/status` | Server status and service connectivity |
| GET | `/system/activity` | Recent activity feed |
| GET | `/system/stats` | Combined library + download stats |
| **Library — Artists** | | |
| GET | `/library/artists` | List/search artists (paginated) |
| GET | `/library/artists/<id>` | Artist detail + albums |
| GET | `/library/artists/<id>/albums` | Albums for an artist |
| **Library — Albums** | | |
| GET | `/library/albums` | List/search albums (paginated) |
| GET | `/library/albums/<id>` | Album detail + tracks |
| GET | `/library/albums/<id>/tracks` | Tracks in an album |
| **Library — Tracks** | | |
| GET | `/library/tracks/<id>` | Track detail |
| GET | `/library/tracks` | Search tracks by title/artist |
| **Library — Browse** | | |
| GET | `/library/genres` | Genre listing with counts |
| GET | `/library/recently-added` | Recently added content |
| GET | `/library/lookup` | External ID lookup |
| GET | `/library/stats` | Library statistics |
| **Search** | | |
| POST | `/search/tracks` | Search external track sources |
| POST | `/search/albums` | Search external album sources |
| POST | `/search/artists` | Search external artist sources |
| **Downloads** | | |
| GET | `/downloads` | List download tasks |
| POST | `/downloads/<id>/cancel` | Cancel a download |
| POST | `/downloads/cancel-all` | Cancel all downloads |
| **Wishlist** | | |
| GET | `/wishlist` | List wishlist tracks |
| POST | `/wishlist` | Add to wishlist |
| DELETE | `/wishlist/<track_id>` | Remove from wishlist |
| POST | `/wishlist/process` | Trigger processing |
| **Watchlist** | | |
| GET | `/watchlist` | List watched artists |
| POST | `/watchlist` | Add artist to watchlist |
| PATCH | `/watchlist/<artist_id>` | Update content filters |
| DELETE | `/watchlist/<artist_id>` | Remove from watchlist |
| POST | `/watchlist/scan` | Trigger scan |
| **Discovery** | | |
| GET | `/discover/pool` | Discovery pool tracks |
| GET | `/discover/similar-artists` | Similar artists |
| GET | `/discover/recent-releases` | Recent releases |
| GET | `/discover/pool/metadata` | Pool metadata |
| GET | `/discover/bubbles` | All bubble snapshots |
| GET | `/discover/bubbles/<type>` | Specific bubble snapshot |
| **Profiles** | | |
| GET | `/profiles` | List all profiles |
| GET | `/profiles/<id>` | Profile detail |
| POST | `/profiles` | Create profile |
| PUT | `/profiles/<id>` | Update profile |
| DELETE | `/profiles/<id>` | Delete profile |
| **Retag Queue** | | |
| GET | `/retag/groups` | List retag groups |
| GET | `/retag/groups/<id>` | Retag group detail + tracks |
| DELETE | `/retag/groups/<id>` | Delete retag group |
| DELETE | `/retag/groups` | Clear all retag groups |
| GET | `/retag/stats` | Retag queue statistics |
| **ListenBrainz** | | |
| GET | `/listenbrainz/playlists` | List cached playlists |
| GET | `/listenbrainz/playlists/<id>` | Playlist detail + tracks |
| **Cache** | | |
| GET | `/cache/musicbrainz` | Browse MusicBrainz cache |
| GET | `/cache/musicbrainz/stats` | MusicBrainz cache statistics |
| GET | `/cache/discovery-matches` | Browse discovery match cache |
| GET | `/cache/discovery-matches/stats` | Discovery match cache statistics |
| **Playlists** | | |
| GET | `/playlists` | List playlists |
| GET | `/playlists/<id>` | Playlist detail + tracks |
| POST | `/playlists/<id>/sync` | Trigger playlist sync |
| **Settings** | | |
| GET | `/settings` | Get settings (redacted) |
| PATCH | `/settings` | Update settings |
| **API Keys** | | |
| GET | `/api-keys` | List API keys |
| POST | `/api-keys` | Generate new key |
| DELETE | `/api-keys/<id>` | Revoke key |
| POST | `/api-keys/bootstrap` | Bootstrap first key (no auth) |