# 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/` 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//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/` 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//tracks` List all tracks in an album with full metadata. --- ### Library — Tracks #### `GET /api/v1/library/tracks/` 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//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/` 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/` 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/` 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/` 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/` 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//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/` 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/` 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/` 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/` 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/` 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/` 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/` 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/` | Artist detail + albums | | GET | `/library/artists//albums` | Albums for an artist | | **Library — Albums** | | | | GET | `/library/albums` | List/search albums (paginated) | | GET | `/library/albums/` | Album detail + tracks | | GET | `/library/albums//tracks` | Tracks in an album | | **Library — Tracks** | | | | GET | `/library/tracks/` | 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//cancel` | Cancel a download | | POST | `/downloads/cancel-all` | Cancel all downloads | | **Wishlist** | | | | GET | `/wishlist` | List wishlist tracks | | POST | `/wishlist` | Add to wishlist | | DELETE | `/wishlist/` | Remove from wishlist | | POST | `/wishlist/process` | Trigger processing | | **Watchlist** | | | | GET | `/watchlist` | List watched artists | | POST | `/watchlist` | Add artist to watchlist | | PATCH | `/watchlist/` | Update content filters | | DELETE | `/watchlist/` | 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/` | Specific bubble snapshot | | **Profiles** | | | | GET | `/profiles` | List all profiles | | GET | `/profiles/` | Profile detail | | POST | `/profiles` | Create profile | | PUT | `/profiles/` | Update profile | | DELETE | `/profiles/` | Delete profile | | **Retag Queue** | | | | GET | `/retag/groups` | List retag groups | | GET | `/retag/groups/` | Retag group detail + tracks | | DELETE | `/retag/groups/` | 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/` | 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/` | Playlist detail + tracks | | POST | `/playlists//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/` | Revoke key | | POST | `/api-keys/bootstrap` | Bootstrap first key (no auth) |